/** * Component: Indexer Configuration Modal * Documentation: documentation/frontend/components.md */ 'use client'; import React, { useState, useEffect } from 'react'; import { Modal } from '@/components/ui/Modal'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import { CategoryTreeView } from './CategoryTreeView'; import { DEFAULT_CATEGORIES } from '@/lib/utils/torrent-categories'; interface IndexerConfigModalProps { isOpen: boolean; onClose: () => void; mode: 'add' | 'edit'; indexer: { id: number; name: string; protocol: string; supportsRss: boolean; }; initialConfig?: { priority: number; seedingTimeMinutes?: number; removeAfterProcessing?: boolean; rssEnabled: boolean; categories: number[]; }; onSave: (config: { id: number; name: string; protocol: string; priority: number; seedingTimeMinutes?: number; removeAfterProcessing?: boolean; rssEnabled: boolean; categories: number[]; }) => void; } export function IndexerConfigModal({ isOpen, onClose, mode, indexer, initialConfig, onSave, }: IndexerConfigModalProps) { // Default values for Add mode const isTorrent = indexer.protocol?.toLowerCase() === 'torrent'; const defaults = { priority: 10, seedingTimeMinutes: 0, removeAfterProcessing: true, // Default to true for Usenet rssEnabled: indexer.supportsRss, categories: DEFAULT_CATEGORIES, // Default to Audio/Audiobook [3030] }; // Form state const [priority, setPriority] = useState( initialConfig?.priority ?? defaults.priority ); const [seedingTimeMinutes, setSeedingTimeMinutes] = useState( initialConfig?.seedingTimeMinutes ?? defaults.seedingTimeMinutes ); const [removeAfterProcessing, setRemoveAfterProcessing] = useState( initialConfig?.removeAfterProcessing ?? defaults.removeAfterProcessing ); const [rssEnabled, setRssEnabled] = useState( initialConfig?.rssEnabled ?? defaults.rssEnabled ); const [selectedCategories, setSelectedCategories] = useState( initialConfig?.categories ?? defaults.categories ); // Validation errors const [errors, setErrors] = useState<{ priority?: string; seedingTimeMinutes?: string; categories?: string; }>({}); // Reset form when modal opens or indexer changes useEffect(() => { if (isOpen) { if (mode === 'add') { setPriority(defaults.priority); setSeedingTimeMinutes(defaults.seedingTimeMinutes); setRemoveAfterProcessing(defaults.removeAfterProcessing); setRssEnabled(defaults.rssEnabled); setSelectedCategories(defaults.categories); } else { setPriority(initialConfig?.priority ?? defaults.priority); setSeedingTimeMinutes(initialConfig?.seedingTimeMinutes ?? defaults.seedingTimeMinutes); setRemoveAfterProcessing(initialConfig?.removeAfterProcessing ?? defaults.removeAfterProcessing); setRssEnabled(initialConfig?.rssEnabled ?? defaults.rssEnabled); setSelectedCategories(initialConfig?.categories ?? defaults.categories); } setErrors({}); } }, [isOpen, mode, indexer.id]); const validate = () => { const newErrors: typeof errors = {}; if (priority < 1 || priority > 25) { newErrors.priority = 'Priority must be between 1 and 25'; } if (isTorrent && seedingTimeMinutes < 0) { newErrors.seedingTimeMinutes = 'Seeding time cannot be negative'; } if (selectedCategories.length === 0) { newErrors.categories = 'At least one category must be selected'; } setErrors(newErrors); return Object.keys(newErrors).length === 0; }; const handleSave = () => { if (!validate()) { return; } const config: any = { id: indexer.id, name: indexer.name, protocol: indexer.protocol, priority, rssEnabled: indexer.supportsRss ? rssEnabled : false, categories: selectedCategories, }; // Add protocol-specific fields if (isTorrent) { config.seedingTimeMinutes = seedingTimeMinutes; } else { config.removeAfterProcessing = removeAfterProcessing; } onSave(config); onClose(); }; const handlePriorityChange = (value: string) => { const parsed = parseInt(value); if (!isNaN(parsed)) { // Clamp value between 1 and 25 setPriority(Math.max(1, Math.min(25, parsed))); } else if (value === '') { setPriority(1); } }; const handleSeedingTimeChange = (value: string) => { if (value === '') { setSeedingTimeMinutes(0); } else { const parsed = parseInt(value); if (!isNaN(parsed)) { setSeedingTimeMinutes(Math.max(0, parsed)); } } }; return (
{/* Indexer Info (readonly) */}
{indexer.name} {indexer.protocol}
{/* Priority */}
handlePriorityChange(e.target.value)} className={errors.priority ? 'border-red-500' : ''} />

Higher values = preferred in ranking algorithm

{errors.priority && (

{errors.priority}

)}
{/* Seeding Time (Torrents only) */} {isTorrent && (
handleSeedingTimeChange(e.target.value)} placeholder="0" className={errors.seedingTimeMinutes ? 'border-red-500' : ''} />

0 = unlimited seeding (files remain seeded indefinitely)

{errors.seedingTimeMinutes && (

{errors.seedingTimeMinutes}

)}
)} {/* Remove After Processing (Usenet only) */} {!isTorrent && (
setRemoveAfterProcessing(e.target.checked)} className="h-5 w-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500" /> Remove download from SABnzbd after files are organized

Recommended: Automatically deletes completed NZB downloads to save disk space

)} {/* RSS Monitoring */}
setRssEnabled(e.target.checked)} disabled={!indexer.supportsRss} className="h-5 w-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed" /> Auto-check RSS feeds every 15 minutes
{!indexer.supportsRss && (

This indexer does not support RSS monitoring

)}
{/* Categories */}

Select categories to search on this indexer. Parent selection locks all children as selected.

{errors.categories && (

{errors.categories}

)}
{/* Action Buttons */}
); }