diff --git a/lib/common/widgets/network_img_layer.dart b/lib/common/widgets/network_img_layer.dart index 84d8ccdf..893dd66f 100644 --- a/lib/common/widgets/network_img_layer.dart +++ b/lib/common/widgets/network_img_layer.dart @@ -2,6 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; import 'package:PiliPalaX/utils/extension.dart'; +import 'package:PiliPalaX/utils/global_data.dart'; import '../../utils/storage.dart'; import '../constants.dart'; @@ -32,8 +33,10 @@ class NetworkImgLayer extends StatelessWidget { @override Widget build(BuildContext context) { + final int defaultImgQuality = GlobalData().imgQuality; final String imageUrl = - '${src!.startsWith('//') ? 'https:${src!}' : src!}@${quality ?? 100}q.webp'; + '${src!.startsWith('//') ? 'https:${src!}' : src!}@${quality ?? defaultImgQuality}q.webp'; + print(imageUrl); int? memCacheWidth, memCacheHeight; if (width > height || (origAspectRatio != null && origAspectRatio! > 1)) { @@ -68,7 +71,7 @@ class NetworkImgLayer extends StatelessWidget { fadeOutDuration ?? const Duration(milliseconds: 120), fadeInDuration: fadeInDuration ?? const Duration(milliseconds: 120), - filterQuality: FilterQuality.high, + filterQuality: FilterQuality.low, errorWidget: (BuildContext context, String url, Object error) => placeholder(context), placeholder: (BuildContext context, String url) => @@ -91,17 +94,19 @@ class NetworkImgLayer extends StatelessWidget { ? 0 : StyleString.imgRadius.x), ), - child: Center( - child: Image.asset( - type == 'avatar' - ? 'assets/images/noface.jpeg' - : 'assets/images/loading.png', - width: width, - height: height, - cacheWidth: width.cacheSize(context), - cacheHeight: height.cacheSize(context), - ), - ), + child: type == 'bg' + ? const SizedBox() + : Center( + child: Image.asset( + type == 'avatar' + ? 'assets/images/noface.jpeg' + : 'assets/images/loading.png', + width: width, + height: height, + cacheWidth: width.cacheSize(context), + cacheHeight: height.cacheSize(context), + ), + ), ); } } diff --git a/lib/http/api.dart b/lib/http/api.dart index ec06cc3f..774bd453 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -505,4 +505,20 @@ class Api { /// 激活buvid3 static const activateBuvidApi = '/x/internal/gaia-gateway/ExClimbWuzhi'; + + /// 我的订阅 + static const userSubFolder = '/x/v3/fav/folder/collected/list'; + + /// 我的订阅详情 + static const userSubFolderDetail = '/x/space/fav/season/list'; + + /// 表情 + static const emojiList = '/x/emote/user/panel/web'; + + /// 已读标记 + static const String ackSessionMsg = + '${HttpString.tUrl}/session_svr/v1/session_svr/update_ack'; + + /// 发送私信 + static const String sendMsg = '${HttpString.tUrl}/web_im/v1/web_im/send_msg'; } diff --git a/lib/http/msg.dart b/lib/http/msg.dart index a33c1f8d..0fcf1b24 100644 --- a/lib/http/msg.dart +++ b/lib/http/msg.dart @@ -1,3 +1,4 @@ +import 'dart:math'; import '../models/msg/account.dart'; import '../models/msg/session.dart'; import '../utils/wbi_sign.dart'; @@ -152,10 +153,18 @@ class MsgHttp { Map signParams = await WbiSign().makSign(params); var res = await Request().get(Api.sessionList, data: signParams); if (res.data['code'] == 0) { - return { - 'status': true, - 'data': SessionDataModel.fromJson(res.data['data']), - }; + try { + return { + 'status': true, + 'data': SessionDataModel.fromJson(res.data['data']), + }; + } catch (err) { + return { + 'status': false, + 'date': [], + 'msg': err.toString(), + }; + } } else { return { 'status': false, @@ -172,12 +181,16 @@ class MsgHttp { 'mobi_app': 'web', }); if (res.data['code'] == 0) { - return { - 'status': true, - 'data': res.data['data'] - .map((e) => AccountListModel.fromJson(e)) - .toList(), - }; + try { + return { + 'status': true, + 'data': res.data['data'] + .map((e) => AccountListModel.fromJson(e)) + .toList(), + }; + } catch (err) { + print('err🔟: $err'); + } } else { return { 'status': false, @@ -217,6 +230,7 @@ class MsgHttp { } } + // 消息标记已读 static Future ackSessionMsg({ int? talkerId, int? ackSeqno, @@ -247,4 +261,93 @@ class MsgHttp { }; } } + + // 发送私信 + static Future sendMsg({ + int? senderUid, + int? receiverId, + int? receiverType, + int? msgType, + dynamic content, + }) async { + String csrf = await Request.getCsrf(); + Map params = await WbiSign().makSign({ + 'msg[sender_uid]': senderUid, + 'msg[receiver_id]': receiverId, + 'msg[receiver_type]': receiverType ?? 1, + 'msg[msg_type]': msgType ?? 1, + 'msg[msg_status]': 0, + 'msg[dev_id]': getDevId(), + 'msg[timestamp]': DateTime.now().millisecondsSinceEpoch ~/ 1000, + 'msg[new_face_version]': 0, + 'msg[content]': content, + 'from_firework': 0, + 'build': 0, + 'mobi_app': 'web', + 'csrf_token': csrf, + 'csrf': csrf, + }); + var res = + await Request().post(Api.sendMsg, queryParameters: { + ...params, + 'csrf_token': csrf, + 'csrf': csrf, + }, data: { + 'w_sender_uid': params['msg[sender_uid]'], + 'w_receiver_id': params['msg[receiver_id]'], + 'w_dev_id': params['msg[dev_id]'], + 'w_rid': params['w_rid'], + 'wts': params['wts'], + 'csrf_token': csrf, + 'csrf': csrf, + }); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': res.data['data'], + }; + } else { + return { + 'status': false, + 'date': [], + 'msg': "message: ${res.data['message']}," + " msg: ${res.data['msg']}," + " code: ${res.data['code']}", + }; + } + } + + static String getDevId() { + final List b = [ + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + 'A', + 'B', + 'C', + 'D', + 'E', + 'F' + ]; + final List s = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".split(''); + for (int i = 0; i < s.length; i++) { + if ('-' == s[i] || '4' == s[i]) { + continue; + } + final int randomInt = Random().nextInt(16); + if ('x' == s[i]) { + s[i] = b[randomInt]; + } else { + s[i] = b[3 & randomInt | 8]; + } + } + return s.join(); + } } diff --git a/lib/http/reply.dart b/lib/http/reply.dart index 17ad1dfc..e367340f 100644 --- a/lib/http/reply.dart +++ b/lib/http/reply.dart @@ -1,4 +1,5 @@ import '../models/video/reply/data.dart'; +import '../models/video/reply/emote.dart'; import 'api.dart'; import 'init.dart'; @@ -101,16 +102,16 @@ class ReplyHttp { } } - static Future getMyEmote({ - required String business, - }) async { + + static Future getEmoteList({String? business}) async { var res = await Request().get(Api.myEmote, data: { - 'business': business, + 'business': business ?? 'reply', + 'web_location': '333.1245', }); if (res.data['code'] == 0) { return { 'status': true, - 'data': res.data['data'], + 'data': EmoteModelData.fromJson(res.data['data']), }; } else { return { diff --git a/lib/http/user.dart b/lib/http/user.dart index c1f86285..7d3def4e 100644 --- a/lib/http/user.dart +++ b/lib/http/user.dart @@ -6,6 +6,8 @@ import '../models/user/fav_folder.dart'; import '../models/user/history.dart'; import '../models/user/info.dart'; import '../models/user/stat.dart'; +import '../models/user/sub_detail.dart'; +import '../models/user/sub_folder.dart'; import 'api.dart'; import 'init.dart'; @@ -305,4 +307,46 @@ class UserHttp { return {'status': false, 'msg': res.data['message']}; } } + + // 我的订阅 + static Future userSubFolder({ + required int mid, + required int pn, + required int ps, + }) async { + var res = await Request().get(Api.userSubFolder, data: { + 'up_mid': mid, + 'ps': ps, + 'pn': pn, + 'platform': 'web', + }); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': SubFolderModelData.fromJson(res.data['data']) + }; + } else { + return {'status': false, 'msg': res.data['message']}; + } + } + + static Future userSubFolderDetail({ + required int seasonId, + required int pn, + required int ps, + }) async { + var res = await Request().get(Api.userSubFolderDetail, data: { + 'season_id': seasonId, + 'ps': ps, + 'pn': pn, + }); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': SubDetailModelData.fromJson(res.data['data']) + }; + } else { + return {'status': false, 'msg': res.data['message']}; + } + } } diff --git a/lib/models/msg/session.dart b/lib/models/msg/session.dart index 20f2d852..a0c2a048 100644 --- a/lib/models/msg/session.dart +++ b/lib/models/msg/session.dart @@ -8,7 +8,7 @@ class SessionDataModel { this.hasMore, }); - List? sessionList; + List? sessionList; int? hasMore; SessionDataModel.fromJson(Map json) { @@ -121,35 +121,37 @@ class LastMsg { this.msgKey, this.msgStatus, this.notifyCode, - this.newFaceVersion, + // this.newFaceVersion, }); int? senderIid; int? receiverType; int? receiverId; int? msgType; - Map? content; + dynamic content; int? msgSeqno; int? timestamp; String? atUids; int? msgKey; int? msgStatus; String? notifyCode; - int? newFaceVersion; + // int? newFaceVersion; LastMsg.fromJson(Map json) { senderIid = json['sender_uid']; receiverType = json['receiver_type']; receiverId = json['receiver_id']; msgType = json['msg_type']; - content = jsonDecode(json['content']); + content = json['content'] != null && json['content'] != '' + ? jsonDecode(json['content']) + : ''; msgSeqno = json['msg_seqno']; timestamp = json['timestamp']; atUids = json['at_uids']; msgKey = json['msg_key']; msgStatus = json['msg_status']; notifyCode = json['notify_code']; - newFaceVersion = json['new_face_version']; + // newFaceVersion = json['new_face_version']; } } @@ -214,7 +216,9 @@ class MessageItem { receiverId = json['receiver_id']; // 1 文本 2 图片 18 系统提示 10 系统通知 5 撤回的消息 msgType = json['msg_type']; - content = jsonDecode(json['content']); + content = json['content'] != null && json['content'] != '' + ? jsonDecode(json['content']) + : ''; msgSeqno = json['msg_seqno']; timestamp = json['timestamp']; atUids = json['at_uids']; diff --git a/lib/models/user/sub_detail.dart b/lib/models/user/sub_detail.dart new file mode 100644 index 00000000..a1e52e55 --- /dev/null +++ b/lib/models/user/sub_detail.dart @@ -0,0 +1,123 @@ +class SubDetailModelData { + DetailInfo? info; + List? medias; + + SubDetailModelData({this.info, this.medias}); + + SubDetailModelData.fromJson(Map json) { + info = DetailInfo.fromJson(json['info']); + if (json['medias'] != null) { + medias = []; + json['medias'].forEach((v) { + medias!.add(SubDetailMediaItem.fromJson(v)); + }); + } + } +} + +class SubDetailMediaItem { + int? id; + String? title; + String? cover; + String? pic; + int? duration; + int? pubtime; + String? bvid; + Map? upper; + Map? cntInfo; + int? enableVt; + String? vtDisplay; + + SubDetailMediaItem({ + this.id, + this.title, + this.cover, + this.pic, + this.duration, + this.pubtime, + this.bvid, + this.upper, + this.cntInfo, + this.enableVt, + this.vtDisplay, + }); + + SubDetailMediaItem.fromJson(Map json) { + id = json['id']; + title = json['title']; + cover = json['cover']; + pic = json['cover']; + duration = json['duration']; + pubtime = json['pubtime']; + bvid = json['bvid']; + upper = json['upper']; + cntInfo = json['cnt_info']; + enableVt = json['enable_vt']; + vtDisplay = json['vt_display']; + } + + Map toJson() { + final data = {}; + data['id'] = id; + data['title'] = title; + data['cover'] = cover; + data['duration'] = duration; + data['pubtime'] = pubtime; + data['bvid'] = bvid; + data['upper'] = upper; + data['cnt_info'] = cntInfo; + data['enable_vt'] = enableVt; + data['vt_display'] = vtDisplay; + return data; + } +} + +class DetailInfo { + int? id; + int? seasonType; + String? title; + String? cover; + Map? upper; + Map? cntInfo; + int? mediaCount; + String? intro; + int? enableVt; + + DetailInfo({ + this.id, + this.seasonType, + this.title, + this.cover, + this.upper, + this.cntInfo, + this.mediaCount, + this.intro, + this.enableVt, + }); + + DetailInfo.fromJson(Map json) { + id = json['id']; + seasonType = json['season_type']; + title = json['title']; + cover = json['cover']; + upper = json['upper']; + cntInfo = json['cnt_info']; + mediaCount = json['media_count']; + intro = json['intro']; + enableVt = json['enable_vt']; + } + + Map toJson() { + final data = {}; + data['id'] = id; + data['season_type'] = seasonType; + data['title'] = title; + data['cover'] = cover; + data['upper'] = upper; + data['cnt_info'] = cntInfo; + data['media_count'] = mediaCount; + data['intro'] = intro; + data['enable_vt'] = enableVt; + return data; + } +} diff --git a/lib/models/user/sub_folder.dart b/lib/models/user/sub_folder.dart new file mode 100644 index 00000000..d496a1cf --- /dev/null +++ b/lib/models/user/sub_folder.dart @@ -0,0 +1,111 @@ +class SubFolderModelData { + final int? count; + final List? list; + + SubFolderModelData({ + this.count, + this.list, + }); + + factory SubFolderModelData.fromJson(Map json) { + return SubFolderModelData( + count: json['count'], + list: json['list'] != null + ? (json['list'] as List) + .map((i) => SubFolderItemData.fromJson(i)) + .toList() + : null, + ); + } +} + +class SubFolderItemData { + final int? id; + final int? fid; + final int? mid; + final int? attr; + final String? title; + final String? cover; + final Upper? upper; + final int? coverType; + final String? intro; + final int? ctime; + final int? mtime; + final int? state; + final int? favState; + final int? mediaCount; + final int? viewCount; + final int? vt; + final int? playSwitch; + final int? type; + final String? link; + final String? bvid; + + SubFolderItemData({ + this.id, + this.fid, + this.mid, + this.attr, + this.title, + this.cover, + this.upper, + this.coverType, + this.intro, + this.ctime, + this.mtime, + this.state, + this.favState, + this.mediaCount, + this.viewCount, + this.vt, + this.playSwitch, + this.type, + this.link, + this.bvid, + }); + + factory SubFolderItemData.fromJson(Map json) { + return SubFolderItemData( + id: json['id'], + fid: json['fid'], + mid: json['mid'], + attr: json['attr'], + title: json['title'], + cover: json['cover'], + upper: json['upper'] != null ? Upper.fromJson(json['upper']) : null, + coverType: json['cover_type'], + intro: json['intro'], + ctime: json['ctime'], + mtime: json['mtime'], + state: json['state'], + favState: json['fav_state'], + mediaCount: json['media_count'], + viewCount: json['view_count'], + vt: json['vt'], + playSwitch: json['play_switch'], + type: json['type'], + link: json['link'], + bvid: json['bvid'], + ); + } +} + +class Upper { + final int? mid; + final String? name; + final String? face; + + Upper({ + this.mid, + this.name, + this.face, + }); + + factory Upper.fromJson(Map json) { + return Upper( + mid: json['mid'], + name: json['name'], + face: json['face'], + ); + } +} diff --git a/lib/models/video/reply/emote.dart b/lib/models/video/reply/emote.dart new file mode 100644 index 00000000..b4071826 --- /dev/null +++ b/lib/models/video/reply/emote.dart @@ -0,0 +1,120 @@ +class EmoteModelData { + final List? packages; + + EmoteModelData({ + required this.packages, + }); + + factory EmoteModelData.fromJson(Map jsonRes) { + final List? packages = + jsonRes['packages'] is List ? [] : null; + if (packages != null) { + for (final dynamic item in jsonRes['packages']!) { + if (item != null) { + try { + packages.add(PackageItem.fromJson(item)); + } catch (_) {} + } + } + } + return EmoteModelData( + packages: packages, + ); + } +} + +class PackageItem { + final int? id; + final String? text; + final String? url; + final int? mtime; + final int? type; + final int? attr; + final Meta? meta; + final List? emote; + + PackageItem({ + required this.id, + required this.text, + required this.url, + required this.mtime, + required this.type, + required this.attr, + required this.meta, + required this.emote, + }); + + factory PackageItem.fromJson(Map jsonRes) { + final List? emote = jsonRes['emote'] is List ? [] : null; + if (emote != null) { + for (final dynamic item in jsonRes['emote']!) { + if (item != null) { + try { + emote.add(Emote.fromJson(item)); + } catch (_) {} + } + } + } + return PackageItem( + id: jsonRes['id'], + text: jsonRes['text'], + url: jsonRes['url'], + mtime: jsonRes['mtime'], + type: jsonRes['type'], + attr: jsonRes['attr'], + meta: Meta.fromJson(jsonRes['meta']), + emote: emote, + ); + } +} + +class Meta { + final int? size; + final List? suggest; + + Meta({ + required this.size, + required this.suggest, + }); + + factory Meta.fromJson(Map jsonRes) => Meta( + size: jsonRes['size'], + suggest: jsonRes['suggest'] is List ? [] : null, + ); +} + +class Emote { + final int? id; + final int? packageId; + final String? text; + final String? url; + final int? mtime; + final int? type; + final int? attr; + final Meta? meta; + final dynamic activity; + + Emote({ + required this.id, + required this.packageId, + required this.text, + required this.url, + required this.mtime, + required this.type, + required this.attr, + required this.meta, + required this.activity, + }); + + factory Emote.fromJson(Map jsonRes) => Emote( + id: jsonRes['id'], + packageId: jsonRes['package_id'], + text: jsonRes['text'], + url: jsonRes['url'], + mtime: jsonRes['mtime'], + type: jsonRes['type'], + attr: jsonRes['attr'], + meta: Meta.fromJson(jsonRes['meta']), + activity: jsonRes['activity'], + ); +} diff --git a/lib/pages/about/index.dart b/lib/pages/about/index.dart index 3f40438f..31b0a482 100644 --- a/lib/pages/about/index.dart +++ b/lib/pages/about/index.dart @@ -156,11 +156,11 @@ class _AboutPageState extends State { var cleanStatus = await CacheManage().clearCacheAll(); if (cleanStatus) { getCacheSize(); + SmartDialog.showToast('清除成功'); } }, title: const Text('清除缓存'), subtitle: Text('图片及网络缓存 $cacheSize', style: subTitleStyle), - trailing: Icon(Icons.arrow_forward_ios, size: 16, color: outline), ), ], ), diff --git a/lib/pages/emote/controller.dart b/lib/pages/emote/controller.dart new file mode 100644 index 00000000..c1a4c504 --- /dev/null +++ b/lib/pages/emote/controller.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../http/reply.dart'; +import '../../models/video/reply/emote.dart'; + +class EmotePanelController extends GetxController + with GetTickerProviderStateMixin { + late List emotePackage; + late TabController tabController; + + Future getEmote() async { + var res = await ReplyHttp.getEmoteList(business: 'reply'); + if (res['status']) { + emotePackage = res['data'].packages; + tabController = TabController(length: emotePackage.length, vsync: this); + } + return res; + } +} diff --git a/lib/pages/emote/index.dart b/lib/pages/emote/index.dart new file mode 100644 index 00000000..32ce53e3 --- /dev/null +++ b/lib/pages/emote/index.dart @@ -0,0 +1,4 @@ +library emote; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/emote/view.dart b/lib/pages/emote/view.dart new file mode 100644 index 00000000..d30767c3 --- /dev/null +++ b/lib/pages/emote/view.dart @@ -0,0 +1,116 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../models/video/reply/emote.dart'; +import 'controller.dart'; + +class EmotePanel extends StatefulWidget { + final Function onChoose; + const EmotePanel({super.key, required this.onChoose}); + + @override + State createState() => _EmotePanelState(); +} + +class _EmotePanelState extends State + with AutomaticKeepAliveClientMixin { + final EmotePanelController _emotePanelController = + Get.put(EmotePanelController()); + late Future _futureBuilderFuture; + + @override + bool get wantKeepAlive => true; + + @override + void initState() { + _futureBuilderFuture = _emotePanelController.getEmote(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return FutureBuilder( + future: _futureBuilderFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + Map data = snapshot.data as Map; + if (data['status']) { + List emotePackage = + _emotePanelController.emotePackage; + + return Column( + children: [ + Expanded( + child: TabBarView( + controller: _emotePanelController.tabController, + children: emotePackage.map( + (e) { + int size = e.emote!.first.meta!.size!; + int type = e.type!; + return Padding( + padding: const EdgeInsets.fromLTRB(12, 6, 12, 0), + child: GridView.builder( + gridDelegate: + SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: size == 1 ? 40 : 60, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + ), + itemCount: e.emote!.length, + itemBuilder: (context, index) { + return Material( + color: Colors.transparent, + clipBehavior: Clip.hardEdge, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + child: InkWell( + onTap: () { + widget.onChoose(e, e.emote![index]); + }, + child: Padding( + padding: const EdgeInsets.all(3), + child: type == 4 + ? Text( + e.emote![index].text!, + overflow: TextOverflow.clip, + maxLines: 1, + ) + : Image.network( + e.emote![index].url!, + width: size * 38, + height: size * 38, + ), + ), + ), + ); + }, + ), + ); + }, + ).toList(), + )), + Divider( + height: 1, + color: Theme.of(context).dividerColor.withOpacity(0.1), + ), + TabBar( + controller: _emotePanelController.tabController, + dividerColor: Colors.transparent, + isScrollable: true, + tabs: _emotePanelController.emotePackage + .map((e) => Tab(text: e.text)) + .toList(), + ), + SizedBox(height: MediaQuery.of(context).padding.bottom + 20), + ], + ); + } else { + return Center(child: Text(data['msg'])); + } + } else { + return const Center(child: Text('加载中...')); + } + }); + } +} diff --git a/lib/pages/fav/controller.dart b/lib/pages/fav/controller.dart index 17720206..ba522ec8 100644 --- a/lib/pages/fav/controller.dart +++ b/lib/pages/fav/controller.dart @@ -24,7 +24,7 @@ class FavController extends GetxController { if (!hasMore.value) { return; } - var res = await await UserHttp.userfavFolder( + var res = await UserHttp.userfavFolder( pn: currentPage, ps: pageSize, mid: userInfo!.mid!, diff --git a/lib/pages/fav_detail/controller.dart b/lib/pages/fav_detail/controller.dart index 2959a18a..9986c9c2 100644 --- a/lib/pages/fav_detail/controller.dart +++ b/lib/pages/fav_detail/controller.dart @@ -34,7 +34,7 @@ class FavDetailController extends GetxController { return; } isLoadingMore = true; - var res = await await UserHttp.userFavFolderDetail( + var res = await UserHttp.userFavFolderDetail( pn: currentPage, ps: 20, mediaId: mediaId!, diff --git a/lib/pages/fav_detail/view.dart b/lib/pages/fav_detail/view.dart index b4b41ab2..60ea7744 100644 --- a/lib/pages/fav_detail/view.dart +++ b/lib/pages/fav_detail/view.dart @@ -31,7 +31,6 @@ class _FavDetailPageState extends State { super.initState(); mediaId = Get.parameters['mediaId']!; _futureBuilderFuture = _favDetailController.queryUserFavFolderDetail(); - mediaId = Get.parameters['mediaId']!; titleStreamC = StreamController(); _controller.addListener( () { diff --git a/lib/pages/history/view.dart b/lib/pages/history/view.dart index 9667a20f..58d89b98 100644 --- a/lib/pages/history/view.dart +++ b/lib/pages/history/view.dart @@ -70,10 +70,6 @@ class _HistoryPageState extends State { child1: AppBar( titleSpacing: 0, centerTitle: false, - leading: IconButton( - onPressed: () => Get.back(), - icon: const Icon(Icons.arrow_back_outlined), - ), title: Text( '观看记录', style: Theme.of(context).textTheme.titleMedium, diff --git a/lib/pages/live_room/view.dart b/lib/pages/live_room/view.dart index 462cf813..3d550639 100644 --- a/lib/pages/live_room/view.dart +++ b/lib/pages/live_room/view.dart @@ -75,41 +75,45 @@ class _LiveRoomPageState extends State { backgroundColor: Colors.black, body: Stack( children: [ - // Obx( - // () => Positioned.fill( - // child: Opacity( - // opacity: 0.8, - // child: _liveRoomController - // .roomInfoH5.value.roomInfo?.appBackground != - // '' && - // _liveRoomController - // .roomInfoH5.value.roomInfo?.appBackground != - // null - // ? NetworkImgLayer( - // width: Get.width, - // height: Get.height, - // src: _liveRoomController - // .roomInfoH5.value.roomInfo?.appBackground ?? - // '', - // ) - // : Image.asset( - // 'assets/images/live/default_bg.webp', - // width: Get.width, - // height: Get.height, - // ), - // ), - // ), - // ), - Positioned.fill( + Positioned( + left: 0, + right: 0, + bottom: 0, child: Opacity( opacity: 0.8, child: Image.asset( 'assets/images/live/default_bg.webp', - width: Get.width, - height: Get.height, + fit: BoxFit.cover, + // width: Get.width, + // height: Get.height, ), ), ), + Obx( + () => Positioned( + left: 0, + right: 0, + bottom: 0, + child: _liveRoomController + .roomInfoH5.value.roomInfo?.appBackground != + '' && + _liveRoomController + .roomInfoH5.value.roomInfo?.appBackground != + null + ? Opacity( + opacity: 0.8, + child: NetworkImgLayer( + width: Get.width, + height: Get.height, + type: 'bg', + src: _liveRoomController + .roomInfoH5.value.roomInfo?.appBackground ?? + '', + ), + ) + : const SizedBox(), + ), + ), Column( children: [ AppBar( diff --git a/lib/pages/media/controller.dart b/lib/pages/media/controller.dart index 4a9d0d7f..6259e54a 100644 --- a/lib/pages/media/controller.dart +++ b/lib/pages/media/controller.dart @@ -28,6 +28,11 @@ class MediaController extends GetxController { 'title': '我的收藏', 'onTap': () => Get.toNamed('/fav'), }, + { + 'icon': Icons.subscriptions_outlined, + 'title': '我的订阅', + 'onTap': () => Get.toNamed('/subscription'), + }, { 'icon': Icons.watch_later_outlined, 'title': '稍后再看', diff --git a/lib/pages/setting/extra_setting.dart b/lib/pages/setting/extra_setting.dart index dcd48fb2..efa0457d 100644 --- a/lib/pages/setting/extra_setting.dart +++ b/lib/pages/setting/extra_setting.dart @@ -1,11 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; import 'package:hive/hive.dart'; import 'package:PiliPalaX/models/common/dynamics_type.dart'; import 'package:PiliPalaX/models/common/reply_sort_type.dart'; import 'package:PiliPalaX/pages/setting/widgets/select_dialog.dart'; import 'package:PiliPalaX/utils/storage.dart'; +import '../home/index.dart'; import 'widgets/switch_item.dart'; class ExtraSetting extends StatefulWidget { @@ -138,18 +140,20 @@ class _ExtraSettingState extends State { ), body: ListView( children: [ - SetSwitchItem( + const SetSwitchItem( title: '大家都在搜', subTitle: '是否展示「大家都在搜」', setKey: SettingBoxKey.enableHotKey, defaultVal: true, - callFn: (val) => {SmartDialog.showToast('下次启动时生效')}, ), - const SetSwitchItem( + SetSwitchItem( title: '搜索默认词', subTitle: '是否展示搜索框默认词', setKey: SettingBoxKey.enableSearchWord, defaultVal: true, + callFn: (val) { + Get.find().defaultSearch.value = ''; + }, ), const SetSwitchItem( title: '快速收藏', diff --git a/lib/pages/setting/style_setting.dart b/lib/pages/setting/style_setting.dart index cb712904..9e7cef45 100644 --- a/lib/pages/setting/style_setting.dart +++ b/lib/pages/setting/style_setting.dart @@ -8,6 +8,7 @@ import 'package:PiliPalaX/models/common/theme_type.dart'; import 'package:PiliPalaX/pages/setting/pages/color_select.dart'; import 'package:PiliPalaX/pages/setting/widgets/select_dialog.dart'; import 'package:PiliPalaX/pages/setting/widgets/slide_dialog.dart'; +import 'package:PiliPalaX/utils/global_data.dart'; import 'package:PiliPalaX/utils/storage.dart'; import '../../models/common/dynamic_badge_mode.dart'; @@ -179,6 +180,8 @@ class _StyleSettingState extends State { SettingBoxKey.defaultPicQa, picQuality); Get.back(); settingController.picQuality.value = picQuality; + GlobalData().imgQuality = picQuality; + SmartDialog.showToast('设置成功'); }, child: const Text('确定'), ) diff --git a/lib/pages/subscription/controller.dart b/lib/pages/subscription/controller.dart new file mode 100644 index 00000000..bf0c593c --- /dev/null +++ b/lib/pages/subscription/controller.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:hive/hive.dart'; +import 'package:pilipala/http/user.dart'; +import 'package:pilipala/models/user/info.dart'; +import 'package:pilipala/utils/storage.dart'; + +import '../../models/user/sub_folder.dart'; + +class SubController extends GetxController { + final ScrollController scrollController = ScrollController(); + Rx subFolderData = SubFolderModelData().obs; + Box userInfoCache = GStrorage.userInfo; + UserInfoData? userInfo; + int currentPage = 1; + int pageSize = 20; + RxBool hasMore = true.obs; + + Future querySubFolder({type = 'init'}) async { + userInfo = userInfoCache.get('userInfoCache'); + if (userInfo == null) { + return {'status': false, 'msg': '账号未登录'}; + } + var res = await UserHttp.userSubFolder( + pn: currentPage, + ps: pageSize, + mid: userInfo!.mid!, + ); + if (res['status']) { + if (type == 'init') { + subFolderData.value = res['data']; + } else { + if (res['data'].list.isNotEmpty) { + subFolderData.value.list!.addAll(res['data'].list); + subFolderData.update((val) {}); + } + } + currentPage++; + } else { + SmartDialog.showToast(res['msg']); + } + return res; + } + + Future onLoad() async { + querySubFolder(type: 'onload'); + } +} diff --git a/lib/pages/subscription/index.dart b/lib/pages/subscription/index.dart new file mode 100644 index 00000000..4d034396 --- /dev/null +++ b/lib/pages/subscription/index.dart @@ -0,0 +1,4 @@ +library sub; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/subscription/view.dart b/lib/pages/subscription/view.dart new file mode 100644 index 00000000..1eee4a4f --- /dev/null +++ b/lib/pages/subscription/view.dart @@ -0,0 +1,84 @@ +import 'package:easy_debounce/easy_throttle.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/common/widgets/http_error.dart'; +import 'controller.dart'; +import 'widgets/item.dart'; + +class SubPage extends StatefulWidget { + const SubPage({super.key}); + + @override + State createState() => _SubPageState(); +} + +class _SubPageState extends State { + final SubController _subController = Get.put(SubController()); + late Future _futureBuilderFuture; + late ScrollController scrollController; + + @override + void initState() { + super.initState(); + _futureBuilderFuture = _subController.querySubFolder(); + scrollController = _subController.scrollController; + scrollController.addListener( + () { + if (scrollController.position.pixels >= + scrollController.position.maxScrollExtent - 300) { + EasyThrottle.throttle('history', const Duration(seconds: 1), () { + _subController.onLoad(); + }); + } + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + centerTitle: false, + titleSpacing: 0, + title: Text( + '我的订阅', + style: Theme.of(context).textTheme.titleMedium, + ), + ), + body: FutureBuilder( + future: _futureBuilderFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + Map? data = snapshot.data; + if (data != null && data['status']) { + return Obx( + () => ListView.builder( + controller: scrollController, + itemCount: _subController.subFolderData.value.list!.length, + itemBuilder: (context, index) { + return SubItem( + subFolderItem: + _subController.subFolderData.value.list![index]); + }, + ), + ); + } else { + return CustomScrollView( + physics: const NeverScrollableScrollPhysics(), + slivers: [ + HttpError( + errMsg: data?['msg'], + fn: () => setState(() {}), + ), + ], + ); + } + } else { + // 骨架屏 + return const Text('请求中'); + } + }, + ), + ); + } +} diff --git a/lib/pages/subscription/widgets/item.dart b/lib/pages/subscription/widgets/item.dart new file mode 100644 index 00000000..fd08ffa5 --- /dev/null +++ b/lib/pages/subscription/widgets/item.dart @@ -0,0 +1,108 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/common/constants.dart'; +import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:pilipala/utils/utils.dart'; + +import '../../../models/user/sub_folder.dart'; + +class SubItem extends StatelessWidget { + final SubFolderItemData subFolderItem; + const SubItem({super.key, required this.subFolderItem}); + + @override + Widget build(BuildContext context) { + String heroTag = Utils.makeHeroTag(subFolderItem.id); + return InkWell( + onTap: () => Get.toNamed( + '/subDetail', + arguments: subFolderItem, + parameters: { + 'heroTag': heroTag, + 'seasonId': subFolderItem.id.toString(), + }, + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(12, 7, 12, 7), + child: LayoutBuilder( + builder: (context, boxConstraints) { + double width = + (boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2; + return SizedBox( + height: width / StyleString.aspectRatio, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AspectRatio( + aspectRatio: StyleString.aspectRatio, + child: LayoutBuilder( + builder: (context, boxConstraints) { + double maxWidth = boxConstraints.maxWidth; + double maxHeight = boxConstraints.maxHeight; + return Hero( + tag: heroTag, + child: NetworkImgLayer( + src: subFolderItem.cover, + width: maxWidth, + height: maxHeight, + ), + ); + }, + ), + ), + VideoContent(subFolderItem: subFolderItem) + ], + ), + ); + }, + ), + ), + ); + } +} + +class VideoContent extends StatelessWidget { + final SubFolderItemData subFolderItem; + const VideoContent({super.key, required this.subFolderItem}); + + @override + Widget build(BuildContext context) { + return Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(10, 2, 6, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + subFolderItem.title!, + textAlign: TextAlign.start, + style: const TextStyle( + fontWeight: FontWeight.w500, + letterSpacing: 0.3, + ), + ), + const SizedBox(height: 2), + Text( + '合集 UP主:${subFolderItem.upper!.name!}', + textAlign: TextAlign.start, + style: TextStyle( + fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, + color: Theme.of(context).colorScheme.outline, + ), + ), + const SizedBox(height: 2), + Text( + '${subFolderItem.mediaCount}个视频', + textAlign: TextAlign.start, + style: TextStyle( + fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, + color: Theme.of(context).colorScheme.outline, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/subscription_detail/controller.dart b/lib/pages/subscription_detail/controller.dart new file mode 100644 index 00000000..6ecb894e --- /dev/null +++ b/lib/pages/subscription_detail/controller.dart @@ -0,0 +1,60 @@ +import 'package:get/get.dart'; +import 'package:pilipala/http/user.dart'; + +import '../../models/user/sub_detail.dart'; +import '../../models/user/sub_folder.dart'; + +class SubDetailController extends GetxController { + late SubFolderItemData item; + + late int seasonId; + late String heroTag; + int currentPage = 1; + bool isLoadingMore = false; + Rx subInfo = DetailInfo().obs; + RxList subList = [].obs; + RxString loadingText = '加载中...'.obs; + int mediaCount = 0; + + @override + void onInit() { + item = Get.arguments; + if (Get.parameters.keys.isNotEmpty) { + seasonId = int.parse(Get.parameters['seasonId']!); + heroTag = Get.parameters['heroTag']!; + } + super.onInit(); + } + + Future queryUserSubFolderDetail({type = 'init'}) async { + if (type == 'onLoad' && subList.length >= mediaCount) { + loadingText.value = '没有更多了'; + return; + } + isLoadingMore = true; + var res = await UserHttp.userSubFolderDetail( + seasonId: seasonId, + ps: 20, + pn: currentPage, + ); + if (res['status']) { + subInfo.value = res['data'].info; + if (currentPage == 1 && type == 'init') { + subList.value = res['data'].medias; + mediaCount = res['data'].info.mediaCount; + } else if (type == 'onLoad') { + subList.addAll(res['data'].medias); + } + if (subList.length >= mediaCount) { + loadingText.value = '没有更多了'; + } + } + currentPage += 1; + isLoadingMore = false; + return res; + } + + onLoad() { + queryUserSubFolderDetail(type: 'onLoad'); + } +} diff --git a/lib/pages/subscription_detail/index.dart b/lib/pages/subscription_detail/index.dart new file mode 100644 index 00000000..71df4b24 --- /dev/null +++ b/lib/pages/subscription_detail/index.dart @@ -0,0 +1,4 @@ +library sub_detail; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/subscription_detail/view.dart b/lib/pages/subscription_detail/view.dart new file mode 100644 index 00000000..d56125cd --- /dev/null +++ b/lib/pages/subscription_detail/view.dart @@ -0,0 +1,257 @@ +import 'dart:async'; + +import 'package:easy_debounce/easy_throttle.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/common/skeleton/video_card_h.dart'; +import 'package:pilipala/common/widgets/http_error.dart'; +import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:pilipala/common/widgets/no_data.dart'; + +import '../../models/user/sub_folder.dart'; +import '../../utils/utils.dart'; +import 'controller.dart'; +import 'widget/sub_video_card.dart'; + +class SubDetailPage extends StatefulWidget { + const SubDetailPage({super.key}); + + @override + State createState() => _SubDetailPageState(); +} + +class _SubDetailPageState extends State { + late final ScrollController _controller = ScrollController(); + final SubDetailController _subDetailController = + Get.put(SubDetailController()); + late StreamController titleStreamC; // a + late Future _futureBuilderFuture; + late String seasonId; + + @override + void initState() { + super.initState(); + seasonId = Get.parameters['seasonId']!; + _futureBuilderFuture = _subDetailController.queryUserSubFolderDetail(); + titleStreamC = StreamController(); + _controller.addListener( + () { + if (_controller.offset > 160) { + titleStreamC.add(true); + } else if (_controller.offset <= 160) { + titleStreamC.add(false); + } + + if (_controller.position.pixels >= + _controller.position.maxScrollExtent - 200) { + EasyThrottle.throttle('subDetail', const Duration(seconds: 1), () { + _subDetailController.onLoad(); + }); + } + }, + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: CustomScrollView( + controller: _controller, + slivers: [ + SliverAppBar( + expandedHeight: 260 - MediaQuery.of(context).padding.top, + pinned: true, + titleSpacing: 0, + title: StreamBuilder( + stream: titleStreamC.stream, + initialData: false, + builder: (context, AsyncSnapshot snapshot) { + return AnimatedOpacity( + opacity: snapshot.data ? 1 : 0, + curve: Curves.easeOut, + duration: const Duration(milliseconds: 500), + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _subDetailController.item.title!, + style: Theme.of(context).textTheme.titleMedium, + ), + Text( + '共${_subDetailController.item.mediaCount!}条视频', + style: Theme.of(context).textTheme.labelMedium, + ) + ], + ) + ], + ), + ); + }, + ), + flexibleSpace: FlexibleSpaceBar( + background: Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Theme.of(context).dividerColor.withOpacity(0.2), + ), + ), + ), + padding: EdgeInsets.only( + top: kTextTabBarHeight + + MediaQuery.of(context).padding.top + + 30, + left: 20, + right: 20), + child: SizedBox( + height: 200, + child: Row( + // mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Hero( + tag: _subDetailController.heroTag, + child: NetworkImgLayer( + width: 180, + height: 110, + src: _subDetailController.item.cover, + ), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 4), + Text( + _subDetailController.item.title!, + style: TextStyle( + fontSize: Theme.of(context) + .textTheme + .titleMedium! + .fontSize, + fontWeight: FontWeight.bold), + ), + const SizedBox(height: 4), + GestureDetector( + onTap: () { + SubFolderItemData item = + _subDetailController.item; + Get.toNamed( + '/member?mid=${item.upper!.mid}', + arguments: { + 'face': item.upper!.face, + }, + ); + }, + child: Text( + _subDetailController.item.upper!.name!, + style: TextStyle( + color: + Theme.of(context).colorScheme.primary), + ), + ), + const SizedBox(height: 4), + Text( + '${Utils.numFormat(_subDetailController.item.viewCount)}次播放', + style: TextStyle( + fontSize: Theme.of(context) + .textTheme + .labelSmall! + .fontSize, + color: Theme.of(context).colorScheme.outline), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only(top: 15, bottom: 8, left: 14), + child: Obx( + () => Text( + '共${_subDetailController.subList.length}条视频', + style: TextStyle( + fontSize: + Theme.of(context).textTheme.labelMedium!.fontSize, + color: Theme.of(context).colorScheme.outline, + letterSpacing: 1), + ), + ), + ), + ), + FutureBuilder( + future: _futureBuilderFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + Map data = snapshot.data; + if (data['status']) { + if (_subDetailController.item.mediaCount == 0) { + return const NoData(); + } else { + List subList = _subDetailController.subList; + return Obx( + () => subList.isEmpty + ? const SliverToBoxAdapter(child: SizedBox()) + : SliverList( + delegate: + SliverChildBuilderDelegate((context, index) { + return SubVideoCardH( + videoItem: subList[index], + ); + }, childCount: subList.length), + ), + ); + } + } else { + return HttpError( + errMsg: data['msg'], + fn: () => setState(() {}), + ); + } + } else { + // 骨架屏 + return SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + return const VideoCardHSkeleton(); + }, childCount: 10), + ); + } + }, + ), + SliverToBoxAdapter( + child: Container( + height: MediaQuery.of(context).padding.bottom + 60, + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).padding.bottom), + child: Center( + child: Obx( + () => Text( + _subDetailController.loadingText.value, + style: TextStyle( + color: Theme.of(context).colorScheme.outline, + fontSize: 13), + ), + ), + ), + ), + ) + ], + ), + ); + } +} diff --git a/lib/pages/subscription_detail/widget/sub_video_card.dart b/lib/pages/subscription_detail/widget/sub_video_card.dart new file mode 100644 index 00000000..11aebc39 --- /dev/null +++ b/lib/pages/subscription_detail/widget/sub_video_card.dart @@ -0,0 +1,168 @@ +import 'package:get/get.dart'; +import 'package:flutter/material.dart'; +import 'package:pilipala/common/constants.dart'; +import 'package:pilipala/common/widgets/stat/danmu.dart'; +import 'package:pilipala/common/widgets/stat/view.dart'; +import 'package:pilipala/http/search.dart'; +import 'package:pilipala/models/common/search_type.dart'; +import 'package:pilipala/utils/utils.dart'; +import 'package:pilipala/common/widgets/network_img_layer.dart'; +import '../../../common/widgets/badge.dart'; +import '../../../models/user/sub_detail.dart'; + +// 收藏视频卡片 - 水平布局 +class SubVideoCardH extends StatelessWidget { + final SubDetailMediaItem videoItem; + final int? searchType; + + const SubVideoCardH({ + Key? key, + required this.videoItem, + this.searchType, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + int id = videoItem.id!; + String bvid = videoItem.bvid!; + String heroTag = Utils.makeHeroTag(id); + return InkWell( + onTap: () async { + int cid = await SearchHttp.ab2c(bvid: bvid); + Map parameters = { + 'bvid': bvid, + 'cid': cid.toString(), + }; + + Get.toNamed('/video', parameters: parameters, arguments: { + 'videoItem': videoItem, + 'heroTag': heroTag, + 'videoType': SearchType.video, + }); + }, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB( + StyleString.safeSpace, 5, StyleString.safeSpace, 5), + child: LayoutBuilder( + builder: (context, boxConstraints) { + double width = + (boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2; + return SizedBox( + height: width / StyleString.aspectRatio, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AspectRatio( + aspectRatio: StyleString.aspectRatio, + child: LayoutBuilder( + builder: (context, boxConstraints) { + double maxWidth = boxConstraints.maxWidth; + double maxHeight = boxConstraints.maxHeight; + return Stack( + children: [ + Hero( + tag: heroTag, + child: NetworkImgLayer( + src: videoItem.cover, + width: maxWidth, + height: maxHeight, + ), + ), + PBadge( + text: Utils.timeFormat(videoItem.duration!), + right: 6.0, + bottom: 6.0, + type: 'gray', + ), + // if (videoItem.ogv != null) ...[ + // PBadge( + // text: videoItem.ogv['type_name'], + // top: 6.0, + // right: 6.0, + // bottom: null, + // left: null, + // ), + // ], + ], + ); + }, + ), + ), + VideoContent( + videoItem: videoItem, + searchType: searchType, + ) + ], + ), + ); + }, + ), + ), + ], + ), + ); + } +} + +class VideoContent extends StatelessWidget { + final dynamic videoItem; + final int? searchType; + const VideoContent({ + super.key, + required this.videoItem, + this.searchType, + }); + + @override + Widget build(BuildContext context) { + return Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(10, 2, 6, 0), + child: Stack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + videoItem.title, + textAlign: TextAlign.start, + style: const TextStyle( + fontWeight: FontWeight.w500, + letterSpacing: 0.3, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const Spacer(), + Text( + Utils.dateFormat(videoItem.pubtime), + style: TextStyle( + fontSize: 11, + color: Theme.of(context).colorScheme.outline), + ), + Padding( + padding: const EdgeInsets.only(top: 2), + child: Row( + children: [ + StatView( + theme: 'gray', + view: videoItem.cntInfo['play'], + ), + const SizedBox(width: 8), + StatDanMu( + theme: 'gray', danmu: videoItem.cntInfo['danmaku']), + const Spacer(), + ], + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/video/detail/reply/reply_emote/view.dart b/lib/pages/video/detail/reply/reply_emote/view.dart deleted file mode 100644 index 68414dc1..00000000 --- a/lib/pages/video/detail/reply/reply_emote/view.dart +++ /dev/null @@ -1,111 +0,0 @@ -import 'dart:async'; -import 'package:PiliPalaX/models/user/my_emote.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; - -import '../../../../../common/widgets/network_img_layer.dart'; -import '../../../../../http/reply.dart'; - -class EmoteTab extends StatefulWidget { - final Function(String) onEmoteTap; - const EmoteTab({Key? key, required this.onEmoteTap}) : super(key: key); - - @override - State createState() => _EmoteTabState(); -} - -class _EmoteTabState extends State with TickerProviderStateMixin { - late TabController _myEmoteTabController; - late MyEmote myEmote; - late Future futureBuild; - Future getMyEmote() async { - var result = await ReplyHttp.getMyEmote(business: "reply"); - if (result['status']) { - myEmote = MyEmote.fromJson(result['data']); - _myEmoteTabController = TabController( - length: myEmote.packages!.length, - initialIndex: myEmote.setting!.focusPkgId! - 1, - vsync: this); - } else { - SmartDialog.showToast(result['msg']); - myEmote = MyEmote(); - } - return; - } - - @override - void initState() { - super.initState(); - futureBuild = getMyEmote(); - } - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: futureBuild, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.connectionState == ConnectionState.done && - myEmote != null && - myEmote.packages != null) { - return Column( - children: [ - Expanded( - child: TabBarView(controller: _myEmoteTabController, children: [ - for (Packages i in myEmote.packages!) ...[ - GridView.builder( - gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: i.type == 4 ? 100 : 36, - mainAxisSpacing: 10, - crossAxisSpacing: 10, - mainAxisExtent: 36, - ), - itemCount: i.emote!.length, - itemBuilder: (BuildContext context, int index) { - return GestureDetector( - onTap: () { - widget.onEmoteTap(i.emote![index].text!); - }, - child: i.type == 4 - ? Text( - i.emote![index].text!, - overflow: TextOverflow.clip, - maxLines: 1, - ) - : NetworkImgLayer( - width: 36, - height: 36, - type: 'emote', - src: i.emote![index].url?.split("@")[0] - ), - ); - }, - ), - ], - ]), - ), - SizedBox( - height: 45, - child: TabBar( - isScrollable: true, - controller: _myEmoteTabController, - tabs: [ - for (var i in myEmote.packages!) - NetworkImgLayer( - width: 36, - height: 36, - type: 'emote', - src: i.url, - ), - ], - )) - ], - ); - } else { - return const Center( - child: CircularProgressIndicator(), - ); - } - }, - ); - } -} diff --git a/lib/pages/video/detail/reply_new/toolbar_icon_button.dart b/lib/pages/video/detail/reply_new/toolbar_icon_button.dart new file mode 100644 index 00000000..c4390796 --- /dev/null +++ b/lib/pages/video/detail/reply_new/toolbar_icon_button.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; + +class ToolbarIconButton extends StatelessWidget { + final VoidCallback onPressed; + final Icon icon; + final String toolbarType; + final bool selected; + + const ToolbarIconButton({ + super.key, + required this.onPressed, + required this.icon, + required this.toolbarType, + required this.selected, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 36, + height: 36, + child: IconButton( + onPressed: onPressed, + icon: icon, + highlightColor: Theme.of(context).colorScheme.secondaryContainer, + color: selected + ? Theme.of(context).colorScheme.onSecondaryContainer + : Theme.of(context).colorScheme.outline, + style: ButtonStyle( + padding: MaterialStateProperty.all(EdgeInsets.zero), + backgroundColor: MaterialStateProperty.resolveWith((states) { + return selected + ? Theme.of(context).colorScheme.secondaryContainer + : null; + }), + ), + ), + ); + } +} diff --git a/lib/pages/video/detail/reply_new/view.dart b/lib/pages/video/detail/reply_new/view.dart index ca6cb1da..9aac77b2 100644 --- a/lib/pages/video/detail/reply_new/view.dart +++ b/lib/pages/video/detail/reply_new/view.dart @@ -4,11 +4,12 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:PiliPalaX/http/video.dart'; import 'package:PiliPalaX/models/common/reply_type.dart'; +import 'package:PiliPalaX/models/video/reply/emote.dart'; import 'package:PiliPalaX/models/video/reply/item.dart'; +import 'package:PiliPalaX/pages/emote/index.dart'; import 'package:PiliPalaX/utils/feed_back.dart'; -import '../../../../common/constants.dart'; -import '../reply/reply_emote/view.dart'; +import 'toolbar_icon_button.dart'; class VideoReplyNewDialog extends StatefulWidget { final int? oid; @@ -35,7 +36,10 @@ class _VideoReplyNewDialogState extends State final TextEditingController _replyContentController = TextEditingController(); final FocusNode replyContentFocusNode = FocusNode(); final GlobalKey _formKey = GlobalKey(); - bool isShowEmote = false; + late double emoteHeight = 0.0; + double keyboardHeight = 0.0; // 键盘高度 + final _debouncer = Debouncer(milliseconds: 200); // 设置延迟时间 + String toolbarType = 'input'; @override void initState() { @@ -46,6 +50,8 @@ class _VideoReplyNewDialogState extends State WidgetsBinding.instance.addObserver(this); // 自动聚焦 _autoFocus(); + // 监听聚焦状态 + _focuslistener(); } _autoFocus() async { @@ -55,6 +61,16 @@ class _VideoReplyNewDialogState extends State } } + _focuslistener() { + replyContentFocusNode.addListener(() { + if (replyContentFocusNode.hasFocus) { + setState(() { + toolbarType = 'input'; + }); + } + }); + } + Future submitReplyAdd() async { feedBack(); String message = _replyContentController.text; @@ -77,18 +93,49 @@ class _VideoReplyNewDialogState extends State } } + void onChooseEmote(Packages package, Emote emote) { + final int cursorPosition = _replyContentController.selection.baseOffset; + final String currentText = _replyContentController.text; + final String newText = currentText.substring(0, cursorPosition) + + emote.text! + + currentText.substring(cursorPosition); + _replyContentController.value = TextEditingValue( + text: newText, + selection: + TextSelection.collapsed(offset: cursorPosition + emote.text!.length), + ); + } + + @override + void didChangeMetrics() { + super.didChangeMetrics(); + WidgetsBinding.instance.addPostFrameCallback((_) { + // 键盘高度 + final viewInsets = EdgeInsets.fromViewPadding( + View.of(context).viewInsets, View.of(context).devicePixelRatio); + _debouncer.run(() { + if (mounted) { + if (keyboardHeight == 0 && emoteHeight == 0) { + setState(() { + emoteHeight = keyboardHeight = + keyboardHeight == 0.0 ? viewInsets.bottom : keyboardHeight; + }); + } + } + }); + }); + } + @override void dispose() { WidgetsBinding.instance.removeObserver(this); _replyContentController.dispose(); + replyContentFocusNode.removeListener(() {}); super.dispose(); } @override Widget build(BuildContext context) { - double keyboardHeight = EdgeInsets.fromViewPadding( - View.of(context).viewInsets, View.of(context).devicePixelRatio) - .bottom; return Container( clipBehavior: Clip.hardEdge, decoration: BoxDecoration( @@ -141,68 +188,32 @@ class _VideoReplyNewDialogState extends State child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - SizedBox( - width: 36, - height: 36, - child: IconButton( - onPressed: () async { - FocusScope.of(context) - .requestFocus(replyContentFocusNode); - await Future.delayed(const Duration(milliseconds: 200)); + ToolbarIconButton( + onPressed: () { + if (toolbarType == 'emote') { setState(() { - isShowEmote = false; + toolbarType = 'input'; }); - }, - icon: Icon(Icons.keyboard, - size: 22, - color: Theme.of(context).colorScheme.onBackground), - highlightColor: - Theme.of(context).colorScheme.onInverseSurface, - style: ButtonStyle( - padding: MaterialStateProperty.all(EdgeInsets.zero), - backgroundColor: - MaterialStateProperty.resolveWith((states) { - if (states.contains(MaterialState.pressed) || !isShowEmote) { - return Theme.of(context).highlightColor; - } - // 默认状态下,返回透明颜色 - return Colors.transparent; - }), - ), - ), + } + FocusScope.of(context).requestFocus(replyContentFocusNode); + }, + icon: const Icon(Icons.keyboard, size: 22), + toolbarType: toolbarType, + selected: toolbarType == 'input', ), - const SizedBox( - width: 10, - ), - SizedBox( - width: 36, - height: 36, - child: IconButton( - onPressed: () { - //收起输入法 - FocusScope.of(context).unfocus(); - // 弹出表情选择 + const SizedBox(width: 20), + ToolbarIconButton( + onPressed: () { + if (toolbarType == 'input') { setState(() { - isShowEmote = true; + toolbarType = 'emote'; }); - }, - icon: Icon(Icons.emoji_emotions, - size: 22, - color: Theme.of(context).colorScheme.onBackground), - highlightColor: - Theme.of(context).colorScheme.onInverseSurface, - style: ButtonStyle( - padding: MaterialStateProperty.all(EdgeInsets.zero), - backgroundColor: - MaterialStateProperty.resolveWith((states) { - if (states.contains(MaterialState.pressed) || isShowEmote) { - return Theme.of(context).highlightColor; - } - // 默认状态下,返回透明颜色 - return Colors.transparent; - }), - ), - ), + } + FocusScope.of(context).unfocus(); + }, + icon: const Icon(Icons.emoji_emotions, size: 22), + toolbarType: toolbarType, + selected: toolbarType == 'emote', ), const Spacer(), TextButton( @@ -210,42 +221,38 @@ class _VideoReplyNewDialogState extends State ], ), ), - if (!isShowEmote) - SizedBox( + AnimatedSize( + curve: Curves.easeInOut, + duration: const Duration(milliseconds: 300), + child: SizedBox( width: double.infinity, - height: keyboardHeight, - ), - if (isShowEmote) - SizedBox( - width: double.infinity, - height: 310, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: StyleString.safeSpace), - child: EmoteTab( - onEmoteTap: onEmoteTap, - ), + height: toolbarType == 'input' ? keyboardHeight : emoteHeight, + child: EmotePanel( + onChoose: (package, emote) => onChooseEmote(package, emote), ), - ) + ), + ), ], ), ); } - - void onEmoteTap(String emoteString) { - // 在光标处插入表情 - final String currentText = _replyContentController.text; - final TextSelection selection = _replyContentController.selection; - final String newText = currentText.replaceRange( - selection.start, - selection.end, - emoteString, - ); - _replyContentController.text = newText; - final int newCursorIndex = selection.start + emoteString.length; - _replyContentController.selection = selection.copyWith( - baseOffset: newCursorIndex, - extentOffset: newCursorIndex, - ); - } } + +typedef DebounceCallback = void Function(); + +class Debouncer { + DebounceCallback? callback; + final int? milliseconds; + Timer? _timer; + + Debouncer({this.milliseconds}); + + run(DebounceCallback callback) { + if (_timer != null) { + _timer!.cancel(); + } + _timer = Timer(Duration(milliseconds: milliseconds!), () { + callback(); + }); + } +} \ No newline at end of file diff --git a/lib/pages/video/detail/widgets/header_control.dart b/lib/pages/video/detail/widgets/header_control.dart index 5de48ca4..883a0c92 100644 --- a/lib/pages/video/detail/widgets/header_control.dart +++ b/lib/pages/video/detail/widgets/header_control.dart @@ -19,6 +19,8 @@ import 'package:PiliPalaX/plugin/pl_player/models/play_repeat.dart'; import 'package:PiliPalaX/utils/storage.dart'; import 'package:PiliPalaX/http/danmaku.dart'; import 'package:PiliPalaX/services/shutdown_timer_service.dart'; +import '../../../../models/video_detail_res.dart'; +import '../introduction/index.dart'; class HeaderControl extends StatefulWidget implements PreferredSizeWidget { const HeaderControl({ @@ -48,11 +50,31 @@ class _HeaderControlState extends State { final Box videoStorage = GStrorage.video; late List speedsList; double buttonSpace = 8; + bool showTitle = false; + late String heroTag; + late VideoIntroController videoIntroController; + late VideoDetailData videoDetail; + @override void initState() { super.initState(); videoInfo = widget.videoDetailCtr!.data; speedsList = widget.controller!.speedsList; + fullScreenStatusListener(); + heroTag = Get.arguments['heroTag']; + videoIntroController = Get.put(VideoIntroController(), tag: heroTag); + } + + void fullScreenStatusListener() { + widget.videoDetailCtr!.plPlayerController.isFullScreen + .listen((bool isFullScreen) { + if (isFullScreen) { + showTitle = true; + } else { + showTitle = false; + } + setState(() {}); + }); } @override @@ -1051,6 +1073,8 @@ class _HeaderControlState extends State { color: Colors.white, fontSize: 12, ); + final bool isLandscape = + MediaQuery.of(context).orientation == Orientation.landscape; return AppBar( backgroundColor: Colors.transparent, foregroundColor: Colors.white, @@ -1085,21 +1109,47 @@ class _HeaderControlState extends State { }, ), SizedBox(width: buttonSpace), - ComBtn( - icon: const Icon( - FontAwesomeIcons.house, - size: 15, - color: Colors.white, + if (showTitle && isLandscape) ...[ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ConstrainedBox( + constraints: BoxConstraints(maxWidth: 200), + child: Text( + videoIntroController.videoDetail.value.title!, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + ), + ), + ), + if (videoIntroController.isShowOnlineTotal) + Text( + '${videoIntroController.total.value}人正在看', + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), + ) + ], + ) + ] else ...[ + ComBtn( + icon: const Icon( + FontAwesomeIcons.house, + size: 15, + color: Colors.white, + ), + fuc: () async { + // 销毁播放器实例 + // await widget.controller!.dispose(type: 'all'); + if (mounted) { + Navigator.popUntil( + context, (Route route) => route.isFirst); + } + }, ), - fuc: () async { - // 销毁播放器实例 - // await widget.controller!.dispose(type: 'all'); - if (mounted) { - Navigator.popUntil( - context, (Route route) => route.isFirst); - } - }, - ), + ], const Spacer(), // ComBtn( // icon: const Icon( diff --git a/lib/pages/whisper/view.dart b/lib/pages/whisper/view.dart index c32a6355..3b6c2427 100644 --- a/lib/pages/whisper/view.dart +++ b/lib/pages/whisper/view.dart @@ -178,7 +178,13 @@ class _WhisperPageState extends State { title: Text(sessionList[i].accountInfo.name), subtitle: Text( - sessionList[i] + sessionList[i].lastMsg.content != + null && + sessionList[i] + .lastMsg + .content != + '' + ? (sessionList[i] .lastMsg .content['text'] ?? sessionList[i] @@ -189,8 +195,8 @@ class _WhisperPageState extends State { .content['title'] ?? sessionList[i] .lastMsg - .content['reply_content'] ?? - '', + .content['reply_content']) + : '不支持的消息类型', maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context) @@ -226,7 +232,9 @@ class _WhisperPageState extends State { ); } else { // 请求错误 - return const SizedBox(); + return Center( + child: Text(data['msg'] ?? '请求异常'), + ); } } else { // 骨架屏 diff --git a/lib/pages/whisper_detail/controller.dart b/lib/pages/whisper_detail/controller.dart index efbbde1f..551a66d2 100644 --- a/lib/pages/whisper_detail/controller.dart +++ b/lib/pages/whisper_detail/controller.dart @@ -1,7 +1,11 @@ +import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; +import 'package:hive/hive.dart'; import 'package:PiliPalaX/http/msg.dart'; import 'package:PiliPalaX/models/msg/session.dart'; +import '../../utils/feed_back.dart'; +import '../../utils/storage.dart'; class WhisperDetailController extends GetxController { late int talkerId; @@ -11,6 +15,8 @@ class WhisperDetailController extends GetxController { RxList messageList = [].obs; //表情转换图片规则 List? eInfos; + final TextEditingController replyContentController = TextEditingController(); + Box userInfoCache = GStrorage.userInfo; @override void onInit() { @@ -36,6 +42,7 @@ class WhisperDetailController extends GetxController { } } + // 消息标记已读 Future ackSessionMsg() async { if (messageList.isEmpty) { return; @@ -44,10 +51,33 @@ class WhisperDetailController extends GetxController { talkerId: talkerId, ackSeqno: messageList.last.msgSeqno, ); - if (res['status']) { - SmartDialog.showToast("已读成功"); - } else { + if (!res['status']) { SmartDialog.showToast(res['msg']); } } + + Future sendMsg() async { + feedBack(); + String message = replyContentController.text; + final userInfo = userInfoCache.get('userInfoCache'); + if (userInfo == null) { + SmartDialog.showToast('请先登录'); + return; + } + if (message == '') { + SmartDialog.showToast('请输入内容'); + return; + } + var result = await MsgHttp.sendMsg( + senderUid: userInfo.mid, + receiverId: int.parse(mid), + content: {'content': message}, + msgType: 1, + ); + if (result['status']) { + SmartDialog.showToast('发送成功'); + } else { + SmartDialog.showToast(result['msg']); + } + } } diff --git a/lib/pages/whisper_detail/view.dart b/lib/pages/whisper_detail/view.dart index 4c39f581..2b632c02 100644 --- a/lib/pages/whisper_detail/view.dart +++ b/lib/pages/whisper_detail/view.dart @@ -1,9 +1,13 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; + +import 'package:hive/hive.dart'; import 'package:PiliPalaX/common/widgets/network_img_layer.dart'; +import 'package:PiliPalaX/pages/emote/index.dart'; import 'package:PiliPalaX/pages/whisper_detail/controller.dart'; import 'package:PiliPalaX/utils/feed_back.dart'; - +import '../../utils/storage.dart'; import 'widget/chat_item.dart'; class WhisperDetailPage extends StatefulWidget { @@ -13,14 +17,62 @@ class WhisperDetailPage extends StatefulWidget { State createState() => _WhisperDetailPageState(); } -class _WhisperDetailPageState extends State { +class _WhisperDetailPageState extends State + with WidgetsBindingObserver { final WhisperDetailController _whisperDetailController = Get.put(WhisperDetailController()); + late TextEditingController _replyContentController; + final FocusNode replyContentFocusNode = FocusNode(); + final _debouncer = Debouncer(milliseconds: 200); // 设置延迟时间 + late double emoteHeight = 0.0; + double keyboardHeight = 0.0; // 键盘高度 + String toolbarType = 'input'; + Box userInfoCache = GStrorage.userInfo; @override void initState() { super.initState(); + WidgetsBinding.instance.addObserver(this); _whisperDetailController.querySessionMsg(); + _replyContentController = _whisperDetailController.replyContentController; + _focuslistener(); + } + + _focuslistener() { + replyContentFocusNode.addListener(() { + if (replyContentFocusNode.hasFocus) { + setState(() { + toolbarType = 'input'; + }); + } + }); + } + + @override + void didChangeMetrics() { + super.didChangeMetrics(); + WidgetsBinding.instance.addPostFrameCallback((_) { + // 键盘高度 + final viewInsets = EdgeInsets.fromViewPadding( + View.of(context).viewInsets, View.of(context).devicePixelRatio); + _debouncer.run(() { + if (mounted) { + if (keyboardHeight == 0) { + setState(() { + emoteHeight = keyboardHeight = + keyboardHeight == 0.0 ? viewInsets.bottom : keyboardHeight; + }); + } + } + }); + }); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + replyContentFocusNode.removeListener(() {}); + super.dispose(); } @override @@ -88,26 +140,35 @@ class _WhisperDetailPageState extends State { ), ), ), - body: Obx(() { - List messageList = _whisperDetailController.messageList; - if (messageList.isEmpty) { - return const Center( - child: CircularProgressIndicator(), + body: GestureDetector( + onTap: () { + FocusScope.of(context).unfocus(); + setState(() { + keyboardHeight = 0; + }); + }, + child: Obx(() { + List messageList = _whisperDetailController.messageList; + if (messageList.isEmpty) { + return const Center( + child: CircularProgressIndicator(), + ); + } + return ListView.builder( + itemCount: messageList.length, + shrinkWrap: true, + reverse: true, + itemBuilder: (_, int i) { + return ChatItem( + item: messageList[i], e_infos: _whisperDetailController.eInfos); + }, ); - } - return ListView.builder( - itemCount: messageList.length, - shrinkWrap: true, - reverse: true, - itemBuilder: (_, int i) { - return ChatItem( - item: messageList[i], e_infos: _whisperDetailController.eInfos); - }, - ); - }), + }), + ), + // resizeToAvoidBottomInset: true, bottomNavigationBar: Container( width: double.infinity, - height: MediaQuery.of(context).padding.bottom + 70, + height: MediaQuery.of(context).padding.bottom + 70 + keyboardHeight, padding: EdgeInsets.only( left: 8, right: 12, @@ -122,48 +183,102 @@ class _WhisperDetailPageState extends State { ), ), ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, + child: Column( children: [ - // IconButton( - // onPressed: () {}, - // icon: Icon( - // Icons.add_circle_outline, - // color: Theme.of(context).colorScheme.outline, - // ), - // ), - IconButton( - onPressed: () {}, - icon: Icon( - Icons.emoji_emotions_outlined, - color: Theme.of(context).colorScheme.outline, - ), - ), - Expanded( - child: Container( - height: 45, - decoration: BoxDecoration( - color: - Theme.of(context).colorScheme.primary.withOpacity(0.08), - borderRadius: BorderRadius.circular(40.0), - ), - child: TextField( - readOnly: true, - style: Theme.of(context).textTheme.titleMedium, - decoration: const InputDecoration( - border: InputBorder.none, // 移除默认边框 - hintText: '开发中 ...', // 提示文本 - contentPadding: EdgeInsets.symmetric( - horizontal: 16.0, vertical: 12.0), // 内边距 + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // IconButton( + // onPressed: () {}, + // icon: Icon( + // Icons.add_circle_outline, + // color: Theme.of(context).colorScheme.outline, + // ), + // ), + IconButton( + onPressed: () { + // if (toolbarType == 'input') { + // setState(() { + // toolbarType = 'emote'; + // }); + // } + // FocusScope.of(context).unfocus(); + }, + icon: Icon( + Icons.emoji_emotions_outlined, + color: Theme.of(context).colorScheme.outline, ), ), + Expanded( + child: Container( + height: 45, + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .primary + .withOpacity(0.08), + borderRadius: BorderRadius.circular(40.0), + ), + child: TextField( + readOnly: true, + style: Theme.of(context).textTheme.titleMedium, + controller: _replyContentController, + autofocus: false, + focusNode: replyContentFocusNode, + decoration: const InputDecoration( + border: InputBorder.none, // 移除默认边框 + hintText: '开发中 ...', // 提示文本 + contentPadding: EdgeInsets.symmetric( + horizontal: 16.0, vertical: 12.0), // 内边距 + ), + ), + ), + ), + IconButton( + // onPressed: _whisperDetailController.sendMsg, + onPressed: null, + icon: Icon( + Icons.send, + color: Theme.of(context).colorScheme.outline, + ), + ), + // const SizedBox(width: 16), + ], + ), + AnimatedSize( + curve: Curves.easeInOut, + duration: const Duration(milliseconds: 300), + child: SizedBox( + width: double.infinity, + height: toolbarType == 'input' ? keyboardHeight : emoteHeight, + child: EmotePanel( + onChoose: (package, emote) => {}, + ), ), ), - const SizedBox(width: 16), ], ), ), ); } } + +typedef DebounceCallback = void Function(); + +class Debouncer { + DebounceCallback? callback; + final int? milliseconds; + Timer? _timer; + + Debouncer({this.milliseconds}); + + run(DebounceCallback callback) { + if (_timer != null) { + _timer!.cancel(); + } + _timer = Timer(Duration(milliseconds: milliseconds!), () { + callback(); + }); + } +} diff --git a/lib/pages/whisper_detail/widget/chat_item.dart b/lib/pages/whisper_detail/widget/chat_item.dart index 86957386..458fea58 100644 --- a/lib/pages/whisper_detail/widget/chat_item.dart +++ b/lib/pages/whisper_detail/widget/chat_item.dart @@ -358,7 +358,9 @@ class ChatItem extends StatelessWidget { )); default: return Text( - content['content'] ?? content.toString(), + content != null && content != '' + ? (content['content'] ?? content.toString()) + : '不支持的消息类型', style: TextStyle( letterSpacing: 0.6, height: 1.5, diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index 121ffd51..db4c40ff 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -49,6 +49,8 @@ import '../pages/setting/play_setting.dart'; import '../pages/setting/privacy_setting.dart'; import '../pages/setting/style_setting.dart'; import '../pages/setting/hidden_settings.dart'; +import '../pages/subscription/index.dart'; +import '../pages/subscription_detail/index.dart'; import '../pages/video/detail/index.dart'; import '../pages/video/detail/reply_reply/index.dart'; import '../pages/webview/index.dart'; @@ -175,6 +177,10 @@ class Routes { CustomGetPage(name: '/logs', page: () => const LogsPage()), // 搜索关注 CustomGetPage(name: '/followSearch', page: () => const FollowSearchPage()), + // 订阅 + CustomGetPage(name: '/subscription', page: () => const SubPage()), + // 订阅详情 + CustomGetPage(name: '/subDetail', page: () => const SubDetailPage()), ]; } diff --git a/lib/utils/global_data.dart b/lib/utils/global_data.dart new file mode 100644 index 00000000..a8a04eba --- /dev/null +++ b/lib/utils/global_data.dart @@ -0,0 +1,12 @@ +class GlobalData { + int imgQuality = 10; + + // 私有构造函数 + GlobalData._(); + + // 单例实例 + static final GlobalData _instance = GlobalData._(); + + // 获取全局实例 + factory GlobalData() => _instance; +} diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart index f7f4fdfd..80a8e713 100644 --- a/lib/utils/storage.dart +++ b/lib/utils/storage.dart @@ -1,11 +1,10 @@ -// import 'package:hive/hive.dart'; import 'dart:io'; - import 'package:hive_flutter/hive_flutter.dart'; import 'package:path_provider/path_provider.dart'; import 'package:PiliPalaX/models/model_owner.dart'; import 'package:PiliPalaX/models/search/hot.dart'; import 'package:PiliPalaX/models/user/info.dart'; +import 'global_data.dart'; class GStrorage { static late final Box userInfo; @@ -44,6 +43,8 @@ class GStrorage { ); // 视频设置 video = await Hive.openBox('video'); + GlobalData().imgQuality = + setting.get(SettingBoxKey.defaultPicQa, defaultValue: 10); // 设置全局变量 } static void regAdapter() {