refactor: User and session management

This commit is contained in:
Aleksi Lassila
2024-06-12 18:32:39 +03:00
parent a73f9d6cca
commit 5c1a4d4206
28 changed files with 364 additions and 388 deletions

View File

@@ -6,6 +6,7 @@
import classNames from 'classnames';
export let topmost = true;
export let sidebar = true;
// Top element, that when focused and back is pressed, will exit the modal
const topSelectable = useRegistrar();
@@ -41,7 +42,9 @@
direction="horizontal"
on:mount
>
<Sidebar />
{#if sidebar}
<Sidebar />
{/if}
<Container on:back={handleGoToTop} focusOnMount class={classNames($$restProps.class)}>
<slot {handleGoBack} registrar={topSelectable.registrar} />
</Container>

View File

@@ -43,7 +43,7 @@
class={classNames('absolute inset-0 bg-center bg-cover', {
'opacity-100': visibleIndex === i,
'opacity-0': visibleIndex !== i,
'scale-125': !hasFocus
'scale-110': !hasFocus
})}
style={`background-image: url('${url}'); transition: opacity 500ms, transform 500ms;`}
/>
@@ -52,7 +52,7 @@
{:else}
<div
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;"
>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import TextField from '../TextField.svelte';
import { appState } from '../../stores/app-state.store';
import { user } from '../../stores/user.store';
import { createEventDispatcher } from 'svelte';
import SelectField from '../SelectField.svelte';
import { jellyfinApi, type JellyfinUser } from '../../apis/jellyfin/jellyfin-api';
@@ -20,9 +20,9 @@
let jellyfinUsers: Promise<JellyfinUser[]> | undefined = undefined;
export let jellyfinUser: JellyfinUser | undefined;
appState.subscribe((appState) => {
baseUrl = baseUrl || appState.user?.settings.jellyfin.baseUrl || '';
apiKey = apiKey || appState.user?.settings.jellyfin.apiKey || '';
user.subscribe((user) => {
baseUrl = baseUrl || user?.settings.jellyfin.baseUrl || '';
apiKey = apiKey || user?.settings.jellyfin.apiKey || '';
originalBaseUrl = baseUrl;
originalApiKey = apiKey;
@@ -35,9 +35,7 @@
baseUrl,
apiKey,
stale:
baseUrl && apiKey
? jellyfinUser?.Id !== get(appState).user?.settings.jellyfin.userId
: !jellyfinUser
baseUrl && apiKey ? jellyfinUser?.Id !== get(user)?.settings.jellyfin.userId : !jellyfinUser
});
function handleChange() {

View File

@@ -1,8 +1,8 @@
<script lang="ts">
import TextField from '../TextField.svelte';
import { appState } from '../../stores/app-state.store';
import { createEventDispatcher } from 'svelte';
import { radarrApi } from '../../apis/radarr/radarr-api';
import { user } from '../../stores/user.store';
const dispatch = createEventDispatcher<{
change: { baseUrl: string; apiKey: string; stale: boolean };
@@ -16,9 +16,9 @@
let error = '';
let healthCheck: Promise<boolean> | undefined;
appState.subscribe((appState) => {
baseUrl = baseUrl || appState.user?.settings.radarr.baseUrl || '';
apiKey = apiKey || appState.user?.settings.radarr.apiKey || '';
user.subscribe((user) => {
baseUrl = baseUrl || user?.settings.radarr.baseUrl || '';
apiKey = apiKey || user?.settings.radarr.apiKey || '';
originalBaseUrl = baseUrl;
originalApiKey = apiKey;

View File

@@ -1,8 +1,8 @@
<script lang="ts">
import TextField from '../TextField.svelte';
import { appState } from '../../stores/app-state.store';
import { sonarrApi } from '../../apis/sonarr/sonarr-api';
import { createEventDispatcher } from 'svelte';
import { user } from '../../stores/user.store';
const dispatch = createEventDispatcher<{
change: { baseUrl: string; apiKey: string; stale: boolean };
@@ -16,9 +16,9 @@
let error = '';
let healthCheck: Promise<boolean> | undefined;
appState.subscribe((appState) => {
baseUrl = baseUrl || appState.user?.settings.sonarr.baseUrl || '';
apiKey = apiKey || appState.user?.settings.sonarr.apiKey || '';
user.subscribe((user) => {
baseUrl = baseUrl || user?.settings.sonarr.baseUrl || '';
apiKey = apiKey || user?.settings.sonarr.apiKey || '';
originalBaseUrl = baseUrl;
originalApiKey = apiKey;

View File

@@ -1,8 +1,8 @@
<script lang="ts">
import TextField from '../TextField.svelte';
import { appState } from '../../stores/app-state.store';
import { sonarrApi } from '../../apis/sonarr/sonarr-api';
import { createEventDispatcher } from 'svelte';
import { user } from '../../stores/user.store';
const dispatch = createEventDispatcher<{
change: { baseUrl: string; apiKey: string; stale: boolean };
@@ -16,9 +16,9 @@
let error = '';
let healthCheck: Promise<boolean> | undefined;
appState.subscribe((appState) => {
baseUrl = baseUrl || appState.user?.settings.sonarr.baseUrl || '';
apiKey = apiKey || appState.user?.settings.sonarr.apiKey || '';
user.subscribe((user) => {
baseUrl = baseUrl || user?.settings.sonarr.baseUrl || '';
apiKey = apiKey || user?.settings.sonarr.apiKey || '';
originalBaseUrl = baseUrl;
originalApiKey = apiKey;

View File

@@ -1,10 +1,10 @@
<script lang="ts">
import Container from '../../../Container.svelte';
import { appState } from '../../stores/app-state.store';
import { tmdbApi } from '../../apis/tmdb/tmdb-api';
import Button from '../Button.svelte';
import { createEventDispatcher } from 'svelte';
import { ExternalLink } from 'radix-icons-svelte';
import { user } from '../../stores/user.store';
const dispatch = createEventDispatcher<{ connected: null }>();
@@ -33,7 +33,7 @@
return; // TODO add notification
}
appState.updateUser((prev) => ({
user.updateUser((prev) => ({
...prev,
settings: {
...prev.settings,

View File

@@ -5,14 +5,28 @@
DotFilled,
Gear,
Laptop,
MagnifyingGlass
MagnifyingGlass,
Person
} from 'radix-icons-svelte';
import classNames from 'classnames';
import { get, type Readable, writable, type Writable } from 'svelte/store';
import Container from '../../../Container.svelte';
import { registrars, Selectable } from '../../selectable';
import { defaultStackRouter, navigate } from '../StackRouter/StackRouter';
import { stackRouter, navigate } from '../StackRouter/StackRouter';
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 activeIndex = -1;
@@ -40,29 +54,31 @@
selectable.focusChild(index);
const path =
{
0: '/',
1: '/movies',
2: '/library',
3: '/search',
4: '/manage'
[Tabs.Users]: '/users',
[Tabs.Series]: '/',
[Tabs.Movies]: '/movies',
[Tabs.Library]: '/library',
[Tabs.Search]: '/search',
[Tabs.Manage]: '/manage'
}[index] || '/';
navigate(path);
selectedIndex = index;
};
onMount(() => {
defaultStackRouter.subscribe((r) => {
// Set active tab based on bottommost page
stackRouter.subscribe((r) => {
const bottomPage = r[0];
console.log('bottomPage', bottomPage);
if (bottomPage) {
activeIndex =
{
'/': 0,
'/series': 0,
'/movies': 1,
'/library': 2,
'/search': 3,
'/manage': 4
'/users': Tabs.Users,
'/': Tabs.Series,
'/series': Tabs.Series,
'/movies': Tabs.Movies,
'/library': Tabs.Library,
'/search': Tabs.Search,
'/manage': Tabs.Manage
}[bottomPage.route.path] ?? -1;
selectable.focusIndex.set(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'}>
<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
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':
!hasFocus && !(!$isNavBarOpen && selectedIndex === 0)
!hasFocus && !(!$isNavBarOpen && selectedIndex === Tabs.Series)
})}
>
<div class="absolute inset-y-0 left-2 flex items-center justify-center">
<DotFilled
class={classNames('text-primary-500', { 'opacity-0': activeIndex !== 0 })}
class={classNames('text-primary-500', { 'opacity-0': activeIndex !== Tabs.Series })}
size={19}
/>
</div>
@@ -132,17 +191,21 @@
</span>
</div>
</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
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':
!hasFocus && !(!$isNavBarOpen && selectedIndex === 1)
!hasFocus && !(!$isNavBarOpen && selectedIndex === Tabs.Movies)
})}
>
<div class="absolute inset-y-0 left-2 flex items-center justify-center">
<DotFilled
class={classNames('text-primary-500', { 'opacity-0': activeIndex !== 1 })}
class={classNames('text-primary-500', { 'opacity-0': activeIndex !== Tabs.Movies })}
size={19}
/>
</div>
@@ -160,17 +223,21 @@
</span>
</div>
</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
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':
!hasFocus && !(!$isNavBarOpen && selectedIndex === 2)
!hasFocus && !(!$isNavBarOpen && selectedIndex === Tabs.Library)
})}
>
<div class="absolute inset-y-0 left-2 flex items-center justify-center">
<DotFilled
class={classNames('text-primary-500', { 'opacity-0': activeIndex !== 2 })}
class={classNames('text-primary-500', { 'opacity-0': activeIndex !== Tabs.Library })}
size={19}
/>
</div>
@@ -188,17 +255,21 @@
</span>
</div>
</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
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':
!hasFocus && !(!$isNavBarOpen && selectedIndex === 3)
!hasFocus && !(!$isNavBarOpen && selectedIndex === Tabs.Search)
})}
>
<div class="absolute inset-y-0 left-2 flex items-center justify-center">
<DotFilled
class={classNames('text-primary-500', { 'opacity-0': activeIndex !== 3 })}
class={classNames('text-primary-500', { 'opacity-0': activeIndex !== Tabs.Search })}
size={19}
/>
</div>
@@ -218,14 +289,18 @@
</Container>
</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
class={classNames(
'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':
!hasFocus && !(!$isNavBarOpen && selectedIndex === 4),
!hasFocus && !(!$isNavBarOpen && selectedIndex === Tabs.Manage),
'opacity-0 pointer-events-none': $isNavBarOpen === false,
'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">
<DotFilled
class={classNames('text-primary-500', { 'opacity-0': activeIndex !== 4 })}
class={classNames('text-primary-500', { 'opacity-0': activeIndex !== Tabs.Manage })}
size={19}
/>
</div>
@@ -251,105 +326,4 @@
</span>
</div>
</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">-->
<!-- &lt;!&ndash; svelte-ignore a11y-click-events-have-key-events &ndash;&gt;-->
<!-- <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>-->
<!-- &lt;!&ndash; svelte-ignore a11y-click-events-have-key-events &ndash;&gt;-->
<!-- <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>-->
<!-- &lt;!&ndash; svelte-ignore a11y-click-events-have-key-events &ndash;&gt;-->
<!-- <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>-->
<!-- &lt;!&ndash; svelte-ignore a11y-click-events-have-key-events &ndash;&gt;-->
<!-- <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>-->
<!-- &lt;!&ndash; svelte-ignore a11y-click-events-have-key-events &ndash;&gt;-->
<!-- <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>

View File

@@ -11,6 +11,7 @@ import SearchPage from '../../pages/SearchPage.svelte';
import PageNotFound from '../../pages/PageNotFound.svelte';
import ManagePage from '../../pages/ManagePage.svelte';
import PersonPage from '../../pages/PersonPage.svelte';
import UsersPage from '../../pages/UsersPage.svelte';
interface Page {
id: symbol;
@@ -179,6 +180,12 @@ export function useStackRouter({
};
}
const usersRoute: Route = {
path: '/users',
root: true,
component: UsersPage
};
const seriesHomeRoute: Route = {
path: '/series',
default: true,
@@ -239,8 +246,9 @@ const notFoundRoute: Route = {
root: true
};
export const defaultStackRouter = useStackRouter({
export const stackRouter = useStackRouter({
routes: [
usersRoute,
seriesHomeRoute,
seriesRoute,
episodeRoute,
@@ -278,6 +286,6 @@ export const defaultStackRouter = useStackRouter({
// // }
// } as const);
export const navigate = defaultStackRouter.navigate;
export const back = defaultStackRouter.back;
defaultStackRouter.subscribe(console.log);
export const navigate = stackRouter.navigate;
export const back = stackRouter.back;
stackRouter.subscribe(console.log);

View File

@@ -6,13 +6,13 @@
import { jellyfinApi } from '../../apis/jellyfin/jellyfin-api';
import getDeviceProfile from '../../apis/jellyfin/playback-profiles';
import { getQualities } from '../../apis/jellyfin/qualities';
import { appState } from '../../stores/app-state.store';
import { onDestroy } from 'svelte';
import { modalStack, modalStackTop } from '../Modal/modal.store';
import { createLocalStorageStore } from '../../stores/localstorage.store';
import { get } from 'svelte/store';
import { ISO_2_LANGUAGES } from '../../utils/iso-2-languages';
import Modal from '../Modal/Modal.svelte';
import { user } from '../../stores/user.store';
type MediaLanguageStore = {
subtitles?: string;
@@ -115,7 +115,7 @@
subtitles = {
kind: 'subtitles',
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
language: ISO_2_LANGUAGES[stream?.Language || '']?.name || 'English'
};
@@ -126,7 +126,7 @@
mediaSource?.MediaStreams?.filter((s) => s.Type === 'Subtitle').map((s) => ({
kind: 'subtitles' as const,
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'
})) || [];
@@ -170,9 +170,9 @@
playbackPosition: progressTime * 10_000_000
}),
directPlay,
playbackUrl: $appState.user?.settings.jellyfin.baseUrl + playbackUri,
playbackUrl: $user?.settings.jellyfin.baseUrl + playbackUri,
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:
(options.playbackPosition || 0) / 10_000_000 ||

View File

@@ -25,7 +25,7 @@
import { jellyfinApi } from '../../apis/jellyfin/jellyfin-api.js';
import { videoPlayerSettings } from '../../stores/localstorage.store';
import { get } from 'svelte/store';
import { appState } from '../../stores/app-state.store';
import { appState } from '../../stores/user.store';
import { getBrowserSpecificMediaFunctions } from './VideoPlayer';
export let jellyfinId: string;