🐛 Fix nullable link preview meta fields

This commit is contained in:
LittleSheep 2025-06-21 16:52:10 +08:00
parent 4ba809a8d6
commit b39248cc58
4 changed files with 59 additions and 57 deletions

View File

@ -9,13 +9,13 @@ sealed class SnEmbedLink with _$SnEmbedLink {
@JsonKey(name: 'Type') required String type, @JsonKey(name: 'Type') required String type,
@JsonKey(name: 'Url') required String url, @JsonKey(name: 'Url') required String url,
@JsonKey(name: 'Title') required String title, @JsonKey(name: 'Title') required String title,
@JsonKey(name: 'Description') required String description, @JsonKey(name: 'Description') required String? description,
@JsonKey(name: 'ImageUrl') required String imageUrl, @JsonKey(name: 'ImageUrl') required String? imageUrl,
@JsonKey(name: 'FaviconUrl') required String faviconUrl, @JsonKey(name: 'FaviconUrl') required String faviconUrl,
@JsonKey(name: 'SiteName') required String siteName, @JsonKey(name: 'SiteName') required String siteName,
@JsonKey(name: 'ContentType') required String contentType, @JsonKey(name: 'ContentType') required String? contentType,
@JsonKey(name: 'Author') required dynamic author, @JsonKey(name: 'Author') required String? author,
@JsonKey(name: 'PublishedDate') required dynamic publishedDate, @JsonKey(name: 'PublishedDate') required DateTime? publishedDate,
}) = _SnEmbedLink; }) = _SnEmbedLink;
factory SnEmbedLink.fromJson(Map<String, dynamic> json) => factory SnEmbedLink.fromJson(Map<String, dynamic> json) =>

View File

@ -16,7 +16,7 @@ T _$identity<T>(T value) => value;
/// @nodoc /// @nodoc
mixin _$SnEmbedLink { 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 /// Create a copy of SnEmbedLink
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@ -29,12 +29,12 @@ $SnEmbedLinkCopyWith<SnEmbedLink> get copyWith => _$SnEmbedLinkCopyWithImpl<SnEm
@override @override
bool operator ==(Object other) { 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) @JsonKey(includeFromJson: false, includeToJson: false)
@override @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 @override
String toString() { String toString() {
@ -49,7 +49,7 @@ abstract mixin class $SnEmbedLinkCopyWith<$Res> {
factory $SnEmbedLinkCopyWith(SnEmbedLink value, $Res Function(SnEmbedLink) _then) = _$SnEmbedLinkCopyWithImpl; factory $SnEmbedLinkCopyWith(SnEmbedLink value, $Res Function(SnEmbedLink) _then) = _$SnEmbedLinkCopyWithImpl;
@useResult @useResult
$Res call({ $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 /// Create a copy of SnEmbedLink
/// with the given fields replaced by the non-null parameter values. /// 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( return _then(_self.copyWith(
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable 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,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,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,description: freezed == 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?,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?,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,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,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?,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 String?,publishedDate: freezed == publishedDate ? _self.publishedDate : publishedDate // ignore: cast_nullable_to_non_nullable
as dynamic, as DateTime?,
)); ));
} }
@ -95,13 +95,13 @@ class _SnEmbedLink implements SnEmbedLink {
@override@JsonKey(name: 'Type') final String type; @override@JsonKey(name: 'Type') final String type;
@override@JsonKey(name: 'Url') final String url; @override@JsonKey(name: 'Url') final String url;
@override@JsonKey(name: 'Title') final String title; @override@JsonKey(name: 'Title') final String title;
@override@JsonKey(name: 'Description') final String description; @override@JsonKey(name: 'Description') final String? description;
@override@JsonKey(name: 'ImageUrl') final String imageUrl; @override@JsonKey(name: 'ImageUrl') final String? imageUrl;
@override@JsonKey(name: 'FaviconUrl') final String faviconUrl; @override@JsonKey(name: 'FaviconUrl') final String faviconUrl;
@override@JsonKey(name: 'SiteName') final String siteName; @override@JsonKey(name: 'SiteName') final String siteName;
@override@JsonKey(name: 'ContentType') final String contentType; @override@JsonKey(name: 'ContentType') final String? contentType;
@override@JsonKey(name: 'Author') final dynamic author; @override@JsonKey(name: 'Author') final String? author;
@override@JsonKey(name: 'PublishedDate') final dynamic publishedDate; @override@JsonKey(name: 'PublishedDate') final DateTime? publishedDate;
/// Create a copy of SnEmbedLink /// Create a copy of SnEmbedLink
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@ -116,12 +116,12 @@ Map<String, dynamic> toJson() {
@override @override
bool operator ==(Object other) { 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) @JsonKey(includeFromJson: false, includeToJson: false)
@override @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 @override
String toString() { String toString() {
@ -136,7 +136,7 @@ abstract mixin class _$SnEmbedLinkCopyWith<$Res> implements $SnEmbedLinkCopyWith
factory _$SnEmbedLinkCopyWith(_SnEmbedLink value, $Res Function(_SnEmbedLink) _then) = __$SnEmbedLinkCopyWithImpl; factory _$SnEmbedLinkCopyWith(_SnEmbedLink value, $Res Function(_SnEmbedLink) _then) = __$SnEmbedLinkCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $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 /// Create a copy of SnEmbedLink
/// with the given fields replaced by the non-null parameter values. /// 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( return _then(_SnEmbedLink(
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable 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,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,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,description: freezed == 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?,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?,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,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,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?,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 String?,publishedDate: freezed == publishedDate ? _self.publishedDate : publishedDate // ignore: cast_nullable_to_non_nullable
as dynamic, as DateTime?,
)); ));
} }

View File

@ -10,13 +10,16 @@ _SnEmbedLink _$SnEmbedLinkFromJson(Map<String, dynamic> json) => _SnEmbedLink(
type: json['Type'] as String, type: json['Type'] as String,
url: json['Url'] as String, url: json['Url'] as String,
title: json['Title'] as String, title: json['Title'] as String,
description: json['Description'] as String, description: json['Description'] as String?,
imageUrl: json['ImageUrl'] as String, imageUrl: json['ImageUrl'] as String?,
faviconUrl: json['FaviconUrl'] as String, faviconUrl: json['FaviconUrl'] as String,
siteName: json['SiteName'] as String, siteName: json['SiteName'] as String,
contentType: json['ContentType'] as String, contentType: json['ContentType'] as String?,
author: json['Author'], author: json['Author'] as String?,
publishedDate: json['PublishedDate'], publishedDate:
json['PublishedDate'] == null
? null
: DateTime.parse(json['PublishedDate'] as String),
); );
Map<String, dynamic> _$SnEmbedLinkToJson(_SnEmbedLink instance) => Map<String, dynamic> _$SnEmbedLinkToJson(_SnEmbedLink instance) =>
@ -30,5 +33,5 @@ Map<String, dynamic> _$SnEmbedLinkToJson(_SnEmbedLink instance) =>
'SiteName': instance.siteName, 'SiteName': instance.siteName,
'ContentType': instance.contentType, 'ContentType': instance.contentType,
'Author': instance.author, 'Author': instance.author,
'PublishedDate': instance.publishedDate, 'PublishedDate': instance.publishedDate?.toIso8601String(),
}; };

View File

@ -1,4 +1,3 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:island/models/embed.dart'; import 'package:island/models/embed.dart';
@ -42,10 +41,10 @@ class EmbedLinkWidget extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Preview Image // Preview Image
if (link.imageUrl.isNotEmpty) if (link.imageUrl != null && link.imageUrl!.isNotEmpty)
AspectRatio( AspectRatio(
aspectRatio: 16 / 9, aspectRatio: 16 / 9,
child: UniversalImage(uri: link.imageUrl, fit: BoxFit.cover), child: UniversalImage(uri: link.imageUrl!, fit: BoxFit.cover),
), ),
// Content // Content
@ -117,9 +116,9 @@ class EmbedLinkWidget extends StatelessWidget {
], ],
// Description // Description
if (link.description.isNotEmpty) ...[ if (link.description != null && link.description!.isNotEmpty) ...[
Text( Text(
link.description, link.description!,
style: theme.textTheme.bodyMedium?.copyWith( style: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant, color: colorScheme.onSurfaceVariant,
), ),
@ -153,7 +152,7 @@ class EmbedLinkWidget extends StatelessWidget {
), ),
const Gap(4), const Gap(4),
Text( Text(
link.author.toString(), link.author!,
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant, color: colorScheme.onSurfaceVariant,
), ),
@ -169,7 +168,7 @@ class EmbedLinkWidget extends StatelessWidget {
), ),
const Gap(4), const Gap(4),
Text( Text(
_formatDate(link.publishedDate), _formatDate(link.publishedDate!),
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant, color: colorScheme.onSurfaceVariant,
), ),
@ -188,20 +187,20 @@ class EmbedLinkWidget extends StatelessWidget {
); );
} }
String _formatDate(dynamic date) { String _formatDate(DateTime date) {
if (date == null) return '';
try { try {
DateTime dateTime; final now = DateTime.now();
if (date is String) { final difference = now.difference(date);
dateTime = DateTime.parse(date);
} else if (date is DateTime) { if (difference.inDays == 0) {
dateTime = date; return 'Today';
} else if (difference.inDays == 1) {
return 'Yesterday';
} else if (difference.inDays < 7) {
return '${difference.inDays} days ago';
} else { } else {
return date.toString(); return '${date.day}/${date.month}/${date.year}';
} }
return DateFormat.yMMMd().format(dateTime);
} catch (e) { } catch (e) {
return date.toString(); return date.toString();
} }