|
|
|
@@ -1,5 +1,5 @@
|
|
|
|
|
import React, { useState, useRef, useEffect } from 'react';
|
|
|
|
|
import { SyncStrategy, ReplacementRule, PathMappingConfig, PathMappingRules, PathMappingMode, SyncState, ScheduleSettings, ScheduleMode } from '../types';
|
|
|
|
|
import { SyncStrategy, ReplacementRule, PathMappingConfig, PathMappingRules, PathMappingMode, SyncState, ScheduleSettings, ScheduleMode, BackupSettings } from '../types';
|
|
|
|
|
import {
|
|
|
|
|
ArrowRightCircle,
|
|
|
|
|
ArrowLeftCircle,
|
|
|
|
@@ -16,10 +16,12 @@ import {
|
|
|
|
|
Calendar,
|
|
|
|
|
Clock,
|
|
|
|
|
Repeat,
|
|
|
|
|
CheckSquare,
|
|
|
|
|
Square,
|
|
|
|
|
Type,
|
|
|
|
|
Code2
|
|
|
|
|
Code2,
|
|
|
|
|
Link,
|
|
|
|
|
Archive,
|
|
|
|
|
History,
|
|
|
|
|
Eye
|
|
|
|
|
} from 'lucide-react';
|
|
|
|
|
|
|
|
|
|
interface StrategyOption {
|
|
|
|
@@ -90,17 +92,12 @@ const MAPPING_THEME = {
|
|
|
|
|
const deriveEffectiveSchedule = (schedule: ScheduleSettings, tab: ScheduleMode): ScheduleSettings => {
|
|
|
|
|
const derived = { ...schedule };
|
|
|
|
|
|
|
|
|
|
if (tab === ScheduleMode.CRON) {
|
|
|
|
|
derived.mode = derived.cronExpression.trim() !== '' ? ScheduleMode.CRON : ScheduleMode.DISABLED;
|
|
|
|
|
// 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 {
|
|
|
|
|
// For Daily/Weekly
|
|
|
|
|
// If the mode matches the tab, we keep it (Enabled).
|
|
|
|
|
// If the mode doesn't match (e.g. it was CRON or DISABLED), then in the context of this tab, it is effectively Disabled until the user checks the box.
|
|
|
|
|
if (derived.mode === tab) {
|
|
|
|
|
derived.mode = tab;
|
|
|
|
|
} else {
|
|
|
|
|
derived.mode = ScheduleMode.DISABLED;
|
|
|
|
|
}
|
|
|
|
|
derived.mode = ScheduleMode.DISABLED;
|
|
|
|
|
}
|
|
|
|
|
return derived;
|
|
|
|
|
};
|
|
|
|
@@ -186,7 +183,7 @@ const MappingGroupEditor: React.FC<MappingGroupEditorProps> = ({
|
|
|
|
|
onChange={(e) => 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}`}
|
|
|
|
|
/>
|
|
|
|
|
<ArrowRightCircle size={10} className="text-gray-600 flex-none opacity-50" />
|
|
|
|
|
<Link size={12} className="text-gray-600 flex-none opacity-50" />
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder={rightPlaceholder}
|
|
|
|
@@ -213,6 +210,8 @@ interface StrategySelectorProps {
|
|
|
|
|
onSelect: (strategy: SyncStrategy, label: string) => void;
|
|
|
|
|
savedPathMapping: PathMappingConfig;
|
|
|
|
|
onSavePathMapping: (config: PathMappingConfig) => void;
|
|
|
|
|
savedBackup: BackupSettings;
|
|
|
|
|
onSaveBackup: (settings: BackupSettings) => void;
|
|
|
|
|
savedSchedule: ScheduleSettings;
|
|
|
|
|
onSaveSchedule: (settings: ScheduleSettings) => Promise<boolean>;
|
|
|
|
|
syncState: SyncState;
|
|
|
|
@@ -224,6 +223,8 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
|
|
|
|
|
onSelect,
|
|
|
|
|
savedPathMapping,
|
|
|
|
|
onSavePathMapping,
|
|
|
|
|
savedBackup,
|
|
|
|
|
onSaveBackup,
|
|
|
|
|
savedSchedule,
|
|
|
|
|
onSaveSchedule,
|
|
|
|
|
syncState,
|
|
|
|
@@ -236,6 +237,10 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
|
|
|
|
|
const [localPathMapping, setLocalPathMapping] = useState<PathMappingConfig>(savedPathMapping);
|
|
|
|
|
const [isMappingDirty, setIsMappingDirty] = useState(false);
|
|
|
|
|
|
|
|
|
|
// Local state for Backup Settings
|
|
|
|
|
const [localBackup, setLocalBackup] = useState<BackupSettings>(savedBackup);
|
|
|
|
|
const [isBackupDirty, setIsBackupDirty] = useState(false);
|
|
|
|
|
|
|
|
|
|
// Local state for Schedule editing
|
|
|
|
|
const [localSchedule, setLocalSchedule] = useState<ScheduleSettings>(savedSchedule);
|
|
|
|
|
const [isScheduleDirty, setIsScheduleDirty] = useState(false);
|
|
|
|
@@ -254,6 +259,11 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
|
|
|
|
|
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) {
|
|
|
|
@@ -268,6 +278,12 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
|
|
|
|
|
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);
|
|
|
|
@@ -351,6 +367,22 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
|
|
|
|
|
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;
|
|
|
|
@@ -469,6 +501,80 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Section 1.5: Backup Retention */}
|
|
|
|
|
<div className="px-4 py-3 bg-gray-900/40 border-b border-white/5 flex-none">
|
|
|
|
|
<div className="flex items-center justify-between mb-3">
|
|
|
|
|
<h3 className="text-[10px] font-bold text-gray-500 uppercase tracking-widest">Backup Retention</h3>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex flex-col space-y-3">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
<div className="p-1.5 rounded-lg bg-indigo-500/10 border border-indigo-500/20 text-indigo-400">
|
|
|
|
|
<Archive size={16} />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex flex-col">
|
|
|
|
|
<span className="text-sm font-medium text-gray-200">Enable Backups</span>
|
|
|
|
|
<span className="text-[10px] text-gray-500">Create a copy before changes</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => handleUpdateBackup('enabled', !localBackup.enabled)}
|
|
|
|
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-plex-orange focus:ring-offset-2 focus:ring-offset-gray-900 ${localBackup.enabled ? 'bg-plex-orange' : 'bg-gray-700'}`}
|
|
|
|
|
>
|
|
|
|
|
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${localBackup.enabled ? 'translate-x-6' : 'translate-x-1'}`} />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Expanded Config */}
|
|
|
|
|
<div className={`overflow-hidden transition-all duration-300 ${localBackup.enabled ? 'max-h-20 opacity-100' : 'max-h-0 opacity-50'}`}>
|
|
|
|
|
<div className="flex items-center justify-between p-2.5 rounded-lg bg-black/20 border border-white/5">
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
<History size={14} className="text-gray-500" />
|
|
|
|
|
<span className="text-xs text-gray-400">Max versions to keep:</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
<input
|
|
|
|
|
type="number"
|
|
|
|
|
min="1"
|
|
|
|
|
max="100"
|
|
|
|
|
value={localBackup.retentionCount}
|
|
|
|
|
onChange={(e) => handleUpdateBackup('retentionCount', parseInt(e.target.value) || 1)}
|
|
|
|
|
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"
|
|
|
|
|
/>
|
|
|
|
|
<span className="text-[10px] text-gray-600 italic">Oldest deleted automatically</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex justify-end items-center gap-2 pt-1">
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleResetBackup}
|
|
|
|
|
disabled={!isBackupDirty}
|
|
|
|
|
className={`flex items-center justify-center space-x-1.5 px-3 py-1.5 rounded-md text-xs font-medium border transition-all
|
|
|
|
|
${isBackupDirty
|
|
|
|
|
? 'bg-gray-800 border-gray-600 text-gray-300 hover:bg-gray-700 hover:text-white'
|
|
|
|
|
: 'bg-transparent border-transparent text-gray-700 cursor-not-allowed'}`}
|
|
|
|
|
>
|
|
|
|
|
<RotateCcw size={12} />
|
|
|
|
|
<span>Revert</span>
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleSaveBackupClick}
|
|
|
|
|
disabled={!isBackupDirty}
|
|
|
|
|
className={`flex items-center justify-center space-x-1.5 px-3 py-1.5 rounded-md text-xs font-bold border transition-all
|
|
|
|
|
${isBackupDirty
|
|
|
|
|
? 'bg-plex-orange border-plex-orange text-gray-900 hover:bg-yellow-500 shadow-lg shadow-plex-orange/10'
|
|
|
|
|
: 'bg-gray-800/30 border-gray-800/50 text-gray-600 cursor-not-allowed'}`}
|
|
|
|
|
>
|
|
|
|
|
<Save size={12} />
|
|
|
|
|
<span>Save</span>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Section 2: Path Mapping (Tabs + Grid) */}
|
|
|
|
|
<div className="p-4 bg-gray-900/40 border-b border-white/5 flex-none">
|
|
|
|
|
<div className="flex items-center justify-between mb-3">
|
|
|
|
@@ -620,35 +726,49 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
|
|
|
|
|
{/* Tab Content */}
|
|
|
|
|
<div className="mb-4 min-h-[50px]">
|
|
|
|
|
{activeScheduleTab === ScheduleMode.CRON && (
|
|
|
|
|
<div className="space-y-2 animate-in fade-in duration-200">
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
<span className="text-gray-500 font-mono text-xs">Cron:</span>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={localSchedule.cronExpression}
|
|
|
|
|
onChange={(e) => 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"
|
|
|
|
|
/>
|
|
|
|
|
<div className="flex flex-col animate-in fade-in duration-200">
|
|
|
|
|
{/* Top Row: Label + Switch */}
|
|
|
|
|
<div className="flex items-center justify-between mb-3 px-1">
|
|
|
|
|
<span className="text-xs text-gray-400 font-medium">Enable Cron Schedule</span>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => toggleScheduleEnable(ScheduleMode.CRON)}
|
|
|
|
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-plex-orange focus:ring-offset-2 focus:ring-offset-gray-900 ${localSchedule.mode === ScheduleMode.CRON ? 'bg-plex-orange' : 'bg-gray-700'}`}
|
|
|
|
|
>
|
|
|
|
|
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${localSchedule.mode === ScheduleMode.CRON ? 'translate-x-6' : 'translate-x-1'}`} />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Content */}
|
|
|
|
|
<div className={`space-y-2 transition-opacity duration-200 ${localSchedule.mode !== ScheduleMode.CRON ? 'opacity-50 pointer-events-none' : ''}`}>
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
<span className="text-gray-500 font-mono text-xs">Cron:</span>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={localSchedule.cronExpression}
|
|
|
|
|
onChange={(e) => 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"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-[10px] text-gray-500">
|
|
|
|
|
Unix-cron format.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-[10px] text-gray-500">
|
|
|
|
|
Unix-cron format. Leave empty to disable schedule.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{activeScheduleTab === ScheduleMode.DAILY && (
|
|
|
|
|
<div className="flex flex-col animate-in fade-in duration-200">
|
|
|
|
|
{/* Top Row: Checkbox + Label */}
|
|
|
|
|
<div className="flex items-center justify-start space-x-2 mb-2">
|
|
|
|
|
{/* Top Row: Label + Switch */}
|
|
|
|
|
<div className="flex items-center justify-between mb-3 px-1">
|
|
|
|
|
<span className="text-xs text-gray-400 font-medium">Enable Daily Run</span>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => toggleScheduleEnable(ScheduleMode.DAILY)}
|
|
|
|
|
className={`transition-colors flex-none ${localSchedule.mode === ScheduleMode.DAILY ? 'text-plex-orange' : 'text-gray-500 hover:text-gray-400'}`}
|
|
|
|
|
title={localSchedule.mode === ScheduleMode.DAILY ? "Schedule Enabled" : "Schedule Disabled"}
|
|
|
|
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-plex-orange focus:ring-offset-2 focus:ring-offset-gray-900 ${localSchedule.mode === ScheduleMode.DAILY ? 'bg-plex-orange' : 'bg-gray-700'}`}
|
|
|
|
|
>
|
|
|
|
|
{localSchedule.mode === ScheduleMode.DAILY ? <CheckSquare size={16} /> : <Square size={16} />}
|
|
|
|
|
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${localSchedule.mode === ScheduleMode.DAILY ? 'translate-x-6' : 'translate-x-1'}`} />
|
|
|
|
|
</button>
|
|
|
|
|
<label className="text-xs text-gray-400 font-medium">Run daily at:</label>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Bottom Row: Centered Native Time Input */}
|
|
|
|
@@ -666,16 +786,15 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
|
|
|
|
|
|
|
|
|
|
{activeScheduleTab === ScheduleMode.WEEKLY && (
|
|
|
|
|
<div className="flex flex-col animate-in fade-in duration-200">
|
|
|
|
|
{/* Top Row: Checkbox + Label */}
|
|
|
|
|
<div className="flex items-center justify-start space-x-2 mb-2">
|
|
|
|
|
<button
|
|
|
|
|
{/* Top Row: Label + Switch */}
|
|
|
|
|
<div className="flex items-center justify-between mb-3 px-1">
|
|
|
|
|
<span className="text-xs text-gray-400 font-medium">Enable Weekly Run</span>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => toggleScheduleEnable(ScheduleMode.WEEKLY)}
|
|
|
|
|
className={`transition-colors flex-none ${localSchedule.mode === ScheduleMode.WEEKLY ? 'text-plex-orange' : 'text-gray-500 hover:text-gray-400'}`}
|
|
|
|
|
title={localSchedule.mode === ScheduleMode.WEEKLY ? "Schedule Enabled" : "Schedule Disabled"}
|
|
|
|
|
>
|
|
|
|
|
{localSchedule.mode === ScheduleMode.WEEKLY ? <CheckSquare size={16} /> : <Square size={16} />}
|
|
|
|
|
</button>
|
|
|
|
|
<label className="text-xs text-gray-400 font-medium">Run on days:</label>
|
|
|
|
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-plex-orange focus:ring-offset-2 focus:ring-offset-gray-900 ${localSchedule.mode === ScheduleMode.WEEKLY ? 'bg-plex-orange' : 'bg-gray-700'}`}
|
|
|
|
|
>
|
|
|
|
|
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${localSchedule.mode === ScheduleMode.WEEKLY ? 'translate-x-6' : 'translate-x-1'}`} />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Middle Row: Full Width Capsules */}
|
|
|
|
@@ -714,20 +833,22 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Auto Watch Checkbox */}
|
|
|
|
|
<div className="flex items-center mb-4 px-1">
|
|
|
|
|
{/* Auto Watch Switch */}
|
|
|
|
|
<div className="flex items-center justify-between mb-4 mt-2 px-1">
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
<div className="p-1.5 rounded-lg bg-orange-500/10 border border-orange-500/20 text-orange-400">
|
|
|
|
|
<Eye size={16} />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex flex-col">
|
|
|
|
|
<span className="text-sm font-medium text-gray-200">Watch Local Changes</span>
|
|
|
|
|
<span className="text-[10px] text-gray-500">Auto-sync when local playlist updates</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => handleUpdateSchedule('autoWatch', !localSchedule.autoWatch)}
|
|
|
|
|
className="flex items-center space-x-2 group"
|
|
|
|
|
onClick={() => handleUpdateSchedule('autoWatch', !localSchedule.autoWatch)}
|
|
|
|
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-plex-orange focus:ring-offset-2 focus:ring-offset-gray-900 ${localSchedule.autoWatch ? 'bg-plex-orange' : 'bg-gray-700'}`}
|
|
|
|
|
>
|
|
|
|
|
{localSchedule.autoWatch ? (
|
|
|
|
|
<CheckSquare size={16} className="text-plex-orange" />
|
|
|
|
|
) : (
|
|
|
|
|
<Square size={16} className="text-gray-600 group-hover:text-gray-400" />
|
|
|
|
|
)}
|
|
|
|
|
<span className={`text-xs ${localSchedule.autoWatch ? 'text-gray-200' : 'text-gray-500 group-hover:text-gray-400'}`}>
|
|
|
|
|
Watch for local playlist changes
|
|
|
|
|
</span>
|
|
|
|
|
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${localSchedule.autoWatch ? 'translate-x-6' : 'translate-x-1'}`} />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
@@ -767,7 +888,7 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
|
|
|
|
|
className={`w-full py-3 rounded-lg text-sm font-bold flex items-center justify-center gap-2 transition-all shadow-lg
|
|
|
|
|
${isLocked
|
|
|
|
|
? 'bg-gray-700/30 text-gray-500 cursor-not-allowed border border-gray-700/50'
|
|
|
|
|
: isMappingDirty
|
|
|
|
|
: isMappingDirty || isBackupDirty
|
|
|
|
|
? 'bg-gray-800 text-gray-500 cursor-not-allowed border border-gray-700'
|
|
|
|
|
: 'bg-green-600 hover:bg-green-500 text-white border border-green-500 shadow-green-900/20 active:scale-[0.98]'
|
|
|
|
|
}`}
|
|
|
|
@@ -784,9 +905,9 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
{(isMappingDirty) && (
|
|
|
|
|
{(isMappingDirty || isBackupDirty) && (
|
|
|
|
|
<p className="text-[10px] text-plex-orange text-center mt-2">
|
|
|
|
|
Please save path mapping changes before syncing.
|
|
|
|
|
Please save pending changes (Backups/Path Mapping) before syncing.
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|