Add grid item size setting and UI

Introduce a persistent gridItemSize user setting (1-10) across the app. Updates include: types (UserSettings.gridItemSize), API mappings (grid_item_size in ApiSettingsItem, CreateSettingsInput, convertApiToSettings, convertSettingsToApi), default setting values, and the App handler (handleGridItemSizeChange) to save changes. UI additions: slider control in SettingsView, slider and value in BrowseView (with syncing to incoming API settings), passing the prop and change callback from App, and a mapping from slider values to responsive Tailwind grid column classes so the grid layout adapts to the chosen size. Also added syncing of itemsPerPage in BrowseView and CastView with API-loaded settings.
This commit is contained in:
Lars Behrends
2026-04-10 23:31:24 +02:00
parent 444c908449
commit f482807387
6 changed files with 112 additions and 11 deletions
+56 -4
View File
@@ -18,14 +18,31 @@ interface BrowseViewProps {
onMediaClick: (media: Media) => void;
activeCategory: MediaCategory;
itemsPerPage?: number;
gridItemSize?: number;
onGridItemSizeChange?: (size: number) => void;
}
export default function BrowseView({ mediaList, onMediaClick, activeCategory, itemsPerPage: initialItemsPerPage = 12 }: BrowseViewProps) {
export default function BrowseView({ mediaList, onMediaClick, activeCategory, itemsPerPage: initialItemsPerPage = 12, gridItemSize: initialGridItemSize = 5, onGridItemSizeChange }: BrowseViewProps) {
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(initialItemsPerPage);
const [sortBy, setSortBy] = useState<string>('default');
const [gridItemSize, setGridItemSize] = useState<number>(initialGridItemSize);
// Sync itemsPerPage with prop when API settings are loaded
useEffect(() => {
if (initialItemsPerPage) {
setItemsPerPage(initialItemsPerPage);
}
}, [initialItemsPerPage]);
// Sync gridItemSize with prop when API settings are loaded
useEffect(() => {
if (initialGridItemSize !== undefined) {
setGridItemSize(initialGridItemSize);
}
}, [initialGridItemSize]);
// Filter states
const [selectedGenre, setSelectedGenre] = useState<string | null>(null);
const [selectedStudio, setSelectedStudio] = useState<string | null>(null);
@@ -67,6 +84,23 @@ export default function BrowseView({ mediaList, onMediaClick, activeCategory, it
return list;
}, [filteredMedia, sortBy]);
const gridColsClass = useMemo(() => {
// Map slider value (1-10) to grid columns
const colsMap: Record<number, string> = {
1: 'grid-cols-1 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
2: 'grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
3: 'grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5',
4: 'grid-cols-2 sm:grid-cols-3 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6',
5: 'grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8',
6: 'grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8',
7: 'grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-7 xl:grid-cols-9',
8: 'grid-cols-3 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8 xl:grid-cols-10',
9: 'grid-cols-4 sm:grid-cols-5 md:grid-cols-6 lg:grid-cols-8 xl:grid-cols-10',
10: 'grid-cols-4 sm:grid-cols-5 md:grid-cols-6 lg:grid-cols-8 xl:grid-cols-12',
};
return `grid ${colsMap[gridItemSize] || colsMap[5]}`;
}, [gridItemSize]);
const totalPages = Math.ceil(sortedMedia.length / itemsPerPage);
const paginatedMedia = useMemo(() => {
@@ -193,6 +227,24 @@ export default function BrowseView({ mediaList, onMediaClick, activeCategory, it
</div>
<div className="flex items-center gap-4">
{/* Grid item size slider */}
<div className="flex items-center gap-3 bg-muted rounded-md px-3 py-2">
<span className="text-xs font-bold text-muted-foreground">Size</span>
<input
type="range"
min="1"
max="10"
value={gridItemSize}
onChange={(e) => {
const newSize = Number(e.target.value);
setGridItemSize(newSize);
onGridItemSizeChange?.(newSize);
}}
className="w-24 h-2 bg-background rounded-lg appearance-none cursor-pointer accent-[#6d28d9]"
/>
<span className="text-xs font-bold text-[#6d28d9] w-5 text-center">{gridItemSize}</span>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button type="button" className="group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50 h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5 text-muted-foreground font-bold gap-2">
@@ -245,8 +297,8 @@ export default function BrowseView({ mediaList, onMediaClick, activeCategory, it
</div>
) : (
<div className={cn(
viewMode === 'grid'
? "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-x-4 gap-y-8"
viewMode === 'grid'
? cn(gridColsClass, "gap-x-4 gap-y-8")
: "flex flex-col gap-2"
)}>
<AnimatePresence mode="popLayout">
+7
View File
@@ -38,6 +38,13 @@ export default function CastView({ onPersonClick, enabledCategories, itemsPerPag
const [itemsPerPage, setItemsPerPage] = useState(initialItemsPerPage);
const [showFilters, setShowFilters] = useState(false);
// Sync itemsPerPage with prop when API settings are loaded
useEffect(() => {
if (initialItemsPerPage) {
setItemsPerPage(initialItemsPerPage);
}
}, [initialItemsPerPage]);
// Persist filters and sorts
useEffect(() => {
localStorage.setItem('castSearchQuery', searchQuery);
+19
View File
@@ -36,6 +36,7 @@ export default function SettingsView({ onSettingsSaved }: SettingsViewProps) {
const [settings, setSettings] = useState<UserSettings>({
enabledCategories: ['Anime', 'Movies', 'TV Series', 'Music', 'Books', 'Consoles', 'Games', 'Adult'],
itemsPerPage: 20,
gridItemSize: 5,
defaultView: 'grid',
showAdultContent: false,
autoPlayTrailers: false,
@@ -233,6 +234,24 @@ export default function SettingsView({ onSettingsSaved }: SettingsViewProps) {
</div>
</div>
{/* Grid item size */}
<div>
<Label className="text-sm font-black text-foreground mb-2 block">Grid item size</Label>
<div className="flex items-center gap-4">
<span className="text-xs font-bold text-muted-foreground">Small</span>
<input
type="range"
min="1"
max="10"
value={settings.gridItemSize}
onChange={(e) => setSettings(prev => ({ ...prev, gridItemSize: Number(e.target.value) }))}
className="flex-1 h-2 bg-muted rounded-lg appearance-none cursor-pointer accent-[#6d28d9]"
/>
<span className="text-xs font-bold text-muted-foreground">Large</span>
<span className="text-sm font-bold text-[#6d28d9] w-8 text-center">{settings.gridItemSize}</span>
</div>
</div>
{/* Theme */}
<div>
<Label className="text-sm font-black text-foreground mb-2 block">Theme</Label>