import React, { useState, useRef, useEffect } from 'react'; import { SyncStrategy, RegexReplacement, SyncState, ScheduleSettings, ScheduleMode } from '../types'; import { ArrowRightCircle, ArrowLeftCircle, GitMerge, ChevronDown, Check, HelpCircle, Plus, Trash2, Save, RotateCcw, Zap, Loader2, Calendar, Clock, Repeat, CheckSquare, Square } from 'lucide-react'; interface StrategyOption { value: SyncStrategy; label: string; description: string; icon: React.ElementType; color: string; } const STRATEGIES: StrategyOption[] = [ { value: SyncStrategy.LOCAL_OVERWRITE, label: 'Local Overwrite', description: 'Local playlist completely overwrites Cloud. (No Diff)', icon: ArrowRightCircle, color: 'text-blue-400' }, { value: SyncStrategy.CLOUD_OVERWRITE, label: 'Cloud Overwrite', description: 'Cloud playlist completely overwrites Local. (No Diff)', icon: ArrowLeftCircle, color: 'text-green-400' }, { value: SyncStrategy.MERGE_LOCAL, label: 'Two-way Merge (Local Priority)', description: 'Merge both. Conflicts resolve to Local version.', icon: GitMerge, color: 'text-blue-300' }, { value: SyncStrategy.MERGE_CLOUD, label: 'Two-way Merge (Cloud Priority)', description: 'Merge both. Conflicts resolve to Cloud version.', icon: GitMerge, color: 'text-green-300' } ]; const WEEK_DAYS = ['S', 'M', 'T', 'W', 'T', 'F', 'S']; interface StrategySelectorProps { currentStrategy: SyncStrategy; onSelect: (strategy: SyncStrategy, label: string) => void; savedRegexReplacements: RegexReplacement[]; onSaveRegex: (replacements: RegexReplacement[]) => void; savedSchedule: ScheduleSettings; onSaveSchedule: (settings: ScheduleSettings) => Promise; syncState: SyncState; onSync: () => void; } const StrategySelector: React.FC = ({ currentStrategy, onSelect, savedRegexReplacements, onSaveRegex, savedSchedule, onSaveSchedule, syncState, onSync }) => { const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef(null); // Local state for regex editing const [localReplacements, setLocalReplacements] = useState([]); const [isRegexDirty, setIsRegexDirty] = useState(false); // Local state for Schedule editing const [localSchedule, setLocalSchedule] = useState(savedSchedule); const [isScheduleDirty, setIsScheduleDirty] = useState(false); // UI State for Schedule Tabs // We initialize active tab based on the saved mode. If DISABLED, default to CRON. const [activeTab, setActiveTab] = useState( savedSchedule.mode === ScheduleMode.DISABLED ? ScheduleMode.CRON : savedSchedule.mode ); const isSyncing = syncState === SyncState.SYNCING; const isLocked = isSyncing || syncState === SyncState.SUCCESS; // Initialize local state when prop updates useEffect(() => { setLocalReplacements(JSON.parse(JSON.stringify(savedRegexReplacements))); setIsRegexDirty(false); }, [savedRegexReplacements]); useEffect(() => { setLocalSchedule(JSON.parse(JSON.stringify(savedSchedule))); // If the saved mode is not disabled, ensure we show that tab. if (savedSchedule.mode !== ScheduleMode.DISABLED) { setActiveTab(savedSchedule.mode); } setIsScheduleDirty(false); }, [savedSchedule]); // Check dirty state whenever local changes useEffect(() => { const isDifferent = JSON.stringify(localReplacements) !== JSON.stringify(savedRegexReplacements); setIsRegexDirty(isDifferent); }, [localReplacements, savedRegexReplacements]); useEffect(() => { const isDifferent = JSON.stringify(localSchedule) !== JSON.stringify(savedSchedule); setIsScheduleDirty(isDifferent); }, [localSchedule, savedSchedule]); const selectedOption = STRATEGIES.find(s => s.value === currentStrategy) || STRATEGIES[0]; useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { setIsOpen(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); const handleSelect = (strategy: StrategyOption) => { if (isLocked) return; onSelect(strategy.value, strategy.label); }; // --- Regex Handlers --- const handleAddRegex = () => { if (isLocked) return; const newId = Date.now().toString(); setLocalReplacements(prev => [...prev, { id: newId, pattern: '', replacement: '' }]); }; const handleDeleteRegex = (id: string) => { if (isLocked) return; setLocalReplacements(prev => prev.filter(r => r.id !== id)); }; const handleUpdateRegex = (id: string, field: 'pattern' | 'replacement', value: string) => { if (isLocked) return; setLocalReplacements(prev => prev.map(r => r.id === id ? { ...r, [field]: value } : r )); }; const handleResetRegex = () => { if (isLocked) return; setLocalReplacements(JSON.parse(JSON.stringify(savedRegexReplacements))); }; const handleSaveRegex = () => { if (isLocked) return; const validReplacements = localReplacements.filter(r => r.pattern.trim() !== ''); setLocalReplacements(validReplacements); onSaveRegex(validReplacements); }; // --- Schedule Handlers --- const handleUpdateSchedule = (field: keyof ScheduleSettings, value: any) => { if (isLocked) return; setLocalSchedule(prev => ({ ...prev, [field]: value })); }; const toggleWeekDay = (dayIndex: number) => { if (isLocked) return; const currentDays = localSchedule.weeklyDays; const newDays = currentDays.includes(dayIndex) ? currentDays.filter(d => d !== dayIndex) : [...currentDays, dayIndex].sort(); handleUpdateSchedule('weeklyDays', newDays); }; const handleResetSchedule = () => { if (isLocked) return; setLocalSchedule(JSON.parse(JSON.stringify(savedSchedule))); if (savedSchedule.mode !== ScheduleMode.DISABLED) { setActiveTab(savedSchedule.mode); } }; const handleSaveScheduleClick = async () => { if (isLocked) return; let settingsToSave = { ...localSchedule }; // Logic to determine mode based on active Tab and checkbox state if (activeTab === ScheduleMode.CRON) { if (settingsToSave.cronExpression.trim() !== '') { settingsToSave.mode = ScheduleMode.CRON; } else { // Empty cron -> disabled settingsToSave.mode = ScheduleMode.DISABLED; } } // For Daily/Weekly, enforce: Save commits what is seen in the active tab. if (activeTab !== ScheduleMode.CRON) { // If the mode matches the active tab, it's enabled. Otherwise disabled. if (localSchedule.mode !== activeTab) { settingsToSave.mode = ScheduleMode.DISABLED; } } // Call API const success = await onSaveSchedule(settingsToSave); if (success) { setLocalSchedule(settingsToSave); setIsScheduleDirty(false); } }; const handleSyncClick = () => { if (isLocked) return; onSync(); }; // Helper to toggle enable/disable for current active tab (Daily/Weekly) const toggleScheduleEnable = (targetMode: ScheduleMode) => { if (isLocked) return; if (localSchedule.mode === targetMode) { handleUpdateSchedule('mode', ScheduleMode.DISABLED); } else { handleUpdateSchedule('mode', targetMode); } }; // If syncing or locked, apply grayscale filter to content sections const contentClass = isLocked ? "opacity-50 pointer-events-none grayscale transition-all" : "transition-all"; return (
{/* Trigger Button */} {/* Dropdown Menu */}
{/* Section 1: Sync Strategy */}

Sync Strategy

{STRATEGIES.map((strategy) => (
handleSelect(strategy)} className={`group flex items-center justify-between p-2 rounded-lg cursor-pointer transition-all border ${ currentStrategy === strategy.value ? 'bg-white/10 border-white/10 shadow-sm' : 'hover:bg-white/5 border-transparent' }`} >
{strategy.label}
{strategy.description}
{currentStrategy === strategy.value && ( )}
))}
{/* Section 2: Regex Preprocessing */}

Regex Rules

{localReplacements.length === 0 && ( )}
{localReplacements.length === 0 ? (
No regex replacements configured.
) : ( localReplacements.map((regex) => (
handleUpdateRegex(regex.id, 'pattern', e.target.value)} className={`w-full bg-gray-900/80 border rounded-md px-2 py-1.5 text-xs text-gray-200 focus:outline-none focus:ring-1 transition-all placeholder-gray-600 ${!regex.pattern && isRegexDirty ? 'border-red-500/30 focus:border-red-500' : 'border-gray-700 focus:border-plex-orange'}`} />
handleUpdateRegex(regex.id, 'replacement', e.target.value)} className="w-full bg-gray-900/80 border border-gray-700 rounded-md px-2 py-1.5 text-xs text-gray-200 focus:outline-none focus:border-plex-orange focus:ring-1 focus:ring-plex-orange transition-all placeholder-gray-600" />
)) )}
{/* Section 3: Scheduled Tasks */}

Scheduled Tasks

{/* Tabs */}
{[ { id: ScheduleMode.CRON, label: 'Cron', icon: Repeat }, { id: ScheduleMode.DAILY, label: 'Daily', icon: Clock }, { id: ScheduleMode.WEEKLY, label: 'Weekly', icon: Calendar }, ].map((tab) => ( ))}
{/* Tab Content */}
{activeTab === ScheduleMode.CRON && (
Cron: handleUpdateSchedule('cronExpression', e.target.value)} placeholder="0 0 * * *" className="flex-1 bg-gray-800 border border-gray-700 rounded-md px-2.5 py-1.5 text-xs text-gray-200 font-mono focus:border-plex-orange focus:outline-none focus:ring-1 focus:ring-plex-orange placeholder-gray-600" />

Unix-cron format. Leave empty to disable schedule.

)} {activeTab === ScheduleMode.DAILY && (
{/* Top Row: Checkbox + Label */}
{/* Bottom Row: Centered Native Time Input */}
handleUpdateSchedule('dailyTime', e.target.value)} disabled={localSchedule.mode !== ScheduleMode.DAILY} className={`bg-gray-800 border border-gray-700 text-white rounded-md px-4 py-2 text-xl font-mono focus:border-plex-orange focus:ring-1 focus:ring-plex-orange focus:outline-none transition-all ${localSchedule.mode !== ScheduleMode.DAILY ? 'opacity-50 cursor-not-allowed' : ''}`} />
)} {activeTab === ScheduleMode.WEEKLY && (
{/* Top Row: Checkbox + Label */}
{/* Middle Row: Full Width Capsules */}
{WEEK_DAYS.map((day, index) => { const isSelected = localSchedule.weeklyDays.includes(index); return ( ) })}
{/* Bottom Row: Centered Native Time Input */}
handleUpdateSchedule('weeklyTime', e.target.value)} disabled={localSchedule.mode !== ScheduleMode.WEEKLY} className={`bg-gray-800 border border-gray-700 text-white rounded-md px-4 py-2 text-xl font-mono focus:border-plex-orange focus:ring-1 focus:ring-plex-orange focus:outline-none transition-all ${localSchedule.mode !== ScheduleMode.WEEKLY ? 'opacity-50 cursor-not-allowed' : ''}`} />
)}
{/* Auto Watch Checkbox */}
{/* Action Buttons (Mirrored from Regex) */}
{/* Section 4: Sync Now Button */}
{(isRegexDirty) && (

Please save regex changes before syncing.

)}
); }; export default StrategySelector;