Files
fnx_web/svelte/src/wrap/Router.svelte

227 lines
6.2 KiB
Svelte
Raw Normal View History

2025-10-09 15:48:23 +02:00
<script lang="ts">
import { onMount, tick, type Component } from "svelte";
2025-10-09 15:48:23 +02:00
import NotFound from "./NotFound.svelte";
import Footer from "layout/Footer.svelte";
import { current_page_store, type Tab } from "./RouterStore";
2026-06-10 23:53:03 +02:00
import { get_user, type User } from "lib/NovaAPI";
import { breadcrumbs_store } from "./HeaderStore";
import { loading_run } from "lib/Loading";
2025-10-09 15:48:23 +02:00
// Pages are downloaded when they are opened for the first time. Someone who
// opens a shared file doesn't have to download the dashboard and admin panel
2025-10-13 16:05:50 +02:00
let pages: Tab[] = [
2025-10-09 15:48:23 +02:00
{
path: "/",
title: "Home",
component: () => import("home_page/HomePage.svelte"),
2025-10-09 15:48:23 +02:00
}, {
path: "/login",
title: "Login",
component: () => import("login/Router.svelte"),
2025-10-09 15:48:23 +02:00
}, {
path: "/register",
title: "Register",
component: () => import("login/Router.svelte"),
2025-10-09 15:48:23 +02:00
}, {
path: "/user",
prefix: "/user/",
title: "Dashboard",
component: () => import("user_home/Router.svelte"),
2025-10-09 15:48:23 +02:00
login: true,
}, {
path: "/d/me",
prefix: "/d/",
title: "Filesystem",
component: () => import("filesystem/Filesystem.svelte"),
2025-10-09 15:48:23 +02:00
footer: false,
2026-04-01 19:38:15 +02:00
custom_breadcrumbs: true,
2025-10-09 15:48:23 +02:00
}, {
path: "/admin",
prefix: "/admin/",
title: "Admin Panel",
component: () => import("admin_panel/Router.svelte"),
2025-10-09 15:48:23 +02:00
login: true,
}, {
path: "/speedtest",
title: "Speedtest",
component: () => import("speedtest/SpeedtestPage.svelte"),
2025-10-09 15:48:23 +02:00
},
]
onMount(async () => {
await load_page(window.location.href, false)
2025-10-09 15:48:23 +02:00
})
2025-10-13 16:05:50 +02:00
let current_page: Tab = $state(null)
let Page: Component = $state(null)
// Every navigation gets a number. A navigation which is still loading when a
// newer one starts is abandoned, else it would replace the newer page when it
// finishes
let navigation_id = 0
2025-10-09 15:48:23 +02:00
// find_page resolves a URL path to the page which renders it. It returns null
// when no page matches, in which case the browser has to handle the navigation
const find_page = (pathname: string): Tab => {
const path = decodeURIComponent(pathname)
let by_prefix: Tab = null
2025-10-09 15:48:23 +02:00
for (const page of pages) {
if (path === page.path) {
return page
2025-10-09 15:48:23 +02:00
}
if (page.prefix !== undefined && path.startsWith(page.prefix)) {
by_prefix = page
2025-10-09 15:48:23 +02:00
}
}
return by_prefix
}
// href can be a full URL or a path, and may contain a query string and hash.
// Those are not used for routing, but they do need to survive the navigation
const load_page = async (href: string, history: boolean) => {
const id = ++navigation_id
const url = new URL(href, window.location.href)
console.debug(
"Page router: Navigating to page", url.pathname,
"current path", window.location.pathname,
"log history:", history,
)
2025-10-09 15:48:23 +02:00
const page = find_page(url.pathname)
if (page === null) {
2025-10-09 15:48:23 +02:00
current_page = {
path: "",
title: "Not Found",
}
Page = NotFound
return
2025-10-09 15:48:23 +02:00
}
// Download the page while we check the login
const component = loading_run(page.component)
let user: User
try {
user = await get_user()
} catch (err) {
alert("Failed to get user details: " + err)
}
2025-10-09 15:48:23 +02:00
// If this page requires login, and the user is not logged in, then we
// redirect the user to the login page. The page is deliberately not mounted
// first, it would fire authenticated requests which are all bound to fail
if (page.login === true && (user === null || user === undefined)) {
2025-10-09 15:48:23 +02:00
console.debug("User is not logged in, redirecting to login page", user)
return load_page(
"/login?redirect=" + encodeURIComponent(url.pathname + url.search),
true,
)
2025-10-09 15:48:23 +02:00
}
let new_page: Component
try {
new_page = (await component).default
} catch (err) {
// An update deletes the code of the previous version from the server.
// Tabs which were opened before the update need a full page load
if (current_page === null) {
throw err
}
window.location.assign(url)
return
}
if (id !== navigation_id) {
return
}
window.document.title = page.title+" / Nova"
2025-10-09 15:48:23 +02:00
// The address bar has to be updated before the page is mounted. Pages read
// their parameters from it when they initialize, the login page picks up
// the redirect target this way
2026-07-30 23:15:00 +02:00
if (history) {
window.history.pushState(
{}, window.document.title, url.pathname + url.search + url.hash,
)
2025-10-09 15:48:23 +02:00
}
current_page = page
Page = new_page
2026-04-01 19:38:15 +02:00
if (current_page.custom_breadcrumbs !== true) {
breadcrumbs_store.set(breadcrumbs)
}
2025-10-09 15:48:23 +02:00
// The current_page_store updates all the listening pages for navigation
// events. We first wait for a tick so that the current page gets unmounted
// before sending the event. That way a stale page will not get events which
// are not meant for them
tick().then(() => {
current_page_store.set(current_page)
})
}
const click = (e: MouseEvent) => {
// Middle clicks, right clicks and modified clicks open the link in a new
// tab or window. That's the browser's job, not ours
if (e.button !== 0 || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) {
return
}
2025-10-09 15:48:23 +02:00
const origin = (e.target as Element).closest("a")
2025-10-09 15:48:23 +02:00
if (origin === null) {
return
}
// Links which explicitly ask for browser behaviour: a new tab, a download,
// or an external destination
if (origin.target !== "" || origin.hasAttribute("download") ||
origin.rel === "external") {
return
}
// Other hosts, and protocols like mailto: and magnet:, have no host match
if (origin.host !== window.location.host) {
2025-10-09 15:48:23 +02:00
return
}
// In-page anchors are left to the browser, it knows how to scroll to them
if (origin.pathname === window.location.pathname && origin.hash !== "") {
return
}
2025-10-09 15:48:23 +02:00
// Paths without a page of their own are served by the backend, like the API
// and file downloads. Loading them here would replace the page the user is
// looking at with a Not Found error while the browser navigates away
if (find_page(origin.pathname) === null) {
return
2025-10-09 15:48:23 +02:00
}
console.log("Caught link click to", origin.pathname)
// The event has to be cancelled before the first await, after that the
// browser has already committed to the navigation
e.preventDefault()
load_page(origin.href, true)
2025-10-09 15:48:23 +02:00
}
const popstate = (e: PopStateEvent) => {
load_page(window.location.href, false)
2025-10-09 15:48:23 +02:00
}
</script>
2026-04-01 19:38:15 +02:00
{#snippet breadcrumbs()}
{current_page.title}
{/snippet}
2025-10-13 16:05:50 +02:00
<svelte:document onclick={click}/>
<svelte:window onpopstate={popstate}/>
2025-10-09 15:48:23 +02:00
{#if current_page !== null}
<Page />
2025-10-09 15:48:23 +02:00
2026-07-30 23:15:00 +02:00
{#if current_page.footer !== false}
2025-10-09 15:48:23 +02:00
<Footer/>
{/if}
{/if}