More components

This commit is contained in:
Eclypsed
2024-01-29 12:29:32 -05:00
parent 4ae54aa14c
commit 098ac487ec
7 changed files with 206 additions and 209 deletions

49
src/app.d.ts vendored
View File

@@ -16,6 +16,55 @@ declare global {
username: string username: string
password?: string password?: string
} }
type ServiceType = 'jellyfin' | 'youtube-music'
interface MediaItem {
connectionId: string
serviceType: string
id: string
name: string
duration: number
thumbnail: string
}
interface Song extends MediaItem {
artists: {
id: string
name: string
}[]
album?: {
id: string
name: string
artists: {
id: string
name: string
}[]
}
audio: string
video?: string
releaseDate: string
}
interface Album extends MediaItem {
artists: {
id: string
name: string
}[]
songs: Song[]
releaseDate: string
}
interface Playlist extends MediaItem {
songs: Song[]
description?: string
}
interface Artist {
id: string
name: string
// Add more here in the future
}
} }
export {} export {}

View File

@@ -0,0 +1,40 @@
<script lang="ts" context="module">
export interface NavTab {
pathname: string
name: string
icon: string
}
</script>
<script lang="ts">
export let disabled = false
export let nav: NavTab
import { createEventDispatcher } from "svelte";
import { goto } from "$app/navigation";
const dispatch = createEventDispatcher()
</script>
<button
class="grid aspect-square w-full place-items-center transition-colors"
{disabled}
on:click={() => {
dispatch('click')
goto(nav.pathname)
}}
>
<span class="pointer-events-none flex flex-col gap-2 text-xs">
<i class="{nav.icon} text-xl" />
{nav.name}
</span>
</button>
<style>
button:not(:disabled) {
color: rgb(163 163, 163);
}
button:not(:disabled):hover {
color: var(--lazuli-primary);
}
</style>

View File

@@ -0,0 +1,38 @@
<script lang="ts" context="module">
export interface PlaylistTab {
id: string
name: string
thumbnail: string
}
</script>
<script lang="ts">
export let disabled = false
export let playlist: PlaylistTab
import { createEventDispatcher } from "svelte";
import { goto } from "$app/navigation";
const dispatch = createEventDispatcher()
</script>
<div class="flex items-center gap-1">
<button
title={playlist.name}
{disabled}
class="relative aspect-square w-full rounded-lg bg-cover bg-center transition-all"
style="background-image: url({playlist.thumbnail});"
on:click={() => {
dispatch('click')
goto(`/library?playlist=${playlist.id}}`)
}}
>
</button>
<span class="translate-x-3 overflow-clip text-ellipsis whitespace-nowrap rounded-md bg-slate-600 px-2 py-1 text-sm">{playlist.name}</span>
</div>
<style>
button:not(:disabled):not(:hover) {
filter: brightness(50%);
}
</style>

View File

@@ -1,80 +0,0 @@
<script context="module" lang="ts">
export interface NavTab {
pathname: string
name: string
icon: string
}
</script>
<script lang="ts">
export let navTabs: NavTab[]
export let currentPathname: string
export let transitionTime: number = 200
import { fade } from 'svelte/transition'
import { createEventDispatcher, onMount } from 'svelte'
const dispatch = createEventDispatcher()
type PageTransitionDirection = 'left' | 'right'
let direction: PageTransitionDirection = 'right'
const calculateDirection = (newPage: string, currentPage: string): PageTransitionDirection => {
const newPageIndex = navTabs.findIndex((tab) => tab.pathname === newPage)
const currentPageIndex = navTabs.findIndex((tab) => tab.pathname === currentPage)
return newPageIndex > currentPageIndex ? 'right' : 'left'
}
let indicatorBar: HTMLDivElement, tabList: HTMLDivElement
const calculateBar = (newPathname: string) => {
const newTab = document.querySelector(`button[data-tab="${newPathname}"]`)
if (newTab && indicatorBar && tabList) {
const listRect = tabList.getBoundingClientRect(),
tabRec = newTab.getBoundingClientRect()
const shiftRight = () => (indicatorBar.style.right = `${listRect.right - tabRec.right}px`),
shiftLeft = () => (indicatorBar.style.left = `${tabRec.left - listRect.left}px`)
if (direction === 'right') {
shiftRight()
setTimeout(shiftLeft, transitionTime)
} else {
shiftLeft()
setTimeout(shiftRight, transitionTime)
}
}
}
</script>
<div bind:this={tabList} id="tab-list" class="relative flex w-full items-center justify-around bg-black">
{#each navTabs as tabData}
<button
class="transition-colors"
data-tab={tabData.pathname}
disabled={currentPathname === tabData.pathname}
on:click={() => {
direction = calculateDirection(tabData.pathname, currentPathname)
dispatch('navigate', { direction, pathname: tabData.pathname })
}}
>
<i class="{tabData.icon} pointer-events-none" />
</button>
{/each}
{#if navTabs.some((tab) => tab.pathname === currentPathname)}
<div bind:this={indicatorBar} transition:fade class="absolute bottom-0 h-1 rounded-full bg-white transition-all duration-300 ease-in-out" />
{/if}
</div>
<style>
#tab-list {
padding: 16px 0px;
font-size: 20px;
line-height: 28px;
}
button:not(:disabled) {
cursor: pointer;
color: rgb(163 163, 163);
}
button:not(:disabled):hover {
color: var(--lazuli-primary);
}
</style>

View File

@@ -15,9 +15,7 @@ type UserQueryParams = {
includePassword?: boolean includePassword?: boolean
} }
type ServiceType = 'jellyfin' | 'youtube-music' interface DBServiceData {
interface Service {
id: string id: string
type: ServiceType type: ServiceType
userId: string userId: string
@@ -31,10 +29,10 @@ interface DBServiceRow {
url: string url: string
} }
interface Connection { interface DBConnectionData {
id: string id: string
user: User user: User
service: Service service: DBServiceData
accessToken: string accessToken: string
refreshToken: string | null refreshToken: string | null
expiry: number | null expiry: number | null
@@ -81,13 +79,13 @@ export class Users {
} }
export class Services { export class Services {
static getService = (id: string): Service => { static getService = (id: string): DBServiceData => {
const { type, userId, url } = db.prepare('SELECT * FROM Users WHERE id = ?').get(id) as DBServiceRow const { type, userId, url } = db.prepare('SELECT * FROM Users WHERE id = ?').get(id) as DBServiceRow
const service: Service = { id, type: type as ServiceType, userId, url: new URL(url) } const service: DBServiceData = { id, type: type as ServiceType, userId, url: new URL(url) }
return service return service
} }
static addService = (type: ServiceType, userId: string, url: URL): Service => { static addService = (type: ServiceType, userId: string, url: URL): DBServiceData => {
const serviceId = generateUUID() const serviceId = generateUUID()
db.prepare('INSERT INTO Services(id, type, userId, url) VALUES(?, ?, ?, ?)').run(serviceId, type, userId, url.origin) db.prepare('INSERT INTO Services(id, type, userId, url) VALUES(?, ?, ?, ?)').run(serviceId, type, userId, url.origin)
return this.getService(serviceId) return this.getService(serviceId)
@@ -100,15 +98,15 @@ export class Services {
} }
export class Connections { export class Connections {
static getConnection = (id: string): Connection => { static getConnection = (id: string): DBConnectionData => {
const { userId, serviceId, accessToken, refreshToken, expiry } = db.prepare('SELECT * FROM Connections WHERE id = ?').get(id) as DBConnectionRow const { userId, serviceId, accessToken, refreshToken, expiry } = db.prepare('SELECT * FROM Connections WHERE id = ?').get(id) as DBConnectionRow
const connection: Connection = { id, user: Users.getUser(userId)!, service: Services.getService(serviceId), accessToken, refreshToken, expiry } const connection: DBConnectionData = { id, user: Users.getUser(userId)!, service: Services.getService(serviceId), accessToken, refreshToken, expiry }
return connection return connection
} }
static getUserConnections = (userId: string): Connection[] => { static getUserConnections = (userId: string): DBConnectionData[] => {
const connectionRows = db.prepare('SELECT * FROM Connections WHERE userId = ?').all(userId) as DBConnectionRow[] const connectionRows = db.prepare('SELECT * FROM Connections WHERE userId = ?').all(userId) as DBConnectionRow[]
const connections: Connection[] = [] const connections: DBConnectionData[] = []
const user = Users.getUser(userId)! const user = Users.getUser(userId)!
connectionRows.forEach((row) => { connectionRows.forEach((row) => {
const { id, serviceId, accessToken, refreshToken, expiry } = row const { id, serviceId, accessToken, refreshToken, expiry } = row
@@ -117,7 +115,7 @@ export class Connections {
return connections return connections
} }
static addConnection = (userId: string, serviceId: string, accessToken: string, refreshToken: string | null, expiry: number | null): Connection => { static addConnection = (userId: string, serviceId: string, accessToken: string, refreshToken: string | null, expiry: number | null): DBConnectionData => {
const connectionId = generateUUID() const connectionId = generateUUID()
db.prepare('INSERT INTO Connections(id, userId, serviceId, accessToken, refreshToken, expiry) VALUES(?, ?, ?, ?, ?, ?)').run(connectionId, userId, serviceId, accessToken, refreshToken, expiry) db.prepare('INSERT INTO Connections(id, userId, serviceId, accessToken, refreshToken, expiry) VALUES(?, ?, ?, ?, ?, ?)').run(connectionId, userId, serviceId, accessToken, refreshToken, expiry)
return this.getConnection(connectionId) return this.getConnection(connectionId)

View File

@@ -3,8 +3,9 @@
import { goto, beforeNavigate } from '$app/navigation' import { goto, beforeNavigate } from '$app/navigation'
import { pageWidth } from '$lib/stores' import { pageWidth } from '$lib/stores'
import type { LayoutData } from './$types' import type { LayoutData } from './$types'
import type { Tab } from './+layout'
import { onMount } from 'svelte' import { onMount } from 'svelte'
import NavTabComponent, { type NavTab } from '$lib/components/navbar/navTab.svelte'
import PlaylistTabComponent, { type PlaylistTab } from '$lib/components/navbar/playlistTab.svelte'
export let data: LayoutData export let data: LayoutData
@@ -21,81 +22,55 @@
return (pathname.startsWith(rootPathname) && rootPathname !== '/') || (pathname === '/' && rootPathname === '/') return (pathname.startsWith(rootPathname) && rootPathname !== '/') || (pathname === '/' && rootPathname === '/')
} }
const calculateDirection = (newTab: Tab): void => { // const calculateDirection = (newTab: Tab): void => {
const newTabIndex = data.tabs.findIndex((tab) => tab === newTab) // const newTabIndex = data.tabs.findIndex((tab) => tab === newTab)
directionMultiplier = newTabIndex > currentTabIndex ? -1 : 1 // directionMultiplier = newTabIndex > currentTabIndex ? -1 : 1
currentTabIndex = newTabIndex // currentTabIndex = newTabIndex
} // }
const navigate = (newPathname: string): void => { // const navigate = (newPathname: string): void => {
const newTabIndex = data.tabs.findIndex((tab) => inPathnameHeirarchy(newPathname, tab.pathname)) // const newTabIndex = data.tabs.findIndex((tab) => inPathnameHeirarchy(newPathname, tab.pathname))
if (newTabIndex < 0) indicatorBar.style.opacity = '0' // if (newTabIndex < 0) indicatorBar.style.opacity = '0'
const newTab = data.tabs[newTabIndex] // const newTab = data.tabs[newTabIndex]
if (!newTab?.button) return // if (!newTab?.button) return
const tabRec = newTab.button.getBoundingClientRect(), // const tabRec = newTab.button.getBoundingClientRect(),
listRect = tabList.getBoundingClientRect() // listRect = tabList.getBoundingClientRect()
const shiftTop = () => (indicatorBar.style.top = `${tabRec.top - listRect.top}px`), // const shiftTop = () => (indicatorBar.style.top = `${tabRec.top - listRect.top}px`),
shiftBottom = () => (indicatorBar.style.bottom = `${listRect.bottom - tabRec.bottom}px`) // shiftBottom = () => (indicatorBar.style.bottom = `${listRect.bottom - tabRec.bottom}px`)
if (directionMultiplier > 0) { // if (directionMultiplier > 0) {
shiftTop() // shiftTop()
setTimeout(shiftBottom, pageTransitionTime) // setTimeout(shiftBottom, pageTransitionTime)
} else { // } else {
shiftBottom() // shiftBottom()
setTimeout(shiftTop, pageTransitionTime) // setTimeout(shiftTop, pageTransitionTime)
} // }
setTimeout(() => (indicatorBar.style.opacity = '100%'), pageTransitionTime + 300) // setTimeout(() => (indicatorBar.style.opacity = '100%'), pageTransitionTime + 300)
} // }
onMount(() => setTimeout(() => navigate(data.url.pathname))) // More stupid fucking non-blocking event loop shit // onMount(() => setTimeout(() => navigate(data.url.pathname))) // More stupid fucking non-blocking event loop shit
beforeNavigate(({ to }) => { // beforeNavigate(({ to }) => {
if (to) navigate(to.url.pathname) // if (to) navigate(to.url.pathname)
}) // })
</script> </script>
{#if $pageWidth >= 768} {#if $pageWidth >= 768}
<div class="grid h-full grid-rows-1 gap-8 overflow-hidden"> <div class="grid h-full grid-rows-1 gap-8 overflow-hidden">
<div class="no-scrollbar fixed left-0 top-0 z-10 grid h-full grid-cols-1 grid-rows-[min-content_auto] gap-6 p-3 pt-12" bind:this={tabList}> <div class="no-scrollbar fixed left-0 top-0 z-10 grid h-full grid-cols-1 grid-rows-[min-content_auto] gap-6 p-3 pt-12 w-20" bind:this={tabList}>
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-6">
{#each data.tabs.filter((tab) => tab.type === 'nav') as nav, index} {#each data.navTabs as nav}
<button <NavTabComponent {nav} disabled={inPathnameHeirarchy(data.url.pathname, nav.pathname)}/>
class="navTab grid aspect-square w-14 place-items-center transition-colors"
bind:this={data.tabs[index].button}
disabled={inPathnameHeirarchy(data.url.pathname, nav.pathname)}
on:click={() => {
calculateDirection(nav)
goto(nav.pathname)
}}
>
<span class="pointer-events-none flex flex-col gap-2 text-xs">
<i class="{nav.icon} text-xl" />
{nav.name}
</span>
</button>
{/each} {/each}
<div bind:this={indicatorBar} class="absolute left-0 w-[0.2rem] rounded-full bg-white transition-all duration-300 ease-in-out" /> <!-- <div bind:this={indicatorBar} class="absolute left-0 w-[0.2rem] rounded-full bg-white transition-all duration-300 ease-in-out" /> -->
</div> </div>
<div class="no-scrollbar flex flex-col gap-6 overflow-y-scroll"> <div class="flex flex-col gap-6">
{#each data.tabs.filter((tab) => tab.type === 'playlist') as playlist} {#each data.playlistTabs as playlist}
<div class="playlistTab-wrapper flex items-center gap-1"> <PlaylistTabComponent {playlist} disabled={new URLSearchParams(data.url.search).get('playlist') === playlist.id} />
<button
title={playlist.name}
disabled={new URLSearchParams(data.url.search).get('playlist') === new URLSearchParams(playlist.pathname.split('?')[1]).get('playlist')}
class="playlistTab relative aspect-square w-14 rounded-lg bg-cover bg-center transition-all"
style="background-image: url({playlist.icon});"
on:click={() => {
calculateDirection(playlist)
goto(playlist.pathname)
}}
>
</button>
<span class="translate-x-3 overflow-clip text-ellipsis whitespace-nowrap rounded-md bg-slate-600 px-2 py-1 text-sm">{playlist.name}</span>
</div>
{/each} {/each}
</div> </div>
</div> </div>
@@ -139,18 +114,3 @@
</footer> </footer>
</div> </div>
{/if} {/if}
<style>
.navTab:not(:disabled) {
color: rgb(163 163, 163);
}
.navTab:not(:disabled):hover {
color: var(--lazuli-primary);
}
.playlistTab-wrapper:hover > span {
display: block;
}
.playlistTab:not(:disabled):not(:hover) {
filter: brightness(50%);
}
</style>

View File

@@ -1,91 +1,83 @@
import type { LayoutLoad } from './$types' import type { LayoutLoad } from './$types'
import type { NavTab } from '$lib/components/navbar/navTab.svelte'
export interface Tab { import type { PlaylistTab } from '$lib/components/navbar/playlistTab.svelte'
type: 'nav' | 'playlist'
pathname: string
name: string
icon: string
button?: HTMLButtonElement
}
export const load: LayoutLoad = () => { export const load: LayoutLoad = () => {
const navTabs: Tab[] = [ const navTabs: NavTab[] = [
{ {
type: 'nav',
pathname: '/', pathname: '/',
name: 'Home', name: 'Home',
icon: 'fa-solid fa-house', icon: 'fa-solid fa-house',
}, },
{ {
type: 'nav',
pathname: '/user', pathname: '/user',
name: 'User', name: 'User',
icon: 'fa-solid fa-user', // This would be a cool spot for a user-uploaded pfp icon: 'fa-solid fa-user', // This would be a cool spot for a user-uploaded pfp
}, },
{ {
type: 'nav',
pathname: '/search', pathname: '/search',
name: 'Search', name: 'Search',
icon: 'fa-solid fa-search', icon: 'fa-solid fa-search',
}, },
{ {
type: 'nav',
pathname: '/library', pathname: '/library',
name: 'Libray', name: 'Libray',
icon: 'fa-solid fa-bars-staggered', icon: 'fa-solid fa-bars-staggered',
}, },
] ]
const playlistTabs: Tab[] = [ const playlistTabs: PlaylistTab[] = [
{ {
type: 'playlist', id: 'AD:TRANCE 10',
pathname: '/library?playlist=AD:TRANCE 10',
name: 'AD:TRANCE 10', name: 'AD:TRANCE 10',
icon: 'https://www.diverse.direct/wp/wp-content/uploads/470_artwork.jpg', thumbnail: 'https://www.diverse.direct/wp/wp-content/uploads/470_artwork.jpg',
}, },
{ {
type: 'playlist', id: 'Fionaredica',
pathname: '/library?playlist=Fionaredica',
name: 'Fionaredica', name: 'Fionaredica',
icon: 'https://f4.bcbits.com/img/a2436961975_10.jpg', thumbnail: 'https://f4.bcbits.com/img/a2436961975_10.jpg',
}, },
{ {
type: 'playlist', id: 'Machinate',
pathname: '/library?playlist=Machinate',
name: 'Machinate', name: 'Machinate',
icon: 'https://f4.bcbits.com/img/a3587136348_10.jpg', thumbnail: 'https://f4.bcbits.com/img/a3587136348_10.jpg',
}, },
{ {
type: 'playlist', id: 'MAGGOD',
pathname: '/library?playlist=MAGGOD',
name: 'MAGGOD', name: 'MAGGOD',
icon: 'https://f4.bcbits.com/img/a3641603617_10.jpg', thumbnail: 'https://f4.bcbits.com/img/a3641603617_10.jpg',
}, },
{ {
type: 'playlist', id: 'The Requiem',
pathname: '/library?playlist=The Requiem',
name: 'The Requiem', name: 'The Requiem',
icon: 'https://f4.bcbits.com/img/a2458067285_10.jpg', thumbnail: 'https://f4.bcbits.com/img/a2458067285_10.jpg',
}, },
{ {
type: 'playlist', id: 'IRREPARABLE HARDCORE IS BACK 2 -Horai Gekka-',
pathname: '/library?playlist=IRREPARABLE HARDCORE IS BACK 2 -Horai Gekka-',
name: 'IRREPARABLE HARDCORE IS BACK 2 -Horai Gekka-', name: 'IRREPARABLE HARDCORE IS BACK 2 -Horai Gekka-',
icon: 'https://f4.bcbits.com/img/a1483629734_10.jpg', thumbnail: 'https://f4.bcbits.com/img/a1483629734_10.jpg',
}, },
{ {
type: 'playlist', id: '妄殺オタクティクス',
pathname: '/library?playlist=妄殺オタクティクス',
name: '妄殺オタクティクス', name: '妄殺オタクティクス',
icon: 'https://f4.bcbits.com/img/a1653481367_10.jpg', thumbnail: 'https://f4.bcbits.com/img/a1653481367_10.jpg',
}, },
{ {
type: 'playlist', id: 'Collapse',
pathname: '/library?playlist=Collapse',
name: 'Collapse', name: 'Collapse',
icon: 'https://f4.bcbits.com/img/a0524413952_10.jpg', thumbnail: 'https://f4.bcbits.com/img/a0524413952_10.jpg',
},
{
id: 'Fleurix',
name: 'Fleurix',
thumbnail: 'https://f4.bcbits.com/img/a1856993876_10.jpg',
},
{
id: '天​才​失​格 -No Longer Prodigy-',
name: '天​才​失​格 -No Longer Prodigy-',
thumbnail: 'https://f4.bcbits.com/img/a2186643420_10.jpg',
}, },
] ]
return { tabs: navTabs.concat(playlistTabs) } return { navTabs, playlistTabs }
} }