Compare commits
79 Commits
v1.0.0.081
...
v1.0.5.082
Author | SHA1 | Date | |
---|---|---|---|
53d4379bb9 | |||
a928c575ef | |||
a0e51c86fc | |||
d670b8123a | |||
2621b096ac | |||
05631f7803 | |||
495ba57ca8 | |||
8990c4ae92 | |||
8bc6a32b06 | |||
6083578f93 | |||
aa7419f352 | |||
c90a6cd86c | |||
2e04c27292 | |||
5741f80536 | |||
161ba1c313 | |||
5d9dc6c1a9 | |||
1abe70d4d4 | |||
5fc959eb59 | |||
6322b29aef | |||
6461f72b5e | |||
e3d561bffd | |||
535cf69967 | |||
4314b0fc3c | |||
9da113726b | |||
201422c150 | |||
b67127123a | |||
3ce7578183 | |||
7b60cc2666 | |||
ead24e90fb | |||
3ad3ca9d48 | |||
8a8e99f30b | |||
0fe6d6c8e2 | |||
5a03bee410 | |||
b6023e35bc | |||
6a1c89f885 | |||
b7c0ef8341 | |||
9e44995082 | |||
8703d9f576 | |||
5812b5cff1 | |||
1884801ed2 | |||
a19ab8d17f | |||
a4078c0a8e | |||
90e811489b | |||
706bb0f924 | |||
01d6308350 | |||
332d5dc38c | |||
8f84c6a6f9 | |||
cc8753e8de | |||
8c8ddc9d93 | |||
5f03244085 | |||
50a5653516 | |||
7bb7159d48 | |||
83341cd62b | |||
8627869309 | |||
6bbbdd7710 | |||
599d6983fc | |||
b7eed8578a | |||
ec9d9739fe | |||
740116e873 | |||
2b0dc9d285 | |||
7c2518bcd2 | |||
9db76e99db | |||
40849cb68d | |||
b435023c99 | |||
b55568ef2a | |||
47a3c964c0 | |||
85ff7c7a92 | |||
083d05ed7e | |||
08862e7f72 | |||
04d953a3a2 | |||
be1cc25d12 | |||
adff2f2828 | |||
592b32fc7a | |||
d9a758464c | |||
0c067f7ca6 | |||
b6aa144205 | |||
438a94e298 | |||
b800f1b83b | |||
ddceda1184 |
23
.github/ISSUE_TEMPLATE/bug-反馈.md
vendored
Normal file
23
.github/ISSUE_TEMPLATE/bug-反馈.md
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
---
|
||||
name: Bug 反馈
|
||||
about: 描述你所遇到的bug
|
||||
title: ''
|
||||
labels: 问题反馈
|
||||
assignees: guozhigq
|
||||
|
||||
---
|
||||
|
||||
### 问题描述
|
||||
请提供一个清晰而简明的问题描述。
|
||||
|
||||
### 复现步骤
|
||||
请提供复现该问题所需的具体步骤。
|
||||
|
||||
### 预期行为
|
||||
请描述你期望的正确行为或结果。
|
||||
|
||||
### 系统信息
|
||||
请提供关于您的环境的详细信息,包括操作系统、浏览器版本等。
|
||||
|
||||
### 相关截图或日志
|
||||
如果有的话,请提供相关的截图、错误日志或其他有助于解决问题的信息。
|
20
.github/ISSUE_TEMPLATE/功能请求.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/功能请求.md
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: 功能请求
|
||||
about: 对于功能的一些建议
|
||||
title: ''
|
||||
labels: 功能
|
||||
assignees: guozhigq
|
||||
|
||||
---
|
||||
|
||||
### 功能描述
|
||||
请提供对所请求功能的清晰描述。
|
||||
|
||||
### 目标
|
||||
请描述你希望通过这个功能实现的目标。
|
||||
|
||||
### 解决方案
|
||||
如果你有任何关于如何实现这个功能的想法或建议,请在这里提供。
|
||||
|
||||
### 其他
|
||||
请提供已实现该功能或类似功能的应用
|
10
.github/workflows/main.yml
vendored
10
.github/workflows/main.yml
vendored
@ -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
|
||||
|
20
README.md
20
README.md
@ -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)
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1
assets/images/error.svg
Normal file
1
assets/images/error.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 26 KiB |
BIN
assets/sreenshot/main_screen.png
Normal file
BIN
assets/sreenshot/main_screen.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 MiB |
7
change_log/1.0.1.0817.md
Normal file
7
change_log/1.0.1.0817.md
Normal file
@ -0,0 +1,7 @@
|
||||
## 1.0.1
|
||||
|
||||
### 修复
|
||||
+ 升级播放器依赖
|
||||
+ android平台 AV1格式视频支持
|
||||
+ 视频全屏功能
|
||||
|
19
change_log/1.0.2.0819.md
Normal file
19
change_log/1.0.2.0819.md
Normal file
@ -0,0 +1,19 @@
|
||||
## 1.0.2
|
||||
|
||||
### 新功能
|
||||
+ 自动检查更新
|
||||
+ 封面图片保存
|
||||
+ 动态跳转番剧
|
||||
+ 历史记录番剧记忆播放
|
||||
+ 一键清空稍后再看
|
||||
|
||||
### 修复
|
||||
+ 切换分P cid未切换
|
||||
+ cookie存储问题
|
||||
+ 登录/退出登录问题
|
||||
|
||||
### 优化
|
||||
+ 页面空/异常状态样式
|
||||
+ 退出登录提示
|
||||
+ 请求节流
|
||||
+ 全屏播放
|
19
change_log/1.0.3.0821.md
Normal file
19
change_log/1.0.3.0821.md
Normal file
@ -0,0 +1,19 @@
|
||||
## 1.0.3
|
||||
|
||||
建议卸载1.0.2版本,重新安装
|
||||
### 新功能
|
||||
+ 底部播放进度条设置
|
||||
+ 复制图片链接
|
||||
|
||||
|
||||
### 修复
|
||||
+ 用户数据格式修改
|
||||
+ video Fit
|
||||
+ 没有audio 资源的视频异常
|
||||
+ 评论区域图片无法点击
|
||||
+ 视频进度条拖动问题
|
||||
|
||||
### 优化
|
||||
+ 页面空/异常状态样式
|
||||
+ 部分页面样式
|
||||
+ 图片预览页面样式
|
21
change_log/1.0.4.0822.md
Normal file
21
change_log/1.0.4.0822.md
Normal file
@ -0,0 +1,21 @@
|
||||
## 1.0.4
|
||||
|
||||
### 新功能
|
||||
+ 热搜刷新
|
||||
+ 视频搜索排序、筛选
|
||||
+ app字体大小自定义
|
||||
+ app主题色自定义
|
||||
+ 「课堂」类动态渲染
|
||||
|
||||
|
||||
### 修复
|
||||
+ 搜索词联想richText渲染异常
|
||||
+ 部分动态点赞异常
|
||||
+ 默认视频解码格式
|
||||
+ 搜索页面返回搜索词未清空
|
||||
+ 动态详情评论加载异常
|
||||
+ 动态页面下拉刷新数据异常
|
||||
|
||||
### 优化
|
||||
+ 一些样式修改
|
||||
+ 取消热搜词缓存
|
30
change_log/1.0.5.0826.md
Normal file
30
change_log/1.0.5.0826.md
Normal file
@ -0,0 +1,30 @@
|
||||
## 1.0.5
|
||||
|
||||
主要是bug修复跟一部分小功能,弹幕功能需要下一版。
|
||||
问题反馈请前往QQ频道或提交issues。
|
||||
感谢🙏酷友「无力感*」「斤斤计较呀」「Pseudopamine」
|
||||
|
||||
### 新功能
|
||||
+ 高帧率支持
|
||||
+ 默认评论排序设置
|
||||
+ 默认动态类别设置
|
||||
+ 动态合集查看
|
||||
+ 同时观看人数
|
||||
+ iOS路由切换效果
|
||||
|
||||
|
||||
### 修复
|
||||
+ 收藏夹翻页
|
||||
+ 首页搜索框频繁点击消失
|
||||
+ 评论排序切换空白
|
||||
+ 快速返回首页
|
||||
+ 重复进入个人中心页面数据未刷新
|
||||
+ 动态goods数据异常
|
||||
+ 大会员切换番剧
|
||||
+ 高画质codes匹配
|
||||
|
||||
|
||||
### 优化
|
||||
+ 倍速选择
|
||||
+ 播放器亮度记忆
|
||||
+ 下载对应版本apk
|
@ -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
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
351
lib/common/widgets/app_expansion_panel_list.dart
Normal file
351
lib/common/widgets/app_expansion_panel_list.dart
Normal 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;
|
||||
}
|
@ -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 ?? '点击重试'),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -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(
|
||||
|
31
lib/common/widgets/no_data.dart
Normal file
31
lib/common/widgets/no_data.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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),
|
||||
)
|
||||
],
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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';
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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());
|
||||
|
||||
|
@ -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});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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']};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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']};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
23
lib/models/common/color_type.dart
Normal file
23
lib/models/common/color_type.dart
Normal 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': '灰色'},
|
||||
];
|
@ -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];
|
||||
}
|
||||
|
@ -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'];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
: [];
|
||||
|
@ -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'];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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'];
|
||||
|
@ -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'];
|
||||
}
|
||||
}
|
||||
|
@ -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>(),
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {}
|
||||
|
@ -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'];
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
// )
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ]
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// )
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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');
|
||||
}
|
||||
|
@ -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);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
|
@ -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))
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
|
@ -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('🙏 暂未支持的类型,请联系开发者反馈 '),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -37,6 +37,12 @@ class _FansPageState extends State<FansPage> {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scrollController.removeListener(() {});
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -77,7 +77,6 @@ class VideoContent extends StatelessWidget {
|
||||
favFolderItem.title,
|
||||
textAlign: TextAlign.start,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: 0.3,
|
||||
),
|
||||
|
@ -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),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -159,7 +159,6 @@ class VideoContent extends StatelessWidget {
|
||||
videoItem.title,
|
||||
textAlign: TextAlign.start,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: 0.3,
|
||||
),
|
||||
|
@ -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 {
|
||||
|
@ -37,6 +37,12 @@ class _FollowPageState extends State<FollowPage> {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scrollController.removeListener(() {});
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
@ -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);
|
||||
|
@ -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(
|
||||
|
@ -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))
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
@ -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 : '';
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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('确认'),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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
Reference in New Issue
Block a user