Moved connections to user page, began search functionality

This commit is contained in:
Eclypsed
2024-03-30 01:15:12 -04:00
parent a4bad9d73b
commit a624f375e4
13 changed files with 242 additions and 165 deletions

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import { pageWidth } from '$lib/stores'
import SearchBar from '$lib/components/util/searchBar.svelte'
import type { LayoutData } from './$types'
import NavTab from '$lib/components/navbar/navTab.svelte'
import PlaylistTab from '$lib/components/navbar/playlistTab.svelte'
@@ -21,8 +22,8 @@
</script>
{#if $pageWidth >= 768}
<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 w-20 grid-cols-1 grid-rows-[min-content_auto] gap-5 px-3 py-16">
<div class="h-full overflow-hidden">
<div class="no-scrollbar fixed left-0 top-0 z-10 grid h-full w-20 grid-cols-1 grid-rows-[min-content_auto] gap-5 px-3 py-12">
<div class="flex flex-col gap-4">
{#each data.navTabs as nav}
<NavTab {nav} disabled={inPathnameHeirarchy(data.url.pathname, nav.pathname)} />
@@ -38,7 +39,10 @@
<div class="overflow-clip text-ellipsis text-neutral-400">Playlist &bull; {data.user.username}</div>
</div>
</div>
<section class="no-scrollbar overflow-y-scroll px-[max(7rem,_7vw)] pt-16">
<section class="no-scrollbar overflow-y-scroll px-[max(7rem,_7vw)]">
<div class="my-6 max-w-xl">
<SearchBar />
</div>
<slot />
</section>
<footer class="fixed bottom-0 flex w-full flex-col items-center justify-center">

View File

@@ -1,4 +1,14 @@
import type { PageServerLoad } from '../$types'
import ytdl from 'ytdl-core'
import { SECRET_INTERNAL_API_KEY } from '$env/static/private'
export const load: PageServerLoad = async ({ fetch }) => {}
export const load: PageServerLoad = async ({ fetch, url, locals }) => {
const query = url.searchParams.get('query')
if (query) {
const searchResults: { searchResults: (Song | Album | Artist | Playlist)[] } = await fetch(`/api/search?query=${query}&userId=${locals.user.id}`, {
method: 'GET',
headers: { apikey: SECRET_INTERNAL_API_KEY },
}).then((response) => response.json())
return searchResults
}
}

View File

@@ -1 +1,11 @@
<h1>Search Page</h1>
<script lang="ts">
import type { PageServerData } from './$types'
export let data: PageServerData
</script>
{#if data.searchResults}
{#each data.searchResults as searchResult}
<div>{searchResult.name} - {searchResult.type}</div>
{/each}
{/if}

View File

@@ -2,8 +2,101 @@
import IconButton from '$lib/components/util/iconButton.svelte'
import { goto } from '$app/navigation'
import type { LayoutData } from '../$types'
import Services from '$lib/services.json'
import JellyfinIcon from '$lib/static/jellyfin-icon.svg'
import YouTubeMusicIcon from '$lib/static/youtube-music-icon.svg'
import JellyfinAuthBox from './jellyfinAuthBox.svelte'
import { newestAlert } from '$lib/stores.js'
import type { PageServerData } from './$types.js'
import type { SubmitFunction } from '@sveltejs/kit'
import { getDeviceUUID } from '$lib/utils'
import { SvelteComponent, type ComponentType } from 'svelte'
import ConnectionProfile from './connectionProfile.svelte'
import { enhance } from '$app/forms'
import { PUBLIC_YOUTUBE_API_CLIENT_ID } from '$env/static/public'
export let data: LayoutData
export let data: PageServerData & LayoutData
let connections: ConnectionInfo[] = data.connections
const authenticateJellyfin: SubmitFunction = ({ formData, cancel }) => {
const { serverUrl, username, password } = Object.fromEntries(formData)
if (!(serverUrl && username && password)) {
$newestAlert = ['caution', 'All fields must be filled out']
return cancel()
}
try {
formData.set('serverUrl', new URL(serverUrl.toString()).origin)
} catch {
$newestAlert = ['caution', 'Server URL is invalid']
return cancel()
}
const deviceId = getDeviceUUID()
formData.append('deviceId', deviceId)
return ({ result }) => {
if (result.type === 'failure') {
return ($newestAlert = ['warning', result.data?.message])
} else if (result.type === 'success') {
const newConnection: ConnectionInfo = result.data!.newConnection
connections = [...connections, newConnection]
newConnectionModal = null
return ($newestAlert = ['success', `Added Jellyfin`])
}
}
}
const authenticateYouTube: SubmitFunction = async ({ formData, cancel }) => {
const googleLoginProcess = (): Promise<string> => {
return new Promise((resolve) => {
// @ts-ignore (google variable is a global variable imported by html script tag)
const client = google.accounts.oauth2.initCodeClient({
client_id: PUBLIC_YOUTUBE_API_CLIENT_ID,
scope: 'https://www.googleapis.com/auth/youtube',
ux_mode: 'popup',
callback: (response: any) => {
resolve(response.code)
},
})
client.requestCode()
})
}
const code = await googleLoginProcess()
if (!code) cancel()
formData.append('code', code)
return ({ result }) => {
if (result.type === 'failure') {
return ($newestAlert = ['warning', result.data?.message])
} else if (result.type === 'success') {
const newConnection: ConnectionInfo = result.data!.newConnection
connections = [...connections, newConnection]
return ($newestAlert = ['success', 'Added Youtube Music'])
}
}
}
const profileActions: SubmitFunction = ({ action, cancel }) => {
return ({ result }) => {
if (result.type === 'failure') {
return ($newestAlert = ['warning', result.data?.message])
} else if (result.type === 'success') {
const id = result.data!.deletedConnectionId
const indexToDelete = connections.findIndex((connection) => connection.id === id)
const serviceType = connections[indexToDelete].type
connections.splice(indexToDelete, 1)
connections = connections
return ($newestAlert = ['success', `Deleted ${Services[serviceType].displayName}`])
}
}
}
let newConnectionModal: ComponentType<SvelteComponent<{ submitFunction: SubmitFunction }>> | null = null
</script>
<main class="flex flex-col gap-8">
@@ -27,6 +120,35 @@
</div>
</section>
<section>
<div>This is where things like history would go</div>
<section class="mb-8 rounded-lg px-4" style="background-color: rgba(82, 82, 82, 0.25);">
<h1 class="py-2 text-xl">Add Connection</h1>
<div class="flex flex-wrap gap-2 pb-4">
<button class="add-connection-button h-14 rounded-md" on:click={() => (newConnectionModal = JellyfinAuthBox)}>
<img src={JellyfinIcon} alt="Jellyfin icon" class="aspect-square h-full p-2" />
</button>
<form method="post" action="?/youtubeMusicLogin" use:enhance={authenticateYouTube}>
<button class="add-connection-button h-14 rounded-md">
<img src={YouTubeMusicIcon} alt="YouTube Music icon" class="aspect-square h-full p-2" />
</button>
</form>
</div>
</section>
<div id="connection-profile-grid" class="grid gap-8">
{#each connections as connection}
<ConnectionProfile {connection} submitFunction={profileActions} />
{/each}
</div>
{#if newConnectionModal !== null}
<svelte:component this={newConnectionModal} submitFunction={authenticateJellyfin} on:close={() => (newConnectionModal = null)} />
{/if}
</section>
</main>
<style>
.add-connection-button {
background-image: linear-gradient(to bottom, rgb(30, 30, 30), rgb(10, 10, 10));
}
#connection-profile-grid {
grid-template-columns: repeat(auto-fit, minmax(24rem, 1fr));
}
</style>

View File

@@ -0,0 +1,19 @@
import type { RequestHandler } from '@sveltejs/kit'
import { Connections } from '$lib/server/connections'
export const GET: RequestHandler = async ({ url }) => {
const query = url.searchParams.get('query')
if (!query) return new Response('Missing query parameter', { status: 400 })
const userId = url.searchParams.get('userId')
if (!userId) return new Response('Missing userId parameter', { status: 400 })
const searchResults: (Song | Album | Artist | Playlist)[] = []
for (const connection of Connections.getUserConnections(userId)) {
await connection
.search(query)
.then((results) => searchResults.push(...results))
.catch((reason) => console.log(`Failed to search "${query}" from connection ${connection.id}: ${reason}`))
}
return Response.json({ searchResults })
}

View File

@@ -1,130 +0,0 @@
<script lang="ts">
import Services from '$lib/services.json'
import JellyfinIcon from '$lib/static/jellyfin-icon.svg'
import YouTubeMusicIcon from '$lib/static/youtube-music-icon.svg'
import JellyfinAuthBox from './jellyfinAuthBox.svelte'
import { newestAlert } from '$lib/stores.js'
import type { PageServerData } from './$types.js'
import type { SubmitFunction } from '@sveltejs/kit'
import { getDeviceUUID } from '$lib/utils'
import { SvelteComponent, type ComponentType } from 'svelte'
import ConnectionProfile from './connectionProfile.svelte'
import { enhance } from '$app/forms'
import { PUBLIC_YOUTUBE_API_CLIENT_ID } from '$env/static/public'
export let data: PageServerData
let connections: ConnectionInfo[] = data.connections
const authenticateJellyfin: SubmitFunction = ({ formData, cancel }) => {
const { serverUrl, username, password } = Object.fromEntries(formData)
if (!(serverUrl && username && password)) {
$newestAlert = ['caution', 'All fields must be filled out']
return cancel()
}
try {
formData.set('serverUrl', new URL(serverUrl.toString()).origin)
} catch {
$newestAlert = ['caution', 'Server URL is invalid']
return cancel()
}
const deviceId = getDeviceUUID()
formData.append('deviceId', deviceId)
return ({ result }) => {
if (result.type === 'failure') {
return ($newestAlert = ['warning', result.data?.message])
} else if (result.type === 'success') {
const newConnection: ConnectionInfo = result.data!.newConnection
connections = [...connections, newConnection]
newConnectionModal = null
return ($newestAlert = ['success', `Added Jellyfin`])
}
}
}
const authenticateYouTube: SubmitFunction = async ({ formData, cancel }) => {
const googleLoginProcess = (): Promise<string> => {
return new Promise((resolve) => {
// @ts-ignore (google variable is a global variable imported by html script tag)
const client = google.accounts.oauth2.initCodeClient({
client_id: PUBLIC_YOUTUBE_API_CLIENT_ID,
scope: 'https://www.googleapis.com/auth/youtube',
ux_mode: 'popup',
callback: (response: any) => {
resolve(response.code)
},
})
client.requestCode()
})
}
const code = await googleLoginProcess()
if (!code) cancel()
formData.append('code', code)
return ({ result }) => {
if (result.type === 'failure') {
return ($newestAlert = ['warning', result.data?.message])
} else if (result.type === 'success') {
const newConnection: ConnectionInfo = result.data!.newConnection
connections = [...connections, newConnection]
return ($newestAlert = ['success', 'Added Youtube Music'])
}
}
}
const profileActions: SubmitFunction = ({ action, cancel }) => {
return ({ result }) => {
if (result.type === 'failure') {
return ($newestAlert = ['warning', result.data?.message])
} else if (result.type === 'success') {
const id = result.data!.deletedConnectionId
const indexToDelete = connections.findIndex((connection) => connection.id === id)
const serviceType = connections[indexToDelete].type
connections.splice(indexToDelete, 1)
connections = connections
return ($newestAlert = ['success', `Deleted ${Services[serviceType].displayName}`])
}
}
}
let newConnectionModal: ComponentType<SvelteComponent<{ submitFunction: SubmitFunction }>> | null = null
</script>
<main>
<section class="mb-8 rounded-lg px-4" style="background-color: rgba(82, 82, 82, 0.25);">
<h1 class="py-2 text-xl">Add Connection</h1>
<div class="flex flex-wrap gap-2 pb-4">
<button class="add-connection-button h-14 rounded-md" on:click={() => (newConnectionModal = JellyfinAuthBox)}>
<img src={JellyfinIcon} alt="Jellyfin icon" class="aspect-square h-full p-2" />
</button>
<form method="post" action="?/youtubeMusicLogin" use:enhance={authenticateYouTube}>
<button class="add-connection-button h-14 rounded-md">
<img src={YouTubeMusicIcon} alt="YouTube Music icon" class="aspect-square h-full p-2" />
</button>
</form>
</div>
</section>
<div id="connection-profile-grid" class="grid gap-8">
{#each connections as connection}
<ConnectionProfile {connection} submitFunction={profileActions} />
{/each}
</div>
{#if newConnectionModal !== null}
<svelte:component this={newConnectionModal} submitFunction={authenticateJellyfin} on:close={() => (newConnectionModal = null)} />
{/if}
</main>
<style>
.add-connection-button {
background-image: linear-gradient(to bottom, rgb(30, 30, 30), rgb(10, 10, 10));
}
#connection-profile-grid {
grid-template-columns: repeat(auto-fit, minmax(24rem, 1fr));
}
</style>