Moved connections to user page, began search functionality
This commit is contained in:
@@ -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 • {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">
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
19
src/routes/api/search/+server.ts
Normal file
19
src/routes/api/search/+server.ts
Normal 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 })
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user