4e91c2acdf
git-subtree-dir: sample-front-end git-subtree-split: 0881bf1c045118585100360b2c47594cd94b89f1
327 lines
14 KiB
TypeScript
327 lines
14 KiB
TypeScript
|
|
import React, { useState, useEffect } from 'react';
|
|
import { PlexConnectionSettings, PlexServerConnection, PlexLibrary } from '../types';
|
|
import { apiService } from '../services/api';
|
|
import { X, Server, Lock, User, Key, Globe, Eye, EyeOff, CheckCircle, Library, ChevronDown } from 'lucide-react';
|
|
|
|
interface ConnectionModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onConnectSuccess: (serverInfo: PlexServerConnection) => void;
|
|
onShowMessage: (message: string) => void;
|
|
}
|
|
|
|
const ConnectionModal: React.FC<ConnectionModalProps> = ({ isOpen, onClose, onConnectSuccess, onShowMessage }) => {
|
|
const [formData, setFormData] = useState<PlexConnectionSettings>({
|
|
protocol: 'http',
|
|
address: '',
|
|
port: '32400',
|
|
token: '',
|
|
username: '',
|
|
password: ''
|
|
});
|
|
|
|
const [isConnecting, setIsConnecting] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
|
|
// Post-connection state
|
|
const [connectedServerInfo, setConnectedServerInfo] = useState<PlexServerConnection | null>(null);
|
|
const [libraries, setLibraries] = useState<PlexLibrary[]>([]);
|
|
const [selectedLibraryId, setSelectedLibraryId] = useState<string>('');
|
|
|
|
// Reset state when opening
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
setError(null);
|
|
setConnectedServerInfo(null);
|
|
setLibraries([]);
|
|
setSelectedLibraryId('');
|
|
}
|
|
}, [isOpen]);
|
|
|
|
if (!isOpen) return null;
|
|
|
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
|
const { name, value } = e.target;
|
|
setFormData(prev => ({ ...prev, [name]: value }));
|
|
};
|
|
|
|
const handleLibraryChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
const newId = e.target.value;
|
|
setSelectedLibraryId(newId);
|
|
|
|
const lib = libraries.find(l => l.id === newId);
|
|
if (lib && connectedServerInfo) {
|
|
const updatedInfo = { ...connectedServerInfo, libraryName: lib.title };
|
|
setConnectedServerInfo(updatedInfo);
|
|
// Notify parent of update
|
|
onConnectSuccess(updatedInfo);
|
|
// Show toast
|
|
onShowMessage(`Library switched to ${lib.title}`);
|
|
}
|
|
};
|
|
|
|
const isTokenProvided = formData.token.trim().length > 0;
|
|
|
|
// Dynamic styles for disabled fields
|
|
const disabledInputClass = isTokenProvided
|
|
? "bg-gray-700/50 text-gray-500 line-through decoration-gray-500 cursor-not-allowed border-gray-700"
|
|
: "bg-gray-800 text-gray-100 border-gray-600 focus:border-plex-orange focus:ring-1 focus:ring-plex-orange";
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setError(null);
|
|
setIsConnecting(true);
|
|
|
|
const result = await apiService.connectToPlex(formData);
|
|
|
|
setIsConnecting(false);
|
|
|
|
if (result.status === 'success' && result.data) {
|
|
// Update Token field and clear user/pass
|
|
setFormData(prev => ({
|
|
...prev,
|
|
token: result.data.token,
|
|
username: '',
|
|
password: ''
|
|
}));
|
|
|
|
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
|
|
});
|
|
} else {
|
|
onConnectSuccess(info);
|
|
}
|
|
} else {
|
|
setError(result.message || "Connection failed");
|
|
}
|
|
};
|
|
|
|
const isConnected = !!connectedServerInfo;
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
|
|
<div className="bg-gray-900 border border-gray-700 rounded-xl shadow-2xl w-full max-w-md overflow-hidden animate-in fade-in zoom-in duration-200">
|
|
|
|
{/* Header */}
|
|
<div className="px-6 py-4 bg-gray-800 border-b border-gray-700 flex items-center justify-between">
|
|
<h3 className="text-lg font-semibold text-white flex items-center gap-2">
|
|
<Server size={18} className={isConnected ? "text-green-400" : "text-plex-orange"} />
|
|
{isConnected ? 'Server Connected' : 'Connect Plex Server'}
|
|
</h3>
|
|
<button onClick={onClose} className="text-gray-400 hover:text-white transition-colors">
|
|
<X size={20} />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Body */}
|
|
<div className="p-6">
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
|
|
{error && (
|
|
<div className="p-3 bg-red-500/10 border border-red-500/20 text-red-400 text-xs rounded-md">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{/* Server Connection */}
|
|
<div className="space-y-3">
|
|
<label className="text-xs font-semibold text-gray-400 uppercase tracking-wider">Server Details</label>
|
|
<div className="grid grid-cols-4 gap-3">
|
|
<div className="col-span-1">
|
|
<select
|
|
name="protocol"
|
|
value={formData.protocol}
|
|
onChange={handleChange}
|
|
disabled={isConnected}
|
|
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' : ''}`}
|
|
>
|
|
<option value="http">HTTP</option>
|
|
<option value="https">HTTPS</option>
|
|
</select>
|
|
</div>
|
|
<div className="col-span-3">
|
|
<div className="relative">
|
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
<Globe size={14} className="text-gray-500" />
|
|
</div>
|
|
<input
|
|
type="text"
|
|
name="address"
|
|
required
|
|
disabled={isConnected}
|
|
placeholder="IP Address or Domain"
|
|
value={formData.address}
|
|
onChange={handleChange}
|
|
className={`w-full h-10 pl-9 pr-3 bg-gray-800 border border-gray-600 rounded-md text-sm text-white placeholder-gray-500 focus:border-plex-orange focus:outline-none focus:ring-1 focus:ring-plex-orange transition-all ${isConnected ? 'opacity-60 cursor-not-allowed' : ''}`}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<input
|
|
type="text"
|
|
name="port"
|
|
disabled={isConnected}
|
|
placeholder="Port (e.g. 32400)"
|
|
value={formData.port}
|
|
onChange={handleChange}
|
|
className={`w-full h-10 px-3 bg-gray-800 border border-gray-600 rounded-md text-sm text-white placeholder-gray-500 focus:border-plex-orange focus:outline-none focus:ring-1 focus:ring-plex-orange transition-all ${isConnected ? 'opacity-60 cursor-not-allowed' : ''}`}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="h-px bg-gray-800 my-4" />
|
|
|
|
{/* Authentication */}
|
|
<div className="space-y-3">
|
|
<label className="text-xs font-semibold text-gray-400 uppercase tracking-wider">Authentication</label>
|
|
|
|
{/* Token */}
|
|
<div className="relative">
|
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
<Key size={14} className="text-plex-orange" />
|
|
</div>
|
|
<input
|
|
type="text"
|
|
name="token"
|
|
disabled={isConnected}
|
|
placeholder="X-Plex-Token (Optional)"
|
|
value={formData.token}
|
|
onChange={handleChange}
|
|
className={`w-full h-10 pl-9 pr-3 bg-gray-800 border border-gray-600 rounded-md text-sm text-white placeholder-gray-500 focus:border-plex-orange focus:outline-none focus:ring-1 focus:ring-plex-orange transition-all font-mono ${isConnected ? 'opacity-60 cursor-not-allowed' : ''}`}
|
|
/>
|
|
</div>
|
|
|
|
{!isConnected && (
|
|
<>
|
|
<div className="text-center text-[10px] text-gray-500 uppercase tracking-widest font-semibold py-1">
|
|
— OR —
|
|
</div>
|
|
|
|
{/* Username */}
|
|
<div className="relative">
|
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
<User size={14} className={isTokenProvided ? "text-gray-600" : "text-gray-400"} />
|
|
</div>
|
|
<input
|
|
type="text"
|
|
name="username"
|
|
disabled={isTokenProvided}
|
|
placeholder="Username / Email"
|
|
value={formData.username}
|
|
onChange={handleChange}
|
|
className={`w-full h-10 pl-9 pr-3 rounded-md text-sm transition-all focus:outline-none ${disabledInputClass}`}
|
|
/>
|
|
</div>
|
|
|
|
{/* Password */}
|
|
<div className="relative">
|
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
<Lock size={14} className={isTokenProvided ? "text-gray-600" : "text-gray-400"} />
|
|
</div>
|
|
<input
|
|
type={showPassword ? "text" : "password"}
|
|
name="password"
|
|
disabled={isTokenProvided}
|
|
placeholder="Password"
|
|
value={formData.password}
|
|
onChange={handleChange}
|
|
className={`w-full h-10 pl-9 pr-10 rounded-md text-sm transition-all focus:outline-none ${disabledInputClass}`}
|
|
/>
|
|
<button
|
|
type="button"
|
|
disabled={isTokenProvided}
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
className={`absolute inset-y-0 right-0 pr-3 flex items-center ${isTokenProvided ? 'cursor-not-allowed opacity-50' : 'cursor-pointer text-gray-400 hover:text-white'}`}
|
|
>
|
|
{showPassword ? <EyeOff size={14} /> : <Eye size={14} />}
|
|
</button>
|
|
</div>
|
|
{isTokenProvided && (
|
|
<p className="text-[10px] text-yellow-500/80 italic text-center">
|
|
Credential login disabled when token is present.
|
|
</p>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{!isConnected ? (
|
|
<button
|
|
type="submit"
|
|
disabled={isConnecting}
|
|
className={`w-full mt-4 py-2.5 rounded-lg text-sm font-bold text-gray-900 transition-all shadow-lg
|
|
${isConnecting
|
|
? 'bg-gray-600 cursor-wait'
|
|
: 'bg-plex-orange hover:bg-yellow-500 active:scale-[0.98] shadow-plex-orange/20'
|
|
}`}
|
|
>
|
|
{isConnecting ? 'Connecting...' : 'Connect Server'}
|
|
</button>
|
|
) : (
|
|
<div className="mt-2 p-2 bg-green-500/10 border border-green-500/20 rounded-lg text-center">
|
|
<p className="text-green-400 text-sm font-semibold flex items-center justify-center gap-2">
|
|
<CheckCircle size={16} />
|
|
Connected Successfully
|
|
</p>
|
|
</div>
|
|
)}
|
|
</form>
|
|
|
|
{/* Library Selection - Appears after connection */}
|
|
{isConnected && libraries.length > 0 && (
|
|
<div className="mt-6 pt-5 border-t border-gray-700 animate-in slide-in-from-top-2 fade-in">
|
|
<label className="text-xs font-semibold text-gray-400 uppercase tracking-wider block mb-2">Select Library to Sync</label>
|
|
<div className="relative">
|
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
<Library size={14} className="text-plex-orange" />
|
|
</div>
|
|
<select
|
|
value={selectedLibraryId}
|
|
onChange={handleLibraryChange}
|
|
className="w-full h-10 pl-9 pr-3 bg-gray-800 border border-gray-600 rounded-md text-sm text-white focus:border-plex-orange focus:outline-none focus:ring-1 focus:ring-plex-orange appearance-none cursor-pointer hover:bg-gray-700/50 transition-colors"
|
|
>
|
|
{libraries.map(lib => (
|
|
<option key={lib.id} value={lib.id}>{lib.title}</option>
|
|
))}
|
|
</select>
|
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
|
<ChevronDown size={14} className="text-gray-500" />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-6 flex justify-end">
|
|
<button
|
|
onClick={onClose}
|
|
className="px-6 py-2 bg-gray-700 hover:bg-gray-600 text-white text-sm font-medium rounded-lg transition-colors border border-gray-600 hover:border-gray-500"
|
|
>
|
|
Done
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ConnectionModal;
|