feat: Manage page tabs
This commit is contained in:
@@ -30,7 +30,7 @@
|
|||||||
document.documentElement.setAttribute('data-platform', navigator.platform );
|
document.documentElement.setAttribute('data-platform', navigator.platform );
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-stone-950 min-h-screen text-white touch-manipulation relative -z-10">
|
<body class="bg-secondary-900 min-h-screen text-white touch-manipulation relative -z-10">
|
||||||
<div id="app" class="h-screen w-screen overflow-hidden relative"></div>
|
<div id="app" class="h-screen w-screen overflow-hidden relative"></div>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -89,6 +89,7 @@
|
|||||||
export const hasFocus = rest.hasFocus;
|
export const hasFocus = rest.hasFocus;
|
||||||
export const hasFocusWithin = rest.hasFocusWithin;
|
export const hasFocusWithin = rest.hasFocusWithin;
|
||||||
export const focusIndex = rest.focusIndex;
|
export const focusIndex = rest.focusIndex;
|
||||||
|
export const activeChild = rest.activeChild;
|
||||||
|
|
||||||
export let tag = 'div';
|
export let tag = 'div';
|
||||||
|
|
||||||
|
|||||||
118
src/lib/components/Integrations/JellyfinIntegration.svelte
Normal file
118
src/lib/components/Integrations/JellyfinIntegration.svelte
Normal 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}
|
||||||
@@ -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>
|
||||||
76
src/lib/components/Integrations/RadarrIntegration.svelte
Normal file
76
src/lib/components/Integrations/RadarrIntegration.svelte
Normal 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}
|
||||||
76
src/lib/components/Integrations/SonarrIntegration.svelte
Normal file
76
src/lib/components/Integrations/SonarrIntegration.svelte
Normal 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}
|
||||||
76
src/lib/components/Integrations/TmdbIntegration.svelte
Normal file
76
src/lib/components/Integrations/TmdbIntegration.svelte
Normal 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}
|
||||||
@@ -1,27 +1,51 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Container from '../../../Container.svelte';
|
import Container from '../../../Container.svelte';
|
||||||
import classNames from 'classnames';
|
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 tab: number;
|
||||||
export let index: number = tab;
|
export let index: number = tab;
|
||||||
export let openTab: number;
|
export let openTab: Writable<number>;
|
||||||
|
|
||||||
let selectable: Selectable;
|
let selectable: Selectable;
|
||||||
|
|
||||||
$: active = tab === openTab;
|
$: active = tab === $openTab;
|
||||||
$: if (active) selectable?.focus();
|
$: 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>
|
</script>
|
||||||
|
|
||||||
<Container
|
<Container
|
||||||
trapFocus
|
class={classNames(
|
||||||
class={classNames($$restProps.class, 'transition-all', {
|
$$restProps.class,
|
||||||
'opacity-0 pointer-events-none': !active,
|
'transition-all col-start-1 col-end-1 row-start-1 row-end-1',
|
||||||
'-translate-x-10': !active && openTab >= index,
|
{
|
||||||
'translate-x-10': !active && openTab < index
|
'opacity-0 pointer-events-none': !active,
|
||||||
})}
|
'-translate-x-10': !active && $openTab >= index,
|
||||||
|
'translate-x-10': !active && $openTab < index
|
||||||
|
}
|
||||||
|
)}
|
||||||
bind:selectable
|
bind:selectable
|
||||||
on:back
|
on:back
|
||||||
|
on:navigate={handleNavigate}
|
||||||
|
disabled={!active}
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
enum TestTabs {
|
export function useTabs(defaultTab: number) {
|
||||||
Tab1 = 'Tab1',
|
const openTab = writable<number>(defaultTab);
|
||||||
Tab2 = 'Tab2',
|
|
||||||
Tab3 = 'Tab3'
|
const next = () => openTab.update((n) => n + 1);
|
||||||
}
|
const previous = () => openTab.update((n) => n - 1);
|
||||||
|
|
||||||
const test = useTabs<TestTabs>(TestTabs.Tab1);
|
return { subscribe: openTab.subscribe, openTab, set: openTab.set, next, previous };
|
||||||
|
|
||||||
export function useTabs<T extends string>(defaultTab: T) {
|
|
||||||
const tab = writable<string>(defaultTab);
|
|
||||||
|
|
||||||
return { subscribe: tab.subscribe };
|
|
||||||
}
|
}
|
||||||
|
|||||||
10
src/lib/components/Tab/TabContainer.svelte
Normal file
10
src/lib/components/Tab/TabContainer.svelte
Normal 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>
|
||||||
@@ -22,6 +22,8 @@
|
|||||||
icon = Cross1;
|
icon = Cross1;
|
||||||
} else if (isValid === true) {
|
} else if (isValid === true) {
|
||||||
icon = Check;
|
icon = Check;
|
||||||
|
} else {
|
||||||
|
icon = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,40 @@
|
|||||||
import Container from '../../Container.svelte';
|
import Container from '../../Container.svelte';
|
||||||
import { appState } from '../stores/app-state.store';
|
import { appState } from '../stores/app-state.store';
|
||||||
import Button from '../components/Button.svelte';
|
import Button from '../components/Button.svelte';
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { isTizen } from '../utils/browser-detection';
|
|
||||||
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 Tab from '../components/Tab/Tab.svelte';
|
||||||
|
import { useTabs } from '../components/Tab/Tab';
|
||||||
|
import TextField from '../components/TextField.svelte';
|
||||||
|
import SonarrIntegration from '../components/Integrations/SonarrIntegration.svelte';
|
||||||
|
import RadarrIntegration from '../components/Integrations/RadarrIntegration.svelte';
|
||||||
|
import type { JellyfinUser } from '../apis/jellyfin/jellyfin-api';
|
||||||
|
import JellyfinIntegration from '../components/Integrations/JellyfinIntegration.svelte';
|
||||||
|
import { createModal } from '../components/Modal/modal.store';
|
||||||
|
import JellyfinIntegrationUsersDialog from '../components/Integrations/JellyfinIntegrationUsersDialog.svelte';
|
||||||
|
import TmdbIntegration from '../components/Integrations/TmdbIntegration.svelte';
|
||||||
|
|
||||||
|
enum Tabs {
|
||||||
|
Interface,
|
||||||
|
Integrations,
|
||||||
|
About
|
||||||
|
}
|
||||||
|
|
||||||
|
const tab = useTabs(Tabs.Integrations);
|
||||||
|
|
||||||
|
let jellyfinBaseUrl = '';
|
||||||
|
let jellyfinApiKey = '';
|
||||||
|
let jellyfinStale = false;
|
||||||
|
let jellyfinUser: JellyfinUser | undefined = undefined;
|
||||||
|
|
||||||
|
let sonarrBaseUrl = '';
|
||||||
|
let sonarrApiKey = '';
|
||||||
|
let sonarrStale = false;
|
||||||
|
|
||||||
|
let radarrBaseUrl = '';
|
||||||
|
let radarrApiKey = '';
|
||||||
|
let radarrStale = false;
|
||||||
|
|
||||||
let lastKeyCode = 0;
|
let lastKeyCode = 0;
|
||||||
let lastKey = '';
|
let lastKey = '';
|
||||||
@@ -25,31 +55,50 @@
|
|||||||
// (tizen as any)?.mediakey?.setMediaKeyEventListener?.(myMediaKeyChangeListener);
|
// (tizen as any)?.mediakey?.setMediaKeyEventListener?.(myMediaKeyChangeListener);
|
||||||
// }
|
// }
|
||||||
// });
|
// });
|
||||||
</script>
|
|
||||||
|
|
||||||
<Container class="pl-24 flex flex-col items-start" focusOnMount>
|
async function handleSaveJellyfin() {
|
||||||
User agent: {window?.navigator?.userAgent}
|
return appState.updateUser((prev) => ({
|
||||||
<div>Last key code: {lastKeyCode}</div>
|
...prev,
|
||||||
<div>Last key: {lastKey}</div>
|
settings: {
|
||||||
{#if tizenMediaKey}
|
...prev.settings,
|
||||||
<div>Tizen media key: {tizenMediaKey}</div>
|
jellyfin: {
|
||||||
{/if}
|
...prev.settings.jellyfin,
|
||||||
<div class="flex items-center justify-between">
|
baseUrl: jellyfinBaseUrl,
|
||||||
<label class="mr-2">Animate scrolling</label>
|
apiKey: jellyfinApiKey,
|
||||||
<Toggle
|
userId: jellyfinUser?.Id ?? ''
|
||||||
checked={$localSettings.animateScrolling}
|
}
|
||||||
on:change={({ detail }) => localSettings.update((p) => ({ ...p, animateScrolling: detail }))}
|
}
|
||||||
/>
|
}));
|
||||||
</div>
|
}
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<label class="mr-2">Use CSS Transitions</label>
|
async function handleSaveSonarr() {
|
||||||
<Toggle
|
return appState.updateUser((prev) => ({
|
||||||
checked={$localSettings.useCssTransitions}
|
...prev,
|
||||||
on:change={({ detail }) => localSettings.update((p) => ({ ...p, useCssTransitions: detail }))}
|
settings: {
|
||||||
/>
|
...prev.settings,
|
||||||
</div>
|
sonarr: {
|
||||||
<Button on:clickOrSelect={appState.logOut} class="hover:bg-red-500">Log Out</Button>
|
...prev.settings.sonarr,
|
||||||
</Container>
|
baseUrl: sonarrBaseUrl,
|
||||||
|
apiKey: sonarrApiKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSaveRadarr() {
|
||||||
|
return appState.updateUser((prev) => ({
|
||||||
|
...prev,
|
||||||
|
settings: {
|
||||||
|
...prev.settings,
|
||||||
|
radarr: {
|
||||||
|
...prev.settings.radarr,
|
||||||
|
baseUrl: radarrBaseUrl,
|
||||||
|
apiKey: radarrApiKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<svelte:window
|
<svelte:window
|
||||||
on:keydown={(e) => {
|
on:keydown={(e) => {
|
||||||
@@ -58,3 +107,157 @@
|
|||||||
lastKey = e.key;
|
lastKey = e.key;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Container class="px-32 py-16 flex flex-col items-start" focusOnMount>
|
||||||
|
<Container
|
||||||
|
direction="horizontal"
|
||||||
|
class="flex space-x-8 header2 pb-3 border-b-2 border-secondary-700 w-full mb-8"
|
||||||
|
>
|
||||||
|
<Container
|
||||||
|
on:enter={() => tab.set(Tabs.Interface)}
|
||||||
|
on:clickOrSelect={() => tab.set(Tabs.Interface)}
|
||||||
|
let:hasFocus
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class={classNames('cursor-pointer', {
|
||||||
|
'text-secondary-400': $tab !== Tabs.Interface,
|
||||||
|
'text-primary-500': hasFocus
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Interface
|
||||||
|
</span>
|
||||||
|
</Container>
|
||||||
|
<Container
|
||||||
|
on:enter={() => tab.set(Tabs.Integrations)}
|
||||||
|
on:clickOrSelect={() => tab.set(Tabs.Integrations)}
|
||||||
|
let:hasFocus
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class={classNames('cursor-pointer', {
|
||||||
|
'text-secondary-400': $tab !== Tabs.Integrations,
|
||||||
|
'text-primary-500': hasFocus
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Integrations
|
||||||
|
</span>
|
||||||
|
</Container>
|
||||||
|
<Container
|
||||||
|
on:enter={() => tab.set(Tabs.About)}
|
||||||
|
on:clickOrSelect={() => tab.set(Tabs.About)}
|
||||||
|
let:hasFocus
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class={classNames('cursor-pointer', {
|
||||||
|
'text-secondary-400': $tab !== Tabs.About,
|
||||||
|
'text-primary-500': hasFocus
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
About
|
||||||
|
</span>
|
||||||
|
</Container>
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
<Container class="grid">
|
||||||
|
<Tab {...tab} tab={Tabs.Integrations} class="space-y-8">
|
||||||
|
<div class="bg-primary-800 rounded-xl p-8">
|
||||||
|
<h1 class="mb-4 header2">Tmdb Account</h1>
|
||||||
|
<TmdbIntegration
|
||||||
|
on:change={({ detail }) => {
|
||||||
|
sonarrBaseUrl = detail.baseUrl;
|
||||||
|
sonarrApiKey = detail.apiKey;
|
||||||
|
sonarrStale = detail.stale;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div class="flex">
|
||||||
|
<Button disabled={!sonarrStale} type="primary-dark" action={handleSaveSonarr}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-primary-800 rounded-xl p-8">
|
||||||
|
<h1 class="mb-4 header2">Jellyfin</h1>
|
||||||
|
<JellyfinIntegration
|
||||||
|
bind:jellyfinUser
|
||||||
|
on:change={({ detail }) => {
|
||||||
|
jellyfinBaseUrl = detail.baseUrl;
|
||||||
|
jellyfinApiKey = detail.apiKey;
|
||||||
|
jellyfinStale = detail.stale;
|
||||||
|
}}
|
||||||
|
on:click-user={({ detail }) =>
|
||||||
|
createModal(JellyfinIntegrationUsersDialog, {
|
||||||
|
selectedUser: detail.user,
|
||||||
|
users: detail.users,
|
||||||
|
handleSelectUser: (u) => (jellyfinUser = u)
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<div class="flex">
|
||||||
|
<Button disabled={!jellyfinStale} type="primary-dark" action={handleSaveJellyfin}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-primary-800 rounded-xl p-8">
|
||||||
|
<h1 class="mb-4 header2">Sonarr</h1>
|
||||||
|
<SonarrIntegration
|
||||||
|
on:change={({ detail }) => {
|
||||||
|
sonarrBaseUrl = detail.baseUrl;
|
||||||
|
sonarrApiKey = detail.apiKey;
|
||||||
|
sonarrStale = detail.stale;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div class="flex">
|
||||||
|
<Button disabled={!sonarrStale} type="primary-dark" action={handleSaveSonarr}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-primary-800 rounded-xl p-8">
|
||||||
|
<h1 class="mb-4 header2">Radarr</h1>
|
||||||
|
<RadarrIntegration
|
||||||
|
on:change={({ detail }) => {
|
||||||
|
radarrBaseUrl = detail.baseUrl;
|
||||||
|
radarrApiKey = detail.apiKey;
|
||||||
|
radarrStale = detail.stale;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div class="flex">
|
||||||
|
<Button disabled={!radarrStale} type="primary-dark" action={handleSaveRadarr}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab {...tab} tab={Tabs.Interface}>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<label class="mr-2">Animate scrolling</label>
|
||||||
|
<Toggle
|
||||||
|
checked={$localSettings.animateScrolling}
|
||||||
|
on:change={({ detail }) =>
|
||||||
|
localSettings.update((p) => ({ ...p, animateScrolling: detail }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<label class="mr-2">Use CSS Transitions</label>
|
||||||
|
<Toggle
|
||||||
|
checked={$localSettings.useCssTransitions}
|
||||||
|
on:change={({ detail }) =>
|
||||||
|
localSettings.update((p) => ({ ...p, useCssTransitions: detail }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab {...tab} tab={Tabs.About}>
|
||||||
|
User agent: {window?.navigator?.userAgent}
|
||||||
|
<div>Last key code: {lastKeyCode}</div>
|
||||||
|
<div>Last key: {lastKey}</div>
|
||||||
|
{#if tizenMediaKey}
|
||||||
|
<div>Tizen media key: {tizenMediaKey}</div>
|
||||||
|
{/if}
|
||||||
|
<Button on:clickOrSelect={appState.logOut} class="hover:bg-red-500">Log Out</Button>
|
||||||
|
</Tab>
|
||||||
|
</Container>
|
||||||
|
</Container>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
import { sonarrApi } from '../apis/sonarr/sonarr-api';
|
import { sonarrApi } from '../apis/sonarr/sonarr-api';
|
||||||
import { radarrApi } from '../apis/radarr/radarr-api';
|
import { radarrApi } from '../apis/radarr/radarr-api';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
|
import { useTabs } from '../components/Tab/Tab';
|
||||||
|
|
||||||
enum Tabs {
|
enum Tabs {
|
||||||
Welcome,
|
Welcome,
|
||||||
@@ -24,7 +25,7 @@
|
|||||||
TmdbConnect = Tmdb + 0.1
|
TmdbConnect = Tmdb + 0.1
|
||||||
}
|
}
|
||||||
|
|
||||||
let openTab: Tabs = Tabs.Welcome;
|
const tab = useTabs(Tabs.Welcome);
|
||||||
|
|
||||||
let tmdbConnectRequestToken: string | undefined = undefined;
|
let tmdbConnectRequestToken: string | undefined = undefined;
|
||||||
let tmdbConnectLink: string | undefined = undefined;
|
let tmdbConnectLink: string | undefined = undefined;
|
||||||
@@ -122,7 +123,7 @@
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
openTab++;
|
tab.next();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +146,7 @@
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
openTab++;
|
tab.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleConnectSonarr() {
|
async function handleConnectSonarr() {
|
||||||
@@ -177,7 +178,7 @@
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
openTab++;
|
tab.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleConnectRadarr() {
|
async function handleConnectRadarr() {
|
||||||
@@ -219,7 +220,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleBack() {
|
function handleBack() {
|
||||||
openTab--;
|
tab.previous();
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabContainer =
|
const tabContainer =
|
||||||
@@ -227,7 +228,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Container focusOnMount class="h-full w-full grid justify-items-center items-center">
|
<Container focusOnMount class="h-full w-full grid justify-items-center items-center">
|
||||||
<Tab {openTab} tab={Tabs.Welcome} class={tabContainer}>
|
<Tab {...tab} tab={Tabs.Welcome} class={tabContainer}>
|
||||||
<h1 class="header2 mb-2">Welcome to Reiverr</h1>
|
<h1 class="header2 mb-2">Welcome to Reiverr</h1>
|
||||||
<div class="body mb-8">
|
<div class="body mb-8">
|
||||||
Looks like this is a new account. This setup will get you started with connecting your
|
Looks like this is a new account. This setup will get you started with connecting your
|
||||||
@@ -236,7 +237,7 @@
|
|||||||
<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={() => appState.logOut()}>Log Out</Button>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<Button type="primary-dark" on:clickOrSelect={() => openTab++}>
|
<Button type="primary-dark" on:clickOrSelect={() => tab.next()}>
|
||||||
Next
|
Next
|
||||||
<div class="absolute inset-y-0 right-0 flex items-center justify-center">
|
<div class="absolute inset-y-0 right-0 flex items-center justify-center">
|
||||||
<ArrowRight size={24} />
|
<ArrowRight size={24} />
|
||||||
@@ -246,7 +247,7 @@
|
|||||||
</Container>
|
</Container>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab {openTab} tab={Tabs.Tmdb} class={tabContainer} on:back={handleBack}>
|
<Tab {...tab} tab={Tabs.Tmdb} class={tabContainer} on:back={handleBack}>
|
||||||
<h1 class="header2 mb-2">Connect a TMDB Account</h1>
|
<h1 class="header2 mb-2">Connect a TMDB Account</h1>
|
||||||
<div class="body mb-8">
|
<div class="body mb-8">
|
||||||
Connect to TMDB for personalized recommendations based on your movie reviews and preferences.
|
Connect to TMDB for personalized recommendations based on your movie reviews and preferences.
|
||||||
@@ -258,7 +259,7 @@
|
|||||||
<SelectField
|
<SelectField
|
||||||
value={account.username || ''}
|
value={account.username || ''}
|
||||||
on:clickOrSelect={() => {
|
on:clickOrSelect={() => {
|
||||||
openTab = Tabs.TmdbConnect;
|
tab.set(Tabs.TmdbConnect);
|
||||||
handleGenerateTMDBLink();
|
handleGenerateTMDBLink();
|
||||||
}}>Logged in as</SelectField
|
}}>Logged in as</SelectField
|
||||||
>
|
>
|
||||||
@@ -266,7 +267,7 @@
|
|||||||
<Button
|
<Button
|
||||||
type="primary-dark"
|
type="primary-dark"
|
||||||
on:clickOrSelect={() => {
|
on:clickOrSelect={() => {
|
||||||
openTab = Tabs.TmdbConnect;
|
tab.set(Tabs.TmdbConnect);
|
||||||
handleGenerateTMDBLink();
|
handleGenerateTMDBLink();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -276,7 +277,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{/await}
|
{/await}
|
||||||
|
|
||||||
<Button type="primary-dark" on:clickOrSelect={() => openTab++}>
|
<Button type="primary-dark" on:clickOrSelect={() => tab.next()}>
|
||||||
{#if $appState.user?.settings.tmdb.userId}
|
{#if $appState.user?.settings.tmdb.userId}
|
||||||
Next
|
Next
|
||||||
{:else}
|
{:else}
|
||||||
@@ -287,7 +288,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab {openTab} tab={Tabs.TmdbConnect} class={tabContainer} on:back={() => (openTab = Tabs.Tmdb)}>
|
<Tab {...tab} tab={Tabs.TmdbConnect} class={tabContainer} on:back={() => tab.set(Tabs.Tmdb)}>
|
||||||
<h1 class="header2 mb-2">Connect a TMDB Account</h1>
|
<h1 class="header2 mb-2">Connect a TMDB Account</h1>
|
||||||
<div class="body mb-8">
|
<div class="body mb-8">
|
||||||
To connect your TMDB account, log in via the link below and then click "Complete Connection".
|
To connect your TMDB account, log in via the link below and then click "Complete Connection".
|
||||||
@@ -313,7 +314,7 @@
|
|||||||
</Container>
|
</Container>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab {openTab} tab={Tabs.Jellyfin} class={tabContainer}>
|
<Tab {...tab} tab={Tabs.Jellyfin} class={tabContainer}>
|
||||||
<h1 class="header2 mb-2">Connect to Jellyfin</h1>
|
<h1 class="header2 mb-2">Connect to Jellyfin</h1>
|
||||||
<div class="mb-8 body">Connect to Jellyfin to watch movies and tv shows.</div>
|
<div class="mb-8 body">Connect to Jellyfin to watch movies and tv shows.</div>
|
||||||
|
|
||||||
@@ -330,7 +331,7 @@
|
|||||||
{#if users.length}
|
{#if users.length}
|
||||||
<SelectField
|
<SelectField
|
||||||
value={jellyfinUser?.Name || 'Select User'}
|
value={jellyfinUser?.Name || 'Select User'}
|
||||||
on:clickOrSelect={() => (openTab = Tabs.SelectUser)}
|
on:clickOrSelect={() => tab.set(Tabs.SelectUser)}
|
||||||
>
|
>
|
||||||
User
|
User
|
||||||
</SelectField>
|
</SelectField>
|
||||||
@@ -342,20 +343,15 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<Container direction="horizontal" class="grid grid-cols-2 gap-4 mt-4">
|
<Container direction="horizontal" class="grid grid-cols-2 gap-4 mt-4">
|
||||||
<Button type="primary-dark" on:clickOrSelect={() => openTab--}>Back</Button>
|
<Button type="primary-dark" on:clickOrSelect={() => tab.previous()}>Back</Button>
|
||||||
{#if jellyfinBaseUrl && jellyfinApiKey && jellyfinUser}
|
{#if jellyfinBaseUrl && jellyfinApiKey && jellyfinUser}
|
||||||
<Button type="primary-dark" action={handleConnectJellyfin}>Connect</Button>
|
<Button type="primary-dark" action={handleConnectJellyfin}>Connect</Button>
|
||||||
{:else}
|
{:else}
|
||||||
<Button type="primary-dark" on:clickOrSelect={() => openTab++}>Skip</Button>
|
<Button type="primary-dark" on:clickOrSelect={() => tab.next()}>Skip</Button>
|
||||||
{/if}
|
{/if}
|
||||||
</Container>
|
</Container>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab {...tab} tab={Tabs.SelectUser} on:back={() => tab.set(Tabs.Jellyfin)} class={tabContainer}>
|
||||||
{openTab}
|
|
||||||
tab={Tabs.SelectUser}
|
|
||||||
on:back={() => (openTab = Tabs.Jellyfin)}
|
|
||||||
class={tabContainer}
|
|
||||||
>
|
|
||||||
<h1 class="header1 mb-2">Select User</h1>
|
<h1 class="header1 mb-2">Select User</h1>
|
||||||
{#await jellyfinUsers then users}
|
{#await jellyfinUsers then users}
|
||||||
{#each users as user}
|
{#each users as user}
|
||||||
@@ -363,7 +359,7 @@
|
|||||||
selected={user?.Id === jellyfinUser?.Id}
|
selected={user?.Id === jellyfinUser?.Id}
|
||||||
on:clickOrSelect={() => {
|
on:clickOrSelect={() => {
|
||||||
jellyfinUser = user;
|
jellyfinUser = user;
|
||||||
openTab = Tabs.Jellyfin;
|
tab.set(Tabs.Jellyfin);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{user.Name}
|
{user.Name}
|
||||||
@@ -372,7 +368,7 @@
|
|||||||
{/await}
|
{/await}
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab {openTab} tab={Tabs.Sonarr} class={tabContainer}>
|
<Tab {...tab} tab={Tabs.Sonarr} class={tabContainer}>
|
||||||
<h1 class="header2 mb-2">Connect to Sonarr</h1>
|
<h1 class="header2 mb-2">Connect to Sonarr</h1>
|
||||||
<div class="mb-8">Connect to Sonarr for requesting and managing tv shows.</div>
|
<div class="mb-8">Connect to Sonarr for requesting and managing tv shows.</div>
|
||||||
|
|
||||||
@@ -386,16 +382,16 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<Container direction="horizontal" class="grid grid-cols-2 gap-4 mt-4">
|
<Container direction="horizontal" class="grid grid-cols-2 gap-4 mt-4">
|
||||||
<Button type="primary-dark" on:clickOrSelect={() => openTab--}>Back</Button>
|
<Button type="primary-dark" on:clickOrSelect={() => tab.previous()}>Back</Button>
|
||||||
{#if sonarrBaseUrl && sonarrApiKey}
|
{#if sonarrBaseUrl && sonarrApiKey}
|
||||||
<Button type="primary-dark" action={handleConnectSonarr}>Connect</Button>
|
<Button type="primary-dark" action={handleConnectSonarr}>Connect</Button>
|
||||||
{:else}
|
{:else}
|
||||||
<Button type="primary-dark" on:clickOrSelect={() => openTab++}>Skip</Button>
|
<Button type="primary-dark" on:clickOrSelect={() => tab.next()}>Skip</Button>
|
||||||
{/if}
|
{/if}
|
||||||
</Container>
|
</Container>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab {openTab} tab={Tabs.Radarr} class={tabContainer}>
|
<Tab {...tab} tab={Tabs.Radarr} class={tabContainer}>
|
||||||
<h1 class="header2 mb-2">Connect to Radarr</h1>
|
<h1 class="header2 mb-2">Connect to Radarr</h1>
|
||||||
<div class="mb-8">Connect to Radarr for requesting and managing movies.</div>
|
<div class="mb-8">Connect to Radarr for requesting and managing movies.</div>
|
||||||
|
|
||||||
@@ -409,7 +405,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<Container direction="horizontal" class="grid grid-cols-2 gap-4 mt-4">
|
<Container direction="horizontal" class="grid grid-cols-2 gap-4 mt-4">
|
||||||
<Button type="primary-dark" on:clickOrSelect={() => openTab--}>Back</Button>
|
<Button type="primary-dark" on:clickOrSelect={() => tab.previous()}>Back</Button>
|
||||||
{#if radarrBaseUrl && radarrApiKey}
|
{#if radarrBaseUrl && radarrApiKey}
|
||||||
<Button type="primary-dark" action={handleConnectRadarr}>Connect</Button>
|
<Button type="primary-dark" action={handleConnectRadarr}>Connect</Button>
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ export type NavigationHandler = (
|
|||||||
) => void;
|
) => void;
|
||||||
export type KeyEventHandler = (selectable: Selectable, options: KeyEventOptions) => void;
|
export type KeyEventHandler = (selectable: Selectable, options: KeyEventOptions) => void;
|
||||||
|
|
||||||
|
export type ActiveChildStore = typeof Selectable.prototype.activeChild;
|
||||||
|
|
||||||
export class Selectable {
|
export class Selectable {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -109,6 +111,22 @@ export class Selectable {
|
|||||||
static focusedObject: Writable<Selectable | undefined> = writable(undefined);
|
static focusedObject: Writable<Selectable | undefined> = writable(undefined);
|
||||||
|
|
||||||
focusIndex: Writable<number> = writable(0);
|
focusIndex: Writable<number> = writable(0);
|
||||||
|
|
||||||
|
activeChild = (() => {
|
||||||
|
const store = derived(this.focusIndex, (focusIndex) => {
|
||||||
|
return this.children[focusIndex];
|
||||||
|
});
|
||||||
|
|
||||||
|
const set = (selectable: Selectable) => {
|
||||||
|
const index = this.children.indexOf(selectable);
|
||||||
|
if (index !== -1) this.focusIndex.set(index);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: store.subscribe,
|
||||||
|
set
|
||||||
|
};
|
||||||
|
})();
|
||||||
hasFocus: Readable<boolean> = derived(Selectable.focusedObject, ($focusedObject) => {
|
hasFocus: Readable<boolean> = derived(Selectable.focusedObject, ($focusedObject) => {
|
||||||
return $focusedObject === this;
|
return $focusedObject === this;
|
||||||
});
|
});
|
||||||
@@ -219,6 +237,24 @@ export class Selectable {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
activate() {
|
||||||
|
const parent = this.parent;
|
||||||
|
if (!parent) {
|
||||||
|
console.error('No parent, undefined behavior?');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentHasFocus = get(parent.hasFocusWithin);
|
||||||
|
|
||||||
|
if (parentHasFocus) {
|
||||||
|
this.focus();
|
||||||
|
} else {
|
||||||
|
const index = parent.children.indexOf(this);
|
||||||
|
if (index === -1) console.error('Child not found in parent when activating', this, parent);
|
||||||
|
this.parent?.focusIndex.update((prev) => (index >= 0 ? index : prev));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {boolean} whether the selectable is focusable
|
* @returns {boolean} whether the selectable is focusable
|
||||||
*/
|
*/
|
||||||
@@ -623,13 +659,15 @@ export class Selectable {
|
|||||||
hasFocusWithin: Readable<boolean>;
|
hasFocusWithin: Readable<boolean>;
|
||||||
registerer: Registerer;
|
registerer: Registerer;
|
||||||
focusIndex: Writable<number>;
|
focusIndex: Writable<number>;
|
||||||
|
activeChild: ActiveChildStore;
|
||||||
} {
|
} {
|
||||||
return {
|
return {
|
||||||
container: this,
|
container: this,
|
||||||
hasFocus: this.hasFocus,
|
hasFocus: this.hasFocus,
|
||||||
hasFocusWithin: this.hasFocusWithin,
|
hasFocusWithin: this.hasFocusWithin,
|
||||||
registerer: this.getRegisterer(),
|
registerer: this.getRegisterer(),
|
||||||
focusIndex: this.focusIndex
|
focusIndex: this.focusIndex,
|
||||||
|
activeChild: this.activeChild
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user