From b39248cc58ef6ae9340699ec6e95ea2238982cd3 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 21 Jun 2025 16:52:10 +0800 Subject: [PATCH] :bug: Fix nullable link preview meta fields --- lib/models/embed.dart | 10 +++--- lib/models/embed.freezed.dart | 56 ++++++++++++++--------------- lib/models/embed.g.dart | 15 ++++---- lib/widgets/content/embed/link.dart | 35 +++++++++--------- 4 files changed, 59 insertions(+), 57 deletions(-) diff --git a/lib/models/embed.dart b/lib/models/embed.dart index 7e8329d..51c3906 100644 --- a/lib/models/embed.dart +++ b/lib/models/embed.dart @@ -9,13 +9,13 @@ sealed class SnEmbedLink with _$SnEmbedLink { @JsonKey(name: 'Type') required String type, @JsonKey(name: 'Url') required String url, @JsonKey(name: 'Title') required String title, - @JsonKey(name: 'Description') required String description, - @JsonKey(name: 'ImageUrl') required String imageUrl, + @JsonKey(name: 'Description') required String? description, + @JsonKey(name: 'ImageUrl') required String? imageUrl, @JsonKey(name: 'FaviconUrl') required String faviconUrl, @JsonKey(name: 'SiteName') required String siteName, - @JsonKey(name: 'ContentType') required String contentType, - @JsonKey(name: 'Author') required dynamic author, - @JsonKey(name: 'PublishedDate') required dynamic publishedDate, + @JsonKey(name: 'ContentType') required String? contentType, + @JsonKey(name: 'Author') required String? author, + @JsonKey(name: 'PublishedDate') required DateTime? publishedDate, }) = _SnEmbedLink; factory SnEmbedLink.fromJson(Map json) => diff --git a/lib/models/embed.freezed.dart b/lib/models/embed.freezed.dart index cc4b0c5..09da60d 100644 --- a/lib/models/embed.freezed.dart +++ b/lib/models/embed.freezed.dart @@ -16,7 +16,7 @@ T _$identity(T value) => value; /// @nodoc mixin _$SnEmbedLink { -@JsonKey(name: 'Type') String get type;@JsonKey(name: 'Url') String get url;@JsonKey(name: 'Title') String get title;@JsonKey(name: 'Description') String get description;@JsonKey(name: 'ImageUrl') String get imageUrl;@JsonKey(name: 'FaviconUrl') String get faviconUrl;@JsonKey(name: 'SiteName') String get siteName;@JsonKey(name: 'ContentType') String get contentType;@JsonKey(name: 'Author') dynamic get author;@JsonKey(name: 'PublishedDate') dynamic get publishedDate; +@JsonKey(name: 'Type') String get type;@JsonKey(name: 'Url') String get url;@JsonKey(name: 'Title') String get title;@JsonKey(name: 'Description') String? get description;@JsonKey(name: 'ImageUrl') String? get imageUrl;@JsonKey(name: 'FaviconUrl') String get faviconUrl;@JsonKey(name: 'SiteName') String get siteName;@JsonKey(name: 'ContentType') String? get contentType;@JsonKey(name: 'Author') String? get author;@JsonKey(name: 'PublishedDate') DateTime? get publishedDate; /// Create a copy of SnEmbedLink /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -29,12 +29,12 @@ $SnEmbedLinkCopyWith get copyWith => _$SnEmbedLinkCopyWithImpl Object.hash(runtimeType,type,url,title,description,imageUrl,faviconUrl,siteName,contentType,const DeepCollectionEquality().hash(author),const DeepCollectionEquality().hash(publishedDate)); +int get hashCode => Object.hash(runtimeType,type,url,title,description,imageUrl,faviconUrl,siteName,contentType,author,publishedDate); @override String toString() { @@ -49,7 +49,7 @@ abstract mixin class $SnEmbedLinkCopyWith<$Res> { factory $SnEmbedLinkCopyWith(SnEmbedLink value, $Res Function(SnEmbedLink) _then) = _$SnEmbedLinkCopyWithImpl; @useResult $Res call({ -@JsonKey(name: 'Type') String type,@JsonKey(name: 'Url') String url,@JsonKey(name: 'Title') String title,@JsonKey(name: 'Description') String description,@JsonKey(name: 'ImageUrl') String imageUrl,@JsonKey(name: 'FaviconUrl') String faviconUrl,@JsonKey(name: 'SiteName') String siteName,@JsonKey(name: 'ContentType') String contentType,@JsonKey(name: 'Author') dynamic author,@JsonKey(name: 'PublishedDate') dynamic publishedDate +@JsonKey(name: 'Type') String type,@JsonKey(name: 'Url') String url,@JsonKey(name: 'Title') String title,@JsonKey(name: 'Description') String? description,@JsonKey(name: 'ImageUrl') String? imageUrl,@JsonKey(name: 'FaviconUrl') String faviconUrl,@JsonKey(name: 'SiteName') String siteName,@JsonKey(name: 'ContentType') String? contentType,@JsonKey(name: 'Author') String? author,@JsonKey(name: 'PublishedDate') DateTime? publishedDate }); @@ -66,19 +66,19 @@ class _$SnEmbedLinkCopyWithImpl<$Res> /// Create a copy of SnEmbedLink /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? type = null,Object? url = null,Object? title = null,Object? description = null,Object? imageUrl = null,Object? faviconUrl = null,Object? siteName = null,Object? contentType = null,Object? author = freezed,Object? publishedDate = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? type = null,Object? url = null,Object? title = null,Object? description = freezed,Object? imageUrl = freezed,Object? faviconUrl = null,Object? siteName = null,Object? contentType = freezed,Object? author = freezed,Object? publishedDate = freezed,}) { return _then(_self.copyWith( type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable -as String,description: null == description ? _self.description : description // ignore: cast_nullable_to_non_nullable -as String,imageUrl: null == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable -as String,faviconUrl: null == faviconUrl ? _self.faviconUrl : faviconUrl // ignore: cast_nullable_to_non_nullable +as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable +as String?,imageUrl: freezed == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable +as String?,faviconUrl: null == faviconUrl ? _self.faviconUrl : faviconUrl // ignore: cast_nullable_to_non_nullable as String,siteName: null == siteName ? _self.siteName : siteName // ignore: cast_nullable_to_non_nullable -as String,contentType: null == contentType ? _self.contentType : contentType // ignore: cast_nullable_to_non_nullable -as String,author: freezed == author ? _self.author : author // ignore: cast_nullable_to_non_nullable -as dynamic,publishedDate: freezed == publishedDate ? _self.publishedDate : publishedDate // ignore: cast_nullable_to_non_nullable -as dynamic, +as String,contentType: freezed == contentType ? _self.contentType : contentType // ignore: cast_nullable_to_non_nullable +as String?,author: freezed == author ? _self.author : author // ignore: cast_nullable_to_non_nullable +as String?,publishedDate: freezed == publishedDate ? _self.publishedDate : publishedDate // ignore: cast_nullable_to_non_nullable +as DateTime?, )); } @@ -95,13 +95,13 @@ class _SnEmbedLink implements SnEmbedLink { @override@JsonKey(name: 'Type') final String type; @override@JsonKey(name: 'Url') final String url; @override@JsonKey(name: 'Title') final String title; -@override@JsonKey(name: 'Description') final String description; -@override@JsonKey(name: 'ImageUrl') final String imageUrl; +@override@JsonKey(name: 'Description') final String? description; +@override@JsonKey(name: 'ImageUrl') final String? imageUrl; @override@JsonKey(name: 'FaviconUrl') final String faviconUrl; @override@JsonKey(name: 'SiteName') final String siteName; -@override@JsonKey(name: 'ContentType') final String contentType; -@override@JsonKey(name: 'Author') final dynamic author; -@override@JsonKey(name: 'PublishedDate') final dynamic publishedDate; +@override@JsonKey(name: 'ContentType') final String? contentType; +@override@JsonKey(name: 'Author') final String? author; +@override@JsonKey(name: 'PublishedDate') final DateTime? publishedDate; /// Create a copy of SnEmbedLink /// with the given fields replaced by the non-null parameter values. @@ -116,12 +116,12 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnEmbedLink&&(identical(other.type, type) || other.type == type)&&(identical(other.url, url) || other.url == url)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.imageUrl, imageUrl) || other.imageUrl == imageUrl)&&(identical(other.faviconUrl, faviconUrl) || other.faviconUrl == faviconUrl)&&(identical(other.siteName, siteName) || other.siteName == siteName)&&(identical(other.contentType, contentType) || other.contentType == contentType)&&const DeepCollectionEquality().equals(other.author, author)&&const DeepCollectionEquality().equals(other.publishedDate, publishedDate)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnEmbedLink&&(identical(other.type, type) || other.type == type)&&(identical(other.url, url) || other.url == url)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.imageUrl, imageUrl) || other.imageUrl == imageUrl)&&(identical(other.faviconUrl, faviconUrl) || other.faviconUrl == faviconUrl)&&(identical(other.siteName, siteName) || other.siteName == siteName)&&(identical(other.contentType, contentType) || other.contentType == contentType)&&(identical(other.author, author) || other.author == author)&&(identical(other.publishedDate, publishedDate) || other.publishedDate == publishedDate)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,type,url,title,description,imageUrl,faviconUrl,siteName,contentType,const DeepCollectionEquality().hash(author),const DeepCollectionEquality().hash(publishedDate)); +int get hashCode => Object.hash(runtimeType,type,url,title,description,imageUrl,faviconUrl,siteName,contentType,author,publishedDate); @override String toString() { @@ -136,7 +136,7 @@ abstract mixin class _$SnEmbedLinkCopyWith<$Res> implements $SnEmbedLinkCopyWith factory _$SnEmbedLinkCopyWith(_SnEmbedLink value, $Res Function(_SnEmbedLink) _then) = __$SnEmbedLinkCopyWithImpl; @override @useResult $Res call({ -@JsonKey(name: 'Type') String type,@JsonKey(name: 'Url') String url,@JsonKey(name: 'Title') String title,@JsonKey(name: 'Description') String description,@JsonKey(name: 'ImageUrl') String imageUrl,@JsonKey(name: 'FaviconUrl') String faviconUrl,@JsonKey(name: 'SiteName') String siteName,@JsonKey(name: 'ContentType') String contentType,@JsonKey(name: 'Author') dynamic author,@JsonKey(name: 'PublishedDate') dynamic publishedDate +@JsonKey(name: 'Type') String type,@JsonKey(name: 'Url') String url,@JsonKey(name: 'Title') String title,@JsonKey(name: 'Description') String? description,@JsonKey(name: 'ImageUrl') String? imageUrl,@JsonKey(name: 'FaviconUrl') String faviconUrl,@JsonKey(name: 'SiteName') String siteName,@JsonKey(name: 'ContentType') String? contentType,@JsonKey(name: 'Author') String? author,@JsonKey(name: 'PublishedDate') DateTime? publishedDate }); @@ -153,19 +153,19 @@ class __$SnEmbedLinkCopyWithImpl<$Res> /// Create a copy of SnEmbedLink /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? url = null,Object? title = null,Object? description = null,Object? imageUrl = null,Object? faviconUrl = null,Object? siteName = null,Object? contentType = null,Object? author = freezed,Object? publishedDate = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? url = null,Object? title = null,Object? description = freezed,Object? imageUrl = freezed,Object? faviconUrl = null,Object? siteName = null,Object? contentType = freezed,Object? author = freezed,Object? publishedDate = freezed,}) { return _then(_SnEmbedLink( type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable -as String,description: null == description ? _self.description : description // ignore: cast_nullable_to_non_nullable -as String,imageUrl: null == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable -as String,faviconUrl: null == faviconUrl ? _self.faviconUrl : faviconUrl // ignore: cast_nullable_to_non_nullable +as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable +as String?,imageUrl: freezed == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable +as String?,faviconUrl: null == faviconUrl ? _self.faviconUrl : faviconUrl // ignore: cast_nullable_to_non_nullable as String,siteName: null == siteName ? _self.siteName : siteName // ignore: cast_nullable_to_non_nullable -as String,contentType: null == contentType ? _self.contentType : contentType // ignore: cast_nullable_to_non_nullable -as String,author: freezed == author ? _self.author : author // ignore: cast_nullable_to_non_nullable -as dynamic,publishedDate: freezed == publishedDate ? _self.publishedDate : publishedDate // ignore: cast_nullable_to_non_nullable -as dynamic, +as String,contentType: freezed == contentType ? _self.contentType : contentType // ignore: cast_nullable_to_non_nullable +as String?,author: freezed == author ? _self.author : author // ignore: cast_nullable_to_non_nullable +as String?,publishedDate: freezed == publishedDate ? _self.publishedDate : publishedDate // ignore: cast_nullable_to_non_nullable +as DateTime?, )); } diff --git a/lib/models/embed.g.dart b/lib/models/embed.g.dart index e6a2ad3..57a5561 100644 --- a/lib/models/embed.g.dart +++ b/lib/models/embed.g.dart @@ -10,13 +10,16 @@ _SnEmbedLink _$SnEmbedLinkFromJson(Map json) => _SnEmbedLink( type: json['Type'] as String, url: json['Url'] as String, title: json['Title'] as String, - description: json['Description'] as String, - imageUrl: json['ImageUrl'] as String, + description: json['Description'] as String?, + imageUrl: json['ImageUrl'] as String?, faviconUrl: json['FaviconUrl'] as String, siteName: json['SiteName'] as String, - contentType: json['ContentType'] as String, - author: json['Author'], - publishedDate: json['PublishedDate'], + contentType: json['ContentType'] as String?, + author: json['Author'] as String?, + publishedDate: + json['PublishedDate'] == null + ? null + : DateTime.parse(json['PublishedDate'] as String), ); Map _$SnEmbedLinkToJson(_SnEmbedLink instance) => @@ -30,5 +33,5 @@ Map _$SnEmbedLinkToJson(_SnEmbedLink instance) => 'SiteName': instance.siteName, 'ContentType': instance.contentType, 'Author': instance.author, - 'PublishedDate': instance.publishedDate, + 'PublishedDate': instance.publishedDate?.toIso8601String(), }; diff --git a/lib/widgets/content/embed/link.dart b/lib/widgets/content/embed/link.dart index ca3ecf3..e26f68d 100644 --- a/lib/widgets/content/embed/link.dart +++ b/lib/widgets/content/embed/link.dart @@ -1,4 +1,3 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:island/models/embed.dart'; @@ -42,10 +41,10 @@ class EmbedLinkWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ // Preview Image - if (link.imageUrl.isNotEmpty) + if (link.imageUrl != null && link.imageUrl!.isNotEmpty) AspectRatio( aspectRatio: 16 / 9, - child: UniversalImage(uri: link.imageUrl, fit: BoxFit.cover), + child: UniversalImage(uri: link.imageUrl!, fit: BoxFit.cover), ), // Content @@ -117,9 +116,9 @@ class EmbedLinkWidget extends StatelessWidget { ], // Description - if (link.description.isNotEmpty) ...[ + if (link.description != null && link.description!.isNotEmpty) ...[ Text( - link.description, + link.description!, style: theme.textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -153,7 +152,7 @@ class EmbedLinkWidget extends StatelessWidget { ), const Gap(4), Text( - link.author.toString(), + link.author!, style: theme.textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -169,7 +168,7 @@ class EmbedLinkWidget extends StatelessWidget { ), const Gap(4), Text( - _formatDate(link.publishedDate), + _formatDate(link.publishedDate!), style: theme.textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -188,20 +187,20 @@ class EmbedLinkWidget extends StatelessWidget { ); } - String _formatDate(dynamic date) { - if (date == null) return ''; - + String _formatDate(DateTime date) { try { - DateTime dateTime; - if (date is String) { - dateTime = DateTime.parse(date); - } else if (date is DateTime) { - dateTime = date; + final now = DateTime.now(); + final difference = now.difference(date); + + if (difference.inDays == 0) { + return 'Today'; + } else if (difference.inDays == 1) { + return 'Yesterday'; + } else if (difference.inDays < 7) { + return '${difference.inDays} days ago'; } else { - return date.toString(); + return '${date.day}/${date.month}/${date.year}'; } - - return DateFormat.yMMMd().format(dateTime); } catch (e) { return date.toString(); }