5a29265854
601ffe4 fix: Refine UI layout and visual elements 4689aaa feat: Add request timeout and cancellation to API calls git-subtree-dir: sample-front-end git-subtree-split: 601ffe468a78955839eef6c839314d9b96ea204d
163 lines
6.1 KiB
TypeScript
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;
|