feat: Improvements to the manage page
This commit is contained in:
@@ -77,7 +77,7 @@ html[data-useragent*="Tizen"] .selectable-secondary {
|
||||
}
|
||||
|
||||
.peer-selectable {
|
||||
@apply peer-focus-visible:outline outline-2 outline-[#f0cd6dc2] outline-offset-2;
|
||||
@apply peer-focus-visible:outline outline-2 outline-primary-500 outline-offset-2;
|
||||
}
|
||||
|
||||
.selectable-explicit {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import type { Readable } from 'svelte/store';
|
||||
import classNames from 'classnames';
|
||||
import AnimatedSelection from './AnimateScale.svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { type ComponentType, createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher<{ clickOrSelect: null }>();
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
export let type: 'primary' | 'secondary' | 'primary-dark' = 'primary';
|
||||
export let confirmDanger = false;
|
||||
export let action: (() => Promise<any>) | null = null;
|
||||
export let icon: ComponentType | undefined = undefined;
|
||||
export let iconAfter: ComponentType | undefined = undefined;
|
||||
export let iconAbsolute: ComponentType | undefined = undefined;
|
||||
|
||||
let actionIsFetching = false;
|
||||
$: _disabled = disabled || actionIsFetching;
|
||||
@@ -20,6 +23,8 @@
|
||||
$: if (!$hasFocus && armed) armed = false;
|
||||
|
||||
function handleClickOrSelect() {
|
||||
if (actionIsFetching || _disabled) return;
|
||||
|
||||
if (confirmDanger && !armed) {
|
||||
armed = true;
|
||||
return;
|
||||
@@ -74,17 +79,32 @@
|
||||
<slot name="icon" />
|
||||
</div>
|
||||
{/if}
|
||||
{#if icon}
|
||||
<div class="mr-2">
|
||||
<svelte:component this={icon} size={19} />
|
||||
</div>
|
||||
{/if}
|
||||
<slot {hasFocus} />
|
||||
{#if $$slots['icon-after']}
|
||||
<div class="ml-2">
|
||||
<slot name="icon-after" />
|
||||
</div>
|
||||
{/if}
|
||||
{#if iconAfter}
|
||||
<div class="ml-2">
|
||||
<svelte:component this={iconAfter} size={19} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if $$slots['icon-absolute']}
|
||||
<div class="absolute inset-y-0 right-0 flex items-center justify-center">
|
||||
<slot name="icon-absolute" />
|
||||
</div>
|
||||
{/if}
|
||||
{#if iconAbsolute}
|
||||
<div class="absolute inset-y-0 right-0 flex items-center justify-center">
|
||||
<svelte:component this={iconAbsolute} size={19} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<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';
|
||||
|
||||
const dispatch = createEventDispatcher<{ connected: null }>();
|
||||
|
||||
let tmdbConnectRequestToken: string | undefined = undefined;
|
||||
let tmdbConnectLink: string | undefined = undefined;
|
||||
let tmdbConnectQrCode: string | undefined = undefined;
|
||||
let tmdbError: string = '';
|
||||
|
||||
async function handleGenerateTMDBLink() {
|
||||
return tmdbApi.getConnectAccountLink().then((res) => {
|
||||
if (res?.status_code !== 1) return; // TODO add notification
|
||||
const link = `https://www.themoviedb.org/auth/access?request_token=${res?.request_token}`;
|
||||
const qrCode = `https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=${link}`;
|
||||
tmdbConnectRequestToken = res?.request_token;
|
||||
tmdbConnectLink = link;
|
||||
tmdbConnectQrCode = qrCode;
|
||||
});
|
||||
}
|
||||
|
||||
async function completeTMDBConnect() {
|
||||
if (!tmdbConnectRequestToken) return;
|
||||
return tmdbApi.getAccountAccessToken(tmdbConnectRequestToken).then((res) => {
|
||||
const { status_code, access_token, account_id } = res || {};
|
||||
if (status_code !== 1 || !access_token || !account_id) {
|
||||
tmdbError = 'Failed to connect account. Did you approve the request?';
|
||||
return; // TODO add notification
|
||||
}
|
||||
|
||||
appState.updateUser((prev) => ({
|
||||
...prev,
|
||||
settings: {
|
||||
...prev.settings,
|
||||
tmdb: {
|
||||
userId: account_id,
|
||||
sessionId: access_token
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
dispatch('connected');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<h1 class="header2 mb-2">Connect a TMDB Account</h1>
|
||||
<div class="body mb-8">
|
||||
To connect your TMDB account, log in via the link below and then click "Complete Connection".
|
||||
</div>
|
||||
|
||||
{#if tmdbConnectQrCode}
|
||||
<div
|
||||
class="w-[150px] h-[150px] bg-contain bg-center mb-8 mx-auto"
|
||||
style={`background-image: url(${tmdbConnectQrCode})`}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if tmdbError}
|
||||
<div class="text-red-500 mb-4">{tmdbError}</div>
|
||||
{/if}
|
||||
|
||||
<Container direction="horizontal" class="flex space-x-4 *:flex-1">
|
||||
{#if !tmdbConnectRequestToken}
|
||||
<Button type="primary-dark" action={handleGenerateTMDBLink}>Generate Link</Button>
|
||||
{:else if tmdbConnectLink}
|
||||
<Button type="primary-dark" action={completeTMDBConnect}>Complete Connection</Button>
|
||||
<Button type="primary-dark" on:clickOrSelect={() => window.open(tmdbConnectLink)}>
|
||||
Open Link
|
||||
<ExternalLink size={19} slot="icon-after" />
|
||||
</Button>
|
||||
{/if}
|
||||
</Container>
|
||||
@@ -0,0 +1,9 @@
|
||||
<script>
|
||||
import Dialog from '../Dialog/Dialog.svelte';
|
||||
import TmdbIntegrationConnect from './TmdbIntegrationConnect.svelte';
|
||||
import { modalStack } from '../Modal/modal.store';
|
||||
</script>
|
||||
|
||||
<Dialog>
|
||||
<TmdbIntegrationConnect on:connected={() => modalStack.closeTopmost()} />
|
||||
</Dialog>
|
||||
@@ -2,14 +2,39 @@
|
||||
import Container from '../../Container.svelte';
|
||||
import { ArrowRight } from 'radix-icons-svelte';
|
||||
import classNames from 'classnames';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher<{ clickOrSelect: null }>();
|
||||
|
||||
export let value: string;
|
||||
export let disabled: boolean = false;
|
||||
export let action: (() => Promise<any>) | undefined = undefined;
|
||||
|
||||
let actionIsFetching = false;
|
||||
$: _disabled = disabled || actionIsFetching;
|
||||
|
||||
function handleClickOrSelect() {
|
||||
if (actionIsFetching || _disabled) return;
|
||||
|
||||
if (action) {
|
||||
actionIsFetching = true;
|
||||
action().then(() => (actionIsFetching = false));
|
||||
}
|
||||
|
||||
dispatch('clickOrSelect');
|
||||
}
|
||||
</script>
|
||||
|
||||
<Container
|
||||
class="flex items-center justify-between bg-primary-900 rounded-xl px-6 py-2.5 mb-4 font-medium
|
||||
border-2 border-transparent focus:border-primary-500 hover:border-primary-500 cursor-pointer group"
|
||||
on:clickOrSelect
|
||||
class={classNames(
|
||||
'flex items-center justify-between bg-primary-900 rounded-xl px-6 py-2.5 mb-4 font-medium',
|
||||
'border-2 border-transparent focus:border-primary-500 hover:border-primary-500 group',
|
||||
{
|
||||
'cursor-pointer': !_disabled,
|
||||
'cursor-not-allowed pointer-events-none opacity-40': _disabled
|
||||
}
|
||||
)}
|
||||
on:clickOrSelect={handleClickOrSelect}
|
||||
let:hasFocus
|
||||
>
|
||||
<div>
|
||||
@@ -20,11 +45,19 @@
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
<ArrowRight
|
||||
class={classNames('transition-transform', {
|
||||
'text-primary-500 translate-x-0.5 scale-110': hasFocus,
|
||||
'group-hover:text-primary-500 group-hover:translate-x-0.5 group-hover:scale-110': true
|
||||
})}
|
||||
<slot
|
||||
name="icon"
|
||||
size={24}
|
||||
/>
|
||||
iconClass={classNames('group-hover:text-primary-500 group-hover:scale-110', {
|
||||
'text-primary-500 scale-110': hasFocus
|
||||
})}
|
||||
>
|
||||
<ArrowRight
|
||||
class={classNames('transition-transform', {
|
||||
'text-primary-500 translate-x-0.5 scale-110': hasFocus,
|
||||
'group-hover:text-primary-500 group-hover:translate-x-0.5 group-hover:scale-110': true
|
||||
})}
|
||||
size={24}
|
||||
/>
|
||||
</slot>
|
||||
</Container>
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
let input: HTMLInputElement;
|
||||
|
||||
const handleChange = (e: Event) => {
|
||||
// @ts-ignore
|
||||
checked = e.target?.checked;
|
||||
// @ts-ignore
|
||||
dispatch('change', e.target?.checked);
|
||||
};
|
||||
</script>
|
||||
@@ -30,7 +32,7 @@
|
||||
on:input={handleChange}
|
||||
/>
|
||||
<div
|
||||
class="w-11 h-6 rounded-full peer bg-zinc-600 bg-opacity-20 peer-checked:bg-amber-200 peer-checked:bg-opacity-30 peer-selectable
|
||||
after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px]"
|
||||
class="w-[3.25rem] h-7 rounded-full bg-secondary-700 peer-checked:bg-primary-500 peer-selectable
|
||||
after:bg-white after:border-gray-300 after:border after:rounded-full after:h-6 after:w-6 after:transition-all peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px]"
|
||||
/>
|
||||
</Container>
|
||||
|
||||
@@ -12,9 +12,13 @@
|
||||
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';
|
||||
import { tmdbApi } from '../apis/tmdb/tmdb-api';
|
||||
import SelectField from '../components/SelectField.svelte';
|
||||
import { ArrowRight, Trash } from 'radix-icons-svelte';
|
||||
import TmdbIntegrationConnectDialog from '../components/Integrations/TmdbIntegrationConnectDialog.svelte';
|
||||
import { createModal } from '../components/Modal/modal.store';
|
||||
|
||||
enum Tabs {
|
||||
Interface,
|
||||
@@ -40,6 +44,7 @@
|
||||
let lastKeyCode = 0;
|
||||
let lastKey = '';
|
||||
let tizenMediaKey = '';
|
||||
$: tmdbAccount = $appState.user?.settings.tmdb.userId ? tmdbApi.getAccountDetails() : undefined;
|
||||
|
||||
// onMount(() => {
|
||||
// if (isTizen()) {
|
||||
@@ -56,6 +61,20 @@
|
||||
// }
|
||||
// });
|
||||
|
||||
async function handleDisconnectTmdb() {
|
||||
return appState.updateUser((prev) => ({
|
||||
...prev,
|
||||
settings: {
|
||||
...prev.settings,
|
||||
tmdb: {
|
||||
...prev.settings.tmdb,
|
||||
userId: '',
|
||||
sessionId: ''
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
async function handleSaveJellyfin() {
|
||||
return appState.updateUser((prev) => ({
|
||||
...prev,
|
||||
@@ -108,7 +127,7 @@
|
||||
}}
|
||||
/>
|
||||
|
||||
<Container class="px-32 py-16 flex flex-col items-start" focusOnMount>
|
||||
<Container class="px-32 py-16" focusOnMount>
|
||||
<Container
|
||||
direction="horizontal"
|
||||
class="flex space-x-8 header2 pb-3 border-b-2 border-secondary-700 w-full mb-8"
|
||||
@@ -117,6 +136,7 @@
|
||||
on:enter={() => tab.set(Tabs.Interface)}
|
||||
on:clickOrSelect={() => tab.set(Tabs.Interface)}
|
||||
let:hasFocus
|
||||
focusOnClick
|
||||
>
|
||||
<span
|
||||
class={classNames('cursor-pointer', {
|
||||
@@ -131,6 +151,7 @@
|
||||
on:enter={() => tab.set(Tabs.Integrations)}
|
||||
on:clickOrSelect={() => tab.set(Tabs.Integrations)}
|
||||
let:hasFocus
|
||||
focusOnClick
|
||||
>
|
||||
<span
|
||||
class={classNames('cursor-pointer', {
|
||||
@@ -145,6 +166,7 @@
|
||||
on:enter={() => tab.set(Tabs.About)}
|
||||
on:clickOrSelect={() => tab.set(Tabs.About)}
|
||||
let:hasFocus
|
||||
focusOnClick
|
||||
>
|
||||
<span
|
||||
class={classNames('cursor-pointer', {
|
||||
@@ -158,81 +180,8 @@
|
||||
</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">
|
||||
<Tab {...tab} tab={Tabs.Interface} class="">
|
||||
<div class="flex items-center justify-between text-lg font-medium text-secondary-100 py-2">
|
||||
<label class="mr-2">Animate scrolling</label>
|
||||
<Toggle
|
||||
checked={$localSettings.animateScrolling}
|
||||
@@ -240,7 +189,7 @@
|
||||
localSettings.update((p) => ({ ...p, animateScrolling: detail }))}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center justify-between text-lg font-medium text-secondary-100 py-2">
|
||||
<label class="mr-2">Use CSS Transitions</label>
|
||||
<Toggle
|
||||
checked={$localSettings.useCssTransitions}
|
||||
@@ -250,6 +199,108 @@
|
||||
</div>
|
||||
</Tab>
|
||||
|
||||
<Tab {...tab} tab={Tabs.Integrations} class="">
|
||||
<Container direction="horizontal" class="gap-8 grid grid-cols-2">
|
||||
<Container class="flex flex-col space-y-8">
|
||||
<Container 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>
|
||||
</Container>
|
||||
|
||||
<Container 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>
|
||||
</Container>
|
||||
</Container>
|
||||
|
||||
<Container class="flex flex-col space-y-8">
|
||||
<Container class="bg-primary-800 rounded-xl p-8">
|
||||
<h1 class="mb-4 header2">Tmdb Account</h1>
|
||||
{#await tmdbAccount then tmdbAccount}
|
||||
{#if tmdbAccount}
|
||||
<SelectField value={tmdbAccount.username || ''} action={handleDisconnectTmdb}>
|
||||
Connected to
|
||||
<Trash
|
||||
slot="icon"
|
||||
let:size
|
||||
let:iconClass
|
||||
{size}
|
||||
class={classNames(iconClass, '')}
|
||||
/>
|
||||
</SelectField>
|
||||
{:else}
|
||||
<div class="flex space-x-4">
|
||||
<Button
|
||||
type="primary-dark"
|
||||
iconAfter={ArrowRight}
|
||||
on:clickOrSelect={() => createModal(TmdbIntegrationConnectDialog, {})}
|
||||
>Connect</Button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
<!-- <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>-->
|
||||
</Container>
|
||||
|
||||
<Container 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>
|
||||
</Container>
|
||||
</Container>
|
||||
</Container>
|
||||
</Tab>
|
||||
|
||||
<Tab {...tab} tab={Tabs.About}>
|
||||
User agent: {window?.navigator?.userAgent}
|
||||
<div>Last key code: {lastKeyCode}</div>
|
||||
@@ -257,7 +308,9 @@
|
||||
{#if tizenMediaKey}
|
||||
<div>Tizen media key: {tizenMediaKey}</div>
|
||||
{/if}
|
||||
<Button on:clickOrSelect={appState.logOut} class="hover:bg-red-500">Log Out</Button>
|
||||
<div class="flex space-x-4 mt-4">
|
||||
<Button on:clickOrSelect={appState.logOut} class="hover:bg-red-500">Log Out</Button>
|
||||
</div>
|
||||
</Tab>
|
||||
</Container>
|
||||
</Container>
|
||||
|
||||
Reference in New Issue
Block a user