mirror of
https://github.com/ThaUnknown/miru.git
synced 2026-04-14 04:00:20 +00:00
Android Compat Layer
This commit is contained in:
parent
a18a3b3602
commit
e3ae1019b6
3 changed files with 253 additions and 84 deletions
|
|
@ -3,6 +3,7 @@
|
|||
import { addMonths, endOfMonth, endOfWeek, format, isSameMonth, isToday, startOfMonth, startOfWeek, subMonths } from 'date-fns'
|
||||
import { persisted } from 'svelte-persisted-store'
|
||||
import Cross2 from 'svelte-radix/Cross2.svelte'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
import type { Schedule, ScheduleMedia } from '$lib/modules/anilist/queries'
|
||||
import type { ResultOf } from 'gql.tada'
|
||||
|
|
@ -18,6 +19,7 @@
|
|||
import { authAggregator, list } from '$lib/modules/auth'
|
||||
import { dragScroll } from '$lib/modules/navigate'
|
||||
import { cn, breakpoints } from '$lib/utils'
|
||||
import { SUPPORTS } from '$lib/modules/settings'
|
||||
import ThreeDayView from './ThreeDayView.svelte'
|
||||
import DayDetailPane from './DayDetailPane.svelte'
|
||||
|
||||
|
|
@ -149,9 +151,28 @@
|
|||
highlightUntil = Date.now() + 3000
|
||||
highlightTimer = setTimeout(() => { highlightUntil = 0 }, 3000)
|
||||
}
|
||||
|
||||
// Android portrait detection
|
||||
let androidPortrait = false
|
||||
let androidLandscape = false
|
||||
onMount(() => {
|
||||
const mq = window.matchMedia('(orientation: portrait)')
|
||||
const update = () => {
|
||||
androidPortrait = SUPPORTS.isAndroid && mq.matches
|
||||
androidLandscape = SUPPORTS.isAndroid && !mq.matches
|
||||
}
|
||||
update()
|
||||
mq.addEventListener('change', update)
|
||||
return () => {
|
||||
mq.removeEventListener('change', update)
|
||||
}
|
||||
})
|
||||
|
||||
// Today state for button styling
|
||||
$: isOnToday = +midnight(selectedDate) === +midnight(new Date())
|
||||
</script>
|
||||
|
||||
<div class='w-full h-full overflow-y-auto p-2 md:p-6 min-w-0' use:dragScroll style='scrollbar-gutter: stable both-edges;'>
|
||||
<div class='w-full h-full overflow-auto p-2 md:p-6 min-w-0' use:dragScroll style='scrollbar-gutter: stable both-edges;'>
|
||||
<div class='space-y-2 mb-4 w-full max-w-none px-2 md:px-4 mx-auto'>
|
||||
<div class='flex items-center justify-between'>
|
||||
<div>
|
||||
|
|
@ -161,47 +182,90 @@
|
|||
<!-- View mode buttons moved to the main title bar below -->
|
||||
</div>
|
||||
|
||||
<!-- Title bar:-->
|
||||
<div class='grid items-center w-full grid-cols-[1fr_auto_1fr]'>
|
||||
<!-- My list -->
|
||||
<div class='flex items-center gap-1 text-muted-foreground justify-self-start'>
|
||||
<Switch bind:checked={$onList} id='schedule-on-list' hideState={true} />
|
||||
<Label for='schedule-on-list'>My list</Label>
|
||||
</div>
|
||||
|
||||
<!-- Day and Month container-->
|
||||
<div class='justify-self-center flex items-center gap-1.5'>
|
||||
<!-- Day prev-->
|
||||
<Button size='icon' on:click={prevDay} variant='outline' class='bg-transparent animated-icon h-7 w-7'>
|
||||
<ChevronLeft class='h-5 w-5' />
|
||||
</Button>
|
||||
<!-- Month prev -->
|
||||
<Button size='icon' on:click={prevMonth} variant='outline' class='bg-transparent animated-icon h-9 w-9'>
|
||||
<ChevronLeft class='h-6 w-6' />
|
||||
</Button>
|
||||
<!-- Month and Day-->
|
||||
<div class='text-xl font-semibold px-1'>
|
||||
{format(selectedDate, 'MMMM, EEEE do')}{selectedDate.getFullYear() !== new Date().getFullYear() ? ` ${format(selectedDate, 'yyyy')}` : ''}
|
||||
<!-- Title bar (Android portrait specific vs default) -->
|
||||
{#if androidPortrait}
|
||||
<!-- Android portrait: top row with month/day and arrows; second row with My list, Today, and View toggle -->
|
||||
<div class='w-full flex flex-col gap-2'>
|
||||
<!-- Top: Month/Day + arrows (stable positions) -->
|
||||
<div class='w-full grid grid-cols-[auto_1fr_auto] items-center gap-1.5'>
|
||||
<div class='flex items-center gap-1.5 justify-self-start'>
|
||||
<Button size='icon' on:click={prevDay} variant='outline' class='bg-transparent animated-icon h-7 w-7'>
|
||||
<ChevronLeft class='h-5 w-5' />
|
||||
</Button>
|
||||
<Button size='icon' on:click={prevMonth} variant='outline' class='bg-transparent animated-icon h-7 w-7'>
|
||||
<ChevronLeft class='h-5 w-5' />
|
||||
</Button>
|
||||
</div>
|
||||
<div class='text-base font-semibold px-1 text-center justify-self-center min-w-[18ch] max-w-[60vw] truncate'>
|
||||
{format(selectedDate, 'MMMM, EEEE do')}{selectedDate.getFullYear() !== new Date().getFullYear() ? ` ${format(selectedDate, 'yyyy')}` : ''}
|
||||
</div>
|
||||
<div class='flex items-center gap-1.5 justify-self-end'>
|
||||
<Button size='icon' on:click={nextMonth} variant='outline' class='bg-transparent animated-icon h-7 w-7'>
|
||||
<ChevronRight class='h-5 w-5' />
|
||||
</Button>
|
||||
<Button size='icon' on:click={nextDay} variant='outline' class='bg-transparent animated-icon h-7 w-7'>
|
||||
<ChevronRight class='h-5 w-5' />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bottom: My list, Today, View toggle -->
|
||||
<div class='w-full grid grid-cols-3 items-center gap-2'>
|
||||
<div class='flex items-center gap-2 text-muted-foreground justify-self-start'>
|
||||
<Switch bind:checked={$onList} id='schedule-on-list' hideState={true} />
|
||||
<Label for='schedule-on-list'>My list</Label>
|
||||
</div>
|
||||
<div class='flex items-center justify-center'>
|
||||
<Button size='sm' variant='outline' class={isOnToday ? 'border-white/70 bg-white/10 text-white' : 'border-white/30 text-white/90 hover:bg-white/5'} on:click={goToday}>Today</Button>
|
||||
</div>
|
||||
<div class='flex items-center gap-2 justify-self-end'>
|
||||
<Button size='sm' variant='outline' class='w-[96px] justify-center' on:click={cycleViewMode}>
|
||||
{$viewMode === 'three-day' ? 'Three-day' : 'Month'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Month next -->
|
||||
<Button size='icon' on:click={nextMonth} variant='outline' class='bg-transparent animated-icon h-9 w-9'>
|
||||
<ChevronRight class='h-6 w-6' />
|
||||
</Button>
|
||||
<!-- Day next -->
|
||||
<Button size='icon' on:click={nextDay} variant='outline' class='bg-transparent animated-icon h-7 w-7'>
|
||||
<ChevronRight class='h-5 w-5' />
|
||||
</Button>
|
||||
<!-- Today button -->
|
||||
<Button class='ml-2' size='sm' variant='outline' on:click={goToday}>Today</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Default (non-Android or landscape): left My list, center Month/Day + arrows + Today, right View toggle -->
|
||||
<div class='grid items-center w-full grid-cols-[1fr_auto_1fr]'>
|
||||
<!-- My list -->
|
||||
<div class='flex items-center gap-1 text-muted-foreground justify-self-start'>
|
||||
<Switch bind:checked={$onList} id='schedule-on-list' hideState={true} />
|
||||
<Label for='schedule-on-list'>My list</Label>
|
||||
</div>
|
||||
|
||||
<!-- View mode toggle -->
|
||||
<div class='flex items-center justify-self-end'>
|
||||
<Button size='sm' variant='outline' class='w-[96px] justify-center' on:click={cycleViewMode}>
|
||||
{$viewMode === 'three-day' ? 'Three-day' : 'Month'}
|
||||
</Button>
|
||||
<!-- Day and Month container (stable positions) -->
|
||||
<div class='justify-self-center grid grid-cols-[auto_1fr_auto] items-center gap-1.5'>
|
||||
<div class='flex items-center gap-1.5 justify-self-start'>
|
||||
<Button size='icon' on:click={prevDay} variant='outline' class='bg-transparent animated-icon h-7 w-7'>
|
||||
<ChevronLeft class='h-5 w-5' />
|
||||
</Button>
|
||||
<Button size='icon' on:click={prevMonth} variant='outline' class='bg-transparent animated-icon h-9 w-9'>
|
||||
<ChevronLeft class='h-6 w-6' />
|
||||
</Button>
|
||||
</div>
|
||||
<div class='text-xl font-semibold px-1 text-center justify-self-center min-w-[24ch]'>
|
||||
{format(selectedDate, 'MMMM, EEEE do')}{selectedDate.getFullYear() !== new Date().getFullYear() ? ` ${format(selectedDate, 'yyyy')}` : ''}
|
||||
</div>
|
||||
<div class='flex items-center gap-1.5 justify-self-end'>
|
||||
<Button size='icon' on:click={nextMonth} variant='outline' class='bg-transparent animated-icon h-9 w-9'>
|
||||
<ChevronRight class='h-6 w-6' />
|
||||
</Button>
|
||||
<Button size='icon' on:click={nextDay} variant='outline' class='bg-transparent animated-icon h-7 w-7'>
|
||||
<ChevronRight class='h-5 w-5' />
|
||||
</Button>
|
||||
</div>
|
||||
<!-- Today button remains on the right grouping in default layout -->
|
||||
<Button class={'ml-2 col-span-3 justify-self-center hidden md:inline-flex ' + (isOnToday ? 'border-white/70 bg-white/10 text-white' : 'border-white/30 text-white/90 hover:bg-white/5')} size='sm' variant='outline' on:click={goToday}>Today</Button>
|
||||
</div>
|
||||
|
||||
<!-- View mode toggle -->
|
||||
<div class='flex items-center justify-self-end'>
|
||||
<Button size='sm' variant='outline' class='w-[96px] justify-center' on:click={cycleViewMode}>
|
||||
{$viewMode === 'three-day' ? 'Three-day' : 'Month'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if $query.fetching}
|
||||
|
|
@ -222,19 +286,38 @@
|
|||
</div>
|
||||
{:else}
|
||||
{#if $viewMode === 'three-day'}
|
||||
<div class='grid w-full gap-4 mx-auto' style='grid-template-columns: 1fr 1fr;'>
|
||||
<ThreeDayView
|
||||
{prevDate}
|
||||
currentDate={selectedDate}
|
||||
{nextDate}
|
||||
{prevEpisodes}
|
||||
{currentEpisodes}
|
||||
{nextEpisodes}
|
||||
onSelectDate={(d) => selectedDate = d}
|
||||
onSelectEpisode={highlightEpisode}
|
||||
/>
|
||||
<DayDetailPane episodes={currentEpisodes} {selectedEpisode} {highlightUntil} onSelectEpisode={(ep) => selectedEpisode = ep} />
|
||||
</div>
|
||||
{#if androidPortrait}
|
||||
<!-- Android portrait: stack thumbnails (detail pane) on top, list (three-day) under -->
|
||||
<div class='flex flex-col w-full gap-4 mx-auto'>
|
||||
<DayDetailPane episodes={currentEpisodes} {selectedEpisode} {highlightUntil} onSelectEpisode={(ep) => selectedEpisode = ep} androidPortrait={true} />
|
||||
<ThreeDayView
|
||||
{prevDate}
|
||||
currentDate={selectedDate}
|
||||
{nextDate}
|
||||
{prevEpisodes}
|
||||
{currentEpisodes}
|
||||
{nextEpisodes}
|
||||
onSelectDate={(d) => selectedDate = d}
|
||||
onSelectEpisode={highlightEpisode}
|
||||
androidPortrait={true}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div class='grid w-full gap-4 mx-auto min-w-[1000px]' style='grid-template-columns: 1fr 1fr;'>
|
||||
<ThreeDayView
|
||||
{prevDate}
|
||||
currentDate={selectedDate}
|
||||
{nextDate}
|
||||
{prevEpisodes}
|
||||
{currentEpisodes}
|
||||
{nextEpisodes}
|
||||
onSelectDate={(d) => selectedDate = d}
|
||||
onSelectEpisode={highlightEpisode}
|
||||
androidLandscape={androidLandscape}
|
||||
/>
|
||||
<DayDetailPane episodes={currentEpisodes} {selectedEpisode} {highlightUntil} onSelectEpisode={(ep) => selectedEpisode = ep} androidLandscape={androidLandscape} />
|
||||
</div>
|
||||
{/if}
|
||||
{:else if $viewMode === 'month'}
|
||||
<div class='grid grid-cols-7 border rounded-lg [&>*:not(:nth-child(7n+1))]:border-l [&>*:nth-last-child(n+8)]:border-b [&>*:nth-child(-n+7)]:border-b w-full mx-auto'>
|
||||
<div class='text-center py-2'>Mon</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { format } from 'date-fns'
|
||||
import { Button as ButtonPrimitive } from 'bits-ui'
|
||||
import { onDestroy, tick } from 'svelte'
|
||||
import { onDestroy, onMount, tick } from 'svelte'
|
||||
import StatusDot from '$lib/components/StatusDot.svelte'
|
||||
import { list as listStatus } from '$lib/modules/auth'
|
||||
import { cn } from '$lib/utils'
|
||||
|
|
@ -10,12 +10,24 @@
|
|||
export let selectedEpisode: any | undefined
|
||||
export let onSelectEpisode: (ep: any) => void
|
||||
export let highlightUntil: number = 0
|
||||
export let androidPortrait: boolean = false
|
||||
export let androidLandscape: boolean = false
|
||||
let localHighlightUntil = 0
|
||||
let prevHighlightUntil = 0
|
||||
let localTimer: any
|
||||
let shortenedOnce = false
|
||||
let listEl: HTMLDivElement | null = null
|
||||
|
||||
// Layout breakpoint detection for 1600px
|
||||
let wide1600 = false
|
||||
onMount(() => {
|
||||
const mq = window.matchMedia('(min-width: 1600px)')
|
||||
const update = () => { wide1600 = mq.matches }
|
||||
update()
|
||||
mq.addEventListener('change', update)
|
||||
return () => mq.removeEventListener('change', update)
|
||||
})
|
||||
|
||||
function coverSrcset(ci?: any) {
|
||||
const list: string[] = []
|
||||
if (ci?.medium) list.push(`${ci.medium} 320w`)
|
||||
|
|
@ -63,7 +75,7 @@
|
|||
</script>
|
||||
|
||||
<div class="flex flex-col h-full rounded-lg overflow-hidden min-h-0">
|
||||
<div class="flex-1 overflow-y-auto p-3 grid grid-cols-2 gap-3" bind:this={listEl}
|
||||
<div class={cn('flex-1 overflow-y-auto p-3 grid grid-cols-2', androidLandscape ? 'gap-2' : 'gap-3')} bind:this={listEl}
|
||||
style="content-visibility:auto; contain-intrinsic-size: 640px 400px;"
|
||||
on:scroll|passive={onScrollPane}>
|
||||
{#if episodes.length === 0}
|
||||
|
|
@ -79,36 +91,58 @@
|
|||
)}
|
||||
on:click={() => onSelectEpisode(ep)}>
|
||||
{#if ep.bannerImage}
|
||||
<img src={ep.bannerImage} alt={ep.title?.userPreferred} class="w-full h-48 object-cover" loading="lazy" decoding="async" sizes={coverSizes} />
|
||||
<img src={ep.bannerImage} alt={ep.title?.userPreferred} class={cn('w-full object-cover', androidLandscape ? 'h-44' : 'h-48')} loading="lazy" decoding="async" sizes={coverSizes} />
|
||||
{:else if ep.coverImage?.extraLarge}
|
||||
<img src={ep.coverImage.extraLarge} alt={ep.title?.userPreferred} class="w-full h-48 object-cover" loading="lazy" decoding="async" srcset={coverSrcset(ep.coverImage)} sizes={coverSizes} />
|
||||
<img src={ep.coverImage.extraLarge} alt={ep.title?.userPreferred} class={cn('w-full object-cover', androidLandscape ? 'h-44' : 'h-48')} loading="lazy" decoding="async" srcset={coverSrcset(ep.coverImage)} sizes={coverSizes} />
|
||||
{:else if ep.coverImage?.large}
|
||||
<img src={ep.coverImage.large} alt={ep.title?.userPreferred} class="w-full h-48 object-cover" loading="lazy" decoding="async" srcset={coverSrcset(ep.coverImage)} sizes={coverSizes} />
|
||||
<img src={ep.coverImage.large} alt={ep.title?.userPreferred} class={cn('w-full object-cover', androidLandscape ? 'h-44' : 'h-48')} loading="lazy" decoding="async" srcset={coverSrcset(ep.coverImage)} sizes={coverSizes} />
|
||||
{:else if ep.coverImage?.medium}
|
||||
<img src={ep.coverImage.medium} alt={ep.title?.userPreferred} class="w-full h-48 object-cover" loading="lazy" decoding="async" srcset={coverSrcset(ep.coverImage)} sizes={coverSizes} />
|
||||
<img src={ep.coverImage.medium} alt={ep.title?.userPreferred} class={cn('w-full object-cover', androidLandscape ? 'h-44' : 'h-48')} loading="lazy" decoding="async" srcset={coverSrcset(ep.coverImage)} sizes={coverSizes} />
|
||||
{:else}
|
||||
<div class="w-full h-48 bg-muted" />
|
||||
{/if}
|
||||
<div class="p-3">
|
||||
<!-- Title row -->
|
||||
<div class="flex items-center gap-2">
|
||||
{#if status}
|
||||
<StatusDot variant={status} class="hidden xl:inline-flex" />
|
||||
{/if}
|
||||
<div class={cn('font-semibold text-ellipsis overflow-hidden whitespace-nowrap', +ep.airTime < Date.now() && 'line-through')} title={ep.title?.userPreferred}>{ep.title?.userPreferred}</div>
|
||||
<div class={cn('font-semibold text-ellipsis overflow-hidden whitespace-nowrap', androidLandscape ? 'text-sm' : '', +ep.airTime < Date.now() && 'line-through')} title={ep.title?.userPreferred}>{ep.title?.userPreferred}</div>
|
||||
</div>
|
||||
<div class="mt-2 flex items-center justify-between gap-3">
|
||||
<div class="text-xs text-muted-foreground flex flex-wrap items-center gap-2">
|
||||
{#if ep.format}<span>{ep.format}</span>{/if}
|
||||
{#if ep.duration}<span>• Length: {ep.duration}m</span>{/if}
|
||||
{#if ep.episodes}<span>• Episodes: {ep.episodes}</span>{/if}
|
||||
|
||||
{#if wide1600}
|
||||
<!-- Wide (>=1600px): keep existing inline meta + chips -->
|
||||
<div class="mt-2 flex items-center justify-between gap-3">
|
||||
<div class="text-xs text-muted-foreground flex flex-wrap items-center gap-2">
|
||||
{#if ep.format}<span>{ep.format}</span>{/if}
|
||||
{#if ep.duration}<span>• Length: {ep.duration}m</span>{/if}
|
||||
{#if ep.episodes}<span>• Episodes: {ep.episodes}</span>{/if}
|
||||
</div>
|
||||
<div class={cn('flex items-center gap-3', androidPortrait ? 'flex-wrap' : '')}>
|
||||
<div class={cn('rounded-md bg-white/80 text-black border font-semibold', androidPortrait ? 'px-1.5 py-0.5 text-[12px]' : androidLandscape ? 'px-2 py-0.5 text-sm' : 'px-2 py-1 text-base md:text-lg')}>Ep {ep.episode}</div>
|
||||
<div class={cn('rounded-md bg-white/80 text-black border font-semibold tabular-nums', androidPortrait ? 'px-1.5 py-0.5 text-[12px]' : androidLandscape ? 'px-2 py-0.5 text-sm' : 'px-2 py-1')}>
|
||||
{format(ep.airTime, 'h:mm')} <span class="text-xs align-top uppercase">{format(ep.airTime, 'a')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="px-2 py-1 rounded-md bg-white/80 text-black border text-base md:text-lg font-semibold whitespace-nowrap">Ep {ep.episode}</div>
|
||||
<div class="px-2 py-1 rounded-md bg-white/80 text-black border font-semibold tabular-nums whitespace-nowrap">
|
||||
{:else}
|
||||
<!-- Compact (<1600px): meta on top (single line), chips below side-by-side -->
|
||||
<div class="mt-2 text-xs text-muted-foreground whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
{#if ep.format}<span>{ep.format}</span>{/if}
|
||||
{#if ep.episodes}
|
||||
<span class="ml-2">• {ep.episodes}eps</span>
|
||||
{/if}
|
||||
{#if ep.duration}
|
||||
<span class="ml-2">• {ep.duration}m</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class={cn('mt-2 flex items-center gap-3', androidPortrait ? 'flex-wrap' : '')}>
|
||||
<div class={cn('rounded-md bg-white/80 text-black border font-semibold', androidPortrait ? 'px-1.5 py-0.5 text-[12px]' : androidLandscape ? 'px-2 py-0.5 text-sm' : 'px-2 py-1 text-base')}>Ep {ep.episode}</div>
|
||||
<div class={cn('rounded-md bg-white/80 text-black border font-semibold tabular-nums', androidPortrait ? 'px-1.5 py-0.5 text-[12px]' : androidLandscape ? 'px-2 py-0.5 text-sm' : 'px-2 py-1')}>
|
||||
{format(ep.airTime, 'h:mm')} <span class="text-xs align-top uppercase">{format(ep.airTime, 'a')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</ButtonPrimitive.Root>
|
||||
{/each}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { Button as ButtonPrimitive } from 'bits-ui'
|
||||
import { goto } from '$app/navigation'
|
||||
import { cn } from '$lib/utils'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
export let prevDate: Date
|
||||
export let currentDate: Date
|
||||
|
|
@ -14,6 +15,8 @@
|
|||
|
||||
export let onSelectDate: (date: Date) => void
|
||||
export let onSelectEpisode: (ep: any) => void
|
||||
export let androidPortrait: boolean = false
|
||||
export let androidLandscape: boolean = false
|
||||
|
||||
let lastClickId: string | null = null
|
||||
let lastClickAt = 0
|
||||
|
|
@ -40,11 +43,43 @@
|
|||
function keydownSelectPrev(e: KeyboardEvent) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); onSelectDate(prevDate) } }
|
||||
function keydownSelectCurrent(e: KeyboardEvent) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); onSelectDate(currentDate) } }
|
||||
function keydownSelectNext(e: KeyboardEvent) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); onSelectDate(nextDate) } }
|
||||
|
||||
// Android portrait: collapse current-day list to 6 items with More+ button
|
||||
let showAllCurrent = false
|
||||
$: visibleCurrentEpisodes = androidPortrait && !showAllCurrent ? currentEpisodes.slice(0, 6) : currentEpisodes
|
||||
|
||||
// Apply same behavior to previous and next day in Android portrait
|
||||
let showAllPrev = false
|
||||
let showAllNext = false
|
||||
$: visiblePrevEpisodes = androidPortrait && !showAllPrev ? prevEpisodes.slice(0, 6) : prevEpisodes
|
||||
$: visibleNextEpisodes = androidPortrait && !showAllNext ? nextEpisodes.slice(0, 6) : nextEpisodes
|
||||
|
||||
// Below 1400px: change 3-day list to 2-day (current + next). Hide Previous.
|
||||
let wide1400 = false
|
||||
onMount(() => {
|
||||
const mq = window.matchMedia('(min-width: 1400px)')
|
||||
const update = () => { wide1400 = mq.matches }
|
||||
update()
|
||||
mq.addEventListener('change', update)
|
||||
return () => mq.removeEventListener('change', update)
|
||||
})
|
||||
|
||||
// Reset expanded state when dates change
|
||||
let _prevKey = 0, _currentKey = 0, _nextKey = 0
|
||||
$: { const k = +prevDate; if (k !== _prevKey) { _prevKey = k; showAllPrev = false } }
|
||||
$: { const k = +currentDate; if (k !== _currentKey) { _currentKey = k; showAllCurrent = false } }
|
||||
$: { const k = +nextDate; if (k !== _nextKey) { _nextKey = k; showAllNext = false } }
|
||||
</script>
|
||||
|
||||
<div class="grid grid-cols-3 gap-3 h-full mt-2">
|
||||
<div class={cn(
|
||||
androidPortrait
|
||||
? 'flex flex-col gap-3 h-full mt-2'
|
||||
: (wide1400 ? (androidLandscape ? 'grid grid-cols-3 gap-2 h-full mt-2' : 'grid grid-cols-3 gap-3 h-full mt-2')
|
||||
: (androidLandscape ? 'grid grid-cols-2 gap-2 h-full mt-2' : 'grid grid-cols-2 gap-3 h-full mt-2'))
|
||||
)}>
|
||||
<!-- Previous Day -->
|
||||
<div class="flex flex-col border border-white/15 bg-muted/20 rounded-lg overflow-hidden min-h-150 min-w-[215px] mt-2 hover:bg-accent/50">
|
||||
{#if wide1400}
|
||||
<div class={cn('flex flex-col border border-white/15 bg-muted/20 rounded-lg overflow-hidden hover:bg-accent/50', androidPortrait ? 'min-h-0 mt-0' : 'min-h-150 min-w-[215px] mt-2')}>
|
||||
<button type="button" class="px-3 py-3 text-neutral-200 text-base font-semibold bg-muted/60 cursor-pointer select-none text-center w-full" on:click={selectPrevDate} aria-label="Previous day">
|
||||
{format(prevDate, 'EEE d')}
|
||||
</button>
|
||||
|
|
@ -60,22 +95,28 @@
|
|||
{#if prevEpisodes.length === 0}
|
||||
<div class="text-sm text-muted-foreground px-2 py-8 text-center">No episodes</div>
|
||||
{:else}
|
||||
{#each prevEpisodes as ep (ep.id + ':' + ep.episode)}
|
||||
{#each visiblePrevEpisodes as ep (ep.id + ':' + ep.episode)}
|
||||
<ButtonPrimitive.Root href={'/app/anime/' + ep.id} class={cn('flex items-center w-full text-neutral-200 group px-1 py-1 rounded-md')}
|
||||
on:click={(e) => handleEpisodeClick(e, ep)}>
|
||||
<div class="w-12 pl-0.4 tabular-nums text-xs text-neutral-400 group-hover:text-neutral-100">{format(ep.airTime, 'HH:mm')}</div>
|
||||
<div class={cn('flex-1 pl-1.5 font-medium text-sm overflow-hidden', +ep.airTime < Date.now() && 'line-through')} style="display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;hyphens:auto;word-break:break-word;">
|
||||
<div class={cn('w-12 pl-0.4 tabular-nums text-neutral-400 group-hover:text-neutral-100', androidPortrait ? 'text-[11px]' : androidLandscape ? 'text-[12px]' : 'text-xs')}>{format(ep.airTime, 'HH:mm')}</div>
|
||||
<div class={cn('flex-1 pl-1.5 font-medium overflow-hidden', androidLandscape ? 'text-xs' : 'text-sm', +ep.airTime < Date.now() && 'line-through')} style="display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;hyphens:auto;word-break:break-word;">
|
||||
{ep.title?.userPreferred}
|
||||
</div>
|
||||
<div class="ml-auto pr-0.3 text-md text-nowrap shrink-0">Ep {ep.episode}</div>
|
||||
<div class={cn('ml-auto pr-0.3 text-nowrap shrink-0', androidPortrait ? 'text-sm' : androidLandscape ? 'text-sm' : 'text-md')}>Ep {ep.episode}</div>
|
||||
</ButtonPrimitive.Root>
|
||||
{/each}
|
||||
{#if androidPortrait && prevEpisodes.length > 6 && !showAllPrev}
|
||||
<div class="flex justify-center py-1">
|
||||
<button type="button" class="px-3 py-1 rounded-md border text-sm opacity-90 hover:opacity-100"
|
||||
on:click={(e) => { e.preventDefault(); e.stopPropagation(); showAllPrev = true }}>More+</button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
<!-- Current Day (Selected) -->
|
||||
<div class="flex flex-col border-2 border-white/25 bg-muted/40 rounded-lg overflow-hidden min-h-0 min-w-[220px] scale-[1.0005]">
|
||||
<div class={cn('flex flex-col border-2 border-white/25 bg-muted/40 rounded-lg overflow-hidden scale-[1.0005]', androidPortrait ? 'min-h-0' : 'min-h-0 min-w-[220px]')}>
|
||||
<button type="button" class="px-3 py-2.5 text-base font-semibold text-neutral-100 bg-muted/90 cursor-pointer select-none text-center w-full" on:click={selectCurrentDate} aria-label="Current day">
|
||||
{format(currentDate, 'EEE d')}
|
||||
</button>
|
||||
|
|
@ -83,22 +124,27 @@
|
|||
{#if currentEpisodes.length === 0}
|
||||
<div class="text-sm text-muted-foreground px-2 py-8 text-center">No episodes</div>
|
||||
{:else}
|
||||
{#each currentEpisodes as ep (ep.id + ':' + ep.episode)}
|
||||
{#each visibleCurrentEpisodes as ep (ep.id + ':' + ep.episode)}
|
||||
<ButtonPrimitive.Root href={'/app/anime/' + ep.id} class={cn('flex items-center w-full group px-1 py-1 rounded-md')}
|
||||
on:click={(e) => handleEpisodeClick(e, ep)}>
|
||||
<div class="w-12 pl-0.4 tabular-nums text-xs md:text-sm text-neutral-400 group-hover:text-neutral-200">{format(ep.airTime, 'HH:mm')}</div>
|
||||
<div class={cn('flex-1 pl-1.5 font-medium text-sm overflow-hidden', +ep.airTime < Date.now() && 'line-through')} style="display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;hyphens:auto;word-break:break-word;">
|
||||
<div class={cn('w-12 pl-0.4 tabular-nums text-neutral-400 group-hover:text-neutral-200', androidPortrait ? 'text-[11px]' : androidLandscape ? 'text-[12px]' : 'text-xs md:text-sm')}>{format(ep.airTime, 'HH:mm')}</div>
|
||||
<div class={cn('flex-1 pl-1.5 font-medium overflow-hidden', androidLandscape ? 'text-xs' : 'text-sm', +ep.airTime < Date.now() && 'line-through')} style="display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;hyphens:auto;word-break:break-word;">
|
||||
{ep.title?.userPreferred}
|
||||
</div>
|
||||
<div class="ml-auto pr-0.3 md:text-base text-nowrap shrink-0">Ep {ep.episode}</div>
|
||||
<div class={cn('ml-auto pr-0.3 text-nowrap shrink-0', androidPortrait ? 'text-sm' : androidLandscape ? 'text-sm' : 'md:text-base')}>Ep {ep.episode}</div>
|
||||
</ButtonPrimitive.Root>
|
||||
{/each}
|
||||
{#if androidPortrait && currentEpisodes.length > 6 && !showAllCurrent}
|
||||
<div class="flex justify-center py-1">
|
||||
<button type="button" class="px-3 py-1 rounded-md border text-sm opacity-90 hover:opacity-100" on:click={() => showAllCurrent = true}>More+</button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Next Day -->
|
||||
<div class="flex flex-col border border-white/15 bg-muted/20 rounded-lg overflow-hidden min-h-0 min-w-[215px] mt-2 hover:bg-accent/25">
|
||||
<div class={cn('flex flex-col border border-white/15 bg-muted/20 rounded-lg overflow-hidden hover:bg-accent/25', androidPortrait ? 'min-h-0 mt-0' : 'min-h-0 min-w-[215px] mt-2')}>
|
||||
<button type="button" class="px-3 py-3 text-base text-neutral-200 font-semibold bg-muted/60 cursor-pointer select-none text-center w-full" on:click={selectNextDate} aria-label="Next day">
|
||||
{format(nextDate, 'EEE d')}
|
||||
</button>
|
||||
|
|
@ -113,16 +159,22 @@
|
|||
{#if nextEpisodes.length === 0}
|
||||
<div class="text-sm text-muted-foreground px-2 py-8 text-center">No episodes</div>
|
||||
{:else}
|
||||
{#each nextEpisodes as ep (ep.id + ':' + ep.episode)}
|
||||
{#each visibleNextEpisodes as ep (ep.id + ':' + ep.episode)}
|
||||
<ButtonPrimitive.Root href={'/app/anime/' + ep.id} class={cn('flex items-center text-neutral-200 w-full group px-1 py-1 rounded-md')}
|
||||
on:click={(e) => handleEpisodeClick(e, ep)}>
|
||||
<div class="w-12 pl-0.4 tabular-nums text-xs text-neutral-400 group-hover:text-neutral-200">{format(ep.airTime, 'HH:mm')}</div>
|
||||
<div class={cn('flex-1 pl-1.5 font-medium overflow-hidden', +ep.airTime < Date.now() && 'line-through')} style="display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;hyphens:auto;word-break:break-word;">
|
||||
<div class={cn('w-12 pl-0.4 tabular-nums text-neutral-400 group-hover:text-neutral-200', androidPortrait ? 'text-[11px]' : androidLandscape ? 'text-[12px]' : 'text-xs')}>{format(ep.airTime, 'HH:mm')}</div>
|
||||
<div class={cn('flex-1 pl-1.5 font-medium overflow-hidden', androidLandscape ? 'text-xs' : 'text-sm', +ep.airTime < Date.now() && 'line-through')} style="display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;hyphens:auto;word-break:break-word;">
|
||||
{ep.title?.userPreferred}
|
||||
</div>
|
||||
<div class="ml-auto pr-0.3 text-md text-nowrap shrink-0">Ep {ep.episode}</div>
|
||||
<div class={cn('ml-auto pr-0.3 text-nowrap shrink-0', androidPortrait ? 'text-sm' : androidLandscape ? 'text-sm' : 'text-md')}>Ep {ep.episode}</div>
|
||||
</ButtonPrimitive.Root>
|
||||
{/each}
|
||||
{#if androidPortrait && nextEpisodes.length > 6 && !showAllNext}
|
||||
<div class="flex justify-center py-1">
|
||||
<button type="button" class="px-3 py-1 rounded-md border text-sm opacity-90 hover:opacity-100"
|
||||
on:click={(e) => { e.preventDefault(); e.stopPropagation(); showAllNext = true }}>More+</button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue