diff --git a/.github/workflows/beta_ci.yml b/.github/workflows/beta_ci.yml new file mode 100644 index 000000000..e839aca1b --- /dev/null +++ b/.github/workflows/beta_ci.yml @@ -0,0 +1,208 @@ +name: Pilipala Beta + +on: + workflow_dispatch: + push: + branches: + - "main" + paths-ignore: + - "**.md" + - "**.txt" + - ".github/**" + - ".idea/**" + - "!.github/workflows/**" + +jobs: + update_version: + name: Read and update version + runs-on: ubuntu-latest + + outputs: + # 定义输出变量 version,以便在其他job中引用 + new_version: ${{ steps.version.outputs.new_version }} + last_commit: ${{ steps.get-last-commit.outputs.last_commit }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.ref_name }} + fetch-depth: 0 + + - name: 获取first parent commit次数 + id: get-first-parent-commit-count + run: | + version=$(yq e .version pubspec.yaml | cut -d "+" -f 1) + recent_release_tag=$(git tag -l | grep $version | egrep -v "[-|+]" || true) + if [[ "x$recent_release_tag" == "x" ]]; then + echo "当前版本tag不存在,请手动生成tag." + exit 1 + fi + git log --oneline --first-parent $recent_release_tag..HEAD + first_parent_commit_count=$(git rev-list --first-parent --count $recent_release_tag..HEAD) + echo "count=$first_parent_commit_count" >> $GITHUB_OUTPUT + + - name: 获取最后一次提交 + id: get-last-commit + run: | + last_commit=$(git log -1 --pretty="%h %s" --first-parent) + echo "last_commit=$last_commit" >> $GITHUB_OUTPUT + + - name: 更新版本号 + id: version + run: | + # 读取版本号 + VERSION=$(yq e .version pubspec.yaml | cut -d "+" -f 1) + + # 获取GitHub Actions的run_number + #RUN_NUMBER=${{ github.run_number }} + + # 构建新版本号 + NEW_VERSION=$VERSION-beta.${{ steps.get-first-parent-commit-count.outputs.count }} + + # 输出新版本号 + echo "New version: $NEW_VERSION" + + # 设置新版本号为输出变量 + echo "new_version=$NEW_VERSION" >>$GITHUB_OUTPUT + + android: + name: Build CI (Android) + needs: update_version + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.ref_name }} + + - name: 构建Java环境 + uses: actions/setup-java@v3 + with: + distribution: "zulu" + java-version: "17" + token: ${{secrets.GIT_TOKEN}} + + - name: 检查缓存 + uses: actions/cache@v2 + id: cache-flutter + with: + path: /root/flutter-sdk + key: ${{ runner.os }}-flutter-${{ hashFiles('**/pubspec.lock') }} + + - name: 安装Flutter + if: steps.cache-flutter.outputs.cache-hit != 'true' + uses: subosito/flutter-action@v2 + with: + flutter-version: 3.16.5 + channel: any + + - name: 下载项目依赖 + run: flutter pub get + + - name: 解码生成 jks + run: echo $KEYSTORE_BASE64 | base64 -di > android/app/vvex.jks + env: + KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }} + + - name: 更新版本号 + id: version + run: | + # 更新pubspec.yaml文件中的版本号 + sed -i "s/version: .*+/version: ${{ needs.update_version.outputs.new_version }}+/g" pubspec.yaml + + - name: flutter build apk + run: flutter build apk --release --split-per-abi + env: + KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} + KEY_ALIAS: ${{ secrets.KEY_ALIAS }} + KEY_PASSWORD: ${{ secrets.KEY_PASSWORD}} + + - name: 重命名应用 + run: | + for file in build/app/outputs/flutter-apk/app-*.apk; do + if [[ $file =~ app-(.?*)release.apk ]]; then + new_file_name="build/app/outputs/flutter-apk/Pili-${BASH_REMATCH[1]}v${{ needs.update_version.outputs.new_version }}.apk" + mv "$file" "$new_file_name" + fi + done + + - name: 上传 + uses: actions/upload-artifact@v3 + with: + name: Pilipala-Beta + path: | + build/app/outputs/flutter-apk/Pili-*.apk + + iOS: + name: Build CI (iOS) + needs: update_version + runs-on: macos-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.ref_name }} + + - name: 安装Flutter + if: steps.cache-flutter.outputs.cache-hit != 'true' + uses: subosito/flutter-action@v2.10.0 + with: + cache: true + flutter-version: 3.16.5 + + - name: 更新版本号 + id: version + run: | + # 更新pubspec.yaml文件中的版本号 + sed -i "" "s/version: .*+/version: ${{ needs.update_version.outputs.new_version }}+/g" pubspec.yaml + + - name: flutter build ipa + run: | + flutter build ios --release --no-codesign + ln -sf ./build/ios/iphoneos Payload + zip -r9 app.ipa Payload/runner.app + + - name: 重命名应用 + run: | + DATE=${{ steps.date.outputs.date }} + for file in app.ipa; do + new_file_name="build/Pili-v${{ needs.update_version.outputs.new_version }}.ipa" + mv "$file" "$new_file_name" + done + + - name: 上传 + uses: actions/upload-artifact@v3 + with: + if-no-files-found: error + name: Pilipala-Beta + path: | + build/Pili-*.ipa + + upload: + runs-on: ubuntu-latest + + needs: + - update_version + - android + - iOS + steps: + - uses: actions/download-artifact@v3 + with: + name: Pilipala-Beta + path: ./Pilipala-Beta + + - name: 发送到Telegram频道 + uses: xireiki/channel-post@v1.0.7 + with: + bot_token: ${{ secrets.BOT_TOKEN }} + chat_id: ${{ secrets.CHAT_ID }} + large_file: true + api_id: ${{ secrets.TELEGRAM_API_ID }} + api_hash: ${{ secrets.TELEGRAM_API_HASH }} + method: sendFile + path: Pilipala-Beta/* + parse_mode: Markdown + context: "*Beta版本: v${{ needs.update_version.outputs.new_version }}*\n更新内容: [${{ needs.update_version.outputs.last_commit }}](${{ github.event.head_commit.url }})" diff --git a/.github/workflows/main.yml b/.github/workflows/release_ci.yml similarity index 100% rename from .github/workflows/main.yml rename to .github/workflows/release_ci.yml diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index d82845fea..c52d8447a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -223,6 +223,10 @@ android:pathPattern="/mobile/video/.*" /> + + diff --git a/assets/images/video/danmu_close.svg b/assets/images/video/danmu_close.svg new file mode 100644 index 000000000..9f48027b0 --- /dev/null +++ b/assets/images/video/danmu_close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/video/danmu_open.svg b/assets/images/video/danmu_open.svg new file mode 100644 index 000000000..24e8d7a99 --- /dev/null +++ b/assets/images/video/danmu_open.svg @@ -0,0 +1 @@ +Layer 1 \ No newline at end of file diff --git a/change_log/1.0.20.0303.md b/change_log/1.0.20.0303.md new file mode 100644 index 000000000..1d8c4e004 --- /dev/null +++ b/change_log/1.0.20.0303.md @@ -0,0 +1,31 @@ +## 1.0.20 + + +### 功能 ++ 评论区增加表情 ++ 首页渐变背景开关 ++ 媒体库显示「我的订阅」 ++ 评论区链接解析 ++ 默认启动页设置 + +### 修复 ++ 评论区内容重复 ++ pip相关问题 ++ 播放多p视频评论不刷新 ++ 视频评论翻页重复 + +### 优化 ++ url scheme优化 ++ 图片预览放大 ++ 图片加载速度 ++ 视频评论区复制 ++ 全屏显示视频标题 ++ 网络异常处理 + + + + + + +更多更新日志可在Github上查看 +问题反馈、功能建议请查看「关于」页面。 diff --git a/change_log/1.0.21.0306.md b/change_log/1.0.21.0306.md new file mode 100644 index 000000000..3a582dbb1 --- /dev/null +++ b/change_log/1.0.21.0306.md @@ -0,0 +1,9 @@ +## 1.0.21 + +### 修复 ++ 推荐视频全屏问题 ++ 番剧全屏播放时灰屏问题 ++ 评论回调导致页面卡死问题 + +更多更新日志可在Github上查看 +问题反馈、功能建议请查看「关于」页面。 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 8db59815b..2c1a635ba 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -9,6 +9,7 @@ PODS: - Flutter - connectivity_plus (0.0.1): - Flutter + - FlutterMacOS - ReachabilitySwift - device_info_plus (0.0.1): - Flutter @@ -38,7 +39,7 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - permission_handler_apple (9.1.1): + - permission_handler_apple (9.3.0): - Flutter - ReachabilitySwift (5.0.0) - saver_gallery (0.0.1): @@ -71,7 +72,7 @@ DEPENDENCIES: - audio_service (from `.symlinks/plugins/audio_service/ios`) - audio_session (from `.symlinks/plugins/audio_session/ios`) - auto_orientation (from `.symlinks/plugins/auto_orientation/ios`) - - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) + - connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - Flutter (from `Flutter`) - flutter_mailer (from `.symlinks/plugins/flutter_mailer/ios`) @@ -113,7 +114,7 @@ EXTERNAL SOURCES: auto_orientation: :path: ".symlinks/plugins/auto_orientation/ios" connectivity_plus: - :path: ".symlinks/plugins/connectivity_plus/ios" + :path: ".symlinks/plugins/connectivity_plus/darwin" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" Flutter: @@ -166,7 +167,7 @@ SPEC CHECKSUMS: audio_service: f509d65da41b9521a61f1c404dd58651f265a567 audio_session: 4f3e461722055d21515cf3261b64c973c062f345 auto_orientation: 102ed811a5938d52c86520ddd7ecd3a126b5d39d - connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a + connectivity_plus: e2dad488011aeb593e219360e804c43cc1af5770 device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83 @@ -180,7 +181,7 @@ SPEC CHECKSUMS: media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 - permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 + permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 saver_gallery: 2b4e584106fde2407ab51560f3851564963e6b78 screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625 @@ -193,7 +194,7 @@ SPEC CHECKSUMS: volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9 wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47 webview_cookie_manager: eaf920722b493bd0f7611b5484771ca53fed03f7 - webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a + webview_flutter_wkwebview: 4f3e50f7273d31e5500066ed267e3ae4309c5ae4 PODFILE CHECKSUM: 637cd290bed23275b5f5ffcc7eb1e73d0a5fb2be diff --git a/lib/common/widgets/network_img_layer.dart b/lib/common/widgets/network_img_layer.dart index 52c56a8a7..06c359743 100644 --- a/lib/common/widgets/network_img_layer.dart +++ b/lib/common/widgets/network_img_layer.dart @@ -2,6 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; import 'package:pilipala/utils/extension.dart'; +import 'package:pilipala/utils/global_data.dart'; import '../../utils/storage.dart'; import '../constants.dart'; @@ -32,8 +33,10 @@ class NetworkImgLayer extends StatelessWidget { @override Widget build(BuildContext context) { + final int defaultImgQuality = GlobalData().imgQuality; final String imageUrl = - '${src!.startsWith('//') ? 'https:${src!}' : src!}@${quality ?? 100}q.webp'; + '${src!.startsWith('//') ? 'https:${src!}' : src!}@${quality ?? defaultImgQuality}q.webp'; + print(imageUrl); int? memCacheWidth, memCacheHeight; double aspectRatio = (width / height).toDouble(); @@ -81,7 +84,7 @@ class NetworkImgLayer extends StatelessWidget { fadeOutDuration ?? const Duration(milliseconds: 120), fadeInDuration: fadeInDuration ?? const Duration(milliseconds: 120), - filterQuality: FilterQuality.high, + filterQuality: FilterQuality.low, errorWidget: (BuildContext context, String url, Object error) => placeholder(context), placeholder: (BuildContext context, String url) => diff --git a/lib/common/widgets/video_card_h.dart b/lib/common/widgets/video_card_h.dart index c78643db0..99059a9e8 100644 --- a/lib/common/widgets/video_card_h.dart +++ b/lib/common/widgets/video_card_h.dart @@ -38,6 +38,10 @@ class VideoCardH extends StatelessWidget { Widget build(BuildContext context) { final int aid = videoItem.aid; final String bvid = videoItem.bvid; + String type = 'video'; + try { + type = videoItem.type; + } catch (_) {} final String heroTag = Utils.makeHeroTag(aid); return GestureDetector( onLongPress: () { @@ -53,6 +57,10 @@ class VideoCardH extends StatelessWidget { child: InkWell( onTap: () async { try { + if (type == 'ketang') { + SmartDialog.showToast('课堂视频暂不支持播放'); + return; + } final int cid = videoItem.cid ?? await SearchHttp.ab2c(aid: aid, bvid: bvid); Get.toNamed('/video?bvid=$bvid&cid=$cid', @@ -95,12 +103,20 @@ class VideoCardH extends StatelessWidget { height: maxHeight, ), ), - PBadge( - text: Utils.timeFormat(videoItem.duration!), - right: 6.0, - bottom: 6.0, - type: 'gray', - ), + if (videoItem.duration != 0) + PBadge( + text: Utils.timeFormat(videoItem.duration!), + right: 6.0, + bottom: 6.0, + type: 'gray', + ), + if (type != 'video') + PBadge( + text: type, + left: 6.0, + bottom: 6.0, + type: 'primary', + ), // if (videoItem.rcmdReason != null && // videoItem.rcmdReason.content != '') // pBadge(videoItem.rcmdReason.content, context, diff --git a/lib/common/widgets/video_card_v.dart b/lib/common/widgets/video_card_v.dart index 43dd05caa..0d96f7b77 100644 --- a/lib/common/widgets/video_card_v.dart +++ b/lib/common/widgets/video_card_v.dart @@ -231,6 +231,7 @@ class VideoContent extends StatelessWidget { const SizedBox(height: 2), VideoStat( videoItem: videoItem, + crossAxisCount: crossAxisCount, ), ], if (crossAxisCount == 1) const SizedBox(height: 4), @@ -294,6 +295,7 @@ class VideoContent extends StatelessWidget { ), VideoStat( videoItem: videoItem, + crossAxisCount: crossAxisCount, ), const Spacer(), ], @@ -317,10 +319,12 @@ class VideoContent extends StatelessWidget { class VideoStat extends StatelessWidget { final dynamic videoItem; + final int crossAxisCount; const VideoStat({ Key? key, required this.videoItem, + required this.crossAxisCount, }) : super(key: key); @override @@ -337,7 +341,7 @@ class VideoStat extends StatelessWidget { danmu: videoItem.stat.danmu, ), if (videoItem is RecVideoItemModel) ...[ - const Spacer(), + crossAxisCount > 1 ? const Spacer() : const SizedBox(width: 8), RichText( maxLines: 1, text: TextSpan( diff --git a/lib/http/api.dart b/lib/http/api.dart index 736030b2e..445f61028 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -484,9 +484,26 @@ class Api { /// 激活buvid3 static const activateBuvidApi = '/x/internal/gaia-gateway/ExClimbWuzhi'; + /// 获取字幕配置 + static const getSubtitleConfig = '/x/player/v2'; + /// 我的订阅 static const userSubFolder = '/x/v3/fav/folder/collected/list'; /// 我的订阅详情 static const userSubFolderDetail = '/x/space/fav/season/list'; + + /// 表情 + static const emojiList = '/x/emote/user/panel/web'; + + /// 已读标记 + static const String ackSessionMsg = + '${HttpString.tUrl}/session_svr/v1/session_svr/update_ack'; + + /// 发送私信 + static const String sendMsg = '${HttpString.tUrl}/web_im/v1/web_im/send_msg'; + + /// 排行榜 + static const String getRankApi = "/x/web-interface/ranking/v2"; + } diff --git a/lib/http/interceptor.dart b/lib/http/interceptor.dart index 362ff17ad..a53592830 100644 --- a/lib/http/interceptor.dart +++ b/lib/http/interceptor.dart @@ -45,10 +45,13 @@ class ApiInterceptor extends Interceptor { void onError(DioException err, ErrorInterceptorHandler handler) async { // 处理网络请求错误 // handler.next(err); - SmartDialog.showToast( - await dioError(err), - displayType: SmartToastType.onlyRefresh, - ); + String url = err.requestOptions.uri.toString(); + if (!url.contains('heartBeat')) { + SmartDialog.showToast( + await dioError(err), + displayType: SmartToastType.onlyRefresh, + ); + } super.onError(err, handler); } @@ -75,23 +78,24 @@ class ApiInterceptor extends Interceptor { } static Future checkConnect() async { - final ConnectivityResult connectivityResult = + final List connectivityResult = await Connectivity().checkConnectivity(); - switch (connectivityResult) { - case ConnectivityResult.mobile: - return '正在使用移动流量'; - case ConnectivityResult.wifi: - return '正在使用wifi'; - case ConnectivityResult.ethernet: - return '正在使用局域网'; - case ConnectivityResult.vpn: - return '正在使用代理网络'; - case ConnectivityResult.other: - return '正在使用其他网络'; - case ConnectivityResult.none: - return '未连接到任何网络'; - default: - return ''; + if (connectivityResult.contains(ConnectivityResult.mobile)) { + return '正在使用移动流量'; + } else if (connectivityResult.contains(ConnectivityResult.wifi)) { + return '正在使用wifi'; + } else if (connectivityResult.contains(ConnectivityResult.ethernet)) { + return '正在使用局域网'; + } else if (connectivityResult.contains(ConnectivityResult.vpn)) { + return '正在使用代理网络'; + } else if (connectivityResult.contains(ConnectivityResult.bluetooth)) { + return '正在使用蓝牙网络'; + } else if (connectivityResult.contains(ConnectivityResult.other)) { + return '正在使用其他网络'; + } else if (connectivityResult.contains(ConnectivityResult.none)) { + return '未连接到任何网络'; + } else { + return ''; } } } diff --git a/lib/http/msg.dart b/lib/http/msg.dart index 70af5b559..d1d319588 100644 --- a/lib/http/msg.dart +++ b/lib/http/msg.dart @@ -1,3 +1,4 @@ +import 'dart:math'; import '../models/msg/account.dart'; import '../models/msg/session.dart'; import '../utils/wbi_sign.dart'; @@ -22,14 +23,22 @@ class MsgHttp { Map signParams = await WbiSign().makSign(params); var res = await Request().get(Api.sessionList, data: signParams); if (res.data['code'] == 0) { - return { - 'status': true, - 'data': SessionDataModel.fromJson(res.data['data']), - }; + try { + return { + 'status': true, + 'data': SessionDataModel.fromJson(res.data['data']), + }; + } catch (err) { + return { + 'status': false, + 'data': [], + 'msg': err.toString(), + }; + } } else { return { 'status': false, - 'date': [], + 'data': [], 'msg': res.data['message'], }; } @@ -42,12 +51,16 @@ class MsgHttp { 'mobi_app': 'web', }); if (res.data['code'] == 0) { - return { - 'status': true, - 'data': res.data['data'] - .map((e) => AccountListModel.fromJson(e)) - .toList(), - }; + try { + return { + 'status': true, + 'data': res.data['data'] + .map((e) => AccountListModel.fromJson(e)) + .toList(), + }; + } catch (err) { + print('err🔟: $err'); + } } else { return { 'status': false, @@ -86,4 +99,125 @@ class MsgHttp { }; } } + + // 消息标记已读 + static Future ackSessionMsg({ + int? talkerId, + int? ackSeqno, + }) async { + String csrf = await Request.getCsrf(); + Map params = await WbiSign().makSign({ + 'talker_id': talkerId, + 'session_type': 1, + 'ack_seqno': ackSeqno, + 'build': 0, + 'mobi_app': 'web', + 'csrf_token': csrf, + 'csrf': csrf + }); + var res = await Request().get(Api.ackSessionMsg, data: params); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': res.data['data'], + }; + } else { + return { + 'status': false, + 'date': [], + 'msg': "message: ${res.data['message']}," + " msg: ${res.data['msg']}," + " code: ${res.data['code']}", + }; + } + } + + // 发送私信 + static Future sendMsg({ + int? senderUid, + int? receiverId, + int? receiverType, + int? msgType, + dynamic content, + }) async { + String csrf = await Request.getCsrf(); + Map params = await WbiSign().makSign({ + 'msg[sender_uid]': senderUid, + 'msg[receiver_id]': receiverId, + 'msg[receiver_type]': receiverType ?? 1, + 'msg[msg_type]': msgType ?? 1, + 'msg[msg_status]': 0, + 'msg[dev_id]': getDevId(), + 'msg[timestamp]': DateTime.now().millisecondsSinceEpoch ~/ 1000, + 'msg[new_face_version]': 0, + 'msg[content]': content, + 'from_firework': 0, + 'build': 0, + 'mobi_app': 'web', + 'csrf_token': csrf, + 'csrf': csrf, + }); + var res = + await Request().post(Api.sendMsg, queryParameters: { + ...params, + 'csrf_token': csrf, + 'csrf': csrf, + }, data: { + 'w_sender_uid': params['msg[sender_uid]'], + 'w_receiver_id': params['msg[receiver_id]'], + 'w_dev_id': params['msg[dev_id]'], + 'w_rid': params['w_rid'], + 'wts': params['wts'], + 'csrf_token': csrf, + 'csrf': csrf, + }); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': res.data['data'], + }; + } else { + return { + 'status': false, + 'date': [], + 'msg': "message: ${res.data['message']}," + " msg: ${res.data['msg']}," + " code: ${res.data['code']}", + }; + } + } + + static String getDevId() { + final List b = [ + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + 'A', + 'B', + 'C', + 'D', + 'E', + 'F' + ]; + final List s = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".split(''); + for (int i = 0; i < s.length; i++) { + if ('-' == s[i] || '4' == s[i]) { + continue; + } + final int randomInt = Random().nextInt(16); + if ('x' == s[i]) { + s[i] = b[randomInt]; + } else { + s[i] = b[3 & randomInt | 8]; + } + } + return s.join(); + } } diff --git a/lib/http/reply.dart b/lib/http/reply.dart index fab433fc7..f080ed514 100644 --- a/lib/http/reply.dart +++ b/lib/http/reply.dart @@ -1,4 +1,5 @@ import '../models/video/reply/data.dart'; +import '../models/video/reply/emote.dart'; import 'api.dart'; import 'init.dart'; @@ -100,4 +101,23 @@ class ReplyHttp { }; } } + + static Future getEmoteList({String? business}) async { + var res = await Request().get(Api.emojiList, data: { + 'business': business ?? 'reply', + 'web_location': '333.1245', + }); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': EmoteModelData.fromJson(res.data['data']), + }; + } else { + return { + 'status': false, + 'date': [], + 'msg': res.data['message'], + }; + } + } } diff --git a/lib/http/video.dart b/lib/http/video.dart index 30df62c3e..d43656b28 100644 --- a/lib/http/video.dart +++ b/lib/http/video.dart @@ -8,9 +8,11 @@ import '../models/model_rec_video_item.dart'; import '../models/user/fav_folder.dart'; import '../models/video/ai.dart'; import '../models/video/play/url.dart'; +import '../models/video/subTitile/result.dart'; import '../models/video_detail_res.dart'; import '../utils/recommend_filter.dart'; import '../utils/storage.dart'; +import '../utils/subtitle.dart'; import '../utils/wbi_sign.dart'; import 'api.dart'; import 'init.dart'; @@ -475,4 +477,54 @@ class VideoHttp { return {'status': false, 'data': []}; } } + + static Future getSubtitle({int? cid, String? bvid}) async { + var res = await Request().get(Api.getSubtitleConfig, data: { + 'cid': cid, + 'bvid': bvid, + }); + try { + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': SubTitlteModel.fromJson(res.data['data']), + }; + } else { + return {'status': false, 'data': [], 'msg': res.data['msg']}; + } + } catch (err) { + print(err); + } + } + + // 视频排行 + static Future getRankVideoList(int rid) async { + try { + var rankApi = "${Api.getRankApi}?rid=$rid&type=all"; + var res = await Request().get(rankApi); + if (res.data['code'] == 0) { + List list = []; + List blackMidsList = + setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]); + for (var i in res.data['data']['list']) { + if (!blackMidsList.contains(i['owner']['mid'])) { + list.add(HotVideoItemModel.fromJson(i)); + } + } + return {'status': true, 'data': list}; + } else { + return {'status': false, 'data': [], 'msg': res.data['message']}; + } + } catch (err) { + return {'status': false, 'data': [], 'msg': err}; + } + } + + // 获取字幕内容 + static Future> getSubtitleContent(url) async { + var res = await Request().get('https:$url'); + final String content = SubTitleUtils.convertToWebVTT(res.data['body']); + final List body = res.data['body']; + return {'content': content, 'body': body}; + } } diff --git a/lib/main.dart b/lib/main.dart index fc2149ded..44bb1dcd6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -16,6 +16,7 @@ import 'package:pilipala/pages/search/index.dart'; import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/router/app_pages.dart'; import 'package:pilipala/pages/main/view.dart'; +import 'package:pilipala/services/disable_battery_opt.dart'; import 'package:pilipala/services/service_locator.dart'; import 'package:pilipala/utils/app_scheme.dart'; import 'package:pilipala/utils/data.dart'; @@ -71,6 +72,7 @@ void main() async { )); Data.init(); PiliSchame.init(); + DisableBatteryOpt(); }); } diff --git a/lib/models/common/dynamics_type.dart b/lib/models/common/dynamics_type.dart index 337f6aecc..f4e20a4b9 100644 --- a/lib/models/common/dynamics_type.dart +++ b/lib/models/common/dynamics_type.dart @@ -7,5 +7,5 @@ enum DynamicsType { extension BusinessTypeExtension on DynamicsType { String get values => ['all', 'video', 'pgc', 'article'][index]; - String get labels => ['全部', '视频', '追番', '专栏'][index]; + String get labels => ['全部', '投稿', '番剧', '专栏'][index]; } diff --git a/lib/models/common/gesture_mode.dart b/lib/models/common/gesture_mode.dart new file mode 100644 index 000000000..1149ae123 --- /dev/null +++ b/lib/models/common/gesture_mode.dart @@ -0,0 +1,12 @@ +enum FullScreenGestureMode { + /// 从上滑到下 + fromToptoBottom, + + /// 从下滑到上 + fromBottomtoTop, +} + +extension FullScreenGestureModeExtension on FullScreenGestureMode { + String get values => ['fromToptoBottom', 'fromBottomtoTop'][index]; + String get labels => ['从上往下滑进入全屏', '从下往上滑进入全屏'][index]; +} diff --git a/lib/models/common/index.dart b/lib/models/common/index.dart new file mode 100644 index 000000000..89a05076a --- /dev/null +++ b/lib/models/common/index.dart @@ -0,0 +1,4 @@ +library commonn_model; + +export './business_type.dart'; +export './gesture_mode.dart'; diff --git a/lib/models/common/nav_bar_config.dart b/lib/models/common/nav_bar_config.dart new file mode 100644 index 000000000..9ebe8e6fd --- /dev/null +++ b/lib/models/common/nav_bar_config.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; + +List defaultNavigationBars = [ + { + 'id': 0, + 'icon': const Icon( + Icons.home_outlined, + size: 21, + ), + 'selectIcon': const Icon( + Icons.home, + size: 21, + ), + 'label': "首页", + 'count': 0, + }, + { + 'id': 1, + 'icon': const Icon( + Icons.trending_up, + size: 21, + ), + 'selectIcon': const Icon( + Icons.trending_up_outlined, + size: 21, + ), + 'label': "排行榜", + 'count': 0, + }, + { + 'id': 2, + 'icon': const Icon( + Icons.motion_photos_on_outlined, + size: 21, + ), + 'selectIcon': const Icon( + Icons.motion_photos_on, + size: 21, + ), + 'label': "动态", + 'count': 0, + }, + { + 'id': 3, + 'icon': const Icon( + Icons.video_collection_outlined, + size: 20, + ), + 'selectIcon': const Icon( + Icons.video_collection, + size: 21, + ), + 'label': "媒体库", + 'count': 0, + } +]; diff --git a/lib/models/common/rank_type.dart b/lib/models/common/rank_type.dart new file mode 100644 index 000000000..2ce6d3b5b --- /dev/null +++ b/lib/models/common/rank_type.dart @@ -0,0 +1,240 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/pages/rank/zone/index.dart'; + +enum RandType { + all, + creation, + animation, + music, + dance, + game, + knowledge, + technology, + sport, + car, + life, + food, + animal, + madness, + fashion, + entertainment, + film, + origin, + rookie +} + +extension RankTypeDesc on RandType { + String get description => [ + '全站', + '国创相关', + '动画', + '音乐', + '舞蹈', + '游戏', + '知识', + '科技', + '运动', + '汽车', + '生活', + '美食', + '动物圈', + '鬼畜', + '时尚', + '娱乐', + '影视' + ][index]; + + String get id => [ + 'all', + 'creation', + 'animation', + 'music', + 'dance', + 'game', + 'knowledge', + 'technology', + 'sport', + 'car', + 'life', + 'food', + 'animal', + 'madness', + 'fashion', + 'entertainment', + 'film' + ][index]; +} + +List tabsConfig = [ + { + 'icon': const Icon( + Icons.live_tv_outlined, + size: 15, + ), + 'label': '全站', + 'type': RandType.all, + 'ctr': Get.put, + 'page': const ZonePage(rid: 0), + }, + { + 'icon': const Icon( + Icons.live_tv_outlined, + size: 15, + ), + 'label': '国创相关', + 'type': RandType.creation, + 'ctr': Get.put, + 'page': const ZonePage(rid: 168), + }, + { + 'icon': const Icon( + Icons.live_tv_outlined, + size: 15, + ), + 'label': '动画', + 'type': RandType.animation, + 'ctr': Get.put, + 'page': const ZonePage(rid: 1), + }, + { + 'icon': const Icon( + Icons.live_tv_outlined, + size: 15, + ), + 'label': '音乐', + 'type': RandType.music, + 'ctr': Get.put, + 'page': const ZonePage(rid: 3), + }, + { + 'icon': const Icon( + Icons.live_tv_outlined, + size: 15, + ), + 'label': '舞蹈', + 'type': RandType.dance, + 'ctr': Get.put, + 'page': const ZonePage(rid: 129), + }, + { + 'icon': const Icon( + Icons.live_tv_outlined, + size: 15, + ), + 'label': '游戏', + 'type': RandType.game, + 'ctr': Get.put, + 'page': const ZonePage(rid: 4), + }, + { + 'icon': const Icon( + Icons.live_tv_outlined, + size: 15, + ), + 'label': '知识', + 'type': RandType.knowledge, + 'ctr': Get.put, + 'page': const ZonePage(rid: 36), + }, + { + 'icon': const Icon( + Icons.live_tv_outlined, + size: 15, + ), + 'label': '科技', + 'type': RandType.technology, + 'ctr': Get.put, + 'page': const ZonePage(rid: 188), + }, + { + 'icon': const Icon( + Icons.live_tv_outlined, + size: 15, + ), + 'label': '运动', + 'type': RandType.sport, + 'ctr': Get.put, + 'page': const ZonePage(rid: 234), + }, + { + 'icon': const Icon( + Icons.live_tv_outlined, + size: 15, + ), + 'label': '汽车', + 'type': RandType.car, + 'ctr': Get.put, + 'page': const ZonePage(rid: 223), + }, + { + 'icon': const Icon( + Icons.live_tv_outlined, + size: 15, + ), + 'label': '生活', + 'type': RandType.life, + 'ctr': Get.put, + 'page': const ZonePage(rid: 160), + }, + { + 'icon': const Icon( + Icons.live_tv_outlined, + size: 15, + ), + 'label': '美食', + 'type': RandType.food, + 'ctr': Get.put, + 'page': const ZonePage(rid: 211), + }, + { + 'icon': const Icon( + Icons.live_tv_outlined, + size: 15, + ), + 'label': '动物圈', + 'type': RandType.animal, + 'ctr': Get.put, + 'page': const ZonePage(rid: 217), + }, + { + 'icon': const Icon( + Icons.live_tv_outlined, + size: 15, + ), + 'label': '鬼畜', + 'type': RandType.madness, + 'ctr': Get.put, + 'page': const ZonePage(rid: 119), + }, + { + 'icon': const Icon( + Icons.live_tv_outlined, + size: 15, + ), + 'label': '时尚', + 'type': RandType.fashion, + 'ctr': Get.put, + 'page': const ZonePage(rid: 155), + }, + { + 'icon': const Icon( + Icons.live_tv_outlined, + size: 15, + ), + 'label': '娱乐', + 'type': RandType.entertainment, + 'ctr': Get.put, + 'page': const ZonePage(rid: 5), + }, + { + 'icon': const Icon( + Icons.live_tv_outlined, + size: 15, + ), + 'label': '影视', + 'type': RandType.film, + 'ctr': Get.put, + 'page': const ZonePage(rid: 181), + } +]; diff --git a/lib/models/common/subtitle_type.dart b/lib/models/common/subtitle_type.dart new file mode 100644 index 000000000..117163517 --- /dev/null +++ b/lib/models/common/subtitle_type.dart @@ -0,0 +1,47 @@ +enum SubtitleType { + // 中文(中国) + zhCN, + // 中文(自动翻译) + aizh, + // 英语(自动生成) + aien, +} + +extension SubtitleTypeExtension on SubtitleType { + String get description { + switch (this) { + case SubtitleType.zhCN: + return '中文(中国)'; + case SubtitleType.aizh: + return '中文(自动翻译)'; + case SubtitleType.aien: + return '英语(自动生成)'; + } + } +} + +extension SubtitleIdExtension on SubtitleType { + String get id { + switch (this) { + case SubtitleType.zhCN: + return 'zh-CN'; + case SubtitleType.aizh: + return 'ai-zh'; + case SubtitleType.aien: + return 'ai-en'; + } + } +} + +extension SubtitleCodeExtension on SubtitleType { + int get code { + switch (this) { + case SubtitleType.zhCN: + return 1; + case SubtitleType.aizh: + return 2; + case SubtitleType.aien: + return 3; + } + } +} diff --git a/lib/models/dynamics/up.dart b/lib/models/dynamics/up.dart index cfd1fa7df..9bb82f70d 100644 --- a/lib/models/dynamics/up.dart +++ b/lib/models/dynamics/up.dart @@ -2,18 +2,28 @@ class FollowUpModel { FollowUpModel({ this.liveUsers, this.upList, + this.liveList, + this.myInfo, }); LiveUsers? liveUsers; List? upList; + List? liveList; + MyInfo? myInfo; FollowUpModel.fromJson(Map json) { liveUsers = json['live_users'] != null ? LiveUsers.fromJson(json['live_users']) : null; + liveList = json['live_users'] != null + ? json['live_users']['items'] + .map((e) => LiveUserItem.fromJson(e)) + .toList() + : []; upList = json['up_list'] != null ? json['up_list'].map((e) => UpItem.fromJson(e)).toList() : []; + myInfo = json['my_info'] != null ? MyInfo.fromJson(json['my_info']) : null; } } @@ -93,3 +103,21 @@ class UpItem { uname = json['uname']; } } + +class MyInfo { + MyInfo({ + this.face, + this.mid, + this.name, + }); + + String? face; + int? mid; + String? name; + + MyInfo.fromJson(Map json) { + face = json['face']; + mid = json['mid']; + name = json['name']; + } +} diff --git a/lib/models/live/quality.dart b/lib/models/live/quality.dart new file mode 100644 index 000000000..677d615b4 --- /dev/null +++ b/lib/models/live/quality.dart @@ -0,0 +1,43 @@ +enum LiveQuality { + dolby, + super4K, + origin, + bluRay, + superHD, + smooth, + flunt, +} + +extension LiveQualityCode on LiveQuality { + static final List _codeList = [ + 30000, + 20000, + 10000, + 400, + 250, + 150, + 80, + ]; + int get code => _codeList[index]; + + static LiveQuality? fromCode(int code) { + final index = _codeList.indexOf(code); + if (index != -1) { + return LiveQuality.values[index]; + } + return null; + } +} + +extension VideoQualityDesc on LiveQuality { + static final List _descList = [ + '杜比', + '4K', + '原画', + '蓝光', + '超清', + '高清', + '流畅', + ]; + get description => _descList[index]; +} diff --git a/lib/models/msg/session.dart b/lib/models/msg/session.dart index ea241249a..b6c1b6a61 100644 --- a/lib/models/msg/session.dart +++ b/lib/models/msg/session.dart @@ -8,7 +8,7 @@ class SessionDataModel { this.hasMore, }); - List? sessionList; + List? sessionList; int? hasMore; SessionDataModel.fromJson(Map json) { @@ -121,35 +121,37 @@ class LastMsg { this.msgKey, this.msgStatus, this.notifyCode, - this.newFaceVersion, + // this.newFaceVersion, }); int? senderIid; int? receiverType; int? receiverId; int? msgType; - Map? content; + dynamic content; int? msgSeqno; int? timestamp; String? atUids; int? msgKey; int? msgStatus; String? notifyCode; - int? newFaceVersion; + // int? newFaceVersion; LastMsg.fromJson(Map json) { senderIid = json['sender_uid']; receiverType = json['receiver_type']; receiverId = json['receiver_id']; msgType = json['msg_type']; - content = jsonDecode(json['content']); + content = json['content'] != null && json['content'] != '' + ? jsonDecode(json['content']) + : ''; msgSeqno = json['msg_seqno']; timestamp = json['timestamp']; atUids = json['at_uids']; msgKey = json['msg_key']; msgStatus = json['msg_status']; notifyCode = json['notify_code']; - newFaceVersion = json['new_face_version']; + // newFaceVersion = json['new_face_version']; } } @@ -214,7 +216,9 @@ class MessageItem { receiverId = json['receiver_id']; // 1 文本 2 图片 18 系统提示 10 系统通知 5 撤回的消息 msgType = json['msg_type']; - content = jsonDecode(json['content']); + content = json['content'] != null && json['content'] != '' + ? jsonDecode(json['content']) + : ''; msgSeqno = json['msg_seqno']; timestamp = json['timestamp']; atUids = json['at_uids']; diff --git a/lib/models/search/result.dart b/lib/models/search/result.dart index 3d381ed90..418fb99d8 100644 --- a/lib/models/search/result.dart +++ b/lib/models/search/result.dart @@ -85,7 +85,9 @@ class SearchVideoItemModel { // title = json['title'].replaceAll(RegExp(r'<.*?>'), ''); title = Em.regTitle(json['title']); description = json['description']; - pic = 'https:${json['pic']}'; + pic = json['pic'] != null && json['pic'].startsWith('//') + ? 'https:${json['pic']}' + : json['pic'] ?? ''; videoReview = json['video_review']; pubdate = json['pubdate']; senddate = json['senddate']; @@ -435,7 +437,8 @@ class SearchArticleItemModel { pubTime = json['pub_time']; like = json['like']; title = Em.regTitle(json['title']); - subTitle = json['title'].replaceAll(RegExp(r'<[^>]*>'), ''); + subTitle = + Em.decodeHtmlEntities(json['title'].replaceAll(RegExp(r'<[^>]*>'), '')); rankOffset = json['rank_offset']; mid = json['mid']; imageUrls = json['image_urls']; diff --git a/lib/models/user/fav_folder.dart b/lib/models/user/fav_folder.dart index 6d3f9975b..c45e2de96 100644 --- a/lib/models/user/fav_folder.dart +++ b/lib/models/user/fav_folder.dart @@ -15,7 +15,7 @@ class FavFolderData { ? json['list'] .map((e) => FavFolderItemData.fromJson(e)) .toList() - : [FavFolderItemData()]; + : []; hasMore = json['has_more']; } } diff --git a/lib/models/video/reply/emote.dart b/lib/models/video/reply/emote.dart new file mode 100644 index 000000000..b40718266 --- /dev/null +++ b/lib/models/video/reply/emote.dart @@ -0,0 +1,120 @@ +class EmoteModelData { + final List? packages; + + EmoteModelData({ + required this.packages, + }); + + factory EmoteModelData.fromJson(Map jsonRes) { + final List? packages = + jsonRes['packages'] is List ? [] : null; + if (packages != null) { + for (final dynamic item in jsonRes['packages']!) { + if (item != null) { + try { + packages.add(PackageItem.fromJson(item)); + } catch (_) {} + } + } + } + return EmoteModelData( + packages: packages, + ); + } +} + +class PackageItem { + final int? id; + final String? text; + final String? url; + final int? mtime; + final int? type; + final int? attr; + final Meta? meta; + final List? emote; + + PackageItem({ + required this.id, + required this.text, + required this.url, + required this.mtime, + required this.type, + required this.attr, + required this.meta, + required this.emote, + }); + + factory PackageItem.fromJson(Map jsonRes) { + final List? emote = jsonRes['emote'] is List ? [] : null; + if (emote != null) { + for (final dynamic item in jsonRes['emote']!) { + if (item != null) { + try { + emote.add(Emote.fromJson(item)); + } catch (_) {} + } + } + } + return PackageItem( + id: jsonRes['id'], + text: jsonRes['text'], + url: jsonRes['url'], + mtime: jsonRes['mtime'], + type: jsonRes['type'], + attr: jsonRes['attr'], + meta: Meta.fromJson(jsonRes['meta']), + emote: emote, + ); + } +} + +class Meta { + final int? size; + final List? suggest; + + Meta({ + required this.size, + required this.suggest, + }); + + factory Meta.fromJson(Map jsonRes) => Meta( + size: jsonRes['size'], + suggest: jsonRes['suggest'] is List ? [] : null, + ); +} + +class Emote { + final int? id; + final int? packageId; + final String? text; + final String? url; + final int? mtime; + final int? type; + final int? attr; + final Meta? meta; + final dynamic activity; + + Emote({ + required this.id, + required this.packageId, + required this.text, + required this.url, + required this.mtime, + required this.type, + required this.attr, + required this.meta, + required this.activity, + }); + + factory Emote.fromJson(Map jsonRes) => Emote( + id: jsonRes['id'], + packageId: jsonRes['package_id'], + text: jsonRes['text'], + url: jsonRes['url'], + mtime: jsonRes['mtime'], + type: jsonRes['type'], + attr: jsonRes['attr'], + meta: Meta.fromJson(jsonRes['meta']), + activity: jsonRes['activity'], + ); +} diff --git a/lib/models/video/subTitile/content.dart b/lib/models/video/subTitile/content.dart new file mode 100644 index 000000000..b18098a49 --- /dev/null +++ b/lib/models/video/subTitile/content.dart @@ -0,0 +1,20 @@ +class SubTitileContentModel { + double? from; + double? to; + int? location; + String? content; + + SubTitileContentModel({ + this.from, + this.to, + this.location, + this.content, + }); + + SubTitileContentModel.fromJson(Map json) { + from = json['from']; + to = json['to']; + location = json['location']; + content = json['content']; + } +} diff --git a/lib/models/video/subTitile/result.dart b/lib/models/video/subTitile/result.dart new file mode 100644 index 000000000..d3e32e558 --- /dev/null +++ b/lib/models/video/subTitile/result.dart @@ -0,0 +1,89 @@ +import 'package:get/get.dart'; +import '../../common/subtitle_type.dart'; + +class SubTitlteModel { + SubTitlteModel({ + this.aid, + this.bvid, + this.cid, + this.loginMid, + this.loginMidHash, + this.isOwner, + this.name, + this.subtitles, + }); + + int? aid; + String? bvid; + int? cid; + int? loginMid; + String? loginMidHash; + bool? isOwner; + String? name; + List? subtitles; + + factory SubTitlteModel.fromJson(Map json) => SubTitlteModel( + aid: json["aid"], + bvid: json["bvid"], + cid: json["cid"], + loginMid: json["login_mid"], + loginMidHash: json["login_mid_hash"], + isOwner: json["is_owner"], + name: json["name"], + subtitles: json["subtitle"] != null + ? json["subtitle"]["subtitles"] + .map((x) => SubTitlteItemModel.fromJson(x)) + .toList() + : [], + ); +} + +class SubTitlteItemModel { + SubTitlteItemModel({ + this.id, + this.lan, + this.lanDoc, + this.isLock, + this.subtitleUrl, + this.type, + this.aiType, + this.aiStatus, + this.title, + this.code, + this.content, + this.body, + }); + + int? id; + String? lan; + String? lanDoc; + bool? isLock; + String? subtitleUrl; + int? type; + int? aiType; + int? aiStatus; + String? title; + int? code; + String? content; + List? body; + + factory SubTitlteItemModel.fromJson(Map json) => + SubTitlteItemModel( + id: json["id"], + lan: json["lan"].replaceAll('-', ''), + lanDoc: json["lan_doc"], + isLock: json["is_lock"], + subtitleUrl: json["subtitle_url"], + type: json["type"], + aiType: json["ai_type"], + aiStatus: json["ai_status"], + title: json["lan_doc"], + code: SubtitleType.values + .firstWhereOrNull( + (element) => element.id.toString() == json["lan"]) + ?.index ?? + -1, + content: '', + body: [], + ); +} diff --git a/lib/pages/about/index.dart b/lib/pages/about/index.dart index 41ee1516c..b381691ab 100644 --- a/lib/pages/about/index.dart +++ b/lib/pages/about/index.dart @@ -205,7 +205,6 @@ class _AboutPageState extends State { }, title: const Text('清除缓存'), subtitle: Text('图片及网络缓存 $cacheSize', style: subTitleStyle), - trailing: Icon(Icons.arrow_forward_ios, size: 16, color: outline), ), SizedBox(height: MediaQuery.of(context).padding.bottom + 20) ], @@ -254,12 +253,16 @@ class AboutController extends GetxController { // 获取远程版本 Future getRemoteApp() async { var result = await Request().get(Api.latestApp, extra: {'ua': 'pc'}); + isLoading.value = false; + if (result.data == null || result.data.isEmpty) { + SmartDialog.showToast('获取远程版本失败,请检查网络'); + return; + } data = LatestDataModel.fromJson(result.data); remoteAppInfo = data; remoteVersion.value = data.tagName!; isUpdate.value = Utils.needUpdate(currentVersion.value, remoteVersion.value); - isLoading.value = false; } // 跳转下载/本地更新 @@ -277,7 +280,7 @@ class AboutController extends GetxController { githubRelease() { launchUrl( - Uri.parse('https://github.com/guozhigq/pilipala/release'), + Uri.parse('https://github.com/guozhigq/pilipala/releases'), mode: LaunchMode.externalApplication, ); } diff --git a/lib/pages/bangumi/controller.dart b/lib/pages/bangumi/controller.dart index 09afc43af..e5748d6c7 100644 --- a/lib/pages/bangumi/controller.dart +++ b/lib/pages/bangumi/controller.dart @@ -7,8 +7,8 @@ import 'package:pilipala/utils/storage.dart'; class BangumiController extends GetxController { final ScrollController scrollController = ScrollController(); - RxList bangumiList = [BangumiListItemModel()].obs; - RxList bangumiFollowList = [BangumiListItemModel()].obs; + RxList bangumiList = [].obs; + RxList bangumiFollowList = [].obs; int _currentPage = 1; bool isLoadingMore = true; Box userInfoCache = GStrorage.userInfo; diff --git a/lib/pages/bangumi/introduction/controller.dart b/lib/pages/bangumi/introduction/controller.dart index 6b3123ea2..12f0c0536 100644 --- a/lib/pages/bangumi/introduction/controller.dart +++ b/lib/pages/bangumi/introduction/controller.dart @@ -25,13 +25,6 @@ class BangumiIntroController extends GetxController { ? int.tryParse(Get.parameters['epId']!) : null; - // 是否预渲染 骨架屏 - bool preRender = false; - - // 视频详情 上个页面传入 - Map? videoItem = {}; - BangumiInfoModel? bangumiItem; - // 请求状态 RxBool isLoading = false.obs; @@ -63,27 +56,6 @@ class BangumiIntroController extends GetxController { @override void onInit() { super.onInit(); - if (Get.arguments.isNotEmpty as bool) { - if (Get.arguments.containsKey('bangumiItem') as bool) { - preRender = true; - bangumiItem = Get.arguments['bangumiItem']; - // bangumiItem!['pic'] = args.pic; - // if (args.title is String) { - // videoItem!['title'] = args.title; - // } else { - // String str = ''; - // for (Map map in args.title) { - // str += map['text']; - // } - // videoItem!['title'] = str; - // } - // if (args.stat != null) { - // videoItem!['stat'] = args.stat; - // } - // videoItem!['pubdate'] = args.pubdate; - // videoItem!['owner'] = args.owner; - } - } userInfo = userInfoCache.get('userInfoCache'); userLogin = userInfo != null; } @@ -183,20 +155,21 @@ class BangumiIntroController extends GetxController { actions: [ TextButton(onPressed: () => Get.back(), child: const Text('取消')), TextButton( - onPressed: () async { - var res = await VideoHttp.coinVideo( - bvid: bvid, multiply: _tempThemeValue); - if (res['status']) { - SmartDialog.showToast('投币成功 👏'); - hasCoin.value = true; - bangumiDetail.value.stat!['coins'] = - bangumiDetail.value.stat!['coins'] + _tempThemeValue; - } else { - SmartDialog.showToast(res['msg']); - } - Get.back(); - }, - child: const Text('确定')) + onPressed: () async { + var res = await VideoHttp.coinVideo( + bvid: bvid, multiply: _tempThemeValue); + if (res['status']) { + SmartDialog.showToast('投币成功 👏'); + hasCoin.value = true; + bangumiDetail.value.stat!['coins'] = + bangumiDetail.value.stat!['coins'] + _tempThemeValue; + } else { + SmartDialog.showToast(res['msg']); + } + Get.back(); + }, + child: const Text('确定'), + ) ], ); }); diff --git a/lib/pages/bangumi/introduction/view.dart b/lib/pages/bangumi/introduction/view.dart index f9efc66c5..6255ffda2 100644 --- a/lib/pages/bangumi/introduction/view.dart +++ b/lib/pages/bangumi/introduction/view.dart @@ -12,11 +12,10 @@ import 'package:pilipala/models/bangumi/info.dart'; import 'package:pilipala/pages/bangumi/widgets/bangumi_panel.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_row_item.dart'; import 'package:pilipala/pages/video/detail/introduction/widgets/fav_panel.dart'; import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/storage.dart'; - +import '../../../common/widgets/http_error.dart'; import 'controller.dart'; import 'widgets/intro_detail.dart'; @@ -51,9 +50,6 @@ class _BangumiIntroPanelState extends State cid = widget.cid!; bangumiIntroController = Get.put(BangumiIntroController(), tag: heroTag); videoDetailCtr = Get.find(tag: heroTag); - bangumiIntroController.bangumiDetail.listen((BangumiInfoModel value) { - bangumiDetail = value; - }); _futureBuilderFuture = bangumiIntroController.queryBangumiIntro(); videoDetailCtr.cid.listen((int p0) { cid = p0; @@ -68,27 +64,32 @@ class _BangumiIntroPanelState extends State future: _futureBuilderFuture, builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.data == null) { + return const SliverToBoxAdapter(child: SizedBox()); + } if (snapshot.data['status']) { // 请求成功 - - return BangumiInfo( - loadingStatus: false, - bangumiDetail: bangumiDetail, - cid: cid, + return Obx( + () => BangumiInfo( + bangumiDetail: bangumiIntroController.bangumiDetail.value, + cid: cid, + ), ); } else { // 请求错误 - // return HttpError( - // errMsg: snapshot.data['msg'], - // fn: () => Get.back(), - // ); - return const SizedBox(); + return HttpError( + errMsg: snapshot.data['msg'], + fn: () => Get.back(), + ); } } else { - return BangumiInfo( - loadingStatus: true, - bangumiDetail: bangumiDetail, - cid: cid, + return const SliverToBoxAdapter( + child: SizedBox( + height: 100, + child: Center( + child: CircularProgressIndicator(), + ), + ), ); } }, @@ -99,12 +100,10 @@ class _BangumiIntroPanelState extends State class BangumiInfo extends StatefulWidget { const BangumiInfo({ super.key, - this.loadingStatus = false, this.bangumiDetail, this.cid, }); - final bool loadingStatus; final BangumiInfoModel? bangumiDetail; final int? cid; @@ -117,7 +116,6 @@ class _BangumiInfoState extends State { late final BangumiIntroController bangumiIntroController; late final VideoDetailController videoDetailCtr; Box localCache = GStrorage.localCache; - late final BangumiInfoModel? bangumiItem; late double sheetHeight; int? cid; bool isProcessing = false; @@ -136,13 +134,10 @@ class _BangumiInfoState extends State { super.initState(); bangumiIntroController = Get.put(BangumiIntroController(), tag: heroTag); videoDetailCtr = Get.find(tag: heroTag); - bangumiItem = bangumiIntroController.bangumiItem; sheetHeight = localCache.get('sheetHeight'); cid = widget.cid!; - print('cid: $cid'); videoDetailCtr.cid.listen((p0) { cid = p0; - print('cid: $cid'); setState(() {}); }); } @@ -182,207 +177,155 @@ class _BangumiInfoState extends State { padding: const EdgeInsets.only( left: StyleString.safeSpace, right: StyleString.safeSpace, top: 20), sliver: SliverToBoxAdapter( - child: !widget.loadingStatus || bangumiItem != null - ? Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Stack( - children: [ - NetworkImgLayer( - width: 105, - height: 160, - src: !widget.loadingStatus - ? widget.bangumiDetail!.cover! - : bangumiItem!.cover!, - ), - if (bangumiItem != null && - bangumiItem!.rating != null) - PBadge( - text: - '评分 ${!widget.loadingStatus ? widget.bangumiDetail!.rating!['score']! : bangumiItem!.rating!['score']!}', - top: null, - right: 6, - bottom: 6, - left: null, - ), - ], - ), - const SizedBox(width: 10), - Expanded( - child: InkWell( - onTap: () => showIntroDetail(), - child: SizedBox( - height: 158, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: [ - Expanded( - child: Text( - !widget.loadingStatus - ? widget.bangumiDetail!.title! - : bangumiItem!.title!, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - const SizedBox(width: 20), - SizedBox( - width: 34, - height: 34, - child: IconButton( - style: ButtonStyle( - padding: MaterialStateProperty.all( - EdgeInsets.zero), - backgroundColor: - MaterialStateProperty.resolveWith( - (Set states) { - return t - .colorScheme.primaryContainer - .withOpacity(0.7); - }), - ), - onPressed: () => - bangumiIntroController.bangumiAdd(), - icon: Icon( - Icons.favorite_border_rounded, - color: t.colorScheme.primary, - size: 22, - ), - ), - ), - ], - ), - Row( - children: [ - StatView( - theme: 'gray', - view: !widget.loadingStatus - ? widget.bangumiDetail!.stat!['views'] - : bangumiItem!.stat!['views'], - size: 'medium', - ), - const SizedBox(width: 6), - StatDanMu( - theme: 'gray', - danmu: !widget.loadingStatus - ? widget - .bangumiDetail!.stat!['danmakus'] - : bangumiItem!.stat!['danmakus'], - size: 'medium', - ), - ], - ), - const SizedBox(height: 6), - Row( - children: [ - Text( - !widget.loadingStatus - ? (widget.bangumiDetail!.areas! - .isNotEmpty - ? widget.bangumiDetail!.areas! - .first['name'] - : '') - : (bangumiItem!.areas!.isNotEmpty - ? bangumiItem! - .areas!.first['name'] - : ''), - style: TextStyle( - fontSize: 12, - color: t.colorScheme.outline, - ), - ), - const SizedBox(width: 6), - Text( - !widget.loadingStatus - ? widget.bangumiDetail! - .publish!['pub_time_show'] - : bangumiItem! - .publish!['pub_time_show'], - style: TextStyle( - fontSize: 12, - color: t.colorScheme.outline, - ), - ), - ], + NetworkImgLayer( + width: 105, + height: 160, + src: widget.bangumiDetail!.cover!, + ), + PBadge( + text: '评分 ${widget.bangumiDetail!.rating!['score']!}', + top: null, + right: 6, + bottom: 6, + left: null, + ), + ], + ), + const SizedBox(width: 10), + Expanded( + child: InkWell( + onTap: () => showIntroDetail(), + child: SizedBox( + height: 158, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Expanded( + child: Text( + widget.bangumiDetail!.title!, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, ), - // const SizedBox(height: 4), - Text( - !widget.loadingStatus - ? widget.bangumiDetail!.newEp!['desc'] - : bangumiItem!.newEp!['desc'], - style: TextStyle( - fontSize: 12, - color: t.colorScheme.outline, - ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 20), + SizedBox( + width: 34, + height: 34, + child: IconButton( + style: ButtonStyle( + padding: MaterialStateProperty.all( + EdgeInsets.zero), + backgroundColor: + MaterialStateProperty.resolveWith( + (Set states) { + return t.colorScheme.primaryContainer + .withOpacity(0.7); + }), ), - // const SizedBox(height: 10), - const Spacer(), - Text( - '简介:${!widget.loadingStatus ? widget.bangumiDetail!.evaluate! : bangumiItem!.evaluate!}', - maxLines: 3, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 13, - color: t.colorScheme.outline, - ), + onPressed: () => + bangumiIntroController.bangumiAdd(), + icon: Icon( + Icons.favorite_border_rounded, + color: t.colorScheme.primary, + size: 22, ), - ], + ), + ), + ], + ), + Row( + children: [ + StatView( + theme: 'gray', + view: widget.bangumiDetail!.stat!['views'], + size: 'medium', + ), + const SizedBox(width: 6), + StatDanMu( + theme: 'gray', + 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 Spacer(), + Text( + '简介:${widget.bangumiDetail!.evaluate!}', + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 13, + color: t.colorScheme.outline, ), ), - ), - ], + ], + ), ), - const SizedBox(height: 6), - // 点赞收藏转发 布局样式1 - // SingleChildScrollView( - // padding: const EdgeInsets.only(top: 7, bottom: 7), - // scrollDirection: Axis.horizontal, - // child: actionRow( - // context, - // bangumiIntroController, - // videoDetailCtr, - // ), - // ), - // 点赞收藏转发 布局样式2 - actionGrid(context, bangumiIntroController), - // 番剧分p - if ((!widget.loadingStatus && - widget.bangumiDetail!.episodes!.isNotEmpty) || - bangumiItem != null && - bangumiItem!.episodes!.isNotEmpty) ...[ - BangumiPanel( - pages: bangumiItem != null - ? bangumiItem!.episodes! - : widget.bangumiDetail!.episodes!, - cid: cid ?? - (bangumiItem != null - ? bangumiItem!.episodes!.first.cid - : widget.bangumiDetail!.episodes!.first.cid), - sheetHeight: sheetHeight, - changeFuc: (bvid, cid, aid) => bangumiIntroController - .changeSeasonOrbangu(bvid, cid, aid), - ) - ], - ], - ) - : const SizedBox( - height: 100, - child: Center( - child: CircularProgressIndicator(), ), ), - ), + ], + ), + const SizedBox(height: 6), + + /// 点赞收藏转发 + actionGrid(context, bangumiIntroController), + // 番剧分p + if (widget.bangumiDetail!.episodes!.isNotEmpty) ...[ + BangumiPanel( + pages: widget.bangumiDetail!.episodes!, + cid: cid ?? widget.bangumiDetail!.episodes!.first.cid, + sheetHeight: sheetHeight, + changeFuc: (bvid, cid, aid) => + bangumiIntroController.changeSeasonOrbangu(bvid, cid, aid), + bangumiDetail: bangumiIntroController.bangumiDetail.value, + ) + ], + ], + )), ); } @@ -402,57 +345,44 @@ class _BangumiInfoState extends State { children: [ Obx( () => ActionItem( - icon: const Icon(FontAwesomeIcons.thumbsUp), - selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp), - onTap: - handleState(bangumiIntroController.actionLikeVideo), - selectStatus: bangumiIntroController.hasLike.value, - loadingStatus: false, - text: !widget.loadingStatus - ? widget.bangumiDetail!.stat!['likes']!.toString() - : bangumiItem!.stat!['likes']!.toString()), + icon: const Icon(FontAwesomeIcons.thumbsUp), + selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp), + onTap: handleState(bangumiIntroController.actionLikeVideo), + selectStatus: bangumiIntroController.hasLike.value, + text: widget.bangumiDetail!.stat!['likes']!.toString(), + ), ), Obx( () => ActionItem( - icon: const Icon(FontAwesomeIcons.b), - selectIcon: const Icon(FontAwesomeIcons.b), - onTap: - handleState(bangumiIntroController.actionCoinVideo), - selectStatus: bangumiIntroController.hasCoin.value, - loadingStatus: false, - text: !widget.loadingStatus - ? widget.bangumiDetail!.stat!['coins']!.toString() - : bangumiItem!.stat!['coins']!.toString()), + icon: const Icon(FontAwesomeIcons.b), + selectIcon: const Icon(FontAwesomeIcons.b), + onTap: handleState(bangumiIntroController.actionCoinVideo), + selectStatus: bangumiIntroController.hasCoin.value, + text: widget.bangumiDetail!.stat!['coins']!.toString(), + ), ), Obx( () => ActionItem( - icon: const Icon(FontAwesomeIcons.star), - selectIcon: const Icon(FontAwesomeIcons.solidStar), - onTap: () => showFavBottomSheet(), - selectStatus: bangumiIntroController.hasFav.value, - loadingStatus: false, - text: !widget.loadingStatus - ? widget.bangumiDetail!.stat!['favorite']!.toString() - : bangumiItem!.stat!['favorite']!.toString()), + icon: const Icon(FontAwesomeIcons.star), + selectIcon: const Icon(FontAwesomeIcons.solidStar), + onTap: () => showFavBottomSheet(), + selectStatus: bangumiIntroController.hasFav.value, + text: widget.bangumiDetail!.stat!['favorite']!.toString(), + ), ), ActionItem( icon: const Icon(FontAwesomeIcons.comment), selectIcon: const Icon(FontAwesomeIcons.reply), onTap: () => videoDetailCtr.tabCtr.animateTo(1), selectStatus: false, - loadingStatus: false, - text: !widget.loadingStatus - ? widget.bangumiDetail!.stat!['reply']!.toString() - : bangumiItem!.stat!['reply']!.toString(), + text: widget.bangumiDetail!.stat!['reply']!.toString(), ), ActionItem( - icon: const Icon(FontAwesomeIcons.shareFromSquare), - onTap: () => bangumiIntroController.actionShareVideo(), - selectStatus: false, - loadingStatus: false, - text: !widget.loadingStatus - ? widget.bangumiDetail!.stat!['share']!.toString() - : bangumiItem!.stat!['share']!.toString()), + icon: const Icon(FontAwesomeIcons.shareFromSquare), + onTap: () => bangumiIntroController.actionShareVideo(), + selectStatus: false, + text: widget.bangumiDetail!.stat!['share']!.toString(), + ), ], ), ), @@ -460,63 +390,4 @@ class _BangumiInfoState extends State { ); }); } - - Widget actionRow(BuildContext context, videoIntroController, videoDetailCtr) { - return Row(children: [ - Obx( - () => ActionRowItem( - icon: const Icon(FontAwesomeIcons.thumbsUp), - onTap: handleState(videoIntroController.actionLikeVideo), - selectStatus: videoIntroController.hasLike.value, - loadingStatus: widget.loadingStatus, - text: !widget.loadingStatus - ? widget.bangumiDetail!.stat!['likes']!.toString() - : '-', - ), - ), - const SizedBox(width: 8), - Obx( - () => ActionRowItem( - icon: const Icon(FontAwesomeIcons.b), - onTap: handleState(videoIntroController.actionCoinVideo), - selectStatus: videoIntroController.hasCoin.value, - loadingStatus: widget.loadingStatus, - text: !widget.loadingStatus - ? widget.bangumiDetail!.stat!['coins']!.toString() - : '-', - ), - ), - const SizedBox(width: 8), - Obx( - () => ActionRowItem( - icon: const Icon(FontAwesomeIcons.heart), - onTap: () => showFavBottomSheet(), - selectStatus: videoIntroController.hasFav.value, - loadingStatus: widget.loadingStatus, - text: !widget.loadingStatus - ? widget.bangumiDetail!.stat!['favorite']!.toString() - : '-', - ), - ), - const SizedBox(width: 8), - ActionRowItem( - icon: const Icon(FontAwesomeIcons.comment), - onTap: () { - videoDetailCtr.tabCtr.animateTo(1); - }, - selectStatus: false, - loadingStatus: widget.loadingStatus, - text: !widget.loadingStatus - ? widget.bangumiDetail!.stat!['reply']!.toString() - : '-', - ), - const SizedBox(width: 8), - ActionRowItem( - icon: const Icon(FontAwesomeIcons.share), - onTap: () => videoIntroController.actionShareVideo(), - selectStatus: false, - loadingStatus: widget.loadingStatus, - text: '转发'), - ]); - } } diff --git a/lib/pages/bangumi/widgets/bangumi_panel.dart b/lib/pages/bangumi/widgets/bangumi_panel.dart index 5996d6c82..05fd814c7 100644 --- a/lib/pages/bangumi/widgets/bangumi_panel.dart +++ b/lib/pages/bangumi/widgets/bangumi_panel.dart @@ -14,12 +14,14 @@ class BangumiPanel extends StatefulWidget { this.cid, this.sheetHeight, this.changeFuc, + this.bangumiDetail, }); final List pages; final int? cid; final double? sheetHeight; final Function? changeFuc; + final BangumiInfoModel? bangumiDetail; @override State createState() => _BangumiPanelState(); @@ -65,6 +67,47 @@ class _BangumiPanelState extends State { super.dispose(); } + Widget buildPageListItem( + EpisodeItem page, + int index, + bool isCurrentIndex, + ) { + Color primary = Theme.of(context).colorScheme.primary; + return ListTile( + onTap: () { + Get.back(); + setState(() { + changeFucCall(page, index); + }); + }, + dense: false, + leading: isCurrentIndex + ? Image.asset( + 'assets/images/live.gif', + color: primary, + height: 12, + ) + : null, + title: Text( + '第${page.title}话 ${page.longTitle!}', + style: TextStyle( + fontSize: 14, + color: isCurrentIndex + ? primary + : Theme.of(context).colorScheme.onSurface, + ), + ), + trailing: page.badge != null + ? Text( + page.badge!, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + ), + ) + : const SizedBox(), + ); + } + void showBangumiPanel() { showBottomSheet( context: context, @@ -105,38 +148,22 @@ class _BangumiPanelState extends State { Expanded( child: Material( child: ScrollablePositionedList.builder( - itemCount: widget.pages.length, - itemBuilder: (BuildContext context, int index) => - ListTile( - onTap: () { - setState(() { - changeFucCall(widget.pages[index], index); - }); - }, - dense: false, - leading: index == currentIndex - ? Image.asset( - 'assets/images/live.gif', - color: Theme.of(context).colorScheme.primary, - height: 12, - ) - : null, - title: Text( - '第${index + 1}话 ${widget.pages[index].longTitle!}', - style: TextStyle( - fontSize: 14, - color: index == currentIndex - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onSurface, - ), - ), - trailing: widget.pages[index].badge != null - ? Image.asset( - 'assets/images/big-vip.png', - height: 20, + itemCount: widget.pages.length + 1, + itemBuilder: (BuildContext context, int index) { + bool isLastItem = index == widget.pages.length; + bool isCurrentIndex = currentIndex == index; + return isLastItem + ? SizedBox( + height: + MediaQuery.of(context).padding.bottom + + 20, ) - : const SizedBox(), - ), + : buildPageListItem( + widget.pages[index], + index, + isCurrentIndex, + ); + }, itemScrollController: itemScrollController, ), ), @@ -178,11 +205,11 @@ class _BangumiPanelState extends State { return Column( children: [ Padding( - padding: const EdgeInsets.only(top: 10, bottom: 6), + padding: const EdgeInsets.only(top: 10, bottom: 10), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text('合集 '), + const Text('选集 '), Expanded( child: Text( ' 正在播放:${widget.pages[currentIndex].longTitle}', @@ -202,7 +229,7 @@ class _BangumiPanelState extends State { ), onPressed: () => showBangumiPanel(), child: Text( - '全${widget.pages.length}话', + '${widget.bangumiDetail!.newEp!['desc']}', style: const TextStyle(fontSize: 13), ), ), @@ -255,23 +282,15 @@ class _BangumiPanelState extends State { ), const SizedBox(width: 2), if (widget.pages[i].badge != null) ...[ - if (widget.pages[i].badge == '会员') ...[ - Image.asset( - 'assets/images/big-vip.png', - height: 16, - ), - ], - if (widget.pages[i].badge != '会员') ...[ - const Spacer(), - Text( - widget.pages[i].badge!, - style: TextStyle( - fontSize: 11, - color: - Theme.of(context).colorScheme.primary, - ), + const Spacer(), + Text( + widget.pages[i].badge!, + style: TextStyle( + fontSize: 12, + color: + Theme.of(context).colorScheme.primary, ), - ], + ), ] ], ), diff --git a/lib/pages/blacklist/index.dart b/lib/pages/blacklist/index.dart index 3bf641463..402790f58 100644 --- a/lib/pages/blacklist/index.dart +++ b/lib/pages/blacklist/index.dart @@ -139,7 +139,7 @@ class BlackListController extends GetxController { int currentPage = 1; int pageSize = 50; RxInt total = 0.obs; - RxList blackList = [BlackListItem()].obs; + RxList blackList = [].obs; Future queryBlacklist({type = 'init'}) async { if (type == 'init') { diff --git a/lib/pages/dynamics/controller.dart b/lib/pages/dynamics/controller.dart index 26ba2b224..b76766639 100644 --- a/lib/pages/dynamics/controller.dart +++ b/lib/pages/dynamics/controller.dart @@ -20,7 +20,7 @@ import 'package:pilipala/utils/utils.dart'; class DynamicsController extends GetxController { int page = 1; String? offset = ''; - RxList dynamicsList = [DynamicItemModel()].obs; + RxList dynamicsList = [].obs; Rx dynamicsType = DynamicsType.values[0].obs; RxString dynamicsTypeLabel = '全部'.obs; final ScrollController scrollController = ScrollController(); @@ -105,7 +105,7 @@ class DynamicsController extends GetxController { onSelectType(value) async { dynamicsType.value = filterTypeList[value]['value']; - dynamicsList.value = [DynamicItemModel()]; + dynamicsList.value = []; page = 1; initialValue.value = value; await queryFollowDynamic(); @@ -249,8 +249,8 @@ class DynamicsController extends GetxController { return {'status': false, 'msg': '账号未登录'}; } if (type == 'init') { - upData.value.upList = []; - upData.value.liveUsers = LiveUsers(); + upData.value.upList = []; + upData.value.liveList = []; } var res = await DynamicsHttp.followUp(); if (res['status']) { @@ -258,20 +258,23 @@ class DynamicsController extends GetxController { if (upData.value.upList!.isEmpty) { mid.value = -1; } + upData.value.upList!.insertAll(0, [ + UpItem(face: '', uname: '全部动态', mid: -1), + UpItem(face: userInfo.face, uname: '我', mid: userInfo.mid), + ]); } return res; } onSelectUp(mid) async { dynamicsType.value = DynamicsType.values[0]; - dynamicsList.value = [DynamicItemModel()]; + dynamicsList.value = []; page = 1; queryFollowDynamic(); } onRefresh() async { page = 1; - print('onRefresh'); await queryFollowUp(); await queryFollowDynamic(); } @@ -293,7 +296,7 @@ class DynamicsController extends GetxController { dynamicsType.value = DynamicsType.values[0]; initialValue.value = 0; SmartDialog.showToast('还原默认加载'); - dynamicsList.value = [DynamicItemModel()]; + dynamicsList.value = []; queryFollowDynamic(); } } diff --git a/lib/pages/dynamics/detail/controller.dart b/lib/pages/dynamics/detail/controller.dart index 4c5f35d6b..8e1173833 100644 --- a/lib/pages/dynamics/detail/controller.dart +++ b/lib/pages/dynamics/detail/controller.dart @@ -17,7 +17,7 @@ class DynamicDetailController extends GetxController { int currentPage = 0; bool isLoadingMore = false; RxString noMore = ''.obs; - RxList replyList = [ReplyItemModel()].obs; + RxList replyList = [].obs; RxInt acount = 0.obs; final ScrollController scrollController = ScrollController(); diff --git a/lib/pages/dynamics/view.dart b/lib/pages/dynamics/view.dart index 4a92cdfb8..fe594a43f 100644 --- a/lib/pages/dynamics/view.dart +++ b/lib/pages/dynamics/view.dart @@ -14,6 +14,7 @@ import 'package:pilipala/pages/main/index.dart'; import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/storage.dart'; +import '../mine/controller.dart'; import 'controller.dart'; import 'widgets/dynamic_panel.dart'; import 'widgets/up_panel.dart'; @@ -28,6 +29,7 @@ class DynamicsPage extends StatefulWidget { class _DynamicsPageState extends State with AutomaticKeepAliveClientMixin { final DynamicsController _dynamicsController = Get.put(DynamicsController()); + final MineController mineController = Get.put(MineController()); late Future _futureBuilderFuture; late Future _futureBuilderFutureUp; Box userInfoCache = GStrorage.userInfo; @@ -256,6 +258,14 @@ class _DynamicsPageState extends State } }, ); + } else if (data['msg'] == "账号未登录") { + return HttpError( + errMsg: data['msg'], + btnText: "去登录", + fn: () { + mineController.onLogin(); + }, + ); } else { return HttpError( errMsg: data['msg'], diff --git a/lib/pages/dynamics/widgets/article_panel.dart b/lib/pages/dynamics/widgets/article_panel.dart index e68d966d0..19707435a 100644 --- a/lib/pages/dynamics/widgets/article_panel.dart +++ b/lib/pages/dynamics/widgets/article_panel.dart @@ -34,25 +34,25 @@ Widget articlePanel(item, context, {floor = 1}) { ), const SizedBox(height: 8), ], - Text( - item.modules.moduleDynamic.major.opus.title, - style: Theme.of(context) - .textTheme - .titleMedium! - .copyWith(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 2), - if (item.modules.moduleDynamic.major.opus.summary.text != - 'undefined') ...[ - Text( - item.modules.moduleDynamic.major.opus.summary.richTextNodes.first - .text, - maxLines: 4, - style: const TextStyle(height: 1.55), - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 2), - ], + // Text( + // item.modules.moduleDynamic.major.opus.title, + // style: Theme.of(context) + // .textTheme + // .titleMedium! + // .copyWith(fontWeight: FontWeight.bold), + // ), + // const SizedBox(height: 2), + // if (item.modules.moduleDynamic.major.opus.summary.text != + // 'undefined') ...[ + // Text( + // item.modules.moduleDynamic.major.opus.summary.richTextNodes.first + // .text, + // maxLines: 4, + // style: const TextStyle(height: 1.55), + // overflow: TextOverflow.ellipsis, + // ), + // const SizedBox(height: 2), + // ], picWidget(item, context) ], ), diff --git a/lib/pages/dynamics/widgets/content_panel.dart b/lib/pages/dynamics/widgets/content_panel.dart index d10804d7e..e1beaeb2c 100644 --- a/lib/pages/dynamics/widgets/content_panel.dart +++ b/lib/pages/dynamics/widgets/content_panel.dart @@ -45,7 +45,9 @@ class _ContentState extends State { if (len == 1) { OpusPicsModel pictureItem = pics.first; picList.add(pictureItem.url!); - spanChilds.add(const TextSpan(text: '\n')); + + /// 图片上方的空白间隔 + // spanChilds.add(const TextSpan(text: '\n')); spanChilds.add( WidgetSpan( child: LayoutBuilder( diff --git a/lib/pages/dynamics/widgets/rich_node_panel.dart b/lib/pages/dynamics/widgets/rich_node_panel.dart index 8f744dd59..5ffee5f10 100644 --- a/lib/pages/dynamics/widgets/rich_node_panel.dart +++ b/lib/pages/dynamics/widgets/rich_node_panel.dart @@ -19,6 +19,17 @@ InlineSpan richNode(item, context) { // 动态页面 richTextNodes 层级可能与主页动态层级不同 richTextNodes = item.modules.moduleDynamic.major.opus.summary.richTextNodes; + if (item.modules.moduleDynamic.major.opus.title != null) { + spanChilds.add( + TextSpan( + text: item.modules.moduleDynamic.major.opus.title + '\n', + style: Theme.of(context) + .textTheme + .titleMedium! + .copyWith(fontWeight: FontWeight.bold), + ), + ); + } } if (richTextNodes == null || richTextNodes.isEmpty) { return spacer; diff --git a/lib/pages/dynamics/widgets/up_panel.dart b/lib/pages/dynamics/widgets/up_panel.dart index 84753ff9b..fd0ae642e 100644 --- a/lib/pages/dynamics/widgets/up_panel.dart +++ b/lib/pages/dynamics/widgets/up_panel.dart @@ -1,16 +1,14 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:hive/hive.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/models/dynamics/up.dart'; import 'package:pilipala/models/live/item.dart'; import 'package:pilipala/pages/dynamics/controller.dart'; import 'package:pilipala/utils/feed_back.dart'; -import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/utils.dart'; class UpPanel extends StatefulWidget { - final FollowUpModel? upData; + final FollowUpModel upData; const UpPanel(this.upData, {Key? key}) : super(key: key); @override @@ -24,38 +22,22 @@ class _UpPanelState extends State { List upList = []; List liveList = []; static const itemPadding = EdgeInsets.symmetric(horizontal: 5, vertical: 0); - Box userInfoCache = GStrorage.userInfo; - var userInfo; + late MyInfo userInfo; - @override - void initState() { - super.initState(); - upList = widget.upData!.upList!; - if (widget.upData!.liveUsers != null) { - liveList = widget.upData!.liveUsers!.items!; - } - upList.insert( - 0, - UpItem(face: '', uname: '全部动态', mid: -1), - ); - userInfo = userInfoCache.get('userInfoCache'); - upList.insert( - 1, - UpItem( - face: userInfo.face, - uname: '我', - mid: userInfo.mid, - ), - ); + void listFormat() { + userInfo = widget.upData.myInfo!; + upList = widget.upData.upList!; + liveList = widget.upData.liveList!; } @override Widget build(BuildContext context) { + listFormat(); return SliverPersistentHeader( floating: true, pinned: false, delegate: _SliverHeaderDelegate( - height: 126, + height: liveList.isNotEmpty || upList.isNotEmpty ? 126 : 0, child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, @@ -90,7 +72,7 @@ class _UpPanelState extends State { color: Theme.of(context).colorScheme.background, child: Row( children: [ - Expanded( + Flexible( child: ListView( scrollDirection: Axis.horizontal, controller: scrollController, diff --git a/lib/pages/emote/controller.dart b/lib/pages/emote/controller.dart new file mode 100644 index 000000000..c1a4c5049 --- /dev/null +++ b/lib/pages/emote/controller.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../http/reply.dart'; +import '../../models/video/reply/emote.dart'; + +class EmotePanelController extends GetxController + with GetTickerProviderStateMixin { + late List emotePackage; + late TabController tabController; + + Future getEmote() async { + var res = await ReplyHttp.getEmoteList(business: 'reply'); + if (res['status']) { + emotePackage = res['data'].packages; + tabController = TabController(length: emotePackage.length, vsync: this); + } + return res; + } +} diff --git a/lib/pages/emote/index.dart b/lib/pages/emote/index.dart new file mode 100644 index 000000000..32ce53e3c --- /dev/null +++ b/lib/pages/emote/index.dart @@ -0,0 +1,4 @@ +library emote; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/emote/view.dart b/lib/pages/emote/view.dart new file mode 100644 index 000000000..d30767c34 --- /dev/null +++ b/lib/pages/emote/view.dart @@ -0,0 +1,116 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../models/video/reply/emote.dart'; +import 'controller.dart'; + +class EmotePanel extends StatefulWidget { + final Function onChoose; + const EmotePanel({super.key, required this.onChoose}); + + @override + State createState() => _EmotePanelState(); +} + +class _EmotePanelState extends State + with AutomaticKeepAliveClientMixin { + final EmotePanelController _emotePanelController = + Get.put(EmotePanelController()); + late Future _futureBuilderFuture; + + @override + bool get wantKeepAlive => true; + + @override + void initState() { + _futureBuilderFuture = _emotePanelController.getEmote(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return FutureBuilder( + future: _futureBuilderFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + Map data = snapshot.data as Map; + if (data['status']) { + List emotePackage = + _emotePanelController.emotePackage; + + return Column( + children: [ + Expanded( + child: TabBarView( + controller: _emotePanelController.tabController, + children: emotePackage.map( + (e) { + int size = e.emote!.first.meta!.size!; + int type = e.type!; + return Padding( + padding: const EdgeInsets.fromLTRB(12, 6, 12, 0), + child: GridView.builder( + gridDelegate: + SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: size == 1 ? 40 : 60, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + ), + itemCount: e.emote!.length, + itemBuilder: (context, index) { + return Material( + color: Colors.transparent, + clipBehavior: Clip.hardEdge, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + child: InkWell( + onTap: () { + widget.onChoose(e, e.emote![index]); + }, + child: Padding( + padding: const EdgeInsets.all(3), + child: type == 4 + ? Text( + e.emote![index].text!, + overflow: TextOverflow.clip, + maxLines: 1, + ) + : Image.network( + e.emote![index].url!, + width: size * 38, + height: size * 38, + ), + ), + ), + ); + }, + ), + ); + }, + ).toList(), + )), + Divider( + height: 1, + color: Theme.of(context).dividerColor.withOpacity(0.1), + ), + TabBar( + controller: _emotePanelController.tabController, + dividerColor: Colors.transparent, + isScrollable: true, + tabs: _emotePanelController.emotePackage + .map((e) => Tab(text: e.text)) + .toList(), + ), + SizedBox(height: MediaQuery.of(context).padding.bottom + 20), + ], + ); + } else { + return Center(child: Text(data['msg'])); + } + } else { + return const Center(child: Text('加载中...')); + } + }); + } +} diff --git a/lib/pages/fan/controller.dart b/lib/pages/fan/controller.dart index 8675ada7f..c1c2a4275 100644 --- a/lib/pages/fan/controller.dart +++ b/lib/pages/fan/controller.dart @@ -10,7 +10,7 @@ class FansController extends GetxController { int pn = 1; int ps = 20; int total = 0; - RxList fansList = [FansItemModel()].obs; + RxList fansList = [].obs; late int mid; late String name; var userInfo; diff --git a/lib/pages/fav_detail/controller.dart b/lib/pages/fav_detail/controller.dart index 69cc939e2..55d5b884e 100644 --- a/lib/pages/fav_detail/controller.dart +++ b/lib/pages/fav_detail/controller.dart @@ -16,7 +16,7 @@ class FavDetailController extends GetxController { RxMap favInfo = {}.obs; RxList favList = [].obs; RxString loadingText = '加载中...'.obs; - int mediaCount = 0; + RxInt mediaCount = 0.obs; @override void onInit() { @@ -29,7 +29,7 @@ class FavDetailController extends GetxController { } Future queryUserFavFolderDetail({type = 'init'}) async { - if (type == 'onLoad' && favList.length >= mediaCount) { + if (type == 'onLoad' && favList.length >= mediaCount.value) { loadingText.value = '没有更多了'; return; } @@ -43,11 +43,11 @@ class FavDetailController extends GetxController { favInfo.value = res['data'].info; if (currentPage == 1 && type == 'init') { favList.value = res['data'].medias; - mediaCount = res['data'].info['media_count']; + mediaCount.value = res['data'].info['media_count']; } else if (type == 'onLoad') { favList.addAll(res['data'].medias); } - if (favList.length >= mediaCount) { + if (favList.length >= mediaCount.value) { loadingText.value = '没有更多了'; } } diff --git a/lib/pages/fav_detail/view.dart b/lib/pages/fav_detail/view.dart index 27d7182b1..d94f5149c 100644 --- a/lib/pages/fav_detail/view.dart +++ b/lib/pages/fav_detail/view.dart @@ -84,7 +84,7 @@ class _FavDetailPageState extends State { style: Theme.of(context).textTheme.titleMedium, ), Text( - '共${_favDetailController.item!.mediaCount!}条视频', + '共${_favDetailController.mediaCount}条视频', style: Theme.of(context).textTheme.labelMedium, ) ], @@ -175,7 +175,7 @@ class _FavDetailPageState extends State { padding: const EdgeInsets.only(top: 15, bottom: 8, left: 14), child: Obx( () => Text( - '共${_favDetailController.favList.length}条视频', + '共${_favDetailController.mediaCount}条视频', style: TextStyle( fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, diff --git a/lib/pages/history/controller.dart b/lib/pages/history/controller.dart index e7822cd9c..a1f18113f 100644 --- a/lib/pages/history/controller.dart +++ b/lib/pages/history/controller.dart @@ -88,8 +88,10 @@ class HistoryController extends GetxController { // 观看历史暂停状态 Future historyStatus() async { var res = await UserHttp.historyStatus(); - pauseStatus.value = res.data['data']; - localCache.put(LocalCacheKey.historyPause, res.data['data']); + if (res.data['code'] == 0) { + pauseStatus.value = res.data['data']; + localCache.put(LocalCacheKey.historyPause, res.data['data']); + } } // 清空观看历史 diff --git a/lib/pages/history/widgets/item.dart b/lib/pages/history/widgets/item.dart index a83e118b1..f4bd92212 100644 --- a/lib/pages/history/widgets/item.dart +++ b/lib/pages/history/widgets/item.dart @@ -185,7 +185,7 @@ class HistoryItem extends StatelessWidget { ? '已看完' : '${Utils.timeFormat(videoItem.progress!)}/${Utils.timeFormat(videoItem.duration!)}', right: 6.0, - bottom: 6.0, + bottom: 8.0, type: 'gray', ), // 右上角 @@ -258,6 +258,24 @@ class HistoryItem extends StatelessWidget { ), ), ), + Positioned( + left: 3, + right: 3, + bottom: 0, + child: ClipRRect( + borderRadius: BorderRadius.only( + bottomLeft: + Radius.circular(StyleString.imgRadius.x), + bottomRight: + Radius.circular(StyleString.imgRadius.x), + ), + child: LinearProgressIndicator( + value: videoItem.progress == -1 + ? 100 + : videoItem.progress / videoItem.duration, + ), + ), + ) ], ), VideoContent(videoItem: videoItem, ctr: ctr) diff --git a/lib/pages/home/controller.dart b/lib/pages/home/controller.dart index 9f6f8ac54..fb85be0b6 100644 --- a/lib/pages/home/controller.dart +++ b/lib/pages/home/controller.dart @@ -34,8 +34,6 @@ class HomeController extends GetxController with GetTickerProviderStateMixin { userInfo = userInfoCache.get('userInfoCache'); userLogin.value = userInfo != null; userFace.value = userInfo != null ? userInfo.face : ''; - // 进行tabs配置 - setTabConfig(); hideSearchBar = setting.get(SettingBoxKey.hideSearchBar, defaultValue: true); if (setting.get(SettingBoxKey.enableSearchWord, defaultValue: true)) { @@ -43,6 +41,8 @@ class HomeController extends GetxController with GetTickerProviderStateMixin { } enableGradientBg = setting.get(SettingBoxKey.enableGradientBg, defaultValue: true); + // 进行tabs配置 + setTabConfig(); } void onRefresh() { @@ -91,19 +91,21 @@ class HomeController extends GetxController with GetTickerProviderStateMixin { vsync: this, ); // 监听 tabController 切换 - tabController.animation!.addListener(() { - if (tabController.indexIsChanging) { - if (initialIndex.value != tabController.index) { - initialIndex.value = tabController.index; - } - } else { - final int temp = tabController.animation!.value.round(); - if (initialIndex.value != temp) { - initialIndex.value = temp; - tabController.index = initialIndex.value; + if (enableGradientBg) { + tabController.animation!.addListener(() { + if (tabController.indexIsChanging) { + if (initialIndex.value != tabController.index) { + initialIndex.value = tabController.index; + } + } else { + final int temp = tabController.animation!.value.round(); + if (initialIndex.value != temp) { + initialIndex.value = temp; + tabController.index = initialIndex.value; + } } - } - }); + }); + } } void searchDefault() async { diff --git a/lib/pages/home/view.dart b/lib/pages/home/view.dart index 91d0ea8c6..b0cef90bd 100644 --- a/lib/pages/home/view.dart +++ b/lib/pages/home/view.dart @@ -58,6 +58,9 @@ class _HomePageState extends State return Scaffold( extendBody: true, extendBodyBehindAppBar: true, + appBar: _homeController.enableGradientBg + ? null + : AppBar(toolbarHeight: 0, elevation: 0), body: Stack( children: [ // gradient background @@ -412,13 +415,16 @@ class SearchBar extends StatelessWidget { ), const SizedBox(width: 10), Obx( - () => Text( - ctr!.defaultSearch.value, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle(color: colorScheme.outline), + () => Expanded( + child: Text( + ctr!.defaultSearch.value, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(color: colorScheme.outline), + ), ), ), + const SizedBox(width: 15), ], ), ), diff --git a/lib/pages/hot/controller.dart b/lib/pages/hot/controller.dart index 65706c32a..850724755 100644 --- a/lib/pages/hot/controller.dart +++ b/lib/pages/hot/controller.dart @@ -7,7 +7,7 @@ class HotController extends GetxController { final ScrollController scrollController = ScrollController(); final int _count = 20; int _currentPage = 1; - RxList videoList = [HotVideoItemModel()].obs; + RxList videoList = [].obs; bool isLoadingMore = false; bool flag = false; OverlayEntry? popupDialog; diff --git a/lib/pages/live_room/controller.dart b/lib/pages/live_room/controller.dart index 2bb1cd0a7..5c2a9800d 100644 --- a/lib/pages/live_room/controller.dart +++ b/lib/pages/live_room/controller.dart @@ -1,6 +1,8 @@ +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:pilipala/http/constants.dart'; import 'package:pilipala/http/live.dart'; +import 'package:pilipala/models/live/quality.dart'; import 'package:pilipala/models/live/room_info.dart'; import 'package:pilipala/plugin/pl_player/index.dart'; import '../../models/live/room_info_h5.dart'; @@ -19,10 +21,16 @@ class LiveRoomController extends GetxController { PlPlayerController.getInstance(videoType: 'live'); Rx roomInfoH5 = RoomInfoH5Model().obs; late bool enableCDN; + late int currentQn; + int? tempCurrentQn; + late List> acceptQnList; + RxString currentQnDesc = ''.obs; @override void onInit() { super.onInit(); + currentQn = setting.get(SettingBoxKey.defaultLiveQa, + defaultValue: LiveQuality.values.last.code); roomId = int.parse(Get.parameters['roomid']!); if (Get.arguments != null) { liveItem = Get.arguments['liveItem']; @@ -57,11 +65,28 @@ class LiveRoomController extends GetxController { } Future queryLiveInfo() async { - var res = await LiveHttp.liveRoomInfo(roomId: roomId, qn: 10000); + var res = await LiveHttp.liveRoomInfo(roomId: roomId, qn: currentQn); if (res['status']) { List codec = res['data'].playurlInfo.playurl.stream.first.format.first.codec; CodecItem item = codec.first; + // 以服务端返回的码率为准 + currentQn = item.currentQn!; + if (tempCurrentQn != null && tempCurrentQn == currentQn) { + SmartDialog.showToast('画质切换失败,请检查登录状态'); + } + List acceptQn = item.acceptQn!; + acceptQnList = acceptQn.map((e) { + return { + 'code': e, + 'desc': LiveQuality.values + .firstWhere((element) => element.code == e) + .description, + }; + }).toList(); + currentQnDesc.value = LiveQuality.values + .firstWhere((element) => element.code == currentQn) + .description; String videoUrl = enableCDN ? VideoUtils.getCdnUrl(item) : (item.urlInfo?.first.host)! + @@ -90,4 +115,17 @@ class LiveRoomController extends GetxController { } return res; } + + // 修改画质 + void changeQn(int qn) async { + tempCurrentQn = currentQn; + if (currentQn == qn) { + return; + } + currentQn = qn; + currentQnDesc.value = LiveQuality.values + .firstWhere((element) => element.code == currentQn) + .description; + await queryLiveInfo(); + } } diff --git a/lib/pages/live_room/widgets/bottom_control.dart b/lib/pages/live_room/widgets/bottom_control.dart index a00f3d92f..3c908d712 100644 --- a/lib/pages/live_room/widgets/bottom_control.dart +++ b/lib/pages/live_room/widgets/bottom_control.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:floating/floating.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:get/get.dart'; import 'package:hive/hive.dart'; import 'package:pilipala/models/video/play/url.dart'; import 'package:pilipala/pages/live_room/index.dart'; @@ -29,7 +30,6 @@ class BottomControl extends StatefulWidget implements PreferredSizeWidget { class _BottomControlState extends State { late PlayUrlModel videoInfo; - List playSpeed = PlaySpeed.values; TextStyle subTitleStyle = const TextStyle(fontSize: 12); TextStyle titleStyle = const TextStyle(fontSize: 14); Size get preferredSize => const Size(double.infinity, kToolbarHeight); @@ -84,6 +84,30 @@ class _BottomControlState extends State { // ), // ), // const SizedBox(width: 4), + SizedBox( + width: 30, + child: PopupMenuButton( + padding: EdgeInsets.zero, + onSelected: (value) { + widget.liveRoomCtr!.changeQn(value); + }, + child: Obx( + () => Text( + widget.liveRoomCtr!.currentQnDesc.value, + style: const TextStyle(color: Colors.white, fontSize: 13), + ), + ), + itemBuilder: (BuildContext context) { + return widget.liveRoomCtr!.acceptQnList.map((e) { + return PopupMenuItem( + value: e['code'], + child: Text(e['desc']), + ); + }).toList(); + }, + ), + ), + const SizedBox(width: 10), if (Platform.isAndroid) ...[ SizedBox( width: 34, @@ -111,7 +135,7 @@ class _BottomControlState extends State { ), ), ), - const SizedBox(width: 4), + const SizedBox(width: 10), ], ComBtn( icon: const Icon( diff --git a/lib/pages/main/controller.dart b/lib/pages/main/controller.dart index 6f33d9cd4..ddbd364a5 100644 --- a/lib/pages/main/controller.dart +++ b/lib/pages/main/controller.dart @@ -9,54 +9,20 @@ import 'package:pilipala/http/common.dart'; import 'package:pilipala/pages/dynamics/index.dart'; import 'package:pilipala/pages/home/view.dart'; import 'package:pilipala/pages/media/index.dart'; +import 'package:pilipala/pages/rank/index.dart'; import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/utils.dart'; import '../../models/common/dynamic_badge_mode.dart'; +import '../../models/common/nav_bar_config.dart'; class MainController extends GetxController { List pages = [ const HomePage(), + const RankPage(), const DynamicsPage(), const MediaPage(), ]; - RxList navigationBars = [ - { - 'icon': const Icon( - Icons.home_outlined, - size: 21, - ), - 'selectIcon': const Icon( - Icons.home, - size: 21, - ), - 'label': "首页", - 'count': 0, - }, - { - 'icon': const Icon( - Icons.motion_photos_on_outlined, - size: 21, - ), - 'selectIcon': const Icon( - Icons.motion_photos_on, - size: 21, - ), - 'label': "动态", - 'count': 0, - }, - { - 'icon': const Icon( - Icons.video_collection_outlined, - size: 20, - ), - 'selectIcon': const Icon( - Icons.video_collection, - size: 21, - ), - 'label': "媒体库", - 'count': 0, - } - ].obs; + RxList navigationBars = defaultNavigationBars.obs; final StreamController bottomBarStream = StreamController.broadcast(); Box setting = GStrorage.setting; @@ -75,6 +41,10 @@ class MainController extends GetxController { Utils.checkUpdata(); } hideTabBar = setting.get(SettingBoxKey.hideTabBar, defaultValue: true); + int defaultHomePage = + setting.get(SettingBoxKey.defaultHomePage, defaultValue: 0) as int; + selectedIndex = defaultNavigationBars + .indexWhere((item) => item['id'] == defaultHomePage); var userInfo = userInfoCache.get('userInfoCache'); userLogin.value = userInfo != null; dynamicBadgeType.value = DynamicBadgeMode.values[setting.get( diff --git a/lib/pages/main/view.dart b/lib/pages/main/view.dart index 04e0f087f..c551e6905 100644 --- a/lib/pages/main/view.dart +++ b/lib/pages/main/view.dart @@ -7,6 +7,7 @@ import 'package:pilipala/models/common/dynamic_badge_mode.dart'; import 'package:pilipala/pages/dynamics/index.dart'; import 'package:pilipala/pages/home/index.dart'; import 'package:pilipala/pages/media/index.dart'; +import 'package:pilipala/pages/rank/index.dart'; import 'package:pilipala/utils/event_bus.dart'; import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/storage.dart'; @@ -22,6 +23,7 @@ class MainApp extends StatefulWidget { class _MainAppState extends State with SingleTickerProviderStateMixin { final MainController _mainController = Get.put(MainController()); final HomeController _homeController = Get.put(HomeController()); + final RankController _rankController = Get.put(RankController()); final DynamicsController _dynamicController = Get.put(DynamicsController()); final MediaController _mediaController = Get.put(MediaController()); @@ -57,6 +59,21 @@ class _MainAppState extends State with SingleTickerProviderStateMixin { _homeController.flag = false; } + if (currentPage is RankPage) { + if (_rankController.flag) { + // 单击返回顶部 双击并刷新 + if (DateTime.now().millisecondsSinceEpoch - _lastSelectTime! < 500) { + _rankController.onRefresh(); + } else { + _rankController.animateToTop(); + } + _lastSelectTime = DateTime.now().millisecondsSinceEpoch; + } + _rankController.flag = true; + } else { + _rankController.flag = false; + } + if (currentPage is DynamicsPage) { if (_dynamicController.flag) { // 单击返回顶部 双击并刷新 diff --git a/lib/pages/media/view.dart b/lib/pages/media/view.dart index c3bad4b97..460c5648f 100644 --- a/lib/pages/media/view.dart +++ b/lib/pages/media/view.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:get/get.dart'; +import 'package:media_kit/media_kit.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/models/user/fav_folder.dart'; import 'package:pilipala/pages/main/index.dart'; @@ -102,7 +103,11 @@ class _MediaPageState extends State ], Obx(() => mediaController.userLogin.value ? favFolder(mediaController, context) - : const SizedBox()) + : const SizedBox()), + SizedBox( + height: MediaQuery.of(context).padding.bottom + + kBottomNavigationBarHeight, + ) ], ), ), diff --git a/lib/pages/member/controller.dart b/lib/pages/member/controller.dart index 70169e3dc..0aa7166ff 100644 --- a/lib/pages/member/controller.dart +++ b/lib/pages/member/controller.dart @@ -20,7 +20,7 @@ class MemberController extends GetxController { Box userInfoCache = GStrorage.userInfo; late int ownerMid; // 投稿列表 - RxList? archiveList = [VListItemModel()].obs; + RxList? archiveList = [].obs; dynamic userInfo; RxInt attribute = (-1).obs; RxString attributeText = '关注'.obs; diff --git a/lib/pages/preview/view.dart b/lib/pages/preview/view.dart index dcffc9730..13868d370 100644 --- a/lib/pages/preview/view.dart +++ b/lib/pages/preview/view.dart @@ -135,115 +135,103 @@ class _ImagePreviewState extends State ), body: Stack( children: [ - DismissiblePage( - backgroundColor: Colors.transparent, - onDismissed: () { - Navigator.of(context).pop(); - }, - // Note that scrollable widget inside DismissiblePage might limit the functionality - // If scroll direction matches DismissiblePage direction - direction: DismissiblePageDismissDirection.down, - disabled: _dismissDisabled, - isFullScreen: true, - child: GestureDetector( - onLongPress: () => onOpenMenu(), - child: ExtendedImageGesturePageView.builder( - controller: ExtendedPageController( - initialPage: _previewController.initialPage.value, - pageSpacing: 0, - ), - onPageChanged: (int index) => - _previewController.onChange(index), - canScrollPage: (GestureDetails? gestureDetails) => - gestureDetails!.totalScale! <= 1.0, - itemCount: widget.imgList!.length, - itemBuilder: (BuildContext context, int index) { - return ExtendedImage.network( - widget.imgList![index], - fit: BoxFit.contain, - mode: ExtendedImageMode.gesture, - onDoubleTap: (ExtendedImageGestureState state) { - final Offset? pointerDownPosition = - state.pointerDownPosition; - final double? begin = state.gestureDetails!.totalScale; - double end; + GestureDetector( + onLongPress: () => onOpenMenu(), + child: ExtendedImageGesturePageView.builder( + controller: ExtendedPageController( + initialPage: _previewController.initialPage.value, + pageSpacing: 0, + ), + onPageChanged: (int index) => _previewController.onChange(index), + canScrollPage: (GestureDetails? gestureDetails) => + gestureDetails!.totalScale! <= 1.0, + itemCount: widget.imgList!.length, + itemBuilder: (BuildContext context, int index) { + return ExtendedImage.network( + widget.imgList![index], + fit: BoxFit.contain, + mode: ExtendedImageMode.gesture, + onDoubleTap: (ExtendedImageGestureState state) { + final Offset? pointerDownPosition = + state.pointerDownPosition; + final double? begin = state.gestureDetails!.totalScale; + double end; - //remove old - _doubleClickAnimation - ?.removeListener(_doubleClickAnimationListener); + //remove old + _doubleClickAnimation + ?.removeListener(_doubleClickAnimationListener); - //stop pre - _doubleClickAnimationController.stop(); + //stop pre + _doubleClickAnimationController.stop(); - //reset to use - _doubleClickAnimationController.reset(); + //reset to use + _doubleClickAnimationController.reset(); - if (begin == doubleTapScales[0]) { - setState(() { - _dismissDisabled = true; - }); - end = doubleTapScales[1]; - } else { - setState(() { - _dismissDisabled = false; - }); - end = doubleTapScales[0]; - } + if (begin == doubleTapScales[0]) { + setState(() { + _dismissDisabled = true; + }); + end = doubleTapScales[1]; + } else { + setState(() { + _dismissDisabled = false; + }); + end = doubleTapScales[0]; + } - _doubleClickAnimationListener = () { - state.handleDoubleTap( - scale: _doubleClickAnimation!.value, - doubleTapPosition: pointerDownPosition); - }; - _doubleClickAnimation = _doubleClickAnimationController - .drive(Tween(begin: begin, end: end)); + _doubleClickAnimationListener = () { + state.handleDoubleTap( + scale: _doubleClickAnimation!.value, + doubleTapPosition: pointerDownPosition); + }; + _doubleClickAnimation = _doubleClickAnimationController + .drive(Tween(begin: begin, end: end)); - _doubleClickAnimation! - .addListener(_doubleClickAnimationListener); + _doubleClickAnimation! + .addListener(_doubleClickAnimationListener); - _doubleClickAnimationController.forward(); - }, - // ignore: body_might_complete_normally_nullable - loadStateChanged: (ExtendedImageState state) { - if (state.extendedImageLoadState == LoadState.loading) { - final ImageChunkEvent? loadingProgress = - state.loadingProgress; - final double? progress = - loadingProgress?.expectedTotalBytes != null - ? loadingProgress!.cumulativeBytesLoaded / - loadingProgress.expectedTotalBytes! - : null; - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox( - width: 150.0, - child: LinearProgressIndicator( - value: progress, - color: Colors.white, - ), + _doubleClickAnimationController.forward(); + }, + // ignore: body_might_complete_normally_nullable + loadStateChanged: (ExtendedImageState state) { + if (state.extendedImageLoadState == LoadState.loading) { + final ImageChunkEvent? loadingProgress = + state.loadingProgress; + final double? progress = + loadingProgress?.expectedTotalBytes != null + ? loadingProgress!.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null; + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: 150.0, + child: LinearProgressIndicator( + value: progress, + color: Colors.white, ), - // const SizedBox(height: 10.0), - // Text('${((progress ?? 0.0) * 100).toInt()}%',), - ], - ), - ); - } - }, - initGestureConfigHandler: (ExtendedImageState state) { - return GestureConfig( - inPageView: true, - initialScale: 1.0, - maxScale: 5.0, - animationMaxScale: 6.0, - initialAlignment: InitialAlignment.center, + ), + // const SizedBox(height: 10.0), + // Text('${((progress ?? 0.0) * 100).toInt()}%',), + ], + ), ); - }, - ); - }, - ), + } + }, + initGestureConfigHandler: (ExtendedImageState state) { + return GestureConfig( + inPageView: true, + initialScale: 1.0, + maxScale: 5.0, + animationMaxScale: 6.0, + initialAlignment: InitialAlignment.center, + ); + }, + ); + }, ), ), Positioned( @@ -251,33 +239,49 @@ class _ImagePreviewState extends State right: 0, bottom: 0, child: Container( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).padding.bottom + 30), - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Colors.transparent, - Colors.black87, - ], - tileMode: TileMode.mirror, + padding: EdgeInsets.only( + left: 20, + right: 20, + bottom: MediaQuery.of(context).padding.bottom + 30), + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.transparent, + Colors.black87, + ], + tileMode: TileMode.mirror, + ), ), - ), - child: Obx( - () => Text.rich( - textAlign: TextAlign.center, - TextSpan( - style: const TextStyle(color: Colors.white, fontSize: 15), - children: [ - TextSpan( - text: _previewController.currentPage.toString()), - const TextSpan(text: ' / '), - TextSpan(text: widget.imgList!.length.toString()), - ]), - ), - ), - ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + widget.imgList!.length > 1 + ? Obx( + () => Text.rich( + textAlign: TextAlign.center, + TextSpan( + style: const TextStyle( + color: Colors.white, fontSize: 16), + children: [ + TextSpan( + text: _previewController.currentPage + .toString()), + const TextSpan(text: ' / '), + TextSpan( + text: + widget.imgList!.length.toString()), + ]), + ), + ) + : const SizedBox(), + IconButton( + onPressed: () => Get.back(), + icon: const Icon(Icons.close, color: Colors.white), + ), + ], + )), ), ], ), diff --git a/lib/pages/rank/controller.dart b/lib/pages/rank/controller.dart new file mode 100644 index 000000000..6fe3d4240 --- /dev/null +++ b/lib/pages/rank/controller.dart @@ -0,0 +1,54 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:hive/hive.dart'; +import 'package:pilipala/models/common/rank_type.dart'; +import 'package:pilipala/utils/storage.dart'; + +class RankController extends GetxController with GetTickerProviderStateMixin { + bool flag = false; + late RxList tabs = [].obs; + RxInt initialIndex = 0.obs; + late TabController tabController; + late List tabsCtrList; + late List tabsPageList; + Box setting = GStrorage.setting; + late final StreamController searchBarStream = + StreamController.broadcast(); + late bool enableGradientBg; + + @override + void onInit() { + super.onInit(); + enableGradientBg = + setting.get(SettingBoxKey.enableGradientBg, defaultValue: true); + // 进行tabs配置 + setTabConfig(); + } + + void onRefresh() { + int index = tabController.index; + var ctr = tabsCtrList[index]; + ctr().onRefresh(); + } + + void animateToTop() { + int index = tabController.index; + var ctr = tabsCtrList[index]; + ctr().animateToTop(); + } + + void setTabConfig() async { + tabs.value = tabsConfig; + initialIndex.value = 0; + tabsCtrList = tabs.map((e) => e['ctr']).toList(); + tabsPageList = tabs.map((e) => e['page']).toList(); + + tabController = TabController( + initialIndex: initialIndex.value, + length: tabs.length, + vsync: this, + ); + } +} diff --git a/lib/pages/rank/index.dart b/lib/pages/rank/index.dart new file mode 100644 index 000000000..eaac0a341 --- /dev/null +++ b/lib/pages/rank/index.dart @@ -0,0 +1,4 @@ +library rank; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/rank/view.dart b/lib/pages/rank/view.dart new file mode 100644 index 000000000..7b5b49065 --- /dev/null +++ b/lib/pages/rank/view.dart @@ -0,0 +1,149 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/utils/feed_back.dart'; +import './controller.dart'; + +class RankPage extends StatefulWidget { + const RankPage({Key? key}) : super(key: key); + + @override + State createState() => _RankPageState(); +} + +class _RankPageState extends State + with AutomaticKeepAliveClientMixin, TickerProviderStateMixin { + final RankController _rankController = Get.put(RankController()); + List videoList = []; + late Stream stream; + + @override + bool get wantKeepAlive => true; + + @override + void initState() { + super.initState(); + stream = _rankController.searchBarStream.stream; + } + + @override + Widget build(BuildContext context) { + super.build(context); + Brightness currentBrightness = MediaQuery.of(context).platformBrightness; + // 设置状态栏图标的亮度 + if (_rankController.enableGradientBg) { + SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( + statusBarIconBrightness: currentBrightness == Brightness.light + ? Brightness.dark + : Brightness.light, + )); + } + return Scaffold( + extendBody: true, + extendBodyBehindAppBar: false, + appBar: _rankController.enableGradientBg + ? null + : AppBar(toolbarHeight: 0, elevation: 0), + body: Stack( + children: [ + // gradient background + if (_rankController.enableGradientBg) ...[ + Align( + alignment: Alignment.topLeft, + child: Opacity( + opacity: 0.6, + child: Container( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Theme.of(context) + .colorScheme + .primary + .withOpacity(0.9), + Theme.of(context) + .colorScheme + .primary + .withOpacity(0.5), + Theme.of(context).colorScheme.surface + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + stops: const [0, 0.0034, 0.34]), + ), + ), + ), + ), + ], + Column( + children: [ + const CustomAppBar(), + if (_rankController.tabs.length > 1) ...[ + const SizedBox(height: 4), + SizedBox( + width: double.infinity, + height: 42, + child: Align( + alignment: Alignment.center, + child: TabBar( + controller: _rankController.tabController, + tabs: [ + for (var i in _rankController.tabs) + Tab(text: i['label']) + ], + isScrollable: true, + dividerColor: Colors.transparent, + enableFeedback: true, + splashBorderRadius: BorderRadius.circular(10), + tabAlignment: TabAlignment.center, + onTap: (value) { + feedBack(); + if (_rankController.initialIndex.value == value) { + _rankController.tabsCtrList[value]().animateToTop(); + } + _rankController.initialIndex.value = value; + }, + ), + ), + ), + ] else ...[ + const SizedBox(height: 6), + ], + Expanded( + child: TabBarView( + controller: _rankController.tabController, + children: _rankController.tabsPageList, + ), + ), + ], + ), + ], + ), + ); + } +} + +class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { + final double height; + + const CustomAppBar({ + super.key, + this.height = kToolbarHeight, + }); + + @override + Size get preferredSize => Size.fromHeight(height); + + @override + Widget build(BuildContext context) { + final double top = MediaQuery.of(context).padding.top; + return Container( + width: MediaQuery.of(context).size.width, + height: top, + color: Colors.transparent, + ); + } +} diff --git a/lib/pages/rank/zone/controller.dart b/lib/pages/rank/zone/controller.dart new file mode 100644 index 000000000..f9f4dc6e0 --- /dev/null +++ b/lib/pages/rank/zone/controller.dart @@ -0,0 +1,53 @@ +import 'package:get/get.dart'; +import 'package:flutter/material.dart'; +import 'package:pilipala/http/video.dart'; +import 'package:pilipala/models/model_hot_video_item.dart'; + +class ZoneController extends GetxController { + final ScrollController scrollController = ScrollController(); + RxList videoList = [].obs; + bool isLoadingMore = false; + bool flag = false; + OverlayEntry? popupDialog; + int zoneID = 0; + + // 获取推荐 + Future queryRankFeed(type, rid) async { + zoneID = rid; + var res = await VideoHttp.getRankVideoList(zoneID); + if (res['status']) { + if (type == 'init') { + videoList.value = res['data']; + } else if (type == 'onRefresh') { + videoList.clear(); + videoList.addAll(res['data']); + } else if (type == 'onLoad') { + videoList.clear(); + videoList.addAll(res['data']); + } + } + isLoadingMore = false; + return res; + } + + // 下拉刷新 + Future onRefresh() async { + queryRankFeed('onRefresh', zoneID); + } + + // 上拉加载 + Future onLoad() async { + queryRankFeed('onLoad', zoneID); + } + + // 返回顶部并刷新 + void animateToTop() async { + if (scrollController.offset >= + MediaQuery.of(Get.context!).size.height * 5) { + scrollController.jumpTo(0); + } else { + await scrollController.animateTo(0, + duration: const Duration(milliseconds: 500), curve: Curves.easeInOut); + } + } +} diff --git a/lib/pages/rank/zone/index.dart b/lib/pages/rank/zone/index.dart new file mode 100644 index 000000000..8f5357361 --- /dev/null +++ b/lib/pages/rank/zone/index.dart @@ -0,0 +1,4 @@ +library rank.zone; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/rank/zone/view.dart b/lib/pages/rank/zone/view.dart new file mode 100644 index 000000000..fbf8a524e --- /dev/null +++ b/lib/pages/rank/zone/view.dart @@ -0,0 +1,154 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/common/constants.dart'; +import 'package:pilipala/common/widgets/animated_dialog.dart'; +import 'package:pilipala/common/widgets/overlay_pop.dart'; +import 'package:pilipala/common/skeleton/video_card_h.dart'; +import 'package:pilipala/common/widgets/http_error.dart'; +import 'package:pilipala/common/widgets/video_card_h.dart'; +import 'package:pilipala/pages/home/index.dart'; +import 'package:pilipala/pages/main/index.dart'; +import 'package:pilipala/pages/rank/zone/index.dart'; + +class ZonePage extends StatefulWidget { + const ZonePage({Key? key, required this.rid}) : super(key: key); + + final int rid; + + @override + State createState() => _ZonePageState(); +} + +class _ZonePageState extends State + with AutomaticKeepAliveClientMixin { + late ZoneController _zoneController; + List videoList = []; + Future? _futureBuilderFuture; + late ScrollController scrollController; + + @override + bool get wantKeepAlive => true; + + @override + void initState() { + super.initState(); + _zoneController = Get.put(ZoneController(), tag: widget.rid.toString()); + _futureBuilderFuture = _zoneController.queryRankFeed('init', widget.rid); + scrollController = _zoneController.scrollController; + StreamController mainStream = + Get.find().bottomBarStream; + StreamController searchBarStream = + Get.find().searchBarStream; + scrollController.addListener( + () { + if (scrollController.position.pixels >= + scrollController.position.maxScrollExtent - 200) { + if (!_zoneController.isLoadingMore) { + _zoneController.isLoadingMore = true; + _zoneController.onLoad(); + } + } + + final ScrollDirection direction = + scrollController.position.userScrollDirection; + if (direction == ScrollDirection.forward) { + mainStream.add(true); + searchBarStream.add(true); + } else if (direction == ScrollDirection.reverse) { + mainStream.add(false); + searchBarStream.add(false); + } + }, + ); + } + + @override + void dispose() { + scrollController.removeListener(() {}); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return RefreshIndicator( + onRefresh: () async { + return await _zoneController.onRefresh(); + }, + child: CustomScrollView( + controller: _zoneController.scrollController, + slivers: [ + SliverPadding( + // 单列布局 EdgeInsets.zero + padding: + const EdgeInsets.fromLTRB(0, StyleString.safeSpace - 5, 0, 0), + sliver: FutureBuilder( + future: _futureBuilderFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + Map data = snapshot.data as Map; + if (data['status']) { + return Obx( + () => SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + return VideoCardH( + videoItem: _zoneController.videoList[index], + showPubdate: true, + longPress: () { + _zoneController.popupDialog = _createPopupDialog( + _zoneController.videoList[index]); + Overlay.of(context) + .insert(_zoneController.popupDialog!); + }, + longPressEnd: () { + _zoneController.popupDialog?.remove(); + }, + ); + }, childCount: _zoneController.videoList.length), + ), + ); + } else { + return HttpError( + errMsg: data['msg'], + fn: () { + setState(() { + _futureBuilderFuture = + _zoneController.queryRankFeed('init', widget.rid); + }); + }, + ); + } + } else { + // 骨架屏 + return SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + return const VideoCardHSkeleton(); + }, childCount: 10), + ); + } + }, + ), + ), + SliverToBoxAdapter( + child: SizedBox( + height: MediaQuery.of(context).padding.bottom + 10, + ), + ) + ], + ), + ); + } + + OverlayEntry _createPopupDialog(videoItem) { + return OverlayEntry( + builder: (context) => AnimatedDialog( + closeFn: _zoneController.popupDialog?.remove, + child: OverlayPop( + videoItem: videoItem, closeFn: _zoneController.popupDialog?.remove), + ), + ); + } +} diff --git a/lib/pages/search/controller.dart b/lib/pages/search/controller.dart index 5ec1710a2..1853c238c 100644 --- a/lib/pages/search/controller.dart +++ b/lib/pages/search/controller.dart @@ -15,7 +15,7 @@ class SSearchController extends GetxController { Box histiryWord = GStrorage.historyword; List historyCacheList = []; RxList historyList = [].obs; - RxList searchSuggestList = [SearchSuggestItem()].obs; + RxList searchSuggestList = [].obs; final _debouncer = Debouncer(delay: const Duration(milliseconds: 200)); // 设置延迟时间 String hintText = '搜索'; @@ -115,7 +115,7 @@ class SSearchController extends GetxController { onLongSelect(word) { int index = historyList.indexOf(word); - historyList.value = historyList.removeAt(index); + historyList.removeAt(index); historyList.refresh(); histiryWord.put('cacheList', historyList); } diff --git a/lib/pages/setting/controller.dart b/lib/pages/setting/controller.dart index 2e6680e52..1fbd7efbe 100644 --- a/lib/pages/setting/controller.dart +++ b/lib/pages/setting/controller.dart @@ -8,6 +8,7 @@ import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/login.dart'; import 'package:pilipala/utils/storage.dart'; import '../../models/common/dynamic_badge_mode.dart'; +import '../../models/common/nav_bar_config.dart'; import '../main/index.dart'; import 'widgets/select_dialog.dart'; @@ -23,6 +24,7 @@ class SettingController extends GetxController { Rx themeType = ThemeType.system.obs; var userInfo; Rx dynamicBadgeType = DynamicBadgeMode.number.obs; + RxInt defaultHomePage = 0.obs; @override void onInit() { @@ -40,6 +42,8 @@ class SettingController extends GetxController { dynamicBadgeType.value = DynamicBadgeMode.values[setting.get( SettingBoxKey.dynamicBadgeMode, defaultValue: DynamicBadgeMode.number.code)]; + defaultHomePage.value = + setting.get(SettingBoxKey.defaultHomePage, defaultValue: 0); } loginOut() async { @@ -110,4 +114,24 @@ class SettingController extends GetxController { SmartDialog.showToast('设置成功'); } } + + // 设置默认启动页 + seteDefaultHomePage(BuildContext context) async { + int? result = await showDialog( + context: context, + builder: (context) { + return SelectDialog( + title: '首页启动页', + value: defaultHomePage.value, + values: defaultNavigationBars.map((e) { + return {'title': e['label'], 'value': e['id']}; + }).toList()); + }, + ); + if (result != null) { + defaultHomePage.value = result; + setting.put(SettingBoxKey.defaultHomePage, result); + SmartDialog.showToast('设置成功,重启生效'); + } + } } diff --git a/lib/pages/setting/extra_setting.dart b/lib/pages/setting/extra_setting.dart index 1580ad8b0..aaaa8b849 100644 --- a/lib/pages/setting/extra_setting.dart +++ b/lib/pages/setting/extra_setting.dart @@ -1,11 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; import 'package:hive/hive.dart'; import 'package:pilipala/models/common/dynamics_type.dart'; import 'package:pilipala/models/common/reply_sort_type.dart'; import 'package:pilipala/pages/setting/widgets/select_dialog.dart'; import 'package:pilipala/utils/storage.dart'; +import '../home/index.dart'; import 'widgets/switch_item.dart'; class ExtraSetting extends StatefulWidget { @@ -138,18 +140,20 @@ class _ExtraSettingState extends State { ), body: ListView( children: [ - SetSwitchItem( + const SetSwitchItem( title: '大家都在搜', subTitle: '是否展示「大家都在搜」', setKey: SettingBoxKey.enableHotKey, defaultVal: true, - callFn: (val) => {SmartDialog.showToast('下次启动时生效')}, ), - const SetSwitchItem( + SetSwitchItem( title: '搜索默认词', subTitle: '是否展示搜索框默认词', setKey: SettingBoxKey.enableSearchWord, defaultVal: true, + callFn: (val) { + Get.find().defaultSearch.value = ''; + }, ), const SetSwitchItem( title: '快速收藏', @@ -169,6 +173,12 @@ class _ExtraSettingState extends State { setKey: SettingBoxKey.enableAi, defaultVal: true, ), + const SetSwitchItem( + title: '相关视频推荐', + subTitle: '视频详情页推荐相关视频', + setKey: SettingBoxKey.enableRelatedVideo, + defaultVal: true, + ), ListTile( dense: false, title: Text('评论展示', style: titleStyle), diff --git a/lib/pages/setting/pages/home_tabbar_set.dart b/lib/pages/setting/pages/home_tabbar_set.dart index 4cb3944c3..63e87d027 100644 --- a/lib/pages/setting/pages/home_tabbar_set.dart +++ b/lib/pages/setting/pages/home_tabbar_set.dart @@ -40,10 +40,6 @@ class _TabbarSetPageState extends State { .where((i) => tabbarSort.contains((i['type'] as TabType).id)) .map((i) => (i['type'] as TabType).id) .toList(); - if (sortedTabbar.isEmpty) { - SmartDialog.showToast('请至少设置一项!'); - return; - } settingStorage.put(SettingBoxKey.tabbarSort, sortedTabbar); SmartDialog.showToast('保存成功,下次启动时生效'); } diff --git a/lib/pages/setting/pages/play_gesture_set.dart b/lib/pages/setting/pages/play_gesture_set.dart new file mode 100644 index 000000000..f688c43c2 --- /dev/null +++ b/lib/pages/setting/pages/play_gesture_set.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:hive/hive.dart'; +import 'package:pilipala/utils/global_data.dart'; + +import '../../../models/common/gesture_mode.dart'; +import '../../../utils/storage.dart'; +import '../widgets/select_dialog.dart'; +import '../widgets/switch_item.dart'; + +class PlayGesturePage extends StatefulWidget { + const PlayGesturePage({super.key}); + + @override + State createState() => _PlayGesturePageState(); +} + +class _PlayGesturePageState extends State { + Box setting = GStrorage.setting; + late int fullScreenGestureMode; + + @override + void initState() { + super.initState(); + fullScreenGestureMode = setting.get(SettingBoxKey.fullScreenGestureMode, + defaultValue: FullScreenGestureMode.values.last.index); + } + + @override + Widget build(BuildContext context) { + TextStyle titleStyle = Theme.of(context).textTheme.titleMedium!; + TextStyle subTitleStyle = Theme.of(context) + .textTheme + .labelMedium! + .copyWith(color: Theme.of(context).colorScheme.outline); + return Scaffold( + appBar: AppBar( + centerTitle: false, + titleSpacing: 0, + title: Text( + '手势设置', + style: Theme.of(context).textTheme.titleMedium, + ), + ), + body: ListView( + children: [ + ListTile( + dense: false, + title: Text('全屏手势', style: titleStyle), + subtitle: Text( + '通过手势快速进入全屏', + style: subTitleStyle, + ), + onTap: () async { + String? result = await showDialog( + context: context, + builder: (context) { + return SelectDialog( + title: '全屏手势', + value: FullScreenGestureMode + .values[fullScreenGestureMode].values, + values: FullScreenGestureMode.values.map((e) { + return {'title': e.labels, 'value': e.values}; + }).toList()); + }, + ); + if (result != null) { + GlobalData().fullScreenGestureMode = FullScreenGestureMode + .values + .firstWhere((element) => element.values == result); + fullScreenGestureMode = + GlobalData().fullScreenGestureMode.index; + setting.put( + SettingBoxKey.fullScreenGestureMode, fullScreenGestureMode); + setState(() {}); + } + }, + ), + const SetSwitchItem( + title: '双击快退/快进', + subTitle: '左侧双击快退,右侧双击快进', + setKey: SettingBoxKey.enableQuickDouble, + defaultVal: true, + ), + ], + ), + ); + } +} diff --git a/lib/pages/setting/pages/play_speed_set.dart b/lib/pages/setting/pages/play_speed_set.dart index ceff07edb..eb81f5861 100644 --- a/lib/pages/setting/pages/play_speed_set.dart +++ b/lib/pages/setting/pages/play_speed_set.dart @@ -17,6 +17,7 @@ class _PlaySpeedPageState extends State { Box videoStorage = GStrorage.video; Box settingStorage = GStrorage.setting; late double playSpeedDefault; + late List playSpeedSystem; late double longPressSpeedDefault; late List customSpeedsList; late bool enableAutoLongPressSpeed; @@ -53,6 +54,9 @@ class _PlaySpeedPageState extends State { @override void initState() { super.initState(); + // 系统预设倍速 + playSpeedSystem = + videoStorage.get(VideoBoxKey.playSpeedSystem, defaultValue: playSpeed); // 默认倍速 playSpeedDefault = videoStorage.get(VideoBoxKey.playSpeedDefault, defaultValue: 1.0); @@ -64,6 +68,7 @@ class _PlaySpeedPageState extends State { videoStorage.get(VideoBoxKey.customSpeedsList, defaultValue: []); enableAutoLongPressSpeed = settingStorage .get(SettingBoxKey.enableAutoLongPressSpeed, defaultValue: false); + // 开启动态长按倍速时不展示 if (enableAutoLongPressSpeed) { Map newItem = sheetMenu[1]; newItem['show'] = false; @@ -123,7 +128,7 @@ class _PlaySpeedPageState extends State { } // 设定倍速弹窗 - void showBottomSheet(type, i) { + void showBottomSheet(String type, int i) { showModalBottomSheet( context: context, isScrollControlled: true, @@ -159,18 +164,11 @@ class _PlaySpeedPageState extends State { } // - void menuAction(type, index, id) async { + void menuAction(type, int index, id) async { double chooseSpeed = 1.0; - if (type == 'system' && id == -1) { - SmartDialog.showToast('系统预设倍速不支持删除'); - return; - } // 获取当前选中的倍速值 - if (type == 'system') { - chooseSpeed = PlaySpeed.values[index].value; - } else { - chooseSpeed = customSpeedsList[index]; - } + chooseSpeed = + type == 'system' ? playSpeedSystem[index] : customSpeedsList[index]; // 设置 if (id == 1) { // 设置默认倍速 @@ -182,17 +180,22 @@ class _PlaySpeedPageState extends State { videoStorage.put( VideoBoxKey.longPressSpeedDefault, longPressSpeedDefault); } else if (id == -1) { - if (customSpeedsList[index] == playSpeedDefault) { - playSpeedDefault = 1.0; - videoStorage.put(VideoBoxKey.playSpeedDefault, playSpeedDefault); + late List speedsList = + type == 'system' ? playSpeedSystem : customSpeedsList; + if (speedsList[index] == playSpeedDefault) { + SmartDialog.showToast('默认倍速不可删除'); } - if (customSpeedsList[index] == longPressSpeedDefault) { + if (speedsList[index] == longPressSpeedDefault) { longPressSpeedDefault = 2.0; videoStorage.put( VideoBoxKey.longPressSpeedDefault, longPressSpeedDefault); } - customSpeedsList.removeAt(index); - await videoStorage.put(VideoBoxKey.customSpeedsList, customSpeedsList); + speedsList.removeAt(index); + await videoStorage.put( + type == 'system' + ? VideoBoxKey.playSpeedSystem + : VideoBoxKey.customSpeedsList, + speedsList); } setState(() {}); SmartDialog.showToast('操作成功'); @@ -249,38 +252,40 @@ class _PlaySpeedPageState extends State { subtitle: Text(longPressSpeedDefault.toString()), ) : const SizedBox(), - Padding( - padding: const EdgeInsets.only( - left: 14, - right: 14, - bottom: 10, - top: 20, - ), - child: Text( - '系统预设倍速', - style: Theme.of(context).textTheme.titleMedium, - ), - ), - Padding( - padding: const EdgeInsets.only( - left: 18, - right: 18, - bottom: 30, - ), - child: Wrap( - alignment: WrapAlignment.start, - spacing: 8, - runSpacing: 2, - children: [ - for (var i in PlaySpeed.values) ...[ - FilledButton.tonal( - onPressed: () => showBottomSheet('system', i.index), - child: Text(i.description), - ), - ] - ], + if (playSpeedSystem.isNotEmpty) ...[ + Padding( + padding: const EdgeInsets.only( + left: 14, + right: 14, + bottom: 10, + top: 20, + ), + child: Text( + '系统预设倍速', + style: Theme.of(context).textTheme.titleMedium, + ), ), - ), + Padding( + padding: const EdgeInsets.only( + left: 18, + right: 18, + bottom: 30, + ), + child: Wrap( + alignment: WrapAlignment.start, + spacing: 8, + runSpacing: 2, + children: [ + for (int i = 0; i < playSpeedSystem.length; i++) ...[ + FilledButton.tonal( + onPressed: () => showBottomSheet('system', i), + child: Text(playSpeedSystem[i].toString()), + ), + ] + ], + ), + ) + ], Padding( padding: const EdgeInsets.only( left: 14, diff --git a/lib/pages/setting/play_setting.dart b/lib/pages/setting/play_setting.dart index bfd5db5fc..07d736e32 100644 --- a/lib/pages/setting/play_setting.dart +++ b/lib/pages/setting/play_setting.dart @@ -7,8 +7,10 @@ import 'package:pilipala/models/video/play/quality.dart'; import 'package:pilipala/pages/setting/widgets/select_dialog.dart'; import 'package:pilipala/plugin/pl_player/index.dart'; import 'package:pilipala/services/service_locator.dart'; +import 'package:pilipala/utils/global_data.dart'; import 'package:pilipala/utils/storage.dart'; +import '../../models/live/quality.dart'; import 'widgets/switch_item.dart'; class PlaySetting extends StatefulWidget { @@ -21,6 +23,7 @@ class PlaySetting extends StatefulWidget { class _PlaySettingState extends State { Box setting = GStrorage.setting; late dynamic defaultVideoQa; + late dynamic defaultLiveQa; late dynamic defaultAudioQa; late dynamic defaultDecode; late int defaultFullScreenMode; @@ -31,6 +34,8 @@ class _PlaySettingState extends State { super.initState(); defaultVideoQa = setting.get(SettingBoxKey.defaultVideoQa, defaultValue: VideoQuality.values.last.code); + defaultLiveQa = setting.get(SettingBoxKey.defaultLiveQa, + defaultValue: LiveQuality.values.last.code); defaultAudioQa = setting.get(SettingBoxKey.defaultAudioQa, defaultValue: AudioQuality.values.last.code); defaultDecode = setting.get(SettingBoxKey.defaultDecode, @@ -73,6 +78,12 @@ class _PlaySettingState extends State { title: Text('倍速设置', style: titleStyle), subtitle: Text('设置视频播放速度', style: subTitleStyle), ), + ListTile( + dense: false, + onTap: () => Get.toNamed('/playerGestureSet'), + title: Text('手势设置', style: titleStyle), + subtitle: Text('设置播放器手势', style: subTitleStyle), + ), const SetSwitchItem( title: '开启1080P', subTitle: '免登录查看1080P视频', @@ -134,23 +145,25 @@ class _PlaySettingState extends State { setKey: SettingBoxKey.enableAutoBrightness, defaultVal: false, ), - const SetSwitchItem( - title: '双击快退/快进', - subTitle: '左侧双击快退,右侧双击快进', - setKey: SettingBoxKey.enableQuickDouble, - defaultVal: true, - ), const SetSwitchItem( title: '弹幕开关', subTitle: '展示弹幕', setKey: SettingBoxKey.enableShowDanmaku, defaultVal: false, ), + SetSwitchItem( + title: '控制栏动画', + subTitle: '播放器控制栏显示动画效果', + setKey: SettingBoxKey.enablePlayerControlAnimation, + defaultVal: true, + callFn: (bool val) { + GlobalData().enablePlayerControlAnimation = val; + }), ListTile( dense: false, - title: Text('默认画质', style: titleStyle), + title: Text('默认视频画质', style: titleStyle), subtitle: Text( - '当前画质${VideoQualityCode.fromCode(defaultVideoQa)!.description!}', + '当前默认画质${VideoQualityCode.fromCode(defaultVideoQa)!.description!}', style: subTitleStyle, ), onTap: () async { @@ -158,7 +171,7 @@ class _PlaySettingState extends State { context: context, builder: (context) { return SelectDialog( - title: '默认画质', + title: '默认视频画质', value: defaultVideoQa, values: VideoQuality.values.reversed.map((e) { return {'title': e.description, 'value': e.code}; @@ -172,6 +185,32 @@ class _PlaySettingState extends State { } }, ), + ListTile( + dense: false, + title: Text('默认直播画质', style: titleStyle), + subtitle: Text( + '当前默认画质${LiveQualityCode.fromCode(defaultLiveQa)!.description!}', + style: subTitleStyle, + ), + onTap: () async { + int? result = await showDialog( + context: context, + builder: (context) { + return SelectDialog( + title: '默认直播画质', + value: defaultLiveQa, + values: LiveQuality.values.reversed.map((e) { + return {'title': e.description, 'value': e.code}; + }).toList()); + }, + ); + if (result != null) { + defaultLiveQa = result; + setting.put(SettingBoxKey.defaultLiveQa, result); + setState(() {}); + } + }, + ), ListTile( dense: false, title: Text('默认音质', style: titleStyle), diff --git a/lib/pages/setting/style_setting.dart b/lib/pages/setting/style_setting.dart index c9bffa691..30b9a30fa 100644 --- a/lib/pages/setting/style_setting.dart +++ b/lib/pages/setting/style_setting.dart @@ -8,9 +8,11 @@ import 'package:pilipala/models/common/theme_type.dart'; import 'package:pilipala/pages/setting/pages/color_select.dart'; import 'package:pilipala/pages/setting/widgets/select_dialog.dart'; import 'package:pilipala/pages/setting/widgets/slide_dialog.dart'; +import 'package:pilipala/utils/global_data.dart'; import 'package:pilipala/utils/storage.dart'; import '../../models/common/dynamic_badge_mode.dart'; +import '../../models/common/nav_bar_config.dart'; import 'controller.dart'; import 'widgets/switch_item.dart'; @@ -28,7 +30,6 @@ class _StyleSettingState extends State { Box setting = GStrorage.setting; late int picQuality; - late double toastOpacity; late ThemeType _tempThemeValue; late dynamic defaultCustomRows; @@ -36,7 +37,6 @@ class _StyleSettingState extends State { void initState() { super.initState(); picQuality = setting.get(SettingBoxKey.defaultPicQa, defaultValue: 10); - toastOpacity = setting.get(SettingBoxKey.defaultToastOp, defaultValue: 1.0); _tempThemeValue = settingController.themeType.value; defaultCustomRows = setting.get(SettingBoxKey.customRows, defaultValue: 2); } @@ -176,6 +176,8 @@ class _StyleSettingState extends State { SettingBoxKey.defaultPicQa, picQuality); Get.back(); settingController.picQuality.value = picQuality; + GlobalData().imgQuality = picQuality; + SmartDialog.showToast('设置成功'); }, child: const Text('确定'), ) @@ -264,6 +266,14 @@ class _StyleSettingState extends State { '当前主题:${colorSelectController.type.value == 0 ? '动态取色' : '指定颜色'}', style: subTitleStyle)), ), + ListTile( + dense: false, + onTap: () => settingController.seteDefaultHomePage(context), + title: Text('默认启动页', style: titleStyle), + subtitle: Obx(() => Text( + '当前启动页:${defaultNavigationBars.firstWhere((e) => e['id'] == settingController.defaultHomePage.value)['label']}', + style: subTitleStyle)), + ), ListTile( dense: false, onTap: () => Get.toNamed('/fontSizeSetting'), diff --git a/lib/pages/subscription/view.dart b/lib/pages/subscription/view.dart index b2a4965bb..1eee4a4f7 100644 --- a/lib/pages/subscription/view.dart +++ b/lib/pages/subscription/view.dart @@ -2,7 +2,6 @@ import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/widgets/http_error.dart'; -import 'package:pilipala/pages/fav/widgets/item.dart'; import 'controller.dart'; import 'widgets/item.dart'; diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart index 7465c6f22..5c4ac14b5 100644 --- a/lib/pages/video/detail/controller.dart +++ b/lib/pages/video/detail/controller.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:hive/hive.dart'; +import 'package:ns_danmaku/ns_danmaku.dart'; import 'package:pilipala/http/constants.dart'; import 'package:pilipala/http/video.dart'; import 'package:pilipala/models/common/reply_type.dart'; @@ -19,6 +20,8 @@ import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/video_utils.dart'; import 'package:screen_brightness/screen_brightness.dart'; +import '../../../models/video/subTitile/content.dart'; +import '../../../http/danmaku.dart'; import '../../../utils/id_utils.dart'; import 'widgets/header_control.dart'; @@ -91,6 +94,10 @@ class VideoDetailController extends GetxController late int cacheAudioQa; PersistentBottomSheetController? replyReplyBottomSheetCtr; + RxList subtitleContents = + [].obs; + late bool enableRelatedVideo; + List subtitles = []; @override void onInit() { @@ -113,7 +120,8 @@ class VideoDetailController extends GetxController autoPlay.value = setting.get(SettingBoxKey.autoPlayEnable, defaultValue: true); enableHA.value = setting.get(SettingBoxKey.enableHA, defaultValue: true); - + enableRelatedVideo = + setting.get(SettingBoxKey.enableRelatedVideo, defaultValue: true); if (userInfo == null || localCache.get(LocalCacheKey.historyPause) == true) { enableHeart = false; @@ -128,6 +136,8 @@ class VideoDetailController extends GetxController controller: plPlayerController, videoDetailCtr: this, floating: floating, + bvid: bvid, + videoType: videoType, ); // CDN优化 enableCDN = setting.get(SettingBoxKey.enableCDN, defaultValue: true); @@ -139,6 +149,7 @@ class VideoDetailController extends GetxController cacheAudioQa = setting.get(SettingBoxKey.defaultAudioQa, defaultValue: AudioQuality.hiRes.code); oid.value = IdUtils.bv2av(Get.parameters['bvid']!); + getSubtitle(); } showReplyReplyPanel() { @@ -245,6 +256,8 @@ class VideoDetailController extends GetxController /// 开启自动全屏时,在player初始化完成后立即传入headerControl plPlayerController.headerControl = headerControl; + + plPlayerController.subtitles.value = subtitles; } // 视频链接 @@ -381,4 +394,125 @@ class VideoDetailController extends GetxController ? replyReplyBottomSheetCtr!.close() : print('replyReplyBottomSheetCtr is null'); } + + // 获取字幕配置 + Future getSubtitle() async { + var result = await VideoHttp.getSubtitle(bvid: bvid, cid: cid.value); + if (result['status']) { + if (result['data'].subtitles.isNotEmpty) { + subtitles = result['data'].subtitles; + if (subtitles.isNotEmpty) { + for (var i in subtitles) { + final Map res = await VideoHttp.getSubtitleContent( + i.subtitleUrl, + ); + i.content = res['content']; + i.body = res['body']; + } + } + } + return result['data']; + } + } + + // 获取字幕内容 + // Future getSubtitleContent(String url) async { + // var res = await Request().get('https:$url'); + // subtitleContents.value = res.data['body'].map((e) { + // return SubTitileContentModel.fromJson(e); + // }).toList(); + // setSubtitleContent(); + // } + + setSubtitleContent() { + plPlayerController.subtitleContent.value = ''; + plPlayerController.subtitles.value = subtitles; + } + + clearSubtitleContent() { + plPlayerController.subtitleContent.value = ''; + plPlayerController.subtitles.value = []; + } + + /// 发送弹幕 + void showShootDanmakuSheet() { + final TextEditingController textController = TextEditingController(); + bool isSending = false; // 追踪是否正在发送 + showDialog( + context: Get.context!, + builder: (BuildContext context) { + // TODO: 支持更多类型和颜色的弹幕 + return AlertDialog( + title: const Text('发送弹幕'), + content: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return TextField( + controller: textController, + ); + }), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: Text( + '取消', + style: TextStyle(color: Theme.of(context).colorScheme.outline), + ), + ), + StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return TextButton( + onPressed: isSending + ? null + : () async { + final String msg = textController.text; + if (msg.isEmpty) { + SmartDialog.showToast('弹幕内容不能为空'); + return; + } else if (msg.length > 100) { + SmartDialog.showToast('弹幕内容不能超过100个字符'); + return; + } + setState(() { + isSending = true; // 开始发送,更新状态 + }); + //修改按钮文字 + // SmartDialog.showToast('弹幕发送中,\n$msg'); + final dynamic res = await DanmakaHttp.shootDanmaku( + oid: cid.value, + msg: textController.text, + bvid: bvid, + progress: + plPlayerController.position.value.inMilliseconds, + type: 1, + ); + setState(() { + isSending = false; // 发送结束,更新状态 + }); + if (res['status']) { + SmartDialog.showToast('发送成功'); + // 发送成功,自动预览该弹幕,避免重新请求 + // TODO: 暂停状态下预览弹幕仍会移动与计时,可考虑添加到dmSegList或其他方式实现 + plPlayerController.danmakuController?.addItems([ + DanmakuItem( + msg, + color: Colors.white, + time: plPlayerController + .position.value.inMilliseconds, + type: DanmakuItemType.scroll, + isSend: true, + ) + ]); + Get.back(); + } else { + SmartDialog.showToast('发送失败,错误信息为${res['msg']}'); + } + }, + child: Text(isSending ? '发送中...' : '发送'), + ); + }) + ], + ); + }, + ); + } } diff --git a/lib/pages/video/detail/introduction/controller.dart b/lib/pages/video/detail/introduction/controller.dart index 26cbd2e0c..8114bdaf4 100644 --- a/lib/pages/video/detail/introduction/controller.dart +++ b/lib/pages/video/detail/introduction/controller.dart @@ -22,15 +22,9 @@ import '../related/index.dart'; import 'widgets/group_panel.dart'; class VideoIntroController extends GetxController { + VideoIntroController({required this.bvid}); // 视频bvid - String bvid = Get.parameters['bvid']!; - - // 是否预渲染 骨架屏 - bool preRender = false; - - // 视频详情 上个页面传入 - Map? videoItem = {}; - + String bvid; // 请求状态 RxBool isLoading = false.obs; @@ -73,26 +67,6 @@ class VideoIntroController extends GetxController { try { heroTag = Get.arguments['heroTag']; } catch (_) {} - if (Get.arguments.isNotEmpty) { - if (Get.arguments.containsKey('videoItem')) { - preRender = true; - var args = Get.arguments['videoItem']; - var keys = Get.arguments.keys.toList(); - videoItem!['pic'] = args.pic; - if (args.title is String) { - videoItem!['title'] = args.title; - } else { - String str = ''; - for (Map map in args.title) { - str += map['text']; - } - videoItem!['title'] = str; - } - videoItem!['stat'] = keys.contains('stat') && args.stat; - videoItem!['pubdate'] = keys.contains('pubdate') && args.pubdate; - videoItem!['owner'] = keys.contains('owner') && args.owner; - } - } userLogin = userInfo != null; lastPlayCid.value = int.parse(Get.parameters['cid']!); isShowOnlineTotal = @@ -111,10 +85,9 @@ class VideoIntroController extends GetxController { if (videoDetail.value.pages!.isNotEmpty && lastPlayCid.value == 0) { lastPlayCid.value = videoDetail.value.pages!.first.cid!; } - // Get.find(tag: heroTag).tabs.value = [ - // '简介', - // '评论 ${result['data']!.stat!.reply}' - // ]; + final VideoDetailController videoDetailCtr = + Get.find(tag: heroTag); + videoDetailCtr.tabs.value = ['简介', '评论 ${result['data']?.stat?.reply}']; // 获取到粉丝数再返回 await queryUserStat(); } diff --git a/lib/pages/video/detail/introduction/view.dart b/lib/pages/video/detail/introduction/view.dart index 0a3ac934b..a990aab83 100644 --- a/lib/pages/video/detail/introduction/view.dart +++ b/lib/pages/video/detail/introduction/view.dart @@ -1,3 +1,4 @@ +import 'package:expandable/expandable.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; @@ -15,16 +16,18 @@ import 'package:pilipala/pages/video/detail/widgets/ai_detail.dart'; import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/utils.dart'; - +import '../../../../http/user.dart'; import 'widgets/action_item.dart'; -import 'widgets/action_row_item.dart'; import 'widgets/fav_panel.dart'; import 'widgets/intro_detail.dart'; import 'widgets/page.dart'; import 'widgets/season.dart'; class VideoIntroPanel extends StatefulWidget { - const VideoIntroPanel({super.key}); + final String bvid; + final String? cid; + + const VideoIntroPanel({super.key, required this.bvid, this.cid}); @override State createState() => _VideoIntroPanelState(); @@ -47,7 +50,8 @@ class _VideoIntroPanelState extends State /// fix 全屏时参数丢失 heroTag = Get.arguments['heroTag']; - videoIntroController = Get.put(VideoIntroController(), tag: heroTag); + videoIntroController = + Get.put(VideoIntroController(bvid: widget.bvid), tag: heroTag); _futureBuilderFuture = videoIntroController.queryVideoIntro(); videoIntroController.videoDetail.listen((value) { videoDetail = value; @@ -74,9 +78,9 @@ class _VideoIntroPanelState extends State // 请求成功 return Obx( () => VideoInfo( - loadingStatus: false, videoDetail: videoIntroController.videoDetail.value, heroTag: heroTag, + bvid: widget.bvid, ), ); } else { @@ -91,10 +95,13 @@ class _VideoIntroPanelState extends State ); } } else { - return VideoInfo( - loadingStatus: true, - videoDetail: videoDetail, - heroTag: heroTag, + return const SliverToBoxAdapter( + child: SizedBox( + height: 100, + child: Center( + child: CircularProgressIndicator(), + ), + ), ); } }, @@ -103,31 +110,28 @@ class _VideoIntroPanelState extends State } class VideoInfo extends StatefulWidget { - final bool loadingStatus; final VideoDetailData? videoDetail; final String? heroTag; + final String bvid; - const VideoInfo( - {Key? key, this.loadingStatus = false, this.videoDetail, this.heroTag}) - : super(key: key); + const VideoInfo({ + Key? key, + this.videoDetail, + this.heroTag, + required this.bvid, + }) : super(key: key); @override State createState() => _VideoInfoState(); } class _VideoInfoState extends State with TickerProviderStateMixin { - // final String heroTag = Get.arguments['heroTag']; late String heroTag; late final VideoIntroController videoIntroController; late final VideoDetailController videoDetailCtr; - late final Map videoItem; - final Box localCache = GStrorage.localCache; final Box setting = GStrorage.setting; late double sheetHeight; - - late final bool loadingStatus; // 加载状态 - late final dynamic owner; late final dynamic follower; late final dynamic followStatus; @@ -135,6 +139,9 @@ class _VideoInfoState extends State with TickerProviderStateMixin { late String memberHeroTag; late bool enableAi; bool isProcessing = false; + RxBool isExpand = false.obs; + late ExpandableController _expandableCtr; + void Function()? handleState(Future Function() action) { return isProcessing ? null @@ -149,18 +156,16 @@ class _VideoInfoState extends State with TickerProviderStateMixin { void initState() { super.initState(); heroTag = widget.heroTag!; - videoIntroController = Get.put(VideoIntroController(), tag: heroTag); + videoIntroController = + Get.put(VideoIntroController(bvid: widget.bvid), tag: heroTag); videoDetailCtr = Get.find(tag: heroTag); - videoItem = videoIntroController.videoItem!; sheetHeight = localCache.get('sheetHeight'); - loadingStatus = widget.loadingStatus; - owner = loadingStatus ? videoItem['owner'] : widget.videoDetail!.owner; - follower = loadingStatus - ? '-' - : Utils.numFormat(videoIntroController.userStat['follower']); + owner = widget.videoDetail!.owner; + follower = Utils.numFormat(videoIntroController.userStat['follower']); followStatus = videoIntroController.followStatus; enableAi = setting.get(SettingBoxKey.enableAi, defaultValue: true); + _expandableCtr = ExpandableController(initialExpanded: false); } // 收藏 @@ -212,29 +217,17 @@ class _VideoInfoState extends State with TickerProviderStateMixin { // 视频介绍 showIntroDetail() { - if (loadingStatus) { - return; - } feedBack(); - showBottomSheet( - context: context, - enableDrag: true, - builder: (BuildContext context) { - return IntroDetail(videoDetail: widget.videoDetail!); - }, - ); + isExpand.value = !(isExpand.value); + _expandableCtr.toggle(); } // 用户主页 onPushMember() { feedBack(); - mid = !loadingStatus - ? widget.videoDetail!.owner!.mid - : videoItem['owner'].mid; + mid = widget.videoDetail!.owner!.mid!; memberHeroTag = Utils.makeHeroTag(mid); - String face = !loadingStatus - ? widget.videoDetail!.owner!.face - : videoItem['owner'].face; + String face = widget.videoDetail!.owner!.face!; Get.toNamed('/member?mid=$mid', arguments: {'face': face, 'heroTag': memberHeroTag}); } @@ -250,229 +243,232 @@ class _VideoInfoState extends State with TickerProviderStateMixin { ); } + @override + void dispose() { + _expandableCtr.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final ThemeData t = Theme.of(context); final Color outline = t.colorScheme.outline; return SliverPadding( padding: const EdgeInsets.only( - left: StyleString.safeSpace, right: StyleString.safeSpace, top: 10), + left: StyleString.safeSpace, + right: StyleString.safeSpace, + top: 16, + ), sliver: SliverToBoxAdapter( - child: !loadingStatus - ? Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () => showIntroDetail(), - child: Text( - !loadingStatus - ? widget.videoDetail!.title - : videoItem['title'], - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - Stack( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () => showIntroDetail(), + child: ExpandablePanel( + controller: _expandableCtr, + collapsed: Text( + widget.videoDetail!.title!, + softWrap: true, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + expanded: Text( + widget.videoDetail!.title!, + softWrap: true, + maxLines: 4, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + theme: const ExpandableThemeData( + animationDuration: Duration(milliseconds: 300), + scrollAnimationDuration: Duration(milliseconds: 300), + crossFadePoint: 0, + fadeCurve: Curves.ease, + sizeCurve: Curves.linear, + ), + ), + ), + Stack( + children: [ + GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () => showIntroDetail(), + child: Padding( + padding: const EdgeInsets.only(top: 7, bottom: 6), + child: Row( children: [ - GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () => showIntroDetail(), - child: Padding( - padding: const EdgeInsets.only(top: 7, bottom: 6), - child: Row( - children: [ - StatView( - theme: 'gray', - view: !loadingStatus - ? widget.videoDetail!.stat!.view - : videoItem['stat'].view, - size: 'medium', - ), - const SizedBox(width: 10), - StatDanMu( - theme: 'gray', - danmu: !loadingStatus - ? widget.videoDetail!.stat!.danmaku - : videoItem['stat'].danmaku, - size: 'medium', - ), - const SizedBox(width: 10), - Text( - Utils.dateFormat( - !loadingStatus - ? widget.videoDetail!.pubdate - : videoItem['pubdate'], - formatType: 'detail'), - style: TextStyle( - fontSize: 12, - color: t.colorScheme.outline, - ), - ), - const SizedBox(width: 10), - if (videoIntroController.isShowOnlineTotal) - Obx( - () => Text( - '${videoIntroController.total.value}人在看', - style: TextStyle( - fontSize: 12, - color: t.colorScheme.outline, - ), - ), - ), - ], - ), - ), + StatView( + theme: 'gray', + view: widget.videoDetail!.stat!.view, + size: 'medium', ), - if (enableAi) - Positioned( - right: 10, - top: 6, - child: GestureDetector( - onTap: () async { - final res = - await videoIntroController.aiConclusion(); - if (res['status']) { - showAiBottomSheet(); - } - }, - child: - Image.asset('assets/images/ai.png', height: 22), - ), - ) - ], - ), - // 点赞收藏转发 布局样式1 - // SingleChildScrollView( - // padding: const EdgeInsets.only(top: 7, bottom: 7), - // scrollDirection: Axis.horizontal, - // child: actionRow( - // context, - // videoIntroController, - // videoDetailCtr, - // ), - // ), - // 点赞收藏转发 布局样式2 - actionGrid(context, videoIntroController), - // 合集 - if (!loadingStatus && - widget.videoDetail!.ugcSeason != null) ...[ - Obx( - () => SeasonPanel( - ugcSeason: widget.videoDetail!.ugcSeason!, - cid: videoIntroController.lastPlayCid.value != 0 - ? videoIntroController.lastPlayCid.value - : widget.videoDetail!.pages!.first.cid, - sheetHeight: sheetHeight, - changeFuc: (bvid, cid, aid) => videoIntroController - .changeSeasonOrbangu(bvid, cid, aid), + const SizedBox(width: 10), + StatDanMu( + theme: 'gray', + danmu: widget.videoDetail!.stat!.danmaku, + size: 'medium', ), - ) - ], - if (!loadingStatus && - widget.videoDetail!.pages != null && - widget.videoDetail!.pages!.length > 1) ...[ - Obx(() => PagesPanel( - pages: widget.videoDetail!.pages!, - cid: videoIntroController.lastPlayCid.value, - sheetHeight: sheetHeight, - changeFuc: (cid) => - videoIntroController.changeSeasonOrbangu( - videoIntroController.bvid, cid, null), - )) - ], - GestureDetector( - onTap: onPushMember, - child: Container( - padding: const EdgeInsets.symmetric( - vertical: 12, horizontal: 4), - child: Row( - children: [ - NetworkImgLayer( - type: 'avatar', - src: loadingStatus - ? owner.face - : widget.videoDetail!.owner!.face, - width: 34, - height: 34, - fadeInDuration: Duration.zero, - fadeOutDuration: Duration.zero, - ), - const SizedBox(width: 10), - Text(owner.name, - style: const TextStyle(fontSize: 13)), - const SizedBox(width: 6), - Text( - follower, + const SizedBox(width: 10), + Text( + Utils.dateFormat(widget.videoDetail!.pubdate, + formatType: 'detail'), + style: TextStyle( + fontSize: 12, + color: t.colorScheme.outline, + ), + ), + const SizedBox(width: 10), + if (videoIntroController.isShowOnlineTotal) + Obx( + () => Text( + '${videoIntroController.total.value}人在看', style: TextStyle( - fontSize: t.textTheme.labelSmall!.fontSize, - color: outline, + fontSize: 12, + color: t.colorScheme.outline, ), ), - const Spacer(), - Obx(() => AnimatedOpacity( - opacity: loadingStatus || - videoIntroController - .followStatus.isEmpty - ? 0 - : 1, - duration: const Duration(milliseconds: 50), - child: SizedBox( - height: 32, - child: Obx( - () => videoIntroController - .followStatus.isNotEmpty - ? TextButton( - onPressed: videoIntroController - .actionRelationMod, - style: TextButton.styleFrom( - padding: const EdgeInsets.only( - left: 8, right: 8), - foregroundColor: - followStatus['attribute'] != 0 - ? outline - : t.colorScheme.onPrimary, - backgroundColor: - followStatus['attribute'] != 0 - ? t.colorScheme - .onInverseSurface - : t.colorScheme - .primary, // 设置按钮背景色 - ), - child: Text( - followStatus['attribute'] != 0 - ? '已关注' - : '关注', - style: TextStyle( - fontSize: t.textTheme - .labelMedium!.fontSize), - ), - ) - : ElevatedButton( - onPressed: videoIntroController - .actionRelationMod, - child: const Text('关注'), - ), - ), - ), - )), - ], - ), + ), + ], + ), + ), + ), + if (enableAi) + Positioned( + right: 10, + top: 6, + child: GestureDetector( + onTap: () async { + final res = await videoIntroController.aiConclusion(); + if (res['status']) { + showAiBottomSheet(); + } + }, + child: Image.asset('assets/images/ai.png', height: 22), + ), + ) + ], + ), + + /// 视频简介 + ExpandablePanel( + controller: _expandableCtr, + collapsed: const SizedBox(height: 0), + expanded: IntroDetail(videoDetail: widget.videoDetail!), + theme: const ExpandableThemeData( + animationDuration: Duration(milliseconds: 300), + scrollAnimationDuration: Duration(milliseconds: 300), + crossFadePoint: 0, + fadeCurve: Curves.ease, + sizeCurve: Curves.linear, + ), + ), + + /// 点赞收藏转发 + actionGrid(context, videoIntroController), + // 合集 + if (widget.videoDetail!.ugcSeason != null) ...[ + Obx( + () => SeasonPanel( + ugcSeason: widget.videoDetail!.ugcSeason!, + cid: videoIntroController.lastPlayCid.value != 0 + ? videoIntroController.lastPlayCid.value + : widget.videoDetail!.pages!.first.cid, + sheetHeight: sheetHeight, + changeFuc: (bvid, cid, aid) => + videoIntroController.changeSeasonOrbangu(bvid, cid, aid), + ), + ) + ], + if (widget.videoDetail!.pages != null && + widget.videoDetail!.pages!.length > 1) ...[ + Obx(() => PagesPanel( + pages: widget.videoDetail!.pages!, + cid: videoIntroController.lastPlayCid.value, + sheetHeight: sheetHeight, + changeFuc: (cid) => videoIntroController.changeSeasonOrbangu( + videoIntroController.bvid, cid, null), + )) + ], + GestureDetector( + onTap: onPushMember, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 4), + child: Row( + children: [ + NetworkImgLayer( + type: 'avatar', + src: widget.videoDetail!.owner!.face, + width: 34, + height: 34, + fadeInDuration: Duration.zero, + fadeOutDuration: Duration.zero, + ), + const SizedBox(width: 10), + Text(owner.name, style: const TextStyle(fontSize: 13)), + const SizedBox(width: 6), + Text( + follower, + style: TextStyle( + fontSize: t.textTheme.labelSmall!.fontSize, + color: outline, ), ), + const Spacer(), + Obx(() => AnimatedOpacity( + opacity: + videoIntroController.followStatus.isEmpty ? 0 : 1, + duration: const Duration(milliseconds: 50), + child: SizedBox( + height: 32, + child: Obx( + () => videoIntroController.followStatus.isNotEmpty + ? TextButton( + onPressed: + videoIntroController.actionRelationMod, + style: TextButton.styleFrom( + padding: const EdgeInsets.only( + left: 8, right: 8), + foregroundColor: + followStatus['attribute'] != 0 + ? outline + : t.colorScheme.onPrimary, + backgroundColor: + followStatus['attribute'] != 0 + ? t.colorScheme.onInverseSurface + : t.colorScheme + .primary, // 设置按钮背景色 + ), + child: Text( + followStatus['attribute'] != 0 + ? '已关注' + : '关注', + style: TextStyle( + fontSize: t + .textTheme.labelMedium!.fontSize), + ), + ) + : ElevatedButton( + onPressed: + videoIntroController.actionRelationMod, + child: const Text('关注'), + ), + ), + ), + )), ], - ) - : const SizedBox( - height: 100, - child: Center( - child: CircularProgressIndicator(), - ), ), - ), + ), + ), + ], + )), ); } @@ -483,6 +479,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { margin: const EdgeInsets.only(top: 6, bottom: 4), height: constraints.maxWidth / 5 * 0.8, child: GridView.count( + physics: const NeverScrollableScrollPhysics(), primary: false, padding: EdgeInsets.zero, crossAxisCount: 5, @@ -494,117 +491,46 @@ class _VideoInfoState extends State with TickerProviderStateMixin { selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp), onTap: handleState(videoIntroController.actionLikeVideo), selectStatus: videoIntroController.hasLike.value, - loadingStatus: loadingStatus, - text: !loadingStatus - ? widget.videoDetail!.stat!.like!.toString() - : '-'), + text: widget.videoDetail!.stat!.like!.toString()), ), - // ActionItem( - // icon: const Icon(FontAwesomeIcons.clock), - // onTap: () => videoIntroController.actionShareVideo(), - // selectStatus: false, - // loadingStatus: loadingStatus, - // text: '稍后再看'), Obx( () => ActionItem( - icon: const Icon(FontAwesomeIcons.b), - selectIcon: const Icon(FontAwesomeIcons.b), - onTap: handleState(videoIntroController.actionCoinVideo), - selectStatus: videoIntroController.hasCoin.value, - loadingStatus: loadingStatus, - text: !loadingStatus - ? widget.videoDetail!.stat!.coin!.toString() - : '-'), + icon: const Icon(FontAwesomeIcons.b), + selectIcon: const Icon(FontAwesomeIcons.b), + onTap: handleState(videoIntroController.actionCoinVideo), + selectStatus: videoIntroController.hasCoin.value, + text: widget.videoDetail!.stat!.coin!.toString(), + ), ), Obx( () => ActionItem( - icon: const Icon(FontAwesomeIcons.star), - selectIcon: const Icon(FontAwesomeIcons.solidStar), - onTap: () => showFavBottomSheet(), - onLongPress: () => showFavBottomSheet(type: 'longPress'), - selectStatus: videoIntroController.hasFav.value, - loadingStatus: loadingStatus, - text: !loadingStatus - ? widget.videoDetail!.stat!.favorite!.toString() - : '-'), + icon: const Icon(FontAwesomeIcons.star), + selectIcon: const Icon(FontAwesomeIcons.solidStar), + onTap: () => showFavBottomSheet(), + onLongPress: () => showFavBottomSheet(type: 'longPress'), + selectStatus: videoIntroController.hasFav.value, + text: widget.videoDetail!.stat!.favorite!.toString(), + ), ), ActionItem( - icon: const Icon(FontAwesomeIcons.comment), - onTap: () => videoDetailCtr.tabCtr.animateTo(1), - selectStatus: false, - loadingStatus: loadingStatus, - text: !loadingStatus - ? widget.videoDetail!.stat!.reply!.toString() - : '评论'), + icon: const Icon(FontAwesomeIcons.clock), + onTap: () async { + final res = + await UserHttp.toViewLater(bvid: widget.videoDetail!.bvid); + SmartDialog.showToast(res['msg']); + }, + selectStatus: false, + text: '稍后看', + ), ActionItem( - icon: const Icon(FontAwesomeIcons.shareFromSquare), - onTap: () => videoIntroController.actionShareVideo(), - selectStatus: false, - loadingStatus: loadingStatus, - text: '分享'), + icon: const Icon(FontAwesomeIcons.shareFromSquare), + onTap: () => videoIntroController.actionShareVideo(), + selectStatus: false, + text: '分享', + ), ], ), ); }); } - - Widget actionRow(BuildContext context, videoIntroController, videoDetailCtr) { - return Row(children: [ - Obx( - () => ActionRowItem( - icon: const Icon(FontAwesomeIcons.thumbsUp), - onTap: handleState(videoIntroController.actionLikeVideo), - selectStatus: videoIntroController.hasLike.value, - loadingStatus: loadingStatus, - text: - !loadingStatus ? widget.videoDetail!.stat!.like!.toString() : '-', - ), - ), - const SizedBox(width: 8), - Obx( - () => ActionRowItem( - icon: const Icon(FontAwesomeIcons.b), - onTap: handleState(videoIntroController.actionCoinVideo), - selectStatus: videoIntroController.hasCoin.value, - loadingStatus: loadingStatus, - text: - !loadingStatus ? widget.videoDetail!.stat!.coin!.toString() : '-', - ), - ), - const SizedBox(width: 8), - Obx( - () => ActionRowItem( - icon: const Icon(FontAwesomeIcons.heart), - onTap: () => showFavBottomSheet(), - onLongPress: () => showFavBottomSheet(type: 'longPress'), - selectStatus: videoIntroController.hasFav.value, - loadingStatus: loadingStatus, - text: !loadingStatus - ? widget.videoDetail!.stat!.favorite!.toString() - : '-', - ), - ), - const SizedBox(width: 8), - ActionRowItem( - icon: const Icon(FontAwesomeIcons.comment), - onTap: () { - videoDetailCtr.tabCtr.animateTo(1); - }, - selectStatus: false, - loadingStatus: loadingStatus, - text: - !loadingStatus ? widget.videoDetail!.stat!.reply!.toString() : '-', - ), - const SizedBox(width: 8), - ActionRowItem( - icon: const Icon(FontAwesomeIcons.share), - onTap: () => videoIntroController.actionShareVideo(), - selectStatus: false, - loadingStatus: loadingStatus, - // text: !loadingStatus - // ? widget.videoDetail!.stat!.share!.toString() - // : '-', - text: '转发'), - ]); - } } diff --git a/lib/pages/video/detail/introduction/widgets/action_item.dart b/lib/pages/video/detail/introduction/widgets/action_item.dart index 95ac103b8..022d9223e 100644 --- a/lib/pages/video/detail/introduction/widgets/action_item.dart +++ b/lib/pages/video/detail/introduction/widgets/action_item.dart @@ -7,7 +7,6 @@ class ActionItem extends StatelessWidget { final Icon? selectIcon; final Function? onTap; final Function? onLongPress; - final bool? loadingStatus; final String? text; final bool selectStatus; @@ -17,7 +16,6 @@ class ActionItem extends StatelessWidget { this.selectIcon, this.onTap, this.onLongPress, - this.loadingStatus, this.text, this.selectStatus = false, }) : super(key: key); @@ -43,25 +41,15 @@ class ActionItem extends StatelessWidget { : Icon(icon!.icon!, size: 18, color: Theme.of(context).colorScheme.outline), const SizedBox(height: 6), - AnimatedOpacity( - opacity: loadingStatus! ? 0 : 1, - duration: const Duration(milliseconds: 200), - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - transitionBuilder: (Widget child, Animation animation) { - return ScaleTransition(scale: animation, child: child); - }, - child: Text( - text ?? '', - key: ValueKey(text ?? ''), - style: TextStyle( - color: selectStatus - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.outline, - fontSize: Theme.of(context).textTheme.labelSmall!.fontSize), - ), + Text( + text ?? '', + style: TextStyle( + color: selectStatus + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.outline, + fontSize: Theme.of(context).textTheme.labelSmall!.fontSize, ), - ), + ) ], ), ); diff --git a/lib/pages/video/detail/introduction/widgets/intro_detail.dart b/lib/pages/video/detail/introduction/widgets/intro_detail.dart index 59ed10a35..1e9bb8424 100644 --- a/lib/pages/video/detail/introduction/widgets/intro_detail.dart +++ b/lib/pages/video/detail/introduction/widgets/intro_detail.dart @@ -1,16 +1,10 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; -import 'package:hive/hive.dart'; -import 'package:pilipala/common/widgets/stat/danmu.dart'; -import 'package:pilipala/common/widgets/stat/view.dart'; -import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/utils.dart'; -Box localCache = GStrorage.localCache; -late double sheetHeight; - class IntroDetail extends StatelessWidget { const IntroDetail({ super.key, @@ -20,102 +14,39 @@ class IntroDetail extends StatelessWidget { @override Widget build(BuildContext context) { - sheetHeight = localCache.get('sheetHeight'); - return Container( - color: Theme.of(context).colorScheme.background, - padding: const EdgeInsets.only(left: 14, right: 14), - height: sheetHeight, + return SizedBox( + width: double.infinity, + child: SelectableRegion( + focusNode: FocusNode(), + selectionControls: MaterialTextSelectionControls(), child: Column( - 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.primary, - borderRadius: - const BorderRadius.all(Radius.circular(3))), - ), - ), + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 4), + GestureDetector( + onTap: () { + Clipboard.setData(ClipboardData(text: videoDetail!.bvid!)); + SmartDialog.showToast('已复制'); + }, + child: Text( + videoDetail!.bvid!, + style: TextStyle( + fontSize: 13, color: Theme.of(context).colorScheme.primary), ), ), - Expanded( - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - videoDetail!.title, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 6), - Row( - children: [ - StatView( - theme: 'gray', - view: videoDetail!.stat!.view, - size: 'medium', - ), - const SizedBox(width: 10), - StatDanMu( - theme: 'gray', - danmu: videoDetail!.stat!.danmaku, - size: 'medium', - ), - const SizedBox(width: 10), - Text( - Utils.dateFormat(videoDetail!.pubdate, - formatType: 'detail'), - style: TextStyle( - fontSize: 12, - color: Theme.of(context).colorScheme.outline, - ), - ), - ], - ), - const SizedBox(height: 20), - SizedBox( - width: double.infinity, - child: SelectableRegion( - focusNode: FocusNode(), - selectionControls: MaterialTextSelectionControls(), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - videoDetail!.bvid!, - style: const TextStyle(fontSize: 13), - ), - const SizedBox(height: 4), - Text.rich( - style: const TextStyle( - height: 1.4, - // fontSize: 13, - ), - TextSpan( - children: [ - buildContent(context, videoDetail!), - ], - ), - ), - ], - ), - ), - ), - ], - ), + const SizedBox(height: 4), + Text.rich( + style: const TextStyle(height: 1.4), + TextSpan( + children: [ + buildContent(context, videoDetail!), + ], ), - ) + ), ], - )); + ), + ), + ); } InlineSpan buildContent(BuildContext context, content) { diff --git a/lib/pages/video/detail/introduction/widgets/page.dart b/lib/pages/video/detail/introduction/widgets/page.dart index 2ba7f507c..8d296050f 100644 --- a/lib/pages/video/detail/introduction/widgets/page.dart +++ b/lib/pages/video/detail/introduction/widgets/page.dart @@ -56,6 +56,37 @@ class _PagesPanelState extends State { super.dispose(); } + Widget buildEpisodeListItem( + Part episode, + int index, + bool isCurrentIndex, + ) { + Color primary = Theme.of(context).colorScheme.primary; + return ListTile( + onTap: () { + changeFucCall(episode, index); + Get.back(); + }, + dense: false, + leading: isCurrentIndex + ? Image.asset( + 'assets/images/live.gif', + color: primary, + height: 12, + ) + : null, + title: Text( + episode.pagePart!, + style: TextStyle( + fontSize: 14, + color: isCurrentIndex + ? primary + : Theme.of(context).colorScheme.onSurface, + ), + ), + ); + } + @override Widget build(BuildContext context) { return Column( @@ -131,39 +162,25 @@ class _PagesPanelState extends State { child: Material( child: ListView.builder( controller: _scrollController, - itemCount: episodes.length, + itemCount: episodes.length + 1, itemBuilder: (BuildContext context, int index) { - return ListTile( - onTap: () { - changeFucCall( - episodes[index], index); - Get.back(); - }, - dense: false, - leading: index == currentIndex - ? Image.asset( - 'assets/images/live.gif', - color: Theme.of(context) - .colorScheme - .primary, - height: 12, - ) - : null, - title: Text( - episodes[index].pagePart!, - style: TextStyle( - fontSize: 14, - color: index == currentIndex - ? Theme.of(context) - .colorScheme - .primary - : Theme.of(context) - .colorScheme - .onSurface, - ), - ), - ); + bool isLastItem = + index == episodes.length; + bool isCurrentIndex = + currentIndex == index; + return isLastItem + ? SizedBox( + height: MediaQuery.of(context) + .padding + .bottom + + 20, + ) + : buildEpisodeListItem( + episodes[index], + index, + isCurrentIndex, + ); }, ), ), @@ -192,6 +209,7 @@ class _PagesPanelState extends State { itemCount: widget.pages.length, itemExtent: 150, itemBuilder: (BuildContext context, int i) { + bool isCurrentIndex = currentIndex == i; return Container( width: 150, margin: const EdgeInsets.only(right: 10), @@ -206,7 +224,7 @@ class _PagesPanelState extends State { vertical: 8, horizontal: 8), child: Row( children: [ - if (i == currentIndex) ...[ + if (isCurrentIndex) ...[ Image.asset( 'assets/images/live.gif', color: Theme.of(context).colorScheme.primary, @@ -220,7 +238,7 @@ class _PagesPanelState extends State { maxLines: 1, style: TextStyle( fontSize: 13, - color: i == currentIndex + color: isCurrentIndex ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.onSurface), overflow: TextOverflow.ellipsis, diff --git a/lib/pages/video/detail/introduction/widgets/season.dart b/lib/pages/video/detail/introduction/widgets/season.dart index 9e5dd34f7..0f3884ede 100644 --- a/lib/pages/video/detail/introduction/widgets/season.dart +++ b/lib/pages/video/detail/introduction/widgets/season.dart @@ -80,6 +80,34 @@ class _SeasonPanelState extends State { super.dispose(); } + Widget buildEpisodeListItem( + EpisodeItem episode, + int index, + bool isCurrentIndex, + ) { + Color primary = Theme.of(context).colorScheme.primary; + return ListTile( + onTap: () => changeFucCall(episode, index), + dense: false, + leading: isCurrentIndex + ? Image.asset( + 'assets/images/live.gif', + color: primary, + height: 12, + ) + : null, + title: Text( + episode.title!, + style: TextStyle( + fontSize: 14, + color: isCurrentIndex + ? primary + : Theme.of(context).colorScheme.onSurface, + ), + ), + ); + } + @override Widget build(BuildContext context) { return Builder(builder: (BuildContext context) { @@ -133,33 +161,23 @@ class _SeasonPanelState extends State { Expanded( child: Material( child: ScrollablePositionedList.builder( - itemCount: episodes.length, - itemBuilder: (BuildContext context, int index) => - ListTile( - onTap: () => - changeFucCall(episodes[index], index), - dense: false, - leading: index == currentIndex - ? Image.asset( - 'assets/images/live.gif', - color: Theme.of(context) - .colorScheme - .primary, - height: 12, + itemCount: episodes.length + 1, + itemBuilder: (BuildContext context, int index) { + bool isLastItem = index == episodes.length; + bool isCurrentIndex = currentIndex == index; + return isLastItem + ? SizedBox( + height: MediaQuery.of(context) + .padding + .bottom + + 20, ) - : null, - title: Text( - episodes[index].title!, - style: TextStyle( - fontSize: 14, - color: index == currentIndex - ? Theme.of(context).colorScheme.primary - : Theme.of(context) - .colorScheme - .onSurface, - ), - ), - ), + : buildEpisodeListItem( + episodes[index], + index, + isCurrentIndex, + ); + }, itemScrollController: itemScrollController, ), ), diff --git a/lib/pages/video/detail/reply/controller.dart b/lib/pages/video/detail/reply/controller.dart index e564ef02e..06ce26ff5 100644 --- a/lib/pages/video/detail/reply/controller.dart +++ b/lib/pages/video/detail/reply/controller.dart @@ -22,7 +22,7 @@ class VideoReplyController extends GetxController { String? replyLevel; // rpid 请求楼中楼回复 String? rpid; - RxList replyList = [ReplyItemModel()].obs; + RxList replyList = [].obs; // 当前页 int currentPage = 0; bool isLoadingMore = false; @@ -62,6 +62,7 @@ class VideoReplyController extends GetxController { noMore.value = ''; } if (noMore.value == '没有更多了') { + isLoadingMore = false; return; } final res = await ReplyHttp.replyList( diff --git a/lib/pages/video/detail/reply/view.dart b/lib/pages/video/detail/reply/view.dart index df8c75b14..2a167fe94 100644 --- a/lib/pages/video/detail/reply/view.dart +++ b/lib/pages/video/detail/reply/view.dart @@ -134,13 +134,13 @@ class _VideoReplyPanelState extends State super.build(context); return RefreshIndicator( onRefresh: () async { - _videoReplyController.currentPage = 0; - return await _videoReplyController.queryReplyList(); + return await _videoReplyController.queryReplyList(type: 'init'); }, child: Stack( children: [ CustomScrollView( controller: scrollController, + physics: const AlwaysScrollableScrollPhysics(), key: const PageStorageKey('评论'), slivers: [ SliverPersistentHeader( @@ -148,34 +148,16 @@ class _VideoReplyPanelState extends State floating: true, delegate: _MySliverPersistentHeaderDelegate( child: Container( - height: 45, + height: 40, padding: const EdgeInsets.fromLTRB(12, 0, 6, 0), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, - border: Border( - bottom: BorderSide( - color: Theme.of(context) - .colorScheme - .outline - .withOpacity(0.1)), - ), - ), + color: Theme.of(context).colorScheme.surface, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Obx( - () => AnimatedSwitcher( - duration: const Duration(milliseconds: 400), - transitionBuilder: - (Widget child, Animation animation) { - return ScaleTransition( - scale: animation, child: child); - }, - child: Text( - '共${_videoReplyController.count.value}条回复', - key: ValueKey( - _videoReplyController.count.value), - ), + () => Text( + '${_videoReplyController.sortTypeLabel.value}评论', + style: const TextStyle(fontSize: 13), ), ), SizedBox( @@ -184,10 +166,12 @@ class _VideoReplyPanelState extends State onPressed: () => _videoReplyController.queryBySort(), icon: const Icon(Icons.sort, size: 16), - label: Obx(() => Text( - _videoReplyController.sortTypeLabel.value, - style: const TextStyle(fontSize: 13), - )), + label: Obx( + () => Text( + _videoReplyController.sortTypeLabel.value, + style: const TextStyle(fontSize: 13), + ), + ), ), ) ], @@ -329,8 +313,8 @@ class _VideoReplyPanelState extends State class _MySliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate { _MySliverPersistentHeaderDelegate({required this.child}); - final double _minExtent = 45; - final double _maxExtent = 45; + final double _minExtent = 40; + final double _maxExtent = 40; final Widget child; @override diff --git a/lib/pages/video/detail/reply/widgets/reply_item.dart b/lib/pages/video/detail/reply/widgets/reply_item.dart index 246d0688c..e79b6159e 100644 --- a/lib/pages/video/detail/reply/widgets/reply_item.dart +++ b/lib/pages/video/detail/reply/widgets/reply_item.dart @@ -280,7 +280,7 @@ class ReplyItem extends StatelessWidget { // 完成评论,数据添加 if (value != null && value['data'] != null) { - addReply!(value['data']) + addReply?.call(value['data']) // replyControl.replies.add(value['data']), } }); @@ -462,6 +462,9 @@ class ReplyItemRow extends StatelessWidget { InlineSpan buildContent( BuildContext context, replyItem, replyReply, fReplyItem) { + final String routePath = Get.currentRoute; + bool isVideoPage = routePath.startsWith('/video'); + // replyItem 当前回复内容 // replyReply 查看二楼回复(回复详情)回调 // fReplyItem 父级回复内容,用作二楼回复(回复详情)展示 @@ -495,7 +498,7 @@ InlineSpan buildContent( return str; }); } - // content.message = content.message.replaceAll(RegExp(r"\{vote:.*?\}"), ' '); + content.message = content.message.replaceAll(RegExp(r"\{vote:.*?\}"), ' '); content.message = content.message .replaceAll('&', '&') .replaceAll('<', '<') @@ -503,29 +506,33 @@ InlineSpan buildContent( .replaceAll('"', '"') .replaceAll(''', "'") .replaceAll(' ', ' '); - // print("content.jumpUrl.keys:" + content.jumpUrl.keys.toString()); // 构建正则表达式 final List specialTokens = [ ...content.emote.keys, ...content.topicsMeta?.keys?.map((e) => '#$e#') ?? [], ...content.atNameToMid.keys.map((e) => '@$e'), - ...content.jumpUrl.keys.map((e) => - e.replaceAll('?', '\\?').replaceAll('+', '\\+').replaceAll('*', '\\*')), ]; + List jumpUrlKeysList = content.jumpUrl.keys.map((e) { + return e.replaceAllMapped( + RegExp(r'[?+*]'), (match) => '\\${match.group(0)}'); + }).toList(); String patternStr = specialTokens.map(RegExp.escape).join('|'); if (patternStr.isNotEmpty) { patternStr += "|"; } patternStr += r'(\b(?:\d+[::])?[0-5]?[0-9][::][0-5]?[0-9]\b)'; + if (jumpUrlKeysList.isNotEmpty) { + patternStr += '|${jumpUrlKeysList.join('|')}'; + } final RegExp pattern = RegExp(patternStr); List matchedStrs = []; void addPlainTextSpan(str) { spanChilds.add(TextSpan( text: str, recognizer: TapGestureRecognizer() - ..onTap = - () => replyReply(replyItem.root == 0 ? replyItem : fReplyItem))); + ..onTap = () => + replyReply?.call(replyItem.root == 0 ? replyItem : fReplyItem))); } // 分割文本并处理每个部分 @@ -571,27 +578,31 @@ InlineSpan buildContent( spanChilds.add( TextSpan( text: ' $matchStr ', - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - ), + style: isVideoPage + ? TextStyle( + color: Theme.of(context).colorScheme.primary, + ) + : null, recognizer: TapGestureRecognizer() ..onTap = () { // 跳转到指定位置 - try { - SmartDialog.showToast('跳转至:$matchStr'); - Get.find(tag: Get.arguments['heroTag']) - .plPlayerController - .seekTo( - Duration(seconds: Utils.duration(matchStr)), - ); - } catch (e) { - SmartDialog.showToast('跳转失败: $e'); + if (isVideoPage) { + try { + SmartDialog.showToast('跳转至:$matchStr'); + Get.find( + tag: Get.arguments['heroTag']) + .plPlayerController + .seekTo( + Duration(seconds: Utils.duration(matchStr)), + ); + } catch (e) { + SmartDialog.showToast('跳转失败: $e'); + } } }, ), ); } else { - print("matchStr=$matchStr"); String appUrlSchema = ''; final bool enableWordRe = setting.get(SettingBoxKey.enableWordRe, defaultValue: false) as bool; @@ -631,6 +642,11 @@ InlineSpan buildContent( } else { final String redirectUrl = await UrlUtils.parseRedirectUrl(matchStr); + if (redirectUrl == matchStr) { + Clipboard.setData(ClipboardData(text: matchStr)); + SmartDialog.showToast('地址可能有误'); + return; + } final String pathSegment = Uri.parse(redirectUrl).path; final String lastPathSegment = pathSegment.split('/').last; diff --git a/lib/pages/video/detail/reply_new/toolbar_icon_button.dart b/lib/pages/video/detail/reply_new/toolbar_icon_button.dart new file mode 100644 index 000000000..c4390796d --- /dev/null +++ b/lib/pages/video/detail/reply_new/toolbar_icon_button.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; + +class ToolbarIconButton extends StatelessWidget { + final VoidCallback onPressed; + final Icon icon; + final String toolbarType; + final bool selected; + + const ToolbarIconButton({ + super.key, + required this.onPressed, + required this.icon, + required this.toolbarType, + required this.selected, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 36, + height: 36, + child: IconButton( + onPressed: onPressed, + icon: icon, + highlightColor: Theme.of(context).colorScheme.secondaryContainer, + color: selected + ? Theme.of(context).colorScheme.onSecondaryContainer + : Theme.of(context).colorScheme.outline, + style: ButtonStyle( + padding: MaterialStateProperty.all(EdgeInsets.zero), + backgroundColor: MaterialStateProperty.resolveWith((states) { + return selected + ? Theme.of(context).colorScheme.secondaryContainer + : null; + }), + ), + ), + ); + } +} diff --git a/lib/pages/video/detail/reply_new/view.dart b/lib/pages/video/detail/reply_new/view.dart index 01c95adc3..053514114 100644 --- a/lib/pages/video/detail/reply_new/view.dart +++ b/lib/pages/video/detail/reply_new/view.dart @@ -4,9 +4,13 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:pilipala/http/video.dart'; import 'package:pilipala/models/common/reply_type.dart'; +import 'package:pilipala/models/video/reply/emote.dart'; import 'package:pilipala/models/video/reply/item.dart'; +import 'package:pilipala/pages/emote/index.dart'; import 'package:pilipala/utils/feed_back.dart'; +import 'toolbar_icon_button.dart'; + class VideoReplyNewDialog extends StatefulWidget { final int? oid; final int? root; @@ -32,6 +36,10 @@ class _VideoReplyNewDialogState extends State final TextEditingController _replyContentController = TextEditingController(); final FocusNode replyContentFocusNode = FocusNode(); final GlobalKey _formKey = GlobalKey(); + late double emoteHeight = 0.0; + double keyboardHeight = 0.0; // 键盘高度 + final _debouncer = Debouncer(milliseconds: 200); // 设置延迟时间 + String toolbarType = 'input'; @override void initState() { @@ -42,6 +50,8 @@ class _VideoReplyNewDialogState extends State WidgetsBinding.instance.addObserver(this); // 自动聚焦 _autoFocus(); + // 监听聚焦状态 + _focuslistener(); } _autoFocus() async { @@ -51,6 +61,16 @@ class _VideoReplyNewDialogState extends State } } + _focuslistener() { + replyContentFocusNode.addListener(() { + if (replyContentFocusNode.hasFocus) { + setState(() { + toolbarType = 'input'; + }); + } + }); + } + Future submitReplyAdd() async { feedBack(); String message = _replyContentController.text; @@ -73,10 +93,50 @@ class _VideoReplyNewDialogState extends State } } + void onChooseEmote(PackageItem package, Emote emote) { + final int cursorPosition = _replyContentController.selection.baseOffset; + final String currentText = _replyContentController.text; + final String newText = currentText.substring(0, cursorPosition) + + emote.text! + + currentText.substring(cursorPosition); + _replyContentController.value = TextEditingValue( + text: newText, + selection: + TextSelection.collapsed(offset: cursorPosition + emote.text!.length), + ); + } + + @override + void didChangeMetrics() { + super.didChangeMetrics(); + final String routePath = Get.currentRoute; + if (mounted && + (routePath.startsWith('/video') || + routePath.startsWith('/dynamicDetail'))) { + WidgetsBinding.instance.addPostFrameCallback((_) { + // 键盘高度 + final viewInsets = EdgeInsets.fromViewPadding( + View.of(context).viewInsets, View.of(context).devicePixelRatio); + _debouncer.run(() { + if (mounted) { + if (keyboardHeight == 0 && emoteHeight == 0) { + setState(() { + emoteHeight = keyboardHeight = + keyboardHeight == 0.0 ? viewInsets.bottom : keyboardHeight; + }); + } + } + }); + }); + } + } + @override void dispose() { WidgetsBinding.instance.removeObserver(this); _replyContentController.dispose(); + replyContentFocusNode.removeListener(() {}); + replyContentFocusNode.dispose(); super.dispose(); } @@ -137,27 +197,32 @@ class _VideoReplyNewDialogState extends State child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - SizedBox( - width: 36, - height: 36, - child: IconButton( - onPressed: () { - FocusScope.of(context) - .requestFocus(replyContentFocusNode); - }, - icon: Icon(Icons.keyboard, - size: 22, - color: Theme.of(context).colorScheme.onBackground), - highlightColor: - Theme.of(context).colorScheme.onInverseSurface, - style: ButtonStyle( - padding: MaterialStateProperty.all(EdgeInsets.zero), - backgroundColor: - MaterialStateProperty.resolveWith((states) { - return Theme.of(context).highlightColor; - }), - ), - ), + ToolbarIconButton( + onPressed: () { + if (toolbarType == 'emote') { + setState(() { + toolbarType = 'input'; + }); + } + FocusScope.of(context).requestFocus(replyContentFocusNode); + }, + icon: const Icon(Icons.keyboard, size: 22), + toolbarType: toolbarType, + selected: toolbarType == 'input', + ), + const SizedBox(width: 20), + ToolbarIconButton( + onPressed: () { + if (toolbarType == 'input') { + setState(() { + toolbarType = 'emote'; + }); + } + FocusScope.of(context).unfocus(); + }, + icon: const Icon(Icons.emoji_emotions, size: 22), + toolbarType: toolbarType, + selected: toolbarType == 'emote', ), const Spacer(), TextButton( @@ -170,7 +235,10 @@ class _VideoReplyNewDialogState extends State duration: const Duration(milliseconds: 300), child: SizedBox( width: double.infinity, - height: keyboardHeight, + height: toolbarType == 'input' ? keyboardHeight : emoteHeight, + child: EmotePanel( + onChoose: (package, emote) => onChooseEmote(package, emote), + ), ), ), ], @@ -178,3 +246,22 @@ class _VideoReplyNewDialogState extends State ); } } + +typedef DebounceCallback = void Function(); + +class Debouncer { + DebounceCallback? callback; + final int? milliseconds; + Timer? _timer; + + Debouncer({this.milliseconds}); + + run(DebounceCallback callback) { + if (_timer != null) { + _timer!.cancel(); + } + _timer = Timer(Duration(milliseconds: milliseconds!), () { + callback(); + }); + } +} diff --git a/lib/pages/video/detail/reply_reply/controller.dart b/lib/pages/video/detail/reply_reply/controller.dart index 0d5b4488f..e94aaea5e 100644 --- a/lib/pages/video/detail/reply_reply/controller.dart +++ b/lib/pages/video/detail/reply_reply/controller.dart @@ -12,7 +12,7 @@ class VideoReplyReplyController extends GetxController { // rpid 请求楼中楼回复 String? rpid; ReplyType replyType = ReplyType.video; - RxList replyList = [ReplyItemModel()].obs; + RxList replyList = [].obs; // 当前页 int currentPage = 0; bool isLoadingMore = false; @@ -30,6 +30,9 @@ class VideoReplyReplyController extends GetxController { if (type == 'init') { currentPage = 0; } + if (isLoadingMore) { + return; + } isLoadingMore = true; final res = await ReplyHttp.replyReplyList( oid: aid!, @@ -41,7 +44,7 @@ class VideoReplyReplyController extends GetxController { final List replies = res['data'].replies; if (replies.isNotEmpty) { noMore.value = '加载中...'; - if (replyList.length == res['data'].page.count) { + if (replies.length == res['data'].page.count) { noMore.value = '没有更多了'; } currentPage++; @@ -50,21 +53,6 @@ class VideoReplyReplyController extends GetxController { noMore.value = currentPage == 0 ? '还没有评论' : '没有更多了'; } if (type == 'init') { - // List replies = res['data'].replies; - // 添加置顶回复 - // if (res['data'].upper.top != null) { - // bool flag = false; - // for (var i = 0; i < res['data'].topReplies.length; i++) { - // if (res['data'].topReplies[i].rpid == res['data'].upper.top.rpid) { - // flag = true; - // } - // } - // if (!flag) { - // replies.insert(0, res['data'].upper.top); - // } - // } - // replies.insertAll(0, res['data'].topReplies); - // res['data'].replies = replies; replyList.value = replies; } else { // 每次回复之后,翻页请求有且只有相同的一条回复数据 diff --git a/lib/pages/video/detail/reply_reply/view.dart b/lib/pages/video/detail/reply_reply/view.dart index c08c81cce..e8754a311 100644 --- a/lib/pages/video/detail/reply_reply/view.dart +++ b/lib/pages/video/detail/reply_reply/view.dart @@ -54,7 +54,8 @@ class _VideoReplyReplyPanelState extends State { () { if (scrollController.position.pixels >= scrollController.position.maxScrollExtent - 300) { - EasyThrottle.throttle('replylist', const Duration(seconds: 2), () { + EasyThrottle.throttle('replylist', const Duration(milliseconds: 200), + () { _videoReplyReplyController.queryReplyList(type: 'onLoad'); }); } @@ -92,7 +93,7 @@ class _VideoReplyReplyPanelState extends State { icon: const Icon(Icons.close, size: 20), onPressed: () { _videoReplyReplyController.currentPage = 0; - widget.closePanel!(); + widget.closePanel?.call; Navigator.pop(context); }, ), @@ -184,6 +185,8 @@ class _VideoReplyReplyPanelState extends State { .add(replyItem); }, replyType: widget.replyType, + replyReply: (replyItem) => + replyReply(replyItem), ); } }, diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index febca105c..c2379f204 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -5,10 +5,11 @@ import 'dart:ui'; import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:floating/floating.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; -import 'package:nil/nil.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/http/user.dart'; import 'package:pilipala/models/common/search_type.dart'; @@ -23,9 +24,8 @@ import 'package:pilipala/plugin/pl_player/models/play_repeat.dart'; import 'package:pilipala/services/service_locator.dart'; import 'package:pilipala/utils/storage.dart'; -import 'package:pilipala/plugin/pl_player/utils/fullscreen.dart'; import '../../../services/shutdown_timer_service.dart'; -import 'widgets/header_control.dart'; +import 'widgets/app_bar.dart'; class VideoDetailPage extends StatefulWidget { const VideoDetailPage({Key? key}) : super(key: key); @@ -38,7 +38,7 @@ class VideoDetailPage extends StatefulWidget { class _VideoDetailPageState extends State with TickerProviderStateMixin, RouteAware { - late VideoDetailController videoDetailController; + late VideoDetailController vdCtr; PlPlayerController? plPlayerController; final ScrollController _extendNestCtr = ScrollController(); late StreamController appbarStream; @@ -58,27 +58,25 @@ class _VideoDetailPageState extends State late bool autoExitFullcreen; late bool autoPlayEnable; late bool autoPiP; - final Floating floating = Floating(); - // 生命周期监听 - late final AppLifecycleListener _lifecycleListener; + late Floating floating; bool isShowing = true; @override void initState() { super.initState(); heroTag = Get.arguments['heroTag']; - videoDetailController = Get.put(VideoDetailController(), tag: heroTag); - videoIntroController = Get.put(VideoIntroController(), tag: heroTag); + vdCtr = Get.put(VideoDetailController(), tag: heroTag); + videoIntroController = Get.put( + VideoIntroController(bvid: Get.parameters['bvid']!), + tag: heroTag); videoIntroController.videoDetail.listen((value) { - videoPlayerServiceHandler.onVideoDetailChange( - value, videoDetailController.cid.value); + videoPlayerServiceHandler.onVideoDetailChange(value, vdCtr.cid.value); }); bangumiIntroController = Get.put(BangumiIntroController(), tag: heroTag); bangumiIntroController.bangumiDetail.listen((value) { - videoPlayerServiceHandler.onVideoDetailChange( - value, videoDetailController.cid.value); + videoPlayerServiceHandler.onVideoDetailChange(value, vdCtr.cid.value); }); - videoDetailController.cid.listen((p0) { + vdCtr.cid.listen((p0) { videoPlayerServiceHandler.onVideoDetailChange( bangumiIntroController.bangumiDetail.value, p0); }); @@ -91,15 +89,18 @@ class _VideoDetailPageState extends State videoSourceInit(); appbarStreamListen(); - lifecycleListener(); fullScreenStatusListener(); + if (Platform.isAndroid) { + floating = vdCtr.floating!; + autoEnterPip(); + } } // 获取视频资源,初始化播放器 Future videoSourceInit() async { - _futureBuilderFuture = videoDetailController.queryVideoUrl(); - if (videoDetailController.autoPlay.value) { - plPlayerController = videoDetailController.plPlayerController; + _futureBuilderFuture = vdCtr.queryVideoUrl(); + if (vdCtr.autoPlay.value) { + plPlayerController = vdCtr.plPlayerController; plPlayerController!.addStatusLister(playerListener); } } @@ -128,10 +129,10 @@ class _VideoDetailPageState extends State /// 顺序播放 列表循环 if (plPlayerController!.playRepeat != PlayRepeat.pause && plPlayerController!.playRepeat != PlayRepeat.singleCycle) { - if (videoDetailController.videoType == SearchType.video) { + if (vdCtr.videoType == SearchType.video) { videoIntroController.nextPlay(); } - if (videoDetailController.videoType == SearchType.media_bangumi) { + if (vdCtr.videoType == SearchType.media_bangumi) { bangumiIntroController.nextPlay(); } } @@ -143,13 +144,16 @@ class _VideoDetailPageState extends State } // 播放完展示控制栏 try { - PiPStatus currentStatus = - await videoDetailController.floating!.pipStatus; + PiPStatus currentStatus = await vdCtr.floating!.pipStatus; if (currentStatus == PiPStatus.disabled) { plPlayerController!.onLockControl(false); } } catch (_) {} } + if (Platform.isAndroid) { + floating.toggleAutoPip( + autoEnter: status == PlayerStatus.playing && autoPiP); + } } // 继续播放或重新播放 @@ -161,38 +165,17 @@ class _VideoDetailPageState extends State /// 未开启自动播放时触发播放 Future handlePlay() async { - await videoDetailController.playerInit(); - plPlayerController = videoDetailController.plPlayerController; + await vdCtr.playerInit(); + plPlayerController = vdCtr.plPlayerController; plPlayerController!.addStatusLister(playerListener); - videoDetailController.autoPlay.value = true; - videoDetailController.isShowCover.value = false; - } - - // 生命周期监听 - void lifecycleListener() { - _lifecycleListener = AppLifecycleListener( - onResume: () => _handleTransition('resume'), - // 后台 - onInactive: () => _handleTransition('inactive'), - // 在Android和iOS端不生效 - onHide: () => _handleTransition('hide'), - onShow: () => _handleTransition('show'), - onPause: () => _handleTransition('pause'), - onRestart: () => _handleTransition('restart'), - onDetach: () => _handleTransition('detach'), - // 只作用于桌面端 - onExitRequested: () { - ScaffoldMessenger.maybeOf(context) - ?.showSnackBar(const SnackBar(content: Text("拦截应用退出"))); - return Future.value(AppExitResponse.cancel); - }, - ); + vdCtr.autoPlay.value = true; + vdCtr.isShowCover.value = false; } void fullScreenStatusListener() { plPlayerController?.isFullScreen.listen((bool isFullScreen) { if (isFullScreen) { - videoDetailController.hiddenReplyReplyPanel(); + vdCtr.hiddenReplyReplyPanel(); } }); } @@ -204,12 +187,15 @@ class _VideoDetailPageState extends State plPlayerController!.removeStatusLister(playerListener); plPlayerController!.dispose(); } - if (videoDetailController.floating != null) { - videoDetailController.floating!.dispose(); + if (vdCtr.floating != null) { + vdCtr.floating!.dispose(); } videoPlayerServiceHandler.onVideoDetailDispose(); - floating.dispose(); - _lifecycleListener.dispose(); + if (Platform.isAndroid) { + floating.toggleAutoPip(autoEnter: false); + floating.dispose(); + } + appbarStream.close(); super.dispose(); } @@ -219,13 +205,14 @@ class _VideoDetailPageState extends State /// 开启 if (setting.get(SettingBoxKey.enableAutoBrightness, defaultValue: false) as bool) { - videoDetailController.brightness = plPlayerController!.brightness.value; + vdCtr.brightness = plPlayerController!.brightness.value; } if (plPlayerController != null) { - videoDetailController.defaultST = plPlayerController!.position.value; + vdCtr.defaultST = plPlayerController!.position.value; videoIntroController.isPaused = true; plPlayerController!.removeStatusLister(playerListener); plPlayerController!.pause(); + vdCtr.clearSubtitleContent(); } setState(() => isShowing = false); super.didPushNext(); @@ -236,19 +223,21 @@ class _VideoDetailPageState extends State void didPopNext() async { if (plPlayerController != null && plPlayerController!.videoPlayerController != null) { - setState(() => isShowing = true); + setState(() { + vdCtr.setSubtitleContent(); + isShowing = true; + }); } - videoDetailController.isFirstTime = false; + vdCtr.isFirstTime = false; final bool autoplay = autoPlayEnable; - videoDetailController.playerInit(autoplay: autoplay); + vdCtr.playerInit(autoplay: autoplay); /// 未开启自动播放时,未播放跳转下一页返回/播放后跳转下一页返回 - videoDetailController.autoPlay.value = - !videoDetailController.isShowCover.value; + vdCtr.autoPlay.value = !vdCtr.isShowCover.value; videoIntroController.isPaused = false; if (_extendNestCtr.position.pixels == 0 && autoplay) { await Future.delayed(const Duration(milliseconds: 300)); - plPlayerController?.seekTo(videoDetailController.defaultST); + plPlayerController?.seekTo(vdCtr.defaultST); plPlayerController?.play(); } plPlayerController?.addStatusLister(playerListener); @@ -262,37 +251,190 @@ class _VideoDetailPageState extends State .subscribe(this, ModalRoute.of(context)! as PageRoute); } - void _handleTransition(String name) { - switch (name) { - case 'inactive': - if (plPlayerController != null && - playerStatus == PlayerStatus.playing) { - autoEnterPip(); - } - break; - } - } - void autoEnterPip() { final String routePath = Get.currentRoute; - final bool isPortrait = - MediaQuery.of(context).orientation == Orientation.portrait; - - /// TODO 横屏全屏状态下误触pip - if (autoPiP && routePath.startsWith('/video') && isPortrait) { - floating.enable( - aspectRatio: Rational( - videoDetailController.data.dash!.video!.first.width!, - videoDetailController.data.dash!.video!.first.height!, - )); + if (autoPiP && routePath.startsWith('/video')) { + floating.toggleAutoPip(autoEnter: autoPiP); } } @override Widget build(BuildContext context) { - final double videoHeight = MediaQuery.sizeOf(context).width * 9 / 16; + // final double videoHeight = MediaQuery.sizeOf(context).width * 9 / 16; + final sizeContext = MediaQuery.sizeOf(context); + final _context = MediaQuery.of(context); + late double defaultVideoHeight = sizeContext.width * 9 / 16; + late RxDouble videoHeight = defaultVideoHeight.obs; final double pinnedHeaderHeight = - statusBarHeight + kToolbarHeight + videoHeight; + statusBarHeight + kToolbarHeight + videoHeight.value; + // ignore: no_leading_underscores_for_local_identifiers + + // 竖屏 + final bool isPortrait = _context.orientation == Orientation.portrait; + // 横屏 + final bool isLandscape = _context.orientation == Orientation.landscape; + final Rx isFullScreen = plPlayerController?.isFullScreen ?? false.obs; + // 全屏时高度撑满 + if (isLandscape || isFullScreen.value == true) { + videoHeight.value = Get.size.height; + enterFullScreen(); + } else { + videoHeight.value = defaultVideoHeight; + exitFullScreen(); + } + + /// 播放器面板 + Widget videoPlayerPanel = FutureBuilder( + future: _futureBuilderFuture, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasData && snapshot.data['status']) { + return Obx( + () { + return !vdCtr.autoPlay.value + ? const SizedBox() + : PLVideoPlayer( + controller: plPlayerController!, + headerControl: vdCtr.headerControl, + danmuWidget: Obx( + () => PlDanmaku( + key: Key(vdCtr.danmakuCid.value.toString()), + cid: vdCtr.danmakuCid.value, + playerController: plPlayerController!, + ), + ), + ); + }, + ); + } else { + // 加载失败异常处理 + return const SizedBox(); + } + }, + ); + + /// tabbar + Widget tabbarBuild = Container( + width: double.infinity, + height: 45, + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + width: 1, + color: Theme.of(context).dividerColor.withOpacity(0.1), + ), + ), + ), + child: Material( + child: Row( + children: [ + Flexible( + flex: 1, + child: Obx( + () => TabBar( + padding: EdgeInsets.zero, + controller: vdCtr.tabCtr, + labelStyle: const TextStyle(fontSize: 13), + labelPadding: + const EdgeInsets.symmetric(horizontal: 10.0), // 设置每个标签的宽度 + dividerColor: Colors.transparent, + tabs: vdCtr.tabs + .map( + (String name) => Tab(text: name), + ) + .toList(), + ), + ), + ), + Flexible( + flex: 1, + child: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + SizedBox( + height: 32, + child: TextButton( + style: ButtonStyle( + padding: MaterialStateProperty.all(EdgeInsets.zero), + ), + onPressed: () => vdCtr.showShootDanmakuSheet(), + child: + const Text('发弹幕', style: TextStyle(fontSize: 12)), + ), + ), + SizedBox( + width: 38, + height: 38, + child: Obx( + () => IconButton( + onPressed: () { + plPlayerController?.isOpenDanmu.value = + !(plPlayerController?.isOpenDanmu.value ?? + false); + }, + icon: !(plPlayerController?.isOpenDanmu.value ?? + false) + ? SvgPicture.asset( + 'assets/images/video/danmu_close.svg', + // ignore: deprecated_member_use + color: + Theme.of(context).colorScheme.outline, + ) + : SvgPicture.asset( + 'assets/images/video/danmu_open.svg', + // ignore: deprecated_member_use + color: + Theme.of(context).colorScheme.primary, + ), + ), + ), + ), + const SizedBox(width: 14), + ], + ), + )), + ], + ), + ), + ); + + /// 手动播放 + Widget handlePlayPanel() { + return Stack( + children: [ + GestureDetector( + onTap: () { + handlePlay(); + }, + child: NetworkImgLayer( + type: 'emote', + src: vdCtr.videoItem['pic'], + width: Get.width, + height: videoHeight.value, + ), + ), + Positioned( + top: 0, + left: 0, + right: 0, + child: buildCustomAppBar(), + ), + Positioned( + right: 12, + bottom: 10, + child: IconButton( + tooltip: '播放', + onPressed: () => handlePlay(), + icon: Image.asset( + 'assets/images/play.png', + width: 60, + height: 60, + )), + ), + ], + ); + } + Widget childWhenDisabled = SafeArea( top: MediaQuery.of(context).orientation == Orientation.portrait && plPlayerController?.isFullScreen.value == true, @@ -304,7 +446,7 @@ class _VideoDetailPageState extends State children: [ Scaffold( resizeToAvoidBottomInset: false, - key: videoDetailController.scaffoldKey, + key: vdCtr.scaffoldKey, backgroundColor: Colors.black, appBar: PreferredSize( preferredSize: const Size.fromHeight(0), @@ -330,21 +472,19 @@ class _VideoDetailPageState extends State return SliverAppBar( automaticallyImplyLeading: false, // 假装使用一个非空变量,避免Obx检测不到而罢工 - pinned: videoDetailController.autoPlay.value ^ - false ^ - videoDetailController.autoPlay.value, + pinned: vdCtr.autoPlay.value, elevation: 0, scrolledUnderElevation: 0, forceElevated: innerBoxIsScrolled, expandedHeight: MediaQuery.of(context).orientation == Orientation.landscape || plPlayerController?.isFullScreen.value == true - ? MediaQuery.sizeOf(context).height - + ? (MediaQuery.sizeOf(context).height - (MediaQuery.of(context).orientation == Orientation.landscape ? 0 - : MediaQuery.of(context).padding.top) - : videoHeight, + : MediaQuery.of(context).padding.top)) + : videoHeight.value, backgroundColor: Colors.black, flexibleSpace: FlexibleSpaceBar( background: PopScope( @@ -364,134 +504,27 @@ class _VideoDetailPageState extends State child: LayoutBuilder( builder: (BuildContext context, BoxConstraints boxConstraints) { - final double maxWidth = - boxConstraints.maxWidth; - final double maxHeight = - boxConstraints.maxHeight; + // final double maxWidth = + // boxConstraints.maxWidth; + // final double maxHeight = + // boxConstraints.maxHeight; return Stack( children: [ - if (isShowing) - FutureBuilder( - future: _futureBuilderFuture, - builder: (BuildContext context, - AsyncSnapshot snapshot) { - if (snapshot.hasData && - snapshot.data['status']) { - return Obx( - () => - !videoDetailController - .autoPlay.value - ? nil - : PLVideoPlayer( - controller: - plPlayerController!, - headerControl: - videoDetailController - .headerControl, - danmuWidget: Obx( - () => PlDanmaku( - key: Key(videoDetailController - .danmakuCid - .value - .toString()), - cid: videoDetailController - .danmakuCid - .value, - playerController: - plPlayerController!, - ), - ), - ), - ); - } else { - return const SizedBox(); - } - }, - ), + if (isShowing) videoPlayerPanel, /// 关闭自动播放时 手动播放 - if (!videoDetailController - .autoPlay.value) ...[ - Obx( - () => Visibility( - visible: videoDetailController - .isShowCover.value, - child: Positioned( - top: 0, - left: 0, - right: 0, - child: GestureDetector( - onTap: () { - handlePlay(); - }, - child: NetworkImgLayer( - type: 'emote', - src: videoDetailController - .videoItem['pic'], - width: maxWidth, - height: maxHeight, - ), - ), - ), + Obx( + () => Visibility( + visible: !vdCtr.autoPlay.value && + vdCtr.isShowCover.value, + child: Positioned( + top: 0, + left: 0, + right: 0, + child: handlePlayPanel(), ), ), - Obx( - () => Visibility( - visible: videoDetailController - .isShowCover.value && - videoDetailController - .isEffective.value, - child: Stack( - children: [ - Positioned( - top: 0, - left: 0, - right: 0, - child: AppBar( - primary: false, - foregroundColor: - Colors.white, - elevation: 0, - scrolledUnderElevation: 0, - backgroundColor: - Colors.transparent, - actions: [ - IconButton( - tooltip: '稍后再看', - onPressed: () async { - var res = await UserHttp - .toViewLater( - bvid: videoDetailController - .bvid); - SmartDialog - .showToast( - res['msg']); - }, - icon: const Icon(Icons - .history_outlined), - ), - const SizedBox( - width: 14) - ], - ), - ), - Positioned( - right: 12, - bottom: 10, - child: IconButton( - tooltip: '播放', - onPressed: () => - handlePlay(), - icon: Image.asset( - 'assets/images/play.png', - width: 60, - height: 60, - )), - ), - ], - )), - ), - ] + ), ], ); }, @@ -502,18 +535,16 @@ class _VideoDetailPageState extends State ), ]; }, - // pinnedHeaderSliverHeightBuilder: () { - // return playerStatus != PlayerStatus.playing - // ? statusBarHeight + kToolbarHeight - // : pinnedHeaderHeight; - // }, + /// 不收回 pinnedHeaderSliverHeightBuilder: () { return MediaQuery.of(context).orientation == Orientation.landscape || plPlayerController?.isFullScreen.value == true ? MediaQuery.sizeOf(context).height - : pinnedHeaderHeight; + : playerStatus != PlayerStatus.playing + ? kToolbarHeight + : pinnedHeaderHeight; }, onlyOneScrollInBody: true, body: ColoredBox( @@ -521,53 +552,23 @@ class _VideoDetailPageState extends State color: Theme.of(context).colorScheme.background, child: Column( children: [ - Opacity( - opacity: 0, - child: SizedBox( - width: double.infinity, - height: 0, - child: Obx( - () => TabBar( - controller: videoDetailController.tabCtr, - dividerColor: Colors.transparent, - indicatorColor: - Theme.of(context).colorScheme.background, - tabs: videoDetailController.tabs - .map((String name) => Tab(text: name)) - .toList(), - ), - ), - ), - ), + tabbarBuild, Expanded( child: TabBarView( - controller: videoDetailController.tabCtr, + controller: vdCtr.tabCtr, children: [ Builder( builder: (BuildContext context) { return CustomScrollView( key: const PageStorageKey('简介'), slivers: [ - if (videoDetailController.videoType == - SearchType.video) ...[ - const VideoIntroPanel(), - ] else if (videoDetailController.videoType == + if (vdCtr.videoType == SearchType.video) ...[ + VideoIntroPanel(bvid: vdCtr.bvid), + ] else if (vdCtr.videoType == SearchType.media_bangumi) ...[ Obx(() => BangumiIntroPanel( - cid: videoDetailController.cid.value)), + cid: vdCtr.cid.value)), ], - // if (videoDetailController.videoType == - // SearchType.video) ...[ - // SliverPersistentHeader( - // floating: true, - // pinned: true, - // delegate: SliverHeaderDelegate( - // height: 50, - // child: - // const MenuRow(loadingStatus: false), - // ), - // ), - // ], SliverToBoxAdapter( child: Divider( indent: 12, @@ -577,15 +578,17 @@ class _VideoDetailPageState extends State .withOpacity(0.06), ), ), - RelatedVideoPanel(), + if (vdCtr.videoType == SearchType.video && + vdCtr.enableRelatedVideo) + const RelatedVideoPanel(), ], ); }, ), Obx( () => VideoReplyPanel( - bvid: videoDetailController.bvid, - oid: videoDetailController.oid.value, + bvid: vdCtr.bvid, + oid: vdCtr.oid.value, ), ) ], @@ -599,58 +602,73 @@ class _VideoDetailPageState extends State /// 重新进入会刷新 // 播放完成/暂停播放 - // StreamBuilder( - // stream: appbarStream.stream, - // initialData: 0, - // builder: ((context, snapshot) { - // return ScrollAppBar( - // snapshot.data!.toDouble(), - // () => continuePlay(), - // playerStatus, - // null, - // ); - // }), - // ) + StreamBuilder( + stream: appbarStream.stream, + initialData: 0, + builder: ((context, snapshot) { + return ScrollAppBar( + snapshot.data!.toDouble(), + () => continuePlay(), + playerStatus, + null, + ); + }), + ) ], ), ); - Widget childWhenEnabled = FutureBuilder( - key: Key(heroTag), - future: _futureBuilderFuture, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData && snapshot.data['status']) { - return Obx( - () => !videoDetailController.autoPlay.value - ? const SizedBox() - : PLVideoPlayer( - controller: plPlayerController!, - headerControl: HeaderControl( - controller: plPlayerController, - videoDetailCtr: videoDetailController, - ), - danmuWidget: Obx( - () => PlDanmaku( - key: Key( - videoDetailController.danmakuCid.value.toString()), - cid: videoDetailController.danmakuCid.value, - playerController: plPlayerController!, - ), - ), - ), - ); - } else { - return nil; - } - }, - ); + if (Platform.isAndroid) { return PiPSwitcher( childWhenDisabled: childWhenDisabled, - childWhenEnabled: childWhenEnabled, + childWhenEnabled: videoPlayerPanel, floating: floating, ); } else { return childWhenDisabled; } } + + Widget buildCustomAppBar() { + return AppBar( + backgroundColor: Colors.transparent, // 使背景透明 + foregroundColor: Colors.white, + elevation: 0, + scrolledUnderElevation: 0, + primary: false, + centerTitle: false, + automaticallyImplyLeading: false, + titleSpacing: 0, + title: Container( + height: kToolbarHeight, + padding: const EdgeInsets.symmetric(horizontal: 14), + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [ + Colors.transparent, + Colors.black54, + ], + tileMode: TileMode.mirror, + )), + child: Row( + children: [ + ComBtn( + icon: const Icon(FontAwesomeIcons.arrowLeft, size: 15), + fuc: () => Get.back(), + ), + const Spacer(), + ComBtn( + icon: const Icon(Icons.history_outlined, size: 22), + fuc: () async { + var res = await UserHttp.toViewLater(bvid: vdCtr.bvid); + SmartDialog.showToast(res['msg']); + }, + ), + ], + ), + ), + ); + } } diff --git a/lib/pages/video/detail/widgets/app_bar.dart b/lib/pages/video/detail/widgets/app_bar.dart index 17f4bec7d..efc0b593b 100644 --- a/lib/pages/video/detail/widgets/app_bar.dart +++ b/lib/pages/video/detail/widgets/app_bar.dart @@ -17,12 +17,16 @@ class ScrollAppBar extends StatelessWidget { Widget build(BuildContext context) { final double statusBarHeight = MediaQuery.of(context).padding.top; final videoHeight = MediaQuery.sizeOf(context).width * 9 / 16; + double scrollDistance = scrollVal; + if (scrollVal > videoHeight - kToolbarHeight) { + scrollDistance = videoHeight - kToolbarHeight; + } return Positioned( - top: -videoHeight + scrollVal + kToolbarHeight + 0.5, + top: -videoHeight + scrollDistance + kToolbarHeight + 0.5, left: 0, right: 0, child: Opacity( - opacity: scrollVal / (videoHeight - kToolbarHeight), + opacity: scrollDistance / (videoHeight - kToolbarHeight), child: Container( height: statusBarHeight + kToolbarHeight, color: Theme.of(context).colorScheme.background, diff --git a/lib/pages/video/detail/widgets/expandable_section.dart b/lib/pages/video/detail/widgets/expandable_section.dart index afa68cc9f..69e73e206 100644 --- a/lib/pages/video/detail/widgets/expandable_section.dart +++ b/lib/pages/video/detail/widgets/expandable_section.dart @@ -32,28 +32,14 @@ class _ExpandedSectionState extends State _runExpandCheck(); } - ///Setting up the animation - // void prepareAnimations() { - // expandController = AnimationController( - // vsync: this, duration: const Duration(milliseconds: 500)); - // animation = CurvedAnimation( - // parent: expandController, - // curve: Curves.fastOutSlowIn, - // ); - // } - void prepareAnimations() { expandController = AnimationController( vsync: this, duration: const Duration(milliseconds: 400)); Animation curve = CurvedAnimation( parent: expandController, - curve: Curves.fastOutSlowIn, + curve: Curves.linear, ); animation = Tween(begin: widget.begin, end: widget.end).animate(curve); - // animation = CurvedAnimation( - // parent: expandController, - // curve: Curves.fastOutSlowIn, - // ); } void _runExpandCheck() { @@ -67,7 +53,9 @@ class _ExpandedSectionState extends State @override void didUpdateWidget(ExpandedSection oldWidget) { super.didUpdateWidget(oldWidget); - _runExpandCheck(); + if (widget.expand != oldWidget.expand) { + _runExpandCheck(); + } } @override diff --git a/lib/pages/video/detail/widgets/header_control.dart b/lib/pages/video/detail/widgets/header_control.dart index 8bc7d84ec..b0b7db179 100644 --- a/lib/pages/video/detail/widgets/header_control.dart +++ b/lib/pages/video/detail/widgets/header_control.dart @@ -1,5 +1,4 @@ import 'dart:io'; -import 'dart:math'; import 'package:floating/floating.dart'; import 'package:flutter/material.dart'; @@ -17,19 +16,25 @@ import 'package:pilipala/pages/video/detail/introduction/widgets/menu_row.dart'; import 'package:pilipala/plugin/pl_player/index.dart'; import 'package:pilipala/plugin/pl_player/models/play_repeat.dart'; import 'package:pilipala/utils/storage.dart'; -import 'package:pilipala/http/danmaku.dart'; import 'package:pilipala/services/shutdown_timer_service.dart'; +import '../../../../models/common/search_type.dart'; +import '../../../../models/video_detail_res.dart'; +import '../introduction/index.dart'; class HeaderControl extends StatefulWidget implements PreferredSizeWidget { const HeaderControl({ this.controller, this.videoDetailCtr, this.floating, + this.bvid, + this.videoType, super.key, }); final PlPlayerController? controller; final VideoDetailController? videoDetailCtr; final Floating? floating; + final String? bvid; + final SearchType? videoType; @override State createState() => _HeaderControlState(); @@ -40,7 +45,6 @@ class HeaderControl extends StatefulWidget implements PreferredSizeWidget { class _HeaderControlState extends State { late PlayUrlModel videoInfo; - List playSpeed = PlaySpeed.values; static const TextStyle subTitleStyle = TextStyle(fontSize: 12); static const TextStyle titleStyle = TextStyle(fontSize: 14); Size get preferredSize => const Size(double.infinity, kToolbarHeight); @@ -48,11 +52,36 @@ class _HeaderControlState extends State { final Box videoStorage = GStrorage.video; late List speedsList; double buttonSpace = 8; + bool showTitle = false; + late String heroTag; + late VideoIntroController videoIntroController; + late VideoDetailData videoDetail; + @override void initState() { super.initState(); videoInfo = widget.videoDetailCtr!.data; speedsList = widget.controller!.speedsList; + fullScreenStatusListener(); + heroTag = Get.arguments['heroTag']; + videoIntroController = + Get.put(VideoIntroController(bvid: widget.bvid!), tag: heroTag); + } + + void fullScreenStatusListener() { + widget.videoDetailCtr!.plPlayerController.isFullScreen + .listen((bool isFullScreen) { + if (isFullScreen) { + showTitle = true; + } else { + showTitle = false; + } + + /// TODO setState() called after dispose() + if (mounted) { + setState(() {}); + } + }); } /// 设置面板 @@ -189,88 +218,6 @@ class _HeaderControlState extends State { ); } - /// 发送弹幕 - void showShootDanmakuSheet() { - final TextEditingController textController = TextEditingController(); - bool isSending = false; // 追踪是否正在发送 - showDialog( - context: Get.context!, - builder: (BuildContext context) { - // TODO: 支持更多类型和颜色的弹幕 - return AlertDialog( - title: const Text('发送弹幕(测试)'), - content: StatefulBuilder( - builder: (BuildContext context, StateSetter setState) { - return TextField( - controller: textController, - ); - }), - actions: [ - TextButton( - onPressed: () => Get.back(), - child: Text( - '取消', - style: TextStyle(color: Theme.of(context).colorScheme.outline), - ), - ), - StatefulBuilder( - builder: (BuildContext context, StateSetter setState) { - return TextButton( - onPressed: isSending - ? null - : () async { - final String msg = textController.text; - if (msg.isEmpty) { - SmartDialog.showToast('弹幕内容不能为空'); - return; - } else if (msg.length > 100) { - SmartDialog.showToast('弹幕内容不能超过100个字符'); - return; - } - setState(() { - isSending = true; // 开始发送,更新状态 - }); - //修改按钮文字 - // SmartDialog.showToast('弹幕发送中,\n$msg'); - final dynamic res = await DanmakaHttp.shootDanmaku( - oid: widget.videoDetailCtr!.cid.value, - msg: textController.text, - bvid: widget.videoDetailCtr!.bvid, - progress: - widget.controller!.position.value.inMilliseconds, - type: 1, - ); - setState(() { - isSending = false; // 发送结束,更新状态 - }); - if (res['status']) { - SmartDialog.showToast('发送成功'); - // 发送成功,自动预览该弹幕,避免重新请求 - // TODO: 暂停状态下预览弹幕仍会移动与计时,可考虑添加到dmSegList或其他方式实现 - widget.controller!.danmakuController!.addItems([ - DanmakuItem( - msg, - color: Colors.white, - time: widget - .controller!.position.value.inMilliseconds, - type: DanmakuItemType.scroll, - isSend: true, - ) - ]); - Get.back(); - } else { - SmartDialog.showToast('发送失败,错误信息为${res['msg']}'); - } - }, - child: Text(isSending ? '发送中...' : '发送'), - ); - }) - ], - ); - }, - ); - } - /// 定时关闭 void scheduleExit() async { const List scheduleTimeChoices = [ @@ -342,8 +289,7 @@ class _HeaderControlState extends State { }, dense: true, contentPadding: const EdgeInsets.only(), - title: - const Text("额外等待视频播放完毕", style: titleStyle), + title: const Text("额外等待视频播放完毕", style: titleStyle), trailing: Switch( // thumb color (round icon) activeColor: Theme.of(context).colorScheme.primary, @@ -398,6 +344,56 @@ class _HeaderControlState extends State { ); } + /// 选择字幕 + void showSubtitleDialog() async { + int tempThemeValue = widget.controller!.subTitleCode.value; + int len = widget.videoDetailCtr!.subtitles.length; + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('选择字幕'), + contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 18), + content: StatefulBuilder(builder: (context, StateSetter setState) { + return len == 0 + ? const SizedBox( + height: 60, + child: Center( + child: Text('没有字幕'), + ), + ) + : Column( + mainAxisSize: MainAxisSize.min, + children: [ + RadioListTile( + value: -1, + title: const Text('关闭弹幕'), + groupValue: tempThemeValue, + onChanged: (value) { + tempThemeValue = value!; + widget.controller?.toggleSubtitle(value); + Get.back(); + }, + ), + ...widget.videoDetailCtr!.subtitles + .map((e) => RadioListTile( + value: e.code, + title: Text(e.title), + groupValue: tempThemeValue, + onChanged: (value) { + tempThemeValue = value!; + widget.controller?.toggleSubtitle(value); + Get.back(); + }, + )) + .toList(), + ], + ); + }), + ); + }); + } + /// 选择倍速 void showSetSpeedSheet() { final double currentSpeed = widget.controller!.playbackSpeed; @@ -891,7 +887,7 @@ class _HeaderControlState extends State { final DanmakuOption currentOption = danmakuController.option; final DanmakuOption updatedOption = - currentOption.copyWith(strokeWidth: val); + currentOption.copyWith(strokeWidth: val); danmakuController.updateOption(updatedOption); } catch (_) {} }, @@ -1047,6 +1043,8 @@ class _HeaderControlState extends State { color: Colors.white, fontSize: 12, ); + final bool isLandscape = + MediaQuery.of(context).orientation == Orientation.landscape; return AppBar( backgroundColor: Colors.transparent, foregroundColor: Colors.white, @@ -1081,21 +1079,49 @@ class _HeaderControlState extends State { }, ), SizedBox(width: buttonSpace), - ComBtn( - icon: const Icon( - FontAwesomeIcons.house, - size: 15, - color: Colors.white, + if (showTitle && + isLandscape && + widget.videoType == SearchType.video) ...[ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 200), + child: Text( + videoIntroController.videoDetail.value.title ?? '', + style: const TextStyle( + color: Colors.white, + fontSize: 16, + ), + ), + ), + if (videoIntroController.isShowOnlineTotal) + Text( + '${videoIntroController.total.value}人正在看', + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), + ) + ], + ) + ] else ...[ + ComBtn( + icon: const Icon( + FontAwesomeIcons.house, + size: 15, + color: Colors.white, + ), + fuc: () async { + // 销毁播放器实例 + await widget.controller!.dispose(type: 'all'); + if (mounted) { + Navigator.popUntil( + context, (Route route) => route.isFirst); + } + }, ), - fuc: () async { - // 销毁播放器实例 - await widget.controller!.dispose(type: 'all'); - if (mounted) { - Navigator.popUntil( - context, (Route route) => route.isFirst); - } - }, - ), + ], const Spacer(), // ComBtn( // icon: const Icon( @@ -1105,41 +1131,6 @@ class _HeaderControlState extends State { // ), // fuc: () => _.screenshot(), // ), - SizedBox( - width: 56, - height: 34, - child: TextButton( - style: ButtonStyle( - padding: MaterialStateProperty.all(EdgeInsets.zero), - ), - onPressed: () => showShootDanmakuSheet(), - child: const Text( - '发弹幕', - style: textStyle, - ), - ), - ), - SizedBox( - width: 34, - height: 34, - child: Obx( - () => IconButton( - style: ButtonStyle( - padding: MaterialStateProperty.all(EdgeInsets.zero), - ), - onPressed: () { - _.isOpenDanmu.value = !_.isOpenDanmu.value; - }, - icon: Icon( - _.isOpenDanmu.value - ? Icons.subtitles_outlined - : Icons.subtitles_off_outlined, - size: 19, - color: Colors.white, - ), - ), - ), - ), SizedBox(width: buttonSpace), if (Platform.isAndroid) ...[ SizedBox( @@ -1174,6 +1165,31 @@ class _HeaderControlState extends State { ), SizedBox(width: buttonSpace), ], + + /// 字幕 + // SizedBox( + // width: 34, + // height: 34, + // child: IconButton( + // style: ButtonStyle( + // padding: MaterialStateProperty.all(EdgeInsets.zero), + // ), + // onPressed: () => showSubtitleDialog(), + // icon: const Icon( + // Icons.closed_caption_off, + // size: 22, + // ), + // ), + // ), + ComBtn( + icon: const Icon( + Icons.closed_caption_off, + size: 22, + color: Colors.white, + ), + fuc: () => showSubtitleDialog(), + ), + SizedBox(width: buttonSpace), Obx( () => SizedBox( width: 45, diff --git a/lib/pages/webview/controller.dart b/lib/pages/webview/controller.dart index 37600ea76..f26f42842 100644 --- a/lib/pages/webview/controller.dart +++ b/lib/pages/webview/controller.dart @@ -131,13 +131,13 @@ class WebviewController extends GetxController { Get.back(); } else { // 获取用户信息失败 - SmartDialog.showToast(result.msg); - Clipboard.setData(ClipboardData(text: result.msg.toString())); + SmartDialog.showToast(result['msg']); + Clipboard.setData(ClipboardData(text: result['msg'])); } } catch (e) { SmartDialog.showNotify(msg: e.toString(), notifyType: NotifyType.warning); content = content + e.toString(); + Clipboard.setData(ClipboardData(text: content)); } - Clipboard.setData(ClipboardData(text: content)); } } diff --git a/lib/pages/whisper/view.dart b/lib/pages/whisper/view.dart index f2779a177..fa7ad60b2 100644 --- a/lib/pages/whisper/view.dart +++ b/lib/pages/whisper/view.dart @@ -108,9 +108,9 @@ class _WhisperPageState extends State { future: _futureBuilderFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { - Map data = snapshot.data as Map; - if (data['status']) { - List sessionList = _whisperController.sessionList; + Map? data = snapshot.data; + if (data != null && data['status']) { + RxList sessionList = _whisperController.sessionList; return Obx( () => sessionList.isEmpty ? const SizedBox() @@ -121,33 +121,35 @@ class _WhisperPageState extends State { const NeverScrollableScrollPhysics(), itemBuilder: (_, int i) { return ListTile( - onTap: () => Get.toNamed( - '/whisperDetail', - parameters: { - 'talkerId': sessionList[i] - .talkerId - .toString(), - 'name': sessionList[i] - .accountInfo - .name, - 'face': sessionList[i] - .accountInfo - .face, - 'mid': sessionList[i] - .accountInfo - .mid - .toString(), - }, - ), + onTap: () { + sessionList[i].unreadCount = 0; + sessionList.refresh(); + Get.toNamed( + '/whisperDetail', + parameters: { + 'talkerId': sessionList[i] + .talkerId + .toString(), + 'name': sessionList[i] + .accountInfo + .name, + 'face': sessionList[i] + .accountInfo + .face, + 'mid': sessionList[i] + .accountInfo + .mid + .toString(), + }, + ); + }, leading: Badge( - isLabelVisible: false, - backgroundColor: Theme.of(context) - .colorScheme - .primary, + isLabelVisible: + sessionList[i].unreadCount > 0, label: Text(sessionList[i] .unreadCount .toString()), - alignment: Alignment.bottomRight, + alignment: Alignment.topRight, child: NetworkImgLayer( width: 45, height: 45, @@ -160,20 +162,27 @@ class _WhisperPageState extends State { title: Text( sessionList[i].accountInfo.name), subtitle: Text( - sessionList[i] - .lastMsg - .content['text'] ?? - sessionList[i] - .lastMsg - .content['content'] ?? - sessionList[i] - .lastMsg - .content['title'] ?? - sessionList[i] + sessionList[i].lastMsg.content != + null && + sessionList[i] + .lastMsg + .content != + '' + ? (sessionList[i] + .lastMsg + .content['text'] ?? + sessionList[i] .lastMsg - .content[ - 'reply_content'] ?? - '', + .content['content'] ?? + sessionList[i] + .lastMsg + .content['title'] ?? + sessionList[i] + .lastMsg + .content[ + 'reply_content'] ?? + '不支持的消息类型') + : '不支持的消息类型', maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context) @@ -210,7 +219,9 @@ class _WhisperPageState extends State { ); } else { // 请求错误 - return const SizedBox(); + return Center( + child: Text(data?['msg'] ?? '请求异常'), + ); } } else { // 骨架屏 diff --git a/lib/pages/whisper_detail/controller.dart b/lib/pages/whisper_detail/controller.dart index 71dd4c031..6e950f815 100644 --- a/lib/pages/whisper_detail/controller.dart +++ b/lib/pages/whisper_detail/controller.dart @@ -1,7 +1,11 @@ +import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; +import 'package:hive/hive.dart'; import 'package:pilipala/http/msg.dart'; import 'package:pilipala/models/msg/session.dart'; +import '../../utils/feed_back.dart'; +import '../../utils/storage.dart'; class WhisperDetailController extends GetxController { late int talkerId; @@ -11,6 +15,8 @@ class WhisperDetailController extends GetxController { RxList messageList = [].obs; //表情转换图片规则 List? eInfos; + final TextEditingController replyContentController = TextEditingController(); + Box userInfoCache = GStrorage.userInfo; @override void onInit() { @@ -25,10 +31,51 @@ class WhisperDetailController extends GetxController { var res = await MsgHttp.sessionMsg(talkerId: talkerId); if (res['status']) { messageList.value = res['data'].messages; - if (messageList.isNotEmpty && res['data'].eInfos != null) { - eInfos = res['data'].eInfos; + if (messageList.isNotEmpty) { + ackSessionMsg(); + if (res['data'].eInfos != null) { + eInfos = res['data'].eInfos; + } } + } else { + SmartDialog.showToast(res['msg']); } return res; } + + // 消息标记已读 + Future ackSessionMsg() async { + if (messageList.isEmpty) { + return; + } + await MsgHttp.ackSessionMsg( + talkerId: talkerId, + ackSeqno: messageList.last.msgSeqno, + ); + } + + Future sendMsg() async { + feedBack(); + String message = replyContentController.text; + final userInfo = userInfoCache.get('userInfoCache'); + if (userInfo == null) { + SmartDialog.showToast('请先登录'); + return; + } + if (message == '') { + SmartDialog.showToast('请输入内容'); + return; + } + var result = await MsgHttp.sendMsg( + senderUid: userInfo.mid, + receiverId: int.parse(mid), + content: {'content': message}, + msgType: 1, + ); + if (result['status']) { + SmartDialog.showToast('发送成功'); + } else { + SmartDialog.showToast(result['msg']); + } + } } diff --git a/lib/pages/whisper_detail/view.dart b/lib/pages/whisper_detail/view.dart index 8d2297c47..1701be33b 100644 --- a/lib/pages/whisper_detail/view.dart +++ b/lib/pages/whisper_detail/view.dart @@ -1,9 +1,12 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:hive/hive.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:pilipala/pages/emote/index.dart'; import 'package:pilipala/pages/whisper_detail/controller.dart'; import 'package:pilipala/utils/feed_back.dart'; - +import '../../utils/storage.dart'; import 'widget/chat_item.dart'; class WhisperDetailPage extends StatefulWidget { @@ -13,15 +16,67 @@ class WhisperDetailPage extends StatefulWidget { State createState() => _WhisperDetailPageState(); } -class _WhisperDetailPageState extends State { +class _WhisperDetailPageState extends State + with WidgetsBindingObserver { final WhisperDetailController _whisperDetailController = Get.put(WhisperDetailController()); late Future _futureBuilderFuture; + late TextEditingController _replyContentController; + final FocusNode replyContentFocusNode = FocusNode(); + final _debouncer = Debouncer(milliseconds: 200); // 设置延迟时间 + late double emoteHeight = 0.0; + double keyboardHeight = 0.0; // 键盘高度 + String toolbarType = 'input'; + Box userInfoCache = GStrorage.userInfo; @override void initState() { super.initState(); + WidgetsBinding.instance.addObserver(this); _futureBuilderFuture = _whisperDetailController.querySessionMsg(); + _replyContentController = _whisperDetailController.replyContentController; + _focuslistener(); + } + + _focuslistener() { + replyContentFocusNode.addListener(() { + if (replyContentFocusNode.hasFocus) { + setState(() { + toolbarType = 'input'; + }); + } + }); + } + + @override + void didChangeMetrics() { + super.didChangeMetrics(); + final String routePath = Get.currentRoute; + if (mounted && routePath.startsWith('/whisper_detail')) { + WidgetsBinding.instance.addPostFrameCallback((_) { + // 键盘高度 + final viewInsets = EdgeInsets.fromViewPadding( + View.of(context).viewInsets, View.of(context).devicePixelRatio); + _debouncer.run(() { + if (mounted) { + if (keyboardHeight == 0) { + setState(() { + emoteHeight = keyboardHeight = + keyboardHeight == 0.0 ? viewInsets.bottom : keyboardHeight; + }); + } + } + }); + }); + } + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + replyContentFocusNode.removeListener(() {}); + replyContentFocusNode.dispose(); + super.dispose(); } @override @@ -89,55 +144,63 @@ class _WhisperDetailPageState extends State { ), ), ), - body: FutureBuilder( - future: _futureBuilderFuture, - builder: (BuildContext context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - if (snapshot.data == null) { - return const SizedBox(); - } - final Map data = snapshot.data as Map; - if (data['status']) { - List messageList = _whisperDetailController.messageList; - return Obx( - () => messageList.isEmpty - ? const SizedBox() - : ListView.builder( - itemCount: messageList.length, - shrinkWrap: true, - reverse: true, - itemBuilder: (_, int i) { - if (i == 0) { - return Column( - children: [ - ChatItem( - item: messageList[i], - e_infos: _whisperDetailController.eInfos), - const SizedBox(height: 12), - ], - ); - } else { - return ChatItem( - item: messageList[i], - e_infos: _whisperDetailController.eInfos); - } - }, - ), - ); + body: GestureDetector( + onTap: () { + FocusScope.of(context).unfocus(); + setState(() { + keyboardHeight = 0; + }); + }, + child: FutureBuilder( + future: _futureBuilderFuture, + builder: (BuildContext context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.data == null) { + return const SizedBox(); + } + final Map data = snapshot.data as Map; + if (data['status']) { + List messageList = _whisperDetailController.messageList; + return Obx( + () => messageList.isEmpty + ? const SizedBox() + : ListView.builder( + itemCount: messageList.length, + shrinkWrap: true, + reverse: true, + itemBuilder: (_, int i) { + if (i == 0) { + return Column( + children: [ + ChatItem( + item: messageList[i], + e_infos: _whisperDetailController.eInfos), + const SizedBox(height: 12), + ], + ); + } else { + return ChatItem( + item: messageList[i], + e_infos: _whisperDetailController.eInfos); + } + }, + ), + ); + } else { + // 请求错误 + return const SizedBox(); + } } else { - // 请求错误 + // 骨架屏 return const SizedBox(); } - } else { - // 骨架屏 - return const SizedBox(); - } - }, + }, + ), ), // resizeToAvoidBottomInset: true, bottomNavigationBar: Container( width: double.infinity, - height: MediaQuery.of(context).padding.bottom + 70, + height: MediaQuery.of(context).padding.bottom + 70 + keyboardHeight, padding: EdgeInsets.only( left: 8, right: 12, @@ -152,48 +215,102 @@ class _WhisperDetailPageState extends State { ), ), ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, + child: Column( children: [ - // IconButton( - // onPressed: () {}, - // icon: Icon( - // Icons.add_circle_outline, - // color: Theme.of(context).colorScheme.outline, - // ), - // ), - IconButton( - onPressed: () {}, - icon: Icon( - Icons.emoji_emotions_outlined, - color: Theme.of(context).colorScheme.outline, - ), - ), - Expanded( - child: Container( - height: 45, - decoration: BoxDecoration( - color: - Theme.of(context).colorScheme.primary.withOpacity(0.08), - borderRadius: BorderRadius.circular(40.0), + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // IconButton( + // onPressed: () {}, + // icon: Icon( + // Icons.add_circle_outline, + // color: Theme.of(context).colorScheme.outline, + // ), + // ), + IconButton( + onPressed: () { + // if (toolbarType == 'input') { + // setState(() { + // toolbarType = 'emote'; + // }); + // } + // FocusScope.of(context).unfocus(); + }, + icon: Icon( + Icons.emoji_emotions_outlined, + color: Theme.of(context).colorScheme.outline, + ), ), - child: TextField( - readOnly: true, - style: Theme.of(context).textTheme.titleMedium, - decoration: const InputDecoration( - border: InputBorder.none, // 移除默认边框 - hintText: '开发中 ...', // 提示文本 - contentPadding: EdgeInsets.symmetric( - horizontal: 16.0, vertical: 12.0), // 内边距 + Expanded( + child: Container( + height: 45, + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .primary + .withOpacity(0.08), + borderRadius: BorderRadius.circular(40.0), + ), + child: TextField( + readOnly: true, + style: Theme.of(context).textTheme.titleMedium, + controller: _replyContentController, + autofocus: false, + focusNode: replyContentFocusNode, + decoration: const InputDecoration( + border: InputBorder.none, // 移除默认边框 + hintText: '开发中 ...', // 提示文本 + contentPadding: EdgeInsets.symmetric( + horizontal: 16.0, vertical: 12.0), // 内边距 + ), + ), ), ), + IconButton( + // onPressed: _whisperDetailController.sendMsg, + onPressed: null, + icon: Icon( + Icons.send, + color: Theme.of(context).colorScheme.outline, + ), + ), + // const SizedBox(width: 16), + ], + ), + AnimatedSize( + curve: Curves.easeInOut, + duration: const Duration(milliseconds: 300), + child: SizedBox( + width: double.infinity, + height: toolbarType == 'input' ? keyboardHeight : emoteHeight, + child: EmotePanel( + onChoose: (package, emote) => {}, + ), ), ), - const SizedBox(width: 16), ], ), ), ); } } + +typedef DebounceCallback = void Function(); + +class Debouncer { + DebounceCallback? callback; + final int? milliseconds; + Timer? _timer; + + Debouncer({this.milliseconds}); + + run(DebounceCallback callback) { + if (_timer != null) { + _timer!.cancel(); + } + _timer = Timer(Duration(milliseconds: milliseconds!), () { + callback(); + }); + } +} diff --git a/lib/pages/whisper_detail/widget/chat_item.dart b/lib/pages/whisper_detail/widget/chat_item.dart index 0925d5694..4fd492546 100644 --- a/lib/pages/whisper_detail/widget/chat_item.dart +++ b/lib/pages/whisper_detail/widget/chat_item.dart @@ -204,7 +204,7 @@ class ChatItem extends StatelessWidget { final int cid = await SearchHttp.ab2c(bvid: bvid); final String heroTag = Utils.makeHeroTag(bvid); SmartDialog.dismiss().then( - (e) => Get.toNamed('/video?bvid=$bvid&cid=$cid', + (e) => Get.toNamed('/video?bvid=$bvid&cid=$cid', arguments: { 'pic': content['thumb'], 'heroTag': heroTag, @@ -352,7 +352,9 @@ class ChatItem extends StatelessWidget { )); default: return Text( - content['content'] ?? content.toString(), + content != null && content != '' + ? (content['content'] ?? content.toString()) + : '不支持的消息类型', style: TextStyle( letterSpacing: 0.6, height: 1.5, diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index dfa580ab6..b385fca8c 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -21,6 +21,8 @@ import 'package:pilipala/utils/storage.dart'; import 'package:screen_brightness/screen_brightness.dart'; import 'package:status_bar_control/status_bar_control.dart'; import 'package:universal_platform/universal_platform.dart'; +import '../../models/video/subTitile/content.dart'; +import '../../models/video/subTitile/result.dart'; // import 'package:wakelock_plus/wakelock_plus.dart'; Box videoStorage = GStrorage.video; @@ -73,6 +75,8 @@ class PlPlayerController { final Rx _doubleSpeedStatus = false.obs; final Rx _controlsLock = false.obs; final Rx _isFullScreen = false.obs; + final Rx _subTitleOpen = false.obs; + final Rx _subTitleCode = (-1).obs; // 默认投稿视频格式 static Rx _videoType = 'archive'.obs; @@ -118,6 +122,7 @@ class PlPlayerController { PreferredSizeWidget? headerControl; PreferredSizeWidget? bottomControl; Widget? danmuWidget; + late RxList subtitles; /// 数据加载监听 Stream get onDataStatusChanged => dataStatus.status.stream; @@ -147,6 +152,11 @@ class PlPlayerController { Rx get mute => _mute; Stream get onMuteChanged => _mute.stream; + /// 字幕开启状态 + Rx get subTitleOpen => _subTitleOpen; + Rx get subTitleCode => _subTitleCode; + // Stream get onSubTitleOpenChanged => _subTitleOpen.stream; + /// [videoPlayerController] instace of Player Player? get videoPlayerController => _videoPlayerController; @@ -231,6 +241,10 @@ class PlPlayerController { // 播放顺序相关 PlayRepeat playRepeat = PlayRepeat.pause; + RxList subtitleContents = + [].obs; + RxString subtitleContent = ''.obs; + void updateSliderPositionSecond() { int newSecond = _sliderPosition.value.inSeconds; if (sliderPositionSeconds.value != newSecond) { @@ -277,8 +291,7 @@ class PlPlayerController { danmakuDurationVal = localCache.get(LocalCacheKey.danmakuDuration, defaultValue: 4.0); // 描边粗细 - strokeWidth = - localCache.get(LocalCacheKey.strokeWidth, defaultValue: 1.5); + strokeWidth = localCache.get(LocalCacheKey.strokeWidth, defaultValue: 1.5); playRepeat = PlayRepeat.values.toList().firstWhere( (e) => e.value == @@ -293,11 +306,19 @@ class PlPlayerController { _longPressSpeed.value = videoStorage .get(VideoBoxKey.longPressSpeedDefault, defaultValue: 2.0); } + // 自定义倍速集合 speedsList = List.from(videoStorage .get(VideoBoxKey.customSpeedsList, defaultValue: [])); - for (final PlaySpeed i in PlaySpeed.values) { - speedsList.add(i.value); - } + // 默认倍速 + speedsList = List.from(videoStorage + .get(VideoBoxKey.customSpeedsList, defaultValue: [])); + //playSpeedSystem + final List playSpeedSystem = + videoStorage.get(VideoBoxKey.playSpeedSystem, defaultValue: playSpeed); + + // for (final PlaySpeed i in PlaySpeed.values) { + speedsList.addAll(playSpeedSystem); + // } // _playerEventSubs = onPlayerStatusChanged.listen((PlayerStatus status) { // if (status == PlayerStatus.playing) { @@ -343,6 +364,8 @@ class PlPlayerController { bool enableHeart = true, // 是否首次加载 bool isFirstTime = true, + // 是否开启字幕 + bool enableSubTitle = false, }) async { try { _autoPlay = autoplay; @@ -357,7 +380,9 @@ class PlPlayerController { _cid = cid; _enableHeart = enableHeart; _isFirstTime = isFirstTime; - + _subTitleOpen.value = enableSubTitle; + subtitles = [].obs; + subtitleContent.value = ''; if (_videoPlayerController != null && _videoPlayerController!.state.playing) { await pause(notify: false); @@ -535,8 +560,10 @@ class PlPlayerController { if (event) { playerStatus.status.value = PlayerStatus.playing; } else { - // playerStatus.status.value = PlayerStatus.paused; + playerStatus.status.value = PlayerStatus.paused; } + videoPlayerServiceHandler.onStatusChange( + playerStatus.status.value, isBuffering.value); /// 触发回调事件 for (var element in _statusListeners) { @@ -566,6 +593,8 @@ class PlPlayerController { _sliderPosition.value = event; updateSliderPositionSecond(); } + querySubtitleContent( + videoPlayerController!.state.position.inSeconds.toDouble()); /// 触发回调事件 for (var element in _positionListeners) { @@ -600,6 +629,10 @@ class PlPlayerController { const Duration(seconds: 1), () => videoPlayerServiceHandler.onPositionChange(event)); }), + + // onSubTitleOpenChanged.listen((bool event) { + // toggleSubtitle(event ? subTitleCode.value : -1); + // }) ], ); } @@ -675,18 +708,6 @@ class PlPlayerController { _playbackSpeed.value = speed; } - /// 设置倍速 - // Future togglePlaybackSpeed() async { - // List allowedSpeeds = - // PlaySpeed.values.map((e) => e.value).toList(); - // int index = allowedSpeeds.indexOf(_playbackSpeed.value); - // if (index < allowedSpeeds.length - 1) { - // setPlaybackSpeed(allowedSpeeds[index + 1]); - // } else { - // setPlaybackSpeed(allowedSpeeds[0]); - // } - // } - /// 播放视频 /// TODO _duration.value丢失 Future play( @@ -1050,12 +1071,61 @@ class PlPlayerController { } } + /// 字幕 + void toggleSubtitle(int code) { + _subTitleOpen.value = code != -1; + _subTitleCode.value = code; + // if (code == -1) { + // // 关闭字幕 + // _subTitleOpen.value = false; + // _subTitleCode.value = code; + // _videoPlayerController?.setSubtitleTrack(SubtitleTrack.no()); + // return; + // } + // final SubTitlteItemModel? subtitle = subtitles?.firstWhereOrNull( + // (element) => element.code == code, + // ); + // _subTitleOpen.value = true; + // _subTitleCode.value = code; + // _videoPlayerController?.setSubtitleTrack( + // SubtitleTrack.data( + // subtitle!.content!, + // title: subtitle.title, + // language: subtitle.lan, + // ), + // ); + } + + void querySubtitleContent(double progress) { + if (subTitleCode.value == -1) { + subtitleContent.value = ''; + return; + } + if (subtitles.isEmpty) { + return; + } + final SubTitlteItemModel? subtitle = subtitles.firstWhereOrNull( + (element) => element.code == subTitleCode.value, + ); + if (subtitle != null && subtitle.body!.isNotEmpty) { + for (var content in subtitle.body!) { + if (progress >= content['from']! && progress <= content['to']!) { + subtitleContent.value = content['content']!; + return; + } + } + } + } + setPlayRepeat(PlayRepeat type) { playRepeat = type; videoStorage.put(VideoBoxKey.playRepeat, type.value); } Future dispose({String type = 'single'}) async { + print('dispose'); + print('dispose: ${playerCount.value}'); + // 每次减1,最后销毁 if (type == 'single' && playerCount.value > 1) { _playerCount.value -= 1; @@ -1065,6 +1135,7 @@ class PlPlayerController { } _playerCount.value = 0; try { + print('dispose dispose ---------'); _timer?.cancel(); _timerForVolume?.cancel(); _timerForGettingVolume?.cancel(); diff --git a/lib/plugin/pl_player/models/bottom_control_type.dart b/lib/plugin/pl_player/models/bottom_control_type.dart new file mode 100644 index 000000000..739e1d383 --- /dev/null +++ b/lib/plugin/pl_player/models/bottom_control_type.dart @@ -0,0 +1,11 @@ +enum BottomControlType { + pre, + playOrPause, + next, + time, + space, + fit, + speed, + fullscreen, + custom, +} diff --git a/lib/plugin/pl_player/models/play_speed.dart b/lib/plugin/pl_player/models/play_speed.dart index fd699eaff..8bb251182 100644 --- a/lib/plugin/pl_player/models/play_speed.dart +++ b/lib/plugin/pl_player/models/play_speed.dart @@ -1,39 +1,15 @@ -enum PlaySpeed { - pointTwoFive, - pointFive, - pointSevenFive, +List generatePlaySpeedList() { + List playSpeed = []; + double startSpeed = 0.25; + double endSpeed = 2.0; + double increment = 0.25; - one, - onePointTwoFive, - onePointFive, - onePointSevenFive, + for (double speed = startSpeed; speed <= endSpeed; speed += increment) { + playSpeed.add(speed); + } - two, + return playSpeed; } -extension PlaySpeedExtension on PlaySpeed { - static final List _descList = [ - '0.25', - '0.5', - '0.75', - '正常', - '1.25', - '1.5', - '1.75', - '2.0', - ]; - String get description => _descList[index]; - - static final List _valueList = [ - 0.25, - 0.5, - 0.75, - 1.0, - 1.25, - 1.5, - 1.75, - 2.0, - ]; - double get value => _valueList[index]; - double get defaultValue => _valueList[3]; -} +// 导出 playSpeed 列表 +List playSpeed = generatePlaySpeedList(); diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view.dart index d073945b7..be24b1050 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:audio_video_progress_bar/audio_video_progress_bar.dart'; +import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/material.dart'; import 'package:flutter_volume_controller/flutter_volume_controller.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -8,7 +9,7 @@ import 'package:get/get.dart'; import 'package:hive/hive.dart'; import 'package:media_kit/media_kit.dart'; import 'package:media_kit_video/media_kit_video.dart'; -import 'package:nil/nil.dart'; +import 'package:pilipala/models/common/gesture_mode.dart'; import 'package:pilipala/plugin/pl_player/controller.dart'; import 'package:pilipala/plugin/pl_player/models/duration.dart'; import 'package:pilipala/plugin/pl_player/models/fullscreen_mode.dart'; @@ -17,12 +18,15 @@ import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/storage.dart'; import 'package:screen_brightness/screen_brightness.dart'; +import '../../utils/global_data.dart'; +import 'models/bottom_control_type.dart'; import 'models/bottom_progress_behavior.dart'; import 'widgets/app_bar_ani.dart'; import 'widgets/backward_seek.dart'; import 'widgets/bottom_control.dart'; import 'widgets/common_btn.dart'; import 'widgets/forward_seek.dart'; +import 'widgets/play_pause_btn.dart'; class PLVideoPlayer extends StatefulWidget { const PLVideoPlayer({ @@ -30,6 +34,9 @@ class PLVideoPlayer extends StatefulWidget { this.headerControl, this.bottomControl, this.danmuWidget, + this.bottomList, + this.customWidget, + this.customWidgets, super.key, }); @@ -37,6 +44,11 @@ class PLVideoPlayer extends StatefulWidget { final PreferredSizeWidget? headerControl; final PreferredSizeWidget? bottomControl; final Widget? danmuWidget; + final List? bottomList; + // List or Widget + + final Widget? customWidget; + final List? customWidgets; @override State createState() => _PLVideoPlayerState(); @@ -46,26 +58,22 @@ class _PLVideoPlayerState extends State with TickerProviderStateMixin { late AnimationController animationController; late VideoController videoController; - final PLVideoPlayerController _ctr = Get.put(PLVideoPlayerController()); - // bool _mountSeekBackwardButton = false; - // bool _mountSeekForwardButton = false; - // bool _hideSeekBackwardButton = false; - // bool _hideSeekForwardButton = false; + final RxBool _mountSeekBackwardButton = false.obs; + final RxBool _mountSeekForwardButton = false.obs; + final RxBool _hideSeekBackwardButton = false.obs; + final RxBool _hideSeekForwardButton = false.obs; - // double _brightnessValue = 0.0; - // bool _brightnessIndicator = false; + final RxDouble _brightnessValue = 0.0.obs; + final RxBool _brightnessIndicator = false.obs; Timer? _brightnessTimer; - // double _volumeValue = 0.0; - // bool _volumeIndicator = false; + final RxDouble _volumeValue = 0.0.obs; + final RxBool _volumeIndicator = false.obs; Timer? _volumeTimer; - double _distance = 0.0; - // 初始手指落下位置 - // double _initTapPositoin = 0.0; - - // bool _volumeInterceptEventStream = false; + final RxDouble _distance = 0.0.obs; + final RxBool _volumeInterceptEventStream = false.obs; Box setting = GStrorage.setting; late FullScreenMode mode; @@ -73,16 +81,18 @@ class _PLVideoPlayerState extends State late bool enableQuickDouble; late bool enableBackgroundPlay; late double screenWidth; + final FullScreenGestureMode fullScreenGestureMode = + GlobalData().fullScreenGestureMode; // 用于记录上一次全屏切换手势触发时间,避免误触 DateTime? lastFullScreenToggleTime; void onDoubleTapSeekBackward() { - _ctr.onDoubleTapSeekBackward(); + _mountSeekBackwardButton.value = true; } void onDoubleTapSeekForward() { - _ctr.onDoubleTapSeekForward(); + _mountSeekForwardButton.value = true; } // 双击播放、暂停 @@ -116,7 +126,11 @@ class _PLVideoPlayerState extends State super.initState(); screenWidth = Get.size.width; animationController = AnimationController( - vsync: this, duration: const Duration(milliseconds: 300)); + vsync: this, + duration: GlobalData().enablePlayerControlAnimation + ? const Duration(milliseconds: 150) + : const Duration(milliseconds: 10), + ); videoController = widget.controller.videoController!; widget.controller.headerControl = widget.headerControl; widget.controller.bottomControl = widget.bottomControl; @@ -130,10 +144,10 @@ class _PLVideoPlayerState extends State Future.microtask(() async { try { FlutterVolumeController.updateShowSystemUI(true); - _ctr.volumeValue.value = (await FlutterVolumeController.getVolume())!; + _volumeValue.value = (await FlutterVolumeController.getVolume())!; FlutterVolumeController.addListener((double value) { - if (mounted && !_ctr.volumeInterceptEventStream.value) { - _ctr.volumeValue.value = value; + if (mounted && !_volumeInterceptEventStream.value) { + _volumeValue.value = value; } }); } catch (_) {} @@ -141,10 +155,10 @@ class _PLVideoPlayerState extends State Future.microtask(() async { try { - _ctr.brightnessValue.value = await ScreenBrightness().current; + _brightnessValue.value = await ScreenBrightness().current; ScreenBrightness().onCurrentBrightnessChanged.listen((double value) { if (mounted) { - _ctr.brightnessValue.value = value; + _brightnessValue.value = value; } }); } catch (_) {} @@ -156,14 +170,14 @@ class _PLVideoPlayerState extends State FlutterVolumeController.updateShowSystemUI(false); await FlutterVolumeController.setVolume(value); } catch (_) {} - _ctr.volumeValue.value = value; - _ctr.volumeIndicator.value = true; - _ctr.volumeInterceptEventStream.value = true; + _volumeValue.value = value; + _volumeIndicator.value = true; + _volumeInterceptEventStream.value = true; _volumeTimer?.cancel(); _volumeTimer = Timer(const Duration(milliseconds: 200), () { if (mounted) { - _ctr.volumeIndicator.value = false; - _ctr.volumeInterceptEventStream.value = false; + _volumeIndicator.value = false; + _volumeInterceptEventStream.value = false; } }); } @@ -172,11 +186,11 @@ class _PLVideoPlayerState extends State try { await ScreenBrightness().setScreenBrightness(value); } catch (_) {} - _ctr.brightnessIndicator.value = true; + _brightnessIndicator.value = true; _brightnessTimer?.cancel(); _brightnessTimer = Timer(const Duration(milliseconds: 200), () { if (mounted) { - _ctr.brightnessIndicator.value = false; + _brightnessIndicator.value = false; } }); widget.controller.brightness.value = value; @@ -189,6 +203,143 @@ class _PLVideoPlayerState extends State super.dispose(); } + // 动态构建底部控制条 + List buildBottomControl() { + const TextStyle textStyle = TextStyle( + color: Colors.white, + fontSize: 12, + ); + final PlPlayerController _ = widget.controller; + Map videoProgressWidgets = { + /// 上一集 + BottomControlType.pre: ComBtn( + icon: const Icon( + Icons.skip_previous_outlined, + size: 15, + color: Colors.white, + ), + fuc: () {}, + ), + + /// 播放暂停 + BottomControlType.playOrPause: PlayOrPauseButton( + controller: _, + ), + + /// 下一集 + BottomControlType.next: ComBtn( + icon: const Icon( + Icons.last_page_outlined, + size: 15, + color: Colors.white, + ), + fuc: () {}, + ), + + /// 时间进度 + BottomControlType.time: Row( + children: [ + Obx(() { + return Text( + _.durationSeconds.value >= 3600 + ? printDurationWithHours( + Duration(seconds: _.positionSeconds.value)) + : printDuration(Duration(seconds: _.positionSeconds.value)), + style: textStyle, + ); + }), + const SizedBox(width: 2), + const Text('/', style: textStyle), + const SizedBox(width: 2), + Obx( + () => Text( + _.durationSeconds.value >= 3600 + ? printDurationWithHours( + Duration(seconds: _.durationSeconds.value)) + : printDuration(Duration(seconds: _.durationSeconds.value)), + style: textStyle, + ), + ), + ], + ), + + /// 空白占位 + BottomControlType.space: const Spacer(), + + /// 画面比例 + BottomControlType.fit: SizedBox( + height: 30, + child: TextButton( + onPressed: () => _.toggleVideoFit(), + style: ButtonStyle( + padding: MaterialStateProperty.all(EdgeInsets.zero), + ), + child: Obx( + () => Text( + _.videoFitDEsc.value, + style: const TextStyle(color: Colors.white, fontSize: 13), + ), + ), + ), + ), + + /// 播放速度 + BottomControlType.speed: SizedBox( + width: 45, + height: 34, + child: TextButton( + style: ButtonStyle( + padding: MaterialStateProperty.all(EdgeInsets.zero), + ), + onPressed: () {}, + child: Obx( + () => Text( + '${_.playbackSpeed.toString()}X', + style: textStyle, + ), + ), + ), + ), + + /// 字幕 + /// 全屏 + BottomControlType.fullscreen: ComBtn( + icon: Obx( + () => Icon( + _.isFullScreen.value + ? FontAwesomeIcons.compress + : FontAwesomeIcons.expand, + size: 15, + color: Colors.white, + ), + ), + fuc: () => _.triggerFullScreen(), + ), + }; + final List list = []; + List userSpecifyItem = widget.bottomList ?? + [ + BottomControlType.playOrPause, + BottomControlType.time, + BottomControlType.space, + BottomControlType.fit, + BottomControlType.fullscreen, + ]; + for (var i = 0; i < userSpecifyItem.length; i++) { + if (userSpecifyItem[i] == BottomControlType.custom) { + if (widget.customWidget != null && widget.customWidget is Widget) { + list.add(widget.customWidget!); + } + if (widget.customWidgets != null && widget.customWidgets!.isNotEmpty) { + list.addAll(widget.customWidgets!); + } + } else { + list.add(videoProgressWidgets[userSpecifyItem[i]]!); + } + } + return list; + } + @override Widget build(BuildContext context) { final PlPlayerController _ = widget.controller; @@ -211,6 +362,7 @@ class _PLVideoPlayerState extends State children: [ Obx( () => Video( + key: ValueKey(_.videoFit.value), controller: videoController, controls: NoVideoControls, pauseUponEnteringBackgroundMode: !enableBackgroundPlay, @@ -308,7 +460,7 @@ class _PLVideoPlayerState extends State () => Align( child: AnimatedOpacity( curve: Curves.easeInOut, - opacity: _ctr.volumeIndicator.value ? 1.0 : 0.0, + opacity: _volumeIndicator.value ? 1.0 : 0.0, duration: const Duration(milliseconds: 150), child: Container( alignment: Alignment.center, @@ -327,9 +479,9 @@ class _PLVideoPlayerState extends State width: 28.0, alignment: Alignment.centerRight, child: Icon( - _ctr.volumeValue.value == 0.0 + _volumeValue.value == 0.0 ? Icons.volume_off - : _ctr.volumeValue.value < 0.5 + : _volumeValue.value < 0.5 ? Icons.volume_down : Icons.volume_up, color: const Color(0xFFFFFFFF), @@ -338,7 +490,7 @@ class _PLVideoPlayerState extends State ), Expanded( child: Text( - '${(_ctr.volumeValue.value * 100.0).round()}%', + '${(_volumeValue.value * 100.0).round()}%', textAlign: TextAlign.center, style: const TextStyle( fontSize: 13.0, @@ -359,7 +511,7 @@ class _PLVideoPlayerState extends State () => Align( child: AnimatedOpacity( curve: Curves.easeInOut, - opacity: _ctr.brightnessIndicator.value ? 1.0 : 0.0, + opacity: _brightnessIndicator.value ? 1.0 : 0.0, duration: const Duration(milliseconds: 150), child: Container( alignment: Alignment.center, @@ -378,9 +530,9 @@ class _PLVideoPlayerState extends State width: 28.0, alignment: Alignment.centerRight, child: Icon( - _ctr.brightnessValue.value < 1.0 / 3.0 + _brightnessValue.value < 1.0 / 3.0 ? Icons.brightness_low - : _ctr.brightnessValue.value < 2.0 / 3.0 + : _brightnessValue.value < 2.0 / 3.0 ? Icons.brightness_medium : Icons.brightness_high, color: const Color(0xFFFFFFFF), @@ -390,7 +542,7 @@ class _PLVideoPlayerState extends State const SizedBox(width: 2.0), Expanded( child: Text( - '${(_ctr.brightnessValue.value * 100.0).round()}%', + '${(_brightnessValue.value * 100.0).round()}%', textAlign: TextAlign.center, style: const TextStyle( fontSize: 13.0, @@ -428,6 +580,45 @@ class _PLVideoPlayerState extends State if (widget.danmuWidget != null) Positioned.fill(top: 4, child: widget.danmuWidget!), + /// 开启且有字幕时展示 + Stack( + children: [ + Positioned( + left: 0, + right: 0, + bottom: 30, + child: Align( + alignment: Alignment.center, + child: Obx( + () => Visibility( + visible: widget.controller.subTitleCode.value != -1, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: widget.controller.subtitleContent.value != '' + ? Colors.black.withOpacity(0.6) + : Colors.transparent, + ), + padding: widget.controller.subTitleCode.value != -1 + ? const EdgeInsets.symmetric( + horizontal: 10, + vertical: 4, + ) + : EdgeInsets.zero, + child: Text( + widget.controller.subtitleContent.value, + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + )), + ), + ), + ), + ], + ), + /// 手势 Positioned.fill( left: 16, @@ -481,7 +672,6 @@ class _PLVideoPlayerState extends State pos.clamp(Duration.zero, _.duration.value); _.onUpdatedSliderProgress(result); _.onChangedSliderStart(); - // _initTapPositoin = tapPosition; }, onHorizontalDragEnd: (DragEndDetails details) { if (_.videoType.value == 'live' || _.controlsLock.value) { @@ -513,38 +703,43 @@ class _PLVideoPlayerState extends State : screenWidth * 9 / 16) * 3; final double brightness = - _ctr.brightnessValue.value - delta / level; + _brightnessValue.value - delta / level; final double result = brightness.clamp(0.0, 1.0); setBrightness(result); } else if (tapPosition < sectionWidth * 2) { // 全屏 final double dy = details.delta.dy; const double threshold = 7.0; // 滑动阈值 - if (dy > _distance && dy > threshold) { - if (_.isFullScreen.value) { + final bool flag = + fullScreenGestureMode != FullScreenGestureMode.values.last; + if (dy > _distance.value && dy > threshold) { + if (_.isFullScreen.value ^ flag) { lastFullScreenToggleTime = DateTime.now(); // 下滑退出全屏 - await widget.controller.triggerFullScreen(status: false); + await widget.controller.triggerFullScreen(status: flag); } - _distance = 0.0; - } else if (dy < _distance && dy < -threshold) { - if (!_.isFullScreen.value) { + _distance.value = 0.0; + } else if (dy < _distance.value && dy < -threshold) { + if (!_.isFullScreen.value ^ flag) { lastFullScreenToggleTime = DateTime.now(); // 上滑进入全屏 - await widget.controller.triggerFullScreen(); + await widget.controller.triggerFullScreen(status: !flag); } - _distance = 0.0; + _distance.value = 0.0; } - _distance = dy; + _distance.value = dy; } else { // 右边区域 👈 - final double level = (_.isFullScreen.value - ? Get.size.height - : screenWidth * 9 / 16) * - 3; - final double volume = _ctr.volumeValue.value - delta / level; - final double result = volume.clamp(0.0, 1.0); - setVolume(result); + EasyThrottle.throttle( + 'setVolume', const Duration(milliseconds: 20), () { + final double level = (_.isFullScreen.value + ? Get.size.height + : screenWidth * 9 / 16); + final double volume = _volumeValue.value - + double.parse(delta.toStringAsFixed(1)) / level; + final double result = volume.clamp(0.0, 1.0); + setVolume(result); + }); } }, onVerticalDragEnd: (DragEndDetails details) {}, @@ -575,9 +770,10 @@ class _PLVideoPlayerState extends State position: 'bottom', child: widget.bottomControl ?? BottomControl( - controller: widget.controller, - triggerFullScreen: - widget.controller.triggerFullScreen), + controller: widget.controller, + triggerFullScreen: _.triggerFullScreen, + buildBottomControl: buildBottomControl(), + ), ), ), ], @@ -597,23 +793,23 @@ class _PLVideoPlayerState extends State } if (defaultBtmProgressBehavior == BtmProgresBehavior.alwaysHide.code) { - return nil; + return const SizedBox(); } if (defaultBtmProgressBehavior == BtmProgresBehavior.onlyShowFullScreen.code && !_.isFullScreen.value) { - return nil; + return const SizedBox(); } else if (defaultBtmProgressBehavior == BtmProgresBehavior.onlyHideFullScreen.code && _.isFullScreen.value) { - return nil; + return const SizedBox(); } if (_.videoType.value == 'live') { return const SizedBox(); } if (value > max || max <= 0) { - return nil; + return const SizedBox(); } return Positioned( bottom: -1.5, @@ -662,7 +858,7 @@ class _PLVideoPlayerState extends State // 锁 Obx( () => Visibility( - visible: _.videoType.value != 'live', + visible: _.videoType.value != 'live' && _.isFullScreen.value, child: Align( alignment: Alignment.centerLeft, child: FractionalTranslation( @@ -710,18 +906,17 @@ class _PLVideoPlayerState extends State /// 点击 快进/快退 Obx( () => Visibility( - visible: _ctr.mountSeekBackwardButton.value || - _ctr.mountSeekForwardButton.value, + visible: + _mountSeekBackwardButton.value || _mountSeekForwardButton.value, child: Positioned.fill( child: Row( children: [ Expanded( - child: _ctr.mountSeekBackwardButton.value + child: _mountSeekBackwardButton.value ? TweenAnimationBuilder( tween: Tween( begin: 0.0, - end: - _ctr.hideSeekBackwardButton.value ? 0.0 : 1.0, + end: _hideSeekBackwardButton.value ? 0.0 : 1.0, ), duration: const Duration(milliseconds: 500), builder: (BuildContext context, double value, @@ -731,17 +926,15 @@ class _PLVideoPlayerState extends State child: child, ), onEnd: () { - if (_ctr.hideSeekBackwardButton.value) { - _ctr.hideSeekBackwardButton.value = false; - _ctr.mountSeekBackwardButton.value = false; + if (_hideSeekBackwardButton.value) { + _hideSeekBackwardButton.value = false; + _mountSeekBackwardButton.value = false; } }, child: BackwardSeekIndicator( - onChanged: (Duration value) { - // _seekBarDeltaValueNotifier.value = -value; - }, + onChanged: (Duration value) => {}, onSubmitted: (Duration value) { - _ctr.hideSeekBackwardButton.value = true; + _hideSeekBackwardButton.value = true; final Player player = widget.controller.videoPlayerController!; Duration result = player.state.position - value; @@ -754,7 +947,7 @@ class _PLVideoPlayerState extends State }, ), ) - : nil, + : const SizedBox(), ), Expanded( child: SizedBox( @@ -762,11 +955,11 @@ class _PLVideoPlayerState extends State ), ), Expanded( - child: _ctr.mountSeekForwardButton.value + child: _mountSeekForwardButton.value ? TweenAnimationBuilder( tween: Tween( begin: 0.0, - end: _ctr.hideSeekForwardButton.value ? 0.0 : 1.0, + end: _hideSeekForwardButton.value ? 0.0 : 1.0, ), duration: const Duration(milliseconds: 500), builder: (BuildContext context, double value, @@ -776,17 +969,15 @@ class _PLVideoPlayerState extends State child: child, ), onEnd: () { - if (_ctr.hideSeekForwardButton.value) { - _ctr.hideSeekForwardButton.value = false; - _ctr.mountSeekForwardButton.value = false; + if (_hideSeekForwardButton.value) { + _hideSeekForwardButton.value = false; + _mountSeekForwardButton.value = false; } }, child: ForwardSeekIndicator( - onChanged: (Duration value) { - // _seekBarDeltaValueNotifier.value = value; - }, + onChanged: (Duration value) => {}, onSubmitted: (Duration value) { - _ctr.hideSeekForwardButton.value = true; + _hideSeekForwardButton.value = true; final Player player = widget.controller.videoPlayerController!; Duration result = player.state.position + value; @@ -799,7 +990,7 @@ class _PLVideoPlayerState extends State }, ), ) - : nil, + : const SizedBox(), ), ], ), @@ -810,31 +1001,3 @@ class _PLVideoPlayerState extends State ); } } - -class PLVideoPlayerController extends GetxController { - RxBool mountSeekBackwardButton = false.obs; - RxBool mountSeekForwardButton = false.obs; - RxBool hideSeekBackwardButton = false.obs; - RxBool hideSeekForwardButton = false.obs; - - RxDouble brightnessValue = 0.0.obs; - RxBool brightnessIndicator = false.obs; - - RxDouble volumeValue = 0.0.obs; - RxBool volumeIndicator = false.obs; - - RxDouble distance = 0.0.obs; - // 初始手指落下位置 - RxDouble initTapPositoin = 0.0.obs; - - RxBool volumeInterceptEventStream = false.obs; - - // 双击快进 展示样式 - void onDoubleTapSeekForward() { - mountSeekForwardButton.value = true; - } - - void onDoubleTapSeekBackward() { - mountSeekBackwardButton.value = true; - } -} diff --git a/lib/plugin/pl_player/widgets/app_bar_ani.dart b/lib/plugin/pl_player/widgets/app_bar_ani.dart index 9a3af2670..53eaad16b 100644 --- a/lib/plugin/pl_player/widgets/app_bar_ani.dart +++ b/lib/plugin/pl_player/widgets/app_bar_ani.dart @@ -19,11 +19,11 @@ class AppBarAni extends StatelessWidget implements PreferredSizeWidget { @override Widget build(BuildContext context) { - visible ? controller.reverse() : controller.forward(); + visible ? controller.forward() : controller.reverse(); return SlideTransition( position: Tween( - begin: Offset.zero, - end: Offset(0, position! == 'top' ? -1 : 1), + begin: Offset(0, position! == 'top' ? -1 : 1), + end: Offset.zero, ).animate(CurvedAnimation( parent: controller, curve: Curves.linear, diff --git a/lib/plugin/pl_player/widgets/bottom_control.dart b/lib/plugin/pl_player/widgets/bottom_control.dart index 8f21fc175..ebb71b54c 100644 --- a/lib/plugin/pl_player/widgets/bottom_control.dart +++ b/lib/plugin/pl_player/widgets/bottom_control.dart @@ -1,17 +1,20 @@ import 'package:audio_video_progress_bar/audio_video_progress_bar.dart'; import 'package:flutter/material.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; import 'package:nil/nil.dart'; import 'package:pilipala/plugin/pl_player/index.dart'; -import 'package:pilipala/plugin/pl_player/widgets/play_pause_btn.dart'; import 'package:pilipala/utils/feed_back.dart'; class BottomControl extends StatelessWidget implements PreferredSizeWidget { final PlPlayerController? controller; final Function? triggerFullScreen; - const BottomControl({this.controller, this.triggerFullScreen, Key? key}) - : super(key: key); + final List? buildBottomControl; + const BottomControl({ + this.controller, + this.triggerFullScreen, + this.buildBottomControl, + Key? key, + }) : super(key: key); @override Size get preferredSize => const Size(double.infinity, kToolbarHeight); @@ -20,11 +23,6 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget { Widget build(BuildContext context) { Color colorTheme = Theme.of(context).colorScheme.primary; final _ = controller!; - const textStyle = TextStyle( - color: Colors.white, - fontSize: 12, - ); - return Container( color: Colors.transparent, height: 90, @@ -71,86 +69,89 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget { }, ), Row( - children: [ - PlayOrPauseButton( - controller: _, - ), - const SizedBox(width: 4), - // 播放时间 - Obx(() { - return Text( - _.durationSeconds.value >= 3600 - ? printDurationWithHours( - Duration(seconds: _.positionSeconds.value)) - : printDuration( - Duration(seconds: _.positionSeconds.value)), - style: textStyle, - ); - }), - const SizedBox(width: 2), - const Text('/', style: textStyle), - const SizedBox(width: 2), - Obx( - () => Text( - _.durationSeconds.value >= 3600 - ? printDurationWithHours( - Duration(seconds: _.durationSeconds.value)) - : printDuration( - Duration(seconds: _.durationSeconds.value)), - style: textStyle, - ), - ), - const Spacer(), - // 倍速 - // Obx( - // () => SizedBox( - // width: 45, - // height: 34, - // child: TextButton( - // style: ButtonStyle( - // padding: MaterialStateProperty.all(EdgeInsets.zero), - // ), - // onPressed: () { - // _.togglePlaybackSpeed(); - // }, - // child: Text( - // '${_.playbackSpeed.toString()}X', - // style: textStyle, - // ), - // ), - // ), - // ), - SizedBox( - height: 30, - child: TextButton( - onPressed: () => _.toggleVideoFit(), - style: ButtonStyle( - padding: MaterialStateProperty.all(EdgeInsets.zero), - ), - child: Obx( - () => Text( - _.videoFitDEsc.value, - style: const TextStyle(color: Colors.white, fontSize: 13), - ), - ), - ), - ), - const SizedBox(width: 10), - // 全屏 - Obx( - () => ComBtn( - icon: Icon( - _.isFullScreen.value - ? FontAwesomeIcons.compress - : FontAwesomeIcons.expand, - size: 15, - color: Colors.white, - ), - fuc: () => triggerFullScreen!(), - ), - ), - ], + children: [...buildBottomControl!], ), + // Row( + // children: [ + // PlayOrPauseButton( + // controller: _, + // ), + // const SizedBox(width: 4), + // // 播放时间 + // Obx(() { + // return Text( + // _.durationSeconds.value >= 3600 + // ? printDurationWithHours( + // Duration(seconds: _.positionSeconds.value)) + // : printDuration( + // Duration(seconds: _.positionSeconds.value)), + // style: textStyle, + // ); + // }), + // const SizedBox(width: 2), + // const Text('/', style: textStyle), + // const SizedBox(width: 2), + // Obx( + // () => Text( + // _.durationSeconds.value >= 3600 + // ? printDurationWithHours( + // Duration(seconds: _.durationSeconds.value)) + // : printDuration( + // Duration(seconds: _.durationSeconds.value)), + // style: textStyle, + // ), + // ), + // const Spacer(), + // // 倍速 + // // Obx( + // // () => SizedBox( + // // width: 45, + // // height: 34, + // // child: TextButton( + // // style: ButtonStyle( + // // padding: MaterialStateProperty.all(EdgeInsets.zero), + // // ), + // // onPressed: () { + // // _.togglePlaybackSpeed(); + // // }, + // // child: Text( + // // '${_.playbackSpeed.toString()}X', + // // style: textStyle, + // // ), + // // ), + // // ), + // // ), + // SizedBox( + // height: 30, + // child: TextButton( + // onPressed: () => _.toggleVideoFit(), + // style: ButtonStyle( + // padding: MaterialStateProperty.all(EdgeInsets.zero), + // ), + // child: Obx( + // () => Text( + // _.videoFitDEsc.value, + // style: const TextStyle(color: Colors.white, fontSize: 13), + // ), + // ), + // ), + // ), + // const SizedBox(width: 10), + // // 全屏 + // Obx( + // () => ComBtn( + // icon: Icon( + // _.isFullScreen.value + // ? FontAwesomeIcons.compress + // : FontAwesomeIcons.expand, + // size: 15, + // color: Colors.white, + // ), + // fuc: () => triggerFullScreen!(), + // ), + // ), + // ], + // ), const SizedBox(height: 12), ], ), diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index 6ebaa638c..1f1ea31e8 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -39,6 +39,7 @@ import '../pages/setting/pages/color_select.dart'; import '../pages/setting/pages/display_mode.dart'; import '../pages/setting/pages/font_size_select.dart'; import '../pages/setting/pages/home_tabbar_set.dart'; +import '../pages/setting/pages/play_gesture_set.dart'; import '../pages/setting/pages/play_speed_set.dart'; import '../pages/setting/recommend_setting.dart'; import '../pages/setting/play_setting.dart'; @@ -166,6 +167,9 @@ class Routes { CustomGetPage(name: '/subscription', page: () => const SubPage()), // 订阅详情 CustomGetPage(name: '/subDetail', page: () => const SubDetailPage()), + // 播放器手势 + CustomGetPage( + name: '/playerGestureSet', page: () => const PlayGesturePage()), ]; } diff --git a/lib/services/audio_handler.dart b/lib/services/audio_handler.dart index ad510e7df..bf98298b8 100644 --- a/lib/services/audio_handler.dart +++ b/lib/services/audio_handler.dart @@ -26,6 +26,7 @@ class VideoPlayerServiceHandler extends BaseAudioHandler with SeekHandler { static final List _item = []; Box setting = GStrorage.setting; bool enableBackgroundPlay = false; + PlPlayerController player = PlPlayerController.getInstance(); VideoPlayerServiceHandler() { revalidateSetting(); @@ -38,12 +39,12 @@ class VideoPlayerServiceHandler extends BaseAudioHandler with SeekHandler { @override Future play() async { - PlPlayerController.getInstance().play(); + player.play(); } @override Future pause() async { - PlPlayerController.getInstance().pause(); + player.pause(); } @override @@ -51,7 +52,7 @@ class VideoPlayerServiceHandler extends BaseAudioHandler with SeekHandler { playbackState.add(playbackState.value.copyWith( updatePosition: position, )); - await PlPlayerController.getInstance().seekTo(position); + await player.seekTo(position); } Future setMediaItem(MediaItem newMediaItem) async { @@ -149,6 +150,8 @@ class VideoPlayerServiceHandler extends BaseAudioHandler with SeekHandler { )); if (_item.isNotEmpty) { _item.removeLast(); + } + if (_item.isNotEmpty) { setMediaItem(_item.last); } if (_item.isEmpty) { diff --git a/lib/services/audio_session.dart b/lib/services/audio_session.dart index 57c42e053..ea83a30aa 100644 --- a/lib/services/audio_session.dart +++ b/lib/services/audio_session.dart @@ -20,7 +20,7 @@ class AudioSessionHandler { session.interruptionEventStream.listen((event) { final player = PlPlayerController.getInstance(); if (event.begin) { - if (player.playerStatus != PlayerStatus.playing) return; + if (!player.playerStatus.playing) return; switch (event.type) { case AudioInterruptionType.duck: player.setVolume(player.volume.value * 0.5); @@ -52,7 +52,7 @@ class AudioSessionHandler { // 耳机拔出暂停 session.becomingNoisyEventStream.listen((_) { final player = PlPlayerController.getInstance(); - if (player.playerStatus == PlayerStatus.playing) { + if (player.playerStatus.playing) { player.pause(); } }); diff --git a/lib/services/disable_battery_opt.dart b/lib/services/disable_battery_opt.dart new file mode 100644 index 000000000..ae0189775 --- /dev/null +++ b/lib/services/disable_battery_opt.dart @@ -0,0 +1,40 @@ +import 'dart:io'; + +import 'package:disable_battery_optimization/disable_battery_optimization.dart'; +import 'package:pilipala/utils/storage.dart'; + +void DisableBatteryOpt() async { + if (!Platform.isAndroid) { + return; + } + // 本地缓存中读取 是否禁用了电池优化 默认未禁用 + bool isDisableBatteryOptLocal = + GStrorage.localCache.get('isDisableBatteryOptLocal', defaultValue: false); + if (!isDisableBatteryOptLocal) { + final isBatteryOptimizationDisabled = + await DisableBatteryOptimization.isBatteryOptimizationDisabled; + if (isBatteryOptimizationDisabled == false) { + final hasDisabled = await DisableBatteryOptimization + .showDisableBatteryOptimizationSettings(); + // 设置为已禁用 + GStrorage.localCache.put('isDisableBatteryOptLocal', hasDisabled == true); + } + } + + bool isManufacturerBatteryOptimizationDisabled = GStrorage.localCache + .get('isManufacturerBatteryOptimizationDisabled', defaultValue: false); + if (!isManufacturerBatteryOptimizationDisabled) { + final isManBatteryOptimizationDisabled = await DisableBatteryOptimization + .isManufacturerBatteryOptimizationDisabled; + if (isManBatteryOptimizationDisabled == false) { + final hasDisabled = await DisableBatteryOptimization + .showDisableManufacturerBatteryOptimizationSettings( + "当前设备可能有额外的电池优化", + "按照步骤操作以禁用电池优化,以保证应用在后台正常运行", + ); + // 设置为已禁用 + GStrorage.localCache.put( + 'isManufacturerBatteryOptimizationDisabled', hasDisabled == true); + } + } +} diff --git a/lib/utils/app_scheme.dart b/lib/utils/app_scheme.dart index 6820050c9..bb9d556f9 100644 --- a/lib/utils/app_scheme.dart +++ b/lib/utils/app_scheme.dart @@ -5,6 +5,7 @@ import 'package:get/get.dart'; import '../http/search.dart'; import '../models/common/search_type.dart'; import 'id_utils.dart'; +import 'url_utils.dart'; import 'utils.dart'; class PiliSchame { @@ -38,23 +39,16 @@ class PiliSchame { final String path = value.path; if (scheme == 'bilibili') { - // bilibili://root if (host == 'root') { Navigator.popUntil( Get.context!, (Route route) => route.isFirst); - } - - // bilibili://space/{uid} - else if (host == 'space') { + } else if (host == 'space') { final String mid = path.split('/').last; Get.toNamed( '/member?mid=$mid', arguments: {'face': null}, ); - } - - // bilibili://video/{aid} - else if (host == 'video') { + } else if (host == 'video') { String pathQuery = path.split('/').last; final numericRegex = RegExp(r'^[0-9]+$'); if (numericRegex.hasMatch(pathQuery)) { @@ -68,24 +62,16 @@ class PiliSchame { } else { SmartDialog.showToast('投稿匹配失败'); } - } - - // bilibili://live/{roomid} - else if (host == 'live') { + } else if (host == 'live') { final String roomId = path.split('/').last; Get.toNamed('/liveRoom?roomid=$roomId', arguments: {'liveItem': null, 'heroTag': roomId}); - } - - // bilibili://bangumi/season/${ssid} - else if (host == 'bangumi') { + } else if (host == 'bangumi') { if (path.startsWith('/season')) { final String seasonId = path.split('/').last; - _bangumiPush(int.parse(seasonId)); + _bangumiPush(int.parse(seasonId), null); } - } - // 专栏 bilibili://opus/detail/883089655985078289 - else if (host == 'opus') { + } else if (host == 'opus') { if (path.startsWith('/detail')) { var opusId = path.split('/').last; Get.toNamed( @@ -101,6 +87,9 @@ class PiliSchame { Get.toNamed('/searchResult', parameters: {'keyword': ''}); } } + if (scheme == 'https') { + _fullPathPush(value); + } } // 投稿跳转 @@ -131,10 +120,10 @@ class PiliSchame { } // 番剧跳转 - static Future _bangumiPush(int seasonId) async { + static Future _bangumiPush(int? seasonId, int? epId) async { SmartDialog.showLoading(msg: '获取中...'); try { - var result = await SearchHttp.bangumiInfo(seasonId: seasonId, epId: null); + var result = await SearchHttp.bangumiInfo(seasonId: seasonId, epId: epId); if (result['status']) { var bangumiDetail = result['data']; final int cid = bangumiDetail.episodes!.first.cid; @@ -151,6 +140,8 @@ class PiliSchame { }, ), ); + } else { + SmartDialog.showToast(result['msg']); } } catch (e) { SmartDialog.showToast('番剧获取失败:$e'); @@ -163,29 +154,67 @@ class PiliSchame { // final String scheme = value.scheme!; final String host = value.host!; final String? path = value.path; - // Map query = value.query!; - if (host.startsWith('live.bilibili')) { + Map? query = value.query; + RegExp regExp = RegExp(r'^(www\.)?m?\.(bilibili\.com)$'); + if (regExp.hasMatch(host)) { + print('bilibili.com'); + } else if (host.contains('live')) { int roomId = int.parse(path!.split('/').last); - // print('直播'); - Get.toNamed('/liveRoom?roomid=$roomId', - arguments: {'liveItem': null, 'heroTag': roomId.toString()}); - return; - } - if (host.startsWith('space.bilibili')) { - print('个人空间'); + Get.toNamed( + '/liveRoom?roomid=$roomId', + arguments: {'liveItem': null, 'heroTag': roomId.toString()}, + ); + } else if (host.contains('space')) { + var mid = path!.split('/').last; + Get.toNamed('/member?mid=$mid', arguments: {'face': ''}); return; + } else if (host == 'b23.tv') { + final String fullPath = 'https://$host$path'; + final String redirectUrl = await UrlUtils.parseRedirectUrl(fullPath); + final String pathSegment = Uri.parse(redirectUrl).path; + final String lastPathSegment = pathSegment.split('/').last; + final RegExp avRegex = RegExp(r'^[aA][vV]\d+', caseSensitive: false); + if (avRegex.hasMatch(lastPathSegment)) { + final Map map = + IdUtils.matchAvorBv(input: lastPathSegment); + if (map.containsKey('AV')) { + _videoPush(map['AV']! as int, null); + } else if (map.containsKey('BV')) { + _videoPush(null, map['BV'] as String); + } else { + SmartDialog.showToast('投稿匹配失败'); + } + } else if (lastPathSegment.startsWith('ep')) { + _handleEpisodePath(lastPathSegment, redirectUrl); + } else if (lastPathSegment.startsWith('ss')) { + _handleSeasonPath(lastPathSegment, redirectUrl); + } else if (lastPathSegment.startsWith('BV')) { + UrlUtils.matchUrlPush( + lastPathSegment, + '', + redirectUrl, + ); + } else { + Get.toNamed( + '/webview', + parameters: {'url': redirectUrl, 'type': 'url', 'pageTitle': ''}, + ); + } } if (path != null) { - final String area = path.split('/')[1]; + final String area = path.split('/').last; switch (area) { case 'bangumi': - // print('番剧'); - final String seasonId = path.split('/').last; - _bangumiPush(matchNum(seasonId).first); + print('番剧'); + if (area.startsWith('ep')) { + _bangumiPush(null, matchNum(area).first); + } else if (area.startsWith('ss')) { + _bangumiPush(matchNum(area).first, null); + } break; case 'video': - // print('投稿'); + print('投稿'); final Map map = IdUtils.matchAvorBv(input: path); if (map.containsKey('AV')) { _videoPush(map['AV']! as int, null); @@ -200,6 +229,7 @@ class PiliSchame { break; case 'space': print('个人空间'); + Get.toNamed('/member?mid=$area', arguments: {'face': ''}); break; } } @@ -211,4 +241,18 @@ class PiliSchame { return matches.map((Match match) => int.parse(match.group(0)!)).toList(); } + + static void _handleEpisodePath(String lastPathSegment, String redirectUrl) { + final String seasonId = _extractIdFromPath(lastPathSegment); + _bangumiPush(null, matchNum(seasonId).first); + } + + static void _handleSeasonPath(String lastPathSegment, String redirectUrl) { + final String seasonId = _extractIdFromPath(lastPathSegment); + _bangumiPush(matchNum(seasonId).first, null); + } + + static String _extractIdFromPath(String lastPathSegment) { + return lastPathSegment.split('/').last; + } } diff --git a/lib/utils/cache_manage.dart b/lib/utils/cache_manage.dart index e250ab70c..d6bbb8165 100644 --- a/lib/utils/cache_manage.dart +++ b/lib/utils/cache_manage.dart @@ -99,10 +99,8 @@ class CacheManage { try { // 清除缓存 图片缓存 await clearLibraryCache(); - Timer(const Duration(milliseconds: 500), () { - SmartDialog.dismiss().then((res) { - SmartDialog.showToast('清除完成'); - }); + SmartDialog.dismiss().then((res) { + SmartDialog.showToast('清除完成'); }); } catch (err) { SmartDialog.dismiss(); diff --git a/lib/utils/download.dart b/lib/utils/download.dart index ad008f6d8..a9c56ec0e 100644 --- a/lib/utils/download.dart +++ b/lib/utils/download.dart @@ -1,40 +1,94 @@ import 'dart:typed_data'; import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:saver_gallery/saver_gallery.dart'; class DownloadUtils { // 获取存储权限 - static requestStoragePer() async { - Map statuses = await [ - Permission.storage, - Permission.photos, - ].request(); - statuses[Permission.storage].toString(); + static Future requestStoragePer() async { + await Permission.storage.request(); + PermissionStatus status = await Permission.storage.status; + if (status == PermissionStatus.denied || + status == PermissionStatus.permanentlyDenied) { + SmartDialog.show( + useSystem: true, + animationType: SmartAnimationType.centerFade_otherSlide, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('提示'), + content: const Text('存储权限未授权'), + actions: [ + TextButton( + onPressed: () async { + openAppSettings(); + }, + child: const Text('去授权'), + ) + ], + ); + }, + ); + return false; + } else { + return true; + } + } + + // 获取相册权限 + static Future requestPhotoPer() async { + await Permission.photos.request(); + PermissionStatus status = await Permission.photos.status; + if (status == PermissionStatus.denied || + status == PermissionStatus.permanentlyDenied) { + SmartDialog.show( + useSystem: true, + animationType: SmartAnimationType.centerFade_otherSlide, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('提示'), + content: const Text('相册权限未授权'), + actions: [ + TextButton( + onPressed: () async { + openAppSettings(); + }, + child: const Text('去授权'), + ) + ], + ); + }, + ); + return false; + } else { + return true; + } } static Future downloadImg(String imgUrl, {String imgType = 'cover'}) async { try { - await requestStoragePer(); + if (!await requestPhotoPer()) { + return false; + } SmartDialog.showLoading(msg: '保存中'); var response = await Dio() .get(imgUrl, options: Options(responseType: ResponseType.bytes)); + final String imgSuffix = imgUrl.split('.').last; String picName = - "plpl_${imgType}_${DateTime.now().toString().split('-').join()}"; + "plpl_${imgType}_${DateTime.now().toString().replaceAll(RegExp(r'[- :]'), '').split('.').first}"; final SaveResult result = await SaverGallery.saveImage( Uint8List.fromList(response.data), - quality: 60, - name: picName, + name: '$picName.$imgSuffix', // 保存到 PiliPala文件夹 androidRelativePath: "Pictures/PiliPala", androidExistNotSave: false, ); SmartDialog.dismiss(); if (result.isSuccess) { - await SmartDialog.showToast('「$picName」已保存 '); + await SmartDialog.showToast('「${'$picName.$imgSuffix'}」已保存 '); } return true; } catch (err) { diff --git a/lib/utils/em.dart b/lib/utils/em.dart index 733f5c357..2c5af8ba0 100644 --- a/lib/utils/em.dart +++ b/lib/utils/em.dart @@ -19,15 +19,7 @@ class Em { return regCate(matchStr); }, onNonMatch: (String str) { if (str != '') { - str = str - .replaceAll('<', '<') - .replaceAll('>', '>') - .replaceAll('"', '"') - .replaceAll(''', "'") - .replaceAll('"', '"') - .replaceAll(''', "'") - .replaceAll(' ', " ") - .replaceAll('&', "&"); + str = decodeHtmlEntities(str); Map map = {'type': 'text', 'text': str}; res.add(map); } @@ -35,4 +27,17 @@ class Em { }); return res; } + + static String decodeHtmlEntities(String title) { + return title + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll(''', "'") + .replaceAll('"', '"') + .replaceAll(''', "'") + .replaceAll(' ', " ") + .replaceAll('&', "&") + .replaceAll(''', "'"); + } } diff --git a/lib/utils/global_data.dart b/lib/utils/global_data.dart new file mode 100644 index 000000000..ef3daf217 --- /dev/null +++ b/lib/utils/global_data.dart @@ -0,0 +1,17 @@ +import '../models/common/index.dart'; + +class GlobalData { + int imgQuality = 10; + FullScreenGestureMode fullScreenGestureMode = + FullScreenGestureMode.values.last; + bool enablePlayerControlAnimation = true; + + // 私有构造函数 + GlobalData._(); + + // 单例实例 + static final GlobalData _instance = GlobalData._(); + + // 获取全局实例 + factory GlobalData() => _instance; +} diff --git a/lib/utils/id_utils.dart b/lib/utils/id_utils.dart index 906f63481..7fefc2689 100644 --- a/lib/utils/id_utils.dart +++ b/lib/utils/id_utils.dart @@ -68,8 +68,9 @@ class IdUtils { if (input == null || input.isEmpty) { return result; } - final RegExp bvRegex = RegExp(r'BV[0-9A-Za-z]{10}', caseSensitive: false); - final RegExp avRegex = RegExp(r'AV\d+', caseSensitive: false); + final RegExp bvRegex = + RegExp(r'[bB][vV][0-9A-Za-z]{10}', caseSensitive: false); + final RegExp avRegex = RegExp(r'[aA][vV]\d+', caseSensitive: false); final Iterable bvMatches = bvRegex.allMatches(input); final Iterable avMatches = avRegex.allMatches(input); diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart index 16cef4639..a82972e05 100644 --- a/lib/utils/storage.dart +++ b/lib/utils/storage.dart @@ -1,11 +1,11 @@ -// import 'package:hive/hive.dart'; import 'dart:io'; - import 'package:hive_flutter/hive_flutter.dart'; import 'package:path_provider/path_provider.dart'; import 'package:pilipala/models/model_owner.dart'; import 'package:pilipala/models/search/hot.dart'; import 'package:pilipala/models/user/info.dart'; +import '../models/common/gesture_mode.dart'; +import 'global_data.dart'; class GStrorage { static late final Box userInfo; @@ -44,6 +44,13 @@ class GStrorage { ); // 视频设置 video = await Hive.openBox('video'); + GlobalData().imgQuality = + setting.get(SettingBoxKey.defaultPicQa, defaultValue: 10); // 设置全局变量 + GlobalData().fullScreenGestureMode = FullScreenGestureMode.values[ + setting.get(SettingBoxKey.fullScreenGestureMode, + defaultValue: FullScreenGestureMode.values.last.index) as int]; + GlobalData().enablePlayerControlAnimation = setting + .get(SettingBoxKey.enablePlayerControlAnimation, defaultValue: true); } static void regAdapter() { @@ -77,6 +84,7 @@ class SettingBoxKey { autoUpgradeEnable = 'autoUpgradeEnable', feedBackEnable = 'feedBackEnable', defaultVideoQa = 'defaultVideoQa', + defaultLiveQa = 'defaultLiveQa', defaultAudioQa = 'defaultAudioQa', autoPlayEnable = 'autoPlayEnable', fullScreenMode = 'fullScreenMode', @@ -93,11 +101,13 @@ class SettingBoxKey { enableCDN = 'enableCDN', autoPiP = 'autoPiP', enableAutoLongPressSpeed = 'enableAutoLongPressSpeed', + enablePlayerControlAnimation = 'enablePlayerControlAnimation', // youtube 双击快进快退 enableQuickDouble = 'enableQuickDouble', enableShowDanmaku = 'enableShowDanmaku', enableBackgroundPlay = 'enableBackgroundPlay', + fullScreenGestureMode = 'fullScreenGestureMode', /// 隐私 blackMidsList = 'blackMidsList', @@ -121,7 +131,9 @@ class SettingBoxKey { enableWordRe = 'enableWordRe', enableSearchWord = 'enableSearchWord', enableSystemProxy = 'enableSystemProxy', - enableAi = 'enableAi'; + enableAi = 'enableAi', + defaultHomePage = 'defaultHomePage', + enableRelatedVideo = 'enableRelatedVideo'; /// 外观 static const String themeMode = 'themeMode', @@ -160,6 +172,10 @@ class LocalCacheKey { // 代理host port systemProxyHost = 'systemProxyHost', systemProxyPort = 'systemProxyPort'; + + static const String isDisableBatteryOptLocal = 'isDisableBatteryOptLocal', + isManufacturerBatteryOptimizationDisabled = + 'isManufacturerBatteryOptimizationDisabled'; } class VideoBoxKey { @@ -171,6 +187,8 @@ class VideoBoxKey { videoSpeed = 'videoSpeed', // 播放顺序 playRepeat = 'playRepeat', + // 系统预设倍速 + playSpeedSystem = 'playSpeedSystem', // 默认倍速 playSpeedDefault = 'playSpeedDefault', // 默认长按倍速 diff --git a/lib/utils/subtitle.dart b/lib/utils/subtitle.dart new file mode 100644 index 000000000..452be5426 --- /dev/null +++ b/lib/utils/subtitle.dart @@ -0,0 +1,32 @@ +class SubTitleUtils { + // 格式整理 + static String convertToWebVTT(List jsonData) { + String webVTTContent = 'WEBVTT FILE\n\n'; + + for (int i = 0; i < jsonData.length; i++) { + final item = jsonData[i]; + double from = item['from'] as double; + double to = item['to'] as double; + int sid = (item['sid'] ?? 0) as int; + String content = item['content'] as String; + + webVTTContent += '$sid\n'; + webVTTContent += '${formatTime(from)} --> ${formatTime(to)}\n'; + webVTTContent += '$content\n\n'; + } + + return webVTTContent; + } + + static String formatTime(num seconds) { + final String h = (seconds / 3600).floor().toString().padLeft(2, '0'); + final String m = (seconds % 3600 / 60).floor().toString().padLeft(2, '0'); + final String s = (seconds % 60).floor().toString().padLeft(2, '0'); + final String ms = + (seconds * 1000 % 1000).floor().toString().padLeft(3, '0'); + if (h == '00') { + return "$m:$s.$ms"; + } + return "$h:$m:$s.$ms"; + } +} diff --git a/lib/utils/url_utils.dart b/lib/utils/url_utils.dart index bac6cdfa0..cf0ef9e23 100644 --- a/lib/utils/url_utils.dart +++ b/lib/utils/url_utils.dart @@ -14,19 +14,23 @@ class UrlUtils { dio.options.validateStatus = (status) { return status == 200 || status == 301 || status == 302; }; - final response = await dio.get(url); - if (response.statusCode == 302) { - redirectUrl = response.headers['location']?.first as String; - if (redirectUrl.endsWith('/')) { - redirectUrl = redirectUrl.substring(0, redirectUrl.length - 1); - } - } else { - if (url.endsWith('/')) { - url = url.substring(0, url.length - 1); + try { + final response = await dio.get(url); + if (response.statusCode == 302) { + redirectUrl = response.headers['location']?.first as String; + if (redirectUrl.endsWith('/')) { + redirectUrl = redirectUrl.substring(0, redirectUrl.length - 1); + } + } else { + if (url.endsWith('/')) { + url = url.substring(0, url.length - 1); + } + return url; } + return redirectUrl; + } catch (err) { return url; } - return redirectUrl; } // 匹配url路由跳转 diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 09d8c97f1..adcc7b5a1 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -50,6 +50,9 @@ class Utils { return time; } if (time < 3600) { + if (time == 0) { + return time; + } final int minute = time ~/ 60; final double res = time / 60; if (minute != res) { @@ -87,6 +90,9 @@ class Utils { // 时间显示,刚刚,x分钟前 static String dateFormat(timeStamp, {formatType = 'list'}) { + if (timeStamp == 0 || timeStamp == null || timeStamp == '') { + return ''; + } // 当前时间 int time = (DateTime.now().millisecondsSinceEpoch / 1000).round(); // 对比 @@ -103,6 +109,7 @@ class Utils { toInt: false, formatType: formatType); } + print('distance: $distance'); if (distance <= 60) { return '刚刚'; } else if (distance <= 3600) { @@ -236,6 +243,10 @@ class Utils { SmartDialog.dismiss(); var currentInfo = await PackageInfo.fromPlatform(); var result = await Request().get(Api.latestApp, extra: {'ua': 'mob'}); + if (result.data == null || result.data.isEmpty) { + SmartDialog.showToast('获取远程版本失败,请检查网络'); + return false; + } LatestDataModel data = LatestDataModel.fromJson(result.data); bool isUpdate = Utils.needUpdate(currentInfo.version, data.tagName!); if (isUpdate) { @@ -344,9 +355,8 @@ class Utils { } static List generateRandomBytes(int minLength, int maxLength) { - return List.generate( - random.nextInt(maxLength-minLength+1), (_) => random.nextInt(0x60) + 0x20 - ); + return List.generate(random.nextInt(maxLength - minLength + 1), + (_) => random.nextInt(0x60) + 0x20); } static String base64EncodeRandomString(int minLength, int maxLength) { diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 3e5f82f76..8af2f922b 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -24,7 +24,7 @@ import wakelock_plus func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin")) AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) - ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) + ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) FlutterVolumeControllerPlugin.register(with: registry.registrar(forPlugin: "FlutterVolumeControllerPlugin")) diff --git a/pubspec.lock b/pubspec.lock index f5d63ca95..84556c065 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: "direct main" description: name: animations - sha256: "708e4b68c23228c264b038fe7003a2f5d01ce85fc64d8cae090e86b27fcea6c5" + sha256: d3d6dcfb218225bbe68e87ccf6378bbb2e32a94900722c5f81611dad089911cb url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.10" + version: "2.0.11" appscheme: dependency: "direct main" description: @@ -101,10 +101,10 @@ packages: dependency: "direct main" description: name: audio_video_progress_bar - sha256: "3384875247cdbea748bd9ae8330631cd06a6cabfcda4945d45c9b406da92bc66" + sha256: ccc7d7b83d2a16c52d4a7fb332faabd1baa053fb0e4c16815aefd3945ab33b81 url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.1" + version: "2.0.2" auto_orientation: dependency: "direct main" description: @@ -157,10 +157,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "67d591d602906ef9201caf93452495ad1812bea2074f04e25dbd7c133785821b" + sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" url: "https://pub.flutter-io.cn" source: hosted - version: "2.4.7" + version: "2.4.8" build_runner_core: dependency: transitive description: @@ -213,10 +213,10 @@ packages: dependency: "direct main" description: name: catcher_2 - sha256: ca94d45ffb52bf4b16a425cdff6734ae8443d36d5f06c276f1c2a593120b11ed + sha256: "9cf33d2befd10058374e5fc6177577fdd938d73d9c06810de81cf91311a7ce98" url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.0" + version: "1.2.3" characters: dependency: transitive description: @@ -269,18 +269,18 @@ packages: dependency: "direct main" description: name: connectivity_plus - sha256: "77a180d6938f78ca7d2382d2240eb626c0f6a735d0bfdce227d8ffb80f95c48b" + sha256: e9feae83b1849f61bad9f6f33ee00646e3410d54ce0821e02f262f9901dad3c9 url: "https://pub.flutter-io.cn" source: hosted - version: "4.0.2" + version: "6.0.1" connectivity_plus_platform_interface: dependency: transitive description: name: connectivity_plus_platform_interface - sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a + sha256: b6a56efe1e6675be240de39107281d4034b64ac23438026355b4234042a35adb url: "https://pub.flutter-io.cn" source: hosted - version: "1.2.4" + version: "2.0.0" convert: dependency: transitive description: @@ -373,10 +373,10 @@ packages: dependency: "direct main" description: name: dio - sha256: "797e1e341c3dd2f69f2dad42564a6feff3bfb87187d05abb93b9609e6f1645c3" + sha256: "49af28382aefc53562459104f64d16b9dfd1e8ef68c862d5af436cc8356ce5a8" url: "https://pub.flutter-io.cn" source: hosted - version: "5.4.0" + version: "5.4.1" dio_cookie_manager: dependency: "direct main" description: @@ -393,6 +393,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.4.0" + disable_battery_optimization: + dependency: "direct main" + description: + name: disable_battery_optimization + sha256: "6b2ba802f984af141faf1b6b5fb956d5ef01f9cd555597c35b9cc335a03185ba" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" dismissible_page: dependency: "direct main" description: @@ -405,10 +413,10 @@ packages: dependency: "direct main" description: name: dynamic_color - sha256: "8b8bd1d798bd393e11eddeaa8ae95b12ff028bf7d5998fc5d003488cd5f4ce2f" + sha256: eae98052fa6e2826bdac3dd2e921c6ce2903be15c6b7f8b6d8a5d49b5086298d url: "https://pub.flutter-io.cn" source: hosted - version: "1.6.8" + version: "1.7.0" easy_debounce: dependency: "direct main" description: @@ -425,6 +433,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "5.0.3" + expandable: + dependency: "direct main" + description: + name: expandable + sha256: "9604d612d4d1146dafa96c6d8eec9c2ff0994658d6d09fed720ab788c7f5afc2" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.0.1" extended_image: dependency: "direct main" description: @@ -461,10 +477,10 @@ packages: dependency: "direct main" description: name: extended_nested_scroll_view - sha256: "444a6f883e6e07effc7639e69a309e1fb491b6c19b095e9281714a51ace2b384" + sha256: "835580d40c2c62b448bd14adecd316acba469ba61f1510ef559d17668a85e777" url: "https://pub.flutter-io.cn" source: hosted - version: "6.1.2" + version: "6.2.1" fake_async: dependency: transitive description: @@ -502,7 +518,7 @@ packages: description: path: "." ref: main - resolved-ref: d2d8421c4d80f6113f832404109853684721e11a + resolved-ref: "8e89669eb9341f9980265306e24ef96fdbd3fd08" url: "https://github.com/guozhigq/floating.git" source: git version: "2.0.1" @@ -584,10 +600,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: d39e7f95621fc84376bc0f7d504f05c3a41488c562f4a8ad410569127507402c + sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2" url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.9" + version: "2.0.10+1" flutter_test: dependency: "direct dev" description: flutter @@ -1059,50 +1075,50 @@ packages: dependency: "direct main" description: name: permission_handler - sha256: "860c6b871c94c78e202dc69546d4d8fd84bd59faeb36f8fb9888668a53ff4f78" + sha256: "74e962b7fad7ff75959161bb2c0ad8fe7f2568ee82621c9c2660b751146bfe44" url: "https://pub.flutter-io.cn" source: hosted - version: "11.1.0" + version: "11.3.0" permission_handler_android: dependency: transitive description: name: permission_handler_android - sha256: "2f1bec180ee2f5665c22faada971a8f024761f632e93ddc23310487df52dcfa6" + sha256: "1acac6bae58144b442f11e66621c062aead9c99841093c38f5bcdcc24c1c3474" url: "https://pub.flutter-io.cn" source: hosted - version: "12.0.1" + version: "12.0.5" permission_handler_apple: dependency: transitive description: name: permission_handler_apple - sha256: "1a816084338ada8d574b1cb48390e6e8b19305d5120fe3a37c98825bacc78306" + sha256: "92861b0f0c2443dd8898398c2baa4f1ae925109b5909ae4a17d0108a6a788932" url: "https://pub.flutter-io.cn" source: hosted - version: "9.2.0" + version: "9.4.2" permission_handler_html: dependency: transitive description: name: permission_handler_html - sha256: "11b762a8c123dced6461933a88ea1edbbe036078c3f9f41b08886e678e7864df" + sha256: "54bf176b90f6eddd4ece307e2c06cf977fb3973719c35a93b85cc7093eb6070d" url: "https://pub.flutter-io.cn" source: hosted - version: "0.1.0+2" + version: "0.1.1" permission_handler_platform_interface: dependency: transitive description: name: permission_handler_platform_interface - sha256: d87349312f7eaf6ce0adaf668daf700ac5b06af84338bd8b8574dfbd93ffe1a1 + sha256: "23dfba8447c076ab5be3dee9ceb66aad345c4a648f0cac292c77b1eb0e800b78" url: "https://pub.flutter-io.cn" source: hosted - version: "4.0.2" + version: "4.2.0" permission_handler_windows: dependency: transitive description: name: permission_handler_windows - sha256: "1e8640c1e39121128da6b816d236e714d2cf17fac5a105dd6acdd3403a628004" + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" url: "https://pub.flutter-io.cn" source: hosted - version: "0.2.0" + version: "0.2.1" petitparser: dependency: transitive description: @@ -1528,26 +1544,26 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "0f0c746dd2d6254a0057218ff980fc7f5670fd0fcf5e4db38a490d31eed4ad43" + sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3" url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.9+1" + version: "1.1.11+1" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: "0edf6d630d1bfd5589114138ed8fada3234deacc37966bec033d3047c29248b7" + sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.9+1" + version: "1.1.11+1" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: d24333727332d9bd20990f1483af4e09abdb9b1fc7c3db940b56ab5c42790c26 + sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81" url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.9+1" + version: "1.1.11+1" vector_math: dependency: transitive description: @@ -1632,10 +1648,10 @@ packages: dependency: "direct main" description: name: webview_flutter - sha256: "42393b4492e629aa3a88618530a4a00de8bb46e50e7b3993fedbfdc5352f0dbf" + sha256: d81b68e88cc353e546afb93fb38958e3717282c5ac6e5d3be4a4aef9fc3c1413 url: "https://pub.flutter-io.cn" source: hosted - version: "4.4.2" + version: "4.5.0" webview_flutter_android: dependency: transitive description: @@ -1656,10 +1672,10 @@ packages: dependency: transitive description: name: webview_flutter_wkwebview - sha256: accdaaa49a2aca2dc3c3230907988954cdd23fed0a19525d6c9789d380f4dc76 + sha256: "4d062ad505390ecef1c4bfb6001cd857a51e00912cc9dfb66edb1886a9ebd80c" url: "https://pub.flutter-io.cn" source: hosted - version: "3.9.4" + version: "3.10.2" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 9597eab75..ba5976ebf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,10 +16,10 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.19+1019 +version: 1.0.21+1021 environment: - sdk: ">=2.19.6 <3.0.0" + sdk: ">=3.0.0 <4.0.0" # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions @@ -36,15 +36,15 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.5 # 动态取色 - dynamic_color: ^1.6.8 + dynamic_color: ^1.7.0 get: ^4.6.5 # 网络 - dio: ^5.3.3 + dio: ^5.4.1 cookie_jar: ^4.0.8 dio_cookie_manager: ^3.1.1 - connectivity_plus: ^4.0.1 + connectivity_plus: ^6.0.1 dio_http2_adapter: ^2.3.1+1 # 图片 @@ -58,17 +58,17 @@ dependencies: hive_flutter: ^1.1.0 # 设备信息 - device_info_plus: ^9.0.2 + device_info_plus: ^9.0.0 # 权限 - permission_handler: ^11.0.1 + permission_handler: ^11.3.0 # 分享 share_plus: ^7.0.2 # cookie 管理 webview_cookie_manager: ^2.0.6 # 浏览器 - webview_flutter: ^4.2.2 + webview_flutter: ^4.5.0 # 解决sliver滑动不同步 - extended_nested_scroll_view: ^6.1.2 + extended_nested_scroll_view: ^6.2.1 # 上拉加载 loading_more_list: ^6.0.0 # 下拉刷新 @@ -99,15 +99,15 @@ dependencies: wakelock_plus: ^1.1.1 universal_platform: ^1.0.0+1 # 进度条 - audio_video_progress_bar: ^2.0.1 + audio_video_progress_bar: ^2.0.2 auto_orientation: ^2.3.1 protobuf: ^3.0.0 - animations: ^2.0.8 + animations: ^2.0.11 # 获取appx信息 package_info_plus: ^4.1.0 url_launcher: ^6.1.14 - flutter_svg: ^2.0.7 + flutter_svg: ^2.0.10+1 # 防抖节流 easy_debounce: ^2.0.3 # 高帧率 @@ -137,9 +137,13 @@ dependencies: uuid: ^3.0.7 scrollable_positioned_list: ^0.3.8 nil: ^1.1.1 - catcher_2: ^1.1.0 + catcher_2: ^1.2.3 logger: ^2.0.2+1 path: 1.8.3 + # 电池优化 + disable_battery_optimization: ^1.1.1 + # 展开/收起 + expandable: ^5.0.1 dev_dependencies: flutter_test: @@ -157,7 +161,7 @@ dev_dependencies: # url: https://github.com/nvi9/flutter_launcher_icons.git # ref: e045d40 hive_generator: ^2.0.0 - build_runner: ^2.3.3 + build_runner: ^2.4.8 flutter_launcher_icons: android: true @@ -189,6 +193,7 @@ flutter: - assets/images/lv/ - assets/images/logo/ - assets/images/live/ + - assets/images/video/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware