Make receiver selection list scrollable/searchable

This commit is contained in:
hensm
2022-11-13 14:59:50 +00:00
parent ab23ab2e03
commit f759d3b727
6 changed files with 164 additions and 3 deletions

13
ext/package-lock.json generated
View File

@@ -11,6 +11,7 @@
"esbuild": "^0.14.38",
"esbuild-svelte": "^0.7.1",
"fs-extra": "^10.1.0",
"fuzzysort": "^2.0.3",
"semver": "^7.3.7",
"svelte": "^3.48.0",
"svelte-preprocess": "^4.10.6",
@@ -3359,6 +3360,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/fuzzysort": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-2.0.3.tgz",
"integrity": "sha512-ridbZloOhbVCom7MDF//Blt4N6d6L4uWNEiBuYBx4Qr+G55A9qItgsAqrRZGgs8q+mFpyBbwi5FL+FWUQPJCew==",
"dev": true
},
"node_modules/fx-runner": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/fx-runner/-/fx-runner-1.2.0.tgz",
@@ -10296,6 +10303,12 @@
"integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
"dev": true
},
"fuzzysort": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-2.0.3.tgz",
"integrity": "sha512-ridbZloOhbVCom7MDF//Blt4N6d6L4uWNEiBuYBx4Qr+G55A9qItgsAqrRZGgs8q+mFpyBbwi5FL+FWUQPJCew==",
"dev": true
},
"fx-runner": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/fx-runner/-/fx-runner-1.2.0.tgz",

View File

@@ -14,6 +14,7 @@
"esbuild": "^0.14.38",
"esbuild-svelte": "^0.7.1",
"fs-extra": "^10.1.0",
"fuzzysort": "^2.0.3",
"semver": "^7.3.7",
"svelte": "^3.48.0",
"svelte-preprocess": "^4.10.6",

View File

@@ -0,0 +1,16 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<style>
path {
fill: rgba(12, 12, 13, .8);
}
@media (prefers-color-scheme: dark) {
path {
fill: rgba(249, 249, 250, .8);
}
}
</style>
<path d="M6.586 8l-2.293 2.293a1 1 0 0 0 1.414 1.414L8 9.414l2.293 2.293a1 1 0 0 0 1.414-1.414L9.414 8l2.293-2.293a1 1 0 1 0-1.414-1.414L8 6.586 5.707 4.293a1 1 0 0 0-1.414 1.414L6.586 8zM8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0z"></path>
</svg>

After

Width:  |  Height:  |  Size: 761 B

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import { afterUpdate, onDestroy, onMount, tick } from "svelte";
import fuzzysort from "fuzzysort";
import messaging, { Message, Port } from "../../messaging";
import options, { Options } from "../../lib/options";
@@ -321,8 +322,47 @@
function openOptionsPage() {
browser.runtime.openOptionsPage();
}
/** Search input element. */
let searchInput: HTMLInputElement | undefined;
/** Current search term. */
let searchTerm: string | undefined;
let isSearching = false;
/** Results of current search term. */
let searchResults: Fuzzysort.KeyResults<ReceiverDevice> | undefined;
async function handleKeyDown(ev: KeyboardEvent) {
if (ev.key === "Escape") {
handleSearchClear();
return;
}
if (!isSearching && ev.key.length === 1) {
isSearching = true;
}
await tick();
searchInput?.focus();
}
function handleSearchInput() {
// Clear search on empty string
if (!searchTerm) {
handleSearchClear();
return;
}
searchResults = fuzzysort.go(searchTerm, devices, {
key: "friendlyName"
});
}
function handleSearchClear() {
isSearching = false;
searchTerm = undefined;
}
</script>
<svelte:window on:keydown={handleKeyDown} />
{#if !isBridgeCompatible}
<div class="banner banner--warn">
{_("popupBridgeErrorBanner")}
@@ -379,8 +419,56 @@
</div>
{/if}
{#if isSearching}
<div class="search">
<input
type="text"
class="search-input"
bind:this={searchInput}
bind:value={searchTerm}
on:input={handleSearchInput}
title={_("popupSearch")}
/>
<button
class="search-clear ghost"
title={_("popupSearchClear")}
on:click={handleSearchClear}
/>
</div>
{/if}
<ul class="receiver-list">
{#if !devices.length}
{#if searchTerm && searchResults}
{#if !searchResults.length}
<div class="receiver-list__not-found">
No devices found for "{searchTerm}"
</div>
{/if}
{#each searchResults as result}
{@const device = devices.find(
device => device.id === result.obj.id
)}
{#if device}
<Receiver
{opts}
{port}
{device}
{result}
{connectedSessionIds}
{isMediaTypeAvailable}
isAnyMediaTypeAvailable={availableMediaTypes !==
ReceiverSelectorMediaType.None &&
isDeviceCompatible(mediaType, device)}
isAnyConnecting={isConnecting}
bind:lastMenuShownDeviceId
on:cast={ev => onReceiverCast(ev.detail.device)}
on:stop={ev => onReceiverStop(ev.detail.device)}
/>
{/if}
{/each}
{:else if !devices.length}
<div class="receiver-list__not-found">
{_("popupNoReceiversFound")}
</div>

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import { createEventDispatcher, onMount } from "svelte";
import fuzzysort from "fuzzysort";
import type { Options } from "../../lib/options";
@@ -39,6 +40,9 @@
export let device: ReceiverDevice;
export let connectedSessionIds: string[];
/** Result object if this receiver is displayed in a search results list. */
export let result: Nullable<Fuzzysort.KeyResult<ReceiverDevice>> = null;
export let opts: Nullable<Options>;
/** Current receiver application (if available) */
@@ -402,7 +406,12 @@
});
</script>
<li class="receiver" bind:this={receiverElement} on:contextmenu={onContextMenu}>
<li
class="receiver"
class:receiver--result={!!result}
bind:this={receiverElement}
on:contextmenu={onContextMenu}
>
<img
class="receiver__icon"
src="icons/{device.capabilities & ReceiverDeviceCapabilities.VIDEO_OUT
@@ -414,7 +423,11 @@
/>
<div class="receiver__details">
<div class="receiver__name">
{device.friendlyName}
{#if result}
{@html fuzzysort.highlight(result)}
{:else}
{device.friendlyName}
{/if}
</div>
{#if application && !application.isIdleScreen}
<div class="receiver__status">

View File

@@ -83,9 +83,36 @@ body {
margin-inline-start: 0.5em;
}
.search {
display: flex;
padding: 1em;
position: relative;
margin-top: -1px;
}
.search-input {
width: 100%;
}
.search-clear {
background-color: transparent !important;
background-image: url("../../assets/photon_cancel.svg");
background-size: 50%;
margin-right: 1em;
opacity: 0.5;
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
}
.search-clear:hover {
opacity: 1;
}
.receiver-list {
list-style: none;
margin: initial;
height: auto;
max-height: 500px;
overflow-y: auto;
padding: 0 1em;
padding-bottom: 0.25em;
}
@@ -108,6 +135,9 @@ body {
padding: 0.75em 0;
position: relative;
}
.receiver--result .receiver__name > b {
font-weight: 600;
}
.receiver:not(:last-child) {
border-bottom: 1px solid var(--border-color);
}