diff --git a/lib/models/common/sponsor_block/action_type.dart b/lib/models/common/sponsor_block/action_type.dart new file mode 100644 index 000000000..0d275b9cb --- /dev/null +++ b/lib/models/common/sponsor_block/action_type.dart @@ -0,0 +1,5 @@ +enum ActionType { skip, mute, full, poi } + +extension ActionTypeExt on ActionType { + String get title => ['跳过', '静音', '整个视频', '精彩时刻'][index]; +} diff --git a/lib/models/common/sponsor_block/post_segment_model.dart b/lib/models/common/sponsor_block/post_segment_model.dart new file mode 100644 index 000000000..5b83bd2c6 --- /dev/null +++ b/lib/models/common/sponsor_block/post_segment_model.dart @@ -0,0 +1,14 @@ +import 'package:PiliPlus/common/widgets/pair.dart'; +import 'package:PiliPlus/models/common/sponsor_block/action_type.dart'; +import 'package:PiliPlus/models/common/sponsor_block/segment_type.dart'; + +class PostSegmentModel { + PostSegmentModel({ + required this.segment, + required this.category, + required this.actionType, + }); + Pair segment; + SegmentType category; + ActionType actionType; +} diff --git a/lib/models/common/sponsor_block/segment_model.dart b/lib/models/common/sponsor_block/segment_model.dart new file mode 100644 index 000000000..6155b0c12 --- /dev/null +++ b/lib/models/common/sponsor_block/segment_model.dart @@ -0,0 +1,20 @@ +import 'package:PiliPlus/common/widgets/pair.dart'; +import 'package:PiliPlus/models/common/sponsor_block/segment_type.dart'; +import 'package:PiliPlus/models/common/sponsor_block/skip_type.dart'; + +class SegmentModel { + SegmentModel({ + // ignore: non_constant_identifier_names + required this.UUID, + required this.segmentType, + required this.segment, + required this.skipType, + this.hasSkipped, + }); + // ignore: non_constant_identifier_names + String UUID; + SegmentType segmentType; + Pair segment; + SkipType skipType; + bool? hasSkipped; +} diff --git a/lib/models/common/sponsor_block/segment_type.dart b/lib/models/common/sponsor_block/segment_type.dart new file mode 100644 index 000000000..7dbd2152f --- /dev/null +++ b/lib/models/common/sponsor_block/segment_type.dart @@ -0,0 +1,69 @@ +import 'dart:ui'; + +enum SegmentType { + sponsor, + selfpromo, + interaction, + intro, + outro, + preview, + music_offtopic, + poi_highlight, + filler, + exclusive_access +} + +extension SegmentTypeExt on SegmentType { + /// from https://github.com/hanydd/BilibiliSponsorBlock/blob/master/public/_locales/zh_CN/messages.json + String get title => [ + '赞助广告', //sponsor + '无偿/自我推广', //selfpromo + '三连/订阅提醒', //interaction + '过场/开场动画', //intro + '鸣谢/结束画面', //outro + '回顾/概要', //preview + '音乐:非音乐部分', //music_offtopic + '精彩时刻/重点', //poi_highlight + '离题闲聊/玩笑', //filler + '柔性推广/品牌合作', //exclusive_access + ][index]; + + String get shortTitle => [ + '赞助广告', //sponsor + '推广', //selfpromo + '订阅提醒', //interaction + '开场', //intro + '片尾', //outro + '预览', //preview + '非音乐', //music_offtopic + '精彩时刻', //poi_highlight + '闲聊', //filler + '品牌合作', //exclusive_access + ][index]; + + String get description => [ + '付费推广、付费推荐和直接广告。不是自我推广或免费提及他们喜欢的商品/创作者/网站/产品。', //sponsor + '类似于 “赞助广告” ,但无报酬或是自我推广。包括有关商品、捐赠的部分或合作者的信息。', //selfpromo + '视频中间简短提醒观众来一键三连或关注。 如果片段较长,或是有具体内容,则应分类为自我推广。', //interaction + '没有实际内容的间隔片段。可以是暂停、静态帧或重复动画。不适用于包含内容的过场。', //intro + '致谢画面或片尾画面。不包含内容的结尾。', //outro + '展示此视频或同系列视频将出现的画面集锦,片段中所有内容都将在之后的正片中再次出现。', //preview + '仅用于音乐视频。此分类只能用于音乐视频中未包括于其他分类的部分。', //music_offtopic + '大部分人都在寻找的空降时间。类似于“封面在12:34”的评论。', //poi_highlight + "仅作为填充内容或增添趣味而添加的离题片段,这些内容对理解视频的主要内容并非必需。这不包括提供背景信息或上下文的片段。这是一个非常激进的分类,适用于当你不想看'娱乐性'内容的时候。", //filler + '仅用于对整个视频进行标记。适用于展示UP主免费或获得补贴后使用的产品、服务或场地的视频。', //exclusive_access + ][index]; + + Color get color => [ + Color(0xFF00d400), //sponsor + Color(0xFFffff00), //selfpromo + Color(0xFFcc00ff), //interaction + Color(0xFF00ffff), //intro + Color(0xFF0202ed), //outro + Color(0xFF008fd6), //preview + Color(0xFFff9900), //music_offtopic + Color(0xFFff1684), //poi_highlight + Color(0xFF7300FF), //filler + Color(0xFF008a5c), //exclusive_access + ][index]; +} diff --git a/lib/models/common/sponsor_block/skip_type.dart b/lib/models/common/sponsor_block/skip_type.dart new file mode 100644 index 000000000..52c65708b --- /dev/null +++ b/lib/models/common/sponsor_block/skip_type.dart @@ -0,0 +1,5 @@ +enum SkipType { alwaysSkip, skipOnce, skipManually, showOnly, disable } + +extension SkipTypeExt on SkipType { + String get title => ['总是跳过', '跳过一次', '手动跳过', '仅显示', '禁用'][index]; +} diff --git a/lib/pages/bangumi/introduction/controller.dart b/lib/pages/bangumi/introduction/controller.dart index 8e45fa2fa..dbe7dc0ab 100644 --- a/lib/pages/bangumi/introduction/controller.dart +++ b/lib/pages/bangumi/introduction/controller.dart @@ -5,6 +5,7 @@ import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/user.dart'; import 'package:PiliPlus/pages/common/common_controller.dart'; import 'package:PiliPlus/pages/video/detail/introduction/controller.dart'; +import 'package:PiliPlus/pages/video/detail/introduction/pay_coins_page.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; diff --git a/lib/pages/dynamics/create_dyn_panel.dart b/lib/pages/dynamics/create_dyn_panel.dart new file mode 100644 index 000000000..ee49e279a --- /dev/null +++ b/lib/pages/dynamics/create_dyn_panel.dart @@ -0,0 +1,527 @@ +import 'dart:io'; + +import 'package:PiliPlus/http/msg.dart'; +import 'package:PiliPlus/pages/dynamics/view.dart'; +import 'package:PiliPlus/utils/storage.dart'; +import 'package:easy_debounce/easy_throttle.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:intl/intl.dart'; + +class CreateDynPanel extends StatefulWidget { + const CreateDynPanel({super.key}); + + @override + State createState() => _CreateDynPanelState(); +} + +class _CreateDynPanelState extends State { + final _ctr = TextEditingController(); + late final _imagePicker = ImagePicker(); + late final int _limit = 18; + + final RxBool _isEnablePub = false.obs; + late final RxList _pathList = [].obs; + + bool _isPrivate = false; + DateTime? _publishTime; + ReplyOption _replyOption = ReplyOption.allow; + + @override + void dispose() { + _ctr.dispose(); + super.dispose(); + } + + Future _onCreate() async { + // if (_pathList.isEmpty) { + // dynamic result = await MsgHttp.createTextDynamic(_ctr.text); + // if (result['status']) { + // Get.back(); + // SmartDialog.showToast('发布成功'); + // } else { + // SmartDialog.showToast(result['msg']); + // } + // } else { + List? pics; + if (_pathList.isNotEmpty) { + pics = []; + for (int i = 0; i < _pathList.length; i++) { + SmartDialog.showLoading(msg: '正在上传图片: ${i + 1}/${_pathList.length}'); + dynamic result = await MsgHttp.uploadBfs( + path: _pathList[i], + category: 'daily', + biz: 'new_dyn', + ); + if (result['status']) { + int imageSize = await File(_pathList[i]).length(); + pics.add({ + 'img_width': result['data']['image_width'], + 'img_height': result['data']['image_height'], + 'img_size': imageSize / 1024, + 'img_src': result['data']['image_url'], + }); + } else { + SmartDialog.dismiss(); + SmartDialog.showToast(result['msg']); + return; + } + if (i == _pathList.length - 1) { + SmartDialog.dismiss(); + } + } + } + SmartDialog.showLoading(msg: '正在发布'); + dynamic result = await MsgHttp.createDynamic( + mid: GStorage.userInfo.get('userInfoCache')?.mid, + rawText: _ctr.text, + pics: pics, + publishTime: _publishTime != null + ? _publishTime!.millisecondsSinceEpoch ~/ 1000 + : null, + replyOption: _replyOption, + privatePub: _isPrivate ? 1 : null, + ); + if (result['status']) { + Get.back(); + SmartDialog.dismiss(); + SmartDialog.showToast('发布成功'); + } else { + SmartDialog.dismiss(); + SmartDialog.showToast(result['msg']); + } + // } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.transparent, + resizeToAvoidBottomInset: true, + appBar: PreferredSize( + preferredSize: Size.fromHeight(66), + child: Padding( + padding: const EdgeInsets.all(16), + child: Stack( + children: [ + Positioned( + top: 0, + left: 0, + bottom: 0, + child: SizedBox( + width: 34, + height: 34, + child: IconButton( + tooltip: '返回', + style: ButtonStyle( + padding: WidgetStateProperty.all(EdgeInsets.zero), + backgroundColor: WidgetStateProperty.resolveWith( + (states) { + return Theme.of(context) + .colorScheme + .secondaryContainer; + }, + ), + ), + onPressed: Get.back, + icon: Icon( + Icons.arrow_back_outlined, + size: 18, + color: Theme.of(context).colorScheme.onSecondaryContainer, + ), + ), + ), + ), + Center( + child: const Text( + '发布动态', + style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold), + ), + ), + Positioned( + top: 0, + right: 0, + child: Obx( + () => FilledButton.tonal( + onPressed: _isEnablePub.value ? _onCreate : null, + style: FilledButton.styleFrom( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 10, + ), + visualDensity: const VisualDensity( + horizontal: -2, + vertical: -2, + ), + ), + child: Text(_publishTime == null ? '发布' : '定时发布'), + ), + ), + ), + ], + ), + ), + ), + body: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: TextField( + controller: _ctr, + minLines: 4, + maxLines: 8, + autofocus: true, + onChanged: (value) { + bool isEmpty = value.trim().isEmpty && _pathList.isEmpty; + if (!isEmpty && !_isEnablePub.value) { + _isEnablePub.value = true; + } else if (isEmpty && _isEnablePub.value) { + _isEnablePub.value = false; + } + }, + decoration: const InputDecoration( + hintText: '说点什么吧', + border: OutlineInputBorder( + borderSide: BorderSide.none, + gapPadding: 0, + ), + contentPadding: EdgeInsets.zero, + ), + ), + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _publishTime == null + ? FilledButton.tonal( + style: FilledButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 10, + ), + visualDensity: const VisualDensity( + horizontal: -2, + vertical: -2, + ), + ), + onPressed: _isPrivate + ? null + : () { + DateTime nowDate = DateTime.now(); + showDatePicker( + context: context, + initialDate: nowDate, + firstDate: nowDate, + lastDate: DateTime( + nowDate.year, + nowDate.month, + nowDate.day + 7, + ), + ).then( + (selectedDate) { + if (selectedDate != null && + context.mounted) { + TimeOfDay nowTime = TimeOfDay.now(); + showTimePicker( + context: context, + initialTime: nowTime.replacing( + hour: nowTime.minute + 6 >= 60 + ? (nowTime.hour + 1) % 24 + : nowTime.hour, + minute: (nowTime.minute + 6) % 60, + ), + ).then((selectedTime) { + if (selectedTime != null) { + if (selectedDate.day == + nowDate.day) { + if (selectedTime.hour < + nowTime.hour) { + SmartDialog.showToast( + '时间设置错误,至少选择6分钟之后'); + return; + } else if (selectedTime.hour == + nowTime.hour) { + if (selectedTime.minute < + nowTime.minute + 6) { + if (selectedDate.day == + nowDate.day) { + SmartDialog.showToast( + '时间设置错误,至少选择6分钟之后'); + } + return; + } + } + } + setState(() { + _publishTime = DateTime( + selectedDate.year, + selectedDate.month, + selectedDate.day, + selectedTime.hour, + selectedTime.minute, + ); + }); + } + }); + } + }, + ); + }, + child: const Text('定时发布'), + ) + : OutlinedButton.icon( + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 10, + ), + visualDensity: const VisualDensity( + horizontal: -2, + vertical: -2, + ), + ), + onPressed: () { + setState(() { + _publishTime = null; + }); + }, + label: Text(DateFormat('yyyy-MM-dd HH:mm') + .format(_publishTime!)), + icon: Icon(Icons.clear, size: 20), + iconAlignment: IconAlignment.end, + ), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + PopupMenuButton( + initialValue: _replyOption, + onSelected: (item) { + setState(() { + _replyOption = item; + }); + }, + itemBuilder: (context) => ReplyOption.values + .map( + (item) => PopupMenuItem( + value: item, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + size: 19, + item.iconData, + ), + const SizedBox(width: 4), + Text(item.title), + ], + ), + ), + ) + .toList(), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + size: 19, + _replyOption.iconData, + color: _replyOption == ReplyOption.close + ? Theme.of(context).colorScheme.error + : Theme.of(context).colorScheme.secondary, + ), + const SizedBox(width: 4), + Text( + _replyOption.title, + style: TextStyle( + height: 1, + color: _replyOption == ReplyOption.close + ? Theme.of(context).colorScheme.error + : Theme.of(context).colorScheme.secondary, + ), + strutStyle: StrutStyle(leading: 0, height: 1), + ), + Icon( + size: 20, + Icons.keyboard_arrow_right, + color: _replyOption == ReplyOption.close + ? Theme.of(context).colorScheme.error + : Theme.of(context).colorScheme.secondary, + ), + ], + ), + ), + ), + const SizedBox(height: 5), + PopupMenuButton( + initialValue: _isPrivate, + onSelected: (value) { + setState(() { + _isPrivate = value; + }); + }, + itemBuilder: (context) => List.generate( + 2, + (index) => PopupMenuItem( + enabled: _publishTime != null && index == 1 + ? false + : true, + value: index == 0 ? false : true, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + size: 19, + index == 0 + ? Icons.visibility + : Icons.visibility_off, + ), + const SizedBox(width: 4), + Text(index == 0 ? '所有人可见' : '仅自己可见'), + ], + ), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + size: 19, + _isPrivate + ? Icons.visibility_off + : Icons.visibility, + color: _isPrivate + ? Theme.of(context).colorScheme.error + : Theme.of(context).colorScheme.secondary, + ), + const SizedBox(width: 4), + Text( + _isPrivate ? '仅自己可见' : '所有人可见', + style: TextStyle( + height: 1, + color: _isPrivate + ? Theme.of(context).colorScheme.error + : Theme.of(context).colorScheme.secondary, + ), + strutStyle: StrutStyle(leading: 0, height: 1), + ), + Icon( + size: 20, + Icons.keyboard_arrow_right, + color: _isPrivate + ? Theme.of(context).colorScheme.error + : Theme.of(context).colorScheme.secondary, + ), + ], + ), + ), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 10), + Obx( + () => SizedBox( + height: 100, + child: ListView.separated( + scrollDirection: Axis.horizontal, + physics: const AlwaysScrollableScrollPhysics( + parent: BouncingScrollPhysics(), + ), + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: _pathList.length == _limit + ? _limit + : _pathList.length + 1, + itemBuilder: (context, index) { + if (_pathList.length != _limit && + index == _pathList.length) { + return Material( + borderRadius: BorderRadius.circular(12), + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: () { + EasyThrottle.throttle('imagePicker', + const Duration(milliseconds: 500), () async { + try { + List pickedFiles = + await _imagePicker.pickMultiImage( + limit: _limit, + imageQuality: 100, + ); + if (pickedFiles.isNotEmpty) { + for (int i = 0; i < pickedFiles.length; i++) { + if (_pathList.length == _limit) { + SmartDialog.showToast('最多选择$_limit张图片'); + break; + } else { + _pathList.add(pickedFiles[i].path); + } + } + if (_pathList.isNotEmpty && + !_isEnablePub.value) { + _isEnablePub.value = true; + } + } + } catch (e) { + SmartDialog.showToast(e.toString()); + } + }); + }, + child: Ink( + width: 100, + height: 100, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Theme.of(context) + .colorScheme + .secondaryContainer, + ), + child: Center(child: Icon(Icons.add, size: 35)), + ), + ), + ); + } else { + return GestureDetector( + onTap: () { + _pathList.removeAt(index); + if (_pathList.isEmpty && _ctr.text.trim().isEmpty) { + _isEnablePub.value = false; + } + }, + child: Image( + height: 100, + fit: BoxFit.fitHeight, + filterQuality: FilterQuality.low, + image: FileImage(File(_pathList[index])), + ), + ); + } + }, + separatorBuilder: (context, index) => + const SizedBox(width: 10), + ), + ), + ), + SizedBox( + height: MediaQuery.paddingOf(context).bottom + 25, + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/dynamics/repost_dyn_panel.dart b/lib/pages/dynamics/repost_dyn_panel.dart new file mode 100644 index 000000000..5ed74667e --- /dev/null +++ b/lib/pages/dynamics/repost_dyn_panel.dart @@ -0,0 +1,303 @@ +import 'package:PiliPlus/common/widgets/network_img_layer.dart'; +import 'package:PiliPlus/http/msg.dart'; +import 'package:PiliPlus/models/dynamics/result.dart'; +import 'package:PiliPlus/utils/storage.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; + +class RepostPanel extends StatefulWidget { + const RepostPanel({ + super.key, + required this.item, + required this.callback, + }); + + final dynamic item; + final Function callback; + + @override + State createState() => _RepostPanelState(); +} + +class _RepostPanelState extends State { + bool _isMax = false; + + final _ctr = TextEditingController(); + final _focusNode = FocusNode(); + + Future _onRepost() async { + dynamic result = await MsgHttp.createDynamic( + mid: GStorage.userInfo.get('userInfoCache')?.mid, + dynIdStr: widget.item.idStr, + rawText: _ctr.text, + ); + if (result['status']) { + Get.back(); + SmartDialog.showToast('转发成功'); + widget.callback(); + } else { + SmartDialog.showToast(result['msg']); + } + } + + @override + void dispose() { + _ctr.dispose(); + _focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + dynamic pic = (widget.item as DynamicItemModel?) + ?.modules + ?.moduleDynamic + ?.major + ?.archive + ?.cover ?? + (widget.item as DynamicItemModel?) + ?.modules + ?.moduleDynamic + ?.major + ?.pgc + ?.cover ?? + (widget.item as DynamicItemModel?) + ?.modules + ?.moduleDynamic + ?.major + ?.opus + ?.pics + ?.firstOrNull + ?.url; + return AnimatedSize( + alignment: Alignment.topCenter, + curve: Curves.ease, + duration: const Duration(milliseconds: 300), + child: Column( + mainAxisSize: _isMax ? MainAxisSize.max : MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: _isMax ? 16 : 10), + if (!_isMax) + Row( + children: [ + const SizedBox(width: 16), + const Text( + '转发动态', + style: TextStyle(fontWeight: FontWeight.bold), + ), + const Spacer(), + TextButton( + onPressed: _onRepost, + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 10), + visualDensity: const VisualDensity( + horizontal: -2, + vertical: -2, + ), + ), + child: const Text('立即转发'), + ), + const SizedBox(width: 16), + ], + ), + if (_isMax) + SizedBox( + height: 34, + child: Stack( + children: [ + Positioned( + left: 16, + top: 0, + child: SizedBox( + width: 34, + height: 34, + child: IconButton( + tooltip: '返回', + style: ButtonStyle( + padding: WidgetStateProperty.all(EdgeInsets.zero), + backgroundColor: + WidgetStateProperty.resolveWith((states) { + return Theme.of(context) + .colorScheme + .secondaryContainer; + }), + ), + onPressed: Get.back, + icon: Icon( + Icons.arrow_back_outlined, + size: 18, + color: Theme.of(context) + .colorScheme + .onSecondaryContainer, + ), + ), + ), + ), + Center( + child: const Text( + '转发动态', + style: + TextStyle(fontSize: 15, fontWeight: FontWeight.bold), + ), + ), + Positioned( + right: 16, + top: 0, + child: FilledButton.tonal( + onPressed: _onRepost, + style: FilledButton.styleFrom( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 10, + ), + visualDensity: const VisualDensity( + horizontal: -2, + vertical: -2, + ), + ), + child: const Text('转发'), + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Container( + width: double.infinity, + decoration: !_isMax + ? BoxDecoration( + border: Border( + left: BorderSide( + width: 2, + color: Theme.of(context).colorScheme.primary, + ), + ), + ) + : null, + child: !_isMax + ? GestureDetector( + onTap: () async { + setState(() => _isMax = true); + await Future.delayed(const Duration(milliseconds: 300)); + if (mounted && context.mounted) { + _focusNode.requestFocus(); + } + }, + child: Text( + '说点什么吧', + style: TextStyle( + height: 1.75, + fontSize: 15, + color: Theme.of(context).colorScheme.outline, + ), + ), + ) + : TextField( + controller: _ctr, + minLines: 4, + maxLines: 8, + focusNode: _focusNode, + decoration: const InputDecoration( + hintText: '说点什么吧', + border: OutlineInputBorder( + borderSide: BorderSide.none, + gapPadding: 0, + ), + contentPadding: EdgeInsets.symmetric(vertical: 10), + ), + ), + ), + ), + const SizedBox(height: 10), + Container( + padding: const EdgeInsets.all(10), + margin: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHigh == + Theme.of(context).colorScheme.surface + ? Theme.of(context).colorScheme.onInverseSurface + : Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + if (pic != null) ...[ + NetworkImgLayer( + radius: 8, + width: 40, + height: 40, + src: pic, + ), + const SizedBox(width: 10), + ], + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '@${(widget.item as DynamicItemModel?)?.modules?.moduleAuthor?.name}', + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontSize: 13, + ), + ), + Text( + (widget.item as DynamicItemModel?) + ?.modules + ?.moduleDynamic + ?.major + ?.opus + ?.summary + ?.text ?? + (widget.item as DynamicItemModel?) + ?.modules + ?.moduleDynamic + ?.desc + ?.text ?? + (widget.item as DynamicItemModel?) + ?.modules + ?.moduleDynamic + ?.major + ?.archive + ?.title ?? + (widget.item as DynamicItemModel?) + ?.modules + ?.moduleDynamic + ?.major + ?.pgc + ?.title ?? + '', + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 10), + if (!_isMax) + ListTile( + dense: true, + onTap: Get.back, + title: Center( + child: Text( + '取消', + style: + TextStyle(color: Theme.of(context).colorScheme.outline), + ), + ), + ), + SizedBox(height: 10 + MediaQuery.of(context).padding.bottom), + ], + ), + ); + } +} diff --git a/lib/pages/dynamics/view.dart b/lib/pages/dynamics/view.dart index bb8e58591..17535839e 100644 --- a/lib/pages/dynamics/view.dart +++ b/lib/pages/dynamics/view.dart @@ -1,18 +1,12 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:PiliPlus/http/msg.dart'; import 'package:PiliPlus/models/common/dynamics_type.dart'; import 'package:PiliPlus/models/common/up_panel_position.dart'; +import 'package:PiliPlus/pages/dynamics/create_dyn_panel.dart'; import 'package:PiliPlus/pages/dynamics/tab/controller.dart'; import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:PiliPlus/utils/feed_back.dart'; import 'package:PiliPlus/utils/storage.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:intl/intl.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'controller.dart'; @@ -65,7 +59,7 @@ class _DynamicsPageState extends State context: context, useSafeArea: true, isScrollControlled: true, - builder: (context) => const CreatePanel(), + builder: (context) => const CreateDynPanel(), ); } }, @@ -198,519 +192,3 @@ class _DynamicsPageState extends State ])); } } - -class CreatePanel extends StatefulWidget { - const CreatePanel({super.key}); - - @override - State createState() => _CreatePanelState(); -} - -class _CreatePanelState extends State { - final _ctr = TextEditingController(); - late final _imagePicker = ImagePicker(); - late final int _limit = 18; - - final RxBool _isEnablePub = false.obs; - late final RxList _pathList = [].obs; - - bool _isPrivate = false; - DateTime? _publishTime; - ReplyOption _replyOption = ReplyOption.allow; - - @override - void dispose() { - _ctr.dispose(); - super.dispose(); - } - - Future _onCreate() async { - // if (_pathList.isEmpty) { - // dynamic result = await MsgHttp.createTextDynamic(_ctr.text); - // if (result['status']) { - // Get.back(); - // SmartDialog.showToast('发布成功'); - // } else { - // SmartDialog.showToast(result['msg']); - // } - // } else { - List? pics; - if (_pathList.isNotEmpty) { - pics = []; - for (int i = 0; i < _pathList.length; i++) { - SmartDialog.showLoading(msg: '正在上传图片: ${i + 1}/${_pathList.length}'); - dynamic result = await MsgHttp.uploadBfs( - path: _pathList[i], - category: 'daily', - biz: 'new_dyn', - ); - if (result['status']) { - int imageSize = await File(_pathList[i]).length(); - pics.add({ - 'img_width': result['data']['image_width'], - 'img_height': result['data']['image_height'], - 'img_size': imageSize / 1024, - 'img_src': result['data']['image_url'], - }); - } else { - SmartDialog.dismiss(); - SmartDialog.showToast(result['msg']); - return; - } - if (i == _pathList.length - 1) { - SmartDialog.dismiss(); - } - } - } - SmartDialog.showLoading(msg: '正在发布'); - dynamic result = await MsgHttp.createDynamic( - mid: GStorage.userInfo.get('userInfoCache')?.mid, - rawText: _ctr.text, - pics: pics, - publishTime: _publishTime != null - ? _publishTime!.millisecondsSinceEpoch ~/ 1000 - : null, - replyOption: _replyOption, - privatePub: _isPrivate ? 1 : null, - ); - if (result['status']) { - Get.back(); - SmartDialog.dismiss(); - SmartDialog.showToast('发布成功'); - } else { - SmartDialog.dismiss(); - SmartDialog.showToast(result['msg']); - } - // } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.transparent, - resizeToAvoidBottomInset: true, - appBar: PreferredSize( - preferredSize: Size.fromHeight(66), - child: Padding( - padding: const EdgeInsets.all(16), - child: Stack( - children: [ - Positioned( - top: 0, - left: 0, - bottom: 0, - child: SizedBox( - width: 34, - height: 34, - child: IconButton( - tooltip: '返回', - style: ButtonStyle( - padding: WidgetStateProperty.all(EdgeInsets.zero), - backgroundColor: WidgetStateProperty.resolveWith( - (states) { - return Theme.of(context) - .colorScheme - .secondaryContainer; - }, - ), - ), - onPressed: Get.back, - icon: Icon( - Icons.arrow_back_outlined, - size: 18, - color: Theme.of(context).colorScheme.onSecondaryContainer, - ), - ), - ), - ), - Center( - child: const Text( - '发布动态', - style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold), - ), - ), - Positioned( - top: 0, - right: 0, - child: Obx( - () => FilledButton.tonal( - onPressed: _isEnablePub.value ? _onCreate : null, - style: FilledButton.styleFrom( - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 10, - ), - visualDensity: const VisualDensity( - horizontal: -2, - vertical: -2, - ), - ), - child: Text(_publishTime == null ? '发布' : '定时发布'), - ), - ), - ), - ], - ), - ), - ), - body: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: TextField( - controller: _ctr, - minLines: 4, - maxLines: 8, - autofocus: true, - onChanged: (value) { - bool isEmpty = value.trim().isEmpty && _pathList.isEmpty; - if (!isEmpty && !_isEnablePub.value) { - _isEnablePub.value = true; - } else if (isEmpty && _isEnablePub.value) { - _isEnablePub.value = false; - } - }, - decoration: const InputDecoration( - hintText: '说点什么吧', - border: OutlineInputBorder( - borderSide: BorderSide.none, - gapPadding: 0, - ), - contentPadding: EdgeInsets.zero, - ), - ), - ), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _publishTime == null - ? FilledButton.tonal( - style: FilledButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 10, - ), - visualDensity: const VisualDensity( - horizontal: -2, - vertical: -2, - ), - ), - onPressed: _isPrivate - ? null - : () { - DateTime nowDate = DateTime.now(); - showDatePicker( - context: context, - initialDate: nowDate, - firstDate: nowDate, - lastDate: DateTime( - nowDate.year, - nowDate.month, - nowDate.day + 7, - ), - ).then( - (selectedDate) { - if (selectedDate != null && - context.mounted) { - TimeOfDay nowTime = TimeOfDay.now(); - showTimePicker( - context: context, - initialTime: nowTime.replacing( - hour: nowTime.minute + 6 >= 60 - ? (nowTime.hour + 1) % 24 - : nowTime.hour, - minute: (nowTime.minute + 6) % 60, - ), - ).then((selectedTime) { - if (selectedTime != null) { - if (selectedDate.day == - nowDate.day) { - if (selectedTime.hour < - nowTime.hour) { - SmartDialog.showToast( - '时间设置错误,至少选择6分钟之后'); - return; - } else if (selectedTime.hour == - nowTime.hour) { - if (selectedTime.minute < - nowTime.minute + 6) { - if (selectedDate.day == - nowDate.day) { - SmartDialog.showToast( - '时间设置错误,至少选择6分钟之后'); - } - return; - } - } - } - setState(() { - _publishTime = DateTime( - selectedDate.year, - selectedDate.month, - selectedDate.day, - selectedTime.hour, - selectedTime.minute, - ); - }); - } - }); - } - }, - ); - }, - child: const Text('定时发布'), - ) - : OutlinedButton.icon( - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 10, - ), - visualDensity: const VisualDensity( - horizontal: -2, - vertical: -2, - ), - ), - onPressed: () { - setState(() { - _publishTime = null; - }); - }, - label: Text(DateFormat('yyyy-MM-dd HH:mm') - .format(_publishTime!)), - icon: Icon(Icons.clear, size: 20), - iconAlignment: IconAlignment.end, - ), - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - PopupMenuButton( - initialValue: _replyOption, - onSelected: (item) { - setState(() { - _replyOption = item; - }); - }, - itemBuilder: (context) => ReplyOption.values - .map( - (item) => PopupMenuItem( - value: item, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - size: 19, - item.iconData, - ), - const SizedBox(width: 4), - Text(item.title), - ], - ), - ), - ) - .toList(), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 2), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - size: 19, - _replyOption.iconData, - color: _replyOption == ReplyOption.close - ? Theme.of(context).colorScheme.error - : Theme.of(context).colorScheme.secondary, - ), - const SizedBox(width: 4), - Text( - _replyOption.title, - style: TextStyle( - height: 1, - color: _replyOption == ReplyOption.close - ? Theme.of(context).colorScheme.error - : Theme.of(context).colorScheme.secondary, - ), - strutStyle: StrutStyle(leading: 0, height: 1), - ), - Icon( - size: 20, - Icons.keyboard_arrow_right, - color: _replyOption == ReplyOption.close - ? Theme.of(context).colorScheme.error - : Theme.of(context).colorScheme.secondary, - ), - ], - ), - ), - ), - const SizedBox(height: 5), - PopupMenuButton( - initialValue: _isPrivate, - onSelected: (value) { - setState(() { - _isPrivate = value; - }); - }, - itemBuilder: (context) => List.generate( - 2, - (index) => PopupMenuItem( - enabled: _publishTime != null && index == 1 - ? false - : true, - value: index == 0 ? false : true, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - size: 19, - index == 0 - ? Icons.visibility - : Icons.visibility_off, - ), - const SizedBox(width: 4), - Text(index == 0 ? '所有人可见' : '仅自己可见'), - ], - ), - ), - ), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 2), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - size: 19, - _isPrivate - ? Icons.visibility_off - : Icons.visibility, - color: _isPrivate - ? Theme.of(context).colorScheme.error - : Theme.of(context).colorScheme.secondary, - ), - const SizedBox(width: 4), - Text( - _isPrivate ? '仅自己可见' : '所有人可见', - style: TextStyle( - height: 1, - color: _isPrivate - ? Theme.of(context).colorScheme.error - : Theme.of(context).colorScheme.secondary, - ), - strutStyle: StrutStyle(leading: 0, height: 1), - ), - Icon( - size: 20, - Icons.keyboard_arrow_right, - color: _isPrivate - ? Theme.of(context).colorScheme.error - : Theme.of(context).colorScheme.secondary, - ), - ], - ), - ), - ), - ], - ), - ], - ), - ), - const SizedBox(height: 10), - Obx( - () => SizedBox( - height: 100, - child: ListView.separated( - scrollDirection: Axis.horizontal, - physics: const AlwaysScrollableScrollPhysics( - parent: BouncingScrollPhysics(), - ), - padding: const EdgeInsets.symmetric(horizontal: 16), - itemCount: _pathList.length == _limit - ? _limit - : _pathList.length + 1, - itemBuilder: (context, index) { - if (_pathList.length != _limit && - index == _pathList.length) { - return Material( - borderRadius: BorderRadius.circular(12), - child: InkWell( - borderRadius: BorderRadius.circular(12), - onTap: () { - EasyThrottle.throttle('imagePicker', - const Duration(milliseconds: 500), () async { - try { - List pickedFiles = - await _imagePicker.pickMultiImage( - limit: _limit, - imageQuality: 100, - ); - if (pickedFiles.isNotEmpty) { - for (int i = 0; i < pickedFiles.length; i++) { - if (_pathList.length == _limit) { - SmartDialog.showToast('最多选择$_limit张图片'); - break; - } else { - _pathList.add(pickedFiles[i].path); - } - } - if (_pathList.isNotEmpty && - !_isEnablePub.value) { - _isEnablePub.value = true; - } - } - } catch (e) { - SmartDialog.showToast(e.toString()); - } - }); - }, - child: Ink( - width: 100, - height: 100, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: Theme.of(context) - .colorScheme - .secondaryContainer, - ), - child: Center(child: Icon(Icons.add, size: 35)), - ), - ), - ); - } else { - return GestureDetector( - onTap: () { - _pathList.removeAt(index); - if (_pathList.isEmpty && _ctr.text.trim().isEmpty) { - _isEnablePub.value = false; - } - }, - child: Image( - height: 100, - fit: BoxFit.fitHeight, - filterQuality: FilterQuality.low, - image: FileImage(File(_pathList[index])), - ), - ); - } - }, - separatorBuilder: (context, index) => - const SizedBox(width: 10), - ), - ), - ), - SizedBox( - height: MediaQuery.paddingOf(context).bottom + 25, - ), - ], - ), - ), - ); - } -} diff --git a/lib/pages/dynamics/widgets/action_panel.dart b/lib/pages/dynamics/widgets/action_panel.dart index 2fe40d9c7..d7b6b50d1 100644 --- a/lib/pages/dynamics/widgets/action_panel.dart +++ b/lib/pages/dynamics/widgets/action_panel.dart @@ -1,12 +1,9 @@ -import 'package:PiliPlus/common/widgets/network_img_layer.dart'; -import 'package:PiliPlus/http/msg.dart'; +import 'package:PiliPlus/pages/dynamics/repost_dyn_panel.dart'; import 'package:PiliPlus/utils/extension.dart'; -import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:get/get.dart'; import 'package:PiliPlus/http/dynamics.dart'; import 'package:PiliPlus/models/dynamics/result.dart'; import 'package:PiliPlus/utils/feed_back.dart'; @@ -176,299 +173,3 @@ class _ActionPanelState extends State { ); } } - -class RepostPanel extends StatefulWidget { - const RepostPanel({ - super.key, - required this.item, - required this.callback, - }); - - final dynamic item; - final Function callback; - - @override - State createState() => _RepostPanelState(); -} - -class _RepostPanelState extends State { - bool _isMax = false; - - final _ctr = TextEditingController(); - final _focusNode = FocusNode(); - - Future _onRepost() async { - dynamic result = await MsgHttp.createDynamic( - mid: GStorage.userInfo.get('userInfoCache')?.mid, - dynIdStr: widget.item.idStr, - rawText: _ctr.text, - ); - if (result['status']) { - Get.back(); - SmartDialog.showToast('转发成功'); - widget.callback(); - } else { - SmartDialog.showToast(result['msg']); - } - } - - @override - void dispose() { - _ctr.dispose(); - _focusNode.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - dynamic pic = (widget.item as DynamicItemModel?) - ?.modules - ?.moduleDynamic - ?.major - ?.archive - ?.cover ?? - (widget.item as DynamicItemModel?) - ?.modules - ?.moduleDynamic - ?.major - ?.pgc - ?.cover ?? - (widget.item as DynamicItemModel?) - ?.modules - ?.moduleDynamic - ?.major - ?.opus - ?.pics - ?.firstOrNull - ?.url; - return AnimatedSize( - alignment: Alignment.topCenter, - curve: Curves.ease, - duration: const Duration(milliseconds: 300), - child: Column( - mainAxisSize: _isMax ? MainAxisSize.max : MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: _isMax ? 16 : 10), - if (!_isMax) - Row( - children: [ - const SizedBox(width: 16), - const Text( - '转发动态', - style: TextStyle(fontWeight: FontWeight.bold), - ), - const Spacer(), - TextButton( - onPressed: _onRepost, - style: TextButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: 20, vertical: 10), - visualDensity: const VisualDensity( - horizontal: -2, - vertical: -2, - ), - ), - child: const Text('立即转发'), - ), - const SizedBox(width: 16), - ], - ), - if (_isMax) - SizedBox( - height: 34, - child: Stack( - children: [ - Positioned( - left: 16, - top: 0, - child: SizedBox( - width: 34, - height: 34, - child: IconButton( - tooltip: '返回', - style: ButtonStyle( - padding: WidgetStateProperty.all(EdgeInsets.zero), - backgroundColor: - WidgetStateProperty.resolveWith((states) { - return Theme.of(context) - .colorScheme - .secondaryContainer; - }), - ), - onPressed: Get.back, - icon: Icon( - Icons.arrow_back_outlined, - size: 18, - color: Theme.of(context) - .colorScheme - .onSecondaryContainer, - ), - ), - ), - ), - Center( - child: const Text( - '转发动态', - style: - TextStyle(fontSize: 15, fontWeight: FontWeight.bold), - ), - ), - Positioned( - right: 16, - top: 0, - child: FilledButton.tonal( - onPressed: _onRepost, - style: FilledButton.styleFrom( - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 10, - ), - visualDensity: const VisualDensity( - horizontal: -2, - vertical: -2, - ), - ), - child: const Text('转发'), - ), - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Container( - width: double.infinity, - decoration: !_isMax - ? BoxDecoration( - border: Border( - left: BorderSide( - width: 2, - color: Theme.of(context).colorScheme.primary, - ), - ), - ) - : null, - child: !_isMax - ? GestureDetector( - onTap: () async { - setState(() => _isMax = true); - await Future.delayed(const Duration(milliseconds: 300)); - if (mounted && context.mounted) { - _focusNode.requestFocus(); - } - }, - child: Text( - '说点什么吧', - style: TextStyle( - height: 1.75, - fontSize: 15, - color: Theme.of(context).colorScheme.outline, - ), - ), - ) - : TextField( - controller: _ctr, - minLines: 4, - maxLines: 8, - focusNode: _focusNode, - decoration: const InputDecoration( - hintText: '说点什么吧', - border: OutlineInputBorder( - borderSide: BorderSide.none, - gapPadding: 0, - ), - contentPadding: EdgeInsets.symmetric(vertical: 10), - ), - ), - ), - ), - const SizedBox(height: 10), - Container( - padding: const EdgeInsets.all(10), - margin: const EdgeInsets.symmetric(horizontal: 16), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHigh == - Theme.of(context).colorScheme.surface - ? Theme.of(context).colorScheme.onInverseSurface - : Theme.of(context).colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(12), - ), - child: Row( - children: [ - if (pic != null) ...[ - NetworkImgLayer( - radius: 8, - width: 40, - height: 40, - src: pic, - ), - const SizedBox(width: 10), - ], - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '@${(widget.item as DynamicItemModel?)?.modules?.moduleAuthor?.name}', - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - fontSize: 13, - ), - ), - Text( - (widget.item as DynamicItemModel?) - ?.modules - ?.moduleDynamic - ?.major - ?.opus - ?.summary - ?.text ?? - (widget.item as DynamicItemModel?) - ?.modules - ?.moduleDynamic - ?.desc - ?.text ?? - (widget.item as DynamicItemModel?) - ?.modules - ?.moduleDynamic - ?.major - ?.archive - ?.title ?? - (widget.item as DynamicItemModel?) - ?.modules - ?.moduleDynamic - ?.major - ?.pgc - ?.title ?? - '', - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - ], - ), - ), - const SizedBox(height: 10), - if (!_isMax) - ListTile( - dense: true, - onTap: Get.back, - title: Center( - child: Text( - '取消', - style: - TextStyle(color: Theme.of(context).colorScheme.outline), - ), - ), - ), - SizedBox(height: 10 + MediaQuery.of(context).padding.bottom), - ], - ), - ); - } -} diff --git a/lib/pages/setting/slide_color_picker.dart b/lib/pages/setting/slide_color_picker.dart new file mode 100644 index 000000000..d8c713b00 --- /dev/null +++ b/lib/pages/setting/slide_color_picker.dart @@ -0,0 +1,179 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; + +class SlideColorPicker extends StatefulWidget { + const SlideColorPicker({ + super.key, + required this.color, + required this.callback, + this.showResetBtn, + }); + + final Color color; + final Function(Color? color) callback; + final bool? showResetBtn; + + @override + State createState() => _SlideColorPickerState(); +} + +class _SlideColorPickerState extends State { + late int _r; + late int _g; + late int _b; + late final TextEditingController _textController; + + @override + void initState() { + super.initState(); + _r = widget.color.red; + _g = widget.color.green; + _b = widget.color.blue; + _textController = TextEditingController(text: _convert); + } + + @override + void dispose() { + _textController.dispose(); + super.dispose(); + } + + String get _convert => Color.fromARGB(255, _r, _g, _b) + .value + .toRadixString(16) + .substring(2) + .toUpperCase(); + + Widget _slider({ + required String title, + required int value, + required ValueChanged onChanged, + }) { + return Row( + children: [ + const SizedBox(width: 16), + Text(title), + Expanded( + child: Slider( + min: 0, + max: 255, + divisions: 255, + value: value.toDouble(), + onChanged: onChanged, + ), + ), + Text( + value.toString(), + ), + const SizedBox(width: 16), + ], + ); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + height: 100, + color: Color.fromARGB(255, _r, _g, _b), + ), + const SizedBox(height: 10), + IntrinsicWidth( + child: TextField( + inputFormatters: [ + LengthLimitingTextInputFormatter(6), + FilteringTextInputFormatter.allow(RegExp('[0-9a-fA-F]')), + ], + controller: _textController, + decoration: InputDecoration( + isDense: true, + prefixText: '#', + contentPadding: const EdgeInsets.all(0), + ), + onChanged: (value) { + _textController.text = value.toUpperCase(); + if (value.length == 6) { + Color color = + Color(int.tryParse('FF$value', radix: 16) ?? 0xFF000000); + setState(() { + _r = color.red; + _g = color.green; + _b = color.blue; + }); + } + }, + ), + ), + _slider( + title: 'R', + value: _r, + onChanged: (value) { + setState(() { + _r = value.round(); + _textController.text = _convert; + }); + }, + ), + _slider( + title: 'G', + value: _g, + onChanged: (value) { + setState(() { + _g = value.round(); + _textController.text = _convert; + }); + }, + ), + _slider( + title: 'B', + value: _b, + onChanged: (value) { + setState(() { + _b = value.round(); + _textController.text = _convert; + }); + }, + ), + Row( + children: [ + if (widget.showResetBtn != false) ...[ + const SizedBox(width: 16), + TextButton( + onPressed: () { + Get.back(); + widget.callback(null); + }, + child: Text( + '重置', + ), + ), + ], + const Spacer(), + TextButton( + onPressed: Get.back, + child: Text( + '取消', + style: TextStyle( + color: Theme.of(context).colorScheme.outline, + ), + ), + ), + TextButton( + onPressed: () { + Get.back(); + widget.callback(Color.fromARGB(255, _r, _g, _b)); + }, + child: const Text('确定'), + ), + const SizedBox(width: 16), + ], + ) + ], + ), + ); + } +} diff --git a/lib/pages/setting/sponsor_block_page.dart b/lib/pages/setting/sponsor_block_page.dart index fadb25ef8..16f23776c 100644 --- a/lib/pages/setting/sponsor_block_page.dart +++ b/lib/pages/setting/sponsor_block_page.dart @@ -3,8 +3,9 @@ import 'dart:math'; import 'package:PiliPlus/common/widgets/pair.dart'; import 'package:PiliPlus/http/constants.dart'; import 'package:PiliPlus/http/index.dart'; -import 'package:PiliPlus/pages/video/detail/controller.dart' - show SegmentType, SegmentTypeExt, SkipType, SkipTypeExt; +import 'package:PiliPlus/models/common/sponsor_block/segment_type.dart'; +import 'package:PiliPlus/models/common/sponsor_block/skip_type.dart'; +import 'package:PiliPlus/pages/setting/slide_color_picker.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:dio/dio.dart'; @@ -566,179 +567,3 @@ class _SponsorBlockPageState extends State { ); } } - -class SlideColorPicker extends StatefulWidget { - const SlideColorPicker({ - super.key, - required this.color, - required this.callback, - this.showResetBtn, - }); - - final Color color; - final Function(Color? color) callback; - final bool? showResetBtn; - - @override - State createState() => _SlideColorPickerState(); -} - -class _SlideColorPickerState extends State { - late int _r; - late int _g; - late int _b; - late final TextEditingController _textController; - - @override - void initState() { - super.initState(); - _r = widget.color.red; - _g = widget.color.green; - _b = widget.color.blue; - _textController = TextEditingController(text: _convert); - } - - @override - void dispose() { - _textController.dispose(); - super.dispose(); - } - - String get _convert => Color.fromARGB(255, _r, _g, _b) - .value - .toRadixString(16) - .substring(2) - .toUpperCase(); - - Widget _slider({ - required String title, - required int value, - required ValueChanged onChanged, - }) { - return Row( - children: [ - const SizedBox(width: 16), - Text(title), - Expanded( - child: Slider( - min: 0, - max: 255, - divisions: 255, - value: value.toDouble(), - onChanged: onChanged, - ), - ), - Text( - value.toString(), - ), - const SizedBox(width: 16), - ], - ); - } - - @override - Widget build(BuildContext context) { - return SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - height: 100, - color: Color.fromARGB(255, _r, _g, _b), - ), - const SizedBox(height: 10), - IntrinsicWidth( - child: TextField( - inputFormatters: [ - LengthLimitingTextInputFormatter(6), - FilteringTextInputFormatter.allow(RegExp('[0-9a-fA-F]')), - ], - controller: _textController, - decoration: InputDecoration( - isDense: true, - prefixText: '#', - contentPadding: const EdgeInsets.all(0), - ), - onChanged: (value) { - _textController.text = value.toUpperCase(); - if (value.length == 6) { - Color color = - Color(int.tryParse('FF$value', radix: 16) ?? 0xFF000000); - setState(() { - _r = color.red; - _g = color.green; - _b = color.blue; - }); - } - }, - ), - ), - _slider( - title: 'R', - value: _r, - onChanged: (value) { - setState(() { - _r = value.round(); - _textController.text = _convert; - }); - }, - ), - _slider( - title: 'G', - value: _g, - onChanged: (value) { - setState(() { - _g = value.round(); - _textController.text = _convert; - }); - }, - ), - _slider( - title: 'B', - value: _b, - onChanged: (value) { - setState(() { - _b = value.round(); - _textController.text = _convert; - }); - }, - ), - Row( - children: [ - if (widget.showResetBtn != false) ...[ - const SizedBox(width: 16), - TextButton( - onPressed: () { - Get.back(); - widget.callback(null); - }, - child: Text( - '重置', - ), - ), - ], - const Spacer(), - TextButton( - onPressed: Get.back, - child: Text( - '取消', - style: TextStyle( - color: Theme.of(context).colorScheme.outline, - ), - ), - ), - TextButton( - onPressed: () { - Get.back(); - widget.callback(Color.fromARGB(255, _r, _g, _b)); - }, - child: const Text('确定'), - ), - const SizedBox(width: 16), - ], - ) - ], - ), - ); - } -} diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart index 2a205ba2a..0d89c04f3 100644 --- a/lib/pages/video/detail/controller.dart +++ b/lib/pages/video/detail/controller.dart @@ -9,6 +9,11 @@ import 'package:PiliPlus/common/widgets/segment_progress_bar.dart'; import 'package:PiliPlus/http/init.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/user.dart'; +import 'package:PiliPlus/models/common/sponsor_block/action_type.dart'; +import 'package:PiliPlus/models/common/sponsor_block/post_segment_model.dart'; +import 'package:PiliPlus/models/common/sponsor_block/segment_model.dart'; +import 'package:PiliPlus/models/common/sponsor_block/segment_type.dart'; +import 'package:PiliPlus/models/common/sponsor_block/skip_type.dart'; import 'package:PiliPlus/models/video/later.dart'; import 'package:PiliPlus/models/video/play/subtitle.dart'; import 'package:PiliPlus/models/video_detail_res.dart'; @@ -43,114 +48,6 @@ import 'package:material_design_icons_flutter/material_design_icons_flutter.dart import '../../../utils/id_utils.dart'; import 'widgets/header_control.dart'; -enum SegmentType { - sponsor, - selfpromo, - interaction, - intro, - outro, - preview, - music_offtopic, - poi_highlight, - filler, - exclusive_access -} - -extension SegmentTypeExt on SegmentType { - /// from https://github.com/hanydd/BilibiliSponsorBlock/blob/master/public/_locales/zh_CN/messages.json - String get title => [ - '赞助广告', //sponsor - '无偿/自我推广', //selfpromo - '三连/订阅提醒', //interaction - '过场/开场动画', //intro - '鸣谢/结束画面', //outro - '回顾/概要', //preview - '音乐:非音乐部分', //music_offtopic - '精彩时刻/重点', //poi_highlight - '离题闲聊/玩笑', //filler - '柔性推广/品牌合作', //exclusive_access - ][index]; - - String get shortTitle => [ - '赞助广告', //sponsor - '推广', //selfpromo - '订阅提醒', //interaction - '开场', //intro - '片尾', //outro - '预览', //preview - '非音乐', //music_offtopic - '精彩时刻', //poi_highlight - '闲聊', //filler - '品牌合作', //exclusive_access - ][index]; - - String get description => [ - '付费推广、付费推荐和直接广告。不是自我推广或免费提及他们喜欢的商品/创作者/网站/产品。', //sponsor - '类似于 “赞助广告” ,但无报酬或是自我推广。包括有关商品、捐赠的部分或合作者的信息。', //selfpromo - '视频中间简短提醒观众来一键三连或关注。 如果片段较长,或是有具体内容,则应分类为自我推广。', //interaction - '没有实际内容的间隔片段。可以是暂停、静态帧或重复动画。不适用于包含内容的过场。', //intro - '致谢画面或片尾画面。不包含内容的结尾。', //outro - '展示此视频或同系列视频将出现的画面集锦,片段中所有内容都将在之后的正片中再次出现。', //preview - '仅用于音乐视频。此分类只能用于音乐视频中未包括于其他分类的部分。', //music_offtopic - '大部分人都在寻找的空降时间。类似于“封面在12:34”的评论。', //poi_highlight - "仅作为填充内容或增添趣味而添加的离题片段,这些内容对理解视频的主要内容并非必需。这不包括提供背景信息或上下文的片段。这是一个非常激进的分类,适用于当你不想看'娱乐性'内容的时候。", //filler - '仅用于对整个视频进行标记。适用于展示UP主免费或获得补贴后使用的产品、服务或场地的视频。', //exclusive_access - ][index]; - - Color get color => [ - Color(0xFF00d400), //sponsor - Color(0xFFffff00), //selfpromo - Color(0xFFcc00ff), //interaction - Color(0xFF00ffff), //intro - Color(0xFF0202ed), //outro - Color(0xFF008fd6), //preview - Color(0xFFff9900), //music_offtopic - Color(0xFFff1684), //poi_highlight - Color(0xFF7300FF), //filler - Color(0xFF008a5c), //exclusive_access - ][index]; -} - -enum SkipType { alwaysSkip, skipOnce, skipManually, showOnly, disable } - -extension SkipTypeExt on SkipType { - String get title => ['总是跳过', '跳过一次', '手动跳过', '仅显示', '禁用'][index]; -} - -class SegmentModel { - SegmentModel({ - // ignore: non_constant_identifier_names - required this.UUID, - required this.segmentType, - required this.segment, - required this.skipType, - this.hasSkipped, - }); - // ignore: non_constant_identifier_names - String UUID; - SegmentType segmentType; - Pair segment; - SkipType skipType; - bool? hasSkipped; -} - -class PostSegmentModel { - PostSegmentModel({ - required this.segment, - required this.category, - required this.actionType, - }); - Pair segment; - SegmentType category; - ActionType actionType; -} - -enum ActionType { skip, mute, full, poi } - -extension ActionTypeExt on ActionType { - String get title => ['跳过', '静音', '整个视频', '精彩时刻'][index]; -} - class VideoDetailController extends GetxController with GetSingleTickerProviderStateMixin { /// 路由传参 diff --git a/lib/pages/video/detail/introduction/controller.dart b/lib/pages/video/detail/introduction/controller.dart index 60572c280..da609421b 100644 --- a/lib/pages/video/detail/introduction/controller.dart +++ b/lib/pages/video/detail/introduction/controller.dart @@ -1,7 +1,7 @@ import 'dart:async'; -import 'dart:math'; import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/pages/video/detail/introduction/pay_coins_page.dart'; import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:expandable/expandable.dart'; @@ -846,328 +846,3 @@ class VideoIntroController extends GetxController return res; } } - -class PayCoinsPage extends StatefulWidget { - const PayCoinsPage({ - super.key, - required this.callback, - this.copyright = 1, - }); - - final Function callback; - final int copyright; - - @override - State createState() => _PayCoinsPageState(); -} - -class _PayCoinsPageState extends State - with TickerProviderStateMixin { - bool _isPaying = false; - late final _controller = PageController(viewportFraction: 0.30); - - int get _index => _controller.hasClients ? _controller.page?.round() ?? 0 : 0; - - late AnimationController _slide22Controller; - late AnimationController _scale22Controller; - late AnimationController _coinSlideController; - late AnimationController _coinFadeController; - late AnimationController _boxAnimController; - - final List _images = [ - 'assets/images/paycoins/ic_thunder_1.png', - 'assets/images/paycoins/ic_thunder_2.png', - 'assets/images/paycoins/ic_thunder_3.png', - ]; - late int _imageIndex = -1; - Timer? _timer; - bool get _showThunder => _imageIndex != -1 && _imageIndex != _images.length; - - @override - void initState() { - super.initState(); - _slide22Controller = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 50), - ); - _scale22Controller = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 50), - ); - _coinSlideController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 200), - ); - _coinFadeController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 100), - ); - _boxAnimController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 50), - ); - _scale(); - } - - @override - void dispose() { - _timer?.cancel(); - _slide22Controller.dispose(); - _scale22Controller.dispose(); - _coinSlideController.dispose(); - _coinFadeController.dispose(); - _boxAnimController.dispose(); - _controller.dispose(); - super.dispose(); - } - - void _scale() { - _scale22Controller.forward().whenComplete(() { - _scale22Controller.reverse(); - }); - } - - void _onScroll(int index) { - _controller.animateToPage( - index, - duration: const Duration(milliseconds: 200), - curve: Curves.ease, - ); - _scale(); - } - - @override - Widget build(BuildContext context) { - return LayoutBuilder(builder: (context, constraints) { - return _buildBody(constraints.maxHeight > constraints.maxWidth); - }); - } - - Widget _buildBody(isV) => Stack( - alignment: Alignment.center, - children: [ - Visibility( - visible: _showThunder, - maintainSize: true, - maintainAnimation: true, - maintainState: true, - child: Image.asset(_images[_showThunder ? _imageIndex : 0]), - ), - Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Row( - children: [ - Visibility( - visible: !_isPaying && widget.copyright == 1, - maintainSize: true, - maintainAnimation: true, - maintainState: true, - child: GestureDetector( - onTap: _index == 0 - ? null - : () { - _onScroll(0); - }, - child: Padding( - padding: const EdgeInsets.only(left: 12), - child: Image.asset( - width: 16, - height: 28, - _index == 0 - ? 'assets/images/paycoins/ic_left_disable.png' - : 'assets/images/paycoins/ic_left.png', - ), - ), - ), - ), - Expanded( - child: SizedBox( - height: 100, - child: PageView.builder( - itemCount: widget.copyright == 1 ? 2 : 1, - controller: _controller, - onPageChanged: (index) => setState(() { - _scale(); - }), - itemBuilder: (context, index) { - return ListenableBuilder( - listenable: _controller, - builder: (context, child) { - double factor = index == 0 ? 1 : 0; - if (_controller.position.hasContentDimensions) { - factor = 1 - (_controller.page! - index).abs(); - } - return Visibility( - visible: !_isPaying || _index == index, - child: Center( - child: SizedBox( - height: 70 + (factor * 30), - width: 70 + (factor * 30), - child: Stack( - alignment: Alignment.center, - children: [ - SlideTransition( - position: _boxAnimController.drive( - Tween( - begin: const Offset(0.0, 0.0), - end: const Offset(0.0, -0.2), - ), - ), - child: Image.asset( - 'assets/images/paycoins/ic_pay_coins_box.png', - ), - ), - SlideTransition( - position: _coinSlideController.drive( - Tween( - begin: const Offset(0.0, 0.0), - end: const Offset(0.0, -2), - ), - ), - child: FadeTransition( - opacity: Tween( - begin: 1, end: 0) - .animate(_coinFadeController), - child: Image.asset( - height: 35 + (factor * 15), - width: 35 + (factor * 15), - index == 0 - ? 'assets/images/paycoins/ic_coins_one.png' - : 'assets/images/paycoins/ic_coins_two.png', - ), - ), - ), - ], - ), - ), - ), - ); - }, - ); - }, - ), - ), - ), - Visibility( - visible: !_isPaying && widget.copyright == 1, - maintainSize: true, - maintainAnimation: true, - maintainState: true, - child: GestureDetector( - onTap: _index == 1 - ? null - : () { - _onScroll(1); - }, - child: Padding( - padding: const EdgeInsets.only(right: 12), - child: Image.asset( - width: 16, - height: 28, - _index == 1 - ? 'assets/images/paycoins/ic_right_disable.png' - : 'assets/images/paycoins/ic_right.png', - ), - ), - ), - ), - ], - ), - const SizedBox(height: 25), - GestureDetector( - behavior: HitTestBehavior.opaque, - onPanUpdate: _handlePanUpdate, - child: SizedBox( - width: double.infinity, - height: 140, - child: Center( - child: GestureDetector( - onTap: _onPayCoin, - onPanUpdate: (e) => _handlePanUpdate(e, true), - child: ScaleTransition( - scale: _scale22Controller.drive( - Tween(begin: 1, end: 1.2), - ), - child: SlideTransition( - position: _slide22Controller.drive( - Tween( - begin: const Offset(0.0, 0.0), - end: const Offset(0.0, -0.2), - ), - ), - child: SizedBox( - width: 100, - height: 140, - child: Image.asset( - _index == 0 - ? 'assets/images/paycoins/ic_22_mario.png' - : 'assets/images/paycoins/ic_22_gun_sister.png', - ), - ), - ), - ), - ), - ), - ), - ), - SizedBox( - height: - (isV ? 50 : 0) + MediaQuery.of(context).padding.bottom), - ], - ), - ], - ); - - void _handlePanUpdate(DragUpdateDetails e, [bool needV = false]) { - if (needV && e.delta.dy.abs() > max(2, e.delta.dx.abs())) { - if (e.delta.dy < 0) { - _onPayCoin(); - } - } else if (widget.copyright == 1 && - e.delta.dx.abs() > max(2, e.delta.dy.abs())) { - if (e.delta.dx > 0) { - if (_index == 1) { - _onScroll(0); - setState(() {}); - } - } else { - if (_index == 0) { - _onScroll(1); - setState(() {}); - } - } - } - } - - void _onPayCoin() { - if (_isPaying) return; - setState(() { - _isPaying = true; - }); - _slide22Controller.forward().whenComplete(() { - _slide22Controller.reverse().whenComplete(() { - if (_index == 1) { - _timer ??= Timer.periodic(const Duration(milliseconds: 50 ~/ 3), (_) { - if (_imageIndex != _images.length) { - setState(() { - _imageIndex = _imageIndex + 1; - }); - } else { - _timer?.cancel(); - } - }); - } - _boxAnimController.forward().whenComplete(() { - _boxAnimController.reverse(); - }); - _coinSlideController.forward().whenComplete(() { - _coinFadeController.forward().whenComplete(() { - Get.back(); - widget.callback(_index + 1); - }); - }); - }); - }); - } -} diff --git a/lib/pages/video/detail/introduction/pay_coins_page.dart b/lib/pages/video/detail/introduction/pay_coins_page.dart new file mode 100644 index 000000000..93c74a4fc --- /dev/null +++ b/lib/pages/video/detail/introduction/pay_coins_page.dart @@ -0,0 +1,330 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class PayCoinsPage extends StatefulWidget { + const PayCoinsPage({ + super.key, + required this.callback, + this.copyright = 1, + }); + + final Function callback; + final int copyright; + + @override + State createState() => _PayCoinsPageState(); +} + +class _PayCoinsPageState extends State + with TickerProviderStateMixin { + bool _isPaying = false; + late final _controller = PageController(viewportFraction: 0.30); + + int get _index => _controller.hasClients ? _controller.page?.round() ?? 0 : 0; + + late AnimationController _slide22Controller; + late AnimationController _scale22Controller; + late AnimationController _coinSlideController; + late AnimationController _coinFadeController; + late AnimationController _boxAnimController; + + final List _images = [ + 'assets/images/paycoins/ic_thunder_1.png', + 'assets/images/paycoins/ic_thunder_2.png', + 'assets/images/paycoins/ic_thunder_3.png', + ]; + late int _imageIndex = -1; + Timer? _timer; + bool get _showThunder => _imageIndex != -1 && _imageIndex != _images.length; + + @override + void initState() { + super.initState(); + _slide22Controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 50), + ); + _scale22Controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 50), + ); + _coinSlideController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 200), + ); + _coinFadeController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 100), + ); + _boxAnimController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 50), + ); + _scale(); + } + + @override + void dispose() { + _timer?.cancel(); + _slide22Controller.dispose(); + _scale22Controller.dispose(); + _coinSlideController.dispose(); + _coinFadeController.dispose(); + _boxAnimController.dispose(); + _controller.dispose(); + super.dispose(); + } + + void _scale() { + _scale22Controller.forward().whenComplete(() { + _scale22Controller.reverse(); + }); + } + + void _onScroll(int index) { + _controller.animateToPage( + index, + duration: const Duration(milliseconds: 200), + curve: Curves.ease, + ); + _scale(); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, constraints) { + return _buildBody(constraints.maxHeight > constraints.maxWidth); + }); + } + + Widget _buildBody(isV) => Stack( + alignment: Alignment.center, + children: [ + Visibility( + visible: _showThunder, + maintainSize: true, + maintainAnimation: true, + maintainState: true, + child: Image.asset(_images[_showThunder ? _imageIndex : 0]), + ), + Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Row( + children: [ + Visibility( + visible: !_isPaying && widget.copyright == 1, + maintainSize: true, + maintainAnimation: true, + maintainState: true, + child: GestureDetector( + onTap: _index == 0 + ? null + : () { + _onScroll(0); + }, + child: Padding( + padding: const EdgeInsets.only(left: 12), + child: Image.asset( + width: 16, + height: 28, + _index == 0 + ? 'assets/images/paycoins/ic_left_disable.png' + : 'assets/images/paycoins/ic_left.png', + ), + ), + ), + ), + Expanded( + child: SizedBox( + height: 100, + child: PageView.builder( + itemCount: widget.copyright == 1 ? 2 : 1, + controller: _controller, + onPageChanged: (index) => setState(() { + _scale(); + }), + itemBuilder: (context, index) { + return ListenableBuilder( + listenable: _controller, + builder: (context, child) { + double factor = index == 0 ? 1 : 0; + if (_controller.position.hasContentDimensions) { + factor = 1 - (_controller.page! - index).abs(); + } + return Visibility( + visible: !_isPaying || _index == index, + child: Center( + child: SizedBox( + height: 70 + (factor * 30), + width: 70 + (factor * 30), + child: Stack( + alignment: Alignment.center, + children: [ + SlideTransition( + position: _boxAnimController.drive( + Tween( + begin: const Offset(0.0, 0.0), + end: const Offset(0.0, -0.2), + ), + ), + child: Image.asset( + 'assets/images/paycoins/ic_pay_coins_box.png', + ), + ), + SlideTransition( + position: _coinSlideController.drive( + Tween( + begin: const Offset(0.0, 0.0), + end: const Offset(0.0, -2), + ), + ), + child: FadeTransition( + opacity: Tween( + begin: 1, end: 0) + .animate(_coinFadeController), + child: Image.asset( + height: 35 + (factor * 15), + width: 35 + (factor * 15), + index == 0 + ? 'assets/images/paycoins/ic_coins_one.png' + : 'assets/images/paycoins/ic_coins_two.png', + ), + ), + ), + ], + ), + ), + ), + ); + }, + ); + }, + ), + ), + ), + Visibility( + visible: !_isPaying && widget.copyright == 1, + maintainSize: true, + maintainAnimation: true, + maintainState: true, + child: GestureDetector( + onTap: _index == 1 + ? null + : () { + _onScroll(1); + }, + child: Padding( + padding: const EdgeInsets.only(right: 12), + child: Image.asset( + width: 16, + height: 28, + _index == 1 + ? 'assets/images/paycoins/ic_right_disable.png' + : 'assets/images/paycoins/ic_right.png', + ), + ), + ), + ), + ], + ), + const SizedBox(height: 25), + GestureDetector( + behavior: HitTestBehavior.opaque, + onPanUpdate: _handlePanUpdate, + child: SizedBox( + width: double.infinity, + height: 140, + child: Center( + child: GestureDetector( + onTap: _onPayCoin, + onPanUpdate: (e) => _handlePanUpdate(e, true), + child: ScaleTransition( + scale: _scale22Controller.drive( + Tween(begin: 1, end: 1.2), + ), + child: SlideTransition( + position: _slide22Controller.drive( + Tween( + begin: const Offset(0.0, 0.0), + end: const Offset(0.0, -0.2), + ), + ), + child: SizedBox( + width: 100, + height: 140, + child: Image.asset( + _index == 0 + ? 'assets/images/paycoins/ic_22_mario.png' + : 'assets/images/paycoins/ic_22_gun_sister.png', + ), + ), + ), + ), + ), + ), + ), + ), + SizedBox( + height: + (isV ? 50 : 0) + MediaQuery.of(context).padding.bottom), + ], + ), + ], + ); + + void _handlePanUpdate(DragUpdateDetails e, [bool needV = false]) { + if (needV && e.delta.dy.abs() > max(2, e.delta.dx.abs())) { + if (e.delta.dy < 0) { + _onPayCoin(); + } + } else if (widget.copyright == 1 && + e.delta.dx.abs() > max(2, e.delta.dy.abs())) { + if (e.delta.dx > 0) { + if (_index == 1) { + _onScroll(0); + setState(() {}); + } + } else { + if (_index == 0) { + _onScroll(1); + setState(() {}); + } + } + } + } + + void _onPayCoin() { + if (_isPaying) return; + setState(() { + _isPaying = true; + }); + _slide22Controller.forward().whenComplete(() { + _slide22Controller.reverse().whenComplete(() { + if (_index == 1) { + _timer ??= Timer.periodic(const Duration(milliseconds: 50 ~/ 3), (_) { + if (_imageIndex != _images.length) { + setState(() { + _imageIndex = _imageIndex + 1; + }); + } else { + _timer?.cancel(); + } + }); + } + _boxAnimController.forward().whenComplete(() { + _boxAnimController.reverse(); + }); + _coinSlideController.forward().whenComplete(() { + _coinFadeController.forward().whenComplete(() { + Get.back(); + widget.callback(_index + 1); + }); + }); + }); + }); + } +} diff --git a/lib/pages/video/detail/reply_new/view.dart b/lib/pages/video/detail/reply_new/view.dart index d0f8b75de..0541df130 100644 --- a/lib/pages/video/detail/reply_new/view.dart +++ b/lib/pages/video/detail/reply_new/view.dart @@ -8,6 +8,7 @@ import 'package:PiliPlus/models/video/reply/emote.dart'; import 'package:PiliPlus/models/video/reply/item.dart'; import 'package:PiliPlus/pages/emote/index.dart'; import 'package:PiliPlus/utils/feed_back.dart'; +import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart'; import 'toolbar_icon_button.dart'; @@ -38,7 +39,8 @@ class _VideoReplyNewDialogState extends State final GlobalKey _formKey = GlobalKey(); late double emoteHeight = 0.0; double keyboardHeight = 0.0; // 键盘高度 - final _debouncer = Debouncer(milliseconds: 200); // 设置延迟时间 + final _debouncer = + Debouncer(delay: const Duration(milliseconds: 200)); // 设置延迟时间 String toolbarType = 'input'; bool _enablePublish = false; final _publishStream = StreamController(); @@ -121,7 +123,7 @@ class _VideoReplyNewDialogState extends State // 键盘高度 final viewInsets = EdgeInsets.fromViewPadding( View.of(context).viewInsets, View.of(context).devicePixelRatio); - _debouncer.run(() { + _debouncer(() { if (!mounted) return; if (keyboardHeight == 0 && emoteHeight == 0) { emoteHeight = keyboardHeight = @@ -275,22 +277,3 @@ class _VideoReplyNewDialogState extends State ); } } - -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/video/detail/widgets/send_danmaku_panel.dart b/lib/pages/video/detail/widgets/send_danmaku_panel.dart index 7e940e56b..9a27bad28 100644 --- a/lib/pages/video/detail/widgets/send_danmaku_panel.dart +++ b/lib/pages/video/detail/widgets/send_danmaku_panel.dart @@ -3,8 +3,7 @@ import 'dart:math'; import 'package:PiliPlus/common/widgets/icon_button.dart'; import 'package:PiliPlus/http/danmaku.dart'; -import 'package:PiliPlus/pages/setting/sponsor_block_page.dart' - show SlideColorPicker; +import 'package:PiliPlus/pages/setting/slide_color_picker.dart'; import 'package:PiliPlus/pages/video/detail/reply_new/reply_page.dart' show PanelType; import 'package:PiliPlus/utils/extension.dart'; diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart index 86e2c3a9c..3ee6f6ab8 100644 --- a/lib/utils/storage.dart +++ b/lib/utils/storage.dart @@ -5,6 +5,8 @@ import 'package:PiliPlus/common/widgets/refresh_indicator.dart' show kDragContainerExtentPercentage, displacement; import 'package:PiliPlus/http/constants.dart'; import 'package:PiliPlus/models/common/dynamic_badge_mode.dart'; +import 'package:PiliPlus/models/common/sponsor_block/segment_type.dart'; +import 'package:PiliPlus/models/common/sponsor_block/skip_type.dart'; import 'package:PiliPlus/models/common/tab_type.dart'; import 'package:PiliPlus/models/common/theme_type.dart'; import 'package:PiliPlus/models/common/up_panel_position.dart'; @@ -12,8 +14,6 @@ import 'package:PiliPlus/models/video/play/CDN.dart'; import 'package:PiliPlus/models/video/play/quality.dart'; import 'package:PiliPlus/models/video/play/subtitle.dart'; import 'package:PiliPlus/pages/member/new/controller.dart' show MemberTabType; -import 'package:PiliPlus/pages/video/detail/controller.dart' - show SegmentType, SegmentTypeExt, SkipType; import 'package:PiliPlus/plugin/pl_player/models/bottom_progress_behavior.dart'; import 'package:PiliPlus/plugin/pl_player/models/fullscreen_mode.dart'; import 'package:flutter/material.dart';