Merge pull request #2 from guozhigq/feature-videoReply

Feature video reply
This commit is contained in:
Infinite
2023-04-29 17:31:47 +08:00
committed by GitHub
26 changed files with 1221 additions and 290 deletions

View File

@ -51,6 +51,7 @@ android {
targetSdkVersion flutter.targetSdkVersion targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
minSdkVersion 17
} }
buildTypes { buildTypes {

BIN
assets/fonts/fansCard.ttf Normal file

Binary file not shown.

View File

@ -2,46 +2,90 @@ PODS:
- connectivity_plus (0.0.1): - connectivity_plus (0.0.1):
- Flutter - Flutter
- ReachabilitySwift - ReachabilitySwift
- device_info_plus (0.0.1):
- Flutter
- Flutter (1.0.0) - 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 (2.7.5):
- FMDB/standard (= 2.7.5) - FMDB/standard (= 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): - path_provider_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- permission_handler_apple (9.0.4):
- Flutter
- ReachabilitySwift (5.0.0) - ReachabilitySwift (5.0.0)
- share_plus (0.0.1):
- Flutter
- sqflite (0.0.2): - sqflite (0.0.2):
- Flutter - Flutter
- FMDB (>= 2.7.5) - FMDB (>= 2.7.5)
- url_launcher_ios (0.0.1):
- Flutter
DEPENDENCIES: DEPENDENCIES:
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`) - 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`) - 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`) - sqflite (from `.symlinks/plugins/sqflite/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
SPEC REPOS: SPEC REPOS:
trunk: trunk:
- FMDB - FMDB
- OrderedSet
- ReachabilitySwift - ReachabilitySwift
EXTERNAL SOURCES: EXTERNAL SOURCES:
connectivity_plus: connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios" :path: ".symlinks/plugins/connectivity_plus/ios"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
Flutter: Flutter:
:path: 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_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/ios" :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: sqflite:
:path: ".symlinks/plugins/sqflite/ios" :path: ".symlinks/plugins/sqflite/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
SPEC CHECKSUMS: SPEC CHECKSUMS:
connectivity_plus: 413a8857dd5d9f1c399a39130850d02fe0feaf7e connectivity_plus: 413a8857dd5d9f1c399a39130850d02fe0feaf7e
device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
image_gallery_saver: 259eab68fb271cfd57d599904f7acdc7832e7ef2
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
path_provider_foundation: c68054786f1b4f3343858c1e1d0caaded73f0be9 path_provider_foundation: c68054786f1b4f3343858c1e1d0caaded73f0be9
permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3 PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3

View File

@ -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<Offset>(
begin: Offset.zero,
end: const Offset(0, -1),
).animate(CurvedAnimation(
parent: controller,
curve: Curves.easeInOutBack,
)),
child: child,
);
}
}

View File

@ -30,8 +30,11 @@ class NetworkImgLayer extends StatelessWidget {
// double pr = 2; // double pr = 2;
return src != '' return src != ''
? ClipRRect( ? ClipRRect(
borderRadius: BorderRadius.circular( borderRadius: BorderRadius.circular(type == 'avatar'
type == 'avatar' ? 50 : StyleString.imgRadius.x), ? 50
: type == 'emote'
? 0
: StyleString.imgRadius.x),
child: CachedNetworkImage( child: CachedNetworkImage(
imageUrl: src!, imageUrl: src!,
width: width ?? double.infinity, width: width ?? double.infinity,

View File

@ -16,4 +16,7 @@ class Api {
// 评论列表 // 评论列表
static const String replyList = '/x/v2/reply'; static const String replyList = '/x/v2/reply';
// 楼中楼
static const String replyReplyList = '/x/v2/reply/reply';
} }

View File

@ -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']] ?? '请求异常',
};
}
}
} }

View File

@ -22,10 +22,12 @@ class ReplyData {
ReplyData.fromJson(Map<String, dynamic> json) { ReplyData.fromJson(Map<String, dynamic> json) {
page = ReplyPage.fromJson(json['page']); page = ReplyPage.fromJson(json['page']);
config = ReplyConfig.fromJson(json['config']); config = ReplyConfig.fromJson(json['config']);
replies = json['replies'] replies = json['replies'] != null
.map<ReplyItemModel>( ? json['replies']
(item) => ReplyItemModel.fromJson(item, json['upper']['mid'])) .map<ReplyItemModel>(
.toList(); (item) => ReplyItemModel.fromJson(item, json['upper']['mid']))
.toList()
: [];
topReplies = json['top_replies'] != null topReplies = json['top_replies'] != null
? json['top_replies'] ? json['top_replies']
.map<ReplyItemModel>((item) => ReplyItemModel.fromJson( .map<ReplyItemModel>((item) => ReplyItemModel.fromJson(

View File

@ -149,6 +149,6 @@ class ReplyControl {
entryText = json['sub_reply_entry_text']; entryText = json['sub_reply_entry_text'];
titleText = json['sub_reply_title_text']; titleText = json['sub_reply_title_text'];
time = json['time_desc']; time = json['time_desc'];
location = json['location']; location = json['location'] ?? '';
} }
} }

View File

@ -1,4 +1,4 @@
import 'dart:convert' as convert; import 'package:get/get.dart';
class ReplyMember { class ReplyMember {
ReplyMember({ ReplyMember({
@ -22,6 +22,7 @@ class ReplyMember {
Map? officialVerify; Map? officialVerify;
Map? vip; Map? vip;
Map? fansDetail; Map? fansDetail;
UserSailing? userSailing;
ReplyMember.fromJson(Map<String, dynamic> json) { ReplyMember.fromJson(Map<String, dynamic> json) {
mid = json['mid']; mid = json['mid'];
@ -30,9 +31,12 @@ class ReplyMember {
avatar = json['avatar']; avatar = json['avatar'];
level = json['level_info']['current_level']; level = json['level_info']['current_level'];
pendant = Pendant.fromJson(json['pendant']); pendant = Pendant.fromJson(json['pendant']);
officialVerify = json['officia_vVerify']; officialVerify = json['officia_verify'];
vip = json['vip']; vip = json['vip'];
fansDetail = json['fans_detail']; fansDetail = json['fans_detail'];
userSailing = json['user_sailing'] != null
? UserSailing.fromJson(json['user_sailing'])
: UserSailing();
} }
} }
@ -53,3 +57,15 @@ class Pendant {
image = json['image']; image = json['image'];
} }
} }
class UserSailing {
UserSailing({this.pendant, this.cardbg});
Map? pendant;
Map? cardbg;
UserSailing.fromJson(Map<String, dynamic> json) {
pendant = json['pendant'];
cardbg = json['cardbg'];
}
}

View File

@ -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<Permission, PermissionStatus> 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]);
}
}

View File

@ -0,0 +1,4 @@
library preview;
export './controller.dart';
export './view.dart';

188
lib/pages/preview/view.dart Normal file
View File

@ -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<ImagePreview>
with TickerProviderStateMixin {
final PreviewController _previewController = Get.put(PreviewController());
late AnimationController animationController;
late AnimationController _doubleClickAnimationController;
Animation<double>? _doubleClickAnimation;
late DoubleClickAnimationListener _doubleClickAnimationListener;
List<double> doubleTapScales = <double>[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) => <PopupMenuEntry>[
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<double>(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: <Widget>[
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),
),
);
}
}

View File

@ -1,22 +1,80 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/http/reply.dart'; import 'package:pilipala/http/reply.dart';
import 'package:pilipala/models/video/reply/data.dart'; import 'package:pilipala/models/video/reply/data.dart';
import 'package:pilipala/models/video/reply/item.dart';
class VideoReplyController extends GetxController { class VideoReplyController extends GetxController {
// 视频aid VideoReplyController(
String aid = Get.parameters['aid']!; this.aid,
this.rpid,
this.level,
);
final ScrollController scrollController = ScrollController();
// 视频aid 请求时使用的oid
String? aid;
// 层级 2为楼中楼
String? level;
// rpid 请求楼中楼回复
String? rpid;
RxList<ReplyItemModel> replyList = [ReplyItemModel()].obs;
// 当前页
int currentPage = 0;
bool isLoadingMore = false;
RxBool noMore = false.obs;
@override Future queryReplyList({type = 'init'}) async {
void onInit() { isLoadingMore = true;
super.onInit(); var res = level == '1'
queryReplyList(); ? await ReplyHttp.replyList(
} oid: aid!, pageNum: currentPage + 1, type: 1)
: await ReplyHttp.replyReplyList(
Future queryReplyList() async { oid: aid!, root: rpid!, pageNum: currentPage + 1, type: 1);
var res = await ReplyHttp.replyList(oid: aid, pageNum: 1, type: 1);
if (res['status']) { if (res['status']) {
res['data'] = ReplyData.fromJson(res['data']); 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<ReplyItemModel> 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; return res;
} }
// 上拉加载
Future onLoad() async {
queryReplyList(type: 'onLoad');
}
} }

View File

@ -7,7 +7,15 @@ import 'controller.dart';
import 'widgets/reply_item.dart'; import 'widgets/reply_item.dart';
class VideoReplyPanel extends StatefulWidget { 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 @override
State<VideoReplyPanel> createState() => _VideoReplyPanelState(); State<VideoReplyPanel> createState() => _VideoReplyPanelState();
@ -15,66 +23,108 @@ class VideoReplyPanel extends StatefulWidget {
class _VideoReplyPanelState extends State<VideoReplyPanel> class _VideoReplyPanelState extends State<VideoReplyPanel>
with AutomaticKeepAliveClientMixin { with AutomaticKeepAliveClientMixin {
final VideoReplyController _videoReplyController = late VideoReplyController _videoReplyController;
Get.put(VideoReplyController(), tag: Get.arguments['heroTag']);
// List<ReplyItemModel>? replyList;
Future? _futureBuilderFuture;
// 添加页面缓存 // 添加页面缓存
@override @override
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
@override @override
Widget build(BuildContext context) { void initState() {
return FutureBuilder( super.initState();
future: _videoReplyController.queryReplyList(), if (widget.level == '2') {
builder: (context, snapshot) { _videoReplyController = Get.put(
if (snapshot.connectionState == ConnectionState.done) { VideoReplyController(
if (snapshot.data['status']) { widget.oid.toString(), widget.rpid.toString(), '2'),
List<ReplyItemModel> replies = snapshot.data['data'].replies; tag: widget.rpid.toString());
// 添加置顶回复 } else {
if (snapshot.data['data'].upper.top != null) { _videoReplyController = Get.put(
bool flag = false; VideoReplyController(Get.parameters['aid']!, '', '1'),
for (var i = 0; tag: Get.arguments['heroTag']);
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);
}
}
replies.insertAll(0, snapshot.data['data'].topReplies); _futureBuilderFuture = _videoReplyController.queryReplyList();
// 请求成功 _videoReplyController.scrollController.addListener(
return SliverList( () {
delegate: SliverChildBuilderDelegate((context, index) { if (_videoReplyController.scrollController.position.pixels >=
if (index == replies.length) { _videoReplyController.scrollController.position.maxScrollExtent -
return SizedBox(height: MediaQuery.of(context).padding.bottom); 300) {
} else { if (!_videoReplyController.isLoadingMore) {
return ReplyItem( _videoReplyController.onLoad();
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(() {}),
);
} }
} 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<String>('评论'),
slivers: <Widget>[
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),
);
}
},
)
],
),
);
}
} }

View File

@ -1,13 +1,14 @@
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/video/reply/item.dart'; import 'package:pilipala/models/video/reply/item.dart';
import 'package:pilipala/pages/video/detail/reply/index.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
class ReplyItem extends StatelessWidget { class ReplyItem extends StatelessWidget {
ReplyItem({super.key, this.replyItem, required this.isUp}); ReplyItem({super.key, this.replyItem});
ReplyItemModel? replyItem; ReplyItemModel? replyItem;
bool isUp = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -16,11 +17,13 @@ class ReplyItem extends StatelessWidget {
child: Column( child: Column(
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.fromLTRB(12, 6, 8, 0), padding: const EdgeInsets.fromLTRB(12, 8, 8, 2),
child: content(context), child: content(context),
), ),
Divider( Divider(
height: 1, height: 1,
indent: 52,
endIndent: 10,
color: Theme.of(context).dividerColor.withOpacity(0.08), color: Theme.of(context).dividerColor.withOpacity(0.08),
) )
], ],
@ -30,14 +33,42 @@ class ReplyItem extends StatelessWidget {
Widget lfAvtar(context) { Widget lfAvtar(context) {
return Container( return Container(
margin: const EdgeInsets.only(top: 5), margin: const EdgeInsets.only(top: 5),
child: NetworkImgLayer( child: Stack(
src: replyItem!.member!.avatar, children: [
width: 30, NetworkImgLayer(
height: 30, src: replyItem!.member!.avatar,
type: '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) { Widget content(context) {
@ -45,51 +76,80 @@ class ReplyItem extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
// 头像、昵称 // 头像、昵称
Row( GestureDetector(
// 两端对齐 // onTap: () =>
mainAxisAlignment: MainAxisAlignment.spaceBetween, // Get.toNamed('/member/${reply.userName}', parameters: {
children: <Widget>[ // 'memberAvatar': reply.avatar,
GestureDetector( // 'heroTag': reply.userName + heroTag,
// onTap: () => // }),
// Get.toNamed('/member/${reply.userName}', parameters: { child: Container(
// 'memberAvatar': reply.avatar, width: double.infinity,
// 'heroTag': reply.userName + heroTag, decoration: BoxDecoration(
// }), image: replyItem!.member!.userSailing!.cardbg != null
child: Row( ? DecorationImage(
crossAxisAlignment: CrossAxisAlignment.center, fit: BoxFit.cover,
mainAxisSize: MainAxisSize.min, image: NetworkImage(
children: <Widget>[ replyItem!.member!.userSailing!.cardbg!['image'],
lfAvtar(context), ),
const SizedBox(width: 12), )
Column( : null,
crossAxisAlignment: CrossAxisAlignment.start, ),
children: [ child: Stack(
Row( children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
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: [ children: [
const Text('NO.'),
Text( Text(
replyItem!.member!.uname!, replyItem!.member!.userSailing!.cardbg!['fan']
style: Theme.of(context) ['num_desc'],
.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,
), ),
], ],
), ),
], ),
) ),
], ],
),
), ),
], ),
), ),
// title // title
Container( Container(
@ -102,6 +162,8 @@ class ReplyItem extends StatelessWidget {
style: const TextStyle(height: 1.65), style: const TextStyle(height: 1.65),
TextSpan( TextSpan(
children: [ children: [
if (replyItem!.isTop!)
WidgetSpan(child: UpTag(tagText: '置顶')),
buildContent(context, replyItem!.content!), buildContent(context, replyItem!.content!),
], ],
), ),
@ -116,6 +178,7 @@ class ReplyItem extends StatelessWidget {
child: ReplyItemRow( child: ReplyItemRow(
replies: replyItem!.replies, replies: replyItem!.replies,
replyControl: replyItem!.replyControl, replyControl: replyItem!.replyControl,
f_rpid: replyItem!.rpid,
), ),
), ),
], ],
@ -136,26 +199,9 @@ class ReplyItem extends StatelessWidget {
.labelMedium! .labelMedium!
.copyWith(color: Theme.of(context).colorScheme.outline), .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(), const Spacer(),
if (replyControl!.isUpTop!)
Icon(Icons.favorite, color: Colors.red[400], size: 18),
SizedBox( SizedBox(
height: 35, height: 35,
child: TextButton( child: TextButton(
@ -191,9 +237,11 @@ class ReplyItemRow extends StatelessWidget {
super.key, super.key,
this.replies, this.replies,
this.replyControl, this.replyControl,
this.f_rpid,
}); });
List? replies; List? replies;
ReplyControl? replyControl; ReplyControl? replyControl;
int? f_rpid;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -202,7 +250,7 @@ class ReplyItemRow extends StatelessWidget {
return Container( return Container(
margin: const EdgeInsets.only(left: 42, right: 4, top: 0), margin: const EdgeInsets.only(left: 42, right: 4, top: 0),
child: Material( child: Material(
color: Theme.of(context).colorScheme.onInverseSurface.withOpacity(0.7), color: Theme.of(context).colorScheme.onInverseSurface,
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
animationDuration: Duration.zero, animationDuration: Duration.zero,
@ -215,7 +263,7 @@ class ReplyItemRow extends StatelessWidget {
if (extraRow == 1 && index == replies!.length) { if (extraRow == 1 && index == replies!.length) {
// 有楼中楼回复,在最后显示 // 有楼中楼回复,在最后显示
return InkWell( return InkWell(
onTap: () {}, onTap: () => replyReply(context),
child: Padding( child: Padding(
padding: padding:
const EdgeInsets.symmetric(vertical: 8, horizontal: 8), const EdgeInsets.symmetric(vertical: 8, horizontal: 8),
@ -243,23 +291,23 @@ class ReplyItemRow extends StatelessWidget {
return InkWell( return InkWell(
onTap: () {}, onTap: () {},
child: Padding( 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( child: Text.rich(
overflow: TextOverflow.ellipsis, overflow: extraRow == 1
maxLines: 2, ? TextOverflow.ellipsis
: TextOverflow.visible,
maxLines: extraRow == 1 ? 2 : null,
TextSpan( TextSpan(
children: [ children: [
if (replies![index].isUp) if (replies![index].isUp)
TextSpan( WidgetSpan(
text: 'UP • ', child: UpTag(),
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize,
color: Theme.of(context).colorScheme.primary,
),
), ),
TextSpan( TextSpan(
text: replies![index].member.uname + ' ', 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) { InlineSpan buildContent(BuildContext context, content) {
@ -301,14 +391,16 @@ InlineSpan buildContent(BuildContext context, content) {
RegExp(r"\[.*?\]"), RegExp(r"\[.*?\]"),
onMatch: (Match match) { onMatch: (Match match) {
String matchStr = match[0]!; String matchStr = match[0]!;
int size = content.emote[matchStr]['meta']['size'];
if (content.emote.isNotEmpty) { if (content.emote.isNotEmpty) {
if (content.emote.keys.contains(matchStr)) { if (content.emote.keys.contains(matchStr)) {
spanChilds.add( spanChilds.add(
WidgetSpan( WidgetSpan(
child: NetworkImgLayer( child: NetworkImgLayer(
src: content.emote[matchStr]['url'], src: content.emote[matchStr]['url'],
width: 20, type: 'emote',
height: 20, width: size * 20,
height: size * 20,
), ),
), ),
); );
@ -394,26 +486,95 @@ InlineSpan buildContent(BuildContext context, content) {
// 图片渲染 // 图片渲染
if (content.pictures.isNotEmpty) { if (content.pictures.isNotEmpty) {
spanChilds.add( List<Widget> list = [];
const TextSpan(text: '\n'), List picList = [];
); int len = content.pictures.length;
for (var i = 0; i < content.pictures.length; i++) { for (var i = 0; i < len; i++) {
spanChilds.add( picList.add(content.pictures[i]['img_src']);
WidgetSpan( list.add(
child: SizedBox( LayoutBuilder(
height: 180, builder: (context, BoxConstraints box) {
child: NetworkImgLayer( return GestureDetector(
src: content.pictures[i]['img_src'], onTap: () {
width: 200, Get.toNamed('/preview',
height: 200 * arguments: {'initialPage': i, 'imgList': picList});
content.pictures[i]['img_height'] / },
content.pictures[i]['img_width'], 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)); // spanChilds.add(TextSpan(text: matchMember));
return TextSpan(children: spanChilds); 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,
),
),
),
);
}
}

View File

@ -1,3 +1,4 @@
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
@ -19,6 +20,10 @@ class _VideoDetailPageState extends State<VideoDetailPage> {
@override @override
Widget build(BuildContext context) { 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( return DefaultTabController(
initialIndex: videoDetailController.tabInitialIndex, initialIndex: videoDetailController.tabInitialIndex,
length: videoDetailController.tabs.length, // tab的数量. length: videoDetailController.tabs.length, // tab的数量.
@ -26,126 +31,115 @@ class _VideoDetailPageState extends State<VideoDetailPage> {
top: false, top: false,
bottom: false, bottom: false,
child: Scaffold( child: Scaffold(
body: NestedScrollView( body: ExtendedNestedScrollView(
headerSliverBuilder: headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) { (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[ return <Widget>[
SliverOverlapAbsorber( SliverAppBar(
handle: title: const Text("视频详情"),
NestedScrollView.sliverOverlapAbsorberHandleFor(context), pinned: true,
sliver: SliverAppBar( elevation: 0,
title: const Text("视频详情"), scrolledUnderElevation: 0,
// floating: true, forceElevated: innerBoxIsScrolled,
// snap: true, expandedHeight: MediaQuery.of(context).size.width * 9 / 16,
pinned: true, collapsedHeight: MediaQuery.of(context).size.width * 9 / 16,
elevation: 0, flexibleSpace: FlexibleSpaceBar(
scrolledUnderElevation: 0, background: Padding(
forceElevated: innerBoxIsScrolled, padding: EdgeInsets.only(
expandedHeight: MediaQuery.of(context).size.width * 9 / 16, top: MediaQuery.of(context).padding.top),
collapsedHeight: MediaQuery.of(context).size.width * 9 / 16, child: LayoutBuilder(
toolbarHeight: kToolbarHeight, builder: (context, boxConstraints) {
flexibleSpace: FlexibleSpaceBar( double maxWidth = boxConstraints.maxWidth;
background: Padding( double maxHeight = boxConstraints.maxHeight;
padding: EdgeInsets.only( double PR = MediaQuery.of(context).devicePixelRatio;
bottom: 50, return Hero(
top: MediaQuery.of(context).padding.top), tag: videoDetailController.heroTag,
child: LayoutBuilder( child: NetworkImgLayer(
builder: (context, boxConstraints) { src: videoDetailController.videoItem['pic'],
double maxWidth = boxConstraints.maxWidth; width: maxWidth,
double maxHeight = boxConstraints.maxHeight; height: 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(),
),
),
), ),
// 弹幕开关 );
// const Spacer(), },
// Flexible(
// flex: 2,
// child: Container(
// height: 50,
// ),
// ),
],
),
), ),
), ),
), ),
), ),
]; ];
}, },
body: TabBarView( pinnedHeaderSliverHeightBuilder: () {
return pinnedHeaderHeight;
},
onlyOneScrollInBody: true,
body: Column(
children: [ children: [
Builder(builder: (context) { Container(
return CustomScrollView( height: 50,
key: const PageStorageKey<String>('简介'), decoration: BoxDecoration(
slivers: <Widget>[ border: Border(
SliverOverlapInjector( bottom: BorderSide(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor( color: Theme.of(context).dividerColor.withOpacity(0.1),
context),
), ),
const VideoIntroPanel(), ),
SliverPadding( ),
padding: const EdgeInsets.only(top: 8, bottom: 5), child: Row(
sliver: SliverToBoxAdapter( mainAxisAlignment: MainAxisAlignment.center,
child: Divider( mainAxisSize: MainAxisSize.max,
height: 1, children: [
color: Container(
Theme.of(context).dividerColor.withOpacity(0.1), 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) { Expanded(
return CustomScrollView( child: TabBarView(
key: const PageStorageKey<String>('评论'), children: [
slivers: <Widget>[ Builder(
SliverOverlapInjector( builder: (context) {
handle: NestedScrollView.sliverOverlapAbsorberHandleFor( return CustomScrollView(
context), key: const PageStorageKey<String>('简介'),
slivers: <Widget>[
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()
], ],
); ),
}) ),
], ],
), ),
), ),

View File

@ -1,6 +1,7 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/pages/home/index.dart'; import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/hot/index.dart'; import 'package:pilipala/pages/hot/index.dart';
import 'package:pilipala/pages/preview/index.dart';
import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/video/detail/index.dart';
class Routes { class Routes {
@ -11,5 +12,7 @@ class Routes {
GetPage(name: '/hot', page: () => const HotPage()), GetPage(name: '/hot', page: () => const HotPage()),
// 视频详情 // 视频详情
GetPage(name: '/video', page: () => const VideoDetailPage()), GetPage(name: '/video', page: () => const VideoDetailPage()),
// 图片预览
GetPage(name: '/preview', page: () => const ImagePreview())
]; ];
} }

View File

@ -1,10 +1,13 @@
// 工具函数 // 工具函数
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:get/get_utils/get_utils.dart'; import 'package:get/get_utils/get_utils.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
class Utils { class Utils {
final ChromeSafariBrowser browser = ChromeSafariBrowser();
static Future<String> getCookiePath() async { static Future<String> getCookiePath() async {
Directory tempDir = await getApplicationSupportDirectory(); Directory tempDir = await getApplicationSupportDirectory();
String tempPath = "${tempDir.path}/.plpl/"; String tempPath = "${tempDir.path}/.plpl/";
@ -135,4 +138,22 @@ class Utils {
static String makeHeroTag(v) { static String makeHeroTag(v) {
return v.toString() + Random().nextInt(9999).toString(); 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));
}
}
} }

View File

@ -7,9 +7,13 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <dynamic_color/dynamic_color_plugin.h> #include <dynamic_color/dynamic_color_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) dynamic_color_registrar = g_autoptr(FlPluginRegistrar) dynamic_color_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin");
dynamic_color_plugin_register_with_registrar(dynamic_color_registrar); 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);
} }

View File

@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
dynamic_color dynamic_color
url_launcher_linux
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

@ -6,13 +6,19 @@ import FlutterMacOS
import Foundation import Foundation
import connectivity_plus import connectivity_plus
import device_info_plus
import dynamic_color import dynamic_color
import path_provider_foundation import path_provider_foundation
import share_plus
import sqflite import sqflite
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
} }

View File

@ -97,6 +97,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.1" 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: crypto:
dependency: transitive dependency: transitive
description: description:
@ -121,6 +129,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.8" 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: dio:
dependency: "direct main" dependency: "direct main"
description: description:
@ -153,6 +177,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.6.3" 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: fake_async:
dependency: transitive dependency: transitive
description: description:
@ -198,6 +246,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.3.0" 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: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -240,6 +296,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.1" 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: http_parser:
dependency: transitive dependency: transitive
description: description:
@ -248,6 +312,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.2" 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: js:
dependency: transitive dependency: transitive
description: description:
@ -288,6 +360,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.8.0" version: "1.8.0"
mime:
dependency: transitive
description:
name: mime
sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e
url: "https://pub.dev"
source: hosted
version: "1.0.4"
nm: nm:
dependency: transitive dependency: transitive
description: description:
@ -368,6 +448,46 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.11.1" 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: petitparser:
dependency: transitive dependency: transitive
description: description:
@ -408,6 +528,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.27.7" 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: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -493,6 +629,70 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.1" 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: uuid:
dependency: transitive dependency: transitive
description: description:
@ -509,14 +709,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.4" 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: win32:
dependency: transitive dependency: transitive
description: description:
name: win32 name: win32
sha256: dd8f9344bc305ae2923e3d11a2a911d9a4e2c7dd6fe0ed10626d63211a69676e sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.3" version: "3.1.4"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:
@ -535,4 +743,4 @@ packages:
version: "6.2.2" version: "6.2.2"
sdks: sdks:
dart: ">=2.19.6 <3.0.0" dart: ">=2.19.6 <3.0.0"
flutter: ">=3.4.0-17.0.pre" flutter: ">=3.7.0"

View File

@ -48,10 +48,24 @@ dependencies:
# 图片 # 图片
cached_network_image: ^3.2.3 cached_network_image: ^3.2.3
extended_image: ^7.0.2
image_gallery_saver: ^1.7.1
# 存储 # 存储
path_provider: ^2.0.14 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: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
@ -88,17 +102,11 @@ flutter:
# "family" key with the font family name, and a "fonts" key with a # "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For # list giving the asset and other descriptors for the font. For
# example: # example:
# fonts: fonts:
# - family: Schyler - family: fansCard
# fonts: fonts:
# - asset: fonts/Schyler-Regular.ttf - asset: assets/fonts/fansCard.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies, # For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages # see https://flutter.dev/custom-fonts/#from-packages

View File

@ -8,10 +8,19 @@
#include <connectivity_plus/connectivity_plus_windows_plugin.h> #include <connectivity_plus/connectivity_plus_windows_plugin.h>
#include <dynamic_color/dynamic_color_plugin_c_api.h> #include <dynamic_color/dynamic_color_plugin_c_api.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
ConnectivityPlusWindowsPluginRegisterWithRegistrar( ConnectivityPlusWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
DynamicColorPluginCApiRegisterWithRegistrar( DynamicColorPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); registry->GetRegistrarForPlugin("DynamicColorPluginCApi"));
PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
} }

View File

@ -5,6 +5,9 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
connectivity_plus connectivity_plus
dynamic_color dynamic_color
permission_handler_windows
share_plus
url_launcher_windows
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST