feat: Add downloads to library page
This commit is contained in:
@@ -199,7 +199,7 @@ export class RadarrApi implements Api<paths> {
|
||||
})
|
||||
.then((res) => res.response.ok) || Promise.resolve(false);
|
||||
|
||||
getRadarrDownloads = (): Promise<MovieDownload[]> =>
|
||||
getDownloads = (): Promise<MovieDownload[]> =>
|
||||
this.getClient()
|
||||
?.GET('/api/v3/queue', {
|
||||
params: {
|
||||
@@ -212,12 +212,10 @@ export class RadarrApi implements Api<paths> {
|
||||
Promise.resolve([]);
|
||||
|
||||
getDownloadsById = (radarrId: number) =>
|
||||
this.getRadarrDownloads().then((downloads) => downloads.filter((d) => d.movie.id === radarrId));
|
||||
this.getDownloads().then((downloads) => downloads.filter((d) => d.movie.id === radarrId));
|
||||
|
||||
getRadarrDownloadsByTmdbId = (tmdbId: number) =>
|
||||
this.getRadarrDownloads().then((downloads) =>
|
||||
downloads.filter((d) => d.movie.tmdbId === tmdbId)
|
||||
);
|
||||
this.getDownloads().then((downloads) => downloads.filter((d) => d.movie.tmdbId === tmdbId));
|
||||
|
||||
private lookupRadarrMovieByTmdbId = (tmdbId: number) =>
|
||||
this.getClient()
|
||||
|
||||
@@ -248,7 +248,7 @@ export class SonarrApi implements ApiAsync<paths> {
|
||||
.then((res) => res.response.ok) || Promise.resolve(false)
|
||||
);
|
||||
|
||||
getSonarrDownloads = (): Promise<EpisodeDownload[]> =>
|
||||
getDownloads = (): Promise<EpisodeDownload[]> =>
|
||||
this.getClient().then(
|
||||
(client) =>
|
||||
client
|
||||
@@ -271,9 +271,8 @@ export class SonarrApi implements ApiAsync<paths> {
|
||||
);
|
||||
|
||||
getDownloadsBySeriesId = (sonarrId: number) =>
|
||||
this.getSonarrDownloads().then((downloads) =>
|
||||
downloads.filter((d) => d.seriesId === sonarrId)
|
||||
) || Promise.resolve([]);
|
||||
this.getDownloads().then((downloads) => downloads.filter((d) => d.seriesId === sonarrId)) ||
|
||||
Promise.resolve([]);
|
||||
|
||||
removeFromSonarr = (id: number): Promise<boolean> =>
|
||||
this.getClient().then(
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<div
|
||||
class={classNames(
|
||||
'relative',
|
||||
'relative hover:scale-105',
|
||||
{
|
||||
'scale-105': hasFocus
|
||||
// 'transition-all': enabled
|
||||
|
||||
@@ -8,12 +8,14 @@
|
||||
import type { Readable } from 'svelte/store';
|
||||
import AnimatedSelection from '../AnimateScale.svelte';
|
||||
import { navigate } from '../StackRouter/StackRouter';
|
||||
import { getCardDimensions } from '../../utils';
|
||||
|
||||
export let tmdbId: number | undefined = undefined;
|
||||
export let tvdbId: number | undefined = undefined;
|
||||
export let jellyfinId: string = '';
|
||||
export let type: TitleType = 'movie';
|
||||
export let backdropUrl: string;
|
||||
export let group = false;
|
||||
|
||||
export let title = '';
|
||||
export let subtitle = '';
|
||||
@@ -27,125 +29,129 @@
|
||||
|
||||
let hasFocus: Readable<boolean>;
|
||||
|
||||
let dimensions = getDimensions(window.innerWidth);
|
||||
|
||||
function getDimensions(viewportWidth: number) {
|
||||
const minWidth = 240;
|
||||
|
||||
const margin = 128;
|
||||
const gap = 32;
|
||||
|
||||
const cols = Math.floor((gap - 2 * margin + viewportWidth) / (minWidth + gap));
|
||||
const scale = -(gap * (cols - 1) + 2 * margin - viewportWidth) / (cols * minWidth);
|
||||
|
||||
const newWidth = minWidth * scale;
|
||||
const newHeight = (3 / 2) * newWidth;
|
||||
|
||||
return {
|
||||
width: newWidth,
|
||||
height: newHeight
|
||||
};
|
||||
}
|
||||
let dimensions = getCardDimensions(window.innerWidth);
|
||||
</script>
|
||||
|
||||
<svelte:window on:resize={(e) => (dimensions = getDimensions(e.currentTarget.innerWidth))} />
|
||||
<svelte:window on:resize={(e) => (dimensions = getCardDimensions(e.currentTarget.innerWidth))} />
|
||||
|
||||
<AnimatedSelection hasFocus={$hasFocus}>
|
||||
<Container
|
||||
{disabled}
|
||||
on:clickOrSelect={() => {
|
||||
if (tmdbId || tvdbId) {
|
||||
// navigate(navigateWithType ? `${type}/${tmdbId || tvdbId}` : `${tmdbId || tvdbId}`);
|
||||
navigate(`/${type}/${tmdbId || tvdbId}`);
|
||||
}
|
||||
}}
|
||||
on:enter
|
||||
class={classNames(
|
||||
'relative flex flex-shrink-0 rounded-xl group hover:text-inherit overflow-hidden text-left cursor-pointer',
|
||||
'selectable',
|
||||
{
|
||||
'aspect-video': orientation === 'landscape',
|
||||
'aspect-[2/3]': orientation === 'portrait',
|
||||
'w-32 h-48': size === 'sm' && orientation === 'portrait',
|
||||
'h-32 w-56': size === 'sm' && orientation === 'landscape',
|
||||
'w-44 h-64': size === 'md' && orientation === 'portrait',
|
||||
'h-44 w-80': size === 'md' && orientation === 'landscape',
|
||||
// 'w-60 h-96': size === 'lg' && orientation === 'portrait',
|
||||
'h-60 w-96': size === 'lg' && orientation === 'landscape',
|
||||
'w-full h-96': size === 'dynamic',
|
||||
'shadow-lg': shadow
|
||||
}
|
||||
)}
|
||||
style={`width: ${dimensions.width}px; height: ${dimensions.height}px;`}
|
||||
focusOnClick
|
||||
bind:hasFocus
|
||||
>
|
||||
<LazyImg
|
||||
src={backdropUrl}
|
||||
class="absolute inset-0 group-hover:scale-105 transition-transform"
|
||||
/>
|
||||
<!-- This is the tinted and blurred hover overlay -->
|
||||
<!-- <div-->
|
||||
<!-- class="absolute inset-0 opacity-0 group-hover:opacity-30 transition-opacity bg-black"-->
|
||||
<!-- style="filter: blur(50px); transform: scale(3);"-->
|
||||
<!-- >-->
|
||||
<!-- <LazyImg src={backdropUrl} />-->
|
||||
<!-- </div>-->
|
||||
<div class="relative">
|
||||
{#if group}
|
||||
<div class="absolute inset-0 scale-95 translate-y-3.5 opacity-50">
|
||||
<LazyImg src={backdropUrl} class="absolute inset-0 rounded-xl" />
|
||||
<div class="absolute inset-0 bg-white/10 rounded-xl" />
|
||||
|
||||
<!-- Mouse hover details -->
|
||||
<!-- <div-->
|
||||
<!-- class={classNames(-->
|
||||
<!-- 'flex-1 flex flex-col justify-between bg-black bg-opacity-40 opacity-0 group-hover:opacity-100 transition-opacity z-[1]',-->
|
||||
<!-- {-->
|
||||
<!-- 'py-2 px-3': true-->
|
||||
<!-- }-->
|
||||
<!-- )}-->
|
||||
<!-- >-->
|
||||
<!-- <div class="flex justify-self-start justify-between">-->
|
||||
<!-- <slot name="top-left">-->
|
||||
<!-- <div>-->
|
||||
<!-- <h1 class="text-zinc-100 font-bold line-clamp-2 text-lg">{title}</h1>-->
|
||||
<!-- <h2 class="text-zinc-300 text-sm font-medium line-clamp-2">{subtitle}</h2>-->
|
||||
<!-- </div>-->
|
||||
<!-- </slot>-->
|
||||
<!-- <slot name="top-right">-->
|
||||
<!-- <div />-->
|
||||
<!-- </slot>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="flex justify-self-end justify-between">-->
|
||||
<!-- <slot name="bottom-left">-->
|
||||
<!-- <div>-->
|
||||
<!-- {#if rating}-->
|
||||
<!-- <h2 class="flex items-center gap-1.5 text-sm text-zinc-300 font-medium">-->
|
||||
<!-- <Star />{rating.toFixed(1)}-->
|
||||
<!-- </h2>-->
|
||||
<!-- {/if}-->
|
||||
<!-- </div>-->
|
||||
<!-- </slot>-->
|
||||
<!-- <slot name="bottom-right">-->
|
||||
<!-- <div />-->
|
||||
<!-- </slot>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<LazyImg
|
||||
src={backdropUrl}
|
||||
class="absolute inset-0 scale-95 translate-y-4 rounded-xl opacity-25"
|
||||
/>
|
||||
<div class="absolute inset-0 scale-95 translate-y-4 rounded-xl bg-white/10 opacity-25" />
|
||||
</div>
|
||||
{/if}
|
||||
<AnimatedSelection hasFocus={$hasFocus}>
|
||||
<Container
|
||||
{disabled}
|
||||
on:clickOrSelect={() => {
|
||||
if (tmdbId || tvdbId) navigate(`/${type}/${tmdbId || tvdbId}`);
|
||||
}}
|
||||
on:enter
|
||||
class={classNames(
|
||||
'relative flex flex-shrink-0 rounded-xl group hover:text-inherit overflow-hidden text-left cursor-pointer',
|
||||
'selectable',
|
||||
{
|
||||
'aspect-video': orientation === 'landscape',
|
||||
'aspect-[2/3]': orientation === 'portrait',
|
||||
'w-32 h-48': size === 'sm' && orientation === 'portrait',
|
||||
'h-32 w-56': size === 'sm' && orientation === 'landscape',
|
||||
'w-44 h-64': size === 'md' && orientation === 'portrait',
|
||||
'h-44 w-80': size === 'md' && orientation === 'landscape',
|
||||
// 'w-60 h-96': size === 'lg' && orientation === 'portrait',
|
||||
'h-60 w-96': size === 'lg' && orientation === 'landscape',
|
||||
'w-full h-96': size === 'dynamic',
|
||||
'shadow-lg': shadow
|
||||
}
|
||||
)}
|
||||
style={`width: ${dimensions.width}px; height: ${dimensions.height}px;`}
|
||||
focusOnClick
|
||||
bind:hasFocus
|
||||
>
|
||||
<!--{#if !group}-->
|
||||
<LazyImg src={backdropUrl} class="absolute inset-0" />
|
||||
<!--{:else}-->
|
||||
<!-- <LazyImg src={backdropUrl} class="absolute inset-0 opacity-10 " />-->
|
||||
<!-- <div class="absolute inset-0 bg-white/10 opacity-10" />-->
|
||||
<!-- <LazyImg-->
|
||||
<!-- src={backdropUrl}-->
|
||||
<!-- class="absolute inset-0 scale-95 translate-y-[0.5rem] rounded-xl opacity-25"-->
|
||||
<!-- />-->
|
||||
<!-- <div class="absolute inset-0 bg-white/10 opacity-10" />-->
|
||||
<!-- <LazyImg-->
|
||||
<!-- src={backdropUrl}-->
|
||||
<!-- class="absolute inset-0 scale-90 translate-y-[1.125rem] rounded-xl "-->
|
||||
<!-- />-->
|
||||
<!--{/if}-->
|
||||
<!-- This is the tinted and blurred hover overlay -->
|
||||
<!-- <div-->
|
||||
<!-- class="absolute inset-0 opacity-0 group-hover:opacity-30 transition-opacity bg-black"-->
|
||||
<!-- style="filter: blur(50px); transform: scale(3);"-->
|
||||
<!-- >-->
|
||||
<!-- <LazyImg src={backdropUrl} />-->
|
||||
<!-- </div>-->
|
||||
|
||||
<!-- Play Button -->
|
||||
{#if jellyfinId}
|
||||
<div class="absolute inset-0 flex items-center justify-center z-[1]">
|
||||
<PlayButton
|
||||
on:click={(e) => {
|
||||
e.preventDefault();
|
||||
jellyfinId && true; //playerState.streamJellyfinId(jellyfinId);
|
||||
}}
|
||||
class="sm:opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if progress}
|
||||
<div
|
||||
class="absolute bottom-2 lg:bottom-3 inset-x-2 lg:inset-x-3 bg-gradient-to-t ease-in-out z-[1]"
|
||||
>
|
||||
<ProgressBar {progress} />
|
||||
</div>
|
||||
{/if}
|
||||
</Container>
|
||||
</AnimatedSelection>
|
||||
<!-- Mouse hover details -->
|
||||
<!-- <div-->
|
||||
<!-- class={classNames(-->
|
||||
<!-- 'flex-1 flex flex-col justify-between bg-black bg-opacity-40 opacity-0 group-hover:opacity-100 transition-opacity z-[1]',-->
|
||||
<!-- {-->
|
||||
<!-- 'py-2 px-3': true-->
|
||||
<!-- }-->
|
||||
<!-- )}-->
|
||||
<!-- >-->
|
||||
<!-- <div class="flex justify-self-start justify-between">-->
|
||||
<!-- <slot name="top-left">-->
|
||||
<!-- <div>-->
|
||||
<!-- <h1 class="text-zinc-100 font-bold line-clamp-2 text-lg">{title}</h1>-->
|
||||
<!-- <h2 class="text-zinc-300 text-sm font-medium line-clamp-2">{subtitle}</h2>-->
|
||||
<!-- </div>-->
|
||||
<!-- </slot>-->
|
||||
<!-- <slot name="top-right">-->
|
||||
<!-- <div />-->
|
||||
<!-- </slot>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="flex justify-self-end justify-between">-->
|
||||
<!-- <slot name="bottom-left">-->
|
||||
<!-- <div>-->
|
||||
<!-- {#if rating}-->
|
||||
<!-- <h2 class="flex items-center gap-1.5 text-sm text-zinc-300 font-medium">-->
|
||||
<!-- <Star />{rating.toFixed(1)}-->
|
||||
<!-- </h2>-->
|
||||
<!-- {/if}-->
|
||||
<!-- </div>-->
|
||||
<!-- </slot>-->
|
||||
<!-- <slot name="bottom-right">-->
|
||||
<!-- <div />-->
|
||||
<!-- </slot>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
|
||||
<!-- Play Button -->
|
||||
{#if jellyfinId}
|
||||
<div class="absolute inset-0 flex items-center justify-center z-[1]">
|
||||
<PlayButton
|
||||
on:click={(e) => {
|
||||
e.preventDefault();
|
||||
jellyfinId && true; //playerState.streamJellyfinId(jellyfinId);
|
||||
}}
|
||||
class="sm:opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if progress}
|
||||
<div
|
||||
class="absolute bottom-2 lg:bottom-3 inset-x-2 lg:inset-x-3 bg-gradient-to-t ease-in-out z-[1]"
|
||||
>
|
||||
<ProgressBar {progress} />
|
||||
</div>
|
||||
{/if}
|
||||
</Container>
|
||||
</AnimatedSelection>
|
||||
</div>
|
||||
|
||||
@@ -2,10 +2,14 @@
|
||||
import Container from '../../Container.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import classNames from 'classnames';
|
||||
import { getCardDimensions } from '../utils';
|
||||
|
||||
export let direction: 'horizontal' | 'vertical' = 'vertical';
|
||||
|
||||
let cols: number = 1;
|
||||
let cols = getCardDimensions(window.innerWidth).columns;
|
||||
$: console.log('cols', cols);
|
||||
|
||||
// let cols: number = 1;
|
||||
const calculateRows = () => {
|
||||
const width = window.innerWidth;
|
||||
if (direction === 'vertical') {
|
||||
@@ -34,11 +38,13 @@
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
calculateRows();
|
||||
});
|
||||
// onMount(() => {
|
||||
// calculateRows();
|
||||
// });
|
||||
</script>
|
||||
|
||||
<svelte:window on:resize={(e) => (cols = getCardDimensions(e.currentTarget.innerWidth).columns)} />
|
||||
|
||||
<Container
|
||||
direction="grid"
|
||||
gridCols={cols}
|
||||
@@ -46,15 +52,16 @@
|
||||
'grid gap-x-8 gap-y-8',
|
||||
{
|
||||
'grid-cols-1 md:grid-cols-1 xl:grid-cols-2 2xl:grid-cols-3 3xl:grid-cols-4':
|
||||
direction === 'horizontal',
|
||||
'grid-cols-4 md:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6': direction === 'vertical'
|
||||
direction === 'horizontal'
|
||||
// 'grid-cols-4 md:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6': direction === 'vertical'
|
||||
},
|
||||
|
||||
$$restProps.class
|
||||
)}
|
||||
style={`grid-template-columns: repeat(${cols}, minmax(0, 1fr));`}
|
||||
on:mount
|
||||
>
|
||||
<slot />
|
||||
</Container>
|
||||
|
||||
<svelte:window on:resize={calculateRows} />
|
||||
<!--<svelte:window on:resize={calculateRows} />-->
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
<div
|
||||
class={classNames(
|
||||
'transition-opacity duration-300',
|
||||
'transition-opacity duration-300 overflow-hidden',
|
||||
{
|
||||
'opacity-0': !loaded,
|
||||
'opacity-100': loaded
|
||||
|
||||
@@ -1,15 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { settings } from '../stores/settings.store';
|
||||
import { jellyfinItemsStore } from '../stores/data.store';
|
||||
import CarouselPlaceholderItems from '../components/Carousel/CarouselPlaceholderItems.svelte';
|
||||
import Container from '../../Container.svelte';
|
||||
import { jellyfinApi } from '../apis/jellyfin/jellyfin-api';
|
||||
import CardGrid from '../components/CardGrid.svelte';
|
||||
import JellyfinCard from '../components/Card/JellyfinCard.svelte';
|
||||
import { scrollIntoView } from '../selectable';
|
||||
import DetachedPage from '../components/DetachedPage/DetachedPage.svelte';
|
||||
import Carousel from '../components/Carousel/Carousel.svelte';
|
||||
import { sonarrApi } from '../apis/sonarr/sonarr-api';
|
||||
import { radarrApi } from '../apis/radarr/radarr-api';
|
||||
import Card from '../components/Card/Card.svelte';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
|
||||
const libraryItemsP = jellyfinApi.getLibraryItems();
|
||||
let sonarrDownloads: Promise<ComponentProps<Card>[]> = sonarrApi.getDownloads().then((items) =>
|
||||
items
|
||||
.filter(
|
||||
(value, index, self) => index === self.findIndex((t) => t.seriesId === value.seriesId)
|
||||
)
|
||||
.map((i) => ({
|
||||
backdropUrl: i.series.images?.find((i) => i.coverType === 'poster')?.remoteUrl || '',
|
||||
group: true
|
||||
}))
|
||||
);
|
||||
let radarrDownloads: Promise<ComponentProps<Card>[]> = radarrApi.getDownloads().then((items) =>
|
||||
items.map((i) => ({
|
||||
backdropUrl: i.movie.images?.find((i) => i.coverType === 'poster')?.remoteUrl || ''
|
||||
}))
|
||||
);
|
||||
|
||||
settings.update((prev) => ({
|
||||
...prev,
|
||||
@@ -23,22 +41,38 @@
|
||||
}));
|
||||
</script>
|
||||
|
||||
<DetachedPage class="px-32 py-16">
|
||||
<div class="mb-6">
|
||||
<div class="header2">Library</div>
|
||||
<DetachedPage class="py-16 space-y-8">
|
||||
{#await Promise.all([sonarrDownloads, radarrDownloads]) then [sonarrDownloads, radarrDownloads]}
|
||||
{#if sonarrDownloads?.length || radarrDownloads?.length}
|
||||
<Carousel scrollClass="px-32" on:enter={scrollIntoView({ vertical: 128 })}>
|
||||
<span slot="header">Downloading</span>
|
||||
{#each sonarrDownloads as props}
|
||||
<Card on:enter={scrollIntoView({ horizontal: 128 })} size="lg" {...props} />
|
||||
{/each}
|
||||
|
||||
{#each radarrDownloads as props}
|
||||
<Card on:enter={scrollIntoView({ horizontal: 128 })} size="lg" {...props} />
|
||||
{/each}
|
||||
</Carousel>
|
||||
{/if}
|
||||
{/await}
|
||||
<div class="px-32">
|
||||
<div class="mb-6">
|
||||
<div class="header2">Library</div>
|
||||
</div>
|
||||
<CardGrid>
|
||||
{#await libraryItemsP}
|
||||
<CarouselPlaceholderItems />
|
||||
{:then items}
|
||||
{#each items as item}
|
||||
<JellyfinCard
|
||||
{item}
|
||||
on:enter={scrollIntoView({ all: 64 })}
|
||||
size="dynamic"
|
||||
navigateWithType
|
||||
/>
|
||||
{/each}
|
||||
{/await}
|
||||
</CardGrid>
|
||||
</div>
|
||||
<CardGrid>
|
||||
{#await libraryItemsP}
|
||||
<CarouselPlaceholderItems />
|
||||
{:then items}
|
||||
{#each items as item}
|
||||
<JellyfinCard
|
||||
{item}
|
||||
on:enter={scrollIntoView({ all: 64 })}
|
||||
size="dynamic"
|
||||
navigateWithType
|
||||
/>
|
||||
{/each}
|
||||
{/await}
|
||||
</CardGrid>
|
||||
</DetachedPage>
|
||||
|
||||
@@ -83,7 +83,7 @@ export function createJellyfinItemStore(tmdbId: number | Promise<number>) {
|
||||
};
|
||||
}
|
||||
|
||||
export const sonarrSeriesStore = _createDataFetchStore(sonarrApi.getSonarrDownloads);
|
||||
export const sonarrSeriesStore = _createDataFetchStore(sonarrApi.getDownloads);
|
||||
export const radarrMoviesStore = _createDataFetchStore(radarrApi.getRadarrMovies);
|
||||
|
||||
export function createRadarrMovieStore(tmdbId: number) {
|
||||
@@ -131,8 +131,8 @@ export function createSonarrSeriesStore(name: Promise<string> | string) {
|
||||
};
|
||||
}
|
||||
|
||||
export const sonarrDownloadsStore = _createDataFetchStore(sonarrApi.getSonarrDownloads);
|
||||
export const radarrDownloadsStore = _createDataFetchStore(radarrApi.getRadarrDownloads);
|
||||
export const sonarrDownloadsStore = _createDataFetchStore(sonarrApi.getDownloads);
|
||||
export const radarrDownloadsStore = _createDataFetchStore(radarrApi.getDownloads);
|
||||
export const servarrDownloadsStore = (() => {
|
||||
const store = derived([sonarrDownloadsStore, radarrDownloadsStore], ([sonarr, radarr]) => {
|
||||
return {
|
||||
|
||||
@@ -133,3 +133,22 @@ export function subscribeUntil<T>(store: Readable<T>, fn: (value: T) => boolean)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function getCardDimensions(viewportWidth: number) {
|
||||
const minWidth = 240;
|
||||
|
||||
const margin = 128;
|
||||
const gap = 32;
|
||||
|
||||
const cols = Math.floor((gap - 2 * margin + viewportWidth) / (minWidth + gap));
|
||||
const scale = -(gap * (cols - 1) + 2 * margin - viewportWidth) / (cols * minWidth);
|
||||
|
||||
const newWidth = minWidth * scale;
|
||||
const newHeight = (3 / 2) * newWidth;
|
||||
|
||||
return {
|
||||
width: newWidth,
|
||||
height: newHeight,
|
||||
columns: cols
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user