Files
PlexPlaylistSync/frontend/components/ServerPanel.tsx
T
2025-11-29 00:25:45 +09:00

163 lines
6.1 KiB
TypeScript

import React from 'react';
import { Playlist, ServerType, PlexServerConnection } from '../types';
import PlaylistCard from './PlaylistCard';
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<ServerPanelProps> = ({ type, playlists, isLoading, onRefresh, onCancel, serverInfo }) => {
const isLocal = type === ServerType.LOCAL;
let Icon = isLocal ? Server : Cloud;
let headerColor = isLocal ? 'text-blue-400' : 'text-green-400';
const borderColor = isLocal ? 'border-blue-500/30' : 'border-green-500/30';
const bgGradient = isLocal
? 'bg-gradient-to-br from-gray-800/80 to-gray-900/80'
: 'bg-gradient-to-bl from-gray-800/80 to-gray-900/80';
// Resolve Title and Subtitle Logic
let displayTitle = '';
let displaySubtitle: React.ReactNode = null;
if (isLocal) {
displayTitle = 'Local Server';
displaySubtitle = (
<p className="text-xs text-gray-400 font-medium mt-0.5 md:mt-0 md:ml-0">
{playlists.length} Playlists
</p>
);
} else {
// Cloud Logic
if (serverInfo) {
if (serverInfo.isConnected) {
displayTitle = serverInfo.name || 'Cloud Server';
displaySubtitle = (
<div className="flex items-center text-xs text-gray-300 font-medium space-x-1.5 truncate mt-0.5 md:mt-0">
<span className="text-plex-orange truncate font-semibold">{serverInfo.libraryName}</span>
<span className="text-gray-600 hidden md:inline"></span>
<span className="text-gray-500 font-mono text-[10px] hidden md:inline">{serverInfo.ip}:{serverInfo.port}</span>
</div>
);
} else {
displayTitle = 'Not Connected';
Icon = WifiOff;
headerColor = 'text-red-400';
displaySubtitle = (
<p className="text-xs text-gray-500 font-medium mt-0.5">
Connection failed
</p>
);
}
} else {
displayTitle = 'Cloud Server';
displaySubtitle = (
<p className="text-xs text-gray-500 font-medium mt-0.5">
{isLoading ? 'Connecting...' : 'Waiting...'}
</p>
);
}
}
// Handle Refresh/Cancel Click
const handleAction = () => {
if (isLoading && onCancel) {
onCancel();
} else {
onRefresh();
}
};
return (
<div className={`flex flex-row md:flex-col h-full ${bgGradient} rounded-2xl border ${borderColor} backdrop-blur-xl shadow-xl overflow-hidden transition-all duration-300`}>
{/* Header */}
<div
className={`
relative flex-none
order-last md:order-first
w-[72px] md:w-full
h-full md:h-auto md:min-h-[80px]
flex flex-col md:flex-row items-center justify-between
py-6 md:py-0 md:px-8
bg-gray-800/60 border-l md:border-l-0 md:border-b border-white/5
`}
>
{/* Title Group */}
<div className="flex flex-col md:flex-row items-center md:space-x-4 overflow-hidden w-full md:w-auto h-full md:h-full md:py-4">
{/* Icon Box */}
<div className={`p-2.5 rounded-xl bg-gray-900/50 border border-white/5 ${headerColor} shadow-inner flex-shrink-0 mb-4 md:mb-0`}>
<Icon size={22} strokeWidth={2} />
</div>
{/* Text Container */}
<div className="flex-1 min-w-0 flex flex-col justify-center items-center md:items-start h-full md:h-auto w-full md:w-auto">
<div className="flex flex-col justify-center w-full md:w-auto [writing-mode:vertical-rl] rotate-180 md:[writing-mode:horizontal-tb] md:rotate-0 items-center md:items-start gap-1 md:gap-0">
<h2 className="text-sm md:text-lg font-bold text-gray-100 tracking-wide whitespace-nowrap" title={displayTitle}>
{displayTitle}
</h2>
<div className="transform md:translate-y-0">
{displaySubtitle}
</div>
</div>
</div>
</div>
{/* Refresh/Stop Button */}
<button
onClick={handleAction}
className={`flex-shrink-0 p-2.5 rounded-full transition-all active:scale-90 mt-4 md:mt-0 md:ml-4 border border-transparent group relative
${isLoading
? 'text-plex-orange bg-plex-orange/10 border-plex-orange/20 hover:bg-red-500/10 hover:border-red-500/30'
: 'text-gray-400 hover:text-white hover:bg-white/10'
}
`}
title={isLoading ? "Cancel Refresh" : "Refresh Playlists"}
>
{isLoading ? (
<div className="relative flex items-center justify-center">
{/* Outer Spinner */}
<RefreshCw size={20} strokeWidth={2} className="animate-spin opacity-40 group-hover:opacity-20 transition-opacity" />
{/* Inner Cancel X */}
<X size={12} strokeWidth={3} className="absolute text-plex-orange group-hover:text-red-400 transition-colors" />
</div>
) : (
<RefreshCw size={20} strokeWidth={2} />
)}
</button>
</div>
{/* Content List */}
<div className="flex-1 overflow-y-auto p-3 md:p-5 custom-scrollbar bg-black/20">
{isLoading && playlists.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-gray-500 space-y-3">
<RefreshCw size={24} className="animate-spin text-plex-orange/50" />
<p className="text-xs font-medium tracking-wide uppercase">Syncing...</p>
</div>
) : playlists.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-gray-500">
<p className="text-sm">No playlists found.</p>
</div>
) : (
<div className="space-y-2.5 md:space-y-3">
{playlists.map((playlist) => (
<PlaylistCard key={playlist.id} playlist={playlist} />
))}
</div>
)}
</div>
</div>
);
};
export default ServerPanel;