diff --git a/backend/src/user/user.controller.ts b/backend/src/user/user.controller.ts index a31f6b3..03a6a2e 100644 --- a/backend/src/user/user.controller.ts +++ b/backend/src/user/user.controller.ts @@ -7,13 +7,14 @@ import { NotFoundException, Param, Post, + Put, UseGuards, } from '@nestjs/common'; import { UserService } from './user.service'; import { AuthGuard, GetUser } from '../auth/auth.guard'; import { ApiNotFoundResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger'; -import { CreateUserDto, UserDto } from './user.dto'; -import { User } from './user.entity'; +import { CreateUserDto, UpdateUserDto, UserDto } from './user.dto'; +import { Settings, User } from './user.entity'; import { ApiException } from '@nanogiants/nestjs-swagger-api-exception-decorator'; @ApiTags('user') @@ -72,4 +73,25 @@ export class UserController { return UserDto.fromEntity(user); } + + @UseGuards(AuthGuard) + @Put(':id') + @ApiOkResponse({ description: 'User updated', type: UserDto }) + async updateUser( + @Param('id') id: string, + @Body() updateUserDto: UpdateUserDto, + @GetUser() callerUser: User, + ): Promise { + if ((!callerUser.isAdmin && callerUser.id !== id) || !id) { + throw new NotFoundException(); + } + + const user = await this.userService.findOne(id); + if (updateUserDto.settings) user.settings = updateUserDto.settings; + if (updateUserDto.onboardingDone) + user.onboardingDone = updateUserDto.onboardingDone; + + const updated = await this.userService.update(user); + return UserDto.fromEntity(updated); + } } diff --git a/backend/src/user/user.dto.ts b/backend/src/user/user.dto.ts index 71343f7..d9e42ef 100644 --- a/backend/src/user/user.dto.ts +++ b/backend/src/user/user.dto.ts @@ -1,4 +1,4 @@ -import { OmitType, PickType } from '@nestjs/swagger'; +import { OmitType, PartialType, PickType } from '@nestjs/swagger'; import { User } from './user.entity'; export class UserDto extends OmitType(User, ['password'] as const) { @@ -8,6 +8,7 @@ export class UserDto extends OmitType(User, ['password'] as const) { name: entity.name, isAdmin: entity.isAdmin, settings: entity.settings, + onboardingDone: entity.onboardingDone, }; } } @@ -18,6 +19,8 @@ export class CreateUserDto extends PickType(User, [ 'isAdmin', ] as const) {} -export class UpdateUserDto extends OmitType(User, ['id'] as const) {} +export class UpdateUserDto extends PartialType( + PickType(User, ['settings', 'onboardingDone', 'name'] as const), +) {} export class SignInDto extends PickType(User, ['name', 'password'] as const) {} diff --git a/backend/src/user/user.entity.ts b/backend/src/user/user.entity.ts index 2d89ef5..c3eb724 100644 --- a/backend/src/user/user.entity.ts +++ b/backend/src/user/user.entity.ts @@ -112,9 +112,13 @@ export class User { password: string; @ApiProperty({ required: true }) - @Column() + @Column({ default: false }) isAdmin: boolean = false; + @ApiProperty({ required: false }) + @Column({ default: false }) + onboardingDone: boolean = false; + @ApiProperty({ required: true, type: Settings }) @Column('json', { default: JSON.stringify(DEFAULT_SETTINGS) }) settings = DEFAULT_SETTINGS; diff --git a/src/App.svelte b/src/App.svelte index ab215f8..68a27e6 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -9,6 +9,7 @@ import StackRouter from './lib/components/StackRouter/StackRouter.svelte'; import { defaultStackRouter } from './lib/components/StackRouter/StackRouter'; import Sidebar from './lib/components/Sidebar/Sidebar.svelte'; + import OnboardingPage from './lib/pages/OnboardingPage.svelte'; appState.subscribe((s) => console.log('appState', s)); @@ -40,6 +41,8 @@ {:else if $appState.user === null} + {:else if $appState.user.onboardingDone === false} + {:else} diff --git a/src/app.css b/src/app.css index 1dd458d..898b443 100644 --- a/src/app.css +++ b/src/app.css @@ -96,6 +96,10 @@ html[data-useragent*="Tizen"] .selectable-secondary { @apply font-semibold text-4xl text-secondary-100 tracking-wider; } +.body { + @apply text-base text-secondary-200; +} + @media tv { html { font-size: 24px; diff --git a/src/lib/apis/jellyfin/jellyfin-api.ts b/src/lib/apis/jellyfin/jellyfin-api.ts index 306b9ab..6f4d898 100644 --- a/src/lib/apis/jellyfin/jellyfin-api.ts +++ b/src/lib/apis/jellyfin/jellyfin-api.ts @@ -8,6 +8,7 @@ import axios from 'axios'; import { log } from '../../utils'; export type JellyfinItem = components['schemas']['BaseItemDto']; +export type JellyfinUser = components['schemas']['UserDto']; type Type = 'movie' | 'series'; @@ -503,7 +504,7 @@ export class JellyfinApi implements Api { getJellyfinUsers = async ( baseUrl: string | undefined = undefined, apiKey: string | undefined = undefined - ): Promise => + ): Promise => axios .get((baseUrl || this.getBaseUrl()) + '/Users', { headers: { diff --git a/src/lib/apis/radarr/radarr-api.ts b/src/lib/apis/radarr/radarr-api.ts index fcf1903..be8310b 100644 --- a/src/lib/apis/radarr/radarr-api.ts +++ b/src/lib/apis/radarr/radarr-api.ts @@ -212,7 +212,7 @@ export class RadarrApi implements Api { }) .then((res) => res.response.ok) || Promise.resolve(false); - getRadarrHealth = async ( + getHealth = async ( baseUrl: string | undefined = undefined, apiKey: string | undefined = undefined ) => @@ -222,8 +222,7 @@ export class RadarrApi implements Api { 'X-Api-Key': apiKey || this.getSettings()?.apiKey } }) - .then((res) => res.status === 200) - .catch(() => false); + .catch((e) => e.response); getRootFolders = async ( baseUrl: string | undefined = undefined, diff --git a/src/lib/apis/reiverr/reiverr-api.ts b/src/lib/apis/reiverr/reiverr-api.ts index c9d1e04..823512a 100644 --- a/src/lib/apis/reiverr/reiverr-api.ts +++ b/src/lib/apis/reiverr/reiverr-api.ts @@ -5,6 +5,7 @@ import { appState } from '../../stores/app-state.store'; import type { Api } from '../api.interface'; export type ReiverrUser = components['schemas']['UserDto']; +export type ReiverrSettings = ReiverrUser['settings']; export class ReiverrApi implements Api { getClient(basePath?: string, _token?: string) { @@ -33,6 +34,18 @@ export class ReiverrApi implements Api { } }); } + + updateUser = (user: ReiverrUser) => + this.getClient() + ?.PUT('/user/{id}', { + params: { + path: { + id: get(appState).user?.id as string + } + }, + body: user + }) + .then((res) => res.data); } export const reiverrApi = new ReiverrApi(); diff --git a/src/lib/apis/reiverr/reiverr.generated.d.ts b/src/lib/apis/reiverr/reiverr.generated.d.ts index 629eeb0..b17f89c 100644 --- a/src/lib/apis/reiverr/reiverr.generated.d.ts +++ b/src/lib/apis/reiverr/reiverr.generated.d.ts @@ -5,17 +5,18 @@ export interface paths { - "/api/user": { + "/user": { get: operations["UserController_getProfile"]; post: operations["UserController_create"]; }; - "/api/user/{id}": { + "/user/{id}": { get: operations["UserController_findById"]; + put: operations["UserController_updateUser"]; }; - "/api/auth": { + "/auth": { post: operations["AuthController_signIn"]; }; - "/api": { + "/": { get: operations["AppController_getHello"]; }; } @@ -59,6 +60,7 @@ export interface components { id: string; name: string; isAdmin: boolean; + onboardingDone?: boolean; settings: components["schemas"]["Settings"]; }; CreateUserDto: { @@ -66,6 +68,11 @@ export interface components { password: string; isAdmin: boolean; }; + UpdateUserDto: { + name?: string; + onboardingDone?: boolean; + settings?: components["schemas"]["Settings"]; + }; SignInDto: { name: string; password: string; @@ -148,6 +155,26 @@ export interface operations { }; }; }; + UserController_updateUser: { + parameters: { + path: { + id: string; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateUserDto"]; + }; + }; + responses: { + /** @description User updated */ + 200: { + content: { + "application/json": components["schemas"]["UserDto"]; + }; + }; + }; + }; AuthController_signIn: { requestBody: { content: { diff --git a/src/lib/apis/sonarr/sonarr-api.ts b/src/lib/apis/sonarr/sonarr-api.ts index f7554eb..e43e276 100644 --- a/src/lib/apis/sonarr/sonarr-api.ts +++ b/src/lib/apis/sonarr/sonarr-api.ts @@ -393,7 +393,7 @@ export class SonarrApi implements ApiAsync { // })); // }; - getSonarrHealth = async ( + getHealth = async ( baseUrl: string | undefined = undefined, apiKey: string | undefined = undefined ) => @@ -403,8 +403,7 @@ export class SonarrApi implements ApiAsync { 'X-Api-Key': apiKey || this.getApiKey() } }) - .then((res) => res.status === 200) - .catch(() => false); + .catch((e) => e.response); _getSonarrRootFolders = async ( baseUrl: string | undefined = undefined, diff --git a/src/lib/apis/tmdb/tmdb-api.ts b/src/lib/apis/tmdb/tmdb-api.ts index 9b328a1..0c27552 100644 --- a/src/lib/apis/tmdb/tmdb-api.ts +++ b/src/lib/apis/tmdb/tmdb-api.ts @@ -1,6 +1,7 @@ import createClient from 'openapi-fetch'; import { get } from 'svelte/store'; import type { operations, paths } from './tmdb.generated'; +import type { operations as operations4, paths as paths4 } from './tmdb4.generated'; import { TMDB_API_KEY, TMDB_BACKDROP_SMALL } from '../../constants'; import { settings } from '../../stores/settings.store'; import type { TitleType } from '../../types'; @@ -62,10 +63,23 @@ export class TmdbApi implements Api { }); } + static getClient4() { + return createClient({ + baseUrl: 'https://api.themoviedb.org', + headers: { + Authorization: `Bearer ${TMDB_API_KEY}` + } + }); + } + getClient() { return TmdbApi.getClient(); } + getClient4() { + return TmdbApi.getClient4(); + } + getSessionId() { return get(appState)?.user?.settings.tmdb.sessionId; } @@ -248,8 +262,7 @@ export class TmdbApi implements Api { const top100: TmdbMovieSmall[] = await Promise.all( [...Array(5).keys()].map((i) => - this.getClient() - // @ts-ignore + this.getClient4() ?.GET('/4/account/{account_object_id}/movie/recommendations', { params: { path: { @@ -324,8 +337,7 @@ export class TmdbApi implements Api { const top100: TmdbSeriesSmall[] = await Promise.all( [...Array(5).keys()].map((i) => - this.getClient() - // @ts-ignore + this.getClient4() ?.GET('/4/account/{account_object_id}/tv/recommendations', { params: { path: { @@ -377,6 +389,36 @@ export class TmdbApi implements Api { mostPopular }; }; + + getConnectAccountLink = () => + this.getClient4() + ?.POST('/4/auth/request_token', {}) + .then((res) => res.data); + + getAccountAccessToken = (requestToken: string) => + this.getClient4() + ?.POST('/4/auth/access_token', { + body: { + // @ts-ignore + request_token: requestToken + } + }) + .then((res) => res.data); + + getAccountDetails = () => { + const userId = this.getUserId(); + if (!userId) return undefined; + + return this.getClient() + ?.GET('/3/account/{account_id}', { + params: { + path: { + account_id: Number(userId) + } + } + }) + .then((res) => res.data); + }; } export const tmdbApi = new TmdbApi(); diff --git a/src/lib/apis/tmdb/tmdb4.generated.d.ts b/src/lib/apis/tmdb/tmdb4.generated.d.ts new file mode 100644 index 0000000..c30df0f --- /dev/null +++ b/src/lib/apis/tmdb/tmdb4.generated.d.ts @@ -0,0 +1,1592 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + + +export interface paths { + "": { + /** Getting Started */ + post: operations["getting-started"]; + }; + "/4/auth/request_token": { + /** Create Request Token */ + post: operations["auth-create-request-token"]; + }; + "/4/auth/access_token": { + /** Create Access Token */ + post: operations["auth-create-access-token"]; + /** + * Logout + * @description Log out of a session. + */ + delete: operations["auth-logout"]; + }; + "/4/list/{list_id}": { + /** + * Details + * @description Retrieve a list by id. + */ + get: operations["list-details"]; + /** + * Update + * @description Update the details of a list. + */ + put: operations["list-update"]; + }; + "/4/list": { + /** + * Create + * @description Create a new list. + */ + post: operations["list-create"]; + }; + "/4/list/{list_id}/clear": { + /** + * Clear + * @description Clear all of the items on a list. + */ + get: operations["list-clear"]; + }; + "/4/{list_id}": { + /** + * Delete + * @description Delete a list. + */ + delete: operations["list-delete"]; + }; + "/4/list/{list_id}/items": { + /** + * Update Items + * @description Update an individual item on a list + */ + put: operations["list-update-items"]; + /** + * Add Items + * @description Add items to a list. + */ + post: operations["list-add-items"]; + /** + * Remove Items + * @description Remove items from a list + */ + delete: operations["list-remove-items"]; + }; + "/4/list/{list_id}/item_status": { + /** + * Item Status + * @description Check if an item is on a list. + */ + get: operations["list-item-status"]; + }; + "/4/account/{account_object_id}/lists": { + /** + * Lists + * @description Get all of the lists you've created. + */ + get: operations["account-lists"]; + }; + "/4/account/{account_object_id}/movie/favorites": { + /** + * Favorite Movies + * @description Get a user's list of favourite movies. + */ + get: operations["account-favorite-movies"]; + }; + "/4/account/{account_object_id}/tv/favorites": { + /** + * Favorite TV Shows + * @description Get a user's list of favourite TV shows. + */ + get: operations["account-favorite-tv"]; + }; + "/4/account/{account_object_id}/tv/recommendations": { + /** + * Recommended TV Shows + * @description Get a user's list of recommended TV shows. + */ + get: operations["account-tv-recommendations"]; + }; + "/4/account/{account_object_id}/movie/recommendations": { + /** + * Recommended Movies + * @description Get a user's list of recommended movies. + */ + get: operations["account-movie-recommendations"]; + }; + "/4/account/{account_object_id}/movie/watchlist": { + /** + * Watchlist Movies + * @description Get a user's movie watchlist. + */ + get: operations["account-movie-watchlist"]; + }; + "/4/account/{account_object_id}/tv/watchlist": { + /** + * Watchlist TV Shows + * @description Get a user's TV watchlist. + */ + get: operations["account-tv-watchlist"]; + }; + "/4/account/{account_object_id}/movie/rated": { + /** + * Rated Movies + * @description Get a user's rated movies. + */ + get: operations["account-rated-movies"]; + }; + "/4/account/{account_object_id}/tv/rated": { + /** + * Rated TV Shows + * @description Get a user's rated TV shows. + */ + get: operations["account-rated-tv"]; + }; +} + +export type webhooks = Record; + +export interface components { + schemas: never; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} + +export type $defs = Record; + +export type external = Record; + +export interface operations { + + /** Getting Started */ + "getting-started": { + }; + /** Create Request Token */ + "auth-create-request-token": { + requestBody?: { + content: { + "application/json": { + /** Format: json */ + RAW_BODY: string; + }; + }; + }; + responses: { + /** @description 200 */ + 200: { + content: { + "application/json": { + /** @example Success. */ + status_message?: string; + /** @example eyJhbGciOiJIfsISNiIaInR5cCI6IkpXVCJ9.eyJuYmYiOjE0NzIwNTQ1ODEsInZlcnNpb24iOjEsImV4zCI6MTQ3MjA1NTQ4MSwiYXXkIjoiM2Y4Nzg1N2JlMjA5ZDM1MTk4MzNiMzAwYTEzZDBlMqIiLCJzY29wZXMiOlsicGVuZGluZ19yZXF1ZXN0X3Rva2VuIl0sImp0aSI6Nd0.e0t83AUvwywXPBb-hSAY_J_y4TjcwA0w98GhCCQM1dA */ + request_token?: string; + /** + * @default true + * @example true + */ + success?: boolean; + /** + * @default 0 + * @example 1 + */ + status_code?: number; + }; + }; + }; + }; + }; + /** Create Access Token */ + "auth-create-access-token": { + requestBody?: { + content: { + "application/json": { + /** Format: json */ + RAW_BODY: string; + }; + }; + }; + responses: { + /** @description 200 */ + 200: { + content: { + "application/json": { + /** @example 4bc8892a017a3c0z92001001 */ + account_id?: string; + /** @example eyJhbGciOiJIUzI1NiIsInR5cCIdIkpXVCJ9.eyJuYmYiOjE0ODM1NzM4MzUsInZlcnNpb24iOjEsInN1YiI6IjRiYzg4OTJhMDE3YTNjMGY5MjAwMDAwMiIsImF1ZCI6IlNmODc4NTdiZTIwOWQzNTE5ODMzYjMwMGExM2QwZTEyIiwic2NvcGVzIjpbImFwaV9yZWFkIiwiYXBpX3dyaXRlIl0sImp0aSI6Ijg4In0.b76OiEs10gdp9oNOoGpBJ94nO9Zi17Y7SvAXJQW8nH2 */ + access_token?: string; + /** + * @default true + * @example true + */ + success?: boolean; + /** @example Success. */ + status_message?: string; + /** + * @default 0 + * @example 1 + */ + status_code?: number; + }; + }; + }; + }; + }; + /** + * Logout + * @description Log out of a session. + */ + "auth-logout": { + requestBody?: { + content: { + "application/json": { + /** Format: json */ + RAW_BODY: string; + }; + }; + }; + responses: { + /** @description 200 */ + 200: { + content: { + "application/json": { + /** @example The item/record was deleted successfully. */ + status_message?: string; + /** + * @default true + * @example true + */ + success?: boolean; + /** + * @default 0 + * @example 13 + */ + status_code?: number; + }; + }; + }; + }; + }; + /** + * Details + * @description Retrieve a list by id. + */ + "list-details": { + parameters: { + query?: { + language?: string; + page?: number; + }; + path: { + list_id: number; + }; + }; + responses: { + /** @description 200 */ + 200: { + content: { + "application/json": { + /** + * @default 0 + * @example 6.7 + */ + average_rating?: number; + /** @example /kaIfm5ryEOwYg8mLbq8HkPuM1Fo.jpg */ + backdrop_path?: string; + results?: { + /** + * @default true + * @example false + */ + adult?: boolean; + /** @example /hFtJz4TvoiJJcw2ZOMdhK22aU9P.jpg */ + backdrop_path?: string; + /** + * @default 0 + * @example 617127 + */ + id?: number; + /** @example Blade */ + title?: string; + /** @example en */ + original_language?: string; + /** @example Blade */ + original_title?: string; + /** @example A film set in the Marvel Cinematic Universe (MCU) based on the Marvel Comics character of the same name. */ + overview?: string; + /** @example /fKqA4rgVJwrM7Gb3tQ9TGHnu8Tr.jpg */ + poster_path?: string; + /** @example movie */ + media_type?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 20.856 + */ + popularity?: number; + /** @example 2025-02-12 */ + release_date?: string; + /** + * @default true + * @example false + */ + video?: boolean; + /** + * @default 0 + * @example 0 + */ + vote_average?: number; + /** + * @default 0 + * @example 0 + */ + vote_count?: number; + }[]; + comments?: { + "movie:617127"?: unknown; + "movie:986056"?: unknown; + "movie:822119"?: unknown; + "movie:533535"?: unknown; + "movie:609681"?: unknown; + "movie:447365"?: unknown; + "movie:640146"?: unknown; + "movie:505642"?: unknown; + "movie:616037"?: unknown; + "movie:453395"?: unknown; + "movie:634649"?: unknown; + "movie:524434"?: unknown; + "movie:566525"?: unknown; + "movie:497698"?: unknown; + "movie:429617"?: unknown; + "movie:299534"?: unknown; + "movie:299537"?: unknown; + "movie:363088"?: unknown; + "movie:299536"?: unknown; + "movie:284054"?: unknown; + }; + created_by?: { + /** @example /xy44UvpbTgzs9kWmp4C3fEaCl5h.png */ + avatar_path?: string; + /** @example c9e9fc152ee756a900db85757c29815d */ + gravatar_hash?: string; + /** @example 4bc8892a017a3c0f92000002 */ + id?: string; + /** @example Travis Bell */ + name?: string; + /** @example travisbell */ + username?: string; + }; + /** @example The idea behind this list is to collect the live action comic book movies from within the Marvel franchise. */ + description?: string; + /** + * @default 0 + * @example 1 + */ + id?: number; + /** @example US */ + iso_3166_1?: string; + /** @example en */ + iso_639_1?: string; + /** + * @default 0 + * @example 69 + */ + item_count?: number; + /** @example The Marvel Universe */ + name?: string; + object_ids?: Record; + /** + * @default 0 + * @example 1 + */ + page?: number; + /** @example /coJVIUEOToAEGViuhclM7pXC75R.jpg */ + poster_path?: string; + /** + * @default true + * @example true + */ + public?: boolean; + /** + * @default 0 + * @example 40672159319 + */ + revenue?: number; + /** + * @default 0 + * @example 8070 + */ + runtime?: number; + /** @example primary_release_date.desc */ + sort_by?: string; + /** + * @default 0 + * @example 4 + */ + total_pages?: number; + /** + * @default 0 + * @example 69 + */ + total_results?: number; + }; + }; + }; + }; + }; + /** + * Update + * @description Update the details of a list. + */ + "list-update": { + parameters: { + path: { + list_id: number; + }; + }; + requestBody?: { + content: { + "application/json": { + /** Format: json */ + RAW_BODY: string; + }; + }; + }; + responses: { + /** @description 200 */ + 200: { + content: { + "application/json": { + /** @example The item/record was updated successfully. */ + status_message?: string; + /** + * @default true + * @example true + */ + success?: boolean; + /** + * @default 0 + * @example 12 + */ + status_code?: number; + }; + }; + }; + }; + }; + /** + * Create + * @description Create a new list. + */ + "list-create": { + requestBody?: { + content: { + "application/json": { + /** Format: json */ + RAW_BODY: string; + }; + }; + }; + responses: { + /** @description 200 */ + 200: { + content: { + "application/json": { + /** @example The item/record was created successfully. */ + status_message?: string; + /** + * @default 0 + * @example 5854 + */ + id?: number; + /** + * @default true + * @example true + */ + success?: boolean; + /** + * @default 0 + * @example 1 + */ + status_code?: number; + }; + }; + }; + }; + }; + /** + * Clear + * @description Clear all of the items on a list. + */ + "list-clear": { + parameters: { + path: { + list_id: number; + }; + }; + responses: { + /** @description 200 */ + 200: { + content: { + "application/json": { + /** + * @default 0 + * @example 1 + */ + items_deleted?: number; + /** @example Success. */ + status_message?: string; + /** + * @default 0 + * @example 10 + */ + id?: number; + /** + * @default 0 + * @example 1 + */ + status_code?: number; + /** + * @default true + * @example true + */ + success?: boolean; + }; + }; + }; + }; + }; + /** + * Delete + * @description Delete a list. + */ + "list-delete": { + parameters: { + path: { + list_id: number; + }; + }; + responses: { + /** @description 200 */ + 200: { + content: { + "application/json": { + /** @example The item/record was deleted successfully. */ + status_message?: string; + /** + * @default true + * @example true + */ + success?: boolean; + /** + * @default 0 + * @example 13 + */ + status_code?: number; + }; + }; + }; + }; + }; + /** + * Update Items + * @description Update an individual item on a list + */ + "list-update-items": { + requestBody?: { + content: { + "application/json": { + /** Format: json */ + RAW_BODY: string; + }; + }; + }; + responses: { + /** @description 200 */ + 200: { + content: { + "application/json": { + /** @example Success. */ + status_message?: string; + results?: { + /** @example movie */ + media_type?: string; + /** + * @default 0 + * @example 194662 + */ + media_id?: number; + /** + * @default true + * @example true + */ + success?: boolean; + }[]; + /** + * @default true + * @example true + */ + success?: boolean; + /** + * @default 0 + * @example 1 + */ + status_code?: number; + }; + }; + }; + }; + }; + /** + * Add Items + * @description Add items to a list. + */ + "list-add-items": { + parameters: { + path: { + list_id: number; + }; + }; + requestBody?: { + content: { + "application/json": { + /** Format: json */ + RAW_BODY: string; + }; + }; + }; + responses: { + /** @description 200 */ + 200: { + content: { + "application/json": { + /** @example Success. */ + status_message?: string; + results?: { + /** @example movie */ + media_type?: string; + /** + * @default 0 + * @example 550 + */ + media_id?: number; + /** + * @default true + * @example false + */ + success?: boolean; + }[]; + /** + * @default true + * @example true + */ + success?: boolean; + /** + * @default 0 + * @example 1 + */ + status_code?: number; + }; + }; + }; + }; + }; + /** + * Remove Items + * @description Remove items from a list + */ + "list-remove-items": { + parameters: { + path: { + list_id: number; + }; + }; + requestBody?: { + content: { + "application/json": { + /** Format: json */ + RAW_BODY: string; + }; + }; + }; + responses: { + /** @description 200 */ + 200: { + content: { + "application/json": { + /** @example Success. */ + status_message?: string; + results?: { + /** @example movie */ + media_type?: string; + /** + * @default 0 + * @example 194662 + */ + media_id?: number; + /** + * @default true + * @example true + */ + success?: boolean; + }[]; + /** + * @default true + * @example true + */ + success?: boolean; + /** + * @default 0 + * @example 1 + */ + status_code?: number; + }; + }; + }; + }; + }; + /** + * Item Status + * @description Check if an item is on a list. + */ + "list-item-status": { + parameters: { + query: { + media_id: number; + media_type: "" | "movie" | "tv"; + }; + path: { + list_id: number; + }; + }; + responses: { + /** @description 200 */ + 200: { + content: { + "application/json": { + /** @example movie */ + media_type?: string; + /** + * @default true + * @example true + */ + success?: boolean; + /** @example Success. */ + status_message?: string; + /** + * @default 0 + * @example 1 + */ + id?: number; + /** + * @default 0 + * @example 99861 + */ + media_id?: number; + /** + * @default 0 + * @example 1 + */ + status_code?: number; + }; + }; + }; + }; + }; + /** + * Lists + * @description Get all of the lists you've created. + */ + "account-lists": { + parameters: { + query?: { + page?: number; + }; + path: { + account_object_id: string; + }; + }; + responses: { + /** @description 200 */ + 200: { + content: { + "application/json": { + /** + * @default 0 + * @example 1 + */ + page?: number; + results?: { + /** @example 4bc8892a017a3c0f92000002 */ + account_object_id?: string; + /** + * @default 0 + * @example 0 + */ + adult?: number; + /** + * @default 0 + * @example 7.90183 + */ + average_rating?: number; + /** @example 2019-08-27 15:13:15 */ + created_at?: string; + /** @example */ + description?: string; + /** + * @default 0 + * @example 0 + */ + featured?: number; + /** + * @default 0 + * @example 120174 + */ + id?: number; + /** @example US */ + iso_3166_1?: string; + /** @example en */ + iso_639_1?: string; + /** @example Test Alpha Sort */ + name?: string; + /** + * @default 0 + * @example 6 + */ + number_of_items?: number; + /** + * @default 0 + * @example 0 + */ + public?: number; + /** @example 586453267 */ + revenue?: string; + /** + * @default 0 + * @example 644 + */ + runtime?: number; + /** + * @default 0 + * @example 7 + */ + sort_by?: number; + /** @example 2023-05-05 16:49:11 */ + updated_at?: string; + }[]; + /** + * @default 0 + * @example 2 + */ + total_pages?: number; + /** + * @default 0 + * @example 25 + */ + total_results?: number; + }; + }; + }; + }; + }; + /** + * Favorite Movies + * @description Get a user's list of favourite movies. + */ + "account-favorite-movies": { + parameters: { + query?: { + page?: number; + language?: string; + }; + path: { + account_object_id: string; + }; + }; + responses: { + /** @description 200 */ + 200: { + content: { + "application/json": { + /** + * @default 0 + * @example 1 + */ + page?: number; + results?: { + /** + * @default true + * @example false + */ + adult?: boolean; + /** @example /se5Hxz7PArQZOG3Nx2bpfOhLhtV.jpg */ + backdrop_path?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 9806 + */ + id?: number; + /** @example en */ + original_language?: string; + /** @example The Incredibles */ + original_title?: string; + /** @example Bob Parr has given up his superhero days to log in time as an insurance adjuster and raise his three children with his formerly heroic wife in suburbia. But when he receives a mysterious assignment, it's time to get back into costume. */ + overview?: string; + /** + * @default 0 + * @example 67.887 + */ + popularity?: number; + /** @example /2LqaLgk4Z226KkgPJuiOQ58wvrm.jpg */ + poster_path?: string; + /** @example 2004-10-27 */ + release_date?: string; + /** @example The Incredibles */ + title?: string; + /** + * @default true + * @example false + */ + video?: boolean; + /** + * @default 0 + * @example 7.702 + */ + vote_average?: number; + /** + * @default 0 + * @example 16188 + */ + vote_count?: number; + }[]; + /** + * @default 0 + * @example 4 + */ + total_pages?: number; + /** + * @default 0 + * @example 80 + */ + total_results?: number; + }; + }; + }; + }; + }; + /** + * Favorite TV Shows + * @description Get a user's list of favourite TV shows. + */ + "account-favorite-tv": { + parameters: { + query?: { + page?: number; + language?: string; + }; + path: { + account_object_id: string; + }; + }; + responses: { + /** @description 200 */ + 200: { + content: { + "application/json": { + /** + * @default 0 + * @example 1 + */ + page?: number; + results?: { + /** + * @default true + * @example false + */ + adult?: boolean; + /** @example /bsNm9z2TJfe0WO3RedPGWQ8mG1X.jpg */ + backdrop_path?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 1396 + */ + id?: number; + origin_country?: string[]; + /** @example en */ + original_language?: string; + /** @example Breaking Bad */ + original_name?: string; + /** @example When Walter White, a New Mexico chemistry teacher, is diagnosed with Stage III cancer and given a prognosis of only two years left to live. He becomes filled with a sense of fearlessness and an unrelenting desire to secure his family's financial future at any cost as he enters the dangerous world of drugs and crime. */ + overview?: string; + /** + * @default 0 + * @example 255.118 + */ + popularity?: number; + /** @example /ggFHVNu6YYI5L9pCfOacjizRGt.jpg */ + poster_path?: string; + /** @example 2008-01-20 */ + first_air_date?: string; + /** @example Breaking Bad */ + name?: string; + /** + * @default 0 + * @example 8.879 + */ + vote_average?: number; + /** + * @default 0 + * @example 11625 + */ + vote_count?: number; + }[]; + /** + * @default 0 + * @example 4 + */ + total_pages?: number; + /** + * @default 0 + * @example 68 + */ + total_results?: number; + }; + }; + }; + }; + }; + /** + * Recommended TV Shows + * @description Get a user's list of recommended TV shows. + */ + "account-tv-recommendations": { + parameters: { + query?: { + page?: number; + language?: string; + }; + path: { + account_object_id: string; + }; + }; + responses: { + /** @description 200 */ + 200: { + content: { + "application/json": { + /** + * @default 0 + * @example 1 + */ + page?: number; + results?: { + /** + * @default true + * @example false + */ + adult?: boolean; + /** @example /7bsHAsS1RDtslictkApeb7cedLL.jpg */ + backdrop_path?: string; + /** + * @default 0 + * @example 152483 + */ + id?: number; + /** @example The Boys Presents: Diabolical */ + name?: string; + /** @example en */ + original_language?: string; + /** @example The Boys Presents: Diabolical */ + original_name?: string; + /** @example From some of the most unhinged and maniacal minds in Hollywood today comes this animated anthology series, a collection of irreverent and emotionally shocking animated short films. Each episode plunges elbow-deep into unseen crevices of The Boys Universe. */ + overview?: string; + /** @example /kZKfZWwFOAicgoKS2IO7oM1GuHZ.jpg */ + poster_path?: string; + /** @example tv */ + media_type?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 24.596 + */ + popularity?: number; + /** @example 2022-03-03 */ + first_air_date?: string; + /** + * @default 0 + * @example 7.201 + */ + vote_average?: number; + /** + * @default 0 + * @example 214 + */ + vote_count?: number; + origin_country?: string[]; + }[]; + /** + * @default 0 + * @example 4 + */ + total_pages?: number; + /** + * @default 0 + * @example 80 + */ + total_results?: number; + }; + }; + }; + }; + }; + /** + * Recommended Movies + * @description Get a user's list of recommended movies. + */ + "account-movie-recommendations": { + parameters: { + query?: { + page?: number; + language?: string; + }; + path: { + account_object_id: string; + }; + }; + responses: { + /** @description 200 */ + 200: { + content: { + "application/json": { + /** + * @default 0 + * @example 1 + */ + page?: number; + results?: { + /** + * @default true + * @example false + */ + adult?: boolean; + /** @example /9sfVyE3sP2dkCwDyV7UlYP5TAAR.jpg */ + backdrop_path?: string; + /** + * @default 0 + * @example 823754 + */ + id?: number; + /** @example Bo Burnham: Inside */ + title?: string; + /** @example en */ + original_language?: string; + /** @example Bo Burnham: Inside */ + original_title?: string; + /** @example Stuck in COVID-19 lockdown, US comedian and musician Bo Burnham attempts to stay sane and happy by writing, shooting and performing a one-man comedy special. */ + overview?: string; + /** @example /ku1UvTWYvhFQbSesOD6zteY7bXT.jpg */ + poster_path?: string; + /** @example movie */ + media_type?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 11.904 + */ + popularity?: number; + /** @example 2021-07-22 */ + release_date?: string; + /** + * @default true + * @example false + */ + video?: boolean; + /** + * @default 0 + * @example 8.178 + */ + vote_average?: number; + /** + * @default 0 + * @example 352 + */ + vote_count?: number; + }[]; + /** + * @default 0 + * @example 4 + */ + total_pages?: number; + /** + * @default 0 + * @example 80 + */ + total_results?: number; + }; + }; + }; + }; + }; + /** + * Watchlist Movies + * @description Get a user's movie watchlist. + */ + "account-movie-watchlist": { + parameters: { + query?: { + page?: number; + language?: string; + }; + path: { + account_object_id: string; + }; + }; + responses: { + /** @description 200 */ + 200: { + content: { + "application/json": { + /** + * @default 0 + * @example 1 + */ + page?: number; + results?: { + /** + * @default true + * @example false + */ + adult?: boolean; + /** @example /9sfVyE3sP2dkCwDyV7UlYP5TAAR.jpg */ + backdrop_path?: string; + /** + * @default 0 + * @example 823754 + */ + id?: number; + /** @example Bo Burnham: Inside */ + title?: string; + /** @example en */ + original_language?: string; + /** @example Bo Burnham: Inside */ + original_title?: string; + /** @example Stuck in COVID-19 lockdown, US comedian and musician Bo Burnham attempts to stay sane and happy by writing, shooting and performing a one-man comedy special. */ + overview?: string; + /** @example /ku1UvTWYvhFQbSesOD6zteY7bXT.jpg */ + poster_path?: string; + /** @example movie */ + media_type?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 11.904 + */ + popularity?: number; + /** @example 2021-07-22 */ + release_date?: string; + /** + * @default true + * @example false + */ + video?: boolean; + /** + * @default 0 + * @example 8.178 + */ + vote_average?: number; + /** + * @default 0 + * @example 352 + */ + vote_count?: number; + }[]; + /** + * @default 0 + * @example 4 + */ + total_pages?: number; + /** + * @default 0 + * @example 80 + */ + total_results?: number; + }; + }; + }; + }; + }; + /** + * Watchlist TV Shows + * @description Get a user's TV watchlist. + */ + "account-tv-watchlist": { + parameters: { + query?: { + page?: number; + language?: string; + }; + path: { + account_object_id: string; + }; + }; + responses: { + /** @description 200 */ + 200: { + content: { + "application/json": { + /** + * @default 0 + * @example 1 + */ + page?: number; + results?: { + /** + * @default true + * @example false + */ + adult?: boolean; + /** @example /7bsHAsS1RDtslictkApeb7cedLL.jpg */ + backdrop_path?: string; + /** + * @default 0 + * @example 152483 + */ + id?: number; + /** @example The Boys Presents: Diabolical */ + name?: string; + /** @example en */ + original_language?: string; + /** @example The Boys Presents: Diabolical */ + original_name?: string; + /** @example From some of the most unhinged and maniacal minds in Hollywood today comes this animated anthology series, a collection of irreverent and emotionally shocking animated short films. Each episode plunges elbow-deep into unseen crevices of The Boys Universe. */ + overview?: string; + /** @example /kZKfZWwFOAicgoKS2IO7oM1GuHZ.jpg */ + poster_path?: string; + /** @example tv */ + media_type?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 24.596 + */ + popularity?: number; + /** @example 2022-03-03 */ + first_air_date?: string; + /** + * @default 0 + * @example 7.201 + */ + vote_average?: number; + /** + * @default 0 + * @example 214 + */ + vote_count?: number; + origin_country?: string[]; + }[]; + /** + * @default 0 + * @example 4 + */ + total_pages?: number; + /** + * @default 0 + * @example 80 + */ + total_results?: number; + }; + }; + }; + }; + }; + /** + * Rated Movies + * @description Get a user's rated movies. + */ + "account-rated-movies": { + parameters: { + query?: { + page?: number; + language?: string; + }; + path: { + account_object_id: string; + }; + }; + responses: { + /** @description 200 */ + 200: { + content: { + "application/json": { + /** + * @default 0 + * @example 1 + */ + page?: number; + results?: { + /** + * @default true + * @example false + */ + adult?: boolean; + /** @example /dUVbWINfRMGojGZRcO6GF1Z2nV8.jpg */ + backdrop_path?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 120 + */ + id?: number; + /** @example en */ + original_language?: string; + /** @example The Lord of the Rings: The Fellowship of the Ring */ + original_title?: string; + /** @example Young hobbit Frodo Baggins, after inheriting a mysterious ring from his uncle Bilbo, must leave his home in order to keep it from falling into the hands of its evil creator. Along the way, a fellowship is formed to protect the ringbearer and make sure that the ring arrives at its final destination: Mt. Doom, the only place where it can be destroyed. */ + overview?: string; + /** + * @default 0 + * @example 79.298 + */ + popularity?: number; + /** @example /6oom5QYQ2yQTMJIbnvbkBL9cHo6.jpg */ + poster_path?: string; + /** @example 2001-12-18 */ + release_date?: string; + /** @example The Lord of the Rings: The Fellowship of the Ring */ + title?: string; + /** + * @default true + * @example false + */ + video?: boolean; + /** + * @default 0 + * @example 8.4 + */ + vote_average?: number; + /** + * @default 0 + * @example 22626 + */ + vote_count?: number; + account_rating?: { + /** @example 2012-02-15T15:18:04.000Z */ + created_at?: string; + /** + * @default 0 + * @example 8 + */ + value?: number; + }; + }[]; + /** + * @default 0 + * @example 47 + */ + total_pages?: number; + /** + * @default 0 + * @example 940 + */ + total_results?: number; + }; + }; + }; + }; + }; + /** + * Rated TV Shows + * @description Get a user's rated TV shows. + */ + "account-rated-tv": { + parameters: { + query?: { + page?: number; + language?: string; + }; + path: { + account_object_id: string; + }; + }; + responses: { + /** @description 200 */ + 200: { + content: { + "application/json": { + /** + * @default 0 + * @example 1 + */ + page?: number; + results?: { + /** + * @default true + * @example false + */ + adult?: boolean; + /** @example /2yZXtM2Kky1Sy0kachbDlwybl3y.jpg */ + backdrop_path?: string; + genre_ids?: number[]; + /** + * @default 0 + * @example 1705 + */ + id?: number; + origin_country?: string[]; + /** @example en */ + original_language?: string; + /** @example Fringe */ + original_name?: string; + /** @example FBI Special Agent Olivia Dunham, brilliant but formerly institutionalized scientist Walter Bishop and his scheming, reluctant son Peter uncover a deadly mystery involving a series of unbelievable events and realize they may be a part of a larger, more disturbing pattern that blurs the line between science fiction and technology. */ + overview?: string; + /** + * @default 0 + * @example 145.5 + */ + popularity?: number; + /** @example /sY9hg5dLJ93RJOyKEiu1nAtBRND.jpg */ + poster_path?: string; + /** @example 2008-09-09 */ + first_air_date?: string; + /** @example Fringe */ + name?: string; + /** + * @default 0 + * @example 8.11 + */ + vote_average?: number; + /** + * @default 0 + * @example 2053 + */ + vote_count?: number; + account_rating?: { + /** @example 2013-10-10T21:03:56.499Z */ + created_at?: string; + /** + * @default 0 + * @example 9 + */ + value?: number; + }; + }[]; + /** + * @default 0 + * @example 15 + */ + total_pages?: number; + /** + * @default 0 + * @example 291 + */ + total_results?: number; + }; + }; + }; + }; + }; +} diff --git a/src/lib/components/Button.svelte b/src/lib/components/Button.svelte index ee06b7a..5045d21 100644 --- a/src/lib/components/Button.svelte +++ b/src/lib/components/Button.svelte @@ -39,7 +39,7 @@ -
+
{#if $$slots.icon}
@@ -80,6 +80,11 @@
{/if} + {#if $$slots['icon-absolute']} +
+ +
+ {/if}
diff --git a/src/lib/components/MediaManagerModal/MMAddToSonarrDialog.svelte b/src/lib/components/MediaManagerModal/MMAddToSonarrDialog.svelte index 18f06e3..b85c3d7 100644 --- a/src/lib/components/MediaManagerModal/MMAddToSonarrDialog.svelte +++ b/src/lib/components/MediaManagerModal/MMAddToSonarrDialog.svelte @@ -13,7 +13,6 @@ import classNames from 'classnames'; import { type BackEvent, scrollIntoView, Selectable } from '../../selectable'; import { fade } from 'svelte/transition'; - import { sonarrService } from '../../stores/sonarr-service.store'; import { createLocalStorageStore } from '../../stores/localstorage.store'; import { formatSize } from '../../utils'; import { capitalize } from '../../utils.js'; @@ -50,7 +49,12 @@ monitorOptions: null }); - $sonarrService.then((s) => { + const sonarrOptions = Promise.all([ + sonarrApi.getRootFolders(), + sonarrApi.getQualityProfiles() + ]).then(([rootFolders, qualityProfiles]) => ({ rootFolders, qualityProfiles })); + + sonarrOptions.then((s) => { addOptionsStore.update((prev) => ({ rootFolderPath: prev.rootFolderPath || s.rootFolders[0]?.path || null, qualityProfileId: prev.qualityProfileId || s.qualityProfiles[0]?.id || null, @@ -108,7 +112,7 @@ /> {/if} - {#await $sonarrService then { qualityProfiles, rootFolders }} + {#await sonarrOptions then { qualityProfiles, rootFolders }} {@const selectedRootFolder = rootFolders.find( (f) => f.path === $addOptionsStore.rootFolderPath )} diff --git a/src/lib/components/SelectField.svelte b/src/lib/components/SelectField.svelte new file mode 100644 index 0000000..86f327f --- /dev/null +++ b/src/lib/components/SelectField.svelte @@ -0,0 +1,30 @@ + + + +
+

+ +

+ + {value} + +
+ +
diff --git a/src/lib/components/SelectItem.svelte b/src/lib/components/SelectItem.svelte new file mode 100644 index 0000000..dc49be2 --- /dev/null +++ b/src/lib/components/SelectItem.svelte @@ -0,0 +1,23 @@ + + + +
+ +
+ {#if selected} + + {/if} +
diff --git a/src/lib/components/Tab/Tab.svelte b/src/lib/components/Tab/Tab.svelte new file mode 100644 index 0000000..2837bd3 --- /dev/null +++ b/src/lib/components/Tab/Tab.svelte @@ -0,0 +1,27 @@ + + += index, + 'translate-x-10': !active && openTab < index + })} + bind:selectable + on:back +> + + diff --git a/src/lib/components/Tab/Tab.ts b/src/lib/components/Tab/Tab.ts new file mode 100644 index 0000000..20cb1a6 --- /dev/null +++ b/src/lib/components/Tab/Tab.ts @@ -0,0 +1,15 @@ +import { writable } from 'svelte/store'; + +enum TestTabs { + Tab1 = 'Tab1', + Tab2 = 'Tab2', + Tab3 = 'Tab3' +} + +const test = useTabs(TestTabs.Tab1); + +export function useTabs(defaultTab: T) { + const tab = writable(defaultTab); + + return { subscribe: tab.subscribe }; +} diff --git a/src/lib/components/TextField.svelte b/src/lib/components/TextField.svelte index a61d9c9..247e8c5 100644 --- a/src/lib/components/TextField.svelte +++ b/src/lib/components/TextField.svelte @@ -1,12 +1,29 @@ @@ -28,18 +47,26 @@ on:clickOrSelect={() => input?.focus()} class={classNames('flex flex-col', $$restProps.class)} let:hasFocus + focusOnClick > -
diff --git a/src/lib/pages/OnboardingPage.svelte b/src/lib/pages/OnboardingPage.svelte new file mode 100644 index 0000000..9faf54a --- /dev/null +++ b/src/lib/pages/OnboardingPage.svelte @@ -0,0 +1,420 @@ + + + + +

Welcome to Reiverr

+
+ Looks like this is a new account. This setup will get you started with connecting your + services to get most out of Reiverr. +
+ + +
+ +
+
+
+ + +

Connect a TMDB Account

+
+ Connect to TMDB for personalized recommendations based on your movie reviews and preferences. +
+ +
+ {#await connectedTmdbAccount then account} + {#if account} + { + openTab = Tabs.TmdbConnect; + handleGenerateTMDBLink(); + }}>Logged in as + {:else} + + {/if} + {/await} + + +
+
+ + (openTab = Tabs.Tmdb)}> +

Connect a TMDB Account

+
+ To connect your TMDB account, log in via the link below and then click "Complete Connection". +
+ + {#if tmdbConnectQrCode} +
+ {/if} + + + {#if !tmdbConnectRequestToken} + + {:else if tmdbConnectLink} + + + {/if} + + + + +

Connect to Jellyfin

+
Connect to Jellyfin to watch movies and tv shows.
+ +
+ !!u?.length)}> + Base Url + + !!u?.length)}> + API Key + +
+ + {#await jellyfinUsers then users} + {#if users.length} + (openTab = Tabs.SelectUser)} + > + User + + {/if} + {/await} + + {#if jellyfinError} +
{jellyfinError}
+ {/if} + + + + {#if jellyfinBaseUrl && jellyfinApiKey && jellyfinUser} + + {:else} + + {/if} + +
+ (openTab = Tabs.Jellyfin)} + class={tabContainer} + > +

Select User

+ {#await jellyfinUsers then users} + {#each users as user} + { + jellyfinUser = user; + openTab = Tabs.Jellyfin; + }} + > + {user.Name} + + {/each} + {/await} +
+ + +

Connect to Sonarr

+
Connect to Sonarr for requesting and managing tv shows.
+ +
+ Base Url + API Key +
+ + {#if sonarrError} +
{sonarrError}
+ {/if} + + + + {#if sonarrBaseUrl && sonarrApiKey} + + {:else} + + {/if} + +
+ + +

Connect to Radarr

+
Connect to Radarr for requesting and managing movies.
+ +
+ Base Url + API Key +
+ + {#if radarrError} +
{radarrError}
+ {/if} + + + + {#if radarrBaseUrl && radarrApiKey} + + {:else} + + {/if} + +
+ diff --git a/src/lib/stores/app-state.store.ts b/src/lib/stores/app-state.store.ts index 166b588..0aef49e 100644 --- a/src/lib/stores/app-state.store.ts +++ b/src/lib/stores/app-state.store.ts @@ -1,6 +1,11 @@ -import { derived, writable } from 'svelte/store'; +import { derived, get, writable } from 'svelte/store'; import { createLocalStorageStore } from './localstorage.store'; -import { getReiverrApiClient, type ReiverrUser } from '../apis/reiverr/reiverr-api'; +import { + getReiverrApiClient, + reiverrApi, + type ReiverrSettings, + type ReiverrUser +} from '../apis/reiverr/reiverr-api'; interface AuthenticationStoreData { token?: string; @@ -11,7 +16,7 @@ interface UserStoreData { user: ReiverrUser | null; } -interface AppStateData extends AuthenticationStoreData { +export interface AppStateData extends AuthenticationStoreData { user: ReiverrUser | null; } @@ -61,11 +66,25 @@ function createAppState() { }); }); + async function updateUser(updateFn: (user: ReiverrUser) => ReiverrUser) { + const user = get(userStore).user; + + if (!user) return; + + const updated = updateFn(user); + const update = await reiverrApi.updateUser(updated); + + if (update) { + setUser(update); + } + } + return { subscribe: combinedStore.subscribe, setBaseUrl, setToken, setUser, + updateUser, logOut, ready }; diff --git a/src/lib/stores/sonarr-service.store.ts b/src/lib/stores/sonarr-service.store.ts deleted file mode 100644 index 8c37bf2..0000000 --- a/src/lib/stores/sonarr-service.store.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { writable } from 'svelte/store'; -import { sonarrApi, type SonarrRootFolder } from '../apis/sonarr/sonarr-api'; - -type SonarrServiceStore = ReturnType; - -async function fetchSonarrService() { - const rootFolders = sonarrApi.getRootFolders(); - const qualityProfiles = sonarrApi.getQualityProfiles(); - - return { - rootFolders: await rootFolders, - qualityProfiles: await qualityProfiles - }; -} - -function useSonarrService() { - const sonarrService = writable(fetchSonarrService()); - - return { - subscribe: sonarrService.subscribe - }; -} - -export const sonarrService = useSonarrService();