Compare commits

..

86 Commits

Author SHA1 Message Date
77a77bc9e3 v1.0.14 更新 2023-12-25 23:40:23 +08:00
e76ca0e291 mod: 移除稍后再看上限数字展示 2023-12-25 22:53:19 +08:00
7d54bd1641 fix: 个人动态无限循环 issues #340 2023-12-25 22:52:20 +08:00
4e8ca590b4 mod: 媒体库页面隐藏底栏 2023-12-25 22:33:55 +08:00
e649fbbc49 mod: 补充web端推荐 2023-12-24 20:36:44 +08:00
579a4f2a81 fix: 视频合集,向下滑动 ui 显示问题 issues #314 2023-12-24 19:39:48 +08:00
cd55914b36 merge main 2023-12-24 17:21:21 +08:00
0a08d349c6 Merge pull request #338 from orz12/refactor-danmaku_controller
Refactor danmaku controller
2023-12-24 15:53:43 +08:00
df4939a3a0 Merge branch 'guozhigq:main' into refactor-danmaku_controller 2023-12-24 03:11:49 +08:00
022b3580dc opt: 梳理逻辑 2023-12-24 02:55:50 +08:00
c7611e436f fix/opt: 重构弹幕逻辑
改用map存储,将同属于100毫秒内的弹幕归入一个元素,无需再排序和二分比较取得,降低时间复杂度与播放时功耗;
分离PlDanmakuController与playerController的功能,避免代码耦合;
精简用于表示状态的变量与相关逻辑,修复播放完毕后因currentSegIndex永久增加而无法再显示弹幕的错误;
为PlDanmakuController添加dispose()。
2023-12-24 02:48:51 +08:00
6fcfce9290 Merge pull request #334 from orz12/fix-no-autoplay-null-check
fix: 修正非自动播放下空检查问题
2023-12-23 23:53:51 +08:00
3a5fa87073 Merge pull request #336 from orz12/fix_profile_followStatus_attribute
fix: profile页未显示关注状态
2023-12-23 23:53:22 +08:00
085df03cf2 Merge pull request #329 from orz12/remove_loading_on_liking_reply
opt: 移除回复点赞冗余加载提示
2023-12-23 19:01:40 +08:00
2b51ec2674 fix: profile页未显示关注状态
经测试,原接口会返回{"code":-403,"message":"访问权限不足","ttl":1},所以统一为hasFollow接口,并收录了更多的状态文本
2023-12-23 17:25:09 +08:00
25d32f8cc8 改为null的形式 2023-12-23 15:49:04 +08:00
eb8feb5773 fix: 修正非自动播放下空检查问题 2023-12-22 14:18:21 +08:00
0cf707b4c5 opt: 为所有点赞、投币添加禁用状态 2023-12-22 11:03:06 +08:00
16c58448b1 fix: controller 个人动态展示不匹配 issues #328 2023-12-22 01:00:55 +08:00
71e1f2f924 Merge branch 'main' into fix 2023-12-22 00:43:36 +08:00
6f7b688fa9 mod: 关系查询接口传参 2023-12-22 00:41:49 +08:00
e32ddeaec0 Merge pull request #325 from orz12/fix_portrait_fullscreen_bottom_cherry_pick
fix: 竖屏非全屏状态小白条适配
2023-12-21 21:24:54 +08:00
6fa4f69af5 opt: 移除回复点赞冗余加载提示 2023-12-21 21:06:35 +08:00
6fb7b4ba73 fix: 全屏时视频底部有可能滑动 2023-12-21 19:21:13 +08:00
a10af323f9 bottom control尺寸优化,更易拖动进度条 2023-12-21 12:24:08 +08:00
621a597d8f 竖屏全屏也隐藏状态栏与导航栏 2023-12-21 12:23:01 +08:00
9676d7d165 Merge pull request #324 from orz12/fix_danmaku_speed
fix: 统一弹幕时间计算方式,speed改为duration
2023-12-20 21:21:10 +08:00
e651ae6232 fix: 竖屏非全屏状态小白条适配 2023-12-20 16:02:33 +08:00
46919596ea fix:设定时除以当前播放速度 2023-12-20 14:16:03 +08:00
faaf416bf0 fix: 统一弹幕时间计算方式,speed改为duration 2023-12-20 13:49:58 +08:00
d9859755e3 fix: 未登录访问up主页异常 2023-12-20 00:34:26 +08:00
7f7154bba4 fix: 视频搜索标题转义 2023-12-20 00:18:31 +08:00
ed91c55b9d fix: iOS自签闪退 issues #320 #306 2023-12-19 23:34:56 +08:00
920f301d62 Merge pull request #319 from orz12/reduce_slider_rebuilds
降低进度条与播放时间的更新频率
2023-12-19 22:26:55 +08:00
3ecb635037 Merge pull request #311 from orz12/fix_orientation
重构全屏逻辑,修复全屏弹幕与横屏错位问题
2023-12-19 22:09:32 +08:00
cd8078a8fa 降低进度条与播放时间的更新频率
新建positionSeconds、sliderPositionSeconds、durationSeconds、bufferedSeconds变量,仅在秒数发生变化时再更新,避免每帧都在重绘控件
2023-12-19 19:25:02 +08:00
16705f008c 修复竖屏全屏尺寸、返回,手势切换问题,还原pinnedHeader 2023-12-19 12:16:10 +08:00
991f002262 fix: 点击关注up后下方的提示不会消失 issues #312 2023-12-19 07:57:41 +08:00
ada1aa5d1d fix: 消息页面夜间模式异常 issues #309 | 包含撤回消息时显示异常 2023-12-19 07:47:18 +08:00
4d07f1508a 重构全屏逻辑,修复全屏弹幕与横屏错位问题
不再使用showDialog覆盖并传递对象的方式实现全屏,改用原控件调整高度(用Obx包裹SliverAppBar)、safeArea切换上下边距、构建detail页时根据屏幕方向切换状态栏可见性的方式实现全屏。
以上方式既能兼容屏幕旋转,也能绕过弹幕不加载的问题,还可以保留播放器上的弹幕避免旋屏时清空。
另外添加了两处针对全屏或旋屏状态的返回处理。
2023-12-18 21:25:28 +08:00
6dd1360a76 Update 1.0.13.1217.md 2023-12-17 23:14:04 +08:00
b6b0a83761 Update 1.0.13.1217.md 2023-12-17 23:13:53 +08:00
00dc919a86 v1.0.13 更新 2023-12-17 23:10:43 +08:00
1351803661 Update main.yml flutter version 2023-12-17 23:01:31 +08:00
3a39571ab4 Merge branch 'fix' 2023-12-17 22:59:44 +08:00
51254f5719 fix: 直播音轨 2023-12-17 22:58:00 +08:00
370a2ddcf7 feat: 视频详情页稍后再看 issues #254 2023-12-17 22:38:07 +08:00
9c1c405d19 Merge branch 'main' into fix 2023-12-17 19:35:02 +08:00
3d6d0b0c44 opt: 长按倍速逻辑优化 issues #240 2023-12-17 16:11:53 +08:00
b82c43c303 Merge branch 'main' into design 2023-12-17 15:26:18 +08:00
8ef3c1d9bb mod: 未登录不显示消息入口 2023-12-17 15:16:34 +08:00
a6ab72cadd feat: 消息分页 2023-12-17 14:55:52 +08:00
a43c071eb5 Merge branch 'main' into feature-notice 2023-12-16 23:59:31 +08:00
e9a356c483 feat: 合集列表自动跳转指定index 2023-12-16 23:11:24 +08:00
52ab78f332 feat: up主页显示获赞数 issues #160 2023-12-16 22:35:01 +08:00
12f90a411b fix: 首页推荐点击加载更多无响应 issues #270 2023-12-16 22:12:12 +08:00
6a888ad72b Merge branch 'fix' 2023-12-16 22:04:18 +08:00
5d4ffd665e Merge branch 'design' 2023-12-16 21:57:30 +08:00
f135a2beae fix: 外观设置超过3列无法刷新 issues #271 2023-12-16 21:37:05 +08:00
676bbe2665 Merge branch 'main' into fix 2023-12-16 21:02:31 +08:00
172ea0fbb6 fix: tabbar alignment 2023-12-16 21:02:16 +08:00
b4b64d9864 fix: duration null error 2023-12-16 20:57:18 +08:00
26ba5bc567 mod 2023-12-16 20:51:53 +08:00
f5be50aaa4 Merge pull request #288 from GuMengYu/setting-style
improve: 设置页面样式改进
2023-12-16 20:46:11 +08:00
0ca877bd25 Merge pull request #279 from orz12/post_danmaku
添加基础发送弹幕功能
2023-12-16 20:39:04 +08:00
7afc973b3e Merge pull request #285 from KoolShow/fix_typo
修复副标题风格不统一
2023-12-16 20:33:28 +08:00
a14283260e improve: 设置页面样式改进
- 下拉选择改为dialog, 参考 Android 系统内部设置关于选项的行为
- 一些列表对齐问题
- 字体设置预览文字居中
2023-12-15 13:22:51 +08:00
173695ace6 修复副标题风格不统一
修复 播放设置 自动PiP播放 选项副标题风格不统一
2023-12-13 17:57:41 +08:00
ebbd280768 添加基础发送弹幕功能 2023-12-11 11:56:54 +08:00
bda56169b0 mod 2023-12-04 00:01:07 +08:00
c85f5abcdb mod: 登录时获取accessKey 2023-12-03 00:58:41 +08:00
4217fa26e2 Merge branch 'fix' 2023-12-02 23:11:40 +08:00
f8ca41e4d1 mod 2023-12-02 23:11:22 +08:00
4550ce2637 feat: ai总结开关 2023-12-02 23:10:11 +08:00
6ebfe5872e feat: 首页顶栏&底栏固定 issues #243 2023-12-02 22:41:24 +08:00
7ed91a72c6 Merge branch 'fix' 2023-12-02 21:55:40 +08:00
7f7e1b2035 mod 2023-12-02 21:35:55 +08:00
00f84e1a1c mod: 依赖升级 2023-12-02 21:33:46 +08:00
ebb1d78dbb fix: accessKey获取 2023-12-01 21:21:09 +08:00
427d1385db Merge branch 'design' 2023-11-27 00:43:53 +08:00
e73e02cf13 mod: 个人主页 2023-11-27 00:41:22 +08:00
1f1804b472 mod: 修改版本号 v1.0.11 -> v1.0.12 2023-11-14 07:52:45 +08:00
9fa9b5c1f3 merge main 2023-11-12 12:02:31 +08:00
4272b8141a merge main 2023-10-17 08:26:33 +08:00
43e2a2a10a Merge branch 'main' into feature-notice 2023-10-16 00:07:13 +08:00
b272d25157 feat: 私信查看 2023-10-15 16:12:09 +08:00
122 changed files with 5066 additions and 2321 deletions

View File

@ -36,7 +36,7 @@ jobs:
if: steps.cache-flutter.outputs.cache-hit != 'true' if: steps.cache-flutter.outputs.cache-hit != 'true'
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
flutter-version: 3.10.6 flutter-version: 3.16.4
channel: any channel: any
- name: 下载项目依赖 - name: 下载项目依赖

View File

@ -63,6 +63,7 @@ android {
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
minSdkVersion 19 minSdkVersion 19
multiDexEnabled true
} }
signingConfigs { signingConfigs {

BIN
assets/images/live.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 B

22
change_log/1.0.13.1217.md Normal file
View File

@ -0,0 +1,22 @@
## 1.0.13
### 新功能
+ 视频详情页稍后再看
+ 发送弹幕 感谢@orz12
+ 消息展示
+ up主页显示获赞数
+ up主页显示合集
+ 视频详情页「ai总结」增加开关
### 修复
+ 首页推荐问题(需要重新登录)
+ 长按倍速逻辑
+ 视频详情页网络异常
### 优化
+ 设置面板样式 感谢@GuMengYu @KoolShow
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

28
change_log/1.0.14.1225.md Normal file
View File

@ -0,0 +1,28 @@
## 1.0.14
圣诞节快乐~ 🎉
大部分内容由@orz12提供,感谢👏
### 修复
+ 全屏弹幕消失
+ iOS全屏/退出全屏视频暂停
+ 个人主页关注状态
+ 视频合集向下滑动UI问题
+ 媒体库滑动底栏不隐藏
+ 个人主页动态加载问题 * 2
+ 未登录状态访问个人主页异常
+ 视频搜索标题特殊字符转义
+ iOS闪退
+ 消息页面夜间模式异常
+ 消息页面含有撤回消息时异常
+ 弹幕速度
### 优化
+ 全屏播放方案优化
+ 弹幕加载逻辑优化
+ 点赞、投币逻辑优化
+ 进度条及播放时间渲染优化
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

View File

@ -37,5 +37,11 @@ end
post_install do |installer| post_install do |installer|
installer.pods_project.targets.each do |target| installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target) flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
deployment_target = config.build_settings['IPHONEOS_DEPLOYMENT_TARGET']
if !deployment_target.nil? && !deployment_target.empty? && deployment_target.to_f < 12.0
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0'
end
end
end end
end end

View File

@ -1,6 +1,12 @@
PODS: PODS:
- appscheme (1.0.4): - appscheme (1.0.4):
- Flutter - Flutter
- audio_service (0.0.1):
- Flutter
- audio_session (0.0.1):
- Flutter
- auto_orientation (0.0.1):
- Flutter
- connectivity_plus (0.0.1): - connectivity_plus (0.0.1):
- Flutter - Flutter
- ReachabilitySwift - ReachabilitySwift
@ -56,6 +62,9 @@ PODS:
DEPENDENCIES: DEPENDENCIES:
- appscheme (from `.symlinks/plugins/appscheme/ios`) - appscheme (from `.symlinks/plugins/appscheme/ios`)
- audio_service (from `.symlinks/plugins/audio_service/ios`)
- audio_session (from `.symlinks/plugins/audio_session/ios`)
- auto_orientation (from `.symlinks/plugins/auto_orientation/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
@ -88,6 +97,12 @@ SPEC REPOS:
EXTERNAL SOURCES: EXTERNAL SOURCES:
appscheme: appscheme:
:path: ".symlinks/plugins/appscheme/ios" :path: ".symlinks/plugins/appscheme/ios"
audio_service:
:path: ".symlinks/plugins/audio_service/ios"
audio_session:
:path: ".symlinks/plugins/audio_session/ios"
auto_orientation:
:path: ".symlinks/plugins/auto_orientation/ios"
connectivity_plus: connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios" :path: ".symlinks/plugins/connectivity_plus/ios"
device_info_plus: device_info_plus:
@ -135,8 +150,11 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS: SPEC CHECKSUMS:
appscheme: b1c3f8862331cb20430cf9e0e4af85dbc1572ad8 appscheme: b1c3f8862331cb20430cf9e0e4af85dbc1572ad8
audio_service: f509d65da41b9521a61f1c404dd58651f265a567
audio_session: 4f3e461722055d21515cf3261b64c973c062f345
auto_orientation: 102ed811a5938d52c86520ddd7ecd3a126b5d39d
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_volume_controller: e4d5832f08008180f76e30faf671ffd5a425e529 flutter_volume_controller: e4d5832f08008180f76e30faf671ffd5a425e529
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
@ -145,22 +163,22 @@ SPEC CHECKSUMS:
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1 media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7 package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
saver_gallery: 2b4e584106fde2407ab51560f3851564963e6b78 saver_gallery: 2b4e584106fde2407ab51560f3851564963e6b78
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625 screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028 share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
status_bar_control: 7c84146799e6a076315cc1550f78ef53aae3e446 status_bar_control: 7c84146799e6a076315cc1550f78ef53aae3e446
system_proxy: bec1a5c5af67dd3e3ebf43979400a8756c04cc44 system_proxy: bec1a5c5af67dd3e3ebf43979400a8756c04cc44
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 url_launcher_ios: bf5ce03e0e2088bad9cc378ea97fa0ed5b49673b
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9 volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47 wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
webview_cookie_manager: eaf920722b493bd0f7611b5484771ca53fed03f7 webview_cookie_manager: eaf920722b493bd0f7611b5484771ca53fed03f7
webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a
PODFILE CHECKSUM: cc1f88378b4bfcf93a6ce00d2c587857c6008d3b PODFILE CHECKSUM: 637cd290bed23275b5f5ffcc7eb1e73d0a5fb2be
COCOAPODS: 1.12.1 COCOAPODS: 1.14.3

View File

@ -121,7 +121,6 @@
3DA6FBBC55FDD1E3261D6D67 /* Pods-Runner.release.xcconfig */, 3DA6FBBC55FDD1E3261D6D67 /* Pods-Runner.release.xcconfig */,
32E2926120A1A8DC0E629BC6 /* Pods-Runner.profile.xcconfig */, 32E2926120A1A8DC0E629BC6 /* Pods-Runner.profile.xcconfig */,
); );
name = Pods;
path = Pods; path = Pods;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@ -157,7 +156,7 @@
97C146E61CF9000F007C117D /* Project object */ = { 97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
LastUpgradeCheck = 1300; LastUpgradeCheck = 1430;
ORGANIZATIONNAME = ""; ORGANIZATIONNAME = "";
TargetAttributes = { TargetAttributes = {
97C146ED1CF9000F007C117D = { 97C146ED1CF9000F007C117D = {
@ -361,7 +360,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos; SUPPORTED_PLATFORMS = iphoneos;
@ -377,7 +376,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = RN352BA826; DEVELOPMENT_TEAM = "";
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@ -439,7 +438,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES; MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@ -488,7 +487,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos; SUPPORTED_PLATFORMS = iphoneos;
@ -506,7 +505,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = RN352BA826; DEVELOPMENT_TEAM = "";
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@ -529,7 +528,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = RN352BA826; DEVELOPMENT_TEAM = "";
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1300" LastUpgradeVersion = "1430"
version = "1.3"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"

View File

@ -1,8 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22504"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<scenes> <scenes>
<!--Flutter View Controller--> <!--Flutter View Controller-->
@ -14,13 +16,14 @@
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/> <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides> </layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC"> <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/> <rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/> <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view> </view>
</viewController> </viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="-11" y="-41"/>
</scene> </scene>
</scenes> </scenes>
</document> </document>

View File

@ -9,7 +9,11 @@ class StyleString {
} }
class Constants { class Constants {
static const String appKey = '27eb53fc9058f8c3'; // 27eb53fc9058f8c3 移动端 Android
// 4409e2ce8ffd12b8 TV端
static const String appKey = '4409e2ce8ffd12b8';
// 59b43e04ad6965f34319062b478f83dd TV端
static const String appSec = '59b43e04ad6965f34319062b478f83dd';
static const String thirdSign = '04224646d1fea004e79606d3b038c84a'; static const String thirdSign = '04224646d1fea004e79606d3b038c84a';
static const String thirdApi = static const String thirdApi =
'https://www.mcbbs.net/template/mcbbs/image/special_photo_bg.png'; 'https://www.mcbbs.net/template/mcbbs/image/special_photo_bg.png';

View File

@ -1,37 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
// Widget pBadge(
// text,
// context,
// double? top,
// double? right,
// double? bottom,
// double? left, {
// type = 'primary',
// }) {
// Color bgColor = Theme.of(context).colorScheme.primary;
// Color color = Theme.of(context).colorScheme.onPrimary;
// if (type == 'gray') {
// bgColor = Colors.black54.withOpacity(0.4);
// color = Colors.white;
// }
// return Positioned(
// top: top,
// left: left,
// right: right,
// bottom: bottom,
// child: Container(
// padding: const EdgeInsets.symmetric(vertical: 1, horizontal: 6),
// decoration:
// BoxDecoration(borderRadius: BorderRadius.circular(4), color: bgColor),
// child: Text(
// text,
// style: TextStyle(fontSize: 11, color: color),
// ),
// ),
// );
// }
class PBadge extends StatelessWidget { class PBadge extends StatelessWidget {
final String? text; final String? text;
final double? top; final double? top;

View File

@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
class ContentContainer extends StatelessWidget {
final Widget? contentWidget;
final Widget? bottomWidget;
final bool isScrollable;
final Clip? childClipBehavior;
const ContentContainer(
{Key? key,
this.contentWidget,
this.bottomWidget,
this.isScrollable = true,
this.childClipBehavior})
: super(key: key);
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return SingleChildScrollView(
clipBehavior: childClipBehavior ?? Clip.hardEdge,
physics: isScrollable ? null : NeverScrollableScrollPhysics(),
child: ConstrainedBox(
constraints: constraints.copyWith(
minHeight: constraints.maxHeight,
maxHeight: double.infinity,
),
child: IntrinsicHeight(
child: Column(
children: <Widget>[
if (contentWidget != null)
Expanded(
child: contentWidget!,
)
else
Spacer(),
if (bottomWidget != null) bottomWidget!,
],
),
),
),
);
},
);
}
}

View File

@ -17,6 +17,10 @@ class VideoCardH extends StatelessWidget {
final Function()? longPress; final Function()? longPress;
final Function()? longPressEnd; final Function()? longPressEnd;
final String source; final String source;
final bool showOwner;
final bool showView;
final bool showDanmaku;
final bool showPubdate;
const VideoCardH({ const VideoCardH({
Key? key, Key? key,
@ -24,6 +28,10 @@ class VideoCardH extends StatelessWidget {
this.longPress, this.longPress,
this.longPressEnd, this.longPressEnd,
this.source = 'normal', this.source = 'normal',
this.showOwner = true,
this.showView = true,
this.showDanmaku = true,
this.showPubdate = false,
}) : super(key: key); }) : super(key: key);
@override @override
@ -103,7 +111,14 @@ class VideoCardH extends StatelessWidget {
}, },
), ),
), ),
VideoContent(videoItem: videoItem, source: source) VideoContent(
videoItem: videoItem,
source: source,
showOwner: showOwner,
showView: showView,
showDanmaku: showDanmaku,
showPubdate: showPubdate,
)
], ],
), ),
); );
@ -119,8 +134,20 @@ class VideoContent extends StatelessWidget {
// ignore: prefer_typing_uninitialized_variables // ignore: prefer_typing_uninitialized_variables
final videoItem; final videoItem;
final String source; final String source;
const VideoContent( final bool showOwner;
{super.key, required this.videoItem, this.source = 'normal'}); final bool showView;
final bool showDanmaku;
final bool showPubdate;
const VideoContent({
super.key,
required this.videoItem,
this.source = 'normal',
this.showOwner = true,
this.showView = true,
this.showDanmaku = true,
this.showPubdate = false,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -179,34 +206,40 @@ class VideoContent extends StatelessWidget {
// ), // ),
// ), // ),
// const SizedBox(height: 4), // const SizedBox(height: 4),
Row( if (showPubdate)
children: [ Text(
Text( Utils.dateFormat(videoItem.pubdate!),
videoItem.owner.name, style: TextStyle(
style: TextStyle( fontSize: 11, color: Theme.of(context).colorScheme.outline),
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, ),
color: Theme.of(context).colorScheme.outline, if (showOwner)
Row(
children: [
Text(
videoItem.owner.name,
style: TextStyle(
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
), ),
), ],
], ),
),
Row( Row(
children: [ children: [
StatView( if (showView) ...[
theme: 'gray', StatView(
view: videoItem.stat.view, theme: 'gray',
), view: videoItem.stat.view,
const SizedBox(width: 8), ),
StatDanMu( const SizedBox(width: 8),
theme: 'gray', ],
danmu: videoItem.stat.danmaku, if (showDanmaku)
), StatDanMu(
// Text( theme: 'gray',
// Utils.dateFormat(videoItem.pubdate!), danmu: videoItem.stat.danmaku,
// style: TextStyle( ),
// fontSize: 11,
// color: Theme.of(context).colorScheme.outline),
// )
const Spacer(), const Spacer(),
// SizedBox( // SizedBox(
// width: 20, // width: 20,

View File

@ -333,8 +333,10 @@ class VideoStat extends StatelessWidget {
color: Theme.of(context).colorScheme.outline, color: Theme.of(context).colorScheme.outline,
), ),
children: [ children: [
TextSpan(text: '${videoItem.stat.view}观看'), if (videoItem.stat.view != '-')
TextSpan(text: '${videoItem.stat.danmu}弹幕'), TextSpan(text: '${videoItem.stat.view}观看'),
if (videoItem.stat.danmu != '-')
TextSpan(text: '${videoItem.stat.danmu}弹幕'),
], ],
), ),
); );

View File

@ -97,8 +97,8 @@ class Api {
// 操作用户关系 // 操作用户关系
static const String relationMod = '/x/relation/modify'; static const String relationMod = '/x/relation/modify';
// 相互关系查询 // 相互关系查询 // 失效
static const String relationSearch = '/x/space/wbi/acc/relation'; // static const String relationSearch = '/x/space/wbi/acc/relation';
// 评论列表 // 评论列表
// https://api.bilibili.com/x/v2/reply/main?csrf=6e22efc1a47225ea25f901f922b5cfdd&mode=3&oid=254175381&pagination_str=%7B%22offset%22:%22%22%7D&plat=1&seek_rpid=0&type=11 // https://api.bilibili.com/x/v2/reply/main?csrf=6e22efc1a47225ea25f901f922b5cfdd&mode=3&oid=254175381&pagination_str=%7B%22offset%22:%22%22%7D&plat=1&seek_rpid=0&type=11
@ -215,7 +215,7 @@ class Api {
// 粉丝 // 粉丝
// vmid 用户id pn 页码 ps 每页个数最大50 order: desc // vmid 用户id pn 页码 ps 每页个数最大50 order: desc
// order_type 排序规则 最近访问传空,最常访问传 attention // order_type 排序规则 最近访问传空,最常访问传 attention
static const String fans = 'https://api.bilibili.com/x/relation/fans'; static const String fans = '/x/relation/fans';
// 直播 // 直播
// ?page=1&page_size=30&platform=web // ?page=1&page_size=30&platform=web
@ -312,6 +312,10 @@ class Api {
static const String webDanmaku = '/x/v2/dm/web/seg.so'; static const String webDanmaku = '/x/v2/dm/web/seg.so';
//发送视频弹幕
//https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/danmaku/action.md
static const String shootDanmaku = '/x/v2/dm/post';
// up主分组 // up主分组
static const String followUpTag = '/x/relation/tags'; static const String followUpTag = '/x/relation/tags';
@ -322,6 +326,51 @@ class Api {
// 获取指定分组下的up // 获取指定分组下的up
static const String followUpGroup = '/x/relation/tag'; static const String followUpGroup = '/x/relation/tag';
/// 私聊
/// 'https://api.vc.bilibili.com/session_svr/v1/session_svr/get_sessions?
/// session_type=1&
/// group_fold=1&
/// unfollow_fold=0&
/// sort_rule=2&
/// build=0&
/// mobi_app=web&
/// w_rid=8641d157fb9a9255eb2159f316ee39e2&
/// wts=1697305010
static const String sessionList =
'https://api.vc.bilibili.com/session_svr/v1/session_svr/get_sessions';
/// 私聊用户信息
/// uids
/// build=0&mobi_app=web
static const String sessionAccountList =
'https://api.vc.bilibili.com/account/v1/user/cards';
/// https://api.vc.bilibili.com/svr_sync/v1/svr_sync/fetch_session_msgs?
/// talker_id=400787461&
/// session_type=1&
/// size=20&
/// sender_device_id=1&
/// build=0&
/// mobi_app=web&
/// web_location=333.1296&
/// w_rid=cfe3bf58c9fe181bbf4dd6c75175e6b0&
/// wts=1697350697
static const String sessionMsg =
'https://api.vc.bilibili.com/svr_sync/v1/svr_sync/fetch_session_msgs';
/// 标记已读 POST
/// talker_id:
/// session_type: 1
/// ack_seqno: 920224140918926
/// build: 0
/// mobi_app: web
/// csrf_token:
/// csrf:
static const String updateAck =
'https://api.vc.bilibili.com/session_svr/v1/session_svr/update_ack';
// 获取某个动态详情 // 获取某个动态详情
// timezone_offset=-480 // timezone_offset=-480
// id=849312409672744983 // id=849312409672744983
@ -372,4 +421,49 @@ class Api {
/// local_id /// local_id
static const getWebKey = static const getWebKey =
'https://passport.bilibili.com/x/passport-login/web/key'; 'https://passport.bilibili.com/x/passport-login/web/key';
/// cookie转access_key
static const cookieToKey =
'https://passport.bilibili.com/x/passport-tv-login/h5/qrcode/confirm';
/// 申请二维码(TV端)
static const getTVCode =
'https://passport.snm0516.aisee.tv/x/passport-tv-login/qrcode/auth_code';
///扫码登录TV端
static const qrcodePoll =
'https://passport.bilibili.com/x/passport-tv-login/qrcode/poll';
/// 置顶视频
static const getTopVideoApi = '/x/space/top/arc';
/// 主页 - 最近投币的视频
/// vmid
/// gaia_source = main_web
/// web_location
/// w_rid
/// wts
static const getRecentCoinVideoApi = '/x/space/coin/video';
/// 最近点赞的视频
static const getRecentLikeVideoApi = '/x/space/like/video';
/// 最近追番
static const getRecentBangumiApi = '/x/space/bangumi/follow/list';
/// 用户专栏
static const getMemberSeasonsApi = '/x/polymer/web-space/home/seasons_series';
/// 获赞数 播放数
/// mid
static const getMemberViewApi = '/x/space/upstat';
/// 查询某个专栏
/// mid
/// season_id
/// sort_reverse
/// page_num
/// page_size
static const getSeasonDetailApi =
'/x/polymer/web-space/seasons_archives_list';
} }

View File

@ -24,4 +24,72 @@ class DanmakaHttp {
); );
return DmSegMobileReply.fromBuffer(response.data); return DmSegMobileReply.fromBuffer(response.data);
} }
static Future shootDanmaku({
int type = 1,//弹幕类选择(1视频弹幕 2漫画弹幕)
required int oid,// 视频cid
required String msg,//弹幕文本(长度小于 100 字符)
int mode = 1,// 弹幕类型(1滚动弹幕 4底端弹幕 5顶端弹幕 6逆向弹幕(不能使用) 7高级弹幕 8代码弹幕不能使用 9BAS弹幕pool必须为2)
// String? aid,// 稿件avid
// String? bvid,// bvid与aid必须有一个
required String bvid,
int? progress,// 弹幕出现在视频内的时间单位为毫秒默认为0
int? color,// 弹幕颜色(默认白色16777215
int? fontsize,// 弹幕字号默认25
int? pool,// 弹幕池选择0普通池 1字幕池 2特殊池代码/BAS弹幕默认普通池0
//int? rnd,// 当前时间戳*1000000若无此项则发送弹幕冷却时间限制为90s若有此项则发送弹幕冷却时间限制为5s
int? colorful,//60001专属渐变彩色需要会员
int? checkbox_type,//是否带 UP 身份标识0普通4带有标识
// String? csrf,//CSRF Token位于 Cookie Cookie 方式必要
// String? access_key,// APP 登录 Token APP 方式必要
}) async {
// 构建参数对象
// assert(aid != null || bvid != null);
// assert(csrf != null || access_key != null);
assert(msg.length < 100);
// 构建参数对象
var params = <String, dynamic>{
'type': type,
'oid': oid,
'msg': msg,
'mode': mode,
//'aid': aid,
'bvid': bvid,
'progress': progress,
'color': color,
'fontsize': fontsize,
'pool': pool,
'rnd': DateTime.now().microsecondsSinceEpoch,
'colorful': colorful,
'checkbox_type': checkbox_type,
'csrf': await Request.getCsrf(),
// 'access_key': access_key,
}..removeWhere((key, value) => value == null);
var response = await Request().post(
Api.shootDanmaku,
data: params,
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
);
if (response.statusCode != 200) {
return {
'status': false,
'data': [],
'msg': '弹幕发送失败,状态码:${response.statusCode}',
};
}
if (response.data['code'] == 0) {
return {
'status': true,
'data': response.data['data'],
};
} else {
return {
'status': false,
'data': [],
'msg': "${response.data['code']}: ${response.data['message']}",
};
}
}
} }

View File

@ -1,9 +1,16 @@
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/http/index.dart'; import 'package:pilipala/http/index.dart';
import 'package:pilipala/models/dynamics/result.dart'; import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/models/follow/result.dart'; import 'package:pilipala/models/follow/result.dart';
import 'package:pilipala/models/member/archive.dart'; import 'package:pilipala/models/member/archive.dart';
import 'package:pilipala/models/member/coin.dart';
import 'package:pilipala/models/member/info.dart'; import 'package:pilipala/models/member/info.dart';
import 'package:pilipala/models/member/seasons.dart';
import 'package:pilipala/models/member/tags.dart'; import 'package:pilipala/models/member/tags.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/utils/wbi_sign.dart'; import 'package:pilipala/utils/wbi_sign.dart';
class MemberHttp { class MemberHttp {
@ -215,4 +222,243 @@ class MemberHttp {
}; };
} }
} }
// 获取up置顶
static Future getTopVideo(String? vmid) async {
var res = await Request().get(Api.getTopVideoApi);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data']
.map<MemberTagItemModel>((e) => MemberTagItemModel.fromJson(e))
.toList()
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
// 获取uo专栏
static Future getMemberSeasons(int? mid, int? pn, int? ps) async {
var res = await Request().get(Api.getMemberSeasonsApi, data: {
'mid': mid,
'page_num': pn,
'page_size': ps,
});
if (res.data['code'] == 0) {
return {
'status': true,
'data': MemberSeasonsDataModel.fromJson(res.data['data']['items_lists'])
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
// 最近投币
static Future getRecentCoinVideo({required int mid}) async {
Map params = await WbiSign().makSign({
'mid': mid,
'gaia_source': 'main_web',
'web_location': 333.999,
});
var res = await Request().get(
Api.getRecentCoinVideoApi,
data: {
'vmid': mid,
'gaia_source': 'main_web',
'web_location': 333.999,
'w_rid': params['w_rid'],
'wts': params['wts'],
},
);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data']
.map<MemberCoinsDataModel>((e) => MemberCoinsDataModel.fromJson(e))
.toList(),
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
// 最近点赞
static Future getRecentLikeVideo({required int mid}) async {
Map params = await WbiSign().makSign({
'mid': mid,
'gaia_source': 'main_web',
'web_location': 333.999,
});
var res = await Request().get(
Api.getRecentLikeVideoApi,
data: {
'vmid': mid,
'gaia_source': 'main_web',
'web_location': 333.999,
'w_rid': params['w_rid'],
'wts': params['wts'],
},
);
if (res.data['code'] == 0) {
return {
'status': true,
'data': MemberSeasonsDataModel.fromJson(res.data['data']['items_lists'])
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
// 查看某个专栏
static Future getSeasonDetail({
required int mid,
required int seasonId,
bool sortReverse = false,
required int pn,
required int ps,
}) async {
var res = await Request().get(
Api.getSeasonDetailApi,
data: {
'mid': mid,
'season_id': seasonId,
'sort_reverse': sortReverse,
'page_num': pn,
'page_size': ps,
},
);
if (res.data['code'] == 0) {
try {
return {
'status': true,
'data': MemberSeasonsList.fromJson(res.data['data'])
};
} catch (err) {
print(err);
}
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
// 获取TV authCode
static Future getTVCode() async {
SmartDialog.showLoading();
var params = {
'appkey': Constants.appKey,
'local_id': '0',
'ts': (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString(),
};
String sign = Utils.appSign(
params,
Constants.appKey,
Constants.appSec,
);
var res = await Request()
.post(Api.getTVCode, queryParameters: {...params, 'sign': sign});
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data']['auth_code'],
'msg': '操作成功'
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
// 获取access_key
static Future cookieToKey() async {
var authCodeRes = await getTVCode();
if (authCodeRes['status']) {
var res = await Request().post(Api.cookieToKey, queryParameters: {
'auth_code': authCodeRes['data'],
'build': 708200,
'csrf': await Request.getCsrf(),
});
await Future.delayed(const Duration(milliseconds: 300));
await qrcodePoll(authCodeRes['data']);
if (res.data['code'] == 0) {
return {'status': true, 'data': [], 'msg': '操作成功'};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
}
static Future qrcodePoll(authCode) async {
var params = {
'appkey': Constants.appKey,
'auth_code': authCode.toString(),
'local_id': '0',
'ts': (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString(),
};
String sign = Utils.appSign(
params,
Constants.appKey,
Constants.appSec,
);
var res = await Request()
.post(Api.qrcodePoll, queryParameters: {...params, 'sign': sign});
SmartDialog.dismiss();
if (res.data['code'] == 0) {
String accessKey = res.data['data']['access_token'];
Box localCache = GStrorage.localCache;
Box userInfoCache = GStrorage.userInfo;
var userInfo = userInfoCache.get('userInfoCache');
localCache.put(
LocalCacheKey.accessKey, {'mid': userInfo.mid, 'value': accessKey});
return {'status': true, 'data': [], 'msg': '操作成功'};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
// 获取up播放数、点赞数
static Future memberView({required int mid}) async {
var res = await Request().get(Api.getMemberViewApi, data: {'mid': mid});
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
} }

89
lib/http/msg.dart Normal file
View File

@ -0,0 +1,89 @@
import 'package:pilipala/http/api.dart';
import 'package:pilipala/http/init.dart';
import 'package:pilipala/models/msg/account.dart';
import 'package:pilipala/models/msg/session.dart';
import 'package:pilipala/utils/wbi_sign.dart';
class MsgHttp {
// 会话列表
static Future sessionList({int? endTs}) async {
Map<String, dynamic> params = {
'session_type': 1,
'group_fold': 1,
'unfollow_fold': 0,
'sort_rule': 2,
'build': 0,
'mobi_app': 'web',
};
if (endTs != null) {
params['end_ts'] = endTs;
}
Map signParams = await WbiSign().makSign(params);
var res = await Request().get(Api.sessionList, data: signParams);
if (res.data['code'] == 0) {
return {
'status': true,
'data': SessionDataModel.fromJson(res.data['data']),
};
} else {
return {
'status': false,
'date': [],
'msg': res.data['message'],
};
}
}
static Future accountList(uids) async {
var res = await Request().get(Api.sessionAccountList, data: {
'uids': uids,
'build': 0,
'mobi_app': 'web',
});
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data']
.map<AccountListModel>((e) => AccountListModel.fromJson(e))
.toList(),
};
} else {
return {
'status': false,
'date': [],
'msg': res.data['message'],
};
}
}
static Future sessionMsg({
int? talkerId,
}) async {
Map params = await WbiSign().makSign({
'talker_id': talkerId,
'session_type': 1,
'size': 20,
'sender_device_id': 1,
'build': 0,
'mobi_app': 'web',
});
var res = await Request().get(Api.sessionMsg, data: params);
if (res.data['code'] == 0) {
try {
return {
'status': true,
'data': SessionMsgDataModel.fromJson(res.data['data']),
};
} catch (err) {
print(err);
}
} else {
return {
'status': false,
'date': [],
'msg': res.data['message'],
};
}
}
}

View File

@ -199,7 +199,7 @@ class UserHttp {
} }
} }
// 获取用户凭证 // 获取用户凭证 失效
static Future thirdLogin() async { static Future thirdLogin() async {
var res = await Request().get( var res = await Request().get(
'https://passport.bilibili.com/login/app/third', 'https://passport.bilibili.com/login/app/third',
@ -251,30 +251,43 @@ class UserHttp {
} }
} }
// 相互关系查询 static Future hasFollow(int mid) async {
static Future relationSearch(int mid) async {
Map params = await WbiSign().makSign({
'mid': mid,
'token': '',
'platform': 'web',
'web_location': 1550101,
});
var res = await Request().get( var res = await Request().get(
Api.relationSearch, Api.hasFollow,
data: { data: {
'mid': mid, 'fid': mid,
'w_rid': params['w_rid'],
'wts': params['wts'],
}, },
); );
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
// relation 主动状态
// 被动状态
return {'status': true, 'data': res.data['data']}; return {'status': true, 'data': res.data['data']};
} else { } else {
return {'status': false, 'msg': res.data['message']}; return {'status': false, 'msg': res.data['message']};
} }
} }
// // 相互关系查询
// static Future relationSearch(int mid) async {
// Map params = await WbiSign().makSign({
// 'mid': mid,
// 'token': '',
// 'platform': 'web',
// 'web_location': 1550101,
// });
// var res = await Request().get(
// Api.relationSearch,
// data: {
// 'mid': mid,
// 'w_rid': params['w_rid'],
// 'wts': params['wts'],
// },
// );
// if (res.data['code'] == 0) {
// // relation 主动状态
// // 被动状态
// return {'status': true, 'data': res.data['data']};
// } else {
// return {'status': false, 'msg': res.data['message']};
// }
// }
// 搜索历史记录 // 搜索历史记录
static Future searchHistory( static Future searchHistory(

View File

@ -118,7 +118,7 @@ class RcmdStat {
RcmdStat.fromJson(Map<String, dynamic> json) { RcmdStat.fromJson(Map<String, dynamic> json) {
view = json["cover_left_text_1"]; view = json["cover_left_text_1"];
danmu = json['cover_left_text_2']; danmu = json['cover_left_text_2'] ?? '-';
} }
} }

View File

@ -0,0 +1,89 @@
class MemberCoinsDataModel {
MemberCoinsDataModel({
this.aid,
this.bvid,
this.cid,
this.coins,
this.copyright,
this.ctime,
this.desc,
this.duration,
this.owner,
this.pic,
this.pubLocation,
this.pubdate,
this.resourceType,
this.state,
this.subtitle,
this.time,
this.title,
this.tname,
this.videos,
this.view,
this.danmaku,
});
int? aid;
String? bvid;
int? cid;
int? coins;
int? copyright;
int? ctime;
String? desc;
int? duration;
Owner? owner;
String? pic;
String? pubLocation;
int? pubdate;
String? resourceType;
int? state;
String? subtitle;
int? time;
String? title;
String? tname;
int? videos;
int? view;
int? danmaku;
MemberCoinsDataModel.fromJson(Map<String, dynamic> json) {
aid = json['aid'];
bvid = json['bvid'];
cid = json['cid'];
coins = json['coins'];
copyright = json['copyright'];
ctime = json['ctime'];
desc = json['desc'];
duration = json['duration'];
owner = Owner.fromJson(json['owner']);
pic = json['pic'];
pubLocation = json['pub_location'];
pubdate = json['pubdate'];
resourceType = json['resource_type'];
state = json['state'];
subtitle = json['subtitle'];
time = json['time'];
title = json['title'];
tname = json['tname'];
videos = json['videos'];
view = json['stat']['view'];
danmaku = json['stat']['danmaku'];
}
}
class Owner {
Owner({
this.mid,
this.name,
this.face,
});
int? mid;
String? name;
String? face;
Owner.fromJson(Map<String, dynamic> json) {
mid = json['mid'];
name = json['name'];
face = json['face'];
}
}

View File

@ -0,0 +1,108 @@
class MemberSeasonsDataModel {
MemberSeasonsDataModel({
this.page,
this.seasonsList,
});
Map? page;
List<MemberSeasonsList>? seasonsList;
MemberSeasonsDataModel.fromJson(Map<String, dynamic> json) {
page = json['page'];
seasonsList = json['seasons_list'] != null
? json['seasons_list']
.map<MemberSeasonsList>((e) => MemberSeasonsList.fromJson(e))
.toList()
: [];
}
}
class MemberSeasonsList {
MemberSeasonsList({
this.archives,
this.meta,
this.recentAids,
this.page,
});
List<MemberArchiveItem>? archives;
MamberMeta? meta;
List? recentAids;
Map? page;
MemberSeasonsList.fromJson(Map<String, dynamic> json) {
archives = json['archives'] != null
? json['archives']
.map<MemberArchiveItem>((e) => MemberArchiveItem.fromJson(e))
.toList()
: [];
meta = MamberMeta.fromJson(json['meta']);
page = json['page'];
}
}
class MemberArchiveItem {
MemberArchiveItem({
this.aid,
this.bvid,
this.ctime,
this.duration,
this.pic,
this.cover,
this.pubdate,
this.view,
this.title,
});
int? aid;
String? bvid;
int? ctime;
int? duration;
String? pic;
String? cover;
int? pubdate;
int? view;
String? title;
MemberArchiveItem.fromJson(Map<String, dynamic> json) {
aid = json['aid'];
bvid = json['bvid'];
ctime = json['ctime'];
duration = json['duration'];
pic = json['pic'];
cover = json['pic'];
pubdate = json['pubdate'];
view = json['stat']['view'];
title = json['title'];
}
}
class MamberMeta {
MamberMeta({
this.cover,
this.description,
this.mid,
this.name,
this.ptime,
this.seasonId,
this.total,
});
String? cover;
String? description;
int? mid;
String? name;
int? ptime;
int? seasonId;
int? total;
MamberMeta.fromJson(Map<String, dynamic> json) {
cover = json['cover'];
description = json['description'];
mid = json['mid'];
name = json['name'];
ptime = json['ptime'];
seasonId = json['season_id'];
total = json['total'];
}
}

View File

@ -36,7 +36,7 @@ class RecVideoItemModel {
@HiveField(6) @HiveField(6)
String? title = ''; String? title = '';
@HiveField(7) @HiveField(7)
int? duration = -1; String? duration = '';
@HiveField(8) @HiveField(8)
int? pubdate = -1; int? pubdate = -1;
@HiveField(9) @HiveField(9)
@ -56,7 +56,7 @@ class RecVideoItemModel {
uri = json["uri"]; uri = json["uri"];
pic = json["pic"]; pic = json["pic"];
title = json["title"]; title = json["title"];
duration = json["duration"]; duration = json["duration"].toString();
pubdate = json["pubdate"]; pubdate = json["pubdate"];
owner = Owner.fromJson(json["owner"]); owner = Owner.fromJson(json["owner"]);
stat = Stat.fromJson(json["stat"]); stat = Stat.fromJson(json["stat"]);
@ -72,19 +72,19 @@ class Stat {
Stat({ Stat({
this.view, this.view,
this.like, this.like,
this.danmaku, this.danmu,
}); });
@HiveField(0) @HiveField(0)
int? view; int? view;
@HiveField(1) @HiveField(1)
int? like; int? like;
@HiveField(2) @HiveField(2)
int? danmaku; int? danmu;
Stat.fromJson(Map<String, dynamic> json) { Stat.fromJson(Map<String, dynamic> json) {
view = json["view"]; view = json["view"];
like = json["like"]; like = json["like"];
danmaku = json['danmaku']; danmu = json['danmaku'];
} }
} }

View File

@ -24,7 +24,7 @@ class RecVideoItemModelAdapter extends TypeAdapter<RecVideoItemModel> {
uri: fields[4] as String?, uri: fields[4] as String?,
pic: fields[5] as String?, pic: fields[5] as String?,
title: fields[6] as String?, title: fields[6] as String?,
duration: fields[7] as int?, duration: fields[7] as String?,
pubdate: fields[8] as int?, pubdate: fields[8] as int?,
owner: fields[9] as Owner?, owner: fields[9] as Owner?,
stat: fields[10] as Stat?, stat: fields[10] as Stat?,
@ -89,7 +89,7 @@ class StatAdapter extends TypeAdapter<Stat> {
return Stat( return Stat(
view: fields[0] as int?, view: fields[0] as int?,
like: fields[1] as int?, like: fields[1] as int?,
danmaku: fields[2] as int?, danmu: fields[2] as int?,
); );
} }
@ -102,7 +102,7 @@ class StatAdapter extends TypeAdapter<Stat> {
..writeByte(1) ..writeByte(1)
..write(obj.like) ..write(obj.like)
..writeByte(2) ..writeByte(2)
..write(obj.danmaku); ..write(obj.danmu);
} }
@override @override

View File

@ -0,0 +1,80 @@
class AccountListModel {
AccountListModel({
this.mid,
this.name,
this.sex,
this.face,
this.sign,
this.rank,
this.level,
this.silence,
this.vip,
this.pendant,
this.nameplate,
this.official,
this.birthday,
this.isFakeAccount,
this.isDeleted,
this.inRegAudit,
this.faceNft,
this.faceNftNew,
this.isSeniorMember,
this.digitalId,
this.digitalType,
this.attestation,
this.expertInfo,
this.honours,
});
int? mid;
String? name;
String? sex;
String? face;
String? sign;
int? rank;
int? level;
int? silence;
Map? vip;
Map? pendant;
Map? nameplate;
Map? official;
int? birthday;
int? isFakeAccount;
int? isDeleted;
int? inRegAudit;
int? faceNft;
int? faceNftNew;
int? isSeniorMember;
String? digitalId;
int? digitalType;
Map? attestation;
Map? expertInfo;
Map? honours;
AccountListModel.fromJson(Map<String, dynamic> json) {
mid = json['mid'];
name = json['name'] ?? '';
sex = json['sex'];
face = json['face'];
sign = json['sign'];
rank = json['rank'];
level = json['level'];
silence = json['silence'];
vip = json['vip'];
pendant = json['pendant'];
nameplate = json['nameplate'];
official = json['official'];
birthday = json['birthday'];
isFakeAccount = json['is_fake_account'];
isDeleted = json['is_deleted'];
inRegAudit = json['in_reg_audit'];
faceNft = json['face_nft'];
faceNftNew = json['face_nft_new'];
isSeniorMember = json['is_senior_member'];
digitalId = json['digital_id'];
digitalType = json['digital_type'];
attestation = json['attestation'];
expertInfo = json['expert_info'];
honours = json['honours'];
}
}

226
lib/models/msg/session.dart Normal file
View File

@ -0,0 +1,226 @@
import 'dart:convert';
import 'package:pilipala/models/msg/account.dart';
class SessionDataModel {
SessionDataModel({
this.sessionList,
this.hasMore,
});
List? sessionList;
int? hasMore;
SessionDataModel.fromJson(Map<String, dynamic> json) {
sessionList = json['session_list']
?.map<SessionList>((e) => SessionList.fromJson(e))
.toList();
hasMore = json['has_more'];
}
}
class SessionList {
SessionList({
this.talkerId,
this.sessionType,
this.atSeqno,
this.topTs,
this.groupName,
this.groupCover,
this.isFollow,
this.isDnd,
this.ackSeqno,
this.ackTs,
this.sessionTs,
this.unreadCount,
this.lastMsg,
this.groupType,
this.canFold,
this.status,
this.maxSeqno,
this.newPushMsg,
this.setting,
this.isGuardian,
this.isIntercept,
this.isTrust,
this.systemMsgType,
this.liveStatus,
this.bizMsgUnreadCount,
// this.userLabel,
});
int? talkerId;
int? sessionType;
int? atSeqno;
int? topTs;
String? groupName;
String? groupCover;
int? isFollow;
int? isDnd;
int? ackSeqno;
int? ackTs;
int? sessionTs;
int? unreadCount;
LastMsg? lastMsg;
int? groupType;
int? canFold;
int? status;
int? maxSeqno;
int? newPushMsg;
int? setting;
int? isGuardian;
int? isIntercept;
int? isTrust;
int? systemMsgType;
int? liveStatus;
int? bizMsgUnreadCount;
// int? userLabel;
AccountListModel? accountInfo;
SessionList.fromJson(Map<String, dynamic> json) {
talkerId = json["talker_id"];
sessionType = json["session_type"];
atSeqno = json["at_seqno"];
topTs = json["top_ts"];
groupName = json["group_name"];
groupCover = json["group_cover"];
isFollow = json["is_follow"];
isDnd = json["is_dnd"];
ackSeqno = json["ack_seqno"];
ackTs = json["ack_ts"];
sessionTs = json["session_ts"];
unreadCount = json["unread_count"];
lastMsg =
json["last_msg"] != null ? LastMsg.fromJson(json["last_msg"]) : null;
groupType = json["group_type"];
canFold = json["can_fold"];
status = json["status"];
maxSeqno = json["max_seqno"];
newPushMsg = json["new_push_msg"];
setting = json["setting"];
isGuardian = json["is_guardian"];
isIntercept = json["is_intercept"];
isTrust = json["is_trust"];
systemMsgType = json["system_msg_type"];
liveStatus = json["live_status"];
bizMsgUnreadCount = json["biz_msg_unread_count"];
// userLabel = json["user_label"];
}
}
class LastMsg {
LastMsg({
this.senderIid,
this.receiverType,
this.receiverId,
this.msgType,
this.content,
this.msgSeqno,
this.timestamp,
this.atUids,
this.msgKey,
this.msgStatus,
this.notifyCode,
this.newFaceVersion,
});
int? senderIid;
int? receiverType;
int? receiverId;
int? msgType;
Map? content;
int? msgSeqno;
int? timestamp;
String? atUids;
int? msgKey;
int? msgStatus;
String? notifyCode;
int? newFaceVersion;
LastMsg.fromJson(Map<String, dynamic> json) {
senderIid = json['sender_uid'];
receiverType = json['receiver_type'];
receiverId = json['receiver_id'];
msgType = json['msg_type'];
content = jsonDecode(json['content']);
msgSeqno = json['msg_seqno'];
timestamp = json['timestamp'];
atUids = json['at_uids'];
msgKey = json['msg_key'];
msgStatus = json['msg_status'];
notifyCode = json['notify_code'];
newFaceVersion = json['new_face_version'];
}
}
class SessionMsgDataModel {
SessionMsgDataModel({
this.messages,
this.hasMore,
this.minSeqno,
this.maxSeqno,
this.eInfos,
});
List<MessageItem>? messages;
int? hasMore;
int? minSeqno;
int? maxSeqno;
List? eInfos;
SessionMsgDataModel.fromJson(Map<String, dynamic> json) {
messages = json['messages']
.map<MessageItem>((e) => MessageItem.fromJson(e))
.toList();
hasMore = json['has_more'];
minSeqno = json['min_seqno'];
maxSeqno = json['max_seqno'];
eInfos = json['e_infos'];
}
}
class MessageItem {
MessageItem({
this.senderUid,
this.receiverType,
this.receiverId,
this.msgType,
this.content,
this.msgSeqno,
this.timestamp,
this.atUids,
this.msgKey,
this.msgStatus,
this.notifyCode,
this.newFaceVersion,
});
int? senderUid;
int? receiverType;
int? receiverId;
int? msgType;
dynamic content;
int? msgSeqno;
int? timestamp;
List? atUids;
int? msgKey;
int? msgStatus;
String? notifyCode;
int? newFaceVersion;
MessageItem.fromJson(Map<String, dynamic> json) {
senderUid = json['sender_uid'];
receiverType = json['receiver_type'];
receiverId = json['receiver_id'];
// 1 文本 2 图片 18 系统提示 10 系统通知 5 撤回的消息
msgType = json['msg_type'];
content = jsonDecode(json['content']);
msgSeqno = json['msg_seqno'];
timestamp = json['timestamp'];
atUids = json['at_uids'];
msgKey = json['msg_key'];
msgStatus = json['msg_status'];
notifyCode = json['notify_code'];
newFaceVersion = json['new_face_version'];
}
}

View File

@ -1,5 +1,3 @@
import 'dart:developer';
import 'package:pilipala/models/video/play/quality.dart'; import 'package:pilipala/models/video/play/quality.dart';
class PlayUrlModel { class PlayUrlModel {

View File

@ -1,6 +1,3 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
@ -34,11 +31,6 @@ class _AboutPageState extends State<AboutPage> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Divider(
thickness: 8,
height: 10,
color: Theme.of(context).colorScheme.onInverseSurface,
),
Image.asset( Image.asset(
'assets/images/logo/logo_android_2.png', 'assets/images/logo/logo_android_2.png',
width: 150, width: 150,
@ -83,9 +75,9 @@ class _AboutPageState extends State<AboutPage> {
// ), // ),
// ), // ),
Divider( Divider(
thickness: 8, thickness: 1,
height: 30, height: 30,
color: Theme.of(context).colorScheme.onInverseSurface, color: Theme.of(context).colorScheme.outlineVariant,
), ),
ListTile( ListTile(
onTap: () => _aboutController.githubUrl(), onTap: () => _aboutController.githubUrl(),
@ -134,11 +126,6 @@ class _AboutPageState extends State<AboutPage> {
title: const Text('赞助'), title: const Text('赞助'),
trailing: Icon(Icons.arrow_forward_ios, size: 16, color: outline), trailing: Icon(Icons.arrow_forward_ios, size: 16, color: outline),
), ),
Divider(
thickness: 8,
height: 30,
color: Theme.of(context).colorScheme.onInverseSurface,
),
], ],
), ),
), ),

View File

@ -5,7 +5,6 @@ import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/badge.dart'; import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/common/widgets/stat/danmu.dart'; import 'package:pilipala/common/widgets/stat/danmu.dart';
import 'package:pilipala/common/widgets/stat/view.dart'; import 'package:pilipala/common/widgets/stat/view.dart';
@ -122,7 +121,14 @@ class _BangumiInfoState extends State<BangumiInfo> {
late final BangumiInfoModel? bangumiItem; late final BangumiInfoModel? bangumiItem;
late double sheetHeight; late double sheetHeight;
int? cid; int? cid;
bool isProcessing = false;
void Function()? handleState(Future Function() action) {
return isProcessing ? null : () async {
setState(() => isProcessing = true);
await action();
setState(() => isProcessing = false);
};
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -396,7 +402,7 @@ class _BangumiInfoState extends State<BangumiInfo> {
() => ActionItem( () => ActionItem(
icon: const Icon(FontAwesomeIcons.thumbsUp), icon: const Icon(FontAwesomeIcons.thumbsUp),
selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp), selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp),
onTap: () => bangumiIntroController.actionLikeVideo(), onTap: handleState(bangumiIntroController.actionLikeVideo),
selectStatus: bangumiIntroController.hasLike.value, selectStatus: bangumiIntroController.hasLike.value,
loadingStatus: false, loadingStatus: false,
text: !widget.loadingStatus text: !widget.loadingStatus
@ -407,7 +413,7 @@ class _BangumiInfoState extends State<BangumiInfo> {
() => ActionItem( () => ActionItem(
icon: const Icon(FontAwesomeIcons.b), icon: const Icon(FontAwesomeIcons.b),
selectIcon: const Icon(FontAwesomeIcons.b), selectIcon: const Icon(FontAwesomeIcons.b),
onTap: () => bangumiIntroController.actionCoinVideo(), onTap: handleState(bangumiIntroController.actionCoinVideo),
selectStatus: bangumiIntroController.hasCoin.value, selectStatus: bangumiIntroController.hasCoin.value,
loadingStatus: false, loadingStatus: false,
text: !widget.loadingStatus text: !widget.loadingStatus
@ -456,7 +462,7 @@ class _BangumiInfoState extends State<BangumiInfo> {
Obx( Obx(
() => ActionRowItem( () => ActionRowItem(
icon: const Icon(FontAwesomeIcons.thumbsUp), icon: const Icon(FontAwesomeIcons.thumbsUp),
onTap: () => videoIntroController.actionLikeVideo(), onTap: handleState(videoIntroController.actionLikeVideo),
selectStatus: videoIntroController.hasLike.value, selectStatus: videoIntroController.hasLike.value,
loadingStatus: widget.loadingStatus, loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus text: !widget.loadingStatus
@ -468,7 +474,7 @@ class _BangumiInfoState extends State<BangumiInfo> {
Obx( Obx(
() => ActionRowItem( () => ActionRowItem(
icon: const Icon(FontAwesomeIcons.b), icon: const Icon(FontAwesomeIcons.b),
onTap: () => videoIntroController.actionCoinVideo(), onTap: handleState(videoIntroController.actionCoinVideo),
selectStatus: videoIntroController.hasCoin.value, selectStatus: videoIntroController.hasCoin.value,
loadingStatus: widget.loadingStatus, loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus text: !widget.loadingStatus

View File

@ -6,6 +6,7 @@ import 'package:flutter/rendering.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/main/index.dart'; import 'package:pilipala/pages/main/index.dart';
import 'package:pilipala/pages/rcmd/view.dart'; import 'package:pilipala/pages/rcmd/view.dart';
@ -35,6 +36,8 @@ class _BangumiPageState extends State<BangumiPage>
scrollController = _bangumidController.scrollController; scrollController = _bangumidController.scrollController;
StreamController<bool> mainStream = StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream; Get.find<MainController>().bottomBarStream;
StreamController<bool> searchBarStream =
Get.find<HomeController>().searchBarStream;
_futureBuilderFuture = _bangumidController.queryBangumiListFeed(); _futureBuilderFuture = _bangumidController.queryBangumiListFeed();
_futureBuilderFutureFollow = _bangumidController.queryBangumiFollow(); _futureBuilderFutureFollow = _bangumidController.queryBangumiFollow();
scrollController.addListener( scrollController.addListener(
@ -51,8 +54,10 @@ class _BangumiPageState extends State<BangumiPage>
scrollController.position.userScrollDirection; scrollController.position.userScrollDirection;
if (direction == ScrollDirection.forward) { if (direction == ScrollDirection.forward) {
mainStream.add(true); mainStream.add(true);
searchBarStream.add(true);
} else if (direction == ScrollDirection.reverse) { } else if (direction == ScrollDirection.reverse) {
mainStream.add(false); mainStream.add(false);
searchBarStream.add(false);
} }
}, },
); );

View File

@ -5,6 +5,7 @@ import 'package:hive/hive.dart';
import 'package:pilipala/models/bangumi/info.dart'; import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
class BangumiPanel extends StatefulWidget { class BangumiPanel extends StatefulWidget {
final List<EpisodeItem> pages; final List<EpisodeItem> pages;
@ -35,6 +36,7 @@ class _BangumiPanelState extends State<BangumiPanel> {
late int cid; late int cid;
String heroTag = Get.arguments['heroTag']; String heroTag = Get.arguments['heroTag'];
late final VideoDetailController videoDetailCtr; late final VideoDetailController videoDetailCtr;
final ItemScrollController itemScrollController = ItemScrollController();
@override @override
void initState() { void initState() {
@ -70,10 +72,11 @@ class _BangumiPanelState extends State<BangumiPanel> {
return StatefulBuilder( return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
WidgetsBinding.instance.addPostFrameCallback((_) async { WidgetsBinding.instance.addPostFrameCallback((_) async {
await Future.delayed(const Duration(milliseconds: 200)); // await Future.delayed(const Duration(milliseconds: 200));
listViewScrollCtr_2.animateTo(currentIndex * 56, // listViewScrollCtr_2.animateTo(currentIndex * 56,
duration: const Duration(milliseconds: 500), // duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut); // curve: Curves.easeInOut);
itemScrollController.jumpTo(index: currentIndex);
}); });
// 在这里使用 setState 更新状态 // 在这里使用 setState 更新状态
return Container( return Container(
@ -101,42 +104,39 @@ class _BangumiPanelState extends State<BangumiPanel> {
), ),
Expanded( Expanded(
child: Material( child: Material(
child: ListView.builder( child: ScrollablePositionedList.builder(
controller: listViewScrollCtr_2,
itemCount: widget.pages.length, itemCount: widget.pages.length,
itemBuilder: (context, index) { itemBuilder: (context, index) => ListTile(
return ListTile( onTap: () {
onTap: () { setState(() {
setState(() { changeFucCall(widget.pages[index], index);
changeFucCall(widget.pages[index], index); });
}); },
}, dense: false,
dense: false, leading: index == currentIndex
leading: index == currentIndex ? Image.asset(
? Image.asset( 'assets/images/live.gif',
'assets/images/live.gif', color: Theme.of(context).colorScheme.primary,
color: height: 12,
Theme.of(context).colorScheme.primary, )
height: 12, : null,
) title: Text(
: null, '${index + 1}${widget.pages[index].longTitle!}',
title: Text( style: TextStyle(
'${index + 1}${widget.pages[index].longTitle!}', fontSize: 14,
style: TextStyle( color: index == currentIndex
fontSize: 14, ? Theme.of(context).colorScheme.primary
color: index == currentIndex : Theme.of(context).colorScheme.onSurface,
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface,
),
), ),
trailing: widget.pages[index].badge != null ),
? Image.asset( trailing: widget.pages[index].badge != null
'assets/images/big-vip.png', ? Image.asset(
height: 20, 'assets/images/big-vip.png',
) height: 20,
: const SizedBox(), )
); : const SizedBox(),
}, ),
itemScrollController: itemScrollController,
), ),
), ),
), ),
@ -236,7 +236,7 @@ class _BangumiPanelState extends State<BangumiPanel> {
children: [ children: [
if (i == currentIndex) ...[ if (i == currentIndex) ...[
Image.asset( Image.asset(
'assets/images/live.gif', 'assets/images/live.png',
color: color:
Theme.of(context).colorScheme.primary, Theme.of(context).colorScheme.primary,
height: 12, height: 12,

View File

@ -3,74 +3,56 @@ import 'package:pilipala/models/danmaku/dm.pb.dart';
import 'package:pilipala/plugin/pl_player/index.dart'; import 'package:pilipala/plugin/pl_player/index.dart';
class PlDanmakuController { class PlDanmakuController {
PlDanmakuController(this.cid, this.playerController); PlDanmakuController(this.cid);
final int cid; final int cid;
final PlPlayerController playerController; Map<int,List<DanmakuElem>> dmSegMap = {};
late Duration videoDuration;
// 按 6min 分段
int segCount = 0;
List<DmSegMobileReply> dmSegList = [];
// 已请求的段落标记 // 已请求的段落标记
List<int> hasrequestSeg = []; List<bool> requestedSeg = [];
int currentSegIndex = 1;
int currentDmIndex = 0;
void calcSegment() { bool get initiated => requestedSeg.isNotEmpty;
dmSegList.clear();
// 视频分段数 static int SEGMENT_LENGTH = 60 * 6 * 1000;
segCount = (videoDuration.inSeconds / (60 * 6)).ceil();
dmSegList = List<DmSegMobileReply>.generate( void initiate(int videoDuration, int progress) {
segCount < 1 ? 1 : segCount, (index) => DmSegMobileReply()); if (requestedSeg.isEmpty) {
// 当前分段 int segCount = (videoDuration / SEGMENT_LENGTH).ceil();
try { requestedSeg = List<bool>.generate(segCount, (index) => false);
currentSegIndex = }
(playerController.position.value.inSeconds / (60 * 6)).ceil(); queryDanmaku(
currentSegIndex = currentSegIndex < 1 ? 1 : currentSegIndex; calcSegment(progress)
} catch (_) {} );
} }
Future<List<DmSegMobileReply>> queryDanmaku() async { void dispose() {
// dmSegList.clear(); dmSegMap.clear();
requestedSeg.clear();
}
int calcSegment(int progress) {
return progress ~/ SEGMENT_LENGTH;
}
void queryDanmaku(int segmentIndex) async {
assert(requestedSeg[segmentIndex] == false);
requestedSeg[segmentIndex] = true;
DmSegMobileReply result = DmSegMobileReply result =
await DanmakaHttp.queryDanmaku(cid: cid, segmentIndex: currentSegIndex); await DanmakaHttp.queryDanmaku(cid: cid, segmentIndex: segmentIndex + 1);
if (result.elems.isNotEmpty) { if (result.elems.isNotEmpty) {
result.elems.sort((a, b) => (a.progress).compareTo(b.progress)); for (var element in result.elems) {
// dmSegList.add(result); int pos = element.progress ~/ 100;//每0.1秒存储一次
currentSegIndex = currentSegIndex < 1 ? 1 : currentSegIndex; if (dmSegMap[pos] == null) {
dmSegList[currentSegIndex - 1] = result; dmSegMap[pos] = [];
}
dmSegMap[pos]!.add(element);
}
} }
if (dmSegList.isNotEmpty) {
findClosestPositionIndex(playerController.position.value.inMilliseconds);
}
return dmSegList;
} }
/// 查询当前最接近的弹幕 List<DanmakuElem>? getCurrentDanmaku(int progress) {
void findClosestPositionIndex(int position) { int segmentIndex = calcSegment(progress);
int segIndex = (position / (6 * 60 * 1000)).ceil() - 1; if (!requestedSeg[segmentIndex]) {
if (segIndex < 0) segIndex = 0; queryDanmaku(segmentIndex);
List elems = dmSegList[segIndex].elems;
if (segIndex < dmSegList.length) {
int left = 0;
int right = elems.length;
while (left < right) {
int mid = (right + left) ~/ 2;
var midPosition = elems[mid].progress;
if (midPosition >= position) {
right = mid;
} else {
left = mid + 1;
}
}
currentSegIndex = segIndex;
currentDmIndex = right;
} else {
currentSegIndex = segIndex;
currentDmIndex = 0;
} }
return dmSegMap[progress ~/ 100];
} }
} }

View File

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:ns_danmaku/ns_danmaku.dart'; import 'package:ns_danmaku/ns_danmaku.dart';
import 'package:pilipala/models/danmaku/dm.pb.dart';
import 'package:pilipala/pages/danmaku/index.dart'; import 'package:pilipala/pages/danmaku/index.dart';
import 'package:pilipala/plugin/pl_player/index.dart'; import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/utils/danmaku.dart'; import 'package:pilipala/utils/danmaku.dart';
@ -27,14 +28,15 @@ class _PlDanmakuState extends State<PlDanmaku> {
late PlPlayerController playerController; late PlPlayerController playerController;
late PlDanmakuController _plDanmakuController; late PlDanmakuController _plDanmakuController;
DanmakuController? _controller; DanmakuController? _controller;
bool danmuPlayStatus = true; // bool danmuPlayStatus = true;
Box setting = GStrorage.setting; Box setting = GStrorage.setting;
late bool enableShowDanmaku; late bool enableShowDanmaku;
late List blockTypes; late List blockTypes;
late double showArea; late double showArea;
late double opacityVal; late double opacityVal;
late double fontSizeVal; late double fontSizeVal;
late double danmakuSpeedVal; late double danmakuDurationVal;
int latestAddedPosition = -1;
@override @override
void initState() { void initState() {
@ -42,33 +44,32 @@ class _PlDanmakuState extends State<PlDanmaku> {
enableShowDanmaku = enableShowDanmaku =
setting.get(SettingBoxKey.enableShowDanmaku, defaultValue: false); setting.get(SettingBoxKey.enableShowDanmaku, defaultValue: false);
_plDanmakuController = _plDanmakuController =
PlDanmakuController(widget.cid, widget.playerController); PlDanmakuController(widget.cid);
if (mounted) { if (mounted) {
playerController = widget.playerController; playerController = widget.playerController;
_plDanmakuController.videoDuration = playerController.duration.value;
if (enableShowDanmaku || playerController.isOpenDanmu.value) { if (enableShowDanmaku || playerController.isOpenDanmu.value) {
_plDanmakuController _plDanmakuController.initiate(
..calcSegment() playerController.duration.value.inMilliseconds,
..queryDanmaku(); playerController.position.value.inMilliseconds
);
} }
playerController playerController
..addStatusLister(playerListener) ..addStatusLister(playerListener)
..addPositionListener(videoPositionListen); ..addPositionListener(videoPositionListen);
} }
playerController.isOpenDanmu.listen((p0) { playerController.isOpenDanmu.listen((p0) {
if (p0) { if (p0 && !_plDanmakuController.initiated) {
if (_plDanmakuController.dmSegList.isEmpty) { _plDanmakuController.initiate(
_plDanmakuController playerController.duration.value.inMilliseconds,
..calcSegment() playerController.position.value.inMilliseconds
..queryDanmaku(); );
}
} }
}); });
blockTypes = playerController.blockTypes; blockTypes = playerController.blockTypes;
showArea = playerController.showArea; showArea = playerController.showArea;
opacityVal = playerController.opacityVal; opacityVal = playerController.opacityVal;
fontSizeVal = playerController.fontSizeVal; fontSizeVal = playerController.fontSizeVal;
danmakuSpeedVal = playerController.danmakuSpeedVal; danmakuDurationVal = playerController.danmakuDurationVal;
} }
// 播放器状态监听 // 播放器状态监听
@ -82,64 +83,32 @@ class _PlDanmakuState extends State<PlDanmaku> {
} }
void videoPositionListen(Duration position) { void videoPositionListen(Duration position) {
if (!danmuPlayStatus) {
_controller!.onResume();
danmuPlayStatus = true;
}
if (!playerController.isOpenDanmu.value) { if (!playerController.isOpenDanmu.value) {
return; return;
} }
PlDanmakuController ctr = _plDanmakuController;
int currentPosition = position.inMilliseconds; int currentPosition = position.inMilliseconds;
blockTypes = playerController.blockTypes; currentPosition -= currentPosition % 100;//取整百的毫秒数
// 根据position判断是否有已缓存弹幕。没有则请求对应段
int segIndex = (currentPosition / (6 * 60 * 1000)).ceil();
segIndex = segIndex < 1 ? 1 : segIndex;
if (ctr.dmSegList[segIndex - 1].elems.isEmpty &&
!ctr.hasrequestSeg.contains(segIndex - 1)) {
ctr.hasrequestSeg.add(segIndex - 1);
ctr.currentSegIndex = segIndex;
EasyThrottle.throttle('follow', const Duration(seconds: 1), () {
ctr.queryDanmaku();
});
}
// 超出分段数返回
if (ctr.currentSegIndex >= ctr.dmSegList.length) {
return;
}
if (ctr.dmSegList.isEmpty ||
ctr.dmSegList[ctr.currentSegIndex].elems.isEmpty) {
return;
}
// 超出当前分段的弹幕总数返回
if (ctr.currentDmIndex >= ctr.dmSegList[ctr.currentSegIndex].elems.length) {
ctr.currentDmIndex = 0;
ctr.currentSegIndex++;
return;
}
var element = ctr.dmSegList[ctr.currentSegIndex].elems[ctr.currentDmIndex];
var delta = currentPosition - element.progress;
if (delta >= 0 && delta < 200) { if (currentPosition == latestAddedPosition) {
// 屏蔽彩色弹幕 return;
if (blockTypes.contains(6) ? element.color == 16777215 : true) { }
_controller!.addItems([ latestAddedPosition = currentPosition;
DanmakuItem(
element.content, List<DanmakuElem>? currentDanmakuList =
color: DmUtils.decimalToColor(element.color), _plDanmakuController.getCurrentDanmaku(currentPosition);
time: element.progress,
type: DmUtils.getPosition(element.mode), if (currentDanmakuList != null) {
) Color? defaultColor = playerController.blockTypes.contains(6) ?
]); DmUtils.decimalToColor(16777215) : null;
}
ctr.currentDmIndex++; _controller!.addItems(
} else { currentDanmakuList.map((e) => DanmakuItem(
if (!playerController.isOpenDanmu.value) { e.content,
_controller!.pause(); color: defaultColor ?? DmUtils.decimalToColor(e.color),
danmuPlayStatus = false; time: e.progress,
return; type: DmUtils.getPosition(e.mode),
} )).toList()
ctr.findClosestPositionIndex(position.inMilliseconds); );
} }
} }
@ -152,7 +121,7 @@ class _PlDanmakuState extends State<PlDanmaku> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, box) { return LayoutBuilder(builder: (context, box) {
double initDuration = box.maxWidth / 12; // double initDuration = box.maxWidth / 12;
return Obx( return Obx(
() => AnimatedOpacity( () => AnimatedOpacity(
opacity: playerController.isOpenDanmu.value ? 1 : 0, opacity: playerController.isOpenDanmu.value ? 1 : 0,
@ -168,8 +137,9 @@ class _PlDanmakuState extends State<PlDanmaku> {
hideTop: blockTypes.contains(5), hideTop: blockTypes.contains(5),
hideScroll: blockTypes.contains(2), hideScroll: blockTypes.contains(2),
hideBottom: blockTypes.contains(4), hideBottom: blockTypes.contains(4),
duration: initDuration / duration: danmakuDurationVal / widget.playerController.playbackSpeed,
(danmakuSpeedVal * widget.playerController.playbackSpeed), // initDuration /
// (danmakuSpeedVal * widget.playerController.playbackSpeed),
), ),
statusChanged: (isPlaying) {}, statusChanged: (isPlaying) {},
), ),

View File

@ -23,7 +23,14 @@ class ActionPanel extends StatefulWidget {
class _ActionPanelState extends State<ActionPanel> { class _ActionPanelState extends State<ActionPanel> {
final DynamicsController _dynamicsController = Get.put(DynamicsController()); final DynamicsController _dynamicsController = Get.put(DynamicsController());
late ModuleStatModel stat; late ModuleStatModel stat;
bool isProcessing = false;
void Function()? handleState(Future Function() action) {
return isProcessing ? null : () async {
setState(() => isProcessing = true);
await action();
setState(() => isProcessing = false);
};
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -31,7 +38,7 @@ class _ActionPanelState extends State<ActionPanel> {
} }
// 动态点赞 // 动态点赞
onLikeDynamic() async { Future onLikeDynamic() async {
feedBack(); feedBack();
var item = widget.item!; var item = widget.item!;
String dynamicId = item.idStr!; String dynamicId = item.idStr!;
@ -101,7 +108,7 @@ class _ActionPanelState extends State<ActionPanel> {
Expanded( Expanded(
flex: 1, flex: 1,
child: TextButton.icon( child: TextButton.icon(
onPressed: () => onLikeDynamic(), onPressed: handleState(onLikeDynamic),
icon: Icon( icon: Icon(
stat.like!.status! stat.like!.status!
? FontAwesomeIcons.solidThumbsUp ? FontAwesomeIcons.solidThumbsUp

View File

@ -1,8 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/pages/preview/index.dart';
// 富文本 // 富文本
InlineSpan richNode(item, context) { InlineSpan richNode(item, context) {
@ -11,13 +9,11 @@ InlineSpan richNode(item, context) {
TextStyle authorStyle = TextStyle authorStyle =
TextStyle(color: Theme.of(context).colorScheme.primary); TextStyle(color: Theme.of(context).colorScheme.primary);
List<InlineSpan> spanChilds = []; List<InlineSpan> spanChilds = [];
String contentType = 'desc';
dynamic richTextNodes; dynamic richTextNodes;
if (item.modules.moduleDynamic.desc != null) { if (item.modules.moduleDynamic.desc != null) {
richTextNodes = item.modules.moduleDynamic.desc.richTextNodes; richTextNodes = item.modules.moduleDynamic.desc.richTextNodes;
} else if (item.modules.moduleDynamic.major != null) { } else if (item.modules.moduleDynamic.major != null) {
contentType = 'major';
// 动态页面 richTextNodes 层级可能与主页动态层级不同 // 动态页面 richTextNodes 层级可能与主页动态层级不同
richTextNodes = richTextNodes =
item.modules.moduleDynamic.major.opus.summary.richTextNodes; item.modules.moduleDynamic.major.opus.summary.richTextNodes;

View File

@ -15,6 +15,7 @@ class FansController extends GetxController {
late String name; late String name;
var userInfo; var userInfo;
RxString loadingText = '加载中...'.obs; RxString loadingText = '加载中...'.obs;
RxBool isOwner = false.obs;
@override @override
void onInit() { void onInit() {
@ -23,6 +24,7 @@ class FansController extends GetxController {
mid = Get.parameters['mid'] != null mid = Get.parameters['mid'] != null
? int.parse(Get.parameters['mid']!) ? int.parse(Get.parameters['mid']!)
: userInfo.mid; : userInfo.mid;
isOwner.value = mid == userInfo.mid;
name = Get.parameters['name'] ?? userInfo.uname; name = Get.parameters['name'] ?? userInfo.uname;
} }

View File

@ -54,7 +54,7 @@ class _FansPageState extends State<FansPage> {
centerTitle: false, centerTitle: false,
titleSpacing: 0, titleSpacing: 0,
title: Text( title: Text(
'${_fansController.name}的粉丝', _fansController.isOwner.value ? '我的粉丝' : '${_fansController.name}的粉丝',
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
), ),

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/follow/result.dart'; import 'package:pilipala/models/follow/result.dart';

View File

@ -1,5 +1,3 @@
import 'dart:math';
import 'package:easy_debounce/easy_throttle.dart'; import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';

View File

@ -11,7 +11,6 @@ import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/common/business_type.dart'; import 'package:pilipala/models/common/business_type.dart';
import 'package:pilipala/models/common/search_type.dart'; import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/models/live/item.dart'; import 'package:pilipala/models/live/item.dart';
import 'package:pilipala/pages/history/index.dart';
import 'package:pilipala/pages/history_search/index.dart'; import 'package:pilipala/pages/history_search/index.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/id_utils.dart'; import 'package:pilipala/utils/id_utils.dart';

View File

@ -131,7 +131,6 @@ class _HistorySearchPageState extends State<HistorySearchPage> {
onChoose: null, onChoose: null,
onUpdateMultiple: () => null, onUpdateMultiple: () => null,
); );
;
} }
}, },
) )

View File

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
@ -15,6 +17,10 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
RxBool userLogin = false.obs; RxBool userLogin = false.obs;
RxString userFace = ''.obs; RxString userFace = ''.obs;
var userInfo; var userInfo;
Box setting = GStrorage.setting;
late final StreamController<bool> searchBarStream =
StreamController<bool>.broadcast();
late bool hideSearchBar;
@override @override
void onInit() { void onInit() {
@ -33,6 +39,8 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
length: tabs.length, length: tabs.length,
vsync: this, vsync: this,
); );
hideSearchBar =
setting.get(SettingBoxKey.hideSearchBar, defaultValue: true);
} }
void onRefresh() { void onRefresh() {

View File

@ -1,7 +1,8 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/pages/main/index.dart';
import 'package:pilipala/pages/mine/index.dart'; import 'package:pilipala/pages/mine/index.dart';
import 'package:pilipala/pages/search/index.dart'; import 'package:pilipala/pages/search/index.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
@ -18,11 +19,17 @@ class _HomePageState extends State<HomePage>
with AutomaticKeepAliveClientMixin, TickerProviderStateMixin { with AutomaticKeepAliveClientMixin, TickerProviderStateMixin {
final HomeController _homeController = Get.put(HomeController()); final HomeController _homeController = Get.put(HomeController());
List videoList = []; List videoList = [];
Stream<bool> stream = Get.find<MainController>().bottomBarStream.stream; late Stream<bool> stream;
@override @override
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
stream = _homeController.searchBarStream.stream;
}
showUserBottonSheet() { showUserBottonSheet() {
feedBack(); feedBack();
showModalBottomSheet( showModalBottomSheet(
@ -46,7 +53,9 @@ class _HomePageState extends State<HomePage>
body: Column( body: Column(
children: [ children: [
CustomAppBar( CustomAppBar(
stream: stream, stream: _homeController.hideSearchBar
? stream
: StreamController<bool>.broadcast().stream,
ctr: _homeController, ctr: _homeController,
callback: showUserBottonSheet, callback: showUserBottonSheet,
), ),
@ -65,6 +74,7 @@ class _HomePageState extends State<HomePage>
dividerColor: Colors.transparent, dividerColor: Colors.transparent,
enableFeedback: true, enableFeedback: true,
splashBorderRadius: BorderRadius.circular(10), splashBorderRadius: BorderRadius.circular(10),
tabAlignment: TabAlignment.center,
onTap: (value) { onTap: (value) {
feedBack(); feedBack();
if (_homeController.initialIndex == value) { if (_homeController.initialIndex == value) {
@ -118,7 +128,7 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
duration: const Duration(milliseconds: 500), duration: const Duration(milliseconds: 500),
height: snapshot.data height: snapshot.data
? MediaQuery.of(context).padding.top + 52 ? MediaQuery.of(context).padding.top + 52
: MediaQuery.of(context).padding.top, : MediaQuery.of(context).padding.top - 10,
child: Container( child: Container(
padding: EdgeInsets.only( padding: EdgeInsets.only(
left: 20, left: 20,
@ -129,7 +139,13 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
child: Row( child: Row(
children: [ children: [
const Expanded(child: SearchPage()), const Expanded(child: SearchPage()),
const SizedBox(width: 10), if (ctr!.userLogin.value) ...[
const SizedBox(width: 6),
IconButton(
onPressed: () => Get.toNamed('/whisper'),
icon: const Icon(Icons.notifications_none))
],
const SizedBox(width: 6),
Obx( Obx(
() => ctr!.userLogin.value () => ctr!.userLogin.value
? Stack( ? Stack(

View File

@ -9,6 +9,7 @@ import 'package:pilipala/common/widgets/overlay_pop.dart';
import 'package:pilipala/common/skeleton/video_card_h.dart'; import 'package:pilipala/common/skeleton/video_card_h.dart';
import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/video_card_h.dart'; import 'package:pilipala/common/widgets/video_card_h.dart';
import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/hot/controller.dart'; import 'package:pilipala/pages/hot/controller.dart';
import 'package:pilipala/pages/main/index.dart'; import 'package:pilipala/pages/main/index.dart';
@ -35,6 +36,8 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
scrollController = _hotController.scrollController; scrollController = _hotController.scrollController;
StreamController<bool> mainStream = StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream; Get.find<MainController>().bottomBarStream;
StreamController<bool> searchBarStream =
Get.find<HomeController>().searchBarStream;
scrollController.addListener( scrollController.addListener(
() { () {
if (scrollController.position.pixels >= if (scrollController.position.pixels >=
@ -49,8 +52,10 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
scrollController.position.userScrollDirection; scrollController.position.userScrollDirection;
if (direction == ScrollDirection.forward) { if (direction == ScrollDirection.forward) {
mainStream.add(true); mainStream.add(true);
searchBarStream.add(true);
} else if (direction == ScrollDirection.reverse) { } else if (direction == ScrollDirection.reverse) {
mainStream.add(false); mainStream.add(false);
searchBarStream.add(false);
} }
}, },
); );

View File

@ -9,7 +9,6 @@ import 'package:pilipala/common/widgets/html_render.dart';
import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/common/reply_type.dart'; import 'package:pilipala/models/common/reply_type.dart';
import 'package:pilipala/pages/mine/index.dart';
import 'package:pilipala/pages/video/detail/reply/widgets/reply_item.dart'; import 'package:pilipala/pages/video/detail/reply/widgets/reply_item.dart';
import 'package:pilipala/pages/video/detail/replyNew/index.dart'; import 'package:pilipala/pages/video/detail/replyNew/index.dart';
import 'package:pilipala/pages/video/detail/replyReply/index.dart'; import 'package:pilipala/pages/video/detail/replyReply/index.dart';

View File

@ -32,7 +32,7 @@ class _LaterPageState extends State<LaterPage> {
title: Obx( title: Obx(
() => _laterController.laterList.isNotEmpty () => _laterController.laterList.isNotEmpty
? Text( ? Text(
'稍后再看 (${_laterController.laterList.length}/100)', '稍后再看 (${_laterController.laterList.length})',
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
) )
: Text( : Text(

View File

@ -9,6 +9,7 @@ import 'package:pilipala/common/skeleton/video_card_v.dart';
import 'package:pilipala/common/widgets/animated_dialog.dart'; import 'package:pilipala/common/widgets/animated_dialog.dart';
import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/overlay_pop.dart'; import 'package:pilipala/common/widgets/overlay_pop.dart';
import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/main/index.dart'; import 'package:pilipala/pages/main/index.dart';
import 'package:pilipala/pages/rcmd/index.dart'; import 'package:pilipala/pages/rcmd/index.dart';
@ -38,6 +39,8 @@ class _LivePageState extends State<LivePage>
scrollController = _liveController.scrollController; scrollController = _liveController.scrollController;
StreamController<bool> mainStream = StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream; Get.find<MainController>().bottomBarStream;
StreamController<bool> searchBarStream =
Get.find<HomeController>().searchBarStream;
scrollController.addListener( scrollController.addListener(
() { () {
if (scrollController.position.pixels >= if (scrollController.position.pixels >=
@ -52,8 +55,10 @@ class _LivePageState extends State<LivePage>
scrollController.position.userScrollDirection; scrollController.position.userScrollDirection;
if (direction == ScrollDirection.forward) { if (direction == ScrollDirection.forward) {
mainStream.add(true); mainStream.add(true);
searchBarStream.add(true);
} else if (direction == ScrollDirection.reverse) { } else if (direction == ScrollDirection.reverse) {
mainStream.add(false); mainStream.add(false);
searchBarStream.add(false);
} }
}, },
); );
@ -67,6 +72,7 @@ class _LivePageState extends State<LivePage>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context);
return Container( return Container(
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
margin: const EdgeInsets.only( margin: const EdgeInsets.only(

View File

@ -52,7 +52,6 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final videoHeight = MediaQuery.of(context).size.width * 9 / 16;
Widget childWhenDisabled = Scaffold( Widget childWhenDisabled = Scaffold(
primary: true, primary: true,
appBar: AppBar( appBar: AppBar(

View File

@ -3,7 +3,6 @@ import 'dart:io';
import 'package:floating/floating.dart'; import 'package:floating/floating.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/models/video/play/url.dart'; import 'package:pilipala/models/video/play/url.dart';
import 'package:pilipala/pages/liveRoom/index.dart'; import 'package:pilipala/pages/liveRoom/index.dart';
@ -43,10 +42,6 @@ class _BottomControlState extends State<BottomControl> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
const textStyle = TextStyle(
color: Colors.white,
fontSize: 12,
);
return AppBar( return AppBar(
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
foregroundColor: Colors.white, foregroundColor: Colors.white,

View File

@ -55,6 +55,7 @@ class MainController extends GetxController {
StreamController<bool>.broadcast(); StreamController<bool>.broadcast();
Box setting = GStrorage.setting; Box setting = GStrorage.setting;
DateTime? _lastPressedAt; DateTime? _lastPressedAt;
late bool hideTabBar;
@override @override
void onInit() { void onInit() {
@ -62,6 +63,7 @@ class MainController extends GetxController {
if (setting.get(SettingBoxKey.autoUpdate, defaultValue: false)) { if (setting.get(SettingBoxKey.autoUpdate, defaultValue: false)) {
Utils.checkUpdata(); Utils.checkUpdata();
} }
hideTabBar = setting.get(SettingBoxKey.hideTabBar, defaultValue: true);
} }
Future<bool> onBackPressed(BuildContext context) { Future<bool> onBackPressed(BuildContext context) {

View File

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
@ -113,8 +115,8 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
MediaQuery.of(context).size.width * 9 / 16; MediaQuery.of(context).size.width * 9 / 16;
localCache.put('sheetHeight', sheetHeight); localCache.put('sheetHeight', sheetHeight);
localCache.put('statusBarHeight', statusBarHeight); localCache.put('statusBarHeight', statusBarHeight);
return WillPopScope( return PopScope(
onWillPop: () => _mainController.onBackPressed(context), onPopInvoked: (bool status) => _mainController.onBackPressed(context),
child: Scaffold( child: Scaffold(
extendBody: true, extendBody: true,
body: FadeTransition( body: FadeTransition(
@ -142,7 +144,9 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
), ),
), ),
bottomNavigationBar: StreamBuilder( bottomNavigationBar: StreamBuilder(
stream: _mainController.bottomBarStream.stream, stream: _mainController.hideTabBar
? _mainController.bottomBarStream.stream
: StreamController<bool>.broadcast().stream,
initialData: true, initialData: true,
builder: (context, AsyncSnapshot snapshot) { builder: (context, AsyncSnapshot snapshot) {
return AnimatedSlide( return AnimatedSlide(

View File

@ -36,6 +36,7 @@ class MediaController extends GetxController {
]; ];
var userInfo; var userInfo;
int? mid; int? mid;
final ScrollController scrollController = ScrollController();
@override @override
void onInit() { void onInit() {

View File

@ -1,7 +1,11 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/user/fav_folder.dart'; import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/pages/main/index.dart';
import 'package:pilipala/pages/media/index.dart'; import 'package:pilipala/pages/media/index.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
@ -25,12 +29,32 @@ class _MediaPageState extends State<MediaPage>
super.initState(); super.initState();
mediaController = Get.put(MediaController()); mediaController = Get.put(MediaController());
_futureBuilderFuture = mediaController.queryFavFolder(); _futureBuilderFuture = mediaController.queryFavFolder();
ScrollController scrollController = mediaController.scrollController;
StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream;
mediaController.userLogin.listen((status) { mediaController.userLogin.listen((status) {
setState(() { setState(() {
_futureBuilderFuture = mediaController.queryFavFolder(); _futureBuilderFuture = mediaController.queryFavFolder();
}); });
}); });
scrollController.addListener(
() {
final ScrollDirection direction =
scrollController.position.userScrollDirection;
if (direction == ScrollDirection.forward) {
mainStream.add(true);
} else if (direction == ScrollDirection.reverse) {
mainStream.add(false);
}
},
);
}
@override
void dispose() {
mediaController.scrollController.removeListener(() {});
super.dispose();
} }
@override @override
@ -40,6 +64,7 @@ class _MediaPageState extends State<MediaPage>
return Scaffold( return Scaffold(
appBar: AppBar(toolbarHeight: 30), appBar: AppBar(toolbarHeight: 30),
body: SingleChildScrollView( body: SingleChildScrollView(
controller: mediaController.scrollController,
child: Column( child: Column(
children: [ children: [
ListTile( ListTile(

View File

@ -1,4 +0,0 @@
library archive_panel;
export './controller.dart';
export 'index.dart';

View File

@ -1,240 +0,0 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:loading_more_list/loading_more_list.dart';
import 'package:pilipala/common/widgets/video_card_h.dart';
import 'package:pilipala/models/member/archive.dart';
import 'package:pilipala/pages/member/archive/index.dart';
import 'package:pilipala/utils/utils.dart';
import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart';
class ArchivePanel extends StatefulWidget {
final int? mid;
const ArchivePanel({super.key, this.mid});
@override
State<ArchivePanel> createState() => _ArchivePanelState();
}
class _ArchivePanelState extends State<ArchivePanel>
with AutomaticKeepAliveClientMixin {
DateTime lastRefreshTime = DateTime.now();
late final LoadMoreListSource source;
late final ArchiveController _archiveController;
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
print('🐶🐶: ${widget.mid}');
_archiveController = Get.put(ArchiveController(widget.mid),
tag: Utils.makeHeroTag(widget.mid));
source = LoadMoreListSource(_archiveController);
}
@override
Widget build(BuildContext context) {
super.build(context);
return PullToRefreshNotification(
onRefresh: () async {
await Future.delayed(const Duration(seconds: 1));
return true;
},
maxDragOffset: 50,
child: GlowNotificationWidget(
Column(
children: <Widget>[
// 下拉刷新指示器
// PullToRefreshContainer(
// (PullToRefreshScrollNotificationInfo? info) {
// return PullToRefreshHeader(info, lastRefreshTime);
// },
// ),
Padding(
padding:
const EdgeInsets.only(left: 14, top: 8, bottom: 8, right: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('排序方式'),
SizedBox(
height: 35,
width: 85,
child: TextButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () {
// _archiveController.order = 'click';
// _archiveController.pn = 1;
_archiveController.toggleSort();
source.refresh(true);
// LoadMoreListSource().loadData();
},
child: Obx(
() => AnimatedSwitcher(
duration: const Duration(milliseconds: 400),
transitionBuilder:
(Widget child, Animation<double> animation) {
return ScaleTransition(
scale: animation, child: child);
},
child: Text(
_archiveController.currentOrder['label']!,
key: ValueKey<String>(
_archiveController.currentOrder['label']!),
),
),
),
),
),
],
),
),
Expanded(
child: LoadingMoreList<VListItemModel>(
ListConfig<VListItemModel>(
sourceList: source,
itemBuilder:
(BuildContext c, VListItemModel item, int index) {
if (index == 0) {
return Column(
children: [
const SizedBox(height: 6),
VideoCardH(videoItem: item)
],
);
} else {
return VideoCardH(videoItem: item);
}
},
indicatorBuilder: _buildIndicator,
),
),
)
],
),
showGlowLeading: false,
),
);
}
Widget _buildIndicator(BuildContext context, IndicatorStatus status) {
TextStyle style =
TextStyle(fontSize: 13, color: Theme.of(context).colorScheme.outline);
Widget? widget;
switch (status) {
case IndicatorStatus.none:
widget = Container(height: 0.0);
break;
case IndicatorStatus.loadingMoreBusying:
widget = Text('加载中...', style: style);
widget = _setbackground(false, widget, height: 60.0);
break;
case IndicatorStatus.fullScreenBusying:
widget = Text('加载中...', style: style);
widget = _setbackground(true, widget);
break;
case IndicatorStatus.error:
/// TODO 异常逻辑
widget = Text('没有更多了', style: style);
widget = _setbackground(false, widget);
widget = GestureDetector(
onTap: () {},
child: widget,
);
break;
case IndicatorStatus.fullScreenError:
/// TODO 异常逻辑
widget = Text('没有更多了', style: style);
widget = _setbackground(true, widget);
widget = GestureDetector(
onTap: () {},
child: widget,
);
break;
case IndicatorStatus.noMoreLoad:
widget = Text('没有更多了', style: style);
widget = _setbackground(false, widget, height: 60.0);
break;
case IndicatorStatus.empty:
widget = Text('用户没有投稿', style: style);
widget = _setbackground(true, widget);
break;
}
return widget;
}
Widget _setbackground(bool full, Widget widget, {double height = 100}) {
widget = Padding(
padding: height == double.infinity
? EdgeInsets.zero
: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
child: Container(
width: double.infinity,
height: height,
color: Theme.of(context).colorScheme.background,
alignment: Alignment.center,
child: widget,
),
);
return widget;
}
Widget getIndicator(BuildContext context) {
final TargetPlatform platform = Theme.of(context).platform;
return platform == TargetPlatform.iOS
? const CupertinoActivityIndicator(
animating: true,
radius: 16.0,
)
: CircularProgressIndicator(
strokeWidth: 2.0,
valueColor:
AlwaysStoppedAnimation<Color>(Theme.of(context).primaryColor),
);
}
}
class LoadMoreListSource extends LoadingMoreBase<VListItemModel> {
late ArchiveController ctr;
LoadMoreListSource(this.ctr);
bool forceRefresh = false;
@override
Future<bool> loadData([bool isloadMoreAction = false]) async {
bool isSuccess = false;
var res = await ctr.getMemberArchive();
if (res['status']) {
if (ctr.pn == 2) {
clear();
}
addAll(res['data'].list.vlist);
}
if (length < res['data'].page['count']) {
isSuccess = true;
} else {
isSuccess = false;
}
return isSuccess;
}
@override
Future<bool> refresh([bool clearBeforeRequest = false]) async {
// _hasMore = true;
// pageindex = 1;
// //force to refresh list when you don't want clear list before request
// //for the case, if your list already has 20 items.
forceRefresh = !clearBeforeRequest;
var result = await super.refresh(clearBeforeRequest);
forceRefresh = false;
return result;
}
}

View File

@ -6,6 +6,7 @@ import 'package:pilipala/http/member.dart';
import 'package:pilipala/http/user.dart'; import 'package:pilipala/http/user.dart';
import 'package:pilipala/http/video.dart'; import 'package:pilipala/http/video.dart';
import 'package:pilipala/models/member/archive.dart'; import 'package:pilipala/models/member/archive.dart';
import 'package:pilipala/models/member/coin.dart';
import 'package:pilipala/models/member/info.dart'; import 'package:pilipala/models/member/info.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
@ -13,16 +14,17 @@ import 'package:share_plus/share_plus.dart';
class MemberController extends GetxController { class MemberController extends GetxController {
late int mid; late int mid;
Rx<MemberInfoModel> memberInfo = MemberInfoModel().obs; Rx<MemberInfoModel> memberInfo = MemberInfoModel().obs;
Map? userStat; late Map userStat;
RxString face = ''.obs; RxString face = ''.obs;
String? heroTag; String? heroTag;
Box userInfoCache = GStrorage.userInfo; Box userInfoCache = GStrorage.userInfo;
late int ownerMid; late int ownerMid;
// 投稿列表 // 投稿列表
RxList<VListItemModel>? archiveList = [VListItemModel()].obs; RxList<VListItemModel>? archiveList = [VListItemModel()].obs;
var userInfo; dynamic userInfo;
RxInt attribute = (-1).obs; RxInt attribute = (-1).obs;
RxString attributeText = '关注'.obs; RxString attributeText = '关注'.obs;
RxList<MemberCoinsDataModel> recentCoinsList = <MemberCoinsDataModel>[].obs;
@override @override
void onInit() { void onInit() {
@ -38,6 +40,7 @@ class MemberController extends GetxController {
// 获取用户信息 // 获取用户信息
Future<Map<String, dynamic>> getInfo() async { Future<Map<String, dynamic>> getInfo() async {
await getMemberStat(); await getMemberStat();
await getMemberView();
var res = await MemberHttp.memberInfo(mid: mid); var res = await MemberHttp.memberInfo(mid: mid);
if (res['status']) { if (res['status']) {
memberInfo.value = res['data']; memberInfo.value = res['data'];
@ -55,13 +58,14 @@ class MemberController extends GetxController {
return res; return res;
} }
// Future getMemberCardInfo() async { // 获取用户播放数 获赞数
// var res = await MemberHttp.memberCardInfo(mid: mid); Future<Map<String, dynamic>> getMemberView() async {
// if (res['status']) { var res = await MemberHttp.memberView(mid: mid);
// print(userStat); if (res['status']) {
// } userStat.addAll(res['data']);
// return res; }
// } return res;
}
// 关注/取关up // 关注/取关up
Future actionRelationMod() async { Future actionRelationMod() async {
@ -112,16 +116,28 @@ class MemberController extends GetxController {
Future relationSearch() async { Future relationSearch() async {
if (userInfo == null) return; if (userInfo == null) return;
if (mid == ownerMid) return; if (mid == ownerMid) return;
var res = await UserHttp.relationSearch(mid); var res = await UserHttp.hasFollow(mid);
if (res['status']) { if (res['status']) {
attribute.value = res['data']['relation']['attribute']; attribute.value = res['data']['attribute'];
attributeText.value = attribute.value == 0 switch (attribute.value) {
? '关注' case 1:
: attribute.value == 2 attributeText.value = '悄悄关注';
? '已关注' break;
: attribute.value == 6 case 2:
? '互粉' attributeText.value = '关注';
: '已拉黑'; break;
case 6:
attributeText.value = '已互关';
break;
case 128:
attributeText.value = '已拉黑';
break;
default:
attributeText.value = '关注';
}
if (res['data']['special'] == 1) {
attributeText.value += 'SP';
}
} }
} }
@ -173,4 +189,35 @@ class MemberController extends GetxController {
void shareUser() { void shareUser() {
Share.share('${memberInfo.value.name} - https://space.bilibili.com/$mid'); Share.share('${memberInfo.value.name} - https://space.bilibili.com/$mid');
} }
// 请求专栏
Future getMemberSeasons() async {
if (userInfo == null) return;
var res = await MemberHttp.getMemberSeasons(mid, 1, 10);
if (!res['status']) {
SmartDialog.showToast("用户专栏请求异常:${res['msg']}");
}
return res;
}
// 请求投币视频
Future getRecentCoinVideo() async {
if (userInfo == null) return;
var res = await MemberHttp.getRecentCoinVideo(mid: mid);
recentCoinsList.value = res['data'];
return res;
}
// 跳转查看动态
void pushDynamicsPage() => Get.toNamed('/memberDynamics?mid=$mid');
// 跳转查看投稿
void pushArchivesPage() => Get.toNamed('/memberArchive?mid=$mid');
// 跳转查看专栏
void pushSeasonsPage() {}
// 跳转查看最近投币
void pushRecentCoinsPage() async {
if (recentCoinsList.isNotEmpty) {}
}
} }

View File

@ -1,31 +0,0 @@
import 'package:get/get.dart';
import 'package:pilipala/http/member.dart';
class MemberDynamicPanelController extends GetxController {
MemberDynamicPanelController(this.mid);
int? mid;
String offset = '';
int count = 0;
bool hasMore = true;
@override
void onInit() {
super.onInit();
mid ??= int.parse(Get.parameters['mid']!);
}
Future getMemberDynamic() async {
if (!hasMore) {
return {'status': false};
}
var res = await MemberHttp.memberDynamic(
offset: offset,
mid: mid,
);
if (res['status']) {
offset = res['data'].offset;
hasMore = res['data'].hasMore;
}
return res;
}
}

View File

@ -1,152 +0,0 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:loading_more_list/loading_more_list.dart';
import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/pages/dynamics/widgets/dynamic_panel.dart';
import 'package:pilipala/utils/utils.dart';
import 'controller.dart';
class MemberDynamicPanel extends StatefulWidget {
final int? mid;
const MemberDynamicPanel({super.key, this.mid});
@override
State<MemberDynamicPanel> createState() => _MemberDynamicPanelState();
}
class _MemberDynamicPanelState extends State<MemberDynamicPanel>
with AutomaticKeepAliveClientMixin {
DateTime lastRefreshTime = DateTime.now();
late final LoadMoreListSource source;
late final MemberDynamicPanelController _dynamicController;
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
_dynamicController = Get.put(MemberDynamicPanelController(widget.mid),
tag: Utils.makeHeroTag(widget.mid));
source = LoadMoreListSource(_dynamicController);
}
@override
Widget build(BuildContext context) {
super.build(context);
return LoadingMoreList<DynamicItemModel>(
ListConfig<DynamicItemModel>(
sourceList: source,
itemBuilder: (BuildContext c, DynamicItemModel item, int index) {
return DynamicPanel(item: item);
},
indicatorBuilder: _buildIndicator,
),
);
}
Widget _buildIndicator(BuildContext context, IndicatorStatus status) {
TextStyle style =
TextStyle(fontSize: 13, color: Theme.of(context).colorScheme.outline);
Widget? widget;
switch (status) {
case IndicatorStatus.none:
widget = Container(height: 0.0);
break;
case IndicatorStatus.loadingMoreBusying:
widget = Text('加载中...', style: style);
widget = _setbackground(false, widget, height: 60.0);
break;
case IndicatorStatus.fullScreenBusying:
widget = Text('加载中...', style: style);
widget = _setbackground(true, widget);
break;
case IndicatorStatus.error:
/// TODO 异常逻辑
widget = Text('没有更多了', style: style);
widget = _setbackground(false, widget);
widget = GestureDetector(
onTap: () {},
child: widget,
);
break;
case IndicatorStatus.fullScreenError:
/// TODO 异常逻辑
widget = Text('没有更多了', style: style);
widget = _setbackground(true, widget);
widget = GestureDetector(
onTap: () {},
child: widget,
);
break;
case IndicatorStatus.noMoreLoad:
widget = Text('没有更多了', style: style);
widget = _setbackground(false, widget, height: 60.0);
break;
case IndicatorStatus.empty:
widget = Text('用户没有投稿', style: style);
widget = _setbackground(true, widget);
break;
}
return widget;
}
Widget _setbackground(bool full, Widget widget, {double height = 100}) {
widget = Padding(
padding: height == double.infinity
? EdgeInsets.zero
: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
child: Container(
width: double.infinity,
height: height,
color: Theme.of(context).colorScheme.background,
alignment: Alignment.center,
child: widget,
),
);
return widget;
}
Widget getIndicator(BuildContext context) {
final TargetPlatform platform = Theme.of(context).platform;
return platform == TargetPlatform.iOS
? const CupertinoActivityIndicator(
animating: true,
radius: 16.0,
)
: CircularProgressIndicator(
strokeWidth: 2.0,
valueColor:
AlwaysStoppedAnimation<Color>(Theme.of(context).primaryColor),
);
}
}
class LoadMoreListSource extends LoadingMoreBase<DynamicItemModel> {
late MemberDynamicPanelController ctr;
LoadMoreListSource(this.ctr);
@override
Future<bool> loadData([bool isloadMoreAction = false]) async {
bool isSuccess = false;
var res = await ctr.getMemberDynamic();
if (res['status']) {
addAll(res['data'].items);
}
try {
if (res['data'].hasMore) {
isSuccess = true;
} else {
isSuccess = false;
}
} catch (_) {}
return isSuccess;
}
}

View File

@ -1,16 +1,16 @@
import 'dart:async'; import 'dart:async';
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/pages/member/archive/view.dart';
import 'package:pilipala/pages/member/dynamic/index.dart';
import 'package:pilipala/pages/member/index.dart'; import 'package:pilipala/pages/member/index.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import 'widgets/conis.dart';
import 'widgets/profile.dart'; import 'widgets/profile.dart';
import 'widgets/seasons.dart';
class MemberPage extends StatefulWidget { class MemberPage extends StatefulWidget {
const MemberPage({super.key}); const MemberPage({super.key});
@ -23,9 +23,10 @@ class _MemberPageState extends State<MemberPage>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
late String heroTag; late String heroTag;
late MemberController _memberController; late MemberController _memberController;
Future? _futureBuilderFuture; late Future _futureBuilderFuture;
late Future _memberSeasonsFuture;
late Future _memberCoinsFuture;
final ScrollController _extendNestCtr = ScrollController(); final ScrollController _extendNestCtr = ScrollController();
late TabController _tabController;
final StreamController<bool> appbarStream = StreamController<bool>(); final StreamController<bool> appbarStream = StreamController<bool>();
late int mid; late int mid;
@ -35,12 +36,13 @@ class _MemberPageState extends State<MemberPage>
mid = int.parse(Get.parameters['mid']!); mid = int.parse(Get.parameters['mid']!);
heroTag = Get.arguments['heroTag'] ?? Utils.makeHeroTag(mid); heroTag = Get.arguments['heroTag'] ?? Utils.makeHeroTag(mid);
_memberController = Get.put(MemberController(), tag: heroTag); _memberController = Get.put(MemberController(), tag: heroTag);
_tabController = TabController(length: 3, vsync: this, initialIndex: 2);
_futureBuilderFuture = _memberController.getInfo(); _futureBuilderFuture = _memberController.getInfo();
_memberSeasonsFuture = _memberController.getMemberSeasons();
_memberCoinsFuture = _memberController.getRecentCoinVideo();
_extendNestCtr.addListener( _extendNestCtr.addListener(
() { () {
double offset = _extendNestCtr.position.pixels; double offset = _extendNestCtr.position.pixels;
if (offset > 230) { if (offset > 100) {
appbarStream.add(true); appbarStream.add(true);
} else { } else {
appbarStream.add(false); appbarStream.add(false);
@ -59,183 +61,222 @@ class _MemberPageState extends State<MemberPage>
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
primary: true, primary: true,
body: ExtendedNestedScrollView( body: Column(
controller: _extendNestCtr, children: [
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { AppBar(
return <Widget>[ title: StreamBuilder(
SliverAppBar( stream: appbarStream.stream,
pinned: false, initialData: false,
primary: true, builder: (context, AsyncSnapshot snapshot) {
elevation: 0, return AnimatedOpacity(
scrolledUnderElevation: 1, opacity: snapshot.data ? 1 : 0,
forceElevated: innerBoxIsScrolled, curve: Curves.easeOut,
expandedHeight: 290, duration: const Duration(milliseconds: 500),
titleSpacing: 0, child: Row(
title: StreamBuilder( children: [
stream: appbarStream.stream, Row(
initialData: false, children: [
builder: (context, AsyncSnapshot snapshot) { Obx(
return AnimatedOpacity( () => NetworkImgLayer(
opacity: snapshot.data ? 1 : 0, width: 35,
curve: Curves.easeOut, height: 35,
duration: const Duration(milliseconds: 500), type: 'avatar',
child: Row( src: _memberController.face.value,
children: [
Row(
children: [
Obx(
() => NetworkImgLayer(
width: 35,
height: 35,
type: 'avatar',
src: _memberController.face.value,
),
), ),
const SizedBox(width: 10), ),
Obx( const SizedBox(width: 10),
() => Text( Obx(
_memberController.memberInfo.value.name ?? '', () => Text(
style: TextStyle( _memberController.memberInfo.value.name ?? '',
color: Theme.of(context) style: TextStyle(
.colorScheme color: Theme.of(context)
.onBackground, .colorScheme
fontSize: 14), .onBackground,
), fontSize: 14),
), ),
], ),
) ],
],
),
);
},
),
actions: [
IconButton(
onPressed: () => Get.toNamed(
'/memberSearch?mid=${Get.parameters['mid']}&uname=${_memberController.memberInfo.value.name!}'),
icon: const Icon(Icons.search_outlined),
),
PopupMenuButton(
icon: const Icon(Icons.more_vert),
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
if (_memberController.ownerMid !=
_memberController.mid) ...[
PopupMenuItem(
onTap: () => _memberController.blockUser(),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.block, size: 19),
const SizedBox(width: 10),
Text(_memberController.attribute.value != 128
? '加入黑名单'
: '移除黑名单'),
],
),
) )
], ],
),
);
},
),
actions: [
IconButton(
onPressed: () => Get.toNamed(
'/memberSearch?mid=${Get.parameters['mid']}&uname=${_memberController.memberInfo.value.name!}'),
icon: const Icon(Icons.search_outlined),
),
PopupMenuButton(
icon: const Icon(Icons.more_vert),
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
if (_memberController.ownerMid != _memberController.mid) ...[
PopupMenuItem( PopupMenuItem(
onTap: () => _memberController.shareUser(), onTap: () => _memberController.blockUser(),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const Icon(Icons.share_outlined, size: 19), const Icon(Icons.block, size: 19),
const SizedBox(width: 10), const SizedBox(width: 10),
Text(_memberController.ownerMid != Text(_memberController.attribute.value != 128
_memberController.mid ? '加入黑名单'
? '分享UP主' : '移除黑名单'),
: '分享我的主页'),
], ],
), ),
), )
], ],
PopupMenuItem(
onTap: () => _memberController.shareUser(),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.share_outlined, size: 19),
const SizedBox(width: 10),
Text(_memberController.ownerMid != _memberController.mid
? '分享UP主'
: '分享我的主页'),
],
),
),
],
),
const SizedBox(width: 4),
],
),
Expanded(
child: SingleChildScrollView(
controller: _extendNestCtr,
child: Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom + 20,
), ),
const SizedBox(width: 4), child: Column(
],
flexibleSpace: FlexibleSpaceBar(
background: Stack(
children: [ children: [
profileWidget(),
/// 动态链接
ListTile(
onTap: _memberController.pushDynamicsPage,
title: const Text('Ta的动态'),
trailing:
const Icon(Icons.arrow_forward_outlined, size: 19),
),
/// 视频
ListTile(
onTap: _memberController.pushArchivesPage,
title: const Text('Ta的投稿'),
trailing:
const Icon(Icons.arrow_forward_outlined, size: 19),
),
/// 专栏
ListTile(
onTap: () {},
title: const Text('Ta的专栏'),
),
MediaQuery.removePadding(
removeTop: true,
removeBottom: true,
context: context,
child: Padding(
padding: const EdgeInsets.only(
left: StyleString.safeSpace,
right: StyleString.safeSpace,
),
child: FutureBuilder(
future: _memberSeasonsFuture,
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.done) {
if (snapshot.data == null) {
return const SizedBox();
}
if (snapshot.data['status']) {
Map data = snapshot.data as Map;
if (data['data'].seasonsList.isEmpty) {
return commenWidget('用户没有设置专栏');
} else {
return MemberSeasonsPanel(data: data['data']);
}
} else {
// 请求错误
return const SizedBox();
}
} else {
return const SizedBox();
}
},
),
),
),
/// 收藏
/// 追番
/// 最近投币
Obx( Obx(
() => _memberController.face.value != '' () => _memberController.recentCoinsList.isNotEmpty
? Positioned.fill( ? ListTile(
bottom: 10, onTap: () {},
child: Container( title: const Text('最近投币的视频'),
decoration: BoxDecoration( // trailing: const Icon(Icons.arrow_forward_outlined,
image: DecorationImage( // size: 19),
fit: BoxFit.fitWidth,
image: NetworkImage(
_memberController.face.value),
alignment: Alignment.topCenter,
isAntiAlias: true,
),
),
foregroundDecoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Theme.of(context)
.colorScheme
.background
.withOpacity(0.44),
Theme.of(context).colorScheme.background,
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
stops: const [0.0, 0.46],
),
),
),
) )
: const SizedBox(), : const SizedBox(),
), ),
Positioned( MediaQuery.removePadding(
left: 0, removeTop: true,
right: 0, removeBottom: true,
bottom: 0, context: context,
height: 20, child: Padding(
child: Container( padding: const EdgeInsets.only(
color: Theme.of(context).colorScheme.background, left: StyleString.safeSpace,
right: StyleString.safeSpace,
),
child: FutureBuilder(
future: _memberCoinsFuture,
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.done) {
if (snapshot.data == null) {
return const SizedBox();
}
if (snapshot.data['status']) {
Map data = snapshot.data as Map;
return MemberCoinsPanel(data: data['data']);
} else {
// 请求错误
return const SizedBox();
}
} else {
return const SizedBox();
}
},
),
), ),
), ),
profileWidget(), // 最近点赞
// ListTile(
// onTap: () {},
// title: const Text('最近点赞的视频'),
// trailing:
// const Icon(Icons.arrow_forward_outlined, size: 19),
// ),
], ],
), ),
), ),
), ),
]; ),
}, ],
pinnedHeaderSliverHeightBuilder: () {
return MediaQuery.of(context).padding.top + kToolbarHeight;
},
onlyOneScrollInBody: true,
body: Column(
children: [
SizedBox(
width: double.infinity,
height: 50,
child: TabBar(controller: _tabController, tabs: const [
Tab(text: '主页'),
Tab(text: '动态'),
Tab(text: '投稿'),
]),
),
Expanded(
child: TabBarView(
controller: _tabController,
children: [
const Text('主页'),
MemberDynamicPanel(mid: mid),
ArchivePanel(mid: mid),
],
))
],
),
), ),
); );
} }
Widget profileWidget() { Widget profileWidget() {
return Padding( return Padding(
padding: const EdgeInsets.only(left: 18, right: 18), padding: const EdgeInsets.only(left: 18, right: 18, bottom: 20),
child: FutureBuilder( child: FutureBuilder(
future: _futureBuilderFuture, future: _futureBuilderFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
@ -249,8 +290,8 @@ class _MemberPageState extends State<MemberPage>
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
profile(_memberController), ProfilePanel(ctr: _memberController),
const SizedBox(height: 14), const SizedBox(height: 20),
Row( Row(
children: [ children: [
Flexible( Flexible(
@ -260,7 +301,7 @@ class _MemberPageState extends State<MemberPage>
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
.bodyLarge! .titleMedium!
.copyWith(fontWeight: FontWeight.bold), .copyWith(fontWeight: FontWeight.bold),
)), )),
const SizedBox(width: 2), const SizedBox(width: 2),
@ -332,29 +373,11 @@ class _MemberPageState extends State<MemberPage>
softWrap: true, softWrap: true,
), ),
], ],
const SizedBox(height: 4), const SizedBox(height: 6),
if (_memberController.memberInfo.value.sign != '') if (_memberController.memberInfo.value.sign != '')
SelectableText( SelectableText(
_memberController.memberInfo.value.sign!, _memberController.memberInfo.value.sign!,
maxLines: _memberController ),
.memberInfo.value.official!['title'] !=
''
? 1
: 2,
style: const TextStyle(
overflow: TextOverflow.ellipsis),
onTap: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
content: SelectableText(_memberController
.memberInfo.value.sign!),
);
},
);
},
)
], ],
), ),
], ],
@ -365,10 +388,28 @@ class _MemberPageState extends State<MemberPage>
} }
} else { } else {
// 骨架屏 // 骨架屏
return profile(_memberController, loadingStatus: true); return ProfilePanel(ctr: _memberController, loadingStatus: true);
} }
}, },
), ),
); );
} }
Widget commenWidget(msg) {
return Padding(
padding: const EdgeInsets.only(
top: 20,
bottom: 30,
),
child: Center(
child: Text(
msg,
style: Theme.of(context)
.textTheme
.labelMedium!
.copyWith(color: Theme.of(context).colorScheme.outline),
),
),
);
}
} }

View File

@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/models/member/coin.dart';
import 'package:pilipala/pages/member_coin/widgets/item.dart';
class MemberCoinsPanel extends StatelessWidget {
final List<MemberCoinsDataModel>? data;
const MemberCoinsPanel({super.key, this.data});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, boxConstraints) {
return GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, // Use a fixed count for GridView
crossAxisSpacing: StyleString.safeSpace,
mainAxisSpacing: StyleString.safeSpace,
childAspectRatio: 0.94,
),
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: data!.length,
itemBuilder: (context, i) {
return MemberCoinsItem(coinItem: data![i]);
},
);
},
);
}
}

View File

@ -6,125 +6,162 @@ import 'package:pilipala/models/live/item.dart';
import 'package:pilipala/models/member/info.dart'; import 'package:pilipala/models/member/info.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
Widget profile(ctr, {loadingStatus = false}) { class ProfilePanel extends StatelessWidget {
MemberInfoModel memberInfo = ctr.memberInfo.value; final dynamic ctr;
return Builder( final bool loadingStatus;
builder: ((context) { const ProfilePanel({
return Padding( super.key,
padding: EdgeInsets.only(top: 3 * MediaQuery.of(context).padding.top), required this.ctr,
child: Row( this.loadingStatus = false,
children: [ });
Hero(
tag: ctr.heroTag!, @override
child: Stack( Widget build(BuildContext context) {
children: [ MemberInfoModel memberInfo = ctr.memberInfo.value;
NetworkImgLayer( return Builder(
width: 90, builder: ((context) {
height: 90, return Padding(
type: 'avatar', padding:
src: !loadingStatus ? memberInfo.face : ctr.face.value, EdgeInsets.only(top: MediaQuery.of(context).padding.top - 20),
), child: Row(
if (!loadingStatus && children: [
memberInfo.liveRoom != null && Hero(
memberInfo.liveRoom!.liveStatus == 1) tag: ctr.heroTag!,
Positioned( child: Stack(
bottom: 0, children: [
left: 14, NetworkImgLayer(
child: GestureDetector( width: 90,
onTap: () { height: 90,
LiveItemModel liveItem = LiveItemModel.fromJson({ type: 'avatar',
'title': memberInfo.liveRoom!.title, src: !loadingStatus ? memberInfo.face : ctr.face.value,
'uname': memberInfo.name, ),
'face': memberInfo.face, if (!loadingStatus &&
'roomid': memberInfo.liveRoom!.roomId, memberInfo.liveRoom != null &&
'watched_show': memberInfo.liveRoom!.watchedShow, memberInfo.liveRoom!.liveStatus == 1)
}); Positioned(
Get.toNamed( bottom: 0,
'/liveRoom?roomid=${memberInfo.liveRoom!.roomId}', left: 14,
arguments: {'liveItem': liveItem}, child: GestureDetector(
);
},
child: Container(
padding: const EdgeInsets.fromLTRB(6, 2, 6, 2),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius:
const BorderRadius.all(Radius.circular(10)),
),
child: Row(children: [
Image.asset(
'assets/images/live.gif',
height: 10,
),
Text(
' 直播中',
style: TextStyle(
color: Colors.white,
fontSize: Theme.of(context)
.textTheme
.labelSmall!
.fontSize),
)
]),
),
),
)
],
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(left: 10, right: 10),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
InkWell(
onTap: () { onTap: () {
LiveItemModel liveItem = LiveItemModel.fromJson({
'title': memberInfo.liveRoom!.title,
'uname': memberInfo.name,
'face': memberInfo.face,
'roomid': memberInfo.liveRoom!.roomId,
'watched_show': memberInfo.liveRoom!.watchedShow,
});
Get.toNamed( Get.toNamed(
'/follow?mid=${memberInfo.mid}&name=${memberInfo.name}'); '/liveRoom?roomid=${memberInfo.liveRoom!.roomId}',
arguments: {'liveItem': liveItem},
);
}, },
child: Column( child: Container(
children: [ padding: const EdgeInsets.fromLTRB(6, 2, 6, 2),
Text( decoration: BoxDecoration(
!loadingStatus color: Theme.of(context).colorScheme.primary,
? ctr.userStat!['following'].toString() borderRadius:
: '-', const BorderRadius.all(Radius.circular(10)),
style: const TextStyle( ),
fontWeight: FontWeight.bold), child: Row(children: [
Image.asset(
'assets/images/live.gif',
height: 10,
), ),
Text( Text(
'关注', ' 直播中',
style: TextStyle( style: TextStyle(
color: Colors.white,
fontSize: Theme.of(context) fontSize: Theme.of(context)
.textTheme .textTheme
.labelMedium! .labelSmall!
.fontSize), .fontSize),
) )
], ]),
), ),
), ),
InkWell( )
onTap: () { ],
Get.toNamed( ),
'/fan?mid=${memberInfo.mid}&name=${memberInfo.name}'); ),
}, const SizedBox(width: 12),
child: Column( Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding:
const EdgeInsets.only(top: 10, left: 10, right: 10),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
InkWell(
onTap: () {
Get.toNamed(
'/follow?mid=${memberInfo.mid}&name=${memberInfo.name}');
},
child: Column(
children: [
Text(
!loadingStatus
? ctr.userStat!['following'].toString()
: '-',
style: const TextStyle(
fontWeight: FontWeight.bold),
),
Text(
'关注',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize),
)
],
),
),
InkWell(
onTap: () {
Get.toNamed(
'/fan?mid=${memberInfo.mid}&name=${memberInfo.name}');
},
child: Column(
children: [
Text(
!loadingStatus
? ctr.userStat!['follower'] != null
? Utils.numFormat(
ctr.userStat!['follower'],
)
: '-'
: '-',
style: const TextStyle(
fontWeight: FontWeight.bold)),
Text(
'粉丝',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize),
)
],
),
),
Column(
children: [ children: [
Text( Text(
!loadingStatus !loadingStatus
? Utils.numFormat( ? ctr.userStat!['likes'] != null
ctr.userStat!['follower'], ? Utils.numFormat(
) ctr.userStat!['likes'],
)
: '-'
: '-', : '-',
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.bold)), fontWeight: FontWeight.bold)),
Text( Text(
'粉丝', '获赞',
style: TextStyle( style: TextStyle(
fontSize: Theme.of(context) fontSize: Theme.of(context)
.textTheme .textTheme
@ -133,86 +170,89 @@ Widget profile(ctr, {loadingStatus = false}) {
) )
], ],
), ),
), ],
Column(
children: [
const Text('-',
style: TextStyle(fontWeight: FontWeight.bold)),
Text(
'获赞',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize),
)
],
),
],
),
),
const SizedBox(height: 10),
if (ctr.ownerMid != ctr.mid) ...[
Row(
children: [
Obx(
() => Expanded(
child: TextButton(
onPressed: () => ctr.actionRelationMod(),
style: TextButton.styleFrom(
foregroundColor: ctr.attribute.value == -1
? Colors.transparent
: ctr.attribute.value != 0
? Theme.of(context).colorScheme.outline
: Theme.of(context)
.colorScheme
.onPrimary,
backgroundColor: ctr.attribute.value != 0
? Theme.of(context)
.colorScheme
.onInverseSurface
: Theme.of(context)
.colorScheme
.primary, // 设置按钮背景色
),
child: Obx(() => Text(ctr.attributeText.value)),
),
),
),
const SizedBox(width: 8),
Expanded(
child: TextButton(
onPressed: () {},
style: TextButton.styleFrom(
backgroundColor: Theme.of(context)
.colorScheme
.onInverseSurface,
),
child: const Text('发消息'),
),
)
],
)
] else ...[
TextButton(
onPressed: () {
SmartDialog.showToast('功能开发中 💪');
},
style: TextButton.styleFrom(
padding: const EdgeInsets.only(left: 80, right: 80),
foregroundColor:
Theme.of(context).colorScheme.onPrimary,
backgroundColor: Theme.of(context).colorScheme.primary,
), ),
child: const Text('编辑资料'), ),
) const SizedBox(height: 10),
] if (ctr.ownerMid != ctr.mid && ctr.ownerMid != -1) ...[
], Row(
children: [
Obx(
() => Expanded(
child: TextButton(
onPressed: () => ctr.actionRelationMod(),
style: TextButton.styleFrom(
foregroundColor: ctr.attribute.value == -1
? Colors.transparent
: ctr.attribute.value != 0
? Theme.of(context)
.colorScheme
.outline
: Theme.of(context)
.colorScheme
.onPrimary,
backgroundColor: ctr.attribute.value != 0
? Theme.of(context)
.colorScheme
.onInverseSurface
: Theme.of(context)
.colorScheme
.primary, // 设置按钮背景色
),
child: Obx(() => Text(ctr.attributeText.value)),
),
),
),
const SizedBox(width: 8),
Expanded(
child: TextButton(
onPressed: () {},
style: TextButton.styleFrom(
backgroundColor: Theme.of(context)
.colorScheme
.onInverseSurface,
),
child: const Text('发消息'),
),
)
],
)
],
if (ctr.ownerMid == ctr.mid && ctr.ownerMid != -1) ...[
TextButton(
onPressed: () {
SmartDialog.showToast('功能开发中 💪');
},
style: TextButton.styleFrom(
padding: const EdgeInsets.only(left: 80, right: 80),
foregroundColor:
Theme.of(context).colorScheme.onPrimary,
backgroundColor:
Theme.of(context).colorScheme.primary,
),
child: const Text('编辑资料'),
)
],
if (ctr.ownerMid == -1) ...[
TextButton(
onPressed: () {},
style: TextButton.styleFrom(
padding: const EdgeInsets.only(left: 80, right: 80),
foregroundColor:
Theme.of(context).colorScheme.outline,
backgroundColor:
Theme.of(context).colorScheme.onInverseSurface,
),
child: const Text('未登录'),
)
]
],
),
), ),
), ],
], ),
), );
); }),
}), );
); }
} }

View File

@ -0,0 +1,85 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/models/member/seasons.dart';
import 'package:pilipala/pages/member_seasons/widgets/item.dart';
class MemberSeasonsPanel extends StatelessWidget {
final MemberSeasonsDataModel? data;
const MemberSeasonsPanel({super.key, this.data});
@override
Widget build(BuildContext context) {
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: data!.seasonsList!.length,
itemBuilder: (context, index) {
MemberSeasonsList item = data!.seasonsList![index];
return Padding(
padding: const EdgeInsets.only(bottom: 12, right: 4),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 12, left: 4),
child: Row(
children: [
Text(
item.meta!.name!,
maxLines: 1,
style: Theme.of(context).textTheme.titleSmall!,
),
const SizedBox(width: 10),
PBadge(
stack: 'relative',
size: 'small',
text: item.meta!.total.toString(),
),
const Spacer(),
SizedBox(
width: 35,
height: 35,
child: IconButton(
onPressed: () => Get.toNamed(
'/memberSeasons?mid=${item.meta!.mid}&seasonId=${item.meta!.seasonId}'),
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
icon: const Icon(
Icons.arrow_forward,
size: 20,
),
),
)
],
),
),
LayoutBuilder(
builder: (context, boxConstraints) {
return GridView.builder(
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, // Use a fixed count for GridView
crossAxisSpacing: StyleString.safeSpace,
mainAxisSpacing: StyleString.safeSpace,
childAspectRatio: 0.94,
),
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: item.archives!.length,
itemBuilder: (context, i) {
return MemberSeasonsItem(seasonItem: item.archives![i]);
},
);
},
),
],
),
);
},
);
}
}

View File

@ -1,9 +1,11 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/http/member.dart'; import 'package:pilipala/http/member.dart';
import 'package:pilipala/models/member/archive.dart';
class ArchiveController extends GetxController { class MemberArchiveController extends GetxController {
ArchiveController(this.mid); final ScrollController scrollController = ScrollController();
int? mid; late int mid;
int pn = 1; int pn = 1;
int count = 0; int count = 0;
RxMap<String, String> currentOrder = <String, String>{}.obs; RxMap<String, String> currentOrder = <String, String>{}.obs;
@ -12,20 +14,27 @@ class ArchiveController extends GetxController {
{'type': 'click', 'label': '最多播放'}, {'type': 'click', 'label': '最多播放'},
{'type': 'stow', 'label': '最多收藏'}, {'type': 'stow', 'label': '最多收藏'},
]; ];
RxList<VListItemModel> archivesList = <VListItemModel>[].obs;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
mid ??= int.parse(Get.parameters['mid']!); mid = int.parse(Get.parameters['mid']!);
print('🐶🐶: $mid');
currentOrder.value = orderList.first; currentOrder.value = orderList.first;
} }
// 稿 // 稿
Future getMemberArchive() async { Future getMemberArchive(type) async {
if (type == 'onRefresh') {
pn = 1;
}
var res = await MemberHttp.memberArchive( var res = await MemberHttp.memberArchive(
mid: mid, pn: pn, order: currentOrder['type']!); mid: mid,
pn: pn,
order: currentOrder['type']!,
);
if (res['status']) { if (res['status']) {
archivesList.addAll(res['data'].list.vlist);
count = res['data'].page['count']; count = res['data'].page['count'];
pn += 1; pn += 1;
} }
@ -34,11 +43,16 @@ class ArchiveController extends GetxController {
toggleSort() async { toggleSort() async {
pn = 1; pn = 1;
int index = orderList.indexOf(currentOrder.value); int index = orderList.indexOf(currentOrder);
if (index == orderList.length - 1) { if (index == orderList.length - 1) {
currentOrder.value = orderList.first; currentOrder.value = orderList.first;
} else { } else {
currentOrder.value = orderList[index + 1]; currentOrder.value = orderList[index + 1];
} }
} }
//
Future onLoad() async {
getMemberArchive('onLoad');
}
} }

View File

@ -0,0 +1,4 @@
library member_archive;
export './controller.dart';
export './view.dart';

View File

@ -0,0 +1,125 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/video_card_h.dart';
import 'package:pilipala/utils/utils.dart';
import 'controller.dart';
class MemberArchivePage extends StatefulWidget {
const MemberArchivePage({super.key});
@override
State<MemberArchivePage> createState() => _MemberArchivePageState();
}
class _MemberArchivePageState extends State<MemberArchivePage> {
late MemberArchiveController _memberArchivesController;
late Future _futureBuilderFuture;
late ScrollController scrollController;
late int mid;
@override
void initState() {
super.initState();
mid = int.parse(Get.parameters['mid']!);
final String heroTag = Utils.makeHeroTag(mid);
_memberArchivesController =
Get.put(MemberArchiveController(), tag: heroTag);
_futureBuilderFuture =
_memberArchivesController.getMemberArchive('onRefresh');
scrollController = _memberArchivesController.scrollController;
scrollController.addListener(
() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
EasyThrottle.throttle(
'member_archives', const Duration(milliseconds: 500), () {
_memberArchivesController.onLoad();
});
}
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('他的投稿'),
// actions: [
// Obx(
// () => PopupMenuButton<String>(
// padding: EdgeInsets.zero,
// tooltip: '投稿排序',
// icon: Icon(
// Icons.more_vert_outlined,
// color: Theme.of(context).colorScheme.outline,
// ),
// position: PopupMenuPosition.under,
// onSelected: (String type) {},
// itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
// for (var i in _memberArchivesController.orderList) ...[
// PopupMenuItem<String>(
// onTap: () {},
// value: _memberArchivesController.currentOrder['label'],
// child: Row(
// mainAxisSize: MainAxisSize.min,
// children: [
// Text(i['label']!),
// if (_memberArchivesController.currentOrder['label'] ==
// i['label']) ...[
// const SizedBox(width: 10),
// const Icon(Icons.done, size: 20),
// ],
// ],
// ),
// ),
// ]
// ],
// ),
// ),
// ],
),
body: CustomScrollView(
controller: _memberArchivesController.scrollController,
slivers: [
FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data != null) {
Map data = snapshot.data as Map;
List list = _memberArchivesController.archivesList;
if (data['status']) {
return Obx(
() => list.isNotEmpty
? SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return VideoCardH(
videoItem: list[index],
showOwner: false,
showPubdate: true,
);
},
childCount: list.length,
),
)
: const SliverToBoxAdapter(),
);
} else {
return const SliverToBoxAdapter();
}
} else {
return const SliverToBoxAdapter();
}
} else {
return const SliverToBoxAdapter();
}
},
),
],
),
);
}
}

View File

@ -0,0 +1,3 @@
import 'package:get/get.dart';
class MemberCoinController extends GetxController {}

View File

@ -1,4 +1,4 @@
library dynamic_panel; library member_coin;
export './controller.dart'; export './controller.dart';
export './view.dart'; export './view.dart';

View File

@ -0,0 +1,15 @@
import 'package:flutter/material.dart';
class MemberCoinPage extends StatefulWidget {
const MemberCoinPage({super.key});
@override
State<MemberCoinPage> createState() => _MemberCoinPageState();
}
class _MemberCoinPageState extends State<MemberCoinPage> {
@override
Widget build(BuildContext context) {
return Scaffold();
}
}

View File

@ -0,0 +1,95 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
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/common/widgets/stat/view.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/member/coin.dart';
import 'package:pilipala/utils/utils.dart';
class MemberCoinsItem extends StatelessWidget {
final MemberCoinsDataModel coinItem;
const MemberCoinsItem({
Key? key,
required this.coinItem,
}) : super(key: key);
@override
Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(coinItem.aid);
return Card(
elevation: 0,
clipBehavior: Clip.hardEdge,
margin: EdgeInsets.zero,
child: InkWell(
onTap: () async {
int cid =
await SearchHttp.ab2c(aid: coinItem.aid, bvid: coinItem.bvid);
Get.toNamed('/video?bvid=${coinItem.bvid}&cid=$cid',
arguments: {'videoItem': coinItem, 'heroTag': heroTag});
},
child: Column(
children: [
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(builder: (context, boxConstraints) {
double maxWidth = boxConstraints.maxWidth;
double maxHeight = boxConstraints.maxHeight;
return Stack(
children: [
NetworkImgLayer(
src: coinItem.pic,
width: maxWidth,
height: maxHeight,
),
if (coinItem.duration != null)
PBadge(
bottom: 6,
right: 6,
type: 'gray',
text: Utils.timeFormat(coinItem.duration),
)
],
);
}),
),
Padding(
padding: const EdgeInsets.fromLTRB(5, 6, 0, 0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
coinItem.title!,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Row(
children: [
StatView(
view: coinItem.view,
theme: 'gray',
),
const Spacer(),
Text(
Utils.CustomStamp_str(
timestamp: coinItem.pubdate, date: 'MM-DD'),
style: TextStyle(
fontSize: 11,
color: Theme.of(context).colorScheme.outline,
),
),
const SizedBox(width: 6)
],
),
],
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/member.dart';
import 'package:pilipala/models/dynamics/result.dart';
class MemberDynamicsController extends GetxController {
final ScrollController scrollController = ScrollController();
late int mid;
String offset = '';
int count = 0;
bool hasMore = true;
RxList<DynamicItemModel> dynamicsList = <DynamicItemModel>[].obs;
@override
void onInit() {
super.onInit();
mid = int.parse(Get.parameters['mid']!);
}
Future getMemberDynamic(type) async {
if (type == 'onRefresh') {
offset = '';
dynamicsList.clear();
}
if (offset == '-1') {
return;
}
var res = await MemberHttp.memberDynamic(
offset: offset,
mid: mid,
);
if (res['status']) {
dynamicsList.addAll(res['data'].items);
offset = res['data'].offset != '' ? res['data'].offset : '-1';
hasMore = res['data'].hasMore;
}
return res;
}
// 上拉加载
Future onLoad() async {
getMemberDynamic('onLoad');
}
}

View File

@ -0,0 +1,4 @@
library member_dynamics;
export './controller.dart';
export './view.dart';

View File

@ -0,0 +1,95 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/pages/member_dynamics/index.dart';
import 'package:pilipala/utils/utils.dart';
import '../dynamics/widgets/dynamic_panel.dart';
class MemberDynamicsPage extends StatefulWidget {
const MemberDynamicsPage({super.key});
@override
State<MemberDynamicsPage> createState() => _MemberDynamicsPageState();
}
class _MemberDynamicsPageState extends State<MemberDynamicsPage> {
late MemberDynamicsController _memberDynamicController;
late Future _futureBuilderFuture;
late ScrollController scrollController;
late int mid;
@override
void initState() {
super.initState();
mid = int.parse(Get.parameters['mid']!);
final String heroTag = Utils.makeHeroTag(mid);
_memberDynamicController =
Get.put(MemberDynamicsController(), tag: heroTag);
_futureBuilderFuture =
_memberDynamicController.getMemberDynamic('onRefresh');
scrollController = _memberDynamicController.scrollController;
scrollController.addListener(
() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
EasyThrottle.throttle(
'member_dynamics', const Duration(milliseconds: 1000), () {
_memberDynamicController.onLoad();
});
}
},
);
}
@override
void dispose() {
_memberDynamicController.scrollController.removeListener(() {});
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('他的动态'),
),
body: CustomScrollView(
controller: _memberDynamicController.scrollController,
slivers: [
FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data != null) {
Map data = snapshot.data as Map;
List list = _memberDynamicController.dynamicsList;
if (data['status']) {
return Obx(
() => list.isNotEmpty
? SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return DynamicPanel(item: list[index]);
},
childCount: list.length,
),
)
: const SliverToBoxAdapter(),
);
} else {
return const SliverToBoxAdapter();
}
} else {
return const SliverToBoxAdapter();
}
} else {
return const SliverToBoxAdapter();
}
},
),
],
),
);
}
}

View File

@ -0,0 +1,3 @@
import 'package:get/get.dart';
class MemberLikeController extends GetxController {}

View File

@ -0,0 +1,4 @@
library member_like;
export './controller.dart';
export './view.dart';

View File

@ -0,0 +1,15 @@
import 'package:flutter/material.dart';
class MemberLikePage extends StatefulWidget {
const MemberLikePage({super.key});
@override
State<MemberLikePage> createState() => _MemberLikePageState();
}
class _MemberLikePageState extends State<MemberLikePage> {
@override
Widget build(BuildContext context) {
return Scaffold();
}
}

View File

@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/member.dart';
import 'package:pilipala/models/member/seasons.dart';
class MemberSeasonsController extends GetxController {
final ScrollController scrollController = ScrollController();
late int mid;
late int seasonId;
int pn = 1;
int ps = 30;
int count = 0;
RxList<MemberArchiveItem> seasonsList = <MemberArchiveItem>[].obs;
late Map page;
@override
void onInit() {
super.onInit();
mid = int.parse(Get.parameters['mid']!);
seasonId = int.parse(Get.parameters['seasonId']!);
}
// 获取专栏详情
Future getSeasonDetail(type) async {
if (type == 'onRefresh') {
pn = 1;
}
var res = await MemberHttp.getSeasonDetail(
mid: mid,
seasonId: seasonId,
pn: pn,
ps: ps,
sortReverse: false,
);
if (res['status']) {
seasonsList.addAll(res['data'].archives);
page = res['data'].page;
pn += 1;
}
return res;
}
// 上拉加载
Future onLoad() async {
getSeasonDetail('onLoad');
}
}

View File

@ -0,0 +1,4 @@
library member_seasons;
export 'controller.dart';
export 'view.dart';

View File

@ -0,0 +1,103 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'controller.dart';
import 'widgets/item.dart';
class MemberSeasonsPage extends StatefulWidget {
const MemberSeasonsPage({super.key});
@override
State<MemberSeasonsPage> createState() => _MemberSeasonsPageState();
}
class _MemberSeasonsPageState extends State<MemberSeasonsPage> {
final MemberSeasonsController _memberSeasonsController =
Get.put(MemberSeasonsController());
late Future _futureBuilderFuture;
late ScrollController scrollController;
@override
void initState() {
super.initState();
_futureBuilderFuture =
_memberSeasonsController.getSeasonDetail('onRefresh');
scrollController = _memberSeasonsController.scrollController;
scrollController.addListener(
() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
EasyThrottle.throttle(
'member_archives', const Duration(milliseconds: 500), () {
_memberSeasonsController.onLoad();
});
}
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('他的专栏'),
),
body: Padding(
padding: const EdgeInsets.only(
left: StyleString.safeSpace,
right: StyleString.safeSpace,
),
child: SingleChildScrollView(
controller: _memberSeasonsController.scrollController,
child: FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data != null) {
Map data = snapshot.data as Map;
List list = _memberSeasonsController.seasonsList;
if (data['status']) {
return Obx(
() => list.isNotEmpty
? LayoutBuilder(
builder: (context, boxConstraints) {
return GridView.builder(
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: StyleString.safeSpace,
mainAxisSpacing: StyleString.safeSpace,
childAspectRatio: 0.94,
),
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: _memberSeasonsController
.seasonsList.length,
itemBuilder: (context, i) {
return MemberSeasonsItem(
seasonItem: _memberSeasonsController
.seasonsList[i],
);
},
);
},
)
: const SizedBox(),
);
} else {
return const SizedBox();
}
} else {
return const SizedBox();
}
} else {
return const SizedBox();
}
},
),
),
),
);
}
}

View File

@ -0,0 +1,98 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
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/common/widgets/stat/view.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/utils/utils.dart';
class MemberSeasonsItem extends StatelessWidget {
final dynamic seasonItem;
const MemberSeasonsItem({
Key? key,
required this.seasonItem,
}) : super(key: key);
@override
Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(seasonItem.aid);
return Card(
elevation: 0,
clipBehavior: Clip.hardEdge,
margin: EdgeInsets.zero,
child: InkWell(
onTap: () async {
int cid =
await SearchHttp.ab2c(aid: seasonItem.aid, bvid: seasonItem.bvid);
Get.toNamed('/video?bvid=${seasonItem.bvid}&cid=$cid',
arguments: {'videoItem': seasonItem, 'heroTag': heroTag});
},
child: Column(
children: [
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: seasonItem.pic,
width: maxWidth,
height: maxHeight,
),
),
if (seasonItem.duration != null)
PBadge(
bottom: 6,
right: 6,
type: 'gray',
text: Utils.timeFormat(seasonItem.duration),
)
],
);
}),
),
Padding(
padding: const EdgeInsets.fromLTRB(5, 6, 0, 0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
seasonItem.title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Row(
children: [
StatView(
view: seasonItem.view,
theme: 'gray',
),
const Spacer(),
Text(
Utils.CustomStamp_str(
timestamp: seasonItem.pubdate, date: 'MM-DD'),
style: TextStyle(
fontSize: 11,
color: Theme.of(context).colorScheme.outline,
),
),
const SizedBox(width: 6)
],
),
],
),
),
],
),
),
);
}
}

View File

@ -129,4 +129,12 @@ class MineController extends GetxController {
} }
Get.toNamed('/fan?mid=${userInfo.value.mid}'); Get.toNamed('/fan?mid=${userInfo.value.mid}');
} }
pushDynamic() {
if (!userLogin.value) {
SmartDialog.showToast('账号未登录');
return;
}
Get.toNamed('/memberDynamics?mid=${userInfo.value.mid}');
}
} }

View File

@ -234,7 +234,7 @@ class _MinePageState extends State<MinePage> {
childAspectRatio: 1.67, childAspectRatio: 1.67,
children: <Widget>[ children: <Widget>[
InkWell( InkWell(
onTap: () {}, onTap: () => _mineController.pushDynamic(),
borderRadius: StyleString.mdRadius, borderRadius: StyleString.mdRadius,
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,

View File

@ -159,7 +159,6 @@ class _ImagePreviewState extends State<ImagePreview>
_previewController.onChange(index), _previewController.onChange(index),
canScrollPage: (GestureDetails? gestureDetails) => canScrollPage: (GestureDetails? gestureDetails) =>
gestureDetails!.totalScale! <= 1.0, gestureDetails!.totalScale! <= 1.0,
preloadPagesCount: 2,
itemCount: widget.imgList!.length, itemCount: widget.imgList!.length,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
return ExtendedImage.network( return ExtendedImage.network(

View File

@ -3,12 +3,14 @@ import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/http/video.dart'; import 'package:pilipala/http/video.dart';
import 'package:pilipala/models/home/rcmd/result.dart'; import 'package:pilipala/models/home/rcmd/result.dart';
// import 'package:pilipala/models/model_rec_video_item.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
class RcmdController extends GetxController { class RcmdController extends GetxController {
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
int _currentPage = 0; int _currentPage = 0;
RxList<RecVideoItemAppModel> videoList = <RecVideoItemAppModel>[].obs; RxList<RecVideoItemAppModel> videoList = <RecVideoItemAppModel>[].obs;
// RxList<RecVideoItemModel> videoList = <RecVideoItemModel>[].obs;
bool isLoadingMore = true; bool isLoadingMore = true;
OverlayEntry? popupDialog; OverlayEntry? popupDialog;
Box recVideo = GStrorage.recVideo; Box recVideo = GStrorage.recVideo;
@ -21,6 +23,7 @@ class RcmdController extends GetxController {
super.onInit(); super.onInit();
crossAxisCount.value = crossAxisCount.value =
setting.get(SettingBoxKey.customRows, defaultValue: 2); setting.get(SettingBoxKey.customRows, defaultValue: 2);
// 读取app端缓存内容
if (recVideo.get('cacheList') != null && if (recVideo.get('cacheList') != null &&
recVideo.get('cacheList').isNotEmpty) { recVideo.get('cacheList').isNotEmpty) {
List<RecVideoItemAppModel> list = []; List<RecVideoItemAppModel> list = [];
@ -35,6 +38,11 @@ class RcmdController extends GetxController {
// 获取推荐 // 获取推荐
Future queryRcmdFeed(type) async { Future queryRcmdFeed(type) async {
return await queryRcmdFeedApp(type);
}
// 获取app端推荐
Future queryRcmdFeedApp(type) async {
if (isLoadingMore == false) { if (isLoadingMore == false) {
return; return;
} }
@ -67,6 +75,40 @@ class RcmdController extends GetxController {
return res; return res;
} }
// 获取web端推荐
Future queryRcmdFeedWeb(type) async {
if (isLoadingMore == false) {
return;
}
if (type == 'onRefresh') {
_currentPage = 0;
}
var res = await VideoHttp.rcmdVideoList(
ps: 20,
freshIdx: _currentPage,
);
if (res['status']) {
if (type == 'init') {
if (videoList.isNotEmpty) {
videoList.addAll(res['data']);
} else {
videoList.value = res['data'];
}
} else if (type == 'onRefresh') {
if (enableSaveLastData) {
videoList.insertAll(0, res['data']);
} else {
videoList.value = res['data'];
}
} else if (type == 'onLoad') {
videoList.addAll(res['data']);
}
_currentPage += 1;
}
isLoadingMore = false;
return res;
}
// 下拉刷新 // 下拉刷新
Future onRefresh() async { Future onRefresh() async {
isLoadingMore = true; isLoadingMore = true;

View File

@ -11,6 +11,7 @@ import 'package:pilipala/common/widgets/animated_dialog.dart';
import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/overlay_pop.dart'; import 'package:pilipala/common/widgets/overlay_pop.dart';
import 'package:pilipala/common/widgets/video_card_v.dart'; import 'package:pilipala/common/widgets/video_card_v.dart';
import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/main/index.dart'; import 'package:pilipala/pages/main/index.dart';
import 'controller.dart'; import 'controller.dart';
@ -37,6 +38,8 @@ class _RcmdPageState extends State<RcmdPage>
ScrollController scrollController = _rcmdController.scrollController; ScrollController scrollController = _rcmdController.scrollController;
StreamController<bool> mainStream = StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream; Get.find<MainController>().bottomBarStream;
StreamController<bool> searchBarStream =
Get.find<HomeController>().searchBarStream;
scrollController.addListener( scrollController.addListener(
() { () {
if (scrollController.position.pixels >= if (scrollController.position.pixels >=
@ -47,13 +50,14 @@ class _RcmdPageState extends State<RcmdPage>
_rcmdController.onLoad(); _rcmdController.onLoad();
}); });
} }
final ScrollDirection direction = final ScrollDirection direction =
scrollController.position.userScrollDirection; scrollController.position.userScrollDirection;
if (direction == ScrollDirection.forward) { if (direction == ScrollDirection.forward) {
mainStream.add(true); mainStream.add(true);
searchBarStream.add(true);
} else if (direction == ScrollDirection.reverse) { } else if (direction == ScrollDirection.reverse) {
mainStream.add(false); mainStream.add(false);
searchBarStream.add(false);
} }
}, },
); );
@ -82,6 +86,7 @@ class _RcmdPageState extends State<RcmdPage>
}, },
child: CustomScrollView( child: CustomScrollView(
controller: _rcmdController.scrollController, controller: _rcmdController.scrollController,
physics: const AlwaysScrollableScrollPhysics(),
slivers: [ slivers: [
SliverPadding( SliverPadding(
padding: padding:
@ -152,11 +157,10 @@ class _RcmdPageState extends State<RcmdPage>
// crossAxisCount = 1; // crossAxisCount = 1;
// } // }
int crossAxisCount = ctr.crossAxisCount.value; int crossAxisCount = ctr.crossAxisCount.value;
double mainAxisExtent = double mainAxisExtent = (Get.size.width /
(Get.size.width / crossAxisCount / StyleString.aspectRatio) + crossAxisCount /
(crossAxisCount == 1 StyleString.aspectRatio) +
? 68 (crossAxisCount == 1 ? 68 : MediaQuery.textScalerOf(context).scale(86));
: 86 * MediaQuery.of(context).textScaleFactor);
return SliverGrid( return SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
// 行间距 // 行间距
@ -191,8 +195,8 @@ class _RcmdPageState extends State<RcmdPage>
} }
class LoadingMore extends StatelessWidget { class LoadingMore extends StatelessWidget {
dynamic ctr; final dynamic ctr;
LoadingMore({super.key, this.ctr}); const LoadingMore({super.key, this.ctr});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -203,12 +207,13 @@ class LoadingMore extends StatelessWidget {
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
if (ctr != null) { if (ctr != null) {
ctr!.isLoadingMore = true;
ctr!.onLoad(); ctr!.onLoad();
} }
}, },
child: Center( child: Center(
child: Text( child: Text(
'加载更多 👇', '点击加载更多 👇',
style: TextStyle( style: TextStyle(
color: Theme.of(context).colorScheme.outline, fontSize: 13), color: Theme.of(context).colorScheme.outline, fontSize: 13),
), ),

View File

@ -160,25 +160,25 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
} }
Widget _searchSuggest() { Widget _searchSuggest() {
SSearchController _ssCtr = _searchController; SSearchController ssCtr = _searchController;
return Obx( return Obx(
() => _ssCtr.searchSuggestList.isNotEmpty && () => ssCtr.searchSuggestList.isNotEmpty &&
_ssCtr.searchSuggestList.first.term != null && ssCtr.searchSuggestList.first.term != null &&
_ssCtr.controller.value.text != '' ssCtr.controller.value.text != ''
? ListView.builder( ? ListView.builder(
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true, shrinkWrap: true,
itemCount: _ssCtr.searchSuggestList.length, itemCount: ssCtr.searchSuggestList.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
return InkWell( return InkWell(
customBorder: RoundedRectangleBorder( customBorder: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
), ),
onTap: () => _ssCtr onTap: () => ssCtr
.onClickKeyword(_ssCtr.searchSuggestList[index].term!), .onClickKeyword(ssCtr.searchSuggestList[index].term!),
child: Padding( child: Padding(
padding: const EdgeInsets.only(left: 20, top: 9, bottom: 9), padding: const EdgeInsets.only(left: 20, top: 9, bottom: 9),
child: _ssCtr.searchSuggestList[index].textRich, child: ssCtr.searchSuggestList[index].textRich,
), ),
); );
}, },
@ -235,6 +235,7 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
return Obx( return Obx(
() => HotKeyword( () => HotKeyword(
width: width, width: width,
// ignore: invalid_use_of_protected_member
hotSearchList: _searchController.hotSearchList.value, hotSearchList: _searchController.hotSearchList.value,
onClick: (keyword) async { onClick: (keyword) async {
_searchController.searchFocusNode.unfocus(); _searchController.searchFocusNode.unfocus();

View File

@ -83,6 +83,7 @@ class _SearchPanelState extends State<SearchPanel>
case SearchType.video: case SearchType.video:
return SearchVideoPanel( return SearchVideoPanel(
ctr: _searchPanelController, ctr: _searchPanelController,
// ignore: invalid_use_of_protected_member
list: list.value, list: list.value,
); );
case SearchType.media_bangumi: case SearchType.media_bangumi:

View File

@ -82,6 +82,7 @@ class _SearchResultPageState extends State<SearchResultPage>
labelStyle: const TextStyle(fontSize: 13), labelStyle: const TextStyle(fontSize: 13),
dividerColor: Colors.transparent, dividerColor: Colors.transparent,
unselectedLabelColor: Theme.of(context).colorScheme.outline, unselectedLabelColor: Theme.of(context).colorScheme.outline,
tabAlignment: TabAlignment.start,
onTap: (index) { onTap: (index) {
if (index == _searchResultController!.tabIndex) { if (index == _searchResultController!.tabIndex) {
Get.find<SearchPanelController>( Get.find<SearchPanelController>(

View File

@ -3,6 +3,7 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/models/common/dynamics_type.dart'; import 'package:pilipala/models/common/dynamics_type.dart';
import 'package:pilipala/models/common/reply_sort_type.dart'; import 'package:pilipala/models/common/reply_sort_type.dart';
import 'package:pilipala/pages/setting/widgets/select_dialog.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'widgets/switch_item.dart'; import 'widgets/switch_item.dart';
@ -169,6 +170,12 @@ class _ExtraSettingState extends State<ExtraSetting> {
setKey: SettingBoxKey.enableSaveLastData, setKey: SettingBoxKey.enableSaveLastData,
defaultVal: false, defaultVal: false,
), ),
const SetSwitchItem(
title: '启用ai总结',
subTitle: '视频详情页开启ai总结',
setKey: SettingBoxKey.enableAi,
defaultVal: true,
),
ListTile( ListTile(
dense: false, dense: false,
title: Text('评论展示', style: titleStyle), title: Text('评论展示', style: titleStyle),
@ -176,23 +183,21 @@ class _ExtraSettingState extends State<ExtraSetting> {
'当前优先展示「${ReplySortType.values[defaultReplySort].titles}', '当前优先展示「${ReplySortType.values[defaultReplySort].titles}',
style: subTitleStyle, style: subTitleStyle,
), ),
trailing: PopupMenuButton( onTap: () async {
initialValue: defaultReplySort, int? result = await showDialog(
icon: const Icon(Icons.more_vert_outlined, size: 22), context: context,
onSelected: (item) { builder: (context) {
defaultReplySort = item; return SelectDialog<int>(title: '评论展示', value: defaultReplySort, values: ReplySortType.values.map((e) {
setting.put(SettingBoxKey.replySortType, item); return {'title': e.titles, 'value': e.index};
}).toList());
},
);
if (result != null) {
defaultReplySort = result;
setting.put(SettingBoxKey.replySortType, result);
setState(() {}); setState(() {});
}, }
itemBuilder: (BuildContext context) => <PopupMenuEntry>[ },
for (var i in ReplySortType.values) ...[
PopupMenuItem(
value: i.index,
child: Text(i.titles),
),
]
],
),
), ),
ListTile( ListTile(
dense: false, dense: false,
@ -201,23 +206,21 @@ class _ExtraSettingState extends State<ExtraSetting> {
'当前优先展示「${DynamicsType.values[defaultDynamicType].labels}', '当前优先展示「${DynamicsType.values[defaultDynamicType].labels}',
style: subTitleStyle, style: subTitleStyle,
), ),
trailing: PopupMenuButton( onTap: () async {
initialValue: defaultDynamicType, int? result = await showDialog(
icon: const Icon(Icons.more_vert_outlined, size: 22), context: context,
onSelected: (item) { builder: (context) {
defaultDynamicType = item; return SelectDialog<int>(title: '动态展示', value: defaultDynamicType, values: DynamicsType.values.map((e) {
setting.put(SettingBoxKey.defaultDynamicType, item); return {'title': e.labels, 'value': e.index};
}).toList());
},
);
if (result != null) {
defaultDynamicType = result;
setting.put(SettingBoxKey.defaultDynamicType, result);
setState(() {}); setState(() {});
}, }
itemBuilder: (BuildContext context) => <PopupMenuEntry>[ },
for (var i in DynamicsType.values) ...[
PopupMenuItem(
value: i.index,
child: Text(i.labels),
),
]
],
),
), ),
ListTile( ListTile(
enableFeedback: true, enableFeedback: true,
@ -225,6 +228,7 @@ class _ExtraSettingState extends State<ExtraSetting> {
title: Text('设置代理', style: titleStyle), title: Text('设置代理', style: titleStyle),
subtitle: Text('设置代理 host:port', style: subTitleStyle), subtitle: Text('设置代理 host:port', style: subTitleStyle),
trailing: Transform.scale( trailing: Transform.scale(
alignment: Alignment.centerRight,
scale: 0.8, scale: 0.8,
child: Switch( child: Switch(
thumbIcon: MaterialStateProperty.resolveWith<Icon?>( thumbIcon: MaterialStateProperty.resolveWith<Icon?>(

View File

@ -44,12 +44,10 @@ class _FontSizeSelectPageState extends State<FontSizeSelectPage> {
body: Column( body: Column(
children: [ children: [
Expanded( Expanded(
child: SingleChildScrollView( child: Center(
child: Center( child: Text(
child: Text( '当前字体大小:${currentSize == 1.0 ? '默认' : currentSize}',
'当前字体大小:${currentSize == 1.0 ? '默认' : currentSize}', style: TextStyle(fontSize: 14 * currentSize),
style: TextStyle(fontSize: 14 * currentSize),
),
), ),
), ),
), ),

View File

@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/pages/setting/widgets/switch_item.dart';
import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/plugin/pl_player/models/play_speed.dart'; import 'package:pilipala/plugin/pl_player/models/play_speed.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
@ -13,9 +15,11 @@ class PlaySpeedPage extends StatefulWidget {
class _PlaySpeedPageState extends State<PlaySpeedPage> { class _PlaySpeedPageState extends State<PlaySpeedPage> {
Box videoStorage = GStrorage.video; Box videoStorage = GStrorage.video;
Box settingStorage = GStrorage.setting;
late double playSpeedDefault; late double playSpeedDefault;
late double longPressSpeedDefault; late double longPressSpeedDefault;
late List customSpeedsList; late List customSpeedsList;
late bool enableAutoLongPressSpeed;
List<Map<dynamic, dynamic>> sheetMenu = [ List<Map<dynamic, dynamic>> sheetMenu = [
{ {
'id': 1, 'id': 1,
@ -24,6 +28,7 @@ class _PlaySpeedPageState extends State<PlaySpeedPage> {
Icons.speed, Icons.speed,
size: 21, size: 21,
), ),
'show': true,
}, },
{ {
'id': 2, 'id': 2,
@ -32,6 +37,7 @@ class _PlaySpeedPageState extends State<PlaySpeedPage> {
Icons.speed_sharp, Icons.speed_sharp,
size: 21, size: 21,
), ),
'show': true,
}, },
{ {
'id': -1, 'id': -1,
@ -40,6 +46,7 @@ class _PlaySpeedPageState extends State<PlaySpeedPage> {
Icons.delete_outline, Icons.delete_outline,
size: 21, size: 21,
), ),
'show': true,
}, },
]; ];
@ -55,6 +62,15 @@ class _PlaySpeedPageState extends State<PlaySpeedPage> {
// 自定义倍速 // 自定义倍速
customSpeedsList = customSpeedsList =
videoStorage.get(VideoBoxKey.customSpeedsList, defaultValue: []); videoStorage.get(VideoBoxKey.customSpeedsList, defaultValue: []);
enableAutoLongPressSpeed = settingStorage
.get(SettingBoxKey.enableAutoLongPressSpeed, defaultValue: false);
if (enableAutoLongPressSpeed) {
Map newItem = sheetMenu[1];
newItem['show'] = false;
setState(() {
sheetMenu[1] = newItem;
});
}
} }
// 添加自定义倍速 // 添加自定义倍速
@ -120,19 +136,21 @@ class _PlaySpeedPageState extends State<PlaySpeedPage> {
//重要 //重要
itemCount: sheetMenu.length, itemCount: sheetMenu.length,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
return ListTile( return sheetMenu[index]['show']
onTap: () { ? ListTile(
Navigator.pop(context); onTap: () {
menuAction(type, i, sheetMenu[index]['id']); Navigator.pop(context);
}, menuAction(type, i, sheetMenu[index]['id']);
minLeadingWidth: 0, },
iconColor: Theme.of(context).colorScheme.onSurface, minLeadingWidth: 0,
leading: sheetMenu[index]['leading'], iconColor: Theme.of(context).colorScheme.onSurface,
title: Text( leading: sheetMenu[index]['leading'],
sheetMenu[index]['title'], title: Text(
style: Theme.of(context).textTheme.titleSmall, sheetMenu[index]['title'],
), style: Theme.of(context).textTheme.titleSmall,
); ),
)
: const SizedBox();
}, },
), ),
); );
@ -210,11 +228,27 @@ class _PlaySpeedPageState extends State<PlaySpeedPage> {
title: const Text('默认倍速'), title: const Text('默认倍速'),
subtitle: Text(playSpeedDefault.toString()), subtitle: Text(playSpeedDefault.toString()),
), ),
ListTile( SetSwitchItem(
dense: false, title: '动态长按倍速',
title: const Text('默认长按倍速'), subTitle: '根据默认倍速长按时自动双倍',
subtitle: Text(longPressSpeedDefault.toString()), setKey: SettingBoxKey.enableAutoLongPressSpeed,
defaultVal: enableAutoLongPressSpeed,
callFn: (val) {
Map newItem = sheetMenu[1];
val ? newItem['show'] = false : newItem['show'] = true;
setState(() {
sheetMenu[1] = newItem;
enableAutoLongPressSpeed = val;
});
},
), ),
!enableAutoLongPressSpeed
? ListTile(
dense: false,
title: const Text('默认长按倍速'),
subtitle: Text(longPressSpeedDefault.toString()),
)
: const SizedBox(),
Padding( Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: 14, left: 14,

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/models/video/play/quality.dart'; import 'package:pilipala/models/video/play/quality.dart';
import 'package:pilipala/pages/setting/widgets/select_dialog.dart';
import 'package:pilipala/plugin/pl_player/index.dart'; import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/services/service_locator.dart'; import 'package:pilipala/services/service_locator.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
@ -68,7 +69,7 @@ class _PlaySettingState extends State<PlaySetting> {
dense: false, dense: false,
onTap: () => Get.toNamed('/playSpeedSet'), onTap: () => Get.toNamed('/playSpeedSet'),
title: Text('倍速设置', style: titleStyle), title: Text('倍速设置', style: titleStyle),
trailing: const Icon(Icons.arrow_forward_ios, size: 17), subtitle: Text('设置视频播放速度', style: subTitleStyle),
), ),
const SetSwitchItem( const SetSwitchItem(
title: '开启1080P', title: '开启1080P',
@ -96,7 +97,7 @@ class _PlaySettingState extends State<PlaySetting> {
), ),
const SetSwitchItem( const SetSwitchItem(
title: '自动PiP播放', title: '自动PiP播放',
subTitle: 'app切换至后台时画中画播放', subTitle: '进入后台时画中画播放',
setKey: SettingBoxKey.autoPiP, setKey: SettingBoxKey.autoPiP,
defaultVal: false, defaultVal: false,
), ),
@ -149,23 +150,21 @@ class _PlaySettingState extends State<PlaySetting> {
'当前画质${VideoQualityCode.fromCode(defaultVideoQa)!.description!}', '当前画质${VideoQualityCode.fromCode(defaultVideoQa)!.description!}',
style: subTitleStyle, style: subTitleStyle,
), ),
trailing: PopupMenuButton( onTap: () async {
initialValue: defaultVideoQa, int? result = await showDialog(
icon: const Icon(Icons.more_vert_outlined, size: 22), context: context,
onSelected: (item) { builder: (context) {
defaultVideoQa = item; return SelectDialog<int>(title: '默认画质', value: defaultVideoQa, values: VideoQuality.values.reversed.map((e) {
setting.put(SettingBoxKey.defaultVideoQa, item); return {'title': e.description, 'value': e.code};
}).toList());
},
);
if (result != null) {
defaultVideoQa = result;
setting.put(SettingBoxKey.defaultVideoQa, result);
setState(() {}); setState(() {});
}, }
itemBuilder: (BuildContext context) => <PopupMenuEntry>[ },
for (var i in VideoQuality.values.reversed) ...[
PopupMenuItem(
value: i.code,
child: Text(i.description),
),
]
],
),
), ),
ListTile( ListTile(
dense: false, dense: false,
@ -174,23 +173,21 @@ class _PlaySettingState extends State<PlaySetting> {
'当前音质${AudioQualityCode.fromCode(defaultAudioQa)!.description!}', '当前音质${AudioQualityCode.fromCode(defaultAudioQa)!.description!}',
style: subTitleStyle, style: subTitleStyle,
), ),
trailing: PopupMenuButton( onTap: () async {
initialValue: defaultAudioQa, int? result = await showDialog(
icon: const Icon(Icons.more_vert_outlined, size: 22), context: context,
onSelected: (item) { builder: (context) {
defaultAudioQa = item; return SelectDialog<int>(title: '默认音质', value: defaultAudioQa, values: AudioQuality.values.reversed.map((e) {
setting.put(SettingBoxKey.defaultAudioQa, item); return {'title': e.description, 'value': e.code};
}).toList());
},
);
if (result != null) {
defaultAudioQa = result;
setting.put(SettingBoxKey.defaultAudioQa, result);
setState(() {}); setState(() {});
}, }
itemBuilder: (BuildContext context) => <PopupMenuEntry>[ },
for (var i in AudioQuality.values.reversed) ...[
PopupMenuItem(
value: i.code,
child: Text(i.description),
),
]
],
),
), ),
ListTile( ListTile(
dense: false, dense: false,
@ -199,23 +196,21 @@ class _PlaySettingState extends State<PlaySetting> {
'当前解码格式${VideoDecodeFormatsCode.fromCode(defaultDecode)!.description!}', '当前解码格式${VideoDecodeFormatsCode.fromCode(defaultDecode)!.description!}',
style: subTitleStyle, style: subTitleStyle,
), ),
trailing: PopupMenuButton( onTap: () async {
initialValue: defaultDecode, String? result = await showDialog(
icon: const Icon(Icons.more_vert_outlined, size: 22), context: context,
onSelected: (item) { builder: (context) {
defaultDecode = item; return SelectDialog<String>(title: '默认解码格式', value: defaultDecode, values: VideoDecodeFormats.values.map((e) {
setting.put(SettingBoxKey.defaultDecode, item); return {'title': e.description, 'value': e.code};
}).toList());
},
);
if (result != null) {
defaultDecode = result;
setting.put(SettingBoxKey.defaultDecode, result);
setState(() {}); setState(() {});
}, }
itemBuilder: (BuildContext context) => <PopupMenuEntry>[ },
for (var i in VideoDecodeFormats.values) ...[
PopupMenuItem(
value: i.code,
child: Text(i.description),
),
]
],
),
), ),
ListTile( ListTile(
dense: false, dense: false,
@ -224,23 +219,21 @@ class _PlaySettingState extends State<PlaySetting> {
'当前全屏方式:${FullScreenModeCode.fromCode(defaultFullScreenMode)!.description}', '当前全屏方式:${FullScreenModeCode.fromCode(defaultFullScreenMode)!.description}',
style: subTitleStyle, style: subTitleStyle,
), ),
trailing: PopupMenuButton( onTap: () async {
initialValue: defaultFullScreenMode, int? result = await showDialog(
icon: const Icon(Icons.more_vert_outlined, size: 22), context: context,
onSelected: (item) { builder: (context) {
defaultFullScreenMode = item; return SelectDialog<int>(title: '默认全屏方式', value: defaultFullScreenMode, values: FullScreenMode.values.map((e) {
setting.put(SettingBoxKey.fullScreenMode, item); return {'title': e.description, 'value': e.code};
}).toList());
},
);
if (result != null) {
defaultFullScreenMode = result;
setting.put(SettingBoxKey.fullScreenMode, result);
setState(() {}); setState(() {});
}, }
itemBuilder: (BuildContext context) => <PopupMenuEntry>[ },
for (var i in FullScreenMode.values) ...[
PopupMenuItem(
value: i.code,
child: Text(i.description),
),
]
],
),
), ),
ListTile( ListTile(
dense: false, dense: false,
@ -249,23 +242,21 @@ class _PlaySettingState extends State<PlaySetting> {
'当前展示方式:${BtmProgresBehaviorCode.fromCode(defaultBtmProgressBehavior)!.description}', '当前展示方式:${BtmProgresBehaviorCode.fromCode(defaultBtmProgressBehavior)!.description}',
style: subTitleStyle, style: subTitleStyle,
), ),
trailing: PopupMenuButton( onTap: () async {
initialValue: defaultBtmProgressBehavior, int? result = await showDialog(
icon: const Icon(Icons.more_vert_outlined, size: 22), context: context,
onSelected: (item) { builder: (context) {
defaultBtmProgressBehavior = item; return SelectDialog<int>(title: '底部进度条展示', value: defaultBtmProgressBehavior, values: BtmProgresBehavior.values.map((e) {
setting.put(SettingBoxKey.btmProgressBehavior, item); return {'title': e.description, 'value': e.code};
}).toList());
},
);
if (result != null) {
defaultBtmProgressBehavior = result;
setting.put(SettingBoxKey.btmProgressBehavior, result);
setState(() {}); setState(() {});
}, }
itemBuilder: (BuildContext context) => <PopupMenuEntry>[ },
for (var i in BtmProgresBehavior.values) ...[
PopupMenuItem(
value: i.code,
child: Text(i.description),
),
]
],
),
), ),
], ],
), ),

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/http/member.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
class PrivacySetting extends StatefulWidget { class PrivacySetting extends StatefulWidget {
@ -53,6 +54,16 @@ class _PrivacySettingState extends State<PrivacySetting> {
title: Text('黑名单管理', style: titleStyle), title: Text('黑名单管理', style: titleStyle),
subtitle: Text('已拉黑用户', style: subTitleStyle), subtitle: Text('已拉黑用户', style: subTitleStyle),
), ),
ListTile(
onTap: () {
if (!userLogin) {
SmartDialog.showToast('请先登录');
}
MemberHttp.cookieToKey();
},
dense: false,
title: Text('刷新access_key', style: titleStyle),
),
], ],
), ),
); );

View File

@ -4,6 +4,8 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/models/common/theme_type.dart'; import 'package:pilipala/models/common/theme_type.dart';
import 'package:pilipala/pages/setting/pages/color_select.dart';
import 'package:pilipala/pages/setting/widgets/select_dialog.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'controller.dart'; import 'controller.dart';
@ -18,6 +20,8 @@ class StyleSetting extends StatefulWidget {
class _StyleSettingState extends State<StyleSetting> { class _StyleSettingState extends State<StyleSetting> {
final SettingController settingController = Get.put(SettingController()); final SettingController settingController = Get.put(SettingController());
final ColorSelectController colorSelectController = Get.put(ColorSelectController());
Box setting = GStrorage.setting; Box setting = GStrorage.setting;
late int picQuality; late int picQuality;
late ThemeType _tempThemeValue; late ThemeType _tempThemeValue;
@ -56,6 +60,7 @@ class _StyleSettingState extends State<StyleSetting> {
title: const Text('震动反馈'), title: const Text('震动反馈'),
subtitle: Text('请确定手机设置中已开启震动反馈', style: subTitleStyle), subtitle: Text('请确定手机设置中已开启震动反馈', style: subTitleStyle),
trailing: Transform.scale( trailing: Transform.scale(
alignment: Alignment.centerRight,
scale: 0.8, scale: 0.8,
child: Switch( child: Switch(
thumbIcon: MaterialStateProperty.resolveWith<Icon?>( thumbIcon: MaterialStateProperty.resolveWith<Icon?>(
@ -83,37 +88,42 @@ class _StyleSettingState extends State<StyleSetting> {
setKey: SettingBoxKey.enableMYBar, setKey: SettingBoxKey.enableMYBar,
defaultVal: true, defaultVal: true,
), ),
// SetSwitchItem( const SetSwitchItem(
// title: '首页单列', title: '首页顶栏收起',
// subTitle: '每行展示一个内容卡片', subTitle: '首页列表滑动时,收起顶栏',
// setKey: SettingBoxKey.enableSingleRow, setKey: SettingBoxKey.hideSearchBar,
// defaultVal: false, defaultVal: true,
// callFn: (val) => {SmartDialog.showToast('下次启动时生效')}, needReboot: true,
// ), ),
const SetSwitchItem(
title: '首页底栏收起',
subTitle: '首页列表滑动时,收起底栏',
setKey: SettingBoxKey.hideTabBar,
defaultVal: true,
needReboot: true,
),
ListTile( ListTile(
onTap: () async {
int? result = await showDialog(
context: context,
builder: (context) {
return SelectDialog<int>(title: '自定义列数', value: defaultCustomRows, values: [1, 2, 3, 4, 5].map((e) {
return {'title': '$e', 'value': e};
}).toList());
},
);
if (result != null) {
defaultCustomRows = result;
setting.put(SettingBoxKey.customRows, result);
setState(() {});
}
},
dense: false, dense: false,
title: Text('自定义列数', style: titleStyle), title: Text('自定义列数', style: titleStyle),
subtitle: Text( subtitle: Text(
'当前列数', '当前列数 $defaultCustomRows',
style: subTitleStyle, style: subTitleStyle,
), ),
trailing: PopupMenuButton(
initialValue: defaultCustomRows,
icon: const Icon(Icons.more_vert_outlined, size: 22),
onSelected: (item) {
defaultCustomRows = item;
setting.put(SettingBoxKey.customRows, item);
setState(() {});
},
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
for (var i in [1, 2, 3, 4, 5]) ...[
PopupMenuItem(
value: i,
child: Text(i.toString()),
),
]
],
),
), ),
ListTile( ListTile(
dense: false, dense: false,
@ -169,88 +179,58 @@ class _StyleSettingState extends State<StyleSetting> {
}, },
title: Text('图片质量', style: titleStyle), title: Text('图片质量', style: titleStyle),
subtitle: Text('选择合适的图片清晰度上限100%', style: subTitleStyle), subtitle: Text('选择合适的图片清晰度上限100%', style: subTitleStyle),
trailing: Obx( trailing: Padding(
() => Text( padding: const EdgeInsets.symmetric(horizontal: 8.0),
'${settingController.picQuality.value}%', child: Obx(
style: Theme.of(context).textTheme.titleSmall, () => Text(
'${settingController.picQuality.value}%',
style: Theme.of(context).textTheme.titleSmall,
),
), ),
), ),
), ),
ListTile( ListTile(
dense: false, dense: false,
onTap: () { onTap: () async {
showDialog( ThemeType? result = await showDialog(
context: context, context: context,
builder: (context) { builder: (context) {
return AlertDialog( return SelectDialog<ThemeType>(title: '主题模式', value: _tempThemeValue, values: ThemeType.values.map((e) {
title: const Text('主题模式'), return {'title': e.description, 'value': e};
contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 12), }).toList());
content: StatefulBuilder(
builder: (context, StateSetter setState) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
for (var i in ThemeType.values) ...[
RadioListTile(
value: i,
title: Text(i.description, style: titleStyle),
groupValue: _tempThemeValue,
onChanged: (ThemeType? value) {
setState(() {
_tempThemeValue = i;
});
},
),
]
],
);
}),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context).colorScheme.outline),
)),
TextButton(
onPressed: () {
settingController.themeType.value = _tempThemeValue;
setting.put(
SettingBoxKey.themeMode, _tempThemeValue.code);
Get.forceAppUpdate();
Get.back();
},
child: const Text('确定'))
],
);
}, },
); );
if (result != null) {
_tempThemeValue = result;
settingController.themeType.value = result;
setting.put(
SettingBoxKey.themeMode, result.code);
Get.forceAppUpdate();
}
}, },
title: Text('主题模式', style: titleStyle), title: Text('主题模式', style: titleStyle),
subtitle: Obx(() => Text( subtitle: Obx(() => Text(
'当前模式:${settingController.themeType.value.description}', '当前模式:${settingController.themeType.value.description}',
style: subTitleStyle)), style: subTitleStyle)),
trailing: const Icon(Icons.arrow_right_alt_outlined),
), ),
ListTile( ListTile(
dense: false, dense: false,
onTap: () => Get.toNamed('/colorSetting'), onTap: () => Get.toNamed('/colorSetting'),
title: Text('应用主题', style: titleStyle), title: Text('应用主题', style: titleStyle),
trailing: const Icon(Icons.arrow_forward_ios, size: 17), subtitle: Obx(() => Text(
'当前主题:${colorSelectController.type.value == 0 ? '动态取色': '指定颜色'}',
style: subTitleStyle)),
), ),
ListTile( ListTile(
dense: false, dense: false,
onTap: () => Get.toNamed('/fontSizeSetting'), onTap: () => Get.toNamed('/fontSizeSetting'),
title: Text('字体大小', style: titleStyle), title: Text('字体大小', style: titleStyle),
trailing: const Icon(Icons.arrow_forward_ios, size: 17),
), ),
if (Platform.isAndroid) if (Platform.isAndroid)
ListTile( ListTile(
dense: false, dense: false,
onTap: () => Get.toNamed('/displayModeSetting'), onTap: () => Get.toNamed('/displayModeSetting'),
title: Text('屏幕帧率', style: titleStyle), title: Text('屏幕帧率', style: titleStyle),
trailing: const Icon(Icons.arrow_forward_ios, size: 17),
) )
], ],
), ),

View File

@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import 'package:pilipala/models/common/theme_type.dart';
class SelectDialog<T> extends StatefulWidget {
final T value;
final String title;
final List<dynamic> values;
const SelectDialog({super.key, required this.value, required this.values, required this.title});
@override
_SelectDialogState<T> createState() => _SelectDialogState<T>();
}
class _SelectDialogState<T> extends State<SelectDialog<T>> {
late T _tempValue;
@override
void initState() {
super.initState();
_tempValue = widget.value;
}
@override
Widget build(BuildContext context) {
TextStyle titleStyle = Theme.of(context).textTheme.titleMedium!;
return AlertDialog(
title: Text(widget.title),
contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 12),
content: StatefulBuilder(
builder: (context, StateSetter setState) {
return SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
for (var i in widget.values) ...[
RadioListTile(
value: i['value'],
title: Text(i['title'], style: titleStyle),
groupValue: _tempValue,
onChanged: (value) {
print(value);
setState(() {
_tempValue = value as T;
});
},
),
]
],
),
);
}),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context).colorScheme.outline),
)),
TextButton(
onPressed: () => Navigator.pop(context, _tempValue),
child: const Text('确定'))
],
);
}
}

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
@ -9,6 +10,7 @@ class SetSwitchItem extends StatefulWidget {
final String? setKey; final String? setKey;
final bool? defaultVal; final bool? defaultVal;
final Function? callFn; final Function? callFn;
final bool? needReboot;
const SetSwitchItem({ const SetSwitchItem({
this.title, this.title,
@ -16,6 +18,7 @@ class SetSwitchItem extends StatefulWidget {
this.setKey, this.setKey,
this.defaultVal, this.defaultVal,
this.callFn, this.callFn,
this.needReboot,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@ -43,6 +46,9 @@ class _SetSwitchItemState extends State<SetSwitchItem> {
if (widget.callFn != null) { if (widget.callFn != null) {
widget.callFn!.call(val); widget.callFn!.call(val);
} }
if (widget.needReboot != null && widget.needReboot!) {
SmartDialog.showToast('重启生效');
}
setState(() {}); setState(() {});
} }
@ -61,6 +67,7 @@ class _SetSwitchItemState extends State<SetSwitchItem> {
? Text(widget.subTitle!, style: subTitleStyle) ? Text(widget.subTitle!, style: subTitleStyle)
: null, : null,
trailing: Transform.scale( trailing: Transform.scale(
alignment: Alignment.centerRight, // 缩放Switch的大小后保持右侧对齐, 避免右侧空隙过大
scale: 0.8, scale: 0.8,
child: Switch( child: Switch(
thumbIcon: MaterialStateProperty.resolveWith<Icon?>( thumbIcon: MaterialStateProperty.resolveWith<Icon?>(

View File

@ -138,8 +138,8 @@ class VideoDetailController extends GetxController
} }
showReplyReplyPanel() { showReplyReplyPanel() {
PersistentBottomSheetController<void>? ctr = PersistentBottomSheetController? ctr =
scaffoldKey.currentState?.showBottomSheet<void>((BuildContext context) { scaffoldKey.currentState?.showBottomSheet((BuildContext context) {
return VideoReplyReplyPanel( return VideoReplyReplyPanel(
oid: oid, oid: oid,
rpid: fRpid, rpid: fRpid,

View File

@ -76,6 +76,7 @@ class VideoIntroController extends GetxController {
if (Get.arguments.containsKey('videoItem')) { if (Get.arguments.containsKey('videoItem')) {
preRender = true; preRender = true;
var args = Get.arguments['videoItem']; var args = Get.arguments['videoItem'];
var keys = Get.arguments.keys.toList();
videoItem!['pic'] = args.pic; videoItem!['pic'] = args.pic;
if (args.title is String) { if (args.title is String) {
videoItem!['title'] = args.title; videoItem!['title'] = args.title;
@ -86,11 +87,9 @@ class VideoIntroController extends GetxController {
} }
videoItem!['title'] = str; videoItem!['title'] = str;
} }
if (args.stat != null) { videoItem!['stat'] = keys.contains('stat') && args.stat;
videoItem!['stat'] = args.stat; videoItem!['pubdate'] = keys.contains('pubdate') && args.pubdate;
} videoItem!['owner'] = keys.contains('owner') && args.owner;
videoItem!['pubdate'] = args.pubdate;
videoItem!['owner'] = args.owner;
} }
} }
userLogin = userInfo != null; userLogin = userInfo != null;
@ -445,6 +444,7 @@ class VideoIntroController extends GetxController {
label: '设置分组', label: '设置分组',
onPressed: setFollowGroup, onPressed: setFollowGroup,
), ),
showCloseIcon: true,
), ),
); );
} }
@ -569,13 +569,7 @@ class VideoIntroController extends GetxController {
upMid: videoDetail.value.owner!.mid!, upMid: videoDetail.value.owner!.mid!,
); );
if (res['status']) { if (res['status']) {
if (res['data'].modelResult.resultType == 0) { modelResult = res['data'].modelResult;
SmartDialog.showToast('该视频不支持ai总结');
}
if (res['data'].modelResult.resultType == 2 ||
res['data'].modelResult.resultType == 1) {
modelResult = res['data'].modelResult;
}
} }
SmartDialog.dismiss(); SmartDialog.dismiss();
return res; return res;

View File

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@ -12,7 +14,6 @@ import 'package:pilipala/common/widgets/stat/view.dart';
import 'package:pilipala/models/video_detail_res.dart'; import 'package:pilipala/models/video_detail_res.dart';
import 'package:pilipala/pages/video/detail/introduction/controller.dart'; import 'package:pilipala/pages/video/detail/introduction/controller.dart';
import 'package:pilipala/pages/video/detail/widgets/ai_detail.dart'; import 'package:pilipala/pages/video/detail/widgets/ai_detail.dart';
import 'package:pilipala/services/service_locator.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
@ -134,7 +135,15 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
late final dynamic followStatus; late final dynamic followStatus;
late int mid; late int mid;
late String memberHeroTag; late String memberHeroTag;
late bool enableAi;
bool isProcessing = false;
void Function()? handleState(Future Function() action) {
return isProcessing ? null : () async {
setState(() => isProcessing = true);
await action();
setState(() => isProcessing = false);
};
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -150,6 +159,7 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
? '-' ? '-'
: Utils.numFormat(videoIntroController.userStat['follower']); : Utils.numFormat(videoIntroController.userStat['follower']);
followStatus = videoIntroController.followStatus; followStatus = videoIntroController.followStatus;
enableAi = setting.get(SettingBoxKey.enableAi, defaultValue: true);
} }
// 收藏 // 收藏
@ -247,7 +257,7 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: StyleString.safeSpace, right: StyleString.safeSpace, top: 10), left: StyleString.safeSpace, right: StyleString.safeSpace, top: 10),
sliver: SliverToBoxAdapter( sliver: SliverToBoxAdapter(
child: !loadingStatus || videoItem.isNotEmpty child: !loadingStatus
? Column( ? Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -277,7 +287,7 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
children: [ children: [
StatView( StatView(
theme: 'gray', theme: 'gray',
view: !widget.loadingStatus view: !loadingStatus
? widget.videoDetail!.stat!.view ? widget.videoDetail!.stat!.view
: videoItem['stat'].view, : videoItem['stat'].view,
size: 'medium', size: 'medium',
@ -285,7 +295,7 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
const SizedBox(width: 10), const SizedBox(width: 10),
StatDanMu( StatDanMu(
theme: 'gray', theme: 'gray',
danmu: !widget.loadingStatus danmu: !loadingStatus
? widget.videoDetail!.stat!.danmaku ? widget.videoDetail!.stat!.danmaku
: videoItem['stat'].danmaku, : videoItem['stat'].danmaku,
size: 'medium', size: 'medium',
@ -293,7 +303,7 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
const SizedBox(width: 10), const SizedBox(width: 10),
Text( Text(
Utils.dateFormat( Utils.dateFormat(
!widget.loadingStatus !loadingStatus
? widget.videoDetail!.pubdate ? widget.videoDetail!.pubdate
: videoItem['pubdate'], : videoItem['pubdate'],
formatType: 'detail'), formatType: 'detail'),
@ -317,23 +327,22 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
), ),
), ),
), ),
Positioned( if (enableAi)
right: 10, Positioned(
top: 6, right: 10,
child: GestureDetector( top: 6,
onTap: () async { child: GestureDetector(
var res = await videoIntroController.aiConclusion(); onTap: () async {
if (res['status']) { var res =
if (res['data'].modelResult.resultType == 2 || await videoIntroController.aiConclusion();
res['data'].modelResult.resultType == 1) { if (res['status']) {
showAiBottomSheet(); showAiBottomSheet();
} }
} },
}, child:
child: Image.asset('assets/images/ai.png', height: 22),
Image.asset('assets/images/ai.png', height: 22), ),
), )
)
], ],
), ),
// 点赞收藏转发 布局样式1 // 点赞收藏转发 布局样式1
@ -477,7 +486,7 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
() => ActionItem( () => ActionItem(
icon: const Icon(FontAwesomeIcons.thumbsUp), icon: const Icon(FontAwesomeIcons.thumbsUp),
selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp), selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp),
onTap: () => videoIntroController.actionLikeVideo(), onTap: handleState(videoIntroController.actionLikeVideo),
selectStatus: videoIntroController.hasLike.value, selectStatus: videoIntroController.hasLike.value,
loadingStatus: loadingStatus, loadingStatus: loadingStatus,
text: !loadingStatus text: !loadingStatus
@ -494,7 +503,7 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
() => ActionItem( () => ActionItem(
icon: const Icon(FontAwesomeIcons.b), icon: const Icon(FontAwesomeIcons.b),
selectIcon: const Icon(FontAwesomeIcons.b), selectIcon: const Icon(FontAwesomeIcons.b),
onTap: () => videoIntroController.actionCoinVideo(), onTap: handleState(videoIntroController.actionCoinVideo),
selectStatus: videoIntroController.hasCoin.value, selectStatus: videoIntroController.hasCoin.value,
loadingStatus: loadingStatus, loadingStatus: loadingStatus,
text: !loadingStatus text: !loadingStatus
@ -538,7 +547,7 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
Obx( Obx(
() => ActionRowItem( () => ActionRowItem(
icon: const Icon(FontAwesomeIcons.thumbsUp), icon: const Icon(FontAwesomeIcons.thumbsUp),
onTap: () => videoIntroController.actionLikeVideo(), onTap: handleState(videoIntroController.actionLikeVideo),
selectStatus: videoIntroController.hasLike.value, selectStatus: videoIntroController.hasLike.value,
loadingStatus: loadingStatus, loadingStatus: loadingStatus,
text: text:
@ -549,7 +558,7 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
Obx( Obx(
() => ActionRowItem( () => ActionRowItem(
icon: const Icon(FontAwesomeIcons.b), icon: const Icon(FontAwesomeIcons.b),
onTap: () => videoIntroController.actionCoinVideo(), onTap: handleState(videoIntroController.actionCoinVideo),
selectStatus: videoIntroController.hasCoin.value, selectStatus: videoIntroController.hasCoin.value,
loadingStatus: loadingStatus, loadingStatus: loadingStatus,
text: text:

View File

@ -27,6 +27,7 @@ class _PagesPanelState extends State<PagesPanel> {
late int currentIndex; late int currentIndex;
String heroTag = Get.arguments['heroTag']; String heroTag = Get.arguments['heroTag'];
late VideoDetailController _videoDetailController; late VideoDetailController _videoDetailController;
final ScrollController _scrollController = ScrollController();
@override @override
void initState() { void initState() {
@ -50,6 +51,12 @@ class _PagesPanelState extends State<PagesPanel> {
setState(() {}); setState(() {});
} }
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
@ -80,73 +87,92 @@ class _PagesPanelState extends State<PagesPanel> {
onPressed: () { onPressed: () {
showBottomSheet( showBottomSheet(
context: context, context: context,
builder: (_) => Container( builder: (BuildContext context) {
height: widget.sheetHeight, return StatefulBuilder(builder:
color: Theme.of(context).colorScheme.background, (BuildContext context, StateSetter setState) {
child: Column( WidgetsBinding.instance
children: [ .addPostFrameCallback((_) async {
Container( await Future.delayed(
height: 45, const Duration(milliseconds: 200));
padding: _scrollController.jumpTo(currentIndex * 56);
const EdgeInsets.only(left: 14, right: 14), });
child: Row( return Container(
mainAxisAlignment: height: widget.sheetHeight,
MainAxisAlignment.spaceBetween, color: Theme.of(context).colorScheme.background,
children: [ child: Column(
Text( children: [
'合集(${episodes.length}', Container(
style: height: 45,
Theme.of(context).textTheme.titleMedium, padding: const EdgeInsets.only(
left: 14, right: 14),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
'合集(${episodes.length}',
style: Theme.of(context)
.textTheme
.titleMedium,
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
),
],
), ),
IconButton( ),
icon: const Icon(Icons.close), Divider(
onPressed: () => Navigator.pop(context), height: 1,
), color: Theme.of(context)
], .dividerColor
), .withOpacity(0.1),
), ),
Divider( Expanded(
height: 1, child: Material(
color: Theme.of(context) child: ListView.builder(
.dividerColor controller: _scrollController,
.withOpacity(0.1), itemCount: episodes.length,
), itemBuilder: (context, index) {
Expanded( return ListTile(
child: Material( onTap: () {
child: ListView.builder( changeFucCall(
itemCount: episodes.length, episodes[index], index);
itemBuilder: (context, index) { Get.back();
return InkWell( },
onTap: () { dense: false,
changeFucCall(episodes[index], index); leading: index == currentIndex
Get.back(); ? Image.asset(
}, 'assets/images/live.gif',
child: Padding( color: Theme.of(context)
padding: const EdgeInsets.only( .colorScheme
top: 10, .primary,
bottom: 10, height: 12,
left: 15, )
right: 15), : null,
child: Text( title: Text(
episodes[index].pagePart!, episodes[index].pagePart!,
style: TextStyle( style: TextStyle(
fontSize: 14,
color: index == currentIndex color: index == currentIndex
? Theme.of(context) ? Theme.of(context)
.colorScheme .colorScheme
.primary .primary
: Theme.of(context) : Theme.of(context)
.colorScheme .colorScheme
.onSurface), .onSurface,
), ),
), ),
); );
}, },
),
),
), ),
), ],
), ),
], );
), });
), },
); );
}, },
child: Text( child: Text(

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