Improved search for Android TV
This commit is contained in:
@@ -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')
|
||||
|
||||
|
||||
Generated
+12
-2
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user