From b44dbdfa09a33eaf1d51b014f38e850379d3e5e4 Mon Sep 17 00:00:00 2001 From: orz12 Date: Fri, 23 Feb 2024 01:35:27 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=AF=84=E8=AE=BA=E5=8C=BA=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E8=A1=A8=E6=83=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/http/api.dart | 5 + lib/http/reply.dart | 20 ++ lib/models/user/my_emote.dart | 265 ++++++++++++++++++ .../video/detail/reply/reply_emote/view.dart | 104 +++++++ lib/pages/video/detail/reply_new/view.dart | 85 +++++- 5 files changed, 472 insertions(+), 7 deletions(-) create mode 100644 lib/models/user/my_emote.dart create mode 100644 lib/pages/video/detail/reply/reply_emote/view.dart diff --git a/lib/http/api.dart b/lib/http/api.dart index b2758531f..9b86ca054 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -120,6 +120,11 @@ class Api { // https://api.bilibili.com/x/relation/stat?vmid=697166795 static const String userStat = '/x/relation/stat'; + // 获取我的表情列表 + // business:reply(回复)dynamic(动态) + //https://api.bilibili.com/x/emote/user/panel/web?business=reply + static const String myEmote = '/x/emote/user/panel/web'; + // 获取用户信息 static const String userInfo = '/x/web-interface/nav'; diff --git a/lib/http/reply.dart b/lib/http/reply.dart index fab433fc7..17ad1dfc1 100644 --- a/lib/http/reply.dart +++ b/lib/http/reply.dart @@ -100,4 +100,24 @@ class ReplyHttp { }; } } + + static Future getMyEmote({ + required String business, + }) async { + var res = await Request().get(Api.myEmote, data: { + 'business': business, + }); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': res.data['data'], + }; + } else { + return { + 'status': false, + 'date': [], + 'msg': res.data['message'], + }; + } + } } diff --git a/lib/models/user/my_emote.dart b/lib/models/user/my_emote.dart new file mode 100644 index 000000000..30301f137 --- /dev/null +++ b/lib/models/user/my_emote.dart @@ -0,0 +1,265 @@ +class MyEmote { + Setting? setting; + List? packages; + + MyEmote({this.setting, this.packages}); + + MyEmote.fromJson(Map json) { + setting = + json['setting'] != null ? Setting.fromJson(json['setting']) : null; + if (json['packages'] != null) { + packages = []; + json['packages'].forEach((v) { + packages!.add(Packages.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = {}; + if (setting != null) { + data['setting'] = setting!.toJson(); + } + if (packages != null) { + data['packages'] = packages!.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class Setting { + int? recentLimit; + int? attr; + int? focusPkgId; + String? schema; + + Setting({this.recentLimit, this.attr, this.focusPkgId, this.schema}); + + Setting.fromJson(Map json) { + recentLimit = json['recent_limit']; + attr = json['attr']; + focusPkgId = json['focus_pkg_id']; + schema = json['schema']; + } + + Map toJson() { + final Map data = {}; + data['recent_limit'] = recentLimit; + data['attr'] = attr; + data['focus_pkg_id'] = focusPkgId; + data['schema'] = schema; + return data; + } +} + +class Packages { + int? id; + String? text; + String? url; + int? mtime; + int? type; + int? attr; + PackagesMeta? meta; + List? emote; + PackagesFlags? flags; + dynamic label; + String? packageSubTitle; + int? refMid; + + Packages( + {this.id, + this.text, + this.url, + this.mtime, + this.type, + this.attr, + this.meta, + this.emote, + this.flags, + this.label, + this.packageSubTitle, + this.refMid}); + + Packages.fromJson(Map json) { + id = json['id']; + text = json['text']; + url = json['url']; + mtime = json['mtime']; + type = json['type']; + attr = json['attr']; + meta = json['meta'] != null ? PackagesMeta.fromJson(json['meta']) : null; + if (json['emote'] != null) { + emote = []; + json['emote'].forEach((v) { + emote!.add(Emote.fromJson(v)); + }); + } + flags = json['flags'] != null ? PackagesFlags.fromJson(json['flags']) : null; + label = json['label']; + packageSubTitle = json['package_sub_title']; + refMid = json['ref_mid']; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['text'] = text; + data['url'] = url; + data['mtime'] = mtime; + data['type'] = type; + data['attr'] = attr; + if (meta != null) { + data['meta'] = meta!.toJson(); + } + if (emote != null) { + data['emote'] = emote!.map((v) => v.toJson()).toList(); + } + if (flags != null) { + data['flags'] = flags!.toJson(); + } + data['label'] = label; + data['package_sub_title'] = packageSubTitle; + data['ref_mid'] = refMid; + return data; + } +} + +class PackagesMeta { + int? size; + int? itemId; + + PackagesMeta({this.size, this.itemId}); + + PackagesMeta.fromJson(Map json) { + size = json['size']; + itemId = json['item_id']; + } + + Map toJson() { + final Map data = {}; + data['size'] = size; + data['item_id'] = itemId; + return data; + } +} + +class Emote { + int? id; + int? packageId; + String? text; + String? url; + int? mtime; + int? type; + int? attr; + EmoteMeta? meta; + EmoteFlags? flags; + dynamic activity; + String? gifUrl; + + Emote( + {this.id, + this.packageId, + this.text, + this.url, + this.mtime, + this.type, + this.attr, + this.meta, + this.flags, + this.activity, + this.gifUrl}); + + Emote.fromJson(Map json) { + id = json['id']; + packageId = json['package_id']; + text = json['text']; + url = json['url']; + mtime = json['mtime']; + type = json['type']; + attr = json['attr']; + meta = json['meta'] != null ? EmoteMeta.fromJson(json['meta']) : null; + flags = json['flags'] != null ? EmoteFlags.fromJson(json['flags']) : null; + activity = json['activity']; + gifUrl = json['gif_url']; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['package_id'] = packageId; + data['text'] = text; + data['url'] = url; + data['mtime'] = mtime; + data['type'] = type; + data['attr'] = attr; + if (meta != null) { + data['meta'] = meta!.toJson(); + } + if (flags != null) { + data['flags'] = flags!.toJson(); + } + data['activity'] = activity; + data['gif_url'] = gifUrl; + return data; + } +} + +class EmoteMeta { + int? size; + List? suggest; + String? alias; + String? gifUrl; + + EmoteMeta({this.size, this.suggest, this.alias, this.gifUrl}); + + EmoteMeta.fromJson(Map json) { + size = json['size']; + suggest = json['suggest'].cast(); + alias = json['alias']; + gifUrl = json['gif_url']; + } + + Map toJson() { + final Map data = {}; + data['size'] = size; + data['suggest'] = suggest; + data['alias'] = alias; + data['gif_url'] = gifUrl; + return data; + } +} + +class EmoteFlags { + bool? unlocked; + + EmoteFlags({this.unlocked}); + + EmoteFlags.fromJson(Map json) { + unlocked = json['unlocked']; + } + + Map toJson() { + final Map data = {}; + data['unlocked'] = unlocked; + return data; + } +} + +class PackagesFlags { + bool? added; + bool? preview; + + PackagesFlags({this.added, this.preview}); + + PackagesFlags.fromJson(Map json) { + added = json['added']; + preview = json['preview']; + } + + Map toJson() { + final Map data = {}; + data['added'] = added; + data['preview'] = preview; + return data; + } +} diff --git a/lib/pages/video/detail/reply/reply_emote/view.dart b/lib/pages/video/detail/reply/reply_emote/view.dart new file mode 100644 index 000000000..297877b7e --- /dev/null +++ b/lib/pages/video/detail/reply/reply_emote/view.dart @@ -0,0 +1,104 @@ +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.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, + ), + ); + }, + ), + ], + ]),), + 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/view.dart b/lib/pages/video/detail/reply_new/view.dart index c14868ae6..ca6cb1da3 100644 --- a/lib/pages/video/detail/reply_new/view.dart +++ b/lib/pages/video/detail/reply_new/view.dart @@ -7,6 +7,9 @@ import 'package:PiliPalaX/models/common/reply_type.dart'; import 'package:PiliPalaX/models/video/reply/item.dart'; import 'package:PiliPalaX/utils/feed_back.dart'; +import '../../../../common/constants.dart'; +import '../reply/reply_emote/view.dart'; + class VideoReplyNewDialog extends StatefulWidget { final int? oid; final int? root; @@ -32,6 +35,7 @@ class _VideoReplyNewDialogState extends State final TextEditingController _replyContentController = TextEditingController(); final FocusNode replyContentFocusNode = FocusNode(); final GlobalKey _formKey = GlobalKey(); + bool isShowEmote = false; @override void initState() { @@ -141,9 +145,13 @@ class _VideoReplyNewDialogState extends State width: 36, height: 36, child: IconButton( - onPressed: () { + onPressed: () async { FocusScope.of(context) .requestFocus(replyContentFocusNode); + await Future.delayed(const Duration(milliseconds: 200)); + setState(() { + isShowEmote = false; + }); }, icon: Icon(Icons.keyboard, size: 22, @@ -154,7 +162,44 @@ class _VideoReplyNewDialogState extends State padding: MaterialStateProperty.all(EdgeInsets.zero), backgroundColor: MaterialStateProperty.resolveWith((states) { - return Theme.of(context).highlightColor; + if (states.contains(MaterialState.pressed) || !isShowEmote) { + return Theme.of(context).highlightColor; + } + // 默认状态下,返回透明颜色 + return Colors.transparent; + }), + ), + ), + ), + const SizedBox( + width: 10, + ), + SizedBox( + width: 36, + height: 36, + child: IconButton( + onPressed: () { + //收起输入法 + FocusScope.of(context).unfocus(); + // 弹出表情选择 + setState(() { + isShowEmote = true; + }); + }, + 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; }), ), ), @@ -165,16 +210,42 @@ class _VideoReplyNewDialogState extends State ], ), ), - AnimatedSize( - curve: Curves.easeInOut, - duration: const Duration(milliseconds: 300), - child: SizedBox( + if (!isShowEmote) + 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, + ), + ), + ) ], ), ); } + + 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, + ); + } }