diff --git a/App.tsx b/App.tsx index d05aef7..a0e94d2 100644 --- a/App.tsx +++ b/App.tsx @@ -22,6 +22,10 @@ const App: React.FC = () => { const [loadingLocal, setLoadingLocal] = useState(false); const [loadingCloud, setLoadingCloud] = useState(false); + // Abort Controllers for Refresh Actions + const localAbortRef = useRef(null); + const cloudAbortRef = useRef(null); + // Connection Modal State const [isConnectionModalOpen, setIsConnectionModalOpen] = useState(false); @@ -100,36 +104,71 @@ const App: React.FC = () => { // Fetch Local Playlists const refreshLocal = useCallback(async () => { + if (localAbortRef.current) localAbortRef.current.abort(); + const abortController = new AbortController(); + localAbortRef.current = abortController; + setLoadingLocal(true); - const result = await apiService.getPlaylists(ServerType.LOCAL); + const result = await apiService.getPlaylists(ServerType.LOCAL, abortController.signal); if (result.status === 'success') { setLocalPlaylists(result.data); } setLoadingLocal(false); + localAbortRef.current = null; }, []); + const cancelLocalRefresh = () => { + if (localAbortRef.current) { + localAbortRef.current.abort(); + localAbortRef.current = null; + setLoadingLocal(false); + addToast("Local refresh cancelled."); + } + }; + // Fetch Cloud Playlists and Info const refreshCloud = useCallback(async () => { + if (cloudAbortRef.current) cloudAbortRef.current.abort(); + const abortController = new AbortController(); + cloudAbortRef.current = abortController; + setLoadingCloud(true); // Fetch playlists - const playlistResult = await apiService.getPlaylists(ServerType.CLOUD); - if (playlistResult.status === 'success') { - setCloudPlaylists(playlistResult.data); + const playlistResult = await apiService.getPlaylists(ServerType.CLOUD, abortController.signal); + if (!abortController.signal.aborted) { + if (playlistResult.status === 'success') { + setCloudPlaylists(playlistResult.data); + } + + // Fetch server info + const infoResult = await apiService.getServerStatus(abortController.signal); + if (infoResult.status === 'success') { + setCloudServerInfo(infoResult.data); + } + + setLoadingCloud(false); + cloudAbortRef.current = null; } - - // Fetch server info - const infoResult = await apiService.getServerStatus(); - if (infoResult.status === 'success') { - setCloudServerInfo(infoResult.data); - } - - setLoadingCloud(false); }, []); + const cancelCloudRefresh = () => { + if (cloudAbortRef.current) { + cloudAbortRef.current.abort(); + cloudAbortRef.current = null; + setLoadingCloud(false); + addToast("Cloud refresh cancelled."); + } + }; + // Initial Load useEffect(() => { refreshLocal(); refreshCloud(); + return () => { + // Cleanup on unmount + if (localAbortRef.current) localAbortRef.current.abort(); + if (cloudAbortRef.current) cloudAbortRef.current.abort(); + } }, [refreshLocal, refreshCloud]); // Handle Strategy Change @@ -146,8 +185,8 @@ const App: React.FC = () => { const handleConnectSuccess = (serverInfo: PlexServerConnection) => { setCloudServerInfo(serverInfo); - // Removed implicit toast here to allow the caller (ConnectionModal) to handle specific messaging - refreshCloud(); // Refresh playlists after new connection + // Refresh playlists after new connection + refreshCloud(); }; const getToastStyles = (toast: Toast): React.CSSProperties => { @@ -221,7 +260,8 @@ const App: React.FC = () => { {/* Main Content Area */}
-
+ {/* Reduced gap from gap-3/gap-6 to gap-2/gap-3 for tighter layout */} +
{/* Left Column - Local */}
@@ -230,12 +270,11 @@ const App: React.FC = () => { playlists={localPlaylists} isLoading={loadingLocal} onRefresh={refreshLocal} + onCancel={cancelLocalRefresh} />
{/* Strategy Selector - Positioned specifically between headers */} - {/* Desktop: Centered Horizontally, Top aligned with Headers (Padding Top 24px + Header Half Height ~40px = ~64px) */} - {/* Mobile: Centered Vertically, Right aligned with Headers (Padding Right 16px + Header Half Width ~36px = ~52px) */}
{ e.preventDefault(); + + // If already connecting, this acts as Cancel + if (isConnecting) { + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + abortControllerRef.current = null; + setIsConnecting(false); + setError("Connection cancelled by user."); + } + return; + } + setError(null); setIsConnecting(true); - const result = await apiService.connectToPlex(formData); + const abortController = new AbortController(); + abortControllerRef.current = abortController; + + const result = await apiService.connectToPlex(formData, abortController.signal); + + // Only proceed if we weren't aborted/cancelled (though apiService handles error msg) + if (abortController.signal.aborted) return; setIsConnecting(false); + abortControllerRef.current = null; if (result.status === 'success' && result.data) { - // Update Token field and clear user/pass setFormData(prev => ({ ...prev, token: result.data.token, @@ -89,17 +119,13 @@ const ConnectionModal: React.FC = ({ isOpen, onClose, onCo const info = result.data.serverInfo; setConnectedServerInfo(info); - - // Explicitly show connection message here onShowMessage(`Successfully connected to ${info.name || 'Plex Server'}`); - // Handle libraries const libs = info.libraries || []; setLibraries(libs); if (libs.length > 0) { const defaultLib = libs[0]; setSelectedLibraryId(defaultLib.id); - // Pass connection info back with default library name explicitly set (though mock already does it) onConnectSuccess({ ...info, libraryName: defaultLib.title @@ -116,10 +142,10 @@ const ConnectionModal: React.FC = ({ isOpen, onClose, onCo return (
-
+
{/* Header */} -
+

{isConnected ? 'Server Connected' : 'Connect Plex Server'} @@ -130,7 +156,7 @@ const ConnectionModal: React.FC = ({ isOpen, onClose, onCo

{/* Body */} -
+
{error && ( @@ -148,7 +174,7 @@ const ConnectionModal: React.FC = ({ isOpen, onClose, onCo name="protocol" value={formData.protocol} onChange={handleChange} - disabled={isConnected} + disabled={isConnected || isConnecting} className={`w-full h-10 px-2 bg-gray-800 border border-gray-600 rounded-md text-sm text-white focus:border-plex-orange focus:outline-none ${isConnected ? 'opacity-60 cursor-not-allowed' : ''}`} > @@ -164,7 +190,7 @@ const ConnectionModal: React.FC = ({ isOpen, onClose, onCo type="text" name="address" required - disabled={isConnected} + disabled={isConnected || isConnecting} placeholder="IP Address or Domain" value={formData.address} onChange={handleChange} @@ -178,7 +204,7 @@ const ConnectionModal: React.FC = ({ isOpen, onClose, onCo = ({ isOpen, onClose, onCo = ({ isOpen, onClose, onCo = ({ isOpen, onClose, onCo = ({ isOpen, onClose, onCo />
- {isTokenProvided && ( -

- Credential login disabled when token is present. -

- )} )}
+ {/* Advanced Options */} + {!isConnected && ( +
+ + + {showAdvanced && ( +
+
+ + +
+
+ )} +
+ )} + {!isConnected ? ( ) : (
diff --git a/components/ServerPanel.tsx b/components/ServerPanel.tsx index 1c6e08e..c9b7ab3 100644 --- a/components/ServerPanel.tsx +++ b/components/ServerPanel.tsx @@ -2,17 +2,18 @@ import React from 'react'; import { Playlist, ServerType, PlexServerConnection } from '../types'; import PlaylistCard from './PlaylistCard'; -import { RefreshCw, Server, Cloud, WifiOff } from 'lucide-react'; +import { RefreshCw, Server, Cloud, WifiOff, X } from 'lucide-react'; interface ServerPanelProps { type: ServerType; playlists: Playlist[]; isLoading: boolean; onRefresh: () => void; + onCancel?: () => void; serverInfo?: PlexServerConnection; } -const ServerPanel: React.FC = ({ type, playlists, isLoading, onRefresh, serverInfo }) => { +const ServerPanel: React.FC = ({ type, playlists, isLoading, onRefresh, onCancel, serverInfo }) => { const isLocal = type === ServerType.LOCAL; let Icon = isLocal ? Server : Cloud; @@ -65,21 +66,30 @@ const ServerPanel: React.FC = ({ type, playlists, isLoading, o } } + // Handle Refresh/Cancel Click + const handleAction = () => { + if (isLoading && onCancel) { + onCancel(); + } else { + onRefresh(); + } + }; + return (
{/* Header */} - {/* Mobile: Order Last (Right side), Vertical Text */} - {/* Desktop: Order First (Top side), Horizontal Text */} -
+
{/* Title Group */}
@@ -89,9 +99,8 @@ const ServerPanel: React.FC = ({ type, playlists, isLoading, o
- {/* Text Container - Vertical on Mobile, Horizontal on Desktop */} + {/* Text Container */}
- {/* Use vertical-rl for vertical text flow, rotate-180 to flip it bottom-up */}

{displayTitle} @@ -103,14 +112,27 @@ const ServerPanel: React.FC = ({ type, playlists, isLoading, o

- {/* Refresh Button */} + {/* Refresh/Stop Button */}
diff --git a/components/StrategySelector.tsx b/components/StrategySelector.tsx index 5830e3c..12c80f9 100644 --- a/components/StrategySelector.tsx +++ b/components/StrategySelector.tsx @@ -129,10 +129,10 @@ const StrategySelector: React.FC = ({ return (
- {/* Trigger Button */} + {/* Trigger Button - Added Ring to create visual 'cutout' over panels */}