Comment list

This commit is contained in:
LittleSheep 2024-11-11 20:06:00 +08:00
parent 9df9674ada
commit 1ff4dc2a4b
11 changed files with 233 additions and 81 deletions

View File

@ -91,6 +91,11 @@
"one": "{} comment", "one": "{} comment",
"other": "{} comments" "other": "{} comments"
}, },
"postCommentsDetailed": {
"zero": "No comments",
"one": "{} comment",
"other": "{} comments"
},
"settingsAppearance": "Appearance", "settingsAppearance": "Appearance",
"settingsBackgroundImage": "Background Image", "settingsBackgroundImage": "Background Image",
"settingsBackgroundImageDescription": "Set the background image that will be applied globally.", "settingsBackgroundImageDescription": "Set the background image that will be applied globally.",

View File

@ -91,6 +91,11 @@
"one": "{} 条评论", "one": "{} 条评论",
"other": "{} 条评论" "other": "{} 条评论"
}, },
"postCommentsDetailed": {
"zero": "没有评论",
"one": "{} 条评论",
"other": "{} 条评论"
},
"settingsAppearance": "外观", "settingsAppearance": "外观",
"settingsBackgroundImage": "背景图片", "settingsBackgroundImage": "背景图片",
"settingsBackgroundImageDescription": "设置应用全局生效的的背景图片。", "settingsBackgroundImageDescription": "设置应用全局生效的的背景图片。",

View File

@ -41,41 +41,18 @@ PODS:
- DKImagePickerController/PhotoGallery - DKImagePickerController/PhotoGallery
- Flutter - Flutter
- Flutter (1.0.0) - Flutter (1.0.0)
- flutter_image_compress_common (1.0.0):
- Flutter
- Mantle
- SDWebImage
- SDWebImageWebPCoder
- flutter_native_splash (0.0.1): - flutter_native_splash (0.0.1):
- Flutter - Flutter
- flutter_secure_storage (3.3.1): - flutter_secure_storage (3.3.1):
- Flutter - Flutter
- image_picker_ios (0.0.1): - image_picker_ios (0.0.1):
- Flutter - Flutter
- libwebp (1.3.2):
- libwebp/demux (= 1.3.2)
- libwebp/mux (= 1.3.2)
- libwebp/sharpyuv (= 1.3.2)
- libwebp/webp (= 1.3.2)
- libwebp/demux (1.3.2):
- libwebp/webp
- libwebp/mux (1.3.2):
- libwebp/demux
- libwebp/sharpyuv (1.3.2)
- libwebp/webp (1.3.2):
- libwebp/sharpyuv
- Mantle (2.2.0):
- Mantle/extobjc (= 2.2.0)
- Mantle/extobjc (2.2.0)
- path_provider_foundation (0.0.1): - path_provider_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- SDWebImage (5.19.7): - SDWebImage (5.19.7):
- SDWebImage/Core (= 5.19.7) - SDWebImage/Core (= 5.19.7)
- SDWebImage/Core (5.19.7) - SDWebImage/Core (5.19.7)
- SDWebImageWebPCoder (0.14.6):
- libwebp (~> 1.0)
- SDWebImage/Core (~> 5.17)
- shared_preferences_foundation (0.0.1): - shared_preferences_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
@ -92,7 +69,6 @@ DEPENDENCIES:
- cupertino_http (from `.symlinks/plugins/cupertino_http/ios`) - cupertino_http (from `.symlinks/plugins/cupertino_http/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
@ -105,10 +81,7 @@ SPEC REPOS:
trunk: trunk:
- DKImagePickerController - DKImagePickerController
- DKPhotoGallery - DKPhotoGallery
- libwebp
- Mantle
- SDWebImage - SDWebImage
- SDWebImageWebPCoder
- SwiftyGif - SwiftyGif
EXTERNAL SOURCES: EXTERNAL SOURCES:
@ -122,8 +95,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/file_picker/ios" :path: ".symlinks/plugins/file_picker/ios"
Flutter: Flutter:
:path: Flutter :path: Flutter
flutter_image_compress_common:
:path: ".symlinks/plugins/flutter_image_compress_common/ios"
flutter_native_splash: flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios" :path: ".symlinks/plugins/flutter_native_splash/ios"
flutter_secure_storage: flutter_secure_storage:
@ -147,15 +118,11 @@ SPEC CHECKSUMS:
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_image_compress_common: ec1d45c362c9d30a3f6a0426c297f47c52007e3e
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778 flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3 SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4

View File

@ -168,6 +168,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
SliverInfiniteList( SliverInfiniteList(
itemCount: _posts.length, itemCount: _posts.length,
isLoading: _isBusy, isLoading: _isBusy,
centerLoading: true,
hasReachedMax: _postCount != null && _posts.length >= _postCount!, hasReachedMax: _postCount != null && _posts.length >= _postCount!,
onFetchData: _fetchPosts, onFetchData: _fetchPosts,
itemBuilder: (context, idx) { itemBuilder: (context, idx) {
@ -182,8 +183,8 @@ class _ExploreScreenState extends State<ExploreScreen> {
}, },
); );
}, },
separatorBuilder: (context, index) => const Divider(), separatorBuilder: (context, index) => const Divider(height: 1),
) ),
], ],
), ),
), ),

View File

@ -2,6 +2,7 @@ import 'dart:math' as math;
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
@ -11,6 +12,7 @@ import 'package:surface/types/post.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/post/post_comment_list.dart';
import 'package:surface/widgets/post/post_item.dart'; import 'package:surface/widgets/post/post_item.dart';
class PostDetailScreen extends StatefulWidget { class PostDetailScreen extends StatefulWidget {
@ -89,13 +91,26 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
], ],
).padding(top: math.max(MediaQuery.of(context).padding.top, 8)), ).padding(top: math.max(MediaQuery.of(context).padding.top, 8)),
), ),
body: SingleChildScrollView( body: CustomScrollView(
child: Column( slivers: [
children: [ SliverToBoxAdapter(
LoadingIndicator(isActive: _isBusy), child: LoadingIndicator(isActive: _isBusy),
if (_data != null) PostItem(data: _data!),
],
), ),
if (_data != null)
SliverToBoxAdapter(
child: PostItem(data: _data!),
),
const SliverToBoxAdapter(child: Divider(height: 1)),
if (_data != null)
SliverToBoxAdapter(
child: Text('postCommentsDetailed')
.plural(_data!.metric.replyCount)
.textStyle(Theme.of(context).textTheme.titleLarge!)
.padding(horizontal: 16, top: 12, bottom: 4),
),
if (_data != null) PostCommentSliverList(parentPostId: _data!.id),
SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
],
), ),
); );
} }

View File

@ -27,6 +27,7 @@ class AttachmentDetailPopup extends StatelessWidget {
child: Hero( child: Hero(
tag: 'attachment-${data.rid}-${heroTag ?? uuid.v4()}', tag: 'attachment-${data.rid}-${heroTag ?? uuid.v4()}',
child: PhotoView( child: PhotoView(
key: Key('attachment-detail-${data.rid}-$heroTag'),
imageProvider: UniversalImage.provider( imageProvider: UniversalImage.provider(
sn.getAttachmentUrl(data.rid), sn.getAttachmentUrl(data.rid),
), ),

View File

@ -26,6 +26,7 @@ class AttachmentItem extends StatelessWidget {
child: LayoutBuilder(builder: (context, constraints) { child: LayoutBuilder(builder: (context, constraints) {
return UniversalImage( return UniversalImage(
sn.getAttachmentUrl(data.rid), sn.getAttachmentUrl(data.rid),
key: Key('attachment-${data.rid}-$heroTag'),
fit: BoxFit.cover, fit: BoxFit.cover,
cacheHeight: constraints.maxHeight, cacheHeight: constraints.maxHeight,
cacheWidth: constraints.maxWidth, cacheWidth: constraints.maxWidth,

View File

@ -0,0 +1,134 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_attachment.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/post.dart';
import 'package:surface/widgets/post/post_item.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class PostCommentSliverList extends StatefulWidget {
final int parentPostId;
const PostCommentSliverList({super.key, required this.parentPostId});
@override
State<PostCommentSliverList> createState() => _PostCommentSliverListState();
}
class _PostCommentSliverListState extends State<PostCommentSliverList> {
bool _isBusy = true;
final List<SnPost> _posts = List.empty(growable: true);
int? _postCount;
Future<void> _fetchPosts() async {
if (_postCount != null && _posts.length >= _postCount!) return;
setState(() => _isBusy = true);
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get(
'/cgi/co/posts/${widget.parentPostId}/replies',
queryParameters: {
'take': 10,
'offset': _posts.length,
},
);
final List<SnPost> out =
List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []);
Set<String> rids = {};
for (var i = 0; i < out.length; i++) {
rids.addAll(out[i].body['attachments']?.cast<String>() ?? []);
}
if (!mounted) return;
final attach = context.read<SnAttachmentProvider>();
final attachments = await attach.getMultiple(rids.toList());
for (var i = 0; i < out.length; i++) {
out[i] = out[i].copyWith(
preload: SnPostPreload(
attachments: attachments
.where(
(ele) => out[i].body['attachments']?.contains(ele.rid) ?? false,
)
.toList(),
),
);
}
_postCount = resp.data['count'];
_posts.addAll(out);
if (mounted) setState(() => _isBusy = false);
}
@override
void initState() {
super.initState();
_fetchPosts();
}
@override
Widget build(BuildContext context) {
return SliverInfiniteList(
itemCount: _posts.length,
isLoading: _isBusy,
hasReachedMax: _postCount != null && _posts.length >= _postCount!,
onFetchData: _fetchPosts,
itemBuilder: (context, idx) {
return GestureDetector(
child: PostItem(data: _posts[idx]),
onTap: () {
GoRouter.of(context).pushNamed(
'postDetail',
pathParameters: {'slug': _posts[idx].id.toString()},
extra: _posts[idx],
);
},
);
},
separatorBuilder: (context, index) => const Divider(height: 1),
);
}
}
class PostCommentListPopup extends StatelessWidget {
final int postId;
final int commentCount;
const PostCommentListPopup({
super.key,
required this.postId,
this.commentCount = 0,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.comment, size: 24),
const Gap(16),
Text('postCommentsDetailed')
.plural(commentCount)
.textStyle(Theme.of(context).textTheme.titleLarge!),
],
).padding(horizontal: 20, top: 16, bottom: 12),
Expanded(
child: CustomScrollView(
slivers: [
PostCommentSliverList(parentPostId: postId),
],
),
),
],
);
}
}

View File

@ -11,10 +11,16 @@ import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/attachment/attachment_list.dart'; import 'package:surface/widgets/attachment/attachment_list.dart';
import 'package:surface/widgets/markdown_content.dart'; import 'package:surface/widgets/markdown_content.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:surface/widgets/post/post_comment_list.dart';
class PostItem extends StatelessWidget { class PostItem extends StatelessWidget {
final SnPost data; final SnPost data;
const PostItem({super.key, required this.data}); final bool showComments;
const PostItem({
super.key,
required this.data,
this.showComments = true,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -24,9 +30,12 @@ class PostItem extends StatelessWidget {
_PostContentHeader(data: data).padding(horizontal: 12, vertical: 8), _PostContentHeader(data: data).padding(horizontal: 12, vertical: 8),
_PostContentBody(data: data.body).padding(horizontal: 16, bottom: 6), _PostContentBody(data: data.body).padding(horizontal: 16, bottom: 6),
if (data.preload?.attachments?.isNotEmpty ?? true) if (data.preload?.attachments?.isNotEmpty ?? true)
AttachmentList(data: data.preload!.attachments!, bordered: true), AttachmentList(
_PostBottomAction(data: data) data: data.preload!.attachments!,
.padding(left: 20, right: 26, top: 8, bottom: 2), bordered: true,
),
_PostBottomAction(data: data, showComments: showComments)
.padding(left: 12, right: 18),
], ],
); );
} }
@ -34,7 +43,8 @@ class PostItem extends StatelessWidget {
class _PostBottomAction extends StatelessWidget { class _PostBottomAction extends StatelessWidget {
final SnPost data; final SnPost data;
const _PostBottomAction({required this.data}); final bool showComments;
const _PostBottomAction({required this.data, required this.showComments});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -53,10 +63,10 @@ class _PostBottomAction extends StatelessWidget {
const Gap(8), const Gap(8),
Text('postReact').tr(), Text('postReact').tr(),
], ],
), ).padding(horizontal: 8, vertical: 8),
onTap: () {}, onTap: () {},
), ),
const Gap(16), if (showComments)
InkWell( InkWell(
child: Row( child: Row(
children: [ children: [
@ -64,14 +74,27 @@ class _PostBottomAction extends StatelessWidget {
const Gap(8), const Gap(8),
Text('postComments').plural(data.metric.replyCount), Text('postComments').plural(data.metric.replyCount),
], ],
).padding(horizontal: 8, vertical: 8),
onTap: () {
showModalBottomSheet(
context: context,
useRootNavigator: true,
builder: (context) => PostCommentListPopup(
postId: data.id,
commentCount: data.metric.replyCount,
), ),
onTap: () {}, );
},
), ),
].expand((ele) => [ele, const Gap(8)]).toList() ].expand((ele) => [ele, const Gap(8)]).toList()
..removeLast(), ..removeLast(),
), ),
InkWell( InkWell(
child: Icon(Symbols.share, size: 20, color: iconColor), child: Icon(
Symbols.share,
size: 20,
color: iconColor,
).padding(horizontal: 8, vertical: 8),
onTap: () {}, onTap: () {},
), ),
], ],

View File

@ -5,23 +5,23 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: _fe_analyzer_shared name: _fe_analyzer_shared
sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "72.0.0" version: "76.0.0"
_macros: _macros:
dependency: transitive dependency: transitive
description: dart description: dart
source: sdk source: sdk
version: "0.3.2" version: "0.3.3"
analyzer: analyzer:
dependency: transitive dependency: transitive
description: description:
name: analyzer name: analyzer
sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.7.0" version: "6.11.0"
animations: animations:
dependency: "direct main" dependency: "direct main"
description: description:
@ -202,10 +202,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: collection name: collection
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.18.0" version: "1.19.0"
connectivity_plus: connectivity_plus:
dependency: transitive dependency: transitive
description: description:
@ -790,18 +790,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker name: leak_tracker
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.0.5" version: "10.0.8"
leak_tracker_flutter_testing: leak_tracker_flutter_testing:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker_flutter_testing name: leak_tracker_flutter_testing
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.5" version: "3.0.9"
leak_tracker_testing: leak_tracker_testing:
dependency: transitive dependency: transitive
description: description:
@ -838,10 +838,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: macros name: macros
sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.1.2-main.4" version: "0.1.3-main.0"
markdown: markdown:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1150,7 +1150,7 @@ packages:
dependency: transitive dependency: transitive
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.99" version: "0.0.0"
source_gen: source_gen:
dependency: transitive dependency: transitive
description: description:
@ -1227,10 +1227,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: stack_trace name: stack_trace
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.11.1" version: "1.12.0"
stream_channel: stream_channel:
dependency: transitive dependency: transitive
description: description:
@ -1251,10 +1251,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: string_scanner name: string_scanner
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.0" version: "1.3.0"
styled_widget: styled_widget:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1291,10 +1291,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.2" version: "0.7.3"
timing: timing:
dependency: transitive dependency: transitive
description: description:
@ -1411,10 +1411,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vm_service name: vm_service
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "14.2.5" version: "14.3.0"
watcher: watcher:
dependency: transitive dependency: transitive
description: description: