Merge feature/source-kinds into ticket/3-fun-section (#3)
Keeps the Learning catalog from #6 and puts the Fun group after it, in the catalog and the README. Adds the end-to-end check #4 left for Fun: a custom Fun feed added on the Sources tab shows up in the Learn tab's Fun section, not in Today. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
1048e4e384
8 files changed
+499
-27
No files matched your search
@@ -212,6 +212,155 @@ const defaultSources = <NewsSource>[
|
||||
description: 'System design explained with diagrams',
|
||||
enabledByDefault: true,
|
||||
),
|
||||
NewsSource(
|
||||
id: 'systemdesignone',
|
||||
name: 'System Design One',
|
||||
feedUrl: 'https://newsletter.systemdesign.one/feed',
|
||||
homepage: 'https://newsletter.systemdesign.one',
|
||||
language: SourceLanguage.en,
|
||||
kind: SourceKind.learning,
|
||||
description: 'System design case studies of real systems',
|
||||
enabledByDefault: true,
|
||||
),
|
||||
NewsSource(
|
||||
id: 'pragmaticengineer-blog',
|
||||
name: 'The Pragmatic Engineer (blog)',
|
||||
feedUrl: 'https://blog.pragmaticengineer.com/rss/',
|
||||
homepage: 'https://blog.pragmaticengineer.com',
|
||||
language: SourceLanguage.en,
|
||||
kind: SourceKind.learning,
|
||||
description: 'How big tech and startups build software (free posts)',
|
||||
enabledByDefault: true,
|
||||
),
|
||||
NewsSource(
|
||||
id: 'martinfowler',
|
||||
name: 'Martin Fowler',
|
||||
feedUrl: 'https://martinfowler.com/feed.atom',
|
||||
homepage: 'https://martinfowler.com',
|
||||
language: SourceLanguage.en,
|
||||
kind: SourceKind.learning,
|
||||
description: 'Software architecture, refactoring, delivery practices',
|
||||
enabledByDefault: true,
|
||||
),
|
||||
NewsSource(
|
||||
id: 'juliaevans',
|
||||
name: 'Julia Evans',
|
||||
feedUrl: 'https://jvns.ca/atom.xml',
|
||||
homepage: 'https://jvns.ca',
|
||||
language: SourceLanguage.en,
|
||||
kind: SourceKind.learning,
|
||||
description: 'Systems, networking and Unix tools, explained plainly',
|
||||
enabledByDefault: true,
|
||||
),
|
||||
NewsSource(
|
||||
id: 'danluu',
|
||||
name: 'Dan Luu',
|
||||
feedUrl: 'https://danluu.com/atom.xml',
|
||||
homepage: 'https://danluu.com',
|
||||
language: SourceLanguage.en,
|
||||
kind: SourceKind.learning,
|
||||
description: 'Long, data-driven essays on computers and engineering',
|
||||
enabledByDefault: true,
|
||||
),
|
||||
NewsSource(
|
||||
id: 'cloudflare-blog',
|
||||
name: 'The Cloudflare Blog',
|
||||
feedUrl: 'https://blog.cloudflare.com/rss/',
|
||||
homepage: 'https://blog.cloudflare.com',
|
||||
language: SourceLanguage.en,
|
||||
kind: SourceKind.learning,
|
||||
description: 'Networking, security and incident write-ups in depth',
|
||||
enabledByDefault: true,
|
||||
),
|
||||
NewsSource(
|
||||
id: 'netflix-techblog',
|
||||
name: 'Netflix Technology Blog',
|
||||
feedUrl: 'https://netflixtechblog.com/feed',
|
||||
homepage: 'https://netflixtechblog.com',
|
||||
language: SourceLanguage.en,
|
||||
kind: SourceKind.learning,
|
||||
description: 'Distributed systems, data and streaming at scale',
|
||||
enabledByDefault: true,
|
||||
),
|
||||
NewsSource(
|
||||
id: 'meta-engineering',
|
||||
name: 'Engineering at Meta',
|
||||
feedUrl: 'https://engineering.fb.com/feed/',
|
||||
homepage: 'https://engineering.fb.com',
|
||||
language: SourceLanguage.en,
|
||||
kind: SourceKind.learning,
|
||||
description: 'Infrastructure, AI and production engineering at Meta',
|
||||
enabledByDefault: true,
|
||||
),
|
||||
NewsSource(
|
||||
id: 'github-engineering',
|
||||
name: 'GitHub Blog: Engineering',
|
||||
feedUrl: 'https://github.blog/engineering/feed/',
|
||||
homepage: 'https://github.blog/engineering/',
|
||||
language: SourceLanguage.en,
|
||||
kind: SourceKind.learning,
|
||||
description: 'How GitHub builds and runs GitHub',
|
||||
enabledByDefault: true,
|
||||
),
|
||||
NewsSource(
|
||||
id: 'latent-space',
|
||||
name: 'Latent Space',
|
||||
feedUrl: 'https://www.latent.space/feed',
|
||||
homepage: 'https://www.latent.space',
|
||||
language: SourceLanguage.en,
|
||||
kind: SourceKind.learning,
|
||||
description: 'AI engineering: interviews and deep dives',
|
||||
enabledByDefault: true,
|
||||
),
|
||||
NewsSource(
|
||||
id: 'import-ai',
|
||||
name: 'Import AI',
|
||||
feedUrl: 'https://importai.substack.com/feed',
|
||||
homepage: 'https://importai.substack.com',
|
||||
language: SourceLanguage.en,
|
||||
kind: SourceKind.learning,
|
||||
description: 'Weekly AI research digest by Jack Clark',
|
||||
enabledByDefault: true,
|
||||
),
|
||||
NewsSource(
|
||||
id: 'interconnects',
|
||||
name: 'Interconnects',
|
||||
feedUrl: 'https://www.interconnects.ai/feed',
|
||||
homepage: 'https://www.interconnects.ai',
|
||||
language: SourceLanguage.en,
|
||||
kind: SourceKind.learning,
|
||||
description: 'Language model training and research, analysed',
|
||||
enabledByDefault: true,
|
||||
),
|
||||
NewsSource(
|
||||
id: 'ahead-of-ai',
|
||||
name: 'Ahead of AI',
|
||||
feedUrl: 'https://magazine.sebastianraschka.com/feed',
|
||||
homepage: 'https://magazine.sebastianraschka.com',
|
||||
language: SourceLanguage.en,
|
||||
kind: SourceKind.learning,
|
||||
description: 'LLM research explained, by Sebastian Raschka',
|
||||
enabledByDefault: true,
|
||||
),
|
||||
NewsSource(
|
||||
id: 'lilianweng',
|
||||
name: 'Lilian Weng',
|
||||
feedUrl: 'https://lilianweng.github.io/index.xml',
|
||||
homepage: 'https://lilianweng.github.io',
|
||||
language: SourceLanguage.en,
|
||||
kind: SourceKind.learning,
|
||||
description: 'Thorough surveys of machine learning topics',
|
||||
enabledByDefault: true,
|
||||
),
|
||||
NewsSource(
|
||||
id: 'infoq',
|
||||
name: 'InfoQ',
|
||||
feedUrl: 'https://feed.infoq.com/',
|
||||
homepage: 'https://www.infoq.com',
|
||||
language: SourceLanguage.en,
|
||||
kind: SourceKind.learning,
|
||||
description: 'Software architecture and engineering talks, high volume',
|
||||
),
|
||||
|
||||
// --- Fun ----------------------------------------------------------------
|
||||
NewsSource(
|
||||
|
||||
+50
-24
@@ -40,29 +40,7 @@ class SourcesScreen extends StatelessWidget {
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.only(bottom: 96),
|
||||
children: [
|
||||
for (final lang in SourceLanguage.values) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 4),
|
||||
child: Text('${lang.flag} ${lang.label}',
|
||||
style: theme.textTheme.titleSmall
|
||||
?.copyWith(color: theme.colorScheme.primary)),
|
||||
),
|
||||
for (final source in state.allSources.where((s) => s.language == lang))
|
||||
_SourceTile(
|
||||
source: source,
|
||||
enabled: state.enabledSourceIds.contains(source.id),
|
||||
onToggle: (v) {
|
||||
state.setSourceEnabled(source, v);
|
||||
onChanged();
|
||||
},
|
||||
onDelete: source.isCustom
|
||||
? () {
|
||||
state.removeCustomSource(source);
|
||||
onChanged();
|
||||
}
|
||||
: null,
|
||||
),
|
||||
],
|
||||
for (final kind in SourceKind.values) ..._kindSection(context, kind),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -70,6 +48,44 @@ class SourcesScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
/// One section per kind, split by language. Kinds and languages without
|
||||
/// Sources are left out instead of showing an empty header.
|
||||
List<Widget> _kindSection(BuildContext context, SourceKind kind) {
|
||||
final theme = Theme.of(context);
|
||||
final sources = state.allSources.where((s) => s.kind == kind).toList();
|
||||
if (sources.isEmpty) return const [];
|
||||
return [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 24, 16, 0),
|
||||
child: Text(kind.label, style: theme.textTheme.titleMedium),
|
||||
),
|
||||
for (final lang in SourceLanguage.values)
|
||||
if (sources.any((s) => s.language == lang)) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||
child: Text('${lang.flag} ${lang.label}',
|
||||
style: theme.textTheme.titleSmall
|
||||
?.copyWith(color: theme.colorScheme.primary)),
|
||||
),
|
||||
for (final source in sources.where((s) => s.language == lang))
|
||||
_SourceTile(
|
||||
source: source,
|
||||
enabled: state.enabledSourceIds.contains(source.id),
|
||||
onToggle: (v) {
|
||||
state.setSourceEnabled(source, v);
|
||||
onChanged();
|
||||
},
|
||||
onDelete: source.isCustom
|
||||
? () {
|
||||
state.removeCustomSource(source);
|
||||
onChanged();
|
||||
}
|
||||
: null,
|
||||
),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
Future<void> _addFeed(BuildContext context) async {
|
||||
final added = await showDialog<bool>(
|
||||
context: context,
|
||||
@@ -134,6 +150,7 @@ class _AddFeedDialogState extends State<_AddFeedDialog> {
|
||||
final _name = TextEditingController();
|
||||
final _url = TextEditingController();
|
||||
SourceLanguage _language = SourceLanguage.de;
|
||||
SourceKind _kind = SourceKind.news;
|
||||
bool _busy = false;
|
||||
String? _error;
|
||||
|
||||
@@ -156,7 +173,7 @@ class _AddFeedDialogState extends State<_AddFeedDialog> {
|
||||
_error = null;
|
||||
});
|
||||
final error = await widget.state
|
||||
.addCustomSource(name: name, feedUrl: url, language: _language);
|
||||
.addCustomSource(name: name, feedUrl: url, language: _language, kind: _kind);
|
||||
if (!mounted) return;
|
||||
if (error == null) {
|
||||
Navigator.pop(context, true);
|
||||
@@ -199,6 +216,15 @@ class _AddFeedDialogState extends State<_AddFeedDialog> {
|
||||
selected: {_language},
|
||||
onSelectionChanged: (s) => setState(() => _language = s.first),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SegmentedButton<SourceKind>(
|
||||
segments: [
|
||||
for (final k in SourceKind.values)
|
||||
ButtonSegment(value: k, label: Text(k.label)),
|
||||
],
|
||||
selected: {_kind},
|
||||
onSelectionChanged: (s) => setState(() => _kind = s.first),
|
||||
),
|
||||
if (_error != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Text(_error!, style: TextStyle(color: Theme.of(context).colorScheme.error)),
|
||||
|
||||
Reference in new issue
Block a user