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/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/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 2e7584396..8aa62233b 100644
--- a/lib/http/api.dart
+++ b/lib/http/api.dart
@@ -483,4 +483,24 @@ class Api {
/// 激活buvid3
static const activateBuvidApi = '/x/internal/gaia-gateway/ExClimbWuzhi';
+
+ /// 我的订阅
+ 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/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/user.dart b/lib/http/user.dart
index c1f86285c..7d3def4ed 100644
--- a/lib/http/user.dart
+++ b/lib/http/user.dart
@@ -6,6 +6,8 @@ import '../models/user/fav_folder.dart';
import '../models/user/history.dart';
import '../models/user/info.dart';
import '../models/user/stat.dart';
+import '../models/user/sub_detail.dart';
+import '../models/user/sub_folder.dart';
import 'api.dart';
import 'init.dart';
@@ -305,4 +307,46 @@ class UserHttp {
return {'status': false, 'msg': res.data['message']};
}
}
+
+ // 我的订阅
+ static Future userSubFolder({
+ required int mid,
+ required int pn,
+ required int ps,
+ }) async {
+ var res = await Request().get(Api.userSubFolder, data: {
+ 'up_mid': mid,
+ 'ps': ps,
+ 'pn': pn,
+ 'platform': 'web',
+ });
+ if (res.data['code'] == 0) {
+ return {
+ 'status': true,
+ 'data': SubFolderModelData.fromJson(res.data['data'])
+ };
+ } else {
+ return {'status': false, 'msg': res.data['message']};
+ }
+ }
+
+ static Future userSubFolderDetail({
+ required int seasonId,
+ required int pn,
+ required int ps,
+ }) async {
+ var res = await Request().get(Api.userSubFolderDetail, data: {
+ 'season_id': seasonId,
+ 'ps': ps,
+ 'pn': pn,
+ });
+ if (res.data['code'] == 0) {
+ return {
+ 'status': true,
+ 'data': SubDetailModelData.fromJson(res.data['data'])
+ };
+ } else {
+ return {'status': false, 'msg': res.data['message']};
+ }
+ }
}
diff --git a/lib/http/video.dart b/lib/http/video.dart
index 30df62c3e..73e8b6988 100644
--- a/lib/http/video.dart
+++ b/lib/http/video.dart
@@ -475,4 +475,27 @@ class VideoHttp {
return {'status': false, 'data': []};
}
}
+
+ // 视频排行
+ 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};
+ }
+ }
}
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/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/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..0067791c0 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'];
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/user/sub_detail.dart b/lib/models/user/sub_detail.dart
new file mode 100644
index 000000000..a1e52e558
--- /dev/null
+++ b/lib/models/user/sub_detail.dart
@@ -0,0 +1,123 @@
+class SubDetailModelData {
+ DetailInfo? info;
+ List? medias;
+
+ SubDetailModelData({this.info, this.medias});
+
+ SubDetailModelData.fromJson(Map json) {
+ info = DetailInfo.fromJson(json['info']);
+ if (json['medias'] != null) {
+ medias = [];
+ json['medias'].forEach((v) {
+ medias!.add(SubDetailMediaItem.fromJson(v));
+ });
+ }
+ }
+}
+
+class SubDetailMediaItem {
+ int? id;
+ String? title;
+ String? cover;
+ String? pic;
+ int? duration;
+ int? pubtime;
+ String? bvid;
+ Map? upper;
+ Map? cntInfo;
+ int? enableVt;
+ String? vtDisplay;
+
+ SubDetailMediaItem({
+ this.id,
+ this.title,
+ this.cover,
+ this.pic,
+ this.duration,
+ this.pubtime,
+ this.bvid,
+ this.upper,
+ this.cntInfo,
+ this.enableVt,
+ this.vtDisplay,
+ });
+
+ SubDetailMediaItem.fromJson(Map json) {
+ id = json['id'];
+ title = json['title'];
+ cover = json['cover'];
+ pic = json['cover'];
+ duration = json['duration'];
+ pubtime = json['pubtime'];
+ bvid = json['bvid'];
+ upper = json['upper'];
+ cntInfo = json['cnt_info'];
+ enableVt = json['enable_vt'];
+ vtDisplay = json['vt_display'];
+ }
+
+ Map toJson() {
+ final data = {};
+ data['id'] = id;
+ data['title'] = title;
+ data['cover'] = cover;
+ data['duration'] = duration;
+ data['pubtime'] = pubtime;
+ data['bvid'] = bvid;
+ data['upper'] = upper;
+ data['cnt_info'] = cntInfo;
+ data['enable_vt'] = enableVt;
+ data['vt_display'] = vtDisplay;
+ return data;
+ }
+}
+
+class DetailInfo {
+ int? id;
+ int? seasonType;
+ String? title;
+ String? cover;
+ Map? upper;
+ Map? cntInfo;
+ int? mediaCount;
+ String? intro;
+ int? enableVt;
+
+ DetailInfo({
+ this.id,
+ this.seasonType,
+ this.title,
+ this.cover,
+ this.upper,
+ this.cntInfo,
+ this.mediaCount,
+ this.intro,
+ this.enableVt,
+ });
+
+ DetailInfo.fromJson(Map json) {
+ id = json['id'];
+ seasonType = json['season_type'];
+ title = json['title'];
+ cover = json['cover'];
+ upper = json['upper'];
+ cntInfo = json['cnt_info'];
+ mediaCount = json['media_count'];
+ intro = json['intro'];
+ enableVt = json['enable_vt'];
+ }
+
+ Map toJson() {
+ final data = {};
+ data['id'] = id;
+ data['season_type'] = seasonType;
+ data['title'] = title;
+ data['cover'] = cover;
+ data['upper'] = upper;
+ data['cnt_info'] = cntInfo;
+ data['media_count'] = mediaCount;
+ data['intro'] = intro;
+ data['enable_vt'] = enableVt;
+ return data;
+ }
+}
diff --git a/lib/models/user/sub_folder.dart b/lib/models/user/sub_folder.dart
new file mode 100644
index 000000000..d496a1cf7
--- /dev/null
+++ b/lib/models/user/sub_folder.dart
@@ -0,0 +1,111 @@
+class SubFolderModelData {
+ final int? count;
+ final List? list;
+
+ SubFolderModelData({
+ this.count,
+ this.list,
+ });
+
+ factory SubFolderModelData.fromJson(Map json) {
+ return SubFolderModelData(
+ count: json['count'],
+ list: json['list'] != null
+ ? (json['list'] as List)
+ .map((i) => SubFolderItemData.fromJson(i))
+ .toList()
+ : null,
+ );
+ }
+}
+
+class SubFolderItemData {
+ final int? id;
+ final int? fid;
+ final int? mid;
+ final int? attr;
+ final String? title;
+ final String? cover;
+ final Upper? upper;
+ final int? coverType;
+ final String? intro;
+ final int? ctime;
+ final int? mtime;
+ final int? state;
+ final int? favState;
+ final int? mediaCount;
+ final int? viewCount;
+ final int? vt;
+ final int? playSwitch;
+ final int? type;
+ final String? link;
+ final String? bvid;
+
+ SubFolderItemData({
+ this.id,
+ this.fid,
+ this.mid,
+ this.attr,
+ this.title,
+ this.cover,
+ this.upper,
+ this.coverType,
+ this.intro,
+ this.ctime,
+ this.mtime,
+ this.state,
+ this.favState,
+ this.mediaCount,
+ this.viewCount,
+ this.vt,
+ this.playSwitch,
+ this.type,
+ this.link,
+ this.bvid,
+ });
+
+ factory SubFolderItemData.fromJson(Map json) {
+ return SubFolderItemData(
+ id: json['id'],
+ fid: json['fid'],
+ mid: json['mid'],
+ attr: json['attr'],
+ title: json['title'],
+ cover: json['cover'],
+ upper: json['upper'] != null ? Upper.fromJson(json['upper']) : null,
+ coverType: json['cover_type'],
+ intro: json['intro'],
+ ctime: json['ctime'],
+ mtime: json['mtime'],
+ state: json['state'],
+ favState: json['fav_state'],
+ mediaCount: json['media_count'],
+ viewCount: json['view_count'],
+ vt: json['vt'],
+ playSwitch: json['play_switch'],
+ type: json['type'],
+ link: json['link'],
+ bvid: json['bvid'],
+ );
+ }
+}
+
+class Upper {
+ final int? mid;
+ final String? name;
+ final String? face;
+
+ Upper({
+ this.mid,
+ this.name,
+ this.face,
+ });
+
+ factory Upper.fromJson(Map json) {
+ return Upper(
+ mid: json['mid'],
+ name: json['name'],
+ face: json['face'],
+ );
+ }
+}
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/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..791cc1080 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,
@@ -106,37 +149,21 @@ class _BangumiPanelState extends State {
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,
+ itemBuilder: (BuildContext context, int index) {
+ bool isLastItem = index == widget.pages.length - 1;
+ 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/controller.dart b/lib/pages/fav/controller.dart
index c3f761867..a5f945259 100644
--- a/lib/pages/fav/controller.dart
+++ b/lib/pages/fav/controller.dart
@@ -24,7 +24,7 @@ class FavController extends GetxController {
if (!hasMore.value) {
return;
}
- var res = await await UserHttp.userfavFolder(
+ var res = await UserHttp.userfavFolder(
pn: currentPage,
ps: pageSize,
mid: userInfo!.mid!,
diff --git a/lib/pages/fav_detail/controller.dart b/lib/pages/fav_detail/controller.dart
index 95130be60..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,12 +29,12 @@ 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;
}
isLoadingMore = true;
- var res = await await UserHttp.userFavFolderDetail(
+ var res = await UserHttp.userFavFolderDetail(
pn: currentPage,
ps: 20,
mediaId: mediaId!,
@@ -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 787fa96ea..d94f5149c 100644
--- a/lib/pages/fav_detail/view.dart
+++ b/lib/pages/fav_detail/view.dart
@@ -31,7 +31,6 @@ class _FavDetailPageState extends State {
super.initState();
mediaId = Get.parameters['mediaId']!;
_futureBuilderFuture = _favDetailController.queryUserFavFolderDetail();
- mediaId = Get.parameters['mediaId']!;
titleStreamC = StreamController();
_controller.addListener(
() {
@@ -85,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,
)
],
@@ -176,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/view.dart b/lib/pages/history/view.dart
index d8fc60f0d..92e1eee71 100644
--- a/lib/pages/history/view.dart
+++ b/lib/pages/history/view.dart
@@ -70,10 +70,6 @@ class _HistoryPageState extends State {
child1: AppBar(
titleSpacing: 0,
centerTitle: false,
- leading: IconButton(
- onPressed: () => Get.back(),
- icon: const Icon(Icons.arrow_back_outlined),
- ),
title: Text(
'观看记录',
style: Theme.of(context).textTheme.titleMedium,
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/widgets/bottom_control.dart b/lib/pages/live_room/widgets/bottom_control.dart
index c98a80ae0..3c908d712 100644
--- a/lib/pages/live_room/widgets/bottom_control.dart
+++ b/lib/pages/live_room/widgets/bottom_control.dart
@@ -30,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);
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/controller.dart b/lib/pages/media/controller.dart
index 8b8755117..757d5ac9e 100644
--- a/lib/pages/media/controller.dart
+++ b/lib/pages/media/controller.dart
@@ -28,6 +28,11 @@ class MediaController extends GetxController {
'title': '我的收藏',
'onTap': () => Get.toNamed('/fav'),
},
+ {
+ 'icon': Icons.subscriptions_outlined,
+ 'title': '我的订阅',
+ 'onTap': () => Get.toNamed('/subscription'),
+ },
{
'icon': Icons.watch_later_outlined,
'title': '稍后再看',
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..61475d97b
--- /dev/null
+++ b/lib/pages/rank/controller.dart
@@ -0,0 +1,70 @@
+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 = 1.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,
+ );
+ // 监听 tabController 切换
+ 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;
+ }
+ }
+ });
+ }
+ }
+}
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..58ca187fa
--- /dev/null
+++ b/lib/pages/rank/zone/view.dart
@@ -0,0 +1,148 @@
+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 {
+ final ZoneController _zoneController = Get.put(ZoneController());
+ List videoList = [];
+ Future? _futureBuilderFuture;
+ late ScrollController scrollController;
+
+ @override
+ void initState() {
+ super.initState();
+ _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) {
+ 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 03e912121..07d736e32 100644
--- a/lib/pages/setting/play_setting.dart
+++ b/lib/pages/setting/play_setting.dart
@@ -7,6 +7,7 @@ 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';
@@ -77,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视频',
@@ -138,18 +145,20 @@ 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),
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/controller.dart b/lib/pages/subscription/controller.dart
new file mode 100644
index 000000000..bf0c593c8
--- /dev/null
+++ b/lib/pages/subscription/controller.dart
@@ -0,0 +1,49 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
+import 'package:get/get.dart';
+import 'package:hive/hive.dart';
+import 'package:pilipala/http/user.dart';
+import 'package:pilipala/models/user/info.dart';
+import 'package:pilipala/utils/storage.dart';
+
+import '../../models/user/sub_folder.dart';
+
+class SubController extends GetxController {
+ final ScrollController scrollController = ScrollController();
+ Rx subFolderData = SubFolderModelData().obs;
+ Box userInfoCache = GStrorage.userInfo;
+ UserInfoData? userInfo;
+ int currentPage = 1;
+ int pageSize = 20;
+ RxBool hasMore = true.obs;
+
+ Future querySubFolder({type = 'init'}) async {
+ userInfo = userInfoCache.get('userInfoCache');
+ if (userInfo == null) {
+ return {'status': false, 'msg': '账号未登录'};
+ }
+ var res = await UserHttp.userSubFolder(
+ pn: currentPage,
+ ps: pageSize,
+ mid: userInfo!.mid!,
+ );
+ if (res['status']) {
+ if (type == 'init') {
+ subFolderData.value = res['data'];
+ } else {
+ if (res['data'].list.isNotEmpty) {
+ subFolderData.value.list!.addAll(res['data'].list);
+ subFolderData.update((val) {});
+ }
+ }
+ currentPage++;
+ } else {
+ SmartDialog.showToast(res['msg']);
+ }
+ return res;
+ }
+
+ Future onLoad() async {
+ querySubFolder(type: 'onload');
+ }
+}
diff --git a/lib/pages/subscription/index.dart b/lib/pages/subscription/index.dart
new file mode 100644
index 000000000..4d0343967
--- /dev/null
+++ b/lib/pages/subscription/index.dart
@@ -0,0 +1,4 @@
+library sub;
+
+export './controller.dart';
+export './view.dart';
diff --git a/lib/pages/subscription/view.dart b/lib/pages/subscription/view.dart
new file mode 100644
index 000000000..1eee4a4f7
--- /dev/null
+++ b/lib/pages/subscription/view.dart
@@ -0,0 +1,84 @@
+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 'controller.dart';
+import 'widgets/item.dart';
+
+class SubPage extends StatefulWidget {
+ const SubPage({super.key});
+
+ @override
+ State createState() => _SubPageState();
+}
+
+class _SubPageState extends State {
+ final SubController _subController = Get.put(SubController());
+ late Future _futureBuilderFuture;
+ late ScrollController scrollController;
+
+ @override
+ void initState() {
+ super.initState();
+ _futureBuilderFuture = _subController.querySubFolder();
+ scrollController = _subController.scrollController;
+ scrollController.addListener(
+ () {
+ if (scrollController.position.pixels >=
+ scrollController.position.maxScrollExtent - 300) {
+ EasyThrottle.throttle('history', const Duration(seconds: 1), () {
+ _subController.onLoad();
+ });
+ }
+ },
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ centerTitle: false,
+ titleSpacing: 0,
+ title: Text(
+ '我的订阅',
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
+ ),
+ body: FutureBuilder(
+ future: _futureBuilderFuture,
+ builder: (context, snapshot) {
+ if (snapshot.connectionState == ConnectionState.done) {
+ Map? data = snapshot.data;
+ if (data != null && data['status']) {
+ return Obx(
+ () => ListView.builder(
+ controller: scrollController,
+ itemCount: _subController.subFolderData.value.list!.length,
+ itemBuilder: (context, index) {
+ return SubItem(
+ subFolderItem:
+ _subController.subFolderData.value.list![index]);
+ },
+ ),
+ );
+ } else {
+ return CustomScrollView(
+ physics: const NeverScrollableScrollPhysics(),
+ slivers: [
+ HttpError(
+ errMsg: data?['msg'],
+ fn: () => setState(() {}),
+ ),
+ ],
+ );
+ }
+ } else {
+ // 骨架屏
+ return const Text('请求中');
+ }
+ },
+ ),
+ );
+ }
+}
diff --git a/lib/pages/subscription/widgets/item.dart b/lib/pages/subscription/widgets/item.dart
new file mode 100644
index 000000000..fd08ffa5b
--- /dev/null
+++ b/lib/pages/subscription/widgets/item.dart
@@ -0,0 +1,108 @@
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import 'package:pilipala/common/constants.dart';
+import 'package:pilipala/common/widgets/network_img_layer.dart';
+import 'package:pilipala/utils/utils.dart';
+
+import '../../../models/user/sub_folder.dart';
+
+class SubItem extends StatelessWidget {
+ final SubFolderItemData subFolderItem;
+ const SubItem({super.key, required this.subFolderItem});
+
+ @override
+ Widget build(BuildContext context) {
+ String heroTag = Utils.makeHeroTag(subFolderItem.id);
+ return InkWell(
+ onTap: () => Get.toNamed(
+ '/subDetail',
+ arguments: subFolderItem,
+ parameters: {
+ 'heroTag': heroTag,
+ 'seasonId': subFolderItem.id.toString(),
+ },
+ ),
+ child: Padding(
+ padding: const EdgeInsets.fromLTRB(12, 7, 12, 7),
+ child: LayoutBuilder(
+ builder: (context, boxConstraints) {
+ double width =
+ (boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2;
+ return SizedBox(
+ height: width / StyleString.aspectRatio,
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.start,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ AspectRatio(
+ aspectRatio: StyleString.aspectRatio,
+ child: LayoutBuilder(
+ builder: (context, boxConstraints) {
+ double maxWidth = boxConstraints.maxWidth;
+ double maxHeight = boxConstraints.maxHeight;
+ return Hero(
+ tag: heroTag,
+ child: NetworkImgLayer(
+ src: subFolderItem.cover,
+ width: maxWidth,
+ height: maxHeight,
+ ),
+ );
+ },
+ ),
+ ),
+ VideoContent(subFolderItem: subFolderItem)
+ ],
+ ),
+ );
+ },
+ ),
+ ),
+ );
+ }
+}
+
+class VideoContent extends StatelessWidget {
+ final SubFolderItemData subFolderItem;
+ const VideoContent({super.key, required this.subFolderItem});
+
+ @override
+ Widget build(BuildContext context) {
+ return Expanded(
+ child: Padding(
+ padding: const EdgeInsets.fromLTRB(10, 2, 6, 0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ subFolderItem.title!,
+ textAlign: TextAlign.start,
+ style: const TextStyle(
+ fontWeight: FontWeight.w500,
+ letterSpacing: 0.3,
+ ),
+ ),
+ const SizedBox(height: 2),
+ Text(
+ '合集 UP主:${subFolderItem.upper!.name!}',
+ textAlign: TextAlign.start,
+ style: TextStyle(
+ fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
+ color: Theme.of(context).colorScheme.outline,
+ ),
+ ),
+ const SizedBox(height: 2),
+ Text(
+ '${subFolderItem.mediaCount}个视频',
+ textAlign: TextAlign.start,
+ style: TextStyle(
+ fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
+ color: Theme.of(context).colorScheme.outline,
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/pages/subscription_detail/controller.dart b/lib/pages/subscription_detail/controller.dart
new file mode 100644
index 000000000..6ecb894e4
--- /dev/null
+++ b/lib/pages/subscription_detail/controller.dart
@@ -0,0 +1,60 @@
+import 'package:get/get.dart';
+import 'package:pilipala/http/user.dart';
+
+import '../../models/user/sub_detail.dart';
+import '../../models/user/sub_folder.dart';
+
+class SubDetailController extends GetxController {
+ late SubFolderItemData item;
+
+ late int seasonId;
+ late String heroTag;
+ int currentPage = 1;
+ bool isLoadingMore = false;
+ Rx subInfo = DetailInfo().obs;
+ RxList subList = [].obs;
+ RxString loadingText = '加载中...'.obs;
+ int mediaCount = 0;
+
+ @override
+ void onInit() {
+ item = Get.arguments;
+ if (Get.parameters.keys.isNotEmpty) {
+ seasonId = int.parse(Get.parameters['seasonId']!);
+ heroTag = Get.parameters['heroTag']!;
+ }
+ super.onInit();
+ }
+
+ Future queryUserSubFolderDetail({type = 'init'}) async {
+ if (type == 'onLoad' && subList.length >= mediaCount) {
+ loadingText.value = '没有更多了';
+ return;
+ }
+ isLoadingMore = true;
+ var res = await UserHttp.userSubFolderDetail(
+ seasonId: seasonId,
+ ps: 20,
+ pn: currentPage,
+ );
+ if (res['status']) {
+ subInfo.value = res['data'].info;
+ if (currentPage == 1 && type == 'init') {
+ subList.value = res['data'].medias;
+ mediaCount = res['data'].info.mediaCount;
+ } else if (type == 'onLoad') {
+ subList.addAll(res['data'].medias);
+ }
+ if (subList.length >= mediaCount) {
+ loadingText.value = '没有更多了';
+ }
+ }
+ currentPage += 1;
+ isLoadingMore = false;
+ return res;
+ }
+
+ onLoad() {
+ queryUserSubFolderDetail(type: 'onLoad');
+ }
+}
diff --git a/lib/pages/subscription_detail/index.dart b/lib/pages/subscription_detail/index.dart
new file mode 100644
index 000000000..71df4b24d
--- /dev/null
+++ b/lib/pages/subscription_detail/index.dart
@@ -0,0 +1,4 @@
+library sub_detail;
+
+export './controller.dart';
+export './view.dart';
diff --git a/lib/pages/subscription_detail/view.dart b/lib/pages/subscription_detail/view.dart
new file mode 100644
index 000000000..d56125cdb
--- /dev/null
+++ b/lib/pages/subscription_detail/view.dart
@@ -0,0 +1,257 @@
+import 'dart:async';
+
+import 'package:easy_debounce/easy_throttle.dart';
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import 'package:pilipala/common/skeleton/video_card_h.dart';
+import 'package:pilipala/common/widgets/http_error.dart';
+import 'package:pilipala/common/widgets/network_img_layer.dart';
+import 'package:pilipala/common/widgets/no_data.dart';
+
+import '../../models/user/sub_folder.dart';
+import '../../utils/utils.dart';
+import 'controller.dart';
+import 'widget/sub_video_card.dart';
+
+class SubDetailPage extends StatefulWidget {
+ const SubDetailPage({super.key});
+
+ @override
+ State createState() => _SubDetailPageState();
+}
+
+class _SubDetailPageState extends State {
+ late final ScrollController _controller = ScrollController();
+ final SubDetailController _subDetailController =
+ Get.put(SubDetailController());
+ late StreamController titleStreamC; // a
+ late Future _futureBuilderFuture;
+ late String seasonId;
+
+ @override
+ void initState() {
+ super.initState();
+ seasonId = Get.parameters['seasonId']!;
+ _futureBuilderFuture = _subDetailController.queryUserSubFolderDetail();
+ titleStreamC = StreamController();
+ _controller.addListener(
+ () {
+ if (_controller.offset > 160) {
+ titleStreamC.add(true);
+ } else if (_controller.offset <= 160) {
+ titleStreamC.add(false);
+ }
+
+ if (_controller.position.pixels >=
+ _controller.position.maxScrollExtent - 200) {
+ EasyThrottle.throttle('subDetail', const Duration(seconds: 1), () {
+ _subDetailController.onLoad();
+ });
+ }
+ },
+ );
+ }
+
+ @override
+ void dispose() {
+ _controller.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ body: CustomScrollView(
+ controller: _controller,
+ slivers: [
+ SliverAppBar(
+ expandedHeight: 260 - MediaQuery.of(context).padding.top,
+ pinned: true,
+ titleSpacing: 0,
+ title: StreamBuilder(
+ stream: titleStreamC.stream,
+ initialData: false,
+ builder: (context, AsyncSnapshot snapshot) {
+ return AnimatedOpacity(
+ opacity: snapshot.data ? 1 : 0,
+ curve: Curves.easeOut,
+ duration: const Duration(milliseconds: 500),
+ child: Row(
+ children: [
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ _subDetailController.item.title!,
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
+ Text(
+ '共${_subDetailController.item.mediaCount!}条视频',
+ style: Theme.of(context).textTheme.labelMedium,
+ )
+ ],
+ )
+ ],
+ ),
+ );
+ },
+ ),
+ flexibleSpace: FlexibleSpaceBar(
+ background: Container(
+ decoration: BoxDecoration(
+ border: Border(
+ bottom: BorderSide(
+ color: Theme.of(context).dividerColor.withOpacity(0.2),
+ ),
+ ),
+ ),
+ padding: EdgeInsets.only(
+ top: kTextTabBarHeight +
+ MediaQuery.of(context).padding.top +
+ 30,
+ left: 20,
+ right: 20),
+ child: SizedBox(
+ height: 200,
+ child: Row(
+ // mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Hero(
+ tag: _subDetailController.heroTag,
+ child: NetworkImgLayer(
+ width: 180,
+ height: 110,
+ src: _subDetailController.item.cover,
+ ),
+ ),
+ const SizedBox(width: 14),
+ Expanded(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.start,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const SizedBox(height: 4),
+ Text(
+ _subDetailController.item.title!,
+ style: TextStyle(
+ fontSize: Theme.of(context)
+ .textTheme
+ .titleMedium!
+ .fontSize,
+ fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 4),
+ GestureDetector(
+ onTap: () {
+ SubFolderItemData item =
+ _subDetailController.item;
+ Get.toNamed(
+ '/member?mid=${item.upper!.mid}',
+ arguments: {
+ 'face': item.upper!.face,
+ },
+ );
+ },
+ child: Text(
+ _subDetailController.item.upper!.name!,
+ style: TextStyle(
+ color:
+ Theme.of(context).colorScheme.primary),
+ ),
+ ),
+ const SizedBox(height: 4),
+ Text(
+ '${Utils.numFormat(_subDetailController.item.viewCount)}次播放',
+ style: TextStyle(
+ fontSize: Theme.of(context)
+ .textTheme
+ .labelSmall!
+ .fontSize,
+ color: Theme.of(context).colorScheme.outline),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ SliverToBoxAdapter(
+ child: Padding(
+ padding: const EdgeInsets.only(top: 15, bottom: 8, left: 14),
+ child: Obx(
+ () => Text(
+ '共${_subDetailController.subList.length}条视频',
+ style: TextStyle(
+ fontSize:
+ Theme.of(context).textTheme.labelMedium!.fontSize,
+ color: Theme.of(context).colorScheme.outline,
+ letterSpacing: 1),
+ ),
+ ),
+ ),
+ ),
+ FutureBuilder(
+ future: _futureBuilderFuture,
+ builder: (context, snapshot) {
+ if (snapshot.connectionState == ConnectionState.done) {
+ Map data = snapshot.data;
+ if (data['status']) {
+ if (_subDetailController.item.mediaCount == 0) {
+ return const NoData();
+ } else {
+ List subList = _subDetailController.subList;
+ return Obx(
+ () => subList.isEmpty
+ ? const SliverToBoxAdapter(child: SizedBox())
+ : SliverList(
+ delegate:
+ SliverChildBuilderDelegate((context, index) {
+ return SubVideoCardH(
+ videoItem: subList[index],
+ );
+ }, childCount: subList.length),
+ ),
+ );
+ }
+ } else {
+ return HttpError(
+ errMsg: data['msg'],
+ fn: () => setState(() {}),
+ );
+ }
+ } else {
+ // 骨架屏
+ return SliverList(
+ delegate: SliverChildBuilderDelegate((context, index) {
+ return const VideoCardHSkeleton();
+ }, childCount: 10),
+ );
+ }
+ },
+ ),
+ SliverToBoxAdapter(
+ child: Container(
+ height: MediaQuery.of(context).padding.bottom + 60,
+ padding: EdgeInsets.only(
+ bottom: MediaQuery.of(context).padding.bottom),
+ child: Center(
+ child: Obx(
+ () => Text(
+ _subDetailController.loadingText.value,
+ style: TextStyle(
+ color: Theme.of(context).colorScheme.outline,
+ fontSize: 13),
+ ),
+ ),
+ ),
+ ),
+ )
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/pages/subscription_detail/widget/sub_video_card.dart b/lib/pages/subscription_detail/widget/sub_video_card.dart
new file mode 100644
index 000000000..11aebc396
--- /dev/null
+++ b/lib/pages/subscription_detail/widget/sub_video_card.dart
@@ -0,0 +1,168 @@
+import 'package:get/get.dart';
+import 'package:flutter/material.dart';
+import 'package:pilipala/common/constants.dart';
+import 'package:pilipala/common/widgets/stat/danmu.dart';
+import 'package:pilipala/common/widgets/stat/view.dart';
+import 'package:pilipala/http/search.dart';
+import 'package:pilipala/models/common/search_type.dart';
+import 'package:pilipala/utils/utils.dart';
+import 'package:pilipala/common/widgets/network_img_layer.dart';
+import '../../../common/widgets/badge.dart';
+import '../../../models/user/sub_detail.dart';
+
+// 收藏视频卡片 - 水平布局
+class SubVideoCardH extends StatelessWidget {
+ final SubDetailMediaItem videoItem;
+ final int? searchType;
+
+ const SubVideoCardH({
+ Key? key,
+ required this.videoItem,
+ this.searchType,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ int id = videoItem.id!;
+ String bvid = videoItem.bvid!;
+ String heroTag = Utils.makeHeroTag(id);
+ return InkWell(
+ onTap: () async {
+ int cid = await SearchHttp.ab2c(bvid: bvid);
+ Map parameters = {
+ 'bvid': bvid,
+ 'cid': cid.toString(),
+ };
+
+ Get.toNamed('/video', parameters: parameters, arguments: {
+ 'videoItem': videoItem,
+ 'heroTag': heroTag,
+ 'videoType': SearchType.video,
+ });
+ },
+ child: Column(
+ children: [
+ Padding(
+ padding: const EdgeInsets.fromLTRB(
+ StyleString.safeSpace, 5, StyleString.safeSpace, 5),
+ child: LayoutBuilder(
+ builder: (context, boxConstraints) {
+ double width =
+ (boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2;
+ return SizedBox(
+ height: width / StyleString.aspectRatio,
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.start,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ AspectRatio(
+ aspectRatio: StyleString.aspectRatio,
+ child: LayoutBuilder(
+ builder: (context, boxConstraints) {
+ double maxWidth = boxConstraints.maxWidth;
+ double maxHeight = boxConstraints.maxHeight;
+ return Stack(
+ children: [
+ Hero(
+ tag: heroTag,
+ child: NetworkImgLayer(
+ src: videoItem.cover,
+ width: maxWidth,
+ height: maxHeight,
+ ),
+ ),
+ PBadge(
+ text: Utils.timeFormat(videoItem.duration!),
+ right: 6.0,
+ bottom: 6.0,
+ type: 'gray',
+ ),
+ // if (videoItem.ogv != null) ...[
+ // PBadge(
+ // text: videoItem.ogv['type_name'],
+ // top: 6.0,
+ // right: 6.0,
+ // bottom: null,
+ // left: null,
+ // ),
+ // ],
+ ],
+ );
+ },
+ ),
+ ),
+ VideoContent(
+ videoItem: videoItem,
+ searchType: searchType,
+ )
+ ],
+ ),
+ );
+ },
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class VideoContent extends StatelessWidget {
+ final dynamic videoItem;
+ final int? searchType;
+ const VideoContent({
+ super.key,
+ required this.videoItem,
+ this.searchType,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Expanded(
+ child: Padding(
+ padding: const EdgeInsets.fromLTRB(10, 2, 6, 0),
+ child: Stack(
+ children: [
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ videoItem.title,
+ textAlign: TextAlign.start,
+ style: const TextStyle(
+ fontWeight: FontWeight.w500,
+ letterSpacing: 0.3,
+ ),
+ maxLines: 2,
+ overflow: TextOverflow.ellipsis,
+ ),
+ const Spacer(),
+ Text(
+ Utils.dateFormat(videoItem.pubtime),
+ style: TextStyle(
+ fontSize: 11,
+ color: Theme.of(context).colorScheme.outline),
+ ),
+ Padding(
+ padding: const EdgeInsets.only(top: 2),
+ child: Row(
+ children: [
+ StatView(
+ theme: 'gray',
+ view: videoItem.cntInfo['play'],
+ ),
+ const SizedBox(width: 8),
+ StatDanMu(
+ theme: 'gray', danmu: videoItem.cntInfo['danmaku']),
+ const Spacer(),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart
index 7465c6f22..fe870873e 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,7 @@ import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/utils/video_utils.dart';
import 'package:screen_brightness/screen_brightness.dart';
+import '../../../http/danmaku.dart';
import '../../../utils/id_utils.dart';
import 'widgets/header_control.dart';
@@ -91,6 +93,7 @@ class VideoDetailController extends GetxController
late int cacheAudioQa;
PersistentBottomSheetController? replyReplyBottomSheetCtr;
+ late bool enableRelatedVideo;
@override
void onInit() {
@@ -113,7 +116,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 +132,8 @@ class VideoDetailController extends GetxController
controller: plPlayerController,
videoDetailCtr: this,
floating: floating,
+ bvid: bvid,
+ videoType: videoType,
);
// CDN优化
enableCDN = setting.get(SettingBoxKey.enableCDN, defaultValue: true);
@@ -381,4 +387,86 @@ class VideoDetailController extends GetxController
? replyReplyBottomSheetCtr!.close()
: print('replyReplyBottomSheetCtr is null');
}
+
+ /// 发送弹幕
+ 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..6714b8876 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 =
diff --git a/lib/pages/video/detail/introduction/view.dart b/lib/pages/video/detail/introduction/view.dart
index 0a3ac934b..9c1b7db0d 100644
--- a/lib/pages/video/detail/introduction/view.dart
+++ b/lib/pages/video/detail/introduction/view.dart
@@ -15,16 +15,17 @@ 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 '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 +48,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 +76,9 @@ class _VideoIntroPanelState extends State
// 请求成功
return Obx(
() => VideoInfo(
- loadingStatus: false,
videoDetail: videoIntroController.videoDetail.value,
heroTag: heroTag,
+ bvid: widget.bvid,
),
);
} else {
@@ -91,10 +93,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 +108,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;
@@ -149,16 +151,13 @@ 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);
}
@@ -212,9 +211,6 @@ class _VideoInfoState extends State with TickerProviderStateMixin {
// 视频介绍
showIntroDetail() {
- if (loadingStatus) {
- return;
- }
feedBack();
showBottomSheet(
context: context,
@@ -228,13 +224,9 @@ class _VideoInfoState extends State with TickerProviderStateMixin {
// 用户主页
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});
}
@@ -256,223 +248,186 @@ class _VideoInfoState extends State with TickerProviderStateMixin {
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: Text(
+ widget.videoDetail!.title!,
+ style: const TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ maxLines: 2,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ 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),
+ ),
+ )
+ ],
+ ),
+
+ /// 点赞收藏转发
+ 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(),
- ),
),
- ),
+ ),
+ ),
+ ],
+ )),
);
}
@@ -494,10 +449,7 @@ 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),
@@ -507,104 +459,38 @@ class _VideoInfoState extends State with TickerProviderStateMixin {
// 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.comment),
+ onTap: () => videoDetailCtr.tabCtr.animateTo(1),
+ selectStatus: false,
+ text: widget.videoDetail!.stat!.reply!.toString(),
+ ),
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..c74e27ee1 100644
--- a/lib/pages/video/detail/introduction/widgets/intro_detail.dart
+++ b/lib/pages/video/detail/introduction/widgets/intro_detail.dart
@@ -23,7 +23,10 @@ class IntroDetail extends StatelessWidget {
sheetHeight = localCache.get('sheetHeight');
return Container(
color: Theme.of(context).colorScheme.background,
- padding: const EdgeInsets.only(left: 14, right: 14),
+ padding: EdgeInsets.only(
+ left: 14,
+ right: 14,
+ bottom: MediaQuery.of(context).padding.bottom + 20),
height: sheetHeight,
child: Column(
children: [
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..c6a943989 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) {
@@ -134,32 +162,22 @@ class _SeasonPanelState extends State {
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,
+ itemBuilder: (BuildContext context, int index) {
+ bool isLastItem = index == episodes.length - 1;
+ 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..b07a61687 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(
diff --git a/lib/pages/video/detail/reply/widgets/reply_item.dart b/lib/pages/video/detail/reply/widgets/reply_item.dart
index 246d0688c..f9f695d48 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 父级回复内容,用作二楼回复(回复详情)展示
@@ -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