Files
Tony0410 9025d1b8f1 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>
2025-11-28 00:42:14 +00:00

185 lines
6.8 KiB
TypeScript

import { v4 as uuidv4 } from 'uuid';
import { RSSFeed, RSSArticle } from '../types';
import { saveRSSFeed, updateRSSFeedLastFetched } from './storageService';
// CORS proxy for RSS feeds
const RSS_PROXY = (url: string) => `https://api.allorigins.win/raw?url=${encodeURIComponent(url)}`;
/**
* Parse RSS/Atom feed XML into articles
*/
const parseRSSFeed = (xml: string, feedId: string): RSSArticle[] => {
const parser = new DOMParser();
const doc = parser.parseFromString(xml, 'text/xml');
const articles: RSSArticle[] = [];
// Try RSS 2.0 format
const items = doc.querySelectorAll('item');
if (items.length > 0) {
items.forEach(item => {
const title = item.querySelector('title')?.textContent || '';
const link = item.querySelector('link')?.textContent || '';
const description = item.querySelector('description')?.textContent || '';
const pubDate = item.querySelector('pubDate')?.textContent || '';
if (title && link) {
articles.push({
title: title.trim(),
url: link.trim(),
description: description.replace(/<[^>]*>/g, '').substring(0, 200),
pubDate,
feedId
});
}
});
return articles;
}
// Try Atom format
const entries = doc.querySelectorAll('entry');
entries.forEach(entry => {
const title = entry.querySelector('title')?.textContent || '';
const link = entry.querySelector('link')?.getAttribute('href') || '';
const summary = entry.querySelector('summary')?.textContent || '';
const published = entry.querySelector('published')?.textContent || '';
if (title && link) {
articles.push({
title: title.trim(),
url: link.trim(),
description: summary.replace(/<[^>]*>/g, '').substring(0, 200),
pubDate: published,
feedId
});
}
});
return articles;
};
/**
* Get feed title from XML
*/
const getFeedTitle = (xml: string): string => {
const parser = new DOMParser();
const doc = parser.parseFromString(xml, 'text/xml');
// RSS 2.0
const channelTitle = doc.querySelector('channel > title')?.textContent;
if (channelTitle) return channelTitle.trim();
// Atom
const feedTitle = doc.querySelector('feed > title')?.textContent;
if (feedTitle) return feedTitle.trim();
return 'Unknown Feed';
};
/**
* Fetch and parse RSS feed
*/
export const fetchRSSFeed = async (feedUrl: string): Promise<{
feed: RSSFeed;
articles: RSSArticle[];
}> => {
const response = await fetch(RSS_PROXY(feedUrl));
if (!response.ok) {
throw new Error(`Failed to fetch feed: ${response.status}`);
}
const xml = await response.text();
const feedId = uuidv4();
const title = getFeedTitle(xml);
const articles = parseRSSFeed(xml, feedId);
const feed: RSSFeed = {
id: feedId,
url: feedUrl,
title,
articleCount: articles.length,
lastFetched: Date.now(),
isActive: true,
addedAt: Date.now()
};
return { feed, articles };
};
/**
* Refresh articles from an existing feed
*/
export const refreshFeed = async (feed: RSSFeed): Promise<RSSArticle[]> => {
const response = await fetch(RSS_PROXY(feed.url));
if (!response.ok) {
throw new Error(`Failed to refresh feed: ${response.status}`);
}
const xml = await response.text();
const articles = parseRSSFeed(xml, feed.id);
updateRSSFeedLastFetched(feed.id);
return articles;
};
/**
* Validate RSS feed URL
*/
export const validateRSSUrl = async (url: string): Promise<boolean> => {
try {
const response = await fetch(RSS_PROXY(url));
if (!response.ok) return false;
const text = await response.text();
// Check if it looks like RSS/Atom
return text.includes('<rss') || text.includes('<feed') || text.includes('<channel');
} catch {
return false;
}
};
// Popular RSS feed suggestions organized by category
export const SUGGESTED_FEEDS = [
// General News
{ name: 'BBC News', url: 'https://feeds.bbci.co.uk/news/rss.xml', category: 'news' },
{ name: 'NPR News', url: 'https://feeds.npr.org/1001/rss.xml', category: 'news' },
{ name: 'Reuters', url: 'https://www.reutersagency.com/feed/', category: 'news' },
{ name: 'The Guardian', url: 'https://www.theguardian.com/world/rss', category: 'news' },
{ name: 'Associated Press', url: 'https://feeds.apnews.com/rss/topnews', category: 'news' },
{ name: 'CNN World', url: 'http://rss.cnn.com/rss/cnn_world.rss', category: 'news' },
{ name: 'Al Jazeera', url: 'https://www.aljazeera.com/xml/rss/all.xml', category: 'news' },
{ name: 'The New York Times', url: 'https://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml', category: 'news' },
{ name: 'Washington Post', url: 'https://feeds.washingtonpost.com/rss/world', category: 'news' },
// Technology
{ name: 'TechCrunch', url: 'https://techcrunch.com/feed/', category: 'tech' },
{ name: 'Hacker News', url: 'https://hnrss.org/frontpage', category: 'tech' },
{ name: 'Ars Technica', url: 'https://feeds.arstechnica.com/arstechnica/index', category: 'tech' },
{ name: 'The Verge', url: 'https://www.theverge.com/rss/index.xml', category: 'tech' },
{ name: 'Wired', url: 'https://www.wired.com/feed/rss', category: 'tech' },
{ name: 'MIT Technology Review', url: 'https://www.technologyreview.com/feed/', category: 'tech' },
{ name: 'Engadget', url: 'https://www.engadget.com/rss.xml', category: 'tech' },
{ name: 'ZDNet', url: 'https://www.zdnet.com/news/rss.xml', category: 'tech' },
// Business & Finance
{ name: 'Financial Times', url: 'https://www.ft.com/?format=rss', category: 'business' },
{ name: 'Bloomberg', url: 'https://feeds.bloomberg.com/markets/news.rss', category: 'business' },
{ name: 'The Economist', url: 'https://www.economist.com/finance-and-economics/rss.xml', category: 'business' },
{ name: 'Forbes', url: 'https://www.forbes.com/real-time/feed2/', category: 'business' },
{ name: 'MarketWatch', url: 'https://feeds.marketwatch.com/marketwatch/topstories/', category: 'business' },
// Science
{ name: 'Nature News', url: 'https://www.nature.com/nature.rss', category: 'science' },
{ name: 'Scientific American', url: 'https://www.scientificamerican.com/feed/', category: 'science' },
{ name: 'ScienceDaily', url: 'https://www.sciencedaily.com/rss/all.xml', category: 'science' },
{ name: 'Phys.org', url: 'https://phys.org/rss-feed/', category: 'science' },
{ name: 'Space.com', url: 'https://www.space.com/feeds/all', category: 'science' },
// International
{ name: 'The Japan Times', url: 'https://www.japantimes.co.jp/feed/', category: 'international' },
{ name: 'South China Morning Post', url: 'https://www.scmp.com/rss/91/feed', category: 'international' },
{ name: 'Deutsche Welle', url: 'https://rss.dw.com/rdf/rss-en-all', category: 'international' },
{ name: 'France 24', url: 'https://www.france24.com/en/rss', category: 'international' }
];