Compare commits
48 Commits
feature-pl
...
v1.0.22.04
Author | SHA1 | Date | |
---|---|---|---|
f598c81b53 | |||
ec7762644b | |||
5500a58c32 | |||
2ba72e3792 | |||
25c416f9bf | |||
9ec9fc2830 | |||
74f6b0ad1e | |||
3fa72f5340 | |||
c0f3b4f3a2 | |||
c6de1fa95a | |||
da9828a295 | |||
e212a32763 | |||
a20217bf39 | |||
336feb4fda | |||
53b103b853 | |||
fd54915399 | |||
157726c4c6 | |||
25272d6d1b | |||
af1163f6e0 | |||
d003f864ce | |||
dd7b226351 | |||
6c2eab86e9 | |||
d806de7d8f | |||
5cb3e578a8 | |||
8f9fbf5d41 | |||
9845f0383a | |||
fb3be848b4 | |||
7d7df17317 | |||
aae08d0688 | |||
9fe5b78cfa | |||
6b028c36af | |||
92c385ff58 | |||
463ee1d5b5 | |||
0a416c95bc | |||
3d09d80007 | |||
fffa15faa3 | |||
d6b972a8ab | |||
d6fd299395 | |||
1953653044 | |||
9faa625d52 | |||
7e7bb1f43a | |||
a925ef63eb | |||
f8326e7cb5 | |||
02d2598d01 | |||
77b509fd17 | |||
11e907d74b | |||
6298711528 | |||
0f0546ae59 |
@ -26,13 +26,13 @@
|
||||
Xcode 13.4 不支持**auto_orientation**,请注释相关代码
|
||||
|
||||
```bash
|
||||
[✓] Flutter (Channel stable, 3.16.4, on macOS 14.1.2 23B92 darwin-arm64, locale
|
||||
[✓] Flutter (Channel stable, 3.16.5, on macOS 14.1.2 23B92 darwin-arm64, locale
|
||||
zh-Hans-CN)
|
||||
[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
|
||||
[✓] Xcode - develop for iOS and macOS (Xcode 15.1)
|
||||
[✓] Chrome - develop for the web
|
||||
[✓] Android Studio (version 2022.3)
|
||||
[✓] VS Code (version 1.85.1)
|
||||
[✓] VS Code (version 1.87.2)
|
||||
[✓] Connected device (3 available)
|
||||
[✓] Network resources
|
||||
|
||||
@ -44,6 +44,9 @@ Xcode 13.4 不支持**auto_orientation**,请注释相关代码
|
||||
## 技术交流
|
||||
|
||||
Telegram: https://t.me/+lm_oOVmF0RJiODk1
|
||||
|
||||
Tg Beta版本:@PiliPala_Beta
|
||||
|
||||
QQ频道: https://pd.qq.com/s/365esodk3
|
||||
|
||||
|
||||
|
29
change_log/1.0.22.0430.md
Normal file
29
change_log/1.0.22.0430.md
Normal file
@ -0,0 +1,29 @@
|
||||
## 1.0.22
|
||||
|
||||
明天晚上或者后天晚上应该会更新
|
||||
|
||||
### 功能
|
||||
+ 字幕
|
||||
+ 全屏时选集
|
||||
+ 动态转发
|
||||
+ 评论视频并转发
|
||||
+ 收藏夹删除
|
||||
+ 合集显示封面
|
||||
+ 底部导航栏编辑、排序功能
|
||||
+ 历史记录进度条展示
|
||||
+ 直播画质切换
|
||||
+ 排行榜功能
|
||||
+ 视频详情页推荐视频开关
|
||||
+ 显示联合投稿up
|
||||
|
||||
### 修复
|
||||
+ 收藏夹个数错误
|
||||
+ 封面保存权限问题
|
||||
+ 合集最后1p未展示
|
||||
+ up主页关注按钮触发灰屏
|
||||
|
||||
### 优化
|
||||
+ 视频简介查看逻辑
|
||||
|
||||
更多更新日志可在Github上查看
|
||||
问题反馈、功能建议请查看「关于」页面。
|
@ -9,6 +9,7 @@ PODS:
|
||||
- Flutter
|
||||
- connectivity_plus (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- ReachabilitySwift
|
||||
- device_info_plus (0.0.1):
|
||||
- Flutter
|
||||
@ -38,7 +39,7 @@ PODS:
|
||||
- path_provider_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- permission_handler_apple (9.1.1):
|
||||
- permission_handler_apple (9.3.0):
|
||||
- Flutter
|
||||
- ReachabilitySwift (5.0.0)
|
||||
- saver_gallery (0.0.1):
|
||||
@ -71,7 +72,7 @@ DEPENDENCIES:
|
||||
- audio_service (from `.symlinks/plugins/audio_service/ios`)
|
||||
- audio_session (from `.symlinks/plugins/audio_session/ios`)
|
||||
- auto_orientation (from `.symlinks/plugins/auto_orientation/ios`)
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_mailer (from `.symlinks/plugins/flutter_mailer/ios`)
|
||||
@ -113,7 +114,7 @@ EXTERNAL SOURCES:
|
||||
auto_orientation:
|
||||
:path: ".symlinks/plugins/auto_orientation/ios"
|
||||
connectivity_plus:
|
||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||
:path: ".symlinks/plugins/connectivity_plus/darwin"
|
||||
device_info_plus:
|
||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||
Flutter:
|
||||
@ -166,7 +167,7 @@ SPEC CHECKSUMS:
|
||||
audio_service: f509d65da41b9521a61f1c404dd58651f265a567
|
||||
audio_session: 4f3e461722055d21515cf3261b64c973c062f345
|
||||
auto_orientation: 102ed811a5938d52c86520ddd7ecd3a126b5d39d
|
||||
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
|
||||
connectivity_plus: e2dad488011aeb593e219360e804c43cc1af5770
|
||||
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
|
||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||
flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83
|
||||
@ -180,7 +181,7 @@ SPEC CHECKSUMS:
|
||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
||||
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
|
||||
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
|
||||
permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6
|
||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||
saver_gallery: 2b4e584106fde2407ab51560f3851564963e6b78
|
||||
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
|
||||
@ -193,7 +194,7 @@ SPEC CHECKSUMS:
|
||||
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
|
||||
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
|
||||
webview_cookie_manager: eaf920722b493bd0f7611b5484771ca53fed03f7
|
||||
webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a
|
||||
webview_flutter_wkwebview: 4f3e50f7273d31e5500066ed267e3ae4309c5ae4
|
||||
|
||||
PODFILE CHECKSUM: 637cd290bed23275b5f5ffcc7eb1e73d0a5fb2be
|
||||
|
||||
|
@ -506,4 +506,6 @@ class Api {
|
||||
/// 排行榜
|
||||
static const String getRankApi = "/x/web-interface/ranking/v2";
|
||||
|
||||
/// 取消订阅
|
||||
static const String cancelSub = '/x/v3/fav/season/unfav';
|
||||
}
|
||||
|
@ -349,4 +349,21 @@ class UserHttp {
|
||||
return {'status': false, 'msg': res.data['message']};
|
||||
}
|
||||
}
|
||||
|
||||
// 取消订阅
|
||||
static Future cancelSub({required int seasonId}) async {
|
||||
var res = await Request().post(
|
||||
Api.cancelSub,
|
||||
queryParameters: {
|
||||
'platform': 'web',
|
||||
'season_id': seasonId,
|
||||
'csrf': await Request.getCsrf(),
|
||||
},
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
return {'status': true};
|
||||
} else {
|
||||
return {'status': false, 'msg': res.data['message']};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -34,6 +34,7 @@ void main() async {
|
||||
.then((_) async {
|
||||
await GStrorage.init();
|
||||
await setupServiceLocator();
|
||||
clearLogs();
|
||||
Request();
|
||||
await Request.setCookie();
|
||||
RecommendFilter();
|
||||
|
@ -47,18 +47,23 @@ class Vip {
|
||||
this.status,
|
||||
this.dueDate,
|
||||
this.label,
|
||||
this.nicknameColor,
|
||||
});
|
||||
|
||||
int? type;
|
||||
int? status;
|
||||
int? dueDate;
|
||||
Map? label;
|
||||
int? nicknameColor;
|
||||
|
||||
Vip.fromJson(Map<String, dynamic> json) {
|
||||
type = json['type'];
|
||||
status = json['status'];
|
||||
dueDate = json['due_date'];
|
||||
label = json['label'];
|
||||
nicknameColor = json['nickname_color'] == ''
|
||||
? null
|
||||
: int.parse("0xFF${json['nickname_color'].replaceAll('#', '')}");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -437,7 +437,8 @@ class SearchArticleItemModel {
|
||||
pubTime = json['pub_time'];
|
||||
like = json['like'];
|
||||
title = Em.regTitle(json['title']);
|
||||
subTitle = json['title'].replaceAll(RegExp(r'<[^>]*>'), '');
|
||||
subTitle =
|
||||
Em.decodeHtmlEntities(json['title'].replaceAll(RegExp(r'<[^>]*>'), ''));
|
||||
rankOffset = json['rank_offset'];
|
||||
mid = json['mid'];
|
||||
imageUrls = json['image_urls'];
|
||||
|
@ -185,7 +185,7 @@ class HistoryItem extends StatelessWidget {
|
||||
? '已看完'
|
||||
: '${Utils.timeFormat(videoItem.progress!)}/${Utils.timeFormat(videoItem.duration!)}',
|
||||
right: 6.0,
|
||||
bottom: 6.0,
|
||||
bottom: 8.0,
|
||||
type: 'gray',
|
||||
),
|
||||
// 右上角
|
||||
@ -258,6 +258,27 @@ class HistoryItem extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
videoItem.progress != 0
|
||||
? Positioned(
|
||||
left: 3,
|
||||
right: 3,
|
||||
bottom: 0,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(
|
||||
StyleString.imgRadius.x),
|
||||
bottomRight: Radius.circular(
|
||||
StyleString.imgRadius.x),
|
||||
),
|
||||
child: LinearProgressIndicator(
|
||||
value: videoItem.progress == -1
|
||||
? 100
|
||||
: videoItem.progress /
|
||||
videoItem.duration,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox()
|
||||
],
|
||||
),
|
||||
VideoContent(videoItem: videoItem, ctr: ctr)
|
||||
|
@ -281,8 +281,8 @@ class _MemberPageState extends State<MemberPage>
|
||||
future: _futureBuilderFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
Map data = snapshot.data!;
|
||||
if (data['status']) {
|
||||
Map? data = snapshot.data;
|
||||
if (data != null && data['status']) {
|
||||
return Obx(
|
||||
() => Stack(
|
||||
alignment: AlignmentDirectional.center,
|
||||
@ -302,7 +302,14 @@ class _MemberPageState extends State<MemberPage>
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium!
|
||||
.copyWith(fontWeight: FontWeight.bold),
|
||||
.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _memberController.memberInfo.value
|
||||
.vip!.nicknameColor !=
|
||||
null
|
||||
? Color(_memberController.memberInfo
|
||||
.value.vip!.nicknameColor!)
|
||||
: null),
|
||||
)),
|
||||
const SizedBox(width: 2),
|
||||
if (_memberController.memberInfo.value.sex == '女')
|
||||
|
@ -18,45 +18,32 @@ class MemberSeasonsPanel extends StatelessWidget {
|
||||
itemBuilder: (context, index) {
|
||||
MemberSeasonsList item = data!.seasonsList![index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12, right: 4),
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12, left: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
item.meta!.name!,
|
||||
maxLines: 1,
|
||||
style: Theme.of(context).textTheme.titleSmall!,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
PBadge(
|
||||
stack: 'relative',
|
||||
size: 'small',
|
||||
text: item.meta!.total.toString(),
|
||||
),
|
||||
const Spacer(),
|
||||
SizedBox(
|
||||
width: 35,
|
||||
height: 35,
|
||||
child: IconButton(
|
||||
onPressed: () => Get.toNamed(
|
||||
'/memberSeasons?mid=${item.meta!.mid}&seasonId=${item.meta!.seasonId}'),
|
||||
style: ButtonStyle(
|
||||
padding: MaterialStateProperty.all(EdgeInsets.zero),
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.arrow_forward,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
ListTile(
|
||||
onTap: () => Get.toNamed(
|
||||
'/memberSeasons?mid=${item.meta!.mid}&seasonId=${item.meta!.seasonId}'),
|
||||
title: Text(
|
||||
item.meta!.name!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleSmall!,
|
||||
),
|
||||
dense: true,
|
||||
leading: PBadge(
|
||||
stack: 'relative',
|
||||
size: 'small',
|
||||
text: item.meta!.total.toString(),
|
||||
),
|
||||
trailing: const Icon(
|
||||
Icons.arrow_forward,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
return GridView.builder(
|
||||
|
@ -50,21 +50,5 @@ class RankController extends GetxController with GetTickerProviderStateMixin {
|
||||
length: tabs.length,
|
||||
vsync: this,
|
||||
);
|
||||
// 监听 tabController 切换
|
||||
if (enableGradientBg) {
|
||||
tabController.animation!.addListener(() {
|
||||
if (tabController.indexIsChanging) {
|
||||
if (initialIndex.value != tabController.index) {
|
||||
initialIndex.value = tabController.index;
|
||||
}
|
||||
} else {
|
||||
final int temp = tabController.animation!.value.round();
|
||||
if (initialIndex.value != temp) {
|
||||
initialIndex.value = temp;
|
||||
tabController.index = initialIndex.value;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -44,6 +44,7 @@ class _SelectDialogState<T> extends State<SelectDialog<T>> {
|
||||
setState(() {
|
||||
_tempValue = value as T;
|
||||
});
|
||||
Navigator.pop(context, _tempValue);
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -51,19 +52,6 @@ class _SelectDialogState<T> extends State<SelectDialog<T>> {
|
||||
),
|
||||
);
|
||||
}),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, _tempValue),
|
||||
child: const Text('确定'),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -46,4 +46,40 @@ class SubController extends GetxController {
|
||||
Future onLoad() async {
|
||||
querySubFolder(type: 'onload');
|
||||
}
|
||||
|
||||
// 取消订阅
|
||||
Future<void> cancelSub(SubFolderItemData subFolderItem) async {
|
||||
showDialog(
|
||||
context: Get.context!,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('提示'),
|
||||
content: const Text('确定取消订阅吗?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
},
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
var res = await UserHttp.cancelSub(seasonId: subFolderItem.id!);
|
||||
if (res['status']) {
|
||||
subFolderData.value.list!.remove(subFolderItem);
|
||||
subFolderData.update((val) {});
|
||||
SmartDialog.showToast('取消订阅成功');
|
||||
} else {
|
||||
SmartDialog.showToast(res['msg']);
|
||||
}
|
||||
Get.back();
|
||||
},
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -58,7 +58,8 @@ class _SubPageState extends State<SubPage> {
|
||||
itemBuilder: (context, index) {
|
||||
return SubItem(
|
||||
subFolderItem:
|
||||
_subController.subFolderData.value.list![index]);
|
||||
_subController.subFolderData.value.list![index],
|
||||
cancelSub: _subController.cancelSub);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
@ -8,7 +8,12 @@ import '../../../models/user/sub_folder.dart';
|
||||
|
||||
class SubItem extends StatelessWidget {
|
||||
final SubFolderItemData subFolderItem;
|
||||
const SubItem({super.key, required this.subFolderItem});
|
||||
final Function(SubFolderItemData) cancelSub;
|
||||
const SubItem({
|
||||
super.key,
|
||||
required this.subFolderItem,
|
||||
required this.cancelSub,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -51,7 +56,10 @@ class SubItem extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
),
|
||||
VideoContent(subFolderItem: subFolderItem)
|
||||
VideoContent(
|
||||
subFolderItem: subFolderItem,
|
||||
cancelSub: cancelSub,
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -64,7 +72,8 @@ class SubItem extends StatelessWidget {
|
||||
|
||||
class VideoContent extends StatelessWidget {
|
||||
final SubFolderItemData subFolderItem;
|
||||
const VideoContent({super.key, required this.subFolderItem});
|
||||
final Function(SubFolderItemData)? cancelSub;
|
||||
const VideoContent({super.key, required this.subFolderItem, this.cancelSub});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -100,6 +109,24 @@ class VideoContent extends StatelessWidget {
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 35,
|
||||
width: 35,
|
||||
child: IconButton(
|
||||
onPressed: () => cancelSub?.call(subFolderItem),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Theme.of(context).colorScheme.outline,
|
||||
padding: const EdgeInsets.fromLTRB(0, 0, 0, 0),
|
||||
),
|
||||
icon: const Icon(Icons.delete_outline, size: 18),
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -91,7 +91,7 @@ class VideoDetailController extends GetxController
|
||||
late bool enableCDN;
|
||||
late int? cacheVideoQa;
|
||||
late String cacheDecode;
|
||||
late int cacheAudioQa;
|
||||
late int defaultAudioQa;
|
||||
|
||||
PersistentBottomSheetController? replyReplyBottomSheetCtr;
|
||||
RxList<SubTitileContentModel> subtitleContents =
|
||||
@ -146,7 +146,7 @@ class VideoDetailController extends GetxController
|
||||
// 预设的解码格式
|
||||
cacheDecode = setting.get(SettingBoxKey.defaultDecode,
|
||||
defaultValue: VideoDecodeFormats.values.last.code);
|
||||
cacheAudioQa = setting.get(SettingBoxKey.defaultAudioQa,
|
||||
defaultAudioQa = setting.get(SettingBoxKey.defaultAudioQa,
|
||||
defaultValue: AudioQuality.hiRes.code);
|
||||
oid.value = IdUtils.bv2av(Get.parameters['bvid']!);
|
||||
getSubtitle();
|
||||
@ -353,9 +353,9 @@ class VideoDetailController extends GetxController
|
||||
|
||||
if (audiosList.isNotEmpty) {
|
||||
final List<int> numbers = audiosList.map((map) => map.id!).toList();
|
||||
int closestNumber = Utils.findClosestNumber(cacheAudioQa, numbers);
|
||||
if (!numbers.contains(cacheAudioQa) &&
|
||||
numbers.any((e) => e > cacheAudioQa)) {
|
||||
int closestNumber = Utils.findClosestNumber(defaultAudioQa, numbers);
|
||||
if (!numbers.contains(defaultAudioQa) &&
|
||||
numbers.any((e) => e > defaultAudioQa)) {
|
||||
closestNumber = 30280;
|
||||
}
|
||||
firstAudio = audiosList.firstWhere((e) => e.id == closestNumber);
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:expandable/expandable.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:get/get.dart';
|
||||
@ -15,7 +16,7 @@ import 'package:pilipala/pages/video/detail/widgets/ai_detail.dart';
|
||||
import 'package:pilipala/utils/feed_back.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
import '../widgets/expandable_section.dart';
|
||||
import '../../../../http/user.dart';
|
||||
import 'widgets/action_item.dart';
|
||||
import 'widgets/fav_panel.dart';
|
||||
import 'widgets/intro_detail.dart';
|
||||
@ -139,6 +140,8 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
||||
late bool enableAi;
|
||||
bool isProcessing = false;
|
||||
RxBool isExpand = false.obs;
|
||||
late ExpandableController _expandableCtr;
|
||||
|
||||
void Function()? handleState(Future Function() action) {
|
||||
return isProcessing
|
||||
? null
|
||||
@ -162,6 +165,7 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
||||
follower = Utils.numFormat(videoIntroController.userStat['follower']);
|
||||
followStatus = videoIntroController.followStatus;
|
||||
enableAi = setting.get(SettingBoxKey.enableAi, defaultValue: true);
|
||||
_expandableCtr = ExpandableController(initialExpanded: false);
|
||||
}
|
||||
|
||||
// 收藏
|
||||
@ -215,6 +219,7 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
||||
showIntroDetail() {
|
||||
feedBack();
|
||||
isExpand.value = !(isExpand.value);
|
||||
_expandableCtr.toggle();
|
||||
}
|
||||
|
||||
// 用户主页
|
||||
@ -238,6 +243,12 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_expandableCtr.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData t = Theme.of(context);
|
||||
@ -255,14 +266,34 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => showIntroDetail(),
|
||||
child: Text(
|
||||
widget.videoDetail!.title!,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
child: ExpandablePanel(
|
||||
controller: _expandableCtr,
|
||||
collapsed: Text(
|
||||
widget.videoDetail!.title!,
|
||||
softWrap: true,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
expanded: Text(
|
||||
widget.videoDetail!.title!,
|
||||
softWrap: true,
|
||||
maxLines: 4,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
theme: const ExpandableThemeData(
|
||||
animationDuration: Duration(milliseconds: 300),
|
||||
scrollAnimationDuration: Duration(milliseconds: 300),
|
||||
crossFadePoint: 0,
|
||||
fadeCurve: Curves.ease,
|
||||
sizeCurve: Curves.linear,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Stack(
|
||||
@ -327,12 +358,16 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
||||
),
|
||||
|
||||
/// 视频简介
|
||||
Obx(
|
||||
() => ExpandedSection(
|
||||
expand: isExpand.value,
|
||||
begin: 0,
|
||||
end: 1,
|
||||
child: IntroDetail(videoDetail: widget.videoDetail!),
|
||||
ExpandablePanel(
|
||||
controller: _expandableCtr,
|
||||
collapsed: const SizedBox(height: 0),
|
||||
expanded: IntroDetail(videoDetail: widget.videoDetail!),
|
||||
theme: const ExpandableThemeData(
|
||||
animationDuration: Duration(milliseconds: 300),
|
||||
scrollAnimationDuration: Duration(milliseconds: 300),
|
||||
crossFadePoint: 0,
|
||||
fadeCurve: Curves.ease,
|
||||
sizeCurve: Curves.linear,
|
||||
),
|
||||
),
|
||||
|
||||
@ -479,7 +514,11 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
||||
),
|
||||
ActionItem(
|
||||
icon: const Icon(FontAwesomeIcons.clock),
|
||||
onTap: () => videoIntroController.actionShareVideo(),
|
||||
onTap: () async {
|
||||
final res =
|
||||
await UserHttp.toViewLater(bvid: widget.videoDetail!.bvid);
|
||||
SmartDialog.showToast(res['msg']);
|
||||
},
|
||||
selectStatus: false,
|
||||
text: '稍后看',
|
||||
),
|
||||
|
@ -149,13 +149,16 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
||||
delegate: _MySliverPersistentHeaderDelegate(
|
||||
child: Container(
|
||||
height: 40,
|
||||
padding: const EdgeInsets.fromLTRB(12, 6, 6, 0),
|
||||
padding: const EdgeInsets.fromLTRB(12, 0, 6, 0),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${_videoReplyController.sortTypeLabel.value}评论',
|
||||
style: const TextStyle(fontSize: 13),
|
||||
Obx(
|
||||
() => Text(
|
||||
'${_videoReplyController.sortTypeLabel.value}评论',
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 35,
|
||||
|
@ -525,14 +525,18 @@ InlineSpan buildContent(
|
||||
if (jumpUrlKeysList.isNotEmpty) {
|
||||
patternStr += '|${jumpUrlKeysList.join('|')}';
|
||||
}
|
||||
RegExp bv23Regex = RegExp(r'https://b23\.tv/[a-zA-Z0-9]{7}');
|
||||
final RegExp pattern = RegExp(patternStr);
|
||||
List<String> matchedStrs = [];
|
||||
void addPlainTextSpan(str) {
|
||||
spanChilds.add(TextSpan(
|
||||
spanChilds.add(
|
||||
TextSpan(
|
||||
text: str,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () =>
|
||||
replyReply?.call(replyItem.root == 0 ? replyItem : fReplyItem)));
|
||||
replyReply?.call(replyItem.root == 0 ? replyItem : fReplyItem),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 分割文本并处理每个部分
|
||||
@ -734,8 +738,36 @@ InlineSpan buildContent(
|
||||
return '';
|
||||
},
|
||||
onNonMatch: (String nonMatchStr) {
|
||||
addPlainTextSpan(nonMatchStr);
|
||||
return nonMatchStr;
|
||||
return nonMatchStr.splitMapJoin(
|
||||
bv23Regex,
|
||||
onMatch: (Match match) {
|
||||
String matchStr = match[0]!;
|
||||
spanChilds.add(
|
||||
TextSpan(
|
||||
text: ' $matchStr ',
|
||||
style: isVideoPage
|
||||
? TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
)
|
||||
: null,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => Get.toNamed(
|
||||
'/webview',
|
||||
parameters: {
|
||||
'url': matchStr,
|
||||
'type': 'url',
|
||||
'pageTitle': matchStr
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
return '';
|
||||
},
|
||||
onNonMatch: (String nonMatchOtherStr) {
|
||||
addPlainTextSpan(nonMatchOtherStr);
|
||||
return nonMatchOtherStr;
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -142,9 +142,10 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
double keyboardHeight = EdgeInsets.fromViewPadding(
|
||||
double _keyboardHeight = EdgeInsets.fromViewPadding(
|
||||
View.of(context).viewInsets, View.of(context).devicePixelRatio)
|
||||
.bottom;
|
||||
print('_keyboardHeight: $_keyboardHeight');
|
||||
return Container(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
@ -235,7 +236,11 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: toolbarType == 'input' ? keyboardHeight : emoteHeight,
|
||||
height: toolbarType == 'input'
|
||||
? (_keyboardHeight > keyboardHeight
|
||||
? _keyboardHeight
|
||||
: keyboardHeight)
|
||||
: emoteHeight,
|
||||
child: EmotePanel(
|
||||
onChoose: (package, emote) => onChooseEmote(package, emote),
|
||||
),
|
||||
|
@ -376,6 +376,9 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
||||
false)
|
||||
? SvgPicture.asset(
|
||||
'assets/images/video/danmu_close.svg',
|
||||
// ignore: deprecated_member_use
|
||||
color:
|
||||
Theme.of(context).colorScheme.outline,
|
||||
)
|
||||
: SvgPicture.asset(
|
||||
'assets/images/video/danmu_open.svg',
|
||||
|
@ -17,6 +17,7 @@ import 'package:pilipala/plugin/pl_player/index.dart';
|
||||
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
import 'package:pilipala/services/shutdown_timer_service.dart';
|
||||
import '../../../../http/danmaku.dart';
|
||||
import '../../../../models/common/search_type.dart';
|
||||
import '../../../../models/video_detail_res.dart';
|
||||
import '../introduction/index.dart';
|
||||
@ -52,7 +53,7 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
final Box<dynamic> videoStorage = GStrorage.video;
|
||||
late List<double> speedsList;
|
||||
double buttonSpace = 8;
|
||||
bool showTitle = false;
|
||||
RxBool isFullScreen = false.obs;
|
||||
late String heroTag;
|
||||
late VideoIntroController videoIntroController;
|
||||
late VideoDetailData videoDetail;
|
||||
@ -69,13 +70,8 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
}
|
||||
|
||||
void fullScreenStatusListener() {
|
||||
widget.videoDetailCtr!.plPlayerController.isFullScreen
|
||||
.listen((bool isFullScreen) {
|
||||
if (isFullScreen) {
|
||||
showTitle = true;
|
||||
} else {
|
||||
showTitle = false;
|
||||
}
|
||||
widget.videoDetailCtr!.plPlayerController.isFullScreen.listen((bool val) {
|
||||
isFullScreen.value = val;
|
||||
|
||||
/// TODO setState() called after dispose()
|
||||
if (mounted) {
|
||||
@ -218,6 +214,87 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 发送弹幕
|
||||
void showShootDanmakuSheet() {
|
||||
final TextEditingController textController = TextEditingController();
|
||||
bool isSending = false; // 追踪是否正在发送
|
||||
showDialog(
|
||||
context: Get.context!,
|
||||
builder: (BuildContext context) {
|
||||
// TODO: 支持更多类型和颜色的弹幕
|
||||
return AlertDialog(
|
||||
title: const Text('发送弹幕(测试)'),
|
||||
content: StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setState) {
|
||||
return TextField(
|
||||
controller: textController,
|
||||
);
|
||||
}),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
),
|
||||
StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setState) {
|
||||
return TextButton(
|
||||
onPressed: isSending
|
||||
? null
|
||||
: () async {
|
||||
final String msg = textController.text;
|
||||
if (msg.isEmpty) {
|
||||
SmartDialog.showToast('弹幕内容不能为空');
|
||||
return;
|
||||
} else if (msg.length > 100) {
|
||||
SmartDialog.showToast('弹幕内容不能超过100个字符');
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
isSending = true; // 开始发送,更新状态
|
||||
});
|
||||
//修改按钮文字
|
||||
final dynamic res = await DanmakaHttp.shootDanmaku(
|
||||
oid: widget.videoDetailCtr!.cid.value,
|
||||
msg: textController.text,
|
||||
bvid: widget.videoDetailCtr!.bvid,
|
||||
progress:
|
||||
widget.controller!.position.value.inMilliseconds,
|
||||
type: 1,
|
||||
);
|
||||
setState(() {
|
||||
isSending = false; // 发送结束,更新状态
|
||||
});
|
||||
if (res['status']) {
|
||||
SmartDialog.showToast('发送成功');
|
||||
// 发送成功,自动预览该弹幕,避免重新请求
|
||||
// TODO: 暂停状态下预览弹幕仍会移动与计时,可考虑添加到dmSegList或其他方式实现
|
||||
widget.controller!.danmakuController!.addItems([
|
||||
DanmakuItem(
|
||||
msg,
|
||||
color: Colors.white,
|
||||
time: widget
|
||||
.controller!.position.value.inMilliseconds,
|
||||
type: DanmakuItemType.scroll,
|
||||
isSend: true,
|
||||
)
|
||||
]);
|
||||
Get.back();
|
||||
} else {
|
||||
SmartDialog.showToast('发送失败,错误信息为${res['msg']}');
|
||||
}
|
||||
},
|
||||
child: Text(isSending ? '发送中...' : '发送'),
|
||||
);
|
||||
})
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 定时关闭
|
||||
void scheduleExit() async {
|
||||
const List<int> scheduleTimeChoices = [
|
||||
@ -1079,7 +1156,7 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
},
|
||||
),
|
||||
SizedBox(width: buttonSpace),
|
||||
if (showTitle &&
|
||||
if (isFullScreen.value &&
|
||||
isLandscape &&
|
||||
widget.videoType == SearchType.video) ...[
|
||||
Column(
|
||||
@ -1131,6 +1208,43 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
// ),
|
||||
// fuc: () => _.screenshot(),
|
||||
// ),
|
||||
if (isFullScreen.value) ...[
|
||||
SizedBox(
|
||||
width: 56,
|
||||
height: 34,
|
||||
child: TextButton(
|
||||
style: ButtonStyle(
|
||||
padding: MaterialStateProperty.all(EdgeInsets.zero),
|
||||
),
|
||||
onPressed: () => showShootDanmakuSheet(),
|
||||
child: const Text(
|
||||
'发弹幕',
|
||||
style: textStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 34,
|
||||
height: 34,
|
||||
child: Obx(
|
||||
() => IconButton(
|
||||
style: ButtonStyle(
|
||||
padding: MaterialStateProperty.all(EdgeInsets.zero),
|
||||
),
|
||||
onPressed: () {
|
||||
_.isOpenDanmu.value = !_.isOpenDanmu.value;
|
||||
},
|
||||
icon: Icon(
|
||||
_.isOpenDanmu.value
|
||||
? Icons.subtitles_outlined
|
||||
: Icons.subtitles_off_outlined,
|
||||
size: 19,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
SizedBox(width: buttonSpace),
|
||||
if (Platform.isAndroid) ...<Widget>[
|
||||
SizedBox(
|
||||
|
@ -19,15 +19,7 @@ class Em {
|
||||
return regCate(matchStr);
|
||||
}, onNonMatch: (String str) {
|
||||
if (str != '') {
|
||||
str = str
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll(''', "'")
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll(''', "'")
|
||||
.replaceAll(' ', " ")
|
||||
.replaceAll('&', "&");
|
||||
str = decodeHtmlEntities(str);
|
||||
Map map = {'type': 'text', 'text': str};
|
||||
res.add(map);
|
||||
}
|
||||
@ -35,4 +27,17 @@ class Em {
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
static String decodeHtmlEntities(String title) {
|
||||
return title
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll(''', "'")
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll(''', "'")
|
||||
.replaceAll(' ', " ")
|
||||
.replaceAll('&', "&")
|
||||
.replaceAll(''', "'");
|
||||
}
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ class Utils {
|
||||
}
|
||||
if (time < 3600) {
|
||||
if (time == 0) {
|
||||
return time;
|
||||
return '00:00';
|
||||
}
|
||||
final int minute = time ~/ 60;
|
||||
final double res = time / 60;
|
||||
@ -208,17 +208,35 @@ class Utils {
|
||||
|
||||
static int findClosestNumber(int target, List<int> numbers) {
|
||||
int minDiff = 127;
|
||||
late int closestNumber;
|
||||
int closestNumber = 0; // 初始化为0,表示没有找到比目标值小的整数
|
||||
|
||||
// 向下查找
|
||||
try {
|
||||
for (int number in numbers) {
|
||||
int diff = (number - target).abs();
|
||||
if (number < target) {
|
||||
int diff = target - number; // 计算目标值与当前整数的差值
|
||||
|
||||
if (diff < minDiff) {
|
||||
minDiff = diff;
|
||||
closestNumber = number;
|
||||
if (diff < minDiff) {
|
||||
minDiff = diff;
|
||||
closestNumber = number;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// 向上查找
|
||||
if (closestNumber == 0) {
|
||||
try {
|
||||
for (int number in numbers) {
|
||||
int diff = (number - target).abs();
|
||||
|
||||
if (diff < minDiff) {
|
||||
minDiff = diff;
|
||||
closestNumber = number;
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
return closestNumber;
|
||||
}
|
||||
|
||||
|
@ -433,6 +433,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.0.3"
|
||||
expandable:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: expandable
|
||||
sha256: "9604d612d4d1146dafa96c6d8eec9c2ff0994658d6d09fed720ab788c7f5afc2"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.0.1"
|
||||
extended_image:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 1.0.21+1021
|
||||
version: 1.0.22+1022
|
||||
|
||||
environment:
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
@ -142,6 +142,8 @@ dependencies:
|
||||
path: 1.8.3
|
||||
# 电池优化
|
||||
disable_battery_optimization: ^1.1.1
|
||||
# 展开/收起
|
||||
expandable: ^5.0.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
Reference in New Issue
Block a user