## 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>
129 lines
5.2 KiB
TypeScript
129 lines
5.2 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { Bookmark } from '../types';
|
|
import { Bookmark as BookmarkIcon, Play, Trash2, Clock } from 'lucide-react';
|
|
import { getBookmarks, removeBookmark } from '../services/storageService';
|
|
|
|
interface BookmarksPanelProps {
|
|
onResumeArticle: (url: string, segmentIndex: number) => void;
|
|
onClose: () => void;
|
|
}
|
|
|
|
export const BookmarksPanel: React.FC<BookmarksPanelProps> = ({
|
|
onResumeArticle,
|
|
onClose
|
|
}) => {
|
|
const [bookmarks, setBookmarks] = useState<Bookmark[]>([]);
|
|
|
|
useEffect(() => {
|
|
setBookmarks(getBookmarks());
|
|
}, []);
|
|
|
|
const handleRemove = (url: string) => {
|
|
removeBookmark(url);
|
|
setBookmarks(prev => prev.filter(b => b.url !== url));
|
|
};
|
|
|
|
const formatDate = (timestamp: number): string => {
|
|
const date = new Date(timestamp);
|
|
const now = new Date();
|
|
const diff = now.getTime() - date.getTime();
|
|
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
|
|
|
if (days === 0) return 'Today';
|
|
if (days === 1) return 'Yesterday';
|
|
if (days < 7) return `${days} days ago`;
|
|
return date.toLocaleDateString('en', { month: 'short', day: 'numeric' });
|
|
};
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
|
<div className="bg-white dark:bg-slate-900 rounded-2xl shadow-2xl max-w-lg w-full max-h-[90vh] overflow-hidden flex flex-col">
|
|
{/* Header */}
|
|
<div className="p-6 border-b border-slate-200 dark:border-slate-800">
|
|
<div className="flex items-center justify-between">
|
|
<h2 className="text-xl font-bold text-slate-900 dark:text-white flex items-center gap-2">
|
|
<BookmarkIcon className="w-6 h-6 text-yellow-500" />
|
|
Bookmarks
|
|
</h2>
|
|
<button
|
|
onClick={onClose}
|
|
className="p-2 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-lg transition-colors"
|
|
>
|
|
<span className="text-slate-500 text-xl">×</span>
|
|
</button>
|
|
</div>
|
|
<p className="text-sm text-slate-500 dark:text-slate-400 mt-2">
|
|
Resume where you left off
|
|
</p>
|
|
</div>
|
|
|
|
{/* Bookmarks List */}
|
|
<div className="flex-grow overflow-y-auto p-4 space-y-3">
|
|
{bookmarks.length === 0 ? (
|
|
<div className="text-center py-12 text-slate-500 dark:text-slate-400">
|
|
<BookmarkIcon className="w-12 h-12 mx-auto mb-3 opacity-30" />
|
|
<p>No bookmarks yet</p>
|
|
<p className="text-sm mt-1">
|
|
Articles are automatically bookmarked when you pause
|
|
</p>
|
|
</div>
|
|
) : (
|
|
bookmarks.map((bookmark) => (
|
|
<div
|
|
key={bookmark.url}
|
|
className="bg-slate-50 dark:bg-slate-800/50 rounded-xl p-4 border border-slate-200 dark:border-slate-700"
|
|
>
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="flex-grow min-w-0">
|
|
<h3 className="font-medium text-slate-800 dark:text-slate-200 truncate">
|
|
{bookmark.title || 'Untitled'}
|
|
</h3>
|
|
<p className="text-xs text-slate-500 dark:text-slate-400 truncate mt-1">
|
|
{bookmark.url}
|
|
</p>
|
|
<div className="flex items-center gap-4 mt-2">
|
|
<span className="text-xs text-slate-500 dark:text-slate-400 flex items-center gap-1">
|
|
<Clock className="w-3 h-3" />
|
|
{formatDate(bookmark.savedAt)}
|
|
</span>
|
|
<span className="text-xs font-medium text-blue-500">
|
|
{bookmark.progress}% complete
|
|
</span>
|
|
</div>
|
|
{/* Progress bar */}
|
|
<div className="w-full h-1.5 bg-slate-200 dark:bg-slate-700 rounded-full mt-2 overflow-hidden">
|
|
<div
|
|
className="h-full bg-blue-500 rounded-full transition-all"
|
|
style={{ width: `${bookmark.progress}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-col gap-2">
|
|
<button
|
|
onClick={() => {
|
|
onResumeArticle(bookmark.url, bookmark.segmentIndex);
|
|
onClose();
|
|
}}
|
|
className="p-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-colors"
|
|
title="Resume"
|
|
>
|
|
<Play className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
onClick={() => handleRemove(bookmark.url)}
|
|
className="p-2 hover:bg-red-100 dark:hover:bg-red-900/30 text-red-500 rounded-lg transition-colors"
|
|
title="Remove"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|