Rename NewsSource to Source now that it models every kind (review item 3)

The class and its file (lib/models/source.dart) follow the glossary term.
Persisted JSON keys are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
CedricandClaude Opus 5.5 committed 2026-10-16 16:03:25 +02:00
1 parent 1a3442ea02
commit a47ff28ed1
33 files changed
+119 -119

No files matched your search

+44 -44
View File
@@ -1,10 +1,10 @@
import '../models/news_source.dart';
import '../models/source.dart';
/// Curated tech news feeds. All URLs were checked to return a valid RSS/Atom
/// feed. Users can toggle them and add their own in the Sources tab.
const defaultSources = <NewsSource>[
const defaultSources = <Source>[
// --- German -------------------------------------------------------------
NewsSource(
Source(
id: 'heise',
name: 'heise online',
feedUrl: 'https://www.heise.de/rss/heise-atom.xml',
@@ -13,7 +13,7 @@ const defaultSources = <NewsSource>[
description: 'IT-Nachrichten, breit aufgestellt (hohes Volumen)',
enabledByDefault: true,
),
NewsSource(
Source(
id: 'golem',
name: 'Golem.de',
feedUrl: 'https://rss.golem.de/rss.php?feed=ATOM1.0',
@@ -22,7 +22,7 @@ const defaultSources = <NewsSource>[
description: 'IT-News für Profis',
enabledByDefault: true,
),
NewsSource(
Source(
id: 'the-decoder',
name: 'The Decoder',
feedUrl: 'https://the-decoder.de/feed/',
@@ -31,7 +31,7 @@ const defaultSources = <NewsSource>[
description: 'KI-News: Modelle, Forschung, Branche',
enabledByDefault: true,
),
NewsSource(
Source(
id: 't3n',
name: 't3n',
feedUrl: 'https://t3n.de/rss.xml',
@@ -40,7 +40,7 @@ const defaultSources = <NewsSource>[
description: 'Digitales Business, Startups, KI',
checkPaywallOnPage: true,
),
NewsSource(
Source(
id: 'computerbase',
name: 'ComputerBase',
feedUrl: 'https://www.computerbase.de/rss/news.xml',
@@ -48,7 +48,7 @@ const defaultSources = <NewsSource>[
language: SourceLanguage.de,
description: 'Hardware, Software, Gaming',
),
NewsSource(
Source(
id: 'spiegel-netzwelt',
name: 'SPIEGEL Netzwelt',
feedUrl: 'https://www.spiegel.de/netzwelt/index.rss',
@@ -57,7 +57,7 @@ const defaultSources = <NewsSource>[
description: 'Digitales aus Sicht eines Nachrichtenmagazins',
checkPaywallOnPage: true,
),
NewsSource(
Source(
id: 'zeit-digital',
name: 'ZEIT Digital',
feedUrl: 'https://newsfeed.zeit.de/digital/index',
@@ -65,7 +65,7 @@ const defaultSources = <NewsSource>[
language: SourceLanguage.de,
description: 'Digitalpolitik und Gesellschaft',
),
NewsSource(
Source(
id: 'netzpolitik',
name: 'netzpolitik.org',
feedUrl: 'https://netzpolitik.org/feed/',
@@ -73,7 +73,7 @@ const defaultSources = <NewsSource>[
language: SourceLanguage.de,
description: 'Digitale Freiheitsrechte, Datenschutz',
),
NewsSource(
Source(
id: 'heise-security',
name: 'heise Security',
feedUrl: 'https://www.heise.de/security/rss/news-atom.xml',
@@ -81,7 +81,7 @@ const defaultSources = <NewsSource>[
language: SourceLanguage.de,
description: 'Security-Teil von heise online (heise online ausschalten, um ihn einzeln zu lesen)',
),
NewsSource(
Source(
id: 'heise-developer',
name: 'heise Developer',
feedUrl: 'https://www.heise.de/developer/rss/news-atom.xml',
@@ -89,7 +89,7 @@ const defaultSources = <NewsSource>[
language: SourceLanguage.de,
description: 'Developer-Teil von heise online (heise online ausschalten, um ihn einzeln zu lesen)',
),
NewsSource(
Source(
id: 'caschy',
name: "Caschys Blog",
feedUrl: 'https://stadt-bremerhaven.de/feed/',
@@ -97,7 +97,7 @@ const defaultSources = <NewsSource>[
language: SourceLanguage.de,
description: 'Consumer-Tech, Smartphones, Streaming',
),
NewsSource(
Source(
id: 'derstandard-web',
name: 'derStandard Web',
feedUrl: 'https://www.derstandard.at/rss/web',
@@ -105,7 +105,7 @@ const defaultSources = <NewsSource>[
language: SourceLanguage.de,
description: 'Tech-Ressort der österreichischen Tageszeitung',
),
NewsSource(
Source(
id: 'borncity',
name: 'Borns IT- und Windows-Blog',
feedUrl: 'https://www.borncity.com/blog/feed/',
@@ -115,7 +115,7 @@ const defaultSources = <NewsSource>[
),
// --- English ------------------------------------------------------------
NewsSource(
Source(
id: 'verge',
name: 'The Verge',
feedUrl: 'https://www.theverge.com/rss/index.xml',
@@ -123,7 +123,7 @@ const defaultSources = <NewsSource>[
language: SourceLanguage.en,
description: 'Consumer tech, culture, policy',
),
NewsSource(
Source(
id: 'arstechnica',
name: 'Ars Technica',
feedUrl: 'https://feeds.arstechnica.com/arstechnica/index',
@@ -132,7 +132,7 @@ const defaultSources = <NewsSource>[
description: 'In-depth tech and science',
enabledByDefault: true,
),
NewsSource(
Source(
id: 'techcrunch',
name: 'TechCrunch',
feedUrl: 'https://techcrunch.com/feed/',
@@ -140,7 +140,7 @@ const defaultSources = <NewsSource>[
language: SourceLanguage.en,
description: 'Startups, funding, big tech',
),
NewsSource(
Source(
id: 'hackernews',
name: 'Hacker News',
feedUrl: 'https://hnrss.org/frontpage?points=150',
@@ -149,7 +149,7 @@ const defaultSources = <NewsSource>[
description: 'Front page stories with 150+ points',
enabledByDefault: true,
),
NewsSource(
Source(
id: 'techmeme',
name: 'Techmeme',
feedUrl: 'https://www.techmeme.com/feed.xml',
@@ -158,7 +158,7 @@ const defaultSources = <NewsSource>[
description: "The day's top tech stories, picked by editors",
enabledByDefault: true,
),
NewsSource(
Source(
id: 'lobsters',
name: 'Lobsters',
feedUrl: 'https://lobste.rs/rss',
@@ -167,7 +167,7 @@ const defaultSources = <NewsSource>[
description: 'Computing links, voted by a programmer community',
enabledByDefault: true,
),
NewsSource(
Source(
id: 'simonwillison',
name: 'Simon Willison',
feedUrl: 'https://simonwillison.net/atom/entries/',
@@ -176,7 +176,7 @@ const defaultSources = <NewsSource>[
description: 'LLMs and AI tools, tried out hands-on',
enabledByDefault: true,
),
NewsSource(
Source(
id: 'mit-tr',
name: 'MIT Technology Review',
feedUrl: 'https://www.technologyreview.com/feed/',
@@ -184,7 +184,7 @@ const defaultSources = <NewsSource>[
language: SourceLanguage.en,
description: 'AI, biotech, climate tech',
),
NewsSource(
Source(
id: 'theregister',
name: 'The Register',
feedUrl: 'https://www.theregister.com/headlines.atom',
@@ -192,7 +192,7 @@ const defaultSources = <NewsSource>[
language: SourceLanguage.en,
description: 'Enterprise IT with attitude',
),
NewsSource(
Source(
id: 'bleepingcomputer',
name: 'BleepingComputer',
feedUrl: 'https://www.bleepingcomputer.com/feed/',
@@ -202,7 +202,7 @@ const defaultSources = <NewsSource>[
),
// --- Learning -----------------------------------------------------------
NewsSource(
Source(
id: 'bytebytego',
name: 'ByteByteGo',
feedUrl: 'https://blog.bytebytego.com/feed',
@@ -212,7 +212,7 @@ const defaultSources = <NewsSource>[
description: 'System design explained with diagrams',
enabledByDefault: true,
),
NewsSource(
Source(
id: 'systemdesignone',
name: 'System Design One',
feedUrl: 'https://newsletter.systemdesign.one/feed',
@@ -222,7 +222,7 @@ const defaultSources = <NewsSource>[
description: 'System design case studies of real systems',
enabledByDefault: true,
),
NewsSource(
Source(
id: 'pragmaticengineer-blog',
name: 'The Pragmatic Engineer (blog)',
feedUrl: 'https://blog.pragmaticengineer.com/rss/',
@@ -232,7 +232,7 @@ const defaultSources = <NewsSource>[
description: 'How big tech and startups build software (free posts)',
enabledByDefault: true,
),
NewsSource(
Source(
id: 'martinfowler',
name: 'Martin Fowler',
feedUrl: 'https://martinfowler.com/feed.atom',
@@ -242,7 +242,7 @@ const defaultSources = <NewsSource>[
description: 'Software architecture, refactoring, delivery practices',
enabledByDefault: true,
),
NewsSource(
Source(
id: 'juliaevans',
name: 'Julia Evans',
feedUrl: 'https://jvns.ca/atom.xml',
@@ -252,7 +252,7 @@ const defaultSources = <NewsSource>[
description: 'Systems, networking and Unix tools, explained plainly',
enabledByDefault: true,
),
NewsSource(
Source(
id: 'danluu',
name: 'Dan Luu',
feedUrl: 'https://danluu.com/atom.xml',
@@ -262,7 +262,7 @@ const defaultSources = <NewsSource>[
description: 'Long, data-driven essays on computers and engineering',
enabledByDefault: true,
),
NewsSource(
Source(
id: 'cloudflare-blog',
name: 'The Cloudflare Blog',
feedUrl: 'https://blog.cloudflare.com/rss/',
@@ -272,7 +272,7 @@ const defaultSources = <NewsSource>[
description: 'Networking, security and incident write-ups in depth',
enabledByDefault: true,
),
NewsSource(
Source(
id: 'netflix-techblog',
name: 'Netflix Technology Blog',
feedUrl: 'https://netflixtechblog.com/feed',
@@ -282,7 +282,7 @@ const defaultSources = <NewsSource>[
description: 'Distributed systems, data and streaming at scale',
enabledByDefault: true,
),
NewsSource(
Source(
id: 'meta-engineering',
name: 'Engineering at Meta',
feedUrl: 'https://engineering.fb.com/feed/',
@@ -292,7 +292,7 @@ const defaultSources = <NewsSource>[
description: 'Infrastructure, AI and production engineering at Meta',
enabledByDefault: true,
),
NewsSource(
Source(
id: 'github-engineering',
name: 'GitHub Blog: Engineering',
feedUrl: 'https://github.blog/engineering/feed/',
@@ -302,7 +302,7 @@ const defaultSources = <NewsSource>[
description: 'How GitHub builds and runs GitHub',
enabledByDefault: true,
),
NewsSource(
Source(
id: 'latent-space',
name: 'Latent Space',
feedUrl: 'https://www.latent.space/feed',
@@ -312,7 +312,7 @@ const defaultSources = <NewsSource>[
description: 'AI engineering: interviews and deep dives',
enabledByDefault: true,
),
NewsSource(
Source(
id: 'import-ai',
name: 'Import AI',
feedUrl: 'https://importai.substack.com/feed',
@@ -322,7 +322,7 @@ const defaultSources = <NewsSource>[
description: 'Weekly AI research digest by Jack Clark',
enabledByDefault: true,
),
NewsSource(
Source(
id: 'interconnects',
name: 'Interconnects',
feedUrl: 'https://www.interconnects.ai/feed',
@@ -332,7 +332,7 @@ const defaultSources = <NewsSource>[
description: 'Language model training and research, analysed',
enabledByDefault: true,
),
NewsSource(
Source(
id: 'ahead-of-ai',
name: 'Ahead of AI',
feedUrl: 'https://magazine.sebastianraschka.com/feed',
@@ -342,7 +342,7 @@ const defaultSources = <NewsSource>[
description: 'LLM research explained, by Sebastian Raschka',
enabledByDefault: true,
),
NewsSource(
Source(
id: 'lilianweng',
name: 'Lilian Weng',
feedUrl: 'https://lilianweng.github.io/index.xml',
@@ -352,7 +352,7 @@ const defaultSources = <NewsSource>[
description: 'Thorough surveys of machine learning topics',
enabledByDefault: true,
),
NewsSource(
Source(
id: 'infoq',
name: 'InfoQ',
feedUrl: 'https://feed.infoq.com/',
@@ -363,7 +363,7 @@ const defaultSources = <NewsSource>[
),
// --- Fun ----------------------------------------------------------------
NewsSource(
Source(
id: 'xkcd',
name: 'xkcd',
feedUrl: 'https://xkcd.com/atom.xml',
@@ -373,7 +373,7 @@ const defaultSources = <NewsSource>[
description: 'Webcomic of romance, sarcasm, math and language',
enabledByDefault: true,
),
NewsSource(
Source(
id: 'thedailywtf',
name: 'The Daily WTF',
feedUrl: 'https://thedailywtf.com/rss',
@@ -383,7 +383,7 @@ const defaultSources = <NewsSource>[
description: 'IT horror stories, bad code and error messages',
enabledByDefault: true,
),
NewsSource(
Source(
id: 'programmerhumor',
name: 'r/ProgrammerHumor',
feedUrl: 'https://www.reddit.com/r/ProgrammerHumor/top/.rss?t=day',
+1 -1
View File
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart' show ThemeMode;
import 'news_source.dart';
import 'source.dart';
enum AiProvider {
none('Off'),
+2 -2
View File
@@ -1,4 +1,4 @@
import 'news_source.dart';
import 'source.dart';
class Article {
const Article({
@@ -14,7 +14,7 @@ class Article {
final String title;
final String link;
final NewsSource source;
final Source source;
final DateTime? published;
/// Short plain-text teaser from the feed (description / summary).
@@ -20,9 +20,9 @@ enum SourceKind {
final String label;
}
/// A Source of any [kind]; the class name predates the kinds.
class NewsSource {
const NewsSource({
/// A Source of any [kind]; see GLOSSARY.md.
class Source {
const Source({
required this.id,
required this.name,
required this.feedUrl,
@@ -59,7 +59,7 @@ class NewsSource {
'description': description,
};
factory NewsSource.custom(Map<String, dynamic> json) => NewsSource(
factory Source.custom(Map<String, dynamic> json) => Source(
id: json['id'] as String,
name: json['name'] as String,
feedUrl: json['feedUrl'] as String,
+1 -1
View File
@@ -5,7 +5,7 @@ import 'package:http/http.dart' as http;
import '../models/article.dart';
import '../models/briefing.dart';
import '../models/news_source.dart';
import '../models/source.dart';
import 'summarizer.dart';
/// Calls the Claude Messages API directly over HTTP (there is no official
+4 -4
View File
@@ -3,13 +3,13 @@ import 'package:html/parser.dart' as html_parser;
import 'package:xml/xml.dart';
import '../models/article.dart';
import '../models/news_source.dart';
import '../models/source.dart';
/// Parses RSS 2.0, RSS 1.0 (RDF) and Atom feeds into [Article]s.
class FeedParser {
static const excerptLength = 400;
static List<Article> parse(String body, NewsSource source) {
static List<Article> parse(String body, Source source) {
final doc = XmlDocument.parse(body);
final root = doc.rootElement;
if (root.name.local == 'feed') {
@@ -27,7 +27,7 @@ class FeedParser {
.toList();
}
static Article? _parseRssItem(XmlElement item, NewsSource source) {
static Article? _parseRssItem(XmlElement item, Source source) {
final title = htmlToText(_text(item, 'title'));
final link = _text(item, 'link').trim().isNotEmpty
? _text(item, 'link').trim()
@@ -53,7 +53,7 @@ class FeedParser {
);
}
static Article? _parseAtomEntry(XmlElement entry, NewsSource source) {
static Article? _parseAtomEntry(XmlElement entry, Source source) {
final title = htmlToText(_text(entry, 'title'));
final links = entry.childElements.where((e) => e.name.local == 'link');
final link = links
+4 -4
View File
@@ -4,7 +4,7 @@ import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import '../models/article.dart';
import '../models/news_source.dart';
import '../models/source.dart';
import 'feed_parser.dart';
class FeedResult {
@@ -30,8 +30,8 @@ class FeedService {
/// [corsProxy] is only used on web, where browsers block cross-origin feed
/// requests. It is prepended to the feed URL, e.g. `https://proxy/?url=`.
Future<FeedResult> fetchAll(
List<NewsSource> sources, {
required Duration Function(NewsSource source) maxAgeOf,
List<Source> sources, {
required Duration Function(Source source) maxAgeOf,
String corsProxy = '',
}) async {
final errors = <String, String>{};
@@ -64,7 +64,7 @@ class FeedService {
return FeedResult(articles, errors);
}
Future<List<Article>> fetchSource(NewsSource source, {String corsProxy = ''}) async {
Future<List<Article>> fetchSource(Source source, {String corsProxy = ''}) async {
final url = kIsWeb && corsProxy.isNotEmpty
? '$corsProxy${Uri.encodeComponent(source.feedUrl)}'
: source.feedUrl;
+1 -1
View File
@@ -4,7 +4,7 @@ import 'package:http/http.dart' as http;
import '../models/article.dart';
import '../models/briefing.dart';
import '../models/news_source.dart';
import '../models/source.dart';
import 'summarizer.dart';
/// Delegates summarizing to an existing n8n workflow via a Webhook trigger.
+4 -4
View File
@@ -9,7 +9,7 @@ import 'package:shared_preferences/shared_preferences.dart';
import '../models/app_settings.dart';
import '../models/briefing.dart';
import '../models/news_source.dart';
import '../models/source.dart';
/// Persists settings, source selection and the last briefing.
/// The Claude API key goes to the platform keystore, everything else to
@@ -81,15 +81,15 @@ class Storage {
Future<void> saveSeenSourceIds(Set<String> ids) =>
_prefs.setStringList('seen_sources', ids.toList());
List<NewsSource> loadCustomSources() {
List<Source> loadCustomSources() {
final raw = _prefs.getString('custom_sources');
if (raw == null) return [];
return (jsonDecode(raw) as List)
.map((j) => NewsSource.custom(j as Map<String, dynamic>))
.map((j) => Source.custom(j as Map<String, dynamic>))
.toList();
}
Future<void> saveCustomSources(List<NewsSource> sources) => _prefs.setString(
Future<void> saveCustomSources(List<Source> sources) => _prefs.setString(
'custom_sources', jsonEncode(sources.map((s) => s.toJson()).toList()));
/// Article URL -> whether its page said it's paid, and when that was checked.
+1 -1
View File
@@ -1,6 +1,6 @@
import '../models/article.dart';
import '../models/briefing.dart';
import '../models/news_source.dart';
import '../models/source.dart';
class SummarizerException implements Exception {
SummarizerException(this.message);
+10 -10
View File
@@ -6,7 +6,7 @@ import '../data/default_sources.dart';
import '../models/app_settings.dart';
import '../models/article.dart';
import '../models/briefing.dart';
import '../models/news_source.dart';
import '../models/source.dart';
import '../services/article_extractor.dart';
import '../services/claude_summarizer.dart';
import '../services/consent_service.dart';
@@ -33,7 +33,7 @@ class AppState extends ChangeNotifier {
this._storage, {
FeedService? feedService,
ArticleExtractor? extractor,
List<NewsSource> curatedSources = defaultSources,
List<Source> curatedSources = defaultSources,
Summarizer? summarizer,
}) : _feeds = feedService ?? FeedService(),
_curated = curatedSources,
@@ -47,7 +47,7 @@ class AppState extends ChangeNotifier {
final FeedService _feeds;
/// The curated catalog; [defaultSources] outside of tests.
final List<NewsSource> _curated;
final List<Source> _curated;
/// Used instead of the AI set up in [settings]; for tests.
final Summarizer? _summarizerOverride;
@@ -58,7 +58,7 @@ class AppState extends ChangeNotifier {
final ConsentService consent;
AppSettings settings = const AppSettings();
List<NewsSource> customSources = [];
List<Source> customSources = [];
Set<String> enabledSourceIds = {};
/// Everything the feeds returned; see [articlesOf] for what is shown.
@@ -99,9 +99,9 @@ class AppState extends ChangeNotifier {
SourceKind.fun => const Duration(days: 3),
};
List<NewsSource> get allSources => [..._curated, ...customSources];
List<Source> get allSources => [..._curated, ...customSources];
List<NewsSource> get enabledSources =>
List<Source> get enabledSources =>
allSources.where((s) => enabledSourceIds.contains(s.id)).toList();
Future<void> init() async {
@@ -156,7 +156,7 @@ class AppState extends ChangeNotifier {
await _storage.saveEnabledSourceIds(enabledSourceIds);
}
Future<void> setSourceEnabled(NewsSource source, bool enabled) async {
Future<void> setSourceEnabled(Source source, bool enabled) async {
enabled ? enabledSourceIds.add(source.id) : enabledSourceIds.remove(source.id);
notifyListeners();
await _storage.saveEnabledSourceIds(enabledSourceIds);
@@ -171,7 +171,7 @@ class AppState extends ChangeNotifier {
SourceKind kind = SourceKind.news,
}) async {
final uri = Uri.tryParse(feedUrl);
final source = NewsSource(
final source = Source(
id: 'custom-${DateTime.now().millisecondsSinceEpoch}',
name: name,
feedUrl: feedUrl,
@@ -194,7 +194,7 @@ class AppState extends ChangeNotifier {
return null;
}
Future<void> removeCustomSource(NewsSource source) async {
Future<void> removeCustomSource(Source source) async {
customSources = customSources.where((s) => s.id != source.id).toList();
enabledSourceIds.remove(source.id);
await _storage.saveCustomSources(customSources);
@@ -235,7 +235,7 @@ class AppState extends ChangeNotifier {
}
final source = allSources.firstWhere(
(s) => s.name == sourceName,
orElse: () => NewsSource(
orElse: () => Source(
id: sourceName,
name: sourceName,
feedUrl: '',
+1 -1
View File
@@ -6,7 +6,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import '../models/article.dart';
import '../models/news_source.dart';
import '../models/source.dart';
import '../services/article_extractor.dart';
import '../services/consent_service.dart';
import '../services/content_blocking.dart';
+1 -1
View File
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import '../models/briefing.dart';
import '../models/news_source.dart';
import '../models/source.dart';
import '../state/app_state.dart';
import 'article_screen.dart';
import 'briefing_history_screen.dart';
+3 -3
View File
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import '../models/article.dart';
import '../models/news_source.dart';
import '../models/source.dart';
import '../state/app_state.dart';
import 'article_screen.dart';
import 'common.dart';
@@ -113,7 +113,7 @@ class _FeedScreenState extends State<FeedScreen> {
);
}
Widget _filterBar(List<NewsSource> sources) {
Widget _filterBar(List<Source> sources) {
sources.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
return SizedBox(
height: 52,
@@ -157,7 +157,7 @@ class _FeedScreenState extends State<FeedScreen> {
);
}
static const _placeholder = NewsSource(
static const _placeholder = Source(
id: '', name: 'All sources', feedUrl: '', homepage: '', language: SourceLanguage.en);
}
+1 -1
View File
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import '../models/news_source.dart';
import '../models/source.dart';
import '../state/app_state.dart';
import 'article_screen.dart';
import 'feed_screen.dart';
+1 -1
View File
@@ -2,7 +2,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import '../models/app_settings.dart';
import '../models/news_source.dart';
import '../models/source.dart';
import '../state/app_state.dart';
import 'common.dart';
+2 -2
View File
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import '../models/news_source.dart';
import '../models/source.dart';
import '../state/app_state.dart';
class SourcesScreen extends StatelessWidget {
@@ -109,7 +109,7 @@ class _SourceTile extends StatelessWidget {
this.onDelete,
});
final NewsSource source;
final Source source;
final bool enabled;
final ValueChanged<bool> onToggle;
final VoidCallback? onDelete;