diff --git a/README.md b/README.md index 12765c8..a2998bc 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ or write a daily briefing. It's meant to replace the n8n β†’ Discord workflow. same request as Golem's own "Zustimmen" button (`/abo/setconsentcookie.php`, valid for a day) and reuses the cookie for reader view and the in-app page. With it off, the app shows the teaser and an "Open website anyway" option. -- **Sources**: 21 curated feeds (12 German, 9 English), toggle each one, or add any +- **Sources**: 23 curated feeds (13 German, 10 English), toggle each one, or add any RSS/Atom feed (it's validated before being added). - **Dark mode**: System / Light / Dark in Settings (applies instantly). The original-page view follows it too: when the phone is dark, sites use their own @@ -145,34 +145,40 @@ existing AI nodes. ## News sources Every feed URL below was checked to load and parse. The β˜… sources are enabled by -default. +default: a small set of high-signal News sources. The others stay in the catalog +to switch on in the Sources tab. | | Source | Feed | Focus | |---|---|---|---| | πŸ‡©πŸ‡ͺ β˜… | heise online | `heise.de/rss/heise-atom.xml` | Broad IT news, high volume (~150/day) | | πŸ‡©πŸ‡ͺ β˜… | Golem.de | `rss.golem.de/rss.php?feed=ATOM1.0` | IT news for professionals | -| πŸ‡©πŸ‡ͺ β˜… | t3n | `t3n.de/rss.xml` | Digital business, startups, AI | -| πŸ‡©πŸ‡ͺ β˜… | ComputerBase | `computerbase.de/rss/news.xml` | Hardware, software, gaming | +| πŸ‡©πŸ‡ͺ β˜… | The Decoder | `the-decoder.de/feed/` | AI news: models, research, industry | +| πŸ‡©πŸ‡ͺ | t3n | `t3n.de/rss.xml` | Digital business, startups, AI | +| πŸ‡©πŸ‡ͺ | ComputerBase | `computerbase.de/rss/news.xml` | Hardware, software, gaming | | πŸ‡©πŸ‡ͺ | SPIEGEL Netzwelt | `spiegel.de/netzwelt/index.rss` | Mainstream news magazine view | | πŸ‡©πŸ‡ͺ | ZEIT Digital | `newsfeed.zeit.de/digital/index` | Digital policy and society | | πŸ‡©πŸ‡ͺ | netzpolitik.org | `netzpolitik.org/feed/` | Digital rights, privacy | -| πŸ‡©πŸ‡ͺ | heise Security | `heise.de/security/rss/news-atom.xml` | Security vulnerabilities | -| πŸ‡©πŸ‡ͺ | heise Developer | `heise.de/developer/rss/news-atom.xml` | Software development | +| πŸ‡©πŸ‡ͺ | heise Security | `heise.de/security/rss/news-atom.xml` | Security section of heise online (turn heise online off to use it) | +| πŸ‡©πŸ‡ͺ | heise Developer | `heise.de/developer/rss/news-atom.xml` | Developer section of heise online (turn heise online off to use it) | | πŸ‡©πŸ‡ͺ | Caschys Blog | `stadt-bremerhaven.de/feed/` | Consumer tech, smartphones | | πŸ‡©πŸ‡ͺ | derStandard Web | `derstandard.at/rss/web` | Austrian daily's tech section | | πŸ‡©πŸ‡ͺ | Borns IT- und Windows-Blog | `borncity.com/blog/feed/` | Windows, patches, admin topics | -| πŸ‡¬πŸ‡§ β˜… | The Verge | `theverge.com/rss/index.xml` | Consumer tech, policy | +| πŸ‡¬πŸ‡§ | The Verge | `theverge.com/rss/index.xml` | Consumer tech, policy | | πŸ‡¬πŸ‡§ β˜… | Ars Technica | `feeds.arstechnica.com/arstechnica/index` | In-depth tech and science | -| πŸ‡¬πŸ‡§ β˜… | TechCrunch | `techcrunch.com/feed/` | Startups, funding | -| πŸ‡¬πŸ‡§ β˜… | Hacker News | `news.ycombinator.com/rss` | Front page links | +| πŸ‡¬πŸ‡§ | TechCrunch | `techcrunch.com/feed/` | Startups, funding | +| πŸ‡¬πŸ‡§ β˜… | Hacker News | `hnrss.org/frontpage?points=150` | Front page stories with 150+ points | +| πŸ‡¬πŸ‡§ β˜… | Techmeme | `techmeme.com/feed.xml` | The day's top tech stories, picked by editors | +| πŸ‡¬πŸ‡§ β˜… | Lobsters | `lobste.rs/rss` | Computing links, voted by a programmer community | +| πŸ‡¬πŸ‡§ β˜… | Simon Willison | `simonwillison.net/atom/entries/` | LLMs and AI tools, tried out hands-on | | πŸ‡¬πŸ‡§ | MIT Technology Review | `technologyreview.com/feed/` | AI, biotech, climate | -| πŸ‡¬πŸ‡§ | WIRED | `wired.com/feed/rss` | Tech and culture | -| πŸ‡¬πŸ‡§ | Engadget | `engadget.com/rss.xml` | Gadgets | | πŸ‡¬πŸ‡§ | The Register | `theregister.com/headlines.atom` | Enterprise IT | | πŸ‡¬πŸ‡§ | BleepingComputer | `bleepingcomputer.com/feed/` | Security, malware | -Reader view works for 20 of the 21 sources. ZEIT Digital blocks non-browser -requests, so the app shows the teaser and offers the original page. +Reader view works for the publishers' own articles except ZEIT Digital, which +blocks non-browser requests, so the app shows the teaser and offers the original +page. Hacker News and Lobsters link to articles all over the web, so reader view +depends on the linked site. Techmeme links to its own summary page, which only +has the teaser; the original page links on to the full story. ## Project layout diff --git a/lib/data/default_sources.dart b/lib/data/default_sources.dart index ce84ec1..5a9d477 100644 --- a/lib/data/default_sources.dart +++ b/lib/data/default_sources.dart @@ -22,6 +22,15 @@ const defaultSources = [ description: 'IT-News fΓΌr Profis', enabledByDefault: true, ), + NewsSource( + id: 'the-decoder', + name: 'The Decoder', + feedUrl: 'https://the-decoder.de/feed/', + homepage: 'https://the-decoder.de', + language: SourceLanguage.de, + description: 'KI-News: Modelle, Forschung, Branche', + enabledByDefault: true, + ), NewsSource( id: 't3n', name: 't3n', @@ -29,7 +38,6 @@ const defaultSources = [ homepage: 'https://t3n.de', language: SourceLanguage.de, description: 'Digitales Business, Startups, KI', - enabledByDefault: true, checkPaywallOnPage: true, ), NewsSource( @@ -39,7 +47,6 @@ const defaultSources = [ homepage: 'https://www.computerbase.de', language: SourceLanguage.de, description: 'Hardware, Software, Gaming', - enabledByDefault: true, ), NewsSource( id: 'spiegel-netzwelt', @@ -72,7 +79,7 @@ const defaultSources = [ feedUrl: 'https://www.heise.de/security/rss/news-atom.xml', homepage: 'https://www.heise.de/security/', language: SourceLanguage.de, - description: 'SicherheitslΓΌcken und Security-News', + description: 'Security-Teil von heise online (heise online ausschalten, um ihn einzeln zu lesen)', ), NewsSource( id: 'heise-developer', @@ -80,7 +87,7 @@ const defaultSources = [ feedUrl: 'https://www.heise.de/developer/rss/news-atom.xml', homepage: 'https://www.heise.de/developer/', language: SourceLanguage.de, - description: 'Softwareentwicklung', + description: 'Developer-Teil von heise online (heise online ausschalten, um ihn einzeln zu lesen)', ), NewsSource( id: 'caschy', @@ -115,7 +122,6 @@ const defaultSources = [ homepage: 'https://www.theverge.com', language: SourceLanguage.en, description: 'Consumer tech, culture, policy', - enabledByDefault: true, ), NewsSource( id: 'arstechnica', @@ -133,15 +139,41 @@ const defaultSources = [ homepage: 'https://techcrunch.com', language: SourceLanguage.en, description: 'Startups, funding, big tech', - enabledByDefault: true, ), NewsSource( id: 'hackernews', name: 'Hacker News', - feedUrl: 'https://news.ycombinator.com/rss', + feedUrl: 'https://hnrss.org/frontpage?points=150', homepage: 'https://news.ycombinator.com', language: SourceLanguage.en, - description: 'Front page links, ranked by readers', + description: 'Front page stories with 150+ points', + enabledByDefault: true, + ), + NewsSource( + id: 'techmeme', + name: 'Techmeme', + feedUrl: 'https://www.techmeme.com/feed.xml', + homepage: 'https://www.techmeme.com', + language: SourceLanguage.en, + description: "The day's top tech stories, picked by editors", + enabledByDefault: true, + ), + NewsSource( + id: 'lobsters', + name: 'Lobsters', + feedUrl: 'https://lobste.rs/rss', + homepage: 'https://lobste.rs', + language: SourceLanguage.en, + description: 'Computing links, voted by a programmer community', + enabledByDefault: true, + ), + NewsSource( + id: 'simonwillison', + name: 'Simon Willison', + feedUrl: 'https://simonwillison.net/atom/entries/', + homepage: 'https://simonwillison.net', + language: SourceLanguage.en, + description: 'LLMs and AI tools, tried out hands-on', enabledByDefault: true, ), NewsSource( @@ -152,22 +184,6 @@ const defaultSources = [ language: SourceLanguage.en, description: 'AI, biotech, climate tech', ), - NewsSource( - id: 'wired', - name: 'WIRED', - feedUrl: 'https://www.wired.com/feed/rss', - homepage: 'https://www.wired.com', - language: SourceLanguage.en, - description: 'Tech, science and culture', - ), - NewsSource( - id: 'engadget', - name: 'Engadget', - feedUrl: 'https://www.engadget.com/rss.xml', - homepage: 'https://www.engadget.com', - language: SourceLanguage.en, - description: 'Gadgets and consumer electronics', - ), NewsSource( id: 'theregister', name: 'The Register', diff --git a/lib/state/app_state.dart b/lib/state/app_state.dart index b1d022d..dc121eb 100644 --- a/lib/state/app_state.dart +++ b/lib/state/app_state.dart @@ -120,18 +120,21 @@ class AppState extends ChangeNotifier { /// Switches on curated sources that are on by default the first time the /// app sees them, so they also reach installs with saved toggles. Sources - /// seen before keep whatever the user chose. + /// seen before keep whatever the user chose. Saved ids of Sources that are + /// no longer in the catalog are dropped. Future _enableNewDefaultSources() async { final saved = _storage.loadEnabledSourceIds(); final seen = _storage.loadSeenSourceIds() ?? (saved == null ? const {} : legacySeenSourceIds); final unseen = _curated.where((s) => !seen.contains(s.id)).toList(); + final known = {for (final s in allSources) s.id}; enabledSourceIds = { - ...?saved, + ...?saved?.where(known.contains), for (final s in unseen) if (s.enabledByDefault) s.id, }; - if (unseen.isEmpty) return; + final dropped = saved != null && saved.any((id) => !known.contains(id)); + if (unseen.isEmpty && !dropped) return; await _storage.saveSeenSourceIds({...seen, ...unseen.map((s) => s.id)}); await _storage.saveEnabledSourceIds(enabledSourceIds); } diff --git a/test/default_sources_test.dart b/test/default_sources_test.dart new file mode 100644 index 0000000..049e4d2 --- /dev/null +++ b/test/default_sources_test.dart @@ -0,0 +1,51 @@ +import 'package:daily_tech_news/data/default_sources.dart'; +import 'package:daily_tech_news/models/news_source.dart'; +import 'package:flutter_test/flutter_test.dart'; + +NewsSource _byId(String id) => defaultSources.singleWhere((s) => s.id == id); + +void main() { + test('the News sources on by default are the curated quality set', () { + final onByDefault = { + for (final s in defaultSources) + if (s.enabledByDefault) s.id, + }; + + expect(onByDefault, { + 'heise', 'golem', 'arstechnica', 'hackernews', + 'techmeme', 'lobsters', 'the-decoder', 'simonwillison', + }); + }); + + test('the added Sources read the agreed feeds', () { + final feeds = { + for (final id in ['techmeme', 'lobsters', 'the-decoder', 'simonwillison']) + id: (_byId(id).feedUrl, _byId(id).language), + }; + + expect(feeds, { + 'techmeme': ('https://www.techmeme.com/feed.xml', SourceLanguage.en), + 'lobsters': ('https://lobste.rs/rss', SourceLanguage.en), + 'the-decoder': ('https://the-decoder.de/feed/', SourceLanguage.de), + 'simonwillison': ('https://simonwillison.net/atom/entries/', SourceLanguage.en), + }); + }); + + test('Hacker News only brings well-voted front page stories', () { + expect(_byId('hackernews').feedUrl, 'https://hnrss.org/frontpage?points=150'); + }); + + test('switched-off Sources stay in the catalog, WIRED and Engadget are gone', () { + final ids = defaultSources.map((s) => s.id).toSet(); + + expect(ids, containsAll(['t3n', 'computerbase', 'techcrunch', 'verge'])); + expect(ids, isNot(contains('wired'))); + expect(ids, isNot(contains('engadget'))); + }); + + test('every Source has its own id', () { + final ids = defaultSources.map((s) => s.id).toList(); + + expect(ids.toSet().length, ids.length); + }); +} diff --git a/test/source_selection_test.dart b/test/source_selection_test.dart index 8ce401a..3701689 100644 --- a/test/source_selection_test.dart +++ b/test/source_selection_test.dart @@ -78,12 +78,32 @@ void main() { test('an install from before the app remembered seen Sources keeps its toggles', () async { // Toggles saved by an earlier version: golem, t3n and others switched off. SharedPreferences.setMockInitialValues({ - 'enabled_sources': ['heise', 'verge', 'wired'], + 'enabled_sources': ['heise', 'verge', 'mit-tr'], }); final newSource = _source('new-source', onByDefault: true); final state = await _launch([...defaultSources, newSource]); - expect(state.enabledSourceIds, {'heise', 'verge', 'wired', 'new-source'}); + expect(state.enabledSourceIds, { + 'heise', 'verge', 'mit-tr', 'new-source', + // Added to the catalog later, on by default. + 'techmeme', 'lobsters', 'the-decoder', 'simonwillison', + }); + }); + + test('an install from before the catalog update gets the new Sources and drops removed ones', + () async { + // Toggles saved by an earlier version, with WIRED and Engadget switched on. + SharedPreferences.setMockInitialValues({ + 'enabled_sources': ['heise', 'golem', 't3n', 'verge', 'wired', 'engadget'], + }); + + final state = await _launch(defaultSources); + + expect(state.enabledSourceIds, { + 'heise', 'golem', 't3n', 'verge', + 'techmeme', 'lobsters', 'the-decoder', 'simonwillison', + }); + expect(state.enabledSources.map((s) => s.id), isNot(contains('wired'))); }); }