feat: Deleting episodes and seasons, work in mm redesign

This commit is contained in:
Aleksi Lassila
2024-05-02 17:18:30 +03:00
parent 0d05e3b530
commit d608c4b917
26 changed files with 594 additions and 264 deletions

View File

@@ -14,7 +14,7 @@
import MoviePage from './lib/pages/MoviePage.svelte';
import ModalStack from './lib/components/Modal/ModalStack.svelte';
import PageNotFound from './lib/pages/PageNotFound.svelte';
import NavigationDebugger from './lib/components/NavigationDebugger.svelte';
import NavigationDebugger from './lib/components/DebugElements.svelte';
appState.subscribe((s) => console.log('appState', s));

View File

@@ -2,13 +2,7 @@
<script lang="ts">
import { createEventDispatcher, onMount } from 'svelte';
import {
Selectable,
type EnterEvent,
type NavigateEvent,
type KeyEvent,
type Registrar
} from './lib/selectable';
import { Selectable, type EnterEvent, type NavigateEvent, type KeyEvent } from './lib/selectable';
import classNames from 'classnames';
const dispatch = createEventDispatcher<{
@@ -30,6 +24,7 @@
export let trapFocus = false;
export let debugOutline = false;
export let focusOnClick = false;
export let focusChildOnMount = false;
export let active = true;
@@ -111,7 +106,7 @@
}
onMount(() => {
rest.container._mountSelectable(focusOnMount);
rest.container._mountSelectable(focusOnMount, focusChildOnMount);
dispatch('mount', rest.container);

View File

@@ -54,6 +54,20 @@ html[data-useragent*="Tizen"] .selectable {
border-width: 2px;
}
.selectable-secondary {
@apply outline-none outline-0 border-2 transition-colors hover:border-primary-500;
}
html:not([data-useragent*="Tizen"]) .selectable-secondary {
@apply focus-visible:border-primary-500;
}
html[data-useragent*="Tizen"] .selectable-secondary {
@apply focus-within:border-primary-500;
}
.selected {
@apply outline-none outline-0 border-2 border-primary-500;
}

View File

@@ -168,7 +168,7 @@ export class SonarrApi implements Api<paths> {
return !!deleteResponse?.response.ok;
};
downloadSonarrEpisode = (guid: string, indexerId: number) =>
downloadSonarrRelease = (guid: string, indexerId: number) =>
this.getClient()
?.POST('/api/v3/release', {
params: {},
@@ -190,6 +190,15 @@ export class SonarrApi implements Api<paths> {
})
.then((res) => res.response.ok) || Promise.resolve(false);
deleteSonarrEpisodes = (ids: number[]) =>
this.getClient()
?.DELETE('/api/v3/episodefile/bulk', {
body: {
episodeFileIds: ids
}
})
.then((res) => res.response.ok) || Promise.resolve(false);
getSonarrDownloads = (): Promise<EpisodeDownload[]> =>
this.getClient()
?.GET('/api/v3/queue', {

View File

@@ -6,7 +6,7 @@
export let inactive: boolean = false;
export let focusOnMount: boolean = false;
export let style: 'primary' | 'secondary' = 'primary';
export let type: 'primary' | 'secondary' = 'primary';
let hasFocus: Readable<boolean>;
</script>
@@ -15,9 +15,11 @@
<Container
bind:hasFocus
class={classNames(
'px-6 py-2 rounded-lg font-medium tracking-wide flex items-center selectable',
'h-12 rounded-lg font-medium tracking-wide flex items-center group',
{
'bg-secondary-700': style === 'primary',
'selectable bg-secondary-800 px-6': type === 'primary',
'border-2 p-1 hover:border-primary-500': type === 'secondary',
'border-primary-500': type === 'secondary' && $hasFocus,
'cursor-pointer': !inactive,
'cursor-not-allowed pointer-events-none opacity-40': inactive
},
@@ -27,21 +29,30 @@
on:select
on:clickOrSelect
on:enter
let:hasFocus
{focusOnMount}
>
{#if $$slots.icon}
<div class="mr-2">
<slot name="icon" />
<div
class={classNames({
contents: type === 'primary',
'border-2 border-transparent h-full w-full rounded-md flex items-center px-6':
type === 'secondary',
'bg-primary-500 text-secondary-950': type === 'secondary' && $hasFocus,
'group-hover:bg-primary-500 group-hover:text-secondary-950': type === 'secondary'
})}
>
{#if $$slots.icon}
<div class="mr-2">
<slot name="icon" />
</div>
{/if}
<div class="flex-1 text-center text-nowrap">
<slot {hasFocus} />
</div>
{/if}
<div class="flex-1 text-center text-nowrap">
<slot {hasFocus} />
{#if $$slots['icon-after']}
<div class="ml-2">
<slot name="icon-after" />
</div>
{/if}
</div>
{#if $$slots['icon-after']}
<div class="ml-2">
<slot name="icon-after" />
</div>
{/if}
</Container>
</AnimatedSelection>

View File

@@ -49,7 +49,39 @@
{#if showOverlay}
<div
class={classNames('fixed bg-red-500 opacity-20 z-50 pointer-events-none')}
class={classNames('fixed bg-red-500/20 z-50 pointer-events-none')}
style={`left: ${x}px; top: ${y}px; width: ${width}px; height: ${height}px;`}
/>
<div class="fixed inset-0 border-x-[96px] border-y-[48px] border-green-500/10 z-50" />
<div class="fixed inset-0 px-32 grid grid-cols-12 gap-x-16 *:bg-purple-500/10 items-stretch z-50">
<div />
<div />
<div />
<div />
<div />
<div />
<div />
<div />
<div />
<div />
<div />
<div />
</div>
<!-- <div class="fixed inset-0 border-x-[96px] border-y-[48px] border-green-500/10 z-50" />-->
<!-- <div class="fixed inset-0 px-28 grid grid-cols-12 gap-x-10 *:bg-purple-500/10 items-stretch z-50">-->
<!-- <div />-->
<!-- <div />-->
<!-- <div />-->
<!-- <div />-->
<!-- <div />-->
<!-- <div />-->
<!-- <div />-->
<!-- <div />-->
<!-- <div />-->
<!-- <div />-->
<!-- <div />-->
<!-- <div />-->
<!-- </div>-->
{/if}

View File

@@ -0,0 +1,53 @@
<script lang="ts">
import Container from '../../../Container.svelte';
import Button from '../Button.svelte';
import Modal from '../Modal/Modal.svelte';
import { modalStack } from '../Modal/modal.store';
export let modalId: symbol;
type ActionFn = (() => Promise<any>) | (() => any);
export let confirm: ActionFn;
export let cancel: ActionFn = () => {};
let fetching = false;
function handleAction(actionFn: ActionFn) {
const result = actionFn();
if (result) {
fetching = true;
result.then(() => {
fetching = false;
modalStack.close(modalId);
});
} else {
modalStack.close(modalId);
}
}
</script>
<Modal {modalId}>
<div class="h-full flex items-center justify-center bg-secondary-950/75">
<div class="bg-secondary-800 rounded-xl max-w-lg p-16">
<div class="text-xl font-semibold tracking-wide mb-2">
<slot name="header" />
</div>
<div class="font-medium text-zinc-300 mb-8">
<slot />
</div>
<Container direction="horizontal" class="flex">
<Button
type="secondary"
inactive={fetching}
on:clickOrSelect={() => handleAction(confirm)}
class="mr-4"
>
Confirm
</Button>
<Button type="secondary" inactive={fetching} on:clickOrSelect={() => handleAction(cancel)}
>Cancel</Button
>
</Container>
</div>
</div>
</Modal>

View File

View File

@@ -1,49 +1,88 @@
<script lang="ts">
import ButtonGhost from '../Ghosts/ButtonGhost.svelte';
import Button from '../Button.svelte';
import { ChevronRight } from 'radix-icons-svelte';
import { formatSize } from '../../utils.js';
import type { FileResource } from '../../apis/combined-types';
import Table from '../Table/Table.svelte';
import MMLocalFileRow from '../MediaManagerModal/MMLocalFileRow.svelte';
import TableHeaderRow from '../Table/TableHeaderRow.svelte';
import TableHeaderSortBy from '../Table/TableHeaderSortBy.svelte';
import TableHeaderCell from '../Table/TableHeaderCell.svelte';
import Container from '../../../Container.svelte';
import Button from '../Button.svelte';
import { Trash } from 'radix-icons-svelte';
import { scrollIntoView } from '../../selectable';
import type { DeleteFile } from '../MediaManagerModal/MediaManagerModal';
import { modalStack } from '../Modal/modal.store';
import MMConfirmDeleteFileDialog from '../MediaManagerModal/Dialogs/MMConfirmDeleteFileDialog.svelte';
import { sonarrApi } from '../../apis/sonarr/sonarr-api';
export let files: Promise<FileResource[]>;
export let handleSelectFile: (file: FileResource) => void;
export let deleteFile: DeleteFile;
// export let handleSelectFile: (file: FileResource) => void;
let sortBy: 'size' | 'quality' | 'title' | 'runtime' | undefined = 'title';
let sortDirection: 'asc' | 'desc' = 'desc';
const toggleSortBy = (sort: typeof sortBy) => () => {
if (sortBy === sort) {
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
} else {
sortBy = sort;
sortDirection = 'desc';
}
};
</script>
<div class="flex flex-col -my-2">
{#await files}
{#each new Array(5) as _, index}
<div class="flex-1 my-2">
<ButtonGhost />
</div>
{#await files}
{#each new Array(5) as _, index}
<div class="flex-1 my-2">
<ButtonGhost />
</div>
{/each}
{:then files}
<div class="grid grid-cols-[1fr_max-content_max-content_max-content_max-content] gap-y-4">
<TableHeaderRow>
<TableHeaderSortBy
icon={sortBy === 'title' ? sortDirection : undefined}
on:clickOrSelect={toggleSortBy('title')}>Title</TableHeaderSortBy
>
<TableHeaderSortBy
icon={sortBy === 'runtime' ? sortDirection : undefined}
on:clickOrSelect={toggleSortBy('runtime')}>Runtime</TableHeaderSortBy
>
<TableHeaderSortBy
icon={sortBy === 'size' ? sortDirection : undefined}
on:clickOrSelect={toggleSortBy('size')}>Size</TableHeaderSortBy
>
<TableHeaderSortBy
icon={sortBy === 'quality' ? sortDirection : undefined}
on:clickOrSelect={toggleSortBy('quality')}>Quality</TableHeaderSortBy
>
<TableHeaderCell />
</TableHeaderRow>
{#each files as file}
<MMLocalFileRow {file} {deleteFile} />
{/each}
{:then files}
{#each files as file, index}
<div class="flex-1 my-2">
<Button
on:clickOrSelect={() => handleSelectFile(file)}
let:hasFocus
on:enter={scrollIntoView({ vertical: 64 })}
>
<div class="flex items-center w-full">
<div class="flex-1">
{file.relativePath}
</div>
{#if hasFocus}
<div class="flex items-center">
Details
<ChevronRight size={19} class="ml-1" />
</div>
{:else}
<div class="flex items-center text-zinc-400">
{formatSize(file.size || 0)}
</div>
{/if}
</div>
</Button>
</div>
{:else}
<div class="text-sm text-zinc-400">No local files found</div>
{/each}
{/await}
</div>
</div>
{#if files?.length}
<Container
direction="horizontal"
class="flex mt-8 mx-12"
on:enter={scrollIntoView({ vertical: 128 })}
>
<Button
on:clickOrSelect={() =>
modalStack.create(MMConfirmDeleteFileDialog, {
deleteFile: () => sonarrApi.deleteSonarrEpisodes(files.map((f) => f.id || -1))
})}
>
Delete all
<Trash size={19} slot="icon" />
</Button>
</Container>
{:else}
<div class="text-zinc-400 font-medium mx-12 flex flex-col items-center justify-center h-full">
<h1 class="text-xl text-zinc-300">No local files found</h1>
<div>Your local files will appear here.</div>
</div>
{/if}
{/await}

View File

@@ -1,26 +1,26 @@
<script lang="ts">
import { type RadarrRelease } from '../../apis/radarr/radarr-api';
import classNames from 'classnames';
import { useRequest } from '../../stores/data.store';
import { derived } from 'svelte/store';
import ButtonGhost from '../Ghosts/ButtonGhost.svelte';
import type { SonarrRelease } from '../../apis/sonarr/sonarr-api';
import Container from '../../../Container.svelte';
import MMReleaseListRow from '../MediaManagerModal/MMReleaseListRow.svelte';
import AnimateScale from '../AnimateScale.svelte';
import Table from '../Table/Table.svelte';
import TableHeaderRow from '../Table/TableHeaderRow.svelte';
import TableHeaderSortBy from '../Table/TableHeaderSortBy.svelte';
import type { GrabRelease } from '../MediaManagerModal/MediaManagerModal';
import Container from '../../../Container.svelte';
import TableHeaderCell from '../Table/TableHeaderCell.svelte';
type Release = RadarrRelease | SonarrRelease;
export let getReleases: () => Promise<Release[]>;
export let selectRelease: (release: Release) => void;
export let releases: Promise<Release[]>;
export let grabRelease: GrabRelease;
let showAll = false;
let sortBy: 'size' | 'quality' | 'seeders' | 'age' | undefined = 'seeders';
let sortDirection: 'asc' | 'desc' = 'desc';
const { data: releases, isLoading } = useRequest(getReleases);
const filteredReleases = derived(releases, ($releases) => {
if (!$releases) return [];
let filtered = $releases.slice();
function getRecommendedReleases(releases: Release[]) {
if (!releases) return [];
let filtered = releases.slice();
const releaseIsEnough = (r: Release) => r?.quality?.quality?.resolution || 0 > 720;
filtered.sort((a, b) => (b.seeders || 0) - (a.seeders || 0));
@@ -30,85 +30,78 @@
filtered.sort((a, b) => (b.size || 0) - (a.size || 0));
return filtered;
});
}
const toggleSortBy = (sort: typeof sortBy) => () => {
if (sortBy === sort) {
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
} else {
sortBy = sort;
sortDirection = 'desc';
}
};
function getSortFn(sb: typeof sortBy, sd: typeof sortDirection) {
return (a: Release, b: Release) => {
if (sb === 'size') {
return (sd === 'asc' ? 1 : -1) * ((a.size || 0) - (b.size || 0));
}
if (sb === 'quality') {
return (
(sd === 'asc' ? 1 : -1) *
((a.quality?.quality?.resolution || 0) - (b.quality?.quality?.resolution || 0))
);
}
if (sb === 'seeders') {
return (sd === 'asc' ? 1 : -1) * ((a.seeders || 0) - (b.seeders || 0));
}
if (sb === 'age') {
return (sd === 'asc' ? 1 : -1) * ((b.ageHours || 0) - (a.ageHours || 0));
}
return 0;
};
}
</script>
<table class="w-full border-spacing-y-2 border-spacing-x-2 border-separate -mx-8">
{#if $isLoading}
{#each new Array(5) as _, index}
<div class="flex-1 my-2">
<ButtonGhost />
</div>
{/each}
{:else}
<thead>
<Container tag="tr" direction="horizontal">
<Container tag="th" let:hasFocus>
<AnimateScale {hasFocus} class="ml-8">
<div
class={classNames('float-left rounded-full px-3 py-1', {
'bg-primary-500 text-secondary-800': hasFocus
})}
>
Title
</div>
</AnimateScale>
</Container>
<Container tag="th" let:hasFocus>
<AnimateScale {hasFocus}>
<div
class={classNames('float-left rounded-full px-3 py-1', {
'bg-primary-500 text-secondary-800': hasFocus
})}
>
Size
</div>
</AnimateScale>
</Container>
<Container tag="th" let:hasFocus>
<AnimateScale {hasFocus}>
<div
class={classNames('float-left rounded-full px-3 py-1', {
'bg-primary-500 text-secondary-800': hasFocus
})}
>
Peers
</div>
</AnimateScale>
</Container>
<Container tag="th" let:hasFocus>
<AnimateScale {hasFocus}>
<div
class={classNames('float-left rounded-full px-3 py-1', {
'bg-primary-500 text-secondary-800': hasFocus
})}
>
Quality
</div>
</AnimateScale>
</Container>
<th />
</Container>
</thead>
<Container focusOnMount tag="tbody" class="">
{#each $filteredReleases?.filter((r) => r.guid && r.indexerId) || [] as release, index}
<MMReleaseListRow {release} />
{#await releases}
{#each new Array(5) as _, index}
<div class="flex-1 my-2">
<ButtonGhost />
</div>
{/each}
{:then releases}
<div class="grid grid-cols-[1fr_max-content_max-content_max-content_max-content] gap-y-4">
<TableHeaderRow>
<TableHeaderSortBy
icon={sortBy === 'age' ? sortDirection : undefined}
on:clickOrSelect={toggleSortBy('age')}>Age</TableHeaderSortBy
>
<TableHeaderSortBy
icon={sortBy === 'size' ? sortDirection : undefined}
on:clickOrSelect={toggleSortBy('size')}>Size</TableHeaderSortBy
>
<TableHeaderSortBy
icon={sortBy === 'seeders' ? sortDirection : undefined}
on:clickOrSelect={toggleSortBy('seeders')}>Peers</TableHeaderSortBy
>
<TableHeaderSortBy
icon={sortBy === 'quality' ? sortDirection : undefined}
on:clickOrSelect={toggleSortBy('quality')}>Quality</TableHeaderSortBy
>
<TableHeaderCell />
</TableHeaderRow>
<Container class="contents" focusOnMount>
{#each getRecommendedReleases(releases).sort(getSortFn(sortBy, sortDirection)) as release, index}
<MMReleaseListRow {release} {grabRelease} />
{/each}
</Container>
<h1 class="text-2xl font-semibold mb-4 mt-8 mx-8">All Releases</h1>
<tbody class="divide-y divide-zinc-500">
{#each $releases?.filter((r) => r.guid && r.indexerId) || [] as release, index}
<MMReleaseListRow {release} />
{/each}
</tbody>
{/if}
</table>
<!--{#if !showAll && $releases?.length}-->
<!-- <div class="my-1 w-full">-->
<!-- <Button on:clickOrSelect={() => (showAll = true)}>Show all {$releases?.length} releases</Button>-->
<!-- </div>-->
<!--{:else if showAll}-->
<!-- <div class="my-1 w-full">-->
<!-- <Button on:clickOrSelect={() => (showAll = false)}>Show less</Button>-->
<!-- </div>-->
<!--{/if}-->
<h1 class="text-2xl font-semibold mb-4 mt-8 col-span-5 mx-12">All Releases</h1>
{#each releases
.filter((r) => r.guid && r.indexerId)
.sort(getSortFn(sortBy, sortDirection)) as release, index}
<MMReleaseListRow {release} {grabRelease} />
{/each}
</div>
{/await}

View File

@@ -26,7 +26,7 @@
const handleGrabRelease = (guid: string, indexerId: number) =>
sonarrApi
.downloadSonarrEpisode(guid, indexerId)
.downloadSonarrRelease(guid, indexerId)
.then((ok) => {
if (!ok) {
// TODO: Show error

View File

@@ -0,0 +1,11 @@
<script lang="ts">
import ConfirmDialog from '../../Dialog/ConfirmDialog.svelte';
export let modalId: symbol;
export let deleteFile: () => Promise<any>;
</script>
<ConfirmDialog {modalId} confirm={deleteFile}>
<h1 slot="header">Delete file?</h1>
<div>Are you sure you want to permanently delete this file?</div>
</ConfirmDialog>

View File

@@ -0,0 +1,38 @@
<script lang="ts">
import TableRow from '../Table/TableRow.svelte';
import { scrollIntoView } from '../../selectable';
import type { FileResource } from '../../apis/combined-types';
import { formatSize } from '../../utils';
import TableButton from '../Table/TableButton.svelte';
import { Trash } from 'radix-icons-svelte';
import TableCell from '../Table/TableCell.svelte';
import type { DeleteFile } from './MediaManagerModal';
import { modalStack } from '../Modal/modal.store';
import MMConfirmDeleteFileDialog from './Dialogs/MMConfirmDeleteFileDialog.svelte';
export let file: FileResource;
export let deleteFile: DeleteFile;
console.log(file);
</script>
<TableRow class="font-medium">
<TableCell>
<div class="font-medium text-lg">
{file.sceneName}
</div>
</TableCell>
<TableCell class="text-zinc-300">{file.mediaInfo?.runTime}</TableCell>
<TableCell class="text-zinc-300">{formatSize(file.size || 0)}</TableCell>
<TableCell class="text-zinc-300">{file.quality?.quality?.name}</TableCell>
<TableCell>
<TableButton
on:enter={scrollIntoView({ vertical: 128 })}
on:clickOrSelect={() =>
modalStack.create(MMConfirmDeleteFileDialog, {
deleteFile: () => deleteFile(file.id || -1)
})}
>
<Trash size={19} />
</TableButton>
</TableCell>
</TableRow>

View File

@@ -6,7 +6,7 @@
</script>
<div class="flex flex-col h-screen">
<div class="flex items-center pb-8 mb-8 pt-16 px-20">
<div class="flex items-center pb-8 mb-8 pt-16 px-28">
<div class="flex-1">
<div class="text-4xl font-semibold">
<slot name="title" />
@@ -20,40 +20,47 @@
<slot name="downloads" />
</div>
</div>
<div class="flex mb-8 mx-20">
<div class="flex mb-8 mx-28">
<h1
class={classNames('text-2xl font-semibold mr-8 transition-opacity', {
class={classNames('text-2xl font-semibold mr-8 transition-opacity cursor-pointer', {
'opacity-40': activeTab !== 'releases'
})}
on:click={() => (activeTab = 'releases')}
>
Releases
</h1>
<h1
class={classNames('text-2xl font-semibold mr-8 transition-opacity', {
class={classNames('text-2xl font-semibold mr-8 transition-opacity cursor-pointer', {
'opacity-40': activeTab !== 'local-files'
})}
on:click={() => (activeTab = 'local-files')}
>
Local Files
</h1>
</div>
<Container
focusOnMount
direction="horizontal"
class="flex-1 grid grid-cols-1 overflow-y-auto overflow-x-hidden px-20 pb-16 scrollbar-hide"
>
<Container focusOnMount direction="horizontal" class="flex-1 grid grid-cols-1 min-h-0">
<Container
focusOnMount
on:enter={() => (activeTab = 'releases')}
class={classNames('transition-all row-start-1 col-start-1 px-6 -mx-6', {
'opacity-50 -translate-x-full': activeTab !== 'releases'
})}
class={classNames(
'row-start-1 col-start-1 pb-16 mx-16',
'transition-all overflow-y-auto overflow-x-hidden scrollbar-hide',
{
'opacity-30 -translate-x-full': activeTab !== 'releases'
}
)}
>
<slot name="releases" />
</Container>
<Container
on:enter={() => (activeTab = 'local-files')}
class={classNames('transition-all row-start-1 col-start-1 px-6 -mx-6', {
'opacity-50 translate-x-full': activeTab !== 'local-files'
})}
class={classNames(
'row-start-1 col-start-1 pb-16 mx-16',
'transition-all overflow-y-auto overflow-x-hidden scrollbar-hide',
{
'opacity-30 translate-x-full': activeTab !== 'local-files'
}
)}
>
<slot name="local-files" />
</Container>

View File

@@ -2,102 +2,60 @@
import { formatMinutesToTime, formatSize } from '../../utils.js';
import type { RadarrRelease } from '../../apis/radarr/radarr-api';
import type { SonarrRelease } from '../../apis/sonarr/sonarr-api';
import Container from '../../../Container.svelte';
import classNames from 'classnames';
import { scrollIntoView } from '../../selectable';
import { Download } from 'radix-icons-svelte';
import type { Readable } from 'svelte/store';
import { Check, Download } from 'radix-icons-svelte';
import TableRow from '../Table/TableRow.svelte';
import type { GrabRelease } from './MediaManagerModal';
import TableButton from '../Table/TableButton.svelte';
import TableCell from '../Table/TableCell.svelte';
export let release: RadarrRelease | SonarrRelease;
let hasFocusWithin: Readable<boolean>;
export let grabRelease: GrabRelease;
let fetching = false;
let didGrab = false;
function handleGrabRelease() {
fetching = true;
grabRelease(release).then((ok) => {
fetching = false;
didGrab = ok;
});
}
</script>
<Container
tag="tr"
class={classNames('h-20 font-medium transition-transform px-4 py-2 rounded-lg relative', {
// 'scale-[102%] bg-primary-500/10': $hasFocusWithin
})}
bind:hasFocusWithin
on:enter={scrollIntoView({ vertical: 64 })}
>
<td class="pl-8">
<!-- Background, has to be inside a td to not create another column -->
<div
class={classNames('absolute inset-0 -z-10 rounded-xl transition-colors', {
'bg-secondary-800 border-primary-500 shadow-xl shadow-secondary-900': $hasFocusWithin,
'bg-transparent border-transparent': !$hasFocusWithin
})}
/>
<h2 class="text-sm font-medium text-zinc-300 mb-1">
{formatMinutesToTime(release.ageMinutes || 0)} ago
</h2>
<h1 class="font-medium text-lg">{release.title}</h1></td
>
<td class="text-zinc-300">
<TableRow class="font-medium">
<TableCell>
<div>
<h2 class="text-sm font-medium text-zinc-300 mb-1">
{formatMinutesToTime(release.ageMinutes || 0)} ago
</h2>
<h1 class="font-medium text-lg">{release.title}</h1>
</div>
</TableCell>
<TableCell class="text-zinc-300">
{formatSize(release.size || 0)}
</td>
<td class="text-zinc-300">
</TableCell>
<TableCell class="text-zinc-300">
<div
class="px-3 py-1 rounded bg-secondary-700 flex items-center justify-center float-left text-sm"
>
{release.seeders} / {release.leechers}
</div>
</td>
<td class="text-zinc-300">
</TableCell>
<TableCell class="text-zinc-300">
<div
class="px-3 py-1 rounded bg-secondary-700 flex items-center justify-center float-left text-sm"
>
{release.quality?.quality?.name}
</div>
</td>
<td class="">
<!-- <Container let:hasFocus on:enter={scrollIntoView({ vertical: 64 })}>-->
<!-- <div-->
<!-- class={classNames(-->
<!-- 'border-2 rounded-2xl p-1 cursor-pointer font-medium tracking-wide transition-colors',-->
<!-- {-->
<!-- 'border-zinc-300': !hasFocus,-->
<!-- 'border-primary-500': hasFocus-->
<!-- }-->
<!-- )}-->
<!-- >-->
<!-- <div-->
<!-- class={classNames(-->
<!-- 'px-4 py-2 rounded-xl flex items-center justify-center transition-colors',-->
<!-- {-->
<!-- 'bg-primary-500 text-secondary-800': hasFocus,-->
<!-- 'bg-transparent': !hasFocus-->
<!-- }-->
<!-- )}-->
<!-- >-->
<!-- Download-->
<!-- <Download class="ml-2" size={19} />-->
<!-- </div>-->
<!-- </div>-->
<!-- </Container>-->
<Container let:hasFocus class="pr-8">
<div
class={classNames(
'border-2 rounded-2xl p-1 cursor-pointer font-medium tracking-wide transition-colors',
{
'border-zinc-400': !hasFocus,
'border-primary-500': hasFocus
}
)}
>
<div
class={classNames(
'px-2 py-2 rounded-xl flex items-center justify-center transition-colors',
{
'bg-primary-500 text-secondary-800': hasFocus,
'bg-transparent': !hasFocus
}
)}
>
<Download size={19} />
</div>
</div>
</Container>
</td>
</Container>
</TableCell>
<TableCell>
<TableButton
active={!didGrab && !fetching}
on:clickOrSelect={handleGrabRelease}
on:enter={scrollIntoView({ vertical: 128 })}
>
<svelte:component this={didGrab ? Check : Download} size={19} />
</TableButton>
</TableCell>
</TableRow>

View File

@@ -0,0 +1,4 @@
import type { Release } from '../../apis/combined-types';
export type GrabRelease = (release: Release) => Promise<boolean>;
export type DeleteFile = (id: number) => Promise<boolean>;

View File

@@ -6,6 +6,8 @@
import ReleaseList from '../MediaManager/ReleaseList.svelte';
import DownloadList from '../MediaManager/DownloadList.svelte';
import FileList from '../MediaManager/FileList.svelte';
import type { DeleteFile, GrabRelease } from './MediaManagerModal';
import type { Release } from '../../apis/combined-types';
export let id: number; // Tmdb ID
export let season: number;
@@ -16,12 +18,15 @@
const downloads = sonarrItem.then((si) => sonarrApi.getDownloadsBySeriesId(si?.id || -1));
const files = sonarrItem.then((si) => sonarrApi.getFilesBySeriesId(si?.id || -1));
const getReleases = () =>
sonarrItem.then((si) => sonarrApi.getSeasonReleases(si?.id || -1, season));
const selectRelease = () => {};
// Releases
const releases: Promise<Release[]> = sonarrItem.then((si) =>
sonarrApi.getSeasonReleases(si?.id || -1, season)
);
const grabRelease: GrabRelease = (release) =>
sonarrApi.downloadSonarrRelease(release.guid || '', release.indexerId || -1);
const cancelDownload = sonarrApi.cancelDownloadSonarrEpisode;
const handleSelectFile = () => {};
const deleteFile: DeleteFile = sonarrApi.deleteSonarrEpisode;
</script>
<MMModal {modalId} {hidden}>
@@ -32,9 +37,9 @@
<MMMainLayout>
<h1 slot="title">{series?.title}</h1>
<h2 slot="subtitle">Season {season} Packs</h2>
<ReleaseList slot="releases" {getReleases} {selectRelease} />
<ReleaseList slot="releases" {releases} {grabRelease} />
<FileList slot="local-files" {files} {deleteFile} />
<DownloadList slot="downloads" {downloads} {cancelDownload} />
<FileList slot="local-files" {files} {handleSelectFile} />
</MMMainLayout>
{/if}
{/await}

View File

@@ -0,0 +1,14 @@
<script lang="ts">
import Container from '../../../Container.svelte';
export let rows: number;
</script>
<table class="w-full grid">
<thead>
<slot name="header" />
</thead>
<Container tag="tbody" {...$$restProps}>
<slot />
</Container>
</table>

View File

@@ -0,0 +1,28 @@
<script lang="ts">
import Container from '../../../Container.svelte';
import classNames from 'classnames';
export let active = true;
</script>
<Container let:hasFocus on:clickOrSelect {active} class="float-left" on:enter>
<div
class={classNames(
'border-2 rounded-2xl p-1 cursor-pointer font-medium tracking-wide transition-colors',
{
'border-zinc-400': !hasFocus,
'border-primary-500': hasFocus,
'opacity-50': !active
}
)}
>
<div
class={classNames('h-10 w-10 rounded-xl flex items-center justify-center transition-colors', {
'bg-primary-500 text-secondary-800': hasFocus,
'bg-transparent': !hasFocus
})}
>
<slot />
</div>
</div>
</Container>

View File

@@ -0,0 +1,29 @@
<script>
import classNames from 'classnames';
</script>
<div class={classNames('_table-cell', $$restProps.class)}>
<slot />
</div>
<style global>
:global(.row-wrapper > ._table-cell) {
@apply h-20 flex items-center px-4;
}
:global(.row-wrapper-selected > ._table-cell) {
@apply bg-secondary-800;
}
:global(.row-wrapper > ._table-cell:first-child) {
@apply rounded-l-xl pl-12;
}
:global(.row-wrapper > ._table-cell:last-child) {
@apply rounded-r-xl pr-12;
}
/*:global(.row-wrapper > ._table-cell:not(:first-child)) {*/
/* @apply rounded-l-xl pl-4;*/
/*}*/
</style>

View File

@@ -0,0 +1,17 @@
<div class="_header-cell self-stretch">
<slot />
</div>
<style global>
:global(.row-wrapper > ._header-cell) {
@apply h-12 flex items-center px-4;
}
:global(.row-wrapper > ._header-cell:first-child) {
@apply pl-12;
}
:global(.row-wrapper > ._header-cell:last-child) {
@apply pr-12;
}
</style>

View File

@@ -0,0 +1,12 @@
<script lang="ts">
import Container from '../../../Container.svelte';
</script>
<Container
direction="horizontal"
on:enter
class="*:sticky *:top-0 *:bg-secondary-900 row-wrapper contents"
>
<!-- <div class="absolute -inset-y-2 -inset-x-8 -z-10 bg-secondary-900" />-->
<slot />
</Container>

View File

@@ -0,0 +1,29 @@
<script lang="ts">
import Container from '../../../Container.svelte';
import classNames from 'classnames';
import { ChevronDown, ChevronUp } from 'radix-icons-svelte';
import type { Readable } from 'svelte/store';
import TableHeaderCell from './TableHeaderCell.svelte';
export let icon: undefined | 'asc' | 'desc' = undefined;
let hasFocus: Readable<boolean>;
</script>
<TableHeaderCell>
<Container
bind:hasFocus
on:clickOrSelect
focusOnClick
class={classNames(
'flex items-center rounded-full py-1 px-3 -mx-3 cursor-pointer select-none font-semibold float-left',
{
'bg-primary-500 text-secondary-800': $hasFocus
}
)}
>
<slot />
{#if icon}
<svelte:component this={icon === 'desc' ? ChevronDown : ChevronUp} size={19} class="ml-2" />
{/if}
</Container>
</TableHeaderCell>

View File

@@ -0,0 +1,30 @@
<script lang="ts">
import Container from '../../../Container.svelte';
import classNames from 'classnames';
import type { Readable } from 'svelte/store';
let hasFocusWithin: Readable<boolean>;
</script>
<Container
class={classNames(
'contents row-wrapper',
{
'row-wrapper-selected': $hasFocusWithin
// 'bg-secondary-800 shadow-xl shadow-secondary-900': $hasFocusWithin
// 'scale-[102%] bg-primary-500/10': $hasFocusWithin
},
$$restProps.class
)}
bind:hasFocusWithin
on:enter
>
<!-- Background, has to be inside a td to not create another column -->
<!-- <div-->
<!-- class={classNames('absolute inset-y-0 -inset-x-8 -z-10 rounded-xl transition-colors', {-->
<!-- 'bg-secondary-800 shadow-xl shadow-secondary-900': $hasFocusWithin,-->
<!-- 'bg-transparent': !$hasFocusWithin-->
<!-- })}-->
<!-- />-->
<slot />
</Container>

View File

@@ -33,7 +33,7 @@
<slot>Label</slot>
</label>
<input
class={classNames('bg-secondary-500 px-4 py-1.5 rounded-lg', {
class={classNames('bg-secondary-800 px-4 py-1.5 rounded-lg', {
selected: hasFocus,
unselected: !hasFocus
})}

View File

@@ -450,13 +450,15 @@ export class Selectable {
* have to wait until every element has htmlElement and then later (here) deduce
* the parent-child relationships.
*/
_mountSelectable(focusOnMount: boolean = false) {
_mountSelectable(focusOnMount: boolean = false, focusChildOnMount = false) {
console.debug('Mounting', this, Selectable._initializationStack.slice());
Selectable.finalizeTreeStructure();
if (!get(this.hasFocusWithin) && this.isFocusable(true) && focusOnMount) {
this.focus(); // TODO: CLEAN UP
} else if (!get(this.hasFocusWithin) && focusChildOnMount) {
this.focus({ setFocusedElement: false, propagate: false });
}
if (!this.htmlElement) {