diff --git a/lib/providers/link_preview.dart b/lib/providers/link_preview.dart index a238804..4f6b2fe 100644 --- a/lib/providers/link_preview.dart +++ b/lib/providers/link_preview.dart @@ -28,7 +28,7 @@ class SnLinkPreviewProvider { _cache[url] = meta; return meta; } catch (err) { - log('[LinkPreview] Failed to fetch $url ($target)'); + log('[LinkPreview] Failed to fetch $url ($target)...'); return null; } } diff --git a/lib/types/link.dart b/lib/types/link.dart index 0fa4f3f..c5f4c30 100644 --- a/lib/types/link.dart +++ b/lib/types/link.dart @@ -5,6 +5,8 @@ part 'link.freezed.dart'; @freezed class SnLinkMeta with _$SnLinkMeta { + const SnLinkMeta._(); + const factory SnLinkMeta({ required int id, required DateTime createdAt, @@ -17,7 +19,7 @@ class SnLinkMeta with _$SnLinkMeta { required String? image, required String? video, required String? audio, - required String description, + required String? description, required String? siteName, required String? type, }) = _SnLinkMeta; diff --git a/lib/types/link.freezed.dart b/lib/types/link.freezed.dart index 197dcd4..1f0bb7d 100644 --- a/lib/types/link.freezed.dart +++ b/lib/types/link.freezed.dart @@ -31,7 +31,7 @@ mixin _$SnLinkMeta { String? get image => throw _privateConstructorUsedError; String? get video => throw _privateConstructorUsedError; String? get audio => throw _privateConstructorUsedError; - String get description => throw _privateConstructorUsedError; + String? get description => throw _privateConstructorUsedError; String? get siteName => throw _privateConstructorUsedError; String? get type => throw _privateConstructorUsedError; @@ -63,7 +63,7 @@ abstract class $SnLinkMetaCopyWith<$Res> { String? image, String? video, String? audio, - String description, + String? description, String? siteName, String? type}); } @@ -94,7 +94,7 @@ class _$SnLinkMetaCopyWithImpl<$Res, $Val extends SnLinkMeta> Object? image = freezed, Object? video = freezed, Object? audio = freezed, - Object? description = null, + Object? description = freezed, Object? siteName = freezed, Object? type = freezed, }) { @@ -143,10 +143,10 @@ class _$SnLinkMetaCopyWithImpl<$Res, $Val extends SnLinkMeta> ? _value.audio : audio // ignore: cast_nullable_to_non_nullable as String?, - description: null == description + description: freezed == description ? _value.description : description // ignore: cast_nullable_to_non_nullable - as String, + as String?, siteName: freezed == siteName ? _value.siteName : siteName // ignore: cast_nullable_to_non_nullable @@ -179,7 +179,7 @@ abstract class _$$SnLinkMetaImplCopyWith<$Res> String? image, String? video, String? audio, - String description, + String? description, String? siteName, String? type}); } @@ -208,7 +208,7 @@ class __$$SnLinkMetaImplCopyWithImpl<$Res> Object? image = freezed, Object? video = freezed, Object? audio = freezed, - Object? description = null, + Object? description = freezed, Object? siteName = freezed, Object? type = freezed, }) { @@ -257,10 +257,10 @@ class __$$SnLinkMetaImplCopyWithImpl<$Res> ? _value.audio : audio // ignore: cast_nullable_to_non_nullable as String?, - description: null == description + description: freezed == description ? _value.description : description // ignore: cast_nullable_to_non_nullable - as String, + as String?, siteName: freezed == siteName ? _value.siteName : siteName // ignore: cast_nullable_to_non_nullable @@ -275,7 +275,7 @@ class __$$SnLinkMetaImplCopyWithImpl<$Res> /// @nodoc @JsonSerializable() -class _$SnLinkMetaImpl implements _SnLinkMeta { +class _$SnLinkMetaImpl extends _SnLinkMeta { const _$SnLinkMetaImpl( {required this.id, required this.createdAt, @@ -290,7 +290,8 @@ class _$SnLinkMetaImpl implements _SnLinkMeta { required this.audio, required this.description, required this.siteName, - required this.type}); + required this.type}) + : super._(); factory _$SnLinkMetaImpl.fromJson(Map json) => _$$SnLinkMetaImplFromJson(json); @@ -318,7 +319,7 @@ class _$SnLinkMetaImpl implements _SnLinkMeta { @override final String? audio; @override - final String description; + final String? description; @override final String? siteName; @override @@ -390,7 +391,7 @@ class _$SnLinkMetaImpl implements _SnLinkMeta { } } -abstract class _SnLinkMeta implements SnLinkMeta { +abstract class _SnLinkMeta extends SnLinkMeta { const factory _SnLinkMeta( {required final int id, required final DateTime createdAt, @@ -403,9 +404,10 @@ abstract class _SnLinkMeta implements SnLinkMeta { required final String? image, required final String? video, required final String? audio, - required final String description, + required final String? description, required final String? siteName, required final String? type}) = _$SnLinkMetaImpl; + const _SnLinkMeta._() : super._(); factory _SnLinkMeta.fromJson(Map json) = _$SnLinkMetaImpl.fromJson; @@ -433,7 +435,7 @@ abstract class _SnLinkMeta implements SnLinkMeta { @override String? get audio; @override - String get description; + String? get description; @override String? get siteName; @override diff --git a/lib/types/link.g.dart b/lib/types/link.g.dart index d813133..3629d5a 100644 --- a/lib/types/link.g.dart +++ b/lib/types/link.g.dart @@ -21,7 +21,7 @@ _$SnLinkMetaImpl _$$SnLinkMetaImplFromJson(Map json) => image: json['image'] as String?, video: json['video'] as String?, audio: json['audio'] as String?, - description: json['description'] as String, + description: json['description'] as String?, siteName: json['site_name'] as String?, type: json['type'] as String?, ); diff --git a/lib/widgets/link_preview.dart b/lib/widgets/link_preview.dart index 29bd10b..615f906 100644 --- a/lib/widgets/link_preview.dart +++ b/lib/widgets/link_preview.dart @@ -1,5 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:gap/gap.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:marquee/marquee.dart'; @@ -50,100 +51,120 @@ class _LinkPreviewWidgetState extends State { return Wrap( spacing: 8, runSpacing: 8, - children: _links - .map( - (e) => Container( - constraints: BoxConstraints( - maxWidth: ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE) ? double.infinity : 480, - ), - child: GestureDetector( - child: Card( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (e.image != null) - Container( - margin: const EdgeInsets.only(bottom: 4), - color: Theme.of(context).colorScheme.surfaceContainer, - child: AspectRatio( - aspectRatio: 16 / 9, - child: ClipRRect( - child: AutoResizeUniversalImage( - e.image!, - fit: BoxFit.contain, - ), - ), - ), - ), - SizedBox( - height: 48, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (e.icon != null) - UniversalImage( - e.icon!, + children: _links.map((e) => _LinkPreviewEntry(meta: e)).toList(), + ); + } +} + +class _LinkPreviewEntry extends StatelessWidget { + final SnLinkMeta meta; + + const _LinkPreviewEntry({ + super.key, + required this.meta, + }); + + @override + Widget build(BuildContext context) { + return Container( + constraints: BoxConstraints( + maxWidth: ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE) ? double.infinity : 480, + ), + child: GestureDetector( + child: Card( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (meta.image != null) + Container( + margin: const EdgeInsets.only(bottom: 4), + color: Theme.of(context).colorScheme.surfaceContainer, + child: AspectRatio( + aspectRatio: 16 / 9, + child: ClipRRect( + child: AutoResizeUniversalImage( + meta.image!, + fit: BoxFit.contain, + ), + ), + ), + ), + SizedBox( + height: 48, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (meta.icon?.isNotEmpty ?? false) + StyledWidget( + meta.icon!.endsWith('.svg') + ? SvgPicture.network(meta.icon!) + : UniversalImage( + meta.icon!, width: 36, height: 36, cacheHeight: 36, cacheWidth: 36, - ).padding(all: 4), - const Gap(12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox( - height: 24, - child: Marquee( - text: e.title ?? 'unknown'.tr(), - style: TextStyle(fontSize: 17, height: 1), - scrollAxis: Axis.horizontal, - showFadingOnlyWhenScrolling: true, - pauseAfterRound: const Duration(seconds: 3), - ), - ), - if (e.siteName != null) - Text( - e.siteName!, - style: TextStyle(fontSize: 13, height: 0.9), - ).fontSize(11), - ], ), - ), - const Gap(6), - ], - ).padding(horizontal: 16), + ).padding(all: 4, right: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + height: 24, + child: ((meta.title?.length ?? 0) > 40) + ? Marquee( + text: meta.title ?? 'unknown'.tr(), + style: TextStyle(fontSize: 17, height: 1), + scrollAxis: Axis.horizontal, + showFadingOnlyWhenScrolling: true, + pauseAfterRound: const Duration(seconds: 3), + ) + : Text( + meta.title ?? 'unknown'.tr(), + style: TextStyle(fontSize: 17, height: 1), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + if (meta.siteName != null) + Text( + meta.siteName!, + style: TextStyle(fontSize: 13, height: 0.9), + ).fontSize(11), + ], ), - Text( - e.description, - maxLines: 3, - overflow: TextOverflow.ellipsis, - ).padding(horizontal: 16), - const Gap(8), - Text( - e.url, - style: GoogleFonts.roboto(fontSize: 11, height: 0.9), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ).opacity(0.75).padding(horizontal: 16), - const Gap(4), - Text( - 'poweredBy'.tr(args: ['HyperNet.Reader']), - style: GoogleFonts.roboto(fontSize: 11, height: 0.9), - ).opacity(0.75).padding(horizontal: 16), - const Gap(16), - ], - ), - ), - onTap: () { - launchUrlString(e.url, mode: LaunchMode.externalApplication); - }, + ), + const Gap(6), + ], + ).padding(horizontal: 16), ), - ), - ) - .toList(), + if (meta.description != null) + Text( + meta.description!, + maxLines: 3, + overflow: TextOverflow.ellipsis, + ).padding(horizontal: 16, bottom: 8), + Text( + meta.url, + style: GoogleFonts.roboto(fontSize: 11, height: 0.9), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ).opacity(0.75).padding(horizontal: 16), + const Gap(4), + Text( + 'poweredBy'.tr(args: ['HyperNet.Reader']), + style: GoogleFonts.roboto(fontSize: 11, height: 0.9), + ).opacity(0.75).padding(horizontal: 16), + const Gap(16), + ], + ), + ), + onTap: () { + launchUrlString(meta.url, mode: LaunchMode.externalApplication); + }, + ), ); } } diff --git a/lib/widgets/post/post_item.dart b/lib/widgets/post/post_item.dart index 611e793..5dc98e6 100644 --- a/lib/widgets/post/post_item.dart +++ b/lib/widgets/post/post_item.dart @@ -24,6 +24,7 @@ import 'package:surface/types/reaction.dart'; import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/attachment/attachment_list.dart'; import 'package:surface/widgets/dialog.dart'; +import 'package:surface/widgets/link_preview.dart'; import 'package:surface/widgets/markdown_content.dart'; import 'package:gap/gap.dart'; import 'package:surface/widgets/post/post_comment_list.dart'; @@ -103,7 +104,7 @@ class PostItem extends StatelessWidget { ).create(); await imageFile.writeAsBytes(capturedImage); - if(!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { + if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { await Share.shareXFiles( [XFile(imageFile.path)], sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, @@ -219,11 +220,12 @@ class PostItem extends StatelessWidget { data: data, isEnlarge: data.type == 'article' && showFullPost, ).padding(horizontal: 16, bottom: 8), - _PostContentBody( - data: data, - isSelectable: showFullPost, - isEnlarge: data.type == 'article' && showFullPost, - ).padding(horizontal: 16, bottom: 6), + if (data.body['content']?.isNotEmpty ?? false) + _PostContentBody( + data: data, + isSelectable: showFullPost, + isEnlarge: data.type == 'article' && showFullPost, + ).padding(horizontal: 16, bottom: 6), if (data.repostTo != null) _PostQuoteContent(child: data.repostTo!).padding( horizontal: 12, @@ -250,6 +252,10 @@ class PostItem extends StatelessWidget { maxHeight: 560, listPadding: const EdgeInsets.symmetric(horizontal: 12), ), + if (data.body['content'] != null) + LinkPreviewWidget( + text: data.body['content'], + ).padding(horizontal: 4), Container( constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity), child: Column( @@ -315,10 +321,11 @@ class PostShareImageWidget extends StatelessWidget { data: data, isEnlarge: data.type == 'article', ).padding(horizontal: 16, bottom: 8), - _PostContentBody( - data: data, - isEnlarge: data.type == 'article', - ).padding(horizontal: 16, bottom: 8), + if (data.body['content']?.isNotEmpty ?? false) + _PostContentBody( + data: data, + isEnlarge: data.type == 'article', + ).padding(horizontal: 16, bottom: 8), if (data.repostTo != null) _PostQuoteContent( child: data.repostTo!, @@ -330,6 +337,10 @@ class PostShareImageWidget extends StatelessWidget { data: data.preload!.attachments!, isFlatted: true, ).padding(horizontal: 16, bottom: 8), + if (data.body['content'] != null) + LinkPreviewWidget( + text: data.body['content'], + ).padding(horizontal: 4), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -375,7 +386,7 @@ class PostShareImageWidget extends StatelessWidget { ], ), ), - if(data.body['content_truncated'] == true) + if (data.body['content_truncated'] == true) Text( 'postImageShareReadMore'.tr(), style: GoogleFonts.robotoMono(fontSize: 11), diff --git a/pubspec.lock b/pubspec.lock index c111153..94a1b2c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -712,6 +712,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.0" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: "54900a1a1243f3c4a5506d853a2b5c2dbc38d5f27e52a52618a8054401431123" + url: "https://pub.dev" + source: hosted + version: "2.0.16" flutter_test: dependency: "direct dev" description: flutter @@ -1242,6 +1250,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" path_provider: dependency: "direct main" description: @@ -1911,6 +1927,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.1" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "27d5fefe86fb9aace4a9f8375b56b3c292b64d8c04510df230f849850d912cb7" + url: "https://pub.dev" + source: hosted + version: "1.1.15" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "2430b973a4ca3c4dbc9999b62b8c719a160100dcbae5c819bae0cacce32c9cdb" + url: "https://pub.dev" + source: hosted + version: "1.1.12" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" + url: "https://pub.dev" + source: hosted + version: "1.1.16" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 571297d..e66c554 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 2.0.1+24 +version: 2.0.1+25 environment: sdk: ^3.5.4 @@ -103,6 +103,7 @@ dependencies: file_saver: ^0.2.14 device_info_plus: ^11.2.0 marquee: ^2.3.0 + flutter_svg: ^2.0.16 dev_dependencies: flutter_test: