diff --git a/lib/grpc/audio.dart b/lib/grpc/audio.dart index 4954984f8..a21f63d39 100644 --- a/lib/grpc/audio.dart +++ b/lib/grpc/audio.dart @@ -41,7 +41,7 @@ class AudioGrpc { int? itemType, PageOption? pageOpt, Int64? extraId, - Pagination? pagination, + String? next, int qn = 80, int fnval = 4048, }) { @@ -64,7 +64,7 @@ class AudioGrpc { ), extraId: extraId, sortOpt: SortOption(order: ListOrder.ORDER_NORMAL), - pagination: pagination, + pagination: Pagination(pageSize: 20, next: next), ), PlaylistResp.fromBuffer, ); diff --git a/lib/pages/audio/controller.dart b/lib/pages/audio/controller.dart index 13e524f01..0286c3cd6 100644 --- a/lib/pages/audio/controller.dart +++ b/lib/pages/audio/controller.dart @@ -10,7 +10,6 @@ import 'package:PiliPlus/grpc/bilibili/app/listener/v1.pb.dart' PlaylistSource, PlayInfo, ThumbUpReq_ThumbType; -import 'package:PiliPlus/grpc/bilibili/pagination.pb.dart'; import 'package:PiliPlus/http/constants.dart'; import 'package:PiliPlus/http/ua_type.dart'; import 'package:PiliPlus/pages/common/common_intro_controller.dart' @@ -28,6 +27,7 @@ import 'package:PiliPlus/utils/id_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:PiliPlus/utils/utils.dart'; +import 'package:PiliPlus/utils/video_utils.dart'; import 'package:fixnum/fixnum.dart' show Int64; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -41,6 +41,7 @@ class AudioController extends GetxController late List subId; late int itemType; late final PlaylistSource from; + late final isVideo = itemType == 1; final Rx audioItem = Rx(null); @@ -120,10 +121,10 @@ class AudioController extends GetxController subId: isInit ? subId : null, itemType: isInit ? itemType : null, from: isInit ? from : null, - pagination: isLoadPrev - ? Pagination(next: _prev) + next: isLoadPrev + ? _prev : isLoadNext - ? Pagination(next: _next) + ? _next : null, ); if (res.isSuccess) { @@ -174,7 +175,7 @@ class AudioController extends GetxController } } - Future _onPlay(PlayURLResp data) async { + void _onPlay(PlayURLResp data) { final PlayInfo? playInfo = data.playerInfo.values.firstOrNull; if (playInfo != null) { if (playInfo.hasPlayDash()) { @@ -188,7 +189,7 @@ class AudioController extends GetxController (e) => e.id <= cacheAudioQa, (a, b) => a.id > b.id ? a : b, ); - _onOpenMedia(audio.baseUrl); + _onOpenMedia(VideoUtils.getCdnUrl(audio.baseUrl)); } else if (playInfo.hasPlayUrl()) { final playUrl = playInfo.playUrl; final durls = playUrl.durl; @@ -197,7 +198,7 @@ class AudioController extends GetxController } final durl = durls.first; position.value = Duration.zero; - _onOpenMedia(durl.url); + _onOpenMedia(VideoUtils.getDurlCdnUrl(durl)); } } } @@ -380,7 +381,7 @@ class AudioController extends GetxController void showReply() { MainReplyPage.toMainReplyPage( oid: oid.toInt(), - replyType: itemType == 1 ? 1 : 14, + replyType: isVideo ? 1 : 14, ); } @@ -388,7 +389,7 @@ class AudioController extends GetxController showDialog( context: context, builder: (_) { - final audioUrl = itemType == 1 + final audioUrl = isVideo ? '${HttpString.baseUrl}/video/${IdUtils.av2bv(oid.toInt())}' : '${HttpString.baseUrl}/audio/au$oid'; return AlertDialog( @@ -452,7 +453,7 @@ class AudioController extends GetxController useSafeArea: true, builder: (context) => RepostPanel( rid: oid.toInt(), - dynType: itemType == 1 ? 8 : 256, + dynType: isVideo ? 8 : 256, pic: audioItem.arc.cover, title: audioItem.arc.title, uname: audioItem.owner.name, @@ -461,7 +462,7 @@ class AudioController extends GetxController } }, ), - if (itemType == 1) + if (isVideo) ListTile( dense: true, title: const Text( @@ -563,7 +564,7 @@ class AudioController extends GetxController // } @override - (Object, int) get getFavRidType => (oid, itemType == 1 ? 2 : 12); + (Object, int) get getFavRidType => (oid, isVideo ? 2 : 12); @override void updateFavCount(int count) { diff --git a/lib/pages/audio/view.dart b/lib/pages/audio/view.dart index 0550e9f1b..78cb8ceb9 100644 --- a/lib/pages/audio/view.dart +++ b/lib/pages/audio/view.dart @@ -19,6 +19,7 @@ import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_key.dart'; import 'package:PiliPlus/utils/utils.dart'; +import 'package:flutter/gestures.dart' show TapGestureRecognizer; import 'package:flutter/material.dart' hide DraggableScrollableSheet; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; @@ -59,6 +60,12 @@ class _AudioPageState extends State { tag: Utils.generateRandomString(8), ); + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _controller.didChangeDependencies(context); + } + @override Widget build(BuildContext context) { final colorScheme = ColorScheme.of(context); @@ -66,17 +73,18 @@ class _AudioPageState extends State { final padding = MediaQuery.viewPaddingOf(context); return Scaffold( appBar: AppBar( - actions: [ - IconButton( - onPressed: _showMore, - icon: const Icon(Icons.more_vert), - ), - const SizedBox(width: 5), - ], + actions: _controller.isVideo + ? [ + IconButton( + onPressed: _showMore, + icon: const Icon(Icons.more_vert), + ), + const SizedBox(width: 5), + ] + : null, ), body: Padding( padding: EdgeInsets.only( - top: 20, left: 20 + padding.left, right: 20 + padding.right, bottom: 30 + padding.bottom, @@ -96,11 +104,7 @@ class _AudioPageState extends State { spacing: 12, children: [ Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [_buildInfo(colorScheme, isPortrait)], - ), + child: _buildInfo(colorScheme, isPortrait), ), Expanded( child: Column( @@ -175,6 +179,9 @@ class _AudioPageState extends State { onRefresh: () => _controller.loadPrev(context), child: CustomScrollView( controller: scrollController, + physics: const AlwaysScrollableScrollPhysics( + parent: ClampingScrollPhysics(), + ), slivers: [ SliverPadding( padding: EdgeInsets.only( @@ -475,18 +482,17 @@ class _AudioPageState extends State { // _controller.showTimerDialog(); // }, // ), - if (_controller.itemType == 1) - ListTile( - dense: true, - title: const Text( - '举报', - style: TextStyle(fontSize: 14), - ), - onTap: () { - Get.back(); - PageUtils.reportVideo(_controller.oid.toInt()); - }, + ListTile( + dense: true, + title: const Text( + '举报', + style: TextStyle(fontSize: 14), ), + onTap: () { + Get.back(); + PageUtils.reportVideo(_controller.oid.toInt()); + }, + ), ], ), ); @@ -565,6 +571,21 @@ class _AudioPageState extends State { audioItem.stat.share, ), ), + if (audioItem.associatedItem.hasOid() && + audioItem.associatedItem.subId.isNotEmpty) + ActionItem( + icon: const Icon(FontAwesomeIcons.circlePlay), + onTap: () { + _controller.player?.pause(); + PageUtils.toVideoPage( + cid: audioItem.associatedItem.subId.first.toInt(), + aid: audioItem.associatedItem.oid.toInt(), + ); + }, + selectStatus: false, + semanticsLabel: '看MV', + text: '看MV', + ), ], ), ); @@ -694,159 +715,114 @@ class _AudioPageState extends State { if (audioItem != null) { final cover = audioItem.arc.cover.http2https; return Column( - crossAxisAlignment: CrossAxisAlignment.start, children: [ - Center( - child: GestureDetector( - onTap: () => PageUtils.imageView( - imgList: [SourceModel(url: cover)], - ), - child: Hero( - tag: cover, - child: NetworkImgLayer( - src: cover, - width: 150, - height: 150, - ), + Expanded( + child: Center( + child: ListView( + key: const PageStorageKey(_AudioPageState), + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + children: [ + Center( + child: GestureDetector( + onTap: () => PageUtils.imageView( + imgList: [SourceModel(url: cover)], + ), + child: Hero( + tag: cover, + child: NetworkImgLayer( + src: cover, + width: 170, + height: 170, + ), + ), + ), + ), + const SizedBox(height: 12), + SelectableText( + audioItem.arc.title, + style: const TextStyle( + height: 1.7, + fontSize: 16, + ), + ), + const SizedBox(height: 12), + if (audioItem.owner.hasName()) ...[ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + _controller.player?.pause(); + Get.toNamed('/member?mid=${audioItem.owner.mid}'); + }, + child: Row( + spacing: 6, + mainAxisSize: MainAxisSize.min, + children: [ + if (audioItem.owner.hasAvatar()) + NetworkImgLayer( + src: audioItem.owner.avatar, + width: 22, + height: 22, + type: ImageType.avatar, + ), + Text( + audioItem.owner.name, + ), + ], + ), + ), + const SizedBox(height: 10), + ], + Row( + children: [ + Icon( + size: 14, + Icons.headphones_outlined, + color: colorScheme.outline, + ), + Text.rich( + TextSpan( + children: [ + TextSpan( + text: + ' ${NumUtils.numFormat(audioItem.stat.view)} ' + '${DateFormatUtils.dateFormat(audioItem.arc.publish.toInt(), long: DateFormatUtils.longFormatD)} ', + ), + TextSpan( + text: audioItem.arc.displayedOid, + style: TextStyle(color: colorScheme.secondary), + recognizer: TapGestureRecognizer() + ..onTap = () => Utils.copyText( + audioItem.arc.displayedOid, + ), + ), + ], + ), + style: TextStyle( + fontSize: 13, + color: colorScheme.outline, + ), + ), + ], + ), + if (audioItem.arc.hasDesc()) ...[ + const SizedBox(height: 10), + SelectableText(audioItem.arc.desc), + ], + ], ), ), ), - const SizedBox(height: 12), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: SelectableText( - audioItem.arc.title, - style: const TextStyle( - height: 1.7, - fontSize: 15, - ), - ), - ), - iconButton( - context: context, - icon: Icons.keyboard_arrow_down, - onPressed: () => _showIntro(audioItem), - bgColor: Colors.transparent, - iconColor: colorScheme.outline, - size: 26, - iconSize: 18, - ), - ], - ), - const SizedBox(height: 10), - if (audioItem.owner.hasName()) - Row( - spacing: 6, - mainAxisSize: MainAxisSize.min, - children: [ - if (audioItem.owner.hasAvatar()) - NetworkImgLayer( - src: audioItem.owner.avatar, - width: 22, - height: 22, - type: ImageType.avatar, - ), - Text( - audioItem.owner.name, - ), - ], - ), - if (isPortrait) - Expanded( - child: Align( - alignment: Alignment.bottomCenter, - child: _buildActions(audioItem), - ), - ), + if (isPortrait) ...[ + const SizedBox(height: 10), + _buildActions(audioItem), + ], ], ); } return const SizedBox.shrink(); }); } - - void _showIntro(DetailItem audioItem) { - final arc = audioItem.arc; - showModalBottomSheet( - context: context, - useSafeArea: true, - constraints: BoxConstraints( - maxWidth: min(640, context.mediaQueryShortestSide), - ), - builder: (context) { - final colorScheme = ColorScheme.of(context); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - InkWell( - onTap: Get.back, - borderRadius: StyleString.bottomSheetRadius, - child: SizedBox( - height: 35, - child: Center( - child: Container( - width: 32, - height: 3, - decoration: BoxDecoration( - color: colorScheme.outline, - borderRadius: const BorderRadius.all( - Radius.circular(3), - ), - ), - ), - ), - ), - ), - Padding( - padding: EdgeInsets.only( - top: 12, - left: 20, - right: 20, - bottom: MediaQuery.viewPaddingOf(context).bottom + 20, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('简介', style: TextStyle(fontSize: 15)), - const SizedBox(height: 20), - Text( - arc.title, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 10), - Row( - children: [ - Icon( - size: 14, - Icons.headphones_outlined, - color: colorScheme.outline, - ), - Text( - ' ${NumUtils.numFormat(audioItem.stat.view)} ' - '${DateFormatUtils.dateFormat(arc.publish.toInt(), long: DateFormatUtils.longFormatD)} ' - '${arc.displayedOid}', - style: TextStyle( - fontSize: 13, - color: colorScheme.outline, - ), - ), - ], - ), - const SizedBox(height: 20), - SelectableText(arc.desc), - ], - ), - ), - ], - ); - }, - ); - } } extension _PlayReatExt on PlayRepeat { diff --git a/lib/pages/fav_detail/widget/fav_video_card.dart b/lib/pages/fav_detail/widget/fav_video_card.dart index 1f3263c22..2a0ba1440 100644 --- a/lib/pages/fav_detail/widget/fav_video_card.dart +++ b/lib/pages/fav_detail/widget/fav_video_card.dart @@ -116,13 +116,21 @@ class FavVideoCardH extends StatelessWidget { bottom: 6.0, type: PBadgeType.gray, ), - PBadge( - text: item.ogv?.typeName, - top: 6.0, - right: 6.0, - bottom: null, - left: null, - ), + if (item.type == 12) + const PBadge( + text: '音频', + top: 6.0, + right: 6.0, + type: PBadgeType.gray, + ) + else + PBadge( + text: item.ogv?.typeName, + top: 6.0, + right: 6.0, + bottom: null, + left: null, + ), if (!isSort) Positioned.fill( child: selectMask( diff --git a/lib/pages/video/controller.dart b/lib/pages/video/controller.dart index 7a156f863..7b0ee1f8c 100644 --- a/lib/pages/video/controller.dart +++ b/lib/pages/video/controller.dart @@ -1756,7 +1756,7 @@ class VideoDetailController extends GetxController oid: aid, subId: [cid.value], from: PlaylistSource.UP_ARCHIVE, - heroTag: heroTag, + heroTag: autoPlay.value ? heroTag : null, start: playedTime, audioUrl: audioUrl, ); diff --git a/lib/utils/app_scheme.dart b/lib/utils/app_scheme.dart index 5108d64ce..3f51f5a95 100644 --- a/lib/utils/app_scheme.dart +++ b/lib/utils/app_scheme.dart @@ -920,7 +920,10 @@ abstract class PiliScheme { return false; case 'audio': // https://www.bilibili.com/audio/au123456 - String? oid = RegExp(r'/au(\d+)').firstMatch(path)?.group(1); + String? oid = RegExp( + r'/au(\d+)', + caseSensitive: false, + ).firstMatch(path)?.group(1); if (oid != null) { AudioPage.toAudioPage( itemType: 3, diff --git a/lib/utils/video_utils.dart b/lib/utils/video_utils.dart index c02fc76cd..13fcba7de 100644 --- a/lib/utils/video_utils.dart +++ b/lib/utils/video_utils.dart @@ -1,10 +1,11 @@ +import 'package:PiliPlus/grpc/bilibili/app/listener/v1.pb.dart' as audio; import 'package:PiliPlus/models/common/video/cdn_type.dart'; import 'package:PiliPlus/models/video/play/url.dart'; import 'package:PiliPlus/models_new/live/live_room_play_info/codec.dart'; import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; -abstract class VideoUtils { +abstract final class VideoUtils { static String cdnService = Pref.defaultCDNService; static bool disableAudioCDN = Pref.disableAudioCDN; @@ -29,6 +30,8 @@ abstract class VideoUtils { (item.urlInfo?.first.host)! + item.baseUrl! + item.urlInfo!.first.extra!; + } else if (item is audio.DashItem) { + backupUrl = item.backupUrl.lastOrNull; } else { backupUrl = item.backupUrl; } @@ -93,4 +96,16 @@ abstract class VideoUtils { return videoUrl; } + + static String getDurlCdnUrl(audio.ResponseUrl item) { + if (disableAudioCDN || cdnService == CDNService.backupUrl.code) { + return item.backupUrl.lastOrNull ?? item.url; + } + if (cdnService == CDNService.baseUrl.code) { + return item.url; + } + return Uri.parse( + item.backupUrl.lastOrNull ?? item.url, + ).replace(host: CDNService.fromCode(cdnService).host, port: 443).toString(); + } }