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
@@ -34,8 +34,10 @@ class AppState extends ChangeNotifier {
|
||||
FeedService? feedService,
|
||||
ArticleExtractor? extractor,
|
||||
List<NewsSource> curatedSources = defaultSources,
|
||||
Summarizer? summarizer,
|
||||
}) : _feeds = feedService ?? FeedService(),
|
||||
_curated = curatedSources,
|
||||
_summarizerOverride = summarizer,
|
||||
consent = ConsentService(),
|
||||
paywall = PaywallService(_storage) {
|
||||
_extractor = extractor ?? ArticleExtractor(consent: consent);
|
||||
@@ -46,6 +48,9 @@ class AppState extends ChangeNotifier {
|
||||
|
||||
/// The curated catalog; [defaultSources] outside of tests.
|
||||
final List<NewsSource> _curated;
|
||||
|
||||
/// Used instead of the AI set up in [settings]; for tests.
|
||||
final Summarizer? _summarizerOverride;
|
||||
late final ArticleExtractor _extractor;
|
||||
final PaywallService paywall;
|
||||
|
||||
@@ -56,7 +61,7 @@ class AppState extends ChangeNotifier {
|
||||
List<NewsSource> customSources = [];
|
||||
Set<String> enabledSourceIds = {};
|
||||
|
||||
/// Everything the feeds returned; see [visibleArticles] for what is shown.
|
||||
/// Everything the feeds returned; see [articlesOf] for what is shown.
|
||||
List<Article> articles = [];
|
||||
Map<String, String> feedErrors = {};
|
||||
bool loading = false;
|
||||
@@ -77,10 +82,22 @@ class AppState extends ChangeNotifier {
|
||||
(settings.hidePaid && paywall.isPaid(a)) ||
|
||||
(settings.hideSponsored && paywall.isSponsored(a));
|
||||
|
||||
/// Articles after the paid/sponsored filters. The feed and the briefing use these.
|
||||
List<Article> get visibleArticles => articles.where((a) => !_isHidden(a)).toList();
|
||||
/// Articles from Sources of [kind] after the paid/sponsored filters. Today
|
||||
/// and the Briefing use News, the Learn tab the other kinds.
|
||||
List<Article> articlesOf(SourceKind kind) =>
|
||||
articles.where((a) => a.source.kind == kind && !_isHidden(a)).toList();
|
||||
|
||||
int get hiddenCount => articles.where(_isHidden).length;
|
||||
/// Articles from Sources of [kind] that the paid/sponsored filters hide.
|
||||
int hiddenCountOf(SourceKind kind) =>
|
||||
articles.where((a) => a.source.kind == kind && _isHidden(a)).length;
|
||||
|
||||
/// How far back articles from Sources of [kind] reach. News follows the
|
||||
/// user's setting; the other kinds age slower.
|
||||
Duration windowOf(SourceKind kind) => switch (kind) {
|
||||
SourceKind.news => Duration(hours: settings.maxAgeHours),
|
||||
SourceKind.learning => const Duration(days: 14),
|
||||
SourceKind.fun => const Duration(days: 3),
|
||||
};
|
||||
|
||||
List<NewsSource> get allSources => [..._curated, ...customSources];
|
||||
|
||||
@@ -102,7 +119,7 @@ class AppState extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
final result = await _feeds.fetchAll(
|
||||
enabledSources,
|
||||
maxAge: Duration(hours: settings.maxAgeHours),
|
||||
maxAgeOf: (source) => windowOf(source.kind),
|
||||
corsProxy: settings.corsProxy,
|
||||
);
|
||||
articles = result.articles;
|
||||
@@ -148,6 +165,7 @@ class AppState extends ChangeNotifier {
|
||||
required String name,
|
||||
required String feedUrl,
|
||||
required SourceLanguage language,
|
||||
SourceKind kind = SourceKind.news,
|
||||
}) async {
|
||||
final uri = Uri.tryParse(feedUrl);
|
||||
final source = NewsSource(
|
||||
@@ -156,6 +174,7 @@ class AppState extends ChangeNotifier {
|
||||
feedUrl: feedUrl,
|
||||
homepage: uri == null ? '' : '${uri.scheme}://${uri.host}',
|
||||
language: language,
|
||||
kind: kind,
|
||||
isCustom: true,
|
||||
);
|
||||
try {
|
||||
@@ -226,7 +245,7 @@ class AppState extends ChangeNotifier {
|
||||
|
||||
// --- AI ------------------------------------------------------------------
|
||||
|
||||
Summarizer? get _summarizer => switch (settings.aiProvider) {
|
||||
Summarizer? get _summarizer => _summarizerOverride ?? switch (settings.aiProvider) {
|
||||
_ when !settings.aiConfigured => null,
|
||||
AiProvider.claude => ClaudeSummarizer(
|
||||
apiKey: settings.claudeApiKey, model: settings.claudeModel),
|
||||
@@ -270,7 +289,7 @@ class AppState extends ChangeNotifier {
|
||||
Future<void> generateBriefing() async {
|
||||
final summarizer = _summarizer;
|
||||
if (summarizer == null || briefingLoading) return;
|
||||
final articles = visibleArticles;
|
||||
final articles = articlesOf(SourceKind.news);
|
||||
if (articles.isEmpty) {
|
||||
briefingError = 'No articles loaded yet. Pull to refresh the feed first.';
|
||||
notifyListeners();
|
||||
|
||||
Reference in new issue
Block a user