Add sync controls and status header animations

This commit is contained in:
Koha9
2025-11-29 04:55:41 +09:00
parent 90294a29bf
commit b6408bf120
5 changed files with 353 additions and 57 deletions
+82 -31
View File
@@ -1,17 +1,19 @@
import React, { useState, useRef, useEffect } from 'react';
import { SyncStrategy, RegexReplacement } from '../types';
import {
ArrowRightCircle,
ArrowLeftCircle,
GitMerge,
import { SyncStrategy, RegexReplacement, SyncState } from '../types';
import {
ArrowRightCircle,
ArrowLeftCircle,
GitMerge,
ChevronDown,
Check,
HelpCircle,
Plus,
Trash2,
Save,
RotateCcw
RotateCcw,
Loader2,
Zap
} from 'lucide-react';
interface StrategyOption {
@@ -58,13 +60,17 @@ interface StrategySelectorProps {
onSelect: (strategy: SyncStrategy, label: string) => void;
savedRegexReplacements: RegexReplacement[];
onSaveRegex: (replacements: RegexReplacement[]) => void;
syncState: SyncState;
onSync: () => void;
}
const StrategySelector: React.FC<StrategySelectorProps> = ({
currentStrategy,
onSelect,
savedRegexReplacements,
onSaveRegex
const StrategySelector: React.FC<StrategySelectorProps> = ({
currentStrategy,
onSelect,
savedRegexReplacements,
onSaveRegex,
syncState,
onSync
}) => {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
@@ -73,6 +79,9 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
const [localReplacements, setLocalReplacements] = useState<RegexReplacement[]>([]);
const [isDirty, setIsDirty] = useState(false);
const isSyncing = syncState === SyncState.SYNCING;
const isLocked = isSyncing || syncState === SyncState.SUCCESS;
// Initialize local state when prop updates (only if not dirty, or initially)
useEffect(() => {
setLocalReplacements(JSON.parse(JSON.stringify(savedRegexReplacements)));
@@ -98,6 +107,7 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
}, []);
const handleSelect = (strategy: StrategyOption) => {
if (isLocked) return;
onSelect(strategy.value, strategy.label);
};
@@ -127,8 +137,13 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
onSaveRegex(validReplacements);
};
const handleSyncClick = () => {
if (isLocked || isDirty) return;
onSync();
};
return (
<div className="relative group" ref={dropdownRef}>
<div className={`relative group ${isLocked ? 'opacity-80' : ''}`} ref={dropdownRef}>
{/* Trigger Button - Added Ring to create visual 'cutout' over panels */}
<button
onClick={() => setIsOpen(!isOpen)}
@@ -216,12 +231,13 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
localReplacements.map((regex) => (
<div key={regex.id} className="flex items-center space-x-2 animate-in slide-in-from-left-2 duration-200">
<div className="flex-1 min-w-0">
<input
type="text"
placeholder="Regex Pattern"
<input
type="text"
placeholder="Regex Pattern"
value={regex.pattern}
onChange={(e) => handleUpdateRegex(regex.id, 'pattern', e.target.value)}
className={`w-full bg-gray-900/80 border rounded-md px-2.5 py-1.5 text-xs text-gray-200 focus:outline-none focus:ring-1 transition-all placeholder-gray-600
disabled={isLocked}
className={`w-full bg-gray-900/80 border rounded-md px-2.5 py-1.5 text-xs text-gray-200 focus:outline-none focus:ring-1 transition-all placeholder-gray-600 disabled:opacity-60 disabled:cursor-not-allowed
${!regex.pattern && isDirty ? 'border-red-500/30 focus:border-red-500' : 'border-gray-700 focus:border-plex-orange'}`}
/>
</div>
@@ -229,17 +245,19 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
<ArrowRightCircle size={12} />
</div>
<div className="flex-1 min-w-0">
<input
type="text"
placeholder="Replacement"
<input
type="text"
placeholder="Replacement"
value={regex.replacement}
onChange={(e) => handleUpdateRegex(regex.id, 'replacement', e.target.value)}
className="w-full bg-gray-900/80 border border-gray-700 rounded-md px-2.5 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"
disabled={isLocked}
className="w-full bg-gray-900/80 border border-gray-700 rounded-md px-2.5 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 disabled:opacity-60 disabled:cursor-not-allowed"
/>
</div>
<button
<button
onClick={() => handleDeleteRegex(regex.id)}
className="text-gray-600 hover:text-red-400 p-1.5 hover:bg-red-500/10 rounded transition-colors"
disabled={isLocked}
className="text-gray-600 hover:text-red-400 p-1.5 hover:bg-red-500/10 rounded transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
title="Delete Rule"
>
<Trash2 size={14} />
@@ -253,9 +271,10 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
<div className="space-y-3 pt-3 border-t border-white/5">
{localReplacements.length > 0 && (
<div className="flex justify-center">
<button
<button
onClick={handleAddRegex}
className="flex items-center space-x-1.5 text-xs text-plex-orange hover:text-yellow-400 transition-colors opacity-80 hover:opacity-100"
disabled={isLocked}
className="flex items-center space-x-1.5 text-xs text-plex-orange hover:text-yellow-400 transition-colors opacity-80 hover:opacity-100 disabled:opacity-40 disabled:cursor-not-allowed"
>
<Plus size={12} />
<span className="font-medium">Add Rule</span>
@@ -266,10 +285,10 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
<div className="grid grid-cols-2 gap-3">
<button
onClick={handleReset}
disabled={!isDirty}
disabled={!isDirty || isLocked}
className={`flex items-center justify-center space-x-2 py-1.5 rounded-lg text-xs font-medium border transition-all
${isDirty
? 'bg-gray-800 border-gray-600 text-gray-300 hover:bg-gray-700 hover:text-white'
${isDirty && !isLocked
? 'bg-gray-800 border-gray-600 text-gray-300 hover:bg-gray-700 hover:text-white'
: 'bg-transparent border-gray-800 text-gray-600 cursor-not-allowed'}`}
>
<RotateCcw size={14} />
@@ -277,18 +296,50 @@ const StrategySelector: React.FC<StrategySelectorProps> = ({
</button>
<button
onClick={handleSave}
disabled={!isDirty}
disabled={!isDirty || isLocked}
className={`flex items-center justify-center space-x-2 py-1.5 rounded-lg text-xs font-bold border transition-all
${isDirty
? 'bg-plex-orange border-plex-orange text-gray-900 hover:bg-yellow-500 shadow-lg shadow-plex-orange/10'
${isDirty && !isLocked
? 'bg-plex-orange border-plex-orange text-gray-900 hover:bg-yellow-500 shadow-lg shadow-plex-orange/10'
: 'bg-gray-800/50 border-gray-800 text-gray-600 cursor-not-allowed'}`}
>
<Save size={14} />
<span>Save Changes</span>
</button>
</div>
</div>
</div>
</div>
{/* Section 3: Sync Now Button */}
<div className="p-4 bg-gray-950/50 border-t border-white/5">
<button
onClick={handleSyncClick}
disabled={isLocked || isDirty}
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'
: isDirty
? '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]'
}`}
>
{isSyncing ? (
<>
<Loader2 size={16} className="animate-spin" />
<span>Sync in Progress...</span>
</>
) : (
<>
<Zap size={16} fill="currentColor" />
<span>Sync Now</span>
</>
)}
</button>
{isDirty && (
<p className="text-[10px] text-plex-orange text-center mt-2">
Please save or revert regex rules changes before syncing.
</p>
)}
</div>
</div>
</div>
);