Improved search for Android TV

This commit is contained in:
WardPearce
2026-01-05 17:30:31 +13:00
parent 5ea4d7390a
commit a9509e9949
13 changed files with 147 additions and 69 deletions
@@ -13,6 +13,7 @@ dependencies {
implementation project(':capacitor-app')
implementation project(':capacitor-browser')
implementation project(':capacitor-clipboard')
implementation project(':capacitor-network')
implementation project(':capacitor-preferences')
implementation project(':capacitor-screen-orientation')
implementation project(':capacitor-share')
@@ -94,6 +94,7 @@
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
@@ -14,6 +14,9 @@ project(':capacitor-browser').projectDir = new File('../node_modules/@capacitor/
include ':capacitor-clipboard'
project(':capacitor-clipboard').projectDir = new File('../node_modules/@capacitor/clipboard/android')
include ':capacitor-network'
project(':capacitor-network').projectDir = new File('../node_modules/@capacitor/network/android')
include ':capacitor-preferences'
project(':capacitor-preferences').projectDir = new File('../node_modules/@capacitor/preferences/android')
+12 -2
View File
@@ -1,12 +1,12 @@
{
"name": "materialious",
"version": "1.12.11",
"version": "1.13.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "materialious",
"version": "1.12.11",
"version": "1.13.0",
"hasInstallScript": true,
"dependencies": {
"@capacitor-community/electron": "^5.0.1",
@@ -16,6 +16,7 @@
"@capacitor/browser": "^8.0.0",
"@capacitor/clipboard": "^8.0.0",
"@capacitor/core": "^8.0.0",
"@capacitor/network": "^8.0.0",
"@capacitor/preferences": "^8.0.0",
"@capacitor/screen-orientation": "^8.0.0",
"@capacitor/share": "^8.0.0",
@@ -2137,6 +2138,15 @@
"tslib": "^2.1.0"
}
},
"node_modules/@capacitor/network": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@capacitor/network/-/network-8.0.0.tgz",
"integrity": "sha512-fgvB7pNKn8pKavuzys218j4YuA5euNfavp7nS3NuwWKWNupZAlbucfnl75lazxCyVF/ZRjzYVTb4vtTEfFrK1A==",
"license": "MIT",
"peerDependencies": {
"@capacitor/core": ">=8.0.0"
}
},
"node_modules/@capacitor/preferences": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@capacitor/preferences/-/preferences-8.0.0.tgz",
+2 -1
View File
@@ -49,6 +49,7 @@
"@capacitor/browser": "^8.0.0",
"@capacitor/clipboard": "^8.0.0",
"@capacitor/core": "^8.0.0",
"@capacitor/network": "^8.0.0",
"@capacitor/preferences": "^8.0.0",
"@capacitor/screen-orientation": "^8.0.0",
"@capacitor/share": "^8.0.0",
@@ -76,4 +77,4 @@
"svelte-infinite-loading": "^1.4.0",
"youtubei.js": "^16.0.1"
}
}
}
+18 -2
View File
@@ -24,6 +24,7 @@
isAndroidTvStore,
playerAlwaysLoopStore,
playerAndroidLockOrientation,
playerAndroidPauseOnNetworkChange,
playerAutoPlayStore,
playerCCByDefault,
playerDefaultLanguage,
@@ -53,6 +54,7 @@
import { injectSabr } from '$lib/sabr';
import type { SabrStreamingAdapter } from 'googlevideo/sabr-streaming-adapter';
import { SvelteSet } from 'svelte/reactivity';
import { Network, type ConnectionStatus } from '@capacitor/network';
interface Props {
data: { video: VideoPlay; content: PhasedDescription; playlistId: string | null };
@@ -76,6 +78,8 @@
let watchProgressInterval: NodeJS.Timeout;
let showVideoRetry = $state(false);
let androidInitialNetworkStatus: ConnectionStatus | undefined;
let player: shaka.Player;
let sabrAdapter: SabrStreamingAdapter | null;
@@ -98,7 +102,6 @@
let playerVideoEndTimePretty: string = $state('');
let playerBufferedTo: number = $state(0);
let playerCloestTimestamp: Timestamp | undefined = $state();
let playerControls: HTMLElement | undefined = $state();
// eslint-disable-next-line no-undef
let playerAndroidUITimeout: NodeJS.Timeout;
@@ -597,6 +600,17 @@
await androidHandleRotate();
if (Capacitor.getPlatform() === 'android') {
androidInitialNetworkStatus = await Network.getStatus();
Network.addListener('networkStatusChange', (networkStatus) => {
if (
androidInitialNetworkStatus?.connectionType !== networkStatus.connectionType &&
$playerAndroidPauseOnNetworkChange
) {
playerElement?.pause();
}
});
await CapacitorMusicControls.create({
track: data.video.title,
artist: data.video.author,
@@ -1024,6 +1038,8 @@
}
}
await Network.removeAllListeners();
await setStatusBarColor();
await CapacitorMusicControls.destroy();
@@ -1123,7 +1139,7 @@
</div>
</div>
{#if !hideControls}
<div id="player-controls" bind:this={playerControls}>
<div id="player-controls">
<label class="slider" id="progress-slider">
{#key playerCurrentTime}
<input
+30 -43
View File
@@ -7,17 +7,14 @@
import {
interfaceSearchHistoryEnabled,
interfaceSearchSuggestionsStore,
isAndroidTvStore,
searchHistoryStore
} from '../store';
import { goToSearch } from '$lib/search';
let {
hideSearchSuggestionsAndHistory = false,
autoFocus = false,
suggestionsForSearch = $bindable([])
}: {
hideSearchSuggestionsAndHistory?: boolean;
autoFocus?: boolean;
suggestionsForSearch?: string[];
} = $props();
@@ -120,11 +117,7 @@
}}
role="presentation"
>
<div
class="field prefix fill no-margin rounded"
class:search={!$isAndroidTvStore}
class:search-tv={$isAndroidTvStore}
>
<div class="field prefix fill no-margin rounded search">
<i class="front" tabindex="-1">search</i>
<input
tabindex="0"
@@ -157,37 +150,35 @@
/>
<i class="front" role="presentation" onclick={resetSearch}>close</i>
</div>
{#if !hideSearchSuggestionsAndHistory}
{#if $interfaceSearchSuggestionsStore}
{#each suggestionsForSearch as suggestion, index (index)}
<li>
<a
onclick={() => {
search = suggestion;
onSubmit();
}}
class:selected={index === selectedSuggestionIndex}
href={resolve(`/search/[search]`, { search: encodeURIComponent(suggestion) })}
>
<div>{suggestion}</div>
</a>
</li>
{/each}
{/if}
{#if !suggestionsForSearch.length && $interfaceSearchHistoryEnabled}
{#each $searchHistoryStore as history (history)}
<li>
<a
onclick={() => {
search = history;
}}
href={resolve(`/search/[search]`, { search: encodeURIComponent(history) })}
>
<div>{history}</div>
</a>
</li>
{/each}
{/if}
{#if $interfaceSearchSuggestionsStore}
{#each suggestionsForSearch as suggestion, index (index)}
<li>
<a
onclick={() => {
search = suggestion;
onSubmit();
}}
class:selected={index === selectedSuggestionIndex}
href={resolve(`/search/[search]`, { search: encodeURIComponent(suggestion) })}
>
<div>{suggestion}</div>
</a>
</li>
{/each}
{/if}
{#if !suggestionsForSearch.length && $interfaceSearchHistoryEnabled}
{#each $searchHistoryStore as history (history)}
<li>
<a
onclick={() => {
search = history;
}}
href={resolve(`/search/[search]`, { search: encodeURIComponent(history) })}
>
<div>{history}</div>
</a>
</li>
{/each}
{/if}
</menu>
{/if}
@@ -199,10 +190,6 @@
width: 500px;
}
.search-tv {
width: 600px;
}
.selected {
background-color: var(--surface-variant);
}
@@ -7,6 +7,7 @@
import {
playerAlwaysLoopStore,
playerAndroidLockOrientation,
playerAndroidPauseOnNetworkChange,
playerAutoPlayStore,
playerAutoplayNextByDefaultStore,
playerCCByDefault,
@@ -222,6 +223,23 @@
</label>
</nav>
</div>
<div class="field no-margin">
<nav class="no-padding">
<div class="max">
<div>{$_('layout.player.networkChangePause')}</div>
</div>
<label class="switch" tabindex="0">
<input
type="checkbox"
bind:checked={$playerAndroidPauseOnNetworkChange}
onclick={() => playerAndroidPauseOnNetworkChange.set(!$playerAndroidPauseOnNetworkChange)}
role="switch"
/>
<span></span>
</label>
</nav>
</div>
{/if}
{#if Capacitor.isNativePlatform()}
@@ -187,6 +187,7 @@
"youtubeJsFallback": "Local video processing fallback",
"youtubeJsAlways": "Always use",
"lockOrientation": "Orientation locked to aspect ratio",
"networkChangePause": "Pause on network change",
"defaultPlaybackSpeed": "Default playback speed",
"playerStatistics": "Show player statistics by default"
},
+5
View File
@@ -170,6 +170,11 @@ export const playerMiniplayerEnabled = persist(
createStorage(),
'miniplayerEnabled'
);
export const playerAndroidPauseOnNetworkChange = persist(
writable(true),
createStorage(),
'pauseOnNetworkChange'
);
export const playerPlaylistHistory: Writable<string[]> = writable([]);
export interface PlayerState {
+1 -10
View File
@@ -38,11 +38,10 @@
import 'beercss';
import ui from 'beercss';
import 'material-dynamic-colors';
import { onDestroy, onMount, tick } from 'svelte';
import { onMount } from 'svelte';
import { _ } from '$lib/i18n';
import { get } from 'svelte/store';
import { pwaInfo } from 'virtual:pwa-info';
import Mousetrap from 'mousetrap';
import { truncate } from '$lib/misc';
import Author from '$lib/components/Author.svelte';
@@ -76,14 +75,6 @@
await setStatusBarColor();
});
App.addListener('backButton', async (data) => {
if (data.canGoBack) {
window.history.back();
} else {
await App.exitApp();
}
});
App.addListener('appUrlOpen', (data) => {
const url = new URL(data.url);
@@ -1,6 +1,6 @@
<script lang="ts">
import { resolve } from '$app/paths';
import Search from '$lib/components/Search.svelte';
import { getSearchSuggestions } from '$lib/api';
import { _ } from '$lib/i18n';
import { goToSearch } from '$lib/search';
import {
interfaceSearchHistoryEnabled,
@@ -8,26 +8,60 @@
searchHistoryStore
} from '$lib/store';
let suggestionsForSearch = $state([]);
let suggestionsForSearch: string[] = $state([]);
let search = $state('');
// eslint-disable-next-line no-undef
let debounceTimer: NodeJS.Timeout;
function debouncedSearch(event: any) {
if (!$interfaceSearchSuggestionsStore) return;
if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(async () => {
if (event.target.value === '') {
suggestionsForSearch = [];
return;
}
suggestionsForSearch = (await getSearchSuggestions(event.target.value)).suggestions;
}, 250);
}
</script>
<div class="space"></div>
<div class="search">
<Search hideSearchSuggestionsAndHistory={true} autoFocus={true} bind:suggestionsForSearch />
<div class="field large prefix" style="width: 600px;">
<i class="front" tabindex="-1">search</i>
<input
tabindex="0"
placeholder={$_('searchPlaceholder')}
type="text"
id="search"
autofocus
required
bind:value={search}
onkeyup={(event) => {
if (event.key === 'Enter') {
goToSearch(search);
} else {
debouncedSearch(event);
}
}}
/>
</div>
<ul class="list border no-space" style="width: 600px;">
{#if $interfaceSearchSuggestionsStore}
{#each suggestionsForSearch as suggestion, index (index)}
<li>
<a
<button
tabindex="0"
onclick={() => {
goToSearch(suggestion);
}}
href={resolve(`/search/[search]`, { search: encodeURIComponent(suggestion) })}
class="border"
>
<div>{suggestion}</div>
</a>
</button>
</li>
{/each}
{/if}
@@ -35,14 +69,15 @@
{#if !suggestionsForSearch.length && $interfaceSearchHistoryEnabled}
{#each $searchHistoryStore as history (history)}
<li>
<a
<button
tabindex="0"
onclick={() => {
goToSearch(history);
}}
href={resolve(`/search/[search]`, { search: encodeURIComponent(history) })}
class="border"
>
<div>{history}</div>
</a>
</button>
</li>
{/each}
{/if}
+9
View File
@@ -1,7 +1,16 @@
<script lang="ts">
import { isAndroidTvStore } from '$lib/store';
import { App } from '@capacitor/app';
let { children } = $props();
App.addListener('backButton', async (data) => {
if (data.canGoBack) {
window.history.back();
} else {
await App.exitApp();
}
});
</script>
<svelte:head>