Add search view
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from 'svelte';
|
import { onMount, tick } from 'svelte';
|
||||||
import PixeldrainLogo from '../util/PixeldrainLogo.svelte';
|
import PixeldrainLogo from '../util/PixeldrainLogo.svelte';
|
||||||
import LoadingIndicator from '../util/LoadingIndicator.svelte';
|
import LoadingIndicator from '../util/LoadingIndicator.svelte';
|
||||||
import EditWindow from './EditWindow.svelte';
|
import EditWindow from './EditWindow.svelte';
|
||||||
@@ -9,6 +9,7 @@ import Breadcrumbs from './Breadcrumbs.svelte';
|
|||||||
import DetailsWindow from './DetailsWindow.svelte';
|
import DetailsWindow from './DetailsWindow.svelte';
|
||||||
import Navigator from './Navigator.svelte';
|
import Navigator from './Navigator.svelte';
|
||||||
import FilePreview from './viewers/FilePreview.svelte';
|
import FilePreview from './viewers/FilePreview.svelte';
|
||||||
|
import SearchView from './SearchView.svelte';
|
||||||
|
|
||||||
let loading = true
|
let loading = true
|
||||||
let toolbar_visible = (window.innerWidth > 600)
|
let toolbar_visible = (window.innerWidth > 600)
|
||||||
@@ -17,6 +18,7 @@ let download_frame
|
|||||||
let details_visible = false
|
let details_visible = false
|
||||||
let edit_window
|
let edit_window
|
||||||
let edit_visible = false
|
let edit_visible = false
|
||||||
|
let view = "file"
|
||||||
|
|
||||||
let fs_navigator
|
let fs_navigator
|
||||||
let state = {
|
let state = {
|
||||||
@@ -67,6 +69,10 @@ const keydown = e => {
|
|||||||
case "r":
|
case "r":
|
||||||
state.shuffle = !state.shuffle
|
state.shuffle = !state.shuffle
|
||||||
break;
|
break;
|
||||||
|
case "/":
|
||||||
|
case "f":
|
||||||
|
view = "search"
|
||||||
|
break
|
||||||
case "a":
|
case "a":
|
||||||
case "ArrowLeft":
|
case "ArrowLeft":
|
||||||
fs_navigator.open_sibling(-1)
|
fs_navigator.open_sibling(-1)
|
||||||
@@ -76,6 +82,8 @@ const keydown = e => {
|
|||||||
fs_navigator.open_sibling(1)
|
fs_navigator.open_sibling(1)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
};
|
};
|
||||||
|
|
||||||
const download = () => {
|
const download = () => {
|
||||||
@@ -90,7 +98,11 @@ const download = () => {
|
|||||||
<Navigator
|
<Navigator
|
||||||
bind:this={fs_navigator}
|
bind:this={fs_navigator}
|
||||||
bind:state
|
bind:state
|
||||||
on:navigation_complete={() => file_preview.state_update()}
|
on:navigation_complete={async () => {
|
||||||
|
view = "file";
|
||||||
|
await tick();
|
||||||
|
file_preview.state_update();
|
||||||
|
}}
|
||||||
on:loading={e => loading = e.detail}
|
on:loading={e => loading = e.detail}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -111,7 +123,7 @@ const download = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="file_preview">
|
<div class="viewer_area">
|
||||||
<Toolbar
|
<Toolbar
|
||||||
visible={toolbar_visible}
|
visible={toolbar_visible}
|
||||||
fs_navigator={fs_navigator}
|
fs_navigator={fs_navigator}
|
||||||
@@ -119,19 +131,29 @@ const download = () => {
|
|||||||
bind:details_visible={details_visible}
|
bind:details_visible={details_visible}
|
||||||
edit_window={edit_window}
|
edit_window={edit_window}
|
||||||
bind:edit_visible={edit_visible}
|
bind:edit_visible={edit_visible}
|
||||||
|
bind:view={view}
|
||||||
on:download={download}
|
on:download={download}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div class="file_preview checkers" class:toolbar_visible>
|
||||||
|
{#if view === "file"}
|
||||||
<FilePreview
|
<FilePreview
|
||||||
bind:this={file_preview}
|
bind:this={file_preview}
|
||||||
fs_navigator={fs_navigator}
|
fs_navigator={fs_navigator}
|
||||||
state={state}
|
state={state}
|
||||||
toolbar_visible={toolbar_visible}
|
|
||||||
edit_window={edit_window}
|
edit_window={edit_window}
|
||||||
on:loading={e => {loading = e.detail}}
|
on:loading={e => {loading = e.detail}}
|
||||||
on:open_sibling={e => fs_navigator.open_sibling(e.detail)}
|
on:open_sibling={e => fs_navigator.open_sibling(e.detail)}
|
||||||
on:download={download}
|
on:download={download}
|
||||||
/>
|
/>
|
||||||
|
{:else if view === "search"}
|
||||||
|
<SearchView
|
||||||
|
state={state}
|
||||||
|
fs_navigator={fs_navigator}
|
||||||
|
on:loading={e => {loading = e.detail}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- This frame will load the download URL when a download button is pressed -->
|
<!-- This frame will load the download URL when a download button is pressed -->
|
||||||
@@ -210,7 +232,7 @@ const download = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* File preview area (row 2) */
|
/* File preview area (row 2) */
|
||||||
.file_preview {
|
.viewer_area {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -220,4 +242,24 @@ const download = () => {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
z-index: 9;
|
z-index: 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file_preview {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: block;
|
||||||
|
min-height: 100px;
|
||||||
|
min-width: 100px;
|
||||||
|
transition: left 0.25s;
|
||||||
|
overflow: auto;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid var(--separator);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file_preview.toolbar_visible {
|
||||||
|
left: 8em;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -82,3 +82,15 @@ export const fs_delete_all = async (bucket, path) => {
|
|||||||
throw new Error(await resp.text())
|
throw new Error(await resp.text())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const fs_search = async (bucket, path, term, limit = 10) => {
|
||||||
|
const resp = await fetch(
|
||||||
|
fs_path_url(bucket, path) +
|
||||||
|
"?search=" + encodeURIComponent(term) +
|
||||||
|
"&limit=" + limit
|
||||||
|
)
|
||||||
|
if (resp.status >= 400) {
|
||||||
|
throw await resp.text()
|
||||||
|
}
|
||||||
|
return resp.json()
|
||||||
|
}
|
||||||
|
174
svelte/src/filesystem/SearchView.svelte
Normal file
174
svelte/src/filesystem/SearchView.svelte
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
import { fs_search } from "./FilesystemAPI";
|
||||||
|
import { fs_thumbnail_url } from "./FilesystemUtil";
|
||||||
|
|
||||||
|
export let state
|
||||||
|
export let fs_navigator
|
||||||
|
|
||||||
|
let dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
let search_term = ""
|
||||||
|
let search_results = []
|
||||||
|
let searching = false
|
||||||
|
let last_searched_term = ""
|
||||||
|
let last_limit = 10
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
last_searched_term = search_term
|
||||||
|
last_limit = limit
|
||||||
|
searching = true
|
||||||
|
dispatch("loading", true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
search_results = await fs_search(state.root.id, state.base.path, search_term, limit)
|
||||||
|
} catch (err) {
|
||||||
|
alert(err)
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
searching = false
|
||||||
|
dispatch("loading", 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Submitting opens the first result
|
||||||
|
const submit_search = () => {
|
||||||
|
if (search_results.length !== 0) {
|
||||||
|
fs_navigator.navigate(search_results[0], true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const node_click = index => {
|
||||||
|
fs_navigator.navigate(search_results[index], true)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<form class="search_bar highlight_shaded" on:submit|preventDefault={submit_search}>
|
||||||
|
<i class="icon">search</i>
|
||||||
|
<!-- svelte-ignore a11y-autofocus -->
|
||||||
|
<input class="term" type="text" style="width: 100%;" bind:value={search_term} on:keyup={() => search()} autofocus />
|
||||||
|
<input class="submit" type="submit" value="Search"/>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="directory">
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td>Name</td>
|
||||||
|
</tr>
|
||||||
|
{#each search_results as result, index}
|
||||||
|
<a
|
||||||
|
href={state.path_root+result}
|
||||||
|
on:click|preventDefault={() => node_click(index)}
|
||||||
|
class="node"
|
||||||
|
>
|
||||||
|
<td>
|
||||||
|
<img src={fs_thumbnail_url(state.root.id, result)} class="node_icon" alt="icon"/>
|
||||||
|
</td>
|
||||||
|
<td class="node_name">
|
||||||
|
{result}
|
||||||
|
</td>
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if search_results.length === last_limit}
|
||||||
|
<div class="node">
|
||||||
|
<td></td>
|
||||||
|
<td class="node_name">
|
||||||
|
<button on:click={() => {search(last_limit + 100)}}>
|
||||||
|
<i class="icon">expand_more</i>
|
||||||
|
More results
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.search_bar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 10px auto;
|
||||||
|
}
|
||||||
|
.term {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
.submit {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.directory {
|
||||||
|
display: table;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 8px auto 16px auto;
|
||||||
|
text-align: left;
|
||||||
|
background: var(--body_color);
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
max-width: 100%;
|
||||||
|
width: 1000px;
|
||||||
|
}
|
||||||
|
.directory > * {
|
||||||
|
display: table-row;
|
||||||
|
}
|
||||||
|
.directory > * > * {
|
||||||
|
display: table-cell;
|
||||||
|
}
|
||||||
|
.node {
|
||||||
|
display: table-row;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--text-color);
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
.node:not(:last-child) {
|
||||||
|
border-bottom: 1px solid var(--separator);
|
||||||
|
}
|
||||||
|
.node:hover:not(.node_selected) {
|
||||||
|
background: var(--input_background);
|
||||||
|
color: var(--input_text);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
padding: 4px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.node_icon {
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
vertical-align: middle;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.node_name {
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
line-height: 1.2em;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
</style>
|
@@ -17,6 +17,7 @@ export let state = {
|
|||||||
shuffle: false
|
shuffle: false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export let view = "file"
|
||||||
export let details_visible = false
|
export let details_visible = false
|
||||||
export let edit_window
|
export let edit_window
|
||||||
export let edit_visible = false
|
export let edit_visible = false
|
||||||
@@ -63,6 +64,13 @@ let share = async () => {
|
|||||||
sharebar_visible = !sharebar_visible
|
sharebar_visible = !sharebar_visible
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let toggle_search = () => {
|
||||||
|
if (view === "search") {
|
||||||
|
view = "file"
|
||||||
|
} else {
|
||||||
|
view = "search"
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="toolbar" class:toolbar_visible={visible}>
|
<div class="toolbar" class:toolbar_visible={visible}>
|
||||||
@@ -94,6 +102,12 @@ let share = async () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if state.root.id === "me" && state.base.type === "dir"}
|
||||||
|
<button on:click={toggle_search} class="toolbar_button" class:button_highlight={view === "search"}>
|
||||||
|
<i class="icon">search</i> Search
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if state.base.type === "file"}
|
{#if state.base.type === "file"}
|
||||||
<button on:click={() => dispatch("download")} class="toolbar_button">
|
<button on:click={() => dispatch("download")} class="toolbar_button">
|
||||||
<i class="icon">save</i> Download
|
<i class="icon">save</i> Download
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import { tick } from "svelte";
|
import { onMount, tick } from "svelte";
|
||||||
import Spinner from "../../util/Spinner.svelte";
|
import Spinner from "../../util/Spinner.svelte";
|
||||||
import { fs_node_type } from "../FilesystemUtil";
|
import { fs_node_type } from "../FilesystemUtil";
|
||||||
import FileManager from "../filemanager/FileManager.svelte";
|
import FileManager from "../filemanager/FileManager.svelte";
|
||||||
@@ -13,7 +13,6 @@ import Torrent from "./Torrent.svelte";
|
|||||||
import Zip from "./Zip.svelte";
|
import Zip from "./Zip.svelte";
|
||||||
|
|
||||||
export let fs_navigator
|
export let fs_navigator
|
||||||
export let toolbar_visible
|
|
||||||
export let edit_window
|
export let edit_window
|
||||||
|
|
||||||
export let state
|
export let state
|
||||||
@@ -32,9 +31,12 @@ export const state_update = async () => {
|
|||||||
viewer.update()
|
viewer.update()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
state_update()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="file_preview checkers" class:toolbar_visible>
|
|
||||||
{#if viewer_type === ""}
|
{#if viewer_type === ""}
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<Spinner></Spinner>
|
<Spinner></Spinner>
|
||||||
@@ -63,29 +65,8 @@ export const state_update = async () => {
|
|||||||
{:else}
|
{:else}
|
||||||
<File state={state} on:download/>
|
<File state={state} on:download/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.file_preview {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
display: block;
|
|
||||||
min-height: 100px;
|
|
||||||
min-width: 100px;
|
|
||||||
transition: left 0.25s;
|
|
||||||
overflow: auto;
|
|
||||||
text-align: center;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 2px solid var(--separator);
|
|
||||||
}
|
|
||||||
|
|
||||||
.file_preview.toolbar_visible {
|
|
||||||
left: 8em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.center{
|
.center{
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
|
Reference in New Issue
Block a user