mirror of
https://github.com/Tony0410/News-reader-pro.git
synced 2026-05-24 21:31:44 +08:00
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`.
100 lines
3.8 KiB
TypeScript
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>
|
|
);
|
|
};
|