2025-10-09 15:48:23 +02:00
|
|
|
<script lang="ts">
|
|
|
|
|
import { onMount, tick } from "svelte";
|
|
|
|
|
import HomePage from "home_page/HomePage.svelte";
|
|
|
|
|
import LoginRouter from "login/Router.svelte";
|
|
|
|
|
import HomeRouter from "user_home/Router.svelte";
|
|
|
|
|
import AdminRouter from "admin_panel/Router.svelte";
|
|
|
|
|
import Filesystem from "filesystem/Filesystem.svelte";
|
|
|
|
|
import NotFound from "./NotFound.svelte";
|
|
|
|
|
import SpeedtestPage from "speedtest/SpeedtestPage.svelte";
|
|
|
|
|
import Appearance from "pages/Appearance.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";
|
2026-04-01 19:38:15 +02:00
|
|
|
import { breadcrumbs_store } from "./BreadcrumbStore";
|
2025-10-09 15:48:23 +02:00
|
|
|
|
2025-10-13 16:05:50 +02:00
|
|
|
let pages: Tab[] = [
|
2025-10-09 15:48:23 +02:00
|
|
|
{
|
|
|
|
|
path: "/",
|
|
|
|
|
title: "Home",
|
|
|
|
|
component: HomePage,
|
|
|
|
|
}, {
|
|
|
|
|
path: "/login",
|
|
|
|
|
title: "Login",
|
|
|
|
|
component: LoginRouter,
|
|
|
|
|
}, {
|
|
|
|
|
path: "/register",
|
|
|
|
|
title: "Register",
|
|
|
|
|
component: LoginRouter,
|
|
|
|
|
}, {
|
|
|
|
|
path: "/user",
|
|
|
|
|
prefix: "/user/",
|
|
|
|
|
title: "Dashboard",
|
|
|
|
|
component: HomeRouter,
|
|
|
|
|
login: true,
|
|
|
|
|
}, {
|
|
|
|
|
path: "/d/me",
|
|
|
|
|
prefix: "/d/",
|
|
|
|
|
title: "Filesystem",
|
|
|
|
|
component: Filesystem,
|
|
|
|
|
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: AdminRouter,
|
|
|
|
|
login: true,
|
|
|
|
|
}, {
|
|
|
|
|
path: "/speedtest",
|
|
|
|
|
title: "Speedtest",
|
|
|
|
|
component: SpeedtestPage,
|
|
|
|
|
}, {
|
|
|
|
|
path: "/appearance",
|
|
|
|
|
title: "Appearance",
|
|
|
|
|
component: Appearance,
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
onMount(async () => {
|
2026-09-30 19:26:21 +02:00
|
|
|
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)
|
2025-10-09 15:48:23 +02:00
|
|
|
|
2026-09-30 19:26:21 +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) {
|
2026-09-30 19:26:21 +02:00
|
|
|
if (path === page.path) {
|
|
|
|
|
return page
|
2025-10-09 15:48:23 +02:00
|
|
|
}
|
2026-09-30 19:26:21 +02:00
|
|
|
if (page.prefix !== undefined && path.startsWith(page.prefix)) {
|
|
|
|
|
by_prefix = page
|
2025-10-09 15:48:23 +02:00
|
|
|
}
|
|
|
|
|
}
|
2026-09-30 19:26:21 +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): Promise<boolean> => {
|
|
|
|
|
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
|
|
|
|
2026-09-30 19:26:21 +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",
|
|
|
|
|
component: NotFound,
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-30 19:26:21 +02:00
|
|
|
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
|
2026-09-30 19:26:21 +02:00
|
|
|
// 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)
|
2026-09-30 19:26:21 +02:00
|
|
|
return load_page(
|
|
|
|
|
"/login?redirect=" + encodeURIComponent(url.pathname + url.search),
|
|
|
|
|
true,
|
|
|
|
|
)
|
2025-10-09 15:48:23 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-30 19:26:21 +02:00
|
|
|
window.document.title = page.title+" / Nova"
|
2025-10-09 15:48:23 +02:00
|
|
|
|
2026-09-30 19:26:21 +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) {
|
2026-09-30 19:26:21 +02:00
|
|
|
window.history.pushState(
|
|
|
|
|
{}, window.document.title, url.pathname + url.search + url.hash,
|
|
|
|
|
)
|
2025-10-09 15:48:23 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-30 19:26:21 +02:00
|
|
|
current_page = 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)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-30 19:26:21 +02:00
|
|
|
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
|
|
|
|
2026-09-30 19:26:21 +02:00
|
|
|
const origin = (e.target as Element).closest("a")
|
2025-10-09 15:48:23 +02:00
|
|
|
if (origin === null) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-30 19:26:21 +02:00
|
|
|
// 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
|
2025-10-09 15:48:23 +02:00
|
|
|
const url = URL.parse(origin.href)
|
2026-09-30 19:26:21 +02:00
|
|
|
if (url === null || url.host !== window.location.host) {
|
2025-10-09 15:48:23 +02:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-30 19:26:21 +02:00
|
|
|
// In-page anchors are left to the browser, it knows how to scroll to them
|
|
|
|
|
if (url.pathname === window.location.pathname && url.hash !== "") {
|
|
|
|
|
return
|
|
|
|
|
}
|
2025-10-09 15:48:23 +02:00
|
|
|
|
2026-09-30 19:26:21 +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(url.pathname) === null) {
|
|
|
|
|
return
|
2025-10-09 15:48:23 +02:00
|
|
|
}
|
2026-09-30 19:26:21 +02:00
|
|
|
|
|
|
|
|
console.log("Caught link click to", url.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) => {
|
2026-09-30 19:26:21 +02:00
|
|
|
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}
|
2025-10-13 16:05:50 +02:00
|
|
|
<current_page.component />
|
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}
|