feat: 长按保存封面

This commit is contained in:
guozhigq
2024-05-01 19:46:27 +08:00
parent 389747d6f4
commit 7dbd832a80
21 changed files with 430 additions and 515 deletions

View File

@ -49,6 +49,8 @@
<true/> <true/>
<key>NSPhotoLibraryAddUsageDescription</key> <key>NSPhotoLibraryAddUsageDescription</key>
<string>请允许APP保存图片到相册</string> <string>请允许APP保存图片到相册</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>请允许APP保存图片到相册</string>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>App需要您的同意,才能访问相册</string> <string>App需要您的同意,才能访问相册</string>
<key>NSAppleMusicUsageDescription</key> <key>NSAppleMusicUsageDescription</key>

View File

@ -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),
)
],
)),
],
),
);
}
}

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/utils/image_save.dart';
import '../../http/search.dart'; import '../../http/search.dart';
import '../../http/user.dart'; import '../../http/user.dart';
import '../../http/video.dart'; import '../../http/video.dart';
@ -16,8 +17,7 @@ class VideoCardH extends StatelessWidget {
const VideoCardH({ const VideoCardH({
super.key, super.key,
required this.videoItem, required this.videoItem,
this.longPress, this.onPressedFn,
this.longPressEnd,
this.source = 'normal', this.source = 'normal',
this.showOwner = true, this.showOwner = true,
this.showView = true, this.showView = true,
@ -27,8 +27,8 @@ class VideoCardH extends StatelessWidget {
}); });
// ignore: prefer_typing_uninitialized_variables // ignore: prefer_typing_uninitialized_variables
final videoItem; final videoItem;
final Function()? longPress; final Function()? onPressedFn;
final Function()? longPressEnd; // normal 推荐, later 稍后再看, search 搜索
final String source; final String source;
final bool showOwner; final bool showOwner;
final bool showView; final bool showView;
@ -45,109 +45,103 @@ class VideoCardH extends StatelessWidget {
type = videoItem.type; type = videoItem.type;
} catch (_) {} } catch (_) {}
final String heroTag = Utils.makeHeroTag(aid); final String heroTag = Utils.makeHeroTag(aid);
return GestureDetector( return InkWell(
onLongPress: () { onTap: () async {
if (longPress != null) { try {
longPress!(); 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) { onLongPress: () => imageSaveDialog(
// if (longPressEnd != null) { context,
// longPressEnd!(); videoItem,
// } SmartDialog.dismiss,
// }, ),
child: InkWell( child: Padding(
onTap: () async { padding: const EdgeInsets.fromLTRB(
try { StyleString.safeSpace, 5, StyleString.safeSpace, 5),
if (type == 'ketang') { child: LayoutBuilder(
SmartDialog.showToast('课堂视频暂不支持播放'); builder: (BuildContext context, BoxConstraints boxConstraints) {
return; final double width = (boxConstraints.maxWidth -
} StyleString.cardSpace *
final int cid = 6 /
videoItem.cid ?? await SearchHttp.ab2c(aid: aid, bvid: bvid); MediaQuery.textScalerOf(context).scale(1.0)) /
Get.toNamed('/video?bvid=$bvid&cid=$cid', 2;
arguments: {'videoItem': videoItem, 'heroTag': heroTag}); return Container(
} catch (err) { constraints: const BoxConstraints(minHeight: 88),
SmartDialog.showToast(err.toString()); height: width / StyleString.aspectRatio,
} child: Row(
}, mainAxisAlignment: MainAxisAlignment.start,
child: Padding( crossAxisAlignment: CrossAxisAlignment.start,
padding: const EdgeInsets.fromLTRB( children: <Widget>[
StyleString.safeSpace, 5, StyleString.safeSpace, 5), AspectRatio(
child: LayoutBuilder( aspectRatio: StyleString.aspectRatio,
builder: (BuildContext context, BoxConstraints boxConstraints) { child: LayoutBuilder(
final double width = (boxConstraints.maxWidth - builder: (BuildContext context,
StyleString.cardSpace * BoxConstraints boxConstraints) {
6 / final double maxWidth = boxConstraints.maxWidth;
MediaQuery.textScalerOf(context).scale(1.0)) / final double maxHeight = boxConstraints.maxHeight;
2; return Stack(
return Container( children: [
constraints: const BoxConstraints(minHeight: 88), Hero(
height: width / StyleString.aspectRatio, tag: heroTag,
child: Row( child: NetworkImgLayer(
mainAxisAlignment: MainAxisAlignment.start, src: videoItem.pic as String,
crossAxisAlignment: CrossAxisAlignment.start, width: maxWidth,
children: <Widget>[ height: maxHeight,
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( if (videoItem.duration != 0)
text: Utils.timeFormat(videoItem.duration!), PBadge(
right: 6.0, text: Utils.timeFormat(videoItem.duration!),
bottom: 6.0, right: 6.0,
type: 'gray', bottom: 6.0,
), type: 'gray',
if (type != 'video') ),
PBadge( if (type != 'video')
text: type, PBadge(
left: 6.0, text: type,
bottom: 6.0, left: 6.0,
type: 'primary', bottom: 6.0,
), type: 'primary',
// if (videoItem.rcmdReason != null && ),
// videoItem.rcmdReason.content != '') // if (videoItem.rcmdReason != null &&
// pBadge(videoItem.rcmdReason.content, context, // videoItem.rcmdReason.content != '')
// 6.0, 6.0, null, null), // pBadge(videoItem.rcmdReason.content, context,
if (showCharge && videoItem?.isChargingSrc) // 6.0, 6.0, null, null),
const PBadge( if (showCharge && videoItem?.isChargingSrc)
text: '充电专属', const PBadge(
right: 6.0, text: '充电专属',
top: 6.0, right: 6.0,
type: 'primary', top: 6.0,
), type: 'primary',
], ),
); ],
}, );
), },
), ),
VideoContent( ),
videoItem: videoItem, VideoContent(
source: source, videoItem: videoItem,
showOwner: showOwner, source: source,
showView: showView, showOwner: showOwner,
showDanmaku: showDanmaku, showView: showView,
showPubdate: showPubdate, showDanmaku: showDanmaku,
) showPubdate: showPubdate,
], onPressedFn: onPressedFn,
), )
); ],
}, ),
), );
},
), ),
), ),
); );
@ -162,6 +156,7 @@ class VideoContent extends StatelessWidget {
final bool showView; final bool showView;
final bool showDanmaku; final bool showDanmaku;
final bool showPubdate; final bool showPubdate;
final Function()? onPressedFn;
const VideoContent({ const VideoContent({
super.key, super.key,
@ -171,6 +166,7 @@ class VideoContent extends StatelessWidget {
this.showView = true, this.showView = true,
this.showDanmaku = true, this.showDanmaku = true,
this.showPubdate = false, this.showPubdate = false,
this.onPressedFn,
}); });
@override @override
@ -181,7 +177,7 @@ class VideoContent extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (videoItem.title is String) ...[ if (source == 'normal' || source == 'later') ...[
Text( Text(
videoItem.title as String, videoItem.title as String,
textAlign: TextAlign.start, textAlign: TextAlign.start,
@ -196,7 +192,7 @@ class VideoContent extends StatelessWidget {
maxLines: 2, maxLines: 2,
text: TextSpan( text: TextSpan(
children: [ children: [
for (final i in videoItem.title) ...[ for (final i in videoItem.titleList) ...[
TextSpan( TextSpan(
text: i['text'] as String, text: i['text'] as String,
style: TextStyle( 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,
),
)
],
], ],
), ),
], ],

View File

@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/image_save.dart';
import '../../models/model_rec_video_item.dart'; import '../../models/model_rec_video_item.dart';
import 'overlay_pop.dart';
import 'stat/danmu.dart'; import 'stat/danmu.dart';
import 'stat/view.dart'; import 'stat/view.dart';
import '../../http/dynamics.dart'; import '../../http/dynamics.dart';
@ -127,14 +127,11 @@ class VideoCardV extends StatelessWidget {
String heroTag = Utils.makeHeroTag(videoItem.id); String heroTag = Utils.makeHeroTag(videoItem.id);
return InkWell( return InkWell(
onTap: () async => onPushDetail(heroTag), onTap: () async => onPushDetail(heroTag),
onLongPress: () { onLongPress: () => imageSaveDialog(
SmartDialog.show( context,
builder: (context) => OverlayPop( videoItem,
videoItem: videoItem, SmartDialog.dismiss,
closeFn: () => SmartDialog.dismiss(), ),
),
);
},
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
child: Column( child: Column(
children: [ children: [

View File

@ -30,6 +30,7 @@ class BangumiListItemModel {
BangumiListItemModel({ BangumiListItemModel({
this.badge, this.badge,
this.badgeType, this.badgeType,
this.pic,
this.cover, this.cover,
// this.firstEp, // this.firstEp,
this.indexShow, this.indexShow,
@ -50,6 +51,7 @@ class BangumiListItemModel {
String? badge; String? badge;
int? badgeType; int? badgeType;
String? pic;
String? cover; String? cover;
String? indexShow; String? indexShow;
int? isFinish; int? isFinish;
@ -70,6 +72,7 @@ class BangumiListItemModel {
BangumiListItemModel.fromJson(Map<String, dynamic> json) { BangumiListItemModel.fromJson(Map<String, dynamic> json) {
badge = json['badge'] == '' ? null : json['badge']; badge = json['badge'] == '' ? null : json['badge'];
badgeType = json['badge_type']; badgeType = json['badge_type'];
pic = json['cover'];
cover = json['cover']; cover = json['cover'];
indexShow = json['index_show']; indexShow = json['index_show'];
isFinish = json['is_finish']; isFinish = json['is_finish'];

View File

@ -25,6 +25,7 @@ class SearchVideoItemModel {
this.aid, this.aid,
this.bvid, this.bvid,
this.title, this.title,
this.titleList,
this.description, this.description,
this.pic, this.pic,
// this.play, // this.play,
@ -54,8 +55,8 @@ class SearchVideoItemModel {
String? arcurl; String? arcurl;
int? aid; int? aid;
String? bvid; String? bvid;
List? title; String? title;
// List? titleList; List? titleList;
String? description; String? description;
String? pic; String? pic;
// String? play; // String? play;
@ -82,8 +83,9 @@ class SearchVideoItemModel {
aid = json['aid']; aid = json['aid'];
bvid = json['bvid']; bvid = json['bvid'];
mid = json['mid']; mid = json['mid'];
// title = json['title'].replaceAll(RegExp(r'<.*?>'), ''); title = json['title'].replaceAll(RegExp(r'<.*?>'), '');
title = Em.regTitle(json['title']); // title = Em.regTitle(json['title']);
titleList = Em.regTitle(json['title']);
description = json['description']; description = json['description'];
pic = json['pic'] != null && json['pic'].startsWith('//') pic = json['pic'] != null && json['pic'].startsWith('//')
? 'https:${json['pic']}' ? 'https:${json['pic']}'
@ -232,6 +234,7 @@ class SearchLiveItemModel {
this.userCover, this.userCover,
this.type, this.type,
this.title, this.title,
this.titleList,
this.cover, this.cover,
this.pic, this.pic,
this.online, this.online,
@ -251,7 +254,8 @@ class SearchLiveItemModel {
String? face; String? face;
String? userCover; String? userCover;
String? type; String? type;
List? title; String? title;
List? titleList;
String? cover; String? cover;
String? pic; String? pic;
int? online; int? online;
@ -272,7 +276,8 @@ class SearchLiveItemModel {
face = json['uface']; face = json['uface'];
userCover = json['user_cover']; userCover = json['user_cover'];
type = json['type']; type = json['type'];
title = Em.regTitle(json['title']); title = json['title'].replaceAll(RegExp(r'<.*?>'), '');
titleList = Em.regTitle(json['title']);
cover = json['cover']; cover = json['cover'];
pic = json['cover']; pic = json['cover'];
online = json['online']; online = json['online'];
@ -302,6 +307,7 @@ class SearchMBangumiItemModel {
this.type, this.type,
this.mediaId, this.mediaId,
this.title, this.title,
this.titleList,
this.orgTitle, this.orgTitle,
this.mediaType, this.mediaType,
this.cv, this.cv,
@ -328,7 +334,8 @@ class SearchMBangumiItemModel {
String? type; String? type;
int? mediaId; int? mediaId;
List? title; String? title;
List? titleList;
String? orgTitle; String? orgTitle;
int? mediaType; int? mediaType;
String? cv; String? cv;
@ -355,7 +362,8 @@ class SearchMBangumiItemModel {
SearchMBangumiItemModel.fromJson(Map<String, dynamic> json) { SearchMBangumiItemModel.fromJson(Map<String, dynamic> json) {
type = json['type']; type = json['type'];
mediaId = json['media_id']; mediaId = json['media_id'];
title = Em.regTitle(json['title']); title = json['title'].replaceAll(RegExp(r'<.*?>'), '');
titleList = Em.regTitle(json['title']);
orgTitle = json['org_title']; orgTitle = json['org_title'];
mediaType = json['media_type']; mediaType = json['media_type'];
cv = json['cv']; cv = json['cv'];

View File

@ -5,7 +5,9 @@ import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/badge.dart'; import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/http/search.dart'; import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/bangumi/info.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/models/common/search_type.dart';
import 'package:pilipala/utils/image_save.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
@ -14,109 +16,87 @@ class BangumiCardV extends StatelessWidget {
const BangumiCardV({ const BangumiCardV({
super.key, super.key,
required this.bangumiItem, required this.bangumiItem,
this.longPress,
this.longPressEnd,
}); });
final bangumiItem; final BangumiListItemModel bangumiItem;
final Function()? longPress;
final Function()? longPressEnd;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(bangumiItem.mediaId); String heroTag = Utils.makeHeroTag(bangumiItem.mediaId);
return Card( return InkWell(
elevation: 0, onTap: () async {
clipBehavior: Clip.hardEdge, final int seasonId = bangumiItem.seasonId!;
margin: EdgeInsets.zero, SmartDialog.showLoading(msg: '获取中...');
child: GestureDetector( final res = await SearchHttp.bangumiInfo(seasonId: seasonId);
// onLongPress: () { SmartDialog.dismiss().then((value) {
// if (longPress != null) { if (res['status']) {
// longPress!(); if (res['data'].episodes.isEmpty) {
// } SmartDialog.showToast('资源加载失败');
// }, return;
// onLongPressEnd: (details) { }
// if (longPressEnd != null) { EpisodeItem episode = res['data'].episodes.first;
// longPressEnd!(); String bvid = episode.bvid!;
// } int cid = episode.cid!;
// }, String pic = episode.cover!;
child: InkWell( String heroTag = Utils.makeHeroTag(cid);
onTap: () async { Get.toNamed(
final int seasonId = bangumiItem.seasonId; '/video?bvid=$bvid&cid=$cid&seasonId=$seasonId',
SmartDialog.showLoading(msg: '获取中...'); arguments: {
final res = await SearchHttp.bangumiInfo(seasonId: seasonId); 'pic': pic,
SmartDialog.dismiss().then((value) { 'heroTag': heroTag,
if (res['status']) { 'videoType': SearchType.media_bangumi,
if (res['data'].episodes.isEmpty) { 'bangumiItem': res['data'],
SmartDialog.showToast('资源加载失败'); },
return; );
} }
EpisodeItem episode = res['data'].episodes.first; });
String bvid = episode.bvid!; },
int cid = episode.cid!; onLongPress: () =>
String pic = episode.cover!; imageSaveDialog(context, bangumiItem, SmartDialog.dismiss),
String heroTag = Utils.makeHeroTag(cid); child: Column(
Get.toNamed( children: [
'/video?bvid=$bvid&cid=$cid&seasonId=$seasonId', ClipRRect(
arguments: { borderRadius: const BorderRadius.all(
'pic': pic, StyleString.imgRadius,
'heroTag': heroTag, ),
'videoType': SearchType.media_bangumi, child: AspectRatio(
'bangumiItem': res['data'], 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)
],
), ),
); );
} }

View File

@ -1,3 +1,4 @@
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pilipala/common/constants.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/http/video.dart';
import 'package:pilipala/models/common/search_type.dart'; import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/utils/id_utils.dart'; import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/image_save.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import '../../../common/widgets/badge.dart'; import '../../../common/widgets/badge.dart';
@ -61,6 +63,11 @@ class FavVideoCardH extends StatelessWidget {
epId != null ? SearchType.media_bangumi : SearchType.video, epId != null ? SearchType.media_bangumi : SearchType.video,
}); });
}, },
onLongPress: () => imageSaveDialog(
context,
videoItem,
SmartDialog.dismiss,
),
child: Column( child: Column(
children: [ children: [
Padding( Padding(

View File

@ -3,8 +3,6 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/constants.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/skeleton/video_card_h.dart';
import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/video_card_h.dart'; import 'package:pilipala/common/widgets/video_card_h.dart';
@ -78,15 +76,6 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
return VideoCardH( return VideoCardH(
videoItem: _hotController.videoList[index], videoItem: _hotController.videoList[index],
showPubdate: true, showPubdate: true,
longPress: () {
_hotController.popupDialog = _createPopupDialog(
_hotController.videoList[index]);
Overlay.of(context)
.insert(_hotController.popupDialog!);
},
longPressEnd: () {
_hotController.popupDialog?.remove();
},
); );
}, childCount: _hotController.videoList.length), }, childCount: _hotController.videoList.length),
), ),
@ -122,14 +111,4 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
), ),
); );
} }
OverlayEntry _createPopupDialog(videoItem) {
return OverlayEntry(
builder: (context) => AnimatedDialog(
closeFn: _hotController.popupDialog?.remove,
child: OverlayPop(
videoItem: videoItem, closeFn: _hotController.popupDialog?.remove),
),
);
}
} }

View File

@ -84,7 +84,7 @@ class _LaterPageState extends State<LaterPage> {
return VideoCardH( return VideoCardH(
videoItem: videoItem, videoItem: videoItem,
source: 'later', source: 'later',
longPress: () => _laterController.toViewDel( onPressedFn: () => _laterController.toViewDel(
aid: videoItem.aid)); aid: videoItem.aid));
}, childCount: _laterController.laterList.length), }, childCount: _laterController.laterList.length),
) )

View File

@ -5,9 +5,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/skeleton/video_card_v.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/http_error.dart';
import 'package:pilipala/common/widgets/overlay_pop.dart';
import 'package:pilipala/utils/main_stream.dart'; import 'package:pilipala/utils/main_stream.dart';
import 'controller.dart'; import 'controller.dart';
@ -112,16 +110,6 @@ class _LivePageState extends State<LivePage>
); );
} }
OverlayEntry _createPopupDialog(liveItem) {
return OverlayEntry(
builder: (context) => AnimatedDialog(
closeFn: _liveController.popupDialog?.remove,
child: OverlayPop(
videoItem: liveItem, closeFn: _liveController.popupDialog?.remove),
),
);
}
Widget contentGrid(ctr, liveList) { Widget contentGrid(ctr, liveList) {
// double maxWidth = Get.size.width; // double maxWidth = Get.size.width;
// int baseWidth = 500; // int baseWidth = 500;
@ -152,14 +140,6 @@ class _LivePageState extends State<LivePage>
? LiveCardV( ? LiveCardV(
liveItem: liveList[index], liveItem: liveList[index],
crossAxisCount: crossAxisCount, crossAxisCount: crossAxisCount,
longPress: () {
_liveController.popupDialog =
_createPopupDialog(liveList[index]);
Overlay.of(context).insert(_liveController.popupDialog!);
},
longPressEnd: () {
_liveController.popupDialog?.remove();
},
) )
: const VideoCardVSkeleton(); : const VideoCardVSkeleton();
}, },

View File

@ -1,7 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/constants.dart';
import 'package:pilipala/models/live/item.dart'; import 'package:pilipala/models/live/item.dart';
import 'package:pilipala/utils/image_save.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/common/widgets/network_img_layer.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 { class LiveCardV extends StatelessWidget {
final LiveItemModel liveItem; final LiveItemModel liveItem;
final int crossAxisCount; final int crossAxisCount;
final Function()? longPress;
final Function()? longPressEnd;
const LiveCardV({ const LiveCardV({
Key? key, Key? key,
required this.liveItem, required this.liveItem,
required this.crossAxisCount, required this.crossAxisCount,
this.longPress,
this.longPressEnd,
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(liveItem.roomId); String heroTag = Utils.makeHeroTag(liveItem.roomId);
return Card( return InkWell(
elevation: 0, onLongPress: () => imageSaveDialog(
clipBehavior: Clip.hardEdge, context,
margin: EdgeInsets.zero, liveItem,
child: GestureDetector( SmartDialog.dismiss,
onLongPress: () { ),
if (longPress != null) { borderRadius: BorderRadius.circular(16),
longPress!(); onTap: () async {
} Get.toNamed('/liveRoom?roomid=${liveItem.roomId}',
}, arguments: {'liveItem': liveItem, 'heroTag': heroTag});
// onLongPressEnd: (details) { },
// if (longPressEnd != null) { child: Column(
// longPressEnd!(); children: [
// } ClipRRect(
// }, borderRadius: const BorderRadius.all(StyleString.imgRadius),
child: InkWell( child: AspectRatio(
onTap: () async { aspectRatio: StyleString.aspectRatio,
Get.toNamed('/liveRoom?roomid=${liveItem.roomId}', child: LayoutBuilder(builder: (context, boxConstraints) {
arguments: {'liveItem': liveItem, 'heroTag': heroTag}); double maxWidth = boxConstraints.maxWidth;
}, double maxHeight = boxConstraints.maxHeight;
child: Column( return Stack(
children: [ children: [
ClipRRect( Hero(
borderRadius: const BorderRadius.all(StyleString.imgRadius), tag: heroTag,
child: AspectRatio( child: NetworkImgLayer(
aspectRatio: StyleString.aspectRatio, src: liveItem.cover!,
child: LayoutBuilder(builder: (context, boxConstraints) { width: maxWidth,
double maxWidth = boxConstraints.maxWidth; height: maxHeight,
double maxHeight = boxConstraints.maxHeight; ),
return Stack( ),
children: [ if (crossAxisCount != 1)
Hero( Positioned(
tag: heroTag, left: 0,
child: NetworkImgLayer( right: 0,
src: liveItem.cover!, bottom: 0,
width: maxWidth, child: AnimatedOpacity(
height: maxHeight, 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)
],
), ),
); );
} }

View File

@ -1,10 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/badge.dart'; import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/common/widgets/stat/view.dart'; import 'package:pilipala/common/widgets/stat/view.dart';
import 'package:pilipala/http/search.dart'; import 'package:pilipala/http/search.dart';
import 'package:pilipala/utils/image_save.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
class MemberSeasonsItem extends StatelessWidget { class MemberSeasonsItem extends StatelessWidget {
@ -29,6 +31,11 @@ class MemberSeasonsItem extends StatelessWidget {
Get.toNamed('/video?bvid=${seasonItem.bvid}&cid=$cid', Get.toNamed('/video?bvid=${seasonItem.bvid}&cid=$cid',
arguments: {'videoItem': seasonItem, 'heroTag': heroTag}); arguments: {'videoItem': seasonItem, 'heroTag': heroTag});
}, },
onLongPress: () => imageSaveDialog(
context,
seasonItem,
SmartDialog.dismiss,
),
child: Column( child: Column(
children: [ children: [
AspectRatio( AspectRatio(

View File

@ -3,8 +3,6 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/constants.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/skeleton/video_card_h.dart';
import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/video_card_h.dart'; import 'package:pilipala/common/widgets/video_card_h.dart';
@ -82,15 +80,6 @@ class _ZonePageState extends State<ZonePage>
return VideoCardH( return VideoCardH(
videoItem: _zoneController.videoList[index], videoItem: _zoneController.videoList[index],
showPubdate: true, showPubdate: true,
longPress: () {
_zoneController.popupDialog = _createPopupDialog(
_zoneController.videoList[index]);
Overlay.of(context)
.insert(_zoneController.popupDialog!);
},
longPressEnd: () {
_zoneController.popupDialog?.remove();
},
); );
}, childCount: _zoneController.videoList.length), }, childCount: _zoneController.videoList.length),
), ),
@ -126,14 +115,4 @@ class _ZonePageState extends State<ZonePage>
), ),
); );
} }
OverlayEntry _createPopupDialog(videoItem) {
return OverlayEntry(
builder: (context) => AnimatedDialog(
closeFn: _zoneController.popupDialog?.remove,
child: OverlayPop(
videoItem: videoItem, closeFn: _zoneController.popupDialog?.remove),
),
);
}
} }

View File

@ -1,7 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/utils/image_save.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
Widget searchLivePanel(BuildContext context, ctr, list) { Widget searchLivePanel(BuildContext context, ctr, list) {
@ -42,15 +44,15 @@ class LiveItem extends StatelessWidget {
Get.toNamed('/liveRoom?roomid=${liveItem.roomid}', Get.toNamed('/liveRoom?roomid=${liveItem.roomid}',
arguments: {'liveItem': liveItem, 'heroTag': heroTag}); arguments: {'liveItem': liveItem, 'heroTag': heroTag});
}, },
onLongPress: () => imageSaveDialog(
context,
liveItem,
SmartDialog.dismiss,
),
child: Column( child: Column(
children: [ children: [
ClipRRect( ClipRRect(
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.all(StyleString.imgRadius),
topLeft: StyleString.imgRadius,
topRight: StyleString.imgRadius,
bottomLeft: StyleString.imgRadius,
bottomRight: StyleString.imgRadius,
),
child: AspectRatio( child: AspectRatio(
aspectRatio: StyleString.aspectRatio, aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(builder: (context, boxConstraints) { child: LayoutBuilder(builder: (context, boxConstraints) {
@ -108,7 +110,7 @@ class LiveContent extends StatelessWidget {
RichText( RichText(
text: TextSpan( text: TextSpan(
children: [ children: [
for (var i in liveItem.title) ...[ for (var i in liveItem.titleList) ...[
TextSpan( TextSpan(
text: i['text'], text: i['text'],
style: TextStyle( style: TextStyle(

View File

@ -63,7 +63,7 @@ Widget searchMbangumiPanel(BuildContext context, ctr, list) {
style: TextStyle( style: TextStyle(
color: Theme.of(context).colorScheme.onSurface), color: Theme.of(context).colorScheme.onSurface),
children: [ children: [
for (var i in i.title) ...[ for (var i in i.titleList) ...[
TextSpan( TextSpan(
text: i['text'], text: i['text'],
style: TextStyle( style: TextStyle(

View File

@ -35,7 +35,11 @@ class SearchVideoPanel extends StatelessWidget {
padding: index == 0 padding: index == 0
? const EdgeInsets.only(top: 2) ? const EdgeInsets.only(top: 2)
: EdgeInsets.zero, : EdgeInsets.zero,
child: VideoCardH(videoItem: i, showPubdate: true), child: VideoCardH(
videoItem: i,
showPubdate: true,
source: 'search',
),
); );
}, },
), ),

View File

@ -1,3 +1,4 @@
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pilipala/common/constants.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/common/widgets/stat/view.dart';
import 'package:pilipala/http/search.dart'; import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/common/search_type.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/utils/utils.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import '../../../common/widgets/badge.dart'; import '../../../common/widgets/badge.dart';
@ -40,6 +42,11 @@ class SubVideoCardH extends StatelessWidget {
'videoType': SearchType.video, 'videoType': SearchType.video,
}); });
}, },
onLongPress: () => imageSaveDialog(
context,
videoItem,
SmartDialog.dismiss,
),
child: Column( child: Column(
children: [ children: [
Padding( Padding(

View File

@ -1,9 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/skeleton/video_card_h.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/http_error.dart';
import 'package:pilipala/common/widgets/overlay_pop.dart';
import 'package:pilipala/common/widgets/video_card_h.dart'; import 'package:pilipala/common/widgets/video_card_h.dart';
import './controller.dart'; import './controller.dart';
@ -54,20 +52,6 @@ class _RelatedVideoPanelState extends State<RelatedVideoPanel>
child: VideoCardH( child: VideoCardH(
videoItem: relatedVideoList[index], videoItem: relatedVideoList[index],
showPubdate: true, 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<RelatedVideoPanel>
}, },
); );
} }
OverlayEntry _createPopupDialog(videoItem) {
return OverlayEntry(
builder: (BuildContext context) => AnimatedDialog(
closeFn: _releatedController.popupDialog?.remove,
child: OverlayPop(
videoItem: videoItem,
closeFn: _releatedController.popupDialog?.remove),
),
);
}
} }

View File

@ -15,24 +15,7 @@ class DownloadUtils {
PermissionStatus status = await Permission.storage.status; PermissionStatus status = await Permission.storage.status;
if (status == PermissionStatus.denied || if (status == PermissionStatus.denied ||
status == PermissionStatus.permanentlyDenied) { status == PermissionStatus.permanentlyDenied) {
SmartDialog.show( await permissionDialog('提示', '存储权限未授权');
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('去授权'),
)
],
);
},
);
return false; return false;
} else { } else {
return true; return true;
@ -45,24 +28,7 @@ class DownloadUtils {
PermissionStatus status = await Permission.photos.status; PermissionStatus status = await Permission.photos.status;
if (status == PermissionStatus.denied || if (status == PermissionStatus.denied ||
status == PermissionStatus.permanentlyDenied) { status == PermissionStatus.permanentlyDenied) {
SmartDialog.show( await permissionDialog('提示', '相册权限未授权');
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('去授权'),
)
],
);
},
);
return false; return false;
} else { } else {
return true; return true;
@ -72,17 +38,16 @@ class DownloadUtils {
static Future<bool> downloadImg(String imgUrl, static Future<bool> downloadImg(String imgUrl,
{String imgType = 'cover'}) async { {String imgType = 'cover'}) async {
try { try {
if (!Platform.isAndroid || !await requestPhotoPer()) { if (Platform.isAndroid) {
return false; final androidInfo = await DeviceInfoPlugin().androidInfo;
} if (androidInfo.version.sdkInt <= 32) {
final androidInfo = await DeviceInfoPlugin().androidInfo; if (!await requestStoragePer()) {
if (androidInfo.version.sdkInt <= 32) { return false;
if (!await requestStoragePer()) { }
return false; } else {
} if (!await requestPhotoPer()) {
} else { return false;
if (!await requestPhotoPer()) { }
return false;
} }
} }
@ -101,13 +66,38 @@ class DownloadUtils {
); );
SmartDialog.dismiss(); SmartDialog.dismiss();
if (result.isSuccess) { if (result.isSuccess) {
await SmartDialog.showToast('${'$picName.$imgSuffix'}」已保存 '); SmartDialog.showToast('${'$picName.$imgSuffix'}」已保存 ');
return true;
} else {
await permissionDialog('保存失败', '相册权限未授权');
return false;
} }
return true;
} catch (err) { } catch (err) {
SmartDialog.dismiss(); SmartDialog.dismiss();
SmartDialog.showToast(err.toString()); 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('去授权'),
)
],
);
},
);
}
} }

88
lib/utils/image_save.dart Normal file
View File

@ -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),
)
],
),
),
],
),
),
);
}