323 lines
8.4 KiB
Svelte
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>
|