diff --git a/.vscode/extensions.json b/.vscode/extensions.json index fb52a8b..84cd9be 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -4,8 +4,9 @@ "dbaeumer.vscode-eslint", "GraphQL.vscode-graphql-syntax", "YoavBls.pretty-ts-errors", - "svelte.svelte-vscode", + "svelte.svelte-vscode", // 109.5.2 or older NOT NEWER "ardenivanov.svelte-intellisense", - "Gruntfuggly.todo-tree" + "Gruntfuggly.todo-tree", + "bradlc.vscode-tailwindcss" ] } \ No newline at end of file diff --git a/package.json b/package.json index 9e0fcc4..47e1cf2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ui", - "version": "6.3.15", + "version": "6.3.16", "license": "BUSL-1.1", "private": true, "packageManager": "pnpm@9.14.4", diff --git a/src/lib/components/ui/irc/irc.svelte b/src/lib/components/ui/irc/irc.svelte index 7e1a0fe..3e2da29 100644 --- a/src/lib/components/ui/irc/irc.svelte +++ b/src/lib/components/ui/irc/irc.svelte @@ -27,7 +27,7 @@ ident = { nick: 'Guest-' + crypto.randomUUID().slice(0, 6), id: crypto.randomUUID().slice(0, 6), pfpid: '0', type: 'guest' } } - if (!irc.value) irc.value = MessageClient.new(ident) + irc.value ??= MessageClient.new(ident) let message = '' let rows = 1 diff --git a/src/lib/components/ui/player/util.ts b/src/lib/components/ui/player/util.ts index 49f23ef..a94c57c 100644 --- a/src/lib/components/ui/player/util.ts +++ b/src/lib/components/ui/player/util.ts @@ -159,7 +159,7 @@ export function normalizeTracks (_tracks: Track[]) { } }) return lang.reduce>((acc, track) => { - if (!acc[track.language]) acc[track.language] = [] + acc[track.language] ??= [] acc[track.language]!.push(track) return acc }, {}) @@ -174,7 +174,7 @@ export function normalizeSubs (_tracks?: Record>((acc, track) => { - if (!acc[track.language]) acc[track.language] = [] + acc[track.language] ??= [] acc[track.language]!.push(track) return acc }, {}) diff --git a/src/lib/modules/auth/client.ts b/src/lib/modules/auth/client.ts index 1e295a8..fa292d7 100644 --- a/src/lib/modules/auth/client.ts +++ b/src/lib/modules/auth/client.ts @@ -19,7 +19,7 @@ export default new class AuthAggregator { return () => unsub.forEach(fn => fn()) }) - syncSettings = persisted('syncSettings', { al: true, local: true }) + syncSettings = persisted('syncSettings', { al: true, local: true, kitsu: true, mal: true }) // AUTH anilist () { diff --git a/src/lib/modules/auth/kitsu-types.d.ts b/src/lib/modules/auth/kitsu-types.d.ts new file mode 100644 index 0000000..7a914b6 --- /dev/null +++ b/src/lib/modules/auth/kitsu-types.d.ts @@ -0,0 +1,8 @@ +export interface KitsuOAuth { + access_token: string + created_at: number + expires_in: number // Seconds until the access_token expires (30 days default) + refresh_token: string + scope: string + token_type: string +} diff --git a/src/lib/modules/auth/kitsu.ts b/src/lib/modules/auth/kitsu.ts new file mode 100644 index 0000000..d9633eb --- /dev/null +++ b/src/lib/modules/auth/kitsu.ts @@ -0,0 +1,195 @@ +import { readable, writable } from 'simple-store-svelte' + +import type { Media } from '../anilist' +import type { KitsuOAuth } from './kitsu-types' +import type { Entry } from '../anilist/queries' +import type { VariablesOf } from 'gql.tada' + +import { safeLocalStorage } from '$lib/utils' + +const ENDPOINTS = { + API_OAUTH: 'https://kitsu.app/api/oauth/token', + API_USER_FETCH: 'https://kitsu.app/api/edge/users', + API_USER_LIBRARY: 'https://kitsu.app/api/edge/library-entries' +} + +export default new class KitsuSync { + auth = writable(safeLocalStorage('kitsuAuth')) + viewer = writable<{id: number} | undefined>(safeLocalStorage('kitsuViewer')) + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async _request (url: string, method: string, body?: any): Promise { + try { + const res = await fetch(url, { + method, + headers: { + 'Content-Type': 'application/vnd.api+json', + Accept: 'application/vnd.api+json', + Authorization: this.auth.value ? `Bearer ${this.auth.value.access_token}` : '' + }, + body: JSON.stringify(body) + }) + + if (res.status === 403 && !body?.refresh_token) { + await this._refresh() + return await this._request(url, method, body) + } + + return await res.json() + } catch (error) { + // TODO: :^) + const err = error as Error + throw err + } + } + + async _refresh () { + try { + const data = await this._request( + ENDPOINTS.API_OAUTH, + 'POST', + { + grant_type: 'refresh_token', + refresh_token: this.auth.value?.refresh_token + } + ) + this.viewer.value = data + await this._user() + } catch (error) { + this.viewer.value = undefined + } + } + + async _login (username: string, password: string) { + const data = await this._request( + ENDPOINTS.API_OAUTH, + 'POST', + { + grant_type: 'password', + username, + password + } + ) + + this.viewer.value = data + await this._user() + } + + async _user () { + const data = await this._request( + ENDPOINTS.API_USER_FETCH, + 'GET', + { 'filter[self]': true } + ) + + const [user] = data + + return user.id + } + + async _getEntry (id: number) { + const data = await this._request( + ENDPOINTS.API_USER_LIBRARY, + 'GET', + { + 'filter[animeId]': id, + 'filter[userId]': this.viewer.value?.id, + 'filter[kind]': 'anime' + } + ) + + const [anime] = data.data + + return anime + } + + async _addEntry (id: number, attributes: Record) { + const data = await this._request( + ENDPOINTS.API_USER_LIBRARY, + 'POST', + { + data: { + attributes: { + status: 'planned' + }, + relationships: { + anime: { + data: { + id, + type: 'anime' + } + }, + user: { + data: { + id: this.viewer.value?.id, + type: 'users' + } + } + }, + type: 'library-entries' + } + } + ) + + return data + } + + async _updateEntry (id: number, attributes: Record) { + const data = await this._request( + `${ENDPOINTS.API_USER_LIBRARY}/${id}`, + 'PATCH', { + data: { + id, + attributes, + type: 'library-entries' + } + } + ) + return data + } + + async _deleteEntry (id: number) { + this._request( + `${ENDPOINTS.API_USER_LIBRARY}/${id}`, + 'DELETE' + ) + } + + hasAuth = readable(false) + + id () { + return -1 + } + + profile () { + } + + // QUERIES/MUTATIONS + + schedule () { + } + + toggleFav (id: number) { + } + + delete (id: number) { + } + + following (id: number) { + } + + planningIDs () { + } + + continueIDs () { + } + + sequelIDs () { + } + + watch (media: Media, progress: number) { + } + + entry (variables: VariablesOf) { + } +}() diff --git a/src/patches/empty.cjs b/src/patches/empty.cjs index 4ba52ba..1576fcf 100644 --- a/src/patches/empty.cjs +++ b/src/patches/empty.cjs @@ -1 +1,2 @@ +// eslint-disable-next-line no-undef module.exports = {}