Group the Sources tab by kind and pick a kind when adding a feed (#4)

The Sources tab shows News, Learning and Fun sections, each split by
language; kinds or languages without Sources get no header. The add-feed
dialog has a kind picker that defaults to News.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
CedricandClaude Opus 5.5 committed 2026-10-16 15:50:43 +02:00
1 parent a6b8d8b537
commit f205452b25
3 files changed
+205 -25

No files matched your search

+50 -24
View File
@@ -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)),