Move search bar into modal

This commit is contained in:
2026-04-01 19:38:15 +02:00
parent dbfe9ff383
commit f578f2c2c0
13 changed files with 299 additions and 202 deletions

View File

@@ -8,24 +8,26 @@ import CompactView from "./CompactView.svelte"
import Button from "layout/Button.svelte";
import { formatDate } from "util/Formatting";
import { drop_target } from "lib/DropTarget"
import SearchBar from "./SearchBar.svelte";
import type { FSNavigator } from "filesystem/FSNavigator";
import FsUploadWidget from "filesystem/upload_widget/FSUploadWidget.svelte";
import EditWindow from "filesystem/edit_window/EditWindow.svelte";
import { FileAction, type FileActionHandler } from "./FileManagerLib";
import FileMenu from "./FileMenu.svelte";
import { loading_finish, loading_start } from "lib/Loading";
import SearchBar from "../SearchBar.svelte";
let {
nav = $bindable(),
upload_widget,
edit_window = $bindable(),
search_bar = $bindable(),
directory_view = $bindable(""),
children
}: {
nav: FSNavigator;
upload_widget: FsUploadWidget;
edit_window: EditWindow;
search_bar: SearchBar;
directory_view?: string;
children?: import('svelte').Snippet;
} = $props();
@@ -306,6 +308,9 @@ onMount(() => {
{#if mode === "viewing"}
<div class="toolbar">
<div class="toolbar_left">
<button onclick={() => {search_bar.open()}} title="Press / to search">
<i class="icon">search</i>
</button>
<button onclick={navigate_back} title="Back">
<i class="icon">arrow_back</i>
</button>
@@ -356,8 +361,6 @@ onMount(() => {
</div>
</div>
<SearchBar nav={nav}/>
{:else if mode === "selecting"}
<div class="toolbar toolbar_edit">
<Button click={viewing_mode} icon="close"/>

View File

@@ -4,9 +4,8 @@ import ListView from "./ListView.svelte"
import GalleryView from "./GalleryView.svelte"
import CompactView from "./CompactView.svelte"
import Modal from "util/Modal.svelte";
import Breadcrumbs from "filesystem/Breadcrumbs.svelte"
import { FSNavigator } from "filesystem/FSNavigator";
import type { FSNode } from "lib/FilesystemAPI.svelte";
import { FSNavigator, path_link } from "filesystem/FSNavigator";
import { fs_encode_path, type FSNode } from "lib/FilesystemAPI.svelte";
import { FileAction, type FileActionHandler } from "./FileManagerLib";
let nav = $state(new FSNavigator(false))
@@ -171,7 +170,26 @@ onMount(() => {
</div>
{/snippet}
<Breadcrumbs nav={nav}/>
{#each $nav.path as node, i (node.path)}
<a
href={"/d"+fs_encode_path(node.path)}
class="breadcrumb button flat"
use:path_link={{nav: nav, node: node}}
>
{#if node.abuse_type !== undefined}
<i class="icon small">block</i>
{:else if node.is_shared()}
<i class="icon small">share</i>
{/if}
<div class="node_name" class:base={$nav.base_index === i}>
{node.name}
</div>
</a>
{#if $nav.base_index !== i}
<i class="icon">chevron_right</i>
{/if}
{/each}
{#if directory_view === "list"}
<ListView

View File

@@ -1,289 +0,0 @@
<script lang="ts">
import { onMount } from "svelte";
import { fs_search, fs_encode_path, fs_thumbnail_url } from "lib/FilesystemAPI.svelte";
import type { FSNavigator } from "filesystem/FSNavigator";
import { loading_finish, loading_start } from "lib/Loading";
let { nav }: {
nav: FSNavigator;
} = $props();
let search_bar: HTMLInputElement = $state()
let error = $state("")
let search_term = $state("")
let search_results: string[] = $state([])
let selected_result = $state(0)
let searching = false
let last_searched_term = ""
let last_limit = $state(10)
onMount(() => {
// Clear results when the user moves to a new directory
return nav.subscribe(nav => {
if (nav.initialized) {
clear_search(false)
}
})
})
const search = async (limit = 10) => {
if (search_term.length < 2 || search_term.length > 100) {
// These are the length limits defined by the API
search_results = []
return
} else if (search_term === last_searched_term && limit === last_limit) {
// If the term is the same we don't have to search
return
} else if (searching) {
// If a search is already running we also don't search
return
}
console.debug("Searching for", search_term)
error = ""
last_searched_term = search_term
last_limit = limit
searching = true
try {
loading_start()
search_results = await fs_search(nav.base.path, search_term, limit)
} catch (err) {
if (err.value) {
error = err.value
} else {
alert(err)
console.error(err)
}
} finally {
loading_finish()
}
if (search_results.length > 0 && selected_result > search_results.length-1) {
selected_result = search_results.length-1
}
searching = false
// It's possible that the user entered another letter while we were
// performing the search reqeust. If this happens we run the search function
// again
if (last_searched_term !== search_term) {
console.debug("Search term changed during search. Searching again")
await search()
}
}
const clear_search = (blur: boolean) => {
error = ""
search_term = ""
search_results = []
selected_result = 0
searching = false
last_searched_term = ""
last_limit = 10
// If blur is true we unfocus the search field. This should only happen when
// the user presses escape
if (blur) {
search_bar.blur()
}
}
// Cursor navigation events can only be prevented with keydown. But we want to
// use keyup for searching, so we use two listeners here
const input_keydown = (e: KeyboardEvent) => {
if (e.key === "Escape" || e.key === "ArrowUp" || e.key === "ArrowDown") {
e.preventDefault()
}
}
const input_keyup = (e: KeyboardEvent) => {
if (e.key === "Escape") {
clear_search(true)
} else if (e.key === "ArrowUp") {
if (selected_result > 0) {
selected_result--
}
} else if (e.key === "ArrowDown") {
if (selected_result+1 < search_results.length) {
selected_result++
}
} else {
search()
}
}
// Submitting opens the selected result
const submit_search = (e: Event) => {
e.preventDefault()
if (search_results.length !== 0) {
open_result(selected_result)
}
}
const open_result = (index: number, e?: MouseEvent) => {
if (e !== undefined) {
e.preventDefault()
}
nav.navigate(search_results[index], true)
clear_search(false)
}
const window_keydown = (e: KeyboardEvent) => {
if (e.ctrlKey || e.altKey || e.metaKey) {
return // prevent custom shortcuts from interfering with system shortcuts
} else if ((document.activeElement as any).type !== undefined && (document.activeElement as any).type === "text") {
return // Prevent shortcuts from interfering with input fields
}
if (e.key === "Escape" && search_term !== "") {
clear_search(true)
e.preventDefault()
} else if (e.key === "/" || e.key === "f") {
e.preventDefault()
e.stopPropagation()
search_bar.focus()
return
}
}
</script>
<svelte:window onkeydown={window_keydown} />
{#if error === "path_not_found" || error === "node_is_a_directory"}
<div class="highlight_yellow center">
Search index not found. The search index is a file called
'.search_index.gz' in your home directory. If you delete this file then
search will not work. The file is regenerated 10 minutes after modifying
a file in your filesystem.
</div>
{:else if error !== ""}
<div class="highlight_red center">
An error ocurred while executing the search request: {error}
</div>
{/if}
<div class="center">
<form class="search_form" onsubmit={submit_search}>
<i class="icon">search</i>
<input
bind:this={search_bar}
class="term"
type="text"
placeholder="Press / to search in {$nav.base.name}"
style="width: 100%;"
bind:value={search_term}
onkeydown={input_keydown}
onkeyup={input_keyup}
/>
{#if search_term !== ""}
<!-- Button needs to be of button type in order to not submit the form -->
<button onclick={() => clear_search(false)} type="button">
<i class="icon">close</i>
</button>
{/if}
</form>
<div class="results">
{#if search_term !== "" && search_results.length === 0}
No results found
{/if}
{#each search_results as result, index}
<a
href={"/d"+fs_encode_path(result)}
onclick={(e) => open_result(index, e)}
class="node"
class:node_selected={selected_result === index}
>
<img src={fs_thumbnail_url(result, 32, 32)} class="node_icon" alt="icon"/>
<span class="node_name">
<!-- Remove the search directory from the result -->
{result.slice($nav.base.path.length+1)}
</span>
</a>
{/each}
{#if search_results.length === last_limit}
<div class="node">
<div class="node_name" style="text-align: center;">
<button onclick={() => {search(last_limit + 100)}}>
<i class="icon">expand_more</i>
More results
</button>
</div>
</div>
{/if}
</div>
</div>
<style>
.center {
margin: auto;
width: 1000px;
max-width: 100%;
padding-top: 2px;
padding-bottom: 2px;
}
.search_form {
display: flex;
flex-direction: row;
align-items: center;
}
.search_form > * {
flex: 0 0 auto;
}
.search_form > .term {
flex: 1 1 auto;
}
.results {
display: flex;
flex-direction: column;
position: relative;
overflow-x: hidden;
overflow-y: auto;
max-height: 80vh;
text-align: left;
background: var(--body_color);
border-radius: 8px;
}
.results > * {
display: flex;
flex-direction: row;
}
.node {
display: flex;
flex-direction: row;
align-items: center;
gap: 4px;
text-decoration: none;
color: var(--text-color);
padding: 2px;
}
.node_selected {
background: var(--highlight_background);
color: var(--highlight_text_color);
}
.node:hover:not(.node_selected) {
background: var(--input_background);
color: var(--input_text);
text-decoration: none;
}
.node_icon {
flex: 0 0 auto;
height: 32px;
width: 32px;
vertical-align: middle;
border-radius: 4px;
}
.node_name {
flex: 1 1 auto;
width: 100%;
line-height: 1.2em;
word-break: break-all;
}
</style>