Compare commits

...

79 Commits

Author SHA1 Message Date
53d4379bb9 v1.0.5 更新 2023-08-26 15:51:14 +08:00
a928c575ef fix: supportFormats codecs match dvh1 2023-08-26 15:23:22 +08:00
a0e51c86fc Merge branch 'design' 2023-08-26 12:38:54 +08:00
d670b8123a feat: 下载对应版本apk 2023-08-26 12:38:31 +08:00
2621b096ac Merge branch 'main' of github.com:guozhigq/pilipala 2023-08-26 11:33:40 +08:00
05631f7803 fix: 大会员切换番剧 2023-08-26 11:33:10 +08:00
495ba57ca8 Update README.md 2023-08-26 10:56:47 +08:00
8990c4ae92 feat: 高帧率 2023-08-26 10:51:14 +08:00
8bc6a32b06 feat: 播放器亮度记忆 2023-08-25 23:51:40 +08:00
6083578f93 fix: 评论渲染异常、评论总数 2023-08-25 21:16:16 +08:00
aa7419f352 Merge branch 'design' 2023-08-25 13:56:08 +08:00
c90a6cd86c feat: 增加iOS路由切换效果 2023-08-25 13:55:46 +08:00
2e04c27292 Merge branch 'design' 2023-08-25 10:21:49 +08:00
5741f80536 feat: 动态合集查看 2023-08-25 10:21:32 +08:00
161ba1c313 mod: 倍速选择 2023-08-25 10:07:00 +08:00
5d9dc6c1a9 Merge branch 'fix' 2023-08-24 22:02:06 +08:00
1abe70d4d4 fix: 重复进入个人中心页面数据未刷新 2023-08-24 22:01:48 +08:00
5fc959eb59 feat: #30 动态默认展示某项 2023-08-24 21:31:19 +08:00
6322b29aef Merge branch 'main' into design 2023-08-24 15:31:06 +08:00
6461f72b5e fix: #28 合集数据渲染错误 2023-08-24 15:24:56 +08:00
e3d561bffd fix: 快速返回首页&销毁播放器 2023-08-24 14:22:15 +08:00
535cf69967 feat: 评论默认排序 2023-08-24 12:23:38 +08:00
4314b0fc3c fix: 首页搜索框频繁点击消失、评论排序切换空白 2023-08-24 09:54:24 +08:00
9da113726b fix: 小白条、导航栏沉浸 2023-08-24 09:25:32 +08:00
201422c150 feat: 同时在看人数 2023-08-24 08:38:01 +08:00
b67127123a fix: 动态goods数据异常 2023-08-24 07:48:24 +08:00
3ce7578183 fix: 收藏夹翻页 2023-08-24 07:25:55 +08:00
7b60cc2666 Update README.md 2023-08-22 22:46:05 +08:00
ead24e90fb Add files via upload 2023-08-22 22:44:43 +08:00
3ad3ca9d48 v1.0.4 更新 2023-08-22 22:02:16 +08:00
8a8e99f30b feat: 字体大小调节 2023-08-22 20:53:50 +08:00
0fe6d6c8e2 mod: expansionPanelList 扩展 2023-08-22 16:51:43 +08:00
5a03bee410 feat: 主题颜色选择 2023-08-22 16:49:49 +08:00
b6023e35bc Update README.md 2023-08-22 14:26:23 +08:00
6a1c89f885 fix: 点赞异常、动态课程内容渲染 2023-08-22 14:17:51 +08:00
b7c0ef8341 mod: 取消热搜缓存、增加刷新功能 2023-08-22 12:27:02 +08:00
9e44995082 feat: 视频搜索时长筛选v2 2023-08-22 11:47:19 +08:00
8703d9f576 feat: 视频搜索条件筛选v1 2023-08-22 09:49:23 +08:00
5812b5cff1 fix: 动态渲染报错、转发评论加载 2023-08-21 21:16:03 +08:00
1884801ed2 fix: 视频解码默认格式 2023-08-21 18:35:50 +08:00
a19ab8d17f Merge branch 'fix' 2023-08-21 14:56:15 +08:00
a4078c0a8e fix: 搜索推荐词richText 2023-08-21 14:55:32 +08:00
90e811489b v1.0.3 更新 2023-08-21 09:41:07 +08:00
706bb0f924 mod: 一些样式修改 2023-08-21 09:03:34 +08:00
01d6308350 fix: richNode img preview 2023-08-20 21:58:38 +08:00
332d5dc38c feat: 底部播放进度条设置 2023-08-20 21:01:15 +08:00
8f84c6a6f9 mod: 图片预览页面样式 2023-08-20 20:34:25 +08:00
cc8753e8de fix: 用户信息字段 2023-08-20 19:37:37 +08:00
8c8ddc9d93 fix: 动态渲染异常 2023-08-20 18:10:54 +08:00
5f03244085 Merge branch 'main' into feature-media_kit 2023-08-20 15:48:51 +08:00
50a5653516 fix: 没有audio 资源的视频异常 2023-08-20 15:47:59 +08:00
7bb7159d48 mod: history 2023-08-20 15:26:10 +08:00
83341cd62b mod: 本地缓存字段修改、登录状态 2023-08-20 14:28:50 +08:00
8627869309 mod: 控制器监听事件移除 2023-08-20 09:12:31 +08:00
6bbbdd7710 fix:issues #26; mod: 部分样式 2023-08-19 23:24:17 +08:00
599d6983fc v1.0.2 更新 2023-08-19 21:33:39 +08:00
b7eed8578a mod: 请求节流 2023-08-19 20:37:42 +08:00
ec9d9739fe mod: 异常状态样式 2023-08-19 17:50:26 +08:00
740116e873 feat: 检查更新 2023-08-19 16:33:39 +08:00
2b0dc9d285 fix: 登录/退出登录页面刷新 2023-08-19 15:25:00 +08:00
7c2518bcd2 mod: 首页推荐picture类型 2023-08-19 11:34:36 +08:00
9db76e99db fix: 登录成功跳转 mod: 退出登录提示 2023-08-19 11:33:39 +08:00
40849cb68d opt: 全屏方式优化 2023-08-19 08:35:17 +08:00
b435023c99 feat: 直播、推荐、热门封面图片保存 2023-08-18 20:16:15 +08:00
b55568ef2a mod: 首页推荐代码 2023-08-18 18:22:20 +08:00
47a3c964c0 fix: 番剧记忆播放 2023-08-18 12:11:26 +08:00
85ff7c7a92 feat: 动态跳转番剧 2023-08-18 11:12:18 +08:00
083d05ed7e feat: 观看记录增加稍后再看功能 2023-08-18 10:24:09 +08:00
08862e7f72 feat: 稍后再看页面完善 2023-08-18 10:11:50 +08:00
04d953a3a2 fix: dio cookie读写 2023-08-18 09:30:31 +08:00
be1cc25d12 mod: 登录后状态 2023-08-17 22:44:01 +08:00
adff2f2828 fix: mac音量控制 2023-08-17 20:14:31 +08:00
592b32fc7a fix: 分p切换cid未更改 2023-08-17 16:00:37 +08:00
d9a758464c Merge branch 'main' of github.com:guozhigq/pilipala 2023-08-17 14:32:38 +08:00
0c067f7ca6 feat: 视频视图比例调整 2023-08-17 14:32:22 +08:00
b6aa144205 Update issue templates 2023-08-17 12:21:57 +08:00
438a94e298 Update main.yml
取消重命名拼接日期
2023-08-17 10:51:45 +08:00
b800f1b83b 更新日志 2023-08-17 10:35:55 +08:00
ddceda1184 mod: 视频播放器依赖升级 2023-08-17 10:12:01 +08:00
163 changed files with 4221 additions and 1529 deletions

23
.github/ISSUE_TEMPLATE/bug-反馈.md vendored Normal file
View File

@ -0,0 +1,23 @@
---
name: Bug 反馈
about: 描述你所遇到的bug
title: ''
labels: 问题反馈
assignees: guozhigq
---
### 问题描述
请提供一个清晰而简明的问题描述。
### 复现步骤
请提供复现该问题所需的具体步骤。
### 预期行为
请描述你期望的正确行为或结果。
### 系统信息
请提供关于您的环境的详细信息,包括操作系统、浏览器版本等。
### 相关截图或日志
如果有的话,请提供相关的截图、错误日志或其他有助于解决问题的信息。

20
.github/ISSUE_TEMPLATE/功能请求.md vendored Normal file
View File

@ -0,0 +1,20 @@
---
name: 功能请求
about: 对于功能的一些建议
title: ''
labels: 功能
assignees: guozhigq
---
### 功能描述
请提供对所请求功能的清晰描述。
### 目标
请描述你希望通过这个功能实现的目标。
### 解决方案
如果你有任何关于如何实现这个功能的想法或建议,请在这里提供。
### 其他
请提供已实现该功能或类似功能的应用

View File

@ -59,16 +59,16 @@ jobs:
id: version
run: echo "version=${GITHUB_REF#refs/tags/v}" >>$GITHUB_OUTPUT
- name: 获取当前日期
id: date
run: echo "date=$(date +'%m%d')" >>$GITHUB_OUTPUT
# - name: 获取当前日期
# id: date
# run: echo "date=$(date +'%m%d')" >>$GITHUB_OUTPUT
- name: 重命名应用 Pili-arm64-v8a-*.*.*.0101.apk
run: |
DATE=${{ steps.date.outputs.date }}
# DATE=${{ steps.date.outputs.date }}
for file in build/app/outputs/flutter-apk/app-*-release.apk; do
if [[ $file =~ app-(.*)-release.apk ]]; then
new_file_name="build/app/outputs/flutter-apk/Pili-${BASH_REMATCH[1]}-${{ steps.version.outputs.version }}($DATE).apk"
new_file_name="build/app/outputs/flutter-apk/Pili-${BASH_REMATCH[1]}-${{ steps.version.outputs.version }}.apk"
mv "$file" "$new_file_name"
fi
done

View File

@ -11,10 +11,11 @@
<img src="https://github.com/guozhigq/pilipala/blob/main/assets/sreenshot/174shots_so.png" width="32%" alt="home" />
<img src="https://github.com/guozhigq/pilipala/blob/main/assets/sreenshot/850shots_so.png" width="32%" alt="home" />
<br/>
<img src="https://github.com/guozhigq/pilipala/blob/main/assets/sreenshot/main_screen.png" width="96%" alt="home" />
<br/>
</div>
### 开发环境
## 开发环境
Xcode 13.4 不支持**auto_orientation**,请注释相关代码
```bash
@ -30,7 +31,7 @@ Xcode 13.4 不支持**auto_orientation**,请注释相关代码
<br/>
### 功能
## 功能
目前着重移动端(Android、iOS)暂时没有适配桌面端、Pad端、手表端等
@ -74,12 +75,14 @@ Xcode 13.4 不支持**auto_orientation**,请注释相关代码
- [ ] 弹幕
- [ ] 字幕
- [x] 记忆播放
- [x] 视频比例:高度/宽度适应、填充、包含等
- [x] 搜索相关
- [x] 热搜
- [x] 搜索历史
- [x] 默认搜索词
- [x] 投稿、番剧、直播间、用户搜索
- [x] 视频搜索排序、按时长筛选
- [x] 视频详情页相关
- [x] 视频选集(分p)切换
@ -96,17 +99,18 @@ Xcode 13.4 不支持**auto_orientation**,请注释相关代码
- [x] 图片质量设定
- [x] 主题模式:亮色/暗色/跟随系统
- [x] 震动反馈(可选)
- [x] 高帧率
- [ ] 等等
<br/>
### 下载
## 下载
可以通过右侧release进行下载或拉取代码到本地进行编译
<br/>
### 声明
## 声明
此项目PiliPala是个人为了兴趣而开发, 仅用于学习和测试。
所用API皆从官方网站收集, 不提供任何破解内容。
@ -115,7 +119,13 @@ Xcode 13.4 不支持**auto_orientation**,请注释相关代码
<br/>
### 致谢
## 技术交流
Telegram https://t.me/+lm_oOVmF0RJiODk1
<br/>
## 致谢
- [bilibili-API-collect](https://github.com/SocialSisterYi/bilibili-API-collect)
- [flutter_meedu_videoplayer](https://github.com/zezo357/flutter_meedu_videoplayer)

1
assets/images/error.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

7
change_log/1.0.1.0817.md Normal file
View File

@ -0,0 +1,7 @@
## 1.0.1
### 修复
+ 升级播放器依赖
+ android平台 AV1格式视频支持
+ 视频全屏功能

19
change_log/1.0.2.0819.md Normal file
View File

@ -0,0 +1,19 @@
## 1.0.2
### 新功能
+ 自动检查更新
+ 封面图片保存
+ 动态跳转番剧
+ 历史记录番剧记忆播放
+ 一键清空稍后再看
### 修复
+ 切换分P cid未切换
+ cookie存储问题
+ 登录/退出登录问题
### 优化
+ 页面空/异常状态样式
+ 退出登录提示
+ 请求节流
+ 全屏播放

19
change_log/1.0.3.0821.md Normal file
View File

@ -0,0 +1,19 @@
## 1.0.3
建议卸载1.0.2版本,重新安装
### 新功能
+ 底部播放进度条设置
+ 复制图片链接
### 修复
+ 用户数据格式修改
+ video Fit
+ 没有audio 资源的视频异常
+ 评论区域图片无法点击
+ 视频进度条拖动问题
### 优化
+ 页面空/异常状态样式
+ 部分页面样式
+ 图片预览页面样式

21
change_log/1.0.4.0822.md Normal file
View File

@ -0,0 +1,21 @@
## 1.0.4
### 新功能
+ 热搜刷新
+ 视频搜索排序、筛选
+ app字体大小自定义
+ app主题色自定义
+ 「课堂」类动态渲染
### 修复
+ 搜索词联想richText渲染异常
+ 部分动态点赞异常
+ 默认视频解码格式
+ 搜索页面返回搜索词未清空
+ 动态详情评论加载异常
+ 动态页面下拉刷新数据异常
### 优化
+ 一些样式修改
+ 取消热搜词缓存

30
change_log/1.0.5.0826.md Normal file
View File

@ -0,0 +1,30 @@
## 1.0.5
主要是bug修复跟一部分小功能弹幕功能需要下一版。
问题反馈请前往QQ频道或提交issues。
感谢🙏酷友「无力感*」「斤斤计较呀」「Pseudopamine」
### 新功能
+ 高帧率支持
+ 默认评论排序设置
+ 默认动态类别设置
+ 动态合集查看
+ 同时观看人数
+ iOS路由切换效果
### 修复
+ 收藏夹翻页
+ 首页搜索框频繁点击消失
+ 评论排序切换空白
+ 快速返回首页
+ 重复进入个人中心页面数据未刷新
+ 动态goods数据异常
+ 大会员切换番剧
+ 高画质codes匹配
### 优化
+ 倍速选择
+ 播放器亮度记忆
+ 下载对应版本apk

View File

@ -5,6 +5,8 @@ PODS:
- device_info_plus (0.0.1):
- Flutter
- Flutter (1.0.0)
- flutter_volume_controller (0.0.1):
- Flutter
- FMDB (2.7.5):
- FMDB/standard (= 2.7.5)
- FMDB/standard (2.7.5)
@ -46,6 +48,7 @@ DEPENDENCIES:
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`)
- flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`)
- image_gallery_saver (from `.symlinks/plugins/image_gallery_saver/ios`)
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
@ -74,6 +77,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/device_info_plus/ios"
Flutter:
:path: Flutter
flutter_volume_controller:
:path: ".symlinks/plugins/flutter_volume_controller/ios"
image_gallery_saver:
:path: ".symlinks/plugins/image_gallery_saver/ios"
media_kit_libs_ios_video:
@ -109,6 +114,7 @@ SPEC CHECKSUMS:
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_volume_controller: e4d5832f08008180f76e30faf671ffd5a425e529
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1

View File

@ -1,9 +1,11 @@
import 'package:flutter/material.dart';
class AnimatedDialog extends StatefulWidget {
const AnimatedDialog({Key? key, required this.child}) : super(key: key);
const AnimatedDialog({Key? key, required this.child, this.closeFn})
: super(key: key);
final Widget child;
final Function? closeFn;
@override
State<StatefulWidget> createState() => AnimatedDialogState();
@ -39,12 +41,16 @@ class AnimatedDialogState extends State<AnimatedDialog>
Widget build(BuildContext context) {
return Material(
color: Colors.black.withOpacity(opacityAnimation!.value),
child: Center(
child: FadeTransition(
opacity: scaleAnimation!,
child: ScaleTransition(
scale: scaleAnimation!,
child: widget.child,
child: InkWell(
splashColor: Colors.transparent,
onTap: () => widget.closeFn!(),
child: Center(
child: FadeTransition(
opacity: scaleAnimation!,
child: ScaleTransition(
scale: scaleAnimation!,
child: widget.child,
),
),
),
),

View File

@ -0,0 +1,351 @@
import 'package:flutter/material.dart';
const double _kPanelHeaderCollapsedHeight = kMinInteractiveDimension;
class _SaltedKey<S, V> extends LocalKey {
const _SaltedKey(this.salt, this.value);
final S salt;
final V value;
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) return false;
return other is _SaltedKey<S, V> &&
other.salt == salt &&
other.value == value;
}
@override
int get hashCode => Object.hash(runtimeType, salt, value);
@override
String toString() {
final String saltString = S == String ? "<'$salt'>" : '<$salt>';
final String valueString = V == String ? "<'$value'>" : '<$value>';
return '[$saltString $valueString]';
}
}
class AppExpansionPanelList extends StatefulWidget {
/// Creates an expansion panel list widget. The [expansionCallback] is
/// triggered when an expansion panel expand/collapse button is pushed.
///
/// The [children] and [animationDuration] arguments must not be null.
const AppExpansionPanelList({
super.key,
required this.children,
this.expansionCallback,
this.animationDuration = kThemeAnimationDuration,
this.expandedHeaderPadding = EdgeInsets.zero,
this.dividerColor,
this.elevation = 2,
}) : _allowOnlyOnePanelOpen = false,
initialOpenPanelValue = null;
/// The children of the expansion panel list. They are laid out in a similar
/// fashion to [ListBody].
final List<AppExpansionPanel> children;
/// The callback that gets called whenever one of the expand/collapse buttons
/// is pressed. The arguments passed to the callback are the index of the
/// pressed panel and whether the panel is currently expanded or not.
///
/// If AppExpansionPanelList.radio is used, the callback may be called a
/// second time if a different panel was previously open. The arguments
/// passed to the second callback are the index of the panel that will close
/// and false, marking that it will be closed.
///
/// For AppExpansionPanelList, the callback needs to setState when it's notified
/// about the closing/opening panel. On the other hand, the callback for
/// AppExpansionPanelList.radio is simply meant to inform the parent widget of
/// changes, as the radio panels' open/close states are managed internally.
///
/// This callback is useful in order to keep track of the expanded/collapsed
/// panels in a parent widget that may need to react to these changes.
final ExpansionPanelCallback? expansionCallback;
/// The duration of the expansion animation.
final Duration animationDuration;
// Whether multiple panels can be open simultaneously
final bool _allowOnlyOnePanelOpen;
/// The value of the panel that initially begins open. (This value is
/// only used when initializing with the [AppExpansionPanelList.radio]
/// constructor.)
final Object? initialOpenPanelValue;
/// The padding that surrounds the panel header when expanded.
///
/// By default, 16px of space is added to the header vertically (above and below)
/// during expansion.
final EdgeInsets expandedHeaderPadding;
/// Defines color for the divider when [AppExpansionPanel.isExpanded] is false.
///
/// If `dividerColor` is null, then [DividerThemeData.color] is used. If that
/// is null, then [ThemeData.dividerColor] is used.
final Color? dividerColor;
/// Defines elevation for the [AppExpansionPanel] while it's expanded.
///
/// By default, the value of elevation is 2.
final double elevation;
@override
State<AppExpansionPanelList> createState() => _AppExpansionPanelListState();
}
class _AppExpansionPanelListState extends State<AppExpansionPanelList> {
ExpansionPanelRadio? _currentOpenPanel;
@override
void initState() {
super.initState();
if (widget._allowOnlyOnePanelOpen) {
assert(_allIdentifiersUnique(),
'All ExpansionPanelRadio identifier values must be unique.');
if (widget.initialOpenPanelValue != null) {
_currentOpenPanel = searchPanelByValue(
widget.children.cast<ExpansionPanelRadio>(),
widget.initialOpenPanelValue);
}
}
}
@override
void didUpdateWidget(AppExpansionPanelList oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget._allowOnlyOnePanelOpen) {
assert(_allIdentifiersUnique(),
'All ExpansionPanelRadio identifier values must be unique.');
// If the previous widget was non-radio AppExpansionPanelList, initialize the
// open panel to widget.initialOpenPanelValue
if (!oldWidget._allowOnlyOnePanelOpen) {
_currentOpenPanel = searchPanelByValue(
widget.children.cast<ExpansionPanelRadio>(),
widget.initialOpenPanelValue);
}
} else {
_currentOpenPanel = null;
}
}
bool _allIdentifiersUnique() {
final Map<Object, bool> identifierMap = <Object, bool>{};
for (final ExpansionPanelRadio child
in widget.children.cast<ExpansionPanelRadio>()) {
identifierMap[child.value] = true;
}
return identifierMap.length == widget.children.length;
}
bool _isChildExpanded(int index) {
if (widget._allowOnlyOnePanelOpen) {
final ExpansionPanelRadio radioWidget =
widget.children[index] as ExpansionPanelRadio;
return _currentOpenPanel?.value == radioWidget.value;
}
return widget.children[index].isExpanded;
}
void _handlePressed(bool isExpanded, int index) {
widget.expansionCallback?.call(index, isExpanded);
if (widget._allowOnlyOnePanelOpen) {
final ExpansionPanelRadio pressedChild =
widget.children[index] as ExpansionPanelRadio;
// If another ExpansionPanelRadio was already open, apply its
// expansionCallback (if any) to false, because it's closing.
for (int childIndex = 0;
childIndex < widget.children.length;
childIndex += 1) {
final ExpansionPanelRadio child =
widget.children[childIndex] as ExpansionPanelRadio;
if (widget.expansionCallback != null &&
childIndex != index &&
child.value == _currentOpenPanel?.value) {
widget.expansionCallback!(childIndex, false);
}
}
setState(() {
_currentOpenPanel = isExpanded ? null : pressedChild;
});
}
}
ExpansionPanelRadio? searchPanelByValue(
List<ExpansionPanelRadio> panels, Object? value) {
for (final ExpansionPanelRadio panel in panels) {
if (panel.value == value) return panel;
}
return null;
}
@override
Widget build(BuildContext context) {
assert(
kElevationToShadow.containsKey(widget.elevation),
'Invalid value for elevation. See the kElevationToShadow constant for'
' possible elevation values.',
);
final List<MergeableMaterialItem> items = <MergeableMaterialItem>[];
for (int index = 0; index < widget.children.length; index += 1) {
//todo: Uncomment to add gap between selected panels
/*if (_isChildExpanded(index) && index != 0 && !_isChildExpanded(index - 1))
items.add(MaterialGap(key: _SaltedKey<BuildContext, int>(context, index * 2 - 1)));*/
final AppExpansionPanel child = widget.children[index];
final Widget headerWidget = child.headerBuilder(
context,
_isChildExpanded(index),
);
Widget? expandIconContainer = ExpandIcon(
isExpanded: _isChildExpanded(index),
onPressed: !child.canTapOnHeader
? (bool isExpanded) => _handlePressed(isExpanded, index)
: null,
);
if (!child.canTapOnHeader) {
final MaterialLocalizations localizations =
MaterialLocalizations.of(context);
expandIconContainer = Semantics(
label: _isChildExpanded(index)
? localizations.expandedIconTapHint
: localizations.collapsedIconTapHint,
container: true,
child: expandIconContainer,
);
}
final iconContainer = child.iconBuilder;
if (iconContainer != null) {
expandIconContainer = iconContainer(
expandIconContainer,
_isChildExpanded(index),
);
}
Widget header = Row(
children: <Widget>[
Expanded(
child: AnimatedContainer(
duration: widget.animationDuration,
curve: Curves.fastOutSlowIn,
margin: _isChildExpanded(index)
? widget.expandedHeaderPadding
: EdgeInsets.zero,
child: ConstrainedBox(
constraints: const BoxConstraints(
minHeight: _kPanelHeaderCollapsedHeight),
child: headerWidget,
),
),
),
if (expandIconContainer != null) expandIconContainer,
],
);
if (child.canTapOnHeader) {
header = MergeSemantics(
child: InkWell(
onTap: () => _handlePressed(_isChildExpanded(index), index),
child: header,
),
);
}
items.add(
MaterialSlice(
key: _SaltedKey<BuildContext, int>(context, index * 2),
color: child.backgroundColor,
child: Column(
children: <Widget>[
header,
AnimatedCrossFade(
firstChild: Container(height: 0.0),
secondChild: child.body,
firstCurve:
const Interval(0.0, 0.6, curve: Curves.fastOutSlowIn),
secondCurve:
const Interval(0.4, 1.0, curve: Curves.fastOutSlowIn),
sizeCurve: Curves.fastOutSlowIn,
crossFadeState: _isChildExpanded(index)
? CrossFadeState.showSecond
: CrossFadeState.showFirst,
duration: widget.animationDuration,
),
],
),
),
);
if (_isChildExpanded(index) && index != widget.children.length - 1) {
items.add(MaterialGap(
key: _SaltedKey<BuildContext, int>(context, index * 2 + 1)));
}
}
return MergeableMaterial(
hasDividers: true,
dividerColor: widget.dividerColor,
elevation: widget.elevation,
children: items,
);
}
}
typedef ExpansionPanelIconBuilder = Widget? Function(
Widget child,
bool isExpanded,
);
class AppExpansionPanel {
/// Creates an expansion panel to be used as a child for [ExpansionPanelList].
/// See [ExpansionPanelList] for an example on how to use this widget.
///
/// The [headerBuilder], [body], and [isExpanded] arguments must not be null.
AppExpansionPanel({
required this.headerBuilder,
required this.body,
this.iconBuilder,
this.isExpanded = false,
this.canTapOnHeader = false,
this.backgroundColor,
});
/// The widget builder that builds the expansion panels' header.
final ExpansionPanelHeaderBuilder headerBuilder;
/// The widget builder that builds the expansion panels' icon.
///
/// If not pass any function, then default icon will be displayed.
///
/// If builder function return null, then icon will not displayed.
final ExpansionPanelIconBuilder? iconBuilder;
/// The body of the expansion panel that's displayed below the header.
///
/// This widget is visible only when the panel is expanded.
final Widget body;
/// Whether the panel is expanded.
///
/// Defaults to false.
final bool isExpanded;
/// Whether tapping on the panel's header will expand/collapse it.
///
/// Defaults to false.
final bool canTapOnHeader;
/// Defines the background color of the panel.
///
/// Defaults to [ThemeData.cardColor].
final Color? backgroundColor;
}

View File

@ -1,31 +1,41 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
class HttpError extends StatelessWidget {
const HttpError({required this.errMsg, required this.fn, super.key});
const HttpError(
{required this.errMsg, required this.fn, this.btnText, super.key});
final String? errMsg;
final Function()? fn;
final String? btnText;
@override
Widget build(BuildContext context) {
return SliverToBoxAdapter(
child: SizedBox(
height: 150,
height: 400,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
SvgPicture.asset(
"assets/images/error.svg",
height: 200,
),
const SizedBox(height: 20),
Text(
errMsg ?? '请求异常',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleMedium,
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 10),
ElevatedButton(
onPressed: () {
fn!();
},
child: const Text('点击重试'))
const SizedBox(height: 30),
OutlinedButton.icon(
onPressed: () {
fn!();
},
icon: const Icon(Icons.arrow_forward_outlined, size: 20),
label: Text(btnText ?? '点击重试'),
)
],
),
),

View File

@ -1,8 +1,6 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/pages/rcmd/controller.dart';
import 'package:pilipala/utils/utils.dart';
class LiveCard extends StatelessWidget {
@ -95,7 +93,7 @@ class LiveContent extends StatelessWidget {
liveItem.title,
textAlign: TextAlign.start,
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
maxLines: Get.find<RcmdController>().crossAxisCount,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
SizedBox(

View File

@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
class NoData extends StatelessWidget {
const NoData({super.key});
@override
Widget build(BuildContext context) {
return SliverToBoxAdapter(
child: SizedBox(
height: 400,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
SvgPicture.asset(
"assets/images/error.svg",
height: 200,
),
const SizedBox(height: 20),
Text(
'没有数据',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleSmall,
),
],
),
),
);
}
}

View File

@ -1,42 +1,82 @@
import 'package:flutter/material.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/utils/download.dart';
class OverlayPop extends StatelessWidget {
final dynamic videoItem;
const OverlayPop({super.key, this.videoItem});
final Function? closeFn;
const OverlayPop({super.key, this.videoItem, this.closeFn});
@override
Widget build(BuildContext context) {
double imgWidth = MediaQuery.of(context).size.width - 8 * 2;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 8.0),
margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(6.0),
borderRadius: BorderRadius.circular(10.0),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(6.0),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
NetworkImgLayer(
width: (MediaQuery.of(context).size.width - 16),
height: (MediaQuery.of(context).size.width - 16) /
StyleString.aspectRatio,
src: videoItem.pic!,
),
Padding(
padding: const EdgeInsets.fromLTRB(12, 15, 10, 15),
child: Text(
videoItem.title!,
// maxLines: 1,
// overflow: TextOverflow.ellipsis,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
children: [
NetworkImgLayer(
width: imgWidth,
height: imgWidth / StyleString.aspectRatio,
src: videoItem.pic!,
quality: 100,
),
),
],
),
Positioned(
right: 8,
top: 8,
child: Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.3),
borderRadius:
const BorderRadius.all(Radius.circular(20))),
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () => closeFn!(),
icon: const Icon(
Icons.close,
size: 18,
color: Colors.white,
),
),
),
),
],
),
Padding(
padding: const EdgeInsets.fromLTRB(12, 10, 8, 10),
child: Row(
children: [
Expanded(
child: Text(
videoItem.title!,
),
),
const SizedBox(width: 4),
IconButton(
tooltip: '保存封面图',
onPressed: () async {
await DownloadUtils.downloadImg(
videoItem.pic ?? videoItem.cover);
// closeFn!();
},
icon: const Icon(Icons.download, size: 20),
)
],
)),
],
),
);
}

View File

@ -37,11 +37,11 @@ class VideoCardH extends StatelessWidget {
longPress!();
}
},
onLongPressEnd: (details) {
if (longPressEnd != null) {
longPressEnd!();
}
},
// onLongPressEnd: (details) {
// if (longPressEnd != null) {
// longPressEnd!();
// }
// },
child: InkWell(
onTap: () async {
try {
@ -55,11 +55,14 @@ class VideoCardH extends StatelessWidget {
},
child: Padding(
padding: const EdgeInsets.fromLTRB(
StyleString.safeSpace, 7, StyleString.safeSpace, 7),
StyleString.safeSpace, 5, StyleString.safeSpace, 5),
child: LayoutBuilder(
builder: (context, boxConstraints) {
double width =
(boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2;
double width = (boxConstraints.maxWidth -
StyleString.cardSpace *
6 /
MediaQuery.of(context).textScaleFactor) /
2;
return Container(
constraints: const BoxConstraints(minHeight: 88),
height: width / StyleString.aspectRatio,
@ -123,7 +126,7 @@ class VideoContent extends StatelessWidget {
Widget build(BuildContext context) {
return Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(10, 2, 6, 0),
padding: const EdgeInsets.fromLTRB(10, 0, 6, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -132,7 +135,6 @@ class VideoContent extends StatelessWidget {
videoItem.title,
textAlign: TextAlign.start,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
),
maxLines: 2,
@ -147,7 +149,6 @@ class VideoContent extends StatelessWidget {
TextSpan(
text: i['text'],
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
letterSpacing: 0.3,
color: i['type'] == 'em'
@ -177,7 +178,7 @@ class VideoContent extends StatelessWidget {
// color: Theme.of(context).colorScheme.surfaceTint),
// ),
// ),
const SizedBox(height: 4),
// const SizedBox(height: 4),
Row(
children: [
Text(

View File

@ -14,8 +14,7 @@ import 'package:pilipala/common/widgets/network_img_layer.dart';
// 视频卡片 - 垂直布局
class VideoCardV extends StatelessWidget {
// ignore: prefer_typing_uninitialized_variables
final videoItem;
final dynamic videoItem;
final Function()? longPress;
final Function()? longPressEnd;
@ -61,6 +60,16 @@ class VideoCardV extends StatelessWidget {
'heroTag': heroTag,
});
break;
default:
SmartDialog.showToast(videoItem.goto);
Get.toNamed(
'/webview',
parameters: {
'url': videoItem.uri,
'type': 'url',
'pageTitle': videoItem.title,
},
);
}
}
@ -68,11 +77,8 @@ class VideoCardV extends StatelessWidget {
Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(videoItem.id);
return Card(
elevation: 0,
elevation: 1,
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: StyleString.mdRadius,
),
margin: EdgeInsets.zero,
child: GestureDetector(
onLongPress: () {
@ -80,57 +86,29 @@ class VideoCardV extends StatelessWidget {
longPress!();
}
},
onLongPressEnd: (details) {
if (longPressEnd != null) {
longPressEnd!();
}
},
// onLongPressEnd: (details) {
// if (longPressEnd != null) {
// longPressEnd!();
// }
// },
child: InkWell(
onTap: () async => onPushDetail(heroTag),
child: Column(
children: [
ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: StyleString.imgRadius,
topRight: StyleString.imgRadius,
bottomLeft: StyleString.imgRadius,
bottomRight: StyleString.imgRadius,
),
child: AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(builder: (context, boxConstraints) {
double maxWidth = boxConstraints.maxWidth;
double maxHeight = boxConstraints.maxHeight;
return Stack(
children: [
Hero(
tag: heroTag,
child: NetworkImgLayer(
src: videoItem.pic,
width: maxWidth,
height: maxHeight,
),
),
// if (videoItem.stat.view is int &&
// videoItem.stat.danmaku is int)
// Positioned(
// left: 0,
// right: 0,
// bottom: 0,
// child: AnimatedOpacity(
// opacity: 1,
// duration: const Duration(milliseconds: 200),
// child: VideoStat(
// view: videoItem.stat.view,
// danmaku: videoItem.stat.danmaku,
// duration: videoItem.duration,
// ),
// ),
// ),
],
);
}),
),
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(builder: (context, boxConstraints) {
double maxWidth = boxConstraints.maxWidth;
double maxHeight = boxConstraints.maxHeight;
return Hero(
tag: heroTag,
child: NetworkImgLayer(
src: videoItem.pic,
width: maxWidth,
height: maxHeight,
),
);
}),
),
VideoContent(videoItem: videoItem)
],
@ -148,15 +126,13 @@ class VideoContent extends StatelessWidget {
Widget build(BuildContext context) {
return Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(4, 8, 0, 3),
padding: const EdgeInsets.fromLTRB(9, 8, 9, 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
videoItem.title,
textAlign: TextAlign.start,
style: const TextStyle(fontSize: 13),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
@ -181,22 +157,25 @@ class VideoContent extends StatelessWidget {
type: 'color',
)
],
if (videoItem.goto == 'picture') ...[
const PBadge(
text: '动态',
stack: 'normal',
size: 'small',
type: 'line',
fs: 9,
)
],
Expanded(
child: LayoutBuilder(builder:
(BuildContext context, BoxConstraints constraints) {
return SizedBox(
width: constraints.maxWidth,
child: Text(
videoItem.owner.name,
maxLines: 1,
style: TextStyle(
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
);
}),
child: Text(
videoItem.owner.name,
maxLines: 1,
style: TextStyle(
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
),
if (videoItem.goto == 'av')
SizedBox(

View File

@ -248,6 +248,9 @@ class Api {
// 移除已观看
static const String toViewDel = '/x/v2/history/toview/del';
// 清空稍后再看
static const String toViewClear = '/x/v2/history/toview/clear';
// 追番
static const String bangumiAdd = '/pgc/web/follow/add';
@ -285,4 +288,8 @@ class Api {
// github 获取最新版
static const String latestApp =
'https://api.github.com/repos/guozhigq/pilipala/releases/latest';
// 多少人在看
// https://api.bilibili.com/x/player/online/total?aid=913663681&cid=1203559746&bvid=BV1MM4y1s7NZ&ts=56427838
static const String onlineTotal = '/x/player/online/total';
}

View File

@ -22,10 +22,18 @@ class DynamicsHttp {
}
var res = await Request().get(Api.followDynamic, data: data);
if (res.data['code'] == 0) {
return {
'status': true,
'data': DynamicsDataModel.fromJson(res.data['data']),
};
try {
return {
'status': true,
'data': DynamicsDataModel.fromJson(res.data['data']),
};
} catch (err) {
return {
'status': false,
'data': [],
'msg': err.toString(),
};
}
} else {
return {
'status': false,

View File

@ -15,21 +15,12 @@ import 'package:dio_cookie_manager/dio_cookie_manager.dart';
class Request {
static final Request _instance = Request._internal();
static late CookieManager cookieManager;
static late final Dio dio;
factory Request() => _instance;
static Dio dio = Dio()
..httpClientAdapter = Http2Adapter(
ConnectionManager(
idleTimeout: const Duration(milliseconds: 10000),
// Ignore bad certificate
onClientCreate: (_, config) => config.onBadCertificate = (_) => true,
),
);
/// 设置cookie
static setCookie() async {
Box user = GStrorage.user;
Box userInfoCache = GStrorage.userInfo;
var cookiePath = await Utils.getCookiePath();
var cookieJar = PersistCookieJar(
ignoreExpires: true,
@ -39,7 +30,8 @@ class Request {
dio.interceptors.add(cookieManager);
var cookie = await cookieManager.cookieJar
.loadForRequest(Uri.parse(HttpString.baseUrl));
if (user.get(UserBoxKey.userMid) != null) {
var userInfo = userInfoCache.get('userInfoCache');
if (userInfo != null && userInfo.mid != null) {
var cookie2 = await cookieManager.cookieJar
.loadForRequest(Uri.parse(HttpString.tUrl));
if (cookie2.isEmpty) {
@ -63,16 +55,6 @@ class Request {
dio.options.headers['cookie'] = cookieString;
}
// 移除cookie
static removeCookie() async {
await cookieManager.cookieJar
.saveFromResponse(Uri.parse(HttpString.baseUrl), []);
await cookieManager.cookieJar
.saveFromResponse(Uri.parse(HttpString.baseApiUrl), []);
cookieManager.cookieJar.deleteAll();
dio.interceptors.add(cookieManager);
}
// 从cookie中获取 csrf token
static Future<String> getCsrf() async {
var cookies = await cookieManager.cookieJar
@ -105,16 +87,26 @@ class Request {
},
);
Box user = GStrorage.user;
if (user.get(UserBoxKey.userMid) != null) {
options.headers['x-bili-mid'] = user.get(UserBoxKey.userMid).toString();
Box userInfoCache = GStrorage.userInfo;
var userInfo = userInfoCache.get('userInfoCache');
if (userInfo != null && userInfo.mid != null) {
options.headers['x-bili-mid'] = userInfo.mid.toString();
options.headers['env'] = 'prod';
options.headers['app-key'] = 'android64';
options.headers['x-bili-aurora-eid'] = 'UlMFQVcABlAH';
options.headers['x-bili-aurora-zone'] = 'sh001';
options.headers['referer'] = 'https://www.bilibili.com/';
}
dio.options = options;
dio = Dio(options)
..httpClientAdapter = Http2Adapter(
ConnectionManager(
idleTimeout: const Duration(milliseconds: 10000),
// Ignore bad certificate
onClientCreate: (_, config) => config.onBadCertificate = (_) => true,
),
);
//添加拦截器
dio.interceptors.add(ApiInterceptor());

View File

@ -17,7 +17,7 @@ class ApiInterceptor extends Interceptor {
handler.next(options);
}
Box setting = GStrorage.setting;
Box localCache = GStrorage.localCache;
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
@ -29,7 +29,8 @@ class ApiInterceptor extends Interceptor {
final uri = Uri.parse(locations.first);
final accessKey = uri.queryParameters['access_key'];
final mid = uri.queryParameters['mid'];
setting.put(UserBoxKey.accessKey, {'mid': mid, 'value': accessKey});
localCache
.put(LocalCacheKey.accessKey, {'mid': mid, 'value': accessKey});
}
}
}

View File

@ -46,14 +46,19 @@ class SearchHttp {
required SearchType searchType,
required String keyword,
required page,
String? order,
int? duration,
}) async {
var res = await Request().get(Api.searchByType, data: {
var reqData = {
'search_type': searchType.type,
'keyword': keyword,
// 'order_sort': 0,
// 'user_type': 0,
'page': page
});
'page': page,
if (order != null) 'order': order,
if (duration != null) 'duration': duration,
};
var res = await Request().get(Api.searchByType, data: reqData);
if (res.data['code'] == 0 && res.data['data']['numPages'] > 0) {
Object data;
switch (searchType) {

View File

@ -1,3 +1,4 @@
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/http/api.dart';
import 'package:pilipala/http/init.dart';
@ -50,10 +51,17 @@ class UserHttp {
'up_mid': mid,
});
if (res.data['code'] == 0) {
FavFolderData data = FavFolderData.fromJson(res.data['data']);
return {'status': true, 'data': data};
late FavFolderData data;
if (res.data['data'] != null) {
data = FavFolderData.fromJson(res.data['data']);
return {'status': true, 'data': data};
}
} else {
return {'status': false, 'data': [], 'msg': '账号未登录'};
return {
'status': false,
'data': [],
'msg': res.data['message'] ?? '账号未登录'
};
}
}
@ -85,6 +93,12 @@ class UserHttp {
static Future<dynamic> seeYouLater() async {
var res = await Request().get(Api.seeYouLater);
if (res.data['code'] == 0) {
if (res.data['data']['count'] == 0) {
return {
'status': true,
'data': {'list': [], 'count': 0}
};
}
List<HotVideoItemModel> list = [];
for (var i in res.data['data']['list']) {
list.add(HotVideoItemModel.fromJson(i));
@ -191,8 +205,28 @@ class UserHttp {
'sign': Constants.thirdSign,
},
);
if (res.data['code'] == 0 && res.data['data']['has_login'] == 1) {
Request().get(res.data['data']['confirm_uri']);
try {
if (res.data['code'] == 0 && res.data['data']['has_login'] == 1) {
Request().get(res.data['data']['confirm_uri']);
}
} catch (err) {
SmartDialog.showNotify(msg: '获取用户凭证: $err', notifyType: NotifyType.error);
}
}
// 清空稍后再看
static Future toViewClear() async {
var res = await Request().post(
Api.toViewClear,
queryParameters: {
'jsonp': 'jsonp',
'csrf': await Request.getCsrf(),
},
);
if (res.data['code'] == 0) {
return {'status': true, 'msg': '操作完成'};
} else {
return {'status': false, 'msg': res.data['message']};
}
}
}

View File

@ -18,6 +18,7 @@ import 'package:pilipala/utils/storage.dart';
/// 返回{'status': bool, 'data': List}
/// view层根据 status 判断渲染逻辑
class VideoHttp {
static Box localCache = GStrorage.localCache;
static Box setting = GStrorage.setting;
// 首页推荐视频
@ -60,8 +61,9 @@ class VideoHttp {
'device_name': 'vivo',
'pull': freshIdx == 0 ? 'true' : 'false',
'appkey': Constants.appKey,
'access_key':
setting.get(UserBoxKey.accessKey, defaultValue: {})['value'] ?? ''
'access_key': localCache
.get(LocalCacheKey.accessKey, defaultValue: {})['value'] ??
''
},
);
if (res.data['code'] == 0) {
@ -136,7 +138,12 @@ class VideoHttp {
'data': PlayUrlModel.fromJson(res.data['data'])
};
} else {
return {'status': false, 'data': []};
return {
'status': false,
'data': [],
'code': res.data['code'],
'msg': res.data['message'],
};
}
} catch (err) {
return {'status': false, 'data': [], 'msg': err};
@ -153,13 +160,14 @@ class VideoHttp {
Map errMap = {
-400: '请求错误',
-403: '权限不足',
-404: '视频',
-404: '视频资源失效',
62002: '稿件不可见',
62004: '稿件审核中',
};
return {
'status': false,
'data': null,
'code': result.code,
'msg': errMap[result.code] ?? '请求异常',
};
}
@ -391,4 +399,16 @@ class VideoHttp {
return {'status': false, 'msg': res.data['result']['toast']};
}
}
// 查看视频同时在看人数
static Future onlineTotal({int? aid, String? bvid, int? cid}) async {
var res = await Request().get(Api.onlineTotal, data: {
'aid': aid,
'bvid': bvid,
'cid': cid,
});
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
}
}
}

View File

@ -7,6 +7,7 @@ import 'package:dynamic_color/dynamic_color.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/common/widgets/custom_toast.dart';
import 'package:pilipala/http/init.dart';
import 'package:pilipala/models/common/color_type.dart';
import 'package:pilipala/models/common/theme_type.dart';
import 'package:pilipala/pages/search/index.dart';
import 'package:pilipala/pages/video/detail/index.dart';
@ -27,6 +28,13 @@ void main() async {
await Request.setCookie();
await Data.init();
await GStrorage.lazyInit();
// 小白条、导航栏沉浸
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
systemNavigationBarColor: Colors.transparent,
systemNavigationBarDividerColor: Colors.transparent,
statusBarColor: Colors.transparent,
));
});
}
@ -35,15 +43,27 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
Color brandColor = const Color.fromARGB(255, 92, 182, 123);
Box setting = GStrorage.setting;
// 主题色
Color defaultColor =
colorThemeTypes[setting.get(SettingBoxKey.customColor, defaultValue: 0)]
['color'];
Color brandColor = defaultColor;
// 主题模式
ThemeType currentThemeValue = ThemeType.values[setting
.get(SettingBoxKey.themeMode, defaultValue: ThemeType.system.code)];
// 是否动态取色
bool isDynamicColor =
setting.get(SettingBoxKey.dynamicColor, defaultValue: true);
// 字体缩放大小
double textScale =
setting.get(SettingBoxKey.defaultTextScale, defaultValue: 1.0);
return DynamicColorBuilder(
builder: ((ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
ColorScheme? lightColorScheme;
ColorScheme? darkColorScheme;
if (lightDynamic != null && darkDynamic != null) {
if (lightDynamic != null && darkDynamic != null && isDynamicColor) {
// dynamic取色成功
lightColorScheme = lightDynamic.harmonized();
darkColorScheme = darkDynamic.harmonized();
@ -93,9 +113,17 @@ class MyApp extends StatelessWidget {
fallbackLocale: const Locale("zh", "CN"),
getPages: Routes.getPages,
home: const MainApp(),
builder: FlutterSmartDialog.init(
toastBuilder: (String msg) => CustomToast(msg: msg),
),
builder: (BuildContext context, Widget? child) {
return FlutterSmartDialog(
toastBuilder: (String msg) => CustomToast(msg: msg),
child: MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaleFactor:
MediaQuery.of(context).textScaleFactor * textScale),
child: child!,
),
);
},
navigatorObservers: [
VideoDetailPage.routeObserver,
SearchPage.routeObserver,

View File

@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
final List<Map<String, dynamic>> colorThemeTypes = [
{'color': const Color.fromARGB(255, 92, 182, 123), 'label': '默认绿'},
{'color': Colors.pink, 'label': '粉红色'},
{'color': Colors.red, 'label': '红色'},
{'color': Colors.orange, 'label': '橙色'},
{'color': Colors.amber, 'label': '琥珀色'},
{'color': Colors.yellow, 'label': '黄色'},
{'color': Colors.lime, 'label': '酸橙色'},
{'color': Colors.lightGreen, 'label': '浅绿色'},
{'color': Colors.green, 'label': '绿色'},
{'color': Colors.teal, 'label': '青色'},
{'color': Colors.cyan, 'label': '蓝绿色'},
{'color': Colors.lightBlue, 'label': '浅蓝色'},
{'color': Colors.blue, 'label': '蓝色'},
{'color': Colors.indigo, 'label': '靛蓝色'},
{'color': Colors.purple, 'label': '紫色'},
{'color': Colors.deepPurple, 'label': '深紫色'},
{'color': Colors.blueGrey, 'label': '蓝灰色'},
{'color': Colors.brown, 'label': '棕色'},
{'color': Colors.grey, 'label': '灰色'},
];

View File

@ -27,3 +27,20 @@ extension SearchTypeExtension on SearchType {
['video', 'media_bangumi', 'live_room', 'bili_user'][index];
String get label => ['视频', '番剧', '直播间', '用户'][index];
}
// 搜索类型为视频、专栏及相簿时
enum ArchiveFilterType {
totalrank,
click,
pubdate,
dm,
stow,
scores,
// 专栏
// attention,
}
extension ArchiveFilterTypeExtension on ArchiveFilterType {
String get description =>
['默认排序', '播放多', '新发布', '弹幕多', '收藏多', '评论多', '最多喜欢'][index];
}

View File

@ -360,7 +360,7 @@ class GoodItem {
String? brief;
String? cover;
String? id;
dynamic id;
String? jumpDesc;
String? jumpUrl;
String? name;
@ -408,6 +408,7 @@ class DynamicMajorModel {
this.live,
this.none,
this.type,
this.courses,
});
DynamicArchiveModel? archive;
@ -422,6 +423,7 @@ class DynamicMajorModel {
// MAJOR_TYPE_ARCHIVE 视频
// MAJOR_TYPE_OPUS 图文/文章
String? type;
Map? courses;
DynamicMajorModel.fromJson(Map<String, dynamic> json) {
archive = json['archive'] != null
@ -444,6 +446,7 @@ class DynamicMajorModel {
none =
json['none'] != null ? DynamicNoneModel.fromJson(json['none']) : null;
type = json['type'];
courses = json['courses'] ?? {};
}
}
@ -478,6 +481,8 @@ class DynamicArchiveModel {
this.stat,
this.title,
this.type,
this.epid,
this.seasonId,
});
int? aid;
@ -491,6 +496,8 @@ class DynamicArchiveModel {
Stat? stat;
String? title;
int? type;
int? epid;
int? seasonId;
DynamicArchiveModel.fromJson(Map<String, dynamic> json) {
aid = json['aid'] is String ? int.parse(json['aid']) : json['aid'];
@ -503,6 +510,8 @@ class DynamicArchiveModel {
stat = json['stat'] != null ? Stat.fromJson(json['stat']) : null;
title = json['title'];
type = json['type'];
epid = json['epid'];
seasonId = json['season_id'];
}
}

View File

@ -8,7 +8,9 @@ class FollowUpModel {
List<UpItem>? upList;
FollowUpModel.fromJson(Map<String, dynamic> json) {
liveUsers = LiveUsers.fromJson(json['live_users']);
liveUsers = json['live_users'] != null
? LiveUsers.fromJson(json['live_users'])
: null;
upList = json['up_list'] != null
? json['up_list'].map<UpItem>((e) => UpItem.fromJson(e)).toList()
: [];

View File

@ -4,12 +4,14 @@ class LatestDataModel {
this.tagName,
this.createdAt,
this.assets,
this.body,
});
String? url;
String? tagName;
String? createdAt;
List? assets;
String? body;
LatestDataModel.fromJson(Map<String, dynamic> json) {
url = json['url'];
@ -17,6 +19,7 @@ class LatestDataModel {
createdAt = json['created_at'];
assets =
json['assets'].map<AssetItem>((e) => AssetItem.fromJson(e)).toList();
body = json['body'];
}
}

View File

@ -1,3 +1,6 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class SearchSuggestModel {
SearchSuggestModel({
this.tag,
@ -19,32 +22,74 @@ class SearchSuggestItem {
SearchSuggestItem({
this.value,
this.term,
this.name,
this.spid,
this.textRich,
});
String? value;
String? term;
List? name;
int? spid;
Widget? textRich;
SearchSuggestItem.fromJson(Map<String, dynamic> json, String inputTerm) {
value = json['value'];
term = json['term'];
String reg = '<em class="suggest_high_light">$inputTerm</em>';
try {
if (json['name'].indexOf(inputTerm) != -1) {
String str = json['name'].replaceAll(reg, '^');
List arr = str.split('^');
arr.insert(arr.length - 1, inputTerm);
name = arr;
} else {
name = ['', '', json['term']];
}
} catch (err) {
name = ['', '', json['term']];
}
spid = json['spid'];
textRich = highlightText(json['name']);
}
}
Widget highlightText(String str) {
// 创建正则表达式,匹配 <em class="suggest_high_light">...</em> 格式的文本
RegExp regex = RegExp(r'<em class="suggest_high_light">(.*?)<\/em>');
// 用于存储每个匹配项的列表
List<InlineSpan> children = [];
// 获取所有匹配项
Iterable<Match> matches = regex.allMatches(str);
// 当前索引位置
int currentIndex = 0;
// 遍历每个匹配项
for (var match in matches) {
// 获取当前匹配项之前的普通文本部分
String normalText = str.substring(currentIndex, match.start);
// 获取需要高亮显示的文本部分
String highlightedText = match.group(1)!;
// 如果普通文本部分不为空,则将其添加到 children 列表中
if (normalText.isNotEmpty) {
children.add(TextSpan(
text: normalText,
style: DefaultTextStyle.of(Get.context!).style,
));
}
// 将需要高亮显示的文本部分添加到 children 列表中,并设置相应样式
children.add(TextSpan(
text: highlightedText,
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(Get.context!).colorScheme.primary),
));
// 更新当前索引位置
currentIndex = match.end;
}
// 如果当前索引位置小于文本长度,表示还有剩余的普通文本部分
if (currentIndex < str.length) {
String remainingText = str.substring(currentIndex);
// 将剩余的普通文本部分添加到 children 列表中
children.add(TextSpan(
text: remainingText,
style: DefaultTextStyle.of(Get.context!).style,
));
}
// 使用 Text.rich 创建包含高亮显示的富文本小部件,并返回
return Text.rich(TextSpan(children: children));
}

View File

@ -121,7 +121,7 @@ class HisListItem {
viewAt = json['view_at'];
progress = json['progress'];
badge = json['badge'];
showTitle = json['show_title'];
showTitle = json['show_title'] == '' ? null : json['show_title'];
duration = json['duration'];
current = json['current'];
total = json['total'];

View File

@ -43,7 +43,7 @@ class UserInfoData {
@HiveField(5)
int? mobileVerified;
@HiveField(6)
int? money;
double? money;
@HiveField(7)
int? moral;
@HiveField(8)
@ -88,7 +88,7 @@ class UserInfoData {
: LevelInfo();
mid = json['mid'];
mobileVerified = json['mobile_verified'];
money = json['money'];
money = json['money'] is int ? json['money'].toDouble() : json['money'];
moral = json['moral'];
official = json['official'];
officialVerify = json['officialVerify'];
@ -130,6 +130,7 @@ class LevelInfo {
currentLevel = json['current_level'];
currentMin = json['current_min'];
currentExp = json['current_exp'];
nextExp = json['next_exp'];
nextExp =
json['current_level'] == 6 ? json['current_exp'] : json['next_exp'];
}
}

View File

@ -23,7 +23,7 @@ class UserInfoDataAdapter extends TypeAdapter<UserInfoData> {
levelInfo: fields[3] as LevelInfo?,
mid: fields[4] as int?,
mobileVerified: fields[5] as int?,
money: fields[6] as int?,
money: fields[6] as double?,
moral: fields[7] as int?,
official: (fields[8] as Map?)?.cast<dynamic, dynamic>(),
officialVerify: (fields[9] as Map?)?.cast<dynamic, dynamic>(),

View File

@ -93,26 +93,19 @@ extension AudioQualityDesc on AudioQuality {
}
enum VideoDecodeFormats {
DVH1,
AV1,
HEVC,
AVC,
}
extension VideoDecodeFormatsDesc on VideoDecodeFormats {
static final List<String> _descList = [
'AV1',
'HEVC',
'AVC',
];
static final List<String> _descList = ['DVH1', 'AV1', 'HEVC', 'AVC'];
get description => _descList[index];
}
extension VideoDecodeFormatsCode on VideoDecodeFormats {
static final List<String> _codeList = [
'av01',
'hev1',
'avc1',
];
static final List<String> _codeList = ['dvh1', 'av01', 'hev1', 'avc1'];
get code => _codeList[index];
static VideoDecodeFormats? fromCode(String code) {

View File

@ -6,7 +6,6 @@ import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path_provider/path_provider.dart';
import 'package:pilipala/http/index.dart';
import 'package:pilipala/models/github/latest.dart';
import 'package:pilipala/utils/utils.dart';
@ -48,6 +47,11 @@ class _AboutPageState extends State<AboutPage> {
'PiliPala',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 6),
Text(
'使用Flutter开发的哔哩哔哩第三方客户端',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
),
const SizedBox(height: 20),
Obx(
() => ListTile(
@ -83,16 +87,6 @@ class _AboutPageState extends State<AboutPage> {
height: 30,
color: Theme.of(context).colorScheme.onInverseSurface,
),
ListTile(
onTap: () {},
title: const Text('作者'),
trailing: Text('guozhigq', style: subTitleStyle),
),
ListTile(
onTap: () {},
title: const Text('酷安'),
trailing: Text('影若风', style: subTitleStyle),
),
ListTile(
onTap: () => _aboutController.githubUrl(),
title: const Text('Github'),
@ -124,6 +118,11 @@ class _AboutPageState extends State<AboutPage> {
title: const Text('TG频道'),
trailing: Icon(Icons.arrow_forward_ios, size: 16, color: outline),
),
ListTile(
onTap: () => _aboutController.aPay(),
title: const Text('赞助'),
trailing: Icon(Icons.arrow_forward_ios, size: 16, color: outline),
),
Divider(
thickness: 8,
height: 30,
@ -142,11 +141,12 @@ class AboutController extends GetxController {
late LatestDataModel remoteAppInfo;
RxBool isUpdate = true.obs;
RxBool isLoading = true.obs;
late LatestDataModel data;
@override
void onInit() {
super.onInit();
init();
// init();
// 获取当前版本
getCurrentApp();
// 获取最新的版本
@ -154,18 +154,18 @@ class AboutController extends GetxController {
}
// 获取设备信息
Future init() async {
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
if (Platform.isAndroid) {
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
print(androidInfo.supportedAbis);
} else if (Platform.isIOS) {
IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
print(iosInfo);
}
}
// Future init() async {
// DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
// if (Platform.isAndroid) {
// AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
// print(androidInfo.supportedAbis);
// } else if (Platform.isIOS) {
// IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
// print(iosInfo);
// }
// }
// 获取当前版本
// 获取当前版本
Future getCurrentApp() async {
var result = await PackageInfo.fromPlatform();
currentVersion.value = result.version;
@ -174,7 +174,7 @@ class AboutController extends GetxController {
// 获取远程版本
Future getRemoteApp() async {
var result = await Request().get(Api.latestApp);
LatestDataModel data = LatestDataModel.fromJson(result.data);
data = LatestDataModel.fromJson(result.data);
remoteAppInfo = data;
remoteVersion.value = data.tagName!;
isUpdate.value =
@ -184,15 +184,7 @@ class AboutController extends GetxController {
// 跳转下载/本地更新
Future onUpdate() async {
// final dir = await getApplicationSupportDirectory();
// final path = '${dir.path}/pilipala.apk';
// var result = await Request()
// .downloadFile(remoteAppInfo.assets!.first.downloadUrl, path);
// print(result);
launchUrl(
Uri.parse('https://github.com/guozhigq/pilipala/releases'),
mode: LaunchMode.externalApplication,
);
Utils.matchVersion(data);
}
// 跳转github
@ -243,4 +235,16 @@ class AboutController extends GetxController {
),
);
}
aPay() {
try {
launchUrl(
Uri.parse(
'alipayqr://platformapi/startapp?saId=10000007&qrcode=https://qr.alipay.com/fkx14623ddwl1ping3ddd73'),
mode: LaunchMode.externalApplication,
);
} catch (e) {
print(e);
}
}
}

View File

@ -11,17 +11,19 @@ class BangumiController extends GetxController {
RxList<BangumiListItemModel> bangumiFollowList = [BangumiListItemModel()].obs;
int _currentPage = 1;
bool isLoadingMore = true;
Box user = GStrorage.user;
Box userInfoCache = GStrorage.userInfo;
RxBool userLogin = false.obs;
late int mid;
var userInfo;
@override
void onInit() {
super.onInit();
if (user.get(UserBoxKey.userMid) != null) {
mid = int.parse(user.get(UserBoxKey.userMid).toString());
userInfo = userInfoCache.get('userInfoCache');
if (userInfo != null) {
mid = userInfo.mid;
}
userLogin.value = user.get(UserBoxKey.userLogin) != null;
userLogin.value = userInfo != null;
}
Future queryBangumiListFeed({type = 'init'}) async {
@ -48,7 +50,11 @@ class BangumiController extends GetxController {
// 我的订阅
Future queryBangumiFollow() async {
var result = await BangumiHttp.bangumiFollow(mid: 17340771);
userInfo = userInfo ?? userInfoCache.get('userInfoCache');
if (userInfo == null) {
return;
}
var result = await BangumiHttp.bangumiFollow(mid: userInfo.mid);
if (result['status']) {
bangumiFollowList.value = result['data'].list;
} else {}

View File

@ -49,7 +49,7 @@ class BangumiIntroController extends GetxController {
RxBool hasCoin = false.obs;
// 是否收藏
RxBool hasFav = false.obs;
Box user = GStrorage.user;
Box userInfoCache = GStrorage.userInfo;
bool userLogin = false;
Rx<FavFolderData> favFolderData = FavFolderData().obs;
List addMediaIdsNew = [];
@ -57,6 +57,7 @@ class BangumiIntroController extends GetxController {
// 关注状态 默认未关注
RxMap followStatus = {}.obs;
int _tempThemeValue = -1;
var userInfo;
@override
void onInit() {
@ -82,7 +83,8 @@ class BangumiIntroController extends GetxController {
// videoItem!['owner'] = args.owner;
}
}
userLogin = user.get(UserBoxKey.userLogin) != null;
userInfo = userInfoCache.get('userInfoCache');
userLogin = userInfo != null;
}
// 获取番剧简介&选集
@ -142,7 +144,7 @@ class BangumiIntroController extends GetxController {
// 投币
Future actionCoinVideo() async {
if (user.get(UserBoxKey.userMid) == null) {
if (userInfo == null) {
SmartDialog.showToast('账号未登录');
return;
}
@ -283,7 +285,7 @@ class BangumiIntroController extends GetxController {
Future queryVideoInFolder() async {
var result = await VideoHttp.videoInFolder(
mid: user.get(UserBoxKey.userMid), rid: IdUtils.bv2av(bvid));
mid: userInfo.mid, rid: IdUtils.bv2av(bvid));
if (result['status']) {
favFolderData.value = result['data'];
}

View File

@ -22,7 +22,11 @@ import 'controller.dart';
import 'widgets/intro_detail.dart';
class BangumiIntroPanel extends StatefulWidget {
const BangumiIntroPanel({super.key});
final int? cid;
const BangumiIntroPanel({
Key? key,
this.cid,
}) : super(key: key);
@override
State<BangumiIntroPanel> createState() => _BangumiIntroPanelState();
@ -69,7 +73,11 @@ class _BangumiIntroPanelState extends State<BangumiIntroPanel>
);
}
} else {
return BangumiInfo(loadingStatus: true, bangumiDetail: bangumiDetail);
return BangumiInfo(
loadingStatus: true,
bangumiDetail: bangumiDetail,
cid: widget.cid,
);
}
},
);
@ -79,11 +87,13 @@ class _BangumiIntroPanelState extends State<BangumiIntroPanel>
class BangumiInfo extends StatefulWidget {
final bool loadingStatus;
final BangumiInfoModel? bangumiDetail;
final int? cid;
const BangumiInfo({
Key? key,
this.loadingStatus = false,
this.bangumiDetail,
this.cid,
}) : super(key: key);
@override
@ -97,6 +107,7 @@ class _BangumiInfoState extends State<BangumiInfo> {
Box localCache = GStrorage.localCache;
late final BangumiInfoModel? bangumiItem;
late double sheetHeight;
int? cid;
@override
void initState() {
@ -105,11 +116,12 @@ class _BangumiInfoState extends State<BangumiInfo> {
videoDetailCtr = Get.find<VideoDetailController>(tag: heroTag);
bangumiItem = bangumiIntroController.bangumiItem;
sheetHeight = localCache.get('sheetHeight');
cid = widget.cid!;
}
// 收藏
showFavBottomSheet() {
if (bangumiIntroController.user.get(UserBoxKey.userMid) == null) {
if (bangumiIntroController.userInfo.mid == null) {
SmartDialog.showToast('账号未登录');
return;
}
@ -320,9 +332,10 @@ class _BangumiInfoState extends State<BangumiInfo> {
pages: bangumiItem != null
? bangumiItem!.episodes!
: widget.bangumiDetail!.episodes!,
cid: bangumiItem != null
? bangumiItem!.episodes!.first.cid
: widget.bangumiDetail!.episodes!.first.cid,
cid: cid ??
(bangumiItem != null
? bangumiItem!.episodes!.first.cid
: widget.bangumiDetail!.episodes!.first.cid),
sheetHeight: sheetHeight,
changeFuc: (bvid, cid, aid) => bangumiIntroController
.changeSeasonOrbangu(bvid, cid, aid),

View File

@ -1,5 +1,6 @@
import 'dart:async';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart';
@ -22,24 +23,28 @@ class _BangumiPageState extends State<BangumiPage>
with AutomaticKeepAliveClientMixin {
final BangumiController _bangumidController = Get.put(BangumiController());
late Future? _futureBuilderFuture;
late Future? _futureBuilderFutureFollow;
late ScrollController scrollController;
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
ScrollController scrollController = _bangumidController.scrollController;
scrollController = _bangumidController.scrollController;
StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream;
_futureBuilderFuture = _bangumidController.queryBangumiListFeed();
_futureBuilderFutureFollow = _bangumidController.queryBangumiFollow();
scrollController.addListener(
() async {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
if (!_bangumidController.isLoadingMore) {
EasyThrottle.throttle('my-throttler', const Duration(seconds: 1), () {
_bangumidController.isLoadingMore = true;
await _bangumidController.onLoad();
}
_bangumidController.onLoad();
});
}
final ScrollDirection direction =
@ -53,6 +58,12 @@ class _BangumiPageState extends State<BangumiPage>
);
}
@override
void dispose() {
scrollController.removeListener(() {});
super.dispose();
}
@override
Widget build(BuildContext context) {
super.build(context);
@ -80,43 +91,61 @@ class _BangumiPageState extends State<BangumiPage>
'最近追番',
style: Theme.of(context).textTheme.titleMedium,
),
IconButton(
onPressed: () {
setState(() {
_futureBuilderFutureFollow =
_bangumidController.queryBangumiFollow();
});
},
icon: const Icon(
Icons.refresh,
size: 20,
),
),
],
),
),
SizedBox(
height: 258,
child: FutureBuilder(
future: _bangumidController.queryBangumiFollow(),
future: _futureBuilderFutureFollow,
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.done) {
Map data = snapshot.data as Map;
List list = _bangumidController.bangumiFollowList;
if (data['status']) {
return Obx(
() => ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: _bangumidController
.bangumiFollowList.length,
itemBuilder: (context, index) {
return Container(
width: Get.size.width / 3,
height: 254,
margin: EdgeInsets.only(
left: StyleString.safeSpace,
right: index ==
_bangumidController
.bangumiFollowList
.length -
1
? StyleString.safeSpace
: 0),
child: BangumiCardV(
bangumiItem: _bangumidController
.bangumiFollowList[index],
() => list.isNotEmpty
? ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: list.length,
itemBuilder: (context, index) {
return Container(
width: Get.size.width / 3,
height: 254,
margin: EdgeInsets.only(
left: StyleString.safeSpace,
right: index ==
_bangumidController
.bangumiFollowList
.length -
1
? StyleString.safeSpace
: 0),
child: BangumiCardV(
bangumiItem: _bangumidController
.bangumiFollowList[index],
),
);
},
)
: const SizedBox(
child: Center(
child: Text('还没有追番'),
),
),
);
},
),
);
} else {
return const SizedBox();
@ -184,7 +213,8 @@ class _BangumiPageState extends State<BangumiPage>
crossAxisSpacing: StyleString.cardSpace,
// 列数
crossAxisCount: 3,
mainAxisExtent: Get.size.width / 3 / 0.65 + 30,
mainAxisExtent: Get.size.width / 3 / 0.65 +
32 * MediaQuery.of(context).textScaleFactor,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {

View File

@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/utils/storage.dart';
class BangumiPanel extends StatefulWidget {
final List<EpisodeItem> pages;
@ -22,82 +24,119 @@ class BangumiPanel extends StatefulWidget {
class _BangumiPanelState extends State<BangumiPanel> {
late int currentIndex;
final ScrollController listViewScrollCtr = ScrollController();
final ScrollController listViewScrollCtr_2 = ScrollController();
Box userInfoCache = GStrorage.userInfo;
dynamic userInfo;
// 默认未开通
int vipStatus = 0;
@override
void initState() {
super.initState();
currentIndex = widget.pages.indexWhere((e) => e.cid == widget.cid!);
scrollToIndex();
userInfo = userInfoCache.get('userInfoCache');
if (userInfo != null) {
vipStatus = userInfo.vipStatus;
}
}
@override
void dispose() {
listViewScrollCtr.dispose();
listViewScrollCtr_2.dispose();
super.dispose();
}
void showBangumiPanel() {
showBottomSheet(
context: context,
builder: (_) => Container(
height: widget.sheetHeight,
color: Theme.of(context).colorScheme.background,
child: Column(
children: [
AppBar(
toolbarHeight: 45,
automaticallyImplyLeading: false,
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
builder: (BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
await Future.delayed(const Duration(milliseconds: 200));
listViewScrollCtr_2.animateTo(currentIndex * 56,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut);
});
// 在这里使用 setState 更新状态
return Container(
height: widget.sheetHeight,
color: Theme.of(context).colorScheme.background,
child: Column(
children: [
Text(
'合集(${widget.pages.length}',
style: Theme.of(context).textTheme.titleMedium,
AppBar(
toolbarHeight: 45,
automaticallyImplyLeading: false,
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'合集(${widget.pages.length}',
style: Theme.of(context).textTheme.titleMedium,
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
),
],
),
titleSpacing: 10,
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
Expanded(
child: Material(
child: ListView.builder(
controller: listViewScrollCtr_2,
itemCount: widget.pages.length,
itemBuilder: (context, index) {
return ListTile(
onTap: () {
setState(() {
changeFucCall(widget.pages[index], index);
});
},
dense: false,
leading: index == currentIndex
? Image.asset(
'assets/images/live.gif',
color:
Theme.of(context).colorScheme.primary,
height: 12,
)
: null,
title: Text(
'${index + 1}${widget.pages[index].longTitle!}',
style: TextStyle(
fontSize: 14,
color: index == currentIndex
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface,
),
),
trailing: widget.pages[index].badge != null
? Image.asset(
'assets/images/big-vip.png',
height: 20,
)
: const SizedBox(),
);
},
),
),
),
],
),
titleSpacing: 10,
),
Expanded(
child: Material(
child: ListView.builder(
itemCount: widget.pages.length,
itemBuilder: (context, index) {
return ListTile(
onTap: () => changeFucCall(widget.pages[index], index),
dense: false,
leading: index == currentIndex
? Image.asset(
'assets/images/live.gif',
color: Theme.of(context).colorScheme.primary,
height: 12,
)
: null,
title: Text(
'${index + 1}${widget.pages[index].longTitle!}',
style: TextStyle(
fontSize: 14,
color: index == currentIndex
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface,
),
),
trailing: widget.pages[index].badge != null
? Image.asset(
'assets/images/big-vip.png',
height: 20,
)
: const SizedBox(),
);
},
),
),
),
],
),
),
);
},
);
},
);
}
void changeFucCall(item, i) async {
if (item.badge != null) {
if (item.badge != null && vipStatus != 1) {
SmartDialog.showToast('需要大会员');
return;
}
@ -108,6 +147,15 @@ class _BangumiPanelState extends State<BangumiPanel> {
);
currentIndex = i;
setState(() {});
scrollToIndex();
}
void scrollToIndex() {
WidgetsBinding.instance.addPostFrameCallback((_) {
// 在回调函数中获取更新后的状态
listViewScrollCtr.animateTo(currentIndex * 150,
duration: const Duration(milliseconds: 500), curve: Curves.easeInOut);
});
}
@override
@ -150,6 +198,7 @@ class _BangumiPanelState extends State<BangumiPanel> {
SizedBox(
height: 60,
child: ListView.builder(
controller: listViewScrollCtr,
scrollDirection: Axis.horizontal,
itemCount: widget.pages.length,
itemExtent: 150,
@ -222,87 +271,6 @@ class _BangumiPanelState extends State<BangumiPanel> {
);
})),
)
// SingleChildScrollView(
// padding: const EdgeInsets.only(top: 7, bottom: 7),
// scrollDirection: Axis.horizontal,
// child: ConstrainedBox(
// constraints: BoxConstraints(
// minWidth: MediaQuery.of(context).size.width,
// ),
// child: Row(
// children: [
// for (int i = 0; i < widget.pages.length; i++) ...[
// Container(
// width: 150,
// margin: const EdgeInsets.only(right: 10),
// child: Material(
// color: Theme.of(context).colorScheme.onInverseSurface,
// borderRadius: BorderRadius.circular(6),
// clipBehavior: Clip.hardEdge,
// child: InkWell(
// onTap: () => changeFucCall(widget.pages[i], i),
// child: Padding(
// padding: const EdgeInsets.symmetric(
// vertical: 8, horizontal: 10),
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Row(
// children: [
// if (i == currentIndex) ...[
// Image.asset(
// 'assets/images/live.gif',
// color:
// Theme.of(context).colorScheme.primary,
// height: 12,
// ),
// const SizedBox(width: 6)
// ],
// Text(
// '第${i + 1}话',
// style: TextStyle(
// fontSize: 13,
// color: i == currentIndex
// ? Theme.of(context)
// .colorScheme
// .primary
// : Theme.of(context)
// .colorScheme
// .onSurface),
// ),
// const SizedBox(width: 2),
// if (widget.pages[i].badge != null) ...[
// Image.asset(
// 'assets/images/big-vip.png',
// height: 16,
// ),
// ],
// ],
// ),
// const SizedBox(height: 3),
// Text(
// widget.pages[i].longTitle!,
// maxLines: 1,
// style: TextStyle(
// fontSize: 13,
// color: i == currentIndex
// ? Theme.of(context).colorScheme.primary
// : Theme.of(context)
// .colorScheme
// .onSurface),
// overflow: TextOverflow.ellipsis,
// )
// ],
// ),
// ),
// ),
// ),
// ),
// ]
// ],
// ),
// ),
// )
],
);
}

View File

@ -29,9 +29,6 @@ class BangumiCardV extends StatelessWidget {
return Card(
elevation: 0,
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: StyleString.mdRadius,
),
margin: EdgeInsets.zero,
child: GestureDetector(
// onLongPress: () {
@ -149,7 +146,6 @@ class BangumiContent extends StatelessWidget {
bangumiItem.title,
textAlign: TextAlign.start,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
letterSpacing: 0.3,
),
@ -158,6 +154,7 @@ class BangumiContent extends StatelessWidget {
)),
],
),
const SizedBox(height: 1),
if (bangumiItem.indexShow != null)
Text(
bangumiItem.indexShow,

View File

@ -46,6 +46,7 @@ class _BlackListPageState extends State<BlackListPage> {
List<int> blackMidsList =
_blackListController.blackList.map<int>((e) => e.mid!).toList();
setting.put(SettingBoxKey.blackMidsList, blackMidsList);
scrollController.removeListener(() {});
super.dispose();
}

View File

@ -3,13 +3,19 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/dynamics.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/common/dynamics_type.dart';
import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/models/dynamics/up.dart';
import 'package:pilipala/models/live/item.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart';
class DynamicsController extends GetxController {
int page = 1;
@ -45,19 +51,47 @@ class DynamicsController extends GetxController {
},
];
bool flag = false;
RxInt initialValue = 1.obs;
RxInt initialValue = 0.obs;
Box userInfoCache = GStrorage.userInfo;
RxBool userLogin = false.obs;
var userInfo;
RxBool isLoadingDynamic = false.obs;
Box setting = GStrorage.setting;
@override
void onInit() {
userInfo = userInfoCache.get('userInfoCache');
userLogin.value = userInfo != null;
super.onInit();
initialValue.value =
setting.get(SettingBoxKey.defaultDynamicType, defaultValue: 0);
dynamicsType = DynamicsType.values[initialValue.value].obs;
}
Future queryFollowDynamic({type = 'init'}) async {
if (!userLogin.value) {
return {'status': false, 'msg': '账号未登录'};
}
if (type == 'init') {
dynamicsList.clear();
}
// 下拉刷新数据渲染时会触发onLoad
if (type == 'onLoad' && page == 1) {
return;
}
isLoadingDynamic.value = true;
var res = await DynamicsHttp.followDynamic(
page: type == 'init' ? 1 : page,
type: dynamicsType.value.values,
offset: offset,
mid: mid.value,
);
isLoadingDynamic.value = false;
if (res['status']) {
if (type == 'onLoad' && res['data'].items.isEmpty) {
SmartDialog.showToast('没有更多了');
return;
}
if (type == 'init') {
dynamicsList.value = res['data'].items;
} else {
@ -70,7 +104,7 @@ class DynamicsController extends GetxController {
}
onSelectType(value) async {
dynamicsType.value = filterTypeList[value - 1]['value'];
dynamicsType.value = filterTypeList[value]['value'];
dynamicsList.value = [DynamicItemModel()];
page = 1;
initialValue.value = value;
@ -80,16 +114,21 @@ class DynamicsController extends GetxController {
pushDetail(item, floor, {action = 'all'}) async {
feedBack();
/// 点击评论action 直接查看评论
if (action == 'comment') {
Get.toNamed('/dynamicDetail',
arguments: {'item': item, 'floor': floor, 'action': action});
return false;
}
switch (item!.type) {
/// 转发的动态
case 'DYNAMIC_TYPE_FORWARD':
Get.toNamed('/dynamicDetail',
arguments: {'item': item, 'floor': floor});
break;
/// 图文动态查看
case 'DYNAMIC_TYPE_DRAW':
Get.toNamed('/dynamicDetail',
arguments: {'item': item, 'floor': floor});
@ -105,6 +144,8 @@ class DynamicsController extends GetxController {
SmartDialog.showToast(err.toString());
}
break;
/// 专栏文章查看
case 'DYNAMIC_TYPE_ARTICLE':
String title = item.modules.moduleDynamic.major.opus.title;
String url = item.modules.moduleDynamic.major.opus.jumpUrl;
@ -115,7 +156,10 @@ class DynamicsController extends GetxController {
break;
case 'DYNAMIC_TYPE_PGC':
print('番剧');
SmartDialog.showToast('暂未支持的类型,请联系开发者');
break;
/// 纯文字动态查看
case 'DYNAMIC_TYPE_WORD':
print('纯文本');
Get.toNamed('/dynamicDetail',
@ -139,16 +183,61 @@ class DynamicsController extends GetxController {
});
break;
/// TODO
/// 合集查看
case 'DYNAMIC_TYPE_UGC_SEASON':
print('合集');
DynamicArchiveModel ugcSeason =
item.modules.moduleDynamic.major.ugcSeason;
int aid = ugcSeason.aid!;
String bvid = IdUtils.av2bv(aid);
String cover = ugcSeason.cover!;
int cid = await SearchHttp.ab2c(bvid: bvid);
Get.toNamed('/video?bvid=$bvid&cid=$cid',
arguments: {'pic': cover, 'heroTag': bvid});
break;
/// 番剧查看
case 'DYNAMIC_TYPE_PGC_UNION':
print('DYNAMIC_TYPE_PGC_UNION 番剧');
DynamicArchiveModel pgc = item.modules.moduleDynamic.major.pgc;
if (pgc.epid != null) {
SmartDialog.showLoading(msg: '获取中...');
var res = await SearchHttp.bangumiInfo(epId: pgc.epid);
SmartDialog.dismiss();
if (res['status']) {
EpisodeItem episode = res['data'].episodes.first;
String bvid = episode.bvid!;
int cid = episode.cid!;
String pic = episode.cover!;
String heroTag = Utils.makeHeroTag(cid);
Get.toNamed(
'/video?bvid=$bvid&cid=$cid&seasonId=${res['data'].seasonId}',
arguments: {
'pic': pic,
'heroTag': heroTag,
'videoType': SearchType.media_bangumi,
'bangumiItem': res['data'],
},
);
}
}
break;
}
}
Future queryFollowUp() async {
Future queryFollowUp({type = 'init'}) async {
if (!userLogin.value) {
return {'status': false, 'msg': '账号未登录'};
}
if (type == 'init') {
upData.value.upList = [];
upData.value.liveUsers = LiveUsers();
}
var res = await DynamicsHttp.followUp();
if (res['status']) {
upData.value = res['data'];
if (upData.value.upList!.isEmpty) {
mid.value = -1;
}
}
return res;
}
@ -162,7 +251,8 @@ class DynamicsController extends GetxController {
onRefresh() async {
page = 1;
queryFollowUp();
print('onRefresh');
await queryFollowUp();
await queryFollowDynamic();
}
@ -181,8 +271,8 @@ class DynamicsController extends GetxController {
void resetSearch() {
mid.value = -1;
dynamicsType.value = DynamicsType.values[0];
initialValue.value = 1;
SmartDialog.showToast('还原默认加载', alignment: Alignment.topCenter);
initialValue.value = 0;
SmartDialog.showToast('还原默认加载');
dynamicsList.value = [DynamicItemModel()];
queryFollowDynamic();
}

View File

@ -1,8 +1,10 @@
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/reply.dart';
import 'package:pilipala/models/common/reply_sort_type.dart';
import 'package:pilipala/models/video/reply/item.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart';
class DynamicDetailController extends GetxController {
DynamicDetailController(this.oid, this.type);
@ -16,9 +18,10 @@ class DynamicDetailController extends GetxController {
RxList<ReplyItemModel> replyList = [ReplyItemModel()].obs;
RxInt acount = 0.obs;
ReplySortType sortType = ReplySortType.time;
ReplySortType _sortType = ReplySortType.time;
RxString sortTypeTitle = ReplySortType.time.titles.obs;
RxString sortTypeLabel = ReplySortType.time.labels.obs;
Box setting = GStrorage.setting;
@override
void onInit() {
@ -29,6 +32,11 @@ class DynamicDetailController extends GetxController {
acount.value =
int.parse(item!.modules!.moduleStat!.comment!.count ?? '0');
}
int deaultReplySortIndex =
setting.get(SettingBoxKey.replySortType, defaultValue: 0);
_sortType = ReplySortType.values[deaultReplySortIndex];
sortTypeTitle.value = _sortType.titles;
sortTypeLabel.value = _sortType.labels;
}
Future queryReplyList({reqType = 'init'}) async {
@ -39,7 +47,7 @@ class DynamicDetailController extends GetxController {
oid: oid!,
pageNum: currentPage + 1,
type: type!,
sort: sortType.index,
sort: _sortType.index,
);
if (res['status']) {
List<ReplyItemModel> replies = res['data'].replies;
@ -76,20 +84,20 @@ class DynamicDetailController extends GetxController {
// 排序搜索评论
queryBySort() {
feedBack();
switch (sortType) {
switch (_sortType) {
case ReplySortType.time:
sortType = ReplySortType.like;
_sortType = ReplySortType.like;
break;
case ReplySortType.like:
sortType = ReplySortType.reply;
_sortType = ReplySortType.reply;
break;
case ReplySortType.reply:
sortType = ReplySortType.time;
_sortType = ReplySortType.time;
break;
default:
}
sortTypeTitle.value = sortType.titles;
sortTypeLabel.value = sortType.labels;
sortTypeTitle.value = _sortType.titles;
sortTypeLabel.value = _sortType.labels;
replyList.clear();
queryReplyList(reqType: 'init');
}

View File

@ -1,5 +1,6 @@
import 'dart:async';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/skeleton/video_reply.dart';
@ -40,7 +41,9 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
} else {
oid = Get.arguments['item'].modules.moduleDynamic.major.draw.id;
}
type = Get.arguments['item'].basic!['comment_type'];
int commentType = Get.arguments['item'].basic!['comment_type'] ?? 11;
type = (commentType == 0) ? 11 : commentType;
action =
Get.arguments.containsKey('action') ? Get.arguments['action'] : null;
_dynamicDetailController = Get.put(DynamicDetailController(oid, type));
@ -56,10 +59,9 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
void _listen() async {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 300) {
if (!_dynamicDetailController!.isLoadingMore) {
_dynamicDetailController!.isLoadingMore = true;
await _dynamicDetailController!.queryReplyList(reqType: 'onLoad');
}
EasyThrottle.throttle('replylist', const Duration(seconds: 2), () {
_dynamicDetailController!.queryReplyList(reqType: 'onLoad');
});
}
if (scrollController.offset > 55 && !_visibleTitle) {
@ -95,6 +97,12 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
);
}
@override
void dispose() {
scrollController.removeListener(() {});
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -236,6 +244,11 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
replyReply: (replyItem) =>
replyReply(replyItem),
replyType: ReplyType.values[type],
addReply: (replyItem) {
_dynamicDetailController!
.replyList[index].replies!
.add(replyItem);
},
);
}
},

View File

@ -1,14 +1,17 @@
import 'dart:async';
import 'package:custom_sliding_segmented_control/custom_sliding_segmented_control.dart';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/common/skeleton/dynamic_card.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/no_data.dart';
import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/pages/main/index.dart';
import 'package:pilipala/utils/event_bus.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart';
@ -28,8 +31,9 @@ class _DynamicsPageState extends State<DynamicsPage>
final DynamicsController _dynamicsController = Get.put(DynamicsController());
late Future _futureBuilderFuture;
late Future _futureBuilderFutureUp;
bool _isLoadingMore = false;
Box user = GStrorage.user;
Box userInfoCache = GStrorage.userInfo;
EventBus eventBus = EventBus();
late ScrollController scrollController;
@override
bool get wantKeepAlive => true;
@ -39,18 +43,17 @@ class _DynamicsPageState extends State<DynamicsPage>
super.initState();
_futureBuilderFuture = _dynamicsController.queryFollowDynamic();
_futureBuilderFutureUp = _dynamicsController.queryFollowUp();
ScrollController scrollController = _dynamicsController.scrollController;
scrollController = _dynamicsController.scrollController;
StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream;
scrollController.addListener(
() async {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
if (!_isLoadingMore) {
_isLoadingMore = true;
await _dynamicsController.queryFollowDynamic(type: 'onLoad');
_isLoadingMore = false;
}
EasyThrottle.throttle(
'queryFollowDynamic', const Duration(seconds: 1), () {
_dynamicsController.queryFollowDynamic(type: 'onLoad');
});
}
final ScrollDirection direction =
@ -62,6 +65,20 @@ class _DynamicsPageState extends State<DynamicsPage>
}
},
);
eventBus.on(EventName.loginEvent, (args) {
_dynamicsController.userLogin.value = args['status'];
setState(() {
_futureBuilderFuture = _dynamicsController.queryFollowDynamic();
_futureBuilderFutureUp = _dynamicsController.queryFollowUp();
});
});
}
@override
void dispose() {
scrollController.removeListener(() {});
super.dispose();
}
@override
@ -107,73 +124,82 @@ class _DynamicsPageState extends State<DynamicsPage>
return const SizedBox();
}
}),
Obx(() => Visibility(
visible: _dynamicsController.mid.value == -1,
child: CustomSlidingSegmentedControl<int>(
initialValue: _dynamicsController.initialValue.value,
children: {
1: Text(
'全部',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize),
Obx(
() => _dynamicsController.userLogin.value
? Visibility(
visible: _dynamicsController.mid.value == -1,
child: CustomSlidingSegmentedControl<int>(
initialValue:
_dynamicsController.initialValue.value,
children: {
0: Text(
'全部',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize),
),
1: Text('投稿',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize)),
2: Text('番剧',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize)),
3: Text('专栏',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize)),
},
padding: 13.0,
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.surfaceVariant
.withOpacity(0.7),
borderRadius: BorderRadius.circular(20),
),
thumbDecoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(20),
),
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
onValueChanged: (v) {
feedBack();
_dynamicsController.onSelectType(v);
},
),
2: Text('投稿',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize)),
3: Text('番剧',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize)),
// 4: Text(
// '专栏',
// style: TextStyle(
// fontSize: Theme.of(context)
// .textTheme
// .labelMedium!
// .fontSize),
// ),
},
padding: 13.0,
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.surfaceVariant
.withOpacity(0.7),
borderRadius: BorderRadius.circular(20),
),
thumbDecoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(20),
),
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
onValueChanged: (v) {
feedBack();
_dynamicsController.onSelectType(v);
},
),
))
)
: Text('动态',
style: Theme.of(context).textTheme.titleMedium),
)
],
),
Positioned(
right: 4,
top: 0,
bottom: 0,
child: IconButton(
padding: EdgeInsets.zero,
onPressed: () =>
{feedBack(), _dynamicsController.resetSearch()},
icon: const Icon(Icons.history, size: 21),
),
),
// Obx(
// () => Visibility(
// visible: _dynamicsController.userLogin.value,
// child: Positioned(
// right: 4,
// top: 0,
// bottom: 0,
// child: IconButton(
// padding: EdgeInsets.zero,
// onPressed: () =>
// {feedBack(), _dynamicsController.resetSearch()},
// icon: const Icon(Icons.history, size: 21),
// ),
// ),
// ),
// ),
],
),
),
@ -212,19 +238,36 @@ class _DynamicsPageState extends State<DynamicsPage>
List<DynamicItemModel> list =
_dynamicsController.dynamicsList;
return Obx(
() => list.isEmpty
? skeleton()
: SliverList(
delegate:
SliverChildBuilderDelegate((context, index) {
() {
if (list.isEmpty) {
if (_dynamicsController.isLoadingDynamic.value) {
return skeleton();
} else {
return const NoData();
}
} else {
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return DynamicPanel(item: list[index]);
}, childCount: list.length),
},
childCount: list.length,
),
);
}
},
);
} else {
return HttpError(
errMsg: data['msg'],
fn: () => _dynamicsController.onRefresh(),
fn: () {
setState(() {
_futureBuilderFuture =
_dynamicsController.queryFollowDynamic();
_futureBuilderFutureUp =
_dynamicsController.queryFollowUp();
});
},
);
}
} else {
@ -233,6 +276,7 @@ class _DynamicsPageState extends State<DynamicsPage>
}
},
),
const SliverToBoxAdapter(child: SizedBox(height: 40))
],
),
),

View File

@ -37,7 +37,7 @@ class _ActionPanelState extends State<ActionPanel> {
String dynamicId = item.idStr!;
// 1 已点赞 2 不喜欢 0 未操作
Like like = item.modules.moduleStat.like;
int count = int.parse(like.count!);
int count = like.count == '点赞' ? 0 : int.parse(like.count ?? '0');
bool status = like.status!;
int up = status ? 2 : 1;
var res = await DynamicsHttp.likeDynamic(dynamicId: dynamicId, up: up);
@ -47,7 +47,11 @@ class _ActionPanelState extends State<ActionPanel> {
item.modules.moduleStat.like.count = (count + 1).toString();
item.modules.moduleStat.like.status = true;
} else {
item.modules.moduleStat.like.count = (count - 1).toString();
if (count == 1) {
item.modules.moduleStat.like.count = '点赞';
} else {
item.modules.moduleStat.like.count = (count - 1).toString();
}
item.modules.moduleStat.like.status = false;
}
setState(() {});
@ -63,54 +67,63 @@ class _ActionPanelState extends State<ActionPanel> {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
TextButton.icon(
onPressed: () {},
icon: const Icon(
FontAwesomeIcons.shareFromSquare,
size: 16,
Expanded(
flex: 1,
child: TextButton.icon(
onPressed: () {},
icon: const Icon(
FontAwesomeIcons.shareFromSquare,
size: 16,
),
style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),
foregroundColor: Theme.of(context).colorScheme.outline,
),
label: Text(stat.forward!.count ?? '转发'),
),
style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),
foregroundColor: Theme.of(context).colorScheme.outline,
),
label: Text(stat.forward!.count ?? '转发'),
),
TextButton.icon(
onPressed: () =>
_dynamicsController.pushDetail(widget.item, 1, action: 'comment'),
icon: const Icon(
FontAwesomeIcons.comment,
size: 16,
Expanded(
flex: 1,
child: TextButton.icon(
onPressed: () => _dynamicsController.pushDetail(widget.item, 1,
action: 'comment'),
icon: const Icon(
FontAwesomeIcons.comment,
size: 16,
),
style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),
foregroundColor: Theme.of(context).colorScheme.outline,
),
label: Text(stat.comment!.count ?? '评论'),
),
style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),
foregroundColor: Theme.of(context).colorScheme.outline,
),
label: Text(stat.comment!.count ?? '评论'),
),
TextButton.icon(
onPressed: () => onLikeDynamic(),
icon: Icon(
stat.like!.status!
? FontAwesomeIcons.solidThumbsUp
: FontAwesomeIcons.thumbsUp,
size: 16,
color: stat.like!.status! ? primary : color,
),
style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),
foregroundColor: Theme.of(context).colorScheme.outline,
),
label: AnimatedSwitcher(
duration: const Duration(milliseconds: 400),
transitionBuilder: (Widget child, Animation<double> animation) {
return ScaleTransition(scale: animation, child: child);
},
child: Text(
stat.like!.count ?? '点赞',
key: ValueKey<String>(stat.like!.count ?? '点赞'),
style: TextStyle(
color: stat.like!.status! ? primary : color,
Expanded(
flex: 1,
child: TextButton.icon(
onPressed: () => onLikeDynamic(),
icon: Icon(
stat.like!.status!
? FontAwesomeIcons.solidThumbsUp
: FontAwesomeIcons.thumbsUp,
size: 16,
color: stat.like!.status! ? primary : color,
),
style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),
foregroundColor: Theme.of(context).colorScheme.outline,
),
label: AnimatedSwitcher(
duration: const Duration(milliseconds: 400),
transitionBuilder: (Widget child, Animation<double> animation) {
return ScaleTransition(scale: animation, child: child);
},
child: Text(
stat.like!.count ?? '点赞',
key: ValueKey<String>(stat.like!.count ?? '点赞'),
style: TextStyle(
color: stat.like!.status! ? primary : color,
),
),
),
),

View File

@ -60,43 +60,47 @@ Widget addWidget(item, context, type, {floor = 1}) {
),
);
case 'ADDITIONAL_TYPE_RESERVE':
return Padding(
padding: const EdgeInsets.only(top: 8),
child: InkWell(
onTap: () {},
child: Container(
width: double.infinity,
padding:
const EdgeInsets.only(left: 12, top: 10, right: 12, bottom: 10),
color: bgColor,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
dynamicProperty[type].title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 1),
Text.rich(
TextSpan(
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize),
return dynamicProperty[type].state != -1
? Padding(
padding: const EdgeInsets.only(top: 8),
child: InkWell(
onTap: () {},
child: Container(
width: double.infinity,
padding: const EdgeInsets.only(
left: 12, top: 10, right: 12, bottom: 10),
color: bgColor,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextSpan(text: dynamicProperty[type].desc1['text']),
const TextSpan(text: ' '),
TextSpan(text: dynamicProperty[type].desc2['text']),
Text(
dynamicProperty[type].title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 1),
Text.rich(
TextSpan(
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize),
children: [
TextSpan(text: dynamicProperty[type].desc1['text']),
const TextSpan(text: ' '),
TextSpan(text: dynamicProperty[type].desc2['text']),
],
),
)
],
),
)
],
),
// TextButton(onPressed: () {}, child: Text('123'))
),
),
);
// TextButton(onPressed: () {}, child: Text('123'))
),
),
)
: const SizedBox();
case 'ADDITIONAL_TYPE_GOODS':
return Padding(
padding: const EdgeInsets.only(top: 6),

View File

@ -36,7 +36,8 @@ Widget author(item, context) {
Text(
item.modules.moduleAuthor.name,
style: TextStyle(
color: item.modules.moduleAuthor!.vip!['status'] > 0
color: item.modules.moduleAuthor!.vip != null &&
item.modules.moduleAuthor!.vip['status'] > 0
? const Color.fromARGB(255, 251, 100, 163)
: Theme.of(context).colorScheme.onBackground,
fontSize: Theme.of(context).textTheme.titleSmall!.fontSize,

View File

@ -100,6 +100,7 @@ Widget forWard(item, context, ctr, source, {floor = 1}) {
// 直播
case 'DYNAMIC_TYPE_LIVE_RCMD':
return liveRcmdPanel(item, context, floor: floor);
// 直播
case 'DYNAMIC_TYPE_LIVE':
return livePanel(item, context, floor: floor);
// 合集
@ -147,6 +148,7 @@ Widget forWard(item, context, ctr, source, {floor = 1}) {
return videoSeasonWidget(item, context, 'pgc', floor: floor);
case 'DYNAMIC_TYPE_PGC_UNION':
return videoSeasonWidget(item, context, 'pgc', floor: floor);
// 直播结束
case 'DYNAMIC_TYPE_NONE':
return Row(
children: [
@ -158,7 +160,23 @@ Widget forWard(item, context, ctr, source, {floor = 1}) {
Text(item.modules.moduleDynamic.major.none.tips)
],
);
// 课堂
case 'DYNAMIC_TYPE_COURSES_SEASON':
return Row(
children: [
Expanded(
child: Text(
"课堂💪:${item.modules.moduleDynamic.major.courses['title']}",
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
)
],
);
default:
return const SizedBox(height: 0);
return const SizedBox(
width: double.infinity,
child: Text('🙏 暂未支持的类型,请联系开发者反馈 '),
);
}
}

View File

@ -24,24 +24,28 @@ class _UpPanelState extends State<UpPanel> {
List<UpItem> upList = [];
List<LiveUserItem> liveList = [];
static const itemPadding = EdgeInsets.symmetric(horizontal: 5, vertical: 0);
Box user = GStrorage.user;
Box userInfoCache = GStrorage.userInfo;
var userInfo;
@override
void initState() {
super.initState();
upList = widget.upData!.upList!;
liveList = widget.upData!.liveUsers!.items!;
if (widget.upData!.liveUsers != null) {
liveList = widget.upData!.liveUsers!.items!;
}
upList.insert(
0,
UpItem(
face: 'https://files.catbox.moe/8uc48f.png', uname: '全部动态', mid: -1),
);
userInfo = userInfoCache.get('userInfoCache');
upList.insert(
1,
UpItem(
face: user.get(UserBoxKey.userFace),
face: userInfo.face,
uname: '',
mid: user.get(UserBoxKey.userMid),
mid: userInfo.mid,
),
);
}
@ -64,15 +68,20 @@ class _UpPanelState extends State<UpPanel> {
controller: scrollController,
children: [
const SizedBox(width: 10),
for (int i = 0; i < liveList.length; i++) ...[
upItemBuild(liveList[i], i)
if (liveList.isNotEmpty) ...[
for (int i = 0; i < liveList.length; i++) ...[
upItemBuild(liveList[i], i)
],
VerticalDivider(
indent: 20,
endIndent: 40,
width: 26,
color: Theme.of(context)
.colorScheme
.primary
.withOpacity(0.5),
),
],
VerticalDivider(
indent: 20,
endIndent: 40,
width: 26,
color: Theme.of(context).primaryColor.withOpacity(0.5),
),
for (int i = 0; i < upList.length; i++) ...[
upItemBuild(upList[i], i)
],
@ -123,7 +132,8 @@ class _UpPanelState extends State<UpPanel> {
double itemWidth = contentWidth + itemPadding.horizontal;
double screenWidth = MediaQuery.of(context).size.width;
double moveDistance = 0.0;
if ((upLen - i - 0.5) * itemWidth > screenWidth / 2) {
if (itemWidth * (upList.length + liveList.length) <= screenWidth) {
} else if ((upLen - i - 0.5) * itemWidth > screenWidth / 2) {
moveDistance =
(i + liveLen + 0.5) * itemWidth + 46 - screenWidth / 2;
} else {

View File

@ -5,19 +5,22 @@ import 'package:pilipala/models/fans/result.dart';
import 'package:pilipala/utils/storage.dart';
class FansController extends GetxController {
Box user = GStrorage.user;
Box userInfoCache = GStrorage.userInfo;
int pn = 1;
int total = 0;
RxList<FansItemModel> fansList = [FansItemModel()].obs;
late int mid;
late String name;
var userInfo;
@override
void onInit() {
super.onInit();
mid = int.parse(
Get.parameters['mid'] ?? user.get(UserBoxKey.userMid).toString());
name = Get.parameters['name'] ?? user.get(UserBoxKey.userName);
userInfo = userInfoCache.get('userInfoCache');
mid = Get.parameters['mid'] != null
? int.parse(Get.parameters['mid']!)
: userInfo.mid;
name = Get.parameters['name'] ?? userInfo.uname;
}
Future queryFans(type) async {

View File

@ -37,6 +37,12 @@ class _FansPageState extends State<FansPage> {
);
}
@override
void dispose() {
scrollController.removeListener(() {});
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(

View File

@ -1,20 +1,52 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/models/user/info.dart';
import 'package:pilipala/utils/storage.dart';
class FavController extends GetxController {
final ScrollController scrollController = ScrollController();
Rx<FavFolderData> favFolderData = FavFolderData().obs;
Box userInfoCache = GStrorage.userInfo;
UserInfoData? userInfo;
int currentPage = 1;
int pageSize = 10;
RxBool hasMore = true.obs;
Future<dynamic> queryFavFolder() async {
Future<dynamic> queryFavFolder({type = 'init'}) async {
userInfo = userInfoCache.get('userInfoCache');
if (userInfo == null) {
return {'status': false, 'msg': '账号未登录'};
}
if (!hasMore.value) {
return;
}
var res = await await UserHttp.userfavFolder(
pn: 1,
ps: 10,
mid: GStrorage.user.get(UserBoxKey.userMid) ?? 0,
pn: currentPage,
ps: pageSize,
mid: userInfo!.mid!,
);
if (res['status']) {
favFolderData.value = res['data'];
if (type == 'init') {
favFolderData.value = res['data'];
} else {
if (res['data'].list.isNotEmpty) {
favFolderData.value.list!.addAll(res['data'].list);
favFolderData.update((val) {});
}
}
hasMore.value = res['data'].hasMore;
currentPage++;
} else {
SmartDialog.showToast(res['msg']);
}
return res;
}
Future onLoad() async {
queryFavFolder(type: 'onload');
}
}

View File

@ -1,3 +1,4 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/http_error.dart';
@ -14,11 +15,23 @@ class FavPage extends StatefulWidget {
class _FavPageState extends State<FavPage> {
final FavController _favController = Get.put(FavController());
late Future _futureBuilderFuture;
late ScrollController scrollController;
@override
void initState() {
super.initState();
_futureBuilderFuture = _favController.queryFavFolder();
scrollController = _favController.scrollController;
scrollController.addListener(
() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 300) {
EasyThrottle.throttle('history', const Duration(seconds: 1), () {
_favController.onLoad();
});
}
},
);
}
@override
@ -40,6 +53,7 @@ class _FavPageState extends State<FavPage> {
if (data['status']) {
return Obx(
() => ListView.builder(
controller: scrollController,
itemCount: _favController.favFolderData.value.list!.length,
itemBuilder: (context, index) {
return FavItem(

View File

@ -77,7 +77,6 @@ class VideoContent extends StatelessWidget {
favFolderItem.title,
textAlign: TextAlign.start,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
letterSpacing: 0.3,
),

View File

@ -61,6 +61,7 @@ class _FavDetailPageState extends State<FavDetailPage> {
SliverAppBar(
expandedHeight: 260 - MediaQuery.of(context).padding.top,
pinned: true,
titleSpacing: 0,
title: StreamBuilder(
stream: titleStreamC.stream,
initialData: false,
@ -126,32 +127,34 @@ class _FavDetailPageState extends State<FavDetailPage> {
),
),
const SizedBox(width: 14),
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
Text(
_favDetailController.item!.title!,
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.titleMedium!
.fontSize,
fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Text(
_favDetailController.item!.upper!.name!,
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelSmall!
.fontSize,
color: Theme.of(context).colorScheme.outline),
)
],
)
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
Text(
_favDetailController.item!.title!,
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.titleMedium!
.fontSize,
fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Text(
_favDetailController.item!.upper!.name!,
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelSmall!
.fontSize,
color: Theme.of(context).colorScheme.outline),
)
],
),
),
],
),
),

View File

@ -159,7 +159,6 @@ class VideoContent extends StatelessWidget {
videoItem.title,
textAlign: TextAlign.start,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
letterSpacing: 0.3,
),

View File

@ -5,19 +5,22 @@ import 'package:pilipala/models/follow/result.dart';
import 'package:pilipala/utils/storage.dart';
class FollowController extends GetxController {
Box user = GStrorage.user;
Box userInfoCache = GStrorage.userInfo;
int pn = 1;
int total = 0;
RxList<FollowItemModel> followList = [FollowItemModel()].obs;
late int mid;
late String name;
var userInfo;
@override
void onInit() {
super.onInit();
mid = int.parse(
Get.parameters['mid'] ?? user.get(UserBoxKey.userMid).toString());
name = Get.parameters['name'] ?? user.get(UserBoxKey.userName);
userInfo = userInfoCache.get('userInfoCache');
mid = Get.parameters['mid'] != null
? int.parse(Get.parameters['mid']!)
: userInfo.mid;
name = Get.parameters['name'] ?? userInfo.uname;
}
Future queryFollowings(type) async {

View File

@ -37,6 +37,12 @@ class _FollowPageState extends State<FollowPage> {
);
}
@override
void dispose() {
scrollController.removeListener(() {});
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(

View File

@ -9,9 +9,10 @@ import 'package:pilipala/utils/storage.dart';
class HistoryController extends GetxController {
final ScrollController scrollController = ScrollController();
RxList<HisListItem> historyList = [HisListItem()].obs;
bool isLoadingMore = false;
RxBool isLoadingMore = false.obs;
RxBool pauseStatus = false.obs;
Box localCache = GStrorage.localCache;
RxBool isLoading = false.obs;
@override
void onInit() {
@ -26,9 +27,9 @@ class HistoryController extends GetxController {
max = historyList.last.history!.oid!;
viewAt = historyList.last.viewAt!;
}
isLoadingMore = true;
isLoadingMore.value = true;
var res = await UserHttp.historyList(max, viewAt);
isLoadingMore = false;
isLoadingMore.value = false;
if (res['status']) {
if (type == 'onload') {
historyList.addAll(res['data'].list);

View File

@ -1,7 +1,9 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/skeleton/video_card_h.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/no_data.dart';
import 'package:pilipala/pages/history/index.dart';
import 'widgets/item.dart';
@ -16,25 +18,33 @@ class HistoryPage extends StatefulWidget {
class _HistoryPageState extends State<HistoryPage> {
final HistoryController _historyController = Get.put(HistoryController());
Future? _futureBuilderFuture;
late ScrollController scrollController;
@override
void initState() {
_futureBuilderFuture = _historyController.queryHistoryList();
super.initState();
_historyController.scrollController.addListener(
scrollController = _historyController.scrollController;
scrollController.addListener(
() {
if (_historyController.scrollController.position.pixels >=
_historyController.scrollController.position.maxScrollExtent -
300) {
if (!_historyController.isLoadingMore) {
_historyController.onLoad();
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 300) {
if (!_historyController.isLoadingMore.value) {
EasyThrottle.throttle('history', const Duration(seconds: 1), () {
_historyController.onLoad();
});
}
}
},
);
}
@override
void dispose() {
scrollController.removeListener(() {});
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -92,13 +102,8 @@ class _HistoryPageState extends State<HistoryPage> {
Map data = snapshot.data;
if (data['status']) {
return Obx(
() => _historyController.historyList.isEmpty
? const SliverToBoxAdapter(
child: Center(
child: Text('没数据'),
),
)
: SliverList(
() => _historyController.historyList.isNotEmpty
? SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return HistoryItem(
@ -108,7 +113,12 @@ class _HistoryPageState extends State<HistoryPage> {
},
childCount:
_historyController.historyList.length),
),
)
: _historyController.isLoadingMore.value
? const SliverToBoxAdapter(
child: Center(child: Text('加载中')),
)
: const NoData(),
);
} else {
return HttpError(

View File

@ -5,6 +5,7 @@ import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/http/video.dart';
import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/common/business_type.dart';
@ -36,20 +37,23 @@ class HistoryItem extends StatelessWidget {
'pageTitle': videoItem.title
},
);
} else if (videoItem.history.business == 'live' &&
videoItem.liveStatus == 1) {
LiveItemModel liveItem = LiveItemModel.fromJson({
'face': videoItem.authorFace,
'roomid': videoItem.history.oid,
'pic': videoItem.cover,
'title': videoItem.title,
'uname': videoItem.authorName,
'cover': videoItem.cover,
});
Get.toNamed(
'/liveRoom?roomid=${videoItem.history.oid}',
arguments: {'liveItem': liveItem},
);
} else if (videoItem.history.business == 'live') {
if (videoItem.liveStatus == 1) {
LiveItemModel liveItem = LiveItemModel.fromJson({
'face': videoItem.authorFace,
'roomid': videoItem.history.oid,
'pic': videoItem.cover,
'title': videoItem.title,
'uname': videoItem.authorName,
'cover': videoItem.cover,
});
Get.toNamed(
'/liveRoom?roomid=${videoItem.history.oid}',
arguments: {'liveItem': liveItem},
);
} else {
SmartDialog.showToast('直播未开播');
}
} else if (videoItem.badge == '番剧' ||
videoItem.tagName.contains('动画')) {
/// hack
@ -115,7 +119,7 @@ class HistoryItem extends StatelessWidget {
children: [
Padding(
padding: const EdgeInsets.fromLTRB(
StyleString.cardSpace, 5, StyleString.cardSpace, 5),
StyleString.safeSpace, 5, StyleString.safeSpace, 5),
child: LayoutBuilder(
builder: (context, boxConstraints) {
double width =
@ -201,7 +205,6 @@ class VideoContent extends StatelessWidget {
videoItem.title,
textAlign: TextAlign.start,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
letterSpacing: 0.3,
),
@ -232,6 +235,7 @@ class VideoContent extends StatelessWidget {
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
Utils.dateFormat(videoItem.viewAt!),
@ -239,7 +243,46 @@ class VideoContent extends StatelessWidget {
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline),
)
),
if (videoItem.badge != '番剧' &&
!videoItem.tagName.contains('动画') &&
videoItem.history.business != 'live' &&
!videoItem.history.business.contains('article'))
SizedBox(
width: 24,
height: 24,
child: PopupMenuButton<String>(
padding: EdgeInsets.zero,
tooltip: '稍后再看',
icon: Icon(
Icons.more_vert_outlined,
color: Theme.of(context).colorScheme.outline,
size: 14,
),
position: PopupMenuPosition.under,
// constraints: const BoxConstraints(maxHeight: 35),
onSelected: (String type) {},
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<String>>[
PopupMenuItem<String>(
onTap: () async {
var res = await UserHttp.toViewLater(
bvid: videoItem.history.bvid);
SmartDialog.showToast(res['msg']);
},
value: 'pause',
height: 35,
child: const Row(
children: [
Icon(Icons.watch_later_outlined, size: 16),
SizedBox(width: 6),
Text('稍后再看', style: TextStyle(fontSize: 13))
],
),
),
],
),
),
],
),
],

View File

@ -11,16 +11,17 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
late TabController tabController;
late List tabsCtrList;
late List<Widget> tabsPageList;
Box user = GStrorage.user;
Box userInfoCache = GStrorage.userInfo;
RxBool userLogin = false.obs;
RxString userFace = ''.obs;
var userInfo;
@override
void onInit() {
super.onInit();
userLogin.value = user.get(UserBoxKey.userLogin) ?? false;
userFace.value = user.get(UserBoxKey.userFace) ?? '';
userInfo = userInfoCache.get('userInfoCache');
userLogin.value = userInfo != null;
userFace.value = userInfo != null ? userInfo.face : '';
// 进行tabs配置
tabs = tabsConfig;
@ -48,7 +49,8 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
// 更新登录状态
void updateLoginStatus(val) {
userInfo = userInfoCache.get('userInfoCache');
userLogin.value = val ?? false;
userFace.value = user.get(UserBoxKey.userFace) ?? '';
userFace.value = userInfo != null ? userInfo.face : '';
}
}

View File

@ -6,13 +6,14 @@ import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/pages/mine/view.dart';
import 'package:pilipala/utils/storage.dart';
Box user = GStrorage.user;
Box userInfoCache = GStrorage.userInfo;
class HomeAppBar extends StatelessWidget {
const HomeAppBar({super.key});
@override
Widget build(BuildContext context) {
var userInfo = userInfoCache.get('userInfoCache');
return SliverAppBar(
// forceElevated: true,
scrolledUnderElevation: 0,
@ -55,7 +56,7 @@ class HomeAppBar extends StatelessWidget {
const SizedBox(width: 6),
/// TODO
if (user.get(UserBoxKey.userLogin)) ...[
if (userInfo != null) ...[
GestureDetector(
onTap: () => showModalBottomSheet(
context: context,
@ -70,7 +71,7 @@ class HomeAppBar extends StatelessWidget {
type: 'avatar',
width: 32,
height: 32,
src: user.get(UserBoxKey.userMid),
src: userInfo.face,
),
),
const SizedBox(width: 10),

View File

@ -23,6 +23,7 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
final HotController _hotController = Get.put(HotController());
List videoList = [];
Future? _futureBuilderFuture;
late ScrollController scrollController;
@override
bool get wantKeepAlive => true;
@ -31,7 +32,7 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
void initState() {
super.initState();
_futureBuilderFuture = _hotController.queryHotFeed('init');
ScrollController scrollController = _hotController.scrollController;
scrollController = _hotController.scrollController;
StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream;
scrollController.addListener(
@ -55,6 +56,12 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
);
}
@override
void dispose() {
scrollController.removeListener(() {});
super.dispose();
}
@override
Widget build(BuildContext context) {
super.build(context);
@ -126,7 +133,9 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
OverlayEntry _createPopupDialog(videoItem) {
return OverlayEntry(
builder: (context) => AnimatedDialog(
child: OverlayPop(videoItem: videoItem),
closeFn: _hotController.popupDialog?.remove,
child: OverlayPop(
videoItem: videoItem, closeFn: _hotController.popupDialog?.remove),
),
);
}

View File

@ -6,15 +6,20 @@ import 'package:pilipala/models/model_hot_video_item.dart';
class LaterController extends GetxController {
final ScrollController scrollController = ScrollController();
RxList<HotVideoItemModel> laterList = [HotVideoItemModel()].obs;
RxList<HotVideoItemModel> laterList = <HotVideoItemModel>[].obs;
int count = 0;
RxBool isLoading = false.obs;
Future queryLaterList() async {
isLoading.value = true;
var res = await UserHttp.seeYouLater();
if (res['status']) {
laterList.value = res['data']['list'];
count = res['data']['count'];
if (count > 0) {
laterList.value = res['data']['list'];
}
}
isLoading.value = false;
return res;
}
@ -47,4 +52,34 @@ class LaterController extends GetxController {
},
);
}
// 一键清空
Future toViewClear() async {
SmartDialog.show(
useSystem: true,
animationType: SmartAnimationType.centerFade_otherSlide,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('清空确认'),
content: const Text('确定要清空你的稍后再看列表吗?'),
actions: [
TextButton(
onPressed: () => SmartDialog.dismiss(),
child: const Text('取消')),
TextButton(
onPressed: () async {
var res = await UserHttp.toViewClear();
if (res['status']) {
laterList.clear();
}
SmartDialog.dismiss();
SmartDialog.showToast(res['msg']);
},
child: const Text('确认'),
)
],
);
},
);
}
}

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/skeleton/video_card_h.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/no_data.dart';
import 'package:pilipala/common/widgets/video_card_h.dart';
import 'package:pilipala/pages/later/index.dart';
@ -29,25 +30,38 @@ class _LaterPageState extends State<LaterPage> {
titleSpacing: 0,
centerTitle: false,
title: Obx(
() => Text(
'稍后再看 (${_laterController.laterList.length}/100)',
style: Theme.of(context).textTheme.titleMedium,
),
() => _laterController.laterList.isNotEmpty
? Text(
'稍后再看 (${_laterController.laterList.length}/100)',
style: Theme.of(context).textTheme.titleMedium,
)
: Text(
'稍后再看',
style: Theme.of(context).textTheme.titleMedium,
),
),
actions: [
TextButton(
onPressed: () => _laterController.toViewDel(),
child: const Text('移除已看'),
Obx(
() => _laterController.laterList.isNotEmpty
? TextButton(
onPressed: () => _laterController.toViewDel(),
child: const Text('移除已看'),
)
: const SizedBox(),
),
Obx(
() => _laterController.laterList.isNotEmpty
? IconButton(
tooltip: '一键清空',
onPressed: () => _laterController.toViewClear(),
icon: Icon(
Icons.clear_all_outlined,
size: 21,
color: Theme.of(context).colorScheme.primary,
),
)
: const SizedBox(),
),
// IconButton(
// tooltip: '一键清空',
// onPressed: () {},
// icon: Icon(
// Icons.clear_all_outlined,
// size: 21,
// color: Theme.of(context).colorScheme.primary,
// ),
// ),
const SizedBox(width: 8),
],
),
@ -61,19 +75,29 @@ class _LaterPageState extends State<LaterPage> {
Map data = snapshot.data as Map;
if (data['status']) {
return Obx(
() => SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return VideoCardH(
videoItem: _laterController.laterList[index],
source: 'later',
);
}, childCount: _laterController.laterList.length),
),
() => _laterController.laterList.isNotEmpty &&
!_laterController.isLoading.value
? SliverList(
delegate:
SliverChildBuilderDelegate((context, index) {
return VideoCardH(
videoItem: _laterController.laterList[index],
source: 'later',
);
}, childCount: _laterController.laterList.length),
)
: _laterController.isLoading.value
? const SliverToBoxAdapter(
child: Center(child: Text('加载中')),
)
: const NoData(),
);
} else {
return HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
fn: () => setState(() {
_futureBuilderFuture = _laterController.queryLaterList();
}),
);
}
} else {
@ -81,7 +105,7 @@ class _LaterPageState extends State<LaterPage> {
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return const VideoCardHSkeleton();
}, childCount: 5),
}, childCount: 10),
);
}
},

View File

@ -1,5 +1,6 @@
import 'dart:async';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart';
@ -23,22 +24,23 @@ class LivePage extends StatefulWidget {
class _LivePageState extends State<LivePage> {
final LiveController _liveController = Get.put(LiveController());
late Future _futureBuilderFuture;
late ScrollController scrollController;
@override
void initState() {
super.initState();
_futureBuilderFuture = _liveController.queryLiveList('init');
ScrollController scrollController = _liveController.scrollController;
scrollController = _liveController.scrollController;
StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream;
scrollController.addListener(
() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
if (!_liveController.isLoadingMore) {
EasyThrottle.throttle('my-throttler', const Duration(seconds: 1), () {
_liveController.isLoadingMore = true;
_liveController.onLoad();
}
});
}
final ScrollDirection direction =
@ -52,6 +54,12 @@ class _LivePageState extends State<LivePage> {
);
}
@override
void dispose() {
scrollController.removeListener(() {});
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
@ -113,7 +121,9 @@ class _LivePageState extends State<LivePage> {
OverlayEntry _createPopupDialog(liveItem) {
return OverlayEntry(
builder: (context) => AnimatedDialog(
child: OverlayPop(videoItem: liveItem),
closeFn: _liveController.popupDialog?.remove,
child: OverlayPop(
videoItem: liveItem, closeFn: _liveController.popupDialog?.remove),
),
);
}
@ -136,7 +146,8 @@ class _LivePageState extends State<LivePage> {
// 列数
crossAxisCount: crossAxisCount,
mainAxisExtent:
Get.size.width / crossAxisCount / StyleString.aspectRatio + 66,
Get.size.width / crossAxisCount / StyleString.aspectRatio +
68 * MediaQuery.of(context).textScaleFactor,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {

View File

@ -23,11 +23,8 @@ class LiveCardV extends StatelessWidget {
Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(liveItem.roomId);
return Card(
elevation: 0,
elevation: 1,
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: StyleString.mdRadius,
),
margin: EdgeInsets.zero,
child: GestureDetector(
onLongPress: () {
@ -35,11 +32,11 @@ class LiveCardV extends StatelessWidget {
longPress!();
}
},
onLongPressEnd: (details) {
if (longPressEnd != null) {
longPressEnd!();
}
},
// onLongPressEnd: (details) {
// if (longPressEnd != null) {
// longPressEnd!();
// }
// },
child: InkWell(
onTap: () async {
Get.toNamed('/liveRoom?roomid=${liveItem.roomId}',
@ -103,7 +100,7 @@ class LiveContent extends StatelessWidget {
return Expanded(
child: Padding(
// 多列
padding: const EdgeInsets.fromLTRB(4, 8, 0, 6),
padding: const EdgeInsets.fromLTRB(9, 9, 9, 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -112,7 +109,6 @@ class LiveContent extends StatelessWidget {
liveItem.title,
textAlign: TextAlign.start,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
letterSpacing: 0.3,
),

View File

@ -74,12 +74,10 @@ class LiveRoomController extends GetxController {
if (value == 0) {
// 设置音量
volumeOff.value = false;
// meeduPlayerController.setVolume(volume);
} else {
// 取消音量
volume = value;
volumeOff.value = true;
// meeduPlayerController.setVolume(0);
}
}
}

View File

@ -2,9 +2,12 @@ import 'dart:async';
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/pages/dynamics/index.dart';
import 'package:pilipala/pages/home/view.dart';
import 'package:pilipala/pages/media/index.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart';
class MainController extends GetxController {
List<Widget> pages = <Widget>[
@ -49,4 +52,13 @@ class MainController extends GetxController {
].obs;
final StreamController<bool> bottomBarStream =
StreamController<bool>.broadcast();
Box setting = GStrorage.setting;
@override
void onInit() {
super.onInit();
if (setting.get(SettingBoxKey.autoUpdate, defaultValue: false)) {
Utils.checkUpdata();
}
}
}

View File

@ -4,6 +4,7 @@ import 'package:hive/hive.dart';
import 'package:pilipala/pages/dynamics/index.dart';
import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/media/index.dart';
import 'package:pilipala/utils/event_bus.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart';
import './controller.dart';
@ -96,6 +97,7 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
@override
void dispose() async {
await GStrorage.close();
EventBus().off(EventName.loginEvent);
super.dispose();
}

View File

@ -8,7 +8,7 @@ import 'package:pilipala/utils/storage.dart';
class MediaController extends GetxController {
Rx<FavFolderData> favFolderData = FavFolderData().obs;
Box user = GStrorage.user;
Box userInfoCache = GStrorage.userInfo;
RxBool userLogin = false.obs;
List list = [
{
@ -34,21 +34,23 @@ class MediaController extends GetxController {
'onTap': () => Get.toNamed('/later'),
},
];
var userInfo;
@override
void onInit() {
super.onInit();
userLogin.value = user.get(UserBoxKey.userLogin) ?? false;
userInfo = userInfoCache.get('userInfoCache');
userLogin.value = userInfo != null;
}
Future<dynamic> queryFavFolder() async {
if (!userLogin.value || GStrorage.user.get(UserBoxKey.userMid) == null) {
if (!userLogin.value || GStrorage.userInfo.get('userInfoCache') == null) {
return {'status': false, 'data': [], 'msg': '未登录'};
}
var res = await await UserHttp.userfavFolder(
pn: 1,
ps: 5,
mid: GStrorage.user.get(UserBoxKey.userMid),
mid: GStrorage.userInfo.get('userInfoCache').mid,
);
favFolderData.value = res['data'];
return res;

View File

@ -3,6 +3,7 @@ import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/pages/media/index.dart';
import 'package:pilipala/utils/event_bus.dart';
import 'package:pilipala/utils/utils.dart';
class MediaPage extends StatefulWidget {
@ -16,6 +17,7 @@ class _MediaPageState extends State<MediaPage>
with AutomaticKeepAliveClientMixin {
late MediaController mediaController;
late Future _futureBuilderFuture;
EventBus eventBus = EventBus();
@override
bool get wantKeepAlive => true;
@ -25,6 +27,12 @@ class _MediaPageState extends State<MediaPage>
super.initState();
mediaController = Get.put(MediaController());
_futureBuilderFuture = mediaController.queryFavFolder();
eventBus.on(EventName.loginEvent, (args) {
mediaController.userLogin.value = args['status'];
setState(() {
_futureBuilderFuture = mediaController.queryFavFolder();
});
});
}
@override
@ -68,7 +76,7 @@ class _MediaPageState extends State<MediaPage>
),
),
],
Obx(() => mediaController.userLogin.value == true
Obx(() => mediaController.userLogin.value
? favFolder(mediaController, context)
: const SizedBox())
],
@ -116,7 +124,11 @@ class _MediaPageState extends State<MediaPage>
),
),
trailing: IconButton(
onPressed: () => _futureBuilderFuture,
onPressed: () {
setState(() {
_futureBuilderFuture = mediaController.queryFavFolder();
});
},
icon: const Icon(
Icons.refresh,
size: 20,
@ -126,7 +138,7 @@ class _MediaPageState extends State<MediaPage>
// const SizedBox(height: 10),
SizedBox(
width: double.infinity,
height: 170,
height: 170 * MediaQuery.of(context).textScaleFactor,
child: FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
@ -149,11 +161,25 @@ class _MediaPageState extends State<MediaPage>
right: 14, bottom: 35),
child: Center(
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(
EdgeInsets.zero),
backgroundColor:
MaterialStateProperty.resolveWith(
(states) {
return Theme.of(context)
.colorScheme
.primaryContainer
.withOpacity(0.5);
}),
),
onPressed: () => Get.toNamed('/fav'),
icon: Icon(
Icons.arrow_forward_ios,
size: 18,
color: Theme.of(context).primaryColor,
color: Theme.of(context)
.colorScheme
.primary,
),
),
));

Some files were not shown because too many files have changed in this diff Show More