diff --git a/index.html b/index.html index c414ada..75614d1 100644 --- a/index.html +++ b/index.html @@ -30,7 +30,7 @@ document.documentElement.setAttribute('data-platform', navigator.platform ); - +
diff --git a/src/Container.svelte b/src/Container.svelte index 3ed4631..1786e9b 100644 --- a/src/Container.svelte +++ b/src/Container.svelte @@ -89,6 +89,7 @@ export const hasFocus = rest.hasFocus; export const hasFocusWithin = rest.hasFocusWithin; export const focusIndex = rest.focusIndex; + export const activeChild = rest.activeChild; export let tag = 'div'; diff --git a/src/lib/components/Integrations/JellyfinIntegration.svelte b/src/lib/components/Integrations/JellyfinIntegration.svelte new file mode 100644 index 0000000..ce75236 --- /dev/null +++ b/src/lib/components/Integrations/JellyfinIntegration.svelte @@ -0,0 +1,118 @@ + + +
+ !!u?.length)} + on:change={handleChange}>Base Url + !!u?.length)} + on:change={handleChange}>API Key +
+ +{#await jellyfinUsers then users} + {#if users?.length} + dispatch('click-user', { user: jellyfinUser, users })} + > + User + + {/if} +{/await} + +{#if error} +
{error}
+{/if} diff --git a/src/lib/components/Integrations/JellyfinIntegrationUsersDialog.svelte b/src/lib/components/Integrations/JellyfinIntegrationUsersDialog.svelte new file mode 100644 index 0000000..94f4b74 --- /dev/null +++ b/src/lib/components/Integrations/JellyfinIntegrationUsersDialog.svelte @@ -0,0 +1,23 @@ + + + + {#each users as user} + handleSelect(user)}> + {user.Name} + + {/each} + diff --git a/src/lib/components/Integrations/RadarrIntegration.svelte b/src/lib/components/Integrations/RadarrIntegration.svelte new file mode 100644 index 0000000..5fd3146 --- /dev/null +++ b/src/lib/components/Integrations/RadarrIntegration.svelte @@ -0,0 +1,76 @@ + + +
+ Base Url + API Key +
+ +{#if error} +
{error}
+{/if} diff --git a/src/lib/components/Integrations/SonarrIntegration.svelte b/src/lib/components/Integrations/SonarrIntegration.svelte new file mode 100644 index 0000000..7ec1a9b --- /dev/null +++ b/src/lib/components/Integrations/SonarrIntegration.svelte @@ -0,0 +1,76 @@ + + +
+ Base Url + API Key +
+ +{#if error} +
{error}
+{/if} diff --git a/src/lib/components/Integrations/TmdbIntegration.svelte b/src/lib/components/Integrations/TmdbIntegration.svelte new file mode 100644 index 0000000..7ec1a9b --- /dev/null +++ b/src/lib/components/Integrations/TmdbIntegration.svelte @@ -0,0 +1,76 @@ + + +
+ Base Url + API Key +
+ +{#if error} +
{error}
+{/if} diff --git a/src/lib/components/Tab/Tab.svelte b/src/lib/components/Tab/Tab.svelte index 2837bd3..1d6edee 100644 --- a/src/lib/components/Tab/Tab.svelte +++ b/src/lib/components/Tab/Tab.svelte @@ -1,27 +1,51 @@ = 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} > diff --git a/src/lib/components/Tab/Tab.ts b/src/lib/components/Tab/Tab.ts index 20cb1a6..808bbb9 100644 --- a/src/lib/components/Tab/Tab.ts +++ b/src/lib/components/Tab/Tab.ts @@ -1,15 +1,10 @@ import { writable } from 'svelte/store'; -enum TestTabs { - Tab1 = 'Tab1', - Tab2 = 'Tab2', - Tab3 = 'Tab3' -} - -const test = useTabs(TestTabs.Tab1); - -export function useTabs(defaultTab: T) { - const tab = writable(defaultTab); - - return { subscribe: tab.subscribe }; +export function useTabs(defaultTab: number) { + const openTab = writable(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 }; } diff --git a/src/lib/components/Tab/TabContainer.svelte b/src/lib/components/Tab/TabContainer.svelte new file mode 100644 index 0000000..e430945 --- /dev/null +++ b/src/lib/components/Tab/TabContainer.svelte @@ -0,0 +1,10 @@ + + + + + diff --git a/src/lib/components/TextField.svelte b/src/lib/components/TextField.svelte index 247e8c5..6990429 100644 --- a/src/lib/components/TextField.svelte +++ b/src/lib/components/TextField.svelte @@ -22,6 +22,8 @@ icon = Cross1; } else if (isValid === true) { icon = Check; + } else { + icon = undefined; } } diff --git a/src/lib/pages/ManagePage.svelte b/src/lib/pages/ManagePage.svelte index fa0e36d..7d28470 100644 --- a/src/lib/pages/ManagePage.svelte +++ b/src/lib/pages/ManagePage.svelte @@ -2,10 +2,40 @@ import Container from '../../Container.svelte'; import { appState } from '../stores/app-state.store'; import Button from '../components/Button.svelte'; - import { onMount } from 'svelte'; - import { isTizen } from '../utils/browser-detection'; import Toggle from '../components/Toggle.svelte'; 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 lastKey = ''; @@ -25,31 +55,50 @@ // (tizen as any)?.mediakey?.setMediaKeyEventListener?.(myMediaKeyChangeListener); // } // }); - - - User agent: {window?.navigator?.userAgent} -
Last key code: {lastKeyCode}
-
Last key: {lastKey}
- {#if tizenMediaKey} -
Tizen media key: {tizenMediaKey}
- {/if} -
- - localSettings.update((p) => ({ ...p, animateScrolling: detail }))} - /> -
-
- - localSettings.update((p) => ({ ...p, useCssTransitions: detail }))} - /> -
- -
+ async function handleSaveJellyfin() { + return appState.updateUser((prev) => ({ + ...prev, + settings: { + ...prev.settings, + jellyfin: { + ...prev.settings.jellyfin, + baseUrl: jellyfinBaseUrl, + apiKey: jellyfinApiKey, + userId: jellyfinUser?.Id ?? '' + } + } + })); + } + + async function handleSaveSonarr() { + return appState.updateUser((prev) => ({ + ...prev, + settings: { + ...prev.settings, + sonarr: { + ...prev.settings.sonarr, + baseUrl: sonarrBaseUrl, + apiKey: sonarrApiKey + } + } + })); + } + + async function handleSaveRadarr() { + return appState.updateUser((prev) => ({ + ...prev, + settings: { + ...prev.settings, + radarr: { + ...prev.settings.radarr, + baseUrl: radarrBaseUrl, + apiKey: radarrApiKey + } + } + })); + } + { @@ -58,3 +107,157 @@ lastKey = e.key; }} /> + + + + tab.set(Tabs.Interface)} + on:clickOrSelect={() => tab.set(Tabs.Interface)} + let:hasFocus + > + + Interface + + + tab.set(Tabs.Integrations)} + on:clickOrSelect={() => tab.set(Tabs.Integrations)} + let:hasFocus + > + + Integrations + + + tab.set(Tabs.About)} + on:clickOrSelect={() => tab.set(Tabs.About)} + let:hasFocus + > + + About + + + + + + +
+

Tmdb Account

+ { + sonarrBaseUrl = detail.baseUrl; + sonarrApiKey = detail.apiKey; + sonarrStale = detail.stale; + }} + /> +
+ +
+
+ +
+

Jellyfin

+ { + 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) + })} + /> +
+ +
+
+ +
+

Sonarr

+ { + sonarrBaseUrl = detail.baseUrl; + sonarrApiKey = detail.apiKey; + sonarrStale = detail.stale; + }} + /> +
+ +
+
+ +
+

Radarr

+ { + radarrBaseUrl = detail.baseUrl; + radarrApiKey = detail.apiKey; + radarrStale = detail.stale; + }} + /> +
+ +
+
+
+ + +
+ + + localSettings.update((p) => ({ ...p, animateScrolling: detail }))} + /> +
+
+ + + localSettings.update((p) => ({ ...p, useCssTransitions: detail }))} + /> +
+
+ + + User agent: {window?.navigator?.userAgent} +
Last key code: {lastKeyCode}
+
Last key: {lastKey}
+ {#if tizenMediaKey} +
Tizen media key: {tizenMediaKey}
+ {/if} + +
+
+
diff --git a/src/lib/pages/OnboardingPage.svelte b/src/lib/pages/OnboardingPage.svelte index 9faf54a..dfee075 100644 --- a/src/lib/pages/OnboardingPage.svelte +++ b/src/lib/pages/OnboardingPage.svelte @@ -12,6 +12,7 @@ import { sonarrApi } from '../apis/sonarr/sonarr-api'; import { radarrApi } from '../apis/radarr/radarr-api'; import { get } from 'svelte/store'; + import { useTabs } from '../components/Tab/Tab'; enum Tabs { Welcome, @@ -24,7 +25,7 @@ TmdbConnect = Tmdb + 0.1 } - let openTab: Tabs = Tabs.Welcome; + const tab = useTabs(Tabs.Welcome); let tmdbConnectRequestToken: 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() { @@ -177,7 +178,7 @@ } })); - openTab++; + tab.next(); } async function handleConnectRadarr() { @@ -219,7 +220,7 @@ } function handleBack() { - openTab--; + tab.previous(); } const tabContainer = @@ -227,7 +228,7 @@ - +

Welcome to Reiverr

Looks like this is a new account. This setup will get you started with connecting your @@ -236,7 +237,7 @@
-
- (openTab = Tabs.Tmdb)}> + tab.set(Tabs.Tmdb)}>

Connect a TMDB Account

To connect your TMDB account, log in via the link below and then click "Complete Connection". @@ -313,7 +314,7 @@ - +

Connect to Jellyfin

Connect to Jellyfin to watch movies and tv shows.
@@ -330,7 +331,7 @@ {#if users.length} (openTab = Tabs.SelectUser)} + on:clickOrSelect={() => tab.set(Tabs.SelectUser)} > User @@ -342,20 +343,15 @@ {/if} - + {#if jellyfinBaseUrl && jellyfinApiKey && jellyfinUser} {:else} - + {/if}
- (openTab = Tabs.Jellyfin)} - class={tabContainer} - > + tab.set(Tabs.Jellyfin)} class={tabContainer}>

Select User

{#await jellyfinUsers then users} {#each users as user} @@ -363,7 +359,7 @@ selected={user?.Id === jellyfinUser?.Id} on:clickOrSelect={() => { jellyfinUser = user; - openTab = Tabs.Jellyfin; + tab.set(Tabs.Jellyfin); }} > {user.Name} @@ -372,7 +368,7 @@ {/await}
- +

Connect to Sonarr

Connect to Sonarr for requesting and managing tv shows.
@@ -386,16 +382,16 @@ {/if} - + {#if sonarrBaseUrl && sonarrApiKey} {:else} - + {/if}
- +

Connect to Radarr

Connect to Radarr for requesting and managing movies.
@@ -409,7 +405,7 @@ {/if} - + {#if radarrBaseUrl && radarrApiKey} {:else} diff --git a/src/lib/selectable.ts b/src/lib/selectable.ts index 609cdb7..d7e7529 100644 --- a/src/lib/selectable.ts +++ b/src/lib/selectable.ts @@ -81,6 +81,8 @@ export type NavigationHandler = ( ) => void; export type KeyEventHandler = (selectable: Selectable, options: KeyEventOptions) => void; +export type ActiveChildStore = typeof Selectable.prototype.activeChild; + export class Selectable { id: number; name: string; @@ -109,6 +111,22 @@ export class Selectable { static focusedObject: Writable = writable(undefined); focusIndex: Writable = 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 = derived(Selectable.focusedObject, ($focusedObject) => { return $focusedObject === this; }); @@ -219,6 +237,24 @@ export class Selectable { 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 */ @@ -623,13 +659,15 @@ export class Selectable { hasFocusWithin: Readable; registerer: Registerer; focusIndex: Writable; + activeChild: ActiveChildStore; } { return { container: this, hasFocus: this.hasFocus, hasFocusWithin: this.hasFocusWithin, registerer: this.getRegisterer(), - focusIndex: this.focusIndex + focusIndex: this.focusIndex, + activeChild: this.activeChild }; }