Merge pull request #1128 from Materialious/update/1.10.14

Update/1.10.14
This commit is contained in:
Ward
2025-10-08 20:06:59 +13:00
committed by GitHub
11 changed files with 138 additions and 147 deletions
+2 -2
View File
@@ -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.
@@ -71,7 +71,11 @@
<release version="1.10.13" date="2025-10-08">
<release version="1.10.14" date="2025-10-08">
<url>https://github.com/Materialious/Materialious/releases/tag/1.10.14</url>
</release>
<release version="1.10.13" date="2025-10-08">
<url>https://github.com/Materialious/Materialious/releases/tag/1.10.13</url>
</release>
<release version="1.10.12" date="2025-10-07">
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "Materialious",
"version": "1.10.13",
"version": "1.10.14",
"description": "Modern material design for Invidious.",
"author": {
"name": "Ward Pearce",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "materialious",
"version": "1.10.13",
"version": "1.10.14",
"private": true,
"scripts": {
"dev": "vite dev",
@@ -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<Response> => {
// 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);
});
}
@@ -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<HTMLElement>();
let focusableItems = $state<HTMLElement[]>([]);
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());
}
});
</script>
<div class="page right active" class:android-container={$isAndroidTvStore}>
<div class={classes} class:android-container={$isAndroidTvStore}>
<div class="space"></div>
<div class="grid" bind:this={gridElement}>
{#each items as item, index}
@@ -16,12 +16,6 @@
});
</script>
<p>
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.
</p>
<div class="grid">
<div class="s12 m4 l4">
<a href="https://github.com/sponsors/WardPearce" target="_blank" referrerpolicy="no-referrer">
+4
View File
@@ -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));
}
+7 -1
View File
@@ -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 : '');
</script>
@@ -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}
<h5 style="margin-bottom: 0;">{$_('playlistVideos')}</h5>
<div class="grid">
{#each $playlistCacheStore[data.playlistId].videos as playlistVideo}
<ContentColumn>
<article
class="no-padding primary-border"
style="height: 100%;"
onclick={() => {
showInfo = false;
}}
role="presentation"
id={playlistVideo.videoId}
class:border={playlistVideo.videoId === data.video.videoId}
>
{#key playlistVideo.videoId}
<Thumbnail
video={playlistVideo}
sideways={true}
playlistId={data.playlistId || undefined}
/>
{/key}
</article>
</ContentColumn>
{/each}
</div>
<ItemsList classes="" items={$playlistCacheStore[data.playlistId].videos} />
{/if}
{#if data.video.recommendedVideos.length > 0}
<h5 style="margin-bottom: 0;">{$_('recommendedVideos')}</h5>
<div class="grid">
{#each data.video.recommendedVideos as recommendedVideo}
<ContentColumn>
<article
onclick={() => {
showInfo = false;
}}
role="presentation"
style="height: 100%;"
class="no-padding"
>
{#key recommendedVideo.videoId}
<Thumbnail video={recommendedVideo} sideways={false} />
{/key}
</article>
</ContentColumn>
{/each}
</div>
<ItemsList classes="" items={data.video.recommendedVideos} />
{/if}
</article>
{/if}
+1 -1
View File
@@ -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")