feat: Deleting episodes and seasons, work in mm redesign
This commit is contained in:
@@ -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));
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
14
src/app.css
14
src/app.css
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
53
src/lib/components/Dialog/ConfirmDialog.svelte
Normal file
53
src/lib/components/Dialog/ConfirmDialog.svelte
Normal 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>
|
||||
0
src/lib/components/Dialog/Dialog.ts
Normal file
0
src/lib/components/Dialog/Dialog.ts
Normal 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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
const handleGrabRelease = (guid: string, indexerId: number) =>
|
||||
sonarrApi
|
||||
.downloadSonarrEpisode(guid, indexerId)
|
||||
.downloadSonarrRelease(guid, indexerId)
|
||||
.then((ok) => {
|
||||
if (!ok) {
|
||||
// TODO: Show error
|
||||
|
||||
@@ -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>
|
||||
38
src/lib/components/MediaManagerModal/MMLocalFileRow.svelte
Normal file
38
src/lib/components/MediaManagerModal/MMLocalFileRow.svelte
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>;
|
||||
@@ -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}
|
||||
|
||||
14
src/lib/components/Table/Table.svelte
Normal file
14
src/lib/components/Table/Table.svelte
Normal 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>
|
||||
28
src/lib/components/Table/TableButton.svelte
Normal file
28
src/lib/components/Table/TableButton.svelte
Normal 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>
|
||||
29
src/lib/components/Table/TableCell.svelte
Normal file
29
src/lib/components/Table/TableCell.svelte
Normal 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>
|
||||
17
src/lib/components/Table/TableHeaderCell.svelte
Normal file
17
src/lib/components/Table/TableHeaderCell.svelte
Normal 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>
|
||||
12
src/lib/components/Table/TableHeaderRow.svelte
Normal file
12
src/lib/components/Table/TableHeaderRow.svelte
Normal 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>
|
||||
29
src/lib/components/Table/TableHeaderSortBy.svelte
Normal file
29
src/lib/components/Table/TableHeaderSortBy.svelte
Normal 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>
|
||||
30
src/lib/components/Table/TableRow.svelte
Normal file
30
src/lib/components/Table/TableRow.svelte
Normal 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>
|
||||
@@ -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
|
||||
})}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user