Files
News-reader-pro/components/QueueItem.tsx
Anthony 0b10d71554 feat: Add buffering indicator and improve queue item removal
Introduces a visual indicator for when the reader is buffering, meaning it's supposed to be playing but the current audio segment is not yet available.

Also, prevents accidental article selection when clicking the "Remove" button in the queue by adding `stopPropagation`.
2025-11-19 20:36:39 +08:00

100 lines
3.8 KiB
TypeScript

import React from 'react';
import { Article, PlaybackStatus } from '../types';
import { Play, Pause, Loader2, AlertCircle, FileText } from 'lucide-react';
interface QueueItemProps {
article: Article;
isActive: boolean;
isPlaying: boolean;
onPlay: () => void;
onPause: () => void;
onRemove: () => void;
}
export const QueueItem: React.FC<QueueItemProps> = ({
article,
isActive,
isPlaying,
onPlay,
onPause,
onRemove
}) => {
// Check if buffering: active, supposed to be playing, but current segment audio is missing
const isBuffering = isActive && isPlaying &&
article.segments[article.currentSegmentIndex] &&
!article.segments[article.currentSegmentIndex].audioUrl;
const getStatusIcon = () => {
if (isBuffering) {
return <Loader2 className="w-5 h-5 animate-spin text-blue-500" />;
}
switch (article.status) {
case PlaybackStatus.LOADING_TEXT:
return <Loader2 className="w-5 h-5 animate-spin text-blue-500" />;
case PlaybackStatus.LOADING_AUDIO:
return <Loader2 className="w-5 h-5 animate-spin text-purple-500" />;
case PlaybackStatus.ERROR:
return <AlertCircle className="w-5 h-5 text-red-500" />;
case PlaybackStatus.PLAYING:
return <div className="w-4 h-4 flex items-end space-x-0.5 h-4 overflow-hidden">
<div className="w-1 bg-blue-500 animate-[bounce_1s_infinite] h-2"></div>
<div className="w-1 bg-blue-500 animate-[bounce_1.2s_infinite] h-4"></div>
<div className="w-1 bg-blue-500 animate-[bounce_0.8s_infinite] h-3"></div>
</div>;
default:
return <FileText className="w-5 h-5 text-slate-400 dark:text-slate-500" />;
}
};
const isReady = article.status === PlaybackStatus.READY || article.status === PlaybackStatus.PAUSED || article.status === PlaybackStatus.PLAYING || article.status === PlaybackStatus.COMPLETED;
return (
<div className={`
relative group flex items-center p-4 rounded-xl border transition-all duration-200
${isActive
? 'bg-blue-50 border-blue-200 dark:bg-blue-900/20 dark:border-blue-800 shadow-sm'
: 'bg-white border-slate-100 hover:border-slate-300 dark:bg-slate-800 dark:border-slate-700 dark:hover:border-slate-600'
}
`}>
<div className="flex-shrink-0 mr-4 w-8 flex justify-center">
{getStatusIcon()}
</div>
<div className="flex-grow min-w-0">
<h3 className={`font-medium truncate ${isActive ? 'text-blue-900 dark:text-blue-300' : 'text-slate-900 dark:text-slate-200'}`}>
{article.title || article.url}
</h3>
<p className="text-xs text-slate-500 dark:text-slate-400 truncate mt-0.5">
{article.url}
</p>
{article.errorMessage && (
<p className="text-xs text-red-500 mt-1">{article.errorMessage}</p>
)}
</div>
<div className="flex-shrink-0 ml-4 flex items-center space-x-2 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity">
{isReady && (
<button
onClick={isActive && isPlaying ? onPause : onPlay}
className="p-2 rounded-full bg-slate-100 hover:bg-blue-100 dark:bg-slate-700 dark:hover:bg-blue-900/50 text-slate-700 dark:text-slate-200 hover:text-blue-700 dark:hover:text-blue-400 transition-colors"
>
{isActive && isPlaying ? <Pause className="w-4 h-4" /> : <Play className="w-4 h-4" />}
</button>
)}
<button
onClick={(e) => {
e.stopPropagation(); // Prevent article selection when removing
onRemove();
}}
className="text-xs text-slate-400 hover:text-red-500 dark:text-slate-500 dark:hover:text-red-400 underline px-2"
>
Remove
</button>
</div>
</div>
);
};