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>
214 lines
5.6 KiB
TypeScript
214 lines
5.6 KiB
TypeScript
import { ReadingStats, Bookmark, RSSFeed, VoiceName } from '../types';
|
|
|
|
const STORAGE_KEYS = {
|
|
STATS: 'newscaster_stats',
|
|
BOOKMARKS: 'newscaster_bookmarks',
|
|
RSS_FEEDS: 'newscaster_rss_feeds',
|
|
LAST_PLAYED: 'newscaster_last_played'
|
|
};
|
|
|
|
// Default stats
|
|
const DEFAULT_STATS: ReadingStats = {
|
|
totalArticlesRead: 0,
|
|
totalMinutesListened: 0,
|
|
totalWordsRead: 0,
|
|
currentStreak: 0,
|
|
longestStreak: 0,
|
|
lastReadDate: '',
|
|
articlesPerDay: {},
|
|
favoriteVoice: VoiceName.Puck,
|
|
voiceUsage: {
|
|
[VoiceName.Puck]: 0,
|
|
[VoiceName.Charon]: 0,
|
|
[VoiceName.Kore]: 0,
|
|
[VoiceName.Fenrir]: 0,
|
|
[VoiceName.Zephyr]: 0,
|
|
[VoiceName.Aoede]: 0
|
|
}
|
|
};
|
|
|
|
// ==================== STATS ====================
|
|
|
|
export const getStats = (): ReadingStats => {
|
|
try {
|
|
const stored = localStorage.getItem(STORAGE_KEYS.STATS);
|
|
if (stored) {
|
|
return { ...DEFAULT_STATS, ...JSON.parse(stored) };
|
|
}
|
|
} catch (e) {
|
|
console.warn('Failed to load stats:', e);
|
|
}
|
|
return DEFAULT_STATS;
|
|
};
|
|
|
|
export const saveStats = (stats: ReadingStats): void => {
|
|
try {
|
|
localStorage.setItem(STORAGE_KEYS.STATS, JSON.stringify(stats));
|
|
} catch (e) {
|
|
console.warn('Failed to save stats:', e);
|
|
}
|
|
};
|
|
|
|
export const updateStatsOnArticleComplete = (
|
|
wordCount: number,
|
|
minutesListened: number,
|
|
voice: VoiceName
|
|
): ReadingStats => {
|
|
const stats = getStats();
|
|
const today = new Date().toISOString().split('T')[0];
|
|
|
|
// Update totals
|
|
stats.totalArticlesRead += 1;
|
|
stats.totalMinutesListened += minutesListened;
|
|
stats.totalWordsRead += wordCount;
|
|
|
|
// Update daily count
|
|
stats.articlesPerDay[today] = (stats.articlesPerDay[today] || 0) + 1;
|
|
|
|
// Update streak
|
|
const yesterday = new Date(Date.now() - 86400000).toISOString().split('T')[0];
|
|
if (stats.lastReadDate === yesterday) {
|
|
stats.currentStreak += 1;
|
|
} else if (stats.lastReadDate !== today) {
|
|
stats.currentStreak = 1;
|
|
}
|
|
stats.longestStreak = Math.max(stats.longestStreak, stats.currentStreak);
|
|
stats.lastReadDate = today;
|
|
|
|
// Update voice usage
|
|
stats.voiceUsage[voice] = (stats.voiceUsage[voice] || 0) + 1;
|
|
|
|
// Determine favorite voice
|
|
let maxUsage = 0;
|
|
Object.entries(stats.voiceUsage).forEach(([v, count]) => {
|
|
if (count > maxUsage) {
|
|
maxUsage = count;
|
|
stats.favoriteVoice = v as VoiceName;
|
|
}
|
|
});
|
|
|
|
saveStats(stats);
|
|
return stats;
|
|
};
|
|
|
|
// ==================== BOOKMARKS ====================
|
|
|
|
export const getBookmarks = (): Bookmark[] => {
|
|
try {
|
|
const stored = localStorage.getItem(STORAGE_KEYS.BOOKMARKS);
|
|
if (stored) {
|
|
return JSON.parse(stored);
|
|
}
|
|
} catch (e) {
|
|
console.warn('Failed to load bookmarks:', e);
|
|
}
|
|
return [];
|
|
};
|
|
|
|
export const saveBookmark = (bookmark: Bookmark): void => {
|
|
try {
|
|
const bookmarks = getBookmarks();
|
|
// Remove existing bookmark for same URL
|
|
const filtered = bookmarks.filter(b => b.url !== bookmark.url);
|
|
filtered.unshift(bookmark); // Add to beginning
|
|
// Keep only last 50 bookmarks
|
|
const trimmed = filtered.slice(0, 50);
|
|
localStorage.setItem(STORAGE_KEYS.BOOKMARKS, JSON.stringify(trimmed));
|
|
} catch (e) {
|
|
console.warn('Failed to save bookmark:', e);
|
|
}
|
|
};
|
|
|
|
export const removeBookmark = (url: string): void => {
|
|
try {
|
|
const bookmarks = getBookmarks();
|
|
const filtered = bookmarks.filter(b => b.url !== url);
|
|
localStorage.setItem(STORAGE_KEYS.BOOKMARKS, JSON.stringify(filtered));
|
|
} catch (e) {
|
|
console.warn('Failed to remove bookmark:', e);
|
|
}
|
|
};
|
|
|
|
export const getBookmarkForUrl = (url: string): Bookmark | undefined => {
|
|
const bookmarks = getBookmarks();
|
|
return bookmarks.find(b => b.url === url);
|
|
};
|
|
|
|
// ==================== RSS FEEDS ====================
|
|
|
|
export const getRSSFeeds = (): RSSFeed[] => {
|
|
try {
|
|
const stored = localStorage.getItem(STORAGE_KEYS.RSS_FEEDS);
|
|
if (stored) {
|
|
return JSON.parse(stored);
|
|
}
|
|
} catch (e) {
|
|
console.warn('Failed to load RSS feeds:', e);
|
|
}
|
|
return [];
|
|
};
|
|
|
|
export const saveRSSFeed = (feed: RSSFeed): void => {
|
|
try {
|
|
const feeds = getRSSFeeds();
|
|
const existing = feeds.findIndex(f => f.id === feed.id);
|
|
if (existing >= 0) {
|
|
feeds[existing] = feed;
|
|
} else {
|
|
feeds.push(feed);
|
|
}
|
|
localStorage.setItem(STORAGE_KEYS.RSS_FEEDS, JSON.stringify(feeds));
|
|
} catch (e) {
|
|
console.warn('Failed to save RSS feed:', e);
|
|
}
|
|
};
|
|
|
|
export const removeRSSFeed = (feedId: string): void => {
|
|
try {
|
|
const feeds = getRSSFeeds();
|
|
const filtered = feeds.filter(f => f.id !== feedId);
|
|
localStorage.setItem(STORAGE_KEYS.RSS_FEEDS, JSON.stringify(filtered));
|
|
} catch (e) {
|
|
console.warn('Failed to remove RSS feed:', e);
|
|
}
|
|
};
|
|
|
|
export const updateRSSFeedLastFetched = (feedId: string): void => {
|
|
try {
|
|
const feeds = getRSSFeeds();
|
|
const feed = feeds.find(f => f.id === feedId);
|
|
if (feed) {
|
|
feed.lastFetched = Date.now();
|
|
localStorage.setItem(STORAGE_KEYS.RSS_FEEDS, JSON.stringify(feeds));
|
|
}
|
|
} catch (e) {
|
|
console.warn('Failed to update RSS feed:', e);
|
|
}
|
|
};
|
|
|
|
// ==================== LAST PLAYED ====================
|
|
|
|
export const saveLastPlayed = (articleId: string, segmentIndex: number): void => {
|
|
try {
|
|
localStorage.setItem(STORAGE_KEYS.LAST_PLAYED, JSON.stringify({
|
|
articleId,
|
|
segmentIndex,
|
|
timestamp: Date.now()
|
|
}));
|
|
} catch (e) {
|
|
console.warn('Failed to save last played:', e);
|
|
}
|
|
};
|
|
|
|
export const getLastPlayed = (): { articleId: string; segmentIndex: number } | null => {
|
|
try {
|
|
const stored = localStorage.getItem(STORAGE_KEYS.LAST_PLAYED);
|
|
if (stored) {
|
|
return JSON.parse(stored);
|
|
}
|
|
} catch (e) {
|
|
console.warn('Failed to load last played:', e);
|
|
}
|
|
return null;
|
|
};
|