class FeedItem { final int? id; final String title; final String description; // the extracted article body — may be empty for older rows or sources // that havent been backfilled yet. final String content; final String link; final String? source; final DateTime? pubDate; FeedItem({ this.id, required this.title, required this.description, this.content = "", required this.link, this.source, this.pubDate, }); @override String toString() => 'FeedItem(title: $title, link: $link)'; FeedItem.fromJson(Map json) : id = json['id'] is int ? json['id'] as int : int.tryParse('${json['id']}'), title = json['title'] ?? '', description = json['description'] ?? '', content = (json['content'] ?? '').toString(), link = json['link'] ?? '', source = json['source'], pubDate = json['pub_date'] != null ? DateTime.tryParse(json['pub_date']) : null; Map toJson() { return { if (id != null) 'id': id, 'title': title, 'description': description, if (content.isNotEmpty) 'content': content, 'link': link, if (source != null) 'source': source, if (pubDate != null) 'pub_date': pubDate!.toIso8601String(), }; } } // median pub date of a cluster — falls back to now if none have dates DateTime medianPubDate(List articles) { final dates = articles .where((a) => a.pubDate != null) .map((a) => a.pubDate!) .toList() ..sort(); if (dates.isEmpty) return DateTime.now(); return dates[dates.length ~/ 2]; }