Big layout improvements, started on miniplayer
This commit is contained in:
@@ -38,7 +38,7 @@
|
|||||||
--youtube-red: #ff0000;
|
--youtube-red: #ff0000;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 640px) {
|
@media screen and (max-width: 768px) {
|
||||||
:root {
|
:root {
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
import Services from '$lib/services.json'
|
import Services from '$lib/services.json'
|
||||||
import IconButton from '$lib/components/utility/iconButton.svelte'
|
import IconButton from '$lib/components/utility/iconButton.svelte'
|
||||||
import { backgroundImage } from '$lib/utils/stores.js'
|
import { backgroundImage, currentlyPlaying } from '$lib/utils/stores.js'
|
||||||
|
|
||||||
const iconClasses = {
|
const iconClasses = {
|
||||||
song: 'fa-solid fa-music',
|
song: 'fa-solid fa-music',
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
const x = (2 * (event.x - cardRect.left)) / cardRect.width - 1 // These are simplified calculations to find the x-y coords relative to the center of the card
|
const x = (2 * (event.x - cardRect.left)) / cardRect.width - 1 // These are simplified calculations to find the x-y coords relative to the center of the card
|
||||||
const y = (2 * (cardRect.top - event.y)) / cardRect.height + 1
|
const y = (2 * (cardRect.top - event.y)) / cardRect.height + 1
|
||||||
|
|
||||||
let angle = Math.atan(x / y) // You'd think it should be y / x but it's actually the inverse
|
const angle = Math.atan(x / y) // You'd think it should be y / x but it's actually the inverse
|
||||||
const distanceFromCorner = Math.sqrt((x - 1) ** 2 + (y - 1) ** 2) // This is a cool little trick, the -1 on the x an y coordinate is effective the same as saying "make the origin of the glare [1, 1]"
|
const distanceFromCorner = Math.sqrt((x - 1) ** 2 + (y - 1) ** 2) // This is a cool little trick, the -1 on the x an y coordinate is effective the same as saying "make the origin of the glare [1, 1]"
|
||||||
|
|
||||||
cardGlare.style.backgroundImage = `linear-gradient(${angle}rad, transparent ${distanceFromCorner * 50 + 50}%, rgba(255, 255, 255, 0.1) ${distanceFromCorner * 50 + 60}%, transparent 100%)`
|
cardGlare.style.backgroundImage = `linear-gradient(${angle}rad, transparent ${distanceFromCorner * 50 + 50}%, rgba(255, 255, 255, 0.1) ${distanceFromCorner * 50 + 60}%, transparent 100%)`
|
||||||
@@ -40,7 +40,12 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<div bind:this={cardGlare} id="card-glare" class="absolute top-0 grid h-full w-full place-items-center rounded-lg opacity-0 transition-opacity duration-200 ease-out">
|
<div bind:this={cardGlare} id="card-glare" class="absolute top-0 grid h-full w-full place-items-center rounded-lg opacity-0 transition-opacity duration-200 ease-out">
|
||||||
<span class="relative h-12">
|
<span class="relative h-12">
|
||||||
<IconButton on:click={() => ($backgroundImage = mediaData.image)}>
|
<IconButton
|
||||||
|
on:click={() => {
|
||||||
|
$currentlyPlaying = mediaData
|
||||||
|
$backgroundImage = mediaData.image
|
||||||
|
}}
|
||||||
|
>
|
||||||
<i slot="icon" class="fa-solid fa-play text-xl" />
|
<i slot="icon" class="fa-solid fa-play text-xl" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
102
src/lib/components/media/miniPlayer.svelte
Normal file
102
src/lib/components/media/miniPlayer.svelte
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
<script>
|
||||||
|
export let displayMode
|
||||||
|
|
||||||
|
import IconButton from '$lib/components/utility/iconButton.svelte'
|
||||||
|
import Slider from '$lib/components/utility/slider.svelte'
|
||||||
|
import { currentlyPlaying } from '$lib/utils/stores.js'
|
||||||
|
import { slide } from 'svelte/transition'
|
||||||
|
import { getVolume, setVolume } from '$lib/utils/utils.js'
|
||||||
|
import { onMount } from 'svelte'
|
||||||
|
|
||||||
|
$: song = $currentlyPlaying
|
||||||
|
|
||||||
|
let songLiked = false
|
||||||
|
|
||||||
|
let volumeSlider
|
||||||
|
|
||||||
|
let volume
|
||||||
|
onMount(() => {
|
||||||
|
volume = getVolume()
|
||||||
|
})
|
||||||
|
$: console.log(volume)
|
||||||
|
|
||||||
|
const formatDuration = (timeMilliseconds) => {
|
||||||
|
const seconds = Math.floor((timeMilliseconds / 1000) % 60)
|
||||||
|
const minutes = Math.floor((timeMilliseconds / 1000 / 60) % 60)
|
||||||
|
|
||||||
|
return [minutes.toString(), seconds.toString.padStart(2, '0')].join(':')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if song}
|
||||||
|
<div id="player-wrapper" class="relative border-t-2 border-t-lazuli-primary bg-neutral-950" transition:slide={{ axis: 'y' }}>
|
||||||
|
{#if displayMode === 'vertical'}
|
||||||
|
<h1>Vertical Mode</h1>
|
||||||
|
{:else}
|
||||||
|
<div class="grid h-full grid-cols-3 grid-rows-1 p-3 text-sm">
|
||||||
|
<section class="flex items-center gap-4">
|
||||||
|
<img class="h-full rounded-lg object-contain" src={song.image} alt="{song.name} thumbnail" />
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<div>{song.name}</div>
|
||||||
|
<div class="text-xs text-neutral-400">{Array.from(song.artists, (artist) => artist.name).join(', ')}</div>
|
||||||
|
</div>
|
||||||
|
<button class="grid aspect-square h-6 place-items-center text-lg transition-all" on:click={() => (songLiked = !songLiked)}>
|
||||||
|
{#if songLiked}
|
||||||
|
<i class="fa-solid fa-heart text-fuchsia-400" />
|
||||||
|
{:else}
|
||||||
|
<i class="fa-regular fa-heart text-neutral-400 hover:text-white" />
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
<section class="flex items-center justify-center gap-4 py-1 text-xl">
|
||||||
|
<IconButton>
|
||||||
|
<i slot="icon" class="fa-solid fa-backward-step" />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton>
|
||||||
|
<i slot="icon" class="fa-solid fa-play" />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton on:click={() => ($currentlyPlaying = null)}>
|
||||||
|
<i slot="icon" class="fa-solid fa-stop" />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton>
|
||||||
|
<i slot="icon" class="fa-solid fa-forward-step" />
|
||||||
|
</IconButton>
|
||||||
|
</section>
|
||||||
|
<section class="flex items-center justify-end gap-4 py-1 text-xl">
|
||||||
|
<div class="flex w-40 items-center gap-4">
|
||||||
|
<Slider
|
||||||
|
bind:this={volumeSlider}
|
||||||
|
initialValue={volume}
|
||||||
|
on:valuechange={(event) => {
|
||||||
|
volume = event.detail.value
|
||||||
|
setVolume(volume)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="transition-colors hover:text-lazuli-primary"
|
||||||
|
on:click={() => {
|
||||||
|
if (volume > 0) {
|
||||||
|
volume = 0
|
||||||
|
} else {
|
||||||
|
volume = getVolume()
|
||||||
|
}
|
||||||
|
volumeSlider.setValue(volume)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i class="fa-solid fa-volume-high" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<IconButton>
|
||||||
|
<i slot="icon" class="fa-solid fa-ellipsis-vertical" />
|
||||||
|
</IconButton>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#player-wrapper {
|
||||||
|
height: 72px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -6,7 +6,12 @@
|
|||||||
import IconButton from '$lib/components/utility/iconButton.svelte'
|
import IconButton from '$lib/components/utility/iconButton.svelte'
|
||||||
|
|
||||||
let scrollable,
|
let scrollable,
|
||||||
|
scrollableWidth,
|
||||||
|
isScrollable = false,
|
||||||
scrollpos = 0
|
scrollpos = 0
|
||||||
|
|
||||||
|
$: isScrollable = scrollable?.scrollWidth > scrollableWidth
|
||||||
|
$: scrollpos = scrollable?.scrollLeft / (scrollable?.scrollWidth - scrollableWidth)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
@@ -15,16 +20,17 @@
|
|||||||
<h1 class="text-4xl"><strong>{header}</strong></h1>
|
<h1 class="text-4xl"><strong>{header}</strong></h1>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="flex h-full gap-2">
|
<div class="flex h-full gap-2">
|
||||||
<IconButton disabled={scrollpos < 0.01} on:click={() => (scrollable.scrollLeft -= scrollable.clientWidth)}>
|
<IconButton disabled={scrollpos < 0.01 || !isScrollable} on:click={() => (scrollable.scrollLeft -= scrollable.clientWidth)}>
|
||||||
<i slot="icon" class="fa-solid fa-angle-left" />
|
<i slot="icon" class="fa-solid fa-angle-left" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton disabled={scrollpos > 0.99} on:click={() => (scrollable.scrollLeft += scrollable.clientWidth)}>
|
<IconButton disabled={scrollpos > 0.99 || !isScrollable} on:click={() => (scrollable.scrollLeft += scrollable.clientWidth)}>
|
||||||
<i slot="icon" class="fa-solid fa-angle-right" />
|
<i slot="icon" class="fa-solid fa-angle-right" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
bind:this={scrollable}
|
bind:this={scrollable}
|
||||||
|
bind:clientWidth={scrollableWidth}
|
||||||
on:scroll={() => (scrollpos = scrollable.scrollLeft / (scrollable.scrollWidth - scrollable.clientWidth))}
|
on:scroll={() => (scrollpos = scrollable.scrollLeft / (scrollable.scrollWidth - scrollable.clientWidth))}
|
||||||
class="no-scrollbar flex gap-6 overflow-y-hidden overflow-x-scroll scroll-smooth py-4"
|
class="no-scrollbar flex gap-6 overflow-y-hidden overflow-x-scroll scroll-smooth py-4"
|
||||||
>
|
>
|
||||||
|
|||||||
12
src/lib/components/utility/footer.svelte
Normal file
12
src/lib/components/utility/footer.svelte
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<footer class="sticky bottom-0 flex flex-col-reverse">
|
||||||
|
<slot name="content" />
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
footer {
|
||||||
|
padding-top: 16px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 28px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,46 +2,35 @@
|
|||||||
import IconButton from './iconButton.svelte'
|
import IconButton from './iconButton.svelte'
|
||||||
import { goto } from '$app/navigation'
|
import { goto } from '$app/navigation'
|
||||||
import { page } from '$app/stores'
|
import { page } from '$app/stores'
|
||||||
|
|
||||||
let windowY = 0
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window bind:scrollY={windowY} />
|
<nav class="sticky top-0 z-10 grid grid-cols-[1fr_auto_1fr] items-center duration-300">
|
||||||
<nav class="sticky top-0 z-10 flex items-center justify-between px-8 duration-300" class:background-active={windowY > 0}>
|
|
||||||
<section class="flex h-full">
|
<section class="flex h-full">
|
||||||
<IconButton>
|
<IconButton on:click={() => goto('/settings')}>
|
||||||
<i slot="icon" class="fa-solid fa-gear" />
|
<i slot="icon" class="fa-solid fa-user-gear" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
{#if $page.url.pathname !== '/'}
|
{#if $page.url.pathname !== '/'}
|
||||||
<IconButton on:click={() => goto('/')}>
|
|
||||||
<i slot="icon" class="fa-solid fa-house" />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton on:click={() => history.back()}>
|
<IconButton on:click={() => history.back()}>
|
||||||
<i slot="icon" class="fa-solid fa-arrow-left" />
|
<i slot="icon" class="fa-solid fa-arrow-left" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
<section class="flex h-full">
|
<section class="flex h-full justify-center">
|
||||||
|
<slot name="center-content" />
|
||||||
|
</section>
|
||||||
|
<section class="flex h-full justify-end">
|
||||||
<IconButton>
|
<IconButton>
|
||||||
<i slot="icon" class="fa-solid fa-magnifying-glass" />
|
<i slot="icon" class="fa-solid fa-magnifying-glass" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton>
|
|
||||||
<i slot="icon" class="fa-solid fa-user" />
|
|
||||||
</IconButton>
|
|
||||||
</section>
|
</section>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
nav {
|
nav {
|
||||||
height: 64px;
|
padding: 20px 2rem;
|
||||||
padding-top: 16px;
|
|
||||||
padding-bottom: 16px;
|
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
line-height: 28px;
|
line-height: 28px;
|
||||||
}
|
}
|
||||||
nav.background-active {
|
|
||||||
background-color: rgb(10, 10, 10);
|
|
||||||
}
|
|
||||||
section {
|
section {
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
<script>
|
|
||||||
export let open = false
|
|
||||||
|
|
||||||
import IconButton from './iconButton.svelte'
|
|
||||||
import { slide } from 'svelte/transition'
|
|
||||||
|
|
||||||
export const toggleOpen = () => (open = !open)
|
|
||||||
|
|
||||||
let sidebar
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:document
|
|
||||||
on:mouseup={(event) => {
|
|
||||||
if (sidebar && open) {
|
|
||||||
if (!sidebar.contains(event.target)) open = false
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{#if open}
|
|
||||||
<section bind:this={sidebar} transition:slide={{ axis: 'x' }} class="fixed left-0 top-0 z-20 h-full w-full max-w-sm bg-slate-600 p-2" style="width: calc(100% - 4rem);">
|
|
||||||
<div class="float-right h-8">
|
|
||||||
<IconButton on:click={toggleOpen}>
|
|
||||||
<i slot="icon" class="fa-solid fa-x" />
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{/if}
|
|
||||||
78
src/lib/components/utility/slider.svelte
Normal file
78
src/lib/components/utility/slider.svelte
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from 'svelte'
|
||||||
|
import { createEventDispatcher } from 'svelte'
|
||||||
|
|
||||||
|
export let initialValue = 0
|
||||||
|
onMount(() => (sliderValue = Number(initialValue)))
|
||||||
|
|
||||||
|
export const setValue = (value) => (sliderValue = value)
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
let sliderValue
|
||||||
|
let sliderTrail, sliderThumb
|
||||||
|
|
||||||
|
const trackThumb = (sliderPos) => {
|
||||||
|
if (sliderThumb) sliderThumb.style.left = `${sliderPos}%`
|
||||||
|
if (sliderTrail) sliderTrail.style.right = `${100 - sliderPos}%`
|
||||||
|
}
|
||||||
|
|
||||||
|
$: trackThumb(sliderValue)
|
||||||
|
|
||||||
|
const handleKeyPress = (key) => {
|
||||||
|
if ((key === 'ArrowRight' || key === 'ArrowUp') && sliderValue < 100) {
|
||||||
|
sliderValue += 1
|
||||||
|
return dispatch('valuechange', { value: sliderValue })
|
||||||
|
}
|
||||||
|
if ((key === 'ArrowLeft' || key === 'ArrowDown') && sliderValue > 0) {
|
||||||
|
sliderValue -= 1
|
||||||
|
return dispatch('valuechange', { value: sliderValue })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="slider-track"
|
||||||
|
class="relative isolate h-1 w-full rounded-full bg-neutral-600"
|
||||||
|
role="slider"
|
||||||
|
tabindex="0"
|
||||||
|
aria-valuenow={sliderValue}
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="100"
|
||||||
|
on:keydown={(event) => handleKeyPress(event.key)}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
class="absolute z-10 h-1 w-full"
|
||||||
|
step="any"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
bind:value={sliderValue}
|
||||||
|
tabindex="-1"
|
||||||
|
aria-hidden="true"
|
||||||
|
aria-disabled="true"
|
||||||
|
on:mouseup={() => dispatch('valuechange', { value: sliderValue })}
|
||||||
|
/>
|
||||||
|
<span bind:this={sliderTrail} id="slider-trail" class="absolute left-0 h-1 rounded-full bg-white transition-colors" />
|
||||||
|
<span bind:this={sliderThumb} id="slider-thumb" class="absolute top-1/2 aspect-square h-4 -translate-x-1/2 -translate-y-1/2 rounded-full bg-white opacity-0 transition-opacity duration-300" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
input[type='range'] {
|
||||||
|
appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
#slider-track:hover > #slider-trail {
|
||||||
|
background-color: var(--lazuli-primary);
|
||||||
|
}
|
||||||
|
#slider-track:focus > #slider-trail {
|
||||||
|
background-color: var(--lazuli-primary);
|
||||||
|
}
|
||||||
|
#slider-track:hover > #slider-thumb {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
#slider-track:focus > #slider-thumb {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
import { writable } from 'svelte/store'
|
import { writable } from 'svelte/store'
|
||||||
|
|
||||||
|
export const pageWidth = writable(null)
|
||||||
|
|
||||||
export const newestAlert = writable([null, null])
|
export const newestAlert = writable([null, null])
|
||||||
|
|
||||||
|
export const currentlyPlaying = writable(null)
|
||||||
|
|
||||||
export const backgroundImage = writable(null)
|
export const backgroundImage = writable(null)
|
||||||
|
|||||||
@@ -1,22 +1,16 @@
|
|||||||
import Joi from 'joi'
|
import Joi from 'joi'
|
||||||
|
|
||||||
export const ticksToTime = (ticks) => {
|
export const getVolume = () => {
|
||||||
const totalSeconds = ~~(ticks / 10000000)
|
const currentVolume = localStorage.getItem('volume')
|
||||||
const totalMinutes = ~~(totalSeconds / 60)
|
if (currentVolume) return currentVolume
|
||||||
const hours = ~~(totalMinutes / 60)
|
|
||||||
|
|
||||||
const remainderMinutes = totalMinutes - hours * 60
|
const defaultVolume = 100
|
||||||
const remainderSeconds = totalSeconds - totalMinutes * 60
|
localStorage.setItem('volume', defaultVolume)
|
||||||
|
return defaultVolume
|
||||||
const format = (value) => {
|
|
||||||
return value < 10 ? `0${value}` : value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hours > 0) {
|
export const setVolume = (volume) => {
|
||||||
return `${hours}:${format(remainderMinutes)}:${format(remainderSeconds)}`
|
if (Number.isFinite(volume)) localStorage.setItem('volume', Math.round(volume))
|
||||||
} else {
|
|
||||||
return `${remainderMinutes}:${format(remainderSeconds)}`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class JellyfinUtils {
|
export class JellyfinUtils {
|
||||||
|
|||||||
6
src/routes/(app)/+layout.js
Normal file
6
src/routes/(app)/+layout.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/** @type {import('./$types').LayoutLoad} */
|
||||||
|
export const load = ({ url }) => {
|
||||||
|
return {
|
||||||
|
url: url.pathname,
|
||||||
|
}
|
||||||
|
}
|
||||||
111
src/routes/(app)/+layout.svelte
Normal file
111
src/routes/(app)/+layout.svelte
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
<script>
|
||||||
|
import Navbar from '$lib/components/utility/navbar.svelte'
|
||||||
|
import Footer from '$lib/components/utility/footer.svelte'
|
||||||
|
import MiniPlayer from '$lib/components/media/miniPlayer.svelte'
|
||||||
|
import { fly, fade } from 'svelte/transition'
|
||||||
|
import { pageWidth } from '$lib/utils/stores.js'
|
||||||
|
|
||||||
|
export let data
|
||||||
|
|
||||||
|
const contentTabs = {
|
||||||
|
'/': {
|
||||||
|
header: 'Home',
|
||||||
|
icon: 'fa-solid fa-house',
|
||||||
|
},
|
||||||
|
'/artist': {
|
||||||
|
header: 'Artists',
|
||||||
|
icon: 'fa-solid fa-guitar',
|
||||||
|
},
|
||||||
|
'/playlist': {
|
||||||
|
header: 'Playlists',
|
||||||
|
icon: 'fa-solid fa-bars-staggered',
|
||||||
|
},
|
||||||
|
'/library': {
|
||||||
|
header: 'Libray',
|
||||||
|
icon: 'fa-solid fa-book-open',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
let previousPage = data.url
|
||||||
|
let direction = 1
|
||||||
|
$: calculateDirection(data.url)
|
||||||
|
|
||||||
|
const calculateDirection = (newPage) => {
|
||||||
|
const contentLinks = Object.keys(contentTabs)
|
||||||
|
const newPageIndex = contentLinks.indexOf(newPage)
|
||||||
|
const previousPageIndex = contentLinks.indexOf(previousPage)
|
||||||
|
if (newPageIndex > previousPageIndex) {
|
||||||
|
direction = 1
|
||||||
|
} else {
|
||||||
|
direction = -1
|
||||||
|
}
|
||||||
|
previousPage = data.url
|
||||||
|
}
|
||||||
|
|
||||||
|
let activeTab, indicatorBar, tabList
|
||||||
|
$: calculateBar(activeTab)
|
||||||
|
|
||||||
|
const calculateBar = (activeTab) => {
|
||||||
|
if (activeTab) {
|
||||||
|
const listRect = tabList.getBoundingClientRect()
|
||||||
|
const tabRec = activeTab.getBoundingClientRect()
|
||||||
|
indicatorBar.style.top = `${listRect.height}px`
|
||||||
|
if (direction === 1) {
|
||||||
|
indicatorBar.style.right = `${listRect.right - tabRec.right}px`
|
||||||
|
setTimeout(() => (indicatorBar.style.left = `${tabRec.left - listRect.left}px`), 300)
|
||||||
|
} else {
|
||||||
|
indicatorBar.style.left = `${tabRec.left - listRect.left}px`
|
||||||
|
setTimeout(() => (indicatorBar.style.right = `${listRect.right - tabRec.right}px`), 300)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex h-full flex-col">
|
||||||
|
{#if $pageWidth > 768}
|
||||||
|
<Navbar>
|
||||||
|
<h1 slot="center-content" bind:this={tabList} class="relative flex items-center gap-12 text-lg">
|
||||||
|
{#each Object.entries(contentTabs) as [page, tabData]}
|
||||||
|
{#if data.url === page}
|
||||||
|
<span bind:this={activeTab} class="pointer-events-none">{tabData.header}</span>
|
||||||
|
{:else}
|
||||||
|
<a class="text-neutral-400 hover:text-lazuli-primary" href={page}>{tabData.header}</a>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
{#if data.url in contentTabs}
|
||||||
|
<div bind:this={indicatorBar} transition:fade class="absolute h-0.5 bg-lazuli-primary transition-all duration-300 ease-in-out" />
|
||||||
|
{/if}
|
||||||
|
</h1>
|
||||||
|
</Navbar>
|
||||||
|
{:else}
|
||||||
|
<Navbar />
|
||||||
|
{/if}
|
||||||
|
<div class="relative flex-1 overflow-hidden">
|
||||||
|
{#key previousPage}
|
||||||
|
<div in:fly={{ x: 200 * direction, duration: 300, delay: 300 }} out:fly={{ x: -200 * direction, duration: 300 }} class="no-scrollbar h-full overflow-y-scroll px-8 py-4 md:px-32">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
{/key}
|
||||||
|
<div class="absolute bottom-0 w-full">
|
||||||
|
<MiniPlayer displayMode={$pageWidth > 768 ? 'horizontal' : 'vertical'} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if $pageWidth < 768}
|
||||||
|
<Footer>
|
||||||
|
<section slot="content" class="flex items-center justify-center" style="height: 32px;">
|
||||||
|
<h1 bind:this={tabList} class="relative flex w-full items-center justify-around">
|
||||||
|
{#each Object.entries(contentTabs) as [page, tabData]}
|
||||||
|
{#if data.url === page}
|
||||||
|
<span bind:this={activeTab} class="pointer-events-none"><i class={tabData.icon} /></span>
|
||||||
|
{:else}
|
||||||
|
<a class="text-neutral-400 hover:text-lazuli-primary" href={page}><i class={tabData.icon} /></a>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
{#if data.url in contentTabs}
|
||||||
|
<div bind:this={indicatorBar} transition:fade class="absolute h-0.5 bg-lazuli-primary transition-all duration-300 ease-in-out" />
|
||||||
|
{/if}
|
||||||
|
</h1>
|
||||||
|
</section>
|
||||||
|
</Footer>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main class="mx-auto grid h-full max-w-screen-xl gap-8 p-8 pt-24">
|
<main class="mx-auto grid h-full max-w-screen-xl gap-8 p-8">
|
||||||
<nav class="h-full rounded-lg p-6">
|
<nav class="h-full rounded-lg p-6">
|
||||||
<h1 class="flex h-6 justify-between text-neutral-400">
|
<h1 class="flex h-6 justify-between text-neutral-400">
|
||||||
<span>
|
<span>
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
export const trailingSlash = 'never'
|
|
||||||
|
|
||||||
/** @type {import('./$types').PageServerLoad} */
|
|
||||||
export const load = ({ url }) => {
|
|
||||||
return {
|
|
||||||
url: url.pathname,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,13 +2,10 @@
|
|||||||
import '../app.css'
|
import '../app.css'
|
||||||
import '@fortawesome/fontawesome-free/css/all.min.css'
|
import '@fortawesome/fontawesome-free/css/all.min.css'
|
||||||
import AlertBox from '$lib/components/utility/alertBox.svelte'
|
import AlertBox from '$lib/components/utility/alertBox.svelte'
|
||||||
import SubLayouts from './subLayouts.svelte'
|
import { newestAlert, backgroundImage, pageWidth } from '$lib/utils/stores.js'
|
||||||
import { newestAlert, backgroundImage } from '$lib/utils/stores.js'
|
|
||||||
import { fade } from 'svelte/transition'
|
import { fade } from 'svelte/transition'
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
|
|
||||||
export let data
|
|
||||||
|
|
||||||
let alertBox
|
let alertBox
|
||||||
$: addAlert($newestAlert)
|
$: addAlert($newestAlert)
|
||||||
|
|
||||||
@@ -22,6 +19,7 @@
|
|||||||
onMount(() => (loaded = true))
|
onMount(() => (loaded = true))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:window bind:innerWidth={$pageWidth} />
|
||||||
<div class="no-scrollbar h-screen font-notoSans text-white">
|
<div class="no-scrollbar h-screen font-notoSans text-white">
|
||||||
<div class="fixed isolate -z-10 h-full w-screen bg-black">
|
<div class="fixed isolate -z-10 h-full w-screen bg-black">
|
||||||
<!-- This whole bg is a complete copy of ytmusic, design own at some point (Place for customization w/ album art etc?) (EDIT: Ok, it looks SICK with album art!) -->
|
<!-- This whole bg is a complete copy of ytmusic, design own at some point (Place for customization w/ album art etc?) (EDIT: Ok, it looks SICK with album art!) -->
|
||||||
@@ -33,9 +31,7 @@
|
|||||||
{/key}
|
{/key}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<SubLayouts currentPage={data.url}>
|
<slot />
|
||||||
<slot slot="innerContent" />
|
|
||||||
</SubLayouts>
|
|
||||||
<AlertBox bind:this={alertBox} />
|
<AlertBox bind:this={alertBox} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
<script>
|
|
||||||
export let currentPage
|
|
||||||
|
|
||||||
import Navbar from '$lib/components/utility/navbar.svelte'
|
|
||||||
import { fly } from 'svelte/transition'
|
|
||||||
|
|
||||||
const contentTabs = {
|
|
||||||
Home: '/',
|
|
||||||
Artists: '/artist',
|
|
||||||
Playlists: '/playlist',
|
|
||||||
Libray: '/library',
|
|
||||||
}
|
|
||||||
|
|
||||||
let previousPage = currentPage
|
|
||||||
let direction = 1
|
|
||||||
$: calculateDirection(currentPage)
|
|
||||||
|
|
||||||
const calculateDirection = (newPage) => {
|
|
||||||
const contentLinks = Object.values(contentTabs)
|
|
||||||
const newPageIndex = contentLinks.indexOf(newPage)
|
|
||||||
const previousPageIndex = contentLinks.indexOf(previousPage)
|
|
||||||
if (newPageIndex > previousPageIndex) {
|
|
||||||
direction = 1
|
|
||||||
} else {
|
|
||||||
direction = -1
|
|
||||||
}
|
|
||||||
previousPage = currentPage
|
|
||||||
}
|
|
||||||
|
|
||||||
let activeTab, indicatorBar, tabList
|
|
||||||
$: calculateBar(activeTab)
|
|
||||||
|
|
||||||
const calculateBar = (activeTab) => {
|
|
||||||
if (activeTab) {
|
|
||||||
const listRect = tabList.getBoundingClientRect()
|
|
||||||
const tabRec = activeTab.getBoundingClientRect()
|
|
||||||
indicatorBar.style.top = `${listRect.height}px`
|
|
||||||
if (direction === 1) {
|
|
||||||
indicatorBar.style.right = `${listRect.right - tabRec.right}px`
|
|
||||||
setTimeout(() => (indicatorBar.style.left = `${tabRec.left - listRect.left}px`), 350)
|
|
||||||
} else {
|
|
||||||
indicatorBar.style.left = `${tabRec.left - listRect.left}px`
|
|
||||||
setTimeout(() => (indicatorBar.style.right = `${listRect.right - tabRec.right}px`), 350)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if Object.values(contentTabs).includes(currentPage)}
|
|
||||||
<Navbar />
|
|
||||||
<div class="flex justify-center py-4">
|
|
||||||
<h1 bind:this={tabList} class="relative flex justify-center gap-12 text-lg">
|
|
||||||
{#each Object.entries(contentTabs) as [header, page]}
|
|
||||||
{#if currentPage === page}
|
|
||||||
<span bind:this={activeTab} class="pointer-events-none">{header}</span>
|
|
||||||
{:else}
|
|
||||||
<a class="text-neutral-400 hover:text-lazuli-primary" href={page}>{header}</a>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
<div bind:this={indicatorBar} class="absolute h-0.5 bg-lazuli-primary transition-all duration-300 ease-in-out" />
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div class="overflow-x-hidden px-8 sm:px-32">
|
|
||||||
{#key previousPage}
|
|
||||||
<div in:fly={{ x: 200 * direction, duration: 300, delay: 300 }} out:fly={{ x: -200 * direction, duration: 300 }}>
|
|
||||||
<slot name="innerContent" />
|
|
||||||
</div>
|
|
||||||
{/key}
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<slot name="innerContent" />
|
|
||||||
{/if}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export const prerender = false
|
|
||||||
export const ssr = false
|
|
||||||
|
|
||||||
/** @type {import('./$types').PageLoad} */
|
|
||||||
export async function load() {}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<script>
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<section class="grid h-full place-items-center">
|
|
||||||
<div class="aspect-video h-2/3 bg-slate-900">
|
|
||||||
<video id="video-player" controls></video>
|
|
||||||
<!-- <iframe
|
|
||||||
src="https://www.youtube.com/embed/uhx-u5peyeY?controls=0&autoplay=0&showinfo=0&rel=0"
|
|
||||||
title="Video"
|
|
||||||
frameBorder="0"
|
|
||||||
allow="accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
|
||||||
allowFullScreen
|
|
||||||
class="h-full w-full"
|
|
||||||
/> -->
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
Reference in New Issue
Block a user