diff --git a/android/app/build.gradle b/android/app/build.gradle index 4c258354..262f823b 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -51,6 +51,7 @@ android { targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName + minSdkVersion 17 } buildTypes { diff --git a/assets/fonts/fansCard.ttf b/assets/fonts/fansCard.ttf new file mode 100644 index 00000000..09ade3a0 Binary files /dev/null and b/assets/fonts/fansCard.ttf differ diff --git a/ios/Podfile.lock b/ios/Podfile.lock index e467820a..8576a625 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2,46 +2,90 @@ PODS: - connectivity_plus (0.0.1): - Flutter - ReachabilitySwift + - device_info_plus (0.0.1): + - Flutter - Flutter (1.0.0) + - flutter_inappwebview (0.0.1): + - Flutter + - flutter_inappwebview/Core (= 0.0.1) + - OrderedSet (~> 5.0) + - flutter_inappwebview/Core (0.0.1): + - Flutter + - OrderedSet (~> 5.0) - FMDB (2.7.5): - FMDB/standard (= 2.7.5) - FMDB/standard (2.7.5) + - image_gallery_saver (1.5.0): + - Flutter + - OrderedSet (5.0.0) - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - permission_handler_apple (9.0.4): + - Flutter - ReachabilitySwift (5.0.0) + - share_plus (0.0.1): + - Flutter - sqflite (0.0.2): - Flutter - FMDB (>= 2.7.5) + - url_launcher_ios (0.0.1): + - Flutter DEPENDENCIES: - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) + - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - Flutter (from `Flutter`) + - flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`) + - image_gallery_saver (from `.symlinks/plugins/image_gallery_saver/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`) + - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + - share_plus (from `.symlinks/plugins/share_plus/ios`) - sqflite (from `.symlinks/plugins/sqflite/ios`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) SPEC REPOS: trunk: - FMDB + - OrderedSet - ReachabilitySwift EXTERNAL SOURCES: connectivity_plus: :path: ".symlinks/plugins/connectivity_plus/ios" + device_info_plus: + :path: ".symlinks/plugins/device_info_plus/ios" Flutter: :path: Flutter + flutter_inappwebview: + :path: ".symlinks/plugins/flutter_inappwebview/ios" + image_gallery_saver: + :path: ".symlinks/plugins/image_gallery_saver/ios" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/ios" + permission_handler_apple: + :path: ".symlinks/plugins/permission_handler_apple/ios" + share_plus: + :path: ".symlinks/plugins/share_plus/ios" sqflite: :path: ".symlinks/plugins/sqflite/ios" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: connectivity_plus: 413a8857dd5d9f1c399a39130850d02fe0feaf7e + device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721 FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a + image_gallery_saver: 259eab68fb271cfd57d599904f7acdc7832e7ef2 + OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c path_provider_foundation: c68054786f1b4f3343858c1e1d0caaded73f0be9 + permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 + share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 + url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3 diff --git a/lib/common/widgets/appbar.dart b/lib/common/widgets/appbar.dart new file mode 100644 index 00000000..ad2b0e3b --- /dev/null +++ b/lib/common/widgets/appbar.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +class AppBarWidget extends StatelessWidget implements PreferredSizeWidget { + const AppBarWidget({ + required this.child, + required this.controller, + required this.visible, + Key? key, + }) : super(key: key); + + final PreferredSizeWidget child; + final AnimationController controller; + final bool visible; + + @override + // TODO: implement preferredSize + Size get preferredSize => child.preferredSize; + + @override + Widget build(BuildContext context) { + visible ? controller.reverse() : controller.forward(); + return SlideTransition( + position: Tween( + begin: Offset.zero, + end: const Offset(0, -1), + ).animate(CurvedAnimation( + parent: controller, + curve: Curves.easeInOutBack, + )), + child: child, + ); + } +} diff --git a/lib/common/widgets/network_img_layer.dart b/lib/common/widgets/network_img_layer.dart index ce98c9a9..a09ec850 100644 --- a/lib/common/widgets/network_img_layer.dart +++ b/lib/common/widgets/network_img_layer.dart @@ -30,8 +30,11 @@ class NetworkImgLayer extends StatelessWidget { // double pr = 2; return src != '' ? ClipRRect( - borderRadius: BorderRadius.circular( - type == 'avatar' ? 50 : StyleString.imgRadius.x), + borderRadius: BorderRadius.circular(type == 'avatar' + ? 50 + : type == 'emote' + ? 0 + : StyleString.imgRadius.x), child: CachedNetworkImage( imageUrl: src!, width: width ?? double.infinity, diff --git a/lib/http/api.dart b/lib/http/api.dart index de89bee9..068d7d24 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -16,4 +16,7 @@ class Api { // 评论列表 static const String replyList = '/x/v2/reply'; + + // 楼中楼 + static const String replyReplyList = '/x/v2/reply/reply'; } diff --git a/lib/http/reply.dart b/lib/http/reply.dart index 88000996..e9a609d2 100644 --- a/lib/http/reply.dart +++ b/lib/http/reply.dart @@ -33,4 +33,38 @@ class ReplyHttp { }; } } + + static Future replyReplyList({ + required String oid, + required String root, + required int pageNum, + required int type, + int sort = 1, + }) async { + var res = await Request().get(Api.replyReplyList, data: { + 'oid': oid, + 'root': root, + 'pn': pageNum, + 'type': type, + 'sort': 1, + }); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': res.data['data'], + }; + } else { + Map errMap = { + -400: '请求错误', + -404: '无此项', + 12002: '评论区已关闭', + 12009: '评论主体的type不合法', + }; + return { + 'status': false, + 'date': [], + 'msg': errMap[res.data['code']] ?? '请求异常', + }; + } + } } diff --git a/lib/models/video/reply/data.dart b/lib/models/video/reply/data.dart index 3b94a008..cc419777 100644 --- a/lib/models/video/reply/data.dart +++ b/lib/models/video/reply/data.dart @@ -22,10 +22,12 @@ class ReplyData { ReplyData.fromJson(Map json) { page = ReplyPage.fromJson(json['page']); config = ReplyConfig.fromJson(json['config']); - replies = json['replies'] - .map( - (item) => ReplyItemModel.fromJson(item, json['upper']['mid'])) - .toList(); + replies = json['replies'] != null + ? json['replies'] + .map( + (item) => ReplyItemModel.fromJson(item, json['upper']['mid'])) + .toList() + : []; topReplies = json['top_replies'] != null ? json['top_replies'] .map((item) => ReplyItemModel.fromJson( diff --git a/lib/models/video/reply/item.dart b/lib/models/video/reply/item.dart index 53b71b6e..c6838a13 100644 --- a/lib/models/video/reply/item.dart +++ b/lib/models/video/reply/item.dart @@ -149,6 +149,6 @@ class ReplyControl { entryText = json['sub_reply_entry_text']; titleText = json['sub_reply_title_text']; time = json['time_desc']; - location = json['location']; + location = json['location'] ?? ''; } } diff --git a/lib/models/video/reply/member.dart b/lib/models/video/reply/member.dart index 196f252b..5576cbd1 100644 --- a/lib/models/video/reply/member.dart +++ b/lib/models/video/reply/member.dart @@ -1,4 +1,4 @@ -import 'dart:convert' as convert; +import 'package:get/get.dart'; class ReplyMember { ReplyMember({ @@ -22,6 +22,7 @@ class ReplyMember { Map? officialVerify; Map? vip; Map? fansDetail; + UserSailing? userSailing; ReplyMember.fromJson(Map json) { mid = json['mid']; @@ -30,9 +31,12 @@ class ReplyMember { avatar = json['avatar']; level = json['level_info']['current_level']; pendant = Pendant.fromJson(json['pendant']); - officialVerify = json['officia_vVerify']; + officialVerify = json['officia_verify']; vip = json['vip']; fansDetail = json['fans_detail']; + userSailing = json['user_sailing'] != null + ? UserSailing.fromJson(json['user_sailing']) + : UserSailing(); } } @@ -53,3 +57,15 @@ class Pendant { image = json['image']; } } + +class UserSailing { + UserSailing({this.pendant, this.cardbg}); + + Map? pendant; + Map? cardbg; + + UserSailing.fromJson(Map json) { + pendant = json['pendant']; + cardbg = json['cardbg']; + } +} diff --git a/lib/pages/preview/controller.dart b/lib/pages/preview/controller.dart new file mode 100644 index 00000000..f2af8b9c --- /dev/null +++ b/lib/pages/preview/controller.dart @@ -0,0 +1,77 @@ +import 'dart:io'; + +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:get/get.dart'; +import 'dart:typed_data'; +import 'package:dio/dio.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:image_gallery_saver/image_gallery_saver.dart'; +import 'package:pilipala/utils/utils.dart'; +import 'package:share_plus/share_plus.dart'; + +class PreviewController extends GetxController { + DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + RxInt initialPage = 0.obs; + RxInt currentPage = 1.obs; + RxList imgList = [].obs; + bool storage = true; + bool videos = true; + bool photos = true; + bool visiable = true; + + @override + void onInit() { + super.onInit(); + if (Get.arguments != null) { + initialPage.value = Get.arguments['initialPage']!; + currentPage.value = Get.arguments['initialPage']! + 1; + imgList.value = Get.arguments['imgList']; + } + } + + requestPermission() async { + Map statuses = await [ + Permission.storage, + // Permission.photos + ].request(); + + final info = statuses[Permission.storage].toString(); + // final photosInfo = statuses[Permission.photos].toString(); + + print('授权状态:$info'); + } + + // 图片保存 + void onSaveImg() async { + var response = await Dio().get(imgList[initialPage.value], + options: Options(responseType: ResponseType.bytes)); + final result = await ImageGallerySaver.saveImage( + Uint8List.fromList(response.data), + quality: 100, + name: "pic_vvex${DateTime.now().toString().split('-').join()}"); + if (result != null) { + if (result['isSuccess']) { + print('已保存到相册'); + } + } + } + + // 图片分享 + void onShareImg() async { + requestPermission(); + var response = await Dio().get(imgList[initialPage.value], + options: Options(responseType: ResponseType.bytes)); + final temp = await getTemporaryDirectory(); + String imgName = + "pic_vvex${DateTime.now().toString().split('-').join()}.jpg"; + var path = '${temp.path}/$imgName'; + File(path).writeAsBytesSync(response.data); + Share.shareXFiles([XFile(path)], subject: imgList[initialPage.value]); + } + + // 浏览器中查看 + void onBrowserImg() async { + Utils.openURL(imgList[initialPage.value]); + } +} diff --git a/lib/pages/preview/index.dart b/lib/pages/preview/index.dart new file mode 100644 index 00000000..9fb82e8d --- /dev/null +++ b/lib/pages/preview/index.dart @@ -0,0 +1,4 @@ +library preview; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/preview/view.dart b/lib/pages/preview/view.dart new file mode 100644 index 00000000..d82cd45e --- /dev/null +++ b/lib/pages/preview/view.dart @@ -0,0 +1,188 @@ +import 'package:get/get.dart'; +import 'package:flutter/material.dart'; +import 'package:extended_image/extended_image.dart'; +import 'package:pilipala/common/widgets/appbar.dart'; +import 'controller.dart'; + +typedef DoubleClickAnimationListener = void Function(); + +class ImagePreview extends StatefulWidget { + const ImagePreview({Key? key}) : super(key: key); + + @override + _ImagePreviewState createState() => _ImagePreviewState(); +} + +class _ImagePreviewState extends State + with TickerProviderStateMixin { + final PreviewController _previewController = Get.put(PreviewController()); + late AnimationController animationController; + late AnimationController _doubleClickAnimationController; + Animation? _doubleClickAnimation; + late DoubleClickAnimationListener _doubleClickAnimationListener; + List doubleTapScales = [1.0, 2.0]; + + @override + void initState() { + super.initState(); + animationController = AnimationController( + vsync: this, duration: const Duration(milliseconds: 400)); + _doubleClickAnimationController = AnimationController( + duration: const Duration(milliseconds: 250), vsync: this); + } + + @override + void dispose() { + animationController.dispose(); + _doubleClickAnimationController.dispose(); + clearGestureDetailsCache(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + extendBodyBehindAppBar: true, + appBar: AppBarWidget( + controller: animationController, + visible: _previewController.visiable, + child: AppBar( + backgroundColor: Theme.of(context).colorScheme.background, + elevation: 0, + centerTitle: false, + title: Obx( + () => Text.rich( + TextSpan(children: [ + TextSpan(text: _previewController.currentPage.toString()), + const TextSpan(text: ' / '), + TextSpan(text: _previewController.imgList.length.toString()), + ]), + ), + ), + actions: [ + PopupMenuButton( + icon: const Icon(Icons.more_vert), + tooltip: 'action', + itemBuilder: (BuildContext context) => [ + PopupMenuItem( + value: 'share', + onTap: _previewController.onShareImg, + child: const Text('分享'), + ), + PopupMenuItem( + value: 'save', + onTap: _previewController.onSaveImg, + child: const Text('保存'), + ), + PopupMenuItem( + value: 'browser', + onTap: _previewController.onBrowserImg, + child: const Text('浏览器中查看'), + ), + ], + ), + ], + ), + ), + body: GestureDetector( + onTap: () { + _previewController.visiable = !_previewController.visiable; + setState(() {}); + }, + child: ExtendedImageGesturePageView.builder( + controller: ExtendedPageController( + initialPage: _previewController.initialPage.value, + pageSpacing: 0, + ), + onPageChanged: (int index) { + _previewController.initialPage.value = index; + _previewController.currentPage.value = index + 1; + }, + canScrollPage: (GestureDetails? gestureDetails) => + gestureDetails!.totalScale! <= 1.0, + preloadPagesCount: 2, + itemCount: _previewController.imgList.length, + itemBuilder: (BuildContext context, int index) { + return ExtendedImage.network( + _previewController.imgList[index], + fit: BoxFit.contain, + mode: ExtendedImageMode.gesture, + onDoubleTap: (ExtendedImageGestureState state) { + final Offset? pointerDownPosition = state.pointerDownPosition; + final double? begin = state.gestureDetails!.totalScale; + double end; + + //remove old + _doubleClickAnimation + ?.removeListener(_doubleClickAnimationListener); + + //stop pre + _doubleClickAnimationController.stop(); + + //reset to use + _doubleClickAnimationController.reset(); + + if (begin == doubleTapScales[0]) { + end = doubleTapScales[1]; + } else { + end = doubleTapScales[0]; + } + + _doubleClickAnimationListener = () { + state.handleDoubleTap( + scale: _doubleClickAnimation!.value, + doubleTapPosition: pointerDownPosition); + }; + _doubleClickAnimation = _doubleClickAnimationController + .drive(Tween(begin: begin, end: end)); + + _doubleClickAnimation! + .addListener(_doubleClickAnimationListener); + + _doubleClickAnimationController.forward(); + }, + loadStateChanged: (ExtendedImageState state) { + if (state.extendedImageLoadState == LoadState.loading) { + final ImageChunkEvent? loadingProgress = + state.loadingProgress; + final double? progress = + loadingProgress?.expectedTotalBytes != null + ? loadingProgress!.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null; + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: 150.0, + child: LinearProgressIndicator(value: progress), + ), + const SizedBox(height: 10.0), + Text('${((progress ?? 0.0) * 100).toInt()}%'), + ], + ), + ); + } + }, + initGestureConfigHandler: (ExtendedImageState state) { + return GestureConfig( + inPageView: true, + initialScale: 1.0, + maxScale: 5.0, + animationMaxScale: 6.0, + initialAlignment: InitialAlignment.center, + ); + }, + ); + }, + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: () => _previewController.onSaveImg(), + child: const Icon(Icons.save_alt_rounded), + ), + ); + } +} diff --git a/lib/pages/video/detail/reply/controller.dart b/lib/pages/video/detail/reply/controller.dart index a823778f..6e813fc8 100644 --- a/lib/pages/video/detail/reply/controller.dart +++ b/lib/pages/video/detail/reply/controller.dart @@ -1,22 +1,80 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/http/reply.dart'; import 'package:pilipala/models/video/reply/data.dart'; +import 'package:pilipala/models/video/reply/item.dart'; class VideoReplyController extends GetxController { - // 视频aid - String aid = Get.parameters['aid']!; + VideoReplyController( + this.aid, + this.rpid, + this.level, + ); + final ScrollController scrollController = ScrollController(); + // 视频aid 请求时使用的oid + String? aid; + // 层级 2为楼中楼 + String? level; + // rpid 请求楼中楼回复 + String? rpid; + RxList replyList = [ReplyItemModel()].obs; + // 当前页 + int currentPage = 0; + bool isLoadingMore = false; + RxBool noMore = false.obs; - @override - void onInit() { - super.onInit(); - queryReplyList(); - } - - Future queryReplyList() async { - var res = await ReplyHttp.replyList(oid: aid, pageNum: 1, type: 1); + Future queryReplyList({type = 'init'}) async { + isLoadingMore = true; + var res = level == '1' + ? await ReplyHttp.replyList( + oid: aid!, pageNum: currentPage + 1, type: 1) + : await ReplyHttp.replyReplyList( + oid: aid!, root: rpid!, pageNum: currentPage + 1, type: 1); if (res['status']) { res['data'] = ReplyData.fromJson(res['data']); + if (res['data'].replies.isNotEmpty) { + currentPage = currentPage + 1; + noMore.value = false; + } else { + if (currentPage == 0) { + } else { + noMore.value = true; + return; + } + } + if (res['data'].replies.length >= res['data'].page.count) { + noMore.value = true; + } + if (type == 'init') { + List replies = res['data'].replies; + // 添加置顶回复 + if (res['data'].upper.top != null) { + bool flag = false; + for (var i = 0; i < res['data'].topReplies.length; i++) { + if (res['data'].topReplies[i].rpid == res['data'].upper.top.rpid) { + flag = true; + } + } + if (!flag) { + replies.insert(0, res['data'].upper.top); + } + } + replies.insertAll(0, res['data'].topReplies); + res['data'].replies = replies; + replyList.value = res['data'].replies!; + } else { + replyList.addAll(res['data'].replies!); + res['data'].replies.addAll(replyList); + } } + isLoadingMore = false; return res; } + + // 上拉加载 + Future onLoad() async { + queryReplyList(type: 'onLoad'); + } } diff --git a/lib/pages/video/detail/reply/view.dart b/lib/pages/video/detail/reply/view.dart index 67341100..e1b2544a 100644 --- a/lib/pages/video/detail/reply/view.dart +++ b/lib/pages/video/detail/reply/view.dart @@ -7,7 +7,15 @@ import 'controller.dart'; import 'widgets/reply_item.dart'; class VideoReplyPanel extends StatefulWidget { - const VideoReplyPanel({super.key}); + int oid; + int rpid; + String level; + VideoReplyPanel({ + this.oid = 0, + this.rpid = 0, + this.level = '', + super.key, + }); @override State createState() => _VideoReplyPanelState(); @@ -15,66 +23,108 @@ class VideoReplyPanel extends StatefulWidget { class _VideoReplyPanelState extends State with AutomaticKeepAliveClientMixin { - final VideoReplyController _videoReplyController = - Get.put(VideoReplyController(), tag: Get.arguments['heroTag']); + late VideoReplyController _videoReplyController; + // List? replyList; + Future? _futureBuilderFuture; // 添加页面缓存 @override bool get wantKeepAlive => true; @override - Widget build(BuildContext context) { - return FutureBuilder( - future: _videoReplyController.queryReplyList(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - if (snapshot.data['status']) { - List replies = snapshot.data['data'].replies; - // 添加置顶回复 - if (snapshot.data['data'].upper.top != null) { - bool flag = false; - for (var i = 0; - i < snapshot.data['data'].topReplies.length; - i++) { - if (snapshot.data['data'].topReplies[i].rpid == - snapshot.data['data'].upper.top.rpid) { - flag = true; - } - } - if (!flag) { - replies.insert(0, snapshot.data['data'].upper.top); - } - } + void initState() { + super.initState(); + if (widget.level == '2') { + _videoReplyController = Get.put( + VideoReplyController( + widget.oid.toString(), widget.rpid.toString(), '2'), + tag: widget.rpid.toString()); + } else { + _videoReplyController = Get.put( + VideoReplyController(Get.parameters['aid']!, '', '1'), + tag: Get.arguments['heroTag']); + } - replies.insertAll(0, snapshot.data['data'].topReplies); - // 请求成功 - return SliverList( - delegate: SliverChildBuilderDelegate((context, index) { - if (index == replies.length) { - return SizedBox(height: MediaQuery.of(context).padding.bottom); - } else { - return ReplyItem( - replyItem: replies[index], - isUp: - replies[index].mid == snapshot.data['data'].upper.mid); - } - }, childCount: replies.length + 1)); - } else { - // 请求错误 - return HttpError( - errMsg: snapshot.data['msg'], - fn: () => setState(() {}), - ); + _futureBuilderFuture = _videoReplyController.queryReplyList(); + _videoReplyController.scrollController.addListener( + () { + if (_videoReplyController.scrollController.position.pixels >= + _videoReplyController.scrollController.position.maxScrollExtent - + 300) { + if (!_videoReplyController.isLoadingMore) { + _videoReplyController.onLoad(); } - } else { - // 骨架屏 - return SliverList( - delegate: SliverChildBuilderDelegate((context, index) { - return const VideoCardHSkeleton(); - }, childCount: 5), - ); } }, ); } + + @override + Widget build(BuildContext context) { + return RefreshIndicator( + onRefresh: () async { + setState(() {}); + _videoReplyController.currentPage = 0; + return await _videoReplyController.queryReplyList(); + }, + child: CustomScrollView( + controller: _videoReplyController.scrollController, + key: const PageStorageKey('评论'), + slivers: [ + FutureBuilder( + future: _futureBuilderFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + Map data = snapshot.data as Map; + if (data['status']) { + // 请求成功 + return Obx( + () => SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + if (index == _videoReplyController.replyList.length) { + return Container( + padding: EdgeInsets.only( + bottom: + MediaQuery.of(context).padding.bottom), + height: + MediaQuery.of(context).padding.bottom + 60, + child: Center( + child: Obx(() => Text( + _videoReplyController.noMore.value + ? '没有更多了' + : '加载中')), + ), + ); + } else { + return ReplyItem( + replyItem: _videoReplyController.replyList[index], + ); + } + }, + childCount: _videoReplyController.replyList.length + 1, + ), + ), + ); + } else { + // 请求错误 + return HttpError( + errMsg: data['msg'], + fn: () => setState(() {}), + ); + } + } else { + // 骨架屏 + return SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + return const VideoCardHSkeleton(); + }, childCount: 5), + ); + } + }, + ) + ], + ), + ); + } } diff --git a/lib/pages/video/detail/reply/widgets/reply_item.dart b/lib/pages/video/detail/reply/widgets/reply_item.dart index 27c22287..73bafe2f 100644 --- a/lib/pages/video/detail/reply/widgets/reply_item.dart +++ b/lib/pages/video/detail/reply/widgets/reply_item.dart @@ -1,13 +1,14 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/models/video/reply/item.dart'; +import 'package:pilipala/pages/video/detail/reply/index.dart'; import 'package:pilipala/utils/utils.dart'; class ReplyItem extends StatelessWidget { - ReplyItem({super.key, this.replyItem, required this.isUp}); + ReplyItem({super.key, this.replyItem}); ReplyItemModel? replyItem; - bool isUp = false; @override Widget build(BuildContext context) { @@ -16,11 +17,13 @@ class ReplyItem extends StatelessWidget { child: Column( children: [ Padding( - padding: const EdgeInsets.fromLTRB(12, 6, 8, 0), + padding: const EdgeInsets.fromLTRB(12, 8, 8, 2), child: content(context), ), Divider( height: 1, + indent: 52, + endIndent: 10, color: Theme.of(context).dividerColor.withOpacity(0.08), ) ], @@ -30,14 +33,42 @@ class ReplyItem extends StatelessWidget { Widget lfAvtar(context) { return Container( - margin: const EdgeInsets.only(top: 5), - child: NetworkImgLayer( - src: replyItem!.member!.avatar, - width: 30, - height: 30, - type: 'avatar', - ), - ); + margin: const EdgeInsets.only(top: 5), + child: Stack( + children: [ + NetworkImgLayer( + src: replyItem!.member!.avatar, + width: 34, + height: 34, + type: 'avatar', + ), + if (replyItem!.member!.officialVerify != null && + replyItem!.member!.officialVerify!['type'] == 0) + Positioned( + right: 0, + bottom: 0, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(7), + color: Theme.of(context).colorScheme.background, + ), + child: Icon( + Icons.offline_bolt, + color: Theme.of(context).colorScheme.primary, + size: 16, + ), + ), + ), + ], + ) + // child: + // NetworkImgLayer( + // src: replyItem!.member!.avatar, + // width: 30, + // height: 30, + // type: 'avatar', + // ), + ); } Widget content(context) { @@ -45,51 +76,80 @@ class ReplyItem extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ // 头像、昵称 - Row( - // 两端对齐 - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - GestureDetector( - // onTap: () => - // Get.toNamed('/member/${reply.userName}', parameters: { - // 'memberAvatar': reply.avatar, - // 'heroTag': reply.userName + heroTag, - // }), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - lfAvtar(context), - const SizedBox(width: 12), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( + GestureDetector( + // onTap: () => + // Get.toNamed('/member/${reply.userName}', parameters: { + // 'memberAvatar': reply.avatar, + // 'heroTag': reply.userName + heroTag, + // }), + child: Container( + width: double.infinity, + decoration: BoxDecoration( + image: replyItem!.member!.userSailing!.cardbg != null + ? DecorationImage( + fit: BoxFit.cover, + image: NetworkImage( + replyItem!.member!.userSailing!.cardbg!['image'], + ), + ) + : null, + ), + child: Stack( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + lfAvtar(context), + const SizedBox(width: 12), + Text( + replyItem!.member!.uname!, + style: TextStyle( + color: replyItem!.isUp! || + replyItem!.member!.vip!['vipType'] > 0 + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.outline, + fontSize: + Theme.of(context).textTheme.titleSmall!.fontSize, + ), + ), + const SizedBox(width: 6), + Image.asset( + 'assets/images/lv/lv${replyItem!.member!.level}.png', + height: 11, + ), + const SizedBox(width: 6), + if (replyItem!.isUp!) UpTag(), + ], + ), + if (replyItem!.member!.userSailing!.cardbg != null && + replyItem!.member!.userSailing!.cardbg!['fan']['number'] > + 0) + Positioned( + top: 8, + left: Get.size.width / 7 * 5.6, + child: DefaultTextStyle( + style: TextStyle( + fontFamily: 'fansCard', + fontSize: 9, + color: Theme.of(context).colorScheme.primary, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, children: [ + const Text('NO.'), Text( - replyItem!.member!.uname!, - style: Theme.of(context) - .textTheme - .titleSmall! - .copyWith( - color: replyItem!.isUp! - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.outline, - ), - ), - const SizedBox(width: 6), - Image.asset( - 'assets/images/lv/lv${replyItem!.member!.level}.png', - height: 13, + replyItem!.member!.userSailing!.cardbg!['fan'] + ['num_desc'], ), ], ), - ], - ) - ], - ), + ), + ), + ], ), - ], + ), ), // title Container( @@ -102,6 +162,8 @@ class ReplyItem extends StatelessWidget { style: const TextStyle(height: 1.65), TextSpan( children: [ + if (replyItem!.isTop!) + WidgetSpan(child: UpTag(tagText: '置顶')), buildContent(context, replyItem!.content!), ], ), @@ -116,6 +178,7 @@ class ReplyItem extends StatelessWidget { child: ReplyItemRow( replies: replyItem!.replies, replyControl: replyItem!.replyControl, + f_rpid: replyItem!.rpid, ), ), ], @@ -136,26 +199,9 @@ class ReplyItem extends StatelessWidget { .labelMedium! .copyWith(color: Theme.of(context).colorScheme.outline), ), - if (replyItem!.isTop!) ...[ - Text( - ' • 置顶', - style: TextStyle( - fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, - color: Theme.of(context).colorScheme.primary, - ), - ), - ], - if (replyControl!.isUpTop!) ...[ - Text( - ' • 超赞', - style: TextStyle( - fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, - color: Theme.of(context).colorScheme.primary, - ), - ), - // const SizedBox(width: 4), - ], const Spacer(), + if (replyControl!.isUpTop!) + Icon(Icons.favorite, color: Colors.red[400], size: 18), SizedBox( height: 35, child: TextButton( @@ -191,9 +237,11 @@ class ReplyItemRow extends StatelessWidget { super.key, this.replies, this.replyControl, + this.f_rpid, }); List? replies; ReplyControl? replyControl; + int? f_rpid; @override Widget build(BuildContext context) { @@ -202,7 +250,7 @@ class ReplyItemRow extends StatelessWidget { return Container( margin: const EdgeInsets.only(left: 42, right: 4, top: 0), child: Material( - color: Theme.of(context).colorScheme.onInverseSurface.withOpacity(0.7), + color: Theme.of(context).colorScheme.onInverseSurface, borderRadius: BorderRadius.circular(6), clipBehavior: Clip.hardEdge, animationDuration: Duration.zero, @@ -215,7 +263,7 @@ class ReplyItemRow extends StatelessWidget { if (extraRow == 1 && index == replies!.length) { // 有楼中楼回复,在最后显示 return InkWell( - onTap: () {}, + onTap: () => replyReply(context), child: Padding( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8), @@ -243,23 +291,23 @@ class ReplyItemRow extends StatelessWidget { return InkWell( onTap: () {}, child: Padding( - padding: EdgeInsets.fromLTRB(8, index == 0 ? 8 : 4, 8, 4), + padding: EdgeInsets.fromLTRB( + 8, + index == 0 && (extraRow == 1 || replies!.length > 1) + ? 8 + : 5, + 8, + 5), child: Text.rich( - overflow: TextOverflow.ellipsis, - maxLines: 2, + overflow: extraRow == 1 + ? TextOverflow.ellipsis + : TextOverflow.visible, + maxLines: extraRow == 1 ? 2 : null, TextSpan( children: [ if (replies![index].isUp) - TextSpan( - text: 'UP • ', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: Theme.of(context) - .textTheme - .labelMedium! - .fontSize, - color: Theme.of(context).colorScheme.primary, - ), + WidgetSpan( + child: UpTag(), ), TextSpan( text: replies![index].member.uname + ' ', @@ -286,6 +334,48 @@ class ReplyItemRow extends StatelessWidget { ), ); } + + void replyReply(context) { + Get.bottomSheet( + barrierColor: Colors.transparent, + useRootNavigator: true, + isScrollControlled: true, + Container( + height: Get.size.height - Get.size.width * 9 / 16 - 50, + color: Theme.of(context).colorScheme.background, + child: Column( + children: [ + AppBar( + automaticallyImplyLeading: false, + centerTitle: false, + title: Text( + '评论详情', + style: Theme.of(context).textTheme.titleMedium, + ), + actions: [ + IconButton( + icon: const Icon(Icons.close), + onPressed: () async { + await Future.delayed(const Duration(milliseconds: 200)); + Get.back(); + }, + ) + ], + ), + Expanded( + child: VideoReplyPanel( + oid: replies!.first.oid, + rpid: f_rpid!, + level: '2', + ), + ) + ], + ), + ), + persistent: false, + backgroundColor: Theme.of(context).bottomSheetTheme.backgroundColor, + ); + } } InlineSpan buildContent(BuildContext context, content) { @@ -301,14 +391,16 @@ InlineSpan buildContent(BuildContext context, content) { RegExp(r"\[.*?\]"), onMatch: (Match match) { String matchStr = match[0]!; + int size = content.emote[matchStr]['meta']['size']; if (content.emote.isNotEmpty) { if (content.emote.keys.contains(matchStr)) { spanChilds.add( WidgetSpan( child: NetworkImgLayer( src: content.emote[matchStr]['url'], - width: 20, - height: 20, + type: 'emote', + width: size * 20, + height: size * 20, ), ), ); @@ -394,26 +486,95 @@ InlineSpan buildContent(BuildContext context, content) { // 图片渲染 if (content.pictures.isNotEmpty) { - spanChilds.add( - const TextSpan(text: '\n'), - ); - for (var i = 0; i < content.pictures.length; i++) { - spanChilds.add( - WidgetSpan( - child: SizedBox( - height: 180, - child: NetworkImgLayer( - src: content.pictures[i]['img_src'], - width: 200, - height: 200 * - content.pictures[i]['img_height'] / - content.pictures[i]['img_width'], - ), - ), + List list = []; + List picList = []; + int len = content.pictures.length; + for (var i = 0; i < len; i++) { + picList.add(content.pictures[i]['img_src']); + list.add( + LayoutBuilder( + builder: (context, BoxConstraints box) { + return GestureDetector( + onTap: () { + Get.toNamed('/preview', + arguments: {'initialPage': i, 'imgList': picList}); + }, + child: NetworkImgLayer( + src: content.pictures[i]['img_src'], + width: box.maxWidth, + height: box.maxWidth, + ), + ); + }, ), ); } + spanChilds.add( + WidgetSpan( + child: LayoutBuilder( + builder: (context, BoxConstraints box) { + double maxWidth = box.maxWidth; + double crossCount = len < 3 ? 2 : 3; + double height = maxWidth / + crossCount * + (len % crossCount == 0 + ? len ~/ crossCount + : len ~/ crossCount + 1) + + 6; + return Container( + padding: const EdgeInsets.only(top: 6), + height: height, + child: GridView( + padding: EdgeInsets.zero, + physics: const NeverScrollableScrollPhysics(), + // 子Item排列规则 + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + //横轴元素个数 + crossAxisCount: crossCount.toInt(), + //纵轴间距 + mainAxisSpacing: 4.0, + //横轴间距 + crossAxisSpacing: 4.0, + //子组件宽高长度比例 + // childAspectRatio: 1, + ), + //GridView中使用的子Widegt + children: list, + ), + ); + }, + ), + ), + ); } // spanChilds.add(TextSpan(text: matchMember)); return TextSpan(children: spanChilds); } + +class UpTag extends StatelessWidget { + String? tagText; + UpTag({super.key, this.tagText = 'UP'}); + + @override + Widget build(BuildContext context) { + return Container( + width: tagText == 'UP' ? 28 : 38, + height: tagText == 'UP' ? 17 : 19, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(3), + // color: Theme.of(context).colorScheme.primary, + border: Border.all(color: Theme.of(context).colorScheme.primary)), + margin: const EdgeInsets.only(right: 4), + // padding: const EdgeInsets.symmetric(vertical: 0.5, horizontal: 4), + child: Center( + child: Text( + tagText!, + style: TextStyle( + fontSize: Theme.of(context).textTheme.labelSmall!.fontSize, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + ); + } +} diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index ed2e1047..6747ea69 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -1,3 +1,4 @@ +import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:get/get.dart'; import 'package:flutter/material.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; @@ -19,6 +20,10 @@ class _VideoDetailPageState extends State { @override Widget build(BuildContext context) { + final double statusBarHeight = MediaQuery.of(context).padding.top; + final double pinnedHeaderHeight = statusBarHeight + + kToolbarHeight + + MediaQuery.of(context).size.width * 9 / 16; return DefaultTabController( initialIndex: videoDetailController.tabInitialIndex, length: videoDetailController.tabs.length, // tab的数量. @@ -26,126 +31,115 @@ class _VideoDetailPageState extends State { top: false, bottom: false, child: Scaffold( - body: NestedScrollView( + body: ExtendedNestedScrollView( headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { return [ - SliverOverlapAbsorber( - handle: - NestedScrollView.sliverOverlapAbsorberHandleFor(context), - sliver: SliverAppBar( - title: const Text("视频详情"), - // floating: true, - // snap: true, - pinned: true, - elevation: 0, - scrolledUnderElevation: 0, - forceElevated: innerBoxIsScrolled, - expandedHeight: MediaQuery.of(context).size.width * 9 / 16, - collapsedHeight: MediaQuery.of(context).size.width * 9 / 16, - toolbarHeight: kToolbarHeight, - flexibleSpace: FlexibleSpaceBar( - background: Padding( - padding: EdgeInsets.only( - bottom: 50, - top: MediaQuery.of(context).padding.top), - child: LayoutBuilder( - builder: (context, boxConstraints) { - double maxWidth = boxConstraints.maxWidth; - double maxHeight = boxConstraints.maxHeight; - double PR = MediaQuery.of(context).devicePixelRatio; - return Hero( - tag: videoDetailController.heroTag, - child: NetworkImgLayer( - src: videoDetailController.videoItem['pic'], - width: maxWidth, - height: maxHeight, - ), - ); - }, - ), - ), - ), - bottom: PreferredSize( - preferredSize: const Size.fromHeight(50.0), - child: Container( - height: 50, - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: Theme.of(context) - .dividerColor - .withOpacity(0.1)))), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - children: [ - Container( - width: 280, - margin: const EdgeInsets.only(left: 20), - child: Obx( - () => TabBar( - splashBorderRadius: BorderRadius.circular(6), - dividerColor: Colors.transparent, - tabs: videoDetailController.tabs - .map((String name) => Tab(text: name)) - .toList(), - ), - ), + SliverAppBar( + title: const Text("视频详情"), + pinned: true, + elevation: 0, + scrolledUnderElevation: 0, + forceElevated: innerBoxIsScrolled, + expandedHeight: MediaQuery.of(context).size.width * 9 / 16, + collapsedHeight: MediaQuery.of(context).size.width * 9 / 16, + flexibleSpace: FlexibleSpaceBar( + background: Padding( + padding: EdgeInsets.only( + top: MediaQuery.of(context).padding.top), + child: LayoutBuilder( + builder: (context, boxConstraints) { + double maxWidth = boxConstraints.maxWidth; + double maxHeight = boxConstraints.maxHeight; + double PR = MediaQuery.of(context).devicePixelRatio; + return Hero( + tag: videoDetailController.heroTag, + child: NetworkImgLayer( + src: videoDetailController.videoItem['pic'], + width: maxWidth, + height: maxHeight, ), - // 弹幕开关 - // const Spacer(), - // Flexible( - // flex: 2, - // child: Container( - // height: 50, - // ), - // ), - ], - ), + ); + }, ), ), ), ), ]; }, - body: TabBarView( + pinnedHeaderSliverHeightBuilder: () { + return pinnedHeaderHeight; + }, + onlyOneScrollInBody: true, + body: Column( children: [ - Builder(builder: (context) { - return CustomScrollView( - key: const PageStorageKey('简介'), - slivers: [ - SliverOverlapInjector( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor( - context), + Container( + height: 50, + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Theme.of(context).dividerColor.withOpacity(0.1), ), - const VideoIntroPanel(), - SliverPadding( - padding: const EdgeInsets.only(top: 8, bottom: 5), - sliver: SliverToBoxAdapter( - child: Divider( - height: 1, - color: - Theme.of(context).dividerColor.withOpacity(0.1), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Container( + width: 280, + margin: const EdgeInsets.only(left: 20), + child: Obx( + () => TabBar( + splashBorderRadius: BorderRadius.circular(6), + dividerColor: Colors.transparent, + tabs: videoDetailController.tabs + .map((String name) => Tab(text: name)) + .toList(), ), ), ), - const RelatedVideoPanel(), + // 弹幕开关 + // const Spacer(), + // Flexible( + // flex: 2, + // child: Container( + // height: 50, + // ), + // ), ], - ); - }), - Builder(builder: (context) { - return CustomScrollView( - key: const PageStorageKey('评论'), - slivers: [ - SliverOverlapInjector( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor( - context), + ), + ), + Expanded( + child: TabBarView( + children: [ + Builder( + builder: (context) { + return CustomScrollView( + key: const PageStorageKey('简介'), + slivers: [ + const VideoIntroPanel(), + SliverPadding( + padding: + const EdgeInsets.only(top: 8, bottom: 5), + sliver: SliverToBoxAdapter( + child: Divider( + height: 1, + color: Theme.of(context) + .dividerColor + .withOpacity(0.1), + ), + ), + ), + const RelatedVideoPanel(), + ], + ); + }, ), - const VideoReplyPanel() + VideoReplyPanel() ], - ); - }) + ), + ), ], ), ), diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index 9ec967f8..0a0bd88b 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -1,6 +1,7 @@ import 'package:get/get.dart'; import 'package:pilipala/pages/home/index.dart'; import 'package:pilipala/pages/hot/index.dart'; +import 'package:pilipala/pages/preview/index.dart'; import 'package:pilipala/pages/video/detail/index.dart'; class Routes { @@ -11,5 +12,7 @@ class Routes { GetPage(name: '/hot', page: () => const HotPage()), // 视频详情 GetPage(name: '/video', page: () => const VideoDetailPage()), + // 图片预览 + GetPage(name: '/preview', page: () => const ImagePreview()) ]; } diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 084a6e6f..03d30181 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -1,10 +1,13 @@ // 工具函数 import 'dart:io'; import 'dart:math'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:get/get_utils/get_utils.dart'; import 'package:path_provider/path_provider.dart'; class Utils { + final ChromeSafariBrowser browser = ChromeSafariBrowser(); + static Future getCookiePath() async { Directory tempDir = await getApplicationSupportDirectory(); String tempPath = "${tempDir.path}/.plpl/"; @@ -135,4 +138,22 @@ class Utils { static String makeHeroTag(v) { return v.toString() + Random().nextInt(9999).toString(); } + + static openURL(aUrl) async { + try { + await Utils().browser.open( + url: Uri.parse(aUrl), + options: ChromeSafariBrowserClassOptions( + android: AndroidChromeCustomTabsOptions( + shareState: CustomTabsShareState.SHARE_STATE_OFF, + isSingleInstance: false, + isTrustedWebActivity: false, + keepAliveEnabled: true, + ), + ), + ); + } catch (err) { + await InAppBrowser.openWithSystemBrowser(url: Uri.parse(aUrl)); + } + } } diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 675b7194..fe56f8d8 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,9 +7,13 @@ #include "generated_plugin_registrant.h" #include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) dynamic_color_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin"); dynamic_color_plugin_register_with_registrar(dynamic_color_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 3e303c15..18366213 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST dynamic_color + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 65feaf20..bb5265fd 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,13 +6,19 @@ import FlutterMacOS import Foundation import connectivity_plus +import device_info_plus import dynamic_color import path_provider_foundation +import share_plus import sqflite +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) + DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 2a6cbf57..9f021f27 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -97,6 +97,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9" + url: "https://pub.dev" + source: hosted + version: "0.3.3+4" crypto: dependency: transitive description: @@ -121,6 +129,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.8" + device_info_plus: + dependency: "direct main" + description: + name: device_info_plus + sha256: f52ab3b76b36ede4d135aab80194df8925b553686f0fa12226b4e2d658e45903 + url: "https://pub.dev" + source: hosted + version: "8.2.2" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + url: "https://pub.dev" + source: hosted + version: "7.0.0" dio: dependency: "direct main" description: @@ -153,6 +177,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.6.3" + extended_image: + dependency: "direct main" + description: + name: extended_image + sha256: "75e4b0ad0f8f63eed7935ff2506c809a670f5e6dd0f61304525879d53fc41a17" + url: "https://pub.dev" + source: hosted + version: "7.0.2" + extended_image_library: + dependency: transitive + description: + name: extended_image_library + sha256: "550743b43ab093aed35ef234500fcc7a304cbac1eca47b0cc991e07e88750758" + url: "https://pub.dev" + source: hosted + version: "3.4.2" + extended_nested_scroll_view: + dependency: "direct main" + description: + name: extended_nested_scroll_view + sha256: fc55b8f7e2c78701320d7eccda3b256387290b8498f0363d8ffd6f16760949d7 + url: "https://pub.dev" + source: hosted + version: "6.0.0" fake_async: dependency: transitive description: @@ -198,6 +246,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.3.0" + flutter_inappwebview: + dependency: "direct main" + description: + name: flutter_inappwebview + sha256: "1c370ac07de80a579a0047c94c5bb586128d4ef50c0f3f501d6e77010374a319" + url: "https://pub.dev" + source: hosted + version: "5.4.4" flutter_lints: dependency: "direct dev" description: @@ -240,6 +296,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + http_client_helper: + dependency: transitive + description: + name: http_client_helper + sha256: "1f32359bd07a064ad256d1f84ae5f973f69bc972e7287223fa198abe1d969c28" + url: "https://pub.dev" + source: hosted + version: "2.0.3" http_parser: dependency: transitive description: @@ -248,6 +312,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + image_gallery_saver: + dependency: "direct main" + description: + name: image_gallery_saver + sha256: be812580c7a320d3bf583af89cac6b376f170d48000aca75215a73285a3223a0 + url: "https://pub.dev" + source: hosted + version: "1.7.1" js: dependency: transitive description: @@ -288,6 +360,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.8.0" + mime: + dependency: transitive + description: + name: mime + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" + source: hosted + version: "1.0.4" nm: dependency: transitive description: @@ -368,6 +448,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.11.1" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: "33c6a1253d1f95fd06fa74b65b7ba907ae9811f9d5c1d3150e51417d04b8d6a8" + url: "https://pub.dev" + source: hosted + version: "10.2.0" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "8028362b40c4a45298f1cbfccd227c8dd6caf0e27088a69f2ba2ab15464159e2" + url: "https://pub.dev" + source: hosted + version: "10.2.0" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: ee96ac32f5a8e6f80756e25b25b9f8e535816c8e6665a96b6d70681f8c4f7e85 + url: "https://pub.dev" + source: hosted + version: "9.0.8" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: "68abbc472002b5e6dfce47fe9898c6b7d8328d58b5d2524f75e277c07a97eb84" + url: "https://pub.dev" + source: hosted + version: "3.9.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: f67cab14b4328574938ecea2db3475dad7af7ead6afab6338772c5f88963e38b + url: "https://pub.dev" + source: hosted + version: "0.1.2" petitparser: dependency: transitive description: @@ -408,6 +528,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.27.7" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: b1f15232d41e9701ab2f04181f21610c36c83a12ae426b79b4bd011c567934b1 + url: "https://pub.dev" + source: hosted + version: "6.3.4" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "0c6e61471bd71b04a138b8b588fa388e66d8b005e6f2deda63371c5c505a0981" + url: "https://pub.dev" + source: hosted + version: "3.2.1" sky_engine: dependency: transitive description: flutter @@ -493,6 +629,70 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: "75f2846facd11168d007529d6cd8fcb2b750186bea046af9711f10b907e1587e" + url: "https://pub.dev" + source: hosted + version: "6.1.10" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "22f8db4a72be26e9e3a4aa3f194b1f7afbc76d20ec141f84be1d787db2155cbd" + url: "https://pub.dev" + source: hosted + version: "6.0.31" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2" + url: "https://pub.dev" + source: hosted + version: "6.1.4" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5" + url: "https://pub.dev" + source: hosted + version: "3.0.5" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "91ee3e75ea9dadf38036200c5d3743518f4a5eb77a8d13fda1ee5764373f185e" + url: "https://pub.dev" + source: hosted + version: "3.0.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "6c9ca697a5ae218ce56cece69d46128169a58aa8653c1b01d26fcd4aad8c4370" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "81fe91b6c4f84f222d186a9d23c73157dc4c8e1c71489c4d08be1ad3b228f1aa" + url: "https://pub.dev" + source: hosted + version: "2.0.16" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "254708f17f7c20a9c8c471f67d86d76d4a3f9c1591aad1e15292008aceb82771" + url: "https://pub.dev" + source: hosted + version: "3.0.6" uuid: dependency: transitive description: @@ -509,14 +709,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + visibility_detector: + dependency: transitive + description: + name: visibility_detector + sha256: "15c54a459ec2c17b4705450483f3d5a2858e733aee893dcee9d75fd04814940d" + url: "https://pub.dev" + source: hosted + version: "0.3.3" win32: dependency: transitive description: name: win32 - sha256: dd8f9344bc305ae2923e3d11a2a911d9a4e2c7dd6fe0ed10626d63211a69676e + sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4 url: "https://pub.dev" source: hosted - version: "4.1.3" + version: "3.1.4" xdg_directories: dependency: transitive description: @@ -535,4 +743,4 @@ packages: version: "6.2.2" sdks: dart: ">=2.19.6 <3.0.0" - flutter: ">=3.4.0-17.0.pre" + flutter: ">=3.7.0" diff --git a/pubspec.yaml b/pubspec.yaml index 7eaab185..7b78fe6a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -48,10 +48,24 @@ dependencies: # 图片 cached_network_image: ^3.2.3 - + extended_image: ^7.0.2 + image_gallery_saver: ^1.7.1 + # 存储 path_provider: ^2.0.14 + # 设备信息 + device_info_plus: ^8.2.0 + # 权限 + permission_handler: ^10.2.0 + # 分享 + share_plus: ^6.3.1 + # webView + url_launcher: ^6.1.9 + flutter_inappwebview: 5.4.4 + + extended_nested_scroll_view: ^6.0.0 + dev_dependencies: flutter_test: sdk: flutter @@ -88,17 +102,11 @@ flutter: # "family" key with the font family name, and a "fonts" key with a # list giving the asset and other descriptors for the font. For # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # + fonts: + - family: fansCard + fonts: + - asset: assets/fonts/fansCard.ttf + + # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index ae0cf3ff..efd465a4 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,10 +8,19 @@ #include #include +#include +#include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { ConnectivityPlusWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); DynamicColorPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); + SharePlusWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index e47678f2..afce192f 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,6 +5,9 @@ list(APPEND FLUTTER_PLUGIN_LIST connectivity_plus dynamic_color + permission_handler_windows + share_plus + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST