feat: Manage page tabs

This commit is contained in:
Aleksi Lassila
2024-05-28 23:57:19 +03:00
parent f7bc8f2739
commit 402dd16f2f
14 changed files with 716 additions and 78 deletions

View File

@@ -0,0 +1,118 @@
<script lang="ts">
import TextField from '../TextField.svelte';
import { appState } from '../../stores/app-state.store';
import { createEventDispatcher } from 'svelte';
import SelectField from '../SelectField.svelte';
import { jellyfinApi, type JellyfinUser } from '../../apis/jellyfin/jellyfin-api';
import { get } from 'svelte/store';
const dispatch = createEventDispatcher<{
change: { baseUrl: string; apiKey: string; stale: boolean };
'click-user': { user: JellyfinUser | undefined; users: JellyfinUser[] };
}>();
export let baseUrl = '';
export let apiKey = '';
let originalBaseUrl: string | undefined;
let originalApiKey: string | undefined;
let timeout: ReturnType<typeof setTimeout>;
let error = '';
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 || '';
originalBaseUrl = baseUrl;
originalApiKey = apiKey;
handleChange();
});
$: if (jellyfinUser)
dispatch('change', {
baseUrl,
apiKey,
stale:
baseUrl && apiKey
? jellyfinUser?.Id !== get(appState).user?.settings.jellyfin.userId
: !jellyfinUser
});
function handleChange() {
clearTimeout(timeout);
error = '';
jellyfinUsers = undefined;
jellyfinUser = undefined;
const baseUrlCopy = baseUrl;
const apiKeyCopy = apiKey;
dispatch('change', {
baseUrl: '',
apiKey: '',
stale: baseUrl === '' && apiKey === '' && jellyfinUser === undefined
});
if (baseUrlCopy === '' || apiKeyCopy === '') return;
timeout = setTimeout(async () => {
jellyfinUsers = jellyfinApi.getJellyfinUsers(baseUrl, apiKey);
const users = await jellyfinUsers;
if (baseUrlCopy !== baseUrl || apiKeyCopy !== apiKey) return;
if (users.length) {
jellyfinUser = users.find((u) => u.Id === get(appState).user?.settings.jellyfin.userId);
const stale =
(baseUrlCopy !== originalBaseUrl || apiKeyCopy !== originalApiKey) &&
jellyfinUser !== undefined;
dispatch('change', { baseUrl: baseUrlCopy, apiKey: apiKeyCopy, stale });
} else {
error = 'Could not connect';
}
// if (res.status !== 200) {
// error =
// res.status === 404
// ? 'Server not found'
// : res.status === 401
// ? 'Invalid api key'
// : 'Could not connect';
//
// return; // TODO add notification
// } else {
// dispatch('change', { baseUrl: baseUrlCopy, apiKey: apiKeyCopy, stale });
// }
}, 1000);
}
</script>
<div class="space-y-4 mb-4">
<TextField
bind:value={baseUrl}
isValid={jellyfinUsers?.then((u) => !!u?.length)}
on:change={handleChange}>Base Url</TextField
>
<TextField
bind:value={apiKey}
isValid={jellyfinUsers?.then((u) => !!u?.length)}
on:change={handleChange}>API Key</TextField
>
</div>
{#await jellyfinUsers then users}
{#if users?.length}
<SelectField
value={jellyfinUser?.Name || 'Select User'}
on:clickOrSelect={() => dispatch('click-user', { user: jellyfinUser, users })}
>
User
</SelectField>
{/if}
{/await}
{#if error}
<div class="text-red-500 mb-4">{error}</div>
{/if}

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import Dialog from '../Dialog/Dialog.svelte';
import type { JellyfinUser } from '../../apis/jellyfin/jellyfin-api';
import SelectItem from '../SelectItem.svelte';
import { modalStack } from '../Modal/modal.store';
export let users: JellyfinUser[];
export let selectedUser: JellyfinUser | undefined;
export let handleSelectUser: (user: JellyfinUser) => void;
function handleSelect(user: JellyfinUser) {
handleSelectUser(user);
modalStack.closeTopmost();
}
</script>
<Dialog>
{#each users as user}
<SelectItem selected={user.Id === selectedUser?.Id} on:clickOrSelect={() => handleSelect(user)}>
{user.Name}
</SelectItem>
{/each}
</Dialog>

View File

@@ -0,0 +1,76 @@
<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';
const dispatch = createEventDispatcher<{
change: { baseUrl: string; apiKey: string; stale: boolean };
}>();
export let baseUrl = '';
export let apiKey = '';
let originalBaseUrl: string | undefined;
let originalApiKey: string | undefined;
let timeout: ReturnType<typeof setTimeout>;
let error = '';
let healthCheck: Promise<boolean> | undefined;
appState.subscribe((appState) => {
baseUrl = baseUrl || appState.user?.settings.radarr.baseUrl || '';
apiKey = apiKey || appState.user?.settings.radarr.apiKey || '';
originalBaseUrl = baseUrl;
originalApiKey = apiKey;
handleChange();
});
function handleChange() {
clearTimeout(timeout);
error = '';
healthCheck = undefined;
const baseUrlCopy = baseUrl;
const apiKeyCopy = apiKey;
const stale = baseUrlCopy !== originalBaseUrl || apiKeyCopy !== originalApiKey;
dispatch('change', {
baseUrl: '',
apiKey: '',
stale: baseUrl === '' && apiKey === ''
});
if (baseUrlCopy === '' || apiKeyCopy === '') return;
timeout = setTimeout(async () => {
const p = radarrApi.getHealth(baseUrlCopy, apiKeyCopy);
healthCheck = p.then((res) => res.status === 200);
const res = await p;
if (baseUrlCopy !== baseUrl || apiKeyCopy !== apiKey) return;
if (res.status !== 200) {
error =
res.status === 404
? 'Server not found'
: res.status === 401
? 'Invalid api key'
: 'Could not connect';
return; // TODO add notification
} else {
dispatch('change', { baseUrl: baseUrlCopy, apiKey: apiKeyCopy, stale });
}
}, 1000);
}
</script>
<div class="space-y-4 mb-4">
<TextField bind:value={baseUrl} isValid={healthCheck} on:change={handleChange}>Base Url</TextField
>
<TextField bind:value={apiKey} isValid={healthCheck} on:change={handleChange}>API Key</TextField>
</div>
{#if error}
<div class="text-red-500 mb-4">{error}</div>
{/if}

View File

@@ -0,0 +1,76 @@
<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';
const dispatch = createEventDispatcher<{
change: { baseUrl: string; apiKey: string; stale: boolean };
}>();
export let baseUrl = '';
export let apiKey = '';
let originalBaseUrl: string | undefined;
let originalApiKey: string | undefined;
let timeout: ReturnType<typeof setTimeout>;
let error = '';
let healthCheck: Promise<boolean> | undefined;
appState.subscribe((appState) => {
baseUrl = baseUrl || appState.user?.settings.sonarr.baseUrl || '';
apiKey = apiKey || appState.user?.settings.sonarr.apiKey || '';
originalBaseUrl = baseUrl;
originalApiKey = apiKey;
handleChange();
});
function handleChange() {
clearTimeout(timeout);
error = '';
healthCheck = undefined;
const baseUrlCopy = baseUrl;
const apiKeyCopy = apiKey;
const stale = baseUrlCopy !== originalBaseUrl || apiKeyCopy !== originalApiKey;
dispatch('change', {
baseUrl: '',
apiKey: '',
stale: baseUrl === '' && apiKey === ''
});
if (baseUrlCopy === '' || apiKeyCopy === '') return;
timeout = setTimeout(async () => {
const p = sonarrApi.getHealth(baseUrlCopy, apiKeyCopy);
healthCheck = p.then((res) => res.status === 200);
const res = await p;
if (baseUrlCopy !== baseUrl || apiKeyCopy !== apiKey) return;
if (res.status !== 200) {
error =
res.status === 404
? 'Server not found'
: res.status === 401
? 'Invalid api key'
: 'Could not connect';
return; // TODO add notification
} else {
dispatch('change', { baseUrl: baseUrlCopy, apiKey: apiKeyCopy, stale });
}
}, 1000);
}
</script>
<div class="space-y-4 mb-4">
<TextField bind:value={baseUrl} isValid={healthCheck} on:change={handleChange}>Base Url</TextField
>
<TextField bind:value={apiKey} isValid={healthCheck} on:change={handleChange}>API Key</TextField>
</div>
{#if error}
<div class="text-red-500 mb-4">{error}</div>
{/if}

View File

@@ -0,0 +1,76 @@
<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';
const dispatch = createEventDispatcher<{
change: { baseUrl: string; apiKey: string; stale: boolean };
}>();
export let baseUrl = '';
export let apiKey = '';
let originalBaseUrl: string | undefined;
let originalApiKey: string | undefined;
let timeout: ReturnType<typeof setTimeout>;
let error = '';
let healthCheck: Promise<boolean> | undefined;
appState.subscribe((appState) => {
baseUrl = baseUrl || appState.user?.settings.sonarr.baseUrl || '';
apiKey = apiKey || appState.user?.settings.sonarr.apiKey || '';
originalBaseUrl = baseUrl;
originalApiKey = apiKey;
handleChange();
});
function handleChange() {
clearTimeout(timeout);
error = '';
healthCheck = undefined;
const baseUrlCopy = baseUrl;
const apiKeyCopy = apiKey;
const stale = baseUrlCopy !== originalBaseUrl || apiKeyCopy !== originalApiKey;
dispatch('change', {
baseUrl: '',
apiKey: '',
stale: baseUrl === '' && apiKey === ''
});
if (baseUrlCopy === '' || apiKeyCopy === '') return;
timeout = setTimeout(async () => {
const p = sonarrApi.getHealth(baseUrlCopy, apiKeyCopy);
healthCheck = p.then((res) => res.status === 200);
const res = await p;
if (baseUrlCopy !== baseUrl || apiKeyCopy !== apiKey) return;
if (res.status !== 200) {
error =
res.status === 404
? 'Server not found'
: res.status === 401
? 'Invalid api key'
: 'Could not connect';
return; // TODO add notification
} else {
dispatch('change', { baseUrl: baseUrlCopy, apiKey: apiKeyCopy, stale });
}
}, 1000);
}
</script>
<div class="space-y-4 mb-4">
<TextField bind:value={baseUrl} isValid={healthCheck} on:change={handleChange}>Base Url</TextField
>
<TextField bind:value={apiKey} isValid={healthCheck} on:change={handleChange}>API Key</TextField>
</div>
{#if error}
<div class="text-red-500 mb-4">{error}</div>
{/if}

View File

@@ -1,27 +1,51 @@
<script lang="ts">
import Container from '../../../Container.svelte';
import classNames from 'classnames';
import type { Selectable } from '../../selectable';
import type { NavigateEvent, Selectable } from '../../selectable';
import type { Writable } from 'svelte/store';
export let tab: number;
export let index: number = tab;
export let openTab: number;
export let openTab: Writable<number>;
let selectable: Selectable;
$: active = tab === openTab;
$: if (active) selectable?.focus();
$: active = tab === $openTab;
$: if (active) selectable?.activate();
function handleNavigate({ detail }: CustomEvent<NavigateEvent>) {
// if (detail.willLeaveContainer) {
// if (
// (trapFocus === 'all' || trapFocus === 'horizontal') &&
// (detail.direction === 'left' || detail.direction === 'right')
// ) {
// detail.preventNavigation();
// detail.stopPropagation();
// } else if (
// (trapFocus === 'all' || trapFocus === 'vertical') &&
// (detail.direction === 'up' || detail.direction === 'down')
// ) {
// detail.preventNavigation();
// detail.stopPropagation();
// }
// }
}
</script>
<Container
trapFocus
class={classNames($$restProps.class, 'transition-all', {
'opacity-0 pointer-events-none': !active,
'-translate-x-10': !active && openTab >= index,
'translate-x-10': !active && openTab < index
})}
class={classNames(
$$restProps.class,
'transition-all col-start-1 col-end-1 row-start-1 row-end-1',
{
'opacity-0 pointer-events-none': !active,
'-translate-x-10': !active && $openTab >= index,
'translate-x-10': !active && $openTab < index
}
)}
bind:selectable
on:back
on:navigate={handleNavigate}
disabled={!active}
>
<slot />
</Container>

View File

@@ -1,15 +1,10 @@
import { writable } from 'svelte/store';
enum TestTabs {
Tab1 = 'Tab1',
Tab2 = 'Tab2',
Tab3 = 'Tab3'
}
const test = useTabs<TestTabs>(TestTabs.Tab1);
export function useTabs<T extends string>(defaultTab: T) {
const tab = writable<string>(defaultTab);
return { subscribe: tab.subscribe };
export function useTabs(defaultTab: number) {
const openTab = writable<number>(defaultTab);
const next = () => openTab.update((n) => n + 1);
const previous = () => openTab.update((n) => n - 1);
return { subscribe: openTab.subscribe, openTab, set: openTab.set, next, previous };
}

View File

@@ -0,0 +1,10 @@
<script lang="ts">
import Container from '../../../Container.svelte';
import type { NavigateEvent } from '../../selectable';
function handleNavigate({ detail }: CustomEvent<NavigateEvent>) {}
</script>
<Container on:navigate={handleNavigate}>
<slot />
</Container>

View File

@@ -22,6 +22,8 @@
icon = Cross1;
} else if (isValid === true) {
icon = Check;
} else {
icon = undefined;
}
}