diff options
| -rw-r--r-- | src/routes/+page.svelte | 282 |
1 files changed, 220 insertions, 62 deletions
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 268e9713..ad3971d8 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,5 +1,6 @@ <script lang="ts"> import type { Component } from "svelte"; +import { browser } from "$app/environment"; import Spacer from "$lib/Layout/Spacer.svelte"; import { onDestroy, onMount } from "svelte"; import userIdentity from "$stores/identity.js"; @@ -41,6 +42,86 @@ let IndexColumnComponent: IndexColumnSvelteComponent | null = $state(null); let MangaListTemplateComponent: MangaListTemplateSvelteComponent | null = $state(null); let authenticatedHomeSurfaceImport: Promise<void> | null = null; +let balancedListFlowElement: HTMLDivElement | undefined = $state(); +let balancedListResizeObserver: ResizeObserver | undefined; +let balancedListMeasurementFrame = 0; +const balancedListPanelElements = new Map<string, HTMLElement>(); +let balancedListPanelAssignments = $state<Record<string, "left" | "right">>({}); + +const resetBalancedListLayout = () => { + balancedListPanelAssignments = {}; +}; + +const updateBalancedListLayout = () => { + if (!balancedListFlowElement) return; + + const balancedListFlowStyles = getComputedStyle(balancedListFlowElement); + const balancedListRowGap = parseFloat( + balancedListFlowStyles.getPropertyValue("row-gap"), + ); + const balancedListColumnCount = balancedListFlowStyles + .getPropertyValue("grid-template-columns") + .split(" ") + .filter(Boolean).length; + + if (balancedListColumnCount <= 1 || !Number.isFinite(balancedListRowGap)) { + balancedListPanelAssignments = Object.fromEntries( + Array.from(balancedListPanelElements.keys()).map((key) => [key, "left"]), + ); + + return; + } + + const balancedListKeys = ["upcoming", "due", "manga"].filter((key) => + balancedListPanelElements.has(key), + ); + const balancedListHeights = { left: 0, right: 0 }; + const nextBalancedListAssignments: Record<string, "left" | "right"> = {}; + + balancedListKeys.forEach((key) => { + const balancedListPanel = balancedListPanelElements.get(key); + if (!balancedListPanel) return; + + const balancedListColumn = + balancedListHeights.left <= balancedListHeights.right ? "left" : "right"; + const balancedListPanelHeight = + balancedListPanel.getBoundingClientRect().height; + + nextBalancedListAssignments[key] = balancedListColumn; + balancedListHeights[balancedListColumn] += + balancedListPanelHeight + + (balancedListHeights[balancedListColumn] > 0 ? balancedListRowGap : 0); + }); + + balancedListPanelAssignments = nextBalancedListAssignments; +}; + +const queueBalancedListLayout = () => { + if (!browser) return; + + cancelAnimationFrame(balancedListMeasurementFrame); + + balancedListMeasurementFrame = requestAnimationFrame(() => { + updateBalancedListLayout(); + }); +}; + +const observeBalancedListPanel = (panel: HTMLElement, key: string) => { + balancedListPanelElements.set(key, panel); + balancedListResizeObserver?.observe(panel); + queueBalancedListLayout(); + + return { + destroy: () => { + balancedListPanelElements.delete(key); + balancedListResizeObserver?.unobserve(panel); + queueBalancedListLayout(); + }, + }; +}; + +const isBalancedListPanelInColumn = (key: string, column: "left" | "right") => + (balancedListPanelAssignments[key] ?? "left") === column; const loadAuthenticatedHomeSurface = () => { if (data.user === undefined) return null; @@ -68,10 +149,30 @@ onMount(async () => { await hydrateStateBin(); $stateBin.upcomingAnimeListOpen ??= true; $stateBin.dueMangaListOpen ??= true; + + balancedListResizeObserver = new ResizeObserver(() => { + queueBalancedListLayout(); + }); + + balancedListPanelElements.forEach((panel) => { + balancedListResizeObserver?.observe(panel); + }); + + window.addEventListener("resize", queueBalancedListLayout); void loadAuthenticatedHomeSurface(); + + queueBalancedListLayout(); }); -onDestroy(() => removeHeightObserver?.()); +onDestroy(() => { + removeHeightObserver?.(); + balancedListResizeObserver?.disconnect(); + if (browser) { + cancelAnimationFrame(balancedListMeasurementFrame); + window.removeEventListener("resize", queueBalancedListLayout); + } + resetBalancedListLayout(); +}); </script> <HeadTitle /> @@ -87,85 +188,140 @@ onDestroy(() => removeHeightObserver?.()); <Landing /> {:else} + {@const balancedListColumnCount = + [!$settings.disableUpcomingAnime, !$settings.disableAnime, !$settings.disableManga] + .map(Number) + .reduce((a, b) => a + b) > 1 + ? 2 + : 1} <div - class="grid-container" + bind:this={balancedListFlowElement} + class="balanced-list-flow" style={` - grid-template-columns: ${ - [!$settings.disableUpcomingAnime, !$settings.disableAnime, !$settings.disableManga] - .map(Number) - .reduce((a, b) => a + b) > 1 - ? '1fr 1fr' - : '1fr' - } + --balanced-list-columns: ${balancedListColumnCount} `} > - <div class="left-column"> - {#if !$settings.disableUpcomingAnime} - <details bind:open={$stateBin.upcomingAnimeListOpen} class="list list-upcoming"> - {#if $userIdentity.id !== -2} - {#if UpcomingAnimeListComponent} - <UpcomingAnimeListComponent user={data.user} /> + <div class="balanced-list-column"> + {#if !$settings.disableUpcomingAnime && isBalancedListPanelInColumn("upcoming", "left")} + <div class="balanced-list-panel" use:observeBalancedListPanel={"upcoming"}> + <details bind:open={$stateBin.upcomingAnimeListOpen} class="list list-upcoming"> + {#if $userIdentity.id !== -2} + {#if UpcomingAnimeListComponent} + <UpcomingAnimeListComponent user={data.user} /> + {:else} + <ListTitle title={$locale().lists.upcoming.episodes} /> + + <Skeleton card={false} count={5} height="0.9rem" list /> + {/if} {:else} <ListTitle title={$locale().lists.upcoming.episodes} /> <Skeleton card={false} count={5} height="0.9rem" list /> {/if} + </details> + </div> + {/if} + + {#if !$settings.disableAnime && isBalancedListPanelInColumn("due", "left")} + <div class="balanced-list-panel" use:observeBalancedListPanel={"due"}> + {#if IndexColumnComponent} + <IndexColumnComponent user={data.user} userIdentity={$userIdentity} /> {:else} - <ListTitle title={$locale().lists.upcoming.episodes} /> + <details bind:open={$stateBin.dueAnimeListOpen} class="list list-due"> + <ListTitle title={$locale().lists.due.episodes} /> - <Skeleton card={false} count={5} height="0.9rem" list /> + <Skeleton card={false} count={5} height="0.9rem" list /> + </details> {/if} - </details> + </div> {/if} - {#if !$settings.disableAnime && !$settings.disableManga} - {#if IndexColumnComponent} - <IndexColumnComponent user={data.user} userIdentity={$userIdentity} /> - {:else} - <details bind:open={$stateBin.dueAnimeListOpen} class="list list-due"> - <ListTitle title={$locale().lists.due.episodes} /> + {#if !$settings.disableManga && isBalancedListPanelInColumn("manga", "left")} + <div class="balanced-list-panel" use:observeBalancedListPanel={"manga"}> + <details bind:open={$stateBin.dueMangaListOpen} class="list list-manga"> + {#if $userIdentity.id !== -2} + {#if MangaListTemplateComponent} + <MangaListTemplateComponent + user={data.user} + displayUnresolved={$settings.displayUnresolved} + due={true} + /> + {:else} + <ListTitle title={$locale().lists.due.mangaAndLightNovels} /> - <Skeleton card={false} count={5} height="0.9rem" list /> + <Skeleton card={false} count={5} height="0.9rem" list /> + {/if} + {:else} + <ListTitle title={$locale().lists.due.mangaAndLightNovels} /> + + <Skeleton card={false} count={5} height="0.9rem" list /> + {/if} </details> - {/if} + </div> {/if} </div> - <div class="right-column"> - {#if !$settings.disableAnime && $settings.disableManga} - {#if IndexColumnComponent} - <IndexColumnComponent user={data.user} userIdentity={$userIdentity} /> - {:else} - <details bind:open={$stateBin.dueAnimeListOpen} class="list list-due"> - <ListTitle title={$locale().lists.due.episodes} /> + {#if balancedListColumnCount > 1} + <div class="balanced-list-column"> + {#if !$settings.disableUpcomingAnime && isBalancedListPanelInColumn("upcoming", "right")} + <div class="balanced-list-panel" use:observeBalancedListPanel={"upcoming"}> + <details bind:open={$stateBin.upcomingAnimeListOpen} class="list list-upcoming"> + {#if $userIdentity.id !== -2} + {#if UpcomingAnimeListComponent} + <UpcomingAnimeListComponent user={data.user} /> + {:else} + <ListTitle title={$locale().lists.upcoming.episodes} /> - <Skeleton card={false} count={5} height="0.9rem" list /> - </details> + <Skeleton card={false} count={5} height="0.9rem" list /> + {/if} + {:else} + <ListTitle title={$locale().lists.upcoming.episodes} /> + + <Skeleton card={false} count={5} height="0.9rem" list /> + {/if} + </details> + </div> {/if} - {/if} - {#if !$settings.disableManga} - <details bind:open={$stateBin.dueMangaListOpen} class="list list-manga"> - {#if $userIdentity.id !== -2} - {#if MangaListTemplateComponent} - <MangaListTemplateComponent - user={data.user} - displayUnresolved={$settings.displayUnresolved} - due={true} - /> + {#if !$settings.disableAnime && isBalancedListPanelInColumn("due", "right")} + <div class="balanced-list-panel" use:observeBalancedListPanel={"due"}> + {#if IndexColumnComponent} + <IndexColumnComponent user={data.user} userIdentity={$userIdentity} /> {:else} - <ListTitle title={$locale().lists.due.mangaAndLightNovels} /> + <details bind:open={$stateBin.dueAnimeListOpen} class="list list-due"> + <ListTitle title={$locale().lists.due.episodes} /> - <Skeleton card={false} count={5} height="0.9rem" list /> + <Skeleton card={false} count={5} height="0.9rem" list /> + </details> {/if} - {:else} - <ListTitle title={$locale().lists.due.mangaAndLightNovels} /> + </div> + {/if} - <Skeleton card={false} count={5} height="0.9rem" list /> - {/if} - </details> - {/if} - </div> + {#if !$settings.disableManga && isBalancedListPanelInColumn("manga", "right")} + <div class="balanced-list-panel" use:observeBalancedListPanel={"manga"}> + <details bind:open={$stateBin.dueMangaListOpen} class="list list-manga"> + {#if $userIdentity.id !== -2} + {#if MangaListTemplateComponent} + <MangaListTemplateComponent + user={data.user} + displayUnresolved={$settings.displayUnresolved} + due={true} + /> + {:else} + <ListTitle title={$locale().lists.due.mangaAndLightNovels} /> + + <Skeleton card={false} count={5} height="0.9rem" list /> + {/if} + {:else} + <ListTitle title={$locale().lists.due.mangaAndLightNovels} /> + + <Skeleton card={false} count={5} height="0.9rem" list /> + {/if} + </details> + </div> + {/if} + </div> + {/if} {#if $settings.disableUpcomingAnime && $settings.disableAnime && $settings.disableManga} <video src="https://video.twimg.com/tweet_video/Do_eDPnX0AAKV9f.mp4" autoplay loop> @@ -176,30 +332,32 @@ onDestroy(() => removeHeightObserver?.()); {/if} <style> - .grid-container { + .balanced-list-flow { display: grid; + grid-template-columns: repeat(var(--balanced-list-columns), minmax(0, 1fr)); gap: 1rem; + align-items: start; } - .left-column { + .balanced-list-column { display: grid; gap: 1rem; align-content: start; + min-width: 0; } - .right-column { - align-self: start; + .balanced-list-panel { + min-width: 0; } .list { overflow-y: auto; - break-inside: avoid; - page-break-inside: avoid; + margin: 0; } @media (max-width: 800px) { - .grid-container { - grid-template-columns: 1fr !important; + .balanced-list-flow { + grid-template-columns: 1fr; } } </style> |