Files
fnx_web/svelte/src/wrap/MainMenu.svelte

366 lines
8.7 KiB
Svelte
Raw Normal View History

2026-02-19 18:15:06 +01:00
<script lang="ts" module>
export let menu_is_open = writable(true)
</script>
2025-10-09 15:48:23 +02:00
<script lang="ts">
import { highlight_current_page } from "lib/HighlightCurrentPage";
import { user } from "lib/UserStore";
import Euro from "util/Euro.svelte";
import { formatDataVolume } from "util/Formatting";
import Router from "wrap/Router.svelte";
import Bookmarks from "./Bookmarks.svelte";
import { onMount } from "svelte";
import { fs_get_node } from "lib/FilesystemAPI";
2025-10-09 15:48:23 +02:00
import { css_from_path } from "filesystem/edit_window/Branding";
import { loading_run, loading_store } from "lib/Loading";
import Spinner from "util/Spinner.svelte";
2026-06-10 23:53:03 +02:00
import { get_user } from "lib/NovaAPI";
2026-01-29 23:41:30 +01:00
import Tree from "./Tree.svelte";
import MenuEntry from "./MenuEntry.svelte";
2026-02-19 18:15:06 +01:00
import { writable } from "svelte/store";
2026-04-01 19:38:15 +02:00
import { breadcrumbs_store } from "./BreadcrumbStore";
2025-10-09 15:48:23 +02:00
2026-02-26 14:47:23 +01:00
// The menu swipe will be detected if it was less than this much pixels from the
// screen edge
2026-04-08 19:54:09 +02:00
const screen_edge_offset = 40
2026-02-26 14:47:23 +01:00
// 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(async () => {
2026-02-26 14:47:23 +01:00
if (document.documentElement.clientWidth < min_screen_size_menu_open) {
2026-02-19 18:15:06 +01:00
menu_close()
}
2025-11-03 19:15:57 +01:00
await loading_run(async () => {
const user = await get_user()
if (user.username === undefined || user.username === "") {
return
}
2025-10-09 15:48:23 +02:00
const root = await fs_get_node("/me")
document.documentElement.style = css_from_path(root.path)
})
})
2026-02-19 18:15:06 +01:00
let menu: HTMLDivElement
let nav: HTMLElement
let dragging: boolean = false
let start_x: number
2026-02-26 14:47:23 +01:00
let start_y: number
2026-02-19 18:15:06 +01:00
let render_offset: number
let initial_offset: number
const touchstart = (e: TouchEvent) => {
start_x = e.touches[0].clientX
2026-02-26 14:47:23 +01:00
start_y = e.touches[0].clientY
2026-02-19 18:15:06 +01:00
const rect = menu.getBoundingClientRect()
2026-02-26 14:47:23 +01:00
if (start_x < Math.max(screen_edge_offset, (rect.width+rect.left))) {
2026-02-19 18:15:06 +01:00
dragging = true
2026-02-26 14:47:23 +01:00
e.stopPropagation()
2026-02-19 18:15:06 +01:00
}
render_offset = rect.left
initial_offset = rect.left
}
const touchmove = (e: TouchEvent) => {
if (!dragging) {
return
}
2026-02-26 14:47:23 +01:00
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)
2026-04-08 19:54:09 +02:00
// The cursor must have moved at least swipe_dead_zone pixels and twice as
// much on the x axis than the y axis for it to count as a swipe
if (abs_x > swipe_dead_zone) {
if (abs_x > abs_y) {
e.preventDefault()
e.stopPropagation()
set_offset(initial_offset+(x*2))
} else {
touchend(e)
}
2026-02-26 14:47:23 +01:00
}
2026-02-19 18:15:06 +01:00
}
const touchend = (e: TouchEvent) => {
if (!dragging) {
return
}
dragging = false
const menu_width = menu.getBoundingClientRect().width
if (render_offset > -(menu_width/2)) {
menu_open()
} else {
menu_close()
}
}
const toggle_menu = (e: MouseEvent) => {
if (menu.style.transform === "") {
menu_close()
} else {
menu_open()
}
}
const menu_open = () => {
set_offset(0)
menu_is_open.set(true)
if (document.documentElement.clientWidth < min_screen_size_fullscreen_menu) {
menu.style.position = "fixed"
menu.style.width = "100%"
nav.style.width = "100%"
nav.style.maxWidth = "100%"
} else {
menu.style.position = "relative"
menu.style.width = ""
nav.style.width = ""
nav.style.maxWidth = ""
}
}
const menu_close = () => {
set_offset(-menu.getBoundingClientRect().width)
menu_is_open.set(false)
menu.style.position = "fixed"
if (document.documentElement.clientWidth >= min_screen_size_fullscreen_menu) {
menu.style.width = ""
nav.style.width = ""
nav.style.maxWidth = ""
}
}
const set_offset = (off: number) => {
render_offset = off
2026-02-26 14:47:23 +01:00
if (off > -swipe_dead_zone) {
2026-02-19 18:15:06 +01:00
// Clear the transformation if the offset is zero
menu.style.transform = ""
} else {
menu.style.transform = "translateX(" + off + "px)"
}
}
2025-10-09 15:48:23 +02:00
</script>
2026-02-19 18:15:06 +01:00
<svelte:window ontouchstart={touchstart} ontouchmove={touchmove} ontouchend={touchend}/>
2026-04-01 19:38:15 +02:00
<div class="wrap">
<div class="nav_container" bind:this={menu}>
<div class="scroll_container">
<nav class="nav bubble" bind:this={nav}>
2026-04-01 19:38:15 +02:00
{#if document.documentElement.clientWidth < min_screen_size_menu_open}
<button class="button" onclick={toggle_menu}>
<i class="icon">menu</i>
Menu
</button>
{/if}
2026-04-08 19:54:09 +02:00
<a class="button" style="flex: 1 1 auto;" href="/" use:highlight_current_page>
<span style="flex: 1 1 auto;">Nova.storage</span>
<img src="/res/img/nova_64.png" style="width: 1.5em; height: 1.5em; flex: 0 0 auto;" alt="Nova.storage logo">
</a>
2026-04-01 19:38:15 +02:00
{#if $user !== null}
2026-04-01 19:38:15 +02:00
<MenuEntry id="subscription_info" collapsed={false}>
{#snippet title()}
<div class="username">{$user.username}</div>
{/snippet}
{#snippet body()}
<div class="stats_table">
<div>Subscription</div>
<div>{$user.subscription.name}</div>
{#if $user.subscription.type === "prepaid"}
<div>Credit</div>
<div><Euro amount={$user.balance_micro_eur}/></div>
{/if}
<div>Storage used</div>
<div>{formatDataVolume($user.filesystem_storage_used, 3)}</div>
<div>Transfer used</div>
<div>{formatDataVolume($user.monthly_transfer_used, 3)}</div>
</div>
{/snippet}
</MenuEntry>
<a class="button" href="/user" use:highlight_current_page>
<i class="icon">person</i>
<span>Account</span>
</a>
2026-04-01 19:38:15 +02:00
<a class="button" href="/d/me" use:highlight_current_page>
<i class="icon">folder</i>
<span>Filesystem</span>
</a>
2026-06-10 23:53:03 +02:00
<a class="button" href="/d/me/.Trash" use:highlight_current_page>
<i class="icon">delete</i>
<span>Trash</span>
</a>
2026-04-01 19:38:15 +02:00
{#if $user.is_admin}
<a class="button" href="/admin" use:highlight_current_page>
<i class="icon">admin_panel_settings</i>
<span>Admin Panel</span>
</a>
{/if}
{:else}
<a class="button" href="/login" use:highlight_current_page>
<i class="icon">login</i>
<span>Login</span>
</a>
<a class="button" href="/register" use:highlight_current_page>
<i class="icon">how_to_reg</i>
<span>Register</span>
</a>
{/if}
2026-04-01 19:38:15 +02:00
<MenuEntry id="other_pages" collapsed={false}>
{#snippet title()}
<span style="flex: 1 1 auto;">
<span style="flex: 1 1 auto;">Other pages</span>
</span>
{/snippet}
{#snippet body()}
<a class="button" href="/speedtest" use:highlight_current_page>
<i class="icon">speed</i>
<span>Speedtest</span>
</a>
<a class="button" href="/appearance" use:highlight_current_page>
<i class="icon">palette</i>
<span>Themes</span>
</a>
{/snippet}
</MenuEntry>
2026-04-08 19:54:09 +02:00
<Bookmarks/>
<Tree/>
2026-04-01 19:38:15 +02:00
</nav>
</div>
</div>
2025-10-09 15:48:23 +02:00
2026-04-01 19:38:15 +02:00
<div class="page">
<div class="header bubble">
2026-04-01 19:38:15 +02:00
<button class="menu_button" onclick={toggle_menu}>
<i class="icon">menu</i>
</button>
2025-10-09 15:48:23 +02:00
2026-04-01 19:38:15 +02:00
<div class="breadcrumbs">
{#if $breadcrumbs_store !== null}
{@render $breadcrumbs_store()}
{/if}
</div>
</div>
2025-10-09 15:48:23 +02:00
2026-04-01 19:38:15 +02:00
<Router/>
2025-10-09 15:48:23 +02:00
</div>
2026-04-01 19:38:15 +02:00
{#if $loading_store !== 0}
<div class="spinner">
<Spinner/>
</div>
{/if}
</div>
2025-10-09 15:48:23 +02:00
<style>
2026-04-01 19:38:15 +02:00
.wrap {
2025-10-09 15:48:23 +02:00
display: flex;
flex-direction: row;
color: var(--body_text_color);
background-image: var(--background_image);
2026-04-03 15:48:51 +02:00
background-color: var(--background_color);
2025-10-09 15:48:23 +02:00
background-size: var(--background_image_size, initial);
background-position: var(--background_image_position, initial);
background-repeat: var(--background_image_repeat, repeat);
background-attachment: fixed;
2026-04-01 19:38:15 +02:00
width: 100%;
min-height: 100vh;
2025-10-09 15:48:23 +02:00
}
.nav_container {
flex: 0 0 auto;
2026-04-08 19:54:09 +02:00
backdrop-filter: blur(3px);
2026-02-19 18:15:06 +01:00
z-index: 9;
2025-10-09 15:48:23 +02:00
}
.scroll_container {
position: sticky;
top: 0;
overflow-x: hidden;
overflow-y: auto;
2026-02-19 18:15:06 +01:00
max-height: 100vh;
height: 100vh;
padding: 4px;
scrollbar-gutter: stable;
}
2025-10-09 15:48:23 +02:00
.nav {
display: flex;
flex-direction: column;
2026-01-29 23:41:30 +01:00
width: 15em;
margin: 0;
padding: 4px;
2026-01-29 23:41:30 +01:00
}
2026-04-08 19:54:09 +02:00
.button {
2025-10-09 15:48:23 +02:00
background: none;
box-shadow: none;
}
.page {
flex: 1 1 auto;
2026-04-01 19:38:15 +02:00
max-width: 100vw;
min-width: 0; /* prevents overflow */
display: flex;
flex-direction: column;
2025-10-09 15:48:23 +02:00
}
.username {
flex: 1 1 auto;
margin: 3px;
}
2025-10-09 15:48:23 +02:00
.stats_table {
display: grid;
grid-template-columns: auto auto;
gap: 0.2em 1em;
margin: 5px;
2025-10-09 15:48:23 +02:00
}
2026-04-01 19:38:15 +02:00
.header {
flex: 0 0 auto;
display: flex;
flex-direction: row;
align-items: center;
justify-content: left;
2026-04-08 19:54:09 +02:00
backdrop-filter: blur(3px);
2026-04-03 15:48:51 +02:00
position: sticky;
top: 4px;
2026-04-03 15:48:51 +02:00
z-index: 8; /* below navigation, on top of most other things */
2026-04-01 19:38:15 +02:00
}
.menu_button {
flex: 0 0 auto;
background: none;
box-shadow: none;
}
.breadcrumbs {
flex: 1 1 auto;
display: flex;
flex-wrap: wrap;
flex-direction: row;
align-items: center;
overflow-x: hidden;
2026-04-01 19:38:15 +02:00
}
2025-10-09 15:48:23 +02:00
.spinner {
position: fixed;
top: 10px;
right: 10px;
height: 120px;
width: 120px;
z-index: 11; /* One above the menu */
2025-10-09 15:48:23 +02:00
}
</style>