TirayaFrontend/src/routes/+layout.svelte
2024-05-23 16:50:36 +02:00

323 lines
8.4 KiB
Svelte

<script lang="ts">
import "@fontsource-variable/inter";
import "../style/app.pcss"; // eslint-disable-line no-relative-import-paths/no-relative-import-paths
import { onMount } from "svelte";
import { page } from "$app/stores";
import { afterNavigate } from "$app/navigation";
import { setLocale } from "$i18n/i18n-svelte";
import type { LayoutData } from "./$types";
import {
clientState,
initClientState,
setNavbarWidth,
setQueuebarWidth,
} from "$lib/stores/clientState";
import { updateTheme } from "$lib/stores/theme";
import {
mainWidth,
mainPhone,
screenWidth,
screenHeight,
mainSmall,
mainElm,
headerColor,
} from "$lib/stores/layout";
import { clamp, getEventCoords } from "$lib/util/functions";
import { WIDTH_PHONE, WIDTH_SMALL } from "$lib/util/constants";
import Navbar from "$lib/components/nav/Navbar.svelte";
import Playerbar from "$lib/components/player/Playerbar.svelte";
import DummyText from "$lib/components/ui/DummyText.svelte";
import NavbarMobile from "$lib/components/nav/NavbarMobile.svelte";
import NavbarMobileLandscape from "$lib/components/nav/NavbarMobileLandscape.svelte";
import PlayerbarMobile from "$lib/components/player/PlayerbarMobile.svelte";
import FixedHeader from "$lib/components/header/FixedHeader.svelte";
export let data: LayoutData;
// at the very top, set the locale before you access the store and before the actual rendering takes place
setLocale(data.locale);
// Layout boundaries
const NAVBAR_MIN = 150;
const NAVBAR_MAX = 500;
const QUEUEBAR_MIN = 250;
const CONTENT_MIN = 300;
let scrollPos = 0;
// Draggable layout
let navbarElm: HTMLElement;
let navbarResizing = false;
let queuebarElm: HTMLElement;
let queuebarResizing = false;
let mobileLandscape = false;
$: mobileLandscape = $screenHeight < WIDTH_PHONE && $screenWidth > WIDTH_PHONE;
let showNavbar = false;
$: showNavbar = $screenWidth >= WIDTH_SMALL && !mobileLandscape;
let showQueue = false;
$: showQueue = showNavbar && $clientState.showQueue;
let navbarWidth = 0;
$: navbarWidth = showNavbar ? $clientState.navbarWidth : 0;
let queuebarWidth = 0;
$: queuebarWidth = showQueue
? clamp(
$clientState.queuebarWidth,
QUEUEBAR_MIN,
$screenWidth - navbarWidth - CONTENT_MIN
)
: 0;
$: $mainWidth = $screenWidth - navbarWidth - queuebarWidth;
$: navbarWidthSty = showNavbar ? navbarWidth : 0;
$: queuebarWidthSty = showQueue ? queuebarWidth : 0;
function initResizeNavbar(e: MouseEvent) {
if (e.button !== 0) return;
window.addEventListener("mousemove", resizeNavbar, false);
window.addEventListener("mouseup", stopResizeNavbar, false);
navbarResizing = true;
e.preventDefault();
}
function initResizeNavbarTouch(_e: TouchEvent) {
window.addEventListener("touchmove", resizeNavbar, false);
window.addEventListener("touchend", stopResizeNavbar, false);
navbarResizing = true;
}
function resizeNavbar(e: MouseEvent | TouchEvent) {
const pos = getEventCoords(e)!.x - navbarElm.offsetLeft;
setNavbarWidth(
clamp(
pos,
NAVBAR_MIN,
Math.min(NAVBAR_MAX, $screenWidth - QUEUEBAR_MIN - CONTENT_MIN)
)
);
}
function stopResizeNavbar() {
window.removeEventListener("mousemove", resizeNavbar, false);
window.removeEventListener("mouseup", stopResizeNavbar, false);
window.removeEventListener("touchmove", resizeNavbar, false);
window.removeEventListener("touchend", stopResizeNavbar, false);
navbarResizing = false;
}
function initResizeQueuebar(e: MouseEvent) {
if (e.button !== 0) return;
window.addEventListener("mousemove", resizeQueuebar, false);
window.addEventListener("mouseup", stopResizeQueuebar, false);
queuebarResizing = true;
e.preventDefault();
}
function initResizeQueuebarTouch(_e: TouchEvent) {
window.addEventListener("touchmove", resizeQueuebar, false);
window.addEventListener("touchend", stopResizeQueuebar, false);
queuebarResizing = true;
}
function resizeQueuebar(e: MouseEvent | TouchEvent) {
const pos = queuebarElm.offsetLeft + queuebarElm.clientWidth - getEventCoords(e)!.x;
setQueuebarWidth(
clamp(pos, QUEUEBAR_MIN, $screenWidth - navbarWidth - CONTENT_MIN)
);
}
function stopResizeQueuebar() {
window.removeEventListener("mousemove", resizeQueuebar, false);
window.removeEventListener("mouseup", stopResizeQueuebar, false);
window.removeEventListener("touchmove", resizeQueuebar, false);
window.removeEventListener("touchend", stopResizeQueuebar, false);
queuebarResizing = false;
}
function onScroll() {
scrollPos = $mainElm?.scrollTop || 0;
}
function onDragEnd() {
delete document.body.dataset.dragType;
}
// Restore scroll state on navigation
export const snapshot = {
capture: () => {
const pos = $mainElm!.scrollTop;
$mainElm!.scrollTo(0, 0);
return pos;
},
restore: (y) => {
$mainElm!.scrollTo(0, y);
},
};
afterNavigate(() => {
headerColor.set(null);
});
onMount(() => {
initClientState();
updateTheme();
});
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
id="app"
class:mobile={!showNavbar && !mobileLandscape}
class:mobile-landscape={mobileLandscape}
bind:clientWidth={$screenWidth}
bind:clientHeight={$screenHeight}
on:dragend={onDragEnd}
>
<div
id="main-content"
style={`--navbar-width: ${navbarWidthSty}px; --queuebar-width: ${queuebarWidthSty}px;`}
>
{#if showNavbar}
<nav class="sidebar" bind:this={navbarElm}>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="handle right-0"
on:mousedown={initResizeNavbar}
on:touchstart|passive={initResizeNavbarTouch}
class:active={navbarResizing}
/>
<Navbar />
</nav>
{:else if mobileLandscape}
<nav class="sidebar">
<NavbarMobileLandscape />
</nav>
{/if}
{#if showQueue}
<aside class="sidebar" bind:this={queuebarElm}>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="handle left-0"
on:mousedown={initResizeQueuebar}
on:touchstart|passive={initResizeQueuebarTouch}
class:active={queuebarResizing}
/>
<DummyText />
</aside>
{/if}
<header>
<FixedHeader {scrollPos} />
</header>
<main
bind:this={$mainElm}
class:mainSmall={$mainSmall}
class:mainPhone={$mainPhone}
on:scroll={onScroll}
>
{#if !$page.data.header?.fading}
<div class="h-12" />
{/if}
<slot />
{#if !showNavbar && !mobileLandscape}
<!-- Spacer to ensure enough bottom space to scroll beyond floating playerbar -->
<div class="h-24" />
{/if}
</main>
</div>
{#if showNavbar && !mobileLandscape}
<Playerbar />
{:else if !mobileLandscape}
<PlayerbarMobile />
<NavbarMobile />
{/if}
</div>
<style lang="postcss">
#app {
display: grid;
grid-template:
"main-content" calc(100vh - 90px)
"player" 90px / 1fr;
height: 100%;
overflow: hidden;
&.mobile {
grid-template:
"main-content" calc(100vh - 3rem)
"player" 3rem / 1fr;
}
&.mobile-landscape {
grid-template: "main-content" 100vh;
}
}
#main-content {
position: relative;
grid-area: main-content / main-content / main-content / main-content;
display: grid;
grid-template: "sidebar main right-sidebar" 1fr / var(--navbar-width) 1fr var(
--queuebar-width
);
}
.mobile-landscape > #main-content {
grid-template: "sidebar main" 1fr/10rem 1fr;
}
main {
grid-area: main;
overflow-x: hidden;
overflow-y: auto;
}
header {
grid-area: main;
height: 3rem;
z-index: 20;
}
nav {
grid-area: sidebar / sidebar / sidebar / sidebar;
}
aside {
grid-area: right-sidebar / right-sidebar / right-sidebar / right-sidebar;
z-index: 60;
}
.handle {
position: absolute;
z-index: 61;
width: 4px;
cursor: ew-resize;
opacity: 0;
transition: opacity 0.4s;
@apply bg-base-content/50;
&.active {
@apply bg-primary;
opacity: 1;
}
&:hover {
opacity: 1;
}
}
.sidebar {
position: relative;
overflow-x: hidden;
overflow-y: auto;
@apply bg-base-300;
& > div {
height: 100%;
}
}
</style>