diff --git a/backend/data-source.ts b/backend/data-source.ts new file mode 100644 index 0000000..f188e42 --- /dev/null +++ b/backend/data-source.ts @@ -0,0 +1,9 @@ +import { DataSource } from 'typeorm'; + +export default new DataSource({ + type: 'sqlite', + database: './config/reiverr.sqlite', + entities: ['dist/**/*.entity.js'], + migrations: ['dist/migrations/*.js'], + // migrations: [__dirname + '/../**/*.migration{.ts,.js}'], +}); diff --git a/backend/migrations/1718397524237-initial-migration.ts b/backend/migrations/1718397524237-initial-migration.ts new file mode 100644 index 0000000..76d4466 --- /dev/null +++ b/backend/migrations/1718397524237-initial-migration.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class InitialMigration1718397524237 implements MigrationInterface { + name = 'InitialMigration1718397524237'; + + public async up(queryRunner: QueryRunner): Promise { + try { + await queryRunner.query( + `CREATE TABLE "user" ("id" varchar PRIMARY KEY NOT NULL, "name" varchar NOT NULL, "password" varchar NOT NULL, "isAdmin" boolean NOT NULL DEFAULT (0), "onboardingDone" boolean NOT NULL DEFAULT (0), "settings" json NOT NULL DEFAULT ('{"autoplayTrailers":true,"language":"en","animationDuration":300,"sonarr":{"apiKey":"","baseUrl":"","qualityProfileId":0,"rootFolderPath":"","languageProfileId":0},"radarr":{"apiKey":"","baseUrl":"","qualityProfileId":0,"rootFolderPath":""},"jellyfin":{"apiKey":"","baseUrl":"","userId":""},"tmdb":{"sessionId":"","userId":""}}'), CONSTRAINT "UQ_065d4d8f3b5adb4a08841eae3c8" UNIQUE ("name"))`, + ); + } catch (ignored) {} + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "user"`); + } +} diff --git a/backend/migrations/1718397928862-add-profile-picture.ts b/backend/migrations/1718397928862-add-profile-picture.ts new file mode 100644 index 0000000..bd4f7e6 --- /dev/null +++ b/backend/migrations/1718397928862-add-profile-picture.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddProfilePicture1718397928862 implements MigrationInterface { + name = 'AddProfilePicture1718397928862' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "temporary_user" ("id" varchar PRIMARY KEY NOT NULL, "name" varchar NOT NULL, "password" varchar NOT NULL, "isAdmin" boolean NOT NULL DEFAULT (0), "onboardingDone" boolean NOT NULL DEFAULT (0), "settings" json NOT NULL DEFAULT ('{"autoplayTrailers":true,"language":"en","animationDuration":300,"sonarr":{"apiKey":"","baseUrl":"","qualityProfileId":0,"rootFolderPath":"","languageProfileId":0},"radarr":{"apiKey":"","baseUrl":"","qualityProfileId":0,"rootFolderPath":""},"jellyfin":{"apiKey":"","baseUrl":"","userId":""},"tmdb":{"sessionId":"","userId":""}}'), "profilePicture" blob, CONSTRAINT "UQ_065d4d8f3b5adb4a08841eae3c8" UNIQUE ("name"))`); + await queryRunner.query(`INSERT INTO "temporary_user"("id", "name", "password", "isAdmin", "onboardingDone", "settings") SELECT "id", "name", "password", "isAdmin", "onboardingDone", "settings" FROM "user"`); + await queryRunner.query(`DROP TABLE "user"`); + await queryRunner.query(`ALTER TABLE "temporary_user" RENAME TO "user"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "user" RENAME TO "temporary_user"`); + await queryRunner.query(`CREATE TABLE "user" ("id" varchar PRIMARY KEY NOT NULL, "name" varchar NOT NULL, "password" varchar NOT NULL, "isAdmin" boolean NOT NULL DEFAULT (0), "onboardingDone" boolean NOT NULL DEFAULT (0), "settings" json NOT NULL DEFAULT ('{"autoplayTrailers":true,"language":"en","animationDuration":300,"sonarr":{"apiKey":"","baseUrl":"","qualityProfileId":0,"rootFolderPath":"","languageProfileId":0},"radarr":{"apiKey":"","baseUrl":"","qualityProfileId":0,"rootFolderPath":""},"jellyfin":{"apiKey":"","baseUrl":"","userId":""},"tmdb":{"sessionId":"","userId":""}}'), CONSTRAINT "UQ_065d4d8f3b5adb4a08841eae3c8" UNIQUE ("name"))`); + await queryRunner.query(`INSERT INTO "user"("id", "name", "password", "isAdmin", "onboardingDone", "settings") SELECT "id", "name", "password", "isAdmin", "onboardingDone", "settings" FROM "temporary_user"`); + await queryRunner.query(`DROP TABLE "temporary_user"`); + } + +} diff --git a/backend/package.json b/backend/package.json index 599e88a..67a9b9d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -18,7 +18,12 @@ "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "jest --config ./test/jest-e2e.json", - "openapi:schema": "ts-node src/generate-openapi.ts" + "openapi:schema": "ts-node src/generate-openapi.ts", + "typeorm": "ts-node ./node_modules/typeorm/cli", + "typeorm:run-migrations": "npm run typeorm migration:run -- -d ./data-source.ts", + "typeorm:generate-migration": "npm run typeorm -- -d ./data-source.ts migration:generate ./migrations/$npm_config_name", + "typeorm:create-migration": "npm run typeorm -- migration:create ./migrations/$npm_config_name", + "typeorm:revert-migration": "npm run typeorm -- -d ./data-source.ts migration:revert" }, "dependencies": { "@nanogiants/nestjs-swagger-api-exception-decorator": "^1.6.11", diff --git a/backend/src/database/database.providers.ts b/backend/src/database/database.providers.ts index 02e954b..d04b400 100644 --- a/backend/src/database/database.providers.ts +++ b/backend/src/database/database.providers.ts @@ -1,4 +1,4 @@ -import { DataSource } from 'typeorm'; +import dataSource from '../../data-source'; export const DATA_SOURCE = 'DATA_SOURCE'; @@ -6,13 +6,6 @@ export const databaseProviders = [ { provide: DATA_SOURCE, useFactory: async () => { - const dataSource = new DataSource({ - type: 'sqlite', - database: './config/reiverr.sqlite', - entities: [__dirname + '/../**/*.entity{.ts,.js}'], - synchronize: true, - }); - return dataSource.initialize(); }, }, diff --git a/backend/src/main.ts b/backend/src/main.ts index 13439cf..5df0940 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -5,6 +5,7 @@ import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import * as fs from 'fs'; import { UserService } from './user/user.service'; import { ADMIN_PASSWORD, ADMIN_USERNAME } from './consts'; +import { json, urlencoded } from 'express'; // import * as proxy from 'express-http-proxy'; async function createAdminUser(userService: UserService) { @@ -21,6 +22,8 @@ async function bootstrap() { const app = await NestFactory.create(AppModule); app.setGlobalPrefix('api'); app.enableCors(); + app.use(json({ limit: '50mb' })); + app.use(urlencoded({ extended: true, limit: '50mb' })); // app.use('/api/proxy/jellyfin', proxy('http://192.168.0.129:8096')); diff --git a/backend/src/user/user.controller.ts b/backend/src/user/user.controller.ts index 0410583..26bf124 100644 --- a/backend/src/user/user.controller.ts +++ b/backend/src/user/user.controller.ts @@ -1,4 +1,5 @@ import { + BadRequestException, Body, Controller, Get, @@ -89,6 +90,7 @@ export class UserController { @UseGuards(AuthGuard) @Put(':id') @ApiOkResponse({ description: 'User updated', type: UserDto }) + @ApiException(() => NotFoundException, { description: 'User not found' }) async updateUser( @Param('id') id: string, @Body() updateUserDto: UpdateUserDto, @@ -99,9 +101,30 @@ export class UserController { } const user = await this.userService.findOne(id); + if (updateUserDto.name) user.name = updateUserDto.name; + if ( + updateUserDto.oldPassword === user.password && + updateUserDto.password !== undefined + ) + user.password = updateUserDto.password; + else if ( + updateUserDto.password && + updateUserDto.oldPassword !== user.password + ) + throw new BadRequestException("Passwords don't match"); if (updateUserDto.settings) user.settings = updateUserDto.settings; if (updateUserDto.onboardingDone) user.onboardingDone = updateUserDto.onboardingDone; + if (updateUserDto.profilePicture) { + try { + user.profilePicture = Buffer.from( + updateUserDto.profilePicture.split(';base64,').pop() as string, + 'base64', + ); + } catch (e) { + console.error(e); + } + } 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 d9e42ef..e05f853 100644 --- a/backend/src/user/user.dto.ts +++ b/backend/src/user/user.dto.ts @@ -1,7 +1,13 @@ -import { OmitType, PartialType, PickType } from '@nestjs/swagger'; +import { ApiProperty, OmitType, PartialType, PickType } from '@nestjs/swagger'; import { User } from './user.entity'; -export class UserDto extends OmitType(User, ['password'] as const) { +export class UserDto extends OmitType(User, [ + 'password', + 'profilePicture', +] as const) { + @ApiProperty({ type: 'string' }) + profilePicture: string | null; + static fromEntity(entity: User): UserDto { return { id: entity.id, @@ -9,6 +15,8 @@ export class UserDto extends OmitType(User, ['password'] as const) { isAdmin: entity.isAdmin, settings: entity.settings, onboardingDone: entity.onboardingDone, + profilePicture: + 'data:image;base64,' + entity.profilePicture?.toString('base64'), }; } } @@ -20,7 +28,13 @@ export class CreateUserDto extends PickType(User, [ ] as const) {} export class UpdateUserDto extends PartialType( - PickType(User, ['settings', 'onboardingDone', 'name'] as const), -) {} + PickType(User, ['settings', 'onboardingDone', 'name', 'password'] as const), +) { + @ApiProperty({ type: 'string', required: false }) + profilePicture?: string; + + @ApiProperty({ type: 'string', required: false }) + oldPassword?: string; +} 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 c3eb724..e39fa84 100644 --- a/backend/src/user/user.entity.ts +++ b/backend/src/user/user.entity.ts @@ -111,6 +111,11 @@ export class User { @Column() password: string; + @ApiProperty({ required: false }) + @Column({ type: 'blob', nullable: true }) + profilePicture: Buffer; + + @Column() @ApiProperty({ required: true }) @Column({ default: false }) isAdmin: boolean = false; diff --git a/public/profile-pictures/ana.webp b/public/profile-pictures/ana.webp new file mode 100644 index 0000000..abeb4d9 Binary files /dev/null and b/public/profile-pictures/ana.webp differ diff --git a/public/profile-pictures/emma.webp b/public/profile-pictures/emma.webp new file mode 100644 index 0000000..b490964 Binary files /dev/null and b/public/profile-pictures/emma.webp differ diff --git a/public/profile-pictures/glen.webp b/public/profile-pictures/glen.webp new file mode 100644 index 0000000..23072cb Binary files /dev/null and b/public/profile-pictures/glen.webp differ diff --git a/public/profile-pictures/henry.webp b/public/profile-pictures/henry.webp new file mode 100644 index 0000000..6f54bb1 Binary files /dev/null and b/public/profile-pictures/henry.webp differ diff --git a/public/profile-pictures/keanu.webp b/public/profile-pictures/keanu.webp new file mode 100644 index 0000000..6f78626 Binary files /dev/null and b/public/profile-pictures/keanu.webp differ diff --git a/public/profile-pictures/leo.webp b/public/profile-pictures/leo.webp new file mode 100644 index 0000000..1eee966 Binary files /dev/null and b/public/profile-pictures/leo.webp differ diff --git a/public/profile-pictures/sydney.webp b/public/profile-pictures/sydney.webp new file mode 100644 index 0000000..ec5f69a Binary files /dev/null and b/public/profile-pictures/sydney.webp differ diff --git a/public/profile-pictures/zendaya.webp b/public/profile-pictures/zendaya.webp new file mode 100644 index 0000000..0aa9bde Binary files /dev/null and b/public/profile-pictures/zendaya.webp differ diff --git a/src/app.css b/src/app.css index d80d4a2..e10c1d3 100644 --- a/src/app.css +++ b/src/app.css @@ -92,6 +92,11 @@ html[data-useragent*="Tizen"] .selectable-secondary { @apply font-semibold text-2xl text-secondary-100; } +.header3 { + @apply font-semibold text-3xl text-secondary-100; +} + + .header4 { @apply font-semibold text-4xl text-secondary-100 tracking-wider; } diff --git a/src/lib/apis/reiverr/reiverr-api.ts b/src/lib/apis/reiverr/reiverr-api.ts index f0f366d..ba337bc 100644 --- a/src/lib/apis/reiverr/reiverr-api.ts +++ b/src/lib/apis/reiverr/reiverr-api.ts @@ -51,7 +51,7 @@ export class ReiverrApi implements Api { }, body: user }) - .then((res) => res.data); + .then((res) => ({ user: res.data, error: res.error?.message })); } 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 159d93f..5c64956 100644 --- a/src/lib/apis/reiverr/reiverr.generated.d.ts +++ b/src/lib/apis/reiverr/reiverr.generated.d.ts @@ -62,6 +62,7 @@ export interface components { isAdmin: boolean; onboardingDone?: boolean; settings: components["schemas"]["Settings"]; + profilePicture: string; }; CreateUserDto: { name: string; @@ -70,8 +71,11 @@ export interface components { }; UpdateUserDto: { name?: string; + password?: string; onboardingDone?: boolean; settings?: components["schemas"]["Settings"]; + profilePicture?: string; + oldPassword?: string; }; SignInDto: { name: string; @@ -174,6 +178,18 @@ export interface operations { "application/json": components["schemas"]["UserDto"]; }; }; + 404: { + content: { + "application/json": { + /** @example 404 */ + statusCode: number; + /** @example Not Found */ + message: string; + /** @example Not Found */ + error?: string; + }; + }; + }; }; }; AuthController_signIn: { diff --git a/src/lib/components/Button.svelte b/src/lib/components/Button.svelte index 55d03a4..94abdb0 100644 --- a/src/lib/components/Button.svelte +++ b/src/lib/components/Button.svelte @@ -101,6 +101,7 @@ {/if} {#if iconAbsolute} +
diff --git a/src/lib/components/Dialog/Dialog.svelte b/src/lib/components/Dialog/Dialog.svelte index d0afd25..d58b920 100644 --- a/src/lib/components/Dialog/Dialog.svelte +++ b/src/lib/components/Dialog/Dialog.svelte @@ -4,7 +4,7 @@ import { fade } from 'svelte/transition'; import { modalStack } from '../Modal/modal.store'; - export let size: 'sm' | 'full' = 'sm'; + export let size: 'sm' | 'full' | 'lg' | 'dynamic' = 'sm'; function handleClose() { modalStack.closeTopmost(); @@ -19,10 +19,12 @@ >
+ import Dialog from './Dialog.svelte'; + import { reiverrApi, type ReiverrUser } from '../../apis/reiverr/reiverr-api'; + import TextField from '../TextField.svelte'; + import Button from '../Button.svelte'; + import { ArrowUp, EyeClosed, EyeOpen, Upload } from 'radix-icons-svelte'; + import Container from '../../../Container.svelte'; + import IconToggle from '../IconToggle.svelte'; + import Tab from '../Tab/Tab.svelte'; + import { useTabs } from '../Tab/Tab'; + import SelectField from '../SelectField.svelte'; + import ProfileIcon from '../ProfileIcon.svelte'; + import { profilePictures } from '../../profile-pictures'; + import { modalStack } from '../Modal/modal.store'; + import { user as userStore } from '../../stores/user.store'; + + enum Tabs { + EditProfile, + ProfilePictures + } + + export let user: ReiverrUser; + + const tab = useTabs(Tabs.EditProfile); + + let name = user?.name || ''; + let oldPassword = ''; + let oldPasswordVisible = false; + let newPassword = ''; + let newPasswordVisible = false; + let profilePictureFiles: FileList; + let profilePictureBase64: string = user.profilePicture; + let profilePictureTitle: string; + let profilePictureFilesInput: HTMLInputElement; + $: { + const file = profilePictureFiles?.[0]; + + if (file) { + const reader = new FileReader(); + reader.onload = () => setProfilePicture(reader.result as string); + reader.readAsDataURL(file); + } + } + + $: { + switch (profilePictureBase64) { + case profilePictures.ana: + profilePictureTitle = 'Ana'; + break; + case profilePictures.emma: + profilePictureTitle = 'Emma'; + break; + case profilePictures.glen: + profilePictureTitle = 'Glen'; + break; + case profilePictures.henry: + profilePictureTitle = 'Henry'; + break; + case profilePictures.keanu: + profilePictureTitle = 'Keanu'; + break; + case profilePictures.leo: + profilePictureTitle = 'Leo'; + break; + case profilePictures.sydney: + profilePictureTitle = 'Sydney'; + break; + case profilePictures.zendaya: + profilePictureTitle = 'Zendaya'; + break; + default: + profilePictureTitle = 'Custom'; + break; + } + } + + $: stale = + (name !== user.name && name !== '') || + oldPassword !== newPassword || + profilePictureBase64 !== user.profilePicture; + let errorMessage = ''; + + function setProfilePicture(image: string) { + profilePictureBase64 = image; + tab.set(Tabs.EditProfile); + } + + async function save() { + const error = await userStore.updateUser((u) => ({ + ...u, + name, + password: newPassword, + oldPassword, + profilePicture: profilePictureBase64 + // password: newPassword + })); + + if (error) { + errorMessage = error; + } else { + modalStack.closeTopmost(); + } + } + + + + +

Edit Profile

+ name + tab.set(Tabs.ProfilePictures)}> + Profile Picture + + + + Old Password + + (oldPasswordVisible = !oldPasswordVisible)} + icon={oldPasswordVisible ? EyeOpen : EyeClosed} + /> + + + + New Password + + (newPasswordVisible = !newPasswordVisible)} + icon={newPasswordVisible ? EyeOpen : EyeClosed} + /> + + {#if errorMessage} +
{errorMessage}
+ {/if} + +
+ + { + tab.set(Tabs.EditProfile); + detail.stopPropagation(); + }} + > +

Select Profile Picture

+ + setProfilePicture(profilePictures.ana)} + focusOnMount={profilePictureBase64 === profilePictures.ana} + /> + setProfilePicture(profilePictures.emma)} + focusOnMount={profilePictureBase64 === profilePictures.emma} + /> + setProfilePicture(profilePictures.glen)} + focusOnMount={profilePictureBase64 === profilePictures.glen} + /> + setProfilePicture(profilePictures.henry)} + focusOnMount={profilePictureBase64 === profilePictures.henry} + /> + setProfilePicture(profilePictures.keanu)} + focusOnMount={profilePictureBase64 === profilePictures.keanu} + /> + setProfilePicture(profilePictures.leo)} + focusOnMount={profilePictureBase64 === profilePictures.leo} + /> + setProfilePicture(profilePictures.sydney)} + focusOnMount={profilePictureBase64 === profilePictures.sydney} + /> + setProfilePicture(profilePictures.zendaya)} + focusOnMount={profilePictureBase64 === profilePictures.zendaya} + /> + profilePictureFilesInput?.click()} + icon={Upload} + /> + + + + + + +
+
diff --git a/src/lib/components/AddElementOverlay.svelte b/src/lib/components/IconOverlay.svelte similarity index 59% rename from src/lib/components/AddElementOverlay.svelte rename to src/lib/components/IconOverlay.svelte index bd675da..f223e4f 100644 --- a/src/lib/components/AddElementOverlay.svelte +++ b/src/lib/components/IconOverlay.svelte @@ -1,9 +1,11 @@
- +
diff --git a/src/lib/components/IconToggle.svelte b/src/lib/components/IconToggle.svelte new file mode 100644 index 0000000..f8d8c41 --- /dev/null +++ b/src/lib/components/IconToggle.svelte @@ -0,0 +1,23 @@ + + + + + diff --git a/src/lib/components/Integrations/JellyfinIntegration.svelte b/src/lib/components/Integrations/JellyfinIntegration.svelte index b7d9f35..41237e0 100644 --- a/src/lib/components/Integrations/JellyfinIntegration.svelte +++ b/src/lib/components/Integrations/JellyfinIntegration.svelte @@ -13,8 +13,8 @@ export let baseUrl = get(user)?.settings.jellyfin.baseUrl || ''; export let apiKey = get(user)?.settings.jellyfin.apiKey || ''; - let originalBaseUrl = derived(user, (user) => user?.settings.jellyfin.baseUrl || ''); - let originalApiKey = derived(user, (user) => user?.settings.jellyfin.apiKey || ''); + const originalBaseUrl = derived(user, (user) => user?.settings.jellyfin.baseUrl || ''); + const originalApiKey = derived(user, (user) => user?.settings.jellyfin.apiKey || ''); let timeout: ReturnType; let error = ''; let jellyfinUsers: Promise | undefined = undefined; diff --git a/src/lib/components/ProfileIcon.svelte b/src/lib/components/ProfileIcon.svelte new file mode 100644 index 0000000..4fa75d4 --- /dev/null +++ b/src/lib/components/ProfileIcon.svelte @@ -0,0 +1,34 @@ + + + + +
+ {#if icon} + + {/if} + + diff --git a/src/lib/components/SeriesPage/ManageSeasonCard.svelte b/src/lib/components/SeriesPage/ManageSeasonCard.svelte index a00906d..a5ed11c 100644 --- a/src/lib/components/SeriesPage/ManageSeasonCard.svelte +++ b/src/lib/components/SeriesPage/ManageSeasonCard.svelte @@ -3,9 +3,9 @@ import type { Readable } from 'svelte/store'; import AnimateScale from '../AnimateScale.svelte'; import classNames from 'classnames'; - import { Plus, PlusCircled } from 'radix-icons-svelte'; + import { Plus } from 'radix-icons-svelte'; import { getCardDimensions } from '../../utils'; - import AddElementOverlay from '../AddElementOverlay.svelte'; + import IconOverlay from '../IconOverlay.svelte'; export let backdropUrl: string; @@ -32,6 +32,6 @@ class="bg-cover bg-center absolute inset-0" style={`background-image: url('${backdropUrl}')`} /> - + diff --git a/src/lib/components/Tab/Tab.svelte b/src/lib/components/Tab/Tab.svelte index 1d6edee..dd872e7 100644 --- a/src/lib/components/Tab/Tab.svelte +++ b/src/lib/components/Tab/Tab.svelte @@ -37,7 +37,7 @@ $$restProps.class, 'transition-all col-start-1 col-end-1 row-start-1 row-end-1', { - 'opacity-0 pointer-events-none': !active, + 'opacity-0 pointer-events-none absolute inset-0': !active, '-translate-x-10': !active && $openTab >= index, 'translate-x-10': !active && $openTab < index } diff --git a/src/lib/pages/ManagePage.svelte b/src/lib/pages/ManagePage.svelte index f7501da..8bb72f8 100644 --- a/src/lib/pages/ManagePage.svelte +++ b/src/lib/pages/ManagePage.svelte @@ -13,20 +13,23 @@ import JellyfinIntegrationUsersDialog from '../components/Integrations/JellyfinIntegrationUsersDialog.svelte'; import { tmdbApi } from '../apis/tmdb/tmdb-api'; import SelectField from '../components/SelectField.svelte'; - import { ArrowRight, Trash } from 'radix-icons-svelte'; + import { ArrowRight, Exit, Pencil1, Pencil2, Trash } from 'radix-icons-svelte'; import TmdbIntegrationConnectDialog from '../components/Integrations/TmdbIntegrationConnectDialog.svelte'; import { createModal } from '../components/Modal/modal.store'; import DetachedPage from '../components/DetachedPage/DetachedPage.svelte'; import { user } from '../stores/user.store'; import { sessions } from '../stores/session.store'; + import TextField from '../components/TextField.svelte'; + import EditProfileModal from '../components/Dialog/EditProfileModal.svelte'; + import { scrollIntoView } from '../selectable'; enum Tabs { Interface, - Integrations, + Account, About } - const tab = useTabs(Tabs.Integrations); + const tab = useTabs(Tabs.Account); let jellyfinBaseUrl = ''; let jellyfinApiKey = ''; @@ -117,11 +120,14 @@ } })); } + + function handleLogOut() { + sessions.removeSession(); + } { - console.log('keypress', e); lastKeyCode = e.keyCode; lastKey = e.key; }} @@ -130,7 +136,7 @@ tab.set(Tabs.Interface)} @@ -144,22 +150,22 @@ 'text-primary-500': hasFocus })} > - General + Options tab.set(Tabs.Integrations)} - on:clickOrSelect={() => tab.set(Tabs.Integrations)} + on:enter={() => tab.set(Tabs.Account)} + on:clickOrSelect={() => tab.set(Tabs.Account)} let:hasFocus focusOnClick > - Integrations + Account - - - - -

Sonarr

- { - sonarrBaseUrl = detail.baseUrl; - sonarrApiKey = detail.apiKey; - sonarrStale = detail.stale; - }} - /> -
- -
-
- - -

Radarr

- { - radarrBaseUrl = detail.baseUrl; - radarrApiKey = detail.apiKey; - radarrStale = detail.stale; - }} - /> -
- -
+ +
+

Profile

+ + { + const u = $user; + if (u) + createModal(EditProfileModal, { + user: u + }); + }} + > + Logged in as + + + + +
- - -

Tmdb Account

- {#await tmdbAccount then tmdbAccount} - {#if tmdbAccount} - - Connected to - - - {:else} -
- -
- {/if} - {/await} - - - - - - - - - - - - +
+

Integrations

+ + + +

Sonarr

+ { + sonarrBaseUrl = detail.baseUrl; + sonarrApiKey = detail.apiKey; + sonarrStale = detail.stale; + }} + /> +
+ +
+
+ + +

Radarr

+ { + radarrBaseUrl = detail.baseUrl; + radarrApiKey = detail.apiKey; + radarrStale = detail.stale; + }} + /> +
+ +
+
- -

Jellyfin

- { - jellyfinBaseUrl = detail.baseUrl; - jellyfinApiKey = detail.apiKey; - jellyfinStale = detail.stale; - }} - on:click-user={({ detail }) => - createModal(JellyfinIntegrationUsersDialog, { - selectedUser: detail.user, - users: detail.users, - handleSelectUser: (u) => (jellyfinUser = u) - })} - /> -
- -
+ + +

Tmdb Account

+ {#await tmdbAccount then tmdbAccount} + {#if tmdbAccount} + + Connected to + + + {:else} +
+ +
+ {/if} + {/await} +
+ + +

Jellyfin

+ { + jellyfinBaseUrl = detail.baseUrl; + jellyfinApiKey = detail.apiKey; + jellyfinStale = detail.stale; + }} + on:click-user={({ detail }) => + createModal(JellyfinIntegrationUsersDialog, { + selectedUser: detail.user, + users: detail.users, + handleSelectUser: (u) => (jellyfinUser = u) + })} + /> +
+ +
+
-
+
@@ -320,9 +351,7 @@
Tizen media key: {tizenMediaKey}
{/if}
- +
diff --git a/src/lib/pages/OnboardingPage.svelte b/src/lib/pages/OnboardingPage.svelte index bbc259c..a2e2d0a 100644 --- a/src/lib/pages/OnboardingPage.svelte +++ b/src/lib/pages/OnboardingPage.svelte @@ -10,7 +10,6 @@ import SelectItem from '../components/SelectItem.svelte'; import { sonarrApi } from '../apis/sonarr/sonarr-api'; import { radarrApi } from '../apis/radarr/radarr-api'; - import { get } from 'svelte/store'; import { useTabs } from '../components/Tab/Tab'; import classNames from 'classnames'; import { user } from '../stores/user.store'; @@ -231,208 +230,216 @@ - -

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. -
+ +

Connect a TMDB Account

+
+ Connect to TMDB for personalized recommendations based on your movie reviews and + preferences. +
-
- {#await connectedTmdbAccount then account} - {#if account} - { - tab.set(Tabs.TmdbConnect); - handleGenerateTMDBLink(); - }}>Logged in as - {:else} - + {/if} + {/await} + + +
+
+ + tab.set(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} - {/await} - + {: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} + tab.set(Tabs.SelectUser)} + > + User + + {/if} + {/await} + + {#if jellyfinError} +
{jellyfinError}
{/if} - - + + + + {#if jellyfinBaseUrl && jellyfinApiKey && jellyfinUser} + + {:else} + + {/if} + +
+ tab.set(Tabs.Jellyfin)}> +

Select User

+ {#await jellyfinUsers then users} + {#each users as user} + { + jellyfinUser = user; + tab.set(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} + +
+ + +
+ +
+

All Set!

+
Reiverr is now ready to use.
+ + + +
+ +
+
+
-
- - tab.set(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} - tab.set(Tabs.SelectUser)} - > - User - - {/if} - {/await} - - {#if jellyfinError} -
{jellyfinError}
- {/if} - - - - {#if jellyfinBaseUrl && jellyfinApiKey && jellyfinUser} - - {:else} - - {/if} - -
- tab.set(Tabs.Jellyfin)} class={tabContainer}> -

Select User

- {#await jellyfinUsers then users} - {#each users as user} - { - jellyfinUser = user; - tab.set(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} - -
- - -
- -
-

All Set!

-
Reiverr is now ready to use.
- - - -
- -
-
-
+
diff --git a/src/lib/pages/UsersPage.svelte b/src/lib/pages/UsersPage.svelte index 4bc1e06..f0d2e0d 100644 --- a/src/lib/pages/UsersPage.svelte +++ b/src/lib/pages/UsersPage.svelte @@ -12,7 +12,8 @@ import AddUserDialog from '../components/Dialog/AddUserDialog.svelte'; import Login from '../components/Login.svelte'; import { Plus, Trash } from 'radix-icons-svelte'; - import AddElementOverlay from '../components/AddElementOverlay.svelte'; + import ProfileIcon from '../components/ProfileIcon.svelte'; + import { profilePictures } from '../profile-pictures'; $: users = getUsers($sessions.sessions); @@ -40,38 +41,24 @@ {#each users as item} {@const user = item.user} - user && handleSwitchUser(item)}> - -
-
- {user?.name} -
- + user && handleSwitchUser(item)}> + user && handleSwitchUser(item)} + /> +
+ {user?.name} +
{/each} - createModal(AddUserDialog, {})}> - -
-
- -
- - - - - + createModal(AddUserDialog, {})} + icon={Plus} + />