refactor: User and session management
This commit is contained in:
@@ -7,13 +7,15 @@ import {
|
|||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { SignInDto } from '../user/user.dto';
|
import { SignInDto, UserDto } from '../user/user.dto';
|
||||||
import { ApiOkResponse, ApiProperty } from '@nestjs/swagger';
|
import { ApiOkResponse, ApiProperty } from '@nestjs/swagger';
|
||||||
import { ApiException } from '@nanogiants/nestjs-swagger-api-exception-decorator';
|
import { ApiException } from '@nanogiants/nestjs-swagger-api-exception-decorator';
|
||||||
|
|
||||||
export class SignInResponse {
|
export class SignInResponse {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
|
@ApiProperty()
|
||||||
|
user: UserDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Controller('auth')
|
@Controller('auth')
|
||||||
@@ -24,12 +26,14 @@ export class AuthController {
|
|||||||
@ApiOkResponse({ description: 'User found', type: SignInResponse })
|
@ApiOkResponse({ description: 'User found', type: SignInResponse })
|
||||||
@ApiException(() => UnauthorizedException)
|
@ApiException(() => UnauthorizedException)
|
||||||
async signIn(@Body() signInDto: SignInDto) {
|
async signIn(@Body() signInDto: SignInDto) {
|
||||||
const { token } = await this.authService.signIn(
|
const { token, user } = await this.authService.signIn(
|
||||||
signInDto.name,
|
signInDto.name,
|
||||||
signInDto.password,
|
signInDto.password,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accessToken: token,
|
accessToken: token,
|
||||||
|
user: UserDto.fromEntity(user),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export class AuthService {
|
|||||||
password: string,
|
password: string,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
token: string;
|
token: string;
|
||||||
|
user: User;
|
||||||
}> {
|
}> {
|
||||||
let user = await this.userService.findOneByName(name);
|
let user = await this.userService.findOneByName(name);
|
||||||
if (!user && (await this.userService.noPreviousAdmins()))
|
if (!user && (await this.userService.noPreviousAdmins()))
|
||||||
@@ -34,6 +35,7 @@ export class AuthService {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
token: await this.jwtService.signAsync(payload),
|
token: await this.jwtService.signAsync(payload),
|
||||||
|
user,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import I18n from './lib/components/Lang/I18n.svelte';
|
import I18n from './lib/components/Lang/I18n.svelte';
|
||||||
import { appState } from './lib/stores/app-state.store';
|
|
||||||
import { handleKeyboardNavigation } from './lib/selectable';
|
import { handleKeyboardNavigation } from './lib/selectable';
|
||||||
import LoginPage from './lib/pages/LoginPage.svelte';
|
import LoginPage from './lib/pages/LoginPage.svelte';
|
||||||
import ModalStack from './lib/components/Modal/ModalStack.svelte';
|
import ModalStack from './lib/components/Modal/ModalStack.svelte';
|
||||||
import NavigationDebugger from './lib/components/DebugElements.svelte';
|
import NavigationDebugger from './lib/components/DebugElements.svelte';
|
||||||
import StackRouter from './lib/components/StackRouter/StackRouter.svelte';
|
import StackRouter from './lib/components/StackRouter/StackRouter.svelte';
|
||||||
import { defaultStackRouter } from './lib/components/StackRouter/StackRouter';
|
import { stackRouter } from './lib/components/StackRouter/StackRouter';
|
||||||
import OnboardingPage from './lib/pages/OnboardingPage.svelte';
|
import OnboardingPage from './lib/pages/OnboardingPage.svelte';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { skippedVersion } from './lib/stores/localstorage.store';
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import NotificationStack from './lib/components/Notifications/NotificationStack.svelte';
|
import NotificationStack from './lib/components/Notifications/NotificationStack.svelte';
|
||||||
import { createModal } from './lib/components/Modal/modal.store';
|
import { createModal } from './lib/components/Modal/modal.store';
|
||||||
import UpdateDialog from './lib/components/Dialog/UpdateDialog.svelte';
|
import UpdateDialog from './lib/components/Dialog/UpdateDialog.svelte';
|
||||||
import { localSettings } from './lib/stores/localstorage.store';
|
import { localSettings } from './lib/stores/localstorage.store';
|
||||||
|
import { user } from './lib/stores/user.store';
|
||||||
|
import { sessions } from './lib/stores/session.store';
|
||||||
|
|
||||||
appState.subscribe((s) => console.log('appState', s));
|
user.subscribe((s) => console.log('user', s));
|
||||||
|
sessions.subscribe((s) => console.log('sessions', s));
|
||||||
|
|
||||||
// onMount(() => {
|
// onMount(() => {
|
||||||
// if (isTizen()) {
|
// if (isTizen()) {
|
||||||
@@ -55,7 +56,7 @@
|
|||||||
|
|
||||||
<I18n />
|
<I18n />
|
||||||
<!--<Container class="w-full h-full overflow-auto text-white scrollbar-hide">-->
|
<!--<Container class="w-full h-full overflow-auto text-white scrollbar-hide">-->
|
||||||
{#if $appState.user === undefined}
|
{#if $user === undefined}
|
||||||
<div class="h-full w-full flex flex-col items-center justify-center">
|
<div class="h-full w-full flex flex-col items-center justify-center">
|
||||||
<div class="flex items-center justify-center hover:text-inherit selectable rounded-sm mb-2">
|
<div class="flex items-center justify-center hover:text-inherit selectable rounded-sm mb-2">
|
||||||
<div class="rounded-full bg-amber-300 h-4 w-4 mr-2" />
|
<div class="rounded-full bg-amber-300 h-4 w-4 mr-2" />
|
||||||
@@ -63,9 +64,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>Loading...</div>
|
<div>Loading...</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if $appState.user === null}
|
{:else if $user === null}
|
||||||
<LoginPage />
|
<LoginPage />
|
||||||
{:else if $appState.user.onboardingDone === false}
|
{:else if $user.onboardingDone === false}
|
||||||
<OnboardingPage />
|
<OnboardingPage />
|
||||||
{:else}
|
{:else}
|
||||||
<!-- <Router primary={false}>-->
|
<!-- <Router primary={false}>-->
|
||||||
@@ -88,7 +89,7 @@
|
|||||||
<!-- <Route path="*">-->
|
<!-- <Route path="*">-->
|
||||||
<!-- <PageNotFound />-->
|
<!-- <PageNotFound />-->
|
||||||
<!-- </Route>-->
|
<!-- </Route>-->
|
||||||
<StackRouter stack={defaultStackRouter} />
|
<StackRouter stack={stackRouter} />
|
||||||
<!-- </Container>-->
|
<!-- </Container>-->
|
||||||
<!-- </Router>-->
|
<!-- </Router>-->
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import createClient from 'openapi-fetch';
|
|||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import type { components, paths } from './jellyfin.generated';
|
import type { components, paths } from './jellyfin.generated';
|
||||||
import type { Api } from '../api.interface';
|
import type { Api } from '../api.interface';
|
||||||
import { appState } from '../../stores/app-state.store';
|
import { user } from '../../stores/user.store';
|
||||||
import type { DeviceProfile } from './playback-profiles';
|
import type { DeviceProfile } from './playback-profiles';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { log } from '../../utils';
|
import { log } from '../../utils';
|
||||||
@@ -16,7 +16,7 @@ export const JELLYFIN_DEVICE_ID = 'Reiverr Client';
|
|||||||
|
|
||||||
export class JellyfinApi implements Api<paths> {
|
export class JellyfinApi implements Api<paths> {
|
||||||
getClient() {
|
getClient() {
|
||||||
const jellyfinSettings = get(appState).user?.settings.jellyfin;
|
const jellyfinSettings = get(user)?.settings.jellyfin;
|
||||||
const baseUrl = jellyfinSettings?.baseUrl;
|
const baseUrl = jellyfinSettings?.baseUrl;
|
||||||
const apiKey = jellyfinSettings?.apiKey;
|
const apiKey = jellyfinSettings?.apiKey;
|
||||||
|
|
||||||
@@ -29,15 +29,15 @@ export class JellyfinApi implements Api<paths> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getUserId() {
|
getUserId() {
|
||||||
return get(appState).user?.settings.jellyfin.userId || '';
|
return get(user)?.settings.jellyfin.userId || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
getApiKey() {
|
getApiKey() {
|
||||||
return get(appState).user?.settings.jellyfin.apiKey || '';
|
return get(user)?.settings.jellyfin.apiKey || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
getBaseUrl() {
|
getBaseUrl() {
|
||||||
return get(appState).user?.settings.jellyfin.baseUrl || '';
|
return get(user)?.settings.jellyfin.baseUrl || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
getContinueWatching = async (type?: Type): Promise<JellyfinItem[] | undefined> =>
|
getContinueWatching = async (type?: Type): Promise<JellyfinItem[] | undefined> =>
|
||||||
@@ -102,7 +102,7 @@ export class JellyfinApi implements Api<paths> {
|
|||||||
|
|
||||||
getPosterUrl(item: JellyfinItem, quality = 100, original = false) {
|
getPosterUrl(item: JellyfinItem, quality = 100, original = false) {
|
||||||
return item.ImageTags?.Primary
|
return item.ImageTags?.Primary
|
||||||
? `${get(appState).user?.settings.jellyfin.baseUrl}/Items/${
|
? `${get(user)?.settings.jellyfin.baseUrl}/Items/${
|
||||||
item?.Id
|
item?.Id
|
||||||
}/Images/Primary?quality=${quality}${original ? '' : '&fillWidth=432'}&tag=${
|
}/Images/Primary?quality=${quality}${original ? '' : '&fillWidth=432'}&tag=${
|
||||||
item?.ImageTags?.Primary
|
item?.ImageTags?.Primary
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { settings } from '../../stores/settings.store';
|
|||||||
import type { components, paths } from './radarr.generated';
|
import type { components, paths } from './radarr.generated';
|
||||||
import { getTmdbMovie } from '../tmdb/tmdb-api';
|
import { getTmdbMovie } from '../tmdb/tmdb-api';
|
||||||
import { log } from '../../utils';
|
import { log } from '../../utils';
|
||||||
import { appState } from '../../stores/app-state.store';
|
|
||||||
import type { Api } from '../api.interface';
|
import type { Api } from '../api.interface';
|
||||||
|
import { user } from '../../stores/user.store';
|
||||||
|
|
||||||
export const movieAvailabilities = [
|
export const movieAvailabilities = [
|
||||||
// 'tba',
|
// 'tba',
|
||||||
@@ -39,7 +39,7 @@ export interface RadarrMovieOptions {
|
|||||||
|
|
||||||
export class RadarrApi implements Api<paths> {
|
export class RadarrApi implements Api<paths> {
|
||||||
getClient() {
|
getClient() {
|
||||||
const radarrSettings = get(appState).user?.settings.radarr;
|
const radarrSettings = get(user)?.settings.radarr;
|
||||||
const baseUrl = radarrSettings?.baseUrl;
|
const baseUrl = radarrSettings?.baseUrl;
|
||||||
const apiKey = radarrSettings?.apiKey;
|
const apiKey = radarrSettings?.apiKey;
|
||||||
|
|
||||||
@@ -52,11 +52,11 @@ export class RadarrApi implements Api<paths> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getBaseUrl() {
|
getBaseUrl() {
|
||||||
return get(appState)?.user?.settings?.radarr.baseUrl || '';
|
return get(user)?.settings?.radarr.baseUrl || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
getSettings() {
|
getSettings() {
|
||||||
return get(appState).user?.settings.radarr;
|
return get(user)?.settings.radarr;
|
||||||
}
|
}
|
||||||
|
|
||||||
getMovieByTmdbId = (tmdbId: number): Promise<RadarrMovie | undefined> =>
|
getMovieByTmdbId = (tmdbId: number): Promise<RadarrMovie | undefined> =>
|
||||||
@@ -277,10 +277,10 @@ export class RadarrApi implements Api<paths> {
|
|||||||
) =>
|
) =>
|
||||||
axios
|
axios
|
||||||
.get<components['schemas']['QualityProfileResource'][]>(
|
.get<components['schemas']['QualityProfileResource'][]>(
|
||||||
(baseUrl || get(appState)?.user?.settings.radarr.baseUrl) + '/api/v3/qualityprofile',
|
(baseUrl || get(user)?.settings.radarr.baseUrl) + '/api/v3/qualityprofile',
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
'X-Api-Key': apiKey || get(appState)?.user?.settings.radarr.apiKey
|
'X-Api-Key': apiKey || get(user)?.settings.radarr.apiKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
import createClient from 'openapi-fetch';
|
import createClient from 'openapi-fetch';
|
||||||
import type { components, paths } from './reiverr.generated';
|
import type { components, paths } from './reiverr.generated';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { appState } from '../../stores/app-state.store';
|
|
||||||
import type { Api } from '../api.interface';
|
import type { Api } from '../api.interface';
|
||||||
|
import { sessions } from '../../stores/session.store';
|
||||||
|
|
||||||
export type ReiverrUser = components['schemas']['UserDto'];
|
export type ReiverrUser = components['schemas']['UserDto'];
|
||||||
export type ReiverrSettings = ReiverrUser['settings'];
|
export type ReiverrSettings = ReiverrUser['settings'];
|
||||||
|
|
||||||
export class ReiverrApi implements Api<paths> {
|
export class ReiverrApi implements Api<paths> {
|
||||||
getClient(basePath?: string, _token?: string) {
|
getClient(basePath?: string, _token?: string) {
|
||||||
const token = _token || get(appState).token;
|
const session = get(sessions).activeSession;
|
||||||
|
const token = _token || session?.token;
|
||||||
|
|
||||||
return createClient<paths>({
|
return createClient<paths>({
|
||||||
baseUrl: (basePath || get(appState).serverBaseUrl) + '/api',
|
baseUrl: (basePath || session?.baseUrl) + '/api',
|
||||||
...(token && {
|
...(token && {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: 'Bearer ' + token
|
Authorization: 'Bearer ' + token
|
||||||
@@ -45,7 +46,7 @@ export class ReiverrApi implements Api<paths> {
|
|||||||
?.PUT('/user/{id}', {
|
?.PUT('/user/{id}', {
|
||||||
params: {
|
params: {
|
||||||
path: {
|
path: {
|
||||||
id: get(appState).user?.id as string
|
id: user.id
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
body: user
|
body: user
|
||||||
|
|||||||
14
src/lib/apis/reiverr/reiverr.generated.d.ts
vendored
14
src/lib/apis/reiverr/reiverr.generated.d.ts
vendored
@@ -13,9 +13,6 @@ export interface paths {
|
|||||||
get: operations["UserController_findById"];
|
get: operations["UserController_findById"];
|
||||||
put: operations["UserController_updateUser"];
|
put: operations["UserController_updateUser"];
|
||||||
};
|
};
|
||||||
"/user/isSetupDone": {
|
|
||||||
get: operations["UserController_isSetupDone"];
|
|
||||||
};
|
|
||||||
"/auth": {
|
"/auth": {
|
||||||
post: operations["AuthController_signIn"];
|
post: operations["AuthController_signIn"];
|
||||||
};
|
};
|
||||||
@@ -82,6 +79,7 @@ export interface components {
|
|||||||
};
|
};
|
||||||
SignInResponse: {
|
SignInResponse: {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
|
user: components["schemas"]["UserDto"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
responses: never;
|
responses: never;
|
||||||
@@ -178,16 +176,6 @@ export interface operations {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
UserController_isSetupDone: {
|
|
||||||
responses: {
|
|
||||||
/** @description Setup done */
|
|
||||||
200: {
|
|
||||||
content: {
|
|
||||||
"application/json": boolean;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
AuthController_signIn: {
|
AuthController_signIn: {
|
||||||
requestBody: {
|
requestBody: {
|
||||||
content: {
|
content: {
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { getTmdbSeries, tmdbApi } from '../tmdb/tmdb-api';
|
|||||||
import type { components, paths } from './sonarr.generated';
|
import type { components, paths } from './sonarr.generated';
|
||||||
import { log } from '../../utils';
|
import { log } from '../../utils';
|
||||||
import type { Api, ApiAsync } from '../api.interface';
|
import type { Api, ApiAsync } from '../api.interface';
|
||||||
import { appState } from '../../stores/app-state.store';
|
|
||||||
import { createLocalStorageStore } from '../../stores/localstorage.store';
|
import { createLocalStorageStore } from '../../stores/localstorage.store';
|
||||||
|
import { user } from '../../stores/user.store';
|
||||||
|
|
||||||
export const sonarrMonitorOptions = [
|
export const sonarrMonitorOptions = [
|
||||||
'unknown',
|
'unknown',
|
||||||
@@ -52,7 +52,7 @@ const tmdbToTvdbCache = createLocalStorageStore<Record<number, number>>('tmdb-to
|
|||||||
|
|
||||||
export class SonarrApi implements ApiAsync<paths> {
|
export class SonarrApi implements ApiAsync<paths> {
|
||||||
async getClient() {
|
async getClient() {
|
||||||
await appState.ready;
|
// await appState.ready;
|
||||||
const sonarrSettings = this.getSettings();
|
const sonarrSettings = this.getSettings();
|
||||||
const baseUrl = this.getBaseUrl();
|
const baseUrl = this.getBaseUrl();
|
||||||
const apiKey = sonarrSettings?.apiKey;
|
const apiKey = sonarrSettings?.apiKey;
|
||||||
@@ -66,15 +66,15 @@ export class SonarrApi implements ApiAsync<paths> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getBaseUrl() {
|
getBaseUrl() {
|
||||||
return get(appState)?.user?.settings?.sonarr.baseUrl || '';
|
return get(user)?.settings?.sonarr.baseUrl || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
getSettings() {
|
getSettings() {
|
||||||
return get(appState).user?.settings.sonarr;
|
return get(user)?.settings.sonarr;
|
||||||
}
|
}
|
||||||
|
|
||||||
getApiKey() {
|
getApiKey() {
|
||||||
return get(appState).user?.settings.sonarr.apiKey;
|
return get(user)?.settings.sonarr.apiKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
tmdbToTvdb = async (tmdbId: number) => {
|
tmdbToTvdb = async (tmdbId: number) => {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { TMDB_API_KEY, TMDB_BACKDROP_SMALL } from '../../constants';
|
|||||||
import { settings } from '../../stores/settings.store';
|
import { settings } from '../../stores/settings.store';
|
||||||
import type { TitleType } from '../../types';
|
import type { TitleType } from '../../types';
|
||||||
import type { Api } from '../api.interface';
|
import type { Api } from '../api.interface';
|
||||||
import { appState } from '../../stores/app-state.store';
|
import { user } from '../../stores/user.store';
|
||||||
|
|
||||||
const CACHE_ONE_DAY = 'max-age=86400';
|
const CACHE_ONE_DAY = 'max-age=86400';
|
||||||
const CACHE_FOUR_DAYS = 'max-age=345600';
|
const CACHE_FOUR_DAYS = 'max-age=345600';
|
||||||
@@ -73,7 +73,7 @@ export class TmdbApi implements Api<paths> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static getClient4l() {
|
static getClient4l() {
|
||||||
const sessionId = get(appState)?.user?.settings.tmdb.sessionId;
|
const sessionId = get(user)?.settings.tmdb.sessionId;
|
||||||
|
|
||||||
return createClient<paths4>({
|
return createClient<paths4>({
|
||||||
baseUrl: 'https://api.themoviedb.org',
|
baseUrl: 'https://api.themoviedb.org',
|
||||||
@@ -96,11 +96,11 @@ export class TmdbApi implements Api<paths> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getSessionId() {
|
getSessionId() {
|
||||||
return get(appState)?.user?.settings.tmdb.sessionId;
|
return get(user)?.settings.tmdb.sessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
getUserId() {
|
getUserId() {
|
||||||
return get(appState)?.user?.settings.tmdb.userId;
|
return get(user)?.settings.tmdb.userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// MOVIES
|
// MOVIES
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
export let topmost = true;
|
export let topmost = true;
|
||||||
|
export let sidebar = true;
|
||||||
|
|
||||||
// Top element, that when focused and back is pressed, will exit the modal
|
// Top element, that when focused and back is pressed, will exit the modal
|
||||||
const topSelectable = useRegistrar();
|
const topSelectable = useRegistrar();
|
||||||
@@ -41,7 +42,9 @@
|
|||||||
direction="horizontal"
|
direction="horizontal"
|
||||||
on:mount
|
on:mount
|
||||||
>
|
>
|
||||||
<Sidebar />
|
{#if sidebar}
|
||||||
|
<Sidebar />
|
||||||
|
{/if}
|
||||||
<Container on:back={handleGoToTop} focusOnMount class={classNames($$restProps.class)}>
|
<Container on:back={handleGoToTop} focusOnMount class={classNames($$restProps.class)}>
|
||||||
<slot {handleGoBack} registrar={topSelectable.registrar} />
|
<slot {handleGoBack} registrar={topSelectable.registrar} />
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
class={classNames('absolute inset-0 bg-center bg-cover', {
|
class={classNames('absolute inset-0 bg-center bg-cover', {
|
||||||
'opacity-100': visibleIndex === i,
|
'opacity-100': visibleIndex === i,
|
||||||
'opacity-0': visibleIndex !== i,
|
'opacity-0': visibleIndex !== i,
|
||||||
'scale-125': !hasFocus
|
'scale-110': !hasFocus
|
||||||
})}
|
})}
|
||||||
style={`background-image: url('${url}'); transition: opacity 500ms, transform 500ms;`}
|
style={`background-image: url('${url}'); transition: opacity 500ms, transform 500ms;`}
|
||||||
/>
|
/>
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<div
|
<div
|
||||||
class={classNames('flex overflow-hidden h-full w-full transition-transform duration-500', {
|
class={classNames('flex overflow-hidden h-full w-full transition-transform duration-500', {
|
||||||
'scale-125': !hasFocus
|
'scale-110': !hasFocus
|
||||||
})}
|
})}
|
||||||
style="perspective: 1px; -webkit-perspective: 1px;"
|
style="perspective: 1px; -webkit-perspective: 1px;"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import TextField from '../TextField.svelte';
|
import TextField from '../TextField.svelte';
|
||||||
import { appState } from '../../stores/app-state.store';
|
import { user } from '../../stores/user.store';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import SelectField from '../SelectField.svelte';
|
import SelectField from '../SelectField.svelte';
|
||||||
import { jellyfinApi, type JellyfinUser } from '../../apis/jellyfin/jellyfin-api';
|
import { jellyfinApi, type JellyfinUser } from '../../apis/jellyfin/jellyfin-api';
|
||||||
@@ -20,9 +20,9 @@
|
|||||||
let jellyfinUsers: Promise<JellyfinUser[]> | undefined = undefined;
|
let jellyfinUsers: Promise<JellyfinUser[]> | undefined = undefined;
|
||||||
export let jellyfinUser: JellyfinUser | undefined;
|
export let jellyfinUser: JellyfinUser | undefined;
|
||||||
|
|
||||||
appState.subscribe((appState) => {
|
user.subscribe((user) => {
|
||||||
baseUrl = baseUrl || appState.user?.settings.jellyfin.baseUrl || '';
|
baseUrl = baseUrl || user?.settings.jellyfin.baseUrl || '';
|
||||||
apiKey = apiKey || appState.user?.settings.jellyfin.apiKey || '';
|
apiKey = apiKey || user?.settings.jellyfin.apiKey || '';
|
||||||
|
|
||||||
originalBaseUrl = baseUrl;
|
originalBaseUrl = baseUrl;
|
||||||
originalApiKey = apiKey;
|
originalApiKey = apiKey;
|
||||||
@@ -35,9 +35,7 @@
|
|||||||
baseUrl,
|
baseUrl,
|
||||||
apiKey,
|
apiKey,
|
||||||
stale:
|
stale:
|
||||||
baseUrl && apiKey
|
baseUrl && apiKey ? jellyfinUser?.Id !== get(user)?.settings.jellyfin.userId : !jellyfinUser
|
||||||
? jellyfinUser?.Id !== get(appState).user?.settings.jellyfin.userId
|
|
||||||
: !jellyfinUser
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleChange() {
|
function handleChange() {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import TextField from '../TextField.svelte';
|
import TextField from '../TextField.svelte';
|
||||||
import { appState } from '../../stores/app-state.store';
|
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import { radarrApi } from '../../apis/radarr/radarr-api';
|
import { radarrApi } from '../../apis/radarr/radarr-api';
|
||||||
|
import { user } from '../../stores/user.store';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{
|
const dispatch = createEventDispatcher<{
|
||||||
change: { baseUrl: string; apiKey: string; stale: boolean };
|
change: { baseUrl: string; apiKey: string; stale: boolean };
|
||||||
@@ -16,9 +16,9 @@
|
|||||||
let error = '';
|
let error = '';
|
||||||
let healthCheck: Promise<boolean> | undefined;
|
let healthCheck: Promise<boolean> | undefined;
|
||||||
|
|
||||||
appState.subscribe((appState) => {
|
user.subscribe((user) => {
|
||||||
baseUrl = baseUrl || appState.user?.settings.radarr.baseUrl || '';
|
baseUrl = baseUrl || user?.settings.radarr.baseUrl || '';
|
||||||
apiKey = apiKey || appState.user?.settings.radarr.apiKey || '';
|
apiKey = apiKey || user?.settings.radarr.apiKey || '';
|
||||||
|
|
||||||
originalBaseUrl = baseUrl;
|
originalBaseUrl = baseUrl;
|
||||||
originalApiKey = apiKey;
|
originalApiKey = apiKey;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import TextField from '../TextField.svelte';
|
import TextField from '../TextField.svelte';
|
||||||
import { appState } from '../../stores/app-state.store';
|
|
||||||
import { sonarrApi } from '../../apis/sonarr/sonarr-api';
|
import { sonarrApi } from '../../apis/sonarr/sonarr-api';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import { user } from '../../stores/user.store';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{
|
const dispatch = createEventDispatcher<{
|
||||||
change: { baseUrl: string; apiKey: string; stale: boolean };
|
change: { baseUrl: string; apiKey: string; stale: boolean };
|
||||||
@@ -16,9 +16,9 @@
|
|||||||
let error = '';
|
let error = '';
|
||||||
let healthCheck: Promise<boolean> | undefined;
|
let healthCheck: Promise<boolean> | undefined;
|
||||||
|
|
||||||
appState.subscribe((appState) => {
|
user.subscribe((user) => {
|
||||||
baseUrl = baseUrl || appState.user?.settings.sonarr.baseUrl || '';
|
baseUrl = baseUrl || user?.settings.sonarr.baseUrl || '';
|
||||||
apiKey = apiKey || appState.user?.settings.sonarr.apiKey || '';
|
apiKey = apiKey || user?.settings.sonarr.apiKey || '';
|
||||||
|
|
||||||
originalBaseUrl = baseUrl;
|
originalBaseUrl = baseUrl;
|
||||||
originalApiKey = apiKey;
|
originalApiKey = apiKey;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import TextField from '../TextField.svelte';
|
import TextField from '../TextField.svelte';
|
||||||
import { appState } from '../../stores/app-state.store';
|
|
||||||
import { sonarrApi } from '../../apis/sonarr/sonarr-api';
|
import { sonarrApi } from '../../apis/sonarr/sonarr-api';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import { user } from '../../stores/user.store';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{
|
const dispatch = createEventDispatcher<{
|
||||||
change: { baseUrl: string; apiKey: string; stale: boolean };
|
change: { baseUrl: string; apiKey: string; stale: boolean };
|
||||||
@@ -16,9 +16,9 @@
|
|||||||
let error = '';
|
let error = '';
|
||||||
let healthCheck: Promise<boolean> | undefined;
|
let healthCheck: Promise<boolean> | undefined;
|
||||||
|
|
||||||
appState.subscribe((appState) => {
|
user.subscribe((user) => {
|
||||||
baseUrl = baseUrl || appState.user?.settings.sonarr.baseUrl || '';
|
baseUrl = baseUrl || user?.settings.sonarr.baseUrl || '';
|
||||||
apiKey = apiKey || appState.user?.settings.sonarr.apiKey || '';
|
apiKey = apiKey || user?.settings.sonarr.apiKey || '';
|
||||||
|
|
||||||
originalBaseUrl = baseUrl;
|
originalBaseUrl = baseUrl;
|
||||||
originalApiKey = apiKey;
|
originalApiKey = apiKey;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Container from '../../../Container.svelte';
|
import Container from '../../../Container.svelte';
|
||||||
import { appState } from '../../stores/app-state.store';
|
|
||||||
import { tmdbApi } from '../../apis/tmdb/tmdb-api';
|
import { tmdbApi } from '../../apis/tmdb/tmdb-api';
|
||||||
import Button from '../Button.svelte';
|
import Button from '../Button.svelte';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import { ExternalLink } from 'radix-icons-svelte';
|
import { ExternalLink } from 'radix-icons-svelte';
|
||||||
|
import { user } from '../../stores/user.store';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{ connected: null }>();
|
const dispatch = createEventDispatcher<{ connected: null }>();
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
return; // TODO add notification
|
return; // TODO add notification
|
||||||
}
|
}
|
||||||
|
|
||||||
appState.updateUser((prev) => ({
|
user.updateUser((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
settings: {
|
settings: {
|
||||||
...prev.settings,
|
...prev.settings,
|
||||||
|
|||||||
@@ -5,14 +5,28 @@
|
|||||||
DotFilled,
|
DotFilled,
|
||||||
Gear,
|
Gear,
|
||||||
Laptop,
|
Laptop,
|
||||||
MagnifyingGlass
|
MagnifyingGlass,
|
||||||
|
Person
|
||||||
} from 'radix-icons-svelte';
|
} from 'radix-icons-svelte';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { get, type Readable, writable, type Writable } from 'svelte/store';
|
import { get, type Readable, writable, type Writable } from 'svelte/store';
|
||||||
import Container from '../../../Container.svelte';
|
import Container from '../../../Container.svelte';
|
||||||
import { registrars, Selectable } from '../../selectable';
|
import { registrars, Selectable } from '../../selectable';
|
||||||
import { defaultStackRouter, navigate } from '../StackRouter/StackRouter';
|
import { stackRouter, navigate } from '../StackRouter/StackRouter';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
import { useTabs } from '../Tab/Tab';
|
||||||
|
import { user } from '../../stores/user.store';
|
||||||
|
|
||||||
|
enum Tabs {
|
||||||
|
Users,
|
||||||
|
Series,
|
||||||
|
Movies,
|
||||||
|
Library,
|
||||||
|
Search,
|
||||||
|
Manage
|
||||||
|
}
|
||||||
|
|
||||||
|
const tab = useTabs(Tabs.Series);
|
||||||
|
|
||||||
let selectedIndex = 0;
|
let selectedIndex = 0;
|
||||||
let activeIndex = -1;
|
let activeIndex = -1;
|
||||||
@@ -40,29 +54,31 @@
|
|||||||
selectable.focusChild(index);
|
selectable.focusChild(index);
|
||||||
const path =
|
const path =
|
||||||
{
|
{
|
||||||
0: '/',
|
[Tabs.Users]: '/users',
|
||||||
1: '/movies',
|
[Tabs.Series]: '/',
|
||||||
2: '/library',
|
[Tabs.Movies]: '/movies',
|
||||||
3: '/search',
|
[Tabs.Library]: '/library',
|
||||||
4: '/manage'
|
[Tabs.Search]: '/search',
|
||||||
|
[Tabs.Manage]: '/manage'
|
||||||
}[index] || '/';
|
}[index] || '/';
|
||||||
navigate(path);
|
navigate(path);
|
||||||
selectedIndex = index;
|
selectedIndex = index;
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
defaultStackRouter.subscribe((r) => {
|
// Set active tab based on bottommost page
|
||||||
|
stackRouter.subscribe((r) => {
|
||||||
const bottomPage = r[0];
|
const bottomPage = r[0];
|
||||||
console.log('bottomPage', bottomPage);
|
|
||||||
if (bottomPage) {
|
if (bottomPage) {
|
||||||
activeIndex =
|
activeIndex =
|
||||||
{
|
{
|
||||||
'/': 0,
|
'/users': Tabs.Users,
|
||||||
'/series': 0,
|
'/': Tabs.Series,
|
||||||
'/movies': 1,
|
'/series': Tabs.Series,
|
||||||
'/library': 2,
|
'/movies': Tabs.Movies,
|
||||||
'/search': 3,
|
'/library': Tabs.Library,
|
||||||
'/manage': 4
|
'/search': Tabs.Search,
|
||||||
|
'/manage': Tabs.Manage
|
||||||
}[bottomPage.route.path] ?? -1;
|
}[bottomPage.route.path] ?? -1;
|
||||||
selectable.focusIndex.set(activeIndex);
|
selectable.focusIndex.set(activeIndex);
|
||||||
selectedIndex = activeIndex;
|
selectedIndex = activeIndex;
|
||||||
@@ -103,18 +119,61 @@
|
|||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Container
|
||||||
|
class="w-full h-12 cursor-pointer"
|
||||||
|
on:clickOrSelect={selectIndex(Tabs.Users)}
|
||||||
|
let:hasFocus
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class={classNames(
|
||||||
|
'w-full h-full relative flex items-center justify-center transition-opacity',
|
||||||
|
{
|
||||||
|
'text-primary-500': hasFocus || (!$isNavBarOpen && selectedIndex === Tabs.Users),
|
||||||
|
'text-stone-300 hover:text-primary-500':
|
||||||
|
!hasFocus && !(!$isNavBarOpen && selectedIndex === Tabs.Users),
|
||||||
|
'opacity-0 pointer-events-none': $isNavBarOpen === false,
|
||||||
|
'group-hover:opacity-100 group-hover:pointer-events-auto': true
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div class="absolute inset-y-0 left-2 flex items-center justify-center">
|
||||||
|
<DotFilled
|
||||||
|
class={classNames('text-primary-500', { 'opacity-0': activeIndex !== Tabs.Users })}
|
||||||
|
size={19}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Person class="w-8 h-8" />
|
||||||
|
<span
|
||||||
|
class={classNames(
|
||||||
|
'text-xl font-medium transition-opacity flex items-center absolute inset-y-0 left-20',
|
||||||
|
{
|
||||||
|
'opacity-0 pointer-events-none': $isNavBarOpen === false,
|
||||||
|
'group-hover:opacity-100 group-hover:pointer-events-auto': true
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{$user?.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
|
||||||
<div class={'flex-1 flex flex-col justify-center self-stretch'}>
|
<div class={'flex-1 flex flex-col justify-center self-stretch'}>
|
||||||
<Container class="w-full h-12 cursor-pointer" on:clickOrSelect={selectIndex(0)} let:hasFocus>
|
<Container
|
||||||
|
class="w-full h-12 cursor-pointer"
|
||||||
|
on:clickOrSelect={selectIndex(Tabs.Series)}
|
||||||
|
let:hasFocus
|
||||||
|
focusedChild
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class={classNames('w-full h-full relative flex items-center justify-center', {
|
class={classNames('w-full h-full relative flex items-center justify-center', {
|
||||||
'text-primary-500': hasFocus || (!$isNavBarOpen && selectedIndex === 0),
|
'text-primary-500': hasFocus || (!$isNavBarOpen && selectedIndex === Tabs.Series),
|
||||||
'text-stone-300 hover:text-primary-500':
|
'text-stone-300 hover:text-primary-500':
|
||||||
!hasFocus && !(!$isNavBarOpen && selectedIndex === 0)
|
!hasFocus && !(!$isNavBarOpen && selectedIndex === Tabs.Series)
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div class="absolute inset-y-0 left-2 flex items-center justify-center">
|
<div class="absolute inset-y-0 left-2 flex items-center justify-center">
|
||||||
<DotFilled
|
<DotFilled
|
||||||
class={classNames('text-primary-500', { 'opacity-0': activeIndex !== 0 })}
|
class={classNames('text-primary-500', { 'opacity-0': activeIndex !== Tabs.Series })}
|
||||||
size={19}
|
size={19}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -132,17 +191,21 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
<Container class="w-full h-12 cursor-pointer" on:clickOrSelect={selectIndex(1)} let:hasFocus>
|
<Container
|
||||||
|
class="w-full h-12 cursor-pointer"
|
||||||
|
on:clickOrSelect={selectIndex(Tabs.Movies)}
|
||||||
|
let:hasFocus
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class={classNames('w-full h-full relative flex items-center justify-center', {
|
class={classNames('w-full h-full relative flex items-center justify-center', {
|
||||||
'text-primary-500': hasFocus || (!$isNavBarOpen && selectedIndex === 1),
|
'text-primary-500': hasFocus || (!$isNavBarOpen && selectedIndex === Tabs.Movies),
|
||||||
'text-stone-300 hover:text-primary-500':
|
'text-stone-300 hover:text-primary-500':
|
||||||
!hasFocus && !(!$isNavBarOpen && selectedIndex === 1)
|
!hasFocus && !(!$isNavBarOpen && selectedIndex === Tabs.Movies)
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div class="absolute inset-y-0 left-2 flex items-center justify-center">
|
<div class="absolute inset-y-0 left-2 flex items-center justify-center">
|
||||||
<DotFilled
|
<DotFilled
|
||||||
class={classNames('text-primary-500', { 'opacity-0': activeIndex !== 1 })}
|
class={classNames('text-primary-500', { 'opacity-0': activeIndex !== Tabs.Movies })}
|
||||||
size={19}
|
size={19}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -160,17 +223,21 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
<Container class="w-full h-12 cursor-pointer" on:clickOrSelect={selectIndex(2)} let:hasFocus>
|
<Container
|
||||||
|
class="w-full h-12 cursor-pointer"
|
||||||
|
on:clickOrSelect={selectIndex(Tabs.Library)}
|
||||||
|
let:hasFocus
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class={classNames('w-full h-full relative flex items-center justify-center', {
|
class={classNames('w-full h-full relative flex items-center justify-center', {
|
||||||
'text-primary-500': hasFocus || (!$isNavBarOpen && selectedIndex === 2),
|
'text-primary-500': hasFocus || (!$isNavBarOpen && selectedIndex === Tabs.Library),
|
||||||
'text-stone-300 hover:text-primary-500':
|
'text-stone-300 hover:text-primary-500':
|
||||||
!hasFocus && !(!$isNavBarOpen && selectedIndex === 2)
|
!hasFocus && !(!$isNavBarOpen && selectedIndex === Tabs.Library)
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div class="absolute inset-y-0 left-2 flex items-center justify-center">
|
<div class="absolute inset-y-0 left-2 flex items-center justify-center">
|
||||||
<DotFilled
|
<DotFilled
|
||||||
class={classNames('text-primary-500', { 'opacity-0': activeIndex !== 2 })}
|
class={classNames('text-primary-500', { 'opacity-0': activeIndex !== Tabs.Library })}
|
||||||
size={19}
|
size={19}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -188,17 +255,21 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
<Container class="w-full h-12 cursor-pointer" on:clickOrSelect={selectIndex(3)} let:hasFocus>
|
<Container
|
||||||
|
class="w-full h-12 cursor-pointer"
|
||||||
|
on:clickOrSelect={selectIndex(Tabs.Search)}
|
||||||
|
let:hasFocus
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class={classNames('w-full h-full relative flex items-center justify-center', {
|
class={classNames('w-full h-full relative flex items-center justify-center', {
|
||||||
'text-primary-500': hasFocus || (!$isNavBarOpen && selectedIndex === 3),
|
'text-primary-500': hasFocus || (!$isNavBarOpen && selectedIndex === Tabs.Search),
|
||||||
'text-stone-300 hover:text-primary-500':
|
'text-stone-300 hover:text-primary-500':
|
||||||
!hasFocus && !(!$isNavBarOpen && selectedIndex === 3)
|
!hasFocus && !(!$isNavBarOpen && selectedIndex === Tabs.Search)
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div class="absolute inset-y-0 left-2 flex items-center justify-center">
|
<div class="absolute inset-y-0 left-2 flex items-center justify-center">
|
||||||
<DotFilled
|
<DotFilled
|
||||||
class={classNames('text-primary-500', { 'opacity-0': activeIndex !== 3 })}
|
class={classNames('text-primary-500', { 'opacity-0': activeIndex !== Tabs.Search })}
|
||||||
size={19}
|
size={19}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -218,14 +289,18 @@
|
|||||||
</Container>
|
</Container>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Container class="w-full h-12 cursor-pointer" on:clickOrSelect={selectIndex(4)} let:hasFocus>
|
<Container
|
||||||
|
class="w-full h-12 cursor-pointer"
|
||||||
|
on:clickOrSelect={selectIndex(Tabs.Manage)}
|
||||||
|
let:hasFocus
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class={classNames(
|
class={classNames(
|
||||||
'w-full h-full relative flex items-center justify-center transition-opacity',
|
'w-full h-full relative flex items-center justify-center transition-opacity',
|
||||||
{
|
{
|
||||||
'text-primary-500': hasFocus || (!$isNavBarOpen && selectedIndex === 4),
|
'text-primary-500': hasFocus || (!$isNavBarOpen && selectedIndex === Tabs.Manage),
|
||||||
'text-stone-300 hover:text-primary-500':
|
'text-stone-300 hover:text-primary-500':
|
||||||
!hasFocus && !(!$isNavBarOpen && selectedIndex === 4),
|
!hasFocus && !(!$isNavBarOpen && selectedIndex === Tabs.Manage),
|
||||||
'opacity-0 pointer-events-none': $isNavBarOpen === false,
|
'opacity-0 pointer-events-none': $isNavBarOpen === false,
|
||||||
'group-hover:opacity-100 group-hover:pointer-events-auto': true
|
'group-hover:opacity-100 group-hover:pointer-events-auto': true
|
||||||
}
|
}
|
||||||
@@ -233,7 +308,7 @@
|
|||||||
>
|
>
|
||||||
<div class="absolute inset-y-0 left-2 flex items-center justify-center">
|
<div class="absolute inset-y-0 left-2 flex items-center justify-center">
|
||||||
<DotFilled
|
<DotFilled
|
||||||
class={classNames('text-primary-500', { 'opacity-0': activeIndex !== 4 })}
|
class={classNames('text-primary-500', { 'opacity-0': activeIndex !== Tabs.Manage })}
|
||||||
size={19}
|
size={19}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -251,105 +326,4 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
<!-- <div class={'flex flex-col flex-1 relative z-20 items-center'}>-->
|
|
||||||
<!-- <div class={'flex flex-col flex-1 justify-center self-stretch'}>-->
|
|
||||||
<!-- <Container-->
|
|
||||||
<!-- class={classNames(itemContainer(0, $focusIndex), 'w-full flex justify-center')}-->
|
|
||||||
<!-- on:clickOrSelect={selectIndex(0)}-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- <Laptop class="w-8 h-8" />-->
|
|
||||||
<!-- </Container>-->
|
|
||||||
<!-- <Container-->
|
|
||||||
<!-- class={classNames(itemContainer(1, $focusIndex), 'w-full flex justify-center')}-->
|
|
||||||
<!-- on:clickOrSelect={selectIndex(1)}-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- <CardStack class="w-8 h-8" />-->
|
|
||||||
<!-- </Container>-->
|
|
||||||
<!-- <Container-->
|
|
||||||
<!-- class={classNames(itemContainer(2, $focusIndex), 'w-full flex justify-center')}-->
|
|
||||||
<!-- on:clickOrSelect={selectIndex(2)}-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- <Bookmark class="w-8 h-8" />-->
|
|
||||||
<!-- </Container>-->
|
|
||||||
<!-- <Container-->
|
|
||||||
<!-- class={classNames(itemContainer(3, $focusIndex), 'w-full flex justify-center')}-->
|
|
||||||
<!-- on:clickOrSelect={selectIndex(3)}-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- <MagnifyingGlass class="w-8 h-8" />-->
|
|
||||||
<!-- </Container>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
|
|
||||||
<!-- <Container-->
|
|
||||||
<!-- class={classNames(itemContainer(4, $focusIndex), 'w-full flex justify-center')}-->
|
|
||||||
<!-- on:clickOrSelect={selectIndex(4)}-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- <Gear class="w-8 h-8" />-->
|
|
||||||
<!-- </Container>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
|
|
||||||
<!-- <div-->
|
|
||||||
<!-- class={classNames(-->
|
|
||||||
<!-- 'absolute inset-y-0 left-0 pl-[64px] pr-96 z-10 transition-all bg-gradient-to-r from-secondary-500 to-transparent',-->
|
|
||||||
<!-- 'flex flex-col flex-1 p-4',-->
|
|
||||||
<!-- {-->
|
|
||||||
<!-- // 'translate-x-full opacity-100': $isNavBarOpen,-->
|
|
||||||
<!-- 'opacity-0 pointer-events-none': !$isNavBarOpen,-->
|
|
||||||
<!-- 'group-hover:translate-x-0 group-hover:opacity-100 group-hover:pointer-events-auto': true-->
|
|
||||||
<!-- }-->
|
|
||||||
<!-- )}-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- <div class="flex flex-col flex-1 justify-center">-->
|
|
||||||
<!-- <!– svelte-ignore a11y-click-events-have-key-events –>-->
|
|
||||||
<!-- <div class={itemContainer(0, $focusIndex)} on:click={selectIndex(0)}>-->
|
|
||||||
<!-- <span-->
|
|
||||||
<!-- class={classNames('text-xl transition-opacity font-medium', {-->
|
|
||||||
<!-- // 'opacity-0': $isNavBarOpen === false-->
|
|
||||||
<!-- })}-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- Series</span-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- </div>-->
|
|
||||||
<!-- <!– svelte-ignore a11y-click-events-have-key-events –>-->
|
|
||||||
<!-- <div class={itemContainer(1, $focusIndex)} on:click={selectIndex(1)}>-->
|
|
||||||
<!-- <span-->
|
|
||||||
<!-- class={classNames('text-xl transition-opacity font-medium', {-->
|
|
||||||
<!-- // 'opacity-0': $isNavBarOpen === false-->
|
|
||||||
<!-- })}-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- Movies</span-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- </div>-->
|
|
||||||
<!-- <!– svelte-ignore a11y-click-events-have-key-events –>-->
|
|
||||||
<!-- <div class={itemContainer(2, $focusIndex)} on:click={selectIndex(2)}>-->
|
|
||||||
<!-- <span-->
|
|
||||||
<!-- class={classNames('text-xl transition-opacity font-medium', {-->
|
|
||||||
<!-- // 'opacity-0': $isNavBarOpen === false-->
|
|
||||||
<!-- })}-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- Library</span-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- </div>-->
|
|
||||||
<!-- <!– svelte-ignore a11y-click-events-have-key-events –>-->
|
|
||||||
<!-- <div class={itemContainer(3, $focusIndex)} on:click={selectIndex(3)}>-->
|
|
||||||
<!-- <span-->
|
|
||||||
<!-- class={classNames('text-xl transition-opacity font-medium', {-->
|
|
||||||
<!-- // 'opacity-0': $isNavBarOpen === false-->
|
|
||||||
<!-- })}-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- Search</span-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- </div>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
|
|
||||||
<!-- <!– svelte-ignore a11y-click-events-have-key-events –>-->
|
|
||||||
<!-- <div class={itemContainer(4, $focusIndex)} on:click={selectIndex(4)}>-->
|
|
||||||
<!-- <span-->
|
|
||||||
<!-- class={classNames('text-xl transition-opacity font-medium', {-->
|
|
||||||
<!-- // 'opacity-0': $isNavBarOpen === false-->
|
|
||||||
<!-- })}-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- Manage</span-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- </div>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import SearchPage from '../../pages/SearchPage.svelte';
|
|||||||
import PageNotFound from '../../pages/PageNotFound.svelte';
|
import PageNotFound from '../../pages/PageNotFound.svelte';
|
||||||
import ManagePage from '../../pages/ManagePage.svelte';
|
import ManagePage from '../../pages/ManagePage.svelte';
|
||||||
import PersonPage from '../../pages/PersonPage.svelte';
|
import PersonPage from '../../pages/PersonPage.svelte';
|
||||||
|
import UsersPage from '../../pages/UsersPage.svelte';
|
||||||
|
|
||||||
interface Page {
|
interface Page {
|
||||||
id: symbol;
|
id: symbol;
|
||||||
@@ -179,6 +180,12 @@ export function useStackRouter({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const usersRoute: Route = {
|
||||||
|
path: '/users',
|
||||||
|
root: true,
|
||||||
|
component: UsersPage
|
||||||
|
};
|
||||||
|
|
||||||
const seriesHomeRoute: Route = {
|
const seriesHomeRoute: Route = {
|
||||||
path: '/series',
|
path: '/series',
|
||||||
default: true,
|
default: true,
|
||||||
@@ -239,8 +246,9 @@ const notFoundRoute: Route = {
|
|||||||
root: true
|
root: true
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultStackRouter = useStackRouter({
|
export const stackRouter = useStackRouter({
|
||||||
routes: [
|
routes: [
|
||||||
|
usersRoute,
|
||||||
seriesHomeRoute,
|
seriesHomeRoute,
|
||||||
seriesRoute,
|
seriesRoute,
|
||||||
episodeRoute,
|
episodeRoute,
|
||||||
@@ -278,6 +286,6 @@ export const defaultStackRouter = useStackRouter({
|
|||||||
// // }
|
// // }
|
||||||
// } as const);
|
// } as const);
|
||||||
|
|
||||||
export const navigate = defaultStackRouter.navigate;
|
export const navigate = stackRouter.navigate;
|
||||||
export const back = defaultStackRouter.back;
|
export const back = stackRouter.back;
|
||||||
defaultStackRouter.subscribe(console.log);
|
stackRouter.subscribe(console.log);
|
||||||
|
|||||||
@@ -6,13 +6,13 @@
|
|||||||
import { jellyfinApi } from '../../apis/jellyfin/jellyfin-api';
|
import { jellyfinApi } from '../../apis/jellyfin/jellyfin-api';
|
||||||
import getDeviceProfile from '../../apis/jellyfin/playback-profiles';
|
import getDeviceProfile from '../../apis/jellyfin/playback-profiles';
|
||||||
import { getQualities } from '../../apis/jellyfin/qualities';
|
import { getQualities } from '../../apis/jellyfin/qualities';
|
||||||
import { appState } from '../../stores/app-state.store';
|
|
||||||
import { onDestroy } from 'svelte';
|
import { onDestroy } from 'svelte';
|
||||||
import { modalStack, modalStackTop } from '../Modal/modal.store';
|
import { modalStack, modalStackTop } from '../Modal/modal.store';
|
||||||
import { createLocalStorageStore } from '../../stores/localstorage.store';
|
import { createLocalStorageStore } from '../../stores/localstorage.store';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { ISO_2_LANGUAGES } from '../../utils/iso-2-languages';
|
import { ISO_2_LANGUAGES } from '../../utils/iso-2-languages';
|
||||||
import Modal from '../Modal/Modal.svelte';
|
import Modal from '../Modal/Modal.svelte';
|
||||||
|
import { user } from '../../stores/user.store';
|
||||||
|
|
||||||
type MediaLanguageStore = {
|
type MediaLanguageStore = {
|
||||||
subtitles?: string;
|
subtitles?: string;
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
subtitles = {
|
subtitles = {
|
||||||
kind: 'subtitles',
|
kind: 'subtitles',
|
||||||
srclang: stream.Language || '',
|
srclang: stream.Language || '',
|
||||||
url: `${$appState.user?.settings.jellyfin.baseUrl}/Videos/${id}/${mediaSource?.Id}/Subtitles/${stream.Index}/${stream.Level}/Stream.vtt`,
|
url: `${$user?.settings.jellyfin.baseUrl}/Videos/${id}/${mediaSource?.Id}/Subtitles/${stream.Index}/${stream.Level}/Stream.vtt`,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
language: ISO_2_LANGUAGES[stream?.Language || '']?.name || 'English'
|
language: ISO_2_LANGUAGES[stream?.Language || '']?.name || 'English'
|
||||||
};
|
};
|
||||||
@@ -126,7 +126,7 @@
|
|||||||
mediaSource?.MediaStreams?.filter((s) => s.Type === 'Subtitle').map((s) => ({
|
mediaSource?.MediaStreams?.filter((s) => s.Type === 'Subtitle').map((s) => ({
|
||||||
kind: 'subtitles' as const,
|
kind: 'subtitles' as const,
|
||||||
srclang: s.Language || '',
|
srclang: s.Language || '',
|
||||||
url: `${$appState.user?.settings.jellyfin.baseUrl}/Videos/${id}/${mediaSource?.Id}/Subtitles/${s.Index}/${s.Level}/Stream.vtt`,
|
url: `${$user?.settings.jellyfin.baseUrl}/Videos/${id}/${mediaSource?.Id}/Subtitles/${s.Index}/${s.Level}/Stream.vtt`,
|
||||||
language: 'English'
|
language: 'English'
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
@@ -170,9 +170,9 @@
|
|||||||
playbackPosition: progressTime * 10_000_000
|
playbackPosition: progressTime * 10_000_000
|
||||||
}),
|
}),
|
||||||
directPlay,
|
directPlay,
|
||||||
playbackUrl: $appState.user?.settings.jellyfin.baseUrl + playbackUri,
|
playbackUrl: $user?.settings.jellyfin.baseUrl + playbackUri,
|
||||||
backdrop: item?.BackdropImageTags?.length
|
backdrop: item?.BackdropImageTags?.length
|
||||||
? `${$appState.user?.settings.jellyfin.baseUrl}/Items/${item?.Id}/Images/Backdrop?quality=100&tag=${item?.BackdropImageTags?.[0]}`
|
? `${$user?.settings.jellyfin.baseUrl}/Items/${item?.Id}/Images/Backdrop?quality=100&tag=${item?.BackdropImageTags?.[0]}`
|
||||||
: '',
|
: '',
|
||||||
startTime:
|
startTime:
|
||||||
(options.playbackPosition || 0) / 10_000_000 ||
|
(options.playbackPosition || 0) / 10_000_000 ||
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
import { jellyfinApi } from '../../apis/jellyfin/jellyfin-api.js';
|
import { jellyfinApi } from '../../apis/jellyfin/jellyfin-api.js';
|
||||||
import { videoPlayerSettings } from '../../stores/localstorage.store';
|
import { videoPlayerSettings } from '../../stores/localstorage.store';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { appState } from '../../stores/app-state.store';
|
import { appState } from '../../stores/user.store';
|
||||||
import { getBrowserSpecificMediaFunctions } from './VideoPlayer';
|
import { getBrowserSpecificMediaFunctions } from './VideoPlayer';
|
||||||
|
|
||||||
export let jellyfinId: string;
|
export let jellyfinId: string;
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { Selectable } from '../selectable';
|
|
||||||
import Carousel from '../components/Carousel/Carousel.svelte';
|
|
||||||
import CarouselPlaceholderItems from '../components/Carousel/CarouselPlaceholderItems.svelte';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
export let container: Selectable;
|
|
||||||
const focusWithin = container.hasFocusWithin;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class={classNames('flex flex-col', {
|
|
||||||
'bg-green-100': $focusWithin
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Carousel>
|
|
||||||
<CarouselPlaceholderItems {container} />
|
|
||||||
</Carousel>
|
|
||||||
<Carousel>
|
|
||||||
<CarouselPlaceholderItems {container} />
|
|
||||||
</Carousel>
|
|
||||||
</div>
|
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getReiverrApiClient, reiverrApi } from '../apis/reiverr/reiverr-api';
|
|
||||||
import Container from '../../Container.svelte';
|
import Container from '../../Container.svelte';
|
||||||
import { appState } from '../stores/app-state.store';
|
|
||||||
import TextField from '../components/TextField.svelte';
|
import TextField from '../components/TextField.svelte';
|
||||||
import Button from '../components/Button.svelte';
|
import Button from '../components/Button.svelte';
|
||||||
|
import { createLocalStorageStore } from '../stores/localstorage.store';
|
||||||
|
|
||||||
let name: string = '';
|
import { sessions } from '../stores/session.store';
|
||||||
|
|
||||||
|
const baseUrl = createLocalStorageStore('baseUrl', window.location.origin || '');
|
||||||
|
const name = createLocalStorageStore('username', '');
|
||||||
let password: string = '';
|
let password: string = '';
|
||||||
let error: string | undefined = undefined;
|
let error: string | undefined = undefined;
|
||||||
|
|
||||||
@@ -14,17 +16,13 @@
|
|||||||
function handleLogin() {
|
function handleLogin() {
|
||||||
loading = true;
|
loading = true;
|
||||||
|
|
||||||
reiverrApi
|
sessions
|
||||||
.authenticate(name, password)
|
.addSession($baseUrl, $name, password)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.error?.statusCode === 401) {
|
if (res.error?.statusCode === 401) {
|
||||||
error = 'Invalid credentials. Please try again.';
|
error = 'Invalid credentials. Please try again.';
|
||||||
} else if (res.error) {
|
} else if (res.error) {
|
||||||
error = 'Error occurred: ' + res.error.message;
|
error = 'Error occurred: ' + res.error.message;
|
||||||
} else {
|
|
||||||
const token = res.data.accessToken;
|
|
||||||
appState.setToken(token);
|
|
||||||
// window.location.reload();
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err: Error) => {
|
.catch((err: Error) => {
|
||||||
@@ -44,15 +42,13 @@
|
|||||||
credentials.
|
credentials.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TextField
|
<TextField value={$baseUrl} on:change={(e) => baseUrl.set(e.detail)} class="mb-4 w-full">
|
||||||
value={$appState.serverBaseUrl}
|
|
||||||
on:change={(e) => appState.setBaseUrl(e.detail)}
|
|
||||||
class="mb-4 w-full"
|
|
||||||
>
|
|
||||||
Server
|
Server
|
||||||
</TextField>
|
</TextField>
|
||||||
|
|
||||||
<TextField bind:value={name} class="mb-4 w-full">Name</TextField>
|
<TextField value={$name} on:change={({ detail }) => name.set(detail)} class="mb-4 w-full">
|
||||||
|
Name
|
||||||
|
</TextField>
|
||||||
<TextField bind:value={password} type="password" class="mb-8 w-full">Password</TextField>
|
<TextField bind:value={password} type="password" class="mb-8 w-full">Password</TextField>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,25 +1,24 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Container from '../../Container.svelte';
|
import Container from '../../Container.svelte';
|
||||||
import { appState } from '../stores/app-state.store';
|
|
||||||
import Button from '../components/Button.svelte';
|
import Button from '../components/Button.svelte';
|
||||||
import Toggle from '../components/Toggle.svelte';
|
import Toggle from '../components/Toggle.svelte';
|
||||||
import { localSettings } from '../stores/localstorage.store';
|
import { localSettings } from '../stores/localstorage.store';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Tab from '../components/Tab/Tab.svelte';
|
import Tab from '../components/Tab/Tab.svelte';
|
||||||
import { useTabs } from '../components/Tab/Tab';
|
import { useTabs } from '../components/Tab/Tab';
|
||||||
import TextField from '../components/TextField.svelte';
|
|
||||||
import SonarrIntegration from '../components/Integrations/SonarrIntegration.svelte';
|
import SonarrIntegration from '../components/Integrations/SonarrIntegration.svelte';
|
||||||
import RadarrIntegration from '../components/Integrations/RadarrIntegration.svelte';
|
import RadarrIntegration from '../components/Integrations/RadarrIntegration.svelte';
|
||||||
import type { JellyfinUser } from '../apis/jellyfin/jellyfin-api';
|
import type { JellyfinUser } from '../apis/jellyfin/jellyfin-api';
|
||||||
import JellyfinIntegration from '../components/Integrations/JellyfinIntegration.svelte';
|
import JellyfinIntegration from '../components/Integrations/JellyfinIntegration.svelte';
|
||||||
import JellyfinIntegrationUsersDialog from '../components/Integrations/JellyfinIntegrationUsersDialog.svelte';
|
import JellyfinIntegrationUsersDialog from '../components/Integrations/JellyfinIntegrationUsersDialog.svelte';
|
||||||
import TmdbIntegration from '../components/Integrations/TmdbIntegration.svelte';
|
|
||||||
import { tmdbApi } from '../apis/tmdb/tmdb-api';
|
import { tmdbApi } from '../apis/tmdb/tmdb-api';
|
||||||
import SelectField from '../components/SelectField.svelte';
|
import SelectField from '../components/SelectField.svelte';
|
||||||
import { ArrowRight, Trash } from 'radix-icons-svelte';
|
import { ArrowRight, Trash } from 'radix-icons-svelte';
|
||||||
import TmdbIntegrationConnectDialog from '../components/Integrations/TmdbIntegrationConnectDialog.svelte';
|
import TmdbIntegrationConnectDialog from '../components/Integrations/TmdbIntegrationConnectDialog.svelte';
|
||||||
import { createModal } from '../components/Modal/modal.store';
|
import { createModal } from '../components/Modal/modal.store';
|
||||||
import DetachedPage from '../components/DetachedPage/DetachedPage.svelte';
|
import DetachedPage from '../components/DetachedPage/DetachedPage.svelte';
|
||||||
|
import { user } from '../stores/user.store';
|
||||||
|
import { sessions } from '../stores/session.store';
|
||||||
|
|
||||||
enum Tabs {
|
enum Tabs {
|
||||||
Interface,
|
Interface,
|
||||||
@@ -45,7 +44,7 @@
|
|||||||
let lastKeyCode = 0;
|
let lastKeyCode = 0;
|
||||||
let lastKey = '';
|
let lastKey = '';
|
||||||
let tizenMediaKey = '';
|
let tizenMediaKey = '';
|
||||||
$: tmdbAccount = $appState.user?.settings.tmdb.userId ? tmdbApi.getAccountDetails() : undefined;
|
$: tmdbAccount = $user?.settings.tmdb.userId ? tmdbApi.getAccountDetails() : undefined;
|
||||||
|
|
||||||
// onMount(() => {
|
// onMount(() => {
|
||||||
// if (isTizen()) {
|
// if (isTizen()) {
|
||||||
@@ -63,7 +62,7 @@
|
|||||||
// });
|
// });
|
||||||
|
|
||||||
async function handleDisconnectTmdb() {
|
async function handleDisconnectTmdb() {
|
||||||
return appState.updateUser((prev) => ({
|
return user.updateUser((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
settings: {
|
settings: {
|
||||||
...prev.settings,
|
...prev.settings,
|
||||||
@@ -77,7 +76,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleSaveJellyfin() {
|
async function handleSaveJellyfin() {
|
||||||
return appState.updateUser((prev) => ({
|
return user.updateUser((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
settings: {
|
settings: {
|
||||||
...prev.settings,
|
...prev.settings,
|
||||||
@@ -92,7 +91,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleSaveSonarr() {
|
async function handleSaveSonarr() {
|
||||||
return appState.updateUser((prev) => ({
|
return user.updateUser((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
settings: {
|
settings: {
|
||||||
...prev.settings,
|
...prev.settings,
|
||||||
@@ -106,7 +105,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleSaveRadarr() {
|
async function handleSaveRadarr() {
|
||||||
return appState.updateUser((prev) => ({
|
return user.updateUser((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
settings: {
|
settings: {
|
||||||
...prev.settings,
|
...prev.settings,
|
||||||
@@ -318,7 +317,9 @@
|
|||||||
<div>Tizen media key: {tizenMediaKey}</div>
|
<div>Tizen media key: {tizenMediaKey}</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="flex space-x-4 mt-4">
|
<div class="flex space-x-4 mt-4">
|
||||||
<Button on:clickOrSelect={appState.logOut} class="hover:bg-red-500">Log Out</Button>
|
<Button on:clickOrSelect={() => sessions.removeSession()} class="hover:bg-red-500">
|
||||||
|
Log Out
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Tab>
|
</Tab>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import Container from '../../Container.svelte';
|
import Container from '../../Container.svelte';
|
||||||
import Tab from '../components/Tab/Tab.svelte';
|
import Tab from '../components/Tab/Tab.svelte';
|
||||||
import Button from '../components/Button.svelte';
|
import Button from '../components/Button.svelte';
|
||||||
import { appState } from '../stores/app-state.store';
|
|
||||||
import { tmdbApi } from '../apis/tmdb/tmdb-api';
|
import { tmdbApi } from '../apis/tmdb/tmdb-api';
|
||||||
import { ArrowLeft, ArrowRight, CheckCircled, ExternalLink } from 'radix-icons-svelte';
|
import { ArrowLeft, ArrowRight, CheckCircled, ExternalLink } from 'radix-icons-svelte';
|
||||||
import TextField from '../components/TextField.svelte';
|
import TextField from '../components/TextField.svelte';
|
||||||
@@ -14,6 +13,8 @@
|
|||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { useTabs } from '../components/Tab/Tab';
|
import { useTabs } from '../components/Tab/Tab';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { user } from '../stores/user.store';
|
||||||
|
import { sessions } from '../stores/session.store';
|
||||||
|
|
||||||
enum Tabs {
|
enum Tabs {
|
||||||
Welcome,
|
Welcome,
|
||||||
@@ -32,7 +33,7 @@
|
|||||||
let tmdbConnectRequestToken: string | undefined = undefined;
|
let tmdbConnectRequestToken: string | undefined = undefined;
|
||||||
let tmdbConnectLink: string | undefined = undefined;
|
let tmdbConnectLink: string | undefined = undefined;
|
||||||
let tmdbConnectQrCode: string | undefined = undefined;
|
let tmdbConnectQrCode: string | undefined = undefined;
|
||||||
$: connectedTmdbAccount = $appState.user?.settings.tmdb.userId && tmdbApi.getAccountDetails();
|
$: connectedTmdbAccount = $user?.settings.tmdb.userId && tmdbApi.getAccountDetails();
|
||||||
let tmdbError: string = '';
|
let tmdbError: string = '';
|
||||||
|
|
||||||
let jellyfinBaseUrl: string = '';
|
let jellyfinBaseUrl: string = '';
|
||||||
@@ -50,15 +51,15 @@
|
|||||||
let radarrApiKey: string = '';
|
let radarrApiKey: string = '';
|
||||||
let radarrError: string = '';
|
let radarrError: string = '';
|
||||||
|
|
||||||
appState.subscribe((appState) => {
|
user.subscribe((user) => {
|
||||||
jellyfinBaseUrl = jellyfinBaseUrl || appState.user?.settings.jellyfin.baseUrl || '';
|
jellyfinBaseUrl = jellyfinBaseUrl || user?.settings.jellyfin.baseUrl || '';
|
||||||
jellyfinApiKey = jellyfinApiKey || appState.user?.settings.jellyfin.apiKey || '';
|
jellyfinApiKey = jellyfinApiKey || user?.settings.jellyfin.apiKey || '';
|
||||||
|
|
||||||
sonarrBaseUrl = sonarrBaseUrl || appState.user?.settings.sonarr.baseUrl || '';
|
sonarrBaseUrl = sonarrBaseUrl || user?.settings.sonarr.baseUrl || '';
|
||||||
sonarrApiKey = sonarrApiKey || appState.user?.settings.sonarr.apiKey || '';
|
sonarrApiKey = sonarrApiKey || user?.settings.sonarr.apiKey || '';
|
||||||
|
|
||||||
radarrBaseUrl = radarrBaseUrl || appState.user?.settings.radarr.baseUrl || '';
|
radarrBaseUrl = radarrBaseUrl || user?.settings.radarr.baseUrl || '';
|
||||||
radarrApiKey = radarrApiKey || appState.user?.settings.radarr.apiKey || '';
|
radarrApiKey = radarrApiKey || user?.settings.radarr.apiKey || '';
|
||||||
|
|
||||||
// if (
|
// if (
|
||||||
// !jellyfinUser &&
|
// !jellyfinUser &&
|
||||||
@@ -86,7 +87,7 @@
|
|||||||
.getJellyfinUsers(jellyfinBaseUrl, jellyfinApiKey)
|
.getJellyfinUsers(jellyfinBaseUrl, jellyfinApiKey)
|
||||||
.then((users) => {
|
.then((users) => {
|
||||||
if (baseUrlCopy === jellyfinBaseUrl && apiKeyCopy === jellyfinApiKey) {
|
if (baseUrlCopy === jellyfinBaseUrl && apiKeyCopy === jellyfinApiKey) {
|
||||||
jellyfinUser = users.find((u) => u.Id === get(appState).user?.settings.jellyfin.userId);
|
jellyfinUser = users.find((u) => u.Id === $user?.settings.jellyfin.userId);
|
||||||
jellyfinError = users.length ? '' : 'Could not connect';
|
jellyfinError = users.length ? '' : 'Could not connect';
|
||||||
}
|
}
|
||||||
// console.log(users, baseUrlCopy, jellyfinBaseUrl, apiKeyCopy, jellyfinApiKey);
|
// console.log(users, baseUrlCopy, jellyfinBaseUrl, apiKeyCopy, jellyfinApiKey);
|
||||||
@@ -114,7 +115,7 @@
|
|||||||
const { status_code, access_token, account_id } = res || {};
|
const { status_code, access_token, account_id } = res || {};
|
||||||
if (status_code !== 1 || !access_token || !account_id) return; // TODO add notification
|
if (status_code !== 1 || !access_token || !account_id) return; // TODO add notification
|
||||||
|
|
||||||
appState.updateUser((prev) => ({
|
user.updateUser((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
settings: {
|
settings: {
|
||||||
...prev.settings,
|
...prev.settings,
|
||||||
@@ -135,7 +136,7 @@
|
|||||||
const apiKey = jellyfinApiKey;
|
const apiKey = jellyfinApiKey;
|
||||||
if (!userId || !baseUrl || !apiKey) return;
|
if (!userId || !baseUrl || !apiKey) return;
|
||||||
|
|
||||||
await appState.updateUser((prev) => ({
|
await user.updateUser((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
settings: {
|
settings: {
|
||||||
...prev.settings,
|
...prev.settings,
|
||||||
@@ -168,7 +169,7 @@
|
|||||||
return; // TODO add notification
|
return; // TODO add notification
|
||||||
}
|
}
|
||||||
|
|
||||||
await appState.updateUser((prev) => ({
|
await user.updateUser((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
settings: {
|
settings: {
|
||||||
...prev.settings,
|
...prev.settings,
|
||||||
@@ -199,7 +200,7 @@
|
|||||||
return; // TODO add notification
|
return; // TODO add notification
|
||||||
}
|
}
|
||||||
|
|
||||||
await appState.updateUser((prev) => ({
|
await user.updateUser((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
settings: {
|
settings: {
|
||||||
...prev.settings,
|
...prev.settings,
|
||||||
@@ -215,7 +216,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function finalizeSetup() {
|
async function finalizeSetup() {
|
||||||
await appState.updateUser((prev) => ({
|
await user.updateUser((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
onboardingDone: true
|
onboardingDone: true
|
||||||
}));
|
}));
|
||||||
@@ -237,7 +238,7 @@
|
|||||||
services to get most out of Reiverr.
|
services to get most out of Reiverr.
|
||||||
</div>
|
</div>
|
||||||
<Container direction="horizontal" class="flex space-x-4">
|
<Container direction="horizontal" class="flex space-x-4">
|
||||||
<Button type="primary-dark" on:clickOrSelect={() => appState.logOut()}>Log Out</Button>
|
<Button type="primary-dark" on:clickOrSelect={() => sessions.removeSession()}>Log Out</Button>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<Button type="primary-dark" on:clickOrSelect={() => tab.next()}>
|
<Button type="primary-dark" on:clickOrSelect={() => tab.next()}>
|
||||||
Next
|
Next
|
||||||
@@ -280,7 +281,7 @@
|
|||||||
{/await}
|
{/await}
|
||||||
|
|
||||||
<Button type="primary-dark" on:clickOrSelect={() => tab.next()}>
|
<Button type="primary-dark" on:clickOrSelect={() => tab.next()}>
|
||||||
{#if $appState.user?.settings.tmdb.userId}
|
{#if $user?.settings.tmdb.userId}
|
||||||
Next
|
Next
|
||||||
{:else}
|
{:else}
|
||||||
Skip
|
Skip
|
||||||
|
|||||||
5
src/lib/pages/UsersPage.svelte
Normal file
5
src/lib/pages/UsersPage.svelte
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<script>
|
||||||
|
import DetachedPage from '../components/DetachedPage/DetachedPage.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DetachedPage sidebar={false} class="px-32 py-16">Users Page</DetachedPage>
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
import { derived, get, writable } from 'svelte/store';
|
|
||||||
import { createLocalStorageStore } from './localstorage.store';
|
|
||||||
import {
|
|
||||||
getReiverrApiClient,
|
|
||||||
reiverrApi,
|
|
||||||
type ReiverrSettings,
|
|
||||||
type ReiverrUser
|
|
||||||
} from '../apis/reiverr/reiverr-api';
|
|
||||||
|
|
||||||
interface AuthenticationStoreData {
|
|
||||||
token?: string;
|
|
||||||
serverBaseUrl?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UserStoreData {
|
|
||||||
user: ReiverrUser | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AppStateData extends AuthenticationStoreData {
|
|
||||||
user: ReiverrUser | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const authenticationStore = createLocalStorageStore<AuthenticationStoreData>(
|
|
||||||
'authentication-token',
|
|
||||||
{
|
|
||||||
token: undefined,
|
|
||||||
serverBaseUrl: window?.location?.origin
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
function createAppState() {
|
|
||||||
const userStore = writable<UserStoreData>(undefined);
|
|
||||||
|
|
||||||
const combinedStore = derived<[typeof userStore, typeof authenticationStore], AppStateData>(
|
|
||||||
[userStore, authenticationStore],
|
|
||||||
([user, auth]) => {
|
|
||||||
return {
|
|
||||||
...user,
|
|
||||||
...auth
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
function setBaseUrl(serverBaseUrl: string | undefined = undefined) {
|
|
||||||
authenticationStore.update((p) => ({ ...p, serverBaseUrl }));
|
|
||||||
}
|
|
||||||
|
|
||||||
function setToken(token: string | undefined = undefined) {
|
|
||||||
authenticationStore.update((p) => ({ ...p, token }));
|
|
||||||
}
|
|
||||||
|
|
||||||
function setUser(user: ReiverrUser | null) {
|
|
||||||
userStore.set({ user });
|
|
||||||
}
|
|
||||||
|
|
||||||
function logOut() {
|
|
||||||
setUser(null);
|
|
||||||
setToken(undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ready = new Promise<AppStateData>((resolve) => {
|
|
||||||
combinedStore.subscribe((state) => {
|
|
||||||
if (state.token && state.serverBaseUrl && state.user !== undefined) {
|
|
||||||
resolve(state);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const appState = createAppState();
|
|
||||||
export const appStateUser = derived(appState, ($state) => $state.user);
|
|
||||||
|
|
||||||
authenticationStore.subscribe((auth) => {
|
|
||||||
if (auth.token) {
|
|
||||||
reiverrApi
|
|
||||||
.getClient(auth.serverBaseUrl, auth.token)
|
|
||||||
?.GET('/user', {})
|
|
||||||
.then((res) => res.data)
|
|
||||||
.then((user) => appState.setUser(user || null))
|
|
||||||
.catch((err) => appState.setUser(null));
|
|
||||||
} else {
|
|
||||||
appState.setUser(null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
70
src/lib/stores/session.store.ts
Normal file
70
src/lib/stores/session.store.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { createLocalStorageStore } from './localstorage.store';
|
||||||
|
import type { operations } from '../apis/reiverr/reiverr.generated';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export interface Session {
|
||||||
|
baseUrl: string;
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function useSessions() {
|
||||||
|
const sessions = createLocalStorageStore<{ sessions: Session[]; activeSession?: Session }>(
|
||||||
|
'sessions',
|
||||||
|
{
|
||||||
|
sessions: []
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function setActiveSession(session: Session) {
|
||||||
|
sessions.update((s) => ({ ...s, activeSession: session }));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addSession(baseUrl: string, name: string, password: string, activate = true) {
|
||||||
|
const res = await axios
|
||||||
|
.post<operations['AuthController_signIn']['responses']['200']['content']['application/json']>(
|
||||||
|
baseUrl + '/api/auth',
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
password
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.catch((e) => e.response);
|
||||||
|
|
||||||
|
if (res.status !== 200) return res;
|
||||||
|
|
||||||
|
const session = {
|
||||||
|
baseUrl,
|
||||||
|
token: res.data.accessToken
|
||||||
|
};
|
||||||
|
|
||||||
|
sessions.update((s) => {
|
||||||
|
const sessions = s.sessions.concat(session);
|
||||||
|
return {
|
||||||
|
sessions,
|
||||||
|
activeSession: activate ? session : s.activeSession
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeSession(_session?: Session) {
|
||||||
|
sessions.update((s) => {
|
||||||
|
const session = _session || s.activeSession;
|
||||||
|
const sessions = s.sessions.filter((s) => s !== session);
|
||||||
|
return {
|
||||||
|
sessions,
|
||||||
|
activeSession: s.activeSession === session ? undefined : s.activeSession
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: sessions.subscribe,
|
||||||
|
setActiveSession,
|
||||||
|
addSession,
|
||||||
|
removeSession
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sessions = useSessions();
|
||||||
53
src/lib/stores/user.store.ts
Normal file
53
src/lib/stores/user.store.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { derived, get, writable } from 'svelte/store';
|
||||||
|
import { reiverrApi, type ReiverrUser } from '../apis/reiverr/reiverr-api';
|
||||||
|
import axios from 'axios';
|
||||||
|
import type { operations } from '../apis/reiverr/reiverr.generated';
|
||||||
|
import { type Session, sessions } from './session.store';
|
||||||
|
|
||||||
|
function useUser() {
|
||||||
|
const activeSession = derived(sessions, (sessions) => sessions.activeSession);
|
||||||
|
|
||||||
|
const userStore = writable<ReiverrUser | undefined | null>(undefined);
|
||||||
|
|
||||||
|
let lastActiveSession: Session | undefined;
|
||||||
|
activeSession.subscribe(async (activeSession) => {
|
||||||
|
if (!activeSession) {
|
||||||
|
userStore.set(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
userStore.set(undefined);
|
||||||
|
lastActiveSession = activeSession;
|
||||||
|
const user = await axios
|
||||||
|
.get<
|
||||||
|
operations['UserController_getProfile']['responses']['200']['content']['application/json']
|
||||||
|
>(activeSession.baseUrl + '/api/user', {
|
||||||
|
headers: {
|
||||||
|
Authorization: 'Bearer ' + activeSession.token
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((r) => r.data);
|
||||||
|
|
||||||
|
if (lastActiveSession === activeSession) userStore.set(user);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function updateUser(updateFn: (user: ReiverrUser) => ReiverrUser) {
|
||||||
|
const user = get(userStore);
|
||||||
|
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
const updated = updateFn(user);
|
||||||
|
const update = await reiverrApi.updateUser(updated);
|
||||||
|
|
||||||
|
if (update) {
|
||||||
|
userStore.set(update);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: userStore.subscribe,
|
||||||
|
updateUser
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const user = useUser();
|
||||||
Reference in New Issue
Block a user