fix(native): overhaul library screen, fix upcoming filters, improve SelectModal

Library screen:
- Replace broken bottom sheet with inline filter strip (horizontal scroll
  chips matching the web pattern) — type pills, status/genre/rating/year/
  content rating dropdown chips with SelectModal pickers
- Replace TabStack with custom layout so filter sort controls appear in
  the native header alongside the avatar
- Fix grid: 3-column layout with computed item widths, proper spacing
- Remove redundant search bar (Search tab already exists)
- Sync sort/filter state via Jotai atoms between layout and screen

Upcoming screen:
- Move filter chips into SectionList ListHeaderComponent so they scroll
  with content and don't overlap the native header
- Single horizontal scroll row with divider between type and status groups
- Status chips toggle on/off instead of redundant dual "All" buttons
- Tighten section header spacing

SelectModal:
- Add ScrollView for long option lists (genres, content ratings)
- Respect safe areas with dynamic max height from screen dimensions
- Add optional multiSelect mode (stays open, toggles checkmarks)
- Add optional clearLabel/onClear for a "Clear" button in the header
- Accept string | string[] for selection to support multi-select

Other:
- Remove Settings tab from tab bar (accessible via avatar dropdown)
- Simplify tab layout by removing unused update check query

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
jakeandClaude Opus 4.6 committed 2026-03-24 10:17:26 -04:00
1 parent 647784d8fc
commit 2cd1964ca8
8 files changed
+562 -191

No files matched your search

+45 -42
View File
@@ -3,7 +3,7 @@ import { IconCalendarEvent } from "@tabler/icons-react-native";
import { useInfiniteQuery } from "@tanstack/react-query";
import { Stack } from "expo-router";
import { useCallback, useMemo, useState } from "react";
import { Pressable, RefreshControl, SectionList, View } from "react-native";
import { Pressable, RefreshControl, ScrollView, SectionList, View } from "react-native";
import Animated, { FadeInDown } from "react-native-reanimated";
import { useCSSVariable, useResolveClassNames } from "uniwind";
@@ -92,6 +92,41 @@ export default function UpcomingScreen() {
}
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
const filterChips = (
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={{ paddingHorizontal: 16, gap: 6, paddingTop: 10, paddingBottom: 6 }}
>
<FilterChip
label={t`All`}
isSelected={mediaType === "all"}
onPress={() => setMediaType("all")}
/>
<FilterChip
label={t`Movies`}
isSelected={mediaType === "movie"}
onPress={() => setMediaType("movie")}
/>
<FilterChip
label={t`TV`}
isSelected={mediaType === "tv"}
onPress={() => setMediaType("tv")}
/>
<View className="bg-border/30 mx-1 w-px self-stretch" />
<FilterChip
label={t`Watching`}
isSelected={statusFilter === "watching"}
onPress={() => setStatusFilter(statusFilter === "watching" ? "all" : "watching")}
/>
<FilterChip
label={t`Watchlist`}
isSelected={statusFilter === "watchlist"}
onPress={() => setStatusFilter(statusFilter === "watchlist" ? "all" : "watchlist")}
/>
</ScrollView>
);
return (
<>
<Stack.Header
@@ -107,63 +142,31 @@ export default function UpcomingScreen() {
{t`Upcoming`}
</Stack.Screen.Title>
<Stack.Screen.BackButton displayMode="minimal" />
<View className="flex-row flex-wrap gap-2 px-4 pt-2 pb-1">
<View className="flex-row gap-1.5">
<FilterChip
label={t`All`}
isSelected={mediaType === "all"}
onPress={() => setMediaType("all")}
/>
<FilterChip
label={t`Movies`}
isSelected={mediaType === "movie"}
onPress={() => setMediaType("movie")}
/>
<FilterChip
label={t`TV Shows`}
isSelected={mediaType === "tv"}
onPress={() => setMediaType("tv")}
/>
</View>
<View className="flex-row gap-1.5">
<FilterChip
label={t`All`}
isSelected={statusFilter === "all"}
onPress={() => setStatusFilter("all")}
/>
<FilterChip
label={t`Watching`}
isSelected={statusFilter === "watching"}
onPress={() => setStatusFilter("watching")}
/>
<FilterChip
label={t`Watchlist`}
isSelected={statusFilter === "watchlist"}
onPress={() => setStatusFilter("watchlist")}
/>
</View>
</View>
{!isPending && allItems.length === 0 ? (
<Animated.View
entering={FadeInDown.duration(300)}
className="flex-1 items-center justify-center px-4"
>
<ScaledIcon icon={IconCalendarEvent} size={48} color={`${mutedColor}66`} />
<Text className="text-muted-foreground mt-4 text-center text-sm">
<Trans>No upcoming episodes or releases in the next 90 days.</Trans>
</Text>
{filterChips}
<View className="flex-1 items-center justify-center">
<ScaledIcon icon={IconCalendarEvent} size={48} color={`${mutedColor}66`} />
<Text className="text-muted-foreground mt-4 text-center text-sm">
<Trans>No upcoming episodes or releases in the next 90 days.</Trans>
</Text>
</View>
</Animated.View>
) : (
<SectionList
sections={sections}
keyExtractor={(item, i) => `${item.titleId}-${item.date}-${i}`}
ListHeaderComponent={filterChips}
renderItem={({ item }) => (
<View className="px-4 py-1">
<UpcomingRow item={item} />
</View>
)}
renderSectionHeader={({ section: { title } }) => (
<View className="bg-background px-4 pt-4 pb-1">
<View className="bg-background px-4 pt-3 pb-1">
<Text className="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
{title}
</Text>