Compare commits

..

1 Commits

Author SHA1 Message Date
296f4108b8 Merge branch 'main' into feature-ad 2024-03-26 22:15:30 +08:00
27 changed files with 150 additions and 526 deletions

View File

@ -4,7 +4,7 @@ on:
workflow_dispatch: workflow_dispatch:
push: push:
branches: branches:
- "x-main" - "main"
paths-ignore: paths-ignore:
- "**.md" - "**.md"
- "**.txt" - "**.txt"

View File

@ -2,6 +2,8 @@
<img width="200" height="200" src="https://github.com/guozhigq/pilipala/blob/main/assets/images/logo/logo_android.png"> <img width="200" height="200" src="https://github.com/guozhigq/pilipala/blob/main/assets/images/logo/logo_android.png">
</div> </div>
<div align="center"> <div align="center">
<h1>PiliPala</h1> <h1>PiliPala</h1>
<div align="center"> <div align="center">
@ -9,9 +11,8 @@
![GitHub repo size](https://img.shields.io/github/repo-size/guozhigq/pilipala) ![GitHub repo size](https://img.shields.io/github/repo-size/guozhigq/pilipala)
![GitHub Repo stars](https://img.shields.io/github/stars/guozhigq/pilipala) ![GitHub Repo stars](https://img.shields.io/github/stars/guozhigq/pilipala)
![GitHub all releases](https://img.shields.io/github/downloads/guozhigq/pilipala/total) ![GitHub all releases](https://img.shields.io/github/downloads/guozhigq/pilipala/total)
</div> </div>
<p>使用 Flutter 开发的 BiliBili 第三方客户端</p> <p>使用Flutter开发的BiliBili第三方客户端</p>
<img src="https://github.com/guozhigq/pilipala/blob/main/assets/screenshots/510shots_so.png" width="32%" alt="home" /> <img src="https://github.com/guozhigq/pilipala/blob/main/assets/screenshots/510shots_so.png" width="32%" alt="home" />
<img src="https://github.com/guozhigq/pilipala/blob/main/assets/screenshots/174shots_so.png" width="32%" alt="home" /> <img src="https://github.com/guozhigq/pilipala/blob/main/assets/screenshots/174shots_so.png" width="32%" alt="home" />
@ -22,41 +23,46 @@
</div> </div>
## 开发环境 ## 开发环境
Xcode 13.4 不支持**auto_orientation**,请注释相关代码
Xcode 13.4 不支持 ```auto_orientation```,请注释相关代码
```bash ```bash
[✓] Flutter (Channel stable, 3.16.5, on macOS 14.1.2 23B92 darwin-arm64, locale [] Flutter (Channel stable, 3.16.4, on macOS 14.1.2 23B92 darwin-arm64, locale
zh-Hans-CN) zh-Hans-CN)
[] Android toolchain - develop for Android devices (Android SDK version 34.0.0) [] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
[] Xcode - develop for iOS and macOS (Xcode 15.1) [] Xcode - develop for iOS and macOS (Xcode 15.1)
[] Chrome - develop for the web [] Chrome - develop for the web
[] Android Studio (version 2022.3) [] Android Studio (version 2022.3)
[✓] VS Code (version 1.87.2) [] VS Code (version 1.85.1)
[] Connected device (3 available) [] Connected device (3 available)
[] Network resources [] Network resources
``` ```
<br/>
## 技术交流 ## 技术交流
Telegram: https://t.me/+lm_oOVmF0RJiODk1 Telegram: https://t.me/+lm_oOVmF0RJiODk1
QQ频道: https://pd.qq.com/s/365esodk3
Telegram Beta 版本:@PiliPala_Beta
QQ 频道: https://pd.qq.com/s/365esodk3 <br/>
## 功能 ## 功能
目前着重移动端 (Android、iOS)暂时没有适配桌面端、Pad 端、手表端等 目前着重移动端(Android、iOS)暂时没有适配桌面端、Pad端、手表端等
<br/>
现有功能及[开发计划](https://github.com/users/guozhigq/projects/5) 现有功能及[开发计划](https://github.com/users/guozhigq/projects/5)
- [x] 推荐视频列表 (app 端)
- [x] 推荐视频列表(app端)
- [x] 最热视频列表 - [x] 最热视频列表
- [x] 热门直播 - [x] 热门直播
- [x] 番剧列表 - [x] 番剧列表
- [x] 屏蔽黑名单内用户视频 - [x] 屏蔽黑名单内用户视频
- [x] 排行榜
- [x] 用户相关 - [x] 用户相关
- [x] 粉丝、关注用户、拉黑用户查看 - [x] 粉丝、关注用户、拉黑用户查看
@ -66,13 +72,11 @@ QQ 频道: https://pd.qq.com/s/365esodk3
- [x] 稍后再看 - [x] 稍后再看
- [x] 观看记录 - [x] 观看记录
- [x] 我的收藏 - [x] 我的收藏
- [x] 黑名单管理
- [x] 动态相关 - [x] 动态相关
- [x] 全部、投稿、番剧分类查看 - [x] 全部、投稿、番剧分类查看
- [x] 动态评论查看 - [x] 动态评论查看
- [x] 动态评论回复功能 - [x] 动态评论回复功能
- [x] 动态未读标记
- [x] 视频播放相关 - [x] 视频播放相关
- [x] 双击快进/快退 - [x] 双击快进/快退
@ -81,13 +85,13 @@ QQ 频道: https://pd.qq.com/s/365esodk3
- [x] 垂直方向上滑全屏、下滑退出全屏 - [x] 垂直方向上滑全屏、下滑退出全屏
- [x] 水平方向手势快进/快退 - [x] 水平方向手势快进/快退
- [x] 全屏方向设置 - [x] 全屏方向设置
- [x] 倍速选择/长按 2 倍速 - [x] 倍速选择/长按2倍速
- [x] 硬件加速 (视机型而定) - [x] 硬件加速视机型而定
- [x] 画质选择 (高清画质未解锁) - [x] 画质选择高清画质未解锁
- [x] 音质选择 (视视频而定) - [x] 音质选择视视频而定
- [x] 解码格式选择 (视视频而定) - [x] 解码格式选择视视频而定
- [x] 弹幕 - [x] 弹幕
- [x] 字幕 - [ ] 字幕
- [x] 记忆播放 - [x] 记忆播放
- [x] 视频比例:高度/宽度适应、填充、包含等 - [x] 视频比例:高度/宽度适应、填充、包含等
@ -99,12 +103,12 @@ QQ 频道: https://pd.qq.com/s/365esodk3
- [x] 视频搜索排序、按时长筛选 - [x] 视频搜索排序、按时长筛选
- [x] 视频详情页相关 - [x] 视频详情页相关
- [x] 视频选集 (分 p) 切换 - [x] 视频选集(分p)切换
- [x] 点赞、投币、收藏/取消收藏 - [x] 点赞、投币、收藏/取消收藏
- [x] 相关视频查看 - [x] 相关视频查看
- [x] 评论用户身份标识 - [x] 评论用户身份标识
- [x] 评论 (排序) 查看、二楼评论查看 - [x] 评论(排序)查看、二楼评论查看
- [x] 主楼、二楼评论/表情回复功能 - [x] 主楼、二楼评论回复功能
- [x] 评论点赞 - [x] 评论点赞
- [x] 评论笔记图片查看、保存 - [x] 评论笔记图片查看、保存
@ -112,30 +116,29 @@ QQ 频道: https://pd.qq.com/s/365esodk3
- [x] 画质、音质、解码方式预设 - [x] 画质、音质、解码方式预设
- [x] 图片质量设定 - [x] 图片质量设定
- [x] 主题模式:亮色/暗色/跟随系统 - [x] 主题模式:亮色/暗色/跟随系统
- [x] 震动反馈 (可选) - [x] 震动反馈(可选)
- [x] 高帧率 - [x] 高帧率
- [x] 自动全屏 - [x] 自动全屏
- [ ] 等等 - [ ] 等等
<br/>
## 下载 ## 下载
可以通过右侧 Releases 进行下载或拉取代码到本地进行编译 可以通过右侧release进行下载或拉取代码到本地进行编译
### 从 F-Droid 安装 <br/>
<a href="https://f-droid.org/packages/com.guozhigq.pilipala">
<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on-zh-cn.png"
alt="Get it on F-Droid"
height="80">
</a>
## 声明 ## 声明
此项目 (PiliPala) 是个人为了兴趣而开发, 仅用于学习和测试。 此项目PiliPala是个人为了兴趣而开发, 仅用于学习和测试。
所用 API 皆从官方网站收集, 不提供任何破解内容。 所用API皆从官方网站收集, 不提供任何破解内容。
感谢使用 感谢使用
<br/>
## 致谢 ## 致谢
- [bilibili-API-collect](https://github.com/SocialSisterYi/bilibili-API-collect) - [bilibili-API-collect](https://github.com/SocialSisterYi/bilibili-API-collect)
@ -143,3 +146,7 @@ QQ 频道: https://pd.qq.com/s/365esodk3
- [media-kit](https://github.com/media-kit/media-kit) - [media-kit](https://github.com/media-kit/media-kit)
- [dio](https://pub.dev/packages/dio) - [dio](https://pub.dev/packages/dio)
- 等等 - 等等
<br/>
<br/>
<br/>

View File

@ -1,29 +0,0 @@
## 1.0.22
明天晚上或者后天晚上应该会更新
### 功能
+ 字幕
+ 全屏时选集
+ 动态转发
+ 评论视频并转发
+ 收藏夹删除
+ 合集显示封面
+ 底部导航栏编辑、排序功能
+ 历史记录进度条展示
+ 直播画质切换
+ 排行榜功能
+ 视频详情页推荐视频开关
+ 显示联合投稿up
### 修复
+ 收藏夹个数错误
+ 封面保存权限问题
+ 合集最后1p未展示
+ up主页关注按钮触发灰屏
### 优化
+ 视频简介查看逻辑
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

View File

@ -1,15 +0,0 @@
## 1.0.23
### 功能
+ 封面下载
### 修复
+ 全屏问题
+ 视频播放器灰屏问题
+ 评论区点击区域问题
+ 动态详情跳转异常问题
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

View File

@ -1,25 +0,0 @@
## 1.0.24
该版本代码未开源,酌情使用。
### 功能
+ 私信功能
+ 回复我的、收到的赞查看
+ 新的登录方式
+ 全屏选集
+ 一键三连
+ 按分区搜索
### 优化
+ 页面跳转动画
+ 评论区跳转
### 修复
+ 音画不同步问题
+ 分集字幕未同步
+ 多语言字幕
+ 弹幕设置未生效
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

View File

@ -506,6 +506,4 @@ class Api {
/// 排行榜 /// 排行榜
static const String getRankApi = "/x/web-interface/ranking/v2"; static const String getRankApi = "/x/web-interface/ranking/v2";
/// 取消订阅
static const String cancelSub = '/x/v3/fav/season/unfav';
} }

View File

@ -349,21 +349,4 @@ class UserHttp {
return {'status': false, 'msg': res.data['message']}; 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']};
}
}
} }

View File

@ -34,7 +34,6 @@ void main() async {
.then((_) async { .then((_) async {
await GStrorage.init(); await GStrorage.init();
await setupServiceLocator(); await setupServiceLocator();
clearLogs();
Request(); Request();
await Request.setCookie(); await Request.setCookie();
RecommendFilter(); RecommendFilter();

View File

@ -47,23 +47,18 @@ class Vip {
this.status, this.status,
this.dueDate, this.dueDate,
this.label, this.label,
this.nicknameColor,
}); });
int? type; int? type;
int? status; int? status;
int? dueDate; int? dueDate;
Map? label; Map? label;
int? nicknameColor;
Vip.fromJson(Map<String, dynamic> json) { Vip.fromJson(Map<String, dynamic> json) {
type = json['type']; type = json['type'];
status = json['status']; status = json['status'];
dueDate = json['due_date']; dueDate = json['due_date'];
label = json['label']; label = json['label'];
nicknameColor = json['nickname_color'] == ''
? null
: int.parse("0xFF${json['nickname_color'].replaceAll('#', '')}");
} }
} }

View File

@ -437,8 +437,7 @@ class SearchArticleItemModel {
pubTime = json['pub_time']; pubTime = json['pub_time'];
like = json['like']; like = json['like'];
title = Em.regTitle(json['title']); title = Em.regTitle(json['title']);
subTitle = subTitle = json['title'].replaceAll(RegExp(r'<[^>]*>'), '');
Em.decodeHtmlEntities(json['title'].replaceAll(RegExp(r'<[^>]*>'), ''));
rankOffset = json['rank_offset']; rankOffset = json['rank_offset'];
mid = json['mid']; mid = json['mid'];
imageUrls = json['image_urls']; imageUrls = json['image_urls'];

View File

@ -185,7 +185,7 @@ class HistoryItem extends StatelessWidget {
? '已看完' ? '已看完'
: '${Utils.timeFormat(videoItem.progress!)}/${Utils.timeFormat(videoItem.duration!)}', : '${Utils.timeFormat(videoItem.progress!)}/${Utils.timeFormat(videoItem.duration!)}',
right: 6.0, right: 6.0,
bottom: 8.0, bottom: 6.0,
type: 'gray', type: 'gray',
), ),
// 右上角 // 右上角
@ -258,27 +258,6 @@ 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) VideoContent(videoItem: videoItem, ctr: ctr)

View File

@ -281,8 +281,8 @@ class _MemberPageState extends State<MemberPage>
future: _futureBuilderFuture, future: _futureBuilderFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) { if (snapshot.connectionState == ConnectionState.done) {
Map? data = snapshot.data; Map data = snapshot.data!;
if (data != null && data['status']) { if (data['status']) {
return Obx( return Obx(
() => Stack( () => Stack(
alignment: AlignmentDirectional.center, alignment: AlignmentDirectional.center,
@ -302,14 +302,7 @@ class _MemberPageState extends State<MemberPage>
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
.titleMedium! .titleMedium!
.copyWith( .copyWith(fontWeight: FontWeight.bold),
fontWeight: FontWeight.bold,
color: _memberController.memberInfo.value
.vip!.nicknameColor !=
null
? Color(_memberController.memberInfo
.value.vip!.nicknameColor!)
: null),
)), )),
const SizedBox(width: 2), const SizedBox(width: 2),
if (_memberController.memberInfo.value.sex == '') if (_memberController.memberInfo.value.sex == '')

View File

@ -18,32 +18,45 @@ class MemberSeasonsPanel extends StatelessWidget {
itemBuilder: (context, index) { itemBuilder: (context, index) {
MemberSeasonsList item = data!.seasonsList![index]; MemberSeasonsList item = data!.seasonsList![index];
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.only(bottom: 12, right: 4),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
ListTile( Padding(
onTap: () => Get.toNamed( padding: const EdgeInsets.only(bottom: 12, left: 4),
'/memberSeasons?mid=${item.meta!.mid}&seasonId=${item.meta!.seasonId}'), child: Row(
title: Text( children: [
item.meta!.name!, Text(
maxLines: 1, item.meta!.name!,
overflow: TextOverflow.ellipsis, maxLines: 1,
style: Theme.of(context).textTheme.titleSmall!, style: Theme.of(context).textTheme.titleSmall!,
), ),
dense: true, const SizedBox(width: 10),
leading: PBadge( PBadge(
stack: 'relative', stack: 'relative',
size: 'small', size: 'small',
text: item.meta!.total.toString(), text: item.meta!.total.toString(),
), ),
trailing: const Icon( const Spacer(),
Icons.arrow_forward, SizedBox(
size: 20, 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,
),
),
)
],
), ),
), ),
const SizedBox(height: 10),
LayoutBuilder( LayoutBuilder(
builder: (context, boxConstraints) { builder: (context, boxConstraints) {
return GridView.builder( return GridView.builder(

View File

@ -44,7 +44,6 @@ class _SelectDialogState<T> extends State<SelectDialog<T>> {
setState(() { setState(() {
_tempValue = value as T; _tempValue = value as T;
}); });
Navigator.pop(context, _tempValue);
}, },
), ),
] ]
@ -52,6 +51,19 @@ 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('确定'),
)
],
); );
} }
} }

View File

@ -46,40 +46,4 @@ class SubController extends GetxController {
Future onLoad() async { Future onLoad() async {
querySubFolder(type: 'onload'); 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('确定'),
),
],
),
);
}
} }

View File

@ -58,8 +58,7 @@ class _SubPageState extends State<SubPage> {
itemBuilder: (context, index) { itemBuilder: (context, index) {
return SubItem( return SubItem(
subFolderItem: subFolderItem:
_subController.subFolderData.value.list![index], _subController.subFolderData.value.list![index]);
cancelSub: _subController.cancelSub);
}, },
), ),
); );

View File

@ -8,12 +8,7 @@ import '../../../models/user/sub_folder.dart';
class SubItem extends StatelessWidget { class SubItem extends StatelessWidget {
final SubFolderItemData subFolderItem; final SubFolderItemData subFolderItem;
final Function(SubFolderItemData) cancelSub; const SubItem({super.key, required this.subFolderItem});
const SubItem({
super.key,
required this.subFolderItem,
required this.cancelSub,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -56,10 +51,7 @@ class SubItem extends StatelessWidget {
}, },
), ),
), ),
VideoContent( VideoContent(subFolderItem: subFolderItem)
subFolderItem: subFolderItem,
cancelSub: cancelSub,
)
], ],
), ),
); );
@ -72,8 +64,7 @@ class SubItem extends StatelessWidget {
class VideoContent extends StatelessWidget { class VideoContent extends StatelessWidget {
final SubFolderItemData subFolderItem; final SubFolderItemData subFolderItem;
final Function(SubFolderItemData)? cancelSub; const VideoContent({super.key, required this.subFolderItem});
const VideoContent({super.key, required this.subFolderItem, this.cancelSub});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -109,24 +100,6 @@ class VideoContent extends StatelessWidget {
color: Theme.of(context).colorScheme.outline, 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),
),
)
],
)
], ],
), ),
), ),

View File

@ -91,7 +91,7 @@ class VideoDetailController extends GetxController
late bool enableCDN; late bool enableCDN;
late int? cacheVideoQa; late int? cacheVideoQa;
late String cacheDecode; late String cacheDecode;
late int defaultAudioQa; late int cacheAudioQa;
PersistentBottomSheetController? replyReplyBottomSheetCtr; PersistentBottomSheetController? replyReplyBottomSheetCtr;
RxList<SubTitileContentModel> subtitleContents = RxList<SubTitileContentModel> subtitleContents =
@ -146,7 +146,7 @@ class VideoDetailController extends GetxController
// 预设的解码格式 // 预设的解码格式
cacheDecode = setting.get(SettingBoxKey.defaultDecode, cacheDecode = setting.get(SettingBoxKey.defaultDecode,
defaultValue: VideoDecodeFormats.values.last.code); defaultValue: VideoDecodeFormats.values.last.code);
defaultAudioQa = setting.get(SettingBoxKey.defaultAudioQa, cacheAudioQa = setting.get(SettingBoxKey.defaultAudioQa,
defaultValue: AudioQuality.hiRes.code); defaultValue: AudioQuality.hiRes.code);
oid.value = IdUtils.bv2av(Get.parameters['bvid']!); oid.value = IdUtils.bv2av(Get.parameters['bvid']!);
getSubtitle(); getSubtitle();
@ -353,9 +353,9 @@ class VideoDetailController extends GetxController
if (audiosList.isNotEmpty) { if (audiosList.isNotEmpty) {
final List<int> numbers = audiosList.map((map) => map.id!).toList(); final List<int> numbers = audiosList.map((map) => map.id!).toList();
int closestNumber = Utils.findClosestNumber(defaultAudioQa, numbers); int closestNumber = Utils.findClosestNumber(cacheAudioQa, numbers);
if (!numbers.contains(defaultAudioQa) && if (!numbers.contains(cacheAudioQa) &&
numbers.any((e) => e > defaultAudioQa)) { numbers.any((e) => e > cacheAudioQa)) {
closestNumber = 30280; closestNumber = 30280;
} }
firstAudio = audiosList.firstWhere((e) => e.id == closestNumber); firstAudio = audiosList.firstWhere((e) => e.id == closestNumber);

View File

@ -1,4 +1,3 @@
import 'package:expandable/expandable.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@ -17,6 +16,7 @@ import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import '../../../../http/user.dart'; import '../../../../http/user.dart';
import '../widgets/expandable_section.dart';
import 'widgets/action_item.dart'; import 'widgets/action_item.dart';
import 'widgets/fav_panel.dart'; import 'widgets/fav_panel.dart';
import 'widgets/intro_detail.dart'; import 'widgets/intro_detail.dart';
@ -140,8 +140,6 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
late bool enableAi; late bool enableAi;
bool isProcessing = false; bool isProcessing = false;
RxBool isExpand = false.obs; RxBool isExpand = false.obs;
late ExpandableController _expandableCtr;
void Function()? handleState(Future Function() action) { void Function()? handleState(Future Function() action) {
return isProcessing return isProcessing
? null ? null
@ -165,7 +163,6 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
follower = Utils.numFormat(videoIntroController.userStat['follower']); follower = Utils.numFormat(videoIntroController.userStat['follower']);
followStatus = videoIntroController.followStatus; followStatus = videoIntroController.followStatus;
enableAi = setting.get(SettingBoxKey.enableAi, defaultValue: true); enableAi = setting.get(SettingBoxKey.enableAi, defaultValue: true);
_expandableCtr = ExpandableController(initialExpanded: false);
} }
// 收藏 // 收藏
@ -219,7 +216,6 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
showIntroDetail() { showIntroDetail() {
feedBack(); feedBack();
isExpand.value = !(isExpand.value); isExpand.value = !(isExpand.value);
_expandableCtr.toggle();
} }
// 用户主页 // 用户主页
@ -243,12 +239,6 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
); );
} }
@override
void dispose() {
_expandableCtr.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ThemeData t = Theme.of(context); final ThemeData t = Theme.of(context);
@ -266,34 +256,14 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
GestureDetector( GestureDetector(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
onTap: () => showIntroDetail(), onTap: () => showIntroDetail(),
child: ExpandablePanel( child: Text(
controller: _expandableCtr, widget.videoDetail!.title!,
collapsed: Text( style: const TextStyle(
widget.videoDetail!.title!, fontSize: 18,
softWrap: true, fontWeight: FontWeight.bold,
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( Stack(
@ -358,16 +328,12 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
), ),
/// 视频简介 /// 视频简介
ExpandablePanel( Obx(
controller: _expandableCtr, () => ExpandedSection(
collapsed: const SizedBox(height: 0), expand: isExpand.value,
expanded: IntroDetail(videoDetail: widget.videoDetail!), begin: 0,
theme: const ExpandableThemeData( end: 1,
animationDuration: Duration(milliseconds: 300), child: IntroDetail(videoDetail: widget.videoDetail!),
scrollAnimationDuration: Duration(milliseconds: 300),
crossFadePoint: 0,
fadeCurve: Curves.ease,
sizeCurve: Curves.linear,
), ),
), ),

View File

@ -149,16 +149,13 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
delegate: _MySliverPersistentHeaderDelegate( delegate: _MySliverPersistentHeaderDelegate(
child: Container( child: Container(
height: 40, height: 40,
padding: const EdgeInsets.fromLTRB(12, 0, 6, 0), padding: const EdgeInsets.fromLTRB(12, 6, 6, 0),
color: Theme.of(context).colorScheme.surface,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Obx( Text(
() => Text( '${_videoReplyController.sortTypeLabel.value}评论',
'${_videoReplyController.sortTypeLabel.value}评论', style: const TextStyle(fontSize: 13),
style: const TextStyle(fontSize: 13),
),
), ),
SizedBox( SizedBox(
height: 35, height: 35,

View File

@ -525,18 +525,14 @@ InlineSpan buildContent(
if (jumpUrlKeysList.isNotEmpty) { if (jumpUrlKeysList.isNotEmpty) {
patternStr += '|${jumpUrlKeysList.join('|')}'; patternStr += '|${jumpUrlKeysList.join('|')}';
} }
RegExp bv23Regex = RegExp(r'https://b23\.tv/[a-zA-Z0-9]{7}');
final RegExp pattern = RegExp(patternStr); final RegExp pattern = RegExp(patternStr);
List<String> matchedStrs = []; List<String> matchedStrs = [];
void addPlainTextSpan(str) { void addPlainTextSpan(str) {
spanChilds.add( spanChilds.add(TextSpan(
TextSpan(
text: str, text: str,
recognizer: TapGestureRecognizer() recognizer: TapGestureRecognizer()
..onTap = () => ..onTap = () =>
replyReply?.call(replyItem.root == 0 ? replyItem : fReplyItem), replyReply?.call(replyItem.root == 0 ? replyItem : fReplyItem)));
),
);
} }
// 分割文本并处理每个部分 // 分割文本并处理每个部分
@ -738,36 +734,8 @@ InlineSpan buildContent(
return ''; return '';
}, },
onNonMatch: (String nonMatchStr) { onNonMatch: (String nonMatchStr) {
return nonMatchStr.splitMapJoin( addPlainTextSpan(nonMatchStr);
bv23Regex, return nonMatchStr;
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;
},
);
}, },
); );

View File

@ -142,10 +142,9 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
double _keyboardHeight = EdgeInsets.fromViewPadding( double keyboardHeight = EdgeInsets.fromViewPadding(
View.of(context).viewInsets, View.of(context).devicePixelRatio) View.of(context).viewInsets, View.of(context).devicePixelRatio)
.bottom; .bottom;
print('_keyboardHeight: $_keyboardHeight');
return Container( return Container(
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
decoration: BoxDecoration( decoration: BoxDecoration(
@ -236,11 +235,7 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
child: SizedBox( child: SizedBox(
width: double.infinity, width: double.infinity,
height: toolbarType == 'input' height: toolbarType == 'input' ? keyboardHeight : emoteHeight,
? (_keyboardHeight > keyboardHeight
? _keyboardHeight
: keyboardHeight)
: emoteHeight,
child: EmotePanel( child: EmotePanel(
onChoose: (package, emote) => onChooseEmote(package, emote), onChoose: (package, emote) => onChooseEmote(package, emote),
), ),

View File

@ -17,7 +17,6 @@ import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart'; import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/services/shutdown_timer_service.dart'; import 'package:pilipala/services/shutdown_timer_service.dart';
import '../../../../http/danmaku.dart';
import '../../../../models/common/search_type.dart'; import '../../../../models/common/search_type.dart';
import '../../../../models/video_detail_res.dart'; import '../../../../models/video_detail_res.dart';
import '../introduction/index.dart'; import '../introduction/index.dart';
@ -53,7 +52,7 @@ class _HeaderControlState extends State<HeaderControl> {
final Box<dynamic> videoStorage = GStrorage.video; final Box<dynamic> videoStorage = GStrorage.video;
late List<double> speedsList; late List<double> speedsList;
double buttonSpace = 8; double buttonSpace = 8;
RxBool isFullScreen = false.obs; bool showTitle = false;
late String heroTag; late String heroTag;
late VideoIntroController videoIntroController; late VideoIntroController videoIntroController;
late VideoDetailData videoDetail; late VideoDetailData videoDetail;
@ -70,8 +69,13 @@ class _HeaderControlState extends State<HeaderControl> {
} }
void fullScreenStatusListener() { void fullScreenStatusListener() {
widget.videoDetailCtr!.plPlayerController.isFullScreen.listen((bool val) { widget.videoDetailCtr!.plPlayerController.isFullScreen
isFullScreen.value = val; .listen((bool isFullScreen) {
if (isFullScreen) {
showTitle = true;
} else {
showTitle = false;
}
/// TODO setState() called after dispose() /// TODO setState() called after dispose()
if (mounted) { if (mounted) {
@ -214,87 +218,6 @@ 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 { void scheduleExit() async {
const List<int> scheduleTimeChoices = [ const List<int> scheduleTimeChoices = [
@ -1156,7 +1079,7 @@ class _HeaderControlState extends State<HeaderControl> {
}, },
), ),
SizedBox(width: buttonSpace), SizedBox(width: buttonSpace),
if (isFullScreen.value && if (showTitle &&
isLandscape && isLandscape &&
widget.videoType == SearchType.video) ...[ widget.videoType == SearchType.video) ...[
Column( Column(
@ -1208,43 +1131,6 @@ class _HeaderControlState extends State<HeaderControl> {
// ), // ),
// fuc: () => _.screenshot(), // 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), SizedBox(width: buttonSpace),
if (Platform.isAndroid) ...<Widget>[ if (Platform.isAndroid) ...<Widget>[
SizedBox( SizedBox(

View File

@ -19,7 +19,15 @@ class Em {
return regCate(matchStr); return regCate(matchStr);
}, onNonMatch: (String str) { }, onNonMatch: (String str) {
if (str != '') { if (str != '') {
str = decodeHtmlEntities(str); str = str
.replaceAll('&lt;', '<')
.replaceAll('&gt;', '>')
.replaceAll('&#34;', '"')
.replaceAll('&#39;', "'")
.replaceAll('&quot;', '"')
.replaceAll('&apos;', "'")
.replaceAll('&nbsp;', " ")
.replaceAll('&amp;', "&");
Map map = {'type': 'text', 'text': str}; Map map = {'type': 'text', 'text': str};
res.add(map); res.add(map);
} }
@ -27,17 +35,4 @@ class Em {
}); });
return res; return res;
} }
static String decodeHtmlEntities(String title) {
return title
.replaceAll('&lt;', '<')
.replaceAll('&gt;', '>')
.replaceAll('&#34;', '"')
.replaceAll('&#39;', "'")
.replaceAll('&quot;', '"')
.replaceAll('&apos;', "'")
.replaceAll('&nbsp;', " ")
.replaceAll('&amp;', "&")
.replaceAll('&#x27;', "'");
}
} }

View File

@ -51,7 +51,7 @@ class Utils {
} }
if (time < 3600) { if (time < 3600) {
if (time == 0) { if (time == 0) {
return '00:00'; return time;
} }
final int minute = time ~/ 60; final int minute = time ~/ 60;
final double res = time / 60; final double res = time / 60;
@ -208,35 +208,17 @@ class Utils {
static int findClosestNumber(int target, List<int> numbers) { static int findClosestNumber(int target, List<int> numbers) {
int minDiff = 127; int minDiff = 127;
int closestNumber = 0; // 初始化为0表示没有找到比目标值小的整数 late int closestNumber;
// 向下查找
try { try {
for (int number in numbers) { for (int number in numbers) {
if (number < target) { int diff = (number - target).abs();
int diff = target - number; // 计算目标值与当前整数的差值
if (diff < minDiff) { if (diff < minDiff) {
minDiff = diff; minDiff = diff;
closestNumber = number; closestNumber = number;
}
} }
} }
} catch (_) {} } 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; return closestNumber;
} }

View File

@ -433,14 +433,6 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "5.0.3" 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: extended_image:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -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 # 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 # 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. # of the product and file versions while build-number is used as the build suffix.
version: 1.0.24+1024 version: 1.0.21+1021
environment: environment:
sdk: ">=3.0.0 <4.0.0" sdk: ">=3.0.0 <4.0.0"
@ -142,8 +142,6 @@ dependencies:
path: 1.8.3 path: 1.8.3
# 电池优化 # 电池优化
disable_battery_optimization: ^1.1.1 disable_battery_optimization: ^1.1.1
# 展开/收起
expandable: ^5.0.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: