Merge branch 'main' into design

This commit is contained in:
guozhigq
2024-05-02 21:06:04 +08:00
36 changed files with 605 additions and 598 deletions

27
change_log/1.0.22.0430.md Normal file
View File

@ -0,0 +1,27 @@
## 1.0.22
### 功能
+ 字幕
+ 全屏时选集
+ 动态转发
+ 评论视频并转发
+ 收藏夹删除
+ 合集显示封面
+ 底部导航栏编辑、排序功能
+ 历史记录进度条展示
+ 直播画质切换
+ 排行榜功能
+ 视频详情页推荐视频开关
+ 显示联合投稿up
### 修复
+ 收藏夹个数错误
+ 封面保存权限问题
+ 合集最后1p未展示
+ up主页关注按钮触发灰屏
### 优化
+ 视频简介查看逻辑
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

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

@ -163,4 +163,20 @@ class SearchHttp {
}; };
} }
} }
static Future<Map<String, dynamic>> ab2cWithPic(
{int? aid, String? bvid}) async {
Map<String, dynamic> data = {};
if (aid != null) {
data['aid'] = aid;
} else if (bvid != null) {
data['bvid'] = bvid;
}
final dynamic res =
await Request().get(Api.ab2c, data: <String, dynamic>{...data});
return {
'cid': res.data['data'].first['cid'],
'pic': res.data['data'].first['first_frame'],
};
}
} }

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

@ -25,6 +25,7 @@ class SubItem extends StatelessWidget {
parameters: { parameters: {
'heroTag': heroTag, 'heroTag': heroTag,
'seasonId': subFolderItem.id.toString(), 'seasonId': subFolderItem.id.toString(),
'type': subFolderItem.type.toString(),
}, },
), ),
child: Padding( child: Padding(

View File

@ -14,13 +14,16 @@ class SubDetailController extends GetxController {
RxList<SubDetailMediaItem> subList = <SubDetailMediaItem>[].obs; RxList<SubDetailMediaItem> subList = <SubDetailMediaItem>[].obs;
RxString loadingText = '加载中...'.obs; RxString loadingText = '加载中...'.obs;
int mediaCount = 0; int mediaCount = 0;
late int channelType;
@override @override
void onInit() { void onInit() {
item = Get.arguments; item = Get.arguments;
if (Get.parameters.keys.isNotEmpty) { final parameters = Get.parameters;
seasonId = int.parse(Get.parameters['seasonId']!); if (parameters.isNotEmpty) {
heroTag = Get.parameters['heroTag']!; seasonId = int.tryParse(parameters['seasonId'] ?? '') ?? 0;
heroTag = parameters['heroTag'] ?? '';
channelType = int.tryParse(parameters['type'] ?? '') ?? 0;
} }
super.onInit(); super.onInit();
} }
@ -31,7 +34,7 @@ class SubDetailController extends GetxController {
return; return;
} }
isLoadingMore = true; isLoadingMore = true;
var res = type == 21 var res = channelType == 21
? await UserHttp.userSeasonList( ? await UserHttp.userSeasonList(
seasonId: seasonId, seasonId: seasonId,
ps: 20, ps: 20,

View File

@ -198,8 +198,8 @@ class _SubDetailPageState extends State<SubDetailPage> {
future: _futureBuilderFuture, future: _futureBuilderFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) { if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data; Map? data = snapshot.data;
if (data['status']) { if (data != null && data['status']) {
if (_subDetailController.item.mediaCount == 0) { if (_subDetailController.item.mediaCount == 0) {
return const NoData(); return const NoData();
} else { } else {
@ -219,7 +219,7 @@ class _SubDetailPageState extends State<SubDetailPage> {
} }
} else { } else {
return HttpError( return HttpError(
errMsg: data['msg'], errMsg: data?['msg'] ?? '请求异常',
fn: () => setState(() {}), fn: () => setState(() {}),
); );
} }

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

@ -547,7 +547,7 @@ class VideoDetailController extends GetxController
} }
void updateCover(String? pic) { void updateCover(String? pic) {
if (pic != null && pic != '') { if (pic != null) {
cover.value = videoItem['pic'] = pic; cover.value = videoItem['pic'] = pic;
} }
} }

View File

@ -1,4 +1,5 @@
import 'package:expandable/expandable.dart'; import 'package:expandable/expandable.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@ -265,6 +266,12 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
GestureDetector( GestureDetector(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
onTap: () => showIntroDetail(), onTap: () => showIntroDetail(),
onLongPress: () async {
feedBack();
await Clipboard.setData(
ClipboardData(text: widget.videoDetail!.title!));
SmartDialog.showToast('标题已复制');
},
child: ExpandablePanel( child: ExpandablePanel(
controller: _expandableCtr, controller: _expandableCtr,
collapsed: Text( collapsed: Text(

View File

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.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/utils.dart'; import 'package:pilipala/utils/utils.dart';
class IntroDetail extends StatelessWidget { class IntroDetail extends StatelessWidget {
@ -16,44 +17,47 @@ class IntroDetail extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return SizedBox(
width: double.infinity, width: double.infinity,
child: SelectableRegion( child: Column(
focusNode: FocusNode(), crossAxisAlignment: CrossAxisAlignment.start,
selectionControls: MaterialTextSelectionControls(), children: <Widget>[
child: Column( const SizedBox(height: 4),
crossAxisAlignment: CrossAxisAlignment.start, Row(
children: <Widget>[ children: [
const SizedBox(height: 4), GestureDetector(
Row( onTap: () {
children: [ feedBack();
GestureDetector( Clipboard.setData(ClipboardData(text: videoDetail!.bvid!));
onTap: () { SmartDialog.showToast('已复制');
Clipboard.setData(ClipboardData(text: videoDetail!.bvid!)); },
SmartDialog.showToast('已复制'); child: Text(
}, videoDetail!.bvid!,
child: Text( style: TextStyle(
videoDetail!.bvid!, fontSize: 13,
style: TextStyle( color: Theme.of(context).colorScheme.primary),
fontSize: 13,
color: Theme.of(context).colorScheme.primary),
),
), ),
const SizedBox(width: 10), ),
GestureDetector( const SizedBox(width: 10),
onTap: () { GestureDetector(
Clipboard.setData(ClipboardData(text: videoDetail!.bvid!)); onTap: () {
SmartDialog.showToast('已复制'); feedBack();
}, Clipboard.setData(
child: Text( ClipboardData(text: videoDetail!.aid!.toString()));
videoDetail!.aid!.toString(), SmartDialog.showToast('已复制');
style: TextStyle( },
fontSize: 13, child: Text(
color: Theme.of(context).colorScheme.primary), videoDetail!.aid!.toString(),
), style: TextStyle(
) fontSize: 13,
], color: Theme.of(context).colorScheme.primary),
), ),
const SizedBox(height: 4), )
Text.rich( ],
),
const SizedBox(height: 4),
SelectableRegion(
focusNode: FocusNode(),
selectionControls: MaterialTextSelectionControls(),
child: Text.rich(
style: const TextStyle(height: 1.4), style: const TextStyle(height: 1.4),
TextSpan( TextSpan(
children: [ children: [
@ -61,8 +65,8 @@ class IntroDetail extends StatelessWidget {
], ],
), ),
), ),
], ),
), ],
), ),
); );
} }

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

@ -44,7 +44,7 @@ class ReplyItem extends StatelessWidget {
onTap: () { onTap: () {
feedBack(); feedBack();
if (replyReply != null) { if (replyReply != null) {
replyReply!(replyItem, null); replyReply!(replyItem);
} }
}, },
onLongPress: () { onLongPress: () {
@ -358,7 +358,7 @@ class ReplyItemRow extends StatelessWidget {
InkWell( InkWell(
// 一楼点击评论展开评论详情 // 一楼点击评论展开评论详情
// onTap: () { // onTap: () {
// replyReply?.call(replyItem, replies![i]); // replyReply?.call(replyItem);
// }, // },
onLongPress: () { onLongPress: () {
feedBack(); feedBack();

View File

@ -535,20 +535,20 @@ class _VideoDetailPageState extends State<VideoDetailPage>
controller: _extendNestCtr, controller: _extendNestCtr,
headerSliverBuilder: headerSliverBuilder:
(BuildContext context2, bool innerBoxIsScrolled) { (BuildContext context2, bool innerBoxIsScrolled) {
final Orientation orientation =
MediaQuery.of(context).orientation;
final bool isFullScreen =
plPlayerController?.isFullScreen.value == true;
final double expandedHeight =
orientation == Orientation.landscape || isFullScreen
? (MediaQuery.sizeOf(context).height -
(orientation == Orientation.landscape
? 0
: MediaQuery.of(context).padding.top))
: videoHeight.value;
return <Widget>[ return <Widget>[
Obx( Obx(
() { () {
final Orientation orientation =
MediaQuery.of(context).orientation;
final bool isFullScreen =
plPlayerController?.isFullScreen.value == true;
final double expandedHeight =
orientation == Orientation.landscape || isFullScreen
? (MediaQuery.sizeOf(context).height -
(orientation == Orientation.landscape
? 0
: MediaQuery.of(context).padding.top))
: videoHeight.value;
if (orientation == Orientation.landscape || if (orientation == Orientation.landscape ||
isFullScreen) { isFullScreen) {
enterFullScreen(); enterFullScreen();

View File

@ -395,7 +395,7 @@ class PlPlayerController {
} }
// 配置Player 音轨、字幕等等 // 配置Player 音轨、字幕等等
_videoPlayerController = await _createVideoController( _videoPlayerController = await _createVideoController(
dataSource, _looping, enableHA, width, height); dataSource, _looping, enableHA, width, height, seekTo);
// 获取视频时长 00:00 // 获取视频时长 00:00
_duration.value = duration ?? _videoPlayerController!.state.duration; _duration.value = duration ?? _videoPlayerController!.state.duration;
updateDurationSecond(); updateDurationSecond();
@ -426,6 +426,7 @@ class PlPlayerController {
bool enableHA, bool enableHA,
double? width, double? width,
double? height, double? height,
Duration seekTo,
) async { ) async {
// 每次配置时先移除监听 // 每次配置时先移除监听
removeListeners(); removeListeners();
@ -507,8 +508,9 @@ class PlPlayerController {
play: false, play: false,
); );
} }
player.open( await player.open(
Media(dataSource.videoSource!, httpHeaders: dataSource.httpHeaders), Media(dataSource.videoSource!,
httpHeaders: dataSource.httpHeaders, start: seekTo),
play: false, play: false,
); );
// 音轨 // 音轨
@ -530,9 +532,9 @@ class PlPlayerController {
// } // }
/// 跳转播放 /// 跳转播放
if (seekTo != Duration.zero) { // if (seekTo != Duration.zero) {
await this.seekTo(seekTo); // await this.seekTo(seekTo);
} // }
/// 自动播放 /// 自动播放
if (_autoPlay) { if (_autoPlay) {

View File

@ -167,8 +167,21 @@ class PiliSchame {
print('bilibili.com host: $host'); print('bilibili.com host: $host');
print('bilibili.com path: $path'); print('bilibili.com path: $path');
final String lastPathSegment = path!.split('/').last; final String lastPathSegment = path!.split('/').last;
if (lastPathSegment.contains('BV')) { if (path.startsWith('/video')) {
_videoPush(null, lastPathSegment); if (lastPathSegment.contains('BV')) {
_videoPush(null, lastPathSegment);
}
if (lastPathSegment.contains('av')) {
_videoPush(matchNum(lastPathSegment)[0], null);
}
}
if (path.startsWith('/bangumi')) {
if (lastPathSegment.contains('ss')) {
_bangumiPush(matchNum(lastPathSegment).first, null);
}
if (lastPathSegment.contains('ep')) {
_bangumiPush(null, matchNum(lastPathSegment).first);
}
} }
} else if (host.contains('live')) { } else if (host.contains('live')) {
int roomId = int.parse(path!.split('/').last); int roomId = int.parse(path!.split('/').last);

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

View File

@ -42,12 +42,14 @@ class UrlUtils {
final Map matchRes = IdUtils.matchAvorBv(input: pathSegment); final Map matchRes = IdUtils.matchAvorBv(input: pathSegment);
if (matchRes.containsKey('BV')) { if (matchRes.containsKey('BV')) {
final String bv = matchRes['BV']; final String bv = matchRes['BV'];
final int cid = await SearchHttp.ab2c(bvid: bv); final Map res = await SearchHttp.ab2cWithPic(bvid: bv);
final int cid = res['cid'];
final String pic = res['pic'];
final String heroTag = Utils.makeHeroTag(bv); final String heroTag = Utils.makeHeroTag(bv);
await Get.toNamed( await Get.toNamed(
'/video?bvid=$bv&cid=$cid', '/video?bvid=$bv&cid=$cid',
arguments: <String, String?>{ arguments: <String, String?>{
'pic': '', 'pic': pic,
'heroTag': heroTag, 'heroTag': heroTag,
}, },
); );

View File

@ -873,10 +873,11 @@ packages:
media_kit: media_kit:
dependency: "direct main" dependency: "direct main"
description: description:
name: media_kit path: media_kit
sha256: "3289062540e3b8b9746e5c50d95bd78a9289826b7227e253dff806d002b9e67a" ref: HEAD
url: "https://pub.flutter-io.cn" resolved-ref: "7775f8b1aa5ec77815d5739bf25549fe37f17cae"
source: hosted url: "https://github.com/media-kit/media-kit"
source: git
version: "1.1.10+1" version: "1.1.10+1"
media_kit_libs_android_video: media_kit_libs_android_video:
dependency: transitive dependency: transitive
@ -913,10 +914,11 @@ packages:
media_kit_libs_video: media_kit_libs_video:
dependency: "direct main" dependency: "direct main"
description: description:
name: media_kit_libs_video path: "libs/universal/media_kit_libs_video"
sha256: "3688e0c31482074578652bf038ce6301a5d21e1eda6b54fc3117ffeb4bdba067" ref: HEAD
url: "https://pub.flutter-io.cn" resolved-ref: "7775f8b1aa5ec77815d5739bf25549fe37f17cae"
source: hosted url: "https://github.com/media-kit/media-kit"
source: git
version: "1.0.4" version: "1.0.4"
media_kit_libs_windows_video: media_kit_libs_windows_video:
dependency: transitive dependency: transitive
@ -937,10 +939,11 @@ packages:
media_kit_video: media_kit_video:
dependency: "direct main" dependency: "direct main"
description: description:
name: media_kit_video path: media_kit_video
sha256: c048d11a19e379aebbe810647636e3fc6d18374637e2ae12def4ff8a4b99a882 ref: HEAD
url: "https://pub.flutter-io.cn" resolved-ref: "7775f8b1aa5ec77815d5739bf25549fe37f17cae"
source: hosted url: "https://github.com/media-kit/media-kit"
source: git
version: "1.2.4" version: "1.2.4"
meta: meta:
dependency: transitive dependency: transitive

View File

@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 1.0.21+1021 version: 1.0.22+1022
environment: environment:
sdk: ">=3.0.0 <4.0.0" sdk: ">=3.0.0 <4.0.0"
@ -165,6 +165,20 @@ dev_dependencies:
hive_generator: ^2.0.0 hive_generator: ^2.0.0
build_runner: ^2.4.8 build_runner: ^2.4.8
dependency_overrides:
media_kit:
git:
url: https://github.com/media-kit/media-kit
path: media_kit
media_kit_video:
git:
url: https://github.com/media-kit/media-kit
path: media_kit_video
media_kit_libs_video:
git:
url: https://github.com/media-kit/media-kit
path: libs/universal/media_kit_libs_video
flutter_launcher_icons: flutter_launcher_icons:
android: true android: true
ios: true ios: true