Add a search scope (all/music/shows/movies) resolved from the entry route and adjustable via filter chips, threaded through the library store's search() into includeItemTypes. Results group by type in a user-configurable order, editable from settings. TRACES: UR-049 | DR-063, DR-064, DR-065; UR-050 | DR-066, DR-067
139 lines
4.5 KiB
Svelte
139 lines
4.5 KiB
Svelte
<script lang="ts">
|
|
// Reorderable list of search result groups.
|
|
//
|
|
// Drag-and-drop alone would leave this unusable with a screen reader or any
|
|
// pointerless input, so every row also carries labelled move up/down buttons
|
|
// which are the primary, always-available mechanism.
|
|
//
|
|
// TRACES: UR-050 | DR-066
|
|
import { searchGroupOrder } from "$lib/stores/searchGroupOrder";
|
|
import { GROUP_LABELS, type SearchGroupId } from "$lib/utils/searchScope";
|
|
|
|
let dragIndex = $state<number | null>(null);
|
|
let overIndex = $state<number | null>(null);
|
|
// Announced to assistive tech after a move, since the list itself reorders
|
|
// silently.
|
|
let announcement = $state("");
|
|
|
|
function announce(id: SearchGroupId) {
|
|
const position = $searchGroupOrder.indexOf(id) + 1;
|
|
announcement = `${GROUP_LABELS[id]} moved to position ${position} of ${$searchGroupOrder.length}`;
|
|
}
|
|
|
|
function move(id: SearchGroupId, delta: number) {
|
|
searchGroupOrder.move(id, delta);
|
|
announce(id);
|
|
}
|
|
|
|
function onDragStart(event: DragEvent, index: number) {
|
|
dragIndex = index;
|
|
event.dataTransfer?.setData("text/plain", String(index));
|
|
if (event.dataTransfer) event.dataTransfer.effectAllowed = "move";
|
|
}
|
|
|
|
function onDragOver(event: DragEvent, index: number) {
|
|
if (dragIndex === null) return;
|
|
event.preventDefault();
|
|
overIndex = index;
|
|
if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
|
|
}
|
|
|
|
function onDrop(event: DragEvent, index: number) {
|
|
event.preventDefault();
|
|
if (dragIndex !== null && dragIndex !== index) {
|
|
const id = $searchGroupOrder[dragIndex];
|
|
searchGroupOrder.reorder(dragIndex, index);
|
|
if (id) announce(id);
|
|
}
|
|
dragIndex = null;
|
|
overIndex = null;
|
|
}
|
|
|
|
function onDragEnd() {
|
|
dragIndex = null;
|
|
overIndex = null;
|
|
}
|
|
</script>
|
|
|
|
<ul class="space-y-2">
|
|
{#each $searchGroupOrder as id, i (id)}
|
|
<li
|
|
draggable="true"
|
|
ondragstart={(e) => onDragStart(e, i)}
|
|
ondragover={(e) => onDragOver(e, i)}
|
|
ondrop={(e) => onDrop(e, i)}
|
|
ondragend={onDragEnd}
|
|
class="flex items-center gap-3 px-3 py-2 rounded-lg bg-gray-800 border transition-colors {overIndex ===
|
|
i && dragIndex !== i
|
|
? 'border-[var(--color-jellyfin)]'
|
|
: 'border-gray-700'} {dragIndex === i ? 'opacity-50' : ''}"
|
|
>
|
|
<!-- Decorative: dragging is the mouse affordance, the buttons below are
|
|
the accessible path. -->
|
|
<svg
|
|
class="w-4 h-4 text-gray-500 flex-shrink-0 cursor-grab"
|
|
fill="currentColor"
|
|
viewBox="0 0 24 24"
|
|
aria-hidden="true"
|
|
>
|
|
<path d="M9 4h2v2H9V4zm4 0h2v2h-2V4zM9 9h2v2H9V9zm4 0h2v2h-2V9zm-4 5h2v2H9v-2zm4 0h2v2h-2v-2zm-4 5h2v2H9v-2zm4 0h2v2h-2v-2z" />
|
|
</svg>
|
|
|
|
<span class="text-sm text-gray-400 w-5 flex-shrink-0">{i + 1}</span>
|
|
<span class="flex-1 text-white">{GROUP_LABELS[id]}</span>
|
|
|
|
<button
|
|
type="button"
|
|
onclick={() => move(id, -1)}
|
|
disabled={i === 0}
|
|
aria-label="Move {GROUP_LABELS[id]} up"
|
|
class="p-2 rounded text-gray-300 hover:bg-gray-700 hover:text-white transition-colors disabled:opacity-30 disabled:hover:bg-transparent"
|
|
>
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7" />
|
|
</svg>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onclick={() => move(id, 1)}
|
|
disabled={i === $searchGroupOrder.length - 1}
|
|
aria-label="Move {GROUP_LABELS[id]} down"
|
|
class="p-2 rounded text-gray-300 hover:bg-gray-700 hover:text-white transition-colors disabled:opacity-30 disabled:hover:bg-transparent"
|
|
>
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
|
</svg>
|
|
</button>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
|
|
<div class="sr-only" role="status" aria-live="polite">{announcement}</div>
|
|
|
|
<div class="flex justify-end pt-3">
|
|
<button
|
|
type="button"
|
|
onclick={() => {
|
|
searchGroupOrder.reset();
|
|
announcement = "Search group order reset to default";
|
|
}}
|
|
class="text-sm text-gray-400 hover:text-white transition-colors"
|
|
>
|
|
Reset to default
|
|
</button>
|
|
</div>
|
|
|
|
<style>
|
|
.sr-only {
|
|
position: absolute;
|
|
width: 1px;
|
|
height: 1px;
|
|
padding: 0;
|
|
margin: -1px;
|
|
overflow: hidden;
|
|
clip: rect(0, 0, 0, 0);
|
|
white-space: nowrap;
|
|
border-width: 0;
|
|
}
|
|
</style>
|