mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
mod: split some pages
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
5
lib/models/common/sponsor_block/action_type.dart
Normal file
5
lib/models/common/sponsor_block/action_type.dart
Normal file
@@ -0,0 +1,5 @@
|
||||
enum ActionType { skip, mute, full, poi }
|
||||
|
||||
extension ActionTypeExt on ActionType {
|
||||
String get title => ['跳过', '静音', '整个视频', '精彩时刻'][index];
|
||||
}
|
||||
14
lib/models/common/sponsor_block/post_segment_model.dart
Normal file
14
lib/models/common/sponsor_block/post_segment_model.dart
Normal file
@@ -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<int, int> segment;
|
||||
SegmentType category;
|
||||
ActionType actionType;
|
||||
}
|
||||
20
lib/models/common/sponsor_block/segment_model.dart
Normal file
20
lib/models/common/sponsor_block/segment_model.dart
Normal file
@@ -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<int, int> segment;
|
||||
SkipType skipType;
|
||||
bool? hasSkipped;
|
||||
}
|
||||
69
lib/models/common/sponsor_block/segment_type.dart
Normal file
69
lib/models/common/sponsor_block/segment_type.dart
Normal file
@@ -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];
|
||||
}
|
||||
5
lib/models/common/sponsor_block/skip_type.dart
Normal file
5
lib/models/common/sponsor_block/skip_type.dart
Normal file
@@ -0,0 +1,5 @@
|
||||
enum SkipType { alwaysSkip, skipOnce, skipManually, showOnly, disable }
|
||||
|
||||
extension SkipTypeExt on SkipType {
|
||||
String get title => ['总是跳过', '跳过一次', '手动跳过', '仅显示', '禁用'][index];
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
527
lib/pages/dynamics/create_dyn_panel.dart
Normal file
527
lib/pages/dynamics/create_dyn_panel.dart
Normal file
@@ -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<CreateDynPanel> createState() => _CreateDynPanelState();
|
||||
}
|
||||
|
||||
class _CreateDynPanelState extends State<CreateDynPanel> {
|
||||
final _ctr = TextEditingController();
|
||||
late final _imagePicker = ImagePicker();
|
||||
late final int _limit = 18;
|
||||
|
||||
final RxBool _isEnablePub = false.obs;
|
||||
late final RxList<String> _pathList = <String>[].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<ReplyOption>(
|
||||
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<bool>(
|
||||
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<XFile> 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
303
lib/pages/dynamics/repost_dyn_panel.dart
Normal file
303
lib/pages/dynamics/repost_dyn_panel.dart
Normal file
@@ -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<RepostPanel> createState() => _RepostPanelState();
|
||||
}
|
||||
|
||||
class _RepostPanelState extends State<RepostPanel> {
|
||||
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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<DynamicsPage>
|
||||
context: context,
|
||||
useSafeArea: true,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => const CreatePanel(),
|
||||
builder: (context) => const CreateDynPanel(),
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -198,519 +192,3 @@ class _DynamicsPageState extends State<DynamicsPage>
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
class CreatePanel extends StatefulWidget {
|
||||
const CreatePanel({super.key});
|
||||
|
||||
@override
|
||||
State<CreatePanel> createState() => _CreatePanelState();
|
||||
}
|
||||
|
||||
class _CreatePanelState extends State<CreatePanel> {
|
||||
final _ctr = TextEditingController();
|
||||
late final _imagePicker = ImagePicker();
|
||||
late final int _limit = 18;
|
||||
|
||||
final RxBool _isEnablePub = false.obs;
|
||||
late final RxList<String> _pathList = <String>[].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<ReplyOption>(
|
||||
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<bool>(
|
||||
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<XFile> 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ActionPanel> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RepostPanel extends StatefulWidget {
|
||||
const RepostPanel({
|
||||
super.key,
|
||||
required this.item,
|
||||
required this.callback,
|
||||
});
|
||||
|
||||
final dynamic item;
|
||||
final Function callback;
|
||||
|
||||
@override
|
||||
State<RepostPanel> createState() => _RepostPanelState();
|
||||
}
|
||||
|
||||
class _RepostPanelState extends State<RepostPanel> {
|
||||
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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
179
lib/pages/setting/slide_color_picker.dart
Normal file
179
lib/pages/setting/slide_color_picker.dart
Normal file
@@ -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<SlideColorPicker> createState() => _SlideColorPickerState();
|
||||
}
|
||||
|
||||
class _SlideColorPickerState extends State<SlideColorPicker> {
|
||||
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<double> 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),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<SponsorBlockPage> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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<SlideColorPicker> createState() => _SlideColorPickerState();
|
||||
}
|
||||
|
||||
class _SlideColorPickerState extends State<SlideColorPicker> {
|
||||
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<double> 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),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<int, int> segment;
|
||||
SkipType skipType;
|
||||
bool? hasSkipped;
|
||||
}
|
||||
|
||||
class PostSegmentModel {
|
||||
PostSegmentModel({
|
||||
required this.segment,
|
||||
required this.category,
|
||||
required this.actionType,
|
||||
});
|
||||
Pair<int, int> 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 {
|
||||
/// 路由传参
|
||||
|
||||
@@ -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<PayCoinsPage> createState() => _PayCoinsPageState();
|
||||
}
|
||||
|
||||
class _PayCoinsPageState extends State<PayCoinsPage>
|
||||
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<double>(
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
330
lib/pages/video/detail/introduction/pay_coins_page.dart
Normal file
330
lib/pages/video/detail/introduction/pay_coins_page.dart
Normal file
@@ -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<PayCoinsPage> createState() => _PayCoinsPageState();
|
||||
}
|
||||
|
||||
class _PayCoinsPageState extends State<PayCoinsPage>
|
||||
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<double>(
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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<VideoReplyNewDialog>
|
||||
final GlobalKey _formKey = GlobalKey<FormState>();
|
||||
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<bool>();
|
||||
@@ -121,7 +123,7 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
|
||||
// 键盘高度
|
||||
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<VideoReplyNewDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
typedef DebounceCallback = void Function();
|
||||
|
||||
class Debouncer {
|
||||
DebounceCallback? callback;
|
||||
final int? milliseconds;
|
||||
Timer? _timer;
|
||||
|
||||
Debouncer({this.milliseconds});
|
||||
|
||||
run(DebounceCallback callback) {
|
||||
if (_timer != null) {
|
||||
_timer!.cancel();
|
||||
}
|
||||
_timer = Timer(Duration(milliseconds: milliseconds!), () {
|
||||
callback();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user