Android Compat Layer

This commit is contained in:
RFait 2025-10-05 22:07:45 -07:00
parent a18a3b3602
commit e3ae1019b6
3 changed files with 253 additions and 84 deletions

View file

@ -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>

View file

@ -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}

View file

@ -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>