feat: 评论增加表情
This commit is contained in:
20
lib/pages/emote/controller.dart
Normal file
20
lib/pages/emote/controller.dart
Normal file
@ -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<PackageItem> 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;
|
||||
}
|
||||
}
|
||||
4
lib/pages/emote/index.dart
Normal file
4
lib/pages/emote/index.dart
Normal file
@ -0,0 +1,4 @@
|
||||
library emote;
|
||||
|
||||
export './controller.dart';
|
||||
export './view.dart';
|
||||
116
lib/pages/emote/view.dart
Normal file
116
lib/pages/emote/view.dart
Normal file
@ -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<EmotePanel> createState() => _EmotePanelState();
|
||||
}
|
||||
|
||||
class _EmotePanelState extends State<EmotePanel>
|
||||
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<PackageItem> 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('加载中...'));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
40
lib/pages/video/detail/reply_new/toolbar_icon_button.dart
Normal file
40
lib/pages/video/detail/reply_new/toolbar_icon_button.dart
Normal file
@ -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;
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -4,9 +4,13 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/http/video.dart';
|
||||
import 'package:pilipala/models/common/reply_type.dart';
|
||||
import 'package:pilipala/models/video/reply/emote.dart';
|
||||
import 'package:pilipala/models/video/reply/item.dart';
|
||||
import 'package:pilipala/pages/emote/index.dart';
|
||||
import 'package:pilipala/utils/feed_back.dart';
|
||||
|
||||
import 'toolbar_icon_button.dart';
|
||||
|
||||
class VideoReplyNewDialog extends StatefulWidget {
|
||||
final int? oid;
|
||||
final int? root;
|
||||
@ -32,6 +36,10 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
|
||||
final TextEditingController _replyContentController = TextEditingController();
|
||||
final FocusNode replyContentFocusNode = FocusNode();
|
||||
final GlobalKey _formKey = GlobalKey<FormState>();
|
||||
late double emoteHeight = 0.0;
|
||||
double keyboardHeight = 0.0; // 键盘高度
|
||||
final _debouncer = Debouncer(milliseconds: 200); // 设置延迟时间
|
||||
String toolbarType = 'input';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -42,6 +50,8 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
// 自动聚焦
|
||||
_autoFocus();
|
||||
// 监听聚焦状态
|
||||
_focuslistener();
|
||||
}
|
||||
|
||||
_autoFocus() async {
|
||||
@ -51,6 +61,16 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
|
||||
}
|
||||
}
|
||||
|
||||
_focuslistener() {
|
||||
replyContentFocusNode.addListener(() {
|
||||
if (replyContentFocusNode.hasFocus) {
|
||||
setState(() {
|
||||
toolbarType = 'input';
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future submitReplyAdd() async {
|
||||
feedBack();
|
||||
String message = _replyContentController.text;
|
||||
@ -73,18 +93,49 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
|
||||
}
|
||||
}
|
||||
|
||||
void onChooseEmote(PackageItem 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(
|
||||
@ -137,27 +188,32 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 36,
|
||||
height: 36,
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
FocusScope.of(context)
|
||||
.requestFocus(replyContentFocusNode);
|
||||
},
|
||||
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) {
|
||||
return Theme.of(context).highlightColor;
|
||||
}),
|
||||
),
|
||||
),
|
||||
ToolbarIconButton(
|
||||
onPressed: () {
|
||||
if (toolbarType == 'emote') {
|
||||
setState(() {
|
||||
toolbarType = 'input';
|
||||
});
|
||||
}
|
||||
FocusScope.of(context).requestFocus(replyContentFocusNode);
|
||||
},
|
||||
icon: const Icon(Icons.keyboard, size: 22),
|
||||
toolbarType: toolbarType,
|
||||
selected: toolbarType == 'input',
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
ToolbarIconButton(
|
||||
onPressed: () {
|
||||
if (toolbarType == 'input') {
|
||||
setState(() {
|
||||
toolbarType = 'emote';
|
||||
});
|
||||
}
|
||||
FocusScope.of(context).unfocus();
|
||||
},
|
||||
icon: const Icon(Icons.emoji_emotions, size: 22),
|
||||
toolbarType: toolbarType,
|
||||
selected: toolbarType == 'emote',
|
||||
),
|
||||
const Spacer(),
|
||||
TextButton(
|
||||
@ -170,7 +226,10 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: keyboardHeight,
|
||||
height: toolbarType == 'input' ? keyboardHeight : emoteHeight,
|
||||
child: EmotePanel(
|
||||
onChoose: (package, emote) => onChooseEmote(package, emote),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -178,3 +237,22 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user