Android TV search improvements
This commit is contained in:
Generated
+2
-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",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@capacitor-community/electron": "^5.0.0",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import { goto } from '$app/navigation';
|
||||
import Mousetrap from 'mousetrap';
|
||||
import { createEventDispatcher, onMount, tick } from 'svelte';
|
||||
import { _ } from '$lib/i18n';
|
||||
@@ -11,15 +10,20 @@
|
||||
isAndroidTvStore,
|
||||
searchHistoryStore
|
||||
} from '../store';
|
||||
import { isVideoID } from '$lib/misc';
|
||||
import { goToSearch } from '$lib/search';
|
||||
|
||||
let {
|
||||
hideSearchSuggestionsAndHistory = false,
|
||||
autoFocus = false,
|
||||
suggestionsForSearch = $bindable([])
|
||||
}: {
|
||||
hideSearchSuggestionsAndHistory?: boolean;
|
||||
autoFocus?: boolean;
|
||||
suggestionsForSearch?: string[];
|
||||
} = $props();
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let searchSuggestions = $state(false);
|
||||
interfaceSearchSuggestionsStore.subscribe((value) => (searchSuggestions = value));
|
||||
|
||||
let search: string = $state('');
|
||||
let suggestionsForSearch: string[] = $state([]);
|
||||
let selectedSuggestionIndex: number = $state(-1);
|
||||
|
||||
let showSearchBox = $state(false);
|
||||
@@ -27,7 +31,7 @@
|
||||
// eslint-disable-next-line no-undef
|
||||
let debounceTimer: NodeJS.Timeout;
|
||||
function debouncedSearch(event: any) {
|
||||
if (!searchSuggestions) return;
|
||||
if (!$interfaceSearchSuggestionsStore) return;
|
||||
|
||||
if (debounceTimer) clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(async () => {
|
||||
@@ -39,32 +43,14 @@
|
||||
}, 250);
|
||||
}
|
||||
|
||||
function handleSubmit(event: Event | undefined = undefined) {
|
||||
function onSubmit(event: Event | undefined = undefined) {
|
||||
event?.preventDefault();
|
||||
|
||||
const searchTrimed = search.trim();
|
||||
|
||||
if (!searchTrimed) return;
|
||||
|
||||
if (isVideoID(searchTrimed)) {
|
||||
// Go directly to video if Video ID provided
|
||||
goto(resolve('/watch/[videoId]', { videoId: searchTrimed }));
|
||||
return;
|
||||
}
|
||||
goToSearch(search);
|
||||
|
||||
selectedSuggestionIndex = -1;
|
||||
goto(resolve(`/search/[search]`, { search: encodeURIComponent(searchTrimed) }));
|
||||
|
||||
suggestionsForSearch = [];
|
||||
showSearchBox = false;
|
||||
|
||||
if ($interfaceSearchHistoryEnabled && !$searchHistoryStore.includes(searchTrimed)) {
|
||||
let pastHistory = $searchHistoryStore;
|
||||
if (pastHistory.length > 15) {
|
||||
pastHistory.pop();
|
||||
}
|
||||
searchHistoryStore.set([searchTrimed, ...pastHistory]);
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
@@ -93,7 +79,7 @@
|
||||
}
|
||||
} else if (event.key === 'Enter' && selectedSuggestionIndex !== -1) {
|
||||
search = suggestionsForSearch[selectedSuggestionIndex];
|
||||
handleSubmit();
|
||||
onSubmit();
|
||||
} else if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
|
||||
resetSearch();
|
||||
event.preventDefault();
|
||||
@@ -116,11 +102,17 @@
|
||||
if (!showSearchBox) resetSearch();
|
||||
return false;
|
||||
});
|
||||
|
||||
if (autoFocus) {
|
||||
showSearchBox = true;
|
||||
await tick();
|
||||
document.getElementById('search')?.focus();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<form
|
||||
onsubmit={handleSubmit}
|
||||
onsubmit={onSubmit}
|
||||
onclick={async () => {
|
||||
showSearchBox = true;
|
||||
await tick();
|
||||
@@ -157,7 +149,7 @@
|
||||
onkeydown={handleKeyDown}
|
||||
onkeyup={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
handleSubmit();
|
||||
onSubmit();
|
||||
} else {
|
||||
debouncedSearch(event);
|
||||
}
|
||||
@@ -165,35 +157,37 @@
|
||||
/>
|
||||
<i class="front" role="presentation" onclick={resetSearch}>close</i>
|
||||
</div>
|
||||
{#if searchSuggestions}
|
||||
{#each suggestionsForSearch as suggestion, index (index)}
|
||||
<li>
|
||||
<a
|
||||
onclick={() => {
|
||||
search = suggestion;
|
||||
handleSubmit();
|
||||
}}
|
||||
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 !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}
|
||||
</menu>
|
||||
{/if}
|
||||
|
||||
@@ -41,7 +41,7 @@ button,
|
||||
}
|
||||
|
||||
nav.top {
|
||||
padding-bottom: 1em;
|
||||
padding: 1em 0;
|
||||
}
|
||||
|
||||
main.root {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { resolve } from '$app/paths';
|
||||
import { get } from 'svelte/store';
|
||||
import { isVideoID } from './misc';
|
||||
import { interfaceSearchHistoryEnabled, searchHistoryStore } from './store';
|
||||
|
||||
export function goToSearch(searchValue: string) {
|
||||
const searchTrimed = searchValue.trim();
|
||||
|
||||
if (!searchTrimed) return;
|
||||
|
||||
if (isVideoID(searchTrimed)) {
|
||||
// Go directly to video if Video ID provided
|
||||
goto(resolve('/watch/[videoId]', { videoId: searchTrimed }));
|
||||
return;
|
||||
}
|
||||
|
||||
goto(resolve(`/search/[search]`, { search: encodeURIComponent(searchTrimed) }));
|
||||
|
||||
if (get(interfaceSearchHistoryEnabled) && !get(searchHistoryStore).includes(searchTrimed)) {
|
||||
const pastHistory = get(searchHistoryStore);
|
||||
if (pastHistory.length > 15) {
|
||||
pastHistory.pop();
|
||||
}
|
||||
searchHistoryStore.set([searchTrimed, ...pastHistory]);
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@
|
||||
import 'beercss';
|
||||
import ui from 'beercss';
|
||||
import 'material-dynamic-colors';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { onDestroy, onMount, tick } from 'svelte';
|
||||
import { _ } from '$lib/i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import { pwaInfo } from 'virtual:pwa-info';
|
||||
@@ -216,20 +216,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
if ($isAndroidTvStore) {
|
||||
const topContent = document.getElementById('top-content') as HTMLElement;
|
||||
Mousetrap.bind(
|
||||
'down',
|
||||
() => {
|
||||
if (topContent.contains(document.activeElement)) {
|
||||
document.getElementById('main-content')?.focus();
|
||||
return true;
|
||||
}
|
||||
},
|
||||
'keyup'
|
||||
);
|
||||
}
|
||||
|
||||
if (Capacitor.getPlatform() === 'android') {
|
||||
document.addEventListener('click', async (event: MouseEvent) => {
|
||||
// Handles opening links in browser for android.
|
||||
@@ -257,12 +243,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if ($isAndroidTvStore) {
|
||||
Mousetrap.unbind('down', 'keyup');
|
||||
}
|
||||
});
|
||||
|
||||
let webManifestLink = $derived(pwaInfo ? pwaInfo.webManifest.linkTag : '');
|
||||
</script>
|
||||
|
||||
@@ -282,6 +262,10 @@
|
||||
>
|
||||
<Logo />
|
||||
</header>
|
||||
<a href={resolve('/search', {})} class:active={$page.url.href.endsWith('/search')}>
|
||||
<i>search</i>
|
||||
<div>{$_('searchPlaceholder')}</div>
|
||||
</a>
|
||||
{#each getPages() as navPage (navPage)}
|
||||
{#if !navPage.requiresAuth || isLoggedIn}
|
||||
<a href={resolve(navPage.href, {})} class:active={$page.url.href.endsWith(navPage.href)}
|
||||
@@ -290,73 +274,93 @@
|
||||
</a>
|
||||
{/if}
|
||||
{/each}
|
||||
</nav>
|
||||
<nav class="top" id="top-content" class:tv-nav={$isAndroidTvStore}>
|
||||
{#if !mobileSearchShow}
|
||||
<button
|
||||
onclick={() => (mobileSearchShow = !mobileSearchShow)}
|
||||
class="transparent s circle large"
|
||||
>
|
||||
<i>search</i>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if Capacitor.getPlatform() === 'electron'}
|
||||
<nav class="no-space">
|
||||
<button onclick={() => window.history.back()} class="border">
|
||||
<i>arrow_back</i>
|
||||
</button>
|
||||
<button onclick={() => window.history.forward()} class="border">
|
||||
<i>arrow_forward</i>
|
||||
</button>
|
||||
</nav>
|
||||
{/if}
|
||||
|
||||
<div class="max m l"></div>
|
||||
|
||||
<div class="m l">
|
||||
<Search />
|
||||
</div>
|
||||
|
||||
{#if !mobileSearchShow}
|
||||
<div class="max"></div>
|
||||
{/if}
|
||||
|
||||
{#if mobileSearchShow}
|
||||
<div style="width: 100%;">
|
||||
<Search on:searchCancelled={() => (mobileSearchShow = false)} />
|
||||
</div>
|
||||
{:else}
|
||||
{#if !Capacitor.isNativePlatform()}
|
||||
<button data-ui="#sync-party" class="circle large transparent">
|
||||
<i class:primary-text={$syncPartyPeerStore}>group</i>
|
||||
<div class="tooltip bottom">{$_('layout.syncParty')}</div>
|
||||
</button>
|
||||
{/if}
|
||||
{#if isLoggedIn}
|
||||
<button class="circle large transparent" onclick={() => ui('#dialog-notifications')}
|
||||
><i>notifications</i>
|
||||
<div class="tooltip bottom">{$_('layout.notifications')}</div>
|
||||
</button>
|
||||
{/if}
|
||||
<button class="circle large transparent" onclick={() => ui('#dialog-settings')}>
|
||||
{#if $isAndroidTvStore}
|
||||
<div class="divider"></div>
|
||||
<a href="#settings" onclick={() => ui('#dialog-settings')}>
|
||||
<i>settings</i>
|
||||
<div class="tooltip bottom">{$_('layout.settings')}</div>
|
||||
</button>
|
||||
|
||||
<div>{$_('layout.settings')}</div>
|
||||
</a>
|
||||
{#if !isLoggedIn}
|
||||
<button onclick={login} class="circle large transparent">
|
||||
<a onclick={login} href="#login">
|
||||
<i>login</i>
|
||||
<div class="tooltip bottom">{$_('layout.login')}</div>
|
||||
</button>
|
||||
<div>{$_('layout.login')}</div>
|
||||
</a>
|
||||
{:else}
|
||||
<button onclick={logout} class="circle large transparent">
|
||||
<a onclick={logout} href="#logout">
|
||||
<i>logout</i>
|
||||
<div class="tooltip bottom">{$_('layout.logout')}</div>
|
||||
</button>
|
||||
<div>{$_('layout.logout')}</div>
|
||||
</a>
|
||||
{/if}
|
||||
{/if}
|
||||
</nav>
|
||||
{#if !$isAndroidTvStore}
|
||||
<nav class="top" id="top-content" class:tv-nav={$isAndroidTvStore}>
|
||||
{#if !mobileSearchShow}
|
||||
<button
|
||||
onclick={() => (mobileSearchShow = !mobileSearchShow)}
|
||||
class="transparent s circle large"
|
||||
>
|
||||
<i>search</i>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if Capacitor.getPlatform() === 'electron'}
|
||||
<nav class="no-space">
|
||||
<button onclick={() => window.history.back()} class="border">
|
||||
<i>arrow_back</i>
|
||||
</button>
|
||||
<button onclick={() => window.history.forward()} class="border">
|
||||
<i>arrow_forward</i>
|
||||
</button>
|
||||
</nav>
|
||||
{/if}
|
||||
|
||||
<div class="max m l"></div>
|
||||
|
||||
<div class="m l">
|
||||
<Search />
|
||||
</div>
|
||||
|
||||
{#if !mobileSearchShow}
|
||||
<div class="max"></div>
|
||||
{/if}
|
||||
|
||||
{#if mobileSearchShow}
|
||||
<div style="width: 100%;">
|
||||
<Search on:searchCancelled={() => (mobileSearchShow = false)} />
|
||||
</div>
|
||||
{:else}
|
||||
{#if !Capacitor.isNativePlatform()}
|
||||
<button data-ui="#sync-party" class="circle large transparent">
|
||||
<i class:primary-text={$syncPartyPeerStore}>group</i>
|
||||
<div class="tooltip bottom">{$_('layout.syncParty')}</div>
|
||||
</button>
|
||||
{/if}
|
||||
{#if isLoggedIn}
|
||||
<button class="circle large transparent" onclick={() => ui('#dialog-notifications')}
|
||||
><i>notifications</i>
|
||||
<div class="tooltip bottom">{$_('layout.notifications')}</div>
|
||||
</button>
|
||||
{/if}
|
||||
<button class="circle large transparent" onclick={() => ui('#dialog-settings')}>
|
||||
<i>settings</i>
|
||||
<div class="tooltip bottom">{$_('layout.settings')}</div>
|
||||
</button>
|
||||
|
||||
{#if !isLoggedIn}
|
||||
<button onclick={login} class="circle large transparent">
|
||||
<i>login</i>
|
||||
<div class="tooltip bottom">{$_('layout.login')}</div>
|
||||
</button>
|
||||
{:else}
|
||||
<button onclick={logout} class="circle large transparent">
|
||||
<i>logout</i>
|
||||
<div class="tooltip bottom">{$_('layout.logout')}</div>
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
</nav>
|
||||
{/if}
|
||||
|
||||
<nav class="bottom s">
|
||||
{#each getPages() as navPage (navPage)}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import Search from '$lib/components/Search.svelte';
|
||||
import { goToSearch } from '$lib/search';
|
||||
import {
|
||||
interfaceSearchHistoryEnabled,
|
||||
interfaceSearchSuggestionsStore,
|
||||
searchHistoryStore
|
||||
} from '$lib/store';
|
||||
|
||||
let suggestionsForSearch = $state([]);
|
||||
</script>
|
||||
|
||||
<div class="space"></div>
|
||||
|
||||
<div class="search">
|
||||
<Search hideSearchSuggestionsAndHistory={true} autoFocus={true} bind:suggestionsForSearch />
|
||||
|
||||
<ul class="list border no-space" style="width: 600px;">
|
||||
{#if $interfaceSearchSuggestionsStore}
|
||||
{#each suggestionsForSearch as suggestion, index (index)}
|
||||
<li>
|
||||
<a
|
||||
onclick={() => {
|
||||
goToSearch(suggestion);
|
||||
}}
|
||||
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={() => {
|
||||
goToSearch(history);
|
||||
}}
|
||||
href={resolve(`/search/[search]`, { search: encodeURIComponent(history) })}
|
||||
>
|
||||
<div>{history}</div>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
{/if}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.search {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user