feat: Improvements to the manage page

This commit is contained in:
Aleksi Lassila
2024-05-29 17:41:33 +03:00
parent 8513de44e2
commit 26e24eaf17
7 changed files with 287 additions and 92 deletions

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>