diff --git a/.github/workflows/beta_ci.yml b/.github/workflows/beta_ci.yml index 40f3f042..14b51780 100644 --- a/.github/workflows/beta_ci.yml +++ b/.github/workflows/beta_ci.yml @@ -206,4 +206,4 @@ jobs: 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 }})" + context: "*Beta版本: v${{ needs.update_version.outputs.new_version }}*\n更新内容: [${{ needs.update_version.outputs.last_commit }}]" diff --git a/lib/common/widgets/network_img_layer.dart b/lib/common/widgets/network_img_layer.dart index 06c35974..173db853 100644 --- a/lib/common/widgets/network_img_layer.dart +++ b/lib/common/widgets/network_img_layer.dart @@ -36,7 +36,7 @@ class NetworkImgLayer extends StatelessWidget { final int defaultImgQuality = GlobalData().imgQuality; final String imageUrl = '${src!.startsWith('//') ? 'https:${src!}' : src!}@${quality ?? defaultImgQuality}q.webp'; - print(imageUrl); + // print(imageUrl); int? memCacheWidth, memCacheHeight; double aspectRatio = (width / height).toDouble(); diff --git a/lib/common/widgets/overlay_pop.dart b/lib/common/widgets/overlay_pop.dart index fe9b9377..4f0a3899 100644 --- a/lib/common/widgets/overlay_pop.dart +++ b/lib/common/widgets/overlay_pop.dart @@ -62,6 +62,7 @@ class OverlayPop extends StatelessWidget { Expanded( child: Text( videoItem.title! as String, + style: Theme.of(context).textTheme.titleSmall, ), ), const SizedBox(width: 4), diff --git a/lib/common/widgets/video_card_v.dart b/lib/common/widgets/video_card_v.dart index 0d96f7b7..9916aa7a 100644 --- a/lib/common/widgets/video_card_v.dart +++ b/lib/common/widgets/video_card_v.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; +import 'package:pilipala/utils/feed_back.dart'; import '../../models/model_rec_video_item.dart'; +import 'overlay_pop.dart'; import 'stat/danmu.dart'; import 'stat/view.dart'; import '../../http/dynamics.dart'; @@ -19,15 +21,11 @@ import 'network_img_layer.dart'; class VideoCardV extends StatelessWidget { final dynamic videoItem; final int crossAxisCount; - final Function()? longPress; - final Function()? longPressEnd; const VideoCardV({ Key? key, required this.videoItem, required this.crossAxisCount, - this.longPress, - this.longPressEnd, }) : super(key: key); bool isStringNumeric(String str) { @@ -127,64 +125,56 @@ class VideoCardV extends StatelessWidget { @override Widget build(BuildContext context) { String heroTag = Utils.makeHeroTag(videoItem.id); - return Card( - elevation: 0, - clipBehavior: Clip.hardEdge, - margin: EdgeInsets.zero, - child: GestureDetector( - onLongPress: () { - if (longPress != null) { - longPress!(); - } - }, - // onLongPressEnd: (details) { - // if (longPressEnd != null) { - // longPressEnd!(); - // } - // }, - child: InkWell( - onTap: () async => onPushDetail(heroTag), - child: Column( - children: [ - AspectRatio( - aspectRatio: StyleString.aspectRatio, - child: LayoutBuilder(builder: (context, boxConstraints) { - double maxWidth = boxConstraints.maxWidth; - double maxHeight = boxConstraints.maxHeight; - return Stack( - children: [ - Hero( - tag: heroTag, - child: NetworkImgLayer( - src: videoItem.pic, - width: maxWidth, - height: maxHeight, - ), - ), - if (videoItem.duration > 0) - if (crossAxisCount == 1) ...[ - PBadge( - bottom: 10, - right: 10, - text: Utils.timeFormat(videoItem.duration), - ) - ] else ...[ - PBadge( - bottom: 6, - right: 7, - size: 'small', - type: 'gray', - text: Utils.timeFormat(videoItem.duration), - ) - ], - ], - ); - }), - ), - VideoContent(videoItem: videoItem, crossAxisCount: crossAxisCount) - ], + return InkWell( + onTap: () async => onPushDetail(heroTag), + onLongPress: () { + SmartDialog.show( + builder: (context) => OverlayPop( + videoItem: videoItem, + closeFn: () => SmartDialog.dismiss(), ), - ), + ); + }, + borderRadius: BorderRadius.circular(16), + child: Column( + children: [ + AspectRatio( + aspectRatio: StyleString.aspectRatio, + child: LayoutBuilder(builder: (context, boxConstraints) { + double maxWidth = boxConstraints.maxWidth; + double maxHeight = boxConstraints.maxHeight; + return Stack( + children: [ + Hero( + tag: heroTag, + child: NetworkImgLayer( + src: videoItem.pic, + width: maxWidth, + height: maxHeight, + ), + ), + if (videoItem.duration > 0) + if (crossAxisCount == 1) ...[ + PBadge( + bottom: 10, + right: 10, + text: Utils.timeFormat(videoItem.duration), + ) + ] else ...[ + PBadge( + bottom: 6, + right: 7, + size: 'small', + type: 'gray', + text: Utils.timeFormat(videoItem.duration), + ) + ], + ], + ); + }), + ), + VideoContent(videoItem: videoItem, crossAxisCount: crossAxisCount) + ], ), ); } @@ -196,122 +186,90 @@ class VideoContent extends StatelessWidget { const VideoContent( {Key? key, required this.videoItem, required this.crossAxisCount}) : super(key: key); + + Widget _buildBadge(String text, String type, [double fs = 12]) { + return PBadge( + text: text, + stack: 'normal', + size: 'small', + type: type, + fs: fs, + ); + } + @override Widget build(BuildContext context) { - return Expanded( - flex: crossAxisCount == 1 ? 0 : 1, - child: Padding( - padding: crossAxisCount == 1 - ? const EdgeInsets.fromLTRB(9, 9, 9, 4) - : const EdgeInsets.fromLTRB(5, 8, 5, 4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Expanded( - child: Text( - videoItem.title, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - if (videoItem.goto == 'av' && crossAxisCount == 1) ...[ - const SizedBox(width: 10), - VideoPopupMenu( - size: 32, - iconSize: 18, - videoItem: videoItem, - ), - ], - ], - ), - if (crossAxisCount > 1) ...[ - const SizedBox(height: 2), - VideoStat( - videoItem: videoItem, - crossAxisCount: crossAxisCount, - ), - ], - if (crossAxisCount == 1) const SizedBox(height: 4), - Row( - children: [ - if (videoItem.goto == 'bangumi') ...[ - PBadge( - text: videoItem.bangumiBadge, - stack: 'normal', - size: 'small', - type: 'line', - fs: 9, - ) - ], - if (videoItem.rcmdReason != null && - videoItem.rcmdReason.content != '') ...[ - PBadge( - text: videoItem.rcmdReason.content, - stack: 'normal', - size: 'small', - type: 'color', - ) - ], - if (videoItem.goto == 'picture') ...[ - const PBadge( - text: '动态', - stack: 'normal', - size: 'small', - type: 'line', - fs: 9, - ) - ], - if (videoItem.isFollowed == 1) ...[ - const PBadge( - text: '已关注', - stack: 'normal', - size: 'small', - type: 'color', - ) - ], - Expanded( - flex: crossAxisCount == 1 ? 0 : 1, - child: Text( - videoItem.owner.name, - maxLines: 1, - style: TextStyle( - fontSize: - Theme.of(context).textTheme.labelMedium!.fontSize, - color: Theme.of(context).colorScheme.outline, - ), - ), - ), - if (crossAxisCount == 1) ...[ - Text( - ' • ', - style: TextStyle( - fontSize: - Theme.of(context).textTheme.labelMedium!.fontSize, - color: Theme.of(context).colorScheme.outline, - ), - ), - VideoStat( - videoItem: videoItem, - crossAxisCount: crossAxisCount, - ), - const Spacer(), - ], - if (videoItem.goto == 'av' && crossAxisCount != 1) ...[ - VideoPopupMenu( - size: 24, - iconSize: 14, - videoItem: videoItem, - ), - ] else ...[ - const SizedBox(height: 24) - ] - ], - ), + return Padding( + padding: crossAxisCount == 1 + ? const EdgeInsets.fromLTRB(9, 9, 9, 4) + : const EdgeInsets.fromLTRB(5, 8, 5, 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + videoItem.title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + if (crossAxisCount > 1) ...[ + const SizedBox(height: 2), + VideoStat(videoItem: videoItem, crossAxisCount: crossAxisCount), ], - ), + if (crossAxisCount == 1) const SizedBox(height: 4), + Row( + children: [ + if (videoItem.goto == 'bangumi') + _buildBadge(videoItem.bangumiBadge, 'line', 9), + if (videoItem.rcmdReason?.content != null && + videoItem.rcmdReason.content != '') + _buildBadge(videoItem.rcmdReason.content, 'color'), + if (videoItem.goto == 'picture') _buildBadge('动态', 'line', 9), + if (videoItem.isFollowed == 1) _buildBadge('已关注', 'color'), + Expanded( + flex: crossAxisCount == 1 ? 0 : 1, + child: Text( + videoItem.owner.name, + maxLines: 1, + style: TextStyle( + fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, + color: Theme.of(context).colorScheme.outline, + ), + ), + ), + if (crossAxisCount == 1) ...[ + const SizedBox(width: 10), + VideoStat( + videoItem: videoItem, + crossAxisCount: crossAxisCount, + ), + const Spacer(), + ], + if (videoItem.goto == 'av') + SizedBox( + width: 24, + height: 24, + child: IconButton( + onPressed: () { + feedBack(); + showModalBottomSheet( + context: context, + useRootNavigator: true, + isScrollControlled: true, + builder: (context) { + return MorePanel(videoItem: videoItem); + }, + ); + }, + icon: Icon( + Icons.more_vert_outlined, + color: Theme.of(context).colorScheme.outline, + size: 14, + ), + ), + ) + ], + ), + ], ), ); } @@ -331,15 +289,9 @@ class VideoStat extends StatelessWidget { Widget build(BuildContext context) { return Row( children: [ - StatView( - theme: 'gray', - view: videoItem.stat.view, - ), + StatView(theme: 'gray', view: videoItem.stat.view), const SizedBox(width: 8), - StatDanMu( - theme: 'gray', - danmu: videoItem.stat.danmu, - ), + StatDanMu(theme: 'gray', danmu: videoItem.stat.danmu), if (videoItem is RecVideoItemModel) ...[ crossAxisCount > 1 ? const Spacer() : const SizedBox(width: 8), RichText( @@ -358,99 +310,98 @@ class VideoStat extends StatelessWidget { } } -class VideoPopupMenu extends StatelessWidget { - final double? size; - final double? iconSize; +class MorePanel extends StatelessWidget { final dynamic videoItem; + const MorePanel({super.key, required this.videoItem}); - const VideoPopupMenu({ - Key? key, - required this.size, - required this.iconSize, - required this.videoItem, - }) : super(key: key); + Future menuActionHandler(String type) async { + switch (type) { + case 'block': + blockUser(); + break; + case 'watchLater': + var res = await UserHttp.toViewLater(bvid: videoItem.bvid as String); + SmartDialog.showToast(res['msg']); + Get.back(); + break; + default: + } + } + + void blockUser() async { + SmartDialog.show( + useSystem: true, + animationType: SmartAnimationType.centerFade_otherSlide, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('提示'), + content: Text('确定拉黑:${videoItem.owner.name}(${videoItem.owner.mid})?' + '\n\n注:被拉黑的Up可以在隐私设置-黑名单管理中解除'), + actions: [ + TextButton( + onPressed: () => SmartDialog.dismiss(), + child: Text( + '点错了', + style: TextStyle(color: Theme.of(context).colorScheme.outline), + ), + ), + TextButton( + onPressed: () async { + var res = await VideoHttp.relationMod( + mid: videoItem.owner.mid, + act: 5, + reSrc: 11, + ); + SmartDialog.dismiss(); + SmartDialog.showToast(res['msg'] ?? '成功'); + }, + child: const Text('确认'), + ) + ], + ); + }, + ); + } @override Widget build(BuildContext context) { - return SizedBox( - width: size, - height: size, - child: PopupMenuButton( - padding: EdgeInsets.zero, - icon: Icon( - Icons.more_vert_outlined, - color: Theme.of(context).colorScheme.outline, - size: iconSize, - ), - position: PopupMenuPosition.under, - // constraints: const BoxConstraints(maxHeight: 35), - onSelected: (String type) {}, - itemBuilder: (BuildContext context) => >[ - PopupMenuItem( - onTap: () async { - var res = - await UserHttp.toViewLater(bvid: videoItem.bvid as String); - SmartDialog.showToast(res['msg']); - }, - value: 'pause', - height: 40, - child: const Row( - children: [ - Icon(Icons.watch_later_outlined, size: 16), - SizedBox(width: 6), - Text('稍后再看', style: TextStyle(fontSize: 13)) - ], + return Container( + padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + onTap: () => Get.back(), + child: Container( + height: 35, + padding: const EdgeInsets.only(bottom: 2), + child: Center( + child: Container( + width: 32, + height: 3, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.outline, + borderRadius: const BorderRadius.all(Radius.circular(3))), + ), + ), ), ), - const PopupMenuDivider(), - PopupMenuItem( - onTap: () async { - SmartDialog.show( - useSystem: true, - animationType: SmartAnimationType.centerFade_otherSlide, - builder: (BuildContext context) { - return AlertDialog( - title: const Text('提示'), - content: Text( - '确定拉黑:${videoItem.owner.name}(${videoItem.owner.mid})?' - '\n\n注:被拉黑的Up可以在隐私设置-黑名单管理中解除'), - actions: [ - TextButton( - onPressed: () => SmartDialog.dismiss(), - child: Text( - '点错了', - style: TextStyle( - color: Theme.of(context).colorScheme.outline), - ), - ), - TextButton( - onPressed: () async { - var res = await VideoHttp.relationMod( - mid: videoItem.owner.mid, - act: 5, - reSrc: 11, - ); - SmartDialog.dismiss(); - SmartDialog.showToast(res['msg'] ?? '成功'); - }, - child: const Text('确认'), - ) - ], - ); - }, - ); - }, - value: 'pause', - height: 40, - child: Row( - children: [ - const Icon(Icons.block, size: 16), - const SizedBox(width: 6), - Text('拉黑:${videoItem.owner.name}', - style: const TextStyle(fontSize: 13)) - ], + ListTile( + onTap: () async => await menuActionHandler('block'), + minLeadingWidth: 0, + leading: const Icon(Icons.block, size: 19), + title: Text( + '拉黑up主 「${videoItem.owner.name}」', + style: Theme.of(context).textTheme.titleSmall, ), ), + ListTile( + onTap: () async => await menuActionHandler('watchLater'), + minLeadingWidth: 0, + leading: const Icon(Icons.watch_later_outlined, size: 19), + title: + Text('添加至稍后再看', style: Theme.of(context).textTheme.titleSmall), + ), ], ), ); diff --git a/lib/http/api.dart b/lib/http/api.dart index b6975c4b..1735902c 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -511,4 +511,13 @@ class Api { /// 取消订阅 static const String cancelSub = '/x/v3/fav/season/unfav'; + + /// 动态转发 + static const String dynamicForwardUrl = '/x/dynamic/feed/create/submit_check'; + + /// 创建动态 + static const String dynamicCreate = '/x/dynamic/feed/create/dyn'; + + /// 删除收藏夹 + static const String delFavFolder = '/x/v3/fav/folder/del'; } diff --git a/lib/http/dynamics.dart b/lib/http/dynamics.dart index d62de12f..63dea4ff 100644 --- a/lib/http/dynamics.dart +++ b/lib/http/dynamics.dart @@ -1,3 +1,4 @@ +import 'dart:math'; import '../models/dynamics/result.dart'; import '../models/dynamics/up.dart'; import 'index.dart'; @@ -117,4 +118,94 @@ class DynamicsHttp { }; } } + + static Future dynamicForward() async { + var res = await Request().post( + Api.dynamicForwardUrl, + queryParameters: { + 'csrf': await Request.getCsrf(), + 'x-bili-device-req-json': {'platform': 'web', 'device': 'pc'}, + 'x-bili-web-req-json': {'spm_id': '333.999'}, + }, + data: { + 'attach_card': null, + 'scene': 4, + 'content': { + 'conetents': [ + {'raw_text': "2", 'type': 1, 'biz_id': ""} + ] + } + }, + ); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': res.data['data'], + }; + } else { + return { + 'status': false, + 'data': [], + 'msg': res.data['message'], + }; + } + } + + static Future dynamicCreate({ + required int mid, + required int scene, + int? oid, + String? dynIdStr, + String? rawText, + }) async { + DateTime now = DateTime.now(); + int timestamp = now.millisecondsSinceEpoch ~/ 1000; + Random random = Random(); + int randomNumber = random.nextInt(9000) + 1000; + String uploadId = '${mid}_${timestamp}_$randomNumber'; + + Map webRepostSrc = { + 'dyn_id_str': dynIdStr ?? '', + }; + + /// 投稿转发 + if (scene == 5) { + webRepostSrc = { + 'revs_id': {'dyn_type': 8, 'rid': oid} + }; + } + var res = await Request().post(Api.dynamicCreate, queryParameters: { + 'platform': 'web', + 'csrf': await Request.getCsrf(), + 'x-bili-device-req-json': {'platform': 'web', 'device': 'pc'}, + 'x-bili-web-req-json': {'spm_id': '333.999'}, + }, data: { + 'dyn_req': { + 'content': { + 'contents': [ + {'raw_text': rawText ?? '', 'type': 1, 'biz_id': ''} + ] + }, + 'scene': scene, + 'attach_card': null, + 'upload_id': uploadId, + 'meta': { + 'app_meta': {'from': 'create.dynamic.web', 'mobi_app': 'web'} + } + }, + 'web_repost_src': webRepostSrc + }); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': res.data['data'], + }; + } else { + return { + 'status': false, + 'data': [], + 'msg': res.data['message'], + }; + } + } } diff --git a/lib/http/user.dart b/lib/http/user.dart index fea0a22e..dfdf187e 100644 --- a/lib/http/user.dart +++ b/lib/http/user.dart @@ -395,4 +395,21 @@ class UserHttp { return {'status': false, 'msg': res.data['message']}; } } + + // 删除文件夹 + static Future delFavFolder({required int mediaIds}) async { + var res = await Request().post( + Api.delFavFolder, + queryParameters: { + 'media_ids': mediaIds, + 'platform': 'web', + 'csrf': await Request.getCsrf(), + }, + ); + if (res.data['code'] == 0) { + return {'status': true}; + } else { + return {'status': false, 'msg': res.data['message']}; + } + } } diff --git a/lib/http/video.dart b/lib/http/video.dart index d43656b2..bdfaedf7 100644 --- a/lib/http/video.dart +++ b/lib/http/video.dart @@ -192,22 +192,15 @@ class VideoHttp { // 视频信息 标题、简介 static Future videoIntro({required String bvid}) async { var res = await Request().get(Api.videoIntro, data: {'bvid': bvid}); - VideoDetailResponse result = VideoDetailResponse.fromJson(res.data); - if (result.code == 0) { + if (res.data['code'] == 0) { + VideoDetailResponse result = VideoDetailResponse.fromJson(res.data); return {'status': true, 'data': result.data!}; } else { - Map errMap = { - -400: '请求错误', - -403: '权限不足', - -404: '视频资源失效', - 62002: '稿件不可见', - 62004: '稿件审核中', - }; return { 'status': false, 'data': null, - 'code': result.code, - 'msg': errMap[result.code] ?? '请求异常', + 'code': res.data['code'], + 'msg': res.data['message'], }; } } diff --git a/lib/main.dart b/lib/main.dart index 604441e8..3877685c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -130,27 +130,10 @@ class MyApp extends StatelessWidget { ); } - final SnackBarThemeData snackBarThemeData = SnackBarThemeData( - actionTextColor: darkColorScheme.primary, - backgroundColor: darkColorScheme.secondaryContainer, - closeIconColor: darkColorScheme.secondary, - contentTextStyle: TextStyle(color: darkColorScheme.secondary), - elevation: 20, - ); - ThemeData themeData = ThemeData( - // fontFamily: 'HarmonyOS', colorScheme: currentThemeValue == ThemeType.dark ? darkColorScheme : lightColorScheme, - snackBarTheme: snackBarThemeData, - pageTransitionsTheme: const PageTransitionsTheme( - builders: { - TargetPlatform.android: ZoomPageTransitionsBuilder( - allowEnterRouteSnapshotting: false, - ), - }, - ), ); // 小白条、导航栏沉浸 @@ -171,9 +154,38 @@ class MyApp extends StatelessWidget { // 图片缓存 // PaintingBinding.instance.imageCache.maximumSizeBytes = 1000 << 20; return GetMaterialApp( - title: 'PiLiPaLa', - theme: themeData, - darkTheme: themeData, + title: 'PiliPala', + theme: ThemeData( + colorScheme: currentThemeValue == ThemeType.dark + ? darkColorScheme + : lightColorScheme, + snackBarTheme: SnackBarThemeData( + actionTextColor: lightColorScheme.primary, + backgroundColor: lightColorScheme.secondaryContainer, + closeIconColor: lightColorScheme.secondary, + contentTextStyle: TextStyle(color: lightColorScheme.secondary), + elevation: 20, + ), + pageTransitionsTheme: const PageTransitionsTheme( + builders: { + TargetPlatform.android: ZoomPageTransitionsBuilder( + allowEnterRouteSnapshotting: false, + ), + }, + ), + ), + darkTheme: ThemeData( + colorScheme: currentThemeValue == ThemeType.light + ? lightColorScheme + : darkColorScheme, + snackBarTheme: SnackBarThemeData( + actionTextColor: darkColorScheme.primary, + backgroundColor: darkColorScheme.secondaryContainer, + closeIconColor: darkColorScheme.secondary, + contentTextStyle: TextStyle(color: darkColorScheme.secondary), + elevation: 20, + ), + ), localizationsDelegates: const [ GlobalCupertinoLocalizations.delegate, GlobalMaterialLocalizations.delegate, diff --git a/lib/models/common/rank_type.dart b/lib/models/common/rank_type.dart index 2ce6d3b5..07be15c2 100644 --- a/lib/models/common/rank_type.dart +++ b/lib/models/common/rank_type.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:pilipala/pages/rank/zone/index.dart'; enum RandType { @@ -74,7 +73,6 @@ List tabsConfig = [ ), 'label': '全站', 'type': RandType.all, - 'ctr': Get.put, 'page': const ZonePage(rid: 0), }, { @@ -84,7 +82,6 @@ List tabsConfig = [ ), 'label': '国创相关', 'type': RandType.creation, - 'ctr': Get.put, 'page': const ZonePage(rid: 168), }, { @@ -94,7 +91,6 @@ List tabsConfig = [ ), 'label': '动画', 'type': RandType.animation, - 'ctr': Get.put, 'page': const ZonePage(rid: 1), }, { @@ -104,7 +100,6 @@ List tabsConfig = [ ), 'label': '音乐', 'type': RandType.music, - 'ctr': Get.put, 'page': const ZonePage(rid: 3), }, { @@ -114,7 +109,6 @@ List tabsConfig = [ ), 'label': '舞蹈', 'type': RandType.dance, - 'ctr': Get.put, 'page': const ZonePage(rid: 129), }, { @@ -124,7 +118,6 @@ List tabsConfig = [ ), 'label': '游戏', 'type': RandType.game, - 'ctr': Get.put, 'page': const ZonePage(rid: 4), }, { @@ -134,7 +127,6 @@ List tabsConfig = [ ), 'label': '知识', 'type': RandType.knowledge, - 'ctr': Get.put, 'page': const ZonePage(rid: 36), }, { @@ -144,7 +136,6 @@ List tabsConfig = [ ), 'label': '科技', 'type': RandType.technology, - 'ctr': Get.put, 'page': const ZonePage(rid: 188), }, { @@ -154,7 +145,6 @@ List tabsConfig = [ ), 'label': '运动', 'type': RandType.sport, - 'ctr': Get.put, 'page': const ZonePage(rid: 234), }, { @@ -164,7 +154,6 @@ List tabsConfig = [ ), 'label': '汽车', 'type': RandType.car, - 'ctr': Get.put, 'page': const ZonePage(rid: 223), }, { @@ -174,7 +163,6 @@ List tabsConfig = [ ), 'label': '生活', 'type': RandType.life, - 'ctr': Get.put, 'page': const ZonePage(rid: 160), }, { @@ -184,7 +172,6 @@ List tabsConfig = [ ), 'label': '美食', 'type': RandType.food, - 'ctr': Get.put, 'page': const ZonePage(rid: 211), }, { @@ -194,7 +181,6 @@ List tabsConfig = [ ), 'label': '动物圈', 'type': RandType.animal, - 'ctr': Get.put, 'page': const ZonePage(rid: 217), }, { @@ -204,7 +190,6 @@ List tabsConfig = [ ), 'label': '鬼畜', 'type': RandType.madness, - 'ctr': Get.put, 'page': const ZonePage(rid: 119), }, { @@ -214,7 +199,6 @@ List tabsConfig = [ ), 'label': '时尚', 'type': RandType.fashion, - 'ctr': Get.put, 'page': const ZonePage(rid: 155), }, { @@ -224,7 +208,6 @@ List tabsConfig = [ ), 'label': '娱乐', 'type': RandType.entertainment, - 'ctr': Get.put, 'page': const ZonePage(rid: 5), }, { @@ -234,7 +217,6 @@ List tabsConfig = [ ), 'label': '影视', 'type': RandType.film, - 'ctr': Get.put, 'page': const ZonePage(rid: 181), } ]; diff --git a/lib/models/common/subtitle_type.dart b/lib/models/common/subtitle_type.dart index 11716351..54b52e8e 100644 --- a/lib/models/common/subtitle_type.dart +++ b/lib/models/common/subtitle_type.dart @@ -5,6 +5,10 @@ enum SubtitleType { aizh, // 英语(自动生成) aien, + // 中文(简体) + zhHans, + // 英文(美国) + enUS, } extension SubtitleTypeExtension on SubtitleType { @@ -16,6 +20,10 @@ extension SubtitleTypeExtension on SubtitleType { return '中文(自动翻译)'; case SubtitleType.aien: return '英语(自动生成)'; + case SubtitleType.zhHans: + return '中文(简体)'; + case SubtitleType.enUS: + return '英文(美国)'; } } } @@ -29,6 +37,10 @@ extension SubtitleIdExtension on SubtitleType { return 'ai-zh'; case SubtitleType.aien: return 'ai-en'; + case SubtitleType.zhHans: + return 'zh-Hans'; + case SubtitleType.enUS: + return 'en-US'; } } } @@ -42,6 +54,10 @@ extension SubtitleCodeExtension on SubtitleType { return 2; case SubtitleType.aien: return 3; + case SubtitleType.zhHans: + return 4; + case SubtitleType.enUS: + return 5; } } } diff --git a/lib/models/video/play/quality.dart b/lib/models/video/play/quality.dart index 6cae84cc..7bedf62a 100644 --- a/lib/models/video/play/quality.dart +++ b/lib/models/video/play/quality.dart @@ -39,6 +39,14 @@ extension VideoQualityCode on VideoQuality { } return null; } + + static int? toCode(VideoQuality quality) { + final index = VideoQuality.values.indexOf(quality); + if (index != -1 && index < _codeList.length) { + return _codeList[index]; + } + return null; + } } extension VideoQualityDesc on VideoQuality { diff --git a/lib/models/video_detail_res.dart b/lib/models/video_detail_res.dart index 38e0b877..a82b6fbb 100644 --- a/lib/models/video_detail_res.dart +++ b/lib/models/video_detail_res.dart @@ -67,6 +67,7 @@ class VideoDetailData { String? likeIcon; bool? needJumpBv; String? epId; + List? staff; VideoDetailData({ this.bvid, @@ -103,6 +104,7 @@ class VideoDetailData { this.likeIcon, this.needJumpBv, this.epId, + this.staff, }); VideoDetailData.fromJson(Map json) { @@ -155,6 +157,9 @@ class VideoDetailData { if (json['redirect_url'] != null) { epId = resolveEpId(json['redirect_url']); } + staff = json["staff"] != null + ? List.from(json["staff"]!.map((e) => Staff.fromJson(e))) + : null; } String resolveEpId(url) { @@ -652,3 +657,43 @@ class EpisodeItem { bvid = json['bvid']; } } + +class Staff { + Staff({ + this.mid, + this.title, + this.name, + this.face, + this.vip, + }); + + int? mid; + String? title; + String? name; + String? face; + int? status; + Vip? vip; + + Staff.fromJson(Map json) { + mid = json['mid']; + title = json['title']; + name = json['name']; + face = json['face']; + vip = Vip.fromJson(json['vip']); + } +} + +class Vip { + Vip({ + this.type, + this.status, + }); + + int? type; + int? status; + + Vip.fromJson(Map json) { + type = json['type']; + status = json['status']; + } +} diff --git a/lib/pages/bangumi/introduction/controller.dart b/lib/pages/bangumi/introduction/controller.dart index 2098302d..575a77de 100644 --- a/lib/pages/bangumi/introduction/controller.dart +++ b/lib/pages/bangumi/introduction/controller.dart @@ -131,51 +131,37 @@ class BangumiIntroController extends GetxController { builder: (context) { return AlertDialog( title: const Text('选择投币个数'), - contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 12), + contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 24), content: StatefulBuilder(builder: (context, StateSetter setState) { return Column( mainAxisSize: MainAxisSize.min, - children: [ - RadioListTile( - value: 1, - title: const Text('1枚'), - groupValue: _tempThemeValue, - onChanged: (value) { - _tempThemeValue = value!; - Get.appUpdate(); - }, - ), - RadioListTile( - value: 2, - title: const Text('2枚'), - groupValue: _tempThemeValue, - onChanged: (value) { - _tempThemeValue = value!; - Get.appUpdate(); - }, - ), - ], + children: [1, 2] + .map( + (e) => RadioListTile( + value: e, + title: Text('$e枚'), + groupValue: _tempThemeValue, + onChanged: (value) async { + _tempThemeValue = value!; + setState(() {}); + 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(); + }, + ), + ) + .toList(), ); }), - 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('确定'), - ) - ], ); }); } @@ -236,6 +222,7 @@ class BangumiIntroController extends GetxController { videoDetailCtr.bvid = bvid; videoDetailCtr.cid.value = cid; videoDetailCtr.danmakuCid.value = cid; + videoDetailCtr.oid.value = aid; videoDetailCtr.queryVideoUrl(); // 重新请求评论 try { diff --git a/lib/pages/bangumi/widgets/bangumi_panel.dart b/lib/pages/bangumi/widgets/bangumi_panel.dart index b01f3be7..3e965f34 100644 --- a/lib/pages/bangumi/widgets/bangumi_panel.dart +++ b/lib/pages/bangumi/widgets/bangumi_panel.dart @@ -85,7 +85,9 @@ class _BangumiPanelState extends State { item.cid, item.aid, ); - _bottomSheetController?.close(); + if (_bottomSheetController != null) { + _bottomSheetController?.close(); + } currentIndex = i; scrollToIndex(); } diff --git a/lib/pages/dynamics/detail/view.dart b/lib/pages/dynamics/detail/view.dart index 9da085f4..c6ec682a 100644 --- a/lib/pages/dynamics/detail/view.dart +++ b/lib/pages/dynamics/detail/view.dart @@ -16,6 +16,7 @@ import 'package:pilipala/pages/video/detail/reply_reply/index.dart'; import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/id_utils.dart'; +import '../../../models/video/reply/item.dart'; import '../widgets/dynamic_panel.dart'; class DynamicDetailPage extends StatefulWidget { @@ -182,6 +183,7 @@ class _DynamicDetailPageState extends State scrollController.removeListener(() {}); fabAnimationCtr.dispose(); scrollController.dispose(); + titleStreamC.close(); super.dispose(); } @@ -210,208 +212,194 @@ class _DynamicDetailPageState extends State onRefresh: () async { await _dynamicDetailController.queryReplyList(); }, - child: Stack( - children: [ - CustomScrollView( - controller: scrollController, - slivers: [ - if (action != 'comment') - SliverToBoxAdapter( - child: DynamicPanel( - item: _dynamicDetailController.item, - source: 'detail', + child: CustomScrollView( + controller: scrollController, + slivers: [ + if (action != 'comment') + SliverToBoxAdapter( + child: DynamicPanel( + item: _dynamicDetailController.item, + source: 'detail', + ), + ), + SliverPersistentHeader( + delegate: _MySliverPersistentHeaderDelegate( + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + border: Border( + top: BorderSide( + width: 0.6, + color: Theme.of(context).dividerColor.withOpacity(0.05), + ), ), ), - SliverPersistentHeader( - delegate: _MySliverPersistentHeaderDelegate( - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - border: Border( - top: BorderSide( - width: 0.6, - color: Theme.of(context) - .dividerColor - .withOpacity(0.05), + height: 45, + padding: const EdgeInsets.only(left: 12, right: 6), + child: Row( + children: [ + Obx( + () => AnimatedSwitcher( + duration: const Duration(milliseconds: 400), + transitionBuilder: + (Widget child, Animation animation) { + return ScaleTransition( + scale: animation, child: child); + }, + child: Text( + '${_dynamicDetailController.acount.value}', + key: ValueKey( + _dynamicDetailController.acount.value), ), ), ), - height: 45, - padding: const EdgeInsets.only(left: 12, right: 6), - child: Row( - children: [ - Obx( - () => AnimatedSwitcher( - duration: const Duration(milliseconds: 400), - transitionBuilder: - (Widget child, Animation animation) { - return ScaleTransition( - scale: animation, child: child); - }, - child: Text( - '${_dynamicDetailController.acount.value}', - key: ValueKey( - _dynamicDetailController.acount.value), - ), - ), - ), - const Text('条回复'), - const Spacer(), - SizedBox( - height: 35, - child: TextButton.icon( - onPressed: () => - _dynamicDetailController.queryBySort(), - icon: const Icon(Icons.sort, size: 16), - label: Obx(() => Text( - _dynamicDetailController - .sortTypeLabel.value, - style: const TextStyle(fontSize: 13), - )), - ), - ) - ], - ), - ), + const Text('条回复'), + const Spacer(), + SizedBox( + height: 35, + child: TextButton.icon( + onPressed: () => + _dynamicDetailController.queryBySort(), + icon: const Icon(Icons.sort, size: 16), + label: Obx(() => Text( + _dynamicDetailController.sortTypeLabel.value, + style: const TextStyle(fontSize: 13), + )), + ), + ) + ], ), - pinned: true, - ), - FutureBuilder( - future: _futureBuilderFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - Map data = snapshot.data as Map; - if (snapshot.data['status']) { - // 请求成功 - return Obx( - () => _dynamicDetailController.replyList.isEmpty && - _dynamicDetailController.isLoadingMore - ? SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - return const VideoReplySkeleton(); - }, childCount: 8), - ) - : SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - if (index == - _dynamicDetailController - .replyList.length) { - return Container( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context) - .padding - .bottom), - height: MediaQuery.of(context) - .padding - .bottom + - 100, - child: Center( - child: Obx( - () => Text( - _dynamicDetailController - .noMore.value, - style: TextStyle( - fontSize: 12, - color: Theme.of(context) - .colorScheme - .outline, - ), - ), - ), - ), - ); - } else { - return ReplyItem( - replyItem: _dynamicDetailController - .replyList[index], - showReplyRow: true, - replyLevel: '1', - replyReply: (replyItem) => - replyReply(replyItem), - replyType: - ReplyType.values[replyType], - addReply: (replyItem) { - _dynamicDetailController - .replyList[index].replies! - .add(replyItem); - }, - ); - } - }, - childCount: _dynamicDetailController - .replyList.length + - 1, - ), - ), - ); - } else { - // 请求错误 - return HttpError( - errMsg: data['msg'], - fn: () => setState(() {}), - ); - } - } else { - // 骨架屏 - return SliverList( - delegate: SliverChildBuilderDelegate((context, index) { - return const VideoReplySkeleton(); - }, childCount: 8), - ); - } - }, - ) - ], - ), - Positioned( - bottom: MediaQuery.of(context).padding.bottom + 14, - right: 14, - child: SlideTransition( - position: Tween( - begin: const Offset(0, 2), - end: const Offset(0, 0), - ).animate(CurvedAnimation( - parent: fabAnimationCtr, - curve: Curves.easeInOut, - )), - child: FloatingActionButton( - heroTag: null, - onPressed: () { - feedBack(); - showModalBottomSheet( - context: context, - isScrollControlled: true, - builder: (BuildContext context) { - return VideoReplyNewDialog( - oid: _dynamicDetailController.oid ?? - IdUtils.bv2av(Get.parameters['bvid']!), - root: 0, - parent: 0, - replyType: ReplyType.values[replyType], - ); - }, - ).then( - (value) => { - // 完成评论,数据添加 - if (value != null && value['data'] != null) - { - _dynamicDetailController.replyList - .add(value['data']), - _dynamicDetailController.acount.value++ - } - }, - ); - }, - tooltip: '评论动态', - child: const Icon(Icons.reply), ), ), + pinned: true, ), + FutureBuilder( + future: _futureBuilderFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + Map data = snapshot.data as Map; + if (snapshot.data['status']) { + RxList replyList = + _dynamicDetailController.replyList; + // 请求成功 + return Obx( + () => replyList.isEmpty && + _dynamicDetailController.isLoadingMore + ? SliverList( + delegate: + SliverChildBuilderDelegate((context, index) { + return const VideoReplySkeleton(); + }, childCount: 8), + ) + : SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + if (index == replyList.length) { + return Container( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context) + .padding + .bottom), + height: MediaQuery.of(context) + .padding + .bottom + + 100, + child: Center( + child: Obx( + () => Text( + _dynamicDetailController + .noMore.value, + style: TextStyle( + fontSize: 12, + color: Theme.of(context) + .colorScheme + .outline, + ), + ), + ), + ), + ); + } else { + return ReplyItem( + replyItem: replyList[index], + showReplyRow: true, + replyLevel: '1', + replyReply: (replyItem) => + replyReply(replyItem), + replyType: ReplyType.values[replyType], + addReply: (replyItem) { + replyList[index] + .replies! + .add(replyItem); + }, + ); + } + }, + childCount: replyList.length + 1, + ), + ), + ); + } else { + // 请求错误 + return HttpError( + errMsg: data['msg'], + fn: () => setState(() {}), + ); + } + } else { + // 骨架屏 + return SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + return const VideoReplySkeleton(); + }, childCount: 8), + ); + } + }, + ) ], ), ), + floatingActionButton: SlideTransition( + position: Tween( + begin: const Offset(0, 2), + end: const Offset(0, 0), + ).animate( + CurvedAnimation( + parent: fabAnimationCtr, + curve: Curves.easeInOut, + ), + ), + child: FloatingActionButton( + heroTag: null, + onPressed: () { + feedBack(); + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (BuildContext context) { + return VideoReplyNewDialog( + oid: _dynamicDetailController.oid ?? + IdUtils.bv2av(Get.parameters['bvid']!), + root: 0, + parent: 0, + replyType: ReplyType.values[replyType], + ); + }, + ).then( + (value) => { + // 完成评论,数据添加 + if (value != null && value['data'] != null) + { + _dynamicDetailController.replyList.add(value['data']), + _dynamicDetailController.acount.value++ + } + }, + ); + }, + tooltip: '评论动态', + child: const Icon(Icons.reply), + ), + ), ); } } diff --git a/lib/pages/dynamics/widgets/action_panel.dart b/lib/pages/dynamics/widgets/action_panel.dart index 0ca09b8c..51ef3952 100644 --- a/lib/pages/dynamics/widgets/action_panel.dart +++ b/lib/pages/dynamics/widgets/action_panel.dart @@ -3,38 +3,58 @@ import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; +import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/http/dynamics.dart'; import 'package:pilipala/models/dynamics/result.dart'; import 'package:pilipala/pages/dynamics/index.dart'; import 'package:pilipala/utils/feed_back.dart'; +import 'package:status_bar_control/status_bar_control.dart'; +import 'rich_node_panel.dart'; class ActionPanel extends StatefulWidget { const ActionPanel({ super.key, - this.item, + required this.item, }); // ignore: prefer_typing_uninitialized_variables - final item; + final DynamicItemModel item; @override State createState() => _ActionPanelState(); } -class _ActionPanelState extends State { +class _ActionPanelState extends State + with TickerProviderStateMixin { final DynamicsController _dynamicsController = Get.put(DynamicsController()); late ModuleStatModel stat; bool isProcessing = false; + double defaultHeight = 260; + RxDouble height = 0.0.obs; + RxBool isExpand = false.obs; + late double statusHeight; + TextEditingController _inputController = TextEditingController(); + FocusNode myFocusNode = FocusNode(); + String _inputText = ''; + void Function()? handleState(Future Function() action) { - return isProcessing ? null : () async { - setState(() => isProcessing = true); - await action(); - setState(() => isProcessing = false); - }; + return isProcessing + ? null + : () async { + isProcessing = true; + await action(); + isProcessing = false; + }; } + @override void initState() { super.initState(); - stat = widget.item!.modules.moduleStat; + stat = widget.item.modules!.moduleStat!; + onInit(); + } + + onInit() async { + statusHeight = await StatusBarControl.getHeight; } // 动态点赞 @@ -43,7 +63,7 @@ class _ActionPanelState extends State { var item = widget.item!; String dynamicId = item.idStr!; // 1 已点赞 2 不喜欢 0 未操作 - Like like = item.modules.moduleStat.like; + Like like = item.modules!.moduleStat!.like!; int count = like.count == '点赞' ? 0 : int.parse(like.count ?? '0'); bool status = like.status!; int up = status ? 2 : 1; @@ -51,15 +71,15 @@ class _ActionPanelState extends State { if (res['status']) { SmartDialog.showToast(!status ? '点赞成功' : '取消赞'); if (up == 1) { - item.modules.moduleStat.like.count = (count + 1).toString(); - item.modules.moduleStat.like.status = true; + item.modules!.moduleStat!.like!.count = (count + 1).toString(); + item.modules!.moduleStat!.like!.status = true; } else { if (count == 1) { - item.modules.moduleStat.like.count = '点赞'; + item.modules!.moduleStat!.like!.count = '点赞'; } else { - item.modules.moduleStat.like.count = (count - 1).toString(); + item.modules!.moduleStat!.like!.count = (count - 1).toString(); } - item.modules.moduleStat.like.status = false; + item.modules!.moduleStat!.like!.status = false; } setState(() {}); } else { @@ -67,17 +87,307 @@ class _ActionPanelState extends State { } } + // 转发动态预览 + Widget dynamicPreview() { + ItemModulesModel? modules = widget.item.modules; + final String type = widget.item.type!; + String? cover = modules?.moduleAuthor?.face; + switch (type) { + /// 图文动态 + case 'DYNAMIC_TYPE_DRAW': + cover = modules?.moduleDynamic?.major?.opus?.pics?.first.url; + + /// 投稿 + case 'DYNAMIC_TYPE_AV': + cover = modules?.moduleDynamic?.major?.archive?.cover; + + /// 转发的动态 + case 'DYNAMIC_TYPE_FORWARD': + String forwardType = widget.item.orig!.type!; + switch (forwardType) { + /// 图文动态 + case 'DYNAMIC_TYPE_DRAW': + cover = modules?.moduleDynamic?.major?.opus?.pics?.first.url; + + /// 投稿 + case 'DYNAMIC_TYPE_AV': + cover = modules?.moduleDynamic?.major?.archive?.cover; + + /// 专栏文章 + case 'DYNAMIC_TYPE_ARTICLE': + cover = ''; + + /// 番剧 + case 'DYNAMIC_TYPE_PGC': + cover = ''; + + /// 纯文字动态 + case 'DYNAMIC_TYPE_WORD': + cover = ''; + + /// 直播 + case 'DYNAMIC_TYPE_LIVE_RCMD': + cover = ''; + + /// 合集查看 + case 'DYNAMIC_TYPE_UGC_SEASON': + cover = ''; + + /// 番剧 + case 'DYNAMIC_TYPE_PGC_UNION': + cover = modules?.moduleDynamic?.major?.pgc?.cover; + + default: + cover = ''; + } + + /// 专栏文章 + case 'DYNAMIC_TYPE_ARTICLE': + cover = ''; + + /// 番剧 + case 'DYNAMIC_TYPE_PGC': + cover = ''; + + /// 纯文字动态 + case 'DYNAMIC_TYPE_WORD': + cover = ''; + + /// 直播 + case 'DYNAMIC_TYPE_LIVE_RCMD': + cover = ''; + + /// 合集查看 + case 'DYNAMIC_TYPE_UGC_SEASON': + cover = ''; + + /// 番剧查看 + case 'DYNAMIC_TYPE_PGC_UNION': + cover = ''; + + default: + cover = ''; + } + return Container( + width: double.infinity, + height: 95, + margin: const EdgeInsets.fromLTRB(12, 0, 12, 14), + decoration: BoxDecoration( + color: + Theme.of(context).colorScheme.secondaryContainer.withOpacity(0.4), + borderRadius: BorderRadius.circular(6), + border: Border( + left: BorderSide( + width: 4, + color: Theme.of(context).colorScheme.primary.withOpacity(0.8)), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 14), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '@${widget.item.modules!.moduleAuthor!.name}', + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + NetworkImgLayer( + src: cover ?? '', + width: 34, + height: 34, + type: 'emote', + ), + const SizedBox(width: 10), + Expanded( + child: Text.rich( + style: const TextStyle(height: 0), + richNode(widget.item, context), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + // Text(data) + ], + ) + ], + ), + ), + ); + } + + // 动态转发 + void forwardHandler() async { + showModalBottomSheet( + context: context, + enableDrag: false, + useRootNavigator: true, + isScrollControlled: true, + builder: (context) { + return Obx( + () => AnimatedContainer( + duration: Durations.medium1, + onEnd: () async { + if (isExpand.value) { + await Future.delayed(const Duration(milliseconds: 80)); + myFocusNode.requestFocus(); + } + }, + height: height.value + MediaQuery.of(context).padding.bottom, + child: Column( + children: [ + AnimatedContainer( + duration: Durations.medium1, + height: isExpand.value ? statusHeight : 0, + ), + Padding( + padding: EdgeInsets.fromLTRB( + isExpand.value ? 10 : 16, + 10, + isExpand.value ? 14 : 12, + 0, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (isExpand.value) ...[ + IconButton( + onPressed: () => togglePanelState(false), + icon: const Icon(Icons.close), + ), + Text( + '转发动态', + style: Theme.of(context) + .textTheme + .titleMedium! + .copyWith(fontWeight: FontWeight.bold), + ) + ] else ...[ + const Text( + '转发动态', + style: TextStyle(fontWeight: FontWeight.bold), + ) + ], + isExpand.value + ? FilledButton( + onPressed: () => dynamicForward('forward'), + child: const Text('转发'), + ) + : TextButton( + onPressed: () {}, + child: const Text('立即转发'), + ) + ], + ), + ), + if (!isExpand.value) ...[ + GestureDetector( + onTap: () => togglePanelState(true), + behavior: HitTestBehavior.translucent, + child: Container( + width: double.infinity, + alignment: Alignment.centerLeft, + padding: const EdgeInsets.fromLTRB(16, 0, 10, 14), + child: Text( + '说点什么吧', + textAlign: TextAlign.start, + style: TextStyle( + color: Theme.of(context).colorScheme.outline), + ), + ), + ), + ] else ...[ + Padding( + padding: const EdgeInsets.fromLTRB(16, 10, 16, 0), + child: TextField( + maxLines: 5, + focusNode: myFocusNode, + controller: _inputController, + onChanged: (value) { + setState(() { + _inputText = value; + }); + }, + decoration: const InputDecoration( + border: InputBorder.none, + hintText: '说点什么吧', + ), + ), + ), + ], + dynamicPreview(), + if (!isExpand.value) ...[ + const Divider(thickness: 0.1, height: 1), + ListTile( + onTap: () => Get.back(), + minLeadingWidth: 0, + dense: true, + title: Text( + '取消', + style: TextStyle( + color: Theme.of(context).colorScheme.outline), + textAlign: TextAlign.center, + ), + ), + ] + ], + ), + ), + ); + }, + ); + } + + togglePanelState(status) { + if (!status) { + Get.back(); + height.value = defaultHeight; + _inputText = ''; + _inputController.clear(); + } else { + height.value = Get.size.height; + } + isExpand.value = !(isExpand.value); + } + + dynamicForward(String type) async { + String dynamicId = widget.item.idStr!; + var res = await DynamicsHttp.dynamicCreate( + dynIdStr: dynamicId, + mid: _dynamicsController.userInfo.mid, + rawText: _inputText, + scene: 4, + ); + if (res['status']) { + SmartDialog.showToast(type == 'forward' ? '转发成功' : '发布成功'); + togglePanelState(false); + } + } + + @override + void dispose() { + myFocusNode.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { var color = Theme.of(context).colorScheme.outline; var primary = Theme.of(context).colorScheme.primary; + height.value = defaultHeight; + print('height.value: ${height.value}'); return Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Expanded( flex: 1, child: TextButton.icon( - onPressed: () {}, + onPressed: forwardHandler, icon: const Icon( FontAwesomeIcons.shareFromSquare, size: 16, diff --git a/lib/pages/dynamics/widgets/dynamic_panel.dart b/lib/pages/dynamics/widgets/dynamic_panel.dart index c85cad45..d273a1a6 100644 --- a/lib/pages/dynamics/widgets/dynamic_panel.dart +++ b/lib/pages/dynamics/widgets/dynamic_panel.dart @@ -1,15 +1,16 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/pages/dynamics/index.dart'; +import '../../../models/dynamics/result.dart'; import 'action_panel.dart'; import 'author_panel.dart'; import 'content_panel.dart'; import 'forward_panel.dart'; class DynamicPanel extends StatelessWidget { - final dynamic item; + final DynamicItemModel item; final String? source; - DynamicPanel({this.item, this.source, Key? key}) : super(key: key); + DynamicPanel({required this.item, this.source, Key? key}) : super(key: key); final DynamicsController _dynamicsController = Get.put(DynamicsController()); @override @@ -41,8 +42,8 @@ class DynamicPanel extends StatelessWidget { padding: const EdgeInsets.fromLTRB(12, 12, 12, 8), child: AuthorPanel(item: item), ), - if (item!.modules!.moduleDynamic!.desc != null || - item!.modules!.moduleDynamic!.major != null) + if (item.modules!.moduleDynamic!.desc != null || + item.modules!.moduleDynamic!.major != null) Content(item: item, source: source), forWard(item, context, _dynamicsController, source), const SizedBox(height: 2), diff --git a/lib/pages/fav/controller.dart b/lib/pages/fav/controller.dart index a5f94525..2307d303 100644 --- a/lib/pages/fav/controller.dart +++ b/lib/pages/fav/controller.dart @@ -10,10 +10,11 @@ import 'package:pilipala/utils/storage.dart'; class FavController extends GetxController { final ScrollController scrollController = ScrollController(); Rx favFolderData = FavFolderData().obs; + RxList favFolderList = [].obs; Box userInfoCache = GStrorage.userInfo; UserInfoData? userInfo; int currentPage = 1; - int pageSize = 10; + int pageSize = 60; RxBool hasMore = true.obs; Future queryFavFolder({type = 'init'}) async { @@ -32,9 +33,10 @@ class FavController extends GetxController { if (res['status']) { if (type == 'init') { favFolderData.value = res['data']; + favFolderList.value = res['data'].list; } else { if (res['data'].list.isNotEmpty) { - favFolderData.value.list!.addAll(res['data'].list); + favFolderList.addAll(res['data'].list); favFolderData.update((val) {}); } } @@ -49,4 +51,13 @@ class FavController extends GetxController { Future onLoad() async { queryFavFolder(type: 'onload'); } + + removeFavFolder({required int mediaIds}) async { + for (var i in favFolderList) { + if (i.id == mediaIds) { + favFolderList.remove(i); + break; + } + } + } } diff --git a/lib/pages/fav/view.dart b/lib/pages/fav/view.dart index b980914a..424a885d 100644 --- a/lib/pages/fav/view.dart +++ b/lib/pages/fav/view.dart @@ -62,11 +62,10 @@ class _FavPageState extends State { return Obx( () => ListView.builder( controller: scrollController, - itemCount: _favController.favFolderData.value.list!.length, + itemCount: _favController.favFolderList.length, itemBuilder: (context, index) { return FavItem( - favFolderItem: - _favController.favFolderData.value.list![index]); + favFolderItem: _favController.favFolderList[index]); }, ), ); diff --git a/lib/pages/fav/widgets/item.dart b/lib/pages/fav/widgets/item.dart index 08730d7b..3c44ec9d 100644 --- a/lib/pages/fav/widgets/item.dart +++ b/lib/pages/fav/widgets/item.dart @@ -13,14 +13,16 @@ class FavItem extends StatelessWidget { Widget build(BuildContext context) { String heroTag = Utils.makeHeroTag(favFolderItem.fid); return InkWell( - onTap: () => Get.toNamed( - '/favDetail', - arguments: favFolderItem, - parameters: { - 'heroTag': heroTag, - 'mediaId': favFolderItem.id.toString(), - }, - ), + onTap: () async { + Get.toNamed( + '/favDetail', + arguments: favFolderItem, + parameters: { + 'heroTag': heroTag, + 'mediaId': favFolderItem.id.toString(), + }, + ); + }, child: Padding( padding: const EdgeInsets.fromLTRB(12, 7, 12, 7), child: LayoutBuilder( diff --git a/lib/pages/fav_detail/controller.dart b/lib/pages/fav_detail/controller.dart index 55d5b884..7af398e8 100644 --- a/lib/pages/fav_detail/controller.dart +++ b/lib/pages/fav_detail/controller.dart @@ -1,9 +1,11 @@ +import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:pilipala/http/user.dart'; import 'package:pilipala/http/video.dart'; import 'package:pilipala/models/user/fav_detail.dart'; import 'package:pilipala/models/user/fav_folder.dart'; +import 'package:pilipala/pages/fav/index.dart'; class FavDetailController extends GetxController { FavFolderItemData? item; @@ -74,4 +76,41 @@ class FavDetailController extends GetxController { onLoad() { queryUserFavFolderDetail(type: 'onLoad'); } + + onDelFavFolder() async { + SmartDialog.show( + useSystem: true, + animationType: SmartAnimationType.centerFade_otherSlide, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('提示'), + content: const Text('确定删除这个收藏夹吗?'), + actions: [ + TextButton( + onPressed: () async { + SmartDialog.dismiss(); + }, + child: Text( + '点错了', + style: TextStyle(color: Theme.of(context).colorScheme.outline), + ), + ), + TextButton( + onPressed: () async { + var res = await UserHttp.delFavFolder(mediaIds: mediaId!); + SmartDialog.dismiss(); + SmartDialog.showToast(res['status'] ? '操作成功' : res['msg']); + if (res['status']) { + FavController favController = Get.find(); + await favController.removeFavFolder(mediaIds: mediaId!); + Get.back(); + } + }, + child: const Text('确认'), + ) + ], + ); + }, + ); + } } diff --git a/lib/pages/fav_detail/view.dart b/lib/pages/fav_detail/view.dart index 74faa829..1bf5cb6f 100644 --- a/lib/pages/fav_detail/view.dart +++ b/lib/pages/fav_detail/view.dart @@ -53,6 +53,7 @@ class _FavDetailPageState extends State { @override void dispose() { _controller.dispose(); + titleStreamC.close(); super.dispose(); } @@ -100,11 +101,19 @@ class _FavDetailPageState extends State { Get.toNamed('/favSearch?searchType=0&mediaId=$mediaId'), icon: const Icon(Icons.search_outlined), ), - // IconButton( - // onPressed: () {}, - // icon: const Icon(Icons.more_vert), - // ), - const SizedBox(width: 6), + PopupMenuButton( + icon: const Icon(Icons.more_vert_outlined), + position: PopupMenuPosition.under, + onSelected: (String type) {}, + itemBuilder: (BuildContext context) => >[ + PopupMenuItem( + onTap: () => _favDetailController.onDelFavFolder(), + value: 'pause', + child: const Text('删除收藏夹'), + ), + ], + ), + const SizedBox(width: 14), ], flexibleSpace: FlexibleSpaceBar( background: Container( diff --git a/lib/pages/home/controller.dart b/lib/pages/home/controller.dart index ca70e1c4..f197fdfa 100644 --- a/lib/pages/home/controller.dart +++ b/lib/pages/home/controller.dart @@ -114,4 +114,10 @@ class HomeController extends GetxController with GetTickerProviderStateMixin { defaultSearch.value = res.data['data']['name']; } } + + @override + void onClose() { + searchBarStream.close(); + super.onClose(); + } } diff --git a/lib/pages/home/view.dart b/lib/pages/home/view.dart index cc228f6b..a25389bd 100644 --- a/lib/pages/home/view.dart +++ b/lib/pages/home/view.dart @@ -214,6 +214,34 @@ class UserInfoWidget extends StatelessWidget { final VoidCallback? callback; final HomeController? ctr; + Widget buildLoggedInWidget(context) { + return Stack( + children: [ + NetworkImgLayer( + type: 'avatar', + width: 34, + height: 34, + src: userFace, + ), + Positioned.fill( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => callback?.call(), + splashColor: Theme.of(context) + .colorScheme + .primaryContainer + .withOpacity(0.3), + borderRadius: const BorderRadius.all( + Radius.circular(50), + ), + ), + ), + ) + ], + ); + } + @override Widget build(BuildContext context) { return Row( @@ -231,31 +259,7 @@ class UserInfoWidget extends StatelessWidget { const SizedBox(width: 8), Obx( () => userLogin.value - ? Stack( - children: [ - NetworkImgLayer( - type: 'avatar', - width: 34, - height: 34, - src: userFace, - ), - Positioned.fill( - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: () => callback?.call(), - splashColor: Theme.of(context) - .colorScheme - .primaryContainer - .withOpacity(0.3), - borderRadius: const BorderRadius.all( - Radius.circular(50), - ), - ), - ), - ) - ], - ) + ? buildLoggedInWidget(context) : DefaultUser(callback: () => callback!()), ), ], @@ -402,30 +406,27 @@ class SearchBar extends StatelessWidget { color: colorScheme.onSecondaryContainer.withOpacity(0.05), child: InkWell( splashColor: colorScheme.primaryContainer.withOpacity(0.3), - onTap: () => Get.toNamed( - '/search', - parameters: {'hintText': ctr!.defaultSearch.value}, - ), - child: Row( - children: [ - const SizedBox(width: 14), - Icon( - Icons.search_outlined, - color: colorScheme.onSecondaryContainer, - ), - const SizedBox(width: 10), - Obx( - () => Expanded( - child: Text( + onTap: () => Get.toNamed('/search', + parameters: {'hintText': ctr!.defaultSearch.value}), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 14), + child: Row( + children: [ + Icon( + Icons.search_outlined, + color: colorScheme.onSecondaryContainer, + ), + const SizedBox(width: 10), + Obx( + () => Text( ctr!.defaultSearch.value, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(color: colorScheme.outline), ), ), - ), - const SizedBox(width: 15), - ], + ], + ), ), ), ), diff --git a/lib/pages/main/controller.dart b/lib/pages/main/controller.dart index c2e5c322..a77d9304 100644 --- a/lib/pages/main/controller.dart +++ b/lib/pages/main/controller.dart @@ -101,4 +101,10 @@ class MainController extends GetxController { selectedIndex = defaultIndex != -1 ? defaultIndex : 0; pages = navigationBars.map((e) => e['page']).toList(); } + + @override + void onClose() { + bottomBarStream.close(); + super.onClose(); + } } diff --git a/lib/pages/member/view.dart b/lib/pages/member/view.dart index 015750db..b6648647 100644 --- a/lib/pages/member/view.dart +++ b/lib/pages/member/view.dart @@ -54,6 +54,7 @@ class _MemberPageState extends State @override void dispose() { _extendNestCtr.removeListener(() {}); + appbarStream.close(); super.dispose(); } diff --git a/lib/pages/member_dynamics/view.dart b/lib/pages/member_dynamics/view.dart index 68aa72d7..2e093bcc 100644 --- a/lib/pages/member_dynamics/view.dart +++ b/lib/pages/member_dynamics/view.dart @@ -5,6 +5,7 @@ import 'package:pilipala/pages/member_dynamics/index.dart'; import 'package:pilipala/utils/utils.dart'; import '../../common/widgets/http_error.dart'; +import '../../models/dynamics/result.dart'; import '../dynamics/widgets/dynamic_panel.dart'; class MemberDynamicsPage extends StatefulWidget { @@ -66,7 +67,8 @@ class _MemberDynamicsPageState extends State { if (snapshot.connectionState == ConnectionState.done) { if (snapshot.data != null) { Map data = snapshot.data as Map; - List list = _memberDynamicController.dynamicsList; + RxList list = + _memberDynamicController.dynamicsList; if (data['status']) { return Obx( () => list.isNotEmpty diff --git a/lib/pages/rank/controller.dart b/lib/pages/rank/controller.dart index 6fe3d424..64395ec7 100644 --- a/lib/pages/rank/controller.dart +++ b/lib/pages/rank/controller.dart @@ -4,6 +4,7 @@ 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/pages/rank/zone/index.dart'; import 'package:pilipala/utils/storage.dart'; class RankController extends GetxController with GetTickerProviderStateMixin { @@ -29,20 +30,22 @@ class RankController extends GetxController with GetTickerProviderStateMixin { void onRefresh() { int index = tabController.index; - var ctr = tabsCtrList[index]; - ctr().onRefresh(); + final ZoneController ctr = tabsCtrList[index]; + ctr.onRefresh(); } void animateToTop() { int index = tabController.index; - var ctr = tabsCtrList[index]; - ctr().animateToTop(); + final ZoneController ctr = tabsCtrList[index]; + ctr.animateToTop(); } void setTabConfig() async { tabs.value = tabsConfig; initialIndex.value = 0; - tabsCtrList = tabs.map((e) => e['ctr']).toList(); + tabsCtrList = tabs + .map((e) => Get.put(ZoneController(), tag: e['rid'].toString())) + .toList(); tabsPageList = tabs.map((e) => e['page']).toList(); tabController = TabController( @@ -51,4 +54,10 @@ class RankController extends GetxController with GetTickerProviderStateMixin { vsync: this, ); } + + @override + void onClose() { + searchBarStream.close(); + super.onClose(); + } } diff --git a/lib/pages/rank/view.dart b/lib/pages/rank/view.dart index 7b5b4906..4efa2b4e 100644 --- a/lib/pages/rank/view.dart +++ b/lib/pages/rank/view.dart @@ -102,7 +102,7 @@ class _RankPageState extends State onTap: (value) { feedBack(); if (_rankController.initialIndex.value == value) { - _rankController.tabsCtrList[value]().animateToTop(); + _rankController.tabsCtrList[value].animateToTop(); } _rankController.initialIndex.value = value; }, diff --git a/lib/pages/rank/zone/controller.dart b/lib/pages/rank/zone/controller.dart index f9f4dc6e..71f27b93 100644 --- a/lib/pages/rank/zone/controller.dart +++ b/lib/pages/rank/zone/controller.dart @@ -42,12 +42,15 @@ class ZoneController extends GetxController { // 返回顶部并刷新 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); + if (scrollController.hasClients) { + 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/rcmd/view.dart b/lib/pages/rcmd/view.dart index acc1e654..67567870 100644 --- a/lib/pages/rcmd/view.dart +++ b/lib/pages/rcmd/view.dart @@ -5,9 +5,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/skeleton/video_card_v.dart'; -import 'package:pilipala/common/widgets/animated_dialog.dart'; import 'package:pilipala/common/widgets/http_error.dart'; -import 'package:pilipala/common/widgets/overlay_pop.dart'; import 'package:pilipala/common/widgets/video_card_v.dart'; import 'package:pilipala/utils/main_stream.dart'; @@ -118,16 +116,6 @@ class _RcmdPageState extends State ); } - OverlayEntry _createPopupDialog(videoItem) { - return OverlayEntry( - builder: (context) => AnimatedDialog( - closeFn: _rcmdController.popupDialog?.remove, - child: OverlayPop( - videoItem: videoItem, closeFn: _rcmdController.popupDialog?.remove), - ), - ); - } - Widget contentGrid(ctr, videoList) { // double maxWidth = Get.size.width; // int baseWidth = 500; @@ -158,14 +146,6 @@ class _RcmdPageState extends State ? VideoCardV( videoItem: videoList[index], crossAxisCount: crossAxisCount, - longPress: () { - _rcmdController.popupDialog = - _createPopupDialog(videoList[index]); - Overlay.of(context).insert(_rcmdController.popupDialog!); - }, - longPressEnd: () { - _rcmdController.popupDialog?.remove(); - }, ) : const VideoCardVSkeleton(); }, diff --git a/lib/pages/setting/widgets/select_dialog.dart b/lib/pages/setting/widgets/select_dialog.dart index 50229f9e..ab5f2ce4 100644 --- a/lib/pages/setting/widgets/select_dialog.dart +++ b/lib/pages/setting/widgets/select_dialog.dart @@ -29,7 +29,7 @@ class _SelectDialogState extends State> { return AlertDialog( title: Text(widget.title), - contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 12), + contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 24), content: StatefulBuilder(builder: (context, StateSetter setState) { return SingleChildScrollView( child: Column( diff --git a/lib/pages/subscription_detail/view.dart b/lib/pages/subscription_detail/view.dart index 2c219e58..63352429 100644 --- a/lib/pages/subscription_detail/view.dart +++ b/lib/pages/subscription_detail/view.dart @@ -53,6 +53,7 @@ class _SubDetailPageState extends State { @override void dispose() { _controller.dispose(); + titleStreamC.close(); super.dispose(); } diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart index 4d40e535..3cc43005 100644 --- a/lib/pages/video/detail/controller.dart +++ b/lib/pages/video/detail/controller.dart @@ -51,7 +51,7 @@ class VideoDetailController extends GetxController /// 播放器配置 画质 音质 解码格式 late VideoQuality currentVideoQa; AudioQuality? currentAudioQa; - late VideoDecodeFormats currentDecodeFormats; + VideoDecodeFormats? currentDecodeFormats; // 是否开始自动播放 存在多p的情况下,第二p需要为true RxBool autoPlay = true.obs; // 视频资源是否有效 @@ -107,6 +107,7 @@ class VideoDetailController extends GetxController BottomControlType.fullscreen, ].obs; RxDouble sheetHeight = 0.0.obs; + RxString archiveSourceType = 'dash'.obs; @override void onInit() { @@ -189,37 +190,43 @@ class VideoDetailController extends GetxController plPlayerController.isBuffering.value = false; plPlayerController.buffered.value = Duration.zero; - /// 根据currentVideoQa和currentDecodeFormats 重新设置videoUrl - List videoList = - data.dash!.video!.where((i) => i.id == currentVideoQa.code).toList(); - try { - firstVideo = videoList - .firstWhere((i) => i.codecs!.startsWith(currentDecodeFormats.code)); - } catch (_) { - if (currentVideoQa == VideoQuality.dolbyVision) { - firstVideo = videoList.first; - currentDecodeFormats = - VideoDecodeFormatsCode.fromString(videoList.first.codecs!)!; - } else { - // 当前格式不可用 - currentDecodeFormats = VideoDecodeFormatsCode.fromString(setting.get( - SettingBoxKey.defaultDecode, - defaultValue: VideoDecodeFormats.values.last.code))!; - firstVideo = videoList - .firstWhere((i) => i.codecs!.startsWith(currentDecodeFormats.code)); + if (archiveSourceType.value == 'dash') { + /// 根据currentVideoQa和currentDecodeFormats 重新设置videoUrl + List videoList = + data.dash!.video!.where((i) => i.id == currentVideoQa.code).toList(); + try { + firstVideo = videoList.firstWhere( + (i) => i.codecs!.startsWith(currentDecodeFormats?.code)); + } catch (_) { + if (currentVideoQa == VideoQuality.dolbyVision) { + firstVideo = videoList.first; + currentDecodeFormats = + VideoDecodeFormatsCode.fromString(videoList.first.codecs!)!; + } else { + // 当前格式不可用 + currentDecodeFormats = VideoDecodeFormatsCode.fromString(setting.get( + SettingBoxKey.defaultDecode, + defaultValue: VideoDecodeFormats.values.last.code))!; + firstVideo = videoList.firstWhere( + (i) => i.codecs!.startsWith(currentDecodeFormats?.code)); + } + } + videoUrl = firstVideo.baseUrl!; + + /// 根据currentAudioQa 重新设置audioUrl + if (currentAudioQa != null) { + final AudioItem firstAudio = data.dash!.audio!.firstWhere( + (AudioItem i) => i.id == currentAudioQa!.code, + orElse: () => data.dash!.audio!.first, + ); + audioUrl = firstAudio.baseUrl ?? ''; } } - videoUrl = firstVideo.baseUrl!; - /// 根据currentAudioQa 重新设置audioUrl - if (currentAudioQa != null) { - final AudioItem firstAudio = data.dash!.audio!.firstWhere( - (AudioItem i) => i.id == currentAudioQa!.code, - orElse: () => data.dash!.audio!.first, - ); - audioUrl = firstAudio.baseUrl ?? ''; + if (archiveSourceType.value == 'durl') { + cacheVideoQa = VideoQualityCode.toCode(currentVideoQa); + queryVideoUrl(); } - playerInit(); } @@ -272,7 +279,8 @@ class VideoDetailController extends GetxController // 视频链接 Future queryVideoUrl() async { - var result = await VideoHttp.videoUrl(cid: cid.value, bvid: bvid); + var result = + await VideoHttp.videoUrl(cid: cid.value, bvid: bvid, qn: cacheVideoQa); if (result['status']) { data = result['data']; if (data.acceptDesc!.isNotEmpty && data.acceptDesc!.contains('试看')) { @@ -290,8 +298,22 @@ class VideoDetailController extends GetxController } return result; } + if (data.durl != null) { + archiveSourceType.value = 'durl'; + videoUrl = data.durl!.first.url!; + audioUrl = ''; + defaultST = Duration.zero; + firstVideo = VideoItem(); + currentVideoQa = VideoQualityCode.fromCode(data.quality!)!; + if (autoPlay.value) { + await playerInit(); + isShowCover.value = false; + } + return result; + } final List allVideosList = data.dash!.video!; try { + archiveSourceType.value = 'dash'; // 当前可播放的最高质量视频 int currentHighVideoQa = allVideosList.first.quality!.code; // 预设的画质为null,则当前可用的最高质量 @@ -321,7 +343,7 @@ class VideoDetailController extends GetxController // 当前视频没有对应格式返回第一个 bool flag = false; for (var i in supportDecodeFormats) { - if (i.startsWith(currentDecodeFormats.code)) { + if (i.startsWith(currentDecodeFormats?.code)) { flag = true; } } @@ -335,7 +357,7 @@ class VideoDetailController extends GetxController /// 取出符合当前解码格式的videoItem try { firstVideo = videosList.firstWhere( - (e) => e.codecs!.startsWith(currentDecodeFormats.code)); + (e) => e.codecs!.startsWith(currentDecodeFormats?.code)); } catch (_) { firstVideo = videosList.first; } diff --git a/lib/pages/video/detail/introduction/controller.dart b/lib/pages/video/detail/introduction/controller.dart index d81bda00..8d602b83 100644 --- a/lib/pages/video/detail/introduction/controller.dart +++ b/lib/pages/video/detail/introduction/controller.dart @@ -58,6 +58,7 @@ class VideoIntroController extends GetxController { String heroTag = ''; late ModelResult modelResult; PersistentBottomSheetController? bottomSheetController; + late bool enableRelatedVideo; @override void onInit() { @@ -74,6 +75,8 @@ class VideoIntroController extends GetxController { queryOnlineTotal(); startTimer(); // 在页面加载时启动定时器 } + enableRelatedVideo = + setting.get(SettingBoxKey.enableRelatedVideo, defaultValue: true); } // 获取视频简介&分p @@ -216,50 +219,36 @@ class VideoIntroController extends GetxController { builder: (context) { return AlertDialog( title: const Text('选择投币个数'), - contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 12), + contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 24), content: StatefulBuilder(builder: (context, StateSetter setState) { return Column( mainAxisSize: MainAxisSize.min, - children: [ - RadioListTile( - value: 1, - title: const Text('1枚'), - groupValue: _tempThemeValue, - onChanged: (value) { - _tempThemeValue = value!; - Get.appUpdate(); - }, - ), - RadioListTile( - value: 2, - title: const Text('2枚'), - groupValue: _tempThemeValue, - onChanged: (value) { - _tempThemeValue = value!; - Get.appUpdate(); - }, - ), - ], + children: [1, 2] + .map( + (e) => RadioListTile( + value: e, + title: Text('$e枚'), + groupValue: _tempThemeValue, + onChanged: (value) async { + _tempThemeValue = value!; + setState(() {}); + var res = await VideoHttp.coinVideo( + bvid: bvid, multiply: _tempThemeValue); + if (res['status']) { + SmartDialog.showToast('投币成功 👏'); + hasCoin.value = true; + videoDetail.value.stat!.coin = + videoDetail.value.stat!.coin! + _tempThemeValue; + } else { + SmartDialog.showToast(res['msg']); + } + Get.back(); + }, + ), + ) + .toList(), ); }), - 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; - videoDetail.value.stat!.coin = - videoDetail.value.stat!.coin! + _tempThemeValue; - } else { - SmartDialog.showToast(res['msg']); - } - Get.back(); - }, - child: const Text('确定')) - ], ); }); } @@ -447,15 +436,18 @@ class VideoIntroController extends GetxController { // 重新获取视频资源 final VideoDetailController videoDetailCtr = Get.find(tag: heroTag); - final ReleatedController releatedCtr = - Get.find(tag: heroTag); + if (enableRelatedVideo) { + final ReleatedController releatedCtr = + Get.find(tag: heroTag); + releatedCtr.bvid = bvid; + releatedCtr.queryRelatedVideo(); + } + videoDetailCtr.bvid = bvid; videoDetailCtr.oid.value = aid ?? IdUtils.bv2av(bvid); videoDetailCtr.cid.value = cid; videoDetailCtr.danmakuCid.value = cid; videoDetailCtr.queryVideoUrl(); - releatedCtr.bvid = bvid; - releatedCtr.queryRelatedVideo(); // 重新请求评论 try { /// 未渲染回复组件时可能异常 diff --git a/lib/pages/video/detail/introduction/view.dart b/lib/pages/video/detail/introduction/view.dart index a7eae6d2..597b6def 100644 --- a/lib/pages/video/detail/introduction/view.dart +++ b/lib/pages/video/detail/introduction/view.dart @@ -22,6 +22,7 @@ import 'widgets/fav_panel.dart'; import 'widgets/intro_detail.dart'; import 'widgets/page_panel.dart'; import 'widgets/season_panel.dart'; +import 'widgets/staff_up_item.dart'; class VideoIntroPanel extends StatefulWidget { final String bvid; @@ -409,32 +410,35 @@ class _VideoInfoState extends State with TickerProviderStateMixin { ), ) ], - 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, + if (widget.videoDetail!.staff == 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 Spacer(), - Obx(() => AnimatedOpacity( + 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), @@ -474,11 +478,58 @@ class _VideoInfoState extends State with TickerProviderStateMixin { ), ), ), - )), - ], + ), + ), + ], + ), ), ), - ), + if (widget.videoDetail!.staff != null) ...[ + const SizedBox(height: 15), + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text.rich( + TextSpan( + style: TextStyle( + fontSize: + Theme.of(context).textTheme.labelMedium!.fontSize, + ), + children: [ + TextSpan( + text: '创作团队', + style: Theme.of(context) + .textTheme + .titleSmall! + .copyWith(fontWeight: FontWeight.bold), + ), + const WidgetSpan(child: SizedBox(width: 6)), + TextSpan( + text: '${widget.videoDetail!.staff!.length}人', + style: TextStyle( + color: Theme.of(context).colorScheme.outline, + ), + ) + ], + ), + ), + SizedBox( + height: 120, + child: ListView( + scrollDirection: Axis.horizontal, + children: [ + for (int i = 0; + i < widget.videoDetail!.staff!.length; + i++) ...[ + StaffUpItem(item: widget.videoDetail!.staff![i]) + ], + ], + ), + ), + ], + ), + ] ], )), ); @@ -545,4 +596,8 @@ class _VideoInfoState extends State with TickerProviderStateMixin { ); }); } + + // Widget StaffPanel(BuildContext context, videoIntroController) { + // return + // } } diff --git a/lib/pages/video/detail/introduction/widgets/intro_detail.dart b/lib/pages/video/detail/introduction/widgets/intro_detail.dart index 1e9bb842..3ec92023 100644 --- a/lib/pages/video/detail/introduction/widgets/intro_detail.dart +++ b/lib/pages/video/detail/introduction/widgets/intro_detail.dart @@ -23,16 +23,34 @@ class IntroDetail extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 4), - GestureDetector( - onTap: () { - Clipboard.setData(ClipboardData(text: videoDetail!.bvid!)); - SmartDialog.showToast('已复制'); - }, - child: Text( - videoDetail!.bvid!, - style: TextStyle( - fontSize: 13, color: Theme.of(context).colorScheme.primary), - ), + Row( + children: [ + GestureDetector( + onTap: () { + Clipboard.setData(ClipboardData(text: videoDetail!.bvid!)); + SmartDialog.showToast('已复制'); + }, + child: Text( + videoDetail!.bvid!, + style: TextStyle( + fontSize: 13, + color: Theme.of(context).colorScheme.primary), + ), + ), + const SizedBox(width: 10), + GestureDetector( + onTap: () { + Clipboard.setData(ClipboardData(text: videoDetail!.bvid!)); + SmartDialog.showToast('已复制'); + }, + child: Text( + videoDetail!.aid!.toString(), + style: TextStyle( + fontSize: 13, + color: Theme.of(context).colorScheme.primary), + ), + ) + ], ), const SizedBox(height: 4), Text.rich( diff --git a/lib/pages/video/detail/introduction/widgets/staff_up_item.dart b/lib/pages/video/detail/introduction/widgets/staff_up_item.dart new file mode 100644 index 00000000..7b18d95d --- /dev/null +++ b/lib/pages/video/detail/introduction/widgets/staff_up_item.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:pilipala/models/video_detail_res.dart'; +import 'package:pilipala/utils/utils.dart'; + +class StaffUpItem extends StatelessWidget { + final Staff item; + + const StaffUpItem({ + super.key, + required this.item, + }); + + @override + Widget build(BuildContext context) { + final String heroTag = Utils.makeHeroTag(item.mid); + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 15), + GestureDetector( + onTap: () => Get.toNamed( + '/member?mid=${item.mid}', + arguments: {'face': item.face, 'heroTag': heroTag}, + ), + child: Hero( + tag: heroTag, + child: NetworkImgLayer( + width: 45, + height: 45, + src: item.face, + type: 'avatar', + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 4), + child: SizedBox( + width: 85, + child: Text( + item.name!, + overflow: TextOverflow.ellipsis, + softWrap: false, + textAlign: TextAlign.center, + style: TextStyle( + color: item.vip!.status == 1 + ? const Color.fromARGB(255, 251, 100, 163) + : null, + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 4), + child: SizedBox( + width: 85, + child: Text( + item.title!, + overflow: TextOverflow.ellipsis, + softWrap: false, + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context).colorScheme.outline, + fontSize: 12, + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/pages/video/detail/reply_new/view.dart b/lib/pages/video/detail/reply_new/view.dart index a94b6071..029e015a 100644 --- a/lib/pages/video/detail/reply_new/view.dart +++ b/lib/pages/video/detail/reply_new/view.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; +import 'package:pilipala/http/dynamics.dart'; import 'package:pilipala/http/video.dart'; import 'package:pilipala/models/common/reply_type.dart'; import 'package:pilipala/models/video/reply/emote.dart'; @@ -40,6 +41,8 @@ class _VideoReplyNewDialogState extends State double keyboardHeight = 0.0; // 键盘高度 final _debouncer = Debouncer(milliseconds: 200); // 设置延迟时间 String toolbarType = 'input'; + RxBool isForward = false.obs; + RxBool showForward = false.obs; @override void initState() { @@ -52,6 +55,10 @@ class _VideoReplyNewDialogState extends State _autoFocus(); // 监听聚焦状态 _focuslistener(); + final String routePath = Get.currentRoute; + if (routePath.startsWith('/video')) { + showForward.value = true; + } } _autoFocus() async { @@ -88,6 +95,16 @@ class _VideoReplyNewDialogState extends State Get.back(result: { 'data': ReplyItemModel.fromJson(result['data']['reply'], ''), }); + + /// 投稿、番剧页面 + if (isForward.value) { + await DynamicsHttp.dynamicCreate( + mid: 0, + rawText: message, + oid: widget.oid!, + scene: 5, + ); + } } else { SmartDialog.showToast(result['msg']); } @@ -145,7 +162,6 @@ class _VideoReplyNewDialogState extends State double _keyboardHeight = EdgeInsets.fromViewPadding( View.of(context).viewInsets, View.of(context).devicePixelRatio) .bottom; - print('_keyboardHeight: $_keyboardHeight'); return Container( clipBehavior: Clip.hardEdge, decoration: BoxDecoration( @@ -225,9 +241,37 @@ class _VideoReplyNewDialogState extends State toolbarType: toolbarType, selected: toolbarType == 'emote', ), + const SizedBox(width: 6), + Obx( + () => showForward.value + ? TextButton.icon( + onPressed: () { + isForward.value = !isForward.value; + }, + icon: Icon( + isForward.value + ? Icons.check_box + : Icons.check_box_outline_blank, + size: 22), + label: const Text('转发到动态'), + style: ButtonStyle( + foregroundColor: MaterialStateProperty.all( + isForward.value + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.outline, + ), + ), + ) + : const SizedBox(), + ), const Spacer(), - TextButton( - onPressed: () => submitReplyAdd(), child: const Text('发送')) + SizedBox( + height: 36, + child: FilledButton( + onPressed: () => submitReplyAdd(), + child: const Text('发送'), + ), + ), ], ), ), diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index 687baaf0..0152a2cb 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -404,27 +404,41 @@ class _VideoDetailPageState extends State width: 38, height: 38, child: Obx( - () => IconButton( - onPressed: () { - plPlayerController?.isOpenDanmu.value = - !(plPlayerController?.isOpenDanmu.value ?? - false); - }, - icon: !(plPlayerController?.isOpenDanmu.value ?? - false) - ? SvgPicture.asset( + () => !vdCtr.isShowCover.value + ? IconButton( + onPressed: () { + plPlayerController?.isOpenDanmu.value = + !(plPlayerController + ?.isOpenDanmu.value ?? + false); + }, + icon: + !(plPlayerController?.isOpenDanmu.value ?? + false) + ? SvgPicture.asset( + 'assets/images/video/danmu_close.svg', + // ignore: deprecated_member_use + color: Theme.of(context) + .colorScheme + .outline, + ) + : SvgPicture.asset( + 'assets/images/video/danmu_open.svg', + // ignore: deprecated_member_use + color: Theme.of(context) + .colorScheme + .primary, + ), + ) + : IconButton( + icon: SvgPicture.asset( 'assets/images/video/danmu_close.svg', // ignore: deprecated_member_use color: Theme.of(context).colorScheme.outline, - ) - : SvgPicture.asset( - 'assets/images/video/danmu_open.svg', - // ignore: deprecated_member_use - color: - Theme.of(context).colorScheme.primary, ), - ), + onPressed: () {}, + ), ), ), const SizedBox(width: 18), diff --git a/lib/pages/video/detail/widgets/ai_detail.dart b/lib/pages/video/detail/widgets/ai_detail.dart index fb280d91..882a9a8b 100644 --- a/lib/pages/video/detail/widgets/ai_detail.dart +++ b/lib/pages/video/detail/widgets/ai_detail.dart @@ -49,7 +49,7 @@ class AiDetail extends StatelessWidget { child: SingleChildScrollView( child: Column( children: [ - Text( + SelectableText( modelResult!.summary!, style: const TextStyle( fontSize: 15, @@ -60,13 +60,15 @@ class AiDetail extends StatelessWidget { const SizedBox(height: 20), ListView.builder( shrinkWrap: true, - itemCount: modelResult!.outline!.length, physics: const NeverScrollableScrollPhysics(), + itemCount: modelResult!.outline!.length, itemBuilder: (context, index) { + final outline = modelResult!.outline![index]; return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - modelResult!.outline![index].title!, + SelectableText( + outline.title!, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, @@ -77,76 +79,59 @@ class AiDetail extends StatelessWidget { ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), - itemCount: modelResult! - .outline![index].partOutline!.length, + itemCount: outline.partOutline!.length, itemBuilder: (context, i) { + final part = outline.partOutline![i]; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Wrap( - children: [ - RichText( - text: TextSpan( - style: TextStyle( - fontSize: 13, - color: Theme.of(context) - .colorScheme - .onBackground, - height: 1.5, + GestureDetector( + onTap: () { + try { + final controller = + Get.find( + tag: Get.arguments['heroTag'], + ); + controller.plPlayerController.seekTo( + Duration( + seconds: Utils.duration( + Utils.tampToSeektime( + part.timestamp!), + ).toInt(), ), - children: [ - TextSpan( - text: Utils.tampToSeektime( - modelResult! - .outline![index] - .partOutline![i] - .timestamp!), - style: TextStyle( - color: Theme.of(context) - .colorScheme - .primary, - ), - recognizer: TapGestureRecognizer() - ..onTap = () { - // 跳转到指定位置 - try { - Get.find( - tag: Get.arguments[ - 'heroTag']) - .plPlayerController - .seekTo( - Duration( - seconds: - Utils.duration( - Utils.tampToSeektime(modelResult! - .outline![ - index] - .partOutline![ - i] - .timestamp!) - .toString(), - ), - ), - ); - } catch (_) {} - }, - ), - const TextSpan(text: ' '), - TextSpan( - text: modelResult! - .outline![index] - .partOutline![i] - .content!), - ], + ); + } catch (_) {} + }, + child: SelectableText.rich( + TextSpan( + style: TextStyle( + fontSize: 13, + color: Theme.of(context) + .colorScheme + .onBackground, + height: 1.5, ), + children: [ + TextSpan( + text: Utils.tampToSeektime( + part.timestamp!), + style: TextStyle( + color: Theme.of(context) + .colorScheme + .primary, + ), + ), + const TextSpan(text: ' '), + TextSpan(text: part.content!), + ], ), - ], + ), ), + const SizedBox(height: 20), ], ); }, ), - const SizedBox(height: 20), ], ); }, diff --git a/lib/pages/video/detail/widgets/header_control.dart b/lib/pages/video/detail/widgets/header_control.dart index e6a324cb..6acd20d7 100644 --- a/lib/pages/video/detail/widgets/header_control.dart +++ b/lib/pages/video/detail/widgets/header_control.dart @@ -180,15 +180,16 @@ class _HeaderControlState extends State { '当前音质 ${widget.videoDetailCtr!.currentAudioQa!.description}', style: subTitleStyle), ), - ListTile( - onTap: () => {Get.back(), showSetDecodeFormats()}, - dense: true, - leading: const Icon(Icons.av_timer_outlined, size: 20), - title: const Text('解码格式', style: titleStyle), - subtitle: Text( - '当前解码格式 ${widget.videoDetailCtr!.currentDecodeFormats.description}', - style: subTitleStyle), - ), + if (widget.videoDetailCtr!.currentDecodeFormats != null) + ListTile( + onTap: () => {Get.back(), showSetDecodeFormats()}, + dense: true, + leading: const Icon(Icons.av_timer_outlined, size: 20), + title: const Text('解码格式', style: titleStyle), + subtitle: Text( + '当前解码格式 ${widget.videoDetailCtr!.currentDecodeFormats!.description}', + style: subTitleStyle), + ), ListTile( onTap: () => {Get.back(), showSetRepeat()}, dense: true, @@ -541,16 +542,24 @@ class _HeaderControlState extends State { /// 可用的质量分类 int userfulQaSam = 0; - final List video = videoInfo.dash!.video!; - final Set idSet = {}; - for (final VideoItem item in video) { - final int id = item.id!; - if (!idSet.contains(id)) { - idSet.add(id); - userfulQaSam++; + if (videoInfo.dash != null) { + // dash格式视频一次请求会返回所有可播放的清晰度video + final List video = videoInfo.dash!.video!; + final Set idSet = {}; + for (final VideoItem item in video) { + final int id = item.id!; + if (!idSet.contains(id)) { + idSet.add(id); + userfulQaSam++; + } } } + if (videoInfo.durl != null) { + // durl格式视频一次请求返回对应清晰度video + userfulQaSam = videoFormat.length - 1; + } + showModalBottomSheet( context: context, elevation: 0, @@ -707,7 +716,7 @@ class _HeaderControlState extends State { void showSetDecodeFormats() { // 当前选中的解码格式 final VideoDecodeFormats currentDecodeFormats = - widget.videoDetailCtr!.currentDecodeFormats; + widget.videoDetailCtr!.currentDecodeFormats!; final VideoItem firstVideo = widget.videoDetailCtr!.firstVideo; // 当前视频可用的解码格式 final List videoFormat = videoInfo.supportFormats!; @@ -1306,20 +1315,6 @@ class _HeaderControlState extends State { ], /// 字幕 - // SizedBox( - // width: 34, - // height: 34, - // child: IconButton( - // style: ButtonStyle( - // padding: MaterialStateProperty.all(EdgeInsets.zero), - // ), - // onPressed: () => showSubtitleDialog(), - // icon: const Icon( - // Icons.closed_caption_off, - // size: 22, - // ), - // ), - // ), ComBtn( icon: const Icon( Icons.closed_caption_off, diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index f936526b..ca147fd1 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -101,7 +101,7 @@ class PlPlayerController { bool _isFirstTime = true; Timer? _timer; - late Timer? _timerForSeek; + Timer? _timerForSeek; Timer? _timerForVolume; Timer? _timerForShowingVolume; Timer? _timerForGettingVolume; @@ -335,8 +335,10 @@ class PlPlayerController { }) { // 如果实例尚未创建,则创建一个新实例 _instance ??= PlPlayerController._(); - _instance!._playerCount.value += 1; - _videoType.value = videoType; + if (videoType != 'none') { + _instance!._playerCount.value += 1; + _videoType.value = videoType; + } return _instance!; } @@ -473,17 +475,17 @@ class PlPlayerController { } // 字幕 - if (dataSource.subFiles != '' && dataSource.subFiles != null) { - await pp.setProperty( - 'sub-files', - UniversalPlatform.isWindows - ? dataSource.subFiles!.replaceAll(';', '\\;') - : dataSource.subFiles!.replaceAll(':', '\\:'), - ); - await pp.setProperty("subs-with-matching-audio", "no"); - await pp.setProperty("sub-forced-only", "yes"); - await pp.setProperty("blend-subtitles", "video"); - } + // if (dataSource.subFiles != '' && dataSource.subFiles != null) { + // await pp.setProperty( + // 'sub-files', + // UniversalPlatform.isWindows + // ? dataSource.subFiles!.replaceAll(';', '\\;') + // : dataSource.subFiles!.replaceAll(':', '\\:'), + // ); + // await pp.setProperty("subs-with-matching-audio", "no"); + // await pp.setProperty("sub-forced-only", "yes"); + // await pp.setProperty("blend-subtitles", "video"); + // } _videoController = _videoController ?? VideoController( @@ -522,7 +524,22 @@ class PlPlayerController { Duration seekTo = Duration.zero, Duration? duration, }) async { - // 设置倍速 + getVideoFit(); + // if (_looping) { + // await setLooping(_looping); + // } + + /// 跳转播放 + if (seekTo != Duration.zero) { + await this.seekTo(seekTo); + } + + /// 自动播放 + if (_autoPlay) { + await play(duration: duration); + } + + /// 设置倍速 if (videoType.value == 'live') { await setPlaybackSpeed(1.0); } else { @@ -532,20 +549,6 @@ class PlPlayerController { await setPlaybackSpeed(1.0); } } - getVideoFit(); - // if (_looping) { - // await setLooping(_looping); - // } - - // 跳转播放 - if (seekTo != Duration.zero) { - await this.seekTo(seekTo); - } - - // 自动播放 - if (_autoPlay) { - await play(duration: duration); - } } List subscriptions = []; @@ -603,7 +606,9 @@ class PlPlayerController { makeHeartBeat(event.inSeconds); }), videoPlayerController!.stream.duration.listen((event) { - duration.value = event; + if (event > Duration.zero) { + duration.value = event; + } }), videoPlayerController!.stream.buffer.listen((event) { _buffered.value = event; @@ -646,32 +651,38 @@ class PlPlayerController { /// 跳转至指定位置 Future seekTo(Duration position, {type = 'seek'}) async { - if (position < Duration.zero) { - position = Duration.zero; - } - _position.value = position; - updatePositionSecond(); - _heartDuration = position.inSeconds; - if (duration.value.inSeconds != 0) { - if (type != 'slider') { - /// 拖动进度条调节时,不等待第一帧,防止抖动 - await _videoPlayerController?.stream.buffer.first; + try { + if (position < Duration.zero) { + position = Duration.zero; } - await _videoPlayerController?.seek(position); - } else { - _timerForSeek?.cancel(); - _timerForSeek ??= - Timer.periodic(const Duration(milliseconds: 200), (Timer t) async { - if (duration.value.inSeconds != 0) { - await _videoPlayerController!.stream.buffer.first; - await _videoPlayerController?.seek(position); - t.cancel(); - _timerForSeek = null; + _position.value = position; + updatePositionSecond(); + _heartDuration = position.inSeconds; + if (duration.value.inSeconds != 0) { + if (type != 'slider') { + await _videoPlayerController?.stream.buffer.first; } - }); + await _videoPlayerController?.seek(position); + } else { + _timerForSeek?.cancel(); + _timerForSeek ??= _startSeekTimer(position); + } + } catch (err) { + print('Error while seeking: $err'); } } + Timer? _startSeekTimer(Duration position) { + return Timer.periodic(const Duration(milliseconds: 200), (Timer t) async { + if (duration.value.inSeconds != 0) { + await _videoPlayerController!.stream.buffer.first; + await _videoPlayerController?.seek(position); + t.cancel(); + _timerForSeek = null; + } + }); + } + /// 设置倍速 Future setPlaybackSpeed(double speed) async { /// TODO _duration.value丢失 @@ -708,11 +719,10 @@ class PlPlayerController { await seekTo(Duration.zero); } await _videoPlayerController?.play(); - + playerStatus.status.value = PlayerStatus.playing; await getCurrentVolume(); await getCurrentBrightness(); - playerStatus.status.value = PlayerStatus.playing; // screenManager.setOverlays(false); /// 临时fix _duration.value丢失 @@ -1112,9 +1122,6 @@ class PlPlayerController { } Future dispose({String type = 'single'}) async { - print('dispose'); - print('dispose: ${playerCount.value}'); - // 每次减1,最后销毁 if (type == 'single' && playerCount.value > 1) { _playerCount.value -= 1; @@ -1124,7 +1131,6 @@ class PlPlayerController { } _playerCount.value = 0; try { - print('dispose dispose ---------'); _timer?.cancel(); _timerForVolume?.cancel(); _timerForGettingVolume?.cancel(); diff --git a/lib/services/audio_handler.dart b/lib/services/audio_handler.dart index bf98298b..b0ca8cd7 100644 --- a/lib/services/audio_handler.dart +++ b/lib/services/audio_handler.dart @@ -26,7 +26,7 @@ class VideoPlayerServiceHandler extends BaseAudioHandler with SeekHandler { static final List _item = []; Box setting = GStrorage.setting; bool enableBackgroundPlay = false; - PlPlayerController player = PlPlayerController.getInstance(); + PlPlayerController player = PlPlayerController.getInstance(videoType: 'none'); VideoPlayerServiceHandler() { revalidateSetting(); diff --git a/lib/services/audio_session.dart b/lib/services/audio_session.dart index ea83a30a..53b497ae 100644 --- a/lib/services/audio_session.dart +++ b/lib/services/audio_session.dart @@ -18,7 +18,7 @@ class AudioSessionHandler { session.configure(const AudioSessionConfiguration.music()); session.interruptionEventStream.listen((event) { - final player = PlPlayerController.getInstance(); + final player = PlPlayerController.getInstance(videoType: 'none'); if (event.begin) { if (!player.playerStatus.playing) return; switch (event.type) { @@ -51,7 +51,7 @@ class AudioSessionHandler { // 耳机拔出暂停 session.becomingNoisyEventStream.listen((_) { - final player = PlPlayerController.getInstance(); + final player = PlPlayerController.getInstance(videoType: 'none'); if (player.playerStatus.playing) { player.pause(); } diff --git a/lib/services/shutdown_timer_service.dart b/lib/services/shutdown_timer_service.dart index aa9c5ceb..156b63c8 100644 --- a/lib/services/shutdown_timer_service.dart +++ b/lib/services/shutdown_timer_service.dart @@ -29,8 +29,8 @@ class ShutdownTimerService { return; } SmartDialog.showToast("设置 $scheduledExitInMinutes 分钟后定时关闭"); - _shutdownTimer = Timer(Duration(minutes: scheduledExitInMinutes), - () => _shutdownDecider()); + _shutdownTimer = Timer( + Duration(minutes: scheduledExitInMinutes), () => _shutdownDecider()); } void _showTimeUpButPauseDialog() { @@ -59,7 +59,7 @@ class ShutdownTimerService { // Start the 10-second timer to auto close the dialog _autoCloseDialogTimer?.cancel(); _autoCloseDialogTimer = Timer(const Duration(seconds: 10), () { - SmartDialog.dismiss();// Close the dialog + SmartDialog.dismiss(); // Close the dialog _executeShutdown(); }); return AlertDialog( @@ -88,7 +88,8 @@ class ShutdownTimerService { _showShutdownDialog(); return; } - PlPlayerController plPlayerController = PlPlayerController.getInstance(); + PlPlayerController plPlayerController = + PlPlayerController.getInstance(videoType: 'none'); if (!exitApp && !waitForPlayingCompleted) { if (!plPlayerController.playerStatus.playing) { //仅提示用户 @@ -108,19 +109,22 @@ class ShutdownTimerService { //该方法依赖耦合实现,不够优雅 isWaiting = true; } - void handleWaitingFinished(){ - if(isWaiting){ + + void handleWaitingFinished() { + if (isWaiting) { _showShutdownDialog(); isWaiting = false; } } + void _executeShutdown() { if (exitApp) { //退出app exit(0); } else { //暂停播放 - PlPlayerController plPlayerController = PlPlayerController.getInstance(); + PlPlayerController plPlayerController = + PlPlayerController.getInstance(videoType: 'none'); if (plPlayerController.playerStatus.playing) { plPlayerController.pause(); waitForPlayingCompleted = true; diff --git a/lib/utils/app_scheme.dart b/lib/utils/app_scheme.dart index bb9d556f..9009df6f 100644 --- a/lib/utils/app_scheme.dart +++ b/lib/utils/app_scheme.dart @@ -20,7 +20,7 @@ class PiliSchame { /// 完整链接进入 b23.无效 appScheme.getLatestScheme().then((SchemeEntity? value) { if (value != null) { - _fullPathPush(value); + _routePush(value); } }); @@ -37,7 +37,6 @@ class PiliSchame { final String scheme = value.scheme; final String host = value.host; final String path = value.path; - if (scheme == 'bilibili') { if (host == 'root') { Navigator.popUntil( @@ -85,6 +84,14 @@ class PiliSchame { } } else if (host == 'search') { Get.toNamed('/searchResult', parameters: {'keyword': ''}); + } else if (host == 'article') { + final String id = path.split('/').last.split('?').first; + Get.toNamed('/htmlRender', parameters: { + 'url': 'https://www.bilibili.com/read/cv$id', + 'title': 'cv$id', + 'id': 'cv$id', + 'dynamicType': 'read' + }); } } if (scheme == 'https') { @@ -155,9 +162,14 @@ class PiliSchame { final String host = value.host!; final String? path = value.path; Map? query = value.query; - RegExp regExp = RegExp(r'^(www\.)?m?\.(bilibili\.com)$'); + RegExp regExp = RegExp(r'^((www\.)|(m\.))?bilibili\.com$'); if (regExp.hasMatch(host)) { - print('bilibili.com'); + print('bilibili.com host: $host'); + print('bilibili.com path: $path'); + final String lastPathSegment = path!.split('/').last; + if (lastPathSegment.contains('BV')) { + _videoPush(null, lastPathSegment); + } } else if (host.contains('live')) { int roomId = int.parse(path!.split('/').last); Get.toNamed( @@ -226,6 +238,13 @@ class PiliSchame { break; case 'read': print('专栏'); + String id = 'cv${matchNum(query!['id']!).first}'; + Get.toNamed('/htmlRender', parameters: { + 'url': value.dataString!, + 'title': '', + 'id': id, + 'dynamicType': 'read' + }); break; case 'space': print('个人空间'); diff --git a/lib/utils/subtitle.dart b/lib/utils/subtitle.dart index 452be542..1b4088f3 100644 --- a/lib/utils/subtitle.dart +++ b/lib/utils/subtitle.dart @@ -5,8 +5,8 @@ class SubTitleUtils { for (int i = 0; i < jsonData.length; i++) { final item = jsonData[i]; - double from = item['from'] as double; - double to = item['to'] as double; + double from = double.parse(item['from'].toString()); + double to = double.parse(item['to'].toString()); int sid = (item['sid'] ?? 0) as int; String content = item['content'] as String; diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index a7273f05..987f57c1 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -210,15 +210,18 @@ class Utils { int minDiff = 127; int closestNumber = 0; // 初始化为0,表示没有找到比目标值小的整数 + if (numbers.contains(target)) { + return target; + } // 向下查找 try { for (int number in numbers) { if (number < target) { int diff = target - number; // 计算目标值与当前整数的差值 - if (diff < minDiff) { minDiff = diff; closestNumber = number; + return closestNumber; } } }