diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 65906625..1e7d9fed 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -49,6 +49,8 @@ NSPhotoLibraryAddUsageDescription 请允许APP保存图片到相册 + NSPhotoLibraryUsageDescription + 请允许APP保存图片到相册 NSCameraUsageDescription App需要您的同意,才能访问相册 NSAppleMusicUsageDescription diff --git a/lib/common/widgets/overlay_pop.dart b/lib/common/widgets/overlay_pop.dart deleted file mode 100644 index 4f0a3899..00000000 --- a/lib/common/widgets/overlay_pop.dart +++ /dev/null @@ -1,87 +0,0 @@ -import 'package:flutter/material.dart'; -import '../../utils/download.dart'; -import '../constants.dart'; -import 'network_img_layer.dart'; - -class OverlayPop extends StatelessWidget { - const OverlayPop({super.key, this.videoItem, this.closeFn}); - - final dynamic videoItem; - final Function? closeFn; - - @override - Widget build(BuildContext context) { - final double imgWidth = MediaQuery.sizeOf(context).width - 8 * 2; - return Container( - margin: const EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, - borderRadius: BorderRadius.circular(10.0), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Stack( - children: [ - NetworkImgLayer( - width: imgWidth, - height: imgWidth / StyleString.aspectRatio, - src: videoItem.pic! as String, - quality: 100, - ), - Positioned( - right: 8, - top: 8, - child: Container( - width: 30, - height: 30, - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.3), - borderRadius: - const BorderRadius.all(Radius.circular(20))), - child: IconButton( - style: ButtonStyle( - padding: MaterialStateProperty.all(EdgeInsets.zero), - ), - onPressed: () => closeFn!(), - icon: const Icon( - Icons.close, - size: 18, - color: Colors.white, - ), - ), - ), - ), - ], - ), - Padding( - padding: const EdgeInsets.fromLTRB(12, 10, 8, 10), - child: Row( - children: [ - Expanded( - child: Text( - videoItem.title! as String, - style: Theme.of(context).textTheme.titleSmall, - ), - ), - const SizedBox(width: 4), - IconButton( - tooltip: '保存封面图', - onPressed: () async { - await DownloadUtils.downloadImg( - videoItem.pic != null - ? videoItem.pic as String - : videoItem.cover as String, - ); - // closeFn!(); - }, - icon: const Icon(Icons.download, size: 20), - ) - ], - )), - ], - ), - ); - } -} diff --git a/lib/common/widgets/video_card_h.dart b/lib/common/widgets/video_card_h.dart index 25e701ac..df0c29b7 100644 --- a/lib/common/widgets/video_card_h.dart +++ b/lib/common/widgets/video_card_h.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; +import 'package:pilipala/utils/image_save.dart'; import '../../http/search.dart'; import '../../http/user.dart'; import '../../http/video.dart'; @@ -16,8 +17,7 @@ class VideoCardH extends StatelessWidget { const VideoCardH({ super.key, required this.videoItem, - this.longPress, - this.longPressEnd, + this.onPressedFn, this.source = 'normal', this.showOwner = true, this.showView = true, @@ -27,8 +27,8 @@ class VideoCardH extends StatelessWidget { }); // ignore: prefer_typing_uninitialized_variables final videoItem; - final Function()? longPress; - final Function()? longPressEnd; + final Function()? onPressedFn; + // normal 推荐, later 稍后再看, search 搜索 final String source; final bool showOwner; final bool showView; @@ -45,109 +45,103 @@ class VideoCardH extends StatelessWidget { type = videoItem.type; } catch (_) {} final String heroTag = Utils.makeHeroTag(aid); - return GestureDetector( - onLongPress: () { - if (longPress != null) { - longPress!(); + return InkWell( + onTap: () async { + try { + if (type == 'ketang') { + SmartDialog.showToast('课堂视频暂不支持播放'); + return; + } + final int cid = + videoItem.cid ?? await SearchHttp.ab2c(aid: aid, bvid: bvid); + Get.toNamed('/video?bvid=$bvid&cid=$cid', + arguments: {'videoItem': videoItem, 'heroTag': heroTag}); + } catch (err) { + SmartDialog.showToast(err.toString()); } }, - // onLongPressEnd: (details) { - // if (longPressEnd != null) { - // longPressEnd!(); - // } - // }, - child: InkWell( - onTap: () async { - try { - if (type == 'ketang') { - SmartDialog.showToast('课堂视频暂不支持播放'); - return; - } - final int cid = - videoItem.cid ?? await SearchHttp.ab2c(aid: aid, bvid: bvid); - Get.toNamed('/video?bvid=$bvid&cid=$cid', - arguments: {'videoItem': videoItem, 'heroTag': heroTag}); - } catch (err) { - SmartDialog.showToast(err.toString()); - } - }, - child: Padding( - padding: const EdgeInsets.fromLTRB( - StyleString.safeSpace, 5, StyleString.safeSpace, 5), - child: LayoutBuilder( - builder: (BuildContext context, BoxConstraints boxConstraints) { - final double width = (boxConstraints.maxWidth - - StyleString.cardSpace * - 6 / - MediaQuery.textScalerOf(context).scale(1.0)) / - 2; - return Container( - constraints: const BoxConstraints(minHeight: 88), - height: width / StyleString.aspectRatio, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - AspectRatio( - aspectRatio: StyleString.aspectRatio, - child: LayoutBuilder( - builder: (BuildContext context, - BoxConstraints boxConstraints) { - final double maxWidth = boxConstraints.maxWidth; - final double maxHeight = boxConstraints.maxHeight; - return Stack( - children: [ - Hero( - tag: heroTag, - child: NetworkImgLayer( - src: videoItem.pic as String, - width: maxWidth, - height: maxHeight, - ), + onLongPress: () => imageSaveDialog( + context, + videoItem, + SmartDialog.dismiss, + ), + child: Padding( + padding: const EdgeInsets.fromLTRB( + StyleString.safeSpace, 5, StyleString.safeSpace, 5), + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints boxConstraints) { + final double width = (boxConstraints.maxWidth - + StyleString.cardSpace * + 6 / + MediaQuery.textScalerOf(context).scale(1.0)) / + 2; + return Container( + constraints: const BoxConstraints(minHeight: 88), + height: width / StyleString.aspectRatio, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AspectRatio( + aspectRatio: StyleString.aspectRatio, + child: LayoutBuilder( + builder: (BuildContext context, + BoxConstraints boxConstraints) { + final double maxWidth = boxConstraints.maxWidth; + final double maxHeight = boxConstraints.maxHeight; + return Stack( + children: [ + Hero( + tag: heroTag, + child: NetworkImgLayer( + src: videoItem.pic as String, + width: maxWidth, + height: maxHeight, ), - if (videoItem.duration != 0) - PBadge( - text: Utils.timeFormat(videoItem.duration!), - right: 6.0, - bottom: 6.0, - type: 'gray', - ), - if (type != 'video') - PBadge( - text: type, - left: 6.0, - bottom: 6.0, - type: 'primary', - ), - // if (videoItem.rcmdReason != null && - // videoItem.rcmdReason.content != '') - // pBadge(videoItem.rcmdReason.content, context, - // 6.0, 6.0, null, null), - if (showCharge && videoItem?.isChargingSrc) - const PBadge( - text: '充电专属', - right: 6.0, - top: 6.0, - type: 'primary', - ), - ], - ); - }, - ), + ), + if (videoItem.duration != 0) + PBadge( + text: Utils.timeFormat(videoItem.duration!), + right: 6.0, + bottom: 6.0, + type: 'gray', + ), + if (type != 'video') + PBadge( + text: type, + left: 6.0, + bottom: 6.0, + type: 'primary', + ), + // if (videoItem.rcmdReason != null && + // videoItem.rcmdReason.content != '') + // pBadge(videoItem.rcmdReason.content, context, + // 6.0, 6.0, null, null), + if (showCharge && videoItem?.isChargingSrc) + const PBadge( + text: '充电专属', + right: 6.0, + top: 6.0, + type: 'primary', + ), + ], + ); + }, ), - VideoContent( - videoItem: videoItem, - source: source, - showOwner: showOwner, - showView: showView, - showDanmaku: showDanmaku, - showPubdate: showPubdate, - ) - ], - ), - ); - }, - ), + ), + VideoContent( + videoItem: videoItem, + source: source, + showOwner: showOwner, + showView: showView, + showDanmaku: showDanmaku, + showPubdate: showPubdate, + onPressedFn: onPressedFn, + ) + ], + ), + ); + }, ), ), ); @@ -162,6 +156,7 @@ class VideoContent extends StatelessWidget { final bool showView; final bool showDanmaku; final bool showPubdate; + final Function()? onPressedFn; const VideoContent({ super.key, @@ -171,6 +166,7 @@ class VideoContent extends StatelessWidget { this.showView = true, this.showDanmaku = true, this.showPubdate = false, + this.onPressedFn, }); @override @@ -181,7 +177,7 @@ class VideoContent extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (videoItem.title is String) ...[ + if (source == 'normal' || source == 'later') ...[ Text( videoItem.title as String, textAlign: TextAlign.start, @@ -196,7 +192,7 @@ class VideoContent extends StatelessWidget { maxLines: 2, text: TextSpan( children: [ - for (final i in videoItem.title) ...[ + for (final i in videoItem.titleList) ...[ TextSpan( text: i['text'] as String, style: TextStyle( @@ -374,6 +370,19 @@ class VideoContent extends StatelessWidget { ], ), ), + if (source == 'later') ...[ + IconButton( + style: ButtonStyle( + padding: MaterialStateProperty.all(EdgeInsets.zero), + ), + onPressed: () => onPressedFn?.call(), + icon: Icon( + Icons.clear_outlined, + color: Theme.of(context).colorScheme.outline, + size: 18, + ), + ) + ], ], ), ], diff --git a/lib/common/widgets/video_card_v.dart b/lib/common/widgets/video_card_v.dart index 9916aa7a..6a97d7e7 100644 --- a/lib/common/widgets/video_card_v.dart +++ b/lib/common/widgets/video_card_v.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:pilipala/utils/feed_back.dart'; +import 'package:pilipala/utils/image_save.dart'; import '../../models/model_rec_video_item.dart'; -import 'overlay_pop.dart'; import 'stat/danmu.dart'; import 'stat/view.dart'; import '../../http/dynamics.dart'; @@ -127,14 +127,11 @@ class VideoCardV extends StatelessWidget { String heroTag = Utils.makeHeroTag(videoItem.id); return InkWell( onTap: () async => onPushDetail(heroTag), - onLongPress: () { - SmartDialog.show( - builder: (context) => OverlayPop( - videoItem: videoItem, - closeFn: () => SmartDialog.dismiss(), - ), - ); - }, + onLongPress: () => imageSaveDialog( + context, + videoItem, + SmartDialog.dismiss, + ), borderRadius: BorderRadius.circular(16), child: Column( children: [ diff --git a/lib/models/bangumi/list.dart b/lib/models/bangumi/list.dart index c15014d0..fe71bb61 100644 --- a/lib/models/bangumi/list.dart +++ b/lib/models/bangumi/list.dart @@ -30,6 +30,7 @@ class BangumiListItemModel { BangumiListItemModel({ this.badge, this.badgeType, + this.pic, this.cover, // this.firstEp, this.indexShow, @@ -50,6 +51,7 @@ class BangumiListItemModel { String? badge; int? badgeType; + String? pic; String? cover; String? indexShow; int? isFinish; @@ -70,6 +72,7 @@ class BangumiListItemModel { BangumiListItemModel.fromJson(Map json) { badge = json['badge'] == '' ? null : json['badge']; badgeType = json['badge_type']; + pic = json['cover']; cover = json['cover']; indexShow = json['index_show']; isFinish = json['is_finish']; diff --git a/lib/models/search/result.dart b/lib/models/search/result.dart index 418fb99d..81917b72 100644 --- a/lib/models/search/result.dart +++ b/lib/models/search/result.dart @@ -25,6 +25,7 @@ class SearchVideoItemModel { this.aid, this.bvid, this.title, + this.titleList, this.description, this.pic, // this.play, @@ -54,8 +55,8 @@ class SearchVideoItemModel { String? arcurl; int? aid; String? bvid; - List? title; - // List? titleList; + String? title; + List? titleList; String? description; String? pic; // String? play; @@ -82,8 +83,9 @@ class SearchVideoItemModel { aid = json['aid']; bvid = json['bvid']; mid = json['mid']; - // title = json['title'].replaceAll(RegExp(r'<.*?>'), ''); - title = Em.regTitle(json['title']); + title = json['title'].replaceAll(RegExp(r'<.*?>'), ''); + // title = Em.regTitle(json['title']); + titleList = Em.regTitle(json['title']); description = json['description']; pic = json['pic'] != null && json['pic'].startsWith('//') ? 'https:${json['pic']}' @@ -232,6 +234,7 @@ class SearchLiveItemModel { this.userCover, this.type, this.title, + this.titleList, this.cover, this.pic, this.online, @@ -251,7 +254,8 @@ class SearchLiveItemModel { String? face; String? userCover; String? type; - List? title; + String? title; + List? titleList; String? cover; String? pic; int? online; @@ -272,7 +276,8 @@ class SearchLiveItemModel { face = json['uface']; userCover = json['user_cover']; type = json['type']; - title = Em.regTitle(json['title']); + title = json['title'].replaceAll(RegExp(r'<.*?>'), ''); + titleList = Em.regTitle(json['title']); cover = json['cover']; pic = json['cover']; online = json['online']; @@ -302,6 +307,7 @@ class SearchMBangumiItemModel { this.type, this.mediaId, this.title, + this.titleList, this.orgTitle, this.mediaType, this.cv, @@ -328,7 +334,8 @@ class SearchMBangumiItemModel { String? type; int? mediaId; - List? title; + String? title; + List? titleList; String? orgTitle; int? mediaType; String? cv; @@ -355,7 +362,8 @@ class SearchMBangumiItemModel { SearchMBangumiItemModel.fromJson(Map json) { type = json['type']; mediaId = json['media_id']; - title = Em.regTitle(json['title']); + title = json['title'].replaceAll(RegExp(r'<.*?>'), ''); + titleList = Em.regTitle(json['title']); orgTitle = json['org_title']; mediaType = json['media_type']; cv = json['cv']; diff --git a/lib/pages/bangumi/widgets/bangumu_card_v.dart b/lib/pages/bangumi/widgets/bangumu_card_v.dart index c1233ddf..3c8f6d2a 100644 --- a/lib/pages/bangumi/widgets/bangumu_card_v.dart +++ b/lib/pages/bangumi/widgets/bangumu_card_v.dart @@ -5,7 +5,9 @@ import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/widgets/badge.dart'; import 'package:pilipala/http/search.dart'; import 'package:pilipala/models/bangumi/info.dart'; +import 'package:pilipala/models/bangumi/list.dart'; import 'package:pilipala/models/common/search_type.dart'; +import 'package:pilipala/utils/image_save.dart'; import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; @@ -14,109 +16,87 @@ class BangumiCardV extends StatelessWidget { const BangumiCardV({ super.key, required this.bangumiItem, - this.longPress, - this.longPressEnd, }); - final bangumiItem; - final Function()? longPress; - final Function()? longPressEnd; + final BangumiListItemModel bangumiItem; @override Widget build(BuildContext context) { String heroTag = Utils.makeHeroTag(bangumiItem.mediaId); - return Card( - elevation: 0, - clipBehavior: Clip.hardEdge, - margin: EdgeInsets.zero, - child: GestureDetector( - // onLongPress: () { - // if (longPress != null) { - // longPress!(); - // } - // }, - // onLongPressEnd: (details) { - // if (longPressEnd != null) { - // longPressEnd!(); - // } - // }, - child: InkWell( - onTap: () async { - final int seasonId = bangumiItem.seasonId; - SmartDialog.showLoading(msg: '获取中...'); - final res = await SearchHttp.bangumiInfo(seasonId: seasonId); - SmartDialog.dismiss().then((value) { - if (res['status']) { - if (res['data'].episodes.isEmpty) { - SmartDialog.showToast('资源加载失败'); - return; - } - EpisodeItem episode = res['data'].episodes.first; - String bvid = episode.bvid!; - int cid = episode.cid!; - String pic = episode.cover!; - String heroTag = Utils.makeHeroTag(cid); - Get.toNamed( - '/video?bvid=$bvid&cid=$cid&seasonId=$seasonId', - arguments: { - 'pic': pic, - 'heroTag': heroTag, - 'videoType': SearchType.media_bangumi, - 'bangumiItem': res['data'], - }, + return InkWell( + onTap: () async { + final int seasonId = bangumiItem.seasonId!; + SmartDialog.showLoading(msg: '获取中...'); + final res = await SearchHttp.bangumiInfo(seasonId: seasonId); + SmartDialog.dismiss().then((value) { + if (res['status']) { + if (res['data'].episodes.isEmpty) { + SmartDialog.showToast('资源加载失败'); + return; + } + EpisodeItem episode = res['data'].episodes.first; + String bvid = episode.bvid!; + int cid = episode.cid!; + String pic = episode.cover!; + String heroTag = Utils.makeHeroTag(cid); + Get.toNamed( + '/video?bvid=$bvid&cid=$cid&seasonId=$seasonId', + arguments: { + 'pic': pic, + 'heroTag': heroTag, + 'videoType': SearchType.media_bangumi, + 'bangumiItem': res['data'], + }, + ); + } + }); + }, + onLongPress: () => + imageSaveDialog(context, bangumiItem, SmartDialog.dismiss), + child: Column( + children: [ + ClipRRect( + borderRadius: const BorderRadius.all( + StyleString.imgRadius, + ), + child: AspectRatio( + aspectRatio: 0.65, + child: LayoutBuilder(builder: (context, boxConstraints) { + final double maxWidth = boxConstraints.maxWidth; + final double maxHeight = boxConstraints.maxHeight; + return Stack( + children: [ + Hero( + tag: heroTag, + child: NetworkImgLayer( + src: bangumiItem.cover, + width: maxWidth, + height: maxHeight, + ), + ), + if (bangumiItem.badge != null) + PBadge( + text: bangumiItem.badge, + top: 6, + right: 6, + bottom: null, + left: null), + if (bangumiItem.order != null) + PBadge( + text: bangumiItem.order, + top: null, + right: null, + bottom: 6, + left: 6, + type: 'gray', + ), + ], ); - } - }); - }, - child: Column( - children: [ - ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: StyleString.imgRadius, - topRight: StyleString.imgRadius, - bottomLeft: StyleString.imgRadius, - bottomRight: StyleString.imgRadius, - ), - child: AspectRatio( - aspectRatio: 0.65, - child: LayoutBuilder(builder: (context, boxConstraints) { - final double maxWidth = boxConstraints.maxWidth; - final double maxHeight = boxConstraints.maxHeight; - return Stack( - children: [ - Hero( - tag: heroTag, - child: NetworkImgLayer( - src: bangumiItem.cover, - width: maxWidth, - height: maxHeight, - ), - ), - if (bangumiItem.badge != null) - PBadge( - text: bangumiItem.badge, - top: 6, - right: 6, - bottom: null, - left: null), - if (bangumiItem.order != null) - PBadge( - text: bangumiItem.order, - top: null, - right: null, - bottom: 6, - left: 6, - type: 'gray', - ), - ], - ); - }), - ), - ), - BangumiContent(bangumiItem: bangumiItem) - ], + }), + ), ), - ), + BangumiContent(bangumiItem: bangumiItem) + ], ), ); } diff --git a/lib/pages/fav_detail/widget/fav_video_card.dart b/lib/pages/fav_detail/widget/fav_video_card.dart index 1c4008ff..79e5c073 100644 --- a/lib/pages/fav_detail/widget/fav_video_card.dart +++ b/lib/pages/fav_detail/widget/fav_video_card.dart @@ -1,3 +1,4 @@ +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:flutter/material.dart'; import 'package:pilipala/common/constants.dart'; @@ -7,6 +8,7 @@ import 'package:pilipala/http/search.dart'; import 'package:pilipala/http/video.dart'; import 'package:pilipala/models/common/search_type.dart'; import 'package:pilipala/utils/id_utils.dart'; +import 'package:pilipala/utils/image_save.dart'; import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; import '../../../common/widgets/badge.dart'; @@ -61,6 +63,11 @@ class FavVideoCardH extends StatelessWidget { epId != null ? SearchType.media_bangumi : SearchType.video, }); }, + onLongPress: () => imageSaveDialog( + context, + videoItem, + SmartDialog.dismiss, + ), child: Column( children: [ Padding( diff --git a/lib/pages/hot/view.dart b/lib/pages/hot/view.dart index e2e20e73..80d08e7b 100644 --- a/lib/pages/hot/view.dart +++ b/lib/pages/hot/view.dart @@ -3,8 +3,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/constants.dart'; -import 'package:pilipala/common/widgets/animated_dialog.dart'; -import 'package:pilipala/common/widgets/overlay_pop.dart'; import 'package:pilipala/common/skeleton/video_card_h.dart'; import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/video_card_h.dart'; @@ -78,15 +76,6 @@ class _HotPageState extends State with AutomaticKeepAliveClientMixin { return VideoCardH( videoItem: _hotController.videoList[index], showPubdate: true, - longPress: () { - _hotController.popupDialog = _createPopupDialog( - _hotController.videoList[index]); - Overlay.of(context) - .insert(_hotController.popupDialog!); - }, - longPressEnd: () { - _hotController.popupDialog?.remove(); - }, ); }, childCount: _hotController.videoList.length), ), @@ -122,14 +111,4 @@ class _HotPageState extends State with AutomaticKeepAliveClientMixin { ), ); } - - OverlayEntry _createPopupDialog(videoItem) { - return OverlayEntry( - builder: (context) => AnimatedDialog( - closeFn: _hotController.popupDialog?.remove, - child: OverlayPop( - videoItem: videoItem, closeFn: _hotController.popupDialog?.remove), - ), - ); - } } diff --git a/lib/pages/later/view.dart b/lib/pages/later/view.dart index d4695154..7c6e158d 100644 --- a/lib/pages/later/view.dart +++ b/lib/pages/later/view.dart @@ -84,7 +84,7 @@ class _LaterPageState extends State { return VideoCardH( videoItem: videoItem, source: 'later', - longPress: () => _laterController.toViewDel( + onPressedFn: () => _laterController.toViewDel( aid: videoItem.aid)); }, childCount: _laterController.laterList.length), ) diff --git a/lib/pages/live/view.dart b/lib/pages/live/view.dart index c61d20b3..83605495 100644 --- a/lib/pages/live/view.dart +++ b/lib/pages/live/view.dart @@ -5,9 +5,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/skeleton/video_card_v.dart'; -import 'package:pilipala/common/widgets/animated_dialog.dart'; import 'package:pilipala/common/widgets/http_error.dart'; -import 'package:pilipala/common/widgets/overlay_pop.dart'; import 'package:pilipala/utils/main_stream.dart'; import 'controller.dart'; @@ -112,16 +110,6 @@ class _LivePageState extends State ); } - OverlayEntry _createPopupDialog(liveItem) { - return OverlayEntry( - builder: (context) => AnimatedDialog( - closeFn: _liveController.popupDialog?.remove, - child: OverlayPop( - videoItem: liveItem, closeFn: _liveController.popupDialog?.remove), - ), - ); - } - Widget contentGrid(ctr, liveList) { // double maxWidth = Get.size.width; // int baseWidth = 500; @@ -152,14 +140,6 @@ class _LivePageState extends State ? LiveCardV( liveItem: liveList[index], crossAxisCount: crossAxisCount, - longPress: () { - _liveController.popupDialog = - _createPopupDialog(liveList[index]); - Overlay.of(context).insert(_liveController.popupDialog!); - }, - longPressEnd: () { - _liveController.popupDialog?.remove(); - }, ) : const VideoCardVSkeleton(); }, diff --git a/lib/pages/live/widgets/live_item.dart b/lib/pages/live/widgets/live_item.dart index 9218d4fb..f70ba82b 100644 --- a/lib/pages/live/widgets/live_item.dart +++ b/lib/pages/live/widgets/live_item.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/constants.dart'; import 'package:pilipala/models/live/item.dart'; +import 'package:pilipala/utils/image_save.dart'; import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; @@ -9,81 +11,66 @@ import 'package:pilipala/common/widgets/network_img_layer.dart'; class LiveCardV extends StatelessWidget { final LiveItemModel liveItem; final int crossAxisCount; - final Function()? longPress; - final Function()? longPressEnd; const LiveCardV({ Key? key, required this.liveItem, required this.crossAxisCount, - this.longPress, - this.longPressEnd, }) : super(key: key); @override Widget build(BuildContext context) { String heroTag = Utils.makeHeroTag(liveItem.roomId); - return Card( - elevation: 0, - clipBehavior: Clip.hardEdge, - margin: EdgeInsets.zero, - child: GestureDetector( - onLongPress: () { - if (longPress != null) { - longPress!(); - } - }, - // onLongPressEnd: (details) { - // if (longPressEnd != null) { - // longPressEnd!(); - // } - // }, - child: InkWell( - onTap: () async { - Get.toNamed('/liveRoom?roomid=${liveItem.roomId}', - arguments: {'liveItem': liveItem, 'heroTag': heroTag}); - }, - child: Column( - children: [ - ClipRRect( - borderRadius: const BorderRadius.all(StyleString.imgRadius), - child: AspectRatio( - aspectRatio: StyleString.aspectRatio, - child: LayoutBuilder(builder: (context, boxConstraints) { - double maxWidth = boxConstraints.maxWidth; - double maxHeight = boxConstraints.maxHeight; - return Stack( - children: [ - Hero( - tag: heroTag, - child: NetworkImgLayer( - src: liveItem.cover!, - width: maxWidth, - height: maxHeight, + return InkWell( + onLongPress: () => imageSaveDialog( + context, + liveItem, + SmartDialog.dismiss, + ), + borderRadius: BorderRadius.circular(16), + onTap: () async { + Get.toNamed('/liveRoom?roomid=${liveItem.roomId}', + arguments: {'liveItem': liveItem, 'heroTag': heroTag}); + }, + child: Column( + children: [ + ClipRRect( + borderRadius: const BorderRadius.all(StyleString.imgRadius), + child: AspectRatio( + aspectRatio: StyleString.aspectRatio, + child: LayoutBuilder(builder: (context, boxConstraints) { + double maxWidth = boxConstraints.maxWidth; + double maxHeight = boxConstraints.maxHeight; + return Stack( + children: [ + Hero( + tag: heroTag, + child: NetworkImgLayer( + src: liveItem.cover!, + width: maxWidth, + height: maxHeight, + ), + ), + if (crossAxisCount != 1) + Positioned( + left: 0, + right: 0, + bottom: 0, + child: AnimatedOpacity( + opacity: 1, + duration: const Duration(milliseconds: 200), + child: VideoStat( + liveItem: liveItem, ), ), - if (crossAxisCount != 1) - Positioned( - left: 0, - right: 0, - bottom: 0, - child: AnimatedOpacity( - opacity: 1, - duration: const Duration(milliseconds: 200), - child: VideoStat( - liveItem: liveItem, - ), - ), - ), - ], - ); - }), - ), - ), - LiveContent(liveItem: liveItem, crossAxisCount: crossAxisCount) - ], + ), + ], + ); + }), + ), ), - ), + LiveContent(liveItem: liveItem, crossAxisCount: crossAxisCount) + ], ), ); } diff --git a/lib/pages/member_seasons/widgets/item.dart b/lib/pages/member_seasons/widgets/item.dart index 4df74b70..85b763b7 100644 --- a/lib/pages/member_seasons/widgets/item.dart +++ b/lib/pages/member_seasons/widgets/item.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/widgets/badge.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/stat/view.dart'; import 'package:pilipala/http/search.dart'; +import 'package:pilipala/utils/image_save.dart'; import 'package:pilipala/utils/utils.dart'; class MemberSeasonsItem extends StatelessWidget { @@ -29,6 +31,11 @@ class MemberSeasonsItem extends StatelessWidget { Get.toNamed('/video?bvid=${seasonItem.bvid}&cid=$cid', arguments: {'videoItem': seasonItem, 'heroTag': heroTag}); }, + onLongPress: () => imageSaveDialog( + context, + seasonItem, + SmartDialog.dismiss, + ), child: Column( children: [ AspectRatio( diff --git a/lib/pages/rank/zone/view.dart b/lib/pages/rank/zone/view.dart index 04631a8c..72d81f95 100644 --- a/lib/pages/rank/zone/view.dart +++ b/lib/pages/rank/zone/view.dart @@ -3,8 +3,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/constants.dart'; -import 'package:pilipala/common/widgets/animated_dialog.dart'; -import 'package:pilipala/common/widgets/overlay_pop.dart'; import 'package:pilipala/common/skeleton/video_card_h.dart'; import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/video_card_h.dart'; @@ -82,15 +80,6 @@ class _ZonePageState extends State return VideoCardH( videoItem: _zoneController.videoList[index], showPubdate: true, - longPress: () { - _zoneController.popupDialog = _createPopupDialog( - _zoneController.videoList[index]); - Overlay.of(context) - .insert(_zoneController.popupDialog!); - }, - longPressEnd: () { - _zoneController.popupDialog?.remove(); - }, ); }, childCount: _zoneController.videoList.length), ), @@ -126,14 +115,4 @@ class _ZonePageState extends State ), ); } - - OverlayEntry _createPopupDialog(videoItem) { - return OverlayEntry( - builder: (context) => AnimatedDialog( - closeFn: _zoneController.popupDialog?.remove, - child: OverlayPop( - videoItem: videoItem, closeFn: _zoneController.popupDialog?.remove), - ), - ); - } } diff --git a/lib/pages/search_panel/widgets/live_panel.dart b/lib/pages/search_panel/widgets/live_panel.dart index 6fb5f5b8..5f797f09 100644 --- a/lib/pages/search_panel/widgets/live_panel.dart +++ b/lib/pages/search_panel/widgets/live_panel.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:pilipala/utils/image_save.dart'; import 'package:pilipala/utils/utils.dart'; Widget searchLivePanel(BuildContext context, ctr, list) { @@ -42,15 +44,15 @@ class LiveItem extends StatelessWidget { Get.toNamed('/liveRoom?roomid=${liveItem.roomid}', arguments: {'liveItem': liveItem, 'heroTag': heroTag}); }, + onLongPress: () => imageSaveDialog( + context, + liveItem, + SmartDialog.dismiss, + ), child: Column( children: [ ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: StyleString.imgRadius, - topRight: StyleString.imgRadius, - bottomLeft: StyleString.imgRadius, - bottomRight: StyleString.imgRadius, - ), + borderRadius: const BorderRadius.all(StyleString.imgRadius), child: AspectRatio( aspectRatio: StyleString.aspectRatio, child: LayoutBuilder(builder: (context, boxConstraints) { @@ -108,7 +110,7 @@ class LiveContent extends StatelessWidget { RichText( text: TextSpan( children: [ - for (var i in liveItem.title) ...[ + for (var i in liveItem.titleList) ...[ TextSpan( text: i['text'], style: TextStyle( diff --git a/lib/pages/search_panel/widgets/media_bangumi_panel.dart b/lib/pages/search_panel/widgets/media_bangumi_panel.dart index 18799d3a..7d88b183 100644 --- a/lib/pages/search_panel/widgets/media_bangumi_panel.dart +++ b/lib/pages/search_panel/widgets/media_bangumi_panel.dart @@ -63,7 +63,7 @@ Widget searchMbangumiPanel(BuildContext context, ctr, list) { style: TextStyle( color: Theme.of(context).colorScheme.onSurface), children: [ - for (var i in i.title) ...[ + for (var i in i.titleList) ...[ TextSpan( text: i['text'], style: TextStyle( diff --git a/lib/pages/search_panel/widgets/video_panel.dart b/lib/pages/search_panel/widgets/video_panel.dart index b96ff004..c24a007c 100644 --- a/lib/pages/search_panel/widgets/video_panel.dart +++ b/lib/pages/search_panel/widgets/video_panel.dart @@ -35,7 +35,11 @@ class SearchVideoPanel extends StatelessWidget { padding: index == 0 ? const EdgeInsets.only(top: 2) : EdgeInsets.zero, - child: VideoCardH(videoItem: i, showPubdate: true), + child: VideoCardH( + videoItem: i, + showPubdate: true, + source: 'search', + ), ); }, ), diff --git a/lib/pages/subscription_detail/widget/sub_video_card.dart b/lib/pages/subscription_detail/widget/sub_video_card.dart index 11aebc39..dcdee4ef 100644 --- a/lib/pages/subscription_detail/widget/sub_video_card.dart +++ b/lib/pages/subscription_detail/widget/sub_video_card.dart @@ -1,3 +1,4 @@ +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:flutter/material.dart'; import 'package:pilipala/common/constants.dart'; @@ -5,6 +6,7 @@ import 'package:pilipala/common/widgets/stat/danmu.dart'; import 'package:pilipala/common/widgets/stat/view.dart'; import 'package:pilipala/http/search.dart'; import 'package:pilipala/models/common/search_type.dart'; +import 'package:pilipala/utils/image_save.dart'; import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; import '../../../common/widgets/badge.dart'; @@ -40,6 +42,11 @@ class SubVideoCardH extends StatelessWidget { 'videoType': SearchType.video, }); }, + onLongPress: () => imageSaveDialog( + context, + videoItem, + SmartDialog.dismiss, + ), child: Column( children: [ Padding( diff --git a/lib/pages/video/detail/related/view.dart b/lib/pages/video/detail/related/view.dart index 0912724e..fe3b0dca 100644 --- a/lib/pages/video/detail/related/view.dart +++ b/lib/pages/video/detail/related/view.dart @@ -1,9 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/skeleton/video_card_h.dart'; -import 'package:pilipala/common/widgets/animated_dialog.dart'; import 'package:pilipala/common/widgets/http_error.dart'; -import 'package:pilipala/common/widgets/overlay_pop.dart'; import 'package:pilipala/common/widgets/video_card_h.dart'; import './controller.dart'; @@ -54,20 +52,6 @@ class _RelatedVideoPanelState extends State child: VideoCardH( videoItem: relatedVideoList[index], showPubdate: true, - longPress: () { - try { - _releatedController.popupDialog = - _createPopupDialog(_releatedController - .relatedVideoList[index]); - Overlay.of(context) - .insert(_releatedController.popupDialog!); - } catch (err) { - return {}; - } - }, - longPressEnd: () { - _releatedController.popupDialog?.remove(); - }, ), ); } @@ -89,15 +73,4 @@ class _RelatedVideoPanelState extends State }, ); } - - OverlayEntry _createPopupDialog(videoItem) { - return OverlayEntry( - builder: (BuildContext context) => AnimatedDialog( - closeFn: _releatedController.popupDialog?.remove, - child: OverlayPop( - videoItem: videoItem, - closeFn: _releatedController.popupDialog?.remove), - ), - ); - } } diff --git a/lib/utils/download.dart b/lib/utils/download.dart index 2aff8999..42dbbecf 100644 --- a/lib/utils/download.dart +++ b/lib/utils/download.dart @@ -15,24 +15,7 @@ class DownloadUtils { PermissionStatus status = await Permission.storage.status; if (status == PermissionStatus.denied || status == PermissionStatus.permanentlyDenied) { - SmartDialog.show( - useSystem: true, - animationType: SmartAnimationType.centerFade_otherSlide, - builder: (BuildContext context) { - return AlertDialog( - title: const Text('提示'), - content: const Text('存储权限未授权'), - actions: [ - TextButton( - onPressed: () async { - openAppSettings(); - }, - child: const Text('去授权'), - ) - ], - ); - }, - ); + await permissionDialog('提示', '存储权限未授权'); return false; } else { return true; @@ -45,24 +28,7 @@ class DownloadUtils { PermissionStatus status = await Permission.photos.status; if (status == PermissionStatus.denied || status == PermissionStatus.permanentlyDenied) { - SmartDialog.show( - useSystem: true, - animationType: SmartAnimationType.centerFade_otherSlide, - builder: (BuildContext context) { - return AlertDialog( - title: const Text('提示'), - content: const Text('相册权限未授权'), - actions: [ - TextButton( - onPressed: () async { - openAppSettings(); - }, - child: const Text('去授权'), - ) - ], - ); - }, - ); + await permissionDialog('提示', '相册权限未授权'); return false; } else { return true; @@ -72,17 +38,16 @@ class DownloadUtils { static Future downloadImg(String imgUrl, {String imgType = 'cover'}) async { try { - if (!Platform.isAndroid || !await requestPhotoPer()) { - return false; - } - final androidInfo = await DeviceInfoPlugin().androidInfo; - if (androidInfo.version.sdkInt <= 32) { - if (!await requestStoragePer()) { - return false; - } - } else { - if (!await requestPhotoPer()) { - return false; + if (Platform.isAndroid) { + final androidInfo = await DeviceInfoPlugin().androidInfo; + if (androidInfo.version.sdkInt <= 32) { + if (!await requestStoragePer()) { + return false; + } + } else { + if (!await requestPhotoPer()) { + return false; + } } } @@ -101,13 +66,38 @@ class DownloadUtils { ); SmartDialog.dismiss(); if (result.isSuccess) { - await SmartDialog.showToast('「${'$picName.$imgSuffix'}」已保存 '); + SmartDialog.showToast('「${'$picName.$imgSuffix'}」已保存 '); + return true; + } else { + await permissionDialog('保存失败', '相册权限未授权'); + return false; } - return true; } catch (err) { SmartDialog.dismiss(); SmartDialog.showToast(err.toString()); - return true; + return false; } } + + static Future permissionDialog(String title, String content, + {Function? onGranted}) async { + await SmartDialog.show( + useSystem: true, + animationType: SmartAnimationType.centerFade_otherSlide, + builder: (BuildContext context) { + return AlertDialog( + title: Text(title), + content: Text(content), + actions: [ + TextButton( + onPressed: () async { + openAppSettings(); + }, + child: const Text('去授权'), + ) + ], + ); + }, + ); + } } diff --git a/lib/utils/image_save.dart b/lib/utils/image_save.dart new file mode 100644 index 00000000..0cd6915c --- /dev/null +++ b/lib/utils/image_save.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:pilipala/common/constants.dart'; +import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:pilipala/utils/download.dart'; + +Future imageSaveDialog(context, videoItem, closeFn) { + final double imgWidth = + MediaQuery.sizeOf(context).width - StyleString.safeSpace * 2; + return SmartDialog.show( + animationType: SmartAnimationType.centerScale_otherSlide, + builder: (context) => Container( + margin: const EdgeInsets.symmetric(horizontal: StyleString.safeSpace), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.circular(10.0), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + NetworkImgLayer( + width: imgWidth, + height: imgWidth / StyleString.aspectRatio, + src: videoItem.pic! as String, + quality: 100, + ), + Positioned( + right: 8, + top: 8, + child: Container( + width: 30, + height: 30, + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.3), + borderRadius: + const BorderRadius.all(Radius.circular(20))), + child: IconButton( + style: ButtonStyle( + padding: MaterialStateProperty.all(EdgeInsets.zero), + ), + onPressed: () => closeFn!(), + icon: const Icon( + Icons.close, + size: 18, + color: Colors.white, + ), + ), + ), + ), + ], + ), + Padding( + padding: const EdgeInsets.fromLTRB(12, 10, 8, 10), + child: Row( + children: [ + Expanded( + child: Text( + videoItem.title! as String, + style: Theme.of(context).textTheme.titleSmall, + ), + ), + const SizedBox(width: 4), + IconButton( + tooltip: '保存封面图', + onPressed: () async { + bool saveStatus = await DownloadUtils.downloadImg( + videoItem.pic != null + ? videoItem.pic as String + : videoItem.cover as String, + ); + // 保存成功,自动关闭弹窗 + if (saveStatus) { + closeFn?.call(); + } + }, + icon: const Icon(Icons.download, size: 20), + ) + ], + ), + ), + ], + ), + ), + ); +}