feat: Logging in multiple users, user selection
This commit is contained in:
@@ -15,6 +15,8 @@
|
||||
import { localSettings } from './lib/stores/localstorage.store';
|
||||
import { user } from './lib/stores/user.store';
|
||||
import { sessions } from './lib/stores/session.store';
|
||||
import SplashScreen from './lib/pages/SplashScreen.svelte';
|
||||
import UsersPage from './lib/pages/UsersPage.svelte';
|
||||
|
||||
user.subscribe((s) => console.log('user', s));
|
||||
sessions.subscribe((s) => console.log('sessions', s));
|
||||
@@ -57,15 +59,9 @@
|
||||
<I18n />
|
||||
<!--<Container class="w-full h-full overflow-auto text-white scrollbar-hide">-->
|
||||
{#if $user === undefined}
|
||||
<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="rounded-full bg-amber-300 h-4 w-4 mr-2" />
|
||||
<h1 class="font-display uppercase font-semibold tracking-wider text-xl">Reiverr</h1>
|
||||
</div>
|
||||
<div>Loading...</div>
|
||||
</div>
|
||||
<SplashScreen />
|
||||
{:else if $user === null}
|
||||
<LoginPage />
|
||||
<UsersPage />
|
||||
{:else if $user.onboardingDone === false}
|
||||
<OnboardingPage />
|
||||
{:else}
|
||||
@@ -92,10 +88,10 @@
|
||||
<StackRouter stack={stackRouter} />
|
||||
<!-- </Container>-->
|
||||
<!-- </Router>-->
|
||||
|
||||
<ModalStack />
|
||||
{/if}
|
||||
|
||||
<ModalStack />
|
||||
|
||||
<NotificationStack />
|
||||
|
||||
<NavigationDebugger />
|
||||
|
||||
@@ -22,10 +22,10 @@ export class ReiverrApi implements Api<paths> {
|
||||
});
|
||||
}
|
||||
|
||||
isSetupDone = async (): Promise<boolean> =>
|
||||
this.getClient()
|
||||
?.GET('/user/isSetupDone')
|
||||
.then((res) => res.data || false) || false;
|
||||
// isSetupDone = async (): Promise<boolean> =>
|
||||
// this.getClient()
|
||||
// ?.GET('/user/isSetupDone')
|
||||
// .then((res) => res.data || false) || false;
|
||||
|
||||
async getUser() {
|
||||
const res = await this.getClient()?.GET('/user', {});
|
||||
|
||||
9
src/lib/components/AddElementOverlay.svelte
Normal file
9
src/lib/components/AddElementOverlay.svelte
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { Plus } from 'radix-icons-svelte';
|
||||
</script>
|
||||
|
||||
<div class="absolute inset-0 bg-secondary-800/75 flex items-center justify-center">
|
||||
<div class="rounded-full p-2.5 bg-secondary-800/75">
|
||||
<Plus size={32} />
|
||||
</div>
|
||||
</div>
|
||||
14
src/lib/components/Dialog/AddUserDialog.svelte
Normal file
14
src/lib/components/Dialog/AddUserDialog.svelte
Normal file
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
import Dialog from './Dialog.svelte';
|
||||
import Login from '../Login.svelte';
|
||||
import { navigate } from '../StackRouter/StackRouter.js';
|
||||
</script>
|
||||
|
||||
<Dialog let:close>
|
||||
<Login
|
||||
on:login={() => {
|
||||
close();
|
||||
navigate('/', { refresh: true });
|
||||
}}
|
||||
/>
|
||||
</Dialog>
|
||||
@@ -27,7 +27,7 @@
|
||||
$$restProps.class
|
||||
)}
|
||||
>
|
||||
<slot />
|
||||
<slot close={handleClose} />
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
64
src/lib/components/Login.svelte
Normal file
64
src/lib/components/Login.svelte
Normal file
@@ -0,0 +1,64 @@
|
||||
<script lang="ts">
|
||||
import Container from '../../Container.svelte';
|
||||
import TextField from '../components/TextField.svelte';
|
||||
import Button from '../components/Button.svelte';
|
||||
import { createLocalStorageStore } from '../stores/localstorage.store';
|
||||
import { sessions } from '../stores/session.store';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher<{ login: null }>();
|
||||
|
||||
const baseUrl = createLocalStorageStore('baseUrl', window.location.origin || '');
|
||||
const name = createLocalStorageStore('username', '');
|
||||
let password: string = '';
|
||||
let error: string | undefined = undefined;
|
||||
|
||||
let loading = false;
|
||||
|
||||
function handleLogin() {
|
||||
loading = true;
|
||||
|
||||
sessions
|
||||
.addSession($baseUrl, $name, password)
|
||||
.then((res) => {
|
||||
console.log('res', res);
|
||||
if (res?.request?.status === 401) {
|
||||
error = 'Invalid credentials. Please try again.';
|
||||
} else if (res?.request.status !== 200) {
|
||||
error = 'Error occurred: ' + res.request.statusText;
|
||||
} else {
|
||||
dispatch('login');
|
||||
}
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
error = err.name + ': ' + err.message;
|
||||
})
|
||||
.finally(() => {
|
||||
loading = false;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<Container class="flex flex-col" focusOnMount>
|
||||
<h1 class="header2 w-full mb-2">Login to Reiverr</h1>
|
||||
<div class="header1 mb-4">
|
||||
If this is your first time logging in, a new account will be created based on your credentials.
|
||||
</div>
|
||||
|
||||
<TextField value={$baseUrl} on:change={(e) => baseUrl.set(e.detail)} class="mb-4 w-full">
|
||||
Server
|
||||
</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>
|
||||
|
||||
<Button type="primary-dark" disabled={loading} on:clickOrSelect={handleLogin} class="mb-4 w-full"
|
||||
>Submit</Button
|
||||
>
|
||||
|
||||
{#if error}
|
||||
<div class="text-red-300 text-center">{error}</div>
|
||||
{/if}
|
||||
</Container>
|
||||
@@ -5,6 +5,7 @@
|
||||
import classNames from 'classnames';
|
||||
import { Plus, PlusCircled } from 'radix-icons-svelte';
|
||||
import { getCardDimensions } from '../../utils';
|
||||
import AddElementOverlay from '../AddElementOverlay.svelte';
|
||||
|
||||
export let backdropUrl: string;
|
||||
|
||||
@@ -31,10 +32,6 @@
|
||||
class="bg-cover bg-center absolute inset-0"
|
||||
style={`background-image: url('${backdropUrl}')`}
|
||||
/>
|
||||
<div class="absolute inset-0 bg-secondary-800/75 flex items-center justify-center">
|
||||
<div class="rounded-full p-2.5 bg-secondary-800/75">
|
||||
<Plus size={32} />
|
||||
</div>
|
||||
</div>
|
||||
<AddElementOverlay />
|
||||
</Container>
|
||||
</AnimateScale>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { useTabs } from '../Tab/Tab';
|
||||
import { user } from '../../stores/user.store';
|
||||
import { sessions } from '../../stores/session.store';
|
||||
|
||||
enum Tabs {
|
||||
Users,
|
||||
@@ -121,7 +122,7 @@
|
||||
|
||||
<Container
|
||||
class="w-full h-12 cursor-pointer"
|
||||
on:clickOrSelect={selectIndex(Tabs.Users)}
|
||||
on:clickOrSelect={() => sessions.setActiveSession()}
|
||||
let:hasFocus
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -113,7 +113,14 @@ export function useStackRouter({
|
||||
};
|
||||
}
|
||||
|
||||
const navigate = (routeString: string, options: { replaceStack?: boolean } = {}) => {
|
||||
const navigate = (
|
||||
routeString: string,
|
||||
options: { replaceStack?: boolean; refresh?: boolean } = {}
|
||||
) => {
|
||||
if (options.refresh) {
|
||||
location.assign(routeString);
|
||||
return;
|
||||
}
|
||||
const page: Page = routeStringToRoute(routeString);
|
||||
const replaceStack = page.route.root || options.replaceStack || false;
|
||||
|
||||
|
||||
@@ -1,65 +1,9 @@
|
||||
<script lang="ts">
|
||||
import Container from '../../Container.svelte';
|
||||
import TextField from '../components/TextField.svelte';
|
||||
import Button from '../components/Button.svelte';
|
||||
import { createLocalStorageStore } from '../stores/localstorage.store';
|
||||
|
||||
import { sessions } from '../stores/session.store';
|
||||
|
||||
const baseUrl = createLocalStorageStore('baseUrl', window.location.origin || '');
|
||||
const name = createLocalStorageStore('username', '');
|
||||
let password: string = '';
|
||||
let error: string | undefined = undefined;
|
||||
|
||||
let loading = false;
|
||||
|
||||
function handleLogin() {
|
||||
loading = true;
|
||||
|
||||
sessions
|
||||
.addSession($baseUrl, $name, password)
|
||||
.then((res) => {
|
||||
if (res.error?.statusCode === 401) {
|
||||
error = 'Invalid credentials. Please try again.';
|
||||
} else if (res.error) {
|
||||
error = 'Error occurred: ' + res.error.message;
|
||||
}
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
error = err.name + ': ' + err.message;
|
||||
})
|
||||
.finally(() => {
|
||||
loading = false;
|
||||
});
|
||||
}
|
||||
import Login from '../components/Login.svelte';
|
||||
</script>
|
||||
|
||||
<div class="w-full h-full flex items-center justify-center">
|
||||
<Container class="flex flex-col bg-primary-800 rounded-2xl p-10 shadow-xl max-w-lg" focusOnMount>
|
||||
<h1 class="header2 w-full mb-2">Login to Reiverr</h1>
|
||||
<div class="header1 mb-4">
|
||||
If this is your first time logging in, a new account will be created based on your
|
||||
credentials.
|
||||
<div class="bg-primary-800 rounded-2xl p-10 shadow-xl max-w-lg">
|
||||
<Login />
|
||||
</div>
|
||||
|
||||
<TextField value={$baseUrl} on:change={(e) => baseUrl.set(e.detail)} class="mb-4 w-full">
|
||||
Server
|
||||
</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>
|
||||
|
||||
<Button
|
||||
type="primary-dark"
|
||||
disabled={loading}
|
||||
on:clickOrSelect={handleLogin}
|
||||
class="mb-4 w-full">Submit</Button
|
||||
>
|
||||
|
||||
{#if error}
|
||||
<div class="text-red-300 text-center">{error}</div>
|
||||
{/if}
|
||||
</Container>
|
||||
</div>
|
||||
|
||||
7
src/lib/pages/SplashScreen.svelte
Normal file
7
src/lib/pages/SplashScreen.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<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="rounded-full bg-amber-300 h-4 w-4 mr-2" />
|
||||
<h1 class="font-display uppercase font-semibold tracking-wider text-xl">Reiverr</h1>
|
||||
</div>
|
||||
<div>Loading...</div>
|
||||
</div>
|
||||
@@ -1,5 +1,93 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import DetachedPage from '../components/DetachedPage/DetachedPage.svelte';
|
||||
import { type Session, sessions } from '../stores/session.store.js';
|
||||
import { reiverrApi } from '../apis/reiverr/reiverr-api';
|
||||
import Container from '../../Container.svelte';
|
||||
import Button from '../components/Button.svelte';
|
||||
import { TMDB_PROFILE_LARGE } from '../constants';
|
||||
import classNames from 'classnames';
|
||||
import AnimateScale from '../components/AnimateScale.svelte';
|
||||
import { navigate } from '../components/StackRouter/StackRouter';
|
||||
import { createModal } from '../components/Modal/modal.store';
|
||||
import AddUserDialog from '../components/Dialog/AddUserDialog.svelte';
|
||||
import Login from '../components/Login.svelte';
|
||||
import { Plus, Trash } from 'radix-icons-svelte';
|
||||
import AddElementOverlay from '../components/AddElementOverlay.svelte';
|
||||
|
||||
$: users = getUsers($sessions.sessions);
|
||||
|
||||
async function getUsers(sessions: Session[]) {
|
||||
return Promise.all(
|
||||
sessions.map(async (session) =>
|
||||
reiverrApi
|
||||
.getClient(session.baseUrl, session.token)
|
||||
.GET('/user')
|
||||
.then((r) => ({ session, user: r.data }))
|
||||
)
|
||||
).then((us) => us.filter((u) => !!u.user));
|
||||
}
|
||||
|
||||
function handleSwitchUser({ session, user }: Awaited<typeof users>[number]) {
|
||||
sessions.setActiveSession(session);
|
||||
navigate('/');
|
||||
}
|
||||
</script>
|
||||
|
||||
<DetachedPage sidebar={false} class="px-32 py-16">Users Page</DetachedPage>
|
||||
<DetachedPage sidebar={false} class="px-32 py-16 h-full flex flex-col items-center justify-center">
|
||||
{#await users then users}
|
||||
{#if users?.length}
|
||||
<h1 class="header4 mb-16">Who is watching?</h1>
|
||||
<Container direction="grid" gridCols={4} class="flex space-x-8 mb-16">
|
||||
{#each users as item}
|
||||
{@const user = item.user}
|
||||
<Container let:hasFocus on:clickOrSelect={() => user && handleSwitchUser(item)}>
|
||||
<AnimateScale {hasFocus}>
|
||||
<div
|
||||
class={classNames('w-40 h-40 bg-center bg-cover mb-4 rounded-xl', {
|
||||
selected: hasFocus
|
||||
})}
|
||||
style={`background-image: url('${TMDB_PROFILE_LARGE}/wo2hJpn04vbtmh0B9utCFdsQhxM.jpg')`}
|
||||
/>
|
||||
<div class={classNames('text-center header1', { '!text-secondary-100': hasFocus })}>
|
||||
{user?.name}
|
||||
</div>
|
||||
</AnimateScale>
|
||||
</Container>
|
||||
{/each}
|
||||
<Container let:hasFocus on:clickOrSelect={() => createModal(AddUserDialog, {})}>
|
||||
<AnimateScale {hasFocus}>
|
||||
<div
|
||||
class={classNames('relative overflow-hidden rounded-xl mb-4 w-40 h-40', {
|
||||
selected: hasFocus
|
||||
})}
|
||||
>
|
||||
<div
|
||||
class={`w-full h-full bg-center bg-cover`}
|
||||
style={`background-image: url('${TMDB_PROFILE_LARGE}/wo2hJpn04vbtmh0B9utCFdsQhxM.jpg')`}
|
||||
/>
|
||||
<AddElementOverlay />
|
||||
</div>
|
||||
<!-- <div class={classNames('text-center header1', { '!text-secondary-100': hasFocus })}>-->
|
||||
<!-- Add User-->
|
||||
<!-- </div>-->
|
||||
</AnimateScale>
|
||||
</Container>
|
||||
</Container>
|
||||
<Container direction="horizontal" class="flex space-x-4">
|
||||
<Button
|
||||
on:clickOrSelect={() => {
|
||||
sessions.removeSessions();
|
||||
navigate('/');
|
||||
}}
|
||||
icon={Trash}
|
||||
>
|
||||
Remove all Accounts
|
||||
</Button>
|
||||
</Container>
|
||||
{:else}
|
||||
<div class="bg-primary-800 rounded-2xl p-10 shadow-xl max-w-lg">
|
||||
<Login />
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
</DetachedPage>
|
||||
|
||||
@@ -724,6 +724,8 @@ export class Selectable {
|
||||
this._addChildCount++;
|
||||
// console.log('Incremented addChildCount to', this._addChildCount);
|
||||
if (focusIndex > 0) this.focusIndex.update((prev) => prev + 1); // TODO: Maybe needs fixing pt1
|
||||
} else if (this.children.length === 0 && get(this.hasFocus)) {
|
||||
childToFocus = child;
|
||||
}
|
||||
|
||||
if (this._removedChildrenCount > 1) {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { createLocalStorageStore } from './localstorage.store';
|
||||
import type { operations } from '../apis/reiverr/reiverr.generated';
|
||||
import axios from 'axios';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
export interface Session {
|
||||
id: string;
|
||||
baseUrl: string;
|
||||
token: string;
|
||||
}
|
||||
@@ -15,7 +17,7 @@ function useSessions() {
|
||||
}
|
||||
);
|
||||
|
||||
function setActiveSession(session: Session) {
|
||||
function setActiveSession(session?: Session) {
|
||||
sessions.update((s) => ({ ...s, activeSession: session }));
|
||||
}
|
||||
|
||||
@@ -33,12 +35,13 @@ function useSessions() {
|
||||
if (res.status !== 200) return res;
|
||||
|
||||
const session = {
|
||||
id: res.data.user.id,
|
||||
baseUrl,
|
||||
token: res.data.accessToken
|
||||
};
|
||||
|
||||
sessions.update((s) => {
|
||||
const sessions = s.sessions.concat(session);
|
||||
const sessions = s.sessions.filter((s) => s.id !== session.id).concat(session);
|
||||
return {
|
||||
sessions,
|
||||
activeSession: activate ? session : s.activeSession
|
||||
@@ -59,11 +62,16 @@ function useSessions() {
|
||||
});
|
||||
}
|
||||
|
||||
function removeSessions() {
|
||||
sessions.set({ sessions: [] });
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: sessions.subscribe,
|
||||
setActiveSession,
|
||||
addSession,
|
||||
removeSession
|
||||
removeSession,
|
||||
removeSessions
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,8 @@ function useUser() {
|
||||
Authorization: 'Bearer ' + activeSession.token
|
||||
}
|
||||
})
|
||||
.then((r) => r.data);
|
||||
.then((r) => r.data)
|
||||
.catch(() => null);
|
||||
|
||||
if (lastActiveSession === activeSession) userStore.set(user);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user