Squashed 'sample-front-end/' changes from 0881bf1..601ffe4

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
This commit is contained in:
2025-11-28 22:43:39 +09:00
parent 4e91c2acdf
commit 5a29265854
6 changed files with 269 additions and 96 deletions
+43 -21
View File
@@ -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<ServerPanelProps> = ({ type, playlists, isLoading, onRefresh, serverInfo }) => {
const ServerPanel: React.FC<ServerPanelProps> = ({ type, playlists, isLoading, onRefresh, onCancel, serverInfo }) => {
const isLocal = type === ServerType.LOCAL;
let Icon = isLocal ? Server : Cloud;
@@ -65,21 +66,30 @@ const ServerPanel: React.FC<ServerPanelProps> = ({ type, playlists, isLoading, o
}
}
// 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 */}
{/* Mobile: Order Last (Right side), Vertical Text */}
{/* Desktop: Order First (Top side), Horizontal Text */}
<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
`}>
<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">
@@ -89,9 +99,8 @@ const ServerPanel: React.FC<ServerPanelProps> = ({ type, playlists, isLoading, o
<Icon size={22} strokeWidth={2} />
</div>
{/* Text Container - Vertical on Mobile, Horizontal on Desktop */}
{/* 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">
{/* Use vertical-rl for vertical text flow, rotate-180 to flip it bottom-up */}
<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}
@@ -103,14 +112,27 @@ const ServerPanel: React.FC<ServerPanelProps> = ({ type, playlists, isLoading, o
</div>
</div>
{/* Refresh Button */}
{/* Refresh/Stop Button */}
<button
onClick={onRefresh}
disabled={isLoading}
className="flex-shrink-0 p-2.5 text-gray-400 hover:text-white hover:bg-white/10 rounded-full transition-all active:scale-90 disabled:opacity-50 disabled:cursor-not-allowed mt-4 md:mt-0 md:ml-4"
title="Refresh Playlists"
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"}
>
<RefreshCw size={20} className={isLoading ? 'animate-spin text-plex-orange' : ''} strokeWidth={2} />
{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>