Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 28b68fa9eb | |||
| 0667fac940 |
@@ -1,5 +1,7 @@
|
|||||||
|
|
||||||
|
|
||||||
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
||||||
import { Playlist, ServerType, SyncStrategy, PlexServerConnection, PathMappingConfig, PathMappingMode, SyncState, ScheduleSettings, ScheduleMode } from './types';
|
import { Playlist, ServerType, SyncStrategy, PlexServerConnection, PathMappingConfig, PathMappingMode, SyncState, ScheduleSettings, ScheduleMode, BackupSettings } from './types';
|
||||||
import { apiService } from './services/api';
|
import { apiService } from './services/api';
|
||||||
import {
|
import {
|
||||||
STRIPE_BASE_SPEED,
|
STRIPE_BASE_SPEED,
|
||||||
@@ -157,6 +159,12 @@ const App: React.FC = () => {
|
|||||||
autoWatch: false
|
autoWatch: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Backup State
|
||||||
|
const [backupSettings, setBackupSettings] = useState<BackupSettings>({
|
||||||
|
enabled: false,
|
||||||
|
retentionCount: 5
|
||||||
|
});
|
||||||
|
|
||||||
// Toast Notification System
|
// Toast Notification System
|
||||||
const [toasts, setToasts] = useState<Toast[]>([]);
|
const [toasts, setToasts] = useState<Toast[]>([]);
|
||||||
const timeoutsRef = useRef<{[key: number]: ReturnType<typeof setTimeout>}>({});
|
const timeoutsRef = useRef<{[key: number]: ReturnType<typeof setTimeout>}>({});
|
||||||
@@ -305,6 +313,17 @@ const App: React.FC = () => {
|
|||||||
addToast('Path mapping rules have been saved.');
|
addToast('Path mapping rules have been saved.');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle Backup Settings Save
|
||||||
|
const handleSaveBackupSettings = async (settings: BackupSettings) => {
|
||||||
|
const result = await apiService.saveBackupSettings(settings);
|
||||||
|
if (result.status === 'success') {
|
||||||
|
setBackupSettings(settings);
|
||||||
|
addToast('Backup settings have been saved.');
|
||||||
|
} else {
|
||||||
|
addToast('Failed to save backup settings.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Handle Schedule Save
|
// Handle Schedule Save
|
||||||
const handleSaveSchedule = async (settings: ScheduleSettings): Promise<boolean> => {
|
const handleSaveSchedule = async (settings: ScheduleSettings): Promise<boolean> => {
|
||||||
// Call API (validation happens in Mock)
|
// Call API (validation happens in Mock)
|
||||||
@@ -719,6 +738,8 @@ const App: React.FC = () => {
|
|||||||
onSelect={handleStrategyChange}
|
onSelect={handleStrategyChange}
|
||||||
savedPathMapping={pathMappingConfig}
|
savedPathMapping={pathMappingConfig}
|
||||||
onSavePathMapping={handleSavePathMapping}
|
onSavePathMapping={handleSavePathMapping}
|
||||||
|
savedBackup={backupSettings}
|
||||||
|
onSaveBackup={handleSaveBackupSettings}
|
||||||
savedSchedule={scheduleSettings}
|
savedSchedule={scheduleSettings}
|
||||||
onSaveSchedule={handleSaveSchedule}
|
onSaveSchedule={handleSaveSchedule}
|
||||||
syncState={syncState}
|
syncState={syncState}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useRef, useEffect } from 'react';
|
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 {
|
import {
|
||||||
ArrowRightCircle,
|
ArrowRightCircle,
|
||||||
ArrowLeftCircle,
|
ArrowLeftCircle,
|
||||||
@@ -16,10 +16,12 @@ import {
|
|||||||
Calendar,
|
Calendar,
|
||||||
Clock,
|
Clock,
|
||||||
Repeat,
|
Repeat,
|
||||||
CheckSquare,
|
|
||||||
Square,
|
|
||||||
Type,
|
Type,
|
||||||
Code2
|
Code2,
|
||||||
|
Link,
|
||||||
|
Archive,
|
||||||
|
History,
|
||||||
|
Eye
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
interface StrategyOption {
|
interface StrategyOption {
|
||||||
@@ -90,17 +92,12 @@ const MAPPING_THEME = {
|
|||||||
const deriveEffectiveSchedule = (schedule: ScheduleSettings, tab: ScheduleMode): ScheduleSettings => {
|
const deriveEffectiveSchedule = (schedule: ScheduleSettings, tab: ScheduleMode): ScheduleSettings => {
|
||||||
const derived = { ...schedule };
|
const derived = { ...schedule };
|
||||||
|
|
||||||
if (tab === ScheduleMode.CRON) {
|
// Unified logic: If the mode matches the tab, we keep it (Enabled).
|
||||||
derived.mode = derived.cronExpression.trim() !== '' ? ScheduleMode.CRON : ScheduleMode.DISABLED;
|
// 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 {
|
} else {
|
||||||
// For Daily/Weekly
|
derived.mode = ScheduleMode.DISABLED;
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return derived;
|
return derived;
|
||||||
};
|
};
|
||||||
@@ -186,7 +183,7 @@ const MappingGroupEditor: React.FC<MappingGroupEditorProps> = ({
|
|||||||
onChange={(e) => handleUpdate(rule.id, 'search', e.target.value)}
|
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}`}
|
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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={rightPlaceholder}
|
placeholder={rightPlaceholder}
|
||||||
@@ -213,6 +210,8 @@ interface StrategySelectorProps {
|
|||||||
onSelect: (strategy: SyncStrategy, label: string) => void;
|
onSelect: (strategy: SyncStrategy, label: string) => void;
|
||||||
savedPathMapping: PathMappingConfig;
|
savedPathMapping: PathMappingConfig;
|
||||||
onSavePathMapping: (config: PathMappingConfig) => void;
|
onSavePathMapping: (config: PathMappingConfig) => void;
|
||||||
|
savedBackup: BackupSettings;
|
||||||
|
onSaveBackup: (settings: BackupSettings) => void;
|
||||||
savedSchedule: ScheduleSettings;
|
savedSchedule: ScheduleSettings;
|
||||||
onSaveSchedule: (settings: ScheduleSettings) => Promise<boolean>;
|
onSaveSchedule: (settings: ScheduleSettings) => Promise<boolean>;
|
||||||
syncState: SyncState;
|
syncState: SyncState;
|
||||||
@@ -224,6 +223,8 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
|
|||||||
onSelect,
|
onSelect,
|
||||||
savedPathMapping,
|
savedPathMapping,
|
||||||
onSavePathMapping,
|
onSavePathMapping,
|
||||||
|
savedBackup,
|
||||||
|
onSaveBackup,
|
||||||
savedSchedule,
|
savedSchedule,
|
||||||
onSaveSchedule,
|
onSaveSchedule,
|
||||||
syncState,
|
syncState,
|
||||||
@@ -236,6 +237,10 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
|
|||||||
const [localPathMapping, setLocalPathMapping] = useState<PathMappingConfig>(savedPathMapping);
|
const [localPathMapping, setLocalPathMapping] = useState<PathMappingConfig>(savedPathMapping);
|
||||||
const [isMappingDirty, setIsMappingDirty] = useState(false);
|
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
|
// Local state for Schedule editing
|
||||||
const [localSchedule, setLocalSchedule] = useState<ScheduleSettings>(savedSchedule);
|
const [localSchedule, setLocalSchedule] = useState<ScheduleSettings>(savedSchedule);
|
||||||
const [isScheduleDirty, setIsScheduleDirty] = useState(false);
|
const [isScheduleDirty, setIsScheduleDirty] = useState(false);
|
||||||
@@ -254,6 +259,11 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
|
|||||||
setIsMappingDirty(false);
|
setIsMappingDirty(false);
|
||||||
}, [savedPathMapping]);
|
}, [savedPathMapping]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLocalBackup(JSON.parse(JSON.stringify(savedBackup)));
|
||||||
|
setIsBackupDirty(false);
|
||||||
|
}, [savedBackup]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLocalSchedule(JSON.parse(JSON.stringify(savedSchedule)));
|
setLocalSchedule(JSON.parse(JSON.stringify(savedSchedule)));
|
||||||
if (savedSchedule.mode !== ScheduleMode.DISABLED) {
|
if (savedSchedule.mode !== ScheduleMode.DISABLED) {
|
||||||
@@ -268,6 +278,12 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
|
|||||||
setIsMappingDirty(isDifferent);
|
setIsMappingDirty(isDifferent);
|
||||||
}, [localPathMapping, savedPathMapping]);
|
}, [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)
|
// Check dirty state for Schedule (including Active Tab changes)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const effectiveLocal = deriveEffectiveSchedule(localSchedule, activeScheduleTab);
|
const effectiveLocal = deriveEffectiveSchedule(localSchedule, activeScheduleTab);
|
||||||
@@ -351,6 +367,22 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
|
|||||||
const regexRules = localPathMapping.regex;
|
const regexRules = localPathMapping.regex;
|
||||||
const simpleRules = localPathMapping.simple;
|
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 ---
|
// --- Schedule Handlers ---
|
||||||
const handleUpdateSchedule = (field: keyof ScheduleSettings, value: any) => {
|
const handleUpdateSchedule = (field: keyof ScheduleSettings, value: any) => {
|
||||||
if (isLocked) return;
|
if (isLocked) return;
|
||||||
@@ -469,6 +501,80 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</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) */}
|
{/* Section 2: Path Mapping (Tabs + Grid) */}
|
||||||
<div className="p-4 bg-gray-900/40 border-b border-white/5 flex-none">
|
<div className="p-4 bg-gray-900/40 border-b border-white/5 flex-none">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
@@ -620,35 +726,49 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
|
|||||||
{/* Tab Content */}
|
{/* Tab Content */}
|
||||||
<div className="mb-4 min-h-[50px]">
|
<div className="mb-4 min-h-[50px]">
|
||||||
{activeScheduleTab === ScheduleMode.CRON && (
|
{activeScheduleTab === ScheduleMode.CRON && (
|
||||||
<div className="space-y-2 animate-in fade-in duration-200">
|
<div className="flex flex-col animate-in fade-in duration-200">
|
||||||
<div className="flex items-center space-x-2">
|
{/* Top Row: Label + Switch */}
|
||||||
<span className="text-gray-500 font-mono text-xs">Cron:</span>
|
<div className="flex items-center justify-between mb-3 px-1">
|
||||||
<input
|
<span className="text-xs text-gray-400 font-medium">Enable Cron Schedule</span>
|
||||||
type="text"
|
<button
|
||||||
value={localSchedule.cronExpression}
|
onClick={() => toggleScheduleEnable(ScheduleMode.CRON)}
|
||||||
onChange={(e) => handleUpdateSchedule('cronExpression', e.target.value)}
|
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'}`}
|
||||||
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"
|
<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>
|
</div>
|
||||||
<p className="text-[10px] text-gray-500">
|
|
||||||
Unix-cron format. Leave empty to disable schedule.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeScheduleTab === ScheduleMode.DAILY && (
|
{activeScheduleTab === ScheduleMode.DAILY && (
|
||||||
<div className="flex flex-col animate-in fade-in duration-200">
|
<div className="flex flex-col animate-in fade-in duration-200">
|
||||||
{/* Top Row: Checkbox + Label */}
|
{/* Top Row: Label + Switch */}
|
||||||
<div className="flex items-center justify-start space-x-2 mb-2">
|
<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
|
<button
|
||||||
onClick={() => toggleScheduleEnable(ScheduleMode.DAILY)}
|
onClick={() => toggleScheduleEnable(ScheduleMode.DAILY)}
|
||||||
className={`transition-colors flex-none ${localSchedule.mode === ScheduleMode.DAILY ? 'text-plex-orange' : 'text-gray-500 hover:text-gray-400'}`}
|
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'}`}
|
||||||
title={localSchedule.mode === ScheduleMode.DAILY ? "Schedule Enabled" : "Schedule Disabled"}
|
|
||||||
>
|
>
|
||||||
{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>
|
</button>
|
||||||
<label className="text-xs text-gray-400 font-medium">Run daily at:</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bottom Row: Centered Native Time Input */}
|
{/* Bottom Row: Centered Native Time Input */}
|
||||||
@@ -666,16 +786,15 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
|
|||||||
|
|
||||||
{activeScheduleTab === ScheduleMode.WEEKLY && (
|
{activeScheduleTab === ScheduleMode.WEEKLY && (
|
||||||
<div className="flex flex-col animate-in fade-in duration-200">
|
<div className="flex flex-col animate-in fade-in duration-200">
|
||||||
{/* Top Row: Checkbox + Label */}
|
{/* Top Row: Label + Switch */}
|
||||||
<div className="flex items-center justify-start space-x-2 mb-2">
|
<div className="flex items-center justify-between mb-3 px-1">
|
||||||
<button
|
<span className="text-xs text-gray-400 font-medium">Enable Weekly Run</span>
|
||||||
|
<button
|
||||||
onClick={() => toggleScheduleEnable(ScheduleMode.WEEKLY)}
|
onClick={() => toggleScheduleEnable(ScheduleMode.WEEKLY)}
|
||||||
className={`transition-colors flex-none ${localSchedule.mode === ScheduleMode.WEEKLY ? 'text-plex-orange' : 'text-gray-500 hover:text-gray-400'}`}
|
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'}`}
|
||||||
title={localSchedule.mode === ScheduleMode.WEEKLY ? "Schedule Enabled" : "Schedule Disabled"}
|
>
|
||||||
>
|
<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'}`} />
|
||||||
{localSchedule.mode === ScheduleMode.WEEKLY ? <CheckSquare size={16} /> : <Square size={16} />}
|
</button>
|
||||||
</button>
|
|
||||||
<label className="text-xs text-gray-400 font-medium">Run on days:</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Middle Row: Full Width Capsules */}
|
{/* Middle Row: Full Width Capsules */}
|
||||||
@@ -714,20 +833,22 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Auto Watch Checkbox */}
|
{/* Auto Watch Switch */}
|
||||||
<div className="flex items-center mb-4 px-1">
|
<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
|
<button
|
||||||
onClick={() => handleUpdateSchedule('autoWatch', !localSchedule.autoWatch)}
|
onClick={() => handleUpdateSchedule('autoWatch', !localSchedule.autoWatch)}
|
||||||
className="flex items-center space-x-2 group"
|
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 ? (
|
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${localSchedule.autoWatch ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||||
<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>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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
|
className={`w-full py-3 rounded-lg text-sm font-bold flex items-center justify-center gap-2 transition-all shadow-lg
|
||||||
${isLocked
|
${isLocked
|
||||||
? 'bg-gray-700/30 text-gray-500 cursor-not-allowed border border-gray-700/50'
|
? '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-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]'
|
: '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>
|
</button>
|
||||||
{(isMappingDirty) && (
|
{(isMappingDirty || isBackupDirty) && (
|
||||||
<p className="text-[10px] text-plex-orange text-center mt-2">
|
<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>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
import { Playlist, ServerType, ApiResponse, PlexServerConnection, PlexConnectionSettings, PlexLibrary, SyncStrategy, PathMappingConfig, ScheduleSettings, ScheduleMode } from '../types';
|
|
||||||
|
import { Playlist, ServerType, ApiResponse, PlexServerConnection, PlexConnectionSettings, PlexLibrary, SyncStrategy, PathMappingConfig, ScheduleSettings, ScheduleMode, BackupSettings } from '../types';
|
||||||
import { MOCK_LOCAL_PLAYLISTS, MOCK_CLOUD_PLAYLISTS } from './mockData';
|
import { MOCK_LOCAL_PLAYLISTS, MOCK_CLOUD_PLAYLISTS } from './mockData';
|
||||||
|
|
||||||
const SIMULATE_DELAY_MS = 800;
|
const SIMULATE_DELAY_MS = 800;
|
||||||
@@ -220,5 +221,13 @@ export const apiService = {
|
|||||||
resolve({ data: null, status: 'success', message: 'Schedule updated successfully' });
|
resolve({ data: null, status: 'success', message: 'Schedule updated successfully' });
|
||||||
}, 500);
|
}, 500);
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
saveBackupSettings: async (settings: BackupSettings): Promise<ApiResponse<null>> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve({ data: null, status: 'success', message: 'Backup settings saved' });
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -59,6 +59,11 @@ export interface PathMappingConfig {
|
|||||||
regex: PathMappingRules;
|
regex: PathMappingRules;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BackupSettings {
|
||||||
|
enabled: boolean;
|
||||||
|
retentionCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
export enum ScheduleMode {
|
export enum ScheduleMode {
|
||||||
DISABLED = 'DISABLED',
|
DISABLED = 'DISABLED',
|
||||||
CRON = 'CRON',
|
CRON = 'CRON',
|
||||||
|
|||||||
Reference in New Issue
Block a user