Merge pull request #1128 from Materialious/update/1.10.14
Update/1.10.14
This commit is contained in:
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "Materialious",
|
||||
"version": "1.10.13",
|
||||
"version": "1.10.14",
|
||||
"description": "Modern material design for Invidious.",
|
||||
"author": {
|
||||
"name": "Ward Pearce",
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user