Merge branch 'main' into feature-liveRoomRender

This commit is contained in:
guozhigq
2024-11-24 21:34:08 +08:00
261 changed files with 13042 additions and 5846 deletions

View File

@ -12,7 +12,6 @@ on:
- ".idea/**" - ".idea/**"
- "!.github/workflows/**" - "!.github/workflows/**"
jobs: jobs:
update_version: update_version:
name: Read and update version name: Read and update version
@ -96,7 +95,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.16.5 flutter-version: 3.19.6
channel: any channel: any
- name: 下载项目依赖 - name: 下载项目依赖

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.16.5 flutter-version: 3.19.6
channel: any channel: any
- name: 下载项目依赖 - name: 下载项目依赖
@ -98,7 +98,7 @@ jobs:
uses: subosito/flutter-action@v2.10.0 uses: subosito/flutter-action@v2.10.0
with: with:
cache: true cache: true
flutter-version: 3.16.5 flutter-version: 3.19.6
- name: flutter build ipa - name: flutter build ipa
run: | run: |

View File

@ -12,7 +12,6 @@
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" /> <data android:scheme="https" />
</intent> </intent>
</queries> </queries>
<queries> <queries>
<intent> <intent>
@ -20,7 +19,6 @@
"android.support.customtabs.action.CustomTabsService" /> "android.support.customtabs.action.CustomTabsService" />
</intent> </intent>
</queries> </queries>
<queries> <queries>
<!-- If your app checks for http support --> <!-- If your app checks for http support -->
<intent> <intent>
@ -34,7 +32,6 @@
<data android:scheme="https" /> <data android:scheme="https" />
</intent> </intent>
</queries> </queries>
<application <application
android:label="PiliPala" android:label="PiliPala"
android:name="${applicationName}" android:name="${applicationName}"
@ -47,13 +44,14 @@
<activity <activity
android:name="com.guozhigq.pilipala.MainActivity" android:name="com.guozhigq.pilipala.MainActivity"
android:exported="true" android:exported="true"
android:launchMode="singleTop" android:launchMode="singleTask"
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize" android:windowSoftInputMode="adjustResize"
android:supportsPictureInPicture="true" android:supportsPictureInPicture="true"
android:resizeableActivity="true" android:resizeableActivity="true"
android:autoVerify="true"
> >
<!-- Specifies an Android theme to apply to this Activity as soon as <!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user the Android process has started. This theme is visible to the user
@ -63,10 +61,21 @@
android:name="io.flutter.embedding.android.NormalTheme" android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" android:resource="@style/NormalTheme"
/> />
<meta-data android:name="flutter_deeplinking_enabled" android:value="false" />
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
<!-- Deep Link -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.SEARCH" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="pili"/>
<data android:scheme="pilipala"/>
</intent-filter>
<!-- App Link -->
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.SEARCH" /> <action android:name="android.intent.action.SEARCH" />
@ -132,102 +141,55 @@
<data android:scheme="bilibili" android:host="assistant" /> <data android:scheme="bilibili" android:host="assistant" />
<data android:scheme="bilibili" android:host="feedback" /> <data android:scheme="bilibili" android:host="feedback" />
<data android:scheme="bilibili" android:host="auth" android:path="/launch" /> <data android:scheme="bilibili" android:host="auth" android:path="/launch" />
<data android:scheme="http" android:host="live.bilibili.com"
android:pathPattern="/live/.*" />
<data android:scheme="https" android:host="live.bilibili.com" <data android:scheme="https" android:host="live.bilibili.com"
android:pathPattern="/live/.*" /> android:pathPattern="/live/.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/video/.*" />
<data android:scheme="https" android:host="www.bilibili.com" <data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/video/.*" /> android:pathPattern="/video/.*" />
<data android:scheme="http" android:host="www.bilibili.tv"
android:pathPattern="/video/.*" />
<data android:scheme="https" android:host="www.bilibili.tv" <data android:scheme="https" android:host="www.bilibili.tv"
android:pathPattern="/video/.*" /> android:pathPattern="/video/.*" />
<data android:scheme="http" android:host="www.bilibili.cn"
android:pathPattern="/video/.*" />
<data android:scheme="https" android:host="www.bilibili.cn" <data android:scheme="https" android:host="www.bilibili.cn"
android:pathPattern="/video/.*" /> android:pathPattern="/video/.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/mobile/video/.*" />
<data android:scheme="https" android:host="www.bilibili.com" <data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/mobile/video/.*" /> android:pathPattern="/mobile/video/.*" />
<data android:scheme="http" android:host="m.bilibili.com"
android:pathPattern="/video/.*" />
<data android:scheme="https" android:host="m.bilibili.com" <data android:scheme="https" android:host="m.bilibili.com"
android:pathPattern="/video/.*" /> android:pathPattern="/video/.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/story/.*" />
<data android:scheme="https" android:host="www.bilibili.com" <data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/story/.*" /> android:pathPattern="/story/.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/bangumi/i/.*" />
<data android:scheme="https" android:host="www.bilibili.com" <data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/bangumi/i/.*" /> android:pathPattern="/bangumi/i/.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/mobile/bangumi/i/.*" />
<data android:scheme="https" android:host="www.bilibili.com" <data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/mobile/bangumi/i/.*" /> android:pathPattern="/mobile/bangumi/i/.*" />
<data android:scheme="http" android:host="bangumi.bilibili.com"
android:pathPattern="/.*" />
<data android:scheme="https" android:host="bangumi.bilibili.com" <data android:scheme="https" android:host="bangumi.bilibili.com"
android:pathPattern="/.*" /> android:pathPattern="/.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/bangumi/.*" />
<data android:scheme="https" android:host="www.bilibili.com" <data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/bangumi/.*" /> android:pathPattern="/bangumi/.*" />
<data android:scheme="http" android:host="m.bilibili.com"
android:pathPattern="/bangumi/.*" />
<data android:scheme="https" android:host="m.bilibili.com" <data android:scheme="https" android:host="m.bilibili.com"
android:pathPattern="/bangumi/.*" /> android:pathPattern="/bangumi/.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/cheese/play/ss.*" />
<data android:scheme="https" android:host="www.bilibili.com" <data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/cheese/play/ss.*" /> android:pathPattern="/cheese/play/ss.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/cheese/play/ep.*" />
<data android:scheme="https" android:host="www.bilibili.com" <data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/cheese/play/ep.*" /> android:pathPattern="/cheese/play/ep.*" />
<data android:scheme="http" android:host="m.bilibili.com"
android:pathPattern="/bangumi/play/ss.*" />
<data android:scheme="https" android:host="m.bilibili.com" <data android:scheme="https" android:host="m.bilibili.com"
android:pathPattern="/cheese/play/ss.*" /> android:pathPattern="/cheese/play/ss.*" />
<data android:scheme="http" android:host="m.bilibili.com"
android:pathPattern="/cheese/play/ep.*" />
<data android:scheme="https" android:host="m.bilibili.com" <data android:scheme="https" android:host="m.bilibili.com"
android:pathPattern="/cheese/play/ep.*" /> android:pathPattern="/cheese/play/ep.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/read/cv.*" />
<data android:scheme="https" android:host="www.bilibili.com" <data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/read/cv.*" /> android:pathPattern="/read/cv.*" />
<data android:scheme="http" android:host="www.bilibili.com" android:path="/review/" />
<data android:scheme="https" android:host="www.bilibili.com" android:path="/review/" /> <data android:scheme="https" android:host="www.bilibili.com" android:path="/review/" />
<data android:scheme="http" android:host="bilibili.cn"
android:pathPattern="/video/.*" />
<data android:scheme="https" android:host="bilibili.cn" <data android:scheme="https" android:host="bilibili.cn"
android:pathPattern="/video/.*" /> android:pathPattern="/video/.*" />
<data android:scheme="http" android:host="bilibili.com"
android:pathPattern="/video/.*" />
<data android:scheme="https" android:host="bilibili.com" <data android:scheme="https" android:host="bilibili.com"
android:pathPattern="/video/.*" /> android:pathPattern="/video/.*" />
<data android:scheme="http" android:host="www.bilibili.cn"
android:pathPattern="/video/.*" />
<data android:scheme="https" android:host="www.bilibili.cn" <data android:scheme="https" android:host="www.bilibili.cn"
android:pathPattern="/video/.*" /> android:pathPattern="/video/.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/video/.*" />
<data android:scheme="https" android:host="www.bilibili.com" <data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/video/.*" /> android:pathPattern="/video/.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/mobile/video/.*" />
<data android:scheme="https" android:host="www.bilibili.com" <data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/mobile/video/.*" /> android:pathPattern="/mobile/video/.*" />
<data android:scheme="https" android:host="b23.tv" <data android:scheme="https" android:host="b23.tv"
android:pathPattern="/*" /> android:pathPattern="/*" />
<data android:scheme="https" android:host="space.bilibili.com" <data android:scheme="https" android:host="space.bilibili.com"
android:pathPattern="/*" /> android:pathPattern="/*" />
</intent-filter> </intent-filter>
</activity> </activity>
<service <service
@ -239,7 +201,6 @@
<action android:name="android.media.browse.MediaBrowserService" /> <action android:name="android.media.browse.MediaBrowserService" />
</intent-filter> </intent-filter>
</service> </service>
<receiver <receiver
android:name="com.ryanheise.audioservice.MediaButtonReceiver" android:name="com.ryanheise.audioservice.MediaButtonReceiver"
android:exported="true" android:exported="true"
@ -247,7 +208,7 @@
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" /> <action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data <meta-data
@ -269,4 +230,4 @@
--> -->
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" /> <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /> <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
</manifest> </manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

39
change_log/1.0.25.1010.md Normal file
View File

@ -0,0 +1,39 @@
## 1.0.25
### 功能
+ 直播弹幕
+ 稍后再看、收藏夹播放全部
+ 收藏夹新建、编辑
+ 评论删除
+ 评论保存为图片
+ 动态页滑动切换up
+ up投稿筛选充电视频
+ 直播tab展示关注up
+ up主页专栏展示
### 优化
+ 视频详情页一键三连
+ 动态页标识充电视频
+ 播放器亮度、音量调整百分比展示
+ 封面预览时视频标题可复制
+ 竖屏直播布局
+ 图片预览
+ 专栏渲染优化
+ 私信图片查看
### 修复
+ 收藏夹点击异常
+ 搜索up异常
+ 系统通知已读异常
+ [赞了我的]展示错误
+ 部分up合集无法打开
+ 切换合集视频投币个数未重置
+ 搜索条件筛选面板无法滚动
+ 部分机型导航条未沉浸
+ 专栏图片渲染问题
+ 专栏浏览历史记录
+ 直播间历史记录
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

View File

@ -1,5 +1,5 @@
PODS: PODS:
- appscheme (1.0.4): - app_links (0.0.2):
- Flutter - Flutter
- audio_service (0.0.1): - audio_service (0.0.1):
- Flutter - Flutter
@ -27,6 +27,8 @@ PODS:
- Flutter - Flutter
- GT3Captcha-iOS - GT3Captcha-iOS
- GT3Captcha-iOS (0.15.8.3) - GT3Captcha-iOS (0.15.8.3)
- image_picker_ios (0.0.1):
- Flutter
- media_kit_libs_ios_video (1.0.4): - media_kit_libs_ios_video (1.0.4):
- Flutter - Flutter
- media_kit_native_event_loop (1.0.0): - media_kit_native_event_loop (1.0.0):
@ -66,7 +68,7 @@ PODS:
- Flutter - Flutter
DEPENDENCIES: DEPENDENCIES:
- appscheme (from `.symlinks/plugins/appscheme/ios`) - app_links (from `.symlinks/plugins/app_links/ios`)
- audio_service (from `.symlinks/plugins/audio_service/ios`) - audio_service (from `.symlinks/plugins/audio_service/ios`)
- audio_session (from `.symlinks/plugins/audio_session/ios`) - audio_session (from `.symlinks/plugins/audio_session/ios`)
- auto_orientation (from `.symlinks/plugins/auto_orientation/ios`) - auto_orientation (from `.symlinks/plugins/auto_orientation/ios`)
@ -77,6 +79,7 @@ DEPENDENCIES:
- flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`) - flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- gt3_flutter_plugin (from `.symlinks/plugins/gt3_flutter_plugin/ios`) - gt3_flutter_plugin (from `.symlinks/plugins/gt3_flutter_plugin/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`) - media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`) - media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`) - media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
@ -102,8 +105,8 @@ SPEC REPOS:
- Toast - Toast
EXTERNAL SOURCES: EXTERNAL SOURCES:
appscheme: app_links:
:path: ".symlinks/plugins/appscheme/ios" :path: ".symlinks/plugins/app_links/ios"
audio_service: audio_service:
:path: ".symlinks/plugins/audio_service/ios" :path: ".symlinks/plugins/audio_service/ios"
audio_session: audio_session:
@ -124,6 +127,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/fluttertoast/ios" :path: ".symlinks/plugins/fluttertoast/ios"
gt3_flutter_plugin: gt3_flutter_plugin:
:path: ".symlinks/plugins/gt3_flutter_plugin/ios" :path: ".symlinks/plugins/gt3_flutter_plugin/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
media_kit_libs_ios_video: media_kit_libs_ios_video:
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios" :path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
media_kit_native_event_loop: media_kit_native_event_loop:
@ -160,7 +165,7 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios" :path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
SPEC CHECKSUMS: SPEC CHECKSUMS:
appscheme: b1c3f8862331cb20430cf9e0e4af85dbc1572ad8 app_links: e7a6750a915a9e161c58d91bc610e8cd1d4d0ad0
audio_service: f509d65da41b9521a61f1c404dd58651f265a567 audio_service: f509d65da41b9521a61f1c404dd58651f265a567
audio_session: 4f3e461722055d21515cf3261b64c973c062f345 audio_session: 4f3e461722055d21515cf3261b64c973c062f345
auto_orientation: 102ed811a5938d52c86520ddd7ecd3a126b5d39d auto_orientation: 102ed811a5938d52c86520ddd7ecd3a126b5d39d
@ -173,6 +178,7 @@ SPEC CHECKSUMS:
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
gt3_flutter_plugin: 5bd2c08d3c19cbb6ee3b08f4358439e54c8ab2ee gt3_flutter_plugin: 5bd2c08d3c19cbb6ee3b08f4358439e54c8ab2ee
GT3Captcha-iOS: 5e3b1077834d8a9d6f4d64a447a30af3e14affe6 GT3Captcha-iOS: 5e3b1077834d8a9d6f4d64a447a30af3e14affe6
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
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

View File

@ -65,44 +65,29 @@
<array> <array>
<dict> <dict>
<key>CFBundleURLName</key> <key>CFBundleURLName</key>
<string></string> <string>bilibili</string>
<key>CFBundleURLSchemes</key> <key>CFBundleURLSchemes</key>
<array> <array>
<string>http</string> <string>http</string>
<string>https</string> <string>https</string>
</array>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string></string>
<key>CFBundleURLSchemes</key>
<array>
<string>m.bilibili.com</string>
<string>bilibili.com</string>
<string>www.bilibili.com</string>
<string>bangumi.bilibili.com</string>
<string>bilibili.cn</string>
<string>www.bilibili.cn</string>
<string>bangumi.bilibili.cn</string>
<string>bilibili.tv</string>
<string>www.bilibili.tv</string>
<string>bangumi.bilibili.tv</string>
<string>miniapp.bilibili.com</string>
<string>live.bilibili.com</string>
</array>
</dict>
</array>
</dict>
<!-- 当其他应用程序或系统通过 bilibili -->
<dict>
<key>CFBundleURLName</key>
<string>bilibili</string>
<key>CFBundleURLSchemes</key>
<array>
<string>bilibili</string> <string>bilibili</string>
<string>m.bilibili.com</string>
<string>bilibili.com</string>
<string>www.bilibili.com</string>
<string>bangumi.bilibili.com</string>
<string>bilibili.cn</string>
<string>www.bilibili.cn</string>
<string>bangumi.bilibili.cn</string>
<string>bilibili.tv</string>
<string>www.bilibili.tv</string>
<string>bangumi.bilibili.tv</string>
<string>miniapp.bilibili.com</string>
<string>live.bilibili.com</string>
<string>pili</string>
<string>pilipala</string>
</array> </array>
<key>FlutterDeepLinkingEnabled</key>
<false/>
</dict> </dict>
</array> </array>
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>

View File

@ -15,6 +15,4 @@ class Constants {
// 59b43e04ad6965f34319062b478f83dd TV端 // 59b43e04ad6965f34319062b478f83dd TV端
static const String appSec = '59b43e04ad6965f34319062b478f83dd'; static const String appSec = '59b43e04ad6965f34319062b478f83dd';
static const String thirdSign = '04224646d1fea004e79606d3b038c84a'; static const String thirdSign = '04224646d1fea004e79606d3b038c84a';
static const String thirdApi =
'https://www.mcbbs.net/template/mcbbs/image/special_photo_bg.png';
} }

View File

@ -1,35 +1,462 @@
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: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:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:pilipala/http/video.dart';
import 'package:pilipala/models/video_detail_res.dart';
import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/utils/utils.dart';
import 'package:scrollview_observer/scrollview_observer.dart';
import '../models/common/video_episode_type.dart'; import '../models/common/video_episode_type.dart';
import 'widgets/badge.dart';
import 'widgets/stat/danmu.dart';
import 'widgets/stat/view.dart';
class EpisodeBottomSheet { class EpisodeBottomSheet {
final List<dynamic> episodes; final List<dynamic> episodes;
final int currentCid; final int currentCid;
final dynamic dataType; final dynamic dataType;
final BuildContext context;
final Function changeFucCall; final Function changeFucCall;
final int? cid; final int? cid;
final double? sheetHeight; final double? sheetHeight;
bool isFullScreen = false; bool isFullScreen = false;
final UgcSeason? ugcSeason;
final int? currentEpisodeIndex;
final int? currentIndex;
EpisodeBottomSheet({ EpisodeBottomSheet({
required this.episodes, required this.episodes,
required this.currentCid, required this.currentCid,
required this.dataType, required this.dataType,
required this.context,
required this.changeFucCall, required this.changeFucCall,
this.cid, this.cid,
this.sheetHeight, this.sheetHeight,
this.isFullScreen = false, this.isFullScreen = false,
this.ugcSeason,
this.currentEpisodeIndex,
this.currentIndex,
}); });
Widget buildEpisodeListItem( Widget buildShowContent() {
dynamic episode, return PagesBottomSheet(
int index, episodes: episodes,
bool isCurrentIndex, currentCid: currentCid,
) { dataType: dataType,
changeFucCall: changeFucCall,
cid: cid,
sheetHeight: sheetHeight,
isFullScreen: isFullScreen,
ugcSeason: ugcSeason,
currentEpisodeIndex: currentEpisodeIndex,
currentIndex: currentIndex,
);
}
PersistentBottomSheetController show(BuildContext context) {
final PersistentBottomSheetController btmSheetCtr = showBottomSheet(
context: context,
builder: (BuildContext context) {
return buildShowContent();
},
);
return btmSheetCtr;
}
}
class PagesBottomSheet extends StatefulWidget {
const PagesBottomSheet({
super.key,
required this.episodes,
required this.currentCid,
required this.dataType,
required this.changeFucCall,
this.cid,
this.sheetHeight,
this.isFullScreen = false,
this.ugcSeason,
this.currentEpisodeIndex,
this.currentIndex,
});
final List<dynamic> episodes;
final int currentCid;
final dynamic dataType;
final Function changeFucCall;
final int? cid;
final double? sheetHeight;
final bool isFullScreen;
final UgcSeason? ugcSeason;
final int? currentEpisodeIndex;
final int? currentIndex;
@override
State<PagesBottomSheet> createState() => _PagesBottomSheetState();
}
class _PagesBottomSheetState extends State<PagesBottomSheet>
with TickerProviderStateMixin {
final ScrollController _listScrollController = ScrollController();
late ListObserverController _listObserverController;
final ScrollController _scrollController = ScrollController();
late int currentIndex;
TabController? tabController;
List<ListObserverController>? _listObserverControllerList;
List<ScrollController>? _listScrollControllerList;
final String heroTag = Get.arguments['heroTag'];
VideoDetailController? _videoDetailController;
RxInt isSubscribe = (-1).obs;
bool isVisible = false;
@override
void initState() {
super.initState();
currentIndex = widget.currentIndex ??
widget.episodes.indexWhere((dynamic e) => e.cid == widget.currentCid);
_scrollToInit();
_scrollPositionInit();
if (widget.dataType == VideoEpidoesType.videoEpisode) {
_videoDetailController = Get.find<VideoDetailController>(tag: heroTag);
_getSubscribeStatus();
}
}
String prefix() {
switch (widget.dataType) {
case VideoEpidoesType.videoEpisode:
return '选集';
case VideoEpidoesType.videoPart:
return '分集';
case VideoEpidoesType.bangumiEpisode:
return '选集';
}
return '选集';
}
// 滚动器初始化
void _scrollToInit() {
/// 单个
_listObserverController =
ListObserverController(controller: _listScrollController);
if (widget.dataType == VideoEpidoesType.videoEpisode &&
widget.ugcSeason?.sections != null &&
widget.ugcSeason!.sections!.length > 1) {
tabController = TabController(
length: widget.ugcSeason!.sections!.length,
vsync: this,
initialIndex: widget.currentEpisodeIndex ?? 0,
);
/// 多tab
_listScrollControllerList = List.generate(
widget.ugcSeason!.sections!.length,
(index) {
return ScrollController();
},
);
_listObserverControllerList = List.generate(
widget.ugcSeason!.sections!.length,
(index) {
return ListObserverController(
controller: _listScrollControllerList![index],
);
},
);
}
}
// 滚动器位置初始化
void _scrollPositionInit() {
if (widget.dataType == VideoEpidoesType.videoEpisode) {
// 单个 多tab
if (widget.ugcSeason?.sections != null) {
if (widget.ugcSeason!.sections!.length == 1) {
_listObserverController.initialIndexModel =
ObserverIndexPositionModel(
index: currentIndex,
isFixedHeight: true,
);
} else {
_listObserverControllerList![widget.currentEpisodeIndex!]
.initialIndexModel = ObserverIndexPositionModel(
index: currentIndex,
isFixedHeight: true,
);
}
}
}
WidgetsBinding.instance.addPostFrameCallback((_) {
if (widget.dataType != VideoEpidoesType.videoEpisode) {
double itemHeight = (widget.isFullScreen
? 400
: Get.size.width - 3 * StyleString.safeSpace) /
5.2;
double offset = ((currentIndex - 1) / 2).ceil() * itemHeight;
_scrollController.jumpTo(offset);
}
});
}
// 获取订阅状态
void _getSubscribeStatus() async {
var res =
await VideoHttp.getSubscribeStatus(bvid: _videoDetailController!.bvid);
if (res['status']) {
isSubscribe.value = res['data']['season_fav'] ? 1 : 0;
}
}
// 更改订阅状态
void _changeSubscribeStatus() async {
if (isSubscribe.value == -1) {
return;
}
dynamic result = await VideoHttp.seasonFav(
isFav: isSubscribe.value == 1,
seasonId: widget.ugcSeason!.id,
);
if (result['status']) {
SmartDialog.showToast(isSubscribe.value == 1 ? '取消订阅成功' : '订阅成功');
isSubscribe.value = isSubscribe.value == 1 ? 0 : 1;
} else {
SmartDialog.showToast(result['msg']);
}
}
// 更改展开状态
void _changeVisible() {
setState(() {
isVisible = !isVisible;
});
}
@override
void dispose() {
try {
_listObserverController.controller?.dispose();
_listScrollController.dispose();
for (var element in _listObserverControllerList!) {
element.controller?.dispose();
}
for (var element in _listScrollControllerList!) {
element.dispose();
}
} catch (_) {}
super.dispose();
}
@override
Widget build(BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return SizedBox(
height: widget.sheetHeight,
child: Column(
children: [
TitleBar(
title: '${prefix()}${widget.episodes.length}',
isFullScreen: widget.isFullScreen,
),
if (widget.ugcSeason != null) ...[
UgcSeasonBuild(
ugcSeason: widget.ugcSeason!,
isSubscribe: isSubscribe,
isVisible: isVisible,
changeFucCall: _changeSubscribeStatus,
changeVisible: _changeVisible,
),
],
Expanded(
child: Material(
child: widget.dataType == VideoEpidoesType.videoEpisode
? (widget.ugcSeason!.sections!.length == 1
? ListViewObserver(
controller: _listObserverController,
child: ListView.builder(
controller: _listScrollController,
itemCount: widget.episodes.length + 1,
itemBuilder: (BuildContext context, int index) {
bool isLastItem =
index == widget.episodes.length;
bool isCurrentIndex = currentIndex == index;
return isLastItem
? SizedBox(
height: MediaQuery.of(context)
.padding
.bottom +
20,
)
: EpisodeListItem(
episode: widget.episodes[index],
index: index,
isCurrentIndex: isCurrentIndex,
dataType: widget.dataType,
changeFucCall: widget.changeFucCall,
isFullScreen: widget.isFullScreen,
);
},
),
)
: buildTabBar())
: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0), // 设置左右间距为12
child: GridView.count(
controller: _scrollController,
crossAxisCount: 2,
crossAxisSpacing: StyleString.safeSpace,
childAspectRatio: 2.6,
children: List.generate(
widget.episodes.length,
(index) {
bool isCurrentIndex = currentIndex == index;
return EpisodeGridItem(
episode: widget.episodes[index],
index: index,
isCurrentIndex: isCurrentIndex,
dataType: widget.dataType,
changeFucCall: widget.changeFucCall,
isFullScreen: widget.isFullScreen,
);
},
),
),
),
),
),
],
),
);
});
}
Widget buildTabBar() {
return Column(
children: [
// 背景色
Container(
color: Theme.of(context).colorScheme.surface,
child: TabBar(
controller: tabController,
isScrollable: true,
indicatorSize: TabBarIndicatorSize.label,
tabAlignment: TabAlignment.start,
splashBorderRadius: BorderRadius.circular(4),
tabs: [
...widget.ugcSeason!.sections!.map((SectionItem section) {
return Tab(
text: section.title,
);
}).toList()
],
),
),
Expanded(
child: TabBarView(
controller: tabController,
children: [
...widget.ugcSeason!.sections!.map((SectionItem section) {
final int fIndex = widget.ugcSeason!.sections!.indexOf(section);
return ListViewObserver(
controller: _listObserverControllerList![fIndex],
child: ListView.builder(
controller: _listScrollControllerList![fIndex],
itemCount: section.episodes!.length + 1,
itemBuilder: (BuildContext context, int index) {
final bool isLastItem = index == section.episodes!.length;
return isLastItem
? SizedBox(
height:
MediaQuery.of(context).padding.bottom + 20,
)
: EpisodeListItem(
episode: section.episodes![index], // 调整索引
index: index, // 调整索引
isCurrentIndex: widget.currentCid ==
section.episodes![index].cid,
dataType: widget.dataType,
changeFucCall: widget.changeFucCall,
isFullScreen: widget.isFullScreen,
);
},
),
);
}).toList()
],
),
),
],
);
}
}
class TitleBar extends StatelessWidget {
final String title;
final bool isFullScreen;
const TitleBar({
Key? key,
required this.title,
required this.isFullScreen,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return AppBar(
toolbarHeight: 45,
automaticallyImplyLeading: false,
centerTitle: false,
elevation: 1,
scrolledUnderElevation: 1,
title: Padding(
padding: const EdgeInsets.only(left: 12),
child: Text(
title,
style: Theme.of(context).textTheme.titleMedium,
),
),
actions: !isFullScreen
? [
SizedBox(
width: 35,
height: 35,
child: IconButton(
icon: const Icon(Icons.close, size: 20),
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () => Navigator.pop(context),
),
),
const SizedBox(width: 8),
]
: null,
);
}
}
class EpisodeListItem extends StatelessWidget {
final dynamic episode;
final int index;
final bool isCurrentIndex;
final dynamic dataType;
final Function changeFucCall;
final bool isFullScreen;
const EpisodeListItem({
Key? key,
required this.episode,
required this.index,
required this.isCurrentIndex,
required this.dataType,
required this.changeFucCall,
required this.isFullScreen,
}) : super(key: key);
@override
Widget build(BuildContext context) {
Color primary = Theme.of(context).colorScheme.primary; Color primary = Theme.of(context).colorScheme.primary;
Color onSurface = Theme.of(context).colorScheme.onSurface; Color onSurface = Theme.of(context).colorScheme.onSurface;
@ -45,128 +472,365 @@ class EpisodeBottomSheet {
title = '${episode.title}${episode.longTitle!}'; title = '${episode.title}${episode.longTitle!}';
break; break;
} }
return isFullScreen || episode?.cover == null || episode?.cover == '' return isFullScreen || episode?.cover == null || episode?.cover == ''
? ListTile( ? _buildListTile(context, title, primary, onSurface)
onTap: () { : _buildInkWell(context, title, primary, onSurface);
SmartDialog.showToast('切换至「$title'); }
changeFucCall.call(episode, index);
}, Widget _buildListTile(
dense: false, BuildContext context, String title, Color primary, Color onSurface) {
leading: isCurrentIndex return ListTile(
? Image.asset( onTap: () {
'assets/images/live.gif', if (isCurrentIndex) {
color: primary, return;
height: 12, }
SmartDialog.showToast('切换至「$title');
changeFucCall.call(episode, index);
},
dense: false,
leading: isCurrentIndex
? Image.asset(
'assets/images/live.png',
color: primary,
height: 12,
)
: null,
title: Text(
title,
style: TextStyle(
fontSize: 14,
color: isCurrentIndex ? primary : onSurface,
),
),
);
}
Widget _buildInkWell(
BuildContext context, String title, Color primary, Color onSurface) {
return InkWell(
onTap: () {
if (isCurrentIndex) {
return;
}
SmartDialog.showToast('切换至「$title');
changeFucCall.call(episode, index);
},
child: Padding(
padding: const EdgeInsets.fromLTRB(
StyleString.safeSpace, 6, StyleString.safeSpace, 6),
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints boxConstraints) {
const double width = 160;
return Container(
constraints: const BoxConstraints(minHeight: 88),
height: width / StyleString.aspectRatio,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(
builder: (BuildContext context,
BoxConstraints boxConstraints) {
final double maxWidth = boxConstraints.maxWidth;
final double maxHeight = boxConstraints.maxHeight;
return Stack(
children: [
NetworkImgLayer(
src: episode?.cover ?? '',
width: maxWidth,
height: maxHeight,
),
if (episode.duration != 0)
PBadge(
text: Utils.timeFormat(episode.duration!),
right: 6.0,
bottom: 6.0,
type: 'gray',
),
],
);
},
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(10, 2, 6, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
episode.title as String,
textAlign: TextAlign.start,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: FontWeight.w500,
color: isCurrentIndex ? primary : onSurface,
),
),
const Spacer(),
if (dataType != VideoEpidoesType.videoPart) ...[
if (episode?.pubdate != null ||
episode.pubTime != null)
Text(
Utils.dateFormat(
episode?.pubdate ?? episode.pubTime),
style: TextStyle(
fontSize: 11,
color:
Theme.of(context).colorScheme.outline),
),
const SizedBox(height: 2),
if (episode.stat != null)
Row(
children: [
StatView(view: episode.stat.view),
const SizedBox(width: 8),
StatDanMu(danmu: episode.stat.danmaku),
const Spacer(),
],
),
const SizedBox(height: 4),
]
],
),
),
) )
: null, ],
title: Text(title, ),
style: TextStyle( );
fontSize: 14, },
color: isCurrentIndex ? primary : onSurface, ),
))) ),
: InkWell( );
}
}
class EpisodeGridItem extends StatelessWidget {
final dynamic episode;
final int index;
final bool isCurrentIndex;
final dynamic dataType;
final Function changeFucCall;
final bool isFullScreen;
const EpisodeGridItem({
Key? key,
required this.episode,
required this.index,
required this.isCurrentIndex,
required this.dataType,
required this.changeFucCall,
required this.isFullScreen,
}) : super(key: key);
@override
Widget build(BuildContext context) {
ColorScheme colorScheme = Theme.of(context).colorScheme;
TextStyle textStyle = TextStyle(
color: isCurrentIndex ? colorScheme.primary : colorScheme.onSurface,
fontSize: 14,
);
return Stack(
children: [
Container(
width: double.infinity,
margin: const EdgeInsets.only(top: StyleString.safeSpace),
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: isCurrentIndex
? colorScheme.primaryContainer.withOpacity(0.6)
: colorScheme.onInverseSurface.withOpacity(0.6),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isCurrentIndex
? colorScheme.primary.withOpacity(0.8)
: Colors.transparent,
width: 1,
),
),
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () { onTap: () {
SmartDialog.showToast('切换至「$title'); if (isCurrentIndex) {
return;
}
SmartDialog.showToast('切换至「${episode.title}');
changeFucCall.call(episode, index); changeFucCall.call(episode, index);
}, },
child: Padding( child: Padding(
padding: padding: const EdgeInsets.symmetric(horizontal: 12.0),
const EdgeInsets.only(left: 14, right: 14, top: 8, bottom: 8), child: Column(
child: Row( mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [ children: [
NetworkImgLayer( Text(
width: 130, height: 75, src: episode?.cover ?? ''), dataType == VideoEpidoesType.bangumiEpisode
const SizedBox(width: 10), ? '${index + 1}'
Expanded( : '${index + 1}p',
child: Text( style: textStyle),
title, const SizedBox(height: 1),
maxLines: 2, Text(
style: TextStyle( episode.title,
fontSize: 14, maxLines: 1,
color: isCurrentIndex ? primary : onSurface, overflow: TextOverflow.ellipsis,
), style: textStyle,
),
), ),
], ],
), ),
), ),
); ),
} ),
if (dataType == VideoEpidoesType.bangumiEpisode &&
Widget buildTitle() { episode.badge != '' &&
return AppBar( episode.badge != null)
toolbarHeight: 45, Positioned(
automaticallyImplyLeading: false, right: 8,
centerTitle: false, top: 18,
title: Text( child: Text(
'合集(${episodes.length}', episode.badge,
style: Theme.of(context).textTheme.titleMedium, style: const TextStyle(fontSize: 11, color: Color(0xFFFF6699)),
), ),
actions: !isFullScreen )
? [ ],
IconButton(
icon: const Icon(Icons.close, size: 20),
onPressed: () => Navigator.pop(context),
),
const SizedBox(width: 14),
]
: null,
); );
} }
}
Widget buildShowContent(BuildContext context) { class UgcSeasonBuild extends StatelessWidget {
final ItemScrollController itemScrollController = ItemScrollController(); final UgcSeason ugcSeason;
int currentIndex = episodes.indexWhere((dynamic e) => e.cid == currentCid); final RxInt isSubscribe;
return StatefulBuilder( final bool isVisible;
builder: (BuildContext context, StateSetter setState) { final Function changeFucCall;
WidgetsBinding.instance.addPostFrameCallback((_) { final Function changeVisible;
itemScrollController.jumpTo(index: currentIndex);
}); const UgcSeasonBuild({
return Container( Key? key,
height: sheetHeight, required this.ugcSeason,
color: Theme.of(context).colorScheme.surface, required this.isSubscribe,
child: Column( required this.isVisible,
children: [ required this.changeFucCall,
buildTitle(), required this.changeVisible,
Expanded( }) : super(key: key);
child: Material(
child: PageStorage( @override
bucket: PageStorageBucket(), Widget build(BuildContext context) {
child: ScrollablePositionedList.builder( final ThemeData theme = Theme.of(context);
itemScrollController: itemScrollController, final Color outline = theme.colorScheme.outline;
itemCount: episodes.length + 1, final Color surface = theme.colorScheme.surface;
itemBuilder: (BuildContext context, int index) { final Color primary = theme.colorScheme.primary;
bool isLastItem = index == episodes.length; final Color onPrimary = theme.colorScheme.onPrimary;
bool isCurrentIndex = currentIndex == index; final Color onInverseSurface = theme.colorScheme.onInverseSurface;
return isLastItem final TextStyle titleMedium = theme.textTheme.titleMedium!;
? SizedBox( final TextStyle labelMedium = theme.textTheme.labelMedium!;
height: final Color dividerColor = theme.dividerColor.withOpacity(0.1);
MediaQuery.of(context).padding.bottom + 20,
) return isVisible
: buildEpisodeListItem( ? Container(
episodes[index], padding: const EdgeInsets.fromLTRB(12, 0, 12, 0),
index, color: surface,
isCurrentIndex, child: Column(
); mainAxisAlignment: MainAxisAlignment.start,
}, crossAxisAlignment: CrossAxisAlignment.start,
children: [
Divider(height: 1, thickness: 1, color: dividerColor),
const SizedBox(height: 10),
Row(
children: [
Expanded(
child: Text(
'合集:${ugcSeason.title}',
style: titleMedium,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 10),
Obx(
() => isSubscribe.value == -1
? const SizedBox(height: 32)
: SizedBox(
height: 32,
child: FilledButton.tonal(
onPressed: () => changeFucCall.call(),
style: TextButton.styleFrom(
padding:
const EdgeInsets.only(left: 8, right: 8),
foregroundColor: isSubscribe.value == 1
? outline
: onPrimary,
backgroundColor: isSubscribe.value == 1
? onInverseSurface
: primary,
),
child:
Text(isSubscribe.value == 1 ? '已订阅' : '订阅'),
),
),
),
],
),
if (ugcSeason.intro != null && ugcSeason.intro != '') ...[
const SizedBox(height: 4),
Text(
ugcSeason.intro!,
style: TextStyle(color: outline, fontSize: 12),
), ),
],
const SizedBox(height: 4),
Text.rich(
TextSpan(
style: TextStyle(
fontSize: labelMedium.fontSize, color: outline),
children: [
TextSpan(
text: '${Utils.numFormat(ugcSeason.stat!.view)}播放'),
const TextSpan(text: ' · '),
TextSpan(
text:
'${Utils.numFormat(ugcSeason.stat!.danmaku)}弹幕'),
],
),
),
const SizedBox(height: 14),
Align(
alignment: Alignment.center,
child: Material(
color: surface,
child: InkWell(
onTap: () => changeVisible.call(),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 10, horizontal: 0),
child: Text(
'收起简介',
style: TextStyle(color: primary, fontSize: 12),
),
),
),
),
),
Divider(height: 1, thickness: 1, color: dividerColor),
],
),
)
: Align(
alignment: Alignment.center,
child: InkWell(
onTap: () => changeVisible.call(),
child: Padding(
padding:
const EdgeInsets.symmetric(vertical: 10, horizontal: 0),
child: Text(
'展开简介',
style: TextStyle(color: primary, fontSize: 12),
), ),
), ),
), ),
], );
),
);
});
}
/// The [BuildContext] of the widget that calls the bottom sheet.
PersistentBottomSheetController show(BuildContext context) {
final PersistentBottomSheetController btmSheetCtr = showBottomSheet(
context: context,
builder: (BuildContext context) {
return buildShowContent(context);
},
);
return btmSheetCtr;
} }
} }

View File

@ -3,14 +3,9 @@ import 'package:pilipala/common/constants.dart';
import 'skeleton.dart'; import 'skeleton.dart';
class MediaBangumiSkeleton extends StatefulWidget { class MediaBangumiSkeleton extends StatelessWidget {
const MediaBangumiSkeleton({super.key}); const MediaBangumiSkeleton({super.key});
@override
State<MediaBangumiSkeleton> createState() => _MediaBangumiSkeletonState();
}
class _MediaBangumiSkeletonState extends State<MediaBangumiSkeleton> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Color bgColor = Theme.of(context).colorScheme.onInverseSurface; Color bgColor = Theme.of(context).colorScheme.onInverseSurface;
@ -35,25 +30,25 @@ class _MediaBangumiSkeletonState extends State<MediaBangumiSkeleton> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container( Container(
color: Theme.of(context).colorScheme.onInverseSurface, color: bgColor,
width: 200, width: 200,
height: 20, height: 20,
margin: const EdgeInsets.only(bottom: 15), margin: const EdgeInsets.only(bottom: 15),
), ),
Container( Container(
color: Theme.of(context).colorScheme.onInverseSurface, color: bgColor,
width: 150, width: 150,
height: 13, height: 13,
margin: const EdgeInsets.only(bottom: 5), margin: const EdgeInsets.only(bottom: 5),
), ),
Container( Container(
color: Theme.of(context).colorScheme.onInverseSurface, color: bgColor,
width: 150, width: 150,
height: 13, height: 13,
margin: const EdgeInsets.only(bottom: 5), margin: const EdgeInsets.only(bottom: 5),
), ),
Container( Container(
color: Theme.of(context).colorScheme.onInverseSurface, color: bgColor,
width: 150, width: 150,
height: 13, height: 13,
), ),
@ -64,7 +59,7 @@ class _MediaBangumiSkeletonState extends State<MediaBangumiSkeleton> {
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: borderRadius:
const BorderRadius.all(Radius.circular(20)), const BorderRadius.all(Radius.circular(20)),
color: Theme.of(context).colorScheme.onInverseSurface, color: bgColor,
), ),
), ),
], ],

View File

@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import '../constants.dart';
class UserListSkeleton extends StatelessWidget {
const UserListSkeleton({super.key});
@override
Widget build(BuildContext context) {
Color bgColor = Theme.of(context).colorScheme.onInverseSurface;
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: StyleString.safeSpace, vertical: 7),
child: Row(
children: [
ClipOval(
child: Container(width: 42, height: 42, color: bgColor),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(color: bgColor, width: 60, height: 13),
const SizedBox(width: 10),
Container(color: bgColor, width: 40, height: 13),
],
),
const SizedBox(height: 6),
Container(
color: bgColor,
width: 100,
height: 13,
),
],
),
),
],
));
}
}

View File

@ -0,0 +1,126 @@
import 'package:flutter/material.dart';
import '../constants.dart';
import 'skeleton.dart';
class VideoIntroSkeleton extends StatelessWidget {
const VideoIntroSkeleton({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
Color bgColor = Theme.of(context).colorScheme.onInverseSurface;
return Skeleton(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: StyleString.safeSpace),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 18),
Container(
width: double.infinity,
height: 20,
margin: const EdgeInsets.only(bottom: 6),
color: bgColor,
),
Container(
width: 220,
height: 20,
margin: const EdgeInsets.only(bottom: 12),
color: bgColor,
),
Row(
children: [
Container(
width: 45,
height: 14,
color: bgColor,
),
const SizedBox(width: 8),
Container(
width: 45,
height: 14,
color: bgColor,
),
const SizedBox(width: 8),
Container(
width: 45,
height: 14,
color: bgColor,
),
const Spacer(),
Container(
width: 35,
height: 14,
color: bgColor,
),
const SizedBox(width: 4),
],
),
const SizedBox(height: 30),
LayoutBuilder(builder: (context, constraints) {
// 并列5个正方形
double width = (constraints.maxWidth - 30) / 5;
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: List.generate(5, (index) {
return Container(
width: width - 24,
height: width - 24,
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(16),
),
);
}),
);
}),
const SizedBox(height: 20),
Container(
width: double.infinity,
height: 30,
margin: const EdgeInsets.symmetric(horizontal: 6),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(8),
),
),
const SizedBox(height: 20),
Row(
children: [
ClipOval(
child: Container(
width: 44,
height: 44,
color: bgColor,
),
),
const SizedBox(width: 12),
Container(
width: 50,
height: 14,
color: bgColor,
),
const SizedBox(width: 8),
Container(
width: 35,
height: 14,
color: bgColor,
),
const Spacer(),
Container(
width: 55,
height: 30,
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(16),
),
),
const SizedBox(width: 2)
],
),
const SizedBox(height: 10),
],
),
),
);
}
}

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
Box<dynamic> setting = GStrorage.setting; Box<dynamic> setting = GStorage.setting;
class CustomToast extends StatelessWidget { class CustomToast extends StatelessWidget {
const CustomToast({super.key, required this.msg}); const CustomToast({super.key, required this.msg});

View File

@ -1,7 +1,9 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_html/flutter_html.dart';
import 'package:get/get.dart'; import 'package:pilipala/plugin/pl_gallery/hero_dialog_route.dart';
import 'network_img_layer.dart'; import 'package:pilipala/plugin/pl_gallery/interactiveviewer_gallery.dart';
import 'package:pilipala/utils/highlight.dart';
// ignore: must_be_immutable // ignore: must_be_immutable
class HtmlRender extends StatelessWidget { class HtmlRender extends StatelessWidget {
@ -22,6 +24,20 @@ class HtmlRender extends StatelessWidget {
data: htmlContent, data: htmlContent,
onLinkTap: (String? url, Map<String, String> buildContext, attributes) {}, onLinkTap: (String? url, Map<String, String> buildContext, attributes) {},
extensions: [ extensions: [
TagExtension(
tagsToExtend: <String>{'pre'},
builder: (ExtensionContext extensionContext) {
final Map<String, dynamic> attributes = extensionContext.attributes;
final String lang = attributes['data-lang'] as String;
final String code = attributes['codecontent'] as String;
List<String> selectedLanguages = [lang.split('@').first];
TextSpan? result = highlightExistingText(code, selectedLanguages);
if (result == null) {
return const Center(child: Text('代码块渲染失败'));
}
return SelectableText.rich(result);
},
),
TagExtension( TagExtension(
tagsToExtend: <String>{'img'}, tagsToExtend: <String>{'img'},
builder: (ExtensionContext extensionContext) { builder: (ExtensionContext extensionContext) {
@ -44,20 +60,52 @@ class HtmlRender extends StatelessWidget {
if (isMall) { if (isMall) {
return const SizedBox(); return const SizedBox();
} }
// bool inTable = return InkWell(
// extensionContext.element!.previousElementSibling == null || onTap: () {
// extensionContext.element!.nextElementSibling == null; Navigator.of(context).push(
// imgUrl = Utils().imageUrl(imgUrl!); HeroDialogRoute<void>(
// return Image.network( builder: (BuildContext context) =>
// imgUrl, InteractiveviewerGallery(
// width: isEmote ? 22 : null, sources: imgList ?? [imgUrl],
// height: isEmote ? 22 : null, initIndex: imgList?.indexOf(imgUrl) ?? 0,
// ); itemBuilder: (
return NetworkImgLayer( BuildContext context,
width: isEmote ? 22 : Get.size.width - 24, int index,
height: isEmote ? 22 : 200, bool isFocus,
src: imgUrl, bool enablePageView,
) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (enablePageView) {
Navigator.of(context).pop();
}
},
child: Center(
child: Hero(
tag: imgList?[index] ?? imgUrl,
child: CachedNetworkImage(
fadeInDuration:
const Duration(milliseconds: 0),
imageUrl: imgList?[index] ?? imgUrl,
fit: BoxFit.contain,
),
),
),
);
},
onPageChanged: (int pageIndex) {},
),
),
);
},
child: CachedNetworkImage(imageUrl: imgUrl),
); );
// return NetworkImgLayer(
// width: isEmote ? 22 : Get.size.width - 24,
// height: isEmote ? 22 : 200,
// src: imgUrl,
// );
} catch (err) { } catch (err) {
return const SizedBox(); return const SizedBox();
} }
@ -66,7 +114,7 @@ class HtmlRender extends StatelessWidget {
], ],
style: { style: {
'html': Style( 'html': Style(
fontSize: FontSize.medium, fontSize: FontSize.large,
lineHeight: LineHeight.percent(140), lineHeight: LineHeight.percent(140),
), ),
'body': Style(margin: Margins.zero, padding: HtmlPaddings.zero), 'body': Style(margin: Margins.zero, padding: HtmlPaddings.zero),
@ -78,7 +126,7 @@ class HtmlRender extends StatelessWidget {
margin: Margins.only(bottom: 10), margin: Margins.only(bottom: 10),
), ),
'span': Style( 'span': Style(
fontSize: FontSize.medium, fontSize: FontSize.large,
height: Height(1.65), height: Height(1.65),
), ),
'div': Style(height: Height.auto()), 'div': Style(height: Height.auto()),

View File

@ -4,9 +4,10 @@ import 'package:flutter_svg/flutter_svg.dart';
class HttpError extends StatelessWidget { class HttpError extends StatelessWidget {
const HttpError({ const HttpError({
required this.errMsg, required this.errMsg,
required this.fn, this.fn,
this.btnText, this.btnText,
this.isShowBtn = true, this.isShowBtn = true,
this.isInSliver = true,
super.key, super.key,
}); });
@ -14,46 +15,41 @@ class HttpError extends StatelessWidget {
final Function()? fn; final Function()? fn;
final String? btnText; final String? btnText;
final bool isShowBtn; final bool isShowBtn;
final bool isInSliver;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SliverToBoxAdapter( Color primary = Theme.of(context).colorScheme.primary;
child: SizedBox( final errorContent = SizedBox(
height: 400, height: 400,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center, children: [
children: [ SvgPicture.asset("assets/images/error.svg", height: 200),
SvgPicture.asset( const SizedBox(height: 30),
"assets/images/error.svg", Text(
height: 200, errMsg ?? '请求异常',
), textAlign: TextAlign.center,
const SizedBox(height: 30), style: Theme.of(context).textTheme.titleSmall,
Text( ),
errMsg ?? '请求异常', const SizedBox(height: 20),
textAlign: TextAlign.center, if (isShowBtn)
style: Theme.of(context).textTheme.titleSmall, FilledButton.tonal(
), onPressed: () => fn?.call(),
const SizedBox(height: 20), style: ButtonStyle(
if (isShowBtn) backgroundColor: MaterialStateProperty.resolveWith((states) {
FilledButton.tonal( return primary.withAlpha(20);
onPressed: () { }),
fn!();
},
style: ButtonStyle(
backgroundColor: MaterialStateProperty.resolveWith((states) {
return Theme.of(context).colorScheme.primary.withAlpha(20);
}),
),
child: Text(
btnText ?? '点击重试',
style:
TextStyle(color: Theme.of(context).colorScheme.primary),
),
), ),
], child: Text(btnText ?? '点击重试', style: TextStyle(color: primary)),
), ),
],
), ),
); );
if (isInSliver) {
return SliverToBoxAdapter(child: errorContent);
} else {
return Align(alignment: Alignment.topCenter, child: errorContent);
}
} }
} }

View File

@ -6,7 +6,7 @@ import 'package:pilipala/utils/global_data_cache.dart';
import '../../utils/storage.dart'; import '../../utils/storage.dart';
import '../constants.dart'; import '../constants.dart';
Box<dynamic> setting = GStrorage.setting; Box<dynamic> setting = GStorage.setting;
class NetworkImgLayer extends StatelessWidget { class NetworkImgLayer extends StatelessWidget {
const NetworkImgLayer({ const NetworkImgLayer({
@ -20,6 +20,7 @@ class NetworkImgLayer extends StatelessWidget {
// 图片质量 默认1% // 图片质量 默认1%
this.quality, this.quality,
this.origAspectRatio, this.origAspectRatio,
this.radius,
}); });
final String? src; final String? src;
@ -30,10 +31,26 @@ class NetworkImgLayer extends StatelessWidget {
final Duration? fadeInDuration; final Duration? fadeInDuration;
final int? quality; final int? quality;
final double? origAspectRatio; final double? origAspectRatio;
final double? radius;
BorderRadius getBorderRadius(String? type, double? radius) {
return BorderRadius.circular(
radius ??
(type == 'avatar'
? 50
: type == 'emote'
? 0
: StyleString.imgRadius.x),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final int defaultImgQuality = GlobalDataCache().imgQuality; int defaultImgQuality = 10;
try {
defaultImgQuality = GlobalDataCache.imgQuality;
} catch (_) {}
if (src == '' || src == null) { if (src == '' || src == null) {
return placeholder(context); return placeholder(context);
} }
@ -68,13 +85,7 @@ class NetworkImgLayer extends StatelessWidget {
return src != '' && src != null return src != '' && src != null
? ClipRRect( ? ClipRRect(
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
borderRadius: BorderRadius.circular( borderRadius: getBorderRadius(type, radius),
type == 'avatar'
? 50
: type == 'emote'
? 0
: StyleString.imgRadius.x,
),
child: CachedNetworkImage( child: CachedNetworkImage(
imageUrl: imageUrl, imageUrl: imageUrl,
width: width, width: width,
@ -103,11 +114,7 @@ class NetworkImgLayer extends StatelessWidget {
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.onInverseSurface.withOpacity(0.4), color: Theme.of(context).colorScheme.onInverseSurface.withOpacity(0.4),
borderRadius: BorderRadius.circular(type == 'avatar' borderRadius: getBorderRadius(type, radius),
? 50
: type == 'emote'
? 0
: StyleString.imgRadius.x),
), ),
child: type == 'bg' child: type == 'bg'
? const SizedBox() ? const SizedBox()

View File

@ -60,17 +60,13 @@ class VideoCardV extends StatelessWidget {
// 动态 // 动态
case 'picture': case 'picture':
try { try {
String dynamicType = 'picture';
String uri = videoItem.uri; String uri = videoItem.uri;
String id = '';
if (videoItem.uri.startsWith('bilibili://article/')) { if (videoItem.uri.startsWith('bilibili://article/')) {
// https://www.bilibili.com/read/cv27063554 // https://www.bilibili.com/read/cv27063554
dynamicType = 'read';
RegExp regex = RegExp(r'\d+'); RegExp regex = RegExp(r'\d+');
Match match = regex.firstMatch(videoItem.uri)!; Match match = regex.firstMatch(videoItem.uri)!;
String matchedNumber = match.group(0)!; String matchedNumber = match.group(0)!;
videoItem.param = int.parse(matchedNumber); videoItem.param = int.parse(matchedNumber);
id = 'cv${videoItem.param}';
} }
if (uri.startsWith('http')) { if (uri.startsWith('http')) {
String path = Uri.parse(uri).path; String path = Uri.parse(uri).path;
@ -88,11 +84,10 @@ class VideoCardV extends StatelessWidget {
return; return;
} }
} }
Get.toNamed('/htmlRender', parameters: { Get.toNamed('/read', parameters: {
'url': uri,
'title': videoItem.title, 'title': videoItem.title,
'id': id, 'id': videoItem.param,
'dynamicType': dynamicType 'articleType': 'read'
}); });
} catch (err) { } catch (err) {
SmartDialog.showToast(err.toString()); SmartDialog.showToast(err.toString());
@ -287,9 +282,10 @@ class VideoStat extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( return Row(
children: [ children: [
StatView(view: videoItem.stat.view), if (videoItem.stat.view != null) StatView(view: videoItem.stat.view),
const SizedBox(width: 8), const SizedBox(width: 8),
StatDanMu(danmu: videoItem.stat.danmu), if (videoItem.stat.danmu != null)
StatDanMu(danmu: videoItem.stat.danmu),
if (videoItem is RecVideoItemModel) ...<Widget>[ if (videoItem is RecVideoItemModel) ...<Widget>[
crossAxisCount > 1 ? const Spacer() : const SizedBox(width: 8), crossAxisCount > 1 ? const Spacer() : const SizedBox(width: 8),
RichText( RichText(

View File

@ -104,7 +104,7 @@ class Api {
// 评论列表 // 评论列表
// 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
static const String replyList = '/x/v2/reply'; static const String replyList = '/x/v2/reply/main';
// 楼中楼 // 楼中楼
static const String replyReplyList = '/x/v2/reply/reply'; static const String replyReplyList = '/x/v2/reply/reply';
@ -175,7 +175,7 @@ class Api {
static const String delHistory = '/x/v2/history/delete'; static const String delHistory = '/x/v2/history/delete';
// 搜索历史记录 // 搜索历史记录
static const String searchHistory = '/x/web-goblin/history/search'; static const String searchHistory = '/x/web-interface/history/search';
// 热搜 // 热搜
static const String hotSearchList = static const String hotSearchList =
@ -301,10 +301,6 @@ class Api {
static const String bangumiList = static const String bangumiList =
'/pgc/season/index/result?st=1&order=3&season_version=-1&spoken_language_type=-1&area=-1&is_finish=-1&copyright=-1&season_status=-1&season_month=-1&year=-1&style_id=-1&sort=0&season_type=1&pagesize=20&type=1'; '/pgc/season/index/result?st=1&order=3&season_version=-1&spoken_language_type=-1&area=-1&is_finish=-1&copyright=-1&season_status=-1&season_month=-1&year=-1&style_id=-1&sort=0&season_type=1&pagesize=20&type=1';
// 我的订阅
static const String bangumiFollow =
'/x/space/bangumi/follow/list?type=1&follow_status=0&pn=1&ps=15&ts=1691544359969';
// 黑名单 // 黑名单
static const String blackLst = '/x/relation/blacks'; static const String blackLst = '/x/relation/blacks';
@ -499,7 +495,7 @@ class Api {
static const activateBuvidApi = '/x/internal/gaia-gateway/ExClimbWuzhi'; static const activateBuvidApi = '/x/internal/gaia-gateway/ExClimbWuzhi';
/// 获取字幕配置 /// 获取字幕配置
static const getSubtitleConfig = '/x/player/v2'; static const getSubtitleConfig = '/x/player/wbi/v2';
/// 我的订阅 /// 我的订阅
static const userSubFolder = '/x/v3/fav/folder/collected/list'; static const userSubFolder = '/x/v3/fav/folder/collected/list';
@ -555,6 +551,10 @@ class Api {
static const String messageSystemAPi = static const String messageSystemAPi =
'${HttpString.messageBaseUrl}/x/sys-msg/query_unified_notify'; '${HttpString.messageBaseUrl}/x/sys-msg/query_unified_notify';
/// 系统通知 个人
static const String userMessageSystemAPi =
'${HttpString.messageBaseUrl}/x/sys-msg/query_user_notify';
/// 系统通知标记已读 /// 系统通知标记已读
static const String systemMarkRead = static const String systemMarkRead =
'${HttpString.messageBaseUrl}/x/sys-msg/update_cursor'; '${HttpString.messageBaseUrl}/x/sys-msg/update_cursor';
@ -575,4 +575,55 @@ class Api {
/// 我的关注 - 正在直播 /// 我的关注 - 正在直播
static const String getFollowingLive = static const String getFollowingLive =
'${HttpString.liveBaseUrl}/xlive/web-ucenter/user/following'; '${HttpString.liveBaseUrl}/xlive/web-ucenter/user/following';
/// 稍后再看&收藏夹视频列表
static const String mediaList = '/x/v2/medialist/resource/list';
/// 用户专栏
static const String opusList = '/x/polymer/web-dynamic/v1/opus/feed/space';
///
static const String getViewInfo = '/x/article/viewinfo';
/// 直播间记录
static const String liveRoomEntry =
'${HttpString.liveBaseUrl}/xlive/web-room/v1/index/roomEntryAction';
/// 用户信息
static const String accountInfo = '/x/member/web/account';
/// 更新用户信息
static const String updateAccountInfo = '/x/member/web/update';
/// 删除评论
static const String replyDel = '/x/v2/reply/del';
/// 图片上传
static const String uploadImage = '/x/dynamic/feed/draw/upload_bfs';
/// 更新追番状态
static const String updateBangumiStatus = '/pgc/web/follow/status/update';
/// 番剧点赞投币收藏状态
static const String bangumiActionStatus = '/pgc/season/episode/community';
/// @我的
static const String messageAtAPi = '/x/msgfeed/at?';
/// 订阅
static const String confirmSub = '/x/v3/fav/season/fav';
/// 订阅状态
static const String videoRelation = '/x/web-interface/archive/relation';
/// 获取空降区间
static const String getSkipSegments =
'${HttpString.sponsorBlockBaseUrl}/api/skipSegments';
/// 视频标签
static const String videoTag = '/x/tag/archive/tags';
/// 修复标题和海报
// /api/view?id=${aid} /all/video/av${aid} /video/av${aid}/
static const String fixTitleAndPic = '${HttpString.biliplusBaseUrl}/api/view';
} }

View File

@ -1,5 +1,8 @@
import 'dart:convert';
import '../models/bangumi/list.dart'; import '../models/bangumi/list.dart';
import 'index.dart'; import 'index.dart';
import 'package:html/parser.dart' as html_parser;
import 'package:html/dom.dart' as html_dom;
class BangumiHttp { class BangumiHttp {
static Future bangumiList({int? page}) async { static Future bangumiList({int? page}) async {
@ -18,8 +21,19 @@ class BangumiHttp {
} }
} }
static Future bangumiFollow({int? mid}) async { static Future getRecentBangumi({
var res = await Request().get(Api.bangumiFollow, data: {'vmid': mid}); int? mid,
int type = 1,
int pn = 1,
int ps = 20,
}) async {
var res = await Request().get(Api.getRecentBangumiApi, data: {
'vmid': mid,
'type': type,
'follow_status': 0,
'pn': pn,
'ps': ps,
});
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return { return {
'status': true, 'status': true,
@ -33,4 +47,62 @@ class BangumiHttp {
}; };
} }
} }
// 获取追番状态
static Future bangumiStatus({required int seasonId}) async {
var res = await Request()
.get('https://www.bilibili.com/bangumi/play/ss$seasonId');
html_dom.Document document = html_parser.parse(res.data);
// 查找 id 为 __NEXT_DATA__ 的 script 元素
html_dom.Element? scriptElement =
document.querySelector('script#\\__NEXT_DATA__');
if (scriptElement != null) {
// 提取 script 元素的内容
String scriptContent = scriptElement.text;
final dynamic scriptContentJson = jsonDecode(scriptContent);
Map followState = scriptContentJson['props']['pageProps']['followState'];
return {
'status': true,
'data': {
'isFollowed': followState['isFollowed'],
'followStatus': followState['followStatus']
}
};
} else {
print('Script element with id "__NEXT_DATA__" not found.');
}
}
// 更新追番状态
static Future updateBangumiStatus({
required int seasonId,
required int status,
}) async {
var res = await Request().post(Api.updateBangumiStatus, data: {
'season_id': seasonId,
'status': status,
});
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
// 获取番剧点赞投币收藏状态
static Future bangumiActionStatus({required int epId}) async {
var res = await Request().get(
Api.bangumiActionStatus,
data: {'ep_id': epId},
);
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
}
}
} }

View File

@ -28,7 +28,7 @@ class BlackHttp {
static Future removeBlack({required int fid}) async { static Future removeBlack({required int fid}) async {
var res = await Request().post( var res = await Request().post(
Api.removeBlack, Api.removeBlack,
queryParameters: { data: {
'act': 6, 'act': 6,
'csrf': await Request.getCsrf(), 'csrf': await Request.getCsrf(),
'fid': fid, 'fid': fid,

View File

@ -1,6 +1,15 @@
import 'package:pilipala/models/common/invalid_video.dart';
import 'dart:convert';
import 'dart:math';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:pilipala/models/sponsor_block/segment.dart';
import 'index.dart'; import 'index.dart';
class CommonHttp { class CommonHttp {
static final RegExp spmPrefixExp =
RegExp(r'<meta name="spm_prefix" content="([^"]+?)">');
static Future unReadDynamic() async { static Future unReadDynamic() async {
var res = await Request().get(Api.getUnreadDynamic, var res = await Request().get(Api.getUnreadDynamic,
data: {'alltype_offset': 0, 'video_offset': '', 'article_offset': 0}); data: {'alltype_offset': 0, 'video_offset': '', 'article_offset': 0});
@ -14,4 +23,95 @@ class CommonHttp {
}; };
} }
} }
static Future querySkipSegments({required String bvid}) async {
var res = await Request().getWithoutCookie(Api.getSkipSegments, data: {
'videoID': bvid,
});
if (res.data is List && res.data.isNotEmpty) {
try {
return {
'status': true,
'data': res.data
.map<SegmentDataModel>((e) => SegmentDataModel.fromJson(e))
.toList(),
};
} catch (err) {
return {
'status': false,
'data': [],
'msg': 'sponsorBlock数据解析失败: $err',
};
}
} else {
return {
'status': false,
'data': [],
};
}
}
static Future fixVideoPicAndTitle({required int aid}) async {
var res = await Request().getWithoutCookie(Api.fixTitleAndPic, data: {
'id': aid,
});
if (res != null) {
if (res.data['code'] == -404) {
return {
'status': false,
'data': null,
'msg': '没有相关信息',
};
} else {
return {
'status': true,
'data': InvalidVideoModel.fromJson(res.data),
};
}
} else {
return {
'status': false,
'data': null,
'msg': '没有相关信息',
};
}
}
static Future buvidActivate() async {
try {
// 获取 HTML 数据
var html = await Request().get(Api.dynamicSpmPrefix);
// 提取 spmPrefix
String spmPrefix = spmPrefixExp.firstMatch(html.data)?.group(1) ?? '';
// 生成随机 PNG 结束部分
Random rand = Random();
String randPngEnd = base64.encode(
List<int>.generate(32, (_) => rand.nextInt(256))
..addAll(List<int>.filled(4, 0))
..addAll([73, 69, 78, 68])
..addAll(List<int>.generate(4, (_) => rand.nextInt(256))),
);
// 构建 JSON 数据
String jsonData = json.encode({
'3064': 1,
'39c8': '$spmPrefix.fp.risk',
'3c43': {
'adca': 'Linux',
'bfe9': randPngEnd.substring(randPngEnd.length - 50),
},
});
// 发送 POST 请求
await Request().post(
Api.activateBuvidApi,
data: {'payload': jsonData},
options: Options(contentType: 'application/json'),
);
} catch (err) {
debugPrint('buvidActivate error: $err');
}
}
} }

View File

@ -7,6 +7,9 @@ class HttpString {
static const String passBaseUrl = 'https://passport.bilibili.com'; static const String passBaseUrl = 'https://passport.bilibili.com';
static const String messageBaseUrl = 'https://message.bilibili.com'; static const String messageBaseUrl = 'https://message.bilibili.com';
static const String bangumiBaseUrl = 'https://bili.meark.me'; static const String bangumiBaseUrl = 'https://bili.meark.me';
static const String sponsorBlockBaseUrl = 'https://www.bsbsb.top';
static const String biliplusBaseUrl = 'https://www.biliplus.com';
static const List<int> validateStatusCodes = [ static const List<int> validateStatusCodes = [
302, 302,
304, 304,

View File

@ -17,7 +17,9 @@ class DanmakaHttp {
var response = await Request().get( var response = await Request().get(
Api.webDanmaku, Api.webDanmaku,
data: params, data: params,
extra: {'resType': ResponseType.bytes}, options: Options(
responseType: ResponseType.bytes,
),
); );
return DmSegMobileReply.fromBuffer(response.data); return DmSegMobileReply.fromBuffer(response.data);
} }
@ -67,9 +69,6 @@ class DanmakaHttp {
var response = await Request().post( var response = await Request().post(
Api.shootDanmaku, Api.shootDanmaku,
data: params, data: params,
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
); );
if (response.statusCode != 200) { if (response.statusCode != 200) {
return { return {

View File

@ -1,4 +1,5 @@
import 'dart:math'; import 'dart:math';
import 'package:dio/dio.dart';
import '../models/dynamics/result.dart'; import '../models/dynamics/result.dart';
import '../models/dynamics/up.dart'; import '../models/dynamics/up.dart';
import 'index.dart'; import 'index.dart';
@ -69,7 +70,7 @@ class DynamicsHttp {
}) async { }) async {
var res = await Request().post( var res = await Request().post(
Api.likeDynamic, Api.likeDynamic,
queryParameters: { data: {
'dynamic_id': dynamicId, 'dynamic_id': dynamicId,
'up': up, 'up': up,
'csrf': await Request.getCsrf(), 'csrf': await Request.getCsrf(),
@ -91,7 +92,7 @@ class DynamicsHttp {
// //
static Future dynamicDetail({ static Future dynamicDetail({
String? id, required String id,
}) async { }) async {
var res = await Request().get(Api.dynamicDetail, data: { var res = await Request().get(Api.dynamicDetail, data: {
'timezone_offset': -480, 'timezone_offset': -480,
@ -175,27 +176,32 @@ class DynamicsHttp {
'revs_id': {'dyn_type': 8, 'rid': oid} 'revs_id': {'dyn_type': 8, 'rid': oid}
}; };
} }
var res = await Request().post(Api.dynamicCreate, queryParameters: { var res = await Request().post(
'platform': 'web', Api.dynamicCreate,
'csrf': await Request.getCsrf(), queryParameters: {
'x-bili-device-req-json': {'platform': 'web', 'device': 'pc'}, 'platform': 'web',
'x-bili-web-req-json': {'spm_id': '333.999'}, 'csrf': await Request.getCsrf(),
}, data: { 'x-bili-device-req-json': {'platform': 'web', 'device': 'pc'},
'dyn_req': { 'x-bili-web-req-json': {'spm_id': '333.999'},
'content': {
'contents': [
{'raw_text': rawText ?? '', 'type': 1, 'biz_id': ''}
]
},
'scene': scene,
'attach_card': null,
'upload_id': uploadId,
'meta': {
'app_meta': {'from': 'create.dynamic.web', 'mobi_app': 'web'}
}
}, },
'web_repost_src': webRepostSrc data: {
}); 'dyn_req': {
'content': {
'contents': [
{'raw_text': rawText ?? '', 'type': 1, 'biz_id': ''}
]
},
'scene': scene,
'attach_card': null,
'upload_id': uploadId,
'meta': {
'app_meta': {'from': 'create.dynamic.web', 'mobi_app': 'web'}
}
},
'web_repost_src': webRepostSrc
},
options: Options(contentType: 'application/json'),
);
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return { return {
'status': true, 'status': true,

View File

@ -11,7 +11,7 @@ class FavHttp {
}) async { }) async {
var res = await Request().post( var res = await Request().post(
Api.editFavFolder, Api.editFavFolder,
queryParameters: { data: {
'title': title, 'title': title,
'intro': intro, 'intro': intro,
'media_id': mediaId, 'media_id': mediaId,
@ -43,7 +43,7 @@ class FavHttp {
}) async { }) async {
var res = await Request().post( var res = await Request().post(
Api.addFavFolder, Api.addFavFolder,
queryParameters: { data: {
'title': title, 'title': title,
'intro': intro, 'intro': intro,
'cover': cover ?? '', 'cover': cover ?? '',

View File

@ -21,7 +21,6 @@ class HtmlHttp {
} }
try { try {
Document rootTree = parse(response.data); Document rootTree = parse(response.data);
// log(response.data.body.toString());
Element body = rootTree.body!; Element body = rootTree.body!;
Element appDom = body.querySelector('#app')!; Element appDom = body.querySelector('#app')!;
Element authorHeader = appDom.querySelector('.fixed-author-header')!; Element authorHeader = appDom.querySelector('.fixed-author-header')!;
@ -52,7 +51,6 @@ class HtmlHttp {
.className .className
.split(' ')[1] .split(' ')[1]
.split('-')[2]; .split('-')[2];
// List imgList = opusDetail.querySelectorAll('bili-album__preview__picture__img');
return { return {
'status': true, 'status': true,
'avatar': avatar, 'avatar': avatar,
@ -76,20 +74,10 @@ class HtmlHttp {
Element body = rootTree.body!; Element body = rootTree.body!;
Element appDom = body.querySelector('#app')!; Element appDom = body.querySelector('#app')!;
Element authorHeader = appDom.querySelector('.up-left')!; Element authorHeader = appDom.querySelector('.up-left')!;
// 头像
// String avatar =
// authorHeader.querySelector('.bili-avatar-img')!.attributes['data-src']!;
// print(avatar);
// avatar = 'https:${avatar.split('@')[0]}';
String uname = authorHeader.querySelector('.up-name')!.text.trim(); String uname = authorHeader.querySelector('.up-name')!.text.trim();
// 动态详情 // 动态详情
Element opusDetail = appDom.querySelector('.article-content')!; Element opusDetail = appDom.querySelector('.article-content')!;
// 发布时间 // 发布时间
// String updateTime =
// opusDetail.querySelector('.opus-module-author__pub__text')!.text;
// print(updateTime);
//
String opusContent = String opusContent =
opusDetail.querySelector('#read-article-holder')!.innerHtml; opusDetail.querySelector('#read-article-holder')!.innerHtml;
RegExp digitRegExp = RegExp(r'\d+'); RegExp digitRegExp = RegExp(r'\d+');

View File

@ -1,19 +1,16 @@
// ignore_for_file: avoid_print // ignore_for_file: avoid_print
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:developer'; import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'dart:math' show Random;
import 'package:cookie_jar/cookie_jar.dart'; import 'package:cookie_jar/cookie_jar.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:dio/io.dart'; import 'package:dio/io.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart'; import 'package:dio_cookie_manager/dio_cookie_manager.dart';
// import 'package:dio_http2_adapter/dio_http2_adapter.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/models/user/info.dart';
import 'package:pilipala/utils/id_utils.dart'; import 'package:pilipala/utils/id_utils.dart';
import '../utils/storage.dart'; import '../utils/storage.dart';
import '../utils/utils.dart'; import '../utils/utils.dart';
import 'api.dart';
import 'constants.dart'; import 'constants.dart';
import 'interceptor.dart'; import 'interceptor.dart';
@ -22,19 +19,17 @@ class Request {
static late CookieManager cookieManager; static late CookieManager cookieManager;
static late final Dio dio; static late final Dio dio;
factory Request() => _instance; factory Request() => _instance;
Box setting = GStrorage.setting; Box setting = GStorage.setting;
static Box localCache = GStrorage.localCache; static Box localCache = GStorage.localCache;
late bool enableSystemProxy; late bool enableSystemProxy;
late String systemProxyHost; late String systemProxyHost;
late String systemProxyPort; late String systemProxyPort;
static final RegExp spmPrefixExp =
RegExp(r'<meta name="spm_prefix" content="([^"]+?)">');
static String? buvid; static String? buvid;
/// 设置cookie /// 设置cookie
static setCookie() async { static setCookie() async {
Box userInfoCache = GStrorage.userInfo; Box userInfoCache = GStorage.userInfo;
Box setting = GStrorage.setting; Box setting = GStorage.setting;
final String cookiePath = await Utils.getCookiePath(); final String cookiePath = await Utils.getCookiePath();
final PersistCookieJar cookieJar = PersistCookieJar( final PersistCookieJar cookieJar = PersistCookieJar(
ignoreExpires: true, ignoreExpires: true,
@ -44,7 +39,7 @@ class Request {
dio.interceptors.add(cookieManager); dio.interceptors.add(cookieManager);
final List<Cookie> cookie = await cookieManager.cookieJar final List<Cookie> cookie = await cookieManager.cookieJar
.loadForRequest(Uri.parse(HttpString.baseUrl)); .loadForRequest(Uri.parse(HttpString.baseUrl));
final userInfo = userInfoCache.get('userInfoCache'); final UserInfoData? userInfo = userInfoCache.get('userInfoCache');
if (userInfo != null && userInfo.mid != null) { if (userInfo != null && userInfo.mid != null) {
final List<Cookie> cookie2 = await cookieManager.cookieJar final List<Cookie> cookie2 = await cookieManager.cookieJar
.loadForRequest(Uri.parse(HttpString.tUrl)); .loadForRequest(Uri.parse(HttpString.tUrl));
@ -62,11 +57,6 @@ class Request {
baseUrlType = 'bangumi'; baseUrlType = 'bangumi';
} }
setBaseUrl(type: baseUrlType); setBaseUrl(type: baseUrlType);
try {
await buvidActivate();
} catch (e) {
log("setCookie, ${e.toString()}");
}
final String cookieString = cookie final String cookieString = cookie
.map((Cookie cookie) => '${cookie.name}=${cookie.value}') .map((Cookie cookie) => '${cookie.name}=${cookie.value}')
@ -122,30 +112,6 @@ class Request {
dio.options.headers['referer'] = 'https://www.bilibili.com/'; dio.options.headers['referer'] = 'https://www.bilibili.com/';
} }
static Future buvidActivate() async {
var html = await Request().get(Api.dynamicSpmPrefix);
String spmPrefix = spmPrefixExp.firstMatch(html.data)!.group(1)!;
Random rand = Random();
String rand_png_end = base64.encode(
List<int>.generate(32, (_) => rand.nextInt(256)) +
List<int>.filled(4, 0) +
[73, 69, 78, 68] +
List<int>.generate(4, (_) => rand.nextInt(256)));
String jsonData = json.encode({
'3064': 1,
'39c8': '${spmPrefix}.fp.risk',
'3c43': {
'adca': 'Linux',
'bfe9': rand_png_end.substring(rand_png_end.length - 50),
},
});
await Request().post(Api.activateBuvidApi,
data: {'payload': jsonData},
options: Options(contentType: 'application/json'));
}
/* /*
* config it and create * config it and create
*/ */
@ -171,15 +137,6 @@ class Request {
dio = Dio(options); dio = Dio(options);
/// fix 第三方登录 302重定向 跟iOS代理问题冲突
// ..httpClientAdapter = Http2Adapter(
// ConnectionManager(
// idleTimeout: const Duration(milliseconds: 10000),
// onClientCreate: (_, ClientSetting config) =>
// config.onBadCertificate = (_) => true,
// ),
// );
/// 设置代理 /// 设置代理
if (enableSystemProxy) { if (enableSystemProxy) {
dio.httpClientAdapter = IOHttpClientAdapter( dio.httpClientAdapter = IOHttpClientAdapter(
@ -217,18 +174,15 @@ class Request {
/* /*
* get请求 * get请求
*/ */
get(url, {data, options, cancelToken, extra}) async { get(url, {data, Options? options, cancelToken, extra}) async {
Response response; Response response;
final Options options = Options();
ResponseType resType = ResponseType.json;
if (extra != null) { if (extra != null) {
resType = extra!['resType'] ?? ResponseType.json;
if (extra['ua'] != null) { if (extra['ua'] != null) {
options.headers = {'user-agent': headerUa(type: extra['ua'])}; options ??= Options();
options.headers ??= <String, dynamic>{};
options.headers?['user-agent'] = headerUa(type: extra['ua']);
} }
} }
options.responseType = resType;
try { try {
response = await dio.get( response = await dio.get(
url, url,
@ -238,32 +192,44 @@ class Request {
); );
return response; return response;
} on DioException catch (e) { } on DioException catch (e) {
Response errResponse = Response( return Response(
data: { data: {'message': await ApiInterceptor.dioError(e)},
'message': await ApiInterceptor.dioError(e)
}, // 将自定义 Map 数据赋值给 Response 的 data 属性
statusCode: 200, statusCode: 200,
requestOptions: RequestOptions(), requestOptions: RequestOptions(),
); );
return errResponse;
} }
} }
/*
* get请求
*/
getWithoutCookie(url, {data}) {
return get(
url,
data: data,
options: Options(
headers: {
'cookie': 'buvid3= ; b_nut= ; sid= ',
'user-agent': headerUa(type: 'pc'),
},
),
);
}
/* /*
* post请求 * post请求
*/ */
post(url, {data, queryParameters, options, cancelToken, extra}) async { post(url, {data, queryParameters, options, cancelToken, extra}) async {
// print('post-data: $data');
Response response; Response response;
try { try {
response = await dio.post( response = await dio.post(
url, url,
data: data, data: data,
queryParameters: queryParameters, queryParameters: queryParameters,
options: options, options:
options ?? Options(contentType: Headers.formUrlEncodedContentType),
cancelToken: cancelToken, cancelToken: cancelToken,
); );
// print('post success: ${response.data}');
return response; return response;
} on DioException catch (e) { } on DioException catch (e) {
Response errResponse = Response( Response errResponse = Response(
@ -319,7 +285,7 @@ class Request {
} }
} else { } else {
headerUa = headerUa =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.2 Safari/605.1.15'; 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36';
} }
return headerUa; return headerUa;
} }

View File

@ -3,8 +3,7 @@
import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.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:pilipala/utils/login.dart';
import '../utils/storage.dart';
class ApiInterceptor extends Interceptor { class ApiInterceptor extends Interceptor {
@override @override
@ -19,20 +18,9 @@ class ApiInterceptor extends Interceptor {
@override @override
void onResponse(Response response, ResponseInterceptorHandler handler) { void onResponse(Response response, ResponseInterceptorHandler handler) {
try { try {
if (response.statusCode == 302) { // 在响应之后处理数据
final List<String> locations = response.headers['location']!; if (response.data is Map && response.data['code'] == -101) {
if (locations.isNotEmpty) { LoginUtils.loginOut();
if (locations.first.startsWith('https://www.mcbbs.net')) {
final Uri uri = Uri.parse(locations.first);
final String? accessKey = uri.queryParameters['access_key'];
final String? mid = uri.queryParameters['mid'];
try {
Box localCache = GStrorage.localCache;
localCache.put(LocalCacheKey.accessKey,
<String, String?>{'mid': mid, 'value': accessKey});
} catch (_) {}
}
}
} }
} catch (err) { } catch (err) {
print('ApiInterceptor: $err'); print('ApiInterceptor: $err');

View File

@ -89,23 +89,26 @@ class LiveHttp {
// 发送弹幕 // 发送弹幕
static Future sendDanmaku({roomId, msg}) async { static Future sendDanmaku({roomId, msg}) async {
var res = await Request().post(Api.sendLiveMsg, queryParameters: { var res = await Request().post(
'bubble': 0, Api.sendLiveMsg,
'msg': msg, data: {
'color': 16777215, // 颜色 'bubble': 0,
'mode': 1, // 模式 'msg': msg,
'room_type': 0, 'color': 16777215, // 颜色
'jumpfrom': 71001, // 直播间来源 'mode': 1, // 模式
'reply_mid': 0, 'room_type': 0,
'reply_attr': 0, 'jumpfrom': 71001, // 直播间来源
'replay_dmid': '', 'reply_mid': 0,
'statistics': {"appId": 100, "platform": 5}, 'reply_attr': 0,
'fontsize': 25, // 字体大小 'replay_dmid': '',
'rnd': DateTime.now().millisecondsSinceEpoch ~/ 1000, // 时间戳 'statistics': {"appId": 100, "platform": 5},
'roomid': roomId, 'fontsize': 25, // 字体大小
'csrf': await Request.getCsrf(), 'rnd': DateTime.now().millisecondsSinceEpoch ~/ 1000, // 时间戳
'csrf_token': await Request.getCsrf(), 'roomid': roomId,
}); 'csrf': await Request.getCsrf(),
'csrf_token': await Request.getCsrf(),
},
);
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return { return {
'status': true, 'status': true,
@ -142,4 +145,18 @@ class LiveHttp {
}; };
} }
} }
// 直播历史记录
static Future liveRoomEntry({required int roomId}) async {
await Request().post(
Api.liveRoomEntry,
data: {
'room_id': roomId,
'platform': 'pc',
'csrf_token': await Request.getCsrf(),
'csrf': await Request.getCsrf(),
'visit_id': '',
},
);
}
} }

View File

@ -71,9 +71,6 @@ class LoginHttp {
var res = await Request().post( var res = await Request().post(
Api.webSmsCode, Api.webSmsCode,
data: formData, data: formData,
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
); );
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return { return {
@ -106,9 +103,6 @@ class LoginHttp {
var res = await Request().post( var res = await Request().post(
Api.webSmsLogin, Api.webSmsLogin,
data: formData, data: formData,
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
); );
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return { return {
@ -155,9 +149,6 @@ class LoginHttp {
var res = await Request().post( var res = await Request().post(
Api.appSmsCode, Api.appSmsCode,
data: data, data: data,
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
); );
print(res); print(res);
} }
@ -208,9 +199,6 @@ class LoginHttp {
var res = await Request().post( var res = await Request().post(
Api.loginInByPwdApi, Api.loginInByPwdApi,
data: data, data: data,
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
); );
print(res); print(res);
} }
@ -239,17 +227,27 @@ class LoginHttp {
var res = await Request().post( var res = await Request().post(
Api.loginInByWebPwd, Api.loginInByWebPwd,
data: formData, data: formData,
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
); );
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return { if (res.data['data']['status'] == 0) {
'status': true, return {
'data': res.data['data'], 'status': true,
}; 'data': res.data['data'],
};
} else {
return {
'status': false,
'code': 1,
'data': res.data['data'],
'msg': res.data['data']['message'],
};
}
} else { } else {
return {'status': false, 'data': [], 'msg': res.data['message']}; return {
'status': false,
'data': [],
'msg': res.data['message'],
};
} }
} }

View File

@ -1,6 +1,11 @@
import 'dart:convert';
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:html/parser.dart';
import 'package:pilipala/models/member/article.dart';
import 'package:pilipala/models/member/like.dart'; import 'package:pilipala/models/member/like.dart';
import 'package:pilipala/models/user/info.dart';
import 'package:pilipala/utils/global_data_cache.dart';
import '../common/constants.dart'; import '../common/constants.dart';
import '../models/dynamics/result.dart'; import '../models/dynamics/result.dart';
import '../models/follow/result.dart'; import '../models/follow/result.dart';
@ -16,14 +21,20 @@ import 'index.dart';
class MemberHttp { class MemberHttp {
static Future memberInfo({ static Future memberInfo({
int? mid, required int mid,
String token = '', String token = '',
}) async { }) async {
String? wWebid;
if ((await getWWebid(mid: mid))['status']) {
wWebid = GlobalDataCache.wWebid;
}
Map params = await WbiSign().makSign({ Map params = await WbiSign().makSign({
'mid': mid, 'mid': mid,
'token': token, 'token': token,
'platform': 'web', 'platform': 'web',
'web_location': 1550101, 'web_location': 1550101,
...wWebid != null ? {'w_webid': wWebid} : {},
}); });
var res = await Request().get( var res = await Request().get(
Api.memberInfo, Api.memberInfo,
@ -195,13 +206,15 @@ class MemberHttp {
// 设置分组 // 设置分组
static Future addUsers(int? fids, String? tagids) async { static Future addUsers(int? fids, String? tagids) async {
var res = await Request().post(Api.addUsers, queryParameters: { var res = await Request().post(
'fids': fids, Api.addUsers,
'tagids': tagids ?? '0', data: {
'csrf': await Request.getCsrf(), 'fids': fids,
}, data: { 'tagids': tagids ?? '0',
'cross_domain': true 'csrf': await Request.getCsrf(),
}); },
queryParameters: {'cross_domain': true},
);
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return {'status': true, 'data': [], 'msg': '操作成功'}; return {'status': true, 'data': [], 'msg': '操作成功'};
} else { } else {
@ -419,11 +432,14 @@ class MemberHttp {
static Future cookieToKey() async { static Future cookieToKey() async {
var authCodeRes = await getTVCode(); var authCodeRes = await getTVCode();
if (authCodeRes['status']) { if (authCodeRes['status']) {
var res = await Request().post(Api.cookieToKey, queryParameters: { var res = await Request().post(
'auth_code': authCodeRes['data'], Api.cookieToKey,
'build': 708200, data: {
'csrf': await Request.getCsrf(), 'auth_code': authCodeRes['data'],
}); 'build': 708200,
'csrf': await Request.getCsrf(),
},
);
await Future.delayed(const Duration(milliseconds: 300)); await Future.delayed(const Duration(milliseconds: 300));
await qrcodePoll(authCodeRes['data']); await qrcodePoll(authCodeRes['data']);
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
@ -455,11 +471,11 @@ class MemberHttp {
SmartDialog.dismiss(); SmartDialog.dismiss();
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
String accessKey = res.data['data']['access_token']; String accessKey = res.data['data']['access_token'];
Box localCache = GStrorage.localCache; Box localCache = GStorage.localCache;
Box userInfoCache = GStrorage.userInfo; Box userInfoCache = GStorage.userInfo;
var userInfo = userInfoCache.get('userInfoCache'); final UserInfoData? userInfo = userInfoCache.get('userInfoCache');
localCache.put( localCache.put(
LocalCacheKey.accessKey, {'mid': userInfo.mid, 'value': accessKey}); LocalCacheKey.accessKey, {'mid': userInfo!.mid, 'value': accessKey});
return {'status': true, 'data': [], 'msg': '操作成功'}; return {'status': true, 'data': [], 'msg': '操作成功'};
} else { } else {
return { return {
@ -556,4 +572,60 @@ class MemberHttp {
}; };
} }
} }
static Future getWWebid({required int mid}) async {
String? wWebid = GlobalDataCache.wWebid;
if (wWebid != null) {
return {'status': true, 'data': wWebid};
}
var res = await Request().get('https://space.bilibili.com/$mid/article');
String? headContent = parse(res.data).head?.outerHtml;
final regex = RegExp(
r'<script id="__RENDER_DATA__" type="application/json">(.*?)</script>');
if (headContent != null) {
final match = regex.firstMatch(headContent);
if (match != null && match.groupCount >= 1) {
final content = match.group(1);
String decodedString = Uri.decodeComponent(content!);
Map<String, dynamic> map = jsonDecode(decodedString);
GlobalDataCache.wWebid = map['access_id'];
return {'status': true, 'data': map['access_id']};
} else {
return {'status': false, 'data': '请检查登录状态'};
}
}
return {'status': false, 'data': '请检查登录状态'};
}
// 获取用户专栏
static Future getMemberArticle({
required int mid,
required int pn,
String? offset,
}) async {
String? wWebid;
if ((await getWWebid(mid: mid))['status']) {
wWebid = GlobalDataCache.wWebid;
}
Map params = await WbiSign().makSign({
'host_mid': mid,
'page': pn,
'offset': offset,
'web_location': 333.999,
...wWebid != null ? {'w_webid': wWebid} : {},
});
var res = await Request().get(Api.opusList, data: params);
if (res.data['code'] == 0) {
return {
'status': true,
'data': MemberArticleDataModel.fromJson(res.data['data'])
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'] ?? '请求异常',
};
}
}
} }

View File

@ -1,6 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'package:dio/dio.dart'; import 'package:flutter/material.dart';
import 'package:pilipala/models/msg/at.dart';
import 'package:pilipala/models/msg/like.dart'; import 'package:pilipala/models/msg/like.dart';
import 'package:pilipala/models/msg/reply.dart'; import 'package:pilipala/models/msg/reply.dart';
import 'package:pilipala/models/msg/system.dart'; import 'package:pilipala/models/msg/system.dart';
@ -64,7 +65,7 @@ class MsgHttp {
.toList(), .toList(),
}; };
} catch (err) { } catch (err) {
print('err🔟: $err'); debugPrint('err: $err');
} }
} else { } else {
return { return {
@ -158,9 +159,6 @@ class MsgHttp {
'csrf_token': csrf, 'csrf_token': csrf,
'csrf': csrf, 'csrf': csrf,
}, },
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
); );
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return { return {
@ -282,10 +280,10 @@ class MsgHttp {
'data': MessageLikeModel.fromJson(res.data['data']), 'data': MessageLikeModel.fromJson(res.data['data']),
}; };
} catch (err) { } catch (err) {
return {'status': false, 'date': [], 'msg': err.toString()}; return {'status': false, 'data': [], 'msg': err.toString()};
} }
} else { } else {
return {'status': false, 'date': [], 'msg': res.data['message']}; return {'status': false, 'data': [], 'msg': res.data['message']};
} }
} }
@ -330,4 +328,47 @@ class MsgHttp {
}; };
} }
} }
static Future messageSystemAccount() async {
var res = await Request().get(Api.userMessageSystemAPi, data: {
'csrf': await Request.getCsrf(),
'page_size': 20,
'build': 0,
'mobi_app': 'web',
});
if (res.data['code'] == 0) {
try {
return {
'status': true,
'data': res.data['data']['system_notify_list']
.map<MessageSystemModel>((e) => MessageSystemModel.fromJson(e))
.toList(),
};
} catch (err) {
return {'status': false, 'date': [], 'msg': err.toString()};
}
} else {
return {'status': false, 'date': [], 'msg': res.data['message']};
}
}
// @我的
static Future messageAt() async {
var res = await Request().get(Api.messageAtAPi, data: {
'build': 0,
'mobi_app': 'web',
});
if (res.data['code'] == 0) {
try {
return {
'status': true,
'data': MessageAtModel.fromJson(res.data['data']),
};
} catch (err) {
return {'status': false, 'data': [], 'msg': err.toString()};
}
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
}
}
} }

122
lib/http/read.dart Normal file
View File

@ -0,0 +1,122 @@
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:html/parser.dart';
import 'package:pilipala/models/read/opus.dart';
import 'package:pilipala/models/read/read.dart';
import 'package:pilipala/utils/wbi_sign.dart';
import 'index.dart';
class ReadHttp {
static List<String> extractScriptContents(String htmlContent) {
RegExp scriptRegExp = RegExp(r'<script>([\s\S]*?)<\/script>');
Iterable<Match> matches = scriptRegExp.allMatches(htmlContent);
List<String> scriptContents = [];
for (Match match in matches) {
String scriptContent = match.group(1)!;
scriptContents.add(scriptContent);
}
return scriptContents;
}
// 解析专栏 opus格式
static Future parseArticleOpus({required String id}) async {
var res = await Request().get('https://www.bilibili.com/opus/$id', extra: {
'ua': 'pc',
});
String? headContent = parse(res.data).head?.outerHtml;
var document = parse(headContent);
var linkTags = document.getElementsByTagName('link');
bool isCv = false;
String cvId = '';
for (var linkTag in linkTags) {
var attributes = linkTag.attributes;
if (attributes.containsKey('rel') &&
attributes['rel'] == 'canonical' &&
attributes.containsKey('data-vue-meta') &&
attributes['data-vue-meta'] == 'true') {
final String cvHref = linkTag.attributes['href']!;
RegExp regex = RegExp(r'cv(\d+)');
RegExpMatch? match = regex.firstMatch(cvHref);
if (match != null) {
cvId = match.group(1)!;
} else {
print('No match found.');
}
isCv = true;
break;
}
}
String scriptContent =
extractScriptContents(parse(res.data).body!.outerHtml)[0];
int startIndex = scriptContent.indexOf('{');
int endIndex = scriptContent.lastIndexOf('};');
String jsonContent = scriptContent.substring(startIndex, endIndex + 1);
// 解析JSON字符串为Map
Map<String, dynamic> jsonData = json.decode(jsonContent);
return {
'status': true,
'data': OpusDataModel.fromJson(jsonData),
'isCv': isCv,
'cvId': cvId,
};
}
// 解析专栏 cv格式
static Future parseArticleCv({required String id}) async {
var res = await Request().get(
'https://www.bilibili.com/read/cv$id',
extra: {'ua': 'pc'},
options: Options(
headers: {
'cookie': 'opus-goback=1',
},
),
);
String scriptContent =
extractScriptContents(parse(res.data).body!.outerHtml)[0];
int startIndex = scriptContent.indexOf('{');
int endIndex = scriptContent.lastIndexOf('};');
String jsonContent = scriptContent.substring(startIndex, endIndex + 1);
// 解析JSON字符串为Map
Map<String, dynamic> jsonData = json.decode(jsonContent);
return {
'status': true,
'data': ReadDataModel.fromJson(jsonData),
};
}
//
static Future getViewInfo({required String id}) async {
Map params = await WbiSign().makSign({
'id': id,
'mobi_app': 'pc',
'from': 'web',
'gaia_source': 'main_web',
'web_location': 333.976,
});
var res = await Request().get(
Api.getViewInfo,
data: {
'id': id,
'mobi_app': 'pc',
'from': 'web',
'gaia_source': 'main_web',
'web_location': 333.976,
'w_rid': params['w_rid'],
'wts': params['wts'],
},
);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data'],
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
}

View File

@ -1,3 +1,8 @@
import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:image_picker/image_picker.dart';
import '../models/video/reply/data.dart'; import '../models/video/reply/data.dart';
import '../models/video/reply/emote.dart'; import '../models/video/reply/emote.dart';
import 'api.dart'; import 'api.dart';
@ -6,17 +11,16 @@ import 'init.dart';
class ReplyHttp { class ReplyHttp {
static Future replyList({ static Future replyList({
required int oid, required int oid,
required int pageNum, required String nextOffset,
required int type, required int type,
int? ps, int? ps,
int sort = 1, int sort = 1,
}) async { }) async {
var res = await Request().get(Api.replyList, data: { var res = await Request().get(Api.replyList, data: {
'oid': oid, 'oid': oid,
'pn': pageNum,
'type': type, 'type': type,
'sort': sort, 'pagination_str': jsonEncode({'offset': nextOffset}),
'ps': ps ?? 20 'mode': sort + 2,
}); });
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return { return {
@ -52,19 +56,13 @@ class ReplyHttp {
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return { return {
'status': true, 'status': true,
'data': ReplyData.fromJson(res.data['data']), 'data': ReplyReplyData.fromJson(res.data['data']),
}; };
} else { } else {
Map errMap = {
-400: '请求错误',
-404: '无此项',
12002: '评论区已关闭',
12009: '评论主体的type不合法',
};
return { return {
'status': false, 'status': false,
'date': [], 'date': [],
'msg': errMap[res.data['code']] ?? '请求异常', 'msg': res.data['message'],
}; };
} }
} }
@ -78,7 +76,7 @@ class ReplyHttp {
}) async { }) async {
var res = await Request().post( var res = await Request().post(
Api.likeReply, Api.likeReply,
queryParameters: { data: {
'type': type, 'type': type,
'oid': oid, 'oid': oid,
'rpid': rpid, 'rpid': rpid,
@ -115,4 +113,65 @@ class ReplyHttp {
}; };
} }
} }
static Future replyDel({
required int type, //replyType
required int oid,
required int rpid,
}) async {
var res = await Request().post(
Api.replyDel,
queryParameters: {
'type': type, //type.index
'oid': oid,
'rpid': rpid,
'csrf': await Request.getCsrf(),
},
);
if (res.data['code'] == 0) {
return {'status': true, 'msg': '删除成功'};
} else {
return {'status': false, 'msg': res.data['message']};
}
}
// 图片上传
static Future uploadImage(
{required XFile xFile, String type = 'new_dyn'}) async {
var formData = FormData.fromMap({
'file_up': await xFileToMultipartFile(xFile),
'biz': type,
'csrf': await Request.getCsrf(),
'category': 'daily',
});
var res = await Request().post(
Api.uploadImage,
data: formData,
);
if (res.data['code'] == 0) {
var data = res.data['data'];
data['img_src'] = data['image_url'];
data['img_width'] = data['image_width'];
data['img_height'] = data['image_height'];
data.remove('image_url');
data.remove('image_width');
data.remove('image_height');
return {
'status': true,
'data': data,
};
} else {
return {
'status': false,
'date': [],
'msg': res.data['message'],
};
}
}
static Future<MultipartFile> xFileToMultipartFile(XFile xFile) async {
var file = File(xFile.path);
var bytes = await file.readAsBytes();
return MultipartFile.fromBytes(bytes, filename: xFile.name);
}
} }

View File

@ -11,7 +11,7 @@ import '../utils/storage.dart';
import 'index.dart'; import 'index.dart';
class SearchHttp { class SearchHttp {
static Box setting = GStrorage.setting; static Box setting = GStorage.setting;
static Future hotSearchList() async { static Future hotSearchList() async {
var res = await Request().get(Api.hotSearchList); var res = await Request().get(Api.hotSearchList);
if (res.data is String) { if (res.data is String) {
@ -143,7 +143,11 @@ class SearchHttp {
} }
final dynamic res = final dynamic res =
await Request().get(Api.ab2c, data: <String, dynamic>{...data}); await Request().get(Api.ab2c, data: <String, dynamic>{...data});
return res.data['data'].first['cid']; if (res.data['code'] == 0) {
return res.data['data'].first['cid'];
} else {
return -1;
}
} }
static Future<Map<String, dynamic>> bangumiInfo( static Future<Map<String, dynamic>> bangumiInfo(

View File

@ -1,5 +1,8 @@
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'dart:convert';
import '../common/constants.dart';
import 'package:dio/dio.dart';
import 'package:html/parser.dart';
import 'package:pilipala/models/video/later.dart';
import '../models/model_hot_video_item.dart'; import '../models/model_hot_video_item.dart';
import '../models/user/fav_detail.dart'; import '../models/user/fav_detail.dart';
import '../models/user/fav_folder.dart'; import '../models/user/fav_folder.dart';
@ -148,7 +151,7 @@ class UserHttp {
// 暂停switchStatus传true 否则false // 暂停switchStatus传true 否则false
var res = await Request().post( var res = await Request().post(
Api.pauseHistory, Api.pauseHistory,
queryParameters: { data: {
'switch': switchStatus, 'switch': switchStatus,
'jsonp': 'jsonp', 'jsonp': 'jsonp',
'csrf': await Request.getCsrf(), 'csrf': await Request.getCsrf(),
@ -167,7 +170,7 @@ class UserHttp {
static Future clearHistory() async { static Future clearHistory() async {
var res = await Request().post( var res = await Request().post(
Api.clearHistory, Api.clearHistory,
queryParameters: { data: {
'jsonp': 'jsonp', 'jsonp': 'jsonp',
'csrf': await Request.getCsrf(), 'csrf': await Request.getCsrf(),
}, },
@ -185,7 +188,7 @@ class UserHttp {
} }
var res = await Request().post( var res = await Request().post(
Api.toViewLater, Api.toViewLater,
queryParameters: data, data: data,
); );
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return {'status': true, 'msg': 'yeah稍后再看'}; return {'status': true, 'msg': 'yeah稍后再看'};
@ -204,7 +207,7 @@ class UserHttp {
params[aid != null ? 'aid' : 'viewed'] = aid ?? true; params[aid != null ? 'aid' : 'viewed'] = aid ?? true;
var res = await Request().post( var res = await Request().post(
Api.toViewDel, Api.toViewDel,
queryParameters: params, data: params,
); );
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return {'status': true, 'msg': 'yeah成功移除'}; return {'status': true, 'msg': 'yeah成功移除'};
@ -213,30 +216,11 @@ class UserHttp {
} }
} }
// 获取用户凭证 失效
static Future thirdLogin() async {
var res = await Request().get(
'https://passport.bilibili.com/login/app/third',
data: {
'appkey': Constants.appKey,
'api': Constants.thirdApi,
'sign': Constants.thirdSign,
},
);
try {
if (res.data['code'] == 0 && res.data['data']['has_login'] == 1) {
Request().get(res.data['data']['confirm_uri']);
}
} catch (err) {
SmartDialog.showNotify(msg: '获取用户凭证: $err', notifyType: NotifyType.error);
}
}
// 清空稍后再看 // 清空稍后再看
static Future toViewClear() async { static Future toViewClear() async {
var res = await Request().post( var res = await Request().post(
Api.toViewClear, Api.toViewClear,
queryParameters: { data: {
'jsonp': 'jsonp', 'jsonp': 'jsonp',
'csrf': await Request.getCsrf(), 'csrf': await Request.getCsrf(),
}, },
@ -252,7 +236,7 @@ class UserHttp {
static Future delHistory(kid) async { static Future delHistory(kid) async {
var res = await Request().post( var res = await Request().post(
Api.delHistory, Api.delHistory,
queryParameters: { data: {
'kid': kid, 'kid': kid,
'jsonp': 'jsonp', 'jsonp': 'jsonp',
'csrf': await Request.getCsrf(), 'csrf': await Request.getCsrf(),
@ -278,30 +262,6 @@ class UserHttp {
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(
@ -401,7 +361,7 @@ class UserHttp {
static Future cancelSub({required int seasonId}) async { static Future cancelSub({required int seasonId}) async {
var res = await Request().post( var res = await Request().post(
Api.cancelSub, Api.cancelSub,
queryParameters: { data: {
'platform': 'web', 'platform': 'web',
'season_id': seasonId, 'season_id': seasonId,
'csrf': await Request.getCsrf(), 'csrf': await Request.getCsrf(),
@ -418,7 +378,7 @@ class UserHttp {
static Future delFavFolder({required int mediaIds}) async { static Future delFavFolder({required int mediaIds}) async {
var res = await Request().post( var res = await Request().post(
Api.delFavFolder, Api.delFavFolder,
queryParameters: { data: {
'media_ids': mediaIds, 'media_ids': mediaIds,
'platform': 'web', 'platform': 'web',
'csrf': await Request.getCsrf(), 'csrf': await Request.getCsrf(),
@ -430,4 +390,160 @@ class UserHttp {
return {'status': false, 'msg': res.data['message']}; return {'status': false, 'msg': res.data['message']};
} }
} }
static List<String> extractScriptContents(String htmlContent) {
RegExp scriptRegExp = RegExp(r'<script>([\s\S]*?)<\/script>');
Iterable<Match> matches = scriptRegExp.allMatches(htmlContent);
List<String> scriptContents = [];
for (Match match in matches) {
String scriptContent = match.group(1)!;
scriptContents.add(scriptContent);
}
return scriptContents;
}
// 稍后再看列表
static Future getMediaList({
required int type,
required int bizId,
required int ps,
int? oid,
}) async {
var res = await Request().get(
Api.mediaList,
data: {
'mobi_app': 'web',
'type': type,
'biz_id': bizId,
'oid': oid ?? '',
'otype': 2,
'ps': ps,
'direction': false,
'desc': true,
'sort_field': 1,
'tid': 0,
'with_current': false,
},
);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data']['media_list'] != null
? res.data['data']['media_list']
.map<MediaVideoItemModel>(
(e) => MediaVideoItemModel.fromJson(e))
.toList()
: []
};
} else {
return {'status': false, 'msg': res.data['message']};
}
}
// 解析收藏夹视频
static Future parseFavVideo({
required int mediaId,
required int oid,
required String bvid,
}) async {
var res = await Request().get(
'https://www.bilibili.com/list/ml$mediaId',
data: {
'oid': mediaId,
'bvid': bvid,
},
);
String scriptContent =
extractScriptContents(parse(res.data).body!.outerHtml)[0];
int startIndex = scriptContent.indexOf('{');
int endIndex = scriptContent.lastIndexOf('};');
String jsonContent = scriptContent.substring(startIndex, endIndex + 1);
// 解析JSON字符串为Map
Map<String, dynamic> jsonData = json.decode(jsonContent);
return {
'status': true,
'data': jsonData['resourceList']
.map<MediaVideoItemModel>((e) => MediaVideoItemModel.fromJson(e))
.toList()
};
}
static Future getAccountInfo() async {
var res = await Request().get(
Api.accountInfo,
data: {'web_location': 333.33},
);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data'],
};
} else {
return {
'status': false,
'data': {},
'mag': res.data['message'],
};
}
}
static Future updateAccountInfo({
required String uname,
required String sign,
required String sex,
required String birthday,
}) async {
var res = await Request().post(
Api.updateAccountInfo,
data: {
'uname': uname,
'usersign': sign,
'sex': sex,
'birthday': birthday,
'csrf': await Request.getCsrf(),
},
options: Options(contentType: Headers.formUrlEncodedContentType),
);
if (res.data['code'] == 0) {
return {
'status': true,
'msg': '更新成功',
};
} else {
return {
'status': false,
'msg': res.data['message'],
};
}
}
// 解析up投稿
static Future parseUpArchiveVideo({
required int mid,
required int oid,
required String bvid,
String sortField = 'pubtime',
}) async {
var res = await Request().get(
'https://www.bilibili.com/list/$mid',
data: {
'oid': oid,
'bvid': bvid,
'sort_field': sortField,
},
);
String scriptContent =
extractScriptContents(parse(res.data).body!.outerHtml)[0];
int startIndex = scriptContent.indexOf('{');
int endIndex = scriptContent.lastIndexOf('};');
String jsonContent = scriptContent.substring(startIndex, endIndex + 1);
// 解析JSON字符串为Map
Map<String, dynamic> jsonData = json.decode(jsonContent);
return {
'status': true,
'data': jsonData['resourceList']
.map<MediaVideoItemModel>((e) => MediaVideoItemModel.fromJson(e))
.toList()
};
}
} }

View File

@ -1,5 +1,9 @@
import 'dart:convert';
import 'dart:developer'; import 'dart:developer';
import 'package:dio/dio.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/models/video/tags.dart';
import '../common/constants.dart'; import '../common/constants.dart';
import '../models/common/reply_type.dart'; import '../models/common/reply_type.dart';
import '../models/home/rcmd/result.dart'; import '../models/home/rcmd/result.dart';
@ -22,11 +26,11 @@ import 'init.dart';
/// 返回{'status': bool, 'data': List} /// 返回{'status': bool, 'data': List}
/// view层根据 status 判断渲染逻辑 /// view层根据 status 判断渲染逻辑
class VideoHttp { class VideoHttp {
static Box localCache = GStrorage.localCache; static Box localCache = GStorage.localCache;
static Box setting = GStrorage.setting; static Box setting = GStorage.setting;
static bool enableRcmdDynamic = static bool enableRcmdDynamic =
setting.get(SettingBoxKey.enableRcmdDynamic, defaultValue: true); setting.get(SettingBoxKey.enableRcmdDynamic, defaultValue: true);
static Box userInfoCache = GStrorage.userInfo; static Box userInfoCache = GStorage.userInfo;
// 首页推荐视频 // 首页推荐视频
static Future rcmdVideoList({required int ps, required int freshIdx}) async { static Future rcmdVideoList({required int ps, required int freshIdx}) async {
@ -95,6 +99,8 @@ class VideoHttp {
for (var i in res.data['data']['items']) { for (var i in res.data['data']['items']) {
// 屏蔽推广和拉黑用户 // 屏蔽推广和拉黑用户
if (i['card_goto'] != 'ad_av' && if (i['card_goto'] != 'ad_av' &&
i['card_goto'] != 'ad_web_s' &&
i['card_goto'] != 'ad_web' &&
(!enableRcmdDynamic ? i['card_goto'] != 'picture' : true) && (!enableRcmdDynamic ? i['card_goto'] != 'picture' : true) &&
(i['args'] != null && (i['args'] != null &&
!blackMidsList.contains(i['args']['up_mid']))) { !blackMidsList.contains(i['args']['up_mid']))) {
@ -243,7 +249,7 @@ class VideoHttp {
static Future coinVideo({required String bvid, required int multiply}) async { static Future coinVideo({required String bvid, required int multiply}) async {
var res = await Request().post( var res = await Request().post(
Api.coinVideo, Api.coinVideo,
queryParameters: { data: {
'bvid': bvid, 'bvid': bvid,
'multiply': multiply, 'multiply': multiply,
'select_like': 0, 'select_like': 0,
@ -271,7 +277,7 @@ class VideoHttp {
static Future oneThree({required String bvid}) async { static Future oneThree({required String bvid}) async {
var res = await Request().post( var res = await Request().post(
Api.oneThree, Api.oneThree,
queryParameters: { data: {
'bvid': bvid, 'bvid': bvid,
'csrf': await Request.getCsrf(), 'csrf': await Request.getCsrf(),
}, },
@ -287,7 +293,7 @@ class VideoHttp {
static Future likeVideo({required String bvid, required bool type}) async { static Future likeVideo({required String bvid, required bool type}) async {
var res = await Request().post( var res = await Request().post(
Api.likeVideo, Api.likeVideo,
queryParameters: { data: {
'bvid': bvid, 'bvid': bvid,
'like': type ? 1 : 2, 'like': type ? 1 : 2,
'csrf': await Request.getCsrf(), 'csrf': await Request.getCsrf(),
@ -303,13 +309,16 @@ class VideoHttp {
// (取消)收藏 // (取消)收藏
static Future favVideo( static Future favVideo(
{required int aid, String? addIds, String? delIds}) async { {required int aid, String? addIds, String? delIds}) async {
var res = await Request().post(Api.favVideo, queryParameters: { var res = await Request().post(
'rid': aid, Api.favVideo,
'type': 2, data: {
'add_media_ids': addIds ?? '', 'rid': aid,
'del_media_ids': delIds ?? '', 'type': 2,
'csrf': await Request.getCsrf(), 'add_media_ids': addIds ?? '',
}); 'del_media_ids': delIds ?? '',
'csrf': await Request.getCsrf(),
},
);
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']}; return {'status': true, 'data': res.data['data']};
} else { } else {
@ -343,20 +352,37 @@ class VideoHttp {
required String message, required String message,
int? root, int? root,
int? parent, int? parent,
List<Map<dynamic, dynamic>>? pictures,
}) async { }) async {
if (message == '') { if (message == '') {
return {'status': false, 'data': [], 'msg': '请输入评论内容'}; return {'status': false, 'data': [], 'msg': '请输入评论内容'};
} }
var res = await Request().post(Api.replyAdd, queryParameters: { var params = <String, dynamic>{
'type': type.index, 'plat': 1,
'oid': oid, 'oid': oid,
'type': type.index,
'root': root == null || root == 0 ? '' : root, 'root': root == null || root == 0 ? '' : root,
'parent': parent == null || parent == 0 ? '' : parent, 'parent': parent == null || parent == 0 ? '' : parent,
'message': message, 'message': message,
'at_name_to_mid': {},
if (pictures != null) 'pictures': jsonEncode(pictures),
'gaia_source': 'main_web',
'csrf': await Request.getCsrf(), 'csrf': await Request.getCsrf(),
}); };
log(res.toString()); Map sign = await WbiSign().makSign(params);
params.remove('wts');
params.remove('w_rid');
FormData formData = FormData.fromMap({...params});
var res = await Request().post(
Api.replyAdd,
queryParameters: {
'w_rid': sign['w_rid'],
'wts': sign['wts'],
},
data: formData,
);
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
log(res.toString());
return {'status': true, 'data': res.data['data']}; return {'status': true, 'data': res.data['data']};
} else { } else {
return {'status': false, 'data': [], 'msg': res.data['message']}; return {'status': false, 'data': [], 'msg': res.data['message']};
@ -376,12 +402,15 @@ class VideoHttp {
// 操作用户关系 // 操作用户关系
static Future relationMod( static Future relationMod(
{required int mid, required int act, required int reSrc}) async { {required int mid, required int act, required int reSrc}) async {
var res = await Request().post(Api.relationMod, queryParameters: { var res = await Request().post(
'fid': mid, Api.relationMod,
'act': act, data: {
're_src': reSrc, 'fid': mid,
'csrf': await Request.getCsrf(), 'act': act,
}); 're_src': reSrc,
'csrf': await Request.getCsrf(),
},
);
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
if (act == 5) { if (act == 5) {
List<int> blackMidsList = List<int> blackMidsList =
@ -397,27 +426,33 @@ class VideoHttp {
// 视频播放进度 // 视频播放进度
static Future heartBeat({bvid, cid, progress, realtime}) async { static Future heartBeat({bvid, cid, progress, realtime}) async {
await Request().post(Api.heartBeat, queryParameters: { await Request().post(
// 'aid': aid, Api.heartBeat,
'bvid': bvid, data: {
'cid': cid, // 'aid': aid,
// 'epid': '', 'bvid': bvid,
// 'sid': '', 'cid': cid,
// 'mid': '', // 'epid': '',
'played_time': progress, // 'sid': '',
// 'realtime': realtime, // 'mid': '',
// 'type': '', 'played_time': progress,
// 'sub_type': '', // 'realtime': realtime,
'csrf': await Request.getCsrf(), // 'type': '',
}); // 'sub_type': '',
'csrf': await Request.getCsrf(),
},
);
} }
// 添加追番 // 添加追番
static Future bangumiAdd({int? seasonId}) async { static Future bangumiAdd({int? seasonId}) async {
var res = await Request().post(Api.bangumiAdd, queryParameters: { var res = await Request().post(
'season_id': seasonId, Api.bangumiAdd,
'csrf': await Request.getCsrf(), data: {
}); 'season_id': seasonId,
'csrf': await Request.getCsrf(),
},
);
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return {'status': true, 'msg': res.data['result']['toast']}; return {'status': true, 'msg': res.data['result']['toast']};
} else { } else {
@ -427,10 +462,13 @@ class VideoHttp {
// 取消追番 // 取消追番
static Future bangumiDel({int? seasonId}) async { static Future bangumiDel({int? seasonId}) async {
var res = await Request().post(Api.bangumiDel, queryParameters: { var res = await Request().post(
'season_id': seasonId, Api.bangumiDel,
'csrf': await Request.getCsrf(), data: {
}); 'season_id': seasonId,
'csrf': await Request.getCsrf(),
},
);
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return {'status': true, 'msg': res.data['result']['toast']}; return {'status': true, 'msg': res.data['result']['toast']};
} else { } else {
@ -473,10 +511,11 @@ class VideoHttp {
} }
} }
static Future getSubtitle({int? cid, String? bvid}) async { static Future getSubtitle({int? cid, String? bvid, String? aid}) async {
var res = await Request().get(Api.getSubtitleConfig, data: { var res = await Request().get(Api.getSubtitleConfig, data: {
'cid': cid, 'cid': cid,
'bvid': bvid, if (bvid != null) 'bvid': bvid,
if (aid != null) 'aid': aid,
}); });
try { try {
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
@ -518,8 +557,70 @@ class VideoHttp {
// 获取字幕内容 // 获取字幕内容
static Future<Map<String, dynamic>> getSubtitleContent(url) async { static Future<Map<String, dynamic>> getSubtitleContent(url) async {
var res = await Request().get('https:$url'); var res = await Request().get('https:$url');
final String content = SubTitleUtils.convertToWebVTT(res.data['body']); final String content =
await SubTitleUtils.convertToWebVTT(res.data['body']);
final List body = res.data['body']; final List body = res.data['body'];
return {'content': content, 'body': body}; return {'content': content, 'body': body};
} }
static Future<Map<String, dynamic>> getSubscribeStatus(
{required dynamic bvid}) async {
var res = await Request().get(
Api.videoRelation,
data: {
'aid': IdUtils.bv2av(bvid),
'bvid': bvid,
},
);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data'],
};
} else {
return {
'status': false,
'msg': res.data['message'],
};
}
}
static Future seasonFav({
required bool isFav,
required dynamic seasonId,
}) async {
var res = await Request().post(
isFav ? Api.cancelSub : Api.confirmSub,
data: {
'platform': 'web',
'season_id': seasonId,
'csrf': await Request.getCsrf(),
},
);
if (res.data['code'] == 0) {
return {
'status': true,
};
} else {
return {
'status': false,
'msg': res.data['message'],
};
}
}
// 获取视频标签
static Future getVideoTag({required String bvid}) async {
var res = await Request().get(Api.videoTag, data: {'bvid': bvid});
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data'].map<VideoTagItem>((e) {
return VideoTagItem.fromJson(e);
}).toList()
};
} else {
return {'status': false, 'data': [], 'msg': res.data['message']};
}
}
} }

View File

@ -10,6 +10,7 @@ import 'package:flutter/material.dart';
import 'package:dynamic_color/dynamic_color.dart'; import 'package:dynamic_color/dynamic_color.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/common/widgets/custom_toast.dart'; import 'package:pilipala/common/widgets/custom_toast.dart';
import 'package:pilipala/http/common.dart';
import 'package:pilipala/http/init.dart'; import 'package:pilipala/http/init.dart';
import 'package:pilipala/models/common/color_type.dart'; import 'package:pilipala/models/common/color_type.dart';
import 'package:pilipala/models/common/theme_type.dart'; import 'package:pilipala/models/common/theme_type.dart';
@ -32,7 +33,7 @@ void main() async {
MediaKit.ensureInitialized(); MediaKit.ensureInitialized();
await SystemChrome.setPreferredOrientations( await SystemChrome.setPreferredOrientations(
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]); [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
await GStrorage.init(); await GStorage.init();
clearLogs(); clearLogs();
Request(); Request();
await Request.setCookie(); await Request.setCookie();
@ -60,11 +61,13 @@ void main() async {
systemNavigationBarColor: Colors.transparent, systemNavigationBarColor: Colors.transparent,
systemNavigationBarDividerColor: Colors.transparent, systemNavigationBarDividerColor: Colors.transparent,
statusBarColor: Colors.transparent, statusBarColor: Colors.transparent,
systemNavigationBarContrastEnforced: false,
)); ));
} }
PiliSchame.init(); PiliSchame.init();
await GlobalDataCache().initialize(); await GlobalDataCache.initialize();
CommonHttp.buvidActivate();
} }
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
@ -72,7 +75,7 @@ class MyApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Box setting = GStrorage.setting; Box setting = GStorage.setting;
// 主题色 // 主题色
Color defaultColor = Color defaultColor =
colorThemeTypes[setting.get(SettingBoxKey.customColor, defaultValue: 0)] colorThemeTypes[setting.get(SettingBoxKey.customColor, defaultValue: 0)]
@ -221,13 +224,23 @@ class BuildMainApp extends StatelessWidget {
elevation: 20, elevation: 20,
); );
return GetMaterialApp( AppBarTheme appBarTheme(ColorScheme colorScheme) {
title: 'PiliPala', return AppBarTheme(
theme: ThemeData( backgroundColor: colorScheme.surface,
colorScheme: currentThemeValue == ThemeType.dark foregroundColor: colorScheme.onSurface,
? darkColorScheme elevation: 0,
: lightColorScheme, titleSpacing: 0,
scrolledUnderElevation: 0,
// titleTextStyle: TextStyle(
// fontSize: Theme.of(context).textTheme.titleLarge!.fontSize),
);
}
ThemeData buildThemeData(ColorScheme colorScheme) {
return ThemeData(
colorScheme: colorScheme,
snackBarTheme: snackBarTheme, snackBarTheme: snackBarTheme,
appBarTheme: appBarTheme(colorScheme),
pageTransitionsTheme: const PageTransitionsTheme( pageTransitionsTheme: const PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{ builders: <TargetPlatform, PageTransitionsBuilder>{
TargetPlatform.android: ZoomPageTransitionsBuilder( TargetPlatform.android: ZoomPageTransitionsBuilder(
@ -235,12 +248,20 @@ class BuildMainApp extends StatelessWidget {
), ),
}, },
), ),
);
}
return GetMaterialApp(
title: 'PiliPala',
theme: buildThemeData(
currentThemeValue == ThemeType.dark
? darkColorScheme
: lightColorScheme,
), ),
darkTheme: ThemeData( darkTheme: buildThemeData(
colorScheme: currentThemeValue == ThemeType.light currentThemeValue == ThemeType.light
? lightColorScheme ? lightColorScheme
: darkColorScheme, : darkColorScheme,
snackBarTheme: snackBarTheme,
), ),
localizationsDelegates: const [ localizationsDelegates: const [
GlobalCupertinoLocalizations.delegate, GlobalCupertinoLocalizations.delegate,

View File

@ -144,6 +144,7 @@ class EpisodeItem {
this.link, this.link,
this.longTitle, this.longTitle,
this.pubTime, this.pubTime,
this.pubdate,
this.pv, this.pv,
this.releaseDate, this.releaseDate,
this.rights, this.rights,
@ -155,6 +156,7 @@ class EpisodeItem {
this.subtitle, this.subtitle,
this.title, this.title,
this.vid, this.vid,
this.stat,
}); });
int? aid; int? aid;
@ -173,6 +175,7 @@ class EpisodeItem {
String? link; String? link;
String? longTitle; String? longTitle;
int? pubTime; int? pubTime;
int? pubdate;
int? pv; int? pv;
String? releaseDate; String? releaseDate;
Map? rights; Map? rights;
@ -184,6 +187,7 @@ class EpisodeItem {
String? subtitle; String? subtitle;
String? title; String? title;
String? vid; String? vid;
String? stat;
EpisodeItem.fromJson(Map<String, dynamic> json) { EpisodeItem.fromJson(Map<String, dynamic> json) {
aid = json['aid']; aid = json['aid'];
@ -202,6 +206,7 @@ class EpisodeItem {
link = json['link']; link = json['link'];
longTitle = json['long_title']; longTitle = json['long_title'];
pubTime = json['pub_time']; pubTime = json['pub_time'];
pubdate = json['pub_time'];
pv = json['pv']; pv = json['pv'];
releaseDate = json['release_date']; releaseDate = json['release_date'];
rights = json['rights']; rights = json['rights'];
@ -211,7 +216,7 @@ class EpisodeItem {
skip = json['skip']; skip = json['skip'];
status = json['status']; status = json['status'];
subtitle = json['subtitle']; subtitle = json['subtitle'];
title = json['title']; title = json['long_title'];
vid = json['vid']; vid = json['vid'];
} }
} }

View File

@ -47,6 +47,7 @@ class BangumiListItemModel {
this.title, this.title,
this.titleIcon, this.titleIcon,
this.progress, this.progress,
this.progressIndex,
}); });
String? badge; String? badge;
@ -66,8 +67,8 @@ class BangumiListItemModel {
String? subTitle; String? subTitle;
String? title; String? title;
String? titleIcon; String? titleIcon;
String? progress; String? progress;
int? progressIndex;
BangumiListItemModel.fromJson(Map<String, dynamic> json) { BangumiListItemModel.fromJson(Map<String, dynamic> json) {
badge = json['badge'] == '' ? null : json['badge']; badge = json['badge'] == '' ? null : json['badge'];
@ -87,7 +88,9 @@ class BangumiListItemModel {
subTitle = json['sub_title']; subTitle = json['sub_title'];
title = json['title']; title = json['title'];
titleIcon = json['title_icon']; titleIcon = json['title_icon'];
progress = json['progress']; progress = json['progress'];
progressIndex = int.parse(
RegExp(r'第(\d+)话').firstMatch(json['progress'] ?? '第1话')?.group(1) ??
'0');
} }
} }

View File

@ -0,0 +1,18 @@
enum CommentRangeType {
video,
bangumi,
// dynamic,
}
extension ActionTypeExtension on CommentRangeType {
String get value => [
'video',
'bangumi',
// 'dynamic',
][index];
String get label => [
'视频',
'番剧',
// '动态',
][index];
}

View File

@ -4,9 +4,12 @@ enum FullScreenGestureMode {
/// 从下滑到上 /// 从下滑到上
fromBottomtoTop, fromBottomtoTop,
/// 关闭手势
none,
} }
extension FullScreenGestureModeExtension on FullScreenGestureMode { extension FullScreenGestureModeExtension on FullScreenGestureMode {
String get values => ['fromToptoBottom', 'fromBottomtoTop'][index]; String get values => ['fromToptoBottom', 'fromBottomtoTop', 'none'][index];
String get labels => ['从上往下滑进入全屏', '从下往上滑进入全屏'][index]; String get labels => ['从上往下滑进入全屏', '从下往上滑进入全屏', '关闭手势'][index];
} }

View File

@ -0,0 +1,73 @@
class InvalidVideoModel {
final int? id;
final int? ver;
final int? aid;
final String? lastupdate;
final int? lastupdatets;
final String? title;
final String? description;
final String? pic;
final int? tid;
final String? typename;
final int? created;
final String? createdAt;
final String? author;
final int? mid;
final String? play;
final String? coins;
final String? review;
final String? videoReview;
final String? favorites;
final String? tag;
final List<String>? tagList;
InvalidVideoModel({
this.id,
this.ver,
this.aid,
this.lastupdate,
this.lastupdatets,
this.title,
this.description,
this.pic,
this.tid,
this.typename,
this.created,
this.createdAt,
this.author,
this.mid,
this.play,
this.coins,
this.review,
this.videoReview,
this.favorites,
this.tag,
this.tagList,
});
factory InvalidVideoModel.fromJson(Map<String, dynamic> json) {
return InvalidVideoModel(
id: json['id'],
ver: json['ver'],
aid: json['aid'],
lastupdate: json['lastupdate'],
lastupdatets: json['lastupdatets'],
title: json['title'],
description: json['description'],
pic: json['pic'],
tid: json['tid'],
typename: json['typename'],
created: json['created'],
createdAt: json['created_at'],
author: json['author'],
mid: json['mid'],
play: json['play'],
coins: json['coins'],
review: json['review'],
videoReview: json['video_review'],
favorites: json['favorites'],
tag: json['tag'],
tagList: json['tag'].toString().split(',').toList(),
);
}
}

View File

@ -1,8 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pilipala/pages/mine/index.dart';
import '../../pages/dynamics/index.dart'; import '../../pages/dynamics/index.dart';
import '../../pages/home/index.dart'; import '../../pages/home/index.dart';
import '../../pages/media/index.dart';
import '../../pages/rank/index.dart'; import '../../pages/rank/index.dart';
List defaultNavigationBars = [ List defaultNavigationBars = [
@ -51,15 +50,15 @@ List defaultNavigationBars = [
{ {
'id': 3, 'id': 3,
'icon': const Icon( 'icon': const Icon(
Icons.video_collection_outlined, Icons.person_outline,
size: 20, size: 20,
), ),
'selectIcon': const Icon( 'selectIcon': const Icon(
Icons.video_collection, Icons.person,
size: 21, size: 21,
), ),
'label': "媒体库", 'label': "我的",
'count': 0, 'count': 0,
'page': const MediaPage(), 'page': const MinePage(),
} },
]; ];

View File

@ -28,7 +28,7 @@ extension SearchTypeExtension on SearchType {
String get label => ['视频', '番剧', '直播间', '用户', '专栏'][index]; String get label => ['视频', '番剧', '直播间', '用户', '专栏'][index];
} }
// 搜索类型为视频、专栏及相簿 // 搜索类型为视频时
enum ArchiveFilterType { enum ArchiveFilterType {
totalrank, totalrank,
click, click,
@ -44,3 +44,21 @@ extension ArchiveFilterTypeExtension on ArchiveFilterType {
String get description => String get description =>
['默认排序', '播放多', '新发布', '弹幕多', '收藏多', '评论多', '最多喜欢'][index]; ['默认排序', '播放多', '新发布', '弹幕多', '收藏多', '评论多', '最多喜欢'][index];
} }
// 搜索类型为专栏时
enum ArticleFilterType {
// 综合排序
totalrank,
// 最新发布
pubdate,
// 最多点击
click,
// 最多喜欢
attention,
// 最多评论
scores,
}
extension ArticleFilterTypeExtension on ArticleFilterType {
String get description => ['综合排序', '最新发布', '最多点击', '最多喜欢', '最多评论'][index];
}

View File

@ -63,7 +63,7 @@ class LiveFollowingItemModel {
String? roomNews; String? roomNews;
String? watchIcon; String? watchIcon;
String? textSmall; String? textSmall;
String? roomCover; String? cover;
String? pic; String? pic;
int? parentAreaId; int? parentAreaId;
int? areaId; int? areaId;
@ -90,7 +90,7 @@ class LiveFollowingItemModel {
this.roomNews, this.roomNews,
this.watchIcon, this.watchIcon,
this.textSmall, this.textSmall,
this.roomCover, this.cover,
this.pic, this.pic,
this.parentAreaId, this.parentAreaId,
this.areaId, this.areaId,
@ -108,7 +108,8 @@ class LiveFollowingItemModel {
isAttention = json['is_attention']; isAttention = json['is_attention'];
clipNum = json['clipnum']; clipNum = json['clipnum'];
fansNum = json['fans_num']; fansNum = json['fans_num'];
areaName = json['area_name']; areaName =
json['area_name'] == '' ? json['area_name_v2'] : json['area_name'];
areaValue = json['area_value']; areaValue = json['area_value'];
tags = json['tags']; tags = json['tags'];
recentRecordIdV2 = json['recent_record_id_v2']; recentRecordIdV2 = json['recent_record_id_v2'];
@ -118,7 +119,7 @@ class LiveFollowingItemModel {
roomNews = json['room_news']; roomNews = json['room_news'];
watchIcon = json['watch_icon']; watchIcon = json['watch_icon'];
textSmall = json['text_small']; textSmall = json['text_small'];
roomCover = json['room_cover']; cover = json['room_cover'];
pic = json['room_cover']; pic = json['room_cover'];
parentAreaId = json['parent_area_id']; parentAreaId = json['parent_area_id'];
areaId = json['area_id']; areaId = json['area_id'];

View File

@ -0,0 +1,46 @@
class MemberArticleDataModel {
MemberArticleDataModel({
this.hasMore,
this.items,
this.offset,
this.updateNum,
});
bool? hasMore;
List<MemberArticleItemModel>? items;
String? offset;
int? updateNum;
MemberArticleDataModel.fromJson(Map<String, dynamic> json) {
hasMore = json['has_more'];
items = json['items']
.map<MemberArticleItemModel>((e) => MemberArticleItemModel.fromJson(e))
.toList();
offset = json['offset'];
updateNum = json['update_num'];
}
}
class MemberArticleItemModel {
MemberArticleItemModel({
this.content,
this.cover,
this.jumpUrl,
this.opusId,
this.stat,
});
String? content;
Map? cover;
String? jumpUrl;
String? opusId;
Map? stat;
MemberArticleItemModel.fromJson(Map<String, dynamic> json) {
content = json['content'];
cover = json['cover'];
jumpUrl = json['jump_url'];
opusId = json['opus_id'];
stat = json['stat'];
}
}

View File

@ -8,6 +8,7 @@ class MemberInfoModel {
this.level, this.level,
this.isFollowed, this.isFollowed,
this.topPhoto, this.topPhoto,
this.silence,
this.official, this.official,
this.vip, this.vip,
this.liveRoom, this.liveRoom,
@ -21,6 +22,7 @@ class MemberInfoModel {
int? level; int? level;
bool? isFollowed; bool? isFollowed;
String? topPhoto; String? topPhoto;
int? silence;
Map? official; Map? official;
Vip? vip; Vip? vip;
LiveRoom? liveRoom; LiveRoom? liveRoom;
@ -34,6 +36,7 @@ class MemberInfoModel {
level = json['level']; level = json['level'];
isFollowed = json['is_followed']; isFollowed = json['is_followed'];
topPhoto = json['top_photo']; topPhoto = json['top_photo'];
silence = json['silence'] ?? 0;
official = json['official']; official = json['official'];
vip = Vip.fromJson(json['vip']); vip = Vip.fromJson(json['vip']);
liveRoom = liveRoom =

140
lib/models/msg/at.dart Normal file
View File

@ -0,0 +1,140 @@
class MessageAtModel {
Cursor? cursor;
List<MessageAtItems>? items;
MessageAtModel({this.cursor, this.items});
MessageAtModel.fromJson(Map<String, dynamic> json) {
cursor = json['cursor'] != null ? Cursor.fromJson(json['cursor']) : null;
if (json['items'] != null) {
items = <MessageAtItems>[];
json['items'].forEach((v) {
items!.add(MessageAtItems.fromJson(v));
});
}
}
}
class Cursor {
Cursor({
this.id,
this.isEnd,
this.time,
});
int? id;
bool? isEnd;
int? time;
Cursor.fromJson(Map<String, dynamic> json) {
id = json['id'];
isEnd = json['isEnd'];
time = json['time'];
}
}
class MessageAtItems {
int? id;
int? atTime;
User? user;
MessageAtItem? item;
MessageAtItems({this.id, this.atTime, this.user, this.item});
MessageAtItems.fromJson(Map<String, dynamic> json) {
id = json['id'];
atTime = json['at_time'];
user = json['user'] != null ? User.fromJson(json['user']) : null;
item = json['item'] != null ? MessageAtItem.fromJson(json['item']) : null;
}
}
class MessageAtItem {
String? type;
String? business;
int? businessId;
String? title;
String? image;
String? uri;
int? subjectId;
int? rootId;
int? targetId;
int? sourceId;
String? sourceContent;
String? nativeUri;
List<User>? atDetails;
List<dynamic>? topicDetails;
bool? hideReplyButton;
MessageAtItem({
this.type,
this.business,
this.businessId,
this.title,
this.image,
this.uri,
this.subjectId,
this.rootId,
this.targetId,
this.sourceId,
this.sourceContent,
this.nativeUri,
this.atDetails,
this.topicDetails,
this.hideReplyButton,
});
MessageAtItem.fromJson(Map<String, dynamic> json) {
type = json['type'];
business = json['business'];
businessId = json['business_id'];
title = json['title'];
image = json['image'];
uri = json['uri'];
subjectId = json['subject_id'];
rootId = json['root_id'];
targetId = json['target_id'];
sourceId = json['source_id'];
sourceContent = json['source_content'];
nativeUri = json['native_uri'];
if (json['at_details'] != null) {
atDetails = <User>[];
json['at_details'].forEach((v) {
atDetails!.add(User.fromJson(v));
});
}
if (json['topic_details'] != null) {
topicDetails = <dynamic>[];
json['topic_details'].forEach((v) {
topicDetails!.add(v);
});
}
hideReplyButton = json['hide_reply_button'];
}
}
class User {
int? mid;
int? fans;
String? nickname;
String? avatar;
String? midLink;
bool? follow;
User(
{this.mid,
this.fans,
this.nickname,
this.avatar,
this.midLink,
this.follow});
User.fromJson(Map<String, dynamic> json) {
mid = json['mid'];
fans = json['fans'];
nickname = json['nickname'];
avatar = json['avatar'];
midLink = json['mid_link'];
follow = json['follow'];
}
}

View File

@ -39,7 +39,7 @@ class Total {
List<MessageLikeItem>? items; List<MessageLikeItem>? items;
factory Total.fromJson(Map<String, dynamic> json) => Total( factory Total.fromJson(Map<String, dynamic> json) => Total(
cursor: Cursor.fromJson(json['cursor']), cursor: json['cursor'] != null ? Cursor.fromJson(json['cursor']) : null,
items: json["items"] == null items: json["items"] == null
? [] ? []
: json["items"].map<MessageLikeItem>((e) { : json["items"].map<MessageLikeItem>((e) {

View File

@ -13,8 +13,9 @@ class SessionDataModel {
SessionDataModel.fromJson(Map<String, dynamic> json) { SessionDataModel.fromJson(Map<String, dynamic> json) {
sessionList = json['session_list'] sessionList = json['session_list']
?.map<SessionList>((e) => SessionList.fromJson(e)) ?.map<SessionList>((e) => SessionList.fromJson(e))
.toList(); .toList() ??
[];
hasMore = json['has_more']; hasMore = json['has_more'];
} }
} }

View File

@ -5,7 +5,7 @@ class MessageSystemModel {
int? cursor; int? cursor;
int? type; int? type;
String? title; String? title;
Map? content; dynamic content;
Source? source; Source? source;
String? timeAt; String? timeAt;
int? cardType; int? cardType;
@ -45,7 +45,9 @@ class MessageSystemModel {
cursor: jsons["cursor"], cursor: jsons["cursor"],
type: jsons["type"], type: jsons["type"],
title: jsons["title"], title: jsons["title"],
content: json.decode(jsons["content"]), content: isValidJson(jsons["content"])
? json.decode(jsons["content"])
: jsons["content"],
source: Source.fromJson(jsons["source"]), source: Source.fromJson(jsons["source"]),
timeAt: jsons["time_at"], timeAt: jsons["time_at"],
cardType: jsons["card_type"], cardType: jsons["card_type"],
@ -75,3 +77,12 @@ class Source {
logo: json["logo"], logo: json["logo"],
); );
} }
bool isValidJson(String str) {
try {
json.decode(str);
} catch (e) {
return false;
}
return true;
}

485
lib/models/read/opus.dart Normal file
View File

@ -0,0 +1,485 @@
class OpusDataModel {
OpusDataModel({
this.id,
this.detail,
this.type,
this.theme,
this.themeMode,
});
String? id;
OpusDetailDataModel? detail;
int? type;
String? theme;
String? themeMode;
OpusDataModel.fromJson(Map<String, dynamic> json) {
id = json['id'];
detail = json['detail'] != null
? OpusDetailDataModel.fromJson(json['detail'])
: null;
type = json['type'];
theme = json['theme'];
themeMode = json['themeMode'];
}
}
class OpusDetailDataModel {
OpusDetailDataModel({
this.basic,
this.idStr,
this.modules,
this.type,
});
Basic? basic;
String? idStr;
List<OpusModuleDataModel>? modules;
int? type;
OpusDetailDataModel.fromJson(Map<String, dynamic> json) {
basic = json['basic'] != null ? Basic.fromJson(json['basic']) : null;
idStr = json['id_str'];
if (json['modules'] != null) {
modules = <OpusModuleDataModel>[];
json['modules'].forEach((v) {
modules!.add(OpusModuleDataModel.fromJson(v));
});
}
type = json['type'];
}
}
class Basic {
Basic({
this.commentIdStr,
this.commentType,
this.ridStr,
this.title,
this.uid,
});
String? commentIdStr;
int? commentType;
String? ridStr;
String? title;
int? uid;
Basic.fromJson(Map<String, dynamic> json) {
commentIdStr = json['comment_id_str'];
commentType = json['comment_type'];
ridStr = json['rid_str'];
title = json['title'];
uid = json['uid'];
}
}
class OpusModuleDataModel {
OpusModuleDataModel({
this.moduleTitle,
this.moduleAuthor,
this.moduleContent,
this.moduleExtend,
this.moduleBottom,
this.moduleStat,
});
ModuleTop? moduleTop;
ModuleTitle? moduleTitle;
ModuleAuthor? moduleAuthor;
ModuleContent? moduleContent;
ModuleExtend? moduleExtend;
ModuleBottom? moduleBottom;
ModuleStat? moduleStat;
OpusModuleDataModel.fromJson(Map<String, dynamic> json) {
moduleTop = json['module_top'] != null
? ModuleTop.fromJson(json['module_top'])
: null;
moduleTitle = json['module_title'] != null
? ModuleTitle.fromJson(json['module_title'])
: null;
moduleAuthor = json['module_author'] != null
? ModuleAuthor.fromJson(json['module_author'])
: null;
moduleContent = json['module_content'] != null
? ModuleContent.fromJson(json['module_content'])
: null;
moduleExtend = json['module_extend'] != null
? ModuleExtend.fromJson(json['module_extend'])
: null;
moduleBottom = json['module_bottom'] != null
? ModuleBottom.fromJson(json['module_bottom'])
: null;
moduleStat = json['module_stat'] != null
? ModuleStat.fromJson(json['module_stat'])
: null;
}
}
class ModuleTop {
ModuleTop({
this.type,
this.video,
});
int? type;
Map? video;
ModuleTop.fromJson(Map<String, dynamic> json) {
type = json['type'];
video = json['video'];
}
}
class ModuleTitle {
ModuleTitle({
this.text,
});
String? text;
ModuleTitle.fromJson(Map<String, dynamic> json) {
text = json['text'];
}
}
class ModuleAuthor {
ModuleAuthor({
this.face,
this.mid,
this.name,
this.pubTime,
});
String? face;
int? mid;
String? name;
String? pubTime;
ModuleAuthor.fromJson(Map<String, dynamic> json) {
face = json['face'];
mid = json['mid'];
name = json['name'];
pubTime = json['pub_time'];
}
}
class ModuleContent {
ModuleContent({
this.paragraphs,
this.moduleType,
});
List<ModuleParagraph>? paragraphs;
String? moduleType;
ModuleContent.fromJson(Map<String, dynamic> json) {
if (json['paragraphs'] != null) {
paragraphs = <ModuleParagraph>[];
json['paragraphs'].forEach((v) {
paragraphs!.add(ModuleParagraph.fromJson(v));
});
}
moduleType = json['module_type'];
}
}
class ModuleParagraph {
ModuleParagraph({
this.align,
this.paraType,
this.pic,
this.text,
});
// 0 左对齐 1 居中 2 右对齐
int? align;
int? paraType;
Pics? pic;
ModuleParagraphText? text;
LinkCard? linkCard;
ModuleParagraph.fromJson(Map<String, dynamic> json) {
align = json['align'];
paraType = json['para_type'] == null && json['link_card'] != null
? 3
: json['para_type'];
pic = json['pic'] != null ? Pics.fromJson(json['pic']) : null;
text = json['text'] != null
? ModuleParagraphText.fromJson(json['text'])
: null;
linkCard =
json['link_card'] != null ? LinkCard.fromJson(json['link_card']) : null;
}
}
class Pics {
Pics({
this.pics,
this.style,
});
List<Pic>? pics;
int? style;
Pics.fromJson(Map<String, dynamic> json) {
if (json['pics'] != null) {
pics = <Pic>[];
json['pics'].forEach((v) {
pics!.add(Pic.fromJson(v));
});
}
style = json['style'];
}
}
class Pic {
Pic({
this.height,
this.size,
this.url,
this.width,
this.aspectRatio,
this.scale,
});
int? height;
double? size;
String? url;
int? width;
double? aspectRatio;
double? scale;
Pic.fromJson(Map<String, dynamic> json) {
height = json['height'];
size = json['size'];
url = json['url'];
width = json['width'];
aspectRatio = json['width'] / json['height'];
scale = customDivision(json['width'], 600);
}
}
class LinkCard {
LinkCard({
this.cover,
this.descSecond,
this.duration,
this.jumpUrl,
this.title,
});
String? cover;
String? descSecond;
String? duration;
String? jumpUrl;
String? title;
LinkCard.fromJson(Map<String, dynamic> json) {
cover = json['card']['cover'];
descSecond = json['card']['desc_second'];
duration = json['card']['duration'];
jumpUrl = json['card']['jump_url'];
title = json['card']['title'];
}
}
class ModuleParagraphText {
ModuleParagraphText({
this.nodes,
});
List<ModuleParagraphTextNode>? nodes;
ModuleParagraphText.fromJson(Map<String, dynamic> json) {
if (json['nodes'] != null) {
nodes = <ModuleParagraphTextNode>[];
json['nodes'].forEach((v) {
nodes!.add(ModuleParagraphTextNode.fromJson(v));
});
}
}
}
class ModuleParagraphTextNode {
ModuleParagraphTextNode({
this.type,
this.nodeType,
this.word,
});
String? type;
int? nodeType;
ModuleParagraphTextNodeWord? word;
ModuleParagraphTextNode.fromJson(Map<String, dynamic> json) {
type = json['type'];
nodeType = json['node_type'];
word = json['word'] != null
? ModuleParagraphTextNodeWord.fromJson(json['word'])
: null;
}
}
class ModuleParagraphTextNodeWord {
ModuleParagraphTextNodeWord({
this.color,
this.fontSize,
this.style,
this.words,
});
String? color;
int? fontSize;
ModuleParagraphTextNodeWordStyle? style;
String? words;
ModuleParagraphTextNodeWord.fromJson(Map<String, dynamic> json) {
color = json['color'];
fontSize = json['font_size'];
style = json['style'] != null
? ModuleParagraphTextNodeWordStyle.fromJson(json['style'])
: null;
words = json['words'];
}
}
class ModuleParagraphTextNodeWordStyle {
ModuleParagraphTextNodeWordStyle({
this.bold,
});
bool? bold;
ModuleParagraphTextNodeWordStyle.fromJson(Map<String, dynamic> json) {
bold = json['bold'];
}
}
class ModuleExtend {
ModuleExtend({
this.items,
});
List<ModuleExtendItem>? items;
ModuleExtend.fromJson(Map<String, dynamic> json) {
if (json['items'] != null) {
items = <ModuleExtendItem>[];
json['items'].forEach((v) {
items!.add(ModuleExtendItem.fromJson(v));
});
}
}
}
class ModuleExtendItem {
ModuleExtendItem({
this.bizId,
this.bizType,
this.icon,
this.jumpUrl,
this.text,
});
dynamic bizId;
int? bizType;
dynamic icon;
String? jumpUrl;
String? text;
ModuleExtendItem.fromJson(Map<String, dynamic> json) {
bizId = json['biz_id'];
bizType = json['biz_type'];
icon = json['icon'];
jumpUrl = json['jump_url'];
text = json['text'];
}
}
class ModuleBottom {
ModuleBottom({
this.shareInfo,
});
ShareInfo? shareInfo;
ModuleBottom.fromJson(Map<String, dynamic> json) {
shareInfo = json['share_info'] != null
? ShareInfo.fromJson(json['share_info'])
: null;
}
}
class ShareInfo {
ShareInfo({
this.pic,
this.summary,
this.title,
});
String? pic;
String? summary;
String? title;
ShareInfo.fromJson(Map<String, dynamic> json) {
pic = json['pic'];
summary = json['summary'];
title = json['title'];
}
}
class ModuleStat {
ModuleStat({
this.coin,
this.comment,
this.favorite,
this.forward,
this.like,
});
StatItem? coin;
StatItem? comment;
StatItem? favorite;
StatItem? forward;
StatItem? like;
ModuleStat.fromJson(Map<String, dynamic> json) {
coin = json['coin'] != null ? StatItem.fromJson(json['coin']) : null;
comment =
json['comment'] != null ? StatItem.fromJson(json['comment']) : null;
favorite =
json['favorite'] != null ? StatItem.fromJson(json['favorite']) : null;
forward =
json['forward'] != null ? StatItem.fromJson(json['forward']) : null;
like = json['like'] != null ? StatItem.fromJson(json['like']) : null;
}
}
class StatItem {
StatItem({
this.count,
this.forbidden,
this.status,
});
int? count;
bool? forbidden;
bool? status;
StatItem.fromJson(Map<String, dynamic> json) {
count = json['count'];
forbidden = json['forbidden'];
status = json['status'];
}
}
double customDivision(int a, int b) {
double result = a / b;
if (result < 1) {
return result;
} else {
return 1.0;
}
}

286
lib/models/read/read.dart Normal file
View File

@ -0,0 +1,286 @@
import 'package:pilipala/models/member/info.dart';
import 'opus.dart';
class ReadDataModel {
ReadDataModel({
this.cvid,
this.readInfo,
this.readViewInfo,
this.upInfo,
this.catalogList,
this.recommendInfoList,
this.hiddenInteraction,
this.isModern,
});
int? cvid;
ReadInfo? readInfo;
Map? readViewInfo;
Map? upInfo;
List<dynamic>? catalogList;
List<dynamic>? recommendInfoList;
bool? hiddenInteraction;
bool? isModern;
ReadDataModel.fromJson(Map<String, dynamic> json) {
cvid = json['cvid'];
readInfo =
json['readInfo'] != null ? ReadInfo.fromJson(json['readInfo']) : null;
readViewInfo = json['readViewInfo'];
upInfo = json['upInfo'];
if (json['catalogList'] != null) {
catalogList = <dynamic>[];
json['catalogList'].forEach((v) {
catalogList!.add(v);
});
}
if (json['recommendInfoList'] != null) {
recommendInfoList = <dynamic>[];
json['recommendInfoList'].forEach((v) {
recommendInfoList!.add(v);
});
}
hiddenInteraction = json['hiddenInteraction'];
isModern = json['isModern'];
}
}
class ReadInfo {
ReadInfo({
this.id,
this.category,
this.title,
this.summary,
this.bannerUrl,
this.author,
this.publishTime,
this.ctime,
this.mtime,
this.stats,
this.attributes,
this.words,
this.originImageUrls,
this.content,
this.opus,
this.dynIdStr,
this.totalArtNum,
});
int? id;
Map? category;
String? title;
String? summary;
String? bannerUrl;
Author? author;
int? publishTime;
int? ctime;
int? mtime;
Map? stats;
int? attributes;
int? words;
List<String>? originImageUrls;
String? content;
Opus? opus;
String? dynIdStr;
int? totalArtNum;
ReadInfo.fromJson(Map<String, dynamic> json) {
id = json['id'];
category = json['category'];
title = json['title'];
summary = json['summary'];
bannerUrl = json['banner_url'];
author = Author.fromJson(json['author']);
publishTime = json['publish_time'];
ctime = json['ctime'];
mtime = json['mtime'];
stats = json['stats'];
attributes = json['attributes'];
words = json['words'];
if (json['origin_image_urls'] != null) {
originImageUrls = <String>[];
json['origin_image_urls'].forEach((v) {
originImageUrls!.add(v);
});
}
content = json['content'];
opus = json['opus'] != null ? Opus.fromJson(json['opus']) : null;
dynIdStr = json['dyn_id_str'];
totalArtNum = json['total_art_num'];
}
}
class Author {
Author({
this.mid,
this.name,
this.face,
this.vip,
this.fans,
this.level,
});
int? mid;
String? name;
String? face;
Vip? vip;
int? fans;
int? level;
Author.fromJson(Map<String, dynamic> json) {
mid = json['mid'];
name = json['name'];
face = json['face'];
vip = json['vip'] != null ? Vip.fromJson(json['vip']) : null;
fans = json['fans'];
level = json['level'];
}
}
class Opus {
// "opus_id": 976625853207150600,
// "opus_source": 2,
// "title": "真的很想骂人 但又没什么好骂的",
// "content": {
// "paragraphs": [{
// "para_type": 1,
// "text": {
// "nodes": [{
// "node_type": 1,
// "word": {
// "words": "21年玩到今年4月的号没了 ow1的时候45的号 玩了三年 后面第9赛季一个英杰5的号虽然是偷的 但我任何违规行为都没有还是给我封了) 最近玩的号叫velleity 只和队友打天梯以及训练赛 又没了 连带着我一个一把没玩过只玩过一场训练赛的小号也没了 实在是无话可说了...",
// "font_size": 17,
// "style": {},
// "font_level": "regular"
// }
// }]
// }
// }, {
// "para_type": 2,
// "pic": {
// "pics": [{
// "url": "https:\u002F\u002Fi0.hdslb.com\u002Fbfs\u002Fnew_dyn\u002Fba4e57459451fe74dcb70fd20bde9823316082117.jpg",
// "width": 1600,
// "height": 1000,
// "size": 588.482421875
// }],
// "style": 1
// }
// }, {
// "para_type": 1,
// "text": {
// "nodes": [{
// "node_type": 1,
// "word": {
// "words": "\n",
// "font_size": 17,
// "style": {},
// "font_level": "regular"
// }
// }]
// }
// }, {
// "para_type": 2,
// "pic": {
// "pics": [{
// "url": "https:\u002F\u002Fi0.hdslb.com\u002Fbfs\u002Fnew_dyn\u002F0945be6b621091ddb8189482a87a36fb316082117.jpg",
// "width": 1600,
// "height": 1002,
// "size": 665.7861328125
// }],
// "style": 1
// }
// }, {
// "para_type": 1,
// "text": {
// "nodes": [{
// "node_type": 1,
// "word": {
// "words": "\n",
// "font_size": 17,
// "style": {},
// "font_level": "regular"
// }
// }]
// }
// }, {
// "para_type": 2,
// "pic": {
// "pics": [{
// "url": "https:\u002F\u002Fi0.hdslb.com\u002Fbfs\u002Fnew_dyn\u002Ffa60649f8786578a764a1e68a2c5d23f316082117.jpg",
// "width": 1600,
// "height": 999,
// "size": 332.970703125
// }],
// "style": 1
// }
// }, {
// "para_type": 1,
// "text": {
// "nodes": [{
// "node_type": 1,
// "word": {
// "words": "\n",
// "font_size": 17,
// "style": {},
// "font_level": "regular"
// }
// }]
// }
// }]
// },
// "pub_info": {
// "uid": 316082117,
// "pub_time": 1726226826
// },
// "article": {
// "category_id": 15,
// "cover": [{
// "url": "https:\u002F\u002Fi0.hdslb.com\u002Fbfs\u002Fnew_dyn\u002Fbanner\u002Feb10074186a62f98c18e1b5b9deb38be316082117.png",
// "width": 1071,
// "height": 315,
// "size": 225.625
// }]
// },
// "version": {
// "cvid": 38660379,
// "version_id": 101683514411343360
// }
Opus({
this.opusId,
this.opusSource,
this.title,
this.content,
});
int? opusId;
int? opusSource;
String? title;
Content? content;
Opus.fromJson(Map<String, dynamic> json) {
opusId = json['opus_id'];
opusSource = json['opus_source'];
title = json['title'];
content =
json['content'] != null ? Content.fromJson(json['content']) : null;
}
}
class Content {
Content({
this.paragraphs,
});
List<ModuleParagraph>? paragraphs;
Content.fromJson(Map<String, dynamic> json) {
if (json['paragraphs'] != null) {
paragraphs = <ModuleParagraph>[];
json['paragraphs'].forEach((v) {
paragraphs!.add(ModuleParagraph.fromJson(v));
});
}
}
}

View File

@ -0,0 +1,26 @@
// 片段类型枚举
enum ActionType {
skip,
mute,
full,
poi,
chapter,
}
extension ActionTypeExtension on ActionType {
String get value => [
'skip',
'mute',
'full',
'poi',
'chapter',
][index];
String get label => [
'跳过',
'静音',
'完整观看',
'亮点',
'章节切换',
][index];
}

View File

@ -0,0 +1,43 @@
import 'action_type.dart';
import 'segment_type.dart';
class SegmentDataModel {
final SegmentType? category;
final ActionType? actionType;
final List? segment;
final String? uuid;
final num? videoDuration;
final int? locked;
final int? votes;
final String? description;
// 是否已经跳过
bool isSkip = false;
SegmentDataModel({
this.category,
this.actionType,
this.segment,
this.uuid,
this.videoDuration,
this.locked,
this.votes,
this.description,
});
factory SegmentDataModel.fromJson(Map<String, dynamic> json) {
return SegmentDataModel(
category: SegmentType.values.firstWhere(
(e) => e.value == json['category'],
orElse: () => SegmentType.sponsor),
actionType: ActionType.values.firstWhere(
(e) => e.value == json['actionType'],
orElse: () => ActionType.skip),
segment: json['segment'],
uuid: json['UUID'],
videoDuration: json['videoDuration'],
locked: json['locked'],
votes: json['votes'],
description: json['description'],
);
}
}

View File

@ -0,0 +1,46 @@
// 片段类型枚举
// ignore_for_file: constant_identifier_names
enum SegmentType {
sponsor,
intro,
outro,
interaction,
selfpromo,
music_offtopic,
preview,
poi_highlight,
filler,
exclusive_access,
chapter,
}
extension SegmentTypeExtension on SegmentType {
String get value => [
'sponsor',
'intro',
'outro',
'interaction',
'selfpromo',
'music_offtopic',
'preview',
'poi_highlight',
'filler',
'exclusive_access',
'chapter',
][index];
String get label => [
'赞助',
'开场介绍',
'片尾致谢',
'互动',
'自我推广',
'音乐',
'预览',
'亮点',
'无效填充',
'独家访问',
'章节',
][index];
}

View File

@ -39,11 +39,11 @@ class ModelResult {
ModelResult.fromJson(Map<String, dynamic> json) { ModelResult.fromJson(Map<String, dynamic> json) {
resultType = json['result_type']; resultType = json['result_type'];
summary = json['summary']; summary = json['summary'];
outline = json['result_type'] == 2 outline = json['result_type'] == 0
? json['outline'] ? <OutlineItem>[]
: json['outline']
.map<OutlineItem>((e) => OutlineItem.fromJson(e)) .map<OutlineItem>((e) => OutlineItem.fromJson(e))
.toList() .toList();
: <OutlineItem>[];
} }
} }

276
lib/models/video/later.dart Normal file
View File

@ -0,0 +1,276 @@
class MediaVideoItemModel {
MediaVideoItemModel({
this.id,
this.aid,
this.offset,
this.index,
this.intro,
this.attr,
this.tid,
this.copyRight,
this.cntInfo,
this.cover,
this.duration,
this.pubtime,
this.likeState,
this.favState,
this.page,
this.cid,
this.pages,
this.title,
this.type,
this.upper,
this.link,
this.bvid,
this.shortLink,
this.rights,
this.elecInfo,
this.coin,
this.progressPercent,
this.badge,
this.forbidFav,
this.moreType,
this.businessOid,
});
int? id;
int? aid;
int? offset;
int? index;
String? intro;
int? attr;
int? tid;
int? copyRight;
Map? cntInfo;
String? cover;
int? duration;
int? pubtime;
int? likeState;
int? favState;
int? page;
int? cid;
List<Page>? pages;
String? title;
int? type;
Upper? upper;
String? link;
String? bvid;
String? shortLink;
Rights? rights;
dynamic elecInfo;
Coin? coin;
double? progressPercent;
dynamic badge;
bool? forbidFav;
int? moreType;
int? businessOid;
factory MediaVideoItemModel.fromJson(Map<String, dynamic> json) =>
MediaVideoItemModel(
id: json["id"],
aid: json["id"],
offset: json["offset"],
index: json["index"],
intro: json["intro"],
attr: json["attr"],
tid: json["tid"],
copyRight: json["copy_right"],
cntInfo: json["cnt_info"],
cover: json["cover"],
duration: json["duration"],
pubtime: json["pubtime"],
likeState: json["like_state"],
favState: json["fav_state"],
page: json["page"],
cid: json["pages"] == null ? -1 : json["pages"].first['id'],
// json["pages"] 可能为null
pages: json["pages"] == null
? []
: List<Page>.from(json["pages"].map((x) => Page.fromJson(x))),
title: json["title"],
type: json["type"],
upper: Upper.fromJson(json["upper"]),
link: json["link"],
bvid: json["bv_id"],
shortLink: json["short_link"],
rights: Rights.fromJson(json["rights"]),
elecInfo: json["elec_info"],
coin: Coin.fromJson(json["coin"]),
progressPercent: json["progress_percent"].toDouble(),
badge: json["badge"],
forbidFav: json["forbid_fav"],
moreType: json["more_type"],
businessOid: json["business_oid"],
);
}
class Coin {
Coin({
this.maxNum,
this.coinNumber,
});
int? maxNum;
int? coinNumber;
factory Coin.fromJson(Map<String, dynamic> json) => Coin(
maxNum: json["max_num"],
coinNumber: json["coin_number"],
);
}
class Page {
Page({
this.id,
this.title,
this.intro,
this.duration,
this.link,
this.page,
this.metas,
this.from,
this.dimension,
});
int? id;
String? title;
String? intro;
int? duration;
String? link;
int? page;
List<Meta>? metas;
String? from;
Dimension? dimension;
factory Page.fromJson(Map<String, dynamic> json) => Page(
id: json["id"],
title: json["title"],
intro: json["intro"],
duration: json["duration"],
link: json["link"],
page: json["page"],
metas: List<Meta>.from(json["metas"].map((x) => Meta.fromJson(x))),
from: json["from"],
dimension: Dimension.fromJson(json["dimension"]),
);
}
class Dimension {
Dimension({
this.width,
this.height,
this.rotate,
});
int? width;
int? height;
int? rotate;
factory Dimension.fromJson(Map<String, dynamic> json) => Dimension(
width: json["width"],
height: json["height"],
rotate: json["rotate"],
);
}
class Meta {
Meta({
this.quality,
this.size,
});
int? quality;
int? size;
factory Meta.fromJson(Map<String, dynamic> json) => Meta(
quality: json["quality"],
size: json["size"],
);
}
class Rights {
Rights({
this.bp,
this.elec,
this.download,
this.movie,
this.pay,
this.ugcPay,
this.hd5,
this.noReprint,
this.autoplay,
this.noBackground,
});
int? bp;
int? elec;
int? download;
int? movie;
int? pay;
int? ugcPay;
int? hd5;
int? noReprint;
int? autoplay;
int? noBackground;
factory Rights.fromJson(Map<String, dynamic> json) => Rights(
bp: json["bp"],
elec: json["elec"],
download: json["download"],
movie: json["movie"],
pay: json["pay"],
ugcPay: json["ugc_pay"],
hd5: json["hd5"],
noReprint: json["no_reprint"],
autoplay: json["autoplay"],
noBackground: json["no_background"],
);
}
class Upper {
Upper({
this.mid,
this.name,
this.face,
this.followed,
this.fans,
this.vipType,
this.vipStatue,
this.vipDueDate,
this.vipPayType,
this.officialRole,
this.officialTitle,
this.officialDesc,
this.displayName,
});
int? mid;
String? name;
String? face;
int? followed;
int? fans;
int? vipType;
int? vipStatue;
int? vipDueDate;
int? vipPayType;
int? officialRole;
String? officialTitle;
String? officialDesc;
String? displayName;
factory Upper.fromJson(Map<String, dynamic> json) => Upper(
mid: json["mid"],
name: json["name"],
face: json["face"],
followed: json["followed"],
fans: json["fans"],
vipType: json["vip_type"],
vipStatue: json["vip_statue"],
vipDueDate: json["vip_due_date"],
vipPayType: json["vip_pay_type"],
officialRole: json["official_role"],
officialTitle: json["official_title"],
officialDesc: json["official_desc"],
displayName: json["display_name"],
);
}

View File

@ -1,6 +1,7 @@
class ReplyContent { class ReplyContent {
ReplyContent({ ReplyContent({
this.message, this.message,
this.message2,
this.atNameToMid, // @的用户的mid null this.atNameToMid, // @的用户的mid null
this.members, // 被@的用户List 如果有的话 [] this.members, // 被@的用户List 如果有的话 []
this.emote, // 表情包 如果有的话 null this.emote, // 表情包 如果有的话 null
@ -13,6 +14,7 @@ class ReplyContent {
}); });
String? message; String? message;
String? message2;
Map? atNameToMid; Map? atNameToMid;
List<MemberItemModel>? members; List<MemberItemModel>? members;
Map? emote; Map? emote;
@ -24,10 +26,17 @@ class ReplyContent {
Map? topicsMeta; Map? topicsMeta;
ReplyContent.fromJson(Map<String, dynamic> json) { ReplyContent.fromJson(Map<String, dynamic> json) {
message = json['message'] message = message2 = json['message']
.replaceAll('&gt;', '>') .replaceAll('&gt;', '>')
.replaceAll('&#34;', '"') .replaceAll('&#34;', '"')
.replaceAll('&#39;', "'"); .replaceAll('&#39;', "'")
.replaceAll('&amp;', '&')
.replaceAll('&lt;', '<')
.replaceAll('&gt;', '>')
.replaceAll('&quot;', '"')
.replaceAll('&apos;', "'")
.replaceAll('&nbsp;', ' ');
atNameToMid = json['at_name_to_mid'] ?? {}; atNameToMid = json['at_name_to_mid'] ?? {};
members = json['members'] != null members = json['members'] != null
? json['members'] ? json['members']
@ -39,8 +48,8 @@ class ReplyContent {
pictures = json['pictures'] ?? []; pictures = json['pictures'] ?? [];
vote = json['vote'] ?? {}; vote = json['vote'] ?? {};
richText = json['rich_text'] ?? {}; richText = json['rich_text'] ?? {};
// 不包含@ 笔记 图片的时候,文字可折叠 // 不包含@ 笔记的时候,文字可折叠
isText = atNameToMid!.isEmpty && vote!.isEmpty && pictures!.isEmpty; isText = atNameToMid!.isEmpty && vote!.isEmpty;
topicsMeta = json['topics_meta'] ?? {}; topicsMeta = json['topics_meta'] ?? {};
} }
} }

View File

@ -6,21 +6,21 @@ import 'upper.dart';
class ReplyData { class ReplyData {
ReplyData({ ReplyData({
this.page, this.cursor,
this.config, this.config,
this.replies, this.replies,
this.topReplies, this.topReplies,
this.upper, this.upper,
}); });
ReplyPage? page; ReplyCursor? cursor;
ReplyConfig? config; ReplyConfig? config;
late List<ReplyItemModel>? replies; late List<ReplyItemModel>? replies;
late List<ReplyItemModel>? topReplies; late List<ReplyItemModel>? topReplies;
ReplyUpper? upper; ReplyUpper? upper;
ReplyData.fromJson(Map<String, dynamic> json) { ReplyData.fromJson(Map<String, dynamic> json) {
page = ReplyPage.fromJson(json['page']); cursor = ReplyCursor.fromJson(json['cursor']);
config = ReplyConfig.fromJson(json['config']); config = ReplyConfig.fromJson(json['config']);
replies = json['replies'] != null replies = json['replies'] != null
? json['replies'] ? json['replies']
@ -38,3 +38,100 @@ class ReplyData {
upper = ReplyUpper.fromJson(json['upper']); upper = ReplyUpper.fromJson(json['upper']);
} }
} }
class ReplyCursor {
ReplyCursor({
this.isBegin,
this.prev,
this.next,
this.isEnd,
this.mode,
this.modeText,
this.allCount,
this.supportMode,
this.name,
this.paginationReply,
this.sessionId,
});
bool? isBegin;
int? prev;
int? next;
bool? isEnd;
int? mode;
String? modeText;
int? allCount;
List<int>? supportMode;
String? name;
PaginationReply? paginationReply;
String? sessionId;
ReplyCursor.fromJson(Map<String, dynamic> json) {
isBegin = json['is_begin'];
prev = json['prev'];
next = json['next'];
isEnd = json['is_end'];
mode = json['mode'];
modeText = json['mode_text'];
allCount = json['all_count'] ?? 0;
supportMode = json['support_mode'].cast<int>();
name = json['name'];
paginationReply = json['pagination_reply'] != null
? PaginationReply.fromJson(json['pagination_reply'])
: null;
sessionId = json['session_id'];
}
}
class PaginationReply {
PaginationReply({
this.nextOffset,
this.prevOffset,
});
String? nextOffset;
String? prevOffset;
PaginationReply.fromJson(Map<String, dynamic> json) {
nextOffset = json['next_offset'];
prevOffset = json['prev_offset'];
}
}
class ReplyReplyData {
ReplyReplyData({
this.page,
this.config,
this.replies,
this.root,
this.topReplies,
this.upper,
});
ReplyPage? page;
ReplyConfig? config;
late List<ReplyItemModel>? replies;
ReplyItemModel? root;
late List<ReplyItemModel>? topReplies;
ReplyUpper? upper;
ReplyReplyData.fromJson(Map<String, dynamic> json) {
page = ReplyPage.fromJson(json['page']);
config = ReplyConfig.fromJson(json['config']);
replies = json['replies'] != null
? json['replies']
.map<ReplyItemModel>(
(item) => ReplyItemModel.fromJson(item, json['upper']['mid']))
.toList()
: [];
root = json['root'] != null
? ReplyItemModel.fromJson(json['root'], false)
: null;
topReplies = json['top_replies'] != null
? json['top_replies']
.map<ReplyItemModel>((item) => ReplyItemModel.fromJson(
item, json['upper']['mid'],
isTopStatus: true))
.toList()
: [];
upper = ReplyUpper.fromJson(json['upper']);
}
}

View File

@ -11,6 +11,7 @@ class ReplyItemModel {
this.parent, this.parent,
this.dialog, this.dialog,
this.count, this.count,
this.rcount,
this.floor, this.floor,
this.state, this.state,
this.fansgrade, this.fansgrade,
@ -41,6 +42,7 @@ class ReplyItemModel {
int? parent; int? parent;
int? dialog; int? dialog;
int? count; int? count;
int? rcount;
int? floor; int? floor;
int? state; int? state;
int? fansgrade; int? fansgrade;
@ -72,6 +74,7 @@ class ReplyItemModel {
parent = json['parent']; parent = json['parent'];
dialog = json['dialog']; dialog = json['dialog'];
count = json['count']; count = json['count'];
rcount = json['rcount'] ?? 0;
floor = json['floor']; floor = json['floor'];
state = json['state']; state = json['state'];
fansgrade = json['fansgrade']; fansgrade = json['fansgrade'];
@ -122,6 +125,7 @@ class ReplyControl {
this.upLike, this.upLike,
this.isShow, this.isShow,
this.entryText, this.entryText,
this.entryTextNum,
this.titleText, this.titleText,
this.time, this.time,
this.location, this.location,
@ -132,6 +136,7 @@ class ReplyControl {
bool? upLike; bool? upLike;
bool? isShow; bool? isShow;
String? entryText; String? entryText;
int? entryTextNum;
String? titleText; String? titleText;
String? time; String? time;
String? location; String? location;
@ -152,6 +157,10 @@ class ReplyControl {
} }
entryText = json['sub_reply_entry_text']; entryText = json['sub_reply_entry_text'];
// 正则匹配
entryTextNum = json['sub_reply_entry_text'] != null
? int.parse(RegExp(r"\d+").stringMatch(json['sub_reply_entry_text']!)!)
: 0;
titleText = json['sub_reply_title_text']; titleText = json['sub_reply_title_text'];
time = json['time_desc']; time = json['time_desc'];
location = json['location'] != null ? json['location'].split('')[1] : ''; location = json['location'] != null ? json['location'].split('')[1] : '';

View File

@ -29,7 +29,7 @@ class ReplyMember {
avatar = json['avatar']; avatar = json['avatar'];
level = json['level_info']['current_level']; level = json['level_info']['current_level'];
pendant = Pendant.fromJson(json['pendant']); pendant = Pendant.fromJson(json['pendant']);
officialVerify = json['officia_verify']; officialVerify = json['official_verify'];
vip = json['vip']; vip = json['vip'];
fansDetail = json['fans_detail']; fansDetail = json['fans_detail'];
userSailing = json['user_sailing'] != null userSailing = json['user_sailing'] != null

View File

@ -0,0 +1,17 @@
class VideoTagItem {
String? tagName;
int? tagId;
int? tagType;
VideoTagItem({
this.tagName,
this.tagId,
this.tagType,
});
VideoTagItem.fromJson(Map<String, dynamic> json) {
tagName = json['tag_name'];
tagId = json['tag_id'];
tagType = json['type'];
}
}

View File

@ -377,6 +377,7 @@ class Part {
int? page; int? page;
String? from; String? from;
String? pagePart; String? pagePart;
String? title;
int? duration; int? duration;
String? vid; String? vid;
String? weblink; String? weblink;
@ -389,6 +390,7 @@ class Part {
this.page, this.page,
this.from, this.from,
this.pagePart, this.pagePart,
this.title,
this.duration, this.duration,
this.vid, this.vid,
this.weblink, this.weblink,
@ -406,6 +408,7 @@ class Part {
page = json["page"]; page = json["page"];
from = json["from"]; from = json["from"];
pagePart = json["part"]; pagePart = json["part"];
title = json["part"];
duration = json["duration"]; duration = json["duration"];
vid = json["vid"]; vid = json["vid"];
weblink = json["weblink"]; weblink = json["weblink"];
@ -638,6 +641,7 @@ class EpisodeItem {
this.page, this.page,
this.bvid, this.bvid,
this.cover, this.cover,
this.pages,
}); });
int? seasonId; int? seasonId;
int? sectionId; int? sectionId;
@ -649,6 +653,10 @@ class EpisodeItem {
Part? page; Part? page;
String? bvid; String? bvid;
String? cover; String? cover;
int? pubdate;
int? duration;
Stat? stat;
List<Page>? pages;
EpisodeItem.fromJson(Map<String, dynamic> json) { EpisodeItem.fromJson(Map<String, dynamic> json) {
seasonId = json['season_id']; seasonId = json['season_id'];
@ -661,6 +669,10 @@ class EpisodeItem {
page = Part.fromJson(json['page']); page = Part.fromJson(json['page']);
bvid = json['bvid']; bvid = json['bvid'];
cover = json['arc']['pic']; cover = json['arc']['pic'];
pubdate = json['arc']['pubdate'];
duration = json['arc']['duration'];
stat = Stat.fromJson(json['arc']['stat']);
pages = json['pages'].map<Page>((e) => Page.fromJson(e)).toList();
} }
} }
@ -703,3 +715,18 @@ class Vip {
status = json['status']; status = json['status'];
} }
} }
class Page {
Page({
this.cid,
this.page,
});
int? cid;
int? page;
Page.fromJson(Map<String, dynamic> json) {
cid = json['cid'];
page = json['page'];
}
}

View File

@ -5,6 +5,7 @@ import 'package:get/get.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:pilipala/http/index.dart'; import 'package:pilipala/http/index.dart';
import 'package:pilipala/models/github/latest.dart'; import 'package:pilipala/models/github/latest.dart';
import 'package:pilipala/plugin/pl_gallery/index.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../../utils/cache_manage.dart'; import '../../utils/cache_manage.dart';
@ -38,9 +39,7 @@ class _AboutPageState extends State<AboutPage> {
TextStyle subTitleStyle = TextStyle subTitleStyle =
TextStyle(fontSize: 13, color: Theme.of(context).colorScheme.outline); TextStyle(fontSize: 13, color: Theme.of(context).colorScheme.outline);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(title: const Text('关于')),
title: Text('关于', style: Theme.of(context).textTheme.titleMedium),
),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Column( child: Column(
children: [ children: [
@ -124,7 +123,7 @@ class _AboutPageState extends State<AboutPage> {
onTap: () => _aboutController.webSiteUrl(), onTap: () => _aboutController.webSiteUrl(),
title: const Text('访问官网'), title: const Text('访问官网'),
trailing: Text( trailing: Text(
'https://pilipalanet.mysxl.cn', 'https://pilipala.life',
style: subTitleStyle, style: subTitleStyle,
), ),
), ),
@ -168,7 +167,7 @@ class _AboutPageState extends State<AboutPage> {
onTap: () => _aboutController.tgChanel(), onTap: () => _aboutController.tgChanel(),
title: const Text('TG频道'), title: const Text('TG频道'),
trailing: Text( trailing: Text(
'https://t.me/+lm_oOVmF0RJiODk1', 'https://t.me/+1DFtqS6usUM5MDNl',
style: subTitleStyle, style: subTitleStyle,
), ),
), ),
@ -295,7 +294,7 @@ class AboutController extends GetxController {
displayTime: const Duration(milliseconds: 500), displayTime: const Duration(milliseconds: 500),
).then( ).then(
(value) => launchUrl( (value) => launchUrl(
Uri.parse('https://www.123pan.com/s/9sVqVv-flu0A.html'), Uri.parse('https://www.123684.com/s/9sVqVv-DEZ0A'),
mode: LaunchMode.externalApplication, mode: LaunchMode.externalApplication,
), ),
); );
@ -321,35 +320,41 @@ class AboutController extends GetxController {
// tg频道 // tg频道
tgChanel() { tgChanel() {
Clipboard.setData( Clipboard.setData(
const ClipboardData(text: 'https://t.me/+lm_oOVmF0RJiODk1'), const ClipboardData(text: 'https://t.me/+1DFtqS6usUM5MDNl'),
); );
SmartDialog.showToast( SmartDialog.showToast(
'已复制,即将在浏览器打开', '已复制,即将在浏览器打开',
displayTime: const Duration(milliseconds: 500), displayTime: const Duration(milliseconds: 500),
).then( ).then(
(value) => launchUrl( (value) => launchUrl(
Uri.parse('https://t.me/+lm_oOVmF0RJiODk1'), Uri.parse('https://t.me/+1DFtqS6usUM5MDNl'),
mode: LaunchMode.externalApplication, mode: LaunchMode.externalApplication,
), ),
); );
} }
aPay() { aPay() {
try { const List<String> sources = [
launchUrl( 'assets/images/pay/wechat.png',
Uri.parse( 'assets/images/pay/alipay.jpg'
'alipayqr://platformapi/startapp?saId=10000007&qrcode=https://qr.alipay.com/fkx14623ddwl1ping3ddd73'), ];
mode: LaunchMode.externalApplication, Navigator.of(Get.context!).push(
); HeroDialogRoute<void>(
} catch (e) { builder: (BuildContext context) => InteractiveviewerGallery(
print(e); sources: sources,
} initIndex: 0,
itemBuilder: (context, index, isFocus, enablePageView) =>
Image.asset(sources[index]),
actionType: const [ImgActionType.save],
),
),
);
} }
// 官网 // 官网
webSiteUrl() { webSiteUrl() {
launchUrl( launchUrl(
Uri.parse('https://pilipalanet.mysxl.cn'), Uri.parse('https://pilipala.life'),
mode: LaunchMode.externalApplication, mode: LaunchMode.externalApplication,
); );
} }

View File

@ -3,25 +3,27 @@ import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/http/bangumi.dart'; import 'package:pilipala/http/bangumi.dart';
import 'package:pilipala/models/bangumi/list.dart'; import 'package:pilipala/models/bangumi/list.dart';
import 'package:pilipala/models/user/info.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
class BangumiController extends GetxController { class BangumiController extends GetxController {
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
RxList<BangumiListItemModel> bangumiList = <BangumiListItemModel>[].obs; RxList<BangumiListItemModel> bangumiList = <BangumiListItemModel>[].obs;
RxList<BangumiListItemModel> bangumiFollowList = <BangumiListItemModel>[].obs; RxList<BangumiListItemModel> bangumiFollowList = <BangumiListItemModel>[].obs;
RxInt total = 0.obs;
int _currentPage = 1; int _currentPage = 1;
bool isLoadingMore = true; bool isLoadingMore = true;
Box userInfoCache = GStrorage.userInfo; Box userInfoCache = GStorage.userInfo;
RxBool userLogin = false.obs; RxBool userLogin = false.obs;
late int mid; late int mid;
var userInfo; UserInfoData? userInfo;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
userInfo = userInfoCache.get('userInfoCache'); userInfo = userInfoCache.get('userInfoCache');
if (userInfo != null) { if (userInfo != null) {
mid = userInfo.mid; mid = userInfo!.mid!;
} }
userLogin.value = userInfo != null; userLogin.value = userInfo != null;
} }
@ -54,9 +56,10 @@ class BangumiController extends GetxController {
if (userInfo == null) { if (userInfo == null) {
return; return;
} }
var result = await BangumiHttp.bangumiFollow(mid: userInfo.mid); var result = await BangumiHttp.getRecentBangumi(mid: userInfo!.mid!);
if (result['status']) { if (result['status']) {
bangumiFollowList.value = result['data'].list; bangumiFollowList.value = result['data'].list;
total.value = result['data'].total;
} else {} } else {}
return result; return result;
} }

View File

@ -2,11 +2,13 @@ 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/bangumi.dart';
import 'package:pilipala/http/constants.dart'; import 'package:pilipala/http/constants.dart';
import 'package:pilipala/http/search.dart'; import 'package:pilipala/http/search.dart';
import 'package:pilipala/http/video.dart'; import 'package:pilipala/http/video.dart';
import 'package:pilipala/models/bangumi/info.dart'; import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/user/fav_folder.dart'; import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/models/user/info.dart';
import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/pages/video/detail/reply/index.dart'; import 'package:pilipala/pages/video/detail/reply/index.dart';
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart'; import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
@ -23,7 +25,7 @@ class BangumiIntroController extends GetxController {
// 视频bvid // 视频bvid
String bvid = Get.parameters['bvid']!; String bvid = Get.parameters['bvid']!;
var seasonId = Get.parameters['seasonId'] != null var seasonId = Get.parameters['seasonId'] != null
? int.parse(Get.parameters['seasonId']!) ? int.tryParse(Get.parameters['seasonId']!)
: null; : null;
var epId = Get.parameters['epId'] != null var epId = Get.parameters['epId'] != null
? int.tryParse(Get.parameters['epId']!) ? int.tryParse(Get.parameters['epId']!)
@ -47,33 +49,40 @@ class BangumiIntroController extends GetxController {
RxBool hasCoin = false.obs; RxBool hasCoin = false.obs;
// 是否收藏 // 是否收藏
RxBool hasFav = false.obs; RxBool hasFav = false.obs;
Box userInfoCache = GStrorage.userInfo; Box userInfoCache = GStorage.userInfo;
bool userLogin = false; bool userLogin = false;
Rx<FavFolderData> favFolderData = FavFolderData().obs; Rx<FavFolderData> favFolderData = FavFolderData().obs;
List addMediaIdsNew = []; List addMediaIdsNew = [];
List delMediaIdsNew = []; List delMediaIdsNew = [];
// 关注状态 默认未关注 // 追番状态 1想看 2在看 3已看
RxMap followStatus = {}.obs; RxBool isFollowed = false.obs;
RxInt followStatus = 1.obs;
int _tempThemeValue = -1; int _tempThemeValue = -1;
var userInfo; UserInfoData? userInfo;
PersistentBottomSheetController? bottomSheetController; PersistentBottomSheetController? bottomSheetController;
List<Map<String, dynamic>> followStatusList = [
{'title': '标记为 「想看」', 'status': 1},
{'title': '标记为 「在看」', 'status': 2},
{'title': '标记为 「已看」', 'status': 3},
{'title': '取消追番', 'status': -1},
];
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
print('bangumi: ${Get.parameters.toString()}');
userInfo = userInfoCache.get('userInfoCache'); userInfo = userInfoCache.get('userInfoCache');
userLogin = userInfo != null; userLogin = userInfo != null;
if (userLogin && seasonId != null) {
bangumiStatus();
}
} }
// 获取番剧简介&选集 // 获取番剧简介&选集
Future queryBangumiIntro() async { Future queryBangumiIntro() async {
if (userLogin) { if (userLogin) {
// 获取点赞状态 // 获取点赞投币收藏状态
queryHasLikeVideo(); bangumiActionStatus();
// 获取投币状态
queryHasCoinVideo();
// 获取收藏状态
queryHasFavVideo();
} }
var result = await SearchHttp.bangumiInfo(seasonId: seasonId, epId: epId); var result = await SearchHttp.bangumiInfo(seasonId: seasonId, epId: epId);
if (result['status']) { if (result['status']) {
@ -83,26 +92,15 @@ class BangumiIntroController extends GetxController {
return result; return result;
} }
// 获取点赞状态 // 获取番剧点赞投币收藏状态
Future queryHasLikeVideo() async { Future bangumiActionStatus() async {
var result = await VideoHttp.hasLikeVideo(bvid: bvid); var result = await BangumiHttp.bangumiActionStatus(epId: epId!);
// data num 被点赞标志 0未点赞 1已点赞
hasLike.value = result["data"] == 1 ? true : false;
}
// 获取投币状态
Future queryHasCoinVideo() async {
var result = await VideoHttp.hasCoinVideo(bvid: bvid);
hasCoin.value = result["data"]['multiply'] == 0 ? false : true;
}
// 获取收藏状态
Future queryHasFavVideo() async {
var result = await VideoHttp.hasFavVideo(aid: IdUtils.bv2av(bvid));
if (result['status']) { if (result['status']) {
hasFav.value = result["data"]['favoured']; hasLike.value = result['data']['like'] == 1;
hasCoin.value = result['data']['coin_number'] != 0;
hasFav.value = result['data']['favorite'] == 1;
} else { } else {
hasFav.value = false; SmartDialog.showToast(result['msg']);
} }
} }
@ -110,7 +108,7 @@ class BangumiIntroController extends GetxController {
Future actionLikeVideo() async { Future actionLikeVideo() async {
var result = await VideoHttp.likeVideo(bvid: bvid, type: !hasLike.value); var result = await VideoHttp.likeVideo(bvid: bvid, type: !hasLike.value);
if (result['status']) { if (result['status']) {
SmartDialog.showToast(!hasLike.value ? '点赞成功 👍' : '取消赞'); SmartDialog.showToast(!hasLike.value ? '点赞成功' : '取消赞');
hasLike.value = !hasLike.value; hasLike.value = !hasLike.value;
bangumiDetail.value.stat!['likes'] = bangumiDetail.value.stat!['likes'] =
bangumiDetail.value.stat!['likes'] + (!hasLike.value ? 1 : -1); bangumiDetail.value.stat!['likes'] + (!hasLike.value ? 1 : -1);
@ -147,7 +145,7 @@ class BangumiIntroController extends GetxController {
var res = await VideoHttp.coinVideo( var res = await VideoHttp.coinVideo(
bvid: bvid, multiply: _tempThemeValue); bvid: bvid, multiply: _tempThemeValue);
if (res['status']) { if (res['status']) {
SmartDialog.showToast('投币成功 👏'); SmartDialog.showToast('投币成功');
hasCoin.value = true; hasCoin.value = true;
bangumiDetail.value.stat!['coins'] = bangumiDetail.value.stat!['coins'] =
bangumiDetail.value.stat!['coins'] + bangumiDetail.value.stat!['coins'] +
@ -185,9 +183,11 @@ class BangumiIntroController extends GetxController {
addMediaIdsNew = []; addMediaIdsNew = [];
delMediaIdsNew = []; delMediaIdsNew = [];
// 重新获取收藏状态 // 重新获取收藏状态
queryHasFavVideo(); bangumiActionStatus();
SmartDialog.showToast('操作成功'); SmartDialog.showToast('操作成功');
Get.back(); Get.back();
} else {
SmartDialog.showToast(result['msg']);
} }
} }
@ -239,21 +239,28 @@ class BangumiIntroController extends GetxController {
// 追番 // 追番
Future bangumiAdd() async { Future bangumiAdd() async {
var result = var result = await VideoHttp.bangumiAdd(
await VideoHttp.bangumiAdd(seasonId: bangumiDetail.value.seasonId); seasonId: seasonId ?? bangumiDetail.value.seasonId);
if (result['status']) {
followStatus.value = 2;
isFollowed.value = true;
}
SmartDialog.showToast(result['msg']); SmartDialog.showToast(result['msg']);
} }
// 取消追番 // 取消追番
Future bangumiDel() async { Future bangumiDel() async {
var result = var result = await VideoHttp.bangumiDel(
await VideoHttp.bangumiDel(seasonId: bangumiDetail.value.seasonId); seasonId: seasonId ?? bangumiDetail.value.seasonId);
if (result['status']) {
isFollowed.value = false;
}
SmartDialog.showToast(result['msg']); SmartDialog.showToast(result['msg']);
} }
Future queryVideoInFolder() async { Future queryVideoInFolder() async {
var result = await VideoHttp.videoInFolder( var result = await VideoHttp.videoInFolder(
mid: userInfo.mid, rid: IdUtils.bv2av(bvid)); mid: userInfo!.mid!, rid: IdUtils.bv2av(bvid));
if (result['status']) { if (result['status']) {
favFolderData.value = result['data']; favFolderData.value = result['data'];
} }
@ -302,18 +309,48 @@ class BangumiIntroController extends GetxController {
episodes: episodes, episodes: episodes,
currentCid: videoDetailCtr.cid.value, currentCid: videoDetailCtr.cid.value,
dataType: dataType, dataType: dataType,
context: Get.context!,
sheetHeight: Get.size.height, sheetHeight: Get.size.height,
isFullScreen: true, isFullScreen: true,
changeFucCall: (item, index) { changeFucCall: (item, index) {
changeSeasonOrbangu(item.bvid, item.cid, item.aid, item.cover); changeSeasonOrbangu(item.bvid, item.cid, item.aid, item.cover);
SmartDialog.dismiss(); SmartDialog.dismiss();
}, },
).buildShowContent(Get.context!), ).buildShowContent(),
); );
} }
hiddenEpisodeBottomSheet() { hiddenEpisodeBottomSheet() {
bottomSheetController?.close(); bottomSheetController?.close();
} }
// 获取追番状态
Future bangumiStatus() async {
var result = await BangumiHttp.bangumiStatus(seasonId: seasonId!);
if (result['status']) {
followStatus.value = result['data']['followStatus'];
isFollowed.value = result['data']['isFollowed'];
}
return result;
}
// 更新追番状态
Future updateBangumiStatus(int status) async {
Get.back();
if (status == -1) {
bangumiDel();
} else {
var result = await BangumiHttp.bangumiStatus(seasonId: seasonId!);
if (result['status']) {
followStatus.value = status;
final title = followStatusList.firstWhere(
(e) => e['status'] == status,
orElse: () => {'title': '未知状态'},
)['title'];
SmartDialog.showToast('追番状态$title');
} else {
SmartDialog.showToast(result['msg']);
}
return result;
}
}
} }

View File

@ -13,6 +13,7 @@ import 'package:pilipala/pages/bangumi/widgets/bangumi_panel.dart';
import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/pages/video/detail/introduction/widgets/action_item.dart'; import 'package:pilipala/pages/video/detail/introduction/widgets/action_item.dart';
import 'package:pilipala/pages/video/detail/introduction/widgets/fav_panel.dart'; import 'package:pilipala/pages/video/detail/introduction/widgets/fav_panel.dart';
import 'package:pilipala/plugin/pl_gallery/index.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 '../../../common/widgets/http_error.dart'; import '../../../common/widgets/http_error.dart';
@ -115,7 +116,7 @@ class _BangumiInfoState extends State<BangumiInfo> {
String heroTag = Get.arguments['heroTag']; String heroTag = Get.arguments['heroTag'];
late final BangumiIntroController bangumiIntroController; late final BangumiIntroController bangumiIntroController;
late final VideoDetailController videoDetailCtr; late final VideoDetailController videoDetailCtr;
Box localCache = GStrorage.localCache; Box localCache = GStorage.localCache;
late double sheetHeight; late double sheetHeight;
int? cid; int? cid;
bool isProcessing = false; bool isProcessing = false;
@ -146,17 +147,34 @@ class _BangumiInfoState extends State<BangumiInfo> {
} }
// 收藏 // 收藏
showFavBottomSheet() { showFavBottomSheet() async {
if (bangumiIntroController.userInfo.mid == null) { if (bangumiIntroController.userInfo?.mid == null) {
SmartDialog.showToast('账号未登录'); SmartDialog.showToast('账号未登录');
return; return;
} }
showModalBottomSheet( final mediaQueryData = MediaQuery.of(context);
context: context, final contentHeight = mediaQueryData.size.height - kToolbarHeight;
useRootNavigator: true, final double initialChildSize =
(contentHeight - Get.width * 9 / 16) / contentHeight;
await showModalBottomSheet(
context: Get.context!,
useSafeArea: true,
isScrollControlled: true, isScrollControlled: true,
builder: (BuildContext context) { builder: (BuildContext context) {
return FavPanel(ctr: bangumiIntroController); return DraggableScrollableSheet(
initialChildSize: initialChildSize,
minChildSize: 0,
maxChildSize: 1,
snap: true,
expand: false,
snapSizes: [initialChildSize],
builder: (BuildContext context, ScrollController scrollController) {
return FavPanel(
ctr: bangumiIntroController,
scrollController: scrollController,
);
},
);
}, },
); );
} }
@ -188,10 +206,24 @@ class _BangumiInfoState extends State<BangumiInfo> {
children: [ children: [
Stack( Stack(
children: [ children: [
NetworkImgLayer( InkWell(
width: 105, onTap: () {
height: 160, Navigator.of(context).push(
src: widget.bangumiDetail!.cover!, HeroDialogRoute<void>(
builder: (BuildContext context) =>
InteractiveviewerGallery(
sources: [widget.bangumiDetail!.cover!],
initIndex: 0,
onPageChanged: (int pageIndex) {},
),
),
);
},
child: NetworkImgLayer(
width: 115,
height: 115 / 0.75,
src: widget.bangumiDetail!.cover!,
),
), ),
PBadge( PBadge(
text: text:
@ -207,104 +239,65 @@ class _BangumiInfoState extends State<BangumiInfo> {
Expanded( Expanded(
child: InkWell( child: InkWell(
onTap: () => showIntroDetail(), onTap: () => showIntroDetail(),
borderRadius: BorderRadius.circular(8),
child: SizedBox( child: SizedBox(
height: 158, height: 115 / 0.75,
child: Column( child: Padding(
crossAxisAlignment: CrossAxisAlignment.start, padding: const EdgeInsets.fromLTRB(6, 4, 6, 6),
mainAxisSize: MainAxisSize.min, child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
Row( mainAxisSize: MainAxisSize.min,
children: [ children: [
Expanded( Row(
child: Text( crossAxisAlignment: CrossAxisAlignment.center,
widget.bangumiDetail!.title!, children: [
style: const TextStyle( Expanded(
fontSize: 16, child: Text(
fontWeight: FontWeight.w500, widget.bangumiDetail!.title!,
), style: const TextStyle(
maxLines: 1, fontSize: 18,
overflow: TextOverflow.ellipsis, fontWeight: FontWeight.bold,
), ),
), maxLines: 2,
const SizedBox(width: 20), overflow: TextOverflow.ellipsis,
SizedBox(
width: 34,
height: 34,
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(
EdgeInsets.zero),
backgroundColor:
MaterialStateProperty.resolveWith(
(Set<MaterialState> states) {
return t.colorScheme.primaryContainer
.withOpacity(0.7);
}),
),
onPressed: () =>
bangumiIntroController.bangumiAdd(),
icon: Icon(
Icons.favorite_border_rounded,
color: t.colorScheme.primary,
size: 22,
), ),
), ),
), const SizedBox(width: 20),
], Obx(
), () => BangumiStatusWidget(
Row( ctr: bangumiIntroController,
children: [ isFollowed:
StatView( bangumiIntroController.isFollowed.value,
view: widget.bangumiDetail!.stat!['views'], ),
size: 'medium',
),
const SizedBox(width: 6),
StatDanMu(
danmu: widget.bangumiDetail!.stat!['danmakus'],
size: 'medium',
),
],
),
const SizedBox(height: 6),
Row(
children: [
Text(
(widget.bangumiDetail!.areas!.isNotEmpty
? widget.bangumiDetail!.areas!.first['name']
: ''),
style: TextStyle(
fontSize: 12,
color: t.colorScheme.outline,
), ),
), ],
const SizedBox(width: 6),
Text(
widget.bangumiDetail!.publish!['pub_time_show'],
style: TextStyle(
fontSize: 12,
color: t.colorScheme.outline,
),
),
],
),
Text(
widget.bangumiDetail!.newEp!['desc'],
style: TextStyle(
fontSize: 12,
color: t.colorScheme.outline,
), ),
), const SizedBox(height: 4),
const Spacer(), Row(
Text( children: [
'简介:${widget.bangumiDetail!.evaluate!}', StatView(
maxLines: 3, view: widget.bangumiDetail!.stat!['views'],
overflow: TextOverflow.ellipsis, size: 'medium',
style: TextStyle( ),
fontSize: 13, const SizedBox(width: 6),
color: t.colorScheme.outline, StatDanMu(
danmu: widget.bangumiDetail!.stat!['danmakus'],
size: 'medium',
),
],
), ),
), const SizedBox(height: 10),
], Text(
'简介:${widget.bangumiDetail!.evaluate!}',
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 14,
color: t.colorScheme.outline,
),
),
],
),
), ),
), ),
), ),
@ -394,3 +387,97 @@ class _BangumiInfoState extends State<BangumiInfo> {
}); });
} }
} }
// 追番状态
class BangumiStatusWidget extends StatelessWidget {
final BangumiIntroController ctr;
final bool isFollowed;
const BangumiStatusWidget({
Key? key,
required this.ctr,
required this.isFollowed,
}) : super(key: key);
@override
Widget build(BuildContext context) {
ColorScheme colorScheme = Theme.of(context).colorScheme;
void updateFollowStatus() {
showModalBottomSheet(
context: context,
useRootNavigator: true,
isScrollControlled: true,
builder: (context) {
return morePanel(context, ctr);
},
);
}
return Obx(
() => SizedBox(
width: 34,
height: 34,
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
backgroundColor:
MaterialStateProperty.resolveWith((Set<MaterialState> states) {
return ctr.isFollowed.value
? colorScheme.primaryContainer.withOpacity(0.7)
: colorScheme.outlineVariant.withOpacity(0.7);
}),
),
onPressed:
isFollowed ? () => updateFollowStatus() : () => ctr.bangumiAdd(),
icon: Icon(
ctr.isFollowed.value
? Icons.favorite
: Icons.favorite_border_rounded,
color: ctr.isFollowed.value
? colorScheme.primary
: colorScheme.outline,
size: 22,
),
),
),
);
}
Widget morePanel(BuildContext context, BangumiIntroController ctr) {
return Container(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
InkWell(
onTap: () => Get.back(),
child: Container(
height: 35,
padding: const EdgeInsets.only(bottom: 2),
child: Center(
child: Container(
width: 32,
height: 3,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.outline,
borderRadius: const BorderRadius.all(Radius.circular(3))),
),
),
),
),
...ctr.followStatusList
.map(
(e) => ListTile(
onTap: () => ctr.updateBangumiStatus(e['status']),
selected: ctr.followStatus == e['status'],
title: Text(e['title']),
),
)
.toList(),
const SizedBox(height: 20),
],
),
);
}
}

View File

@ -4,7 +4,7 @@ import 'package:pilipala/common/widgets/stat/danmu.dart';
import 'package:pilipala/common/widgets/stat/view.dart'; import 'package:pilipala/common/widgets/stat/view.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
Box localCache = GStrorage.localCache; Box localCache = GStorage.localCache;
late double sheetHeight; late double sheetHeight;
class IntroDetail extends StatelessWidget { class IntroDetail extends StatelessWidget {

View File

@ -76,9 +76,14 @@ class _BangumiPageState extends State<BangumiPage>
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Obx(
'最近追番', () => 0 != _bangumidController.total.value
style: Theme.of(context).textTheme.titleMedium, ? Text(
'我的追番(${_bangumidController.total.value})',
style:
Theme.of(context).textTheme.titleMedium,
)
: const SizedBox(),
), ),
IconButton( IconButton(
onPressed: () { onPressed: () {
@ -96,7 +101,8 @@ class _BangumiPageState extends State<BangumiPage>
), ),
), ),
SizedBox( SizedBox(
height: 268, height: Get.size.width / 3 / 0.75 +
MediaQuery.textScalerOf(context).scale(50.0),
child: FutureBuilder( child: FutureBuilder(
future: _futureBuilderFutureFollow, future: _futureBuilderFutureFollow,
builder: builder:
@ -117,7 +123,6 @@ class _BangumiPageState extends State<BangumiPage>
itemBuilder: (context, index) { itemBuilder: (context, index) {
return Container( return Container(
width: Get.size.width / 3, width: Get.size.width / 3,
height: 254,
margin: EdgeInsets.only( margin: EdgeInsets.only(
left: StyleString.safeSpace, left: StyleString.safeSpace,
right: index == right: index ==
@ -183,8 +188,10 @@ class _BangumiPageState extends State<BangumiPage>
return HttpError( return HttpError(
errMsg: data['msg'], errMsg: data['msg'],
fn: () { fn: () {
_futureBuilderFuture = setState(() {
_bangumidController.queryBangumiListFeed(); _futureBuilderFuture =
_bangumidController.queryBangumiListFeed();
});
}, },
); );
} }
@ -208,8 +215,8 @@ class _BangumiPageState extends State<BangumiPage>
crossAxisSpacing: StyleString.cardSpace, crossAxisSpacing: StyleString.cardSpace,
// 列数 // 列数
crossAxisCount: 3, crossAxisCount: 3,
mainAxisExtent: Get.size.width / 3 / 0.65 + mainAxisExtent: Get.size.width / 3 / 0.75 +
MediaQuery.textScalerOf(context).scale(32.0), MediaQuery.textScalerOf(context).scale(42.0),
), ),
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) { (BuildContext context, int index) {

View File

@ -5,9 +5,9 @@ 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/models/bangumi/info.dart'; import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/user/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';
import '../../../common/pages_bottom_sheet.dart'; import '../../../common/pages_bottom_sheet.dart';
import '../../../models/common/video_episode_type.dart'; import '../../../models/common/video_episode_type.dart';
import '../introduction/controller.dart'; import '../introduction/controller.dart';
@ -37,14 +37,13 @@ class BangumiPanel extends StatefulWidget {
class _BangumiPanelState extends State<BangumiPanel> { class _BangumiPanelState extends State<BangumiPanel> {
late RxInt currentIndex = (-1).obs; late RxInt currentIndex = (-1).obs;
final ScrollController listViewScrollCtr = ScrollController(); final ScrollController listViewScrollCtr = ScrollController();
Box userInfoCache = GStrorage.userInfo; Box userInfoCache = GStorage.userInfo;
dynamic userInfo; UserInfoData? userInfo;
// 默认未开通 // 默认未开通
int vipStatus = 0; int vipStatus = 0;
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();
late PersistentBottomSheetController? _bottomSheetController; late PersistentBottomSheetController? _bottomSheetController;
@override @override
@ -65,7 +64,7 @@ class _BangumiPanelState extends State<BangumiPanel> {
/// 获取大会员状态 /// 获取大会员状态
userInfo = userInfoCache.get('userInfoCache'); userInfo = userInfoCache.get('userInfoCache');
if (userInfo != null) { if (userInfo != null) {
vipStatus = userInfo.vipStatus; vipStatus = userInfo!.vipStatus!;
} }
} }
@ -86,9 +85,11 @@ class _BangumiPanelState extends State<BangumiPanel> {
item.aid, item.aid,
item.cover, item.cover,
); );
if (_bottomSheetController != null) { try {
_bottomSheetController?.close(); if (_bottomSheetController != null) {
} _bottomSheetController?.close();
}
} catch (_) {}
currentIndex.value = i; currentIndex.value = i;
scrollToIndex(); scrollToIndex();
} }
@ -149,7 +150,6 @@ class _BangumiPanelState extends State<BangumiPanel> {
changeFucCall: changeFucCall, changeFucCall: changeFucCall,
sheetHeight: widget.sheetHeight, sheetHeight: widget.sheetHeight,
dataType: VideoEpidoesType.bangumiEpisode, dataType: VideoEpidoesType.bangumiEpisode,
context: context,
).show(context); ).show(context);
}, },
child: Text( child: Text(
@ -174,59 +174,60 @@ class _BangumiPanelState extends State<BangumiPanel> {
return Container( return Container(
width: 150, width: 150,
margin: const EdgeInsets.only(right: 10), margin: const EdgeInsets.only(right: 10),
child: Material( clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.onInverseSurface, color: Theme.of(context).colorScheme.onInverseSurface,
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(8),
clipBehavior: Clip.hardEdge, ),
child: InkWell( child: InkWell(
onTap: () => changeFucCall(page, i), borderRadius: BorderRadius.circular(8),
child: Padding( onTap: () => changeFucCall(page, i),
padding: const EdgeInsets.symmetric( child: Padding(
vertical: 8, padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 8,
), horizontal: 10,
child: Column( ),
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
children: <Widget>[ crossAxisAlignment: CrossAxisAlignment.start,
Row( children: <Widget>[
children: [ Row(
if (isSelected) ...<Widget>[ children: [
Image.asset('assets/images/live.png', if (isSelected) ...<Widget>[
color: primary, height: 12), Image.asset('assets/images/live.png',
const SizedBox(width: 6) color: primary, height: 12),
], const SizedBox(width: 6)
],
Text(
'${i + 1}',
style: TextStyle(
fontSize: 13,
color: isSelected ? primary : onSurface,
),
),
const SizedBox(width: 2),
if (page.badge != null) ...[
const Spacer(),
Text( Text(
'${i + 1}', page.badge!,
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 12,
color: isSelected ? primary : onSurface, color: primary,
), ),
), ),
const SizedBox(width: 2), ]
if (page.badge != null) ...[ ],
const Spacer(), ),
Text( const SizedBox(height: 3),
page.badge!, Text(
style: TextStyle( page.longTitle!,
fontSize: 12, maxLines: 1,
color: primary, style: TextStyle(
), fontSize: 13,
), color: isSelected ? primary : onSurface,
]
],
), ),
const SizedBox(height: 3), overflow: TextOverflow.ellipsis,
Text( )
page.longTitle!, ],
maxLines: 1,
style: TextStyle(
fontSize: 13,
color: isSelected ? primary : onSurface,
),
overflow: TextOverflow.ellipsis,
)
],
),
), ),
), ),
), ),

View File

@ -25,6 +25,7 @@ class BangumiCardV extends StatelessWidget {
RoutePush.bangumiPush( RoutePush.bangumiPush(
bangumiItem.seasonId, bangumiItem.seasonId,
null, null,
progressIndex: bangumiItem.progressIndex,
heroTag: heroTag, heroTag: heroTag,
); );
}, },
@ -37,7 +38,7 @@ class BangumiCardV extends StatelessWidget {
StyleString.imgRadius, StyleString.imgRadius,
), ),
child: AspectRatio( child: AspectRatio(
aspectRatio: 0.65, aspectRatio: 0.75,
child: LayoutBuilder(builder: (context, boxConstraints) { child: LayoutBuilder(builder: (context, boxConstraints) {
final double maxWidth = boxConstraints.maxWidth; final double maxWidth = boxConstraints.maxWidth;
final double maxHeight = boxConstraints.maxHeight; final double maxHeight = boxConstraints.maxHeight;

View File

@ -22,7 +22,7 @@ class _BlackListPageState extends State<BlackListPage> {
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
Future? _futureBuilderFuture; Future? _futureBuilderFuture;
bool _isLoadingMore = false; bool _isLoadingMore = false;
Box setting = GStrorage.setting; Box setting = GStorage.setting;
@override @override
void initState() { void initState() {
@ -55,14 +55,9 @@ class _BlackListPageState extends State<BlackListPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
elevation: 0,
scrolledUnderElevation: 0,
titleSpacing: 0,
centerTitle: false,
title: Obx( title: Obx(
() => Text( () => Text(
'黑名单管理 - ${_blackListController.total.value}', '黑名单管理 ${_blackListController.total.value == 0 ? '' : '- ${_blackListController.total.value}'}',
style: Theme.of(context).textTheme.titleMedium,
), ),
), ),
), ),
@ -76,14 +71,20 @@ class _BlackListPageState extends State<BlackListPage> {
if (data['status']) { if (data['status']) {
List<BlackListItem> list = _blackListController.blackList; List<BlackListItem> list = _blackListController.blackList;
return Obx( return Obx(
() => list.length == 1 () => list.isEmpty
? const SizedBox() ? HttpError(
errMsg: '你没有拉黑任何人哦_',
fn: () => {},
isInSliver: false,
)
: ListView.builder( : ListView.builder(
controller: scrollController, controller: scrollController,
itemCount: list.length, itemCount: list.length,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
return ListTile( return ListTile(
onTap: () {}, onTap: () => Get.toNamed(
'/member?mid=${list[index].mid}',
arguments: {'face': list[index].face}),
leading: NetworkImgLayer( leading: NetworkImgLayer(
width: 45, width: 45,
height: 45, height: 45,
@ -115,13 +116,10 @@ class _BlackListPageState extends State<BlackListPage> {
), ),
); );
} else { } else {
return CustomScrollView( return HttpError(
slivers: [ errMsg: data['msg'],
HttpError( fn: () => _blackListController.queryBlacklist(),
errMsg: data['msg'], isInSliver: false,
fn: () => _blackListController.queryBlacklist(),
)
],
); );
} }
} else { } else {

View File

@ -32,7 +32,7 @@ class _PlDanmakuState extends State<PlDanmaku> {
late PlDanmakuController _plDanmakuController; late PlDanmakuController _plDanmakuController;
DanmakuController? _controller; DanmakuController? _controller;
// bool danmuPlayStatus = true; // bool danmuPlayStatus = true;
Box setting = GStrorage.setting; Box setting = GStorage.setting;
late bool enableShowDanmaku; late bool enableShowDanmaku;
late List blockTypes; late List blockTypes;
late double showArea; late double showArea;

View File

@ -10,6 +10,7 @@ import 'package:pilipala/models/common/dynamics_type.dart';
import 'package:pilipala/models/dynamics/result.dart'; import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/models/dynamics/up.dart'; import 'package:pilipala/models/dynamics/up.dart';
import 'package:pilipala/models/live/item.dart'; import 'package:pilipala/models/live/item.dart';
import 'package:pilipala/models/user/info.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';
import 'package:pilipala/utils/route_push.dart'; import 'package:pilipala/utils/route_push.dart';
@ -50,11 +51,11 @@ class DynamicsController extends GetxController {
]; ];
bool flag = false; bool flag = false;
RxInt initialValue = 0.obs; RxInt initialValue = 0.obs;
Box userInfoCache = GStrorage.userInfo; Box userInfoCache = GStorage.userInfo;
RxBool userLogin = false.obs; RxBool userLogin = false.obs;
var userInfo; UserInfoData? userInfo;
RxBool isLoadingDynamic = false.obs; RxBool isLoadingDynamic = false.obs;
Box setting = GStrorage.setting; Box setting = GStorage.setting;
@override @override
void onInit() { void onInit() {
@ -146,20 +147,26 @@ class DynamicsController extends GetxController {
/// 专栏文章查看 /// 专栏文章查看
case 'DYNAMIC_TYPE_ARTICLE': case 'DYNAMIC_TYPE_ARTICLE':
String title = item.modules.moduleDynamic.major.opus.title; String title = item.modules.moduleDynamic.major.opus.title;
String url = item.modules.moduleDynamic.major.opus.jumpUrl; String jumpUrl = item.modules.moduleDynamic.major.opus.jumpUrl;
if (url.contains('opus') || url.contains('read')) { String url =
jumpUrl.startsWith('//') ? jumpUrl.split('//').last : jumpUrl;
if (jumpUrl.contains('opus') || jumpUrl.contains('read')) {
RegExp digitRegExp = RegExp(r'\d+'); RegExp digitRegExp = RegExp(r'\d+');
Iterable<Match> matches = digitRegExp.allMatches(url); Iterable<Match> matches = digitRegExp.allMatches(jumpUrl);
String number = matches.first.group(0)!; String number = matches.first.group(0)!;
if (url.contains('read')) { if (jumpUrl.contains('read')) {
number = 'cv$number'; Get.toNamed('/read', parameters: {
'title': title,
'id': number,
'articleType': url.split('/')[1]
});
} else {
Get.toNamed('/opus', parameters: {
'title': title,
'id': number,
'articleType': 'opus'
});
} }
Get.toNamed('/htmlRender', parameters: {
'url': url.startsWith('//') ? url.split('//').last : url,
'title': title,
'id': number,
'dynamicType': url.split('//').last.split('/')[1]
});
} else { } else {
Get.toNamed( Get.toNamed(
'/webview', '/webview',
@ -240,7 +247,7 @@ class DynamicsController extends GetxController {
} }
upData.value.upList!.insertAll(0, [ upData.value.upList!.insertAll(0, [
UpItem(face: '', uname: '全部动态', mid: -1), UpItem(face: '', uname: '全部动态', mid: -1),
UpItem(face: userInfo.face, uname: '', mid: userInfo.mid), UpItem(face: userInfo!.face, uname: '', mid: userInfo!.mid),
]); ]);
} }
return res; return res;

View File

@ -14,7 +14,7 @@ class DynamicDetailController extends GetxController {
int? type; int? type;
dynamic item; dynamic item;
int? floor; int? floor;
int currentPage = 0; String nextOffset = "";
bool isLoadingMore = false; bool isLoadingMore = false;
RxString noMore = ''.obs; RxString noMore = ''.obs;
RxList<ReplyItemModel> replyList = <ReplyItemModel>[].obs; RxList<ReplyItemModel> replyList = <ReplyItemModel>[].obs;
@ -24,8 +24,9 @@ class DynamicDetailController extends GetxController {
ReplySortType _sortType = ReplySortType.time; ReplySortType _sortType = ReplySortType.time;
RxString sortTypeTitle = ReplySortType.time.titles.obs; RxString sortTypeTitle = ReplySortType.time.titles.obs;
RxString sortTypeLabel = ReplySortType.time.labels.obs; RxString sortTypeLabel = ReplySortType.time.labels.obs;
Box setting = GStrorage.setting; Box setting = GStorage.setting;
RxInt replyReqCode = 200.obs; RxInt replyReqCode = 200.obs;
bool isEnd = false;
@override @override
void onInit() { void onInit() {
@ -36,38 +37,42 @@ class DynamicDetailController extends GetxController {
acount.value = acount.value =
int.parse(item!.modules!.moduleStat!.comment!.count ?? '0'); int.parse(item!.modules!.moduleStat!.comment!.count ?? '0');
} }
int deaultReplySortIndex = int defaultReplySortIndex =
setting.get(SettingBoxKey.replySortType, defaultValue: 0); setting.get(SettingBoxKey.replySortType, defaultValue: 0);
if (deaultReplySortIndex == 2) { if (defaultReplySortIndex == 2) {
setting.put(SettingBoxKey.replySortType, 0); setting.put(SettingBoxKey.replySortType, 0);
deaultReplySortIndex = 0; defaultReplySortIndex = 0;
} }
_sortType = ReplySortType.values[deaultReplySortIndex]; _sortType = ReplySortType.values[defaultReplySortIndex];
sortTypeTitle.value = _sortType.titles; sortTypeTitle.value = _sortType.titles;
sortTypeLabel.value = _sortType.labels; sortTypeLabel.value = _sortType.labels;
} }
Future queryReplyList({reqType = 'init'}) async { Future queryReplyList({reqType = 'init'}) async {
if (isLoadingMore || noMore.value == '没有更多了' || isEnd) {
return;
}
isLoadingMore = true;
if (reqType == 'init') { if (reqType == 'init') {
currentPage = 0; nextOffset = '';
noMore.value = '';
} }
var res = await ReplyHttp.replyList( var res = await ReplyHttp.replyList(
oid: oid!, oid: oid!,
pageNum: currentPage + 1, nextOffset: nextOffset,
type: type!, type: type!,
sort: _sortType.index, sort: _sortType.index,
); );
if (res['status']) { if (res['status']) {
List<ReplyItemModel> replies = res['data'].replies; List<ReplyItemModel> replies = res['data'].replies;
acount.value = res['data'].page.acount; isEnd = res['data'].cursor.isEnd ?? false;
acount.value = res['data'].cursor.allCount;
nextOffset = res['data'].cursor.paginationReply.nextOffset ?? "";
if (replies.isNotEmpty) { if (replies.isNotEmpty) {
currentPage++; noMore.value = isEnd ? '没有更多了' : '加载中...';
noMore.value = '加载中...';
if (replies.length < 20) {
noMore.value = '没有更多了';
}
} else { } else {
noMore.value = currentPage == 0 ? '还没有评论' : '没有更多了'; noMore.value =
replyList.isEmpty && nextOffset == "" ? '还没有评论' : '没有更多了';
} }
if (reqType == 'init') { if (reqType == 'init') {
// 添加置顶回复 // 添加置顶回复
@ -113,4 +118,20 @@ class DynamicDetailController extends GetxController {
var res = await HtmlHttp.reqHtml(id, 'opus'); var res = await HtmlHttp.reqHtml(id, 'opus');
oid = res['commentId']; oid = res['commentId'];
} }
// 上拉加载
Future onLoad() async {
queryReplyList(reqType: 'onLoad');
}
Future removeReply(int? rpid, int? frpid) async {
// 移除一楼评论
if (rpid != null) {
replyList.removeWhere((item) {
return item.rpid == rpid;
});
}
/// TODO 移除二楼评论
}
} }

View File

@ -31,8 +31,9 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
with TickerProviderStateMixin { with TickerProviderStateMixin {
late DynamicDetailController _dynamicDetailController; late DynamicDetailController _dynamicDetailController;
late AnimationController fabAnimationCtr; late AnimationController fabAnimationCtr;
Future? _futureBuilderFuture; late Future _futureBuilderFuture;
late StreamController<bool> titleStreamC; // appBar title late StreamController<bool> titleStreamC =
StreamController<bool>.broadcast(); // appBar title
late ScrollController scrollController; late ScrollController scrollController;
bool _visibleTitle = false; bool _visibleTitle = false;
String? action; String? action;
@ -48,7 +49,6 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
super.initState(); super.initState();
// floor 1原创 2转发 // floor 1原创 2转发
init(); init();
titleStreamC = StreamController<bool>();
if (action == 'comment') { if (action == 'comment') {
_visibleTitle = true; _visibleTitle = true;
titleStreamC.add(true); titleStreamC.add(true);
@ -111,14 +111,7 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
int rpid = replyItem.rpid!; int rpid = replyItem.rpid!;
Get.to( Get.to(
() => Scaffold( () => Scaffold(
appBar: AppBar( appBar: AppBar(title: const Text('评论详情')),
titleSpacing: 0,
centerTitle: false,
title: Text(
'评论详情',
style: Theme.of(context).textTheme.titleMedium,
),
),
body: VideoReplyReplyPanel( body: VideoReplyReplyPanel(
oid: oid, oid: oid,
rpid: rpid, rpid: rpid,
@ -140,7 +133,7 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
if (scrollController.position.pixels >= if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 300) { scrollController.position.maxScrollExtent - 300) {
EasyThrottle.throttle('replylist', const Duration(seconds: 2), () { EasyThrottle.throttle('replylist', const Duration(seconds: 2), () {
_dynamicDetailController.queryReplyList(reqType: 'onLoad'); _dynamicDetailController.onLoad();
}); });
} }
@ -192,10 +185,7 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
elevation: 0,
scrolledUnderElevation: 1, scrolledUnderElevation: 1,
centerTitle: false,
titleSpacing: 0,
title: StreamBuilder( title: StreamBuilder(
stream: titleStreamC.stream, stream: titleStreamC.stream,
initialData: false, initialData: false,
@ -278,8 +268,8 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
future: _futureBuilderFuture, future: _futureBuilderFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) { if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map; Map? data = snapshot.data;
if (snapshot.data['status']) { if (data != null && snapshot.data['status']) {
RxList<ReplyItemModel> replyList = RxList<ReplyItemModel> replyList =
_dynamicDetailController.replyList; _dynamicDetailController.replyList;
// 请求成功 // 请求成功
@ -335,6 +325,8 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
.replies! .replies!
.add(replyItem); .add(replyItem);
}, },
onDelete:
_dynamicDetailController.removeReply,
); );
} }
}, },
@ -345,8 +337,11 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
} else { } else {
// 请求错误 // 请求错误
return HttpError( return HttpError(
errMsg: data['msg'], errMsg: data?['msg'] ?? '请求异常',
fn: () => setState(() {}), fn: () => setState(() {
_futureBuilderFuture =
_dynamicDetailController.queryReplyList();
}),
); );
} }
} else { } else {

View File

@ -0,0 +1,310 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/http/dynamics.dart';
import 'package:pilipala/models/dynamics/result.dart';
import '../widgets/rich_node_panel.dart';
class DynamicForwardPage extends StatefulWidget {
const DynamicForwardPage({
super.key,
this.item,
this.mid,
this.cb,
});
final DynamicItemModel? item;
final int? mid;
final Function()? cb;
@override
State<DynamicForwardPage> createState() => _DynamicForwardPageState();
}
class _DynamicForwardPageState extends State<DynamicForwardPage> {
final TextEditingController _inputController = TextEditingController();
final FocusNode _focusNode = FocusNode();
RxBool isExpand = false.obs;
@override
void dispose() {
_focusNode.dispose();
super.dispose();
}
void dynamicForward(String type) async {
String dynamicId = widget.item!.idStr!;
var res = await DynamicsHttp.dynamicCreate(
dynIdStr: dynamicId,
mid: widget.mid!,
rawText: type == 'quickForward' ? '' : _inputController.text,
scene: 4,
);
if (res['status']) {
SmartDialog.showToast('转发成功');
widget.cb?.call();
_onClose();
} else {
SmartDialog.showToast(res['msg']);
}
}
void _onClose() async {
_focusNode.unfocus();
await Future.delayed(const Duration(milliseconds: 120));
Get.back();
}
@override
Widget build(BuildContext context) {
return Obx(
() {
final EdgeInsetsGeometry padding = EdgeInsets.fromLTRB(
isExpand.value ? 10 : 16,
10,
isExpand.value ? 12 : 12,
0,
);
return AnimatedSize(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
onEnd: () => isExpand.value ? _focusNode.requestFocus() : null,
child: Column(
mainAxisSize: isExpand.value ? MainAxisSize.max : MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: padding,
child: isExpand.value ? _topBarExpand() : _topBar(),
),
isExpand.value ? _contentExpand() : _content(),
dynamicPreview(),
if (!isExpand.value) ..._bottomBar(),
],
),
);
},
);
}
// 转发动态预览
Widget dynamicPreview() {
ItemModulesModel? modules = widget.item!.modules;
final String type = widget.item!.type!;
String? cover = modules?.moduleAuthor?.face;
switch (type) {
/// 图文动态
case 'DYNAMIC_TYPE_DRAW':
cover = modules?.moduleDynamic?.major?.opus?.pics?.first.url;
/// 投稿
case 'DYNAMIC_TYPE_AV':
cover = modules?.moduleDynamic?.major?.archive?.cover;
/// 转发的动态
case 'DYNAMIC_TYPE_FORWARD':
String forwardType = widget.item!.orig!.type!;
switch (forwardType) {
/// 图文动态
case 'DYNAMIC_TYPE_DRAW':
cover = modules?.moduleDynamic?.major?.opus?.pics?.first.url;
/// 投稿
case 'DYNAMIC_TYPE_AV':
cover = modules?.moduleDynamic?.major?.archive?.cover;
/// 番剧
case 'DYNAMIC_TYPE_PGC_UNION':
cover = modules?.moduleDynamic?.major?.pgc?.cover;
// 专栏文章
case 'DYNAMIC_TYPE_ARTICLE':
// 番剧
case 'DYNAMIC_TYPE_PGC':
// 纯文字动态
case 'DYNAMIC_TYPE_WORD':
// 直播
case 'DYNAMIC_TYPE_LIVE_RCMD':
// 合集查看
case 'DYNAMIC_TYPE_UGC_SEASON':
cover = '';
default:
cover = '';
}
// 专栏文章
case 'DYNAMIC_TYPE_ARTICLE':
// 番剧
case 'DYNAMIC_TYPE_PGC':
// 纯文字动态
case 'DYNAMIC_TYPE_WORD':
// 直播
case 'DYNAMIC_TYPE_LIVE_RCMD':
// 合集查看
case 'DYNAMIC_TYPE_UGC_SEASON':
// 番剧查看
case 'DYNAMIC_TYPE_PGC_UNION':
cover = '';
default:
cover = '';
}
return Container(
width: double.infinity,
height: 98,
margin: const EdgeInsets.fromLTRB(12, 0, 12, 14),
decoration: BoxDecoration(
color:
Theme.of(context).colorScheme.secondaryContainer.withOpacity(0.4),
borderRadius: BorderRadius.circular(6),
border: Border(
left: BorderSide(
width: 4,
color: Theme.of(context).colorScheme.primary.withOpacity(0.8)),
),
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 14),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'@${widget.item!.modules!.moduleAuthor!.name}',
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
),
const SizedBox(height: 8),
Row(
children: [
NetworkImgLayer(
src: cover ?? '',
width: 45,
height: 45,
radius: 6,
),
const SizedBox(width: 10),
Expanded(
child: Text.rich(
richNode(widget.item, context),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
// Text(data)
],
)
],
),
),
);
}
// 未展开时的顶部
Widget _topBar() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'转发动态',
style: TextStyle(fontWeight: FontWeight.bold),
),
TextButton(
onPressed: () => dynamicForward('quickForward'),
child: const Text('快速转发'),
)
],
);
}
// 展开时的顶部
Widget _topBarExpand() {
return Stack(
alignment: Alignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
onPressed: _onClose,
icon: const Icon(Icons.close),
),
FilledButton(
onPressed: () => dynamicForward('forward'),
child: const Text('转发'),
),
],
),
Align(
alignment: Alignment.center,
child: Text(
'转发动态',
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontWeight: FontWeight.bold),
),
),
],
);
}
// 未展开时的底部
List<Widget> _bottomBar() {
return [
const Divider(thickness: 0.1, height: 1),
ListTile(
onTap: () => Get.back(),
minLeadingWidth: 0,
dense: true,
title: Text(
'取消',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
textAlign: TextAlign.center,
),
),
SizedBox(
height: MediaQuery.of(context).padding.bottom,
)
];
}
// 未展开时的内容区
Widget _content() {
return GestureDetector(
onTap: () {
isExpand.value = true;
},
behavior: HitTestBehavior.translucent,
child: Container(
width: double.infinity,
alignment: Alignment.centerLeft,
padding: const EdgeInsets.fromLTRB(16, 10, 10, 16),
child: Text(
'说点什么吧',
textAlign: TextAlign.start,
style: TextStyle(color: Theme.of(context).colorScheme.outline),
),
),
);
}
// 展开时的内容区
Widget _contentExpand() {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 10, 16, 0),
child: TextField(
maxLines: null,
minLines: 3,
focusNode: _focusNode,
controller: _inputController,
decoration: const InputDecoration(
border: InputBorder.none,
hintText: '说点什么吧',
),
),
);
}
}

View File

@ -9,7 +9,6 @@ import 'package:pilipala/common/skeleton/dynamic_card.dart';
import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/no_data.dart'; import 'package:pilipala/common/widgets/no_data.dart';
import 'package:pilipala/models/dynamics/result.dart'; import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/plugin/pl_popup/index.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/main_stream.dart'; import 'package:pilipala/utils/main_stream.dart';
import 'package:pilipala/utils/route_push.dart'; import 'package:pilipala/utils/route_push.dart';
@ -18,7 +17,6 @@ import 'package:pilipala/utils/storage.dart';
import '../mine/controller.dart'; import '../mine/controller.dart';
import 'controller.dart'; import 'controller.dart';
import 'widgets/dynamic_panel.dart'; import 'widgets/dynamic_panel.dart';
import 'up_dynamic/route_panel.dart';
import 'widgets/up_panel.dart'; import 'widgets/up_panel.dart';
class DynamicsPage extends StatefulWidget { class DynamicsPage extends StatefulWidget {
@ -34,7 +32,7 @@ class _DynamicsPageState extends State<DynamicsPage>
final MineController mineController = Get.put(MineController()); final MineController mineController = Get.put(MineController());
late Future _futureBuilderFuture; late Future _futureBuilderFuture;
late Future _futureBuilderFutureUp; late Future _futureBuilderFutureUp;
Box userInfoCache = GStrorage.userInfo; Box userInfoCache = GStorage.userInfo;
late ScrollController scrollController; late ScrollController scrollController;
@override @override
@ -80,8 +78,6 @@ class _DynamicsPageState extends State<DynamicsPage>
super.build(context); super.build(context);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
elevation: 0,
scrolledUnderElevation: 0,
title: SizedBox( title: SizedBox(
height: 34, height: 34,
child: Stack( child: Stack(
@ -90,32 +86,34 @@ class _DynamicsPageState extends State<DynamicsPage>
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Obx(() { Obx(() {
if (_dynamicsController.mid.value != -1 && final mid = _dynamicsController.mid.value;
_dynamicsController.upInfo.value.uname != null) { final uname = _dynamicsController.upInfo.value.uname;
return SizedBox(
height: 36, if (mid == -1 || uname == null) {
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder:
(Widget child, Animation<double> animation) {
return ScaleTransition(
scale: animation, child: child);
},
child: Text(
'${_dynamicsController.upInfo.value.uname!}的动态',
key: ValueKey<String>(
_dynamicsController.upInfo.value.uname!),
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelLarge!
.fontSize,
)),
),
);
} else {
return const SizedBox(); return const SizedBox();
} }
return SizedBox(
height: 36,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder:
(Widget child, Animation<double> animation) {
return ScaleTransition(
scale: animation, child: child);
},
child: Text(
'$uname的动态',
key: ValueKey<String>(uname),
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelLarge!
.fontSize,
),
),
),
);
}), }),
Obx( Obx(
() => _dynamicsController.userLogin.value () => _dynamicsController.userLogin.value
@ -206,16 +204,7 @@ class _DynamicsPageState extends State<DynamicsPage>
return Obx( return Obx(
() => UpPanel( () => UpPanel(
upData: _dynamicsController.upData.value, upData: _dynamicsController.upData.value,
onClickUpCb: (data) { dynamicsController: _dynamicsController,
// _dynamicsController.onTapUp(data);
Navigator.push(
context,
PlPopupRoute(
child: OverlayPanel(
ctr: _dynamicsController, upInfo: data),
),
);
},
), ),
); );
} else { } else {

View File

@ -3,20 +3,19 @@ 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: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/widgets/network_img_layer.dart';
import 'package:pilipala/http/dynamics.dart'; import 'package:pilipala/http/dynamics.dart';
import 'package:pilipala/models/dynamics/result.dart'; import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/pages/dynamics/index.dart'; import 'package:pilipala/pages/dynamics/index.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:status_bar_control/status_bar_control.dart'; import 'package:status_bar_control/status_bar_control.dart';
import 'rich_node_panel.dart'; import '../forward/index.dart';
class ActionPanel extends StatefulWidget { class ActionPanel extends StatefulWidget {
const ActionPanel({ const ActionPanel({
super.key, super.key,
required this.item, required this.item,
}); });
// ignore: prefer_typing_uninitialized_variables
final DynamicItemModel item; final DynamicItemModel item;
@override @override
@ -32,9 +31,6 @@ class _ActionPanelState extends State<ActionPanel>
RxDouble height = 0.0.obs; RxDouble height = 0.0.obs;
RxBool isExpand = false.obs; RxBool isExpand = false.obs;
late double statusHeight; late double statusHeight;
TextEditingController _inputController = TextEditingController();
FocusNode myFocusNode = FocusNode();
String _inputText = '';
void Function()? handleState(Future Function() action) { void Function()? handleState(Future Function() action) {
return isProcessing return isProcessing
@ -60,7 +56,7 @@ class _ActionPanelState extends State<ActionPanel>
// 动态点赞 // 动态点赞
Future onLikeDynamic() async { Future onLikeDynamic() async {
feedBack(); feedBack();
var item = widget.item!; var item = widget.item;
String dynamicId = item.idStr!; String dynamicId = item.idStr!;
// 1 已点赞 2 不喜欢 0 未操作 // 1 已点赞 2 不喜欢 0 未操作
Like like = item.modules!.moduleStat!.like!; Like like = item.modules!.moduleStat!.like!;
@ -87,300 +83,38 @@ class _ActionPanelState extends State<ActionPanel>
} }
} }
// 转发动态预览
Widget dynamicPreview() {
ItemModulesModel? modules = widget.item.modules;
final String type = widget.item.type!;
String? cover = modules?.moduleAuthor?.face;
switch (type) {
/// 图文动态
case 'DYNAMIC_TYPE_DRAW':
cover = modules?.moduleDynamic?.major?.opus?.pics?.first.url;
/// 投稿
case 'DYNAMIC_TYPE_AV':
cover = modules?.moduleDynamic?.major?.archive?.cover;
/// 转发的动态
case 'DYNAMIC_TYPE_FORWARD':
String forwardType = widget.item.orig!.type!;
switch (forwardType) {
/// 图文动态
case 'DYNAMIC_TYPE_DRAW':
cover = modules?.moduleDynamic?.major?.opus?.pics?.first.url;
/// 投稿
case 'DYNAMIC_TYPE_AV':
cover = modules?.moduleDynamic?.major?.archive?.cover;
/// 专栏文章
case 'DYNAMIC_TYPE_ARTICLE':
cover = '';
/// 番剧
case 'DYNAMIC_TYPE_PGC':
cover = '';
/// 纯文字动态
case 'DYNAMIC_TYPE_WORD':
cover = '';
/// 直播
case 'DYNAMIC_TYPE_LIVE_RCMD':
cover = '';
/// 合集查看
case 'DYNAMIC_TYPE_UGC_SEASON':
cover = '';
/// 番剧
case 'DYNAMIC_TYPE_PGC_UNION':
cover = modules?.moduleDynamic?.major?.pgc?.cover;
default:
cover = '';
}
/// 专栏文章
case 'DYNAMIC_TYPE_ARTICLE':
cover = '';
/// 番剧
case 'DYNAMIC_TYPE_PGC':
cover = '';
/// 纯文字动态
case 'DYNAMIC_TYPE_WORD':
cover = '';
/// 直播
case 'DYNAMIC_TYPE_LIVE_RCMD':
cover = '';
/// 合集查看
case 'DYNAMIC_TYPE_UGC_SEASON':
cover = '';
/// 番剧查看
case 'DYNAMIC_TYPE_PGC_UNION':
cover = '';
default:
cover = '';
}
return Container(
width: double.infinity,
height: 95,
margin: const EdgeInsets.fromLTRB(12, 0, 12, 14),
decoration: BoxDecoration(
color:
Theme.of(context).colorScheme.secondaryContainer.withOpacity(0.4),
borderRadius: BorderRadius.circular(6),
border: Border(
left: BorderSide(
width: 4,
color: Theme.of(context).colorScheme.primary.withOpacity(0.8)),
),
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 14),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'@${widget.item.modules!.moduleAuthor!.name}',
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
),
const SizedBox(height: 8),
Row(
children: [
NetworkImgLayer(
src: cover ?? '',
width: 34,
height: 34,
type: 'emote',
),
const SizedBox(width: 10),
Expanded(
child: Text.rich(
style: const TextStyle(height: 0),
richNode(widget.item, context),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
// Text(data)
],
)
],
),
),
);
}
// 动态转发 // 动态转发
void forwardHandler() async { void forwardHandler() async {
final userInfo = _dynamicsController.userInfo;
if (userInfo == null) {
SmartDialog.showToast('请先登录');
return;
}
int mid = userInfo.mid!;
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
enableDrag: false, enableDrag: true,
useRootNavigator: true, useRootNavigator: true,
isScrollControlled: true, isScrollControlled: true,
useSafeArea: true,
builder: (context) { builder: (context) {
return Obx( return DynamicForwardPage(
() => AnimatedContainer( item: widget.item,
duration: Durations.medium1, mid: mid,
onEnd: () async { cb: () => setState(() {
if (isExpand.value) { stat.forward!.count =
await Future.delayed(const Duration(milliseconds: 80)); (int.parse(stat.forward!.count ?? '0') + 1).toString();
myFocusNode.requestFocus(); }),
}
},
height: height.value + MediaQuery.of(context).padding.bottom,
child: Column(
children: [
AnimatedContainer(
duration: Durations.medium1,
height: isExpand.value ? statusHeight : 0,
),
Padding(
padding: EdgeInsets.fromLTRB(
isExpand.value ? 10 : 16,
10,
isExpand.value ? 14 : 12,
0,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (isExpand.value) ...[
IconButton(
onPressed: () => togglePanelState(false),
icon: const Icon(Icons.close),
),
Text(
'转发动态',
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontWeight: FontWeight.bold),
)
] else ...[
const Text(
'转发动态',
style: TextStyle(fontWeight: FontWeight.bold),
)
],
isExpand.value
? FilledButton(
onPressed: () => dynamicForward('forward'),
child: const Text('转发'),
)
: TextButton(
onPressed: () {},
child: const Text('立即转发'),
)
],
),
),
if (!isExpand.value) ...[
GestureDetector(
onTap: () => togglePanelState(true),
behavior: HitTestBehavior.translucent,
child: Container(
width: double.infinity,
alignment: Alignment.centerLeft,
padding: const EdgeInsets.fromLTRB(16, 0, 10, 14),
child: Text(
'说点什么吧',
textAlign: TextAlign.start,
style: TextStyle(
color: Theme.of(context).colorScheme.outline),
),
),
),
] else ...[
Padding(
padding: const EdgeInsets.fromLTRB(16, 10, 16, 0),
child: TextField(
maxLines: 5,
focusNode: myFocusNode,
controller: _inputController,
onChanged: (value) {
setState(() {
_inputText = value;
});
},
decoration: const InputDecoration(
border: InputBorder.none,
hintText: '说点什么吧',
),
),
),
],
dynamicPreview(),
if (!isExpand.value) ...[
const Divider(thickness: 0.1, height: 1),
ListTile(
onTap: () => Get.back(),
minLeadingWidth: 0,
dense: true,
title: Text(
'取消',
style: TextStyle(
color: Theme.of(context).colorScheme.outline),
textAlign: TextAlign.center,
),
),
]
],
),
),
); );
}, },
); );
} }
togglePanelState(status) {
if (!status) {
Get.back();
height.value = defaultHeight;
_inputText = '';
_inputController.clear();
} else {
height.value = Get.size.height;
}
isExpand.value = !(isExpand.value);
}
dynamicForward(String type) async {
String dynamicId = widget.item.idStr!;
var res = await DynamicsHttp.dynamicCreate(
dynIdStr: dynamicId,
mid: _dynamicsController.userInfo.mid,
rawText: _inputText,
scene: 4,
);
if (res['status']) {
SmartDialog.showToast(type == 'forward' ? '转发成功' : '发布成功');
togglePanelState(false);
}
}
@override
void dispose() {
myFocusNode.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var color = Theme.of(context).colorScheme.outline; var color = Theme.of(context).colorScheme.outline;
var primary = Theme.of(context).colorScheme.primary; var primary = Theme.of(context).colorScheme.primary;
height.value = defaultHeight; height.value = defaultHeight;
print('height.value: ${height.value}');
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [ children: [

View File

@ -1,5 +1,4 @@
// 内容 // 内容
import 'package:cached_network_image/cached_network_image.dart';
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/badge.dart'; import 'package:pilipala/common/widgets/badge.dart';
@ -166,31 +165,6 @@ class _ContentState extends State<Content> {
builder: (BuildContext context) => InteractiveviewerGallery( builder: (BuildContext context) => InteractiveviewerGallery(
sources: picList, sources: picList,
initIndex: initIndex, initIndex: initIndex,
itemBuilder: (
BuildContext context,
int index,
bool isFocus,
bool enablePageView,
) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (enablePageView) {
Navigator.of(context).pop();
}
},
child: Center(
child: Hero(
tag: picList[index],
child: CachedNetworkImage(
fadeInDuration: const Duration(milliseconds: 0),
imageUrl: picList[index],
fit: BoxFit.contain,
),
),
),
);
},
onPageChanged: (int pageIndex) {}, onPageChanged: (int pageIndex) {},
), ),
), ),

View File

@ -66,76 +66,85 @@ Widget liveRcmdPanel(item, context, {floor = 1}) {
}, },
child: LayoutBuilder(builder: (context, box) { child: LayoutBuilder(builder: (context, box) {
double width = box.maxWidth; double width = box.maxWidth;
return Stack( return Container(
children: [ margin: floor == 1
Hero( ? const EdgeInsets.only(
tag: liveRcmd.roomId.toString(), left: StyleString.safeSpace, right: StyleString.safeSpace)
child: NetworkImgLayer( : EdgeInsets.zero,
type: floor == 1 ? 'emote' : null, clipBehavior: Clip.hardEdge,
width: width, decoration: const BoxDecoration(
height: width / StyleString.aspectRatio, borderRadius: BorderRadius.all(StyleString.imgRadius)),
src: item.modules.moduleDynamic.major.liveRcmd.cover, child: Stack(
), children: [
), Hero(
PBadge( tag: liveRcmd.roomId.toString(),
text: watchedShow['text_large'], child: NetworkImgLayer(
top: 6, type: floor == 1 ? 'emote' : null,
right: 56, width: width,
bottom: null, height: width / StyleString.aspectRatio,
left: null, src: item.modules.moduleDynamic.major.liveRcmd.cover,
type: 'gray',
),
PBadge(
text: liveStatus == 1 ? '直播中' : '直播结束',
top: 6,
right: 6,
bottom: null,
left: null,
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
height: 80,
padding: const EdgeInsets.fromLTRB(12, 0, 10, 10),
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: <Color>[
Colors.transparent,
Colors.black45,
],
),
borderRadius: floor == 1
? null
: const BorderRadius.all(Radius.circular(6))),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
DefaultTextStyle.merge(
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize,
color: Colors.white),
child: Row(
children: [
Text(item.modules.moduleDynamic.major.liveRcmd
.areaName ??
''),
],
),
),
],
), ),
), ),
), PBadge(
], text: watchedShow['text_large'],
top: 8.0,
right: 62.0,
bottom: null,
left: null,
type: 'gray',
),
PBadge(
text: liveStatus == 1 ? '直播中' : '直播结束',
top: 8.0,
right: 10.0,
bottom: null,
left: null,
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
height: 80,
padding: const EdgeInsets.fromLTRB(12, 0, 10, 10),
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: <Color>[
Colors.transparent,
Colors.black45,
],
),
borderRadius: floor == 1
? null
: const BorderRadius.all(Radius.circular(6))),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
DefaultTextStyle.merge(
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize,
color: Colors.white),
child: Row(
children: [
Text(item.modules.moduleDynamic.major.liveRcmd
.areaName ??
''),
],
),
),
],
),
),
),
],
),
); );
}), }),
), ),

View File

@ -1,4 +1,3 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/constants.dart';
@ -12,31 +11,6 @@ void onPreviewImg(currentUrl, picList, initIndex, context) {
builder: (BuildContext context) => InteractiveviewerGallery( builder: (BuildContext context) => InteractiveviewerGallery(
sources: picList, sources: picList,
initIndex: initIndex, initIndex: initIndex,
itemBuilder: (
BuildContext context,
int index,
bool isFocus,
bool enablePageView,
) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (enablePageView) {
Navigator.of(context).pop();
}
},
child: Center(
child: Hero(
tag: picList[index],
child: CachedNetworkImage(
fadeInDuration: const Duration(milliseconds: 0),
imageUrl: picList[index],
fit: BoxFit.contain,
),
),
),
);
},
onPageChanged: (int pageIndex) {}, onPageChanged: (int pageIndex) {},
), ),
), ),

View File

@ -4,17 +4,22 @@ 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/up.dart'; import 'package:pilipala/models/dynamics/up.dart';
import 'package:pilipala/models/live/item.dart'; import 'package:pilipala/models/live/item.dart';
import 'package:pilipala/plugin/pl_popup/index.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/global_data_cache.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import '../controller.dart';
import '../up_dynamic/route_panel.dart';
class UpPanel extends StatefulWidget { class UpPanel extends StatefulWidget {
final FollowUpModel upData; final FollowUpModel upData;
final Function? onClickUpCb; final DynamicsController dynamicsController;
const UpPanel({ const UpPanel({
super.key, super.key,
required this.upData, required this.upData,
this.onClickUpCb, required this.dynamicsController,
}); });
@override @override
@ -23,12 +28,13 @@ class UpPanel extends StatefulWidget {
class _UpPanelState extends State<UpPanel> { class _UpPanelState extends State<UpPanel> {
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
int currentMid = -1; RxInt currentMid = (-1).obs;
late double contentWidth = 56; late double contentWidth = 56;
List<UpItem> upList = []; List<UpItem> upList = [];
List<LiveUserItem> liveList = []; List<LiveUserItem> liveList = [];
static const itemPadding = EdgeInsets.symmetric(horizontal: 5, vertical: 0); static const itemPadding = EdgeInsets.symmetric(horizontal: 5, vertical: 0);
late MyInfo userInfo; late MyInfo userInfo;
RxBool showLiveUser = false.obs;
void listFormat() { void listFormat() {
userInfo = widget.upData.myInfo!; userInfo = widget.upData.myInfo!;
@ -37,26 +43,44 @@ class _UpPanelState extends State<UpPanel> {
} }
void onClickUp(data, i) { void onClickUp(data, i) {
currentMid = data.mid; currentMid.value = data.mid;
widget.onClickUpCb?.call(data); Navigator.push(
// int liveLen = liveList.length; context,
// int upLen = upList.length; PlPopupRoute(
// double itemWidth = contentWidth + itemPadding.horizontal; child: OverlayPanel(
// double screenWidth = MediaQuery.sizeOf(context).width; ctr: widget.dynamicsController,
// double moveDistance = 0.0; upInfo: data,
// if (itemWidth * (upList.length + liveList.length) <= screenWidth) { ),
// } else if ((upLen - i - 0.5) * itemWidth > screenWidth / 2) { ),
// moveDistance = (i + liveLen + 0.5) * itemWidth + 46 - screenWidth / 2; ).then((value) => {currentMid.value = -1});
// } else { }
// moveDistance = (upLen + liveLen) * itemWidth + 46 - screenWidth;
// } void onClickUpAni(data, i) {
// data.hasUpdate = false; final screenWidth = MediaQuery.sizeOf(context).width;
// scrollController.animateTo( final itemWidth = contentWidth + itemPadding.horizontal;
// moveDistance, final liveLen = liveList.length;
// duration: const Duration(milliseconds: 200), final upLen = upList.length;
// curve: Curves.linear,
// ); currentMid.value = data.mid;
// setState(() {}); widget.dynamicsController.onTapUp(data);
double moveDistance = 0.0;
final totalItemsWidth = itemWidth * (upLen + liveLen);
if (totalItemsWidth > screenWidth) {
if ((upLen - i - 0.5) * itemWidth > screenWidth / 2) {
moveDistance = (i + liveLen + 0.5) * itemWidth + 46 - screenWidth / 2;
} else {
moveDistance = totalItemsWidth + 46 - screenWidth;
}
}
data.hasUpdate = false;
scrollController.animateTo(
moveDistance,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
} }
@override @override
@ -108,21 +132,70 @@ class _UpPanelState extends State<UpPanel> {
children: [ children: [
const SizedBox(width: 10), const SizedBox(width: 10),
if (liveList.isNotEmpty) ...[ if (liveList.isNotEmpty) ...[
for (int i = 0; i < liveList.length; i++) ...[ Obx(
upItemBuild(liveList[i], i) () => AnimatedSwitcher(
], duration: const Duration(milliseconds: 300),
VerticalDivider( transitionBuilder: (Widget child,
indent: 20, Animation<double> animation) {
endIndent: 40, return FadeTransition(
width: 26, opacity: animation, child: child);
color: Theme.of(context) },
.colorScheme child: showLiveUser.value
.primary ? Row(
.withOpacity(0.5), key: ValueKey<bool>(showLiveUser.value),
children: [
for (int i = 0;
i < liveList.length;
i++)
UpItemWidget(
data: liveList[i],
index: i,
currentMid: currentMid,
onClickUp: onClickUp,
onClickUpAni: onClickUpAni,
itemPadding: itemPadding,
contentWidth: contentWidth,
)
],
)
: SizedBox.shrink(
key: ValueKey<bool>(showLiveUser.value),
),
),
),
Obx(
() => IconButton(
onPressed: () {
showLiveUser.value = !showLiveUser.value;
},
icon: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (Widget child,
Animation<double> animation) {
return ScaleTransition(
scale: animation, child: child);
},
child: Icon(
!showLiveUser.value
? Icons.arrow_forward_ios_rounded
: Icons.arrow_back_ios_rounded,
key: ValueKey<bool>(showLiveUser.value),
size: 18,
),
),
),
), ),
], ],
for (int i = 0; i < upList.length; i++) ...[ for (int i = 0; i < upList.length; i++) ...[
upItemBuild(upList[i], i) UpItemWidget(
data: upList[i],
index: i,
currentMid: currentMid,
onClickUp: onClickUp,
onClickUpAni: onClickUpAni,
itemPadding: itemPadding,
contentWidth: contentWidth,
)
], ],
const SizedBox(width: 10), const SizedBox(width: 10),
], ],
@ -142,100 +215,6 @@ class _UpPanelState extends State<UpPanel> {
)), )),
); );
} }
Widget upItemBuild(data, i) {
bool isCurrent = currentMid == data.mid || currentMid == -1;
return InkWell(
onTap: () {
feedBack();
if (data.type == 'up') {
EasyThrottle.throttle('follow', const Duration(milliseconds: 300),
() {
onClickUp(data, i);
});
} else if (data.type == 'live') {
LiveItemModel liveItem = LiveItemModel.fromJson({
'title': data.title,
'uname': data.uname,
'face': data.face,
'roomid': data.roomId,
});
Get.toNamed(
'/liveRoom?roomid=${data.roomId}',
arguments: {'liveItem': liveItem},
);
}
},
onLongPress: () {
feedBack();
if (data.mid == -1) {
return;
}
String heroTag = Utils.makeHeroTag(data.mid);
Get.toNamed('/member?mid=${data.mid}',
arguments: {'face': data.face, 'heroTag': heroTag});
},
child: Padding(
padding: itemPadding,
child: AnimatedOpacity(
opacity: isCurrent ? 1 : 0.3,
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Badge(
smallSize: 8,
label: data.type == 'live' ? const Text('Live') : null,
textColor: Theme.of(context).colorScheme.onSecondaryContainer,
alignment: data.type == 'live'
? AlignmentDirectional.topCenter
: AlignmentDirectional.topEnd,
padding: const EdgeInsets.only(left: 6, right: 6),
isLabelVisible: data.type == 'live' ||
(data.type == 'up' && (data.hasUpdate ?? false)),
backgroundColor: data.type == 'live'
? Theme.of(context).colorScheme.secondaryContainer
: Theme.of(context).colorScheme.primary,
child: data.face != ''
? NetworkImgLayer(
width: 50,
height: 50,
src: data.face,
type: 'avatar',
)
: const CircleAvatar(
radius: 25,
backgroundImage: AssetImage(
'assets/images/noface.jpeg',
),
),
),
Padding(
padding: const EdgeInsets.only(top: 4),
child: SizedBox(
width: contentWidth,
child: Text(
data.uname,
overflow: TextOverflow.ellipsis,
softWrap: false,
textAlign: TextAlign.center,
style: TextStyle(
color: currentMid == data.mid
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline,
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize),
),
),
),
],
),
),
),
);
}
} }
class _SliverHeaderDelegate extends SliverPersistentHeaderDelegate { class _SliverHeaderDelegate extends SliverPersistentHeaderDelegate {
@ -298,3 +277,129 @@ class UpPanelSkeleton extends StatelessWidget {
); );
} }
} }
class UpItemWidget extends StatelessWidget {
final dynamic data;
final int index;
final RxInt currentMid;
final Function(dynamic, int) onClickUp;
final Function(dynamic, int) onClickUpAni;
// final Function() feedBack;
final EdgeInsets itemPadding;
final double contentWidth;
const UpItemWidget({
Key? key,
required this.data,
required this.index,
required this.currentMid,
required this.onClickUp,
required this.onClickUpAni,
// required this.feedBack,
required this.itemPadding,
required this.contentWidth,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () {
feedBack();
if (data.type == 'up') {
EasyThrottle.throttle('follow', const Duration(milliseconds: 300),
() {
if (GlobalDataCache.enableDynamicSwitch) {
onClickUp(data, index);
} else {
onClickUpAni(data, index);
}
});
} else if (data.type == 'live') {
LiveItemModel liveItem = LiveItemModel.fromJson({
'title': data.title,
'uname': data.uname,
'face': data.face,
'roomid': data.roomId,
});
Get.toNamed(
'/liveRoom?roomid=${data.roomId}',
arguments: {'liveItem': liveItem},
);
}
},
onLongPress: () {
feedBack();
if (data.mid == -1) {
return;
}
String heroTag = Utils.makeHeroTag(data.mid);
Get.toNamed('/member?mid=${data.mid}',
arguments: {'face': data.face, 'heroTag': heroTag});
},
child: Padding(
padding: itemPadding,
child: Obx(
() => AnimatedOpacity(
opacity: currentMid.value == data.mid || currentMid.value == -1
? 1
: 0.3,
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Badge(
smallSize: 8,
label: data.type == 'live' ? const Text('Live') : null,
textColor: Theme.of(context).colorScheme.onSecondaryContainer,
alignment: data.type == 'live'
? AlignmentDirectional.topCenter
: AlignmentDirectional.topEnd,
padding: const EdgeInsets.only(left: 6, right: 6),
isLabelVisible: data.type == 'live' ||
(data.type == 'up' && (data.hasUpdate ?? false)),
backgroundColor: data.type == 'live'
? Theme.of(context).colorScheme.secondaryContainer
: Theme.of(context).colorScheme.primary,
child: data.face != ''
? NetworkImgLayer(
width: 50,
height: 50,
src: data.face,
type: 'avatar',
)
: const CircleAvatar(
radius: 25,
backgroundImage: AssetImage(
'assets/images/noface.jpeg',
),
),
),
Padding(
padding: const EdgeInsets.only(top: 4),
child: SizedBox(
width: contentWidth,
child: Text(
data.uname,
overflow: TextOverflow.ellipsis,
softWrap: false,
textAlign: TextAlign.center,
style: TextStyle(
color: currentMid.value == data.mid
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline,
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
),
),
),
),
],
),
),
),
),
);
}
}

View File

@ -78,72 +78,83 @@ Widget videoSeasonWidget(item, context, type, {floor = 1}) {
], ],
LayoutBuilder(builder: (context, box) { LayoutBuilder(builder: (context, box) {
double width = box.maxWidth; double width = box.maxWidth;
return Stack( return Container(
children: [ margin: floor == 1
NetworkImgLayer( ? const EdgeInsets.only(
type: floor == 1 ? 'emote' : null, left: StyleString.safeSpace, right: StyleString.safeSpace)
width: width, : EdgeInsets.zero,
height: width / StyleString.aspectRatio, clipBehavior: Clip.hardEdge,
src: content.cover, decoration: const BoxDecoration(
), borderRadius: BorderRadius.all(StyleString.imgRadius)),
if (content.badge != null && content.badge['text'] != null) child: Stack(
PBadge( children: [
text: content.badge['text'], NetworkImgLayer(
top: 8.0, type: floor == 1 ? 'emote' : null,
right: 10.0, width: width,
bottom: null, height: width / StyleString.aspectRatio,
left: null, src: content.cover,
), ),
Positioned( if (content.badge != null && content.badge['text'] != null)
left: 0, PBadge(
right: 0, text: content.badge['text'],
bottom: 0, top: 8.0,
child: Container( right: 10.0,
height: 80, bottom: null,
padding: const EdgeInsets.fromLTRB(12, 0, 10, 10), left: null,
clipBehavior: Clip.hardEdge, ),
decoration: BoxDecoration( Positioned(
gradient: const LinearGradient( left: 0,
begin: Alignment.topCenter, right: 0,
end: Alignment.bottomCenter, bottom: 0,
colors: <Color>[ child: Container(
Colors.transparent, height: 80,
Colors.black54, padding: const EdgeInsets.fromLTRB(12, 0, 10, 10),
], clipBehavior: Clip.hardEdge,
), decoration: BoxDecoration(
borderRadius: floor == 1 gradient: const LinearGradient(
? null begin: Alignment.topCenter,
: const BorderRadius.all(Radius.circular(6))), end: Alignment.bottomCenter,
child: Row( colors: <Color>[
mainAxisAlignment: MainAxisAlignment.spaceBetween, Colors.transparent,
crossAxisAlignment: CrossAxisAlignment.end, Colors.black54,
children: [
DefaultTextStyle.merge(
style: TextStyle(
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
color: Colors.white),
child: Row(
children: [
Text(content.durationText ?? ''),
if (content.durationText != null)
const SizedBox(width: 10),
Text(content.stat.play + '次围观'),
const SizedBox(width: 10),
Text(content.stat.danmaku + '条弹幕')
], ],
), ),
), borderRadius: floor == 1
Image.asset( ? null
'assets/images/play.png', : const BorderRadius.all(Radius.circular(6))),
width: 60, child: Row(
height: 60, mainAxisAlignment: MainAxisAlignment.spaceBetween,
), crossAxisAlignment: CrossAxisAlignment.end,
], children: [
DefaultTextStyle.merge(
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize,
color: Colors.white),
child: Row(
children: [
Text(content.durationText ?? ''),
if (content.durationText != null)
const SizedBox(width: 10),
Text(content.stat.play + '次围观'),
const SizedBox(width: 10),
Text(content.stat.danmaku + '条弹幕')
],
),
),
Image.asset(
'assets/images/play.png',
width: 60,
height: 60,
),
],
),
), ),
), ),
), ],
], ),
); );
}), }),
const SizedBox(height: 6), const SizedBox(height: 6),

View File

@ -3,17 +3,18 @@ import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/http/fan.dart'; import 'package:pilipala/http/fan.dart';
import 'package:pilipala/models/fans/result.dart'; import 'package:pilipala/models/fans/result.dart';
import 'package:pilipala/models/user/info.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
class FansController extends GetxController { class FansController extends GetxController {
Box userInfoCache = GStrorage.userInfo; Box userInfoCache = GStorage.userInfo;
int pn = 1; int pn = 1;
int ps = 20; int ps = 20;
int total = 0; int total = 0;
RxList<FansItemModel> fansList = <FansItemModel>[].obs; RxList<FansItemModel> fansList = <FansItemModel>[].obs;
late int mid; late int mid;
late String name; late String name;
var userInfo; UserInfoData? userInfo;
RxString loadingText = '加载中...'.obs; RxString loadingText = '加载中...'.obs;
RxBool isOwner = false.obs; RxBool isOwner = false.obs;
@ -23,9 +24,9 @@ class FansController extends GetxController {
userInfo = userInfoCache.get('userInfoCache'); userInfo = userInfoCache.get('userInfoCache');
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; isOwner.value = mid == userInfo?.mid;
name = Get.parameters['name'] ?? userInfo.uname; name = Get.parameters['name'] ?? userInfo?.uname ?? '';
} }
Future queryFans(type) async { Future queryFans(type) async {
@ -49,7 +50,6 @@ class FansController extends GetxController {
} else if (type == 'onLoad') { } else if (type == 'onLoad') {
fansList.addAll(res['data'].list); fansList.addAll(res['data'].list);
} }
print(total);
if ((pn == 1 && total < ps) || res['data'].list.isEmpty) { if ((pn == 1 && total < ps) || res['data'].list.isEmpty) {
loadingText.value = '没有更多了'; loadingText.value = '没有更多了';
} }

View File

@ -49,13 +49,8 @@ class _FansPageState extends State<FansPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
elevation: 0,
scrolledUnderElevation: 0,
centerTitle: false,
titleSpacing: 0,
title: Text( title: Text(
_fansController.isOwner.value ? '我的粉丝' : '${_fansController.name}的粉丝', _fansController.isOwner.value ? '我的粉丝' : '${_fansController.name}的粉丝',
style: Theme.of(context).textTheme.titleMedium,
), ),
), ),
body: RefreshIndicator( body: RefreshIndicator(
@ -105,7 +100,12 @@ class _FansPageState extends State<FansPage> {
} else { } else {
return HttpError( return HttpError(
errMsg: data['msg'], errMsg: data['msg'],
fn: () => _fansController.queryFans('init'), fn: () {
setState(() {
_futureBuilderFuture = _fansController.queryFans('init');
});
},
isInSliver: false,
); );
} }
} else { } else {

View File

@ -11,7 +11,7 @@ class FavController extends GetxController {
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
Rx<FavFolderData> favFolderData = FavFolderData().obs; Rx<FavFolderData> favFolderData = FavFolderData().obs;
RxList<FavFolderItemData> favFolderList = <FavFolderItemData>[].obs; RxList<FavFolderItemData> favFolderList = <FavFolderItemData>[].obs;
Box userInfoCache = GStrorage.userInfo; Box userInfoCache = GStorage.userInfo;
UserInfoData? userInfo; UserInfoData? userInfo;
int currentPage = 1; int currentPage = 1;
int pageSize = 60; int pageSize = 60;

View File

@ -40,11 +40,8 @@ class _FavPageState extends State<FavPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
centerTitle: false,
titleSpacing: 0,
title: Obx(() => Text( title: Obx(() => Text(
'${_favController.isOwner.value ? '' : 'Ta'}的收藏', '${_favController.isOwner.value ? '' : 'Ta'}的收藏',
style: Theme.of(context).textTheme.titleMedium,
)), )),
actions: [ actions: [
Obx(() => !_favController.isOwner.value Obx(() => !_favController.isOwner.value
@ -79,56 +76,64 @@ class _FavPageState extends State<FavPage> {
const SizedBox(width: 14), const SizedBox(width: 14),
], ],
), ),
body: FutureBuilder( body: RefreshIndicator(
future: _futureBuilderFuture, onRefresh: () async {
builder: (context, snapshot) { _favController.hasMore.value = true;
if (snapshot.connectionState == ConnectionState.done) { _favController.currentPage = 1;
Map? data = snapshot.data; setState(() {
if (data != null && data['status']) { _futureBuilderFuture = _favController.queryFavFolder(type: 'init');
return Obx( });
() => ListView.builder(
controller: scrollController,
itemCount: _favController.favFolderList.length,
itemBuilder: (context, index) {
return FavItem(
favFolderItem: _favController.favFolderList[index],
isOwner: _favController.isOwner.value,
);
},
),
);
} else {
return CustomScrollView(
physics: const NeverScrollableScrollPhysics(),
slivers: [
HttpError(
errMsg: data?['msg'] ?? '请求异常',
btnText: data?['code'] == -101 ? '去登录' : null,
fn: () {
if (data?['code'] == -101) {
RoutePush.loginRedirectPush();
} else {
setState(() {
_futureBuilderFuture =
_favController.queryFavFolder();
});
}
},
),
],
);
}
} else {
// 骨架屏
return ListView.builder(
itemBuilder: (context, index) {
return const VideoCardHSkeleton();
},
itemCount: 10,
);
}
}, },
child: _buildBody(),
), ),
); );
} }
Widget _buildBody() {
return FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map? data = snapshot.data;
if (data != null && data['status']) {
return Obx(
() => ListView.builder(
controller: scrollController,
itemCount: _favController.favFolderList.length,
itemBuilder: (context, index) {
return FavItem(
favFolderItem: _favController.favFolderList[index],
isOwner: _favController.isOwner.value,
);
},
),
);
} else {
return HttpError(
errMsg: data?['msg'] ?? '请求异常',
btnText: data?['code'] == -101 ? '去登录' : null,
fn: () {
if (data?['code'] == -101) {
RoutePush.loginRedirectPush();
} else {
setState(() {
_futureBuilderFuture = _favController.queryFavFolder();
});
}
},
isInSliver: false,
);
}
} else {
// 骨架屏
return ListView.builder(
itemBuilder: (context, index) {
return const VideoCardHSkeleton();
},
itemCount: 10,
);
}
},
);
}
} }

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.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/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/utils/logic_utils.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
class FavItem extends StatelessWidget { class FavItem extends StatelessWidget {
@ -96,7 +97,7 @@ class VideoContent extends StatelessWidget {
), ),
const Spacer(), const Spacer(),
Text( Text(
[23, 1].contains(favFolderItem.attr) ? '私密' : '公开', LogicUtils.isPublic(favFolderItem.attr) ? '公开' : '私密',
textAlign: TextAlign.start, textAlign: TextAlign.start,
style: TextStyle( style: TextStyle(
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,

View File

@ -1,29 +1,35 @@
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:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/http/common.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/user/fav_detail.dart'; import 'package:pilipala/models/user/fav_detail.dart';
import 'package:pilipala/models/user/fav_folder.dart'; import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/pages/fav/index.dart'; import 'package:pilipala/pages/fav/index.dart';
import 'package:pilipala/utils/utils.dart';
import 'widget/invalid_video_card.dart';
class FavDetailController extends GetxController { class FavDetailController extends GetxController {
FavFolderItemData? item; FavFolderItemData? item;
Rx<FavDetailData> favDetailData = FavDetailData().obs; RxString title = ''.obs;
int? mediaId; int? mediaId;
late String heroTag; late String heroTag;
int currentPage = 1; int currentPage = 1;
bool isLoadingMore = false; bool isLoadingMore = false;
RxMap favInfo = {}.obs; RxMap favInfo = {}.obs;
RxList favList = [].obs; RxList<FavDetailItemData> favList = <FavDetailItemData>[].obs;
RxString loadingText = '加载中...'.obs; RxString loadingText = '加载中...'.obs;
RxInt mediaCount = 0.obs; RxInt mediaCount = 0.obs;
late String isOwner; late String isOwner;
late bool hasMore = true;
@override @override
void onInit() { void onInit() {
item = Get.arguments; item = Get.arguments;
title.value = item!.title!;
if (Get.parameters.keys.isNotEmpty) { if (Get.parameters.keys.isNotEmpty) {
mediaId = int.parse(Get.parameters['mediaId']!); mediaId = int.parse(Get.parameters['mediaId']!);
heroTag = Get.parameters['heroTag']!; heroTag = Get.parameters['heroTag']!;
@ -33,7 +39,7 @@ class FavDetailController extends GetxController {
} }
Future<dynamic> queryUserFavFolderDetail({type = 'init'}) async { Future<dynamic> queryUserFavFolderDetail({type = 'init'}) async {
if (type == 'onLoad' && favList.length >= mediaCount.value) { if (type == 'onLoad' && !hasMore) {
loadingText.value = '没有更多了'; loadingText.value = '没有更多了';
return; return;
} }
@ -45,17 +51,18 @@ class FavDetailController extends GetxController {
); );
if (res['status']) { if (res['status']) {
favInfo.value = res['data'].info; favInfo.value = res['data'].info;
hasMore = res['data'].hasMore;
if (currentPage == 1 && type == 'init') { if (currentPage == 1 && type == 'init') {
favList.value = res['data'].medias; favList.value = res['data'].medias;
mediaCount.value = res['data'].info['media_count']; mediaCount.value = res['data'].info['media_count'];
} else if (type == 'onLoad') { } else if (type == 'onLoad') {
favList.addAll(res['data'].medias); favList.addAll(res['data'].medias);
} }
if (favList.length >= mediaCount.value) { if (!hasMore) {
loadingText.value = '没有更多了'; loadingText.value = '没有更多了';
} }
currentPage += 1;
} }
currentPage += 1;
isLoadingMore = false; isLoadingMore = false;
return res; return res;
} }
@ -117,15 +124,53 @@ class FavDetailController extends GetxController {
} }
onEditFavFolder() async { onEditFavFolder() async {
Get.toNamed( var res = await Get.toNamed(
'/favEdit', '/favEdit',
arguments: { arguments: {
'mediaId': mediaId.toString(), 'mediaId': mediaId.toString(),
'title': item!.title, 'title': item!.title,
'intro': item!.intro, 'intro': item!.intro,
'cover': item!.cover, 'cover': item!.cover,
'privacy': item!.attr, 'privacy': [22, 0].contains(item!.attr) ? 0 : 1,
},
);
title.value = res['title'];
print(title);
}
Future toViewPlayAll() async {
final FavDetailItemData firstItem = favList.first;
final String heroTag = Utils.makeHeroTag(firstItem.bvid);
Get.toNamed(
'/video?bvid=${firstItem.bvid}&cid=${firstItem.cid}',
arguments: {
'videoItem': firstItem,
'heroTag': heroTag,
'sourceType': 'fav',
'mediaId': favInfo['id'],
'oid': firstItem.id,
'favTitle': favInfo['title'],
'favInfo': favInfo,
'count': favInfo['media_count'],
}, },
); );
} }
// 查看无效视频信息
Future toViewInvalidVideo(FavDetailItemData item) async {
SmartDialog.showLoading(msg: '加载中...');
var res = await CommonHttp.fixVideoPicAndTitle(aid: item.id!);
SmartDialog.dismiss();
if (res['status']) {
showModalBottomSheet(
context: Get.context!,
isScrollControlled: true,
builder: (context) {
return InvalidVideoCard(videoInfo: res['data']);
},
);
} else {
SmartDialog.showToast(res['msg']);
}
}
} }

View File

@ -22,7 +22,8 @@ class _FavDetailPageState extends State<FavDetailPage> {
late final ScrollController _controller = ScrollController(); late final ScrollController _controller = ScrollController();
final FavDetailController _favDetailController = final FavDetailController _favDetailController =
Get.put(FavDetailController()); Get.put(FavDetailController());
late StreamController<bool> titleStreamC; // a late StreamController<bool> titleStreamC =
StreamController<bool>.broadcast(); // a
Future? _futureBuilderFuture; Future? _futureBuilderFuture;
late String mediaId; late String mediaId;
@ -31,7 +32,6 @@ class _FavDetailPageState extends State<FavDetailPage> {
super.initState(); super.initState();
mediaId = Get.parameters['mediaId']!; mediaId = Get.parameters['mediaId']!;
_futureBuilderFuture = _favDetailController.queryUserFavFolderDetail(); _futureBuilderFuture = _favDetailController.queryUserFavFolderDetail();
titleStreamC = StreamController<bool>();
_controller.addListener( _controller.addListener(
() { () {
if (_controller.offset > 160) { if (_controller.offset > 160) {
@ -80,9 +80,11 @@ class _FavDetailPageState extends State<FavDetailPage> {
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Obx(
_favDetailController.item!.title!, () => Text(
style: Theme.of(context).textTheme.titleMedium, _favDetailController.title.value,
style: Theme.of(context).textTheme.titleMedium,
),
), ),
Text( Text(
'${_favDetailController.mediaCount}条视频', '${_favDetailController.mediaCount}条视频',
@ -156,14 +158,16 @@ class _FavDetailPageState extends State<FavDetailPage> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Obx(
_favDetailController.item!.title!, () => Text(
style: TextStyle( _favDetailController.title.value,
fontSize: Theme.of(context) style: TextStyle(
.textTheme fontSize: Theme.of(context)
.titleMedium! .textTheme
.fontSize, .titleMedium!
fontWeight: FontWeight.bold), .fontSize,
fontWeight: FontWeight.bold),
),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
@ -189,7 +193,9 @@ class _FavDetailPageState extends State<FavDetailPage> {
padding: const EdgeInsets.only(top: 15, bottom: 8, left: 14), padding: const EdgeInsets.only(top: 15, bottom: 8, left: 14),
child: Obx( child: Obx(
() => Text( () => Text(
'${_favDetailController.mediaCount}条视频', _favDetailController.mediaCount > 0
? '${_favDetailController.mediaCount}条视频'
: '',
style: TextStyle( style: TextStyle(
fontSize: fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize, Theme.of(context).textTheme.labelMedium!.fontSize,
@ -211,7 +217,7 @@ class _FavDetailPageState extends State<FavDetailPage> {
List favList = _favDetailController.favList; List favList = _favDetailController.favList;
return Obx( return Obx(
() => favList.isEmpty () => favList.isEmpty
? const SliverToBoxAdapter(child: SizedBox()) ? const NoData()
: SliverList( : SliverList(
delegate: delegate:
SliverChildBuilderDelegate((context, index) { SliverChildBuilderDelegate((context, index) {
@ -220,6 +226,8 @@ class _FavDetailPageState extends State<FavDetailPage> {
isOwner: _favDetailController.isOwner, isOwner: _favDetailController.isOwner,
callFn: () => _favDetailController callFn: () => _favDetailController
.onCancelFav(favList[index].id), .onCancelFav(favList[index].id),
viewInvalidVideoCb: () => _favDetailController
.toViewInvalidVideo(favList[index]),
); );
}, childCount: favList.length), }, childCount: favList.length),
), ),
@ -243,23 +251,34 @@ class _FavDetailPageState extends State<FavDetailPage> {
), ),
SliverToBoxAdapter( SliverToBoxAdapter(
child: Container( child: Container(
height: MediaQuery.of(context).padding.bottom + 60, height: MediaQuery.of(context).padding.bottom + 90,
padding: EdgeInsets.only( padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom), bottom: MediaQuery.of(context).padding.bottom),
child: Center( child: Center(
child: Obx( child: Obx(() {
() => Text( final mediaCount = _favDetailController.mediaCount;
_favDetailController.loadingText.value, final loadingText = _favDetailController.loadingText.value;
style: TextStyle( final textColor = Theme.of(context).colorScheme.outline;
color: Theme.of(context).colorScheme.outline,
fontSize: 13), return Text(
), mediaCount > 0 ? loadingText : '',
), style: TextStyle(color: textColor, fontSize: 13),
);
}),
), ),
), ),
) )
], ],
), ),
floatingActionButton: Obx(
() => _favDetailController.mediaCount > 0
? FloatingActionButton.extended(
onPressed: _favDetailController.toViewPlayAll,
label: const Text('播放全部'),
icon: const Icon(Icons.playlist_play),
)
: const SizedBox(),
),
); );
} }
} }

View File

@ -19,6 +19,7 @@ class FavVideoCardH extends StatelessWidget {
final Function? callFn; final Function? callFn;
final int? searchType; final int? searchType;
final String isOwner; final String isOwner;
final Function? viewInvalidVideoCb;
const FavVideoCardH({ const FavVideoCardH({
Key? key, Key? key,
@ -26,6 +27,7 @@ class FavVideoCardH extends StatelessWidget {
this.callFn, this.callFn,
this.searchType, this.searchType,
required this.isOwner, required this.isOwner,
this.viewInvalidVideoCb,
}) : super(key: key); }) : super(key: key);
@override @override
@ -36,6 +38,10 @@ class FavVideoCardH extends StatelessWidget {
return InkWell( return InkWell(
onTap: () async { onTap: () async {
// int? seasonId; // int? seasonId;
if (videoItem.title == '已失效视频') {
viewInvalidVideoCb?.call();
return;
}
String? epId; String? epId;
if (videoItem.ogv != null && if (videoItem.ogv != null &&
(videoItem.ogv['type_name'] == '番剧' || (videoItem.ogv['type_name'] == '番剧' ||
@ -65,11 +71,17 @@ class FavVideoCardH extends StatelessWidget {
epId != null ? SearchType.media_bangumi : SearchType.video, epId != null ? SearchType.media_bangumi : SearchType.video,
}); });
}, },
onLongPress: () => imageSaveDialog( onLongPress: () {
context, if (videoItem.title == '已失效视频') {
videoItem, SmartDialog.showToast('视频已失效');
SmartDialog.dismiss, return;
), }
imageSaveDialog(
context,
videoItem,
SmartDialog.dismiss,
);
},
child: Column( child: Column(
children: [ children: [
Padding( Padding(

View File

@ -0,0 +1,97 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/common/invalid_video.dart';
class InvalidVideoCard extends StatelessWidget {
const InvalidVideoCard({required this.videoInfo, Key? key}) : super(key: key);
final InvalidVideoModel videoInfo;
@override
Widget build(BuildContext context) {
const TextStyle textStyle = TextStyle(fontSize: 14.0);
return Padding(
padding: EdgeInsets.fromLTRB(
12,
14,
12,
MediaQuery.of(context).padding.bottom + 20,
),
child: LayoutBuilder(
builder: (context, constraints) {
double maxWidth = constraints.maxWidth;
double maxHeight = maxWidth * 9 / 16;
return SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
NetworkImgLayer(
width: maxWidth,
height: maxHeight,
src: videoInfo.pic,
radius: 20,
),
const SizedBox(height: 10),
SelectableText(
videoInfo.title!,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 2),
SelectableText(videoInfo.author!, style: textStyle),
const SizedBox(height: 2),
SelectableText('创建时间:${videoInfo.createdAt}', style: textStyle),
SelectableText('更新时间:${videoInfo.lastupdate}',
style: textStyle),
SelectableText('分类:${videoInfo.typename}', style: textStyle),
SelectableText(
'投币:${videoInfo.coins} 收藏:${videoInfo.favorites}',
style: textStyle),
if (videoInfo.tagList != null &&
videoInfo.tagList!.isNotEmpty) ...[
const SizedBox(height: 6),
_buildTags(context, videoInfo.tagList),
],
],
),
);
},
),
);
}
Widget _buildTags(BuildContext context, List<String>? videoTags) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
return Wrap(
spacing: 6,
runSpacing: 6,
direction: Axis.horizontal,
textDirection: TextDirection.ltr,
children: videoTags!.map((tag) {
return InkWell(
onTap: () {
Get.toNamed('/searchResult', parameters: {'keyword': tag});
},
borderRadius: BorderRadius.circular(6),
child: Container(
decoration: BoxDecoration(
color: colorScheme.surfaceVariant.withOpacity(0.5),
borderRadius: BorderRadius.circular(6),
),
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 10),
child: Text(
tag,
style: TextStyle(
fontSize: 12,
color: colorScheme.onSurfaceVariant,
),
),
),
);
}).toList(),
);
}
}

View File

@ -56,7 +56,7 @@ class FavEditController extends GetxController {
); );
if (res['status']) { if (res['status']) {
SmartDialog.showToast('编辑成功'); SmartDialog.showToast('编辑成功');
Get.back(); Get.back(result: {'title': title});
} else { } else {
SmartDialog.showToast(res['msg']); SmartDialog.showToast(res['msg']);
} }

View File

@ -19,8 +19,6 @@ class _FavEditPageState extends State<FavEditPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
elevation: 0,
scrolledUnderElevation: 0,
title: Obx( title: Obx(
() => _favEditController.type.value == 'add' () => _favEditController.type.value == 'add'
? Text( ? Text(
@ -32,7 +30,6 @@ class _FavEditPageState extends State<FavEditPage> {
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
), ),
centerTitle: false,
actions: [ actions: [
Obx( Obx(
() => _favEditController.privacy.value == 0 () => _favEditController.privacy.value == 0

View File

@ -47,7 +47,6 @@ class _FavSearchPageState extends State<FavSearchPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
titleSpacing: 0,
actions: [ actions: [
IconButton( IconButton(
onPressed: () => _favSearchCtr.submit(), onPressed: () => _favSearchCtr.submit(),

View File

@ -6,19 +6,20 @@ import 'package:pilipala/http/follow.dart';
import 'package:pilipala/http/member.dart'; import 'package:pilipala/http/member.dart';
import 'package:pilipala/models/follow/result.dart'; import 'package:pilipala/models/follow/result.dart';
import 'package:pilipala/models/member/tags.dart'; import 'package:pilipala/models/member/tags.dart';
import 'package:pilipala/models/user/info.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
/// 查看自己的关注时,可以查看分类 /// 查看自己的关注时,可以查看分类
/// 查看其他人的关注时,只可以看全部 /// 查看其他人的关注时,只可以看全部
class FollowController extends GetxController with GetTickerProviderStateMixin { class FollowController extends GetxController with GetTickerProviderStateMixin {
Box userInfoCache = GStrorage.userInfo; Box userInfoCache = GStorage.userInfo;
int pn = 1; int pn = 1;
int ps = 20; int ps = 20;
int total = 0; int total = 0;
RxList<FollowItemModel> followList = <FollowItemModel>[].obs; RxList<FollowItemModel> followList = <FollowItemModel>[].obs;
late int mid; late int mid;
late String name; late String name;
var userInfo; UserInfoData? userInfo;
RxString loadingText = '加载中...'.obs; RxString loadingText = '加载中...'.obs;
RxBool isOwner = false.obs; RxBool isOwner = false.obs;
late List<MemberTagItemModel> followTags; late List<MemberTagItemModel> followTags;
@ -30,9 +31,9 @@ class FollowController extends GetxController with GetTickerProviderStateMixin {
userInfo = userInfoCache.get('userInfoCache'); userInfo = userInfoCache.get('userInfoCache');
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; isOwner.value = mid == userInfo?.mid;
name = Get.parameters['name'] ?? userInfo.uname; name = Get.parameters['name'] ?? userInfo?.uname ?? '';
} }
Future queryFollowings(type) async { Future queryFollowings(type) async {
@ -68,7 +69,7 @@ class FollowController extends GetxController with GetTickerProviderStateMixin {
// 当查看当前用户的关注时,请求关注分组 // 当查看当前用户的关注时,请求关注分组
Future followUpTags() async { Future followUpTags() async {
if (userInfo != null && mid == userInfo.mid) { if (userInfo != null && mid == userInfo!.mid) {
var res = await MemberHttp.followUpTags(); var res = await MemberHttp.followUpTags();
if (res['status']) { if (res['status']) {
followTags = res['data']; followTags = res['data'];

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