Add source kinds and the Learn tab with Learning sources (#2)
Every Source now has a kind: News, Learning or Fun. Curated Sources default to News, and custom Sources persist their kind (ones saved before default to News). Each kind has its own time window: News follows the 12/24/48 h setting, Learning reaches back 14 days, Fun 3 days. A new Learn tab between Briefing and Sources lists Learning articles with the same paid/sponsored filters and reader view as Today. It is built from a list of kinds so a Fun section can follow. Today and the Briefing only use News. ByteByteGo is the first Learning source, on by default. The home shell names its tabs instead of using hard-coded indexes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8c8ffca5dc
commit
64c4a11105
12 files changed
+498
-32
No files matched your search
@@ -0,0 +1,68 @@
|
||||
import 'package:daily_tech_news/models/article.dart';
|
||||
import 'package:daily_tech_news/models/news_source.dart';
|
||||
import 'package:daily_tech_news/services/feed_service.dart';
|
||||
import 'package:daily_tech_news/services/storage.dart';
|
||||
import 'package:daily_tech_news/services/summarizer.dart';
|
||||
import 'package:daily_tech_news/state/app_state.dart';
|
||||
|
||||
/// Serves canned articles per Source instead of the network. The real
|
||||
/// [FeedService.fetchAll] still applies the time windows.
|
||||
class FakeFeeds extends FeedService {
|
||||
FakeFeeds([this.byFeedUrl = const {}]);
|
||||
|
||||
/// Feed URL -> (title, age) of each article in the feed.
|
||||
final Map<String, List<(String, Duration)>> byFeedUrl;
|
||||
|
||||
/// How often the app refreshed its feeds.
|
||||
int refreshes = 0;
|
||||
|
||||
@override
|
||||
Future<FeedResult> fetchAll(List<NewsSource> sources,
|
||||
{required Duration Function(NewsSource source) maxAgeOf, String corsProxy = ''}) {
|
||||
refreshes++;
|
||||
return super.fetchAll(sources, maxAgeOf: maxAgeOf, corsProxy: corsProxy);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Article>> fetchSource(NewsSource source, {String corsProxy = ''}) async {
|
||||
final now = DateTime.now().toUtc();
|
||||
// A feed nobody set up still has an article, so it can be added.
|
||||
if (!byFeedUrl.containsKey(source.feedUrl)) {
|
||||
return [Article(title: 'Any', link: '${source.feedUrl}#any', source: source)];
|
||||
}
|
||||
return [
|
||||
for (final (title, age) in byFeedUrl[source.feedUrl]!)
|
||||
Article(
|
||||
title: title,
|
||||
link: '${source.feedUrl}#$title',
|
||||
source: source,
|
||||
published: now.subtract(age),
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
NewsSource testSource(String id, SourceKind kind) => NewsSource(
|
||||
id: id,
|
||||
name: id,
|
||||
feedUrl: 'https://example.com/$id.xml',
|
||||
homepage: 'https://example.com',
|
||||
language: SourceLanguage.en,
|
||||
kind: kind,
|
||||
enabledByDefault: true,
|
||||
);
|
||||
|
||||
/// Starts the app the way a launch does, against whatever is in preferences.
|
||||
Future<AppState> launchApp(
|
||||
List<NewsSource> curatedSources, {
|
||||
Map<String, List<(String, Duration)>> feeds = const {},
|
||||
FeedService? feedService,
|
||||
Summarizer? summarizer,
|
||||
}) async {
|
||||
final state = AppState(await Storage.open(),
|
||||
feedService: feedService ?? FakeFeeds(feeds),
|
||||
curatedSources: curatedSources,
|
||||
summarizer: summarizer);
|
||||
await state.init();
|
||||
return state;
|
||||
}
|
||||
Reference in new issue
Block a user