mirror of
https://github.com/Tony0410/News-reader-pro.git
synced 2026-05-24 21:31:44 +08:00
Major feature update: Enhanced RSS, voice fixes, dark mode, and UI improvements
## New Features - Article Summary Mode: AI-generated 30-second summaries with complexity analysis - Reading Stats Dashboard: Track articles read, listening time, and streaks - Bookmark/Resume: Auto-save progress when pausing, resume from where you left off - Audio Export: Export articles as downloadable WAV files - RSS Feed Manager: Subscribe to feeds with real-time validation and 31+ recommendations - Smart Speed: Auto-adjust playback based on article complexity - Voice Moods: Quick presets for different listening scenarios ## RSS Enhancements - Expanded recommendations from 8 to 31 sources across 5 categories: * General News (9 sources) * Technology (8 sources) * Business & Finance (5 sources) * Science & Research (5 sources) * International News (4 sources) - Real-time URL validation with visual feedback - Detailed error messages for different failure scenarios - Always-visible categorized recommendations - Auto-loading articles when feeds are added ## Bug Fixes - Fixed voice selection: Selected voice now consistently applies to playback - Implemented voice generation counter to prevent voice mixing between paragraphs - Fixed speed control to snap to clean 0.5 increments (1.0, 1.5, 2.0, etc.) - Fixed dark mode toggle by configuring Tailwind CDN for class-based dark mode - Removed vibe visualizer animation ## UI/UX Improvements - Redesigned voice selector with prominent voice panel and preview functionality - Added voice cards with emojis and descriptions - Enhanced feature toolbar with quick access to all new features - Improved reader view with better typography and reading modes - Added ambient reading modes (clean, sepia, night light) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
134
components/SummaryCard.tsx
Normal file
134
components/SummaryCard.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { Article, VoiceName } from '../types';
|
||||
import { Sparkles, Play, Pause, Loader2, Clock, Brain } from 'lucide-react';
|
||||
import { generateSpeechFromText } from '../services/geminiService';
|
||||
import { base64ToUint8Array, createWavBlob } from '../services/audioUtils';
|
||||
|
||||
interface SummaryCardProps {
|
||||
article: Article;
|
||||
selectedVoice: VoiceName;
|
||||
onPlayFull: () => void;
|
||||
}
|
||||
|
||||
export const SummaryCard: React.FC<SummaryCardProps> = ({
|
||||
article,
|
||||
selectedVoice,
|
||||
onPlayFull
|
||||
}) => {
|
||||
const [isPlayingSummary, setIsPlayingSummary] = useState(false);
|
||||
const [isLoadingAudio, setIsLoadingAudio] = useState(false);
|
||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||
|
||||
const handlePlaySummary = async () => {
|
||||
if (!article.summary) return;
|
||||
|
||||
if (isPlayingSummary && audioRef.current) {
|
||||
audioRef.current.pause();
|
||||
setIsPlayingSummary(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we already have the audio URL cached
|
||||
if (article.summaryAudioUrl && audioRef.current) {
|
||||
audioRef.current.src = article.summaryAudioUrl;
|
||||
await audioRef.current.play();
|
||||
setIsPlayingSummary(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate audio
|
||||
setIsLoadingAudio(true);
|
||||
try {
|
||||
const base64Audio = await generateSpeechFromText(article.summary, selectedVoice);
|
||||
const pcmData = base64ToUint8Array(base64Audio);
|
||||
const wavBlob = createWavBlob(pcmData);
|
||||
const audioUrl = URL.createObjectURL(wavBlob);
|
||||
|
||||
if (!audioRef.current) {
|
||||
audioRef.current = new Audio();
|
||||
}
|
||||
|
||||
audioRef.current.src = audioUrl;
|
||||
audioRef.current.onended = () => setIsPlayingSummary(false);
|
||||
await audioRef.current.play();
|
||||
setIsPlayingSummary(true);
|
||||
} catch (e) {
|
||||
console.error('Failed to play summary:', e);
|
||||
} finally {
|
||||
setIsLoadingAudio(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!article.summary && !article.isSummaryLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const complexityColors = {
|
||||
simple: 'text-green-500 bg-green-50 dark:bg-green-900/30',
|
||||
moderate: 'text-yellow-500 bg-yellow-50 dark:bg-yellow-900/30',
|
||||
complex: 'text-red-500 bg-red-50 dark:bg-red-900/30'
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-gradient-to-br from-indigo-50 to-purple-50 dark:from-indigo-900/20 dark:to-purple-900/20 rounded-2xl p-4 border border-indigo-100 dark:border-indigo-800/50">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className="w-4 h-4 text-indigo-500" />
|
||||
<span className="text-sm font-semibold text-indigo-700 dark:text-indigo-300">
|
||||
30-Second Summary
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{article.complexity && (
|
||||
<span className={`text-xs px-2 py-1 rounded-full flex items-center gap-1 ${complexityColors[article.complexity]}`}>
|
||||
<Brain className="w-3 h-3" />
|
||||
{article.complexity}
|
||||
</span>
|
||||
)}
|
||||
{article.estimatedReadTime && (
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400 flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
{article.estimatedReadTime}m read
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{article.isSummaryLoading ? (
|
||||
<div className="flex items-center gap-2 text-slate-500 dark:text-slate-400 py-4">
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
<span className="text-sm">Generating summary...</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm text-slate-700 dark:text-slate-300 leading-relaxed">
|
||||
{article.summary}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center gap-3 mt-4">
|
||||
<button
|
||||
onClick={handlePlaySummary}
|
||||
disabled={isLoadingAudio}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-indigo-500 hover:bg-indigo-600 text-white rounded-xl text-sm font-medium transition-all disabled:opacity-50"
|
||||
>
|
||||
{isLoadingAudio ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : isPlayingSummary ? (
|
||||
<Pause className="w-4 h-4" />
|
||||
) : (
|
||||
<Play className="w-4 h-4" />
|
||||
)}
|
||||
{isPlayingSummary ? 'Pause' : 'Listen to Summary'}
|
||||
</button>
|
||||
<button
|
||||
onClick={onPlayFull}
|
||||
className="text-sm text-indigo-600 dark:text-indigo-400 hover:underline"
|
||||
>
|
||||
Play full article →
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user