diff --git a/lib/http/danmaku.dart b/lib/http/danmaku.dart index eff7e72f..2a2aa45d 100644 --- a/lib/http/danmaku.dart +++ b/lib/http/danmaku.dart @@ -29,8 +29,8 @@ class DanmakaHttp { int type = 1, //弹幕类选择(1:视频弹幕 2:漫画弹幕) required int oid, // 视频cid required String msg, //弹幕文本(长度小于 100 字符) - int mode = - 1, // 弹幕类型(1:滚动弹幕 4:底端弹幕 5:顶端弹幕 6:逆向弹幕(不能使用) 7:高级弹幕 8:代码弹幕(不能使用) 9:BAS弹幕(pool必须为2)) + // 弹幕类型(1:滚动弹幕 4:底端弹幕 5:顶端弹幕 6:逆向弹幕(不能使用) 7:高级弹幕 8:代码弹幕(不能使用) 9:BAS弹幕(pool必须为2)) + int mode = 1, // String? aid,// 稿件avid // String? bvid,// bvid与aid必须有一个 required String bvid, @@ -47,7 +47,6 @@ class DanmakaHttp { // 构建参数对象 // assert(aid != null || bvid != null); // assert(csrf != null || access_key != null); - assert(msg.length < 100); // 构建参数对象 var params = { 'type': type, diff --git a/lib/pages/bangumi/introduction/controller.dart b/lib/pages/bangumi/introduction/controller.dart index 9679c555..203e5243 100644 --- a/lib/pages/bangumi/introduction/controller.dart +++ b/lib/pages/bangumi/introduction/controller.dart @@ -372,15 +372,18 @@ class BangumiIntroController extends CommonController { // 修改分P或番剧分集 Future changeSeasonOrbangu(epId, bvid, cid, aid, cover) async { // 重新获取视频资源 - VideoDetailController videoDetailCtr = - Get.find(tag: Get.arguments['heroTag']) - ..epId = epId; this.epId = epId; this.bvid = bvid; - videoDetailCtr.bvid = bvid; - videoDetailCtr.cid.value = cid; - videoDetailCtr.danmakuCid.value = cid; - videoDetailCtr.queryVideoUrl(); + + final videoDetailCtr = + Get.find(tag: Get.arguments['heroTag']) + ..vttSubtitlesIndex = null + ..savedDanmaku = null + ..epId = epId + ..bvid = bvid + ..cid.value = cid + ..danmakuCid.value = cid + ..queryVideoUrl(); if (cover is String && cover.isNotEmpty) { videoDetailCtr.videoItem['pic'] = cover; } diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart index 28d00c9a..c7c92f56 100644 --- a/lib/pages/video/detail/controller.dart +++ b/lib/pages/video/detail/controller.dart @@ -6,7 +6,6 @@ import 'package:PiliPalaX/common/widgets/icon_button.dart'; import 'package:PiliPalaX/common/widgets/loading_widget.dart'; import 'package:PiliPalaX/common/widgets/pair.dart'; import 'package:PiliPalaX/common/widgets/segment_progress_bar.dart'; -import 'package:PiliPalaX/http/danmaku.dart'; import 'package:PiliPalaX/http/init.dart'; import 'package:PiliPalaX/http/loading_state.dart'; import 'package:PiliPalaX/http/user.dart'; @@ -17,9 +16,9 @@ import 'package:PiliPalaX/pages/search/widgets/search_text.dart'; import 'package:PiliPalaX/pages/video/detail/introduction/controller.dart'; import 'package:PiliPalaX/pages/video/detail/related/controller.dart'; import 'package:PiliPalaX/pages/video/detail/reply/controller.dart'; +import 'package:PiliPalaX/pages/video/detail/widgets/send_danmaku_panel.dart'; import 'package:PiliPalaX/pages/video/detail/widgets/watch_later_list.dart'; import 'package:PiliPalaX/utils/extension.dart'; -import 'package:canvas_danmaku/models/danmaku_content_item.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:dio/dio.dart'; import 'package:easy_debounce/easy_throttle.dart'; @@ -37,6 +36,7 @@ import 'package:PiliPalaX/plugin/pl_player/index.dart'; import 'package:PiliPalaX/utils/storage.dart'; import 'package:PiliPalaX/utils/utils.dart'; import 'package:PiliPalaX/utils/video_utils.dart'; +import 'package:get/get_navigation/src/dialog/dialog_route.dart'; import 'package:hive/hive.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; @@ -917,80 +917,49 @@ class VideoDetailController extends GetxController } } + String? savedDanmaku; + /// 发送弹幕 - void showShootDanmakuSheet() { - final TextEditingController textController = TextEditingController(); - bool isSending = false; // 追踪是否正在发送 - showDialog( - context: Get.context!, - builder: (BuildContext context) { - // TODO: 支持更多类型和颜色的弹幕 - return AlertDialog( - title: const Text('发送弹幕'), - content: StatefulBuilder( - builder: (BuildContext context, StateSetter setState) { - return TextField( - controller: textController, - autofocus: true, - ); - }), - actions: [ - TextButton( - onPressed: () => Get.back(), - child: Text( - '取消', - style: TextStyle(color: Theme.of(context).colorScheme.outline), - ), - ), - StatefulBuilder( - builder: (BuildContext context, StateSetter setState) { - return TextButton( - onPressed: isSending - ? null - : () async { - final String msg = textController.text; - if (msg.isEmpty) { - SmartDialog.showToast('弹幕内容不能为空'); - return; - } else if (msg.length > 100) { - SmartDialog.showToast('弹幕内容不能超过100个字符'); - return; - } - isSending = true; // 开始发送,更新状态 - //修改按钮文字 - // SmartDialog.showToast('弹幕发送中,\n$msg'); - final dynamic res = await DanmakaHttp.shootDanmaku( - oid: cid.value, - msg: textController.text, - bvid: bvid, - progress: - plPlayerController.position.value.inMilliseconds, - type: 1, - ); - isSending = false; // 发送结束,更新状态 - if (res['status']) { - SmartDialog.showToast('发送成功'); - // 发送成功,自动预览该弹幕,避免重新请求 - // TODO: 暂停状态下预览弹幕仍会移动与计时,可考虑添加到dmSegList或其他方式实现 - plPlayerController.danmakuController - ?.addDanmaku(DanmakuContentItem( - msg, - color: Colors.white, - type: DanmakuItemType.scroll, - selfSend: true, - )); - Get.back(); - } else { - SmartDialog.showToast('发送失败,错误信息为${res['msg']}'); - } - }, - child: Text(isSending ? '发送中...' : '发送'), - ); - }) - ], - ); - }, + void showShootDanmakuSheet() async { + bool isPlaying = + plPlayerController.playerStatus.status.value == PlayerStatus.playing; + if (isPlaying) { + plPlayerController.pause(); + } + await Navigator.of(Get.context!).push( + GetDialogRoute( + pageBuilder: (buildContext, animation, secondaryAnimation) { + return SendDanmakuPanel( + cid: cid.value, + bvid: bvid, + progress: plPlayerController.position.value.inMilliseconds, + savedDanmaku: savedDanmaku, + onSaveDanmaku: (danmaku) => savedDanmaku = danmaku, + callback: (danmakuModel) { + savedDanmaku = null; + plPlayerController.danmakuController?.addDanmaku(danmakuModel); + }, + ); + }, + transitionDuration: const Duration(milliseconds: 500), + transitionBuilder: (context, animation, secondaryAnimation, child) { + const begin = Offset(0.0, 1.0); + const end = Offset.zero; + const curve = Curves.linear; + + var tween = + Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); + + return SlideTransition( + position: animation.drive(tween), + child: child, + ); + }, + ), ); + if (isPlaying) { + plPlayerController.play(); + } } /// 更新画质、音质 diff --git a/lib/pages/video/detail/introduction/controller.dart b/lib/pages/video/detail/introduction/controller.dart index ac7fbe46..098f182e 100644 --- a/lib/pages/video/detail/introduction/controller.dart +++ b/lib/pages/video/detail/introduction/controller.dart @@ -557,18 +557,18 @@ class VideoIntroController extends GetxController // 修改分P或番剧分集 Future changeSeasonOrbangu(epid, bvid, cid, aid, cover) async { // 重新获取视频资源 - final VideoDetailController videoDetailCtr = - Get.find(tag: heroTag); - videoDetailCtr.updateMediaListHistory(aid); - videoDetailCtr.vttSubtitlesIndex = null; - videoDetailCtr.bvid = bvid; - videoDetailCtr.oid.value = aid ?? IdUtils.bv2av(bvid); - videoDetailCtr.cid.value = cid; + final videoDetailCtr = Get.find(tag: heroTag) + ..updateMediaListHistory(aid) + ..vttSubtitlesIndex = null + ..savedDanmaku = null + ..bvid = bvid + ..oid.value = aid ?? IdUtils.bv2av(bvid) + ..cid.value = cid + ..danmakuCid.value = cid + ..queryVideoUrl(); if (cover is String && cover.isNotEmpty) { videoDetailCtr.videoItem['pic'] = cover; } - videoDetailCtr.danmakuCid.value = cid; - videoDetailCtr.queryVideoUrl(); // 重新请求相关视频 if (videoDetailCtr.showRelatedVideo) { diff --git a/lib/pages/video/detail/reply_new/toolbar_icon_button.dart b/lib/pages/video/detail/reply_new/toolbar_icon_button.dart index 48e9e3d3..8f906495 100644 --- a/lib/pages/video/detail/reply_new/toolbar_icon_button.dart +++ b/lib/pages/video/detail/reply_new/toolbar_icon_button.dart @@ -1,17 +1,17 @@ import 'package:flutter/material.dart'; class ToolbarIconButton extends StatelessWidget { - final VoidCallback onPressed; + final VoidCallback? onPressed; final Icon icon; final bool selected; - final String tooltip; + final String? tooltip; const ToolbarIconButton({ super.key, - required this.onPressed, + this.onPressed, required this.icon, required this.selected, - required this.tooltip, + this.tooltip, }); @override diff --git a/lib/pages/video/detail/widgets/send_danmaku_panel.dart b/lib/pages/video/detail/widgets/send_danmaku_panel.dart new file mode 100644 index 00000000..f9d59022 --- /dev/null +++ b/lib/pages/video/detail/widgets/send_danmaku_panel.dart @@ -0,0 +1,588 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:PiliPalaX/http/danmaku.dart'; +import 'package:PiliPalaX/pages/setting/sponsor_block_page.dart' + show SlideColorPicker; +import 'package:PiliPalaX/pages/video/detail/reply_new/reply_page.dart' + show PanelType; +import 'package:PiliPalaX/utils/extension.dart'; +import 'package:canvas_danmaku/models/danmaku_content_item.dart'; +import 'package:chat_bottom_container/chat_bottom_container.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:PiliPalaX/pages/video/detail/reply_new/toolbar_icon_button.dart'; + +class SendDanmakuPanel extends StatefulWidget { + final dynamic cid; + final dynamic bvid; + final dynamic progress; + final String? savedDanmaku; + final ValueChanged? onSaveDanmaku; + final ValueChanged callback; + + const SendDanmakuPanel({ + super.key, + required this.cid, + required this.bvid, + required this.progress, + required this.savedDanmaku, + required this.onSaveDanmaku, + required this.callback, + }); + + @override + State createState() => _SendDanmakuPanelState(); +} + +class _SendDanmakuPanelState extends State + with SingleTickerProviderStateMixin, WidgetsBindingObserver { + late final _focusNode = FocusNode(); + late final _controller = ChatBottomPanelContainerController(); + late final _textController = TextEditingController(text: widget.savedDanmaku); + final RxBool _readOnly = false.obs; + final RxBool _enablePublish = false.obs; + final RxBool _selectKeyboard = true.obs; + final RxInt _mode = 1.obs; + final RxInt _fontsize = 25.obs; + final Rx _color = Colors.white.obs; + + final List _colorList = [ + Colors.white, + Color(0xFFFE0302), + Color(0xFFFF7204), + Color(0xFFFFAA02), + Color(0xFFFFD302), + Color(0xFFFFFF00), + Color(0xFFA0EE00), + Color(0xFF00CD00), + Color(0xFF019899), + Color(0xFF4266BE), + Color(0xFF89D5FF), + Color(0xFFCC0273), + Color(0xFF222222), + Color(0xFF9B9B9B), + ]; + + get _buildColorPanel => Expanded( + child: Obx( + () => LayoutBuilder( + key: ValueKey(_color.value), + builder: (context, constraints) { + final int crossAxisCount = (constraints.maxWidth / 40).toInt(); + final bool isCustomColor = _colorList.contains(_color.value).not; + final int length = + _colorList.length + (isCustomColor ? 1 : 0) + 1; + return GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + crossAxisSpacing: 4, + mainAxisSpacing: 4, + ), + itemCount: length, + itemBuilder: (context, index) { + if (index == length - 1) { + return GestureDetector( + onTap: () { + _showColorPicker(); + }, + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: + Theme.of(context).colorScheme.secondaryContainer, + borderRadius: BorderRadius.circular(8), + ), + alignment: Alignment.center, + margin: const EdgeInsets.all(2), + child: Icon( + size: 22, + Icons.edit, + color: Theme.of(context) + .colorScheme + .onSecondaryContainer, + ), + ), + ); + } else if (index == length - 2 && isCustomColor) { + return _buildColorItem(_color.value); + } + return _buildColorItem(_colorList[index]); + }, + ); + }, + ), + ), + ); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + + if (widget.savedDanmaku.isNullOrEmpty.not) { + _enablePublish.value = true; + } + + () async { + await Future.delayed(const Duration(milliseconds: 300)); + if (mounted) { + _focusNode.requestFocus(); + } + }(); + } + + @override + void dispose() async { + _focusNode.dispose(); + _textController.dispose(); + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + void _requestFocus() async { + await Future.delayed(const Duration(microseconds: 200)); + _focusNode.requestFocus(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + if (mounted && _selectKeyboard.value) { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { + if (_focusNode.hasFocus) { + _focusNode.unfocus(); + _requestFocus(); + } else { + _requestFocus(); + } + }); + } + } else if (state == AppLifecycleState.paused) { + _controller.keepChatPanel(); + if (_focusNode.hasFocus) { + _focusNode.unfocus(); + } + } + } + + @override + Widget build(BuildContext context) { + return MediaQuery.removePadding( + removeTop: true, + context: context, + child: GestureDetector( + onTap: Get.back, + child: LayoutBuilder( + builder: (context, constraints) { + bool isH = constraints.maxWidth > constraints.maxHeight; + late double padding = constraints.maxWidth * 0.12; + return Padding( + padding: EdgeInsets.symmetric(horizontal: isH ? padding : 0), + child: Scaffold( + resizeToAvoidBottomInset: false, + backgroundColor: Colors.transparent, + body: GestureDetector( + onTap: () {}, + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + _buildInputView(), + _buildPanelContainer(), + ], + ), + ), + ), + ); + }, + ), + ), + ); + } + + Widget _buildPanelContainer() { + return ChatBottomPanelContainer( + controller: _controller, + inputFocusNode: _focusNode, + otherPanelWidget: (type) { + if (type == null) return const SizedBox.shrink(); + switch (type) { + case PanelType.emoji: + return _buildEmojiPickerPanel(); + default: + return const SizedBox.shrink(); + } + }, + panelBgColor: Theme.of(context).colorScheme.surface, + ); + } + + Widget _buildEmojiPickerPanel() { + double height = 170; + final keyboardHeight = _controller.keyboardHeight; + if (keyboardHeight != 0) { + height = max(height, keyboardHeight); + } + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + height: height, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Text('弹幕字号', style: TextStyle(fontSize: 15)), + const SizedBox(width: 16), + _buildFontSizeItem(18, '小'), + const SizedBox(width: 5), + _buildFontSizeItem(25, '标准'), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + const Text('弹幕样式', style: TextStyle(fontSize: 15)), + const SizedBox(width: 16), + _buildPositionItem(1, '滚动'), + const SizedBox(width: 5), + _buildPositionItem(5, '顶部'), + const SizedBox(width: 5), + _buildPositionItem(4, '底部'), + ], + ), + const SizedBox(height: 12), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('弹幕颜色', style: TextStyle(fontSize: 15)), + const SizedBox(width: 16), + _buildColorPanel, + ], + ), + ], + ), + ), + ); + } + + Widget _buildColorItem(Color color) { + return GestureDetector( + onTap: () { + if (_color.value != color) { + _color.value = color; + } + }, + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: _color.value != color + ? null + : Border.all( + width: 2, + color: Theme.of(context).colorScheme.primary, + ), + ), + child: Container( + margin: const EdgeInsets.all(2), + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(6), + ), + ), + ), + ); + } + + Widget _buildPositionItem(int mode, String title) { + return Obx( + () => Expanded( + child: GestureDetector( + onTap: () { + if (_mode.value != mode) { + _mode.value = mode; + } + }, + child: Container( + alignment: Alignment.center, + decoration: BoxDecoration( + color: _mode.value == mode + ? Theme.of(context).colorScheme.secondaryContainer + : Theme.of(context).colorScheme.onInverseSurface, + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.symmetric(vertical: 5), + child: Text( + title, + style: TextStyle( + color: _mode.value == mode + ? Theme.of(context).colorScheme.onSecondaryContainer + : Theme.of(context).colorScheme.outline, + ), + ), + ), + ), + ), + ); + } + + Widget _buildFontSizeItem(int fontsize, String title) { + return Obx( + () => Expanded( + child: GestureDetector( + onTap: () { + if (_fontsize.value != fontsize) { + _fontsize.value = fontsize; + } + }, + child: Container( + alignment: Alignment.center, + decoration: BoxDecoration( + color: _fontsize.value == fontsize + ? Theme.of(context).colorScheme.secondaryContainer + : Theme.of(context).colorScheme.onInverseSurface, + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.symmetric(vertical: 5), + child: Text( + title, + style: TextStyle( + color: _fontsize.value == fontsize + ? Theme.of(context).colorScheme.onSecondaryContainer + : Theme.of(context).colorScheme.outline, + ), + ), + ), + ), + ), + ); + } + + Widget _buildInputView() { + return Container( + clipBehavior: Clip.hardEdge, + margin: EdgeInsets.only(top: MediaQuery.of(context).padding.top), + padding: const EdgeInsets.only(left: 16, top: 2, right: 16), + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + ), + color: Theme.of(context).colorScheme.surface, + ), + child: Row( + children: [ + Obx( + () => ToolbarIconButton( + tooltip: '弹幕样式', + onPressed: () { + if (_selectKeyboard.value) { + _selectKeyboard.value = false; + updatePanelType(PanelType.emoji); + } else { + _selectKeyboard.value = true; + updatePanelType(PanelType.keyboard); + } + }, + icon: const Icon(Icons.text_format, size: 22), + selected: _selectKeyboard.value.not, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Form( + autovalidateMode: AutovalidateMode.onUserInteraction, + child: Listener( + onPointerUp: (event) { + if (_readOnly.value) { + updatePanelType(PanelType.keyboard); + _selectKeyboard.value = true; + } + }, + child: Obx( + () => TextField( + controller: _textController, + autofocus: false, + readOnly: _readOnly.value, + inputFormatters: [ + LengthLimitingTextInputFormatter(100), + ], + onChanged: (value) { + bool isEmpty = value.trim().isEmpty; + if (!isEmpty && !_enablePublish.value) { + _enablePublish.value = true; + } else if (isEmpty && _enablePublish.value) { + _enablePublish.value = false; + } + widget.onSaveDanmaku?.call(value); + }, + textInputAction: TextInputAction.send, + onSubmitted: (value) { + if (value.trim().isNotEmpty) { + onSendDanmaku(); + } + }, + focusNode: _focusNode, + decoration: const InputDecoration( + hintText: "输入弹幕内容", + border: InputBorder.none, + hintStyle: TextStyle(fontSize: 15), + ), + style: Theme.of(context).textTheme.bodyLarge, + ), + ), + ), + ), + ), + const SizedBox(width: 12), + Obx( + () => ToolbarIconButton( + tooltip: '发送', + selected: _enablePublish.value, + onPressed: _enablePublish.value ? onSendDanmaku : null, + icon: const Icon(size: 22, Icons.send), + ), + ) + ], + ), + ); + } + + updatePanelType(PanelType type) async { + final isSwitchToKeyboard = PanelType.keyboard == type; + final isSwitchToEmojiPanel = PanelType.emoji == type; + bool isUpdated = false; + switch (type) { + case PanelType.keyboard: + updateInputView(isReadOnly: false); + break; + case PanelType.emoji: + isUpdated = updateInputView(isReadOnly: true); + break; + default: + break; + } + + updatePanelTypeFunc() { + _controller.updatePanelType( + isSwitchToKeyboard + ? ChatBottomPanelType.keyboard + : ChatBottomPanelType.other, + data: type, + forceHandleFocus: isSwitchToEmojiPanel + ? ChatBottomHandleFocus.requestFocus + : ChatBottomHandleFocus.none, + ); + } + + if (isUpdated) { + // Waiting for the input view to update. + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + updatePanelTypeFunc(); + }); + } else { + updatePanelTypeFunc(); + } + } + + hidePanel() async { + if (_focusNode.hasFocus) { + await Future.delayed(const Duration(milliseconds: 100)); + _focusNode.unfocus(); + } + updateInputView(isReadOnly: false); + if (ChatBottomPanelType.none == _controller.currentPanelType) return; + _controller.updatePanelType(ChatBottomPanelType.none); + } + + bool updateInputView({ + required bool isReadOnly, + }) { + if (_readOnly.value != isReadOnly) { + _readOnly.value = isReadOnly; + return true; + } + return false; + } + + Future onSendDanmaku() async { + SmartDialog.showLoading(msg: '发送中...'); + final dynamic res = await DanmakaHttp.shootDanmaku( + oid: widget.cid, + bvid: widget.bvid, + progress: widget.progress, + msg: _textController.text, + mode: _mode.value, + fontsize: _fontsize.value, + color: _color.value.value & 0xFFFFFF, + ); + SmartDialog.dismiss(); + if (res['status']) { + Get.back(); + SmartDialog.showToast('发送成功'); + widget.callback( + DanmakuContentItem( + _textController.text, + color: Colors.white, + type: DanmakuItemType.scroll, + selfSend: true, + ), + ); + } else { + SmartDialog.showToast('发送失败: ${res['msg']}'); + } + } + + void _showColorPicker() { + _controller.keepChatPanel(); + showDialog( + context: context, + builder: (context) => AlertDialog( + clipBehavior: Clip.hardEdge, + contentPadding: const EdgeInsets.symmetric(vertical: 16), + title: Text.rich( + TextSpan( + children: [ + TextSpan( + text: 'Color Picker ', + style: TextStyle(fontSize: 15), + ), + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Container( + height: MediaQuery.textScalerOf(context).scale(13), + width: MediaQuery.textScalerOf(context).scale(13), + alignment: Alignment.center, + child: Container( + height: 10, + width: 10, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: _color.value, + ), + ), + ), + style: TextStyle(fontSize: 13), + ), + ], + ), + ), + content: SlideColorPicker( + color: _color.value, + callback: (Color? color) { + if (color != null) { + _color.value = color; + } + }, + ), + ), + ); + } +}