mirror of
https://github.com/Tony0410/News-reader-pro.git
synced 2026-05-24 13:21:40 +08:00
## 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>
185 lines
6.8 KiB
TypeScript
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' }
|
|
];
|