import React, { useState, useRef, useEffect } from 'react'; import { SyncStrategy, ReplacementRule, PathMappingConfig, PathMappingRules, PathMappingMode, SyncState, ScheduleSettings, ScheduleMode, BackupSettings } from '../types'; import { ArrowRightCircle, ArrowLeftCircle, GitMerge, ChevronDown, Check, HelpCircle, Plus, Trash2, Save, RotateCcw, Zap, Loader2, Calendar, Clock, Repeat, Type, Code2, Link, Archive, History, Eye } from 'lucide-react'; import { useLanguage } from '../LanguageContext'; // Generate a UUID for mapping rules const generateUUID = (): string => { // Use crypto.randomUUID() if available (modern browsers) if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { return crypto.randomUUID(); } // Fallback to manual UUID v4 generation return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); }; interface StrategyOption { value: SyncStrategy; labelKey: string; descKey: string; icon: React.ElementType; color: string; } const STRATEGIES: StrategyOption[] = [ { value: SyncStrategy.LOCAL_OVERWRITE, labelKey: 'strategies.localOverwrite.label', descKey: 'strategies.localOverwrite.desc', icon: ArrowRightCircle, color: 'text-blue-400' }, { value: SyncStrategy.CLOUD_OVERWRITE, labelKey: 'strategies.cloudOverwrite.label', descKey: 'strategies.cloudOverwrite.desc', icon: ArrowLeftCircle, color: 'text-green-400' }, { value: SyncStrategy.MERGE_LOCAL, labelKey: 'strategies.mergeLocal.label', descKey: 'strategies.mergeLocal.desc', icon: GitMerge, color: 'text-blue-300' }, { value: SyncStrategy.MERGE_CLOUD, labelKey: 'strategies.mergeCloud.label', descKey: 'strategies.mergeCloud.desc', icon: GitMerge, color: 'text-green-300' } ]; const WEEK_DAY_INDEXES = [0, 1, 2, 3, 4, 5, 6]; // Color Theme Variables for Mapping Editors const MAPPING_THEME = { // Container Themes local: { borderColor: "border-blue-500/20", bgColor: "bg-blue-900/10" }, remote: { borderColor: "border-green-500/20", bgColor: "bg-green-900/10" }, simple: { borderColor: "border-gray-700/50", bgColor: "bg-gray-900/40" }, // Input Field Themes inputs: { default: "bg-gray-800 border-gray-700 text-gray-200 focus:border-plex-orange placeholder-gray-600", local: "bg-blue-500/10 border-blue-500/30 text-blue-100 focus:border-blue-400 placeholder-blue-300/30", cloud: "bg-green-500/10 border-green-500/30 text-green-100 focus:border-green-400 placeholder-green-300/30" } }; // Helper to determine the actual mode and settings that would be saved based on the current UI state const deriveEffectiveSchedule = (schedule: ScheduleSettings, tab: ScheduleMode): ScheduleSettings => { const derived = { ...schedule }; // Unified logic: If the mode matches the tab, we keep it (Enabled). // If the mode doesn't match (e.g. it was DISABLED), then in the context of this tab, it remains Disabled until the user toggles the switch. if (derived.mode === tab) { derived.mode = tab; } else { derived.mode = ScheduleMode.DISABLED; } return derived; }; // Sub-component for a single Mapping Group Editor interface MappingGroupEditorProps { title: string; subtitle?: string; rules: ReplacementRule[]; onChange: (newRules: ReplacementRule[]) => void; isLocked: boolean; borderColor?: string; bgColor?: string; // Input specific props leftPlaceholder?: string; rightPlaceholder?: string; leftInputClass?: string; rightInputClass?: string; } const MappingGroupEditor: React.FC = ({ title, subtitle, rules, onChange, isLocked, borderColor = "border-gray-700", bgColor = "bg-gray-900/50", leftPlaceholder = "Pattern", rightPlaceholder = "Replace", leftInputClass, rightInputClass }) => { const { t } = useLanguage(); const handleAdd = () => { if (isLocked) return; const newId = generateUUID(); onChange([...rules, { id: newId, search: '', replace: '' }]); }; const handleUpdate = (id: string, field: 'search' | 'replace', value: string) => { if (isLocked) return; onChange(rules.map(r => r.id === id ? { ...r, [field]: value } : r)); }; const handleDelete = (id: string) => { if (isLocked) return; onChange(rules.filter(r => r.id !== id)); }; // Default input style if not provided const defaultInputStyle = MAPPING_THEME.inputs.default; return (

{title}

{subtitle &&

{subtitle}

}
{rules.length === 0 ? (
{t('mapping.noRules')}
) : ( rules.map((rule) => (
handleUpdate(rule.id, 'search', e.target.value)} className={`flex-1 min-w-0 border rounded px-1.5 py-1 text-xs focus:outline-none transition-colors ${leftInputClass || defaultInputStyle}`} /> handleUpdate(rule.id, 'replace', e.target.value)} className={`flex-1 min-w-0 border rounded px-1.5 py-1 text-xs focus:outline-none transition-colors ${rightInputClass || defaultInputStyle}`} />
)) )}
); }; interface StrategySelectorProps { currentStrategy: SyncStrategy; onSelect: (strategy: SyncStrategy, label: string) => void; savedPathMapping: PathMappingConfig; onSavePathMapping: (config: PathMappingConfig) => void; savedBackup: BackupSettings; onSaveBackup: (settings: BackupSettings) => void; savedSchedule: ScheduleSettings; onSaveSchedule: (settings: ScheduleSettings) => Promise; syncState: SyncState; onSync: () => void; } const StrategySelector: React.FC = ({ currentStrategy, onSelect, savedPathMapping, onSavePathMapping, savedBackup, onSaveBackup, savedSchedule, onSaveSchedule, syncState, onSync }) => { const { t } = useLanguage(); const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef(null); // Local state for path mapping editing (stores all lists for both modes) const [localPathMapping, setLocalPathMapping] = useState(savedPathMapping); const [isMappingDirty, setIsMappingDirty] = useState(false); // Local state for Backup Settings const [localBackup, setLocalBackup] = useState(savedBackup); const [isBackupDirty, setIsBackupDirty] = useState(false); // Local state for Schedule editing const [localSchedule, setLocalSchedule] = useState(savedSchedule); const [isScheduleDirty, setIsScheduleDirty] = useState(false); // UI State for Schedule Tabs const [activeScheduleTab, setActiveScheduleTab] = 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(() => { setLocalPathMapping(JSON.parse(JSON.stringify(savedPathMapping))); setIsMappingDirty(false); }, [savedPathMapping]); useEffect(() => { setLocalBackup(JSON.parse(JSON.stringify(savedBackup))); setIsBackupDirty(false); }, [savedBackup]); useEffect(() => { setLocalSchedule(JSON.parse(JSON.stringify(savedSchedule))); if (savedSchedule.mode !== ScheduleMode.DISABLED) { setActiveScheduleTab(savedSchedule.mode); } setIsScheduleDirty(false); }, [savedSchedule]); // Check dirty state whenever local mapping changes useEffect(() => { const isDifferent = JSON.stringify(localPathMapping) !== JSON.stringify(savedPathMapping); setIsMappingDirty(isDifferent); }, [localPathMapping, savedPathMapping]); // Check dirty state for backup useEffect(() => { const isDifferent = JSON.stringify(localBackup) !== JSON.stringify(savedBackup); setIsBackupDirty(isDifferent); }, [localBackup, savedBackup]); // Check dirty state for Schedule (including Active Tab changes) useEffect(() => { const effectiveLocal = deriveEffectiveSchedule(localSchedule, activeScheduleTab); const isDifferent = JSON.stringify(effectiveLocal) !== JSON.stringify(savedSchedule); setIsScheduleDirty(isDifferent); }, [localSchedule, savedSchedule, activeScheduleTab]); 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 isScheduleActionable = isScheduleDirty || (activeScheduleTab !== (savedSchedule.mode === ScheduleMode.DISABLED ? ScheduleMode.CRON : savedSchedule.mode)); const handleSelect = (strategy: StrategyOption) => { if (isLocked) return; onSelect(strategy.value, t(strategy.labelKey)); }; // --- Path Mapping Handlers --- const currentMappingMode = localPathMapping.mode; const updateRegexGroup = (section: keyof PathMappingRules, newRules: ReplacementRule[]) => { if (isLocked) return; setLocalPathMapping(prev => ({ ...prev, regex: { ...prev.regex, [section]: newRules } })); }; const updateSimpleGroup = (newRules: ReplacementRule[]) => { if (isLocked) return; setLocalPathMapping(prev => ({ ...prev, simple: newRules })); }; const setMappingMode = (mode: PathMappingMode) => { if (isLocked) return; setLocalPathMapping(prev => ({ ...prev, mode })); }; const handleResetMapping = () => { if (isLocked) return; setLocalPathMapping(JSON.parse(JSON.stringify(savedPathMapping))); }; const handleSaveMappingClick = () => { if (isLocked) return; const clean = (rules: ReplacementRule[]) => rules.filter(r => r.search.trim() !== ''); // Clean regex rules const cleanRegex = (rules: PathMappingRules): PathMappingRules => ({ localPre: clean(rules.localPre), localPost: clean(rules.localPost), remotePre: clean(rules.remotePre), remotePost: clean(rules.remotePost), }); const cleanedConfig: PathMappingConfig = { mode: localPathMapping.mode, simple: clean(localPathMapping.simple), regex: cleanRegex(localPathMapping.regex), }; setLocalPathMapping(cleanedConfig); onSavePathMapping(cleanedConfig); }; const regexRules = localPathMapping.regex; const simpleRules = localPathMapping.simple; // --- Backup Handlers --- const handleUpdateBackup = (field: keyof BackupSettings, value: any) => { if (isLocked) return; setLocalBackup(prev => ({ ...prev, [field]: value })); }; const handleResetBackup = () => { if (isLocked) return; setLocalBackup(JSON.parse(JSON.stringify(savedBackup))); }; const handleSaveBackupClick = () => { if (isLocked) return; onSaveBackup(localBackup); }; // --- 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) { setActiveScheduleTab(savedSchedule.mode); } else { setActiveScheduleTab(ScheduleMode.CRON); } }; const handleSaveScheduleClick = async () => { if (isLocked) return; const settingsToSave = deriveEffectiveSchedule(localSchedule, activeScheduleTab); const success = await onSaveSchedule(settingsToSave); if (success) { setLocalSchedule(settingsToSave); } }; const handleSyncClick = () => { if (isLocked) return; onSync(); }; const toggleScheduleEnable = (targetMode: ScheduleMode) => { if (isLocked) return; if (localSchedule.mode === targetMode) { handleUpdateSchedule('mode', ScheduleMode.DISABLED); } else { handleUpdateSchedule('mode', targetMode); } }; const contentClass = isLocked ? "opacity-50 pointer-events-none grayscale transition-all" : "transition-all"; return (
{/* Trigger Button */} {/* Dropdown Menu */}
{/* Section 1: Sync Strategy */}

{t('strategies.title')}

{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' }`} >
{t(strategy.labelKey)}
{t(strategy.descKey)}
{currentStrategy === strategy.value && ( )}
))}
{/* Section 1.5: Backup Retention */}

{t('backup.title')}

{t('backup.enable')} {t('backup.enableDesc')}
{/* Expanded Config */}
{t('backup.maxVersions')}
{ const value = parseInt(e.target.value, 10); handleUpdateBackup('retentionCount', isNaN(value) ? 0 : Math.max(0, value)); }} className="w-16 bg-gray-800 border border-gray-700 text-center text-sm rounded py-1 text-white focus:border-plex-orange focus:outline-none" /> {localBackup.retentionCount === 0 ? t('backup.noAutoDelete') : t('backup.autoDelete')}
{/* Section 2: Path Mapping (Tabs + Grid) */}

{t('mapping.title')}

{/* Tabs for Path Mapping Mode */}
{[ { id: PathMappingMode.SIMPLE, label: t('mapping.simple'), icon: Type }, { id: PathMappingMode.REGEX, label: t('mapping.regex'), icon: Code2 }, ].map((tab) => ( ))}
{/* Content Area */}
{currentMappingMode === PathMappingMode.SIMPLE ? ( // Simple Mode: Single Editor
) : ( // Regex Mode: 2x2 Grid
{/* Row 1: Pre-Processing */} updateRegexGroup('localPre', rules)} isLocked={isLocked} borderColor={MAPPING_THEME.local.borderColor} bgColor={MAPPING_THEME.local.bgColor} /> updateRegexGroup('remotePre', rules)} isLocked={isLocked} borderColor={MAPPING_THEME.remote.borderColor} bgColor={MAPPING_THEME.remote.bgColor} /> {/* Row 2: Post-Processing */} updateRegexGroup('localPost', rules)} isLocked={isLocked} borderColor={MAPPING_THEME.local.borderColor} bgColor={MAPPING_THEME.local.bgColor} /> updateRegexGroup('remotePost', rules)} isLocked={isLocked} borderColor={MAPPING_THEME.remote.borderColor} bgColor={MAPPING_THEME.remote.bgColor} />
)}
{/* Section 3: Scheduled Tasks */}

{t('schedule.title')}

{[ { id: ScheduleMode.CRON, label: t('schedule.cron'), icon: Repeat }, { id: ScheduleMode.DAILY, label: t('schedule.daily'), icon: Clock }, { id: ScheduleMode.WEEKLY, label: t('schedule.weekly'), icon: Calendar }, ].map((tab) => ( ))}
{/* Tab Content */}
{activeScheduleTab === ScheduleMode.CRON && (
{/* Top Row: Label + Switch */}
{t('schedule.enableCron')}
{/* Content */}
Cron: handleUpdateSchedule('cronExpression', e.target.value)} placeholder="0 0 * * *" disabled={localSchedule.mode !== ScheduleMode.CRON} 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.

)} {activeScheduleTab === ScheduleMode.DAILY && (
{/* Top Row: Label + Switch */}
{t('schedule.enableDaily')}
{/* 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' : ''}`} />
)} {activeScheduleTab === ScheduleMode.WEEKLY && (
{/* Top Row: Label + Switch */}
{t('schedule.enableWeekly')}
{/* Middle Row: Full Width Capsules */}
{WEEK_DAY_INDEXES.map((dayIndex) => { const isSelected = localSchedule.weeklyDays.includes(dayIndex); 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 Switch */}
{t('schedule.watchLocal')} {t('schedule.watchDesc')}
{/* Action Buttons */}
{/* Section 4: Sync Now Button */}
{(isMappingDirty || isBackupDirty) && (

{t('strategies.saveWarning')}

)}
); }; export default StrategySelector;