diff --git a/lib/common/widgets/image_viewer/gallery_viewer.dart b/lib/common/widgets/image_viewer/gallery_viewer.dart index ba652f144..77d327d9c 100644 --- a/lib/common/widgets/image_viewer/gallery_viewer.dart +++ b/lib/common/widgets/image_viewer/gallery_viewer.dart @@ -72,11 +72,9 @@ class _GalleryViewerState extends State late final RxInt _currIndex; GlobalKey? _key; + late bool _hasInit = false; Player? _player; - Player get _effectivePlayer => _player ??= Player(); VideoController? _videoController; - VideoController get _effectiveVideoController => - _videoController ??= VideoController(_effectivePlayer); late final PageController _pageController; @@ -99,6 +97,23 @@ class _GalleryViewerState extends State : url.http2https; } + Future _initPlayer() async { + assert(_player == null); + final player = await Player.create(); + _videoController = await VideoController.create(player); + if (!mounted) { + player.dispose(); + _videoController = null; + return; + } + _player = player; + final currItem = widget.sources[_currIndex.value]; + if (currItem.sourceType == .livePhoto) { + player.open(Media(currItem.liveUrl!)); + _currIndex.refresh(); + } + } + @override void initState() { super.initState(); @@ -230,7 +245,6 @@ class _GalleryViewerState extends State ..onDoubleTap = null ..dispose(); _longPressGestureRecognizer.dispose(); - _currIndex.close(); if (widget.quality != _quality) { for (final item in widget.sources) { if (item.sourceType == SourceType.networkImage) { @@ -238,6 +252,7 @@ class _GalleryViewerState extends State } } } + Future.delayed(const Duration(milliseconds: 200), _currIndex.close); super.dispose(); } @@ -317,7 +332,12 @@ class _GalleryViewerState extends State void _playIfNeeded(SourceModel item) { if (item.sourceType == .livePhoto) { - _effectivePlayer.open(Media(item.liveUrl!)); + if (_player != null) { + _player!.open(Media(item.liveUrl!)); + } else if (!_hasInit) { + _hasInit = true; + _initPlayer(); + } } } @@ -421,7 +441,7 @@ class _GalleryViewerState extends State case SourceType.livePhoto: child = Obx( key: _key, - () => _currIndex.value == index + () => _currIndex.value == index && _videoController != null ? Viewer( minScale: widget.minScale, maxScale: widget.maxScale, @@ -434,9 +454,9 @@ class _GalleryViewerState extends State horizontalDragGestureRecognizer: _horizontalDragGestureRecognizer, onChangePage: _onChangePage, - child: AbsorbPointer( - child: Video( - controller: _effectiveVideoController, + child: FittedBox( + child: SimpleVideo( + controller: _videoController!, fill: Colors.transparent, ), ), diff --git a/lib/http/api.dart b/lib/http/api.dart index a8d17ab3f..fb7c91133 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -997,4 +997,6 @@ abstract final class Api { '/x/v2/reply/subject/interaction-status'; static const String replySubjectModify = '/x/v2/reply/subject/modify'; + + static const String videoshot = '/x/player/videoshot'; } diff --git a/lib/http/browser_ua.dart b/lib/http/browser_ua.dart new file mode 100644 index 000000000..9414fbd1c --- /dev/null +++ b/lib/http/browser_ua.dart @@ -0,0 +1,11 @@ +import 'package:PiliPlus/utils/platform_utils.dart'; + +abstract final class BrowserUa { + static String get platform => PlatformUtils.isMobile ? mob : pc; + + static const pc = + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.2 Safari/605.1.15'; + + static const mob = + 'Mozilla/5.0 (Linux; Android 10; SM-G975F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Mobile Safari/537.36'; +} diff --git a/lib/http/live.dart b/lib/http/live.dart index f1d0afeb9..2871d6d98 100644 --- a/lib/http/live.dart +++ b/lib/http/live.dart @@ -1,9 +1,9 @@ import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/http/api.dart'; +import 'package:PiliPlus/http/browser_ua.dart'; import 'package:PiliPlus/http/init.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/login.dart'; -import 'package:PiliPlus/http/ua_type.dart'; import 'package:PiliPlus/models/common/account_type.dart'; import 'package:PiliPlus/models/common/live/live_contribution_rank_type.dart'; import 'package:PiliPlus/models/common/live/live_search_type.dart'; @@ -132,7 +132,7 @@ abstract final class LiveHttp { options: Options( headers: { 'referer': 'https://live.bilibili.com/$roomId', - 'user-agent': UaType.pc.ua, + 'user-agent': BrowserUa.pc, }, ), ); diff --git a/lib/http/member.dart b/lib/http/member.dart index f722abdcd..f46d62425 100644 --- a/lib/http/member.dart +++ b/lib/http/member.dart @@ -2,10 +2,10 @@ import 'dart:io'; import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/http/api.dart'; +import 'package:PiliPlus/http/browser_ua.dart'; import 'package:PiliPlus/http/constants.dart'; import 'package:PiliPlus/http/init.dart'; import 'package:PiliPlus/http/loading_state.dart'; -import 'package:PiliPlus/http/ua_type.dart'; import 'package:PiliPlus/models/common/member/contribute_type.dart'; import 'package:PiliPlus/models/dynamics/result.dart'; import 'package:PiliPlus/models/member/info.dart'; @@ -306,7 +306,7 @@ abstract final class MemberHttp { headers: { 'origin': 'https://space.bilibili.com', 'referer': 'https://space.bilibili.com/$mid/dynamic', - 'user-agent': UaType.pc.ua, + 'user-agent': BrowserUa.pc, }, ), ); @@ -377,7 +377,7 @@ abstract final class MemberHttp { queryParameters: params, options: Options( headers: { - HttpHeaders.userAgentHeader: UaType.pc.ua, + HttpHeaders.userAgentHeader: BrowserUa.pc, HttpHeaders.refererHeader: '${HttpString.spaceBaseUrl}/$mid', 'origin': HttpString.spaceBaseUrl, }, @@ -420,7 +420,7 @@ abstract final class MemberHttp { queryParameters: params, options: Options( headers: { - 'user-agent': UaType.pc.ua, + 'user-agent': BrowserUa.pc, 'origin': 'https://space.bilibili.com', 'referer': 'https://space.bilibili.com/$mid/dynamic', }, diff --git a/lib/http/ua_type.dart b/lib/http/ua_type.dart deleted file mode 100644 index 124b56525..000000000 --- a/lib/http/ua_type.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:PiliPlus/utils/platform_utils.dart'; - -enum UaType { - mob( - 'Mozilla/5.0 (Linux; Android 10; SM-G975F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Mobile Safari/537.36', - ), - pc( - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.2 Safari/605.1.15', - ) - ; - - static UaType get platformUA => PlatformUtils.isMobile ? mob : pc; - - final String ua; - const UaType(this.ua); -} diff --git a/lib/http/video.dart b/lib/http/video.dart index 61257d631..98d42b1fa 100644 --- a/lib/http/video.dart +++ b/lib/http/video.dart @@ -2,10 +2,10 @@ import 'dart:convert'; import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/http/api.dart'; +import 'package:PiliPlus/http/browser_ua.dart'; import 'package:PiliPlus/http/init.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/login.dart'; -import 'package:PiliPlus/http/ua_type.dart'; import 'package:PiliPlus/models/common/account_type.dart'; import 'package:PiliPlus/models/common/video/video_type.dart'; import 'package:PiliPlus/models/home/rcmd/result.dart'; @@ -25,6 +25,7 @@ import 'package:PiliPlus/models_new/video/video_detail/video_detail_response.dar import 'package:PiliPlus/models_new/video/video_note_list/data.dart'; import 'package:PiliPlus/models_new/video/video_play_info/data.dart'; import 'package:PiliPlus/models_new/video/video_relation/data.dart'; +import 'package:PiliPlus/models_new/video/video_shot/data.dart'; import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/app_sign.dart'; import 'package:PiliPlus/utils/extension/string_ext.dart'; @@ -223,10 +224,7 @@ abstract final class VideoHttp { }); try { - final res = await Request().get( - videoType.api, - queryParameters: params, - ); + final res = await Request().get(videoType.api, queryParameters: params); if (res.data['code'] == 0) { late PlayUrlModel data; @@ -295,10 +293,7 @@ abstract final class VideoHttp { }) async { final res = await Request().get( Api.videoRelation, - queryParameters: { - 'aid': IdUtils.bv2av(bvid), - 'bvid': bvid, - }, + queryParameters: {'aid': IdUtils.bv2av(bvid), 'bvid': bvid}, ); if (res.data['code'] == 0) { return Success(VideoRelation.fromJson(res.data['data'])); @@ -374,16 +369,14 @@ abstract final class VideoHttp { }) async { final res = await Request().post( Api.pgcTriple, - data: { - 'ep_id': epId, - 'csrf': Accounts.main.csrf, - }, + data: {'ep_id': epId, 'csrf': Accounts.main.csrf}, options: Options( contentType: Headers.formUrlEncodedContentType, headers: { 'origin': 'https://www.bilibili.com', - 'referer': 'https://www.bilibili.com/bangumi/play/ss$seasonId', - 'user-agent': UaType.pc.ua, + 'referer': + 'https://www.bilibili.com/bangumi/play/${seasonId == null ? "ep$epId" : "ss$seasonId"}', + 'user-agent': BrowserUa.pc, }, ), ); @@ -415,7 +408,7 @@ abstract final class VideoHttp { headers: { 'origin': 'https://www.bilibili.com', 'referer': 'https://www.bilibili.com/video/$bvid', - 'user-agent': UaType.pc.ua, + 'user-agent': BrowserUa.pc, }, ), ); @@ -433,10 +426,7 @@ abstract final class VideoHttp { }) async { final res = await Request().post( Api.likeVideo, - data: { - 'aid': IdUtils.bv2av(bvid).toString(), - 'like': type ? '0' : '1', - }, + data: {'aid': IdUtils.bv2av(bvid).toString(), 'like': type ? '0' : '1'}, options: Options(contentType: Headers.formUrlEncodedContentType), ); if (res.data['code'] == 0) { @@ -612,7 +602,7 @@ abstract final class VideoHttp { 'extend_content': jsonEncode({ "entity": "user", "entity_id": mid, - 'fp': UaType.pc.ua, + 'fp': BrowserUa.pc, }), 'csrf': Accounts.main.csrf, }, @@ -621,7 +611,7 @@ abstract final class VideoHttp { headers: { 'origin': 'https://space.bilibili.com', 'referer': 'https://space.bilibili.com/$mid/dynamic', - 'user-agent': UaType.pc.ua, + 'user-agent': BrowserUa.pc, }, ), ); @@ -639,18 +629,11 @@ abstract final class VideoHttp { } } - static Future roomEntryAction({ - required Object roomId, - }) { + static Future roomEntryAction({required Object roomId}) { return Request().post( Api.roomEntryAction, - queryParameters: { - 'csrf': Accounts.heartbeat.csrf, - }, - data: { - 'room_id': roomId, - 'platform': 'pc', - }, + queryParameters: {'csrf': Accounts.heartbeat.csrf}, + data: {'room_id': roomId, 'platform': 'pc'}, ); } @@ -660,11 +643,7 @@ abstract final class VideoHttp { }) { return Request().post( Api.historyReport, - data: { - 'aid': aid, - 'type': type, - 'csrf': Accounts.heartbeat.csrf, - }, + data: {'aid': aid, 'type': type, 'csrf': Accounts.heartbeat.csrf}, options: Options(contentType: Headers.formUrlEncodedContentType), ); } @@ -718,10 +697,7 @@ abstract final class VideoHttp { static Future> pgcAdd({int? seasonId}) async { final res = await Request().post( Api.pgcAdd, - data: { - 'season_id': seasonId, - 'csrf': Accounts.main.csrf, - }, + data: {'season_id': seasonId, 'csrf': Accounts.main.csrf}, options: Options(contentType: Headers.formUrlEncodedContentType), ); if (res.data['code'] == 0) { @@ -735,10 +711,7 @@ abstract final class VideoHttp { static Future> pgcDel({int? seasonId}) async { final res = await Request().post( Api.pgcDel, - data: { - 'season_id': seasonId, - 'csrf': Accounts.main.csrf, - }, + data: {'season_id': seasonId, 'csrf': Accounts.main.csrf}, options: Options(contentType: Headers.formUrlEncodedContentType), ); if (res.data['code'] == 0) { @@ -759,9 +732,7 @@ abstract final class VideoHttp { 'status': status, 'csrf': Accounts.main.csrf, }, - options: Options( - contentType: Headers.formUrlEncodedContentType, - ), + options: Options(contentType: Headers.formUrlEncodedContentType), ); if (res.data['code'] == 0) { return Success(res.data['result']['toast']); @@ -779,11 +750,7 @@ abstract final class VideoHttp { assert(aid != null || bvid != null); final res = await Request().get( Api.onlineTotal, - queryParameters: { - 'aid': aid, - 'bvid': bvid, - 'cid': cid, - }, + queryParameters: {'aid': aid, 'bvid': bvid, 'cid': cid}, ); if (res.data['code'] == 0) { return Success(res.data['data']['total']); @@ -895,10 +862,7 @@ abstract final class VideoHttp { ) async { final res = await Request().get( Api.getRankApi, - queryParameters: await WbiSign.makSign({ - 'rid': rid, - 'type': 'all', - }), + queryParameters: await WbiSign.makSign({'rid': rid, 'type': 'all'}), ); if (res.data['code'] == 0) { List list = []; @@ -994,9 +958,7 @@ abstract final class VideoHttp { popularSeriesList() async { final res = await Request().get( Api.popularSeriesList, - queryParameters: await WbiSign.makSign({ - 'web_location': 333.934, - }), + queryParameters: await WbiSign.makSign({'web_location': 333.934}), ); if (res.data['code'] == 0) { return Success( @@ -1068,14 +1030,41 @@ abstract final class VideoHttp { 'qn': qn ?? 80, }; AppSign.appSign(params); - final res = await Request().get( - Api.tvPlayUrl, - queryParameters: params, - ); + final res = await Request().get(Api.tvPlayUrl, queryParameters: params); if (res.data['code'] == 0) { return Success(PlayUrlModel.fromJson(res.data['data'])); } else { return Error(res.data['message']); } } + + static Future> videoshot({ + required String bvid, + required int cid, + }) async { + final res = await Request().get( + Api.videoshot, + queryParameters: { + // 'aid': IdUtils.bv2av(_bvid), + 'bvid': bvid, + 'cid': cid, + 'index': 1, + }, + options: Options( + headers: { + 'user-agent': BrowserUa.pc, + 'referer': 'https://www.bilibili.com/video/$bvid', + }, + ), + ); + if (res.data['code'] == 0) { + final data = VideoShotData.fromJson(res.data['data']); + if (data.index.isNotEmpty) { + return Success(data); + } else { + return const Error(null); + } + } + return Error(res.data['message']); + } } diff --git a/lib/pages/audio/controller.dart b/lib/pages/audio/controller.dart index 6e669bb3a..0cdc58a08 100644 --- a/lib/pages/audio/controller.dart +++ b/lib/pages/audio/controller.dart @@ -12,9 +12,9 @@ import 'package:PiliPlus/grpc/bilibili/app/listener/v1.pb.dart' ListOrder, DashItem, ResponseUrl; +import 'package:PiliPlus/http/browser_ua.dart'; import 'package:PiliPlus/http/constants.dart'; import 'package:PiliPlus/http/loading_state.dart'; -import 'package:PiliPlus/http/ua_type.dart'; import 'package:PiliPlus/pages/common/common_intro_controller.dart' show FavMixin; import 'package:PiliPlus/pages/dynamics_repost/view.dart'; @@ -59,8 +59,9 @@ class AudioController extends GetxController @override late final bool isUgc = itemType == 1; - final Rx audioItem = Rx(null); + final audioItem = Rxn(); + bool _hasInit = false; @override Player? player; late int cacheAudioQa; @@ -71,7 +72,7 @@ class AudioController extends GetxController late final AnimationController animController; - Set? _subscriptions; + List? _subscriptions; int? index; List? playlist; @@ -118,11 +119,7 @@ class AudioController extends GetxController final hasAudioUrl = audioUrl != null; if (hasAudioUrl) { _querySponsorBlock(); - _onOpenMedia( - audioUrl, - ua: UaType.pc.ua, - referer: HttpString.baseUrl, - ); + _onOpenMedia(audioUrl, ua: BrowserUa.pc, referer: HttpString.baseUrl); } Utils.isWiFi.then((isWiFi) { cacheAudioQa = isWiFi ? Pref.defaultAudioQa : Pref.defaultAudioQaCellular; @@ -155,7 +152,7 @@ class AudioController extends GetxController return player?.play(); } - Future? onPause() async { + Future? onPause() { return player?.pause(); } @@ -277,28 +274,33 @@ class AudioController extends GetxController } } - void _onOpenMedia( + Future _onOpenMedia( String url, { - String? referer, String ua = Constants.userAgentApp, - }) { - _initPlayerIfNeeded(); - player!.open( - Media( - url, - start: _start, - httpHeaders: { - 'user-agent': ua, - 'referer': ?referer, - }, - ), - ); + String? referer, + }) async { + await _initPlayerIfNeeded(); + player + ?..setMediaHeader( + userAgent: ua, + // mpv cannot clear referer option + headers: {'Referer': ?referer}, + ) + ..open(Media(url, start: _start)); _start = null; } - void _initPlayerIfNeeded() { - player ??= Player(); - _subscriptions ??= { + Future _initPlayerIfNeeded() async { + if (_hasInit) return; + _hasInit = true; + assert(player == null, _subscriptions = null); + player = await Player.create(); + if (isClosed) { + player!.dispose(); + player = null; + return; + } + _subscriptions = [ player!.stream.position.listen((position) { if (isDragging) return; if (position.inSeconds != this.position.value.inSeconds) { @@ -307,11 +309,9 @@ class AudioController extends GetxController videoPlayerServiceHandler?.onPositionChange(position); } }), - player!.stream.duration.listen((duration) { - this.duration.value = duration; - }), + player!.stream.duration.listen(duration.call), player!.stream.playing.listen((playing) { - PlayerStatus playerStatus; + final PlayerStatus playerStatus; if (playing) { animController.forward(); playerStatus = PlayerStatus.playing; @@ -339,14 +339,14 @@ class AudioController extends GetxController playNext(nextPart: true); break; case PlayRepeat.singleCycle: - _replay(); + onPlay(); break; case PlayRepeat.listCycle: if (!playNext(nextPart: true)) { if (index != null && index != 0 && playlist != null) { playIndex(0); } else { - _replay(); + onPlay(); } } break; @@ -356,11 +356,7 @@ class AudioController extends GetxController } } }), - }; - } - - void _replay() { - player?.seek(Duration.zero).whenComplete(player!.play); + ]; } @override diff --git a/lib/pages/audio/view.dart b/lib/pages/audio/view.dart index 48c7c1ca8..b76887bb0 100644 --- a/lib/pages/audio/view.dart +++ b/lib/pages/audio/view.dart @@ -744,7 +744,7 @@ class _AudioPageState extends State { void _onSeek(Duration value) { _controller - ..player?.platform?.seek(value) + ..player?.seek(value) ..isDragging = false; } diff --git a/lib/pages/common/publish/publish_route.dart b/lib/pages/common/publish/publish_route.dart index 074e73d55..6d03acae4 100644 --- a/lib/pages/common/publish/publish_route.dart +++ b/lib/pages/common/publish/publish_route.dart @@ -64,7 +64,7 @@ class PublishRoute extends PopupRoute { Tween( begin: const Offset(0.0, 1.0), end: Offset.zero, - ).chain(CurveTween(curve: Curves.linear)), + ), ), child: child, ); diff --git a/lib/pages/danmaku/controller.dart b/lib/pages/danmaku/controller.dart index f91be9d43..bfc60217a 100644 --- a/lib/pages/danmaku/controller.dart +++ b/lib/pages/danmaku/controller.dart @@ -5,6 +5,7 @@ import 'package:PiliPlus/grpc/bilibili/community/service/dm/v1.pb.dart'; import 'package:PiliPlus/grpc/dm.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/plugin/pl_player/controller.dart'; +import 'package:PiliPlus/plugin/pl_player/models/data_source.dart'; import 'package:PiliPlus/plugin/pl_player/utils/danmaku_options.dart'; import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/path_utils.dart'; @@ -121,7 +122,10 @@ class PlDanmakuController { Future _initFileDm() async { try { final file = File( - path.join(_plPlayerController.dirPath!, PathUtils.danmakuName), + path.join( + (_plPlayerController.dataSource as FileSource).dir, + PathUtils.danmakuName, + ), ); if (!file.existsSync()) return; final bytes = await file.readAsBytes(); diff --git a/lib/pages/download/detail/view.dart b/lib/pages/download/detail/view.dart index d8548e5e2..71d4a00a5 100644 --- a/lib/pages/download/detail/view.dart +++ b/lib/pages/download/detail/view.dart @@ -54,7 +54,7 @@ class _DownloadDetailPageState extends State }); } - Future _closeSub() async { + Future _closeSub() async { if (_sub != null) { await _sub?.cancel(); _sub = null; diff --git a/lib/pages/dynamics/widgets/content_panel.dart b/lib/pages/dynamics/widgets/content_panel.dart index dcd52996d..3f8dd400d 100644 --- a/lib/pages/dynamics/widgets/content_panel.dart +++ b/lib/pages/dynamics/widgets/content_panel.dart @@ -28,6 +28,8 @@ Widget content( ); final moduleDynamic = item.modules.moduleDynamic; final pics = moduleDynamic?.major?.opus?.pics; + final text = + moduleDynamic?.desc?.text ?? moduleDynamic?.major?.opus?.summary?.text; return Padding( padding: floor == 1 ? const EdgeInsets.fromLTRB(12, 0, 12, 6) @@ -78,8 +80,9 @@ Widget content( style: isSave ? const TextStyle(fontSize: 15) : const TextStyle(fontSize: 16), - contextMenuBuilder: (_, state) => - _contextMenuBuilder(state, moduleDynamic), + contextMenuBuilder: text == null || text.isEmpty + ? null + : (_, state) => _contextMenuBuilder(state, text), ) : custom_text.Text.rich( style: floor == 1 @@ -110,26 +113,17 @@ Widget content( ); } -Widget _contextMenuBuilder( - EditableTextState state, - ModuleDynamicModel? moduleDynamic, -) { +Widget _contextMenuBuilder(EditableTextState state, String text) { return AdaptiveTextSelectionToolbar.buttonItems( buttonItems: state.contextMenuButtonItems ..add( - ContextMenuButtonItem( - label: '文本', - onPressed: () => _onCopyText(moduleDynamic), - ), + ContextMenuButtonItem(label: '文本', onPressed: () => _onCopyText(text)), ), anchors: state.contextMenuAnchors, ); } -void _onCopyText(ModuleDynamicModel? moduleDynamic) { - final text = - moduleDynamic?.desc?.text ?? moduleDynamic?.major?.opus?.summary?.text; - if (text == null || text.isEmpty) return; +void _onCopyText(String text) { showDialog( context: Get.context!, builder: (context) => Dialog( diff --git a/lib/pages/dynamics/widgets/vote.dart b/lib/pages/dynamics/widgets/vote.dart index 558625da7..b04ad142b 100644 --- a/lib/pages/dynamics/widgets/vote.dart +++ b/lib/pages/dynamics/widgets/vote.dart @@ -533,7 +533,7 @@ class PercentageChip extends StatelessWidget { } } -Future showVoteDialog( +Future showVoteDialog( BuildContext context, int voteId, [ int? dynamicId, diff --git a/lib/pages/dynamics_mention/view.dart b/lib/pages/dynamics_mention/view.dart index 617a0fc1b..6b14f96fb 100644 --- a/lib/pages/dynamics_mention/view.dart +++ b/lib/pages/dynamics_mention/view.dart @@ -27,7 +27,7 @@ class DynMentionPanel extends StatefulWidget { final ScrollController? scrollController; final ValueChanged? onCachePos; - static Future onDynMention( + static Future */> onDynMention( BuildContext context, { double offset = 0, ValueChanged? onCachePos, diff --git a/lib/pages/live_room/controller.dart b/lib/pages/live_room/controller.dart index fdfba57c1..752bbeb00 100644 --- a/lib/pages/live_room/controller.dart +++ b/lib/pages/live_room/controller.dart @@ -3,7 +3,6 @@ import 'dart:convert'; import 'package:PiliPlus/common/widgets/dialog/report.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/controller.dart'; -import 'package:PiliPlus/http/constants.dart'; import 'package:PiliPlus/http/live.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/video.dart'; @@ -200,16 +199,7 @@ class LiveRoomController extends GetxController { return null; } return plPlayerController.setDataSource( - DataSource( - videoSource: videoUrl, - audioSource: null, - type: DataSourceType.network, - httpHeaders: { - 'user-agent': - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 13_3_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Safari/605.1.15', - 'referer': HttpString.baseUrl, - }, - ), + NetworkSource(videoSource: videoUrl!, audioSource: null), isLive: true, autoplay: autoplay, isVertical: isPortrait.value, diff --git a/lib/pages/live_room/view.dart b/lib/pages/live_room/view.dart index 5e3027694..bda5f3dc9 100644 --- a/lib/pages/live_room/view.dart +++ b/lib/pages/live_room/view.dart @@ -28,7 +28,7 @@ import 'package:PiliPlus/plugin/pl_player/controller.dart'; import 'package:PiliPlus/plugin/pl_player/models/play_status.dart'; import 'package:PiliPlus/plugin/pl_player/utils/danmaku_options.dart'; import 'package:PiliPlus/plugin/pl_player/utils/fullscreen.dart'; -import 'package:PiliPlus/plugin/pl_player/view.dart'; +import 'package:PiliPlus/plugin/pl_player/view/view.dart'; import 'package:PiliPlus/services/service_locator.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/extension/size_ext.dart'; diff --git a/lib/pages/login/geetest/geetest_webview_dialog.dart b/lib/pages/login/geetest/geetest_webview_dialog.dart index b63d9cd24..2ce9966d6 100644 --- a/lib/pages/login/geetest/geetest_webview_dialog.dart +++ b/lib/pages/login/geetest/geetest_webview_dialog.dart @@ -1,8 +1,8 @@ import 'dart:convert'; +import 'package:PiliPlus/http/browser_ua.dart'; import 'package:PiliPlus/http/init.dart'; import 'package:PiliPlus/http/loading_state.dart'; -import 'package:PiliPlus/http/ua_type.dart'; import 'package:PiliPlus/main.dart'; import 'package:PiliPlus/utils/accounts/account.dart'; import 'package:dio/dio.dart'; @@ -79,7 +79,7 @@ class GeetestWebviewDialog extends StatelessWidget { useHybridComposition: false, algorithmicDarkeningAllowed: true, useShouldOverrideUrlLoading: true, - userAgent: UaType.mob.ua, + userAgent: BrowserUa.mob, mixedContentMode: MixedContentMode.MIXED_CONTENT_ALWAYS_ALLOW, ), initialData: InAppWebViewInitialData( diff --git a/lib/pages/setting/widgets/select_dialog.dart b/lib/pages/setting/widgets/select_dialog.dart index 6f0c7c403..5097e3173 100644 --- a/lib/pages/setting/widgets/select_dialog.dart +++ b/lib/pages/setting/widgets/select_dialog.dart @@ -1,7 +1,7 @@ import 'dart:async'; +import 'package:PiliPlus/http/browser_ua.dart'; import 'package:PiliPlus/http/constants.dart'; -import 'package:PiliPlus/http/ua_type.dart'; import 'package:PiliPlus/http/video.dart'; import 'package:PiliPlus/models/common/video/cdn_type.dart'; import 'package:PiliPlus/models/common/video/video_type.dart'; @@ -99,7 +99,7 @@ class _CdnSelectDialogState extends State { ), ) ..options.headers = { - 'user-agent': UaType.pc.ua, + 'user-agent': BrowserUa.pc, 'referer': HttpString.baseUrl, }; final length = CDNService.values.length; diff --git a/lib/pages/video/controller.dart b/lib/pages/video/controller.dart index d9809f3f5..bd2ba4219 100644 --- a/lib/pages/video/controller.dart +++ b/lib/pages/video/controller.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'dart:math' show min; import 'dart:ui'; @@ -7,11 +8,9 @@ import 'package:PiliPlus/common/widgets/pair.dart'; import 'package:PiliPlus/common/widgets/progress_bar/segment_progress_bar.dart'; import 'package:PiliPlus/grpc/bilibili/app/listener/v1.pbenum.dart' show PlaylistSource; -import 'package:PiliPlus/http/constants.dart'; import 'package:PiliPlus/http/fav.dart'; import 'package:PiliPlus/http/init.dart'; import 'package:PiliPlus/http/loading_state.dart'; -import 'package:PiliPlus/http/ua_type.dart'; import 'package:PiliPlus/http/user.dart'; import 'package:PiliPlus/http/video.dart'; import 'package:PiliPlus/main.dart'; @@ -55,10 +54,12 @@ import 'package:PiliPlus/plugin/pl_player/models/play_status.dart'; import 'package:PiliPlus/services/download/download_service.dart'; import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/extension/context_ext.dart'; +import 'package:PiliPlus/utils/extension/file_ext.dart'; import 'package:PiliPlus/utils/extension/iterable_ext.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/extension/size_ext.dart'; import 'package:PiliPlus/utils/page_utils.dart'; +import 'package:PiliPlus/utils/path_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; @@ -71,7 +72,8 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_volume_controller/flutter_volume_controller.dart'; import 'package:get/get.dart'; import 'package:hive/hive.dart'; -import 'package:media_kit/media_kit.dart'; +import 'package:media_kit/media_kit.dart' hide Subtitle; +import 'package:path/path.dart' as path; class VideoDetailController extends GetxController with GetTickerProviderStateMixin, BlockMixin { @@ -675,27 +677,21 @@ class VideoDetailController extends GetxController bool? autoplay, Volume? volume, }) async { - final onlyPlayAudio = plPlayerController.onlyPlayAudio.value; Duration? seek = seekToTime ?? defaultST ?? playedTime; if (seek == null || seek == Duration.zero) { seek = getFirstSegment(); } await plPlayerController.setDataSource( - DataSource( - videoSource: isFileSource - ? null - : onlyPlayAudio - ? audio ?? audioUrl - : video ?? videoUrl, - audioSource: isFileSource || onlyPlayAudio ? null : audio ?? audioUrl, - type: isFileSource ? DataSourceType.file : DataSourceType.network, - httpHeaders: isFileSource - ? null - : { - 'user-agent': UaType.pc.ua, - 'referer': HttpString.baseUrl, - }, - ), + isFileSource + ? FileSource( + dir: args['dirPath'], + typeTag: entry.typeTag!, + isMp4: entry.mediaType == 1, + ) + : NetworkSource( + videoSource: video ?? videoUrl!, + audioSource: audio ?? audioUrl, + ), seekTo: seek, duration: duration ?? @@ -720,9 +716,6 @@ class VideoDetailController extends GetxController width: firstVideo.width, height: firstVideo.height, volume: volume ?? this.volume, - dirPath: isFileSource ? args['dirPath'] : null, - typeTag: isFileSource ? entry.typeTag : null, - mediaType: isFileSource ? entry.mediaType : null, ); if (isClosed) return; @@ -1013,14 +1006,24 @@ class VideoDetailController extends GetxController Future setSub(({bool isData, String id}) subtitle) async { final sub = subtitles[index - 1]; + + String subUri = subtitle.id; + File? file; + if (subtitle.isData) { + subUri = path.join(tmpDirPath, '${cid.value}-${sub.lan}.vtt'); + file = File(subUri); + if (!file.existsSync()) { + await file.writeAsString(subtitle.id); + if (plPlayerController.videoPlayerController?.disposed == false) { + plPlayerController.videoPlayerController!.release.add(file.tryDel); + } else { + file.tryDel(); + return; + } + } + } await plPlayerController.videoPlayerController?.setSubtitleTrack( - SubtitleTrack( - subtitle.id, - sub.lanDoc, - sub.lan, - uri: !subtitle.isData, - data: subtitle.isData, - ), + SubtitleTrack(subUri, sub.lanDoc, sub.lan, uri: true), ); vttSubtitlesIndex.value = index; } diff --git a/lib/pages/video/pay_coins/view.dart b/lib/pages/video/pay_coins/view.dart index 5c79c06dc..53db2a1a5 100644 --- a/lib/pages/video/pay_coins/view.dart +++ b/lib/pages/video/pay_coins/view.dart @@ -67,7 +67,6 @@ class _PayCoinsPageState extends State late final AnimationController _slide22Controller; late final Animation _slide22Anim; late final AnimationController _scale22Controller; - late final Animation _scale22Anim; late final AnimationController _coinController; late final Animation _coinSlideAnim; late final Animation _coinFadeAnim; @@ -87,28 +86,19 @@ class _PayCoinsPageState extends State } final num? _coins = GlobalData().coins; - late final List _payState; - late final List _payImg; - late final List _payFilter; bool _canPay(int index) { if (index == 1 && widget.hasCoin) { return false; } - if (_coins == null) { - return true; - } - if (index == 0 && _coins >= 1) { - return true; - } - if (index == 1 && _coins >= 2) { + if (_coins == null || _coins >= 1 + index) { return true; } return false; } - String _getPayImage(int index) { - if (!_payState[index]) { + String _getPayImage(int index, bool canPay) { + if (!canPay) { return 'assets/images/paycoins/ic_22_not_enough_pay.png'; } return index == 0 @@ -117,21 +107,7 @@ class _PayCoinsPageState extends State } late final color = Colors.black.withValues(alpha: 0.4); - Color _getPayFilter(int index) { - if (index == 1 && widget.hasCoin) { - return color; - } - if (_coins == null) { - return Colors.transparent; - } - if (index == 0 && _coins == 0) { - return color; - } - if (index == 1 && _coins < 2) { - return color; - } - return Colors.transparent; - } + Color _getPayFilter(int index) => _canPay(index) ? Colors.transparent : color; @override void initState() { @@ -139,10 +115,6 @@ class _PayCoinsPageState extends State if (_hasCopyright) { _controller = PageController(viewportFraction: 0.30); } - final count = _hasCopyright ? 2 : 1; - _payState = List.generate(count, _canPay); - _payImg = List.generate(count, _getPayImage); - _payFilter = List.generate(count, _getPayFilter); _slide22Controller = AnimationController( vsync: this, @@ -157,9 +129,8 @@ class _PayCoinsPageState extends State _scale22Controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 50), - ); - _scale22Anim = _scale22Controller.drive( - Tween(begin: 1.0, end: 1.1), + lowerBound: 1.0, + upperBound: 1.1, ); _coinController = AnimationController( vsync: this, @@ -188,7 +159,7 @@ class _PayCoinsPageState extends State ), ); - WidgetsBinding.instance.addPostFrameCallback((_) => _scale()); + WidgetsBinding.instance.addPostFrameCallback(_scale); } @override @@ -202,7 +173,7 @@ class _PayCoinsPageState extends State super.dispose(); } - void _scale() { + void _scale([_]) { _scale22Controller.forward().whenComplete(_scale22Controller.reverse); } @@ -235,7 +206,7 @@ class _PayCoinsPageState extends State width: 70 + (factor * 30), child: ColorFiltered( colorFilter: ColorFilter.mode( - _payFilter[index], + _getPayFilter(index), BlendMode.srcATop, ), child: Stack( @@ -270,8 +241,8 @@ class _PayCoinsPageState extends State Widget _build22() { final index = _pageIndex.value; - final canPay = _payState[index]; - final payImg = _payImg[index]; + final canPay = _canPay(index); + final payImg = _getPayImage(index, canPay); return GestureDetector( onTap: canPay ? _onPayCoin : null, onVerticalDragStart: canPay @@ -285,7 +256,7 @@ class _PayCoinsPageState extends State onVerticalDragCancel: canPay ? _onDragEnd : null, behavior: HitTestBehavior.opaque, child: ScaleTransition( - scale: _scale22Anim, + scale: _scale22Controller, child: SlideTransition( position: _slide22Anim, child: SizedBox( @@ -318,7 +289,7 @@ class _PayCoinsPageState extends State }), Align( alignment: Alignment.bottomCenter, - child: GestureDetector( + child: Listener( behavior: HitTestBehavior.opaque, child: Column( mainAxisSize: MainAxisSize.min, @@ -334,7 +305,6 @@ class _PayCoinsPageState extends State child: SizedBox( height: 100, child: PageView( - key: const PageStorageKey(_PayCoinsPageState), physics: clampingScrollPhysics, controller: _controller, onPageChanged: (index) { diff --git a/lib/pages/video/post_panel/view.dart b/lib/pages/video/post_panel/view.dart index bae1e1ed0..5b18773cf 100644 --- a/lib/pages/video/post_panel/view.dart +++ b/lib/pages/video/post_panel/view.dart @@ -464,20 +464,34 @@ class _PostPanelState extends State tooltip: '预览', icon: const Icon(Icons.preview_outlined), onPressed: () async { - final videoCtr = widget.plPlayerController.videoPlayerController; - if (videoCtr != null) { + final player = plPlayerController.videoPlayerController; + if (player != null) { final start = (item.segment.first * 1000).round(); final seek = max(0, start - 2000); - await videoCtr.seek(Duration(milliseconds: seek)); - if (!videoCtr.state.playing) { - await videoCtr.play(); + await player.seek(Duration(milliseconds: seek)); + if (!player.state.playing) { + await player.play(); } - final delay = start - seek; - Future seekTo() => videoCtr.seek( + Future seekTo() => player.seek( Duration(milliseconds: (item.segment.second * 1000).round()), ); - if (delay > 0) { - Timer(Duration(milliseconds: delay), seekTo); + if (start > seek) { + final posSub = player.stream.position.listen( + null, + cancelOnError: true, + ); + final timer = Timer( + const Duration(seconds: 10), + posSub.cancel, + ); + final duration = Duration(milliseconds: start); + posSub.onData((pos) { + if (pos >= duration) { + seekTo(); + timer.cancel(); + posSub.cancel(); + } + }); } else { seekTo(); } diff --git a/lib/pages/video/reply_new/view.dart b/lib/pages/video/reply_new/view.dart index 2a47e22bf..b7fb4fae6 100644 --- a/lib/pages/video/reply_new/view.dart +++ b/lib/pages/video/reply_new/view.dart @@ -380,13 +380,12 @@ class _ReplyPageState extends CommonRichTextPubPageState { final res = await plPlayerController .plPlayerController .videoPlayerController - ?.screenshot(format: 'image/png'); + ?.screenshot(format: .png); if (res != null) { - final file = File( - '$tmpDirPath/${Utils.generateRandomString(8)}.png', - ); - await file.writeAsBytes(res); - imageList.add(FilePicModel(path: file.path)); + final path = + '$tmpDirPath/${Utils.generateRandomString(8)}.png'; + await File(path).writeAsBytes(res); + imageList.add(FilePicModel(path: path)); } else { debugPrint('null screenshot'); } diff --git a/lib/pages/video/view.dart b/lib/pages/video/view.dart index f8e1ae209..be08256ea 100644 --- a/lib/pages/video/view.dart +++ b/lib/pages/video/view.dart @@ -44,7 +44,7 @@ import 'package:PiliPlus/plugin/pl_player/models/fullscreen_mode.dart'; import 'package:PiliPlus/plugin/pl_player/models/play_repeat.dart'; import 'package:PiliPlus/plugin/pl_player/models/play_status.dart'; import 'package:PiliPlus/plugin/pl_player/utils/fullscreen.dart'; -import 'package:PiliPlus/plugin/pl_player/view.dart'; +import 'package:PiliPlus/plugin/pl_player/view/view.dart'; import 'package:PiliPlus/services/service_locator.dart'; import 'package:PiliPlus/services/shutdown_timer_service.dart' show shutdownTimerService; diff --git a/lib/pages/video/widgets/header_control.dart b/lib/pages/video/widgets/header_control.dart index ae6c5d7e3..dcfa49d41 100644 --- a/lib/pages/video/widgets/header_control.dart +++ b/lib/pages/video/widgets/header_control.dart @@ -33,6 +33,7 @@ import 'package:PiliPlus/pages/video/introduction/ugc/widgets/action_item.dart'; import 'package:PiliPlus/pages/video/introduction/ugc/widgets/menu_row.dart'; import 'package:PiliPlus/pages/video/widgets/header_mixin.dart'; import 'package:PiliPlus/plugin/pl_player/controller.dart'; +import 'package:PiliPlus/plugin/pl_player/models/data_source.dart'; import 'package:PiliPlus/plugin/pl_player/models/play_repeat.dart'; import 'package:PiliPlus/plugin/pl_player/utils/fullscreen.dart'; import 'package:PiliPlus/services/service_locator.dart'; @@ -542,7 +543,9 @@ class HeaderControlState extends State ); }, ), - if ((isFileSource && plPlayerController.mediaType != 1) || + if ((isFileSource && + !(plPlayerController.dataSource as FileSource) + .isMp4) || (!isFileSource && videoDetailCtr.audioUrl?.isNotEmpty == true)) Obx( @@ -751,19 +754,16 @@ class HeaderControlState extends State ); } - static Future showPlayerInfo( + static void showPlayerInfo( BuildContext context, { required PlPlayerController plPlayerController, - }) async { + }) { final player = plPlayerController.videoPlayerController; if (player == null) { SmartDialog.showToast('播放器未初始化'); return; } - final hwdec = await player.platform!.getProperty( - 'hwdec-current', - ); - if (!context.mounted) return; + final hwdec = player.getProperty('hwdec-current'); showDialog( context: context, builder: (context) { @@ -855,16 +855,6 @@ class HeaderControlState extends State subtitle: Text(state.rate.toString()), onTap: () => Utils.copyText('rate\n${state.rate}'), ), - ListTile( - dense: true, - title: const Text("AudioBitrate"), - subtitle: Text( - state.audioBitrate.toString(), - ), - onTap: () => Utils.copyText( - 'AudioBitrate\n${state.audioBitrate}', - ), - ), ListTile( dense: true, title: const Text("Volume"), diff --git a/lib/pages/video/widgets/player_focus.dart b/lib/pages/video/widgets/player_focus.dart index a9609775b..34b0493d9 100644 --- a/lib/pages/video/widgets/player_focus.dart +++ b/lib/pages/video/widgets/player_focus.dart @@ -73,17 +73,15 @@ class PlayerFocus extends StatelessWidget { void _updateVolume(KeyEvent event, {required bool isIncrease}) { if (event is KeyDownEvent) { if (hasPlayer) { + _setVolume(isIncrease: isIncrease); plPlayerController - ..cancelLongPressTimer() - ..longPressTimer ??= Timer.periodic( + ..longPressTimer?.cancel() + ..longPressTimer = Timer.periodic( const Duration(milliseconds: 150), (_) => _setVolume(isIncrease: isIncrease), ); } } else if (event is KeyUpEvent) { - if (plPlayerController.longPressTimer?.tick == 0 && hasPlayer) { - _setVolume(isIncrease: isIncrease); - } plPlayerController.cancelLongPressTimer(); } } @@ -122,8 +120,8 @@ class PlayerFocus extends StatelessWidget { if (event is KeyDownEvent) { if (hasPlayer && !plPlayerController.longPressStatus.value) { plPlayerController - ..cancelLongPressTimer() - ..longPressTimer ??= Timer( + ..longPressTimer?.cancel() + ..longPressTimer = Timer( const Duration(milliseconds: 200), () => plPlayerController ..cancelLongPressTimer() diff --git a/lib/pages/webview/view.dart b/lib/pages/webview/view.dart index 45e39a05d..2e64eaa7b 100644 --- a/lib/pages/webview/view.dart +++ b/lib/pages/webview/view.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:PiliPlus/http/ua_type.dart'; +import 'package:PiliPlus/http/browser_ua.dart'; import 'package:PiliPlus/main.dart'; import 'package:PiliPlus/models/common/webview_menu_type.dart'; import 'package:PiliPlus/utils/app_scheme.dart'; @@ -15,14 +15,20 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; class WebviewPage extends StatefulWidget { - const WebviewPage({super.key, this.url, this.oid, this.title, this.uaType}); + const WebviewPage({ + super.key, + this.url, + this.oid, + this.title, + this.userAgent, + }); final String? url; // note final int? oid; final String? title; - final UaType? uaType; + final String? userAgent; @override State createState() => _WebviewPageState(); @@ -30,7 +36,7 @@ class WebviewPage extends StatefulWidget { class _WebviewPageState extends State { late final String _url = widget.url ?? Get.parameters['url'] ?? ''; - late final UaType uaType; + late final String userAgent; final RxString title = ''.obs; final RxDouble progress = 1.0.obs; bool _inApp = false; @@ -46,10 +52,13 @@ class _WebviewPageState extends State { @override void initState() { super.initState(); - late final uaType = Get.parameters['uaType']; - this.uaType = - widget.uaType ?? - (uaType != null ? UaType.values.byName(uaType) : UaType.platformUA); + userAgent = + widget.userAgent ?? + switch (Get.parameters['uaType']) { + 'pc' => BrowserUa.pc, + 'mob' => BrowserUa.mob, + _ => BrowserUa.platform, + }; if (Get.arguments case final Map map) { _inApp = map['inApp'] ?? false; _off = map['off'] ?? false; @@ -169,7 +178,7 @@ class _WebviewPageState extends State { useHybridComposition: false, algorithmicDarkeningAllowed: true, useShouldOverrideUrlLoading: true, - userAgent: uaType.ua, + userAgent: userAgent, mixedContentMode: MixedContentMode.MIXED_CONTENT_ALWAYS_ALLOW, ), initialUrlRequest: URLRequest( diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index 1f4a5494e..5ed07e272 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -1,13 +1,14 @@ import 'dart:async' show StreamSubscription, Timer; import 'dart:convert' show ascii; -import 'dart:io' show Platform, File, Directory; +import 'dart:io' show Platform; import 'dart:math' show max, min; import 'dart:ui' as ui; import 'package:PiliPlus/common/constants.dart'; +import 'package:PiliPlus/http/browser_ua.dart'; +import 'package:PiliPlus/http/constants.dart'; import 'package:PiliPlus/http/init.dart'; import 'package:PiliPlus/http/loading_state.dart'; -import 'package:PiliPlus/http/ua_type.dart'; import 'package:PiliPlus/http/video.dart'; import 'package:PiliPlus/models/common/account_type.dart'; import 'package:PiliPlus/models/common/audio_normalization.dart'; @@ -31,6 +32,7 @@ import 'package:PiliPlus/plugin/pl_player/models/video_fit_type.dart'; import 'package:PiliPlus/plugin/pl_player/utils/fullscreen.dart'; import 'package:PiliPlus/services/service_locator.dart'; import 'package:PiliPlus/utils/accounts.dart'; +import 'package:PiliPlus/utils/asset_utils.dart'; import 'package:PiliPlus/utils/extension/box_ext.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/extension/string_ext.dart'; @@ -50,8 +52,7 @@ import 'package:easy_debounce/easy_throttle.dart'; import 'package:floating/floating.dart'; import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart' - show rootBundle, HapticFeedback, Uint8List; +import 'package:flutter/services.dart' show HapticFeedback; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_volume_controller/flutter_volume_controller.dart'; import 'package:get/get.dart'; @@ -156,8 +157,6 @@ class PlPlayerController with BlockConfigMixin { /// final RxBool isSliderMoving = false.obs; - /// 是否循环 - PlaylistMode _looping = PlaylistMode.none; bool _autoPlay = false; // 记录历史记录 @@ -177,7 +176,7 @@ class PlPlayerController with BlockConfigMixin { late DataSource dataSource; Timer? _timer; - Timer? _timerForSeek; + StreamSubscription? _subForSeek; Box setting = GStorage.setting; @@ -255,10 +254,16 @@ class PlPlayerController with BlockConfigMixin { windowManager.setTitleBarStyle(TitleBarStyle.hidden); } - late final Size size; - final state = videoController!.player.state; - final width = state.width ?? this.width ?? 16; - final height = state.height ?? this.height ?? 9; + final Size size; + final state = videoPlayerController!.state; + int width = state.width; + int height = state.height; + if (width == 0) { + width = this.width ?? 16; + } + if (height == 0) { + height = this.height ?? 9; + } if (height > width) { size = Size(280.0, 280.0 * height / width); } else { @@ -299,13 +304,13 @@ class PlPlayerController with BlockConfigMixin { } void enterPip({bool isAuto = false}) { - if (videoController != null) { + if (videoPlayerController != null) { controls = false; - final state = videoController!.player.state; + final state = videoPlayerController!.state; PageUtils.enterPip( isAuto: isAuto, - width: state.width ?? width, - height: state.height ?? height, + width: state.width == 0 ? width : state.width, + height: state.height == 0 ? height : state.height, ); } } @@ -387,9 +392,7 @@ class PlPlayerController with BlockConfigMixin { late int? cacheVideoQa = PlatformUtils.isMobile ? null : Pref.defaultVideoQa; late int cacheAudioQa = Pref.defaultAudioQa; bool enableHeart = true; - - late final bool enableHA = Pref.enableHA; - late final String hwdec = Pref.hardwareDecoding; + late final String? hwdec = Pref.enableHA ? Pref.hardwareDecoding : null; late final progressType = Pref.btmProgressBehavior; late final enableQuickDouble = Pref.enableQuickDouble; @@ -517,8 +520,8 @@ class PlPlayerController with BlockConfigMixin { return _instance?.volume.value; } - static Future setVolumeIfExists(double volumeNew) async { - await _instance?.setVolume(volumeNew); + static Future? setVolumeIfExists(double volumeNew) { + return _instance?.setVolume(volumeNew); } Box video = GStorage.video; @@ -549,29 +552,22 @@ class PlPlayerController with BlockConfigMixin { // 获取实例 传参 static PlPlayerController getInstance({bool isLive = false}) { // 如果实例尚未创建,则创建一个新实例 - _instance ??= PlPlayerController._(); - _instance! + return (_instance ??= PlPlayerController._()) ..isLive = isLive .._playerCount += 1; - return _instance!; } bool _processing = false; bool get processing => _processing; // offline - bool isFileSource = false; - String? dirPath; - String? typeTag; - int? mediaType; + bool get isFileSource => dataSource is FileSource; // 初始化资源 Future setDataSource( DataSource dataSource, { bool isLive = false, bool autoplay = true, - // 默认不循环 - PlaylistMode looping = PlaylistMode.none, // 初始化播放位置 Duration? seekTo, // 初始化播放速度 @@ -591,15 +587,8 @@ class PlPlayerController with BlockConfigMixin { VideoType? videoType, VoidCallback? onInit, Volume? volume, - String? dirPath, - String? typeTag, - int? mediaType, }) async { try { - this.dirPath = dirPath; - this.typeTag = typeTag; - this.mediaType = mediaType; - isFileSource = dataSource.type == DataSourceType.file; _processing = true; this.isLive = isLive; _videoType = videoType ?? VideoType.ugc; @@ -607,7 +596,6 @@ class PlPlayerController with BlockConfigMixin { this.height = height; this.dataSource = dataSource; _autoPlay = autoplay; - _looping = looping; // 初始化视频倍速 // _playbackSpeed.value = speed; // 初始化数据加载状态 @@ -634,12 +622,15 @@ class PlPlayerController with BlockConfigMixin { return; } // 配置Player 音轨、字幕等等 - _videoPlayerController = await _createVideoController( - dataSource, - _looping, - seekTo, - volume, - ); + await _createVideoController(dataSource, seekTo, volume); + + if (_playerCount == 0) { + _videoPlayerController?.dispose(); + _videoPlayerController = null; + _videoController = null; + return; + } + // 获取视频时长 00:00 this.duration.value = duration ?? _videoPlayerController!.state.duration; position = buffered.value = sliderPosition = seekTo ?? Duration.zero; @@ -670,32 +661,11 @@ class PlPlayerController with BlockConfigMixin { return shadersDirPath!; } - final dir = Directory(path.join(appSupportDirPath, 'anime_shaders')); - if (!dir.existsSync()) { - await dir.create(recursive: true); - } - - final shaderFilesPath = - (Constants.mpvAnime4KShaders + Constants.mpvAnime4KShadersLite) - .map((e) => 'assets/shaders/$e') - .toList(); - - for (final filePath in shaderFilesPath) { - final fileName = filePath.split('/').last; - final targetFile = File(path.join(dir.path, fileName)); - if (targetFile.existsSync()) { - continue; - } - - try { - final data = await rootBundle.load(filePath); - final List bytes = data.buffer.asUint8List(); - await targetFile.writeAsBytes(bytes); - } catch (e) { - if (kDebugMode) debugPrint('$e'); - } - } - return shadersDirPath = dir.path; + return shadersDirPath = await AssetUtils.getOrCopy( + 'assets/shaders', + Constants.mpvAnime4KShaders.followedBy(Constants.mpvAnime4KShadersLite), + path.join(appSupportDirPath, 'anime_shaders'), + ); } late final isAnim = _pgcType == 1 || _pgcType == 4; @@ -710,12 +680,10 @@ class PlPlayerController with BlockConfigMixin { setting.put(SettingBoxKey.superResolutionType, type.index); } } - pp ??= _videoPlayerController!.platform!; - await pp.waitForPlayerInitialization; - await pp.waitForVideoControllerInitializationIfAttached; + pp ??= _videoPlayerController!; switch (type) { case SuperResolutionType.disable: - return pp.command(['change-list', 'glsl-shaders', 'clr', '']); + return pp.command(const ['change-list', 'glsl-shaders', 'clr', '']); case SuperResolutionType.efficiency: return pp.command([ 'change-list', @@ -741,10 +709,54 @@ class PlPlayerController with BlockConfigMixin { static final loudnormRegExp = RegExp('loudnorm=([^,]+)'); + Future _initPlayer() async { + assert(_videoPlayerController == null); + final opt = { + 'video-sync': Pref.videoSync, + }; + if (Platform.isAndroid) { + opt['volume-max'] = '100'; + opt['ao'] = Pref.audioOutput; + } else if (PlatformUtils.isDesktop) { + opt['volume'] = (volume.value * 100).toString(); + } + final autosync = Pref.autosync; + if (autosync != '0') { + opt['autosync'] = autosync; + } + + final player = await Player.create( + configuration: PlayerConfiguration( + bufferSize: Pref.expandBuffer + ? (isLive ? 64 * 1024 * 1024 : 32 * 1024 * 1024) + : (isLive ? 16 * 1024 * 1024 : 4 * 1024 * 1024), + logLevel: kDebugMode ? .warn : .error, + options: opt, + ), + ); + + assert(_videoController == null); + + _videoController = await VideoController.create( + player, + configuration: VideoControllerConfiguration( + enableHardwareAcceleration: hwdec != null, + androidAttachSurfaceAfterVideoParameters: false, + hwdec: hwdec, + ), + ); + + player.setMediaHeader( + userAgent: BrowserUa.pc, + referer: HttpString.baseUrl, + ); + // await player.setAudioTrack(.auto()); + return player; + } + // 配置播放器 - Future _createVideoController( + Future _createVideoController( DataSource dataSource, - PlaylistMode looping, Duration? seekTo, Volume? volume, ) async { @@ -757,81 +769,34 @@ class PlPlayerController with BlockConfigMixin { // 初始化时清空弹幕,防止上次重叠 danmakuController?.clear(); - Player player = - _videoPlayerController ?? - Player( - configuration: PlayerConfiguration( - // 默认缓冲 4M 大小 - bufferSize: Pref.expandBuffer - ? (isLive ? 64 * 1024 * 1024 : 32 * 1024 * 1024) - : (isLive ? 16 * 1024 * 1024 : 4 * 1024 * 1024), - logLevel: kDebugMode ? MPVLogLevel.warn : MPVLogLevel.error, - ), - ); - final pp = player.platform!; - if (_videoPlayerController == null) { - if (PlatformUtils.isDesktop) { - pp.setVolume(this.volume.value * 100); + var player = _videoPlayerController; + + if (player == null) { + player = await _initPlayer(); + if (_playerCount == 0) { + player.dispose(); + player = null; + _videoController = null; + return; } - if (isAnim) { - setShader(superResolutionType.value, pp); - } - // await pp.setProperty('audio-pitch-correction', 'yes'); // default yes - if (Platform.isAndroid) { - await pp.setProperty("volume-max", "100"); - await pp.setProperty("ao", Pref.audioOutput); - } - // video-sync=display-resample - await pp.setProperty("video-sync", Pref.videoSync); - final autosync = Pref.autosync; - if (autosync != '0') { - await pp.setProperty("autosync", autosync); - } - // vo=gpu-next & gpu-context=android & gpu-api=opengl - // await pp.setProperty("vo", "gpu-next"); - // await pp.setProperty("gpu-context", "android"); - // await pp.setProperty("gpu-api", "opengl"); - await player.setAudioTrack(AudioTrack.auto()); - if (Pref.enableSystemProxy) { - final systemProxyHost = Pref.systemProxyHost; - final systemProxyPort = int.tryParse(Pref.systemProxyPort); - if (systemProxyPort != null && systemProxyHost.isNotEmpty) { - await pp.setProperty( - "http-proxy", - 'http://$systemProxyHost:$systemProxyPort', - ); - } + _videoPlayerController = player; + } + + if (isAnim) await setShader(); + + final Map extras = {}; + + String video = dataSource.videoSource; + if (dataSource.audioSource case final audio? when (audio.isNotEmpty)) { + if (onlyPlayAudio.value) { + video = audio; + } else { + extras['audio-files'] = + '"${Platform.isWindows ? audio.replaceAll(';', r'\;') : audio.replaceAll(':', r'\:')}"'; } } - // 音轨 - final String audioUri; - if (isFileSource) { - audioUri = onlyPlayAudio.value || mediaType == 1 - ? '' - : path.join(dirPath!, typeTag!, PathUtils.audioNameType2); - } else if (dataSource.audioSource?.isNotEmpty == true) { - audioUri = Platform.isWindows - ? dataSource.audioSource!.replaceAll(';', r'\;') - : dataSource.audioSource!.replaceAll(':', r'\:'); - } else { - audioUri = ''; - } - await pp.setProperty('audio-files', audioUri); - - _videoController ??= VideoController( - player, - configuration: VideoControllerConfiguration( - enableHardwareAcceleration: enableHA, - androidAttachSurfaceAfterVideoParameters: false, - hwdec: enableHA ? hwdec : null, - ), - ); - - player.setPlaylistMode(looping); - - final Map? filters; - if (Platform.isAndroid) { + if (kDebugMode || Platform.isAndroid) { String audioNormalization = AudioNormalization.getParamFromConfig( Pref.audioNormalization, ); @@ -854,44 +819,23 @@ class PlPlayerController with BlockConfigMixin { AudioNormalization.getParamFromConfig(Pref.fallbackNormalization), ); } - filters = audioNormalization.isEmpty - ? null - : {'lavfi-complex': '"[aid1] $audioNormalization [ao]"'}; - } else { - filters = null; + if (audioNormalization.isNotEmpty) { + extras['lavfi-complex'] = '"[aid1] $audioNormalization [ao]"'; + } } - // if (kDebugMode) debugPrint(filters.toString()); - - late final String videoUri; - if (isFileSource) { - videoUri = path.join( - dirPath!, - typeTag!, - mediaType == 1 - ? PathUtils.videoNameType1 - : onlyPlayAudio.value - ? PathUtils.audioNameType2 - : PathUtils.videoNameType2, - ); - } else { - videoUri = dataSource.videoSource!; - } await player.open( Media( - videoUri, - httpHeaders: dataSource.httpHeaders, + video, start: seekTo, - extras: filters, + extras: extras.isEmpty ? null : extras, ), play: false, ); - - return player; } Future refreshPlayer() async { - if (isFileSource) { + if (dataSource is FileSource) { return true; } if (_videoPlayerController == null) { @@ -902,23 +846,21 @@ class PlPlayerController with BlockConfigMixin { SmartDialog.showToast('视频源为空,请重新进入本页面'); return false; } + String? audioUri; if (!isLive) { if (dataSource.audioSource.isNullOrEmpty) { SmartDialog.showToast('音频源为空'); } else { - await (_videoPlayerController!.platform!).setProperty( - 'audio-files', - Platform.isWindows - ? dataSource.audioSource!.replaceAll(';', '\\;') - : dataSource.audioSource!.replaceAll(':', '\\:'), - ); + audioUri = Platform.isWindows + ? dataSource.audioSource!.replaceAll(';', '\\;') + : dataSource.audioSource!.replaceAll(':', '\\:'); } } await _videoPlayerController!.open( Media( - dataSource.videoSource!, - httpHeaders: dataSource.httpHeaders, + dataSource.videoSource, start: position, + extras: audioUri == null ? null : {'audio-files': '"$audioUri"'}, ), play: true, ); @@ -974,14 +916,14 @@ class PlPlayerController with BlockConfigMixin { return null; } - Set subscriptions = {}; + List subscriptions = []; final Set _positionListeners = {}; final Set _statusListeners = {}; /// 播放事件监听 void startListeners() { final controllerStream = videoPlayerController!.stream; - subscriptions = { + subscriptions = [ controllerStream.playing.listen((event) { WakelockPlus.toggle(enable: event); if (event) { @@ -1062,7 +1004,8 @@ class PlPlayerController with BlockConfigMixin { } })), controllerStream.error.listen((String event) { - if (isFileSource && event.startsWith("Failed to open file")) { + if (dataSource is FileSource && + event.startsWith("Failed to open file")) { return; } if (isLive) { @@ -1131,7 +1074,7 @@ class PlPlayerController with BlockConfigMixin { videoPlayerServiceHandler!.onPositionChange(Duration(seconds: event)); }), ], - }; + ]; } /// 移除事件监听 @@ -1139,6 +1082,13 @@ class PlPlayerController with BlockConfigMixin { return Future.wait(subscriptions.map((e) => e.cancel())); } + void _cancelSubForSeek() { + if (_subForSeek != null) { + _subForSeek!.cancel(); + _subForSeek = null; + } + } + /// 跳转至指定位置 Future seekTo(Duration position, {bool isSeek = true}) async { // if (position >= duration.value) { @@ -1153,7 +1103,8 @@ class PlPlayerController with BlockConfigMixin { this.position = position; updatePositionSecond(); _heartDuration = position.inSeconds; - if (duration.value.inSeconds != 0) { + + Future seek() async { if (isSeek) { /// 拖动进度条调节时,不等待第一帧,防止抖动 await _videoPlayerController?.stream.buffer.first; @@ -1164,33 +1115,16 @@ class PlPlayerController with BlockConfigMixin { } catch (e) { if (kDebugMode) debugPrint('seek failed: $e'); } - // if (playerStatus.stopped) { - // play(); - // } + } + + if (duration.value != Duration.zero) { + seek(); } else { // if (kDebugMode) debugPrint('seek duration else'); - _timerForSeek?.cancel(); - _timerForSeek = Timer.periodic(const Duration(milliseconds: 200), ( - Timer t, - ) async { - //_timerForSeek = null; - if (_playerCount == 0) { - _timerForSeek?.cancel(); - _timerForSeek = null; - } else if (duration.value != Duration.zero) { - try { - await _videoPlayerController?.stream.buffer.first; - danmakuController?.clear(); - await _videoPlayerController?.seek(position); - } catch (e) { - if (kDebugMode) debugPrint('seek failed: $e'); - } - // if (playerStatus.isPaused) { - // play(); - // } - t.cancel(); - _timerForSeek = null; - } + _subForSeek?.cancel(); + _subForSeek = duration.listen((_) { + seek(); + _cancelSubForSeek(); }); } } @@ -1304,7 +1238,7 @@ class PlPlayerController with BlockConfigMixin { final RxBool volumeIndicator = false.obs; Timer? volumeTimer; - final RxBool volumeInterceptEventStream = false.obs; + bool volumeInterceptEventStream = false; static final double maxVolume = PlatformUtils.isDesktop ? 2.0 : 1.0; Future setVolume(double volume) async { @@ -1322,11 +1256,11 @@ class PlPlayerController with BlockConfigMixin { } } volumeIndicator.value = true; - volumeInterceptEventStream.value = true; + volumeInterceptEventStream = true; volumeTimer?.cancel(); volumeTimer = Timer(const Duration(milliseconds: 200), () { volumeIndicator.value = false; - volumeInterceptEventStream.value = false; + volumeInterceptEventStream = false; if (PlatformUtils.isDesktop) { setting.put(SettingBoxKey.desktopVolume, volume.toPrecision(3)); } @@ -1341,7 +1275,7 @@ class PlPlayerController with BlockConfigMixin { /// 读取fit int fitValue = Pref.cacheVideoFit; - Future getVideoFit() async { + void getVideoFit() { var attr = VideoFitType.values[fitValue]; // 由于none与scaleDown涉及视频原始尺寸,需要等待视频加载后再设置,否则尺寸会变为0,出现错误; if (attr == VideoFitType.none || attr == VideoFitType.scaleDown) { @@ -1570,14 +1504,6 @@ class PlPlayerController with BlockConfigMixin { void removeStatusLister(Function(PlayerStatus status) listener) => _statusListeners.remove(listener); - /// 截屏 - Future screenshot() async { - final Uint8List? screenshot = await _videoPlayerController!.screenshot( - format: 'image/png', - ); - return screenshot; - } - // 记录播放记录 Future? makeHeartBeat( int progress, { @@ -1657,9 +1583,10 @@ class PlPlayerController with BlockConfigMixin { } bool isCloseAll = false; - Future dispose() async { + void dispose() { // 每次减1,最后销毁 cancelLongPressTimer(); + _cancelSubForSeek(); if (!isCloseAll && _playerCount > 1) { _playerCount -= 1; _heartDuration = 0; @@ -1681,7 +1608,6 @@ class PlPlayerController with BlockConfigMixin { } Utils.channel.setMethodCallHandler(null); _timer?.cancel(); - _timerForSeek?.cancel(); // _position.close(); // _playerEventSubs?.cancel(); // _sliderPosition.close(); @@ -1699,7 +1625,7 @@ class PlPlayerController with BlockConfigMixin { windowManager.setAlwaysOnTop(false); } - await removeListeners(); + removeListeners(); subscriptions.clear(); _positionListeners.clear(); _statusListeners.clear(); @@ -1781,7 +1707,7 @@ class PlPlayerController with BlockConfigMixin { }, options: Options( headers: { - 'user-agent': UaType.pc.ua, + 'user-agent': BrowserUa.pc, 'referer': 'https://www.bilibili.com/video/$bvid', }, ), @@ -1802,7 +1728,7 @@ class PlPlayerController with BlockConfigMixin { void takeScreenshot() { SmartDialog.showToast('截图中'); - videoPlayerController?.screenshot(format: 'image/png').then((value) { + videoPlayerController?.screenshot(format: .png).then((value) { if (value != null) { SmartDialog.showToast('点击弹窗保存截图'); showDialog( diff --git a/lib/plugin/pl_player/models/data_source.dart b/lib/plugin/pl_player/models/data_source.dart index a453b1b38..4a1671a1d 100644 --- a/lib/plugin/pl_player/models/data_source.dart +++ b/lib/plugin/pl_player/models/data_source.dart @@ -1,39 +1,39 @@ -/// The way in which the video was originally loaded. -/// -/// This has nothing to do with the video's file type. It's just the place -/// from which the video is fetched from. -enum DataSourceType { - /// The video was downloaded from the internet. - network, +import 'package:PiliPlus/utils/path_utils.dart'; +import 'package:path/path.dart' as path; - /// The video was loaded off of the local filesystem. - file, -} - -class DataSource { - String? videoSource; - String? audioSource; - DataSourceType type; - Map? httpHeaders; // for headers +sealed class DataSource { + final String videoSource; + final String? audioSource; DataSource({ - this.videoSource, - this.audioSource, - required this.type, - this.httpHeaders, + required this.videoSource, + required this.audioSource, }); - - DataSource copyWith({ - String? videoSource, - String? audioSource, - DataSourceType? type, - Map? httpHeaders, - }) { - return DataSource( - videoSource: videoSource ?? this.videoSource, - audioSource: audioSource ?? this.audioSource, - type: type ?? this.type, - httpHeaders: httpHeaders ?? this.httpHeaders, - ); - } +} + +class NetworkSource extends DataSource { + NetworkSource({ + required super.videoSource, + required super.audioSource, + }); +} + +class FileSource extends DataSource { + final String dir; + final bool isMp4; + + FileSource({ + required this.dir, + required this.isMp4, + required String typeTag, + }) : super( + videoSource: path.join( + dir, + typeTag, + isMp4 ? PathUtils.videoNameType1 : PathUtils.videoNameType2, + ), + audioSource: isMp4 + ? null + : path.join(dir, typeTag, PathUtils.audioNameType2), + ); } diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view/view.dart similarity index 86% rename from lib/plugin/pl_player/view.dart rename to lib/plugin/pl_player/view/view.dart index 3085bf50b..fd3ff8dc2 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view/view.dart @@ -80,6 +80,8 @@ import 'package:media_kit_video/media_kit_video.dart'; import 'package:screen_brightness_platform_interface/screen_brightness_platform_interface.dart'; import 'package:window_manager/window_manager.dart'; +part 'widgets.dart'; + class PLVideoPlayer extends StatefulWidget { const PLVideoPlayer({ required this.maxWidth, @@ -226,8 +228,7 @@ class _PLVideoPlayerState extends State plPlayerController.volume.value = (await FlutterVolumeController.getVolume())!; FlutterVolumeController.addListener((double value) { - if (mounted && - !plPlayerController.volumeInterceptEventStream.value) { + if (mounted && !plPlayerController.volumeInterceptEventStream) { plPlayerController.volume.value = value; if (Platform.isIOS && !FlutterVolumeController.showSystemUI) { plPlayerController @@ -240,7 +241,7 @@ class _PLVideoPlayerState extends State }); } } - }); + }, emitOnStart: false); } catch (_) {} }); @@ -293,7 +294,7 @@ class _PLVideoPlayerState extends State @override void didChangeAppLifecycleState(AppLifecycleState state) { if (!plPlayerController.continuePlayInBackground.value) { - late final player = plPlayerController.videoController?.player; + late final player = plPlayerController.videoPlayerController; if (const [ AppLifecycleState.paused, AppLifecycleState.detached, @@ -2436,505 +2437,3 @@ class _PLVideoPlayerState extends State ); } } - -Widget buildDmChart( - Color color, - List dmTrend, - VideoDetailController videoDetailController, [ - double offset = 0, -]) { - return IgnorePointer( - child: Container( - height: 12, - margin: EdgeInsets.only( - bottom: - videoDetailController.viewPointList.isNotEmpty && - videoDetailController.showVP.value - ? 19.25 + offset - : 4.25 + offset, - ), - child: LineChart( - LineChartData( - titlesData: const FlTitlesData(show: false), - lineTouchData: const LineTouchData(enabled: false), - gridData: const FlGridData(show: false), - borderData: FlBorderData(show: false), - minX: 0, - maxX: (dmTrend.length - 1).toDouble(), - minY: 0, - maxY: dmTrend.max, - lineBarsData: [ - LineChartBarData( - spots: List.generate( - dmTrend.length, - (index) => FlSpot( - index.toDouble(), - dmTrend[index], - ), - ), - isCurved: true, - barWidth: 1, - color: color, - dotData: const FlDotData(show: false), - belowBarData: BarAreaData( - show: true, - color: color.withValues(alpha: 0.4), - ), - ), - ], - ), - ), - ), - ); -} - -Widget buildSeekPreviewWidget( - PlPlayerController plPlayerController, - double maxWidth, - double maxHeight, - ValueGetter isMounted, -) { - return Obx( - () { - if (!plPlayerController.showPreview.value) { - return const SizedBox.shrink(); - } - - try { - final data = plPlayerController.videoShot!.data; - - final double scale = - plPlayerController.isFullScreen.value && - (PlatformUtils.isDesktop || !plPlayerController.isVertical) - ? 4 - : 3; - double height = 27 * scale; - final compatHeight = maxHeight - 140; - if (compatHeight > 50) { - height = math.min(height, compatHeight); - } - - final int imgXLen = data.imgXLen; - final int imgYLen = data.imgYLen; - final int totalPerImage = data.totalPerImage; - double imgXSize = data.imgXSize; - double imgYSize = data.imgYSize; - - return Align( - alignment: Alignment.center, - child: Obx( - () { - final index = plPlayerController.previewIndex.value!; - int pageIndex = (index ~/ totalPerImage).clamp( - 0, - data.image.length - 1, - ); - int align = index % totalPerImage; - int x = align % imgXLen; - int y = align ~/ imgYLen; - final url = data.image[pageIndex]; - - return ClipRRect( - borderRadius: StyleString.mdRadius, - child: VideoShotImage( - url: url, - x: x, - y: y, - imgXSize: imgXSize, - imgYSize: imgYSize, - height: height, - imageCache: plPlayerController.previewCache, - onSetSize: (xSize, ySize) => data - ..imgXSize = imgXSize = xSize - ..imgYSize = imgYSize = ySize, - isMounted: isMounted, - ), - ); - }, - ), - ); - } catch (e) { - if (kDebugMode) rethrow; - return const SizedBox.shrink(); - } - }, - ); -} - -class VideoShotImage extends StatefulWidget { - const VideoShotImage({ - super.key, - required this.imageCache, - required this.url, - required this.x, - required this.y, - required this.imgXSize, - required this.imgYSize, - required this.height, - required this.onSetSize, - required this.isMounted, - }); - - final Map imageCache; - final String url; - final int x; - final int y; - final double imgXSize; - final double imgYSize; - final double height; - final Function(double imgXSize, double imgYSize) onSetSize; - final ValueGetter isMounted; - - @override - State createState() => _VideoShotImageState(); -} - -Future _getImg(String url) async { - final cacheManager = DefaultCacheManager(); - final cacheKey = Utils.getFileName(url, fileExt: false); - try { - final fileInfo = await cacheManager.getSingleFile( - ImageUtils.safeThumbnailUrl(url), - key: cacheKey, - headers: Constants.baseHeaders, - ); - return _loadImg(fileInfo.path); - } catch (_) { - return null; - } -} - -Future _loadImg(String path) async { - final codec = await ui.instantiateImageCodecFromBuffer( - await ImmutableBuffer.fromFilePath(path), - ); - final frame = await codec.getNextFrame(); - codec.dispose(); - return frame.image; -} - -class _VideoShotImageState extends State { - late Size _size; - late Rect _srcRect; - late Rect _dstRect; - late RRect _rrect; - ui.Image? _image; - - @override - void initState() { - super.initState(); - _initSize(); - _loadImg(); - } - - void _initSizeIfNeeded() { - if (_size.width.isNaN) { - _initSize(); - } - } - - void _initSize() { - if (widget.imgXSize == 0) { - if (_image != null) { - final imgXSize = _image!.width / 10; - final imgYSize = _image!.height / 10; - final height = widget.height; - final width = height * imgXSize / imgYSize; - _setRect(width, height); - _setSrcRect(imgXSize, imgYSize); - widget.onSetSize(imgXSize, imgYSize); - } else { - _setRect(double.nan, double.nan); - _setSrcRect(widget.imgXSize, widget.imgYSize); - } - } else { - final height = widget.height; - final width = height * widget.imgXSize / widget.imgYSize; - _setRect(width, height); - _setSrcRect(widget.imgXSize, widget.imgYSize); - } - } - - void _setRect(double width, double height) { - _size = Size(width, height); - _dstRect = Rect.fromLTRB(0, 0, width, height); - _rrect = RRect.fromRectAndRadius(_dstRect, const Radius.circular(10)); - } - - void _setSrcRect(double imgXSize, double imgYSize) { - _srcRect = Rect.fromLTWH( - widget.x * imgXSize, - widget.y * imgYSize, - imgXSize, - imgYSize, - ); - } - - void _loadImg() { - final url = widget.url; - _image = widget.imageCache[url]; - if (_image != null) { - _initSizeIfNeeded(); - } else if (!widget.imageCache.containsKey(url)) { - widget.imageCache[url] = null; - _getImg(url).then((image) { - if (image != null) { - if (widget.isMounted()) { - widget.imageCache[url] = image; - } - if (mounted) { - _image = image; - _initSizeIfNeeded(); - setState(() {}); - } - } else { - widget.imageCache.remove(url); - } - }); - } - } - - @override - void didUpdateWidget(VideoShotImage oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.url != widget.url) { - _loadImg(); - } - if (oldWidget.x != widget.x || oldWidget.y != widget.y) { - _setSrcRect(widget.imgXSize, widget.imgYSize); - } - } - - late final _imgPaint = Paint()..filterQuality = FilterQuality.medium; - late final _borderPaint = Paint() - ..color = Colors.white - ..style = PaintingStyle.stroke - ..strokeWidth = 1.5; - - @override - Widget build(BuildContext context) { - if (_image != null) { - return CroppedImage( - size: _size, - image: _image!, - srcRect: _srcRect, - dstRect: _dstRect, - rrect: _rrect, - imgPaint: _imgPaint, - borderPaint: _borderPaint, - ); - } - return const SizedBox.shrink(); - } -} - -const double _triangleHeight = 5.6; - -class _DanmakuTip extends SingleChildRenderObjectWidget { - const _DanmakuTip({ - this.offset = 0, - super.child, - }); - - final double offset; - - @override - RenderObject createRenderObject(BuildContext context) { - return _RenderDanmakuTip(offset: offset); - } - - @override - void updateRenderObject( - BuildContext context, - _RenderDanmakuTip renderObject, - ) { - renderObject.offset = offset; - } -} - -class _RenderDanmakuTip extends RenderProxyBox { - _RenderDanmakuTip({ - required double offset, - }) : _offset = offset; - - double _offset; - double get offset => _offset; - set offset(double value) { - if (_offset == value) return; - _offset = value; - markNeedsPaint(); - } - - @override - void paint(PaintingContext context, Offset offset) { - final paint = Paint() - ..color = const Color(0xB3000000) - ..style = .fill; - - final radius = size.height / 2; - const triangleBase = _triangleHeight * 2 / 3; - - final triangleCenterX = (size.width / 2 + _offset).clamp( - radius + triangleBase, - size.width - radius - triangleBase, - ); - final path = Path() - // triangle (exceed) - ..moveTo(triangleCenterX - triangleBase, 0) - ..lineTo(triangleCenterX, -_triangleHeight) - ..lineTo(triangleCenterX + triangleBase, 0) - // top - ..lineTo(size.width - radius, 0) - // right - ..arcToPoint( - Offset(size.width - radius, size.height), - radius: Radius.circular(radius), - ) - // bottom - ..lineTo(radius, size.height) - // left - ..arcToPoint( - Offset(radius, 0), - radius: Radius.circular(radius), - ) - ..close(); - - context.canvas - ..save() - ..translate(offset.dx, offset.dy) - ..drawPath(path, paint) - ..drawPath( - path, - paint - ..color = const Color(0x7EFFFFFF) - ..style = PaintingStyle.stroke - ..strokeWidth = 1.25, - ) - ..restore(); - - super.paint(context, offset); - } -} - -class _VideoTime extends LeafRenderObjectWidget { - const _VideoTime({ - required this.position, - required this.duration, - }); - - final String position; - final String duration; - - @override - _RenderVideoTime createRenderObject(BuildContext context) => _RenderVideoTime( - position: position, - duration: duration, - ); - - @override - void updateRenderObject( - BuildContext context, - covariant _RenderVideoTime renderObject, - ) { - renderObject - ..position = position - ..duration = duration; - } -} - -class _RenderVideoTime extends RenderBox { - _RenderVideoTime({ - required String position, - required String duration, - }) : _position = position, - _duration = duration; - - String _duration; - set duration(String value) { - _duration = value; - final paragraph = _buildParagraph(const Color(0xFFD0D0D0), _duration); - if (paragraph.maxIntrinsicWidth != _cache?.maxIntrinsicWidth) { - markNeedsLayout(); - } - _cache?.dispose(); - _cache = paragraph; - markNeedsSemanticsUpdate(); - } - - String _position; - set position(String value) { - _position = value; - markNeedsPaint(); - markNeedsSemanticsUpdate(); - } - - ui.Paragraph? _cache; - - ui.Paragraph _buildParagraph(Color color, String time) { - final builder = - ui.ParagraphBuilder( - ui.ParagraphStyle( - fontSize: 10, - height: 1.4, - fontFamily: 'Monospace', - ), - ) - ..pushStyle( - ui.TextStyle( - color: color, - fontSize: 10, - height: 1.4, - fontFamily: 'Monospace', - fontFeatures: const [FontFeature.tabularFigures()], - ), - ) - ..addText(time); - return builder.build() - ..layout(const ui.ParagraphConstraints(width: .infinity)); - } - - @override - ui.Size computeDryLayout(covariant BoxConstraints constraints) { - final paragraph = _cache ??= _buildParagraph( - const Color(0xFFD0D0D0), - _duration, - ); - return Size(paragraph.maxIntrinsicWidth, paragraph.height * 2); - } - - @override - void describeSemanticsConfiguration(SemanticsConfiguration config) { - super.describeSemanticsConfiguration(config); - config.label = 'position:$_position\nduration:$_duration'; - } - - @override - void performLayout() { - size = computeDryLayout(constraints); - } - - @override - void paint(PaintingContext context, ui.Offset offset) { - final para = _buildParagraph(Colors.white, _position); - context.canvas - ..drawParagraph( - para, - Offset( - offset.dx + _cache!.maxIntrinsicWidth - para.maxIntrinsicWidth, - offset.dy, - ), - ) - ..drawParagraph(_cache!, Offset(offset.dx, offset.dy + para.height)); - para.dispose(); - } - - @override - void dispose() { - _cache?.dispose(); - _cache = null; - super.dispose(); - } - - @override - bool get isRepaintBoundary => true; -} diff --git a/lib/plugin/pl_player/view/widgets.dart b/lib/plugin/pl_player/view/widgets.dart new file mode 100644 index 000000000..38c9c04ff --- /dev/null +++ b/lib/plugin/pl_player/view/widgets.dart @@ -0,0 +1,503 @@ +part of 'view.dart'; + +Widget buildDmChart( + Color color, + List dmTrend, + VideoDetailController videoDetailController, [ + double offset = 0, +]) { + return IgnorePointer( + child: Container( + height: 12, + margin: EdgeInsets.only( + bottom: + videoDetailController.viewPointList.isNotEmpty && + videoDetailController.showVP.value + ? 19.25 + offset + : 4.25 + offset, + ), + child: LineChart( + LineChartData( + titlesData: const FlTitlesData(show: false), + lineTouchData: const LineTouchData(enabled: false), + gridData: const FlGridData(show: false), + borderData: FlBorderData(show: false), + minX: 0, + maxX: (dmTrend.length - 1).toDouble(), + minY: 0, + maxY: dmTrend.max, + lineBarsData: [ + LineChartBarData( + spots: List.generate( + dmTrend.length, + (index) => FlSpot( + index.toDouble(), + dmTrend[index], + ), + ), + isCurved: true, + barWidth: 1, + color: color, + dotData: const FlDotData(show: false), + belowBarData: BarAreaData( + show: true, + color: color.withValues(alpha: 0.4), + ), + ), + ], + ), + ), + ), + ); +} + +Widget buildSeekPreviewWidget( + PlPlayerController plPlayerController, + double maxWidth, + double maxHeight, + ValueGetter isMounted, +) { + return Obx( + () { + if (!plPlayerController.showPreview.value) { + return const SizedBox.shrink(); + } + + try { + final data = plPlayerController.videoShot!.data; + + final double scale = + plPlayerController.isFullScreen.value && + (PlatformUtils.isDesktop || !plPlayerController.isVertical) + ? 4 + : 3; + double height = 27 * scale; + final compatHeight = maxHeight - 140; + if (compatHeight > 50) { + height = math.min(height, compatHeight); + } + + final int imgXLen = data.imgXLen; + final int imgYLen = data.imgYLen; + final int totalPerImage = data.totalPerImage; + double imgXSize = data.imgXSize; + double imgYSize = data.imgYSize; + + return Align( + alignment: Alignment.center, + child: Obx( + () { + final index = plPlayerController.previewIndex.value!; + int pageIndex = (index ~/ totalPerImage).clamp( + 0, + data.image.length - 1, + ); + int align = index % totalPerImage; + int x = align % imgXLen; + int y = align ~/ imgYLen; + final url = data.image[pageIndex]; + + return ClipRRect( + borderRadius: StyleString.mdRadius, + child: VideoShotImage( + url: url, + x: x, + y: y, + imgXSize: imgXSize, + imgYSize: imgYSize, + height: height, + imageCache: plPlayerController.previewCache, + onSetSize: (xSize, ySize) => data + ..imgXSize = imgXSize = xSize + ..imgYSize = imgYSize = ySize, + isMounted: isMounted, + ), + ); + }, + ), + ); + } catch (e) { + if (kDebugMode) rethrow; + return const SizedBox.shrink(); + } + }, + ); +} + +class VideoShotImage extends StatefulWidget { + const VideoShotImage({ + super.key, + required this.imageCache, + required this.url, + required this.x, + required this.y, + required this.imgXSize, + required this.imgYSize, + required this.height, + required this.onSetSize, + required this.isMounted, + }); + + final Map imageCache; + final String url; + final int x; + final int y; + final double imgXSize; + final double imgYSize; + final double height; + final Function(double imgXSize, double imgYSize) onSetSize; + final ValueGetter isMounted; + + @override + State createState() => _VideoShotImageState(); +} + +Future _getImg(String url) async { + final cacheManager = DefaultCacheManager(); + final cacheKey = Utils.getFileName(url, fileExt: false); + try { + final fileInfo = await cacheManager.getSingleFile( + ImageUtils.safeThumbnailUrl(url), + key: cacheKey, + headers: Constants.baseHeaders, + ); + return _loadImg(fileInfo.path); + } catch (_) { + return null; + } +} + +Future _loadImg(String path) async { + final codec = await ui.instantiateImageCodecFromBuffer( + await ImmutableBuffer.fromFilePath(path), + ); + final frame = await codec.getNextFrame(); + codec.dispose(); + return frame.image; +} + +class _VideoShotImageState extends State { + late Size _size; + late Rect _srcRect; + late Rect _dstRect; + late RRect _rrect; + ui.Image? _image; + + @override + void initState() { + super.initState(); + _initSize(); + _loadImg(); + } + + void _initSizeIfNeeded() { + if (_size.width.isNaN) { + _initSize(); + } + } + + void _initSize() { + if (widget.imgXSize == 0) { + if (_image != null) { + final imgXSize = _image!.width / 10; + final imgYSize = _image!.height / 10; + final height = widget.height; + final width = height * imgXSize / imgYSize; + _setRect(width, height); + _setSrcRect(imgXSize, imgYSize); + widget.onSetSize(imgXSize, imgYSize); + } else { + _setRect(double.nan, double.nan); + _setSrcRect(widget.imgXSize, widget.imgYSize); + } + } else { + final height = widget.height; + final width = height * widget.imgXSize / widget.imgYSize; + _setRect(width, height); + _setSrcRect(widget.imgXSize, widget.imgYSize); + } + } + + void _setRect(double width, double height) { + _size = Size(width, height); + _dstRect = Rect.fromLTRB(0, 0, width, height); + _rrect = RRect.fromRectAndRadius(_dstRect, const Radius.circular(10)); + } + + void _setSrcRect(double imgXSize, double imgYSize) { + _srcRect = Rect.fromLTWH( + widget.x * imgXSize, + widget.y * imgYSize, + imgXSize, + imgYSize, + ); + } + + void _loadImg() { + final url = widget.url; + _image = widget.imageCache[url]; + if (_image != null) { + _initSizeIfNeeded(); + } else if (!widget.imageCache.containsKey(url)) { + widget.imageCache[url] = null; + _getImg(url).then((image) { + if (image != null) { + if (widget.isMounted()) { + widget.imageCache[url] = image; + } + if (mounted) { + _image = image; + _initSizeIfNeeded(); + setState(() {}); + } + } else { + widget.imageCache.remove(url); + } + }); + } + } + + @override + void didUpdateWidget(VideoShotImage oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.url != widget.url) { + _loadImg(); + } + if (oldWidget.x != widget.x || oldWidget.y != widget.y) { + _setSrcRect(widget.imgXSize, widget.imgYSize); + } + } + + late final _imgPaint = Paint()..filterQuality = FilterQuality.medium; + late final _borderPaint = Paint() + ..color = Colors.white + ..style = PaintingStyle.stroke + ..strokeWidth = 1.5; + + @override + Widget build(BuildContext context) { + if (_image != null) { + return CroppedImage( + size: _size, + image: _image!, + srcRect: _srcRect, + dstRect: _dstRect, + rrect: _rrect, + imgPaint: _imgPaint, + borderPaint: _borderPaint, + ); + } + return const SizedBox.shrink(); + } +} + +const double _triangleHeight = 5.6; + +class _DanmakuTip extends SingleChildRenderObjectWidget { + const _DanmakuTip({ + this.offset = 0, + super.child, + }); + + final double offset; + + @override + RenderObject createRenderObject(BuildContext context) { + return _RenderDanmakuTip(offset: offset); + } + + @override + void updateRenderObject( + BuildContext context, + _RenderDanmakuTip renderObject, + ) { + renderObject.offset = offset; + } +} + +class _RenderDanmakuTip extends RenderProxyBox { + _RenderDanmakuTip({ + required double offset, + }) : _offset = offset; + + double _offset; + double get offset => _offset; + set offset(double value) { + if (_offset == value) return; + _offset = value; + markNeedsPaint(); + } + + @override + void paint(PaintingContext context, Offset offset) { + final paint = Paint() + ..color = const Color(0xB3000000) + ..style = .fill; + + final radius = size.height / 2; + const triangleBase = _triangleHeight * 2 / 3; + + final triangleCenterX = (size.width / 2 + _offset).clamp( + radius + triangleBase, + size.width - radius - triangleBase, + ); + final path = Path() + // triangle (exceed) + ..moveTo(triangleCenterX - triangleBase, 0) + ..lineTo(triangleCenterX, -_triangleHeight) + ..lineTo(triangleCenterX + triangleBase, 0) + // top + ..lineTo(size.width - radius, 0) + // right + ..arcToPoint( + Offset(size.width - radius, size.height), + radius: Radius.circular(radius), + ) + // bottom + ..lineTo(radius, size.height) + // left + ..arcToPoint( + Offset(radius, 0), + radius: Radius.circular(radius), + ) + ..close(); + + context.canvas + ..save() + ..translate(offset.dx, offset.dy) + ..drawPath(path, paint) + ..drawPath( + path, + paint + ..color = const Color(0x7EFFFFFF) + ..style = PaintingStyle.stroke + ..strokeWidth = 1.25, + ) + ..restore(); + + super.paint(context, offset); + } +} + +class _VideoTime extends LeafRenderObjectWidget { + const _VideoTime({ + required this.position, + required this.duration, + }); + + final String position; + final String duration; + + @override + _RenderVideoTime createRenderObject(BuildContext context) => _RenderVideoTime( + position: position, + duration: duration, + ); + + @override + void updateRenderObject( + BuildContext context, + covariant _RenderVideoTime renderObject, + ) { + renderObject + ..position = position + ..duration = duration; + } +} + +class _RenderVideoTime extends RenderBox { + _RenderVideoTime({ + required String position, + required String duration, + }) : _position = position, + _duration = duration; + + String _duration; + set duration(String value) { + _duration = value; + final paragraph = _buildParagraph(const Color(0xFFD0D0D0), _duration); + if (paragraph.maxIntrinsicWidth != _cache?.maxIntrinsicWidth) { + markNeedsLayout(); + } + _cache?.dispose(); + _cache = paragraph; + markNeedsSemanticsUpdate(); + } + + String _position; + set position(String value) { + _position = value; + markNeedsPaint(); + markNeedsSemanticsUpdate(); + } + + ui.Paragraph? _cache; + + ui.Paragraph _buildParagraph(Color color, String time) { + final builder = + ui.ParagraphBuilder( + ui.ParagraphStyle( + fontSize: 10, + height: 1.4, + fontFamily: 'Monospace', + ), + ) + ..pushStyle( + ui.TextStyle( + color: color, + fontSize: 10, + height: 1.4, + fontFamily: 'Monospace', + fontFeatures: const [FontFeature.tabularFigures()], + ), + ) + ..addText(time); + return builder.build() + ..layout(const ui.ParagraphConstraints(width: .infinity)); + } + + @override + ui.Size computeDryLayout(covariant BoxConstraints constraints) { + final paragraph = _cache ??= _buildParagraph( + const Color(0xFFD0D0D0), + _duration, + ); + return Size(paragraph.maxIntrinsicWidth, paragraph.height * 2); + } + + @override + void describeSemanticsConfiguration(SemanticsConfiguration config) { + super.describeSemanticsConfiguration(config); + config.label = 'position:$_position\nduration:$_duration'; + } + + @override + void performLayout() { + size = computeDryLayout(constraints); + } + + @override + void paint(PaintingContext context, ui.Offset offset) { + final para = _buildParagraph(Colors.white, _position); + context.canvas + ..drawParagraph( + para, + Offset( + offset.dx + _cache!.maxIntrinsicWidth - para.maxIntrinsicWidth, + offset.dy, + ), + ) + ..drawParagraph(_cache!, Offset(offset.dx, offset.dy + para.height)); + para.dispose(); + } + + @override + void dispose() { + _cache?.dispose(); + _cache = null; + super.dispose(); + } + + @override + bool get isRepaintBoundary => true; +} diff --git a/lib/plugin/pl_player/widgets/bottom_control.dart b/lib/plugin/pl_player/widgets/bottom_control.dart index c3cc6128b..ac449c0ec 100644 --- a/lib/plugin/pl_player/widgets/bottom_control.dart +++ b/lib/plugin/pl_player/widgets/bottom_control.dart @@ -2,7 +2,7 @@ import 'package:PiliPlus/common/widgets/progress_bar/audio_video_progress_bar.da import 'package:PiliPlus/common/widgets/progress_bar/segment_progress_bar.dart'; import 'package:PiliPlus/pages/video/controller.dart'; import 'package:PiliPlus/plugin/pl_player/controller.dart'; -import 'package:PiliPlus/plugin/pl_player/view.dart'; +import 'package:PiliPlus/plugin/pl_player/view/view.dart'; import 'package:PiliPlus/utils/extension/theme_ext.dart'; import 'package:PiliPlus/utils/feed_back.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; diff --git a/lib/plugin/pl_player/widgets/mpv_convert_webp.dart b/lib/plugin/pl_player/widgets/mpv_convert_webp.dart index 2210b3352..716191b47 100644 --- a/lib/plugin/pl_player/widgets/mpv_convert_webp.dart +++ b/lib/plugin/pl_player/widgets/mpv_convert_webp.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'dart:ffi'; +import 'package:PiliPlus/http/browser_ua.dart'; import 'package:PiliPlus/http/constants.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:flutter/foundation.dart' show kDebugMode; @@ -13,10 +14,9 @@ import 'package:media_kit/ffi/src/utf8.dart'; import 'package:media_kit/generated/libmpv/bindings.dart' as generated; import 'package:media_kit/media_kit.dart'; import 'package:media_kit/src/player/native/core/initializer.dart'; -import 'package:media_kit/src/player/native/core/native_library.dart'; class MpvConvertWebp { - final _mpv = generated.MPV(DynamicLibrary.open(NativeLibrary.path)); + final _mpv = NativePlayer.mpv; late final Pointer _ctx; final _completer = Completer(); @@ -41,7 +41,7 @@ class MpvConvertWebp { Future _init() async { final enableHA = Pref.enableHA; _ctx = await Initializer.create( - NativeLibrary.path, + _mpv, _onEvent, options: { 'o': outFile, @@ -58,13 +58,10 @@ class MpvConvertWebp { }, ); NativePlayer.setHeader( - const { - 'user-agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36', - 'referer': HttpString.baseUrl, - }, _mpv, _ctx, + userAgent: BrowserUa.pc, + referer: HttpString.baseUrl, ); if (progress != null) { _observeProperty('time-pos'); @@ -86,7 +83,7 @@ class MpvConvertWebp { return _completer.future; } - Future _onEvent(Pointer event) async { + Future? _onEvent(Pointer event) { switch (event.ref.event_id) { case generated.mpv_event_id.MPV_EVENT_PROPERTY_CHANGE: final prop = event.ref.data.cast().ref; @@ -117,11 +114,12 @@ class MpvConvertWebp { dispose(); break; } + return null; } void _command(List args) { final pointers = args.map((e) => e.toNativeUtf8()).toList(); - final arr = calloc>(128); + final arr = calloc>(pointers.length + 1); for (int i = 0; i < args.length; i++) { arr[i] = pointers[i]; } diff --git a/lib/services/shutdown_timer_service.dart b/lib/services/shutdown_timer_service.dart index 9903dd4a8..53b17b583 100644 --- a/lib/services/shutdown_timer_service.dart +++ b/lib/services/shutdown_timer_service.dart @@ -21,13 +21,10 @@ enum _ShutdownType with EnumWithLabel { const _ShutdownType(this.label); } -final shutdownTimerService = ShutdownTimerService(); +final shutdownTimerService = ShutdownTimerService._internal(); class ShutdownTimerService { ShutdownTimerService._internal(); - factory ShutdownTimerService() => _instance; - static final ShutdownTimerService _instance = - ShutdownTimerService._internal(); VoidCallback? onPause; ValueGetter? isPlaying; @@ -37,8 +34,8 @@ class ShutdownTimerService { int _durationInMinutes = 0; _ShutdownType _shutdownType = .pause; - bool? _isWaiting; - bool get isWaiting => _isWaiting ?? false; + bool _isWaiting = false; + bool get isWaiting => _isWaiting; bool _waitUntilCompleted = false; void _stopTimer() { @@ -50,7 +47,7 @@ class ShutdownTimerService { void reset([int durationInMinutes = 0]) { _stopTimer(); - _isWaiting = null; + _isWaiting = false; _durationInMinutes = durationInMinutes; } @@ -100,7 +97,7 @@ class ShutdownTimerService { void handleWaiting() { switch (_shutdownType) { case _ShutdownType.pause: - _isWaiting = null; + _isWaiting = false; _durationInMinutes = 0; SmartDialog.showToast('定时时间已到,已暂停'); case _ShutdownType.exit: diff --git a/lib/utils/accounts/api_type.dart b/lib/utils/accounts/api_type.dart index e98b6eb1c..18076e94b 100644 --- a/lib/utils/accounts/api_type.dart +++ b/lib/utils/accounts/api_type.dart @@ -94,6 +94,7 @@ abstract final class ApiType { Api.pgcUrl, Api.pugvUrl, Api.tvPlayUrl, + Api.videoshot, }, }; diff --git a/lib/utils/asset_utils.dart b/lib/utils/asset_utils.dart new file mode 100644 index 000000000..9d17a8ff5 --- /dev/null +++ b/lib/utils/asset_utils.dart @@ -0,0 +1,70 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/services.dart'; +import 'package:path/path.dart' as path; + +abstract final class AssetUtils { + /// from media-kit AssetLoader + static String? tryGetPath(String key) { + if (Platform.isWindows || Platform.isLinux) { + return path.join( + path.dirname(Platform.resolvedExecutable), + 'data', + 'flutter_assets', + key, + ); + } else if (Platform.isMacOS) { + return path.join( + path.dirname(Platform.resolvedExecutable), + '..', + 'Frameworks', + 'App.framework', + 'Resources', + 'flutter_assets', + key, + ); + } else if (Platform.isIOS) { + return path.join( + path.dirname(Platform.resolvedExecutable), + 'Frameworks', + 'App.framework', + 'flutter_assets', + key, + ); + } + return null; + } + + static FutureOr getOrCopy( + String src, + Iterable files, + String dst, + ) async { + final parsedSrc = tryGetPath(src); + if (parsedSrc != null) { + final srcDir = Directory(parsedSrc); + if (srcDir.existsSync()) { + return srcDir.absolute.path; + } + } + + final dstDir = Directory(dst); + if (!dstDir.existsSync()) { + await dstDir.create(recursive: true); + } + + for (final file in files) { + final targetFile = File(path.join(dst, file)); + if (targetFile.existsSync()) { + continue; + } + + try { + final data = await rootBundle.load(file); + await targetFile.writeAsBytes(data.buffer.asUint8List()); + } catch (_) {} + } + return dst; + } +} diff --git a/lib/utils/update.dart b/lib/utils/update.dart index 63b345d84..71a6450f3 100644 --- a/lib/utils/update.dart +++ b/lib/utils/update.dart @@ -3,8 +3,8 @@ import 'dart:io' show Platform; import 'package:PiliPlus/build_config.dart'; import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/http/api.dart'; +import 'package:PiliPlus/http/browser_ua.dart'; import 'package:PiliPlus/http/init.dart'; -import 'package:PiliPlus/http/ua_type.dart'; import 'package:PiliPlus/utils/accounts/account.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/storage.dart'; @@ -24,7 +24,7 @@ abstract final class Update { final res = await Request().get( Api.latestApp, options: Options( - headers: {'user-agent': UaType.mob.ua}, + headers: {'user-agent': BrowserUa.mob}, extra: {'account': const NoAccount()}, ), ); diff --git a/pubspec.lock b/pubspec.lock index 33419d91e..073c5fd63 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1131,8 +1131,8 @@ packages: description: path: media_kit ref: "version_1.2.5" - resolved-ref: ac35bc96c5560a7c9a55bc421bd4115d1e47bc70 - url: "https://github.com/bggRGjQaUbCoE/media-kit.git" + resolved-ref: b69eaa09ec1035c70ba7d7bdb7b74e6c51ed2dbb + url: "https://github.com/My-Responsitories/media-kit.git" source: git version: "1.1.11" media_kit_libs_android_video: @@ -1140,8 +1140,8 @@ packages: description: path: "libs/android/media_kit_libs_android_video" ref: "version_1.2.5" - resolved-ref: ac35bc96c5560a7c9a55bc421bd4115d1e47bc70 - url: "https://github.com/bggRGjQaUbCoE/media-kit.git" + resolved-ref: b69eaa09ec1035c70ba7d7bdb7b74e6c51ed2dbb + url: "https://github.com/My-Responsitories/media-kit.git" source: git version: "1.3.7" media_kit_libs_ios_video: @@ -1173,8 +1173,8 @@ packages: description: path: "libs/universal/media_kit_libs_video" ref: "version_1.2.5" - resolved-ref: ac35bc96c5560a7c9a55bc421bd4115d1e47bc70 - url: "https://github.com/bggRGjQaUbCoE/media-kit.git" + resolved-ref: b69eaa09ec1035c70ba7d7bdb7b74e6c51ed2dbb + url: "https://github.com/My-Responsitories/media-kit.git" source: git version: "1.0.5" media_kit_libs_windows_video: @@ -1182,8 +1182,8 @@ packages: description: path: "libs/windows/media_kit_libs_windows_video" ref: "version_1.2.5" - resolved-ref: ac35bc96c5560a7c9a55bc421bd4115d1e47bc70 - url: "https://github.com/bggRGjQaUbCoE/media-kit.git" + resolved-ref: b69eaa09ec1035c70ba7d7bdb7b74e6c51ed2dbb + url: "https://github.com/My-Responsitories/media-kit.git" source: git version: "1.0.10" media_kit_native_event_loop: @@ -1191,8 +1191,8 @@ packages: description: path: media_kit_native_event_loop ref: "version_1.2.5" - resolved-ref: ac35bc96c5560a7c9a55bc421bd4115d1e47bc70 - url: "https://github.com/bggRGjQaUbCoE/media-kit.git" + resolved-ref: b69eaa09ec1035c70ba7d7bdb7b74e6c51ed2dbb + url: "https://github.com/My-Responsitories/media-kit.git" source: git version: "1.0.9" media_kit_video: @@ -1200,8 +1200,8 @@ packages: description: path: media_kit_video ref: "version_1.2.5" - resolved-ref: ac35bc96c5560a7c9a55bc421bd4115d1e47bc70 - url: "https://github.com/bggRGjQaUbCoE/media-kit.git" + resolved-ref: b69eaa09ec1035c70ba7d7bdb7b74e6c51ed2dbb + url: "https://github.com/My-Responsitories/media-kit.git" source: git version: "1.2.5" menu_base: @@ -1476,14 +1476,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.28.0" - safe_local_storage: - dependency: transitive - description: - name: safe_local_storage - sha256: "287ea1f667c0b93cdc127dccc707158e2d81ee59fba0459c31a0c7da4d09c755" - url: "https://pub.dev" - source: hosted - version: "2.0.3" saver_gallery: dependency: "direct main" description: @@ -1810,14 +1802,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.0" - uri_parser: - dependency: transitive - description: - name: uri_parser - sha256: "051c62e5f693de98ca9f130ee707f8916e2266945565926be3ff20659f7853ce" - url: "https://pub.dev" - source: hosted - version: "3.0.2" url_launcher: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index dbbebcf9a..dd29eeb90 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -250,32 +250,32 @@ dependency_overrides: rxdart: ^0.28.0 media_kit: git: - url: https://github.com/bggRGjQaUbCoE/media-kit.git + url: https://github.com/My-Responsitories/media-kit.git path: media_kit ref: version_1.2.5 media_kit_video: git: - url: https://github.com/bggRGjQaUbCoE/media-kit.git + url: https://github.com/My-Responsitories/media-kit.git path: media_kit_video ref: version_1.2.5 media_kit_libs_video: git: - url: https://github.com/bggRGjQaUbCoE/media-kit.git + url: https://github.com/My-Responsitories/media-kit.git path: libs/universal/media_kit_libs_video ref: version_1.2.5 media_kit_native_event_loop: git: - url: https://github.com/bggRGjQaUbCoE/media-kit.git + url: https://github.com/My-Responsitories/media-kit.git path: media_kit_native_event_loop ref: version_1.2.5 media_kit_libs_android_video: git: - url: https://github.com/bggRGjQaUbCoE/media-kit.git + url: https://github.com/My-Responsitories/media-kit.git path: libs/android/media_kit_libs_android_video ref: version_1.2.5 media_kit_libs_windows_video: git: - url: https://github.com/bggRGjQaUbCoE/media-kit.git + url: https://github.com/My-Responsitories/media-kit.git path: libs/windows/media_kit_libs_windows_video ref: version_1.2.5 font_awesome_flutter: 10.9.0