Changed the DB schema AGAIN
This commit is contained in:
15
src/app.d.ts
vendored
15
src/app.d.ts
vendored
@@ -29,6 +29,21 @@ declare global {
|
|||||||
|
|
||||||
type ServiceType = 'jellyfin' | 'youtube-music'
|
type ServiceType = 'jellyfin' | 'youtube-music'
|
||||||
|
|
||||||
|
interface Service {
|
||||||
|
type: ServiceType
|
||||||
|
userId: string
|
||||||
|
urlOrigin: string
|
||||||
|
username?: string
|
||||||
|
serverName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Connection {
|
||||||
|
id: string
|
||||||
|
user: User
|
||||||
|
service: Service
|
||||||
|
accessToken: string
|
||||||
|
}
|
||||||
|
|
||||||
interface MediaItem {
|
interface MediaItem {
|
||||||
connectionId: string
|
connectionId: string
|
||||||
serviceType: string
|
serviceType: string
|
||||||
|
|||||||
Binary file not shown.
@@ -1,50 +1,22 @@
|
|||||||
import Database from 'better-sqlite3'
|
import Database from 'better-sqlite3'
|
||||||
import { generateUUID } from '$lib/utils'
|
import { generateUUID } from '$lib/utils'
|
||||||
|
import { isValidURL } from '$lib/utils'
|
||||||
|
|
||||||
const db = new Database('./src/lib/server/users.db', { verbose: console.info })
|
const db = new Database('./src/lib/server/users.db', { verbose: console.info })
|
||||||
db.pragma('foreign_keys = ON')
|
db.pragma('foreign_keys = ON')
|
||||||
const initUsersTable = 'CREATE TABLE IF NOT EXISTS Users(id VARCHAR(36) PRIMARY KEY, username VARCHAR(30) UNIQUE NOT NULL, password VARCHAR(72) NOT NULL)'
|
const initUsersTable = 'CREATE TABLE IF NOT EXISTS Users(id VARCHAR(36) PRIMARY KEY, username VARCHAR(30) UNIQUE NOT NULL, password VARCHAR(72) NOT NULL)'
|
||||||
const initServicesTable = 'CREATE TABLE IF NOT EXISTS Services(id VARCHAR(36) PRIMARY KEY, type VARCHAR(64) NOT NULL, serviceUserId TEXT NOT NULL, url TEXT NOT NULL)'
|
const initConnectionsTable = 'CREATE TABLE IF NOT EXISTS Connections(id VARCHAR(36) PRIMARY KEY, userId VARCHAR(36) NOT NULL, service TEXT NOT NULL, accessToken TEXT NOT NULL, FOREIGN KEY(userId) REFERENCES Users(id))'
|
||||||
const initConnectionsTable =
|
db.exec(initUsersTable), db.exec(initConnectionsTable)
|
||||||
'CREATE TABLE IF NOT EXISTS Connections(id VARCHAR(36) PRIMARY KEY, userId VARCHAR(36) NOT NULL, serviceId VARCHAR(36), accessToken TEXT NOT NULL, refreshToken TEXT, expiry INTEGER, FOREIGN KEY(userId) REFERENCES Users(id), FOREIGN KEY(serviceId) REFERENCES Services(id))'
|
|
||||||
db.exec(initUsersTable)
|
|
||||||
db.exec(initServicesTable)
|
|
||||||
db.exec(initConnectionsTable)
|
|
||||||
|
|
||||||
type UserQueryParams = {
|
type UserQueryParams = {
|
||||||
includePassword?: boolean
|
includePassword?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DBServiceData {
|
interface ConnectionsTableSchema {
|
||||||
id: string
|
|
||||||
type: ServiceType
|
|
||||||
serviceUserId: string
|
|
||||||
url: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DBServiceRow {
|
|
||||||
id: string
|
|
||||||
type: string
|
|
||||||
serviceUserId: string
|
|
||||||
url: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DBConnectionData {
|
|
||||||
id: string
|
|
||||||
user: User
|
|
||||||
service: DBServiceData
|
|
||||||
accessToken: string
|
|
||||||
refreshToken: string | null
|
|
||||||
expiry: number | null
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DBConnectionRow {
|
|
||||||
id: string
|
id: string
|
||||||
userId: string
|
userId: string
|
||||||
serviceId: string
|
service: string
|
||||||
accessToken: string
|
accessToken: string
|
||||||
refreshToken: string | null
|
|
||||||
expiry: number | null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Users {
|
export class Users {
|
||||||
@@ -78,46 +50,28 @@ export class Users {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Services {
|
|
||||||
static getService = (id: string): DBServiceData => {
|
|
||||||
const { type, serviceUserId, url } = db.prepare('SELECT * FROM Users WHERE id = ?').get(id) as DBServiceRow
|
|
||||||
const service: DBServiceData = { id, type: type as ServiceType, serviceUserId, url }
|
|
||||||
return service
|
|
||||||
}
|
|
||||||
|
|
||||||
static addService = (type: ServiceType, serviceUserId: string, url: URL): DBServiceData => {
|
|
||||||
const serviceId = generateUUID()
|
|
||||||
db.prepare('INSERT INTO Services(id, type, serviceUserId, url) VALUES(?, ?, ?, ?)').run(serviceId, type, serviceUserId, url.origin)
|
|
||||||
return this.getService(serviceId)
|
|
||||||
}
|
|
||||||
|
|
||||||
static deleteService = (id: string): void => {
|
|
||||||
const commandInfo = db.prepare('DELETE FROM Services WHERE id = ?').run(id)
|
|
||||||
if (commandInfo.changes === 0) throw new Error(`Serivce with id ${id} does not exist`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Connections {
|
export class Connections {
|
||||||
static getConnection = (id: string): DBConnectionData => {
|
static getConnection = (id: string): Connection => {
|
||||||
const { userId, serviceId, accessToken, refreshToken, expiry } = db.prepare('SELECT * FROM Connections WHERE id = ?').get(id) as DBConnectionRow
|
const { userId, service, accessToken } = db.prepare('SELECT * FROM Connections WHERE id = ?').get(id) as ConnectionsTableSchema
|
||||||
const connection: DBConnectionData = { id, user: Users.getUser(userId)!, service: Services.getService(serviceId), accessToken, refreshToken, expiry }
|
const connection: Connection = { id, user: Users.getUser(userId)!, service: JSON.parse(service), accessToken }
|
||||||
return connection
|
return connection
|
||||||
}
|
}
|
||||||
|
|
||||||
static getUserConnections = (userId: string): DBConnectionData[] => {
|
static getUserConnections = (userId: string): Connection[] => {
|
||||||
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 ConnectionsTableSchema[]
|
||||||
const connections: DBConnectionData[] = []
|
const connections: Connection[] = []
|
||||||
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, service, accessToken } = row
|
||||||
connections.push({ id, user, service: Services.getService(serviceId), accessToken, refreshToken, expiry })
|
connections.push({ id, user, service: JSON.parse(service), accessToken })
|
||||||
})
|
})
|
||||||
return connections
|
return connections
|
||||||
}
|
}
|
||||||
|
|
||||||
static addConnection = (userId: string, serviceId: string, accessToken: string, refreshToken: string | null, expiry: number | null): DBConnectionData => {
|
static addConnection = (userId: string, service: Service, accessToken: string): Connection => {
|
||||||
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)
|
if (!isValidURL(service.urlOrigin)) throw new Error('Service does not have valid url')
|
||||||
|
db.prepare('INSERT INTO Connections(id, userId, service, accessToken) VALUES(?, ?, ?, ?)').run(connectionId, userId, JSON.stringify(service), accessToken)
|
||||||
return this.getConnection(connectionId)
|
return this.getConnection(connectionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Services, Connections } from '$lib/server/users'
|
import { Connections } from '$lib/server/users'
|
||||||
import { isValidURL } from '$lib/utils'
|
import { isValidURL } from '$lib/utils'
|
||||||
import type { RequestHandler } from '@sveltejs/kit'
|
import type { RequestHandler } from '@sveltejs/kit'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
@@ -13,10 +13,8 @@ export const GET: RequestHandler = async ({ params }) => {
|
|||||||
const connectionSchema = z.object({
|
const connectionSchema = z.object({
|
||||||
serviceType: z.enum(['jellyfin', 'youtube-music']),
|
serviceType: z.enum(['jellyfin', 'youtube-music']),
|
||||||
serviceUserId: z.string(),
|
serviceUserId: z.string(),
|
||||||
url: z.string().refine((val) => isValidURL(val)),
|
urlOrigin: z.string().refine((val) => isValidURL(val)),
|
||||||
accessToken: z.string(),
|
accessToken: z.string(),
|
||||||
refreshToken: z.string().nullable().optional(),
|
|
||||||
expiry: z.number().nullable().optional(),
|
|
||||||
})
|
})
|
||||||
export type NewConnection = z.infer<typeof connectionSchema>
|
export type NewConnection = z.infer<typeof connectionSchema>
|
||||||
|
|
||||||
@@ -28,9 +26,13 @@ export const POST: RequestHandler = async ({ params, request }) => {
|
|||||||
const connectionValidation = connectionSchema.safeParse(connection)
|
const connectionValidation = connectionSchema.safeParse(connection)
|
||||||
if (!connectionValidation.success) return new Response(connectionValidation.error.message, { status: 400 })
|
if (!connectionValidation.success) return new Response(connectionValidation.error.message, { status: 400 })
|
||||||
|
|
||||||
const { serviceType, serviceUserId, url, accessToken, refreshToken, expiry } = connectionValidation.data
|
const { serviceType, serviceUserId, urlOrigin, accessToken } = connectionValidation.data
|
||||||
const newService = Services.addService(serviceType as ServiceType, serviceUserId, new URL(url))
|
const service: Service = {
|
||||||
const newConnection = Connections.addConnection(userId, newService.id, accessToken, refreshToken as string | null, expiry as number | null)
|
type: serviceType,
|
||||||
|
userId: serviceUserId,
|
||||||
|
urlOrigin: new URL(urlOrigin).origin,
|
||||||
|
}
|
||||||
|
const newConnection = Connections.addConnection(userId, service, accessToken)
|
||||||
return new Response(JSON.stringify(newConnection))
|
return new Response(JSON.stringify(newConnection))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,10 +64,10 @@
|
|||||||
<input name="username" type="text" autocomplete="off" placeholder="Username" class="h-10 w-full border-b-2 border-lazuli-primary bg-transparent px-1 outline-none" />
|
<input name="username" type="text" autocomplete="off" placeholder="Username" class="h-10 w-full border-b-2 border-lazuli-primary bg-transparent px-1 outline-none" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-4 p-4">
|
<div class="flex items-center gap-4 p-4">
|
||||||
<div class="w-full">
|
<div class="w-full flex-shrink">
|
||||||
<input name="password" type={passwordVisible ? 'text' : 'password'} placeholder="Password" class="h-10 w-full border-b-2 border-lazuli-primary bg-transparent px-1 outline-none" />
|
<input name="password" type={passwordVisible ? 'text' : 'password'} placeholder="Password" class="h-10 w-full border-b-2 border-lazuli-primary bg-transparent px-1 outline-none" />
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-hidden transition-[width] duration-300" style="width: {formMode === 'newUser' ? '100%' : 0};" aria-hidden={formMode !== 'newUser'}>
|
<div class="flex-shrink overflow-hidden transition-[width] duration-300" style="width: {formMode === 'newUser' ? '100%' : 0};" aria-hidden={formMode !== 'newUser'}>
|
||||||
<input
|
<input
|
||||||
name="confirmPassword"
|
name="confirmPassword"
|
||||||
type={passwordVisible ? 'text' : 'password'}
|
type={passwordVisible ? 'text' : 'password'}
|
||||||
|
|||||||
@@ -1,22 +1,56 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import IconButton from '$lib/components/util/iconButton.svelte'
|
import IconButton from '$lib/components/util/iconButton.svelte'
|
||||||
|
import type { LayoutServerData } from '../$types'
|
||||||
|
|
||||||
|
export let data: LayoutServerData
|
||||||
|
|
||||||
|
interface SettingRoute {
|
||||||
|
pathname: string
|
||||||
|
displayName: string
|
||||||
|
icon: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountRoutes: SettingRoute[] = [
|
||||||
|
{
|
||||||
|
pathname: '/settings/connections',
|
||||||
|
displayName: 'Connections',
|
||||||
|
icon: 'fa-solid fa-circle-nodes',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pathname: '/settings/devices',
|
||||||
|
displayName: 'Devices',
|
||||||
|
icon: 'fa-solid fa-mobile-screen',
|
||||||
|
},
|
||||||
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main class="h-full">
|
<main class="grid h-full grid-rows-[min-content_auto] pb-12">
|
||||||
<h1 class="sticky top-0 grid grid-cols-[1fr_auto_1fr] grid-rows-1 items-center text-2xl">
|
<h1 class="sticky top-0 grid grid-cols-[1fr_auto_1fr] grid-rows-1 items-center p-6 text-2xl">
|
||||||
<IconButton on:click={() => history.back()}>
|
<span class="h-12">
|
||||||
<i slot="icon" class="fa-solid fa-arrow-left" />
|
<IconButton on:click={() => history.back()}>
|
||||||
</IconButton>
|
<i slot="icon" class="fa-solid fa-arrow-left" />
|
||||||
<span>Account</span>
|
</IconButton>
|
||||||
|
</span>
|
||||||
|
<span>Settings</span>
|
||||||
</h1>
|
</h1>
|
||||||
<section class="px-[5vw]">
|
<section class="grid grid-cols-[min-content_auto] grid-rows-1 gap-8 px-[5vw]">
|
||||||
|
<nav class="h-full">
|
||||||
|
<a class="whitespace-nowrap text-lg {data.url.pathname === '/settings' ? 'text-lazuli-primary' : 'text-neutral-400'}" href="/settings">
|
||||||
|
<i class="fa-solid fa-user mr-1 w-4 text-center" />
|
||||||
|
Account
|
||||||
|
</a>
|
||||||
|
<ol class="ml-2 mt-4 flex flex-col gap-3 border-2 border-transparent border-l-neutral-500 px-2">
|
||||||
|
{#each accountRoutes as route}
|
||||||
|
{@const isActive = route.pathname === data.url.pathname}
|
||||||
|
<li class="w-60 px-3 py-1">
|
||||||
|
<a class="whitespace-nowrap {isActive ? 'text-lazuli-primary' : 'text-neutral-400'}" href={route.pathname}>
|
||||||
|
<i class="{route.icon} mr-1 w-4 text-center" />
|
||||||
|
{route.displayName}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
<slot />
|
<slot />
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
|
||||||
h1 {
|
|
||||||
height: 80px;
|
|
||||||
padding: 16px 5vw;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,57 +1 @@
|
|||||||
<script lang="ts">
|
<h1>Main Settings Page</h1>
|
||||||
import IconButton from '$lib/components/util/iconButton.svelte'
|
|
||||||
import { goto } from '$app/navigation'
|
|
||||||
import type { LayoutServerData } from '../$types.js'
|
|
||||||
|
|
||||||
export let data: LayoutServerData
|
|
||||||
|
|
||||||
interface SettingRoute {
|
|
||||||
pathname: string
|
|
||||||
displayName: string
|
|
||||||
icon: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const settingRoutes: SettingRoute[] = [
|
|
||||||
{
|
|
||||||
pathname: '/settings/connections',
|
|
||||||
displayName: 'Connections',
|
|
||||||
icon: 'fa-solid fa-circle-nodes',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pathname: '/settings/devices',
|
|
||||||
displayName: 'Devices',
|
|
||||||
icon: 'fa-solid fa-mobile-screen',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<nav class="h-full rounded-lg bg-neutral-950 p-6">
|
|
||||||
<h1 class="flex h-6 justify-between text-neutral-400">
|
|
||||||
<span>
|
|
||||||
<i class="fa-solid fa-gear" />
|
|
||||||
Settings
|
|
||||||
</span>
|
|
||||||
{#if data.url.pathname.split('/').at(-1) !== 'settings'}
|
|
||||||
<IconButton on:click={() => goto('/settings')}>
|
|
||||||
<i slot="icon" class="fa-solid fa-caret-left" />
|
|
||||||
</IconButton>
|
|
||||||
{/if}
|
|
||||||
</h1>
|
|
||||||
<ol class="ml-2 mt-4 flex flex-col gap-3 border-2 border-transparent border-l-neutral-500 px-2">
|
|
||||||
{#each settingRoutes as route}
|
|
||||||
<li>
|
|
||||||
{#if data.url.pathname === route.pathname}
|
|
||||||
<div class="rounded-lg bg-neutral-500 px-3 py-1">
|
|
||||||
<i class={route.icon} />
|
|
||||||
{route.displayName}
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<a href={route.pathname} class="block rounded-lg px-3 py-1 opacity-50 hover:bg-neutral-700">
|
|
||||||
<i class={route.icon} />
|
|
||||||
{route.displayName}
|
|
||||||
</a>
|
|
||||||
{/if}
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ol>
|
|
||||||
</nav>
|
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import { fail } from '@sveltejs/kit'
|
import { fail } from '@sveltejs/kit'
|
||||||
import { SECRET_INTERNAL_API_KEY } from '$env/static/private'
|
import { SECRET_INTERNAL_API_KEY } from '$env/static/private'
|
||||||
import type { DBConnectionData } from '$lib/server/users'
|
|
||||||
import type { NewConnection } from '../../api/users/[userId]/connections/+server'
|
import type { NewConnection } from '../../api/users/[userId]/connections/+server'
|
||||||
import type { PageServerLoad, Actions } from './$types'
|
import type { PageServerLoad, Actions } from './$types'
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ fetch, locals }) => {
|
export const load: PageServerLoad = async ({ fetch, locals }) => {
|
||||||
const connectionsResponse = await fetch(`/api/user/${locals.user.id}/connections`, {
|
const connectionsResponse = await fetch(`/api/users/${locals.user.id}/connections`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: { apikey: SECRET_INTERNAL_API_KEY },
|
headers: { apikey: SECRET_INTERNAL_API_KEY },
|
||||||
})
|
})
|
||||||
|
|
||||||
const userConnections: DBConnectionData[] = await connectionsResponse.json()
|
const userConnections: Connection[] = await connectionsResponse.json()
|
||||||
return { userConnections }
|
return { userConnections }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,7 +31,7 @@ export const actions: Actions = {
|
|||||||
|
|
||||||
const authData: Jellyfin.AuthData = await jellyfinAuthResponse.json()
|
const authData: Jellyfin.AuthData = await jellyfinAuthResponse.json()
|
||||||
const newConnectionPayload: NewConnection = {
|
const newConnectionPayload: NewConnection = {
|
||||||
url: serverUrl.toString(),
|
urlOrigin: serverUrl.toString(),
|
||||||
serviceType: 'jellyfin',
|
serviceType: 'jellyfin',
|
||||||
serviceUserId: authData.User.Id,
|
serviceUserId: authData.User.Id,
|
||||||
accessToken: authData.AccessToken,
|
accessToken: authData.AccessToken,
|
||||||
@@ -45,7 +44,21 @@ export const actions: Actions = {
|
|||||||
|
|
||||||
if (!newConnectionResponse.ok) return fail(500, { message: 'Internal Server Error' })
|
if (!newConnectionResponse.ok) return fail(500, { message: 'Internal Server Error' })
|
||||||
|
|
||||||
const newConnection: DBConnectionData = await newConnectionResponse.json()
|
const newConnection: Connection = await newConnectionResponse.json()
|
||||||
return { newConnection }
|
return { newConnection }
|
||||||
},
|
},
|
||||||
|
deleteConnection: async ({ request, fetch, locals }) => {
|
||||||
|
const formData = await request.formData()
|
||||||
|
const connectionId = formData.get('connectionId')
|
||||||
|
|
||||||
|
const deleteConnectionResponse = await fetch(`/api/users/${locals.user.id}/connections`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { apikey: SECRET_INTERNAL_API_KEY },
|
||||||
|
body: JSON.stringify({ connectionId }),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!deleteConnectionResponse.ok) return fail(500, { message: 'Internal Server Error' })
|
||||||
|
|
||||||
|
return { deletedConnectionId: connectionId }
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,14 @@
|
|||||||
import { fly } from 'svelte/transition'
|
import { fly } from 'svelte/transition'
|
||||||
import Services from '$lib/services.json'
|
import Services from '$lib/services.json'
|
||||||
import JellyfinAuthBox from './jellyfinAuthBox.svelte'
|
import JellyfinAuthBox from './jellyfinAuthBox.svelte'
|
||||||
import { newestAlert } from '$lib/stores'
|
import { newestAlert } from '$lib/stores.js'
|
||||||
import IconButton from '$lib/components/util/iconButton.svelte'
|
import IconButton from '$lib/components/util/iconButton.svelte'
|
||||||
import Toggle from '$lib/components/util/toggle.svelte'
|
import Toggle from '$lib/components/util/toggle.svelte'
|
||||||
|
import type { PageServerData } from './$types.js'
|
||||||
import type { SubmitFunction } from '@sveltejs/kit'
|
import type { SubmitFunction } from '@sveltejs/kit'
|
||||||
|
|
||||||
export let data
|
export let data: PageServerData
|
||||||
let connectionProfiles = data.connectionProfiles
|
let connections = data.userConnections
|
||||||
|
|
||||||
const submitCredentials: SubmitFunction = ({ formData, action, cancel }) => {
|
const submitCredentials: SubmitFunction = ({ formData, action, cancel }) => {
|
||||||
switch (action.search) {
|
switch (action.search) {
|
||||||
@@ -39,32 +40,26 @@
|
|||||||
return async ({ result }) => {
|
return async ({ result }) => {
|
||||||
switch (result.type) {
|
switch (result.type) {
|
||||||
case 'failure':
|
case 'failure':
|
||||||
$newestAlert = ['warning', result.data.message]
|
return ($newestAlert = ['warning', result.data?.message])
|
||||||
return
|
|
||||||
case 'success':
|
case 'success':
|
||||||
modal = null
|
|
||||||
if (result.data?.newConnection) {
|
if (result.data?.newConnection) {
|
||||||
const newConnection = result.data.newConnection
|
const newConnection: Connection = result.data.newConnection
|
||||||
connectionProfiles = [newConnection, ...connectionProfiles]
|
connections = [newConnection, ...connections]
|
||||||
|
|
||||||
$newestAlert = ['success', `Added ${Services[newConnection.serviceType].displayName}`]
|
return ($newestAlert = ['success', `Added ${Services[newConnection.service.type].displayName}`])
|
||||||
return
|
|
||||||
} else if (result.data?.deletedConnectionId) {
|
} else if (result.data?.deletedConnectionId) {
|
||||||
const id = result.data.deletedConnectionId
|
const id = result.data.deletedConnectionId
|
||||||
const indexToDelete = connectionProfiles.findIndex((profile) => profile.connectionId === id)
|
const indexToDelete = connections.findIndex((connection) => connection.id === id)
|
||||||
const serviceType = connectionProfiles[indexToDelete].serviceType
|
const serviceType = connections[indexToDelete].service.type
|
||||||
|
|
||||||
connectionProfiles.splice(indexToDelete, 1)
|
connections.splice(indexToDelete, 1)
|
||||||
connectionProfiles = connectionProfiles
|
connections = connections
|
||||||
|
|
||||||
$newestAlert = ['success', `Deleted ${Services[serviceType].displayName}`]
|
return ($newestAlert = ['success', `Deleted ${Services[serviceType].displayName}`])
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let modal
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
@@ -72,35 +67,29 @@
|
|||||||
<h1 class="py-2 text-xl">Add Connection</h1>
|
<h1 class="py-2 text-xl">Add Connection</h1>
|
||||||
<div class="flex flex-wrap gap-2 pb-4">
|
<div class="flex flex-wrap gap-2 pb-4">
|
||||||
{#each Object.entries(Services) as [serviceType, serviceData]}
|
{#each Object.entries(Services) as [serviceType, serviceData]}
|
||||||
<button
|
<button class="bg-ne h-14 rounded-md" style="background-image: linear-gradient(to bottom, rgb(30, 30, 30), rgb(10, 10, 10));">
|
||||||
class="bg-ne h-14 rounded-md"
|
|
||||||
style="background-image: linear-gradient(to bottom, rgb(30, 30, 30), rgb(10, 10, 10));"
|
|
||||||
on:click={() => {
|
|
||||||
if (serviceType === 'jellyfin') modal = JellyfinAuthBox
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img src={serviceData.icon} alt="{serviceData.displayName} icon" class="aspect-square h-full p-2" />
|
<img src={serviceData.icon} alt="{serviceData.displayName} icon" class="aspect-square h-full p-2" />
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<div class="grid gap-8">
|
<div class="grid gap-8">
|
||||||
{#each connectionProfiles as connectionProfile}
|
{#each connections as connection}
|
||||||
{@const serviceData = Services[connectionProfile.serviceType]}
|
{@const serviceData = Services[connection.service.type]}
|
||||||
<section class="overflow-hidden rounded-lg" style="background-color: rgba(82, 82, 82, 0.25);" transition:fly={{ x: 50 }}>
|
<section class="overflow-hidden rounded-lg" style="background-color: rgba(82, 82, 82, 0.25);" transition:fly={{ x: 50 }}>
|
||||||
<header class="flex h-20 items-center gap-4 p-4">
|
<header class="flex h-20 items-center gap-4 p-4">
|
||||||
<img src={serviceData.icon} alt="{serviceData.displayName} icon" class="aspect-square h-full p-1" />
|
<img src={serviceData.icon} alt="{serviceData.displayName} icon" class="aspect-square h-full p-1" />
|
||||||
<div>
|
<div>
|
||||||
<div>{connectionProfile?.username ? connectionProfile.username : 'Placeholder Account Name'}</div>
|
<div>{connection.service?.username ? connection.service.username : 'Placeholder Account Name'}</div>
|
||||||
<div class="text-sm text-neutral-500">
|
<div class="text-sm text-neutral-500">
|
||||||
{serviceData.displayName}
|
{serviceData.displayName}
|
||||||
{#if connectionProfile.serviceType === 'jellyfin' && connectionProfile?.serverName}
|
{#if connection.service.type === 'jellyfin' && connection.service?.serverName}
|
||||||
- {connectionProfile.serverName}
|
- {connection.service.serverName}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-auto h-8">
|
<div class="ml-auto h-8">
|
||||||
<IconButton on:click={() => (modal = `delete-${connectionProfile.connectionId}`)}>
|
<IconButton>
|
||||||
<i slot="icon" class="fa-solid fa-link-slash" />
|
<i slot="icon" class="fa-solid fa-link-slash" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
@@ -115,12 +104,12 @@
|
|||||||
</section>
|
</section>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{#if modal}
|
<!-- {#if modal}
|
||||||
<form method="post" use:enhance={submitCredentials} transition:fly={{ y: -15 }} class="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
|
<form method="post" use:enhance={submitCredentials} transition:fly={{ y: -15 }} class="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||||
{#if typeof modal === 'string'}
|
{#if typeof modal === 'string'}
|
||||||
{@const connectionId = modal.replace('delete-', '')}
|
{@const connectionId = modal.replace('delete-', '')}
|
||||||
{@const connection = connectionProfiles.find((profile) => profile.connectionId === connectionId)}
|
{@const connection = connections.find((connection) => connection.id === connectionId)}
|
||||||
{@const serviceData = Services[connection.serviceType]}
|
{@const serviceData = Services[connection.service.type]}
|
||||||
<div class="rounded-lg bg-neutral-900 p-5">
|
<div class="rounded-lg bg-neutral-900 p-5">
|
||||||
<h1 class="pb-4 text-center">Delete {serviceData.displayName} connection?</h1>
|
<h1 class="pb-4 text-center">Delete {serviceData.displayName} connection?</h1>
|
||||||
<div class="flex w-60 justify-around">
|
<div class="flex w-60 justify-around">
|
||||||
@@ -133,5 +122,5 @@
|
|||||||
<svelte:component this={modal} on:close={() => (modal = null)} />
|
<svelte:component this={modal} on:close={() => (modal = null)} />
|
||||||
{/if}
|
{/if}
|
||||||
</form>
|
</form>
|
||||||
{/if}
|
{/if} -->
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
Reference in New Issue
Block a user