Compare commits

..

6 Commits

Author SHA1 Message Date
dbfe9ff383 Limit size of siblings list and some bug fixes 2026-04-01 15:30:11 +02:00
8a2cdb4acd Add copy button for node ID 2026-04-01 15:28:01 +02:00
c0c61b07f0 Update dependencies 2026-04-01 15:26:36 +02:00
158ee0a206 Add sum and average to host metrics 2026-04-01 15:26:17 +02:00
0851d16cac Improve menu swipe detection 2026-02-26 14:47:23 +01:00
00b432f3dc Add context menu to breadcrumbs 2026-02-26 14:47:01 +01:00
17 changed files with 175 additions and 92 deletions

2
go.mod
View File

@@ -1,6 +1,6 @@
module fornaxian.tech/fnx_web module fornaxian.tech/fnx_web
go 1.25.1 go 1.26
replace ( replace (
fornaxian.tech/pixeldrain_api_client => ../pixeldrain_api_client fornaxian.tech/pixeldrain_api_client => ../pixeldrain_api_client

View File

@@ -28,6 +28,7 @@
"@rollup/plugin-typescript": "^11.1.6", "@rollup/plugin-typescript": "^11.1.6",
"@types/jsmediatags": "^3.9.6", "@types/jsmediatags": "^3.9.6",
"@types/node": "^24.10.0", "@types/node": "^24.10.0",
"baseline-browser-mapping": "^2.10.13",
"rollup": "^4.24.4", "rollup": "^4.24.4",
"rollup-plugin-livereload": "^2.0.5", "rollup-plugin-livereload": "^2.0.5",
"rollup-plugin-svelte": "^7.2.2", "rollup-plugin-svelte": "^7.2.2",
@@ -2701,13 +2702,16 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/baseline-browser-mapping": { "node_modules/baseline-browser-mapping": {
"version": "2.8.16", "version": "2.10.13",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.13.tgz",
"integrity": "sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==", "integrity": "sha512-BL2sTuHOdy0YT1lYieUxTw/QMtPBC3pmlJC6xk8BBYVv6vcw3SGdKemQ+Xsx9ik2F/lYDO9tqsFQH1r9PFuHKw==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"baseline-browser-mapping": "dist/cli.js" "baseline-browser-mapping": "dist/cli.cjs"
},
"engines": {
"node": ">=6.0.0"
} }
}, },
"node_modules/behave-js": { "node_modules/behave-js": {

View File

@@ -18,6 +18,7 @@
"@rollup/plugin-typescript": "^11.1.6", "@rollup/plugin-typescript": "^11.1.6",
"@types/jsmediatags": "^3.9.6", "@types/jsmediatags": "^3.9.6",
"@types/node": "^24.10.0", "@types/node": "^24.10.0",
"baseline-browser-mapping": "^2.10.13",
"rollup": "^4.24.4", "rollup": "^4.24.4",
"rollup-plugin-livereload": "^2.0.5", "rollup-plugin-livereload": "^2.0.5",
"rollup-plugin-svelte": "^7.2.2", "rollup-plugin-svelte": "^7.2.2",

View File

@@ -158,13 +158,9 @@ const load_metrics = async (window: number, interval: number) => {
for (const host of Object.keys(metrics.metrics[graph.agg_base])) { for (const host of Object.keys(metrics.metrics[graph.agg_base])) {
metrics.metrics[graph.metric][host] = [] metrics.metrics[graph.metric][host] = []
for (let i = 0; i < metrics.metrics[graph.agg_base][host].length; i++) { for (let i = 0; i < metrics.metrics[graph.agg_base][host].length; i++) {
if (metrics.metrics[graph.agg_divisor][host][i] > 0) { metrics.metrics[graph.metric][host].push(
metrics.metrics[graph.metric][host].push( metrics.metrics[graph.agg_base][host][i] / Math.max(metrics.metrics[graph.agg_divisor][host][i], 1)
metrics.metrics[graph.agg_base][host][i] / metrics.metrics[graph.agg_divisor][host][i] )
)
} else {
metrics.metrics[graph.metric][host].push(0)
}
} }
} }
} }
@@ -204,7 +200,7 @@ onDestroy(() => {
<button onclick={() => setWindow(1051200, 1440)}>Two Years 1d</button> <button onclick={() => setWindow(1051200, 1440)}>Two Years 1d</button>
<button onclick={() => setWindow(2628000, 1440)}>Five Years 1d</button> <button onclick={() => setWindow(2628000, 1440)}>Five Years 1d</button>
<br/> <br/>
<ToggleButton bind:on={showAggregate}>Aggregate</ToggleButton> <ToggleButton bind:on={showAggregate}>Sum and Average</ToggleButton>
</div> </div>
{#each groups as group (group.title)} {#each groups as group (group.title)}
@@ -236,6 +232,6 @@ onDestroy(() => {
} }
.grid { .grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
} }
</style> </style>

View File

@@ -28,39 +28,49 @@ const update_chart = async (timestamps: string[], metrics: {[key: string]: numbe
// there are in the response // there are in the response
chart.data().datasets.length = Object.keys(metrics).length chart.data().datasets.length = Object.keys(metrics).length
let i = 0
if (aggregate === true) { if (aggregate === true) {
i = 1 chart.data().datasets.length = 2
chart.data().datasets[0] = { chart.data().datasets[0] = {
label: "aggregate", label: "sum",
data: create_aggregate_dataset(metrics), data: create_sum_dataset(metrics),
borderWidth: 1, borderWidth: 1.5,
pointRadius: 0, pointRadius: 0,
borderColor: "#ffffff", borderColor: "#ff0000",
backgroundColor: "#ffffff", backgroundColor: "#ff0000",
} }
} chart.data().datasets[1] = {
label: "average",
for (const host of Object.keys(metrics).sort()) { data: create_avg_dataset(metrics),
if (chart.data().datasets[i] === undefined) { borderWidth: 1.5,
chart.data().datasets[i] = { pointRadius: 0,
label: "", borderColor: "#00ff00",
data: [], backgroundColor: "#00ff00",
borderWidth: 1, }
pointRadius: 0, } else {
chart.data().datasets.length = Object.keys(metrics).length
let i = 0
for (const host of Object.keys(metrics).sort()) {
if (chart.data().datasets[i] === undefined) {
chart.data().datasets[i] = {
label: "",
data: [],
borderWidth: 1,
pointRadius: 0,
}
} }
chart.data().datasets[i].label = await host_label(host)
chart.data().datasets[i].borderWidth = 1
chart.data().datasets[i].borderColor = host_colour(host)
chart.data().datasets[i].backgroundColor = host_colour(host)
chart.data().datasets[i].data = [...metrics[host]]
i++
} }
chart.data().datasets[i].label = await host_label(host)
chart.data().datasets[i].borderColor = host_colour(host)
chart.data().datasets[i].backgroundColor = host_colour(host)
chart.data().datasets[i].data = [...metrics[host]]
i++
} }
chart.update() chart.update()
} }
const create_aggregate_dataset = (hosts: {[key:string]: number[]}): number[] => { const create_sum_dataset = (hosts: {[key:string]: number[]}): number[] => {
let data: number[] = [] let data: number[] = []
for (const host of Object.keys(hosts)) { for (const host of Object.keys(hosts)) {
for (let idx = 0; idx < hosts[host].length; idx++) { for (let idx = 0; idx < hosts[host].length; idx++) {
@@ -72,6 +82,16 @@ const create_aggregate_dataset = (hosts: {[key:string]: number[]}): number[] =>
} }
return data return data
} }
const create_avg_dataset = (hosts: {[key:string]: number[]}): number[] => {
// The calculate the average, we take the sum and divide it by the number of
// hosts
let data: number[] = create_sum_dataset(hosts)
const num_hosts = Object.keys(hosts).length
for (let idx=0; idx < data.length; idx++) {
data[idx] /= num_hosts
}
return data
}
</script> </script>
<div> <div>

View File

@@ -2,15 +2,28 @@
import { fs_encode_path } from "lib/FilesystemAPI.svelte"; import { fs_encode_path } from "lib/FilesystemAPI.svelte";
import { path_link, type FSNavigator } from "./FSNavigator"; import { path_link, type FSNavigator } from "./FSNavigator";
import { menu_is_open } from "wrap/MainMenu.svelte"; import { menu_is_open } from "wrap/MainMenu.svelte";
import FileMenu from "./filemanager/FileMenu.svelte";
import EditWindow from "./edit_window/EditWindow.svelte";
let { nav }: { let {
nav,
edit_window = null
}: {
nav: FSNavigator; nav: FSNavigator;
edit_window?: EditWindow;
} = $props(); } = $props();
let file_menu: FileMenu = $state()
</script> </script>
<div class="breadcrumbs" class:menu_closed={!$menu_is_open}> <div class="breadcrumbs" class:menu_closed={!$menu_is_open}>
{#each $nav.path as node, i (node.path)} {#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}}> <a
href={"/d"+fs_encode_path(node.path)}
class="breadcrumb button flat"
use:path_link={{nav: nav, node: node}}
oncontextmenu={e => file_menu.open(node, e.target, e)}
>
{#if node.abuse_type !== undefined} {#if node.abuse_type !== undefined}
<i class="icon small">block</i> <i class="icon small">block</i>
{:else if node.is_shared()} {:else if node.is_shared()}
@@ -26,6 +39,8 @@ let { nav }: {
{/each} {/each}
</div> </div>
<FileMenu bind:this={file_menu} nav={nav} edit_window={edit_window} />
<style> <style>
.breadcrumbs { .breadcrumbs {
flex: 0 0 auto; flex: 0 0 auto;

View File

@@ -172,12 +172,13 @@ run(() => {
<td>Mode</td> <td>Mode</td>
<td>{$nav.base.mode_string}</td> <td>{$nav.base.mode_string}</td>
</tr> </tr>
{#if $nav.base.id} <tr>
<tr> <td>Node ID</td>
<td>Public ID</td> <td>
<td><a href="/d/{$nav.base.id}">{$nav.base.id}</a></td> <CopyButton text={$nav.base.id}>Copy</CopyButton>
</tr> <a href="/d/{$nav.base.id}">{$nav.base.id}</a>
{/if} </td>
</tr>
{#if $nav.base.type === "file"} {#if $nav.base.type === "file"}
<tr> <tr>
<td>File type</td> <td>File type</td>

View File

@@ -54,7 +54,13 @@ onMount(() => {
const keydown = (e: KeyboardEvent) => { const keydown = (e: KeyboardEvent) => {
if (e.ctrlKey || e.altKey || e.metaKey) { if (e.ctrlKey || e.altKey || e.metaKey) {
return // prevent custom shortcuts from interfering with system shortcuts return // prevent custom shortcuts from interfering with system shortcuts
} else if ((document.activeElement as any).type !== undefined && (document.activeElement as any).type === "text") { } else if (
(document.activeElement as any).type !== undefined &&
(
(document.activeElement as any).type === "text" ||
(document.activeElement as any).type === "textarea"
)
) {
return // Prevent shortcuts from interfering with input fields return // Prevent shortcuts from interfering with input fields
} }
@@ -131,7 +137,7 @@ const keydown = (e: KeyboardEvent) => {
<svelte:window onkeydown={keydown} /> <svelte:window onkeydown={keydown} />
<div class="filesystem"> <div class="filesystem">
<Breadcrumbs nav={nav}/> <Breadcrumbs nav={nav} edit_window={edit_window}/>
<div class="file_preview"> <div class="file_preview">
<FilePreview <FilePreview

View File

@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import { run } from 'svelte/legacy'; import { fs_delete_all, fs_rename } from "lib/FilesystemAPI.svelte"
import { fs_delete_all, fs_rename, type FSNode } from "lib/FilesystemAPI.svelte"
import { onMount } from "svelte" import { onMount } from "svelte"
import CreateDirectory from "./CreateDirectory.svelte" import CreateDirectory from "./CreateDirectory.svelte"
import ListView from "./ListView.svelte" import ListView from "./ListView.svelte"
@@ -225,18 +224,21 @@ const select_node = (index: number) => {
last_selected_node = index last_selected_node = index
} }
const update = (children: FSNode[]) => { // When the directory is reloaded we want to keep our selection, so this
// function watches the children array for changes and updates the selection
// when it changes
$effect.pre(() => {
creating_dir = false creating_dir = false
// Highlight the files which were previously selected // Highlight the files which were previously selected
for (let i = 0; i < children.length; i++) { for (let i = 0; i < $nav.children.length; i++) {
for (let j = 0; j < moving_items.length; j++) { for (let j = 0; j < moving_items.length; j++) {
if (moving_items[j].path === children[i].path) { if (moving_items[j].path === $nav.children[i].path) {
children[i].fm_selected = true $nav.children[i].fm_selected = true
} }
} }
} }
} })
let moving_files = $state(0) let moving_files = $state(0)
let moving_directories = $state(0) let moving_directories = $state(0)
@@ -289,12 +291,6 @@ onMount(() => {
directory_view = "list" directory_view = "list"
} }
}) })
// When the directory is reloaded we want to keep our selection, so this
// function watches the children array for changes and updates the selection
// when it changes
run(() => {
update($nav.children)
});
</script> </script>
<svelte:window onkeydown={keypress} onkeyup={keypress} /> <svelte:window onkeydown={keypress} onkeyup={keypress} />

View File

@@ -10,16 +10,19 @@ import { tick } from "svelte";
let { let {
nav, nav,
edit_window edit_window = null
}: { }: {
nav: FSNavigator; nav: FSNavigator;
edit_window: EditWindow; edit_window?: EditWindow;
} = $props(); } = $props();
let dialog: Dialog = $state() let dialog: Dialog = $state()
let node: FSNode = $state(null) let node: FSNode = $state(null)
export const open = async (n: FSNode, target: EventTarget, event: Event) => { export const open = async (n: FSNode, target: EventTarget, event: Event) => {
event.preventDefault()
event.stopPropagation()
node = n node = n
// Wait for the view to update, so the dialog gets the proper measurements // Wait for the view to update, so the dialog gets the proper measurements
await tick() await tick()
@@ -61,9 +64,11 @@ const delete_node = async () => {
{/if} {/if}
{#if $nav.permissions.write} {#if $nav.permissions.write}
<Button click={() => {dialog.close(); delete_node()}} icon="delete" label="Delete"/> <Button click={() => {dialog.close(); delete_node()}} icon="delete" label="Delete"/>
<Button click={() => {dialog.close(); edit_window.edit(node, false, "file")}} icon="edit" label="Edit"/> {#if edit_window !== null}
<Button click={() => {dialog.close(); edit_window.edit(node, false, "share")}} icon="share" label="Share"/> <Button click={() => {dialog.close(); edit_window.edit(node, false, "file")}} icon="edit" label="Edit"/>
<Button click={() => {dialog.close(); edit_window.edit(node, false, "branding")}} icon="palette" label="Branding"/> <Button click={() => {dialog.close(); edit_window.edit(node, false, "share")}} icon="share" label="Share"/>
<Button click={() => {dialog.close(); edit_window.edit(node, false, "branding")}} icon="palette" label="Branding"/>
{/if}
{/if} {/if}
</div> </div>
</Dialog> </Dialog>

View File

@@ -34,7 +34,7 @@ let {
</button> </button>
</IconBlock> </IconBlock>
{#if node.name === ".search_index.gz"} {#if node.name === ".search_index.zstd"}
<TextBlock> <TextBlock>
<p> <p>
Congratulations! You have found the search index. One of the Congratulations! You have found the search index. One of the
@@ -50,7 +50,7 @@ let {
have a lot of repetitive elements it compresses incredibly well. have a lot of repetitive elements it compresses incredibly well.
You'd be hard-pressed to grow this index over even 1 MB. Honestly, You'd be hard-pressed to grow this index over even 1 MB. Honestly,
this search system is incredibly efficient, I'd be surprised if this search system is incredibly efficient, I'd be surprised if
EleasticSearch could even match it. ElasticSearch could even match it.
</p> </p>
<p> <p>
This file is updated 10 minutes after the last time you modify a This file is updated 10 minutes after the last time you modify a

View File

@@ -7,6 +7,7 @@ let {
group_middle = false, group_middle = false,
group_last = false, group_last = false,
highlight = true, highlight = true,
flat = false,
action, action,
children, children,
}: { }: {
@@ -17,6 +18,7 @@ let {
group_middle?: boolean; group_middle?: boolean;
group_last?: boolean; group_last?: boolean;
highlight?: boolean; highlight?: boolean;
flat?: boolean;
action?: (e: MouseEvent) => void; action?: (e: MouseEvent) => void;
children?: import('svelte').Snippet; children?: import('svelte').Snippet;
} = $props(); } = $props();
@@ -37,6 +39,7 @@ const click = (e: MouseEvent) => {
class:group_first class:group_first
class:group_middle class:group_middle
class:group_last class:group_last
class:flat
> >
{#if on} {#if on}
<i class="icon">{icon_on}</i> <i class="icon">{icon_on}</i>

View File

@@ -1,5 +1,5 @@
// Dead zone before the swipe action gets detected // Dead zone before the swipe action gets detected
const swipe_inital_offset = 25 const swipe_initial_offset = 25
// Amount of pixels after which the navigation triggers // Amount of pixels after which the navigation triggers
const swipe_trigger_offset = 75 const swipe_trigger_offset = 75
@@ -38,8 +38,8 @@ export const swipe_nav = (
// The cursor must have moved at least 50 pixels and three times as much // The cursor must have moved at least 50 pixels and three times as much
// on the x axis than the y axis for it to count as a swipe // on the x axis than the y axis for it to count as a swipe
if (abs_x > swipe_inital_offset && abs_y < abs_x / 3) { if (abs_x > swipe_initial_offset && abs_y < abs_x / 3) {
set_offset((abs_x - swipe_inital_offset) * neg, false) set_offset((abs_x - swipe_initial_offset) * neg, false)
} else { } else {
set_offset(0, true) set_offset(0, true)
} }

View File

@@ -79,7 +79,7 @@ const drop = (e: DragEvent, drop_idx: number) => {
<MenuEntry id="bookmarks" collapsed={menu_collapsed}> <MenuEntry id="bookmarks" collapsed={menu_collapsed}>
{#snippet title()} {#snippet title()}
<div class="title">Bookmarks</div> <div class="title">Bookmarks</div>
<button onclick={toggle_edit} class:button_highlight={editing}> <button onclick={toggle_edit} class:button_highlight={editing} class="button flat">
{#if editing} {#if editing}
<i class="icon">save</i> <i class="icon">save</i>
{:else} {:else}
@@ -129,7 +129,6 @@ const drop = (e: DragEvent, drop_idx: number) => {
<style> <style>
.title { .title {
flex: 1 1 auto; flex: 1 1 auto;
text-align: center;
} }
.row { .row {
display: flex; display: flex;

View File

@@ -18,8 +18,21 @@ import Tree from "./Tree.svelte";
import MenuEntry from "./MenuEntry.svelte"; import MenuEntry from "./MenuEntry.svelte";
import { writable } from "svelte/store"; import { writable } from "svelte/store";
// The menu swipe will be detected if it was less than this much pixels from the
// screen edge
const screen_edge_offset = 50
// Dead zone before the swipe action gets detected
const swipe_dead_zone = screen_edge_offset/4
// If the screen is less wide than this, the menu will appear full screen
const min_screen_size_fullscreen_menu = 500
// If the screen is smaller than this the menu will be closed by default
const min_screen_size_menu_open = 800
onMount(() => { onMount(() => {
if (document.documentElement.clientWidth < 1000) { if (document.documentElement.clientWidth < min_screen_size_menu_open) {
menu_close() menu_close()
} }
@@ -33,21 +46,20 @@ onMount(() => {
}) })
}) })
// Dead zone before the swipe action gets detected
const swipe_initial_offset = 50
const min_screen_size_fullscreen_menu = 500
let menu: HTMLDivElement let menu: HTMLDivElement
let nav: HTMLElement let nav: HTMLElement
let dragging: boolean = false let dragging: boolean = false
let start_x: number let start_x: number
let start_y: number
let render_offset: number let render_offset: number
let initial_offset: number let initial_offset: number
const touchstart = (e: TouchEvent) => { const touchstart = (e: TouchEvent) => {
start_x = e.touches[0].clientX start_x = e.touches[0].clientX
start_y = e.touches[0].clientY
const rect = menu.getBoundingClientRect() const rect = menu.getBoundingClientRect()
if (start_x < Math.max(swipe_initial_offset, (rect.width+rect.left))) { if (start_x < Math.max(screen_edge_offset, (rect.width+rect.left))) {
dragging = true dragging = true
e.stopPropagation()
} }
render_offset = rect.left render_offset = rect.left
@@ -58,7 +70,19 @@ const touchmove = (e: TouchEvent) => {
if (!dragging) { if (!dragging) {
return return
} }
set_offset(initial_offset+(e.touches[0].clientX - start_x))
const x = e.touches[0].clientX - start_x
const y = e.touches[0].clientY - start_y
const abs_x = Math.abs(x)
const abs_y = Math.abs(y)
// The cursor must have moved at least swipe_dead_zone pixels and three
// times as much on the x axis than the y axis for it to count as a swipe
if (abs_x > swipe_dead_zone && abs_x / 3 > abs_y) {
set_offset(initial_offset+(x*2))
} else {
set_offset(initial_offset)
}
} }
const touchend = (e: TouchEvent) => { const touchend = (e: TouchEvent) => {
@@ -112,7 +136,7 @@ const menu_close = () => {
const set_offset = (off: number) => { const set_offset = (off: number) => {
render_offset = off render_offset = off
if (off > -swipe_initial_offset) { if (off > -swipe_dead_zone) {
// Clear the transformation if the offset is zero // Clear the transformation if the offset is zero
menu.style.transform = "" menu.style.transform = ""
} else { } else {
@@ -123,8 +147,11 @@ const set_offset = (off: number) => {
<svelte:window ontouchstart={touchstart} ontouchmove={touchmove} ontouchend={touchend}/> <svelte:window ontouchstart={touchstart} ontouchmove={touchmove} ontouchend={touchend}/>
<button class="button_toggle_navigation" onclick={toggle_menu}> <button class="menu_button" onclick={toggle_menu}>
<i class="icon">menu</i> <i class="icon">menu</i>
{#if $menu_is_open}
<span>Menu</span>
{/if}
</button> </button>
<div class="nav_container" bind:this={menu}> <div class="nav_container" bind:this={menu}>
@@ -225,7 +252,7 @@ const set_offset = (off: number) => {
background-repeat: var(--background_image_repeat, repeat); background-repeat: var(--background_image_repeat, repeat);
background-attachment: fixed; background-attachment: fixed;
} }
.button_toggle_navigation { .menu_button {
position: fixed; position: fixed;
backface-visibility: hidden; backface-visibility: hidden;
z-index: 10; z-index: 10;
@@ -236,10 +263,10 @@ const set_offset = (off: number) => {
margin: 0; margin: 0;
padding: 4px; padding: 4px;
border-radius: 0; border-radius: 0;
border-bottom-right-radius: 0px; border-bottom-right-radius: 4px;
border-bottom-right-radius: 2px;
backdrop-filter: blur(6px); backdrop-filter: blur(6px);
} }
.nav_container { .nav_container {
flex: 0 0 auto; flex: 0 0 auto;
border-right: 1px solid var(--separator); border-right: 1px solid var(--separator);
@@ -259,8 +286,6 @@ const set_offset = (off: number) => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 15em; width: 15em;
min-width: 10em;
max-width: 15em;
padding-top: 2em; padding-top: 2em;
} }
.nav > .button { .nav > .button {
@@ -274,7 +299,6 @@ const set_offset = (off: number) => {
} }
.username { .username {
flex: 1 1 auto; flex: 1 1 auto;
text-align: center;
margin: 3px; margin: 3px;
} }

View File

@@ -44,7 +44,7 @@ const toggle = (e: MouseEvent) => {
</script> </script>
<div class="title"> <div class="title">
<ToggleButton bind:on={expanded} action={toggle} icon_on="arrow_drop_down" icon_off="arrow_drop_up" highlight={false}/> <ToggleButton bind:on={expanded} action={toggle} icon_on="arrow_drop_down" icon_off="arrow_drop_up" highlight={false} flat/>
{#if !collapsed} {#if !collapsed}
{@render title()} {@render title()}

View File

@@ -10,7 +10,21 @@ let siblings: FSNode[] = $state([])
onMount(() => { onMount(() => {
return global_navigator.subscribe(async () => { return global_navigator.subscribe(async () => {
siblings = await global_navigator.get_siblings() const all_siblings = await global_navigator.get_siblings()
// Find the base
let base_idx = 0
for (let i = 0; i < all_siblings.length; i++) {
if (global_navigator.base.id === all_siblings[i].id) {
base_idx = i
break
}
}
siblings = all_siblings.slice(
Math.max(base_idx-50,0),
Math.min(base_idx+50, all_siblings.length),
)
}) })
}) })
</script> </script>
@@ -18,7 +32,7 @@ onMount(() => {
<MenuEntry id="tree_parents" collapsed={menu_collapsed}> <MenuEntry id="tree_parents" collapsed={menu_collapsed}>
{#snippet title()} {#snippet title()}
<div class="title">Parent directories</div> <div class="title">Parent directories</div>
<button title="Navigate up" onclick={() => global_navigator.navigate_up()}> <button title="Navigate up" onclick={() => global_navigator.navigate_up()} class="button flat">
<i class="icon">north</i> <i class="icon">north</i>
</button> </button>
{/snippet} {/snippet}
@@ -40,10 +54,10 @@ onMount(() => {
<MenuEntry id="tree_siblings" collapsed={menu_collapsed}> <MenuEntry id="tree_siblings" collapsed={menu_collapsed}>
{#snippet title()} {#snippet title()}
<div class="title">Siblings</div> <div class="title">Siblings</div>
<button title="Open previous sibling" onclick={() => global_navigator.open_sibling(-1)}> <button title="Open previous sibling" onclick={() => global_navigator.open_sibling(-1)} class="button flat">
<i class="icon">west</i> <i class="icon">west</i>
</button> </button>
<button title="Open next sibling" onclick={() => global_navigator.open_sibling(1)}> <button title="Open next sibling" onclick={() => global_navigator.open_sibling(1)} class="button flat">
<i class="icon">east</i> <i class="icon">east</i>
</button> </button>
{/snippet} {/snippet}
@@ -65,7 +79,6 @@ onMount(() => {
<style> <style>
.title { .title {
flex: 1 1 auto; flex: 1 1 auto;
text-align: center;
margin: 3px; margin: 3px;
} }
.row { .row {