Merge branch 'main' into feature-updateVideoDetailStructure

This commit is contained in:
guozhigq
2024-05-18 19:40:01 +08:00
79 changed files with 1288 additions and 1066 deletions

View File

@ -45,7 +45,7 @@
android:fullBackupContent="false" android:fullBackupContent="false"
tools:replace="android:allowBackup"> tools:replace="android:allowBackup">
<activity <activity
android:name="com.ryanheise.audioservice.AudioServiceActivity" android:name="com.guozhigq.pilipala.MainActivity"
android:exported="true" android:exported="true"
android:launchMode="singleTop" android:launchMode="singleTop"
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"

View File

@ -1,6 +1,8 @@
package com.guozhigq.pilipala package com.guozhigq.pilipala
import io.flutter.embedding.android.FlutterActivity // import io.flutter.embedding.android.FlutterActivity
import com.ryanheise.audioservice.AudioServiceActivity;
class MainActivity: AudioServiceActivity() {
class MainActivity: FlutterActivity() {
} }

27
change_log/1.0.22.0430.md Normal file
View File

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

14
change_log/1.0.23.0504.md Normal file
View File

@ -0,0 +1,14 @@
## 1.0.23
### 功能
+ 封面下载
### 修复
+ 全屏问题
+ 视频播放器灰屏问题
+ 评论区点击区域问题
更多更新日志可在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

@ -34,6 +34,9 @@ class NetworkImgLayer extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final int defaultImgQuality = GlobalData().imgQuality; final int defaultImgQuality = GlobalData().imgQuality;
if (src == '' || src == null) {
return placeholder(context);
}
final String imageUrl = final String imageUrl =
'${src!.startsWith('//') ? 'https:${src!}' : src!}@${quality ?? defaultImgQuality}q.webp'; '${src!.startsWith('//') ? 'https:${src!}' : src!}@${quality ?? defaultImgQuality}q.webp';
int? memCacheWidth, memCacheHeight; int? memCacheWidth, memCacheHeight;

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,11 @@
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/http/constants.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/image_save.dart';
import 'package:pilipala/utils/route_push.dart';
import 'package:pilipala/utils/url_utils.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 +21,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 +31,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,24 +49,27 @@ 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: () {
if (longPress != null) {
longPress!();
}
},
// onLongPressEnd: (details) {
// if (longPressEnd != null) {
// longPressEnd!();
// }
// },
child: InkWell(
onTap: () async { onTap: () async {
try { try {
if (type == 'ketang') { if (type == 'ketang') {
SmartDialog.showToast('课堂视频暂不支持播放'); SmartDialog.showToast('课堂视频暂不支持播放');
return; return;
} }
if (showCharge && videoItem?.typeid == 33) {
final String redirectUrl = await UrlUtils.parseRedirectUrl(
'${HttpString.baseUrl}/video/$bvid/');
final String lastPathSegment = redirectUrl.split('/').last;
if (lastPathSegment.contains('ss')) {
RoutePush.bangumiPush(
Utils.matchNum(lastPathSegment).first, null);
}
if (lastPathSegment.contains('ep')) {
RoutePush.bangumiPush(
null, Utils.matchNum(lastPathSegment).first);
}
return;
}
final int cid = final int cid =
videoItem.cid ?? await SearchHttp.ab2c(aid: aid, bvid: bvid); videoItem.cid ?? await SearchHttp.ab2c(aid: aid, bvid: bvid);
Get.toNamed('/video?bvid=$bvid&cid=$cid', Get.toNamed('/video?bvid=$bvid&cid=$cid',
@ -71,6 +78,11 @@ class VideoCardH extends StatelessWidget {
SmartDialog.showToast(err.toString()); SmartDialog.showToast(err.toString());
} }
}, },
onLongPress: () => imageSaveDialog(
context,
videoItem,
SmartDialog.dismiss,
),
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB( padding: const EdgeInsets.fromLTRB(
StyleString.safeSpace, 5, StyleString.safeSpace, 5), StyleString.safeSpace, 5, StyleString.safeSpace, 5),
@ -142,6 +154,7 @@ class VideoCardH extends StatelessWidget {
showView: showView, showView: showView,
showDanmaku: showDanmaku, showDanmaku: showDanmaku,
showPubdate: showPubdate, showPubdate: showPubdate,
onPressedFn: onPressedFn,
) )
], ],
), ),
@ -149,7 +162,6 @@ class VideoCardH extends StatelessWidget {
}, },
), ),
), ),
),
); );
} }
} }
@ -162,6 +174,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 +184,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 +195,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 +210,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(
@ -263,82 +277,86 @@ class VideoContent extends StatelessWidget {
theme: 'gray', theme: 'gray',
danmu: videoItem.stat.danmaku as int, danmu: videoItem.stat.danmaku as int,
), ),
const Spacer(), const Spacer(),
// SizedBox(
// width: 20,
// height: 20,
// child: IconButton(
// tooltip: '稍后再看',
// style: ButtonStyle(
// padding: MaterialStateProperty.all(EdgeInsets.zero),
// ),
// onPressed: () async {
// var res =
// await UserHttp.toViewLater(bvid: videoItem.bvid);
// SmartDialog.showToast(res['msg']);
// },
// icon: Icon(
// Icons.more_vert_outlined,
// color: Theme.of(context).colorScheme.outline,
// size: 14,
// ),
// ),
// ),
if (source == 'normal') if (source == 'normal')
SizedBox( SizedBox(
width: 24, width: 24,
height: 24, height: 24,
child: PopupMenuButton<String>( child: IconButton(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
onPressed: () {
feedBack();
showModalBottomSheet(
context: context,
useRootNavigator: true,
isScrollControlled: true,
builder: (context) {
return MorePanel(videoItem: videoItem);
},
);
},
icon: Icon( icon: Icon(
Icons.more_vert_outlined, Icons.more_vert_outlined,
color: Theme.of(context).colorScheme.outline, color: Theme.of(context).colorScheme.outline,
size: 14, size: 14,
), ),
position: PopupMenuPosition.under, ),
// constraints: const BoxConstraints(maxHeight: 35), ),
onSelected: (String type) {}, if (source == 'later') ...[
itemBuilder: (BuildContext context) => IconButton(
<PopupMenuEntry<String>>[ style: ButtonStyle(
PopupMenuItem<String>( padding: MaterialStateProperty.all(EdgeInsets.zero),
onTap: () async { ),
var res = await UserHttp.toViewLater( onPressed: () => onPressedFn?.call(),
bvid: videoItem.bvid as String); icon: Icon(
SmartDialog.showToast(res['msg']); Icons.clear_outlined,
}, color: Theme.of(context).colorScheme.outline,
value: 'pause', size: 18,
height: 40, ),
child: const Row( )
children: [ ],
Icon(Icons.watch_later_outlined, size: 16), ],
SizedBox(width: 6), ),
Text('稍后再看', style: TextStyle(fontSize: 13))
], ],
), ),
), ),
const PopupMenuDivider(), );
PopupMenuItem<String>( }
onTap: () async { }
class MorePanel extends StatelessWidget {
final dynamic videoItem;
const MorePanel({super.key, required this.videoItem});
Future<dynamic> menuActionHandler(String type) async {
switch (type) {
case 'block':
blockUser();
break;
case 'watchLater':
var res = await UserHttp.toViewLater(bvid: videoItem.bvid as String);
SmartDialog.showToast(res['msg']);
Get.back();
break;
default:
}
}
void blockUser() async {
SmartDialog.show( SmartDialog.show(
useSystem: true, useSystem: true,
animationType: animationType: SmartAnimationType.centerFade_otherSlide,
SmartAnimationType.centerFade_otherSlide,
builder: (BuildContext context) { builder: (BuildContext context) {
return AlertDialog( return AlertDialog(
title: const Text('提示'), title: const Text('提示'),
content: Text( content: Text('确定拉黑:${videoItem.owner.name}(${videoItem.owner.mid})?'
'确定拉黑:${videoItem.owner.name}(${videoItem.owner.mid})?'
'\n\n被拉黑的Up可以在隐私设置-黑名单管理中解除'), '\n\n被拉黑的Up可以在隐私设置-黑名单管理中解除'),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => SmartDialog.dismiss(), onPressed: () => SmartDialog.dismiss(),
child: Text( child: Text(
'点错了', '点错了',
style: TextStyle( style: TextStyle(color: Theme.of(context).colorScheme.outline),
color: Theme.of(context)
.colorScheme
.outline),
), ),
), ),
TextButton( TextButton(
@ -349,9 +367,7 @@ class VideoContent extends StatelessWidget {
reSrc: 11, reSrc: 11,
); );
SmartDialog.dismiss(); SmartDialog.dismiss();
SmartDialog.showToast(res['code'] == 0 SmartDialog.showToast(res['msg'] ?? '成功');
? '成功'
: res['msg']);
}, },
child: const Text('确认'), child: const Text('确认'),
) )
@ -359,26 +375,58 @@ class VideoContent extends StatelessWidget {
); );
}, },
); );
}, }
value: 'pause',
height: 40, @override
child: Row( Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [ children: [
const Icon(Icons.block, size: 16), InkWell(
const SizedBox(width: 6), onTap: () => Get.back(),
Text('拉黑:${videoItem.owner.name}', child: Container(
style: const TextStyle(fontSize: 13)) height: 35,
padding: const EdgeInsets.only(bottom: 2),
child: Center(
child: Container(
width: 32,
height: 3,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.outline,
borderRadius: const BorderRadius.all(Radius.circular(3))),
),
),
),
),
ListTile(
onTap: () async => await menuActionHandler('block'),
minLeadingWidth: 0,
leading: const Icon(Icons.block, size: 19),
title: Text(
'拉黑up主 「${videoItem.owner.name}',
style: Theme.of(context).textTheme.titleSmall,
),
),
ListTile(
onTap: () async => await menuActionHandler('watchLater'),
minLeadingWidth: 0,
leading: const Icon(Icons.watch_later_outlined, size: 19),
title:
Text('添加至稍后再看', style: Theme.of(context).textTheme.titleSmall),
),
ListTile(
onTap: () =>
imageSaveDialog(context, videoItem, SmartDialog.dismiss),
minLeadingWidth: 0,
leading: const Icon(Icons.photo_outlined, size: 19),
title:
Text('查看视频封面', style: Theme.of(context).textTheme.titleSmall),
),
const SizedBox(height: 20),
], ],
), ),
),
],
),
),
],
),
],
),
),
); );
} }
} }

View File

@ -2,15 +2,14 @@ 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 'package:pilipala/utils/route_push.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';
import '../../http/search.dart';
import '../../http/user.dart'; import '../../http/user.dart';
import '../../http/video.dart'; import '../../http/video.dart';
import '../../models/common/search_type.dart';
import '../../utils/id_utils.dart'; import '../../utils/id_utils.dart';
import '../../utils/utils.dart'; import '../../utils/utils.dart';
import '../constants.dart'; import '../constants.dart';
@ -42,23 +41,11 @@ class VideoCardV extends StatelessWidget {
return; return;
} }
int epId = videoItem.param; int epId = videoItem.param;
SmartDialog.showLoading(msg: '资源获取中'); RoutePush.bangumiPush(
var result = await SearchHttp.bangumiInfo(seasonId: null, epId: epId); null,
if (result['status']) { epId,
var bangumiDetail = result['data']; heroTag: heroTag,
int cid = bangumiDetail.episodes!.first.cid;
String bvid = IdUtils.av2bv(bangumiDetail.episodes!.first.aid);
SmartDialog.dismiss().then(
(value) => Get.toNamed(
'/video?bvid=$bvid&cid=$cid&epId=$epId',
arguments: {
'pic': videoItem.pic,
'heroTag': heroTag,
'videoType': SearchType.media_bangumi,
},
),
); );
}
break; break;
case 'av': case 'av':
String bvid = videoItem.bvid ?? IdUtils.av2bv(videoItem.aid); String bvid = videoItem.bvid ?? IdUtils.av2bv(videoItem.aid);
@ -127,14 +114,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: [
@ -249,6 +233,7 @@ class VideoContent extends StatelessWidget {
width: 24, width: 24,
height: 24, height: 24,
child: IconButton( child: IconButton(
padding: EdgeInsets.zero,
onPressed: () { onPressed: () {
feedBack(); feedBack();
showModalBottomSheet( showModalBottomSheet(
@ -402,6 +387,15 @@ class MorePanel extends StatelessWidget {
title: title:
Text('添加至稍后再看', style: Theme.of(context).textTheme.titleSmall), Text('添加至稍后再看', style: Theme.of(context).textTheme.titleSmall),
), ),
ListTile(
onTap: () =>
imageSaveDialog(context, videoItem, SmartDialog.dismiss),
minLeadingWidth: 0,
leading: const Icon(Icons.photo_outlined, size: 19),
title:
Text('查看视频封面', style: Theme.of(context).textTheme.titleSmall),
),
const SizedBox(height: 20),
], ],
), ),
); );

View File

@ -520,4 +520,7 @@ class Api {
/// 删除收藏夹 /// 删除收藏夹
static const String delFavFolder = '/x/v3/fav/folder/del'; static const String delFavFolder = '/x/v3/fav/folder/del';
/// 搜索结果计数
static const String searchCount = '/x/web-interface/wbi/search/all/v2';
} }

View File

@ -41,6 +41,7 @@ class DynamicsHttp {
'status': false, 'status': false,
'data': [], 'data': [],
'msg': res.data['message'], 'msg': res.data['message'],
'code': res.data['code'],
}; };
} }
} }

View File

@ -46,7 +46,7 @@ class ApiInterceptor extends Interceptor {
// 处理网络请求错误 // 处理网络请求错误
// handler.next(err); // handler.next(err);
String url = err.requestOptions.uri.toString(); String url = err.requestOptions.uri.toString();
if (!url.contains('heartBeat')) { if (!url.contains('heartbeat')) {
SmartDialog.showToast( SmartDialog.showToast(
await dioError(err), await dioError(err),
displayType: SmartToastType.onlyRefresh, displayType: SmartToastType.onlyRefresh,

View File

@ -1,5 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/models/search/all.dart';
import 'package:pilipala/utils/wbi_sign.dart';
import '../models/bangumi/info.dart'; import '../models/bangumi/info.dart';
import '../models/common/search_type.dart'; import '../models/common/search_type.dart';
import '../models/search/hot.dart'; import '../models/search/hot.dart';
@ -163,4 +165,42 @@ 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'],
};
}
static Future<Map<String, dynamic>> searchCount(
{required String keyword}) async {
Map<String, dynamic> data = {
'keyword': keyword,
'web_location': 333.999,
};
Map params = await WbiSign().makSign(data);
final dynamic res = await Request().get(Api.searchCount, data: params);
if (res.data['code'] == 0) {
return {
'status': true,
'data': SearchAllModel.fromJson(res.data['data']),
};
} else {
return {
'status': false,
'data': [],
'msg': '请求错误 🙅',
};
}
}
} }

View File

@ -62,7 +62,8 @@ class UserHttp {
return { return {
'status': false, 'status': false,
'data': [], 'data': [],
'msg': res.data['message'] ?? '账号未登录' 'msg': res.data['message'],
'code': res.data['code'],
}; };
} }
} }
@ -111,7 +112,12 @@ class UserHttp {
'data': {'list': list, 'count': res.data['data']['count']} 'data': {'list': list, 'count': res.data['data']['count']}
}; };
} else { } else {
return {'status': false, 'data': [], 'msg': res.data['message']}; return {
'status': false,
'data': [],
'msg': res.data['message'],
'code': res.data['code'],
};
} }
} }
@ -126,7 +132,12 @@ class UserHttp {
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return {'status': true, 'data': HistoryData.fromJson(res.data['data'])}; return {'status': true, 'data': HistoryData.fromJson(res.data['data'])};
} else { } else {
return {'status': false, 'data': [], 'msg': res.data['message']}; return {
'status': false,
'data': [],
'msg': res.data['message'],
'code': res.data['code'],
};
} }
} }
@ -326,7 +337,12 @@ class UserHttp {
'data': SubFolderModelData.fromJson(res.data['data']) 'data': SubFolderModelData.fromJson(res.data['data'])
}; };
} else { } else {
return {'status': false, 'msg': res.data['message']}; return {
'status': false,
'data': [],
'msg': res.data['message'],
'code': res.data['code'],
};
} }
} }

View File

@ -1,5 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
@ -67,9 +68,8 @@ void main() async {
// 小白条、导航栏沉浸 // 小白条、导航栏沉浸
if (Platform.isAndroid) { if (Platform.isAndroid) {
List<String> versionParts = Platform.version.split('.'); final androidInfo = await DeviceInfoPlugin().androidInfo;
int androidVersion = int.parse(versionParts[0]); if (androidInfo.version.sdkInt >= 29) {
if (androidVersion >= 29) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
} }
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(

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

@ -415,6 +415,7 @@ class DynamicMajorModel {
this.type, this.type,
this.courses, this.courses,
this.common, this.common,
this.music,
}); });
DynamicArchiveModel? archive; DynamicArchiveModel? archive;
@ -431,6 +432,7 @@ class DynamicMajorModel {
String? type; String? type;
Map? courses; Map? courses;
Map? common; Map? common;
Map? music;
DynamicMajorModel.fromJson(Map<String, dynamic> json) { DynamicMajorModel.fromJson(Map<String, dynamic> json) {
archive = json['archive'] != null archive = json['archive'] != null
@ -455,6 +457,7 @@ class DynamicMajorModel {
type = json['type']; type = json['type'];
courses = json['courses'] ?? {}; courses = json['courses'] ?? {};
common = json['common'] ?? {}; common = json['common'] ?? {};
music = json['music'] ?? {};
} }
} }

View File

@ -0,0 +1,9 @@
class SearchAllModel {
SearchAllModel({this.topTList});
Map? topTList;
SearchAllModel.fromJson(Map<String, dynamic> json) {
topTList = json['top_tlist'];
}
}

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

@ -218,7 +218,7 @@ class AboutController extends GetxController {
RxString currentVersion = ''.obs; RxString currentVersion = ''.obs;
RxString remoteVersion = ''.obs; RxString remoteVersion = ''.obs;
late LatestDataModel remoteAppInfo; late LatestDataModel remoteAppInfo;
RxBool isUpdate = true.obs; RxBool isUpdate = false.obs;
RxBool isLoading = true.obs; RxBool isLoading = true.obs;
late LatestDataModel data; late LatestDataModel data;

View File

@ -194,7 +194,8 @@ class _BangumiInfoState extends State<BangumiInfo> {
src: widget.bangumiDetail!.cover!, src: widget.bangumiDetail!.cover!,
), ),
PBadge( PBadge(
text: '评分 ${widget.bangumiDetail!.rating!['score']!}', text:
'评分 ${widget.bangumiDetail?.rating?['score']! ?? '暂无'}',
top: null, top: null,
right: 6, right: 6,
bottom: 6, bottom: 6,

View File

@ -1,11 +1,10 @@
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: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/http/search.dart'; import 'package:pilipala/models/bangumi/list.dart';
import 'package:pilipala/models/bangumi/info.dart'; import 'package:pilipala/utils/image_save.dart';
import 'package:pilipala/models/common/search_type.dart'; import 'package:pilipala/utils/route_push.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,68 +13,28 @@ 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: () {
clipBehavior: Clip.hardEdge, RoutePush.bangumiPush(
margin: EdgeInsets.zero, bangumiItem.seasonId,
child: GestureDetector( null,
// onLongPress: () { heroTag: heroTag,
// if (longPress != null) {
// longPress!();
// }
// },
// onLongPressEnd: (details) {
// if (longPressEnd != null) {
// longPressEnd!();
// }
// },
child: InkWell(
onTap: () async {
final int seasonId = bangumiItem.seasonId;
SmartDialog.showLoading(msg: '获取中...');
final res = await SearchHttp.bangumiInfo(seasonId: seasonId);
SmartDialog.dismiss().then((value) {
if (res['status']) {
if (res['data'].episodes.isEmpty) {
SmartDialog.showToast('资源加载失败');
return;
}
EpisodeItem episode = res['data'].episodes.first;
String bvid = episode.bvid!;
int cid = episode.cid!;
String pic = episode.cover!;
String heroTag = Utils.makeHeroTag(cid);
Get.toNamed(
'/video?bvid=$bvid&cid=$cid&seasonId=$seasonId',
arguments: {
'pic': pic,
'heroTag': heroTag,
'videoType': SearchType.media_bangumi,
'bangumiItem': res['data'],
},
); );
}
});
}, },
onLongPress: () =>
imageSaveDialog(context, bangumiItem, SmartDialog.dismiss),
child: Column( child: Column(
children: [ children: [
ClipRRect( ClipRRect(
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.all(
topLeft: StyleString.imgRadius, StyleString.imgRadius,
topRight: StyleString.imgRadius,
bottomLeft: StyleString.imgRadius,
bottomRight: StyleString.imgRadius,
), ),
child: AspectRatio( child: AspectRatio(
aspectRatio: 0.65, aspectRatio: 0.65,
@ -116,8 +75,6 @@ class BangumiCardV extends StatelessWidget {
BangumiContent(bangumiItem: bangumiItem) BangumiContent(bangumiItem: bangumiItem)
], ],
), ),
),
),
); );
} }
} }

View File

@ -14,6 +14,7 @@ import 'package:pilipala/models/dynamics/up.dart';
import 'package:pilipala/models/live/item.dart'; import 'package:pilipala/models/live/item.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/id_utils.dart'; import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/route_push.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
@ -70,7 +71,7 @@ class DynamicsController extends GetxController {
Future queryFollowDynamic({type = 'init'}) async { Future queryFollowDynamic({type = 'init'}) async {
if (!userLogin.value) { if (!userLogin.value) {
return {'status': false, 'msg': '账号未登录'}; return {'status': false, 'msg': '账号未登录', 'code': -101};
} }
if (type == 'init') { if (type == 'init') {
dynamicsList.clear(); dynamicsList.clear();
@ -220,25 +221,7 @@ class DynamicsController extends GetxController {
print('DYNAMIC_TYPE_PGC_UNION 番剧'); print('DYNAMIC_TYPE_PGC_UNION 番剧');
DynamicArchiveModel pgc = item.modules.moduleDynamic.major.pgc; DynamicArchiveModel pgc = item.modules.moduleDynamic.major.pgc;
if (pgc.epid != null) { if (pgc.epid != null) {
SmartDialog.showLoading(msg: '获取中...'); RoutePush.bangumiPush(null, pgc.epid);
var res = await SearchHttp.bangumiInfo(epId: pgc.epid);
SmartDialog.dismiss();
if (res['status']) {
EpisodeItem episode = res['data'].episodes.first;
String bvid = episode.bvid!;
int cid = episode.cid!;
String pic = episode.cover!;
String heroTag = Utils.makeHeroTag(cid);
Get.toNamed(
'/video?bvid=$bvid&cid=$cid&seasonId=${res['data'].seasonId}',
arguments: {
'pic': pic,
'heroTag': heroTag,
'videoType': SearchType.media_bangumi,
'bangumiItem': res['data'],
},
);
}
} }
break; break;
} }
@ -246,7 +229,7 @@ class DynamicsController extends GetxController {
Future queryFollowUp({type = 'init'}) async { Future queryFollowUp({type = 'init'}) async {
if (!userLogin.value) { if (!userLogin.value) {
return {'status': false, 'msg': '账号未登录'}; return {'status': false, 'msg': '账号未登录', 'code': -101};
} }
if (type == 'init') { if (type == 'init') {
upData.value.upList = <UpItem>[]; upData.value.upList = <UpItem>[];

View File

@ -196,7 +196,7 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
centerTitle: false, centerTitle: false,
titleSpacing: 0, titleSpacing: 0,
title: StreamBuilder( title: StreamBuilder(
stream: titleStreamC.stream.distinct(), stream: titleStreamC.stream,
initialData: false, initialData: false,
builder: (context, AsyncSnapshot snapshot) { builder: (context, AsyncSnapshot snapshot) {
return AnimatedOpacity( return AnimatedOpacity(

View File

@ -11,6 +11,7 @@ import 'package:pilipala/common/widgets/no_data.dart';
import 'package:pilipala/models/dynamics/result.dart'; import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/main_stream.dart'; import 'package:pilipala/utils/main_stream.dart';
import 'package:pilipala/utils/route_push.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import '../mine/controller.dart'; import '../mine/controller.dart';
@ -224,8 +225,8 @@ class _DynamicsPageState extends State<DynamicsPage>
if (snapshot.data == null) { if (snapshot.data == null) {
return const SliverToBoxAdapter(child: SizedBox()); return const SliverToBoxAdapter(child: SizedBox());
} }
Map data = snapshot.data; Map? data = snapshot.data;
if (data['status']) { if (data != null && data['status']) {
List<DynamicItemModel> list = List<DynamicItemModel> list =
_dynamicsController.dynamicsList; _dynamicsController.dynamicsList;
return Obx( return Obx(
@ -248,24 +249,21 @@ class _DynamicsPageState extends State<DynamicsPage>
} }
}, },
); );
} else if (data['msg'] == "账号未登录") {
return HttpError(
errMsg: data['msg'],
btnText: "去登录",
fn: () {
mineController.onLogin();
},
);
} else { } else {
return HttpError( return HttpError(
errMsg: data['msg'], errMsg: data?['msg'] ?? '请求异常',
btnText: data?['code'] == -101 ? '去登录' : null,
fn: () { fn: () {
if (data?['code'] == -101) {
RoutePush.loginRedirectPush();
} else {
setState(() { setState(() {
_futureBuilderFuture = _futureBuilderFuture =
_dynamicsController.queryFollowDynamic(); _dynamicsController.queryFollowDynamic();
_futureBuilderFutureUp = _futureBuilderFutureUp =
_dynamicsController.queryFollowUp(); _dynamicsController.queryFollowUp();
}); });
}
}, },
); );
} }

View File

@ -1,14 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/pages/dynamics/index.dart'; import 'package:pilipala/pages/dynamics/index.dart';
import '../../../models/dynamics/result.dart';
import 'action_panel.dart'; import 'action_panel.dart';
import 'author_panel.dart'; import 'author_panel.dart';
import 'content_panel.dart'; import 'content_panel.dart';
import 'forward_panel.dart'; import 'forward_panel.dart';
class DynamicPanel extends StatelessWidget { class DynamicPanel extends StatelessWidget {
final DynamicItemModel item; final dynamic item;
final String? source; final String? source;
DynamicPanel({required this.item, this.source, Key? key}) : super(key: key); DynamicPanel({required this.item, this.source, Key? key}) : super(key: key);
final DynamicsController _dynamicsController = Get.put(DynamicsController()); final DynamicsController _dynamicsController = Get.put(DynamicsController());

View File

@ -238,6 +238,61 @@ Widget forWard(item, context, ctr, source, {floor = 1}) {
), ),
), ),
); );
case 'DYNAMIC_TYPE_MUSIC':
final Map music = item.modules.moduleDynamic.major.music;
return Padding(
padding: const EdgeInsets.only(top: 8),
child: InkWell(
onTap: () {
Get.toNamed('/webview', parameters: {
'url': "https:${music['jump_url']}",
'type': 'url',
'pageTitle': music['title']
});
},
child: Container(
width: double.infinity,
padding:
const EdgeInsets.only(left: 12, top: 10, right: 12, bottom: 10),
color: Theme.of(context).dividerColor.withOpacity(0.08),
child: Row(
children: [
NetworkImgLayer(
width: 45,
height: 45,
src: music['cover'],
),
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
music['title'],
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
music['label'],
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
)
],
),
// TextButton(onPressed: () {}, child: Text('123'))
),
),
);
default: default:
return const SizedBox( return const SizedBox(
width: double.infinity, width: double.infinity,

View File

@ -80,15 +80,12 @@ Widget videoSeasonWidget(item, context, type, {floor = 1}) {
double width = box.maxWidth; double width = box.maxWidth;
return Stack( return Stack(
children: [ children: [
Hero( NetworkImgLayer(
tag: content.bvid,
child: NetworkImgLayer(
type: floor == 1 ? 'emote' : null, type: floor == 1 ? 'emote' : null,
width: width, width: width,
height: width / StyleString.aspectRatio, height: width / StyleString.aspectRatio,
src: content.cover, src: content.cover,
), ),
),
if (content.badge != null && type == 'pgc') if (content.badge != null && type == 'pgc')
PBadge( PBadge(
text: content.badge['text'], text: content.badge['text'],

View File

@ -17,10 +17,15 @@ class FavController extends GetxController {
int pageSize = 60; int pageSize = 60;
RxBool hasMore = true.obs; RxBool hasMore = true.obs;
Future<dynamic> queryFavFolder({type = 'init'}) async { @override
void onInit() {
userInfo = userInfoCache.get('userInfoCache'); userInfo = userInfoCache.get('userInfoCache');
super.onInit();
}
Future<dynamic> queryFavFolder({type = 'init'}) async {
if (userInfo == null) { if (userInfo == null) {
return {'status': false, 'msg': '账号未登录'}; return {'status': false, 'msg': '账号未登录', 'code': -101};
} }
if (!hasMore.value) { if (!hasMore.value) {
return; return;

View File

@ -4,6 +4,7 @@ import 'package:get/get.dart';
import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/pages/fav/index.dart'; import 'package:pilipala/pages/fav/index.dart';
import 'package:pilipala/pages/fav/widgets/item.dart'; import 'package:pilipala/pages/fav/widgets/item.dart';
import 'package:pilipala/utils/route_push.dart';
class FavPage extends StatefulWidget { class FavPage extends StatefulWidget {
const FavPage({super.key}); const FavPage({super.key});
@ -57,8 +58,8 @@ class _FavPageState extends State<FavPage> {
future: _futureBuilderFuture, future: _futureBuilderFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) { if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map; Map? data = snapshot.data;
if (data['status']) { if (data != null && data['status']) {
return Obx( return Obx(
() => ListView.builder( () => ListView.builder(
controller: scrollController, controller: scrollController,
@ -74,8 +75,18 @@ class _FavPageState extends State<FavPage> {
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
slivers: [ slivers: [
HttpError( HttpError(
errMsg: data['msg'], errMsg: data?['msg'] ?? '请求异常',
fn: () => setState(() {}), btnText: data?['code'] == -101 ? '去登录' : null,
fn: () {
if (data?['code'] == -101) {
RoutePush.loginRedirectPush();
} else {
setState(() {
_futureBuilderFuture =
_favController.queryFavFolder();
});
}
},
), ),
], ],
); );

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

@ -4,6 +4,7 @@ import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/http/user.dart'; import 'package:pilipala/http/user.dart';
import 'package:pilipala/models/user/history.dart'; import 'package:pilipala/models/user/history.dart';
import 'package:pilipala/models/user/info.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
class HistoryController extends GetxController { class HistoryController extends GetxController {
@ -15,14 +16,20 @@ class HistoryController extends GetxController {
RxBool isLoading = false.obs; RxBool isLoading = false.obs;
RxBool enableMultiple = false.obs; RxBool enableMultiple = false.obs;
RxInt checkedCount = 0.obs; RxInt checkedCount = 0.obs;
Box userInfoCache = GStrorage.userInfo;
UserInfoData? userInfo;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
historyStatus(); historyStatus();
userInfo = userInfoCache.get('userInfoCache');
} }
Future queryHistoryList({type = 'init'}) async { Future queryHistoryList({type = 'init'}) async {
if (userInfo == null) {
return {'status': false, 'msg': '账号未登录', 'code': -101};
}
int max = 0; int max = 0;
int viewAt = 0; int viewAt = 0;
if (type == 'onload') { if (type == 'onload') {

View File

@ -5,6 +5,7 @@ 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/no_data.dart'; import 'package:pilipala/common/widgets/no_data.dart';
import 'package:pilipala/pages/history/index.dart'; import 'package:pilipala/pages/history/index.dart';
import 'package:pilipala/utils/route_push.dart';
import 'widgets/item.dart'; import 'widgets/item.dart';
@ -183,8 +184,8 @@ class _HistoryPageState extends State<HistoryPage> {
if (snapshot.data == null) { if (snapshot.data == null) {
return const SliverToBoxAdapter(child: SizedBox()); return const SliverToBoxAdapter(child: SizedBox());
} }
Map data = snapshot.data; Map? data = snapshot.data;
if (data['status']) { if (data != null && data['status']) {
return Obx( return Obx(
() => _historyController.historyList.isNotEmpty () => _historyController.historyList.isNotEmpty
? SliverList( ? SliverList(
@ -209,8 +210,18 @@ class _HistoryPageState extends State<HistoryPage> {
); );
} else { } else {
return HttpError( return HttpError(
errMsg: data['msg'], errMsg: data?['msg'] ?? '请求异常',
fn: () => setState(() {}), btnText: data?['code'] == -101 ? '去登录' : null,
fn: () {
if (data?['code'] == -101) {
RoutePush.loginRedirectPush();
} else {
setState(() {
_futureBuilderFuture =
_historyController.queryHistoryList();
});
}
},
); );
} }
} else { } else {

View File

@ -7,13 +7,13 @@ import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/http/search.dart'; import 'package:pilipala/http/search.dart';
import 'package:pilipala/http/user.dart'; import 'package:pilipala/http/user.dart';
import 'package:pilipala/http/video.dart'; import 'package:pilipala/http/video.dart';
import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/common/business_type.dart'; import 'package:pilipala/models/common/business_type.dart';
import 'package:pilipala/models/common/search_type.dart'; import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/models/live/item.dart'; import 'package:pilipala/models/live/item.dart';
import 'package:pilipala/pages/history_search/index.dart'; import 'package:pilipala/pages/history_search/index.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/id_utils.dart'; import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/route_push.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
class HistoryItem extends StatelessWidget { class HistoryItem extends StatelessWidget {
@ -101,28 +101,13 @@ class HistoryItem extends StatelessWidget {
} }
} else { } else {
if (videoItem.history.epid != '') { if (videoItem.history.epid != '') {
SmartDialog.showLoading(msg: '获取中...'); RoutePush.bangumiPush(
var res = null,
await SearchHttp.bangumiInfo(epId: videoItem.history.epid); videoItem.history.epid,
SmartDialog.dismiss(); heroTag: heroTag,
if (res['status']) {
EpisodeItem episode = res['data'].episodes.first;
String bvid = episode.bvid!;
int cid = episode.cid!;
String pic = episode.cover!;
String heroTag = Utils.makeHeroTag(cid);
Get.toNamed(
'/video?bvid=$bvid&cid=$cid&seasonId=${res['data'].seasonId}',
arguments: {
'pic': pic,
'heroTag': heroTag,
'videoType': SearchType.media_bangumi,
'bangumiItem': res['data'],
},
); );
} }
} }
}
} else { } else {
int cid = videoItem.history.cid ?? int cid = videoItem.history.cid ??
// videoItem.history.oid ?? // videoItem.history.oid ??
@ -213,7 +198,8 @@ class HistoryItem extends StatelessWidget {
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(
StyleString.imgRadius.x),
color: Colors.black.withOpacity( color: Colors.black.withOpacity(
ctr!.enableMultiple.value && ctr!.enableMultiple.value &&
videoItem.checked videoItem.checked

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

@ -1,16 +1,30 @@
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:hive/hive.dart';
import 'package:pilipala/http/user.dart'; import 'package:pilipala/http/user.dart';
import 'package:pilipala/models/model_hot_video_item.dart'; import 'package:pilipala/models/model_hot_video_item.dart';
import 'package:pilipala/models/user/info.dart';
import 'package:pilipala/utils/storage.dart';
class LaterController extends GetxController { class LaterController extends GetxController {
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
RxList<HotVideoItemModel> laterList = <HotVideoItemModel>[].obs; RxList<HotVideoItemModel> laterList = <HotVideoItemModel>[].obs;
int count = 0; int count = 0;
RxBool isLoading = false.obs; RxBool isLoading = false.obs;
Box userInfoCache = GStrorage.userInfo;
UserInfoData? userInfo;
@override
void onInit() {
super.onInit();
userInfo = userInfoCache.get('userInfoCache');
}
Future queryLaterList() async { Future queryLaterList() async {
if (userInfo == null) {
return {'status': false, 'msg': '账号未登录', 'code': -101};
}
isLoading.value = true; isLoading.value = true;
var res = await UserHttp.seeYouLater(); var res = await UserHttp.seeYouLater();
if (res['status']) { if (res['status']) {

View File

@ -5,6 +5,7 @@ import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/no_data.dart'; import 'package:pilipala/common/widgets/no_data.dart';
import 'package:pilipala/common/widgets/video_card_h.dart'; import 'package:pilipala/common/widgets/video_card_h.dart';
import 'package:pilipala/pages/later/index.dart'; import 'package:pilipala/pages/later/index.dart';
import 'package:pilipala/utils/route_push.dart';
class LaterPage extends StatefulWidget { class LaterPage extends StatefulWidget {
const LaterPage({super.key}); const LaterPage({super.key});
@ -72,8 +73,8 @@ class _LaterPageState extends State<LaterPage> {
future: _futureBuilderFuture, future: _futureBuilderFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) { if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map; Map? data = snapshot.data;
if (data['status']) { if (data != null && data['status']) {
return Obx( return Obx(
() => _laterController.laterList.isNotEmpty && () => _laterController.laterList.isNotEmpty &&
!_laterController.isLoading.value !_laterController.isLoading.value
@ -84,7 +85,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),
) )
@ -96,10 +97,18 @@ class _LaterPageState extends State<LaterPage> {
); );
} else { } else {
return HttpError( return HttpError(
errMsg: data['msg'], errMsg: data?['msg'] ?? '请求异常',
fn: () => setState(() { btnText: data?['code'] == -101 ? '去登录' : null,
_futureBuilderFuture = _laterController.queryLaterList(); fn: () {
}), if (data?['code'] == -101) {
RoutePush.loginRedirectPush();
} else {
setState(() {
_futureBuilderFuture =
_laterController.queryLaterList();
});
}
},
); );
} }
} else { } else {

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,36 +11,23 @@ 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!();
}
},
// onLongPressEnd: (details) {
// if (longPressEnd != null) {
// longPressEnd!();
// }
// },
child: InkWell(
onTap: () async { onTap: () async {
Get.toNamed('/liveRoom?roomid=${liveItem.roomId}', Get.toNamed('/liveRoom?roomid=${liveItem.roomId}',
arguments: {'liveItem': liveItem, 'heroTag': heroTag}); arguments: {'liveItem': liveItem, 'heroTag': heroTag});
@ -83,8 +72,6 @@ class LiveCardV extends StatelessWidget {
LiveContent(liveItem: liveItem, crossAxisCount: crossAxisCount) LiveContent(liveItem: liveItem, crossAxisCount: crossAxisCount)
], ],
), ),
),
),
); );
} }
} }

View File

@ -17,8 +17,7 @@ class LiveRoomController extends GetxController {
double volume = 0.0; double volume = 0.0;
// 静音状态 // 静音状态
RxBool volumeOff = false.obs; RxBool volumeOff = false.obs;
PlPlayerController plPlayerController = PlPlayerController plPlayerController = PlPlayerController(videoType: 'live');
PlPlayerController.getInstance(videoType: 'live');
Rx<RoomInfoH5Model> roomInfoH5 = RoomInfoH5Model().obs; Rx<RoomInfoH5Model> roomInfoH5 = RoomInfoH5Model().obs;
late bool enableCDN; late bool enableCDN;
late int currentQn; late int currentQn;

View File

@ -153,7 +153,8 @@ class _BottomControlState extends State<BottomControl> {
size: 20, size: 20,
color: Colors.white, color: Colors.white,
), ),
fuc: () => widget.controller!.triggerFullScreen(), fuc: () => widget.controller!.triggerFullScreen(
status: !(widget.controller!.isFullScreen.value)),
), ),
], ],
), ),

View File

@ -40,10 +40,10 @@ class MainController extends GetxController {
dynamicBadgeType.value = DynamicBadgeMode.values[setting.get( dynamicBadgeType.value = DynamicBadgeMode.values[setting.get(
SettingBoxKey.dynamicBadgeMode, SettingBoxKey.dynamicBadgeMode,
defaultValue: DynamicBadgeMode.number.code)]; defaultValue: DynamicBadgeMode.number.code)];
setNavBarConfig();
if (dynamicBadgeType.value != DynamicBadgeMode.hidden) { if (dynamicBadgeType.value != DynamicBadgeMode.hidden) {
getUnreadDynamic(); getUnreadDynamic();
} }
setNavBarConfig();
} }
void onBackPressed(BuildContext context) { void onBackPressed(BuildContext context) {

View File

@ -138,14 +138,14 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
duration: const Duration(milliseconds: 500), duration: const Duration(milliseconds: 500),
offset: Offset(0, snapshot.data ? 0 : 1), offset: Offset(0, snapshot.data ? 0 : 1),
child: GlobalData().enableMYBar child: GlobalData().enableMYBar
? NavigationBar( ? Obx(
() => NavigationBar(
onDestinationSelected: (value) => setIndex(value), onDestinationSelected: (value) => setIndex(value),
selectedIndex: _mainController.selectedIndex, selectedIndex: _mainController.selectedIndex,
destinations: <Widget>[ destinations: <Widget>[
..._mainController.navigationBars.map((e) { ..._mainController.navigationBars.map((e) {
return NavigationDestination( return NavigationDestination(
icon: Obx( icon: Badge(
() => Badge(
label: _mainController label: _mainController
.dynamicBadgeType.value == .dynamicBadgeType.value ==
DynamicBadgeMode.number DynamicBadgeMode.number
@ -159,14 +159,15 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
e['count'] > 0, e['count'] > 0,
child: e['icon'], child: e['icon'],
), ),
),
selectedIcon: e['selectIcon'], selectedIcon: e['selectIcon'],
label: e['label'], label: e['label'],
); );
}).toList(), }).toList(),
], ],
),
) )
: BottomNavigationBar( : Obx(
() => BottomNavigationBar(
currentIndex: _mainController.selectedIndex, currentIndex: _mainController.selectedIndex,
type: BottomNavigationBarType.fixed, type: BottomNavigationBarType.fixed,
onTap: (value) => setIndex(value), onTap: (value) => setIndex(value),
@ -176,8 +177,7 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
items: [ items: [
..._mainController.navigationBars.map((e) { ..._mainController.navigationBars.map((e) {
return BottomNavigationBarItem( return BottomNavigationBarItem(
icon: Obx( icon: Badge(
() => Badge(
label: _mainController label: _mainController
.dynamicBadgeType.value == .dynamicBadgeType.value ==
DynamicBadgeMode.number DynamicBadgeMode.number
@ -191,13 +191,13 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
e['count'] > 0, e['count'] > 0,
child: e['icon'], child: e['icon'],
), ),
),
activeIcon: e['selectIcon'], activeIcon: e['selectIcon'],
label: e['label'], label: e['label'],
); );
}).toList(), }).toList(),
], ],
), ),
),
); );
}, },
) )

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

@ -6,6 +6,7 @@ import 'package:pilipala/http/user.dart';
import 'package:pilipala/models/common/theme_type.dart'; import 'package:pilipala/models/common/theme_type.dart';
import 'package:pilipala/models/user/info.dart'; import 'package:pilipala/models/user/info.dart';
import 'package:pilipala/models/user/stat.dart'; import 'package:pilipala/models/user/stat.dart';
import 'package:pilipala/utils/route_push.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
class MineController extends GetxController { class MineController extends GetxController {
@ -33,14 +34,7 @@ class MineController extends GetxController {
onLogin() async { onLogin() async {
if (!userLogin.value) { if (!userLogin.value) {
Get.toNamed( RoutePush.loginPush();
'/webview',
parameters: {
'url': 'https://passport.bilibili.com/h5-app/passport/login',
'type': 'login',
'pageTitle': '登录bilibili',
},
);
// Get.toNamed('/loginPage'); // Get.toNamed('/loginPage');
} else { } else {
int mid = userInfo.value.mid!; int mid = userInfo.value.mid!;

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

@ -7,6 +7,7 @@ import 'package:pilipala/common/widgets/network_img_layer.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/common/search_type.dart'; import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/utils/route_push.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
Widget searchMbangumiPanel(BuildContext context, ctr, list) { Widget searchMbangumiPanel(BuildContext context, ctr, list) {
@ -63,7 +64,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(
@ -108,28 +109,8 @@ Widget searchMbangumiPanel(BuildContext context, ctr, list) {
SizedBox( SizedBox(
height: 32, height: 32,
child: ElevatedButton( child: ElevatedButton(
onPressed: () async { onPressed: () {
SmartDialog.showLoading(msg: '获取中...'); RoutePush.bangumiPush(i.seasonId, null);
var res = await SearchHttp.bangumiInfo(
seasonId: i.seasonId);
SmartDialog.dismiss().then((value) {
if (res['status']) {
EpisodeItem episode = res['data'].episodes.first;
String bvid = episode.bvid!;
int cid = episode.cid!;
String pic = episode.cover!;
String heroTag = Utils.makeHeroTag(cid);
Get.toNamed(
'/video?bvid=$bvid&cid=$cid&seasonId=${i.seasonId}',
arguments: {
'pic': pic,
'heroTag': heroTag,
'videoType': SearchType.media_bangumi,
'bangumiItem': res['data'],
},
);
}
});
}, },
child: const Text('观看'), child: const Text('观看'),
), ),

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,8 +1,11 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/common/search_type.dart';
class SearchResultController extends GetxController { class SearchResultController extends GetxController {
String? keyword; String? keyword;
int tabIndex = 0; int tabIndex = 0;
RxList searchTabs = [].obs;
@override @override
void onInit() { void onInit() {
@ -10,5 +13,21 @@ class SearchResultController extends GetxController {
if (Get.parameters.keys.isNotEmpty) { if (Get.parameters.keys.isNotEmpty) {
keyword = Get.parameters['keyword']; keyword = Get.parameters['keyword'];
} }
searchTabs.value = SearchType.values
.map((type) => {'label': type.label, 'id': type.type})
.toList();
querySearchCount();
}
Future querySearchCount() async {
var result = await SearchHttp.searchCount(keyword: keyword!);
if (result['status']) {
for (var i in searchTabs) {
final count = result['data'].topTList[i['id']];
i['count'] = count > 99 ? '99+' : count.toString();
}
searchTabs.refresh();
}
return result;
} }
} }

View File

@ -13,7 +13,7 @@ class SearchResultPage extends StatefulWidget {
class _SearchResultPageState extends State<SearchResultPage> class _SearchResultPageState extends State<SearchResultPage>
with TickerProviderStateMixin { with TickerProviderStateMixin {
late SearchResultController? _searchResultController; late SearchResultController _searchResultController;
late TabController? _tabController; late TabController? _tabController;
@override @override
@ -25,7 +25,7 @@ class _SearchResultPageState extends State<SearchResultPage>
_tabController = TabController( _tabController = TabController(
vsync: this, vsync: this,
length: SearchType.values.length, length: SearchType.values.length,
initialIndex: _searchResultController!.tabIndex, initialIndex: _searchResultController.tabIndex,
); );
} }
@ -46,7 +46,7 @@ class _SearchResultPageState extends State<SearchResultPage>
child: SizedBox( child: SizedBox(
width: double.infinity, width: double.infinity,
child: Text( child: Text(
'${_searchResultController!.keyword}', '${_searchResultController.keyword}',
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
), ),
@ -64,10 +64,12 @@ class _SearchResultPageState extends State<SearchResultPage>
splashColor: Colors.transparent, // 点击时的水波纹颜色设置为透明 splashColor: Colors.transparent, // 点击时的水波纹颜色设置为透明
highlightColor: Colors.transparent, // 点击时的背景高亮颜色设置为透明 highlightColor: Colors.transparent, // 点击时的背景高亮颜色设置为透明
), ),
child: TabBar( child: Obx(
() => (TabBar(
controller: _tabController, controller: _tabController,
tabs: [ tabs: [
for (var i in SearchType.values) Tab(text: i.label), for (var i in _searchResultController.searchTabs)
Tab(text: "${i['label']} ${i['count'] ?? ''}")
], ],
isScrollable: true, isScrollable: true,
indicatorWeight: 0, indicatorWeight: 0,
@ -78,21 +80,23 @@ class _SearchResultPageState extends State<SearchResultPage>
borderRadius: const BorderRadius.all(Radius.circular(20)), borderRadius: const BorderRadius.all(Radius.circular(20)),
), ),
indicatorSize: TabBarIndicatorSize.tab, indicatorSize: TabBarIndicatorSize.tab,
labelColor: Theme.of(context).colorScheme.onSecondaryContainer, labelColor:
Theme.of(context).colorScheme.onSecondaryContainer,
labelStyle: const TextStyle(fontSize: 13), labelStyle: const TextStyle(fontSize: 13),
dividerColor: Colors.transparent, dividerColor: Colors.transparent,
unselectedLabelColor: Theme.of(context).colorScheme.outline, unselectedLabelColor: Theme.of(context).colorScheme.outline,
tabAlignment: TabAlignment.start, tabAlignment: TabAlignment.start,
onTap: (index) { onTap: (index) {
if (index == _searchResultController!.tabIndex) { if (index == _searchResultController.tabIndex) {
Get.find<SearchPanelController>( Get.find<SearchPanelController>(
tag: SearchType.values[index].type + tag: SearchType.values[index].type +
_searchResultController!.keyword!) _searchResultController.keyword!)
.animateToTop(); .animateToTop();
} }
_searchResultController!.tabIndex = index; _searchResultController.tabIndex = index;
}, },
)),
), ),
), ),
), ),
@ -102,7 +106,7 @@ class _SearchResultPageState extends State<SearchResultPage>
children: [ children: [
for (var i in SearchType.values) ...{ for (var i in SearchType.values) ...{
SearchPanel( SearchPanel(
keyword: _searchResultController!.keyword, keyword: _searchResultController.keyword,
searchType: i, searchType: i,
tag: DateTime.now().millisecondsSinceEpoch.toString(), tag: DateTime.now().millisecondsSinceEpoch.toString(),
) )

View File

@ -17,10 +17,15 @@ class SubController extends GetxController {
int pageSize = 20; int pageSize = 20;
RxBool hasMore = true.obs; RxBool hasMore = true.obs;
Future<dynamic> querySubFolder({type = 'init'}) async { @override
void onInit() {
super.onInit();
userInfo = userInfoCache.get('userInfoCache'); userInfo = userInfoCache.get('userInfoCache');
}
Future<dynamic> querySubFolder({type = 'init'}) async {
if (userInfo == null) { if (userInfo == null) {
return {'status': false, 'msg': '账号未登录'}; return {'status': false, 'msg': '账号未登录', 'code': -101};
} }
var res = await UserHttp.userSubFolder( var res = await UserHttp.userSubFolder(
pn: currentPage, pn: currentPage,

View File

@ -2,6 +2,7 @@ import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/utils/route_push.dart';
import 'controller.dart'; import 'controller.dart';
import 'widgets/item.dart'; import 'widgets/item.dart';
@ -68,8 +69,18 @@ class _SubPageState extends State<SubPage> {
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
slivers: [ slivers: [
HttpError( HttpError(
errMsg: data?['msg'], errMsg: data?['msg'] ?? '请求异常',
fn: () => setState(() {}), btnText: data?['code'] == -101 ? '去登录' : null,
fn: () {
if (data?['code'] == -101) {
RoutePush.loginRedirectPush();
} else {
setState(() {
_futureBuilderFuture =
_subController.querySubFolder();
});
}
},
), ),
], ],
); );

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

@ -74,7 +74,7 @@ class VideoDetailController extends GetxController
final scaffoldKey = GlobalKey<ScaffoldState>(); final scaffoldKey = GlobalKey<ScaffoldState>();
RxString bgCover = ''.obs; RxString bgCover = ''.obs;
RxString cover = ''.obs; RxString cover = ''.obs;
PlPlayerController plPlayerController = PlPlayerController.getInstance(); PlPlayerController plPlayerController = PlPlayerController();
late VideoItem firstVideo; late VideoItem firstVideo;
late AudioItem firstAudio; late AudioItem firstAudio;
@ -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

@ -450,6 +450,7 @@ class VideoIntroController extends GetxController {
videoDetailCtr.danmakuCid.value = cid; videoDetailCtr.danmakuCid.value = cid;
videoDetailCtr.cover.value = cover; videoDetailCtr.cover.value = cover;
videoDetailCtr.queryVideoUrl(); videoDetailCtr.queryVideoUrl();
videoDetailCtr.getSubtitle();
// 重新请求评论 // 重新请求评论
try { try {
/// 未渲染回复组件时可能异常 /// 未渲染回复组件时可能异常

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,9 +17,6 @@ class IntroDetail extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return SizedBox(
width: double.infinity, width: double.infinity,
child: SelectableRegion(
focusNode: FocusNode(),
selectionControls: MaterialTextSelectionControls(),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
@ -27,6 +25,7 @@ class IntroDetail extends StatelessWidget {
children: [ children: [
GestureDetector( GestureDetector(
onTap: () { onTap: () {
feedBack();
Clipboard.setData(ClipboardData(text: videoDetail!.bvid!)); Clipboard.setData(ClipboardData(text: videoDetail!.bvid!));
SmartDialog.showToast('已复制'); SmartDialog.showToast('已复制');
}, },
@ -40,7 +39,9 @@ class IntroDetail extends StatelessWidget {
const SizedBox(width: 10), const SizedBox(width: 10),
GestureDetector( GestureDetector(
onTap: () { onTap: () {
Clipboard.setData(ClipboardData(text: videoDetail!.bvid!)); feedBack();
Clipboard.setData(
ClipboardData(text: videoDetail!.aid!.toString()));
SmartDialog.showToast('已复制'); SmartDialog.showToast('已复制');
}, },
child: Text( child: Text(
@ -53,7 +54,10 @@ class IntroDetail extends StatelessWidget {
], ],
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text.rich( 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

@ -12,6 +12,7 @@ import 'package:pilipala/pages/preview/index.dart';
import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/pages/video/detail/reply_new/index.dart'; import 'package:pilipala/pages/video/detail/reply_new/index.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/url_utils.dart'; import 'package:pilipala/utils/url_utils.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
@ -44,7 +45,7 @@ class ReplyItem extends StatelessWidget {
onTap: () { onTap: () {
feedBack(); feedBack();
if (replyReply != null) { if (replyReply != null) {
replyReply!(replyItem, null); replyReply!(replyItem);
} }
}, },
onLongPress: () { onLongPress: () {
@ -358,7 +359,7 @@ class ReplyItemRow extends StatelessWidget {
InkWell( InkWell(
// 一楼点击评论展开评论详情 // 一楼点击评论展开评论详情
// onTap: () { // onTap: () {
// replyReply?.call(replyItem, replies![i]); // replyReply?.call(replyItem);
// }, // },
onLongPress: () { onLongPress: () {
feedBack(); feedBack();
@ -642,23 +643,25 @@ InlineSpan buildContent(
'', '',
); );
} else { } else {
final String redirectUrl = final String pathSegment = Uri.parse(matchStr).path;
await UrlUtils.parseRedirectUrl(matchStr); Map matchRes = IdUtils.matchAvorBv(input: pathSegment);
if (redirectUrl == matchStr) { List matchKeys = matchRes.keys.toList();
Clipboard.setData(ClipboardData(text: matchStr)); if (matchKeys.isNotEmpty) {
SmartDialog.showToast('地址可能有误');
return;
}
final String pathSegment = Uri.parse(redirectUrl).path;
final String lastPathSegment =
pathSegment.split('/').last;
if (lastPathSegment.startsWith('BV')) {
UrlUtils.matchUrlPush( UrlUtils.matchUrlPush(
lastPathSegment, matchRes.containsKey('AV')
? matchRes['AV']! as int
: matchRes['BV'],
title, title,
redirectUrl, matchStr,
); );
} else { } else {
final String redirectUrl =
await UrlUtils.parseRedirectUrl(matchStr);
// if (redirectUrl == matchStr) {
// Clipboard.setData(ClipboardData(text: matchStr));
// SmartDialog.showToast('地址可能有误');
// return;
// }
Get.toNamed( Get.toNamed(
'/webview', '/webview',
parameters: { parameters: {

View File

@ -323,11 +323,13 @@ class _VideoDetailPageState extends State<VideoDetailPage>
children: [ children: [
GestureDetector( GestureDetector(
onTap: handlePlay, onTap: handlePlay,
child: Image.network( child: Obx(
vdCtr.videoItem['pic'], () => NetworkImgLayer(
src: vdCtr.cover.value,
width: Get.width, width: Get.width,
height: videoHeight, height: videoHeight,
fit: BoxFit.cover, // 适应方式根据需要调整 type: 'emote',
),
), ),
), ),
buildCustomAppBar(), buildCustomAppBar(),
@ -535,6 +537,9 @@ class _VideoDetailPageState extends State<VideoDetailPage>
controller: _extendNestCtr, controller: _extendNestCtr,
headerSliverBuilder: headerSliverBuilder:
(BuildContext context2, bool innerBoxIsScrolled) { (BuildContext context2, bool innerBoxIsScrolled) {
return <Widget>[
Obx(
() {
final Orientation orientation = final Orientation orientation =
MediaQuery.of(context).orientation; MediaQuery.of(context).orientation;
final bool isFullScreen = final bool isFullScreen =
@ -546,9 +551,6 @@ class _VideoDetailPageState extends State<VideoDetailPage>
? 0 ? 0
: MediaQuery.of(context).padding.top)) : MediaQuery.of(context).padding.top))
: videoHeight.value; : videoHeight.value;
return <Widget>[
Obx(
() {
if (orientation == Orientation.landscape || if (orientation == Orientation.landscape ||
isFullScreen) { isFullScreen) {
enterFullScreen(); enterFullScreen();

View File

@ -52,7 +52,10 @@ class WebviewController extends GetxController {
loadProgress.value = progress; loadProgress.value = progress;
}, },
onPageStarted: (String url) { onPageStarted: (String url) {
final String str = Uri.parse(url).pathSegments[0]; final List pathSegments = Uri.parse(url).pathSegments;
if (pathSegments.isNotEmpty &&
url != 'https://passport.bilibili.com/h5-app/passport/login') {
final String str = pathSegments[0];
final Map matchRes = IdUtils.matchAvorBv(input: str); final Map matchRes = IdUtils.matchAvorBv(input: str);
final List matchKeys = matchRes.keys.toList(); final List matchKeys = matchRes.keys.toList();
if (matchKeys.isNotEmpty) { if (matchKeys.isNotEmpty) {
@ -63,6 +66,7 @@ class WebviewController extends GetxController {
); );
} }
} }
}
}, },
// 加载完成 // 加载完成
onUrlChange: (UrlChange urlChange) async { onUrlChange: (UrlChange urlChange) async {
@ -106,6 +110,9 @@ class WebviewController extends GetxController {
SmartDialog.showToast('登录成功'); SmartDialog.showToast('登录成功');
try { try {
Box userInfoCache = GStrorage.userInfo; Box userInfoCache = GStrorage.userInfo;
if (!userInfoCache.isOpen) {
userInfoCache = await Hive.openBox('userInfo');
}
await userInfoCache.put('userInfoCache', result['data']); await userInfoCache.put('userInfoCache', result['data']);
final HomeController homeCtr = Get.find<HomeController>(); final HomeController homeCtr = Get.find<HomeController>();

View File

@ -6,6 +6,7 @@ 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/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/utils/route_push.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
@ -154,16 +155,33 @@ class ChatItem extends StatelessWidget {
GestureDetector( GestureDetector(
onTap: () async { onTap: () async {
SmartDialog.showLoading(); SmartDialog.showLoading();
var bvid = content["bvid"]; final String bvid = content["bvid"];
// 16番剧 5投稿
final int source = content["source"];
final String? url = content["url"];
final int cid = await SearchHttp.ab2c(bvid: bvid); final int cid = await SearchHttp.ab2c(bvid: bvid);
final String heroTag = Utils.makeHeroTag(bvid); final String heroTag = Utils.makeHeroTag(bvid);
SmartDialog.dismiss<dynamic>().then( await SmartDialog.dismiss();
(e) => Get.toNamed<dynamic>('/video?bvid=$bvid&cid=$cid', if (source == 5) {
Get.toNamed<dynamic>(
'/video?bvid=$bvid&cid=$cid',
arguments: <String, String?>{ arguments: <String, String?>{
'pic': content['thumb'], 'pic': content['thumb'],
'heroTag': heroTag, 'heroTag': heroTag,
}), },
); );
}
if (source == 16) {
if (url != null) {
final String area = url.split('/').last;
if (area.startsWith('ep')) {
RoutePush.bangumiPush(null, Utils.matchNum(area).first);
} else if (area.startsWith('ss')) {
RoutePush.bangumiPush(Utils.matchNum(area).first, null);
}
}
}
}, },
child: NetworkImgLayer( child: NetworkImgLayer(
width: 220, width: 220,
@ -183,7 +201,7 @@ class ChatItem extends StatelessWidget {
), ),
const SizedBox(height: 1), const SizedBox(height: 1),
Text( Text(
content['author'], content['author'] ?? '',
style: TextStyle( style: TextStyle(
letterSpacing: 0.6, letterSpacing: 0.6,
height: 1.5, height: 1.5,
@ -206,7 +224,7 @@ class ChatItem extends StatelessWidget {
SmartDialog.dismiss<dynamic>().then( SmartDialog.dismiss<dynamic>().then(
(e) => Get.toNamed<dynamic>('/video?bvid=$bvid&cid=$cid', (e) => Get.toNamed<dynamic>('/video?bvid=$bvid&cid=$cid',
arguments: <String, String?>{ arguments: <String, String?>{
'pic': content['thumb'], 'pic': content['thumb'] ?? '',
'heroTag': heroTag, 'heroTag': heroTag,
}), }),
); );

View File

@ -123,6 +123,7 @@ class PlPlayerController {
PreferredSizeWidget? bottomControl; PreferredSizeWidget? bottomControl;
Widget? danmuWidget; Widget? danmuWidget;
late RxList subtitles; late RxList subtitles;
String videoType = 'archive';
/// 数据加载监听 /// 数据加载监听
Stream<DataStatus> get onDataStatusChanged => dataStatus.status.stream; Stream<DataStatus> get onDataStatusChanged => dataStatus.status.stream;
@ -220,7 +221,7 @@ class PlPlayerController {
Rx<int> get playerCount => _playerCount; Rx<int> get playerCount => _playerCount;
/// ///
Rx<String> get videoType => _videoType; // Rx<String> get videoType => _videoType;
/// 弹幕开关 /// 弹幕开关
Rx<bool> isOpenDanmu = false.obs; Rx<bool> isOpenDanmu = false.obs;
@ -274,8 +275,7 @@ class PlPlayerController {
} }
// 添加一个私有构造函数 // 添加一个私有构造函数
PlPlayerController._() { PlPlayerController._internal(this.videoType) {
_videoType = videoType;
isOpenDanmu.value = isOpenDanmu.value =
setting.get(SettingBoxKey.enableShowDanmaku, defaultValue: false); setting.get(SettingBoxKey.enableShowDanmaku, defaultValue: false);
blockTypes = blockTypes =
@ -330,11 +330,11 @@ class PlPlayerController {
} }
// 获取实例 传参 // 获取实例 传参
static PlPlayerController getInstance({ factory PlPlayerController({
String videoType = 'archive', String videoType = 'archive',
}) { }) {
// 如果实例尚未创建,则创建一个新实例 // 如果实例尚未创建,则创建一个新实例
_instance ??= PlPlayerController._(); _instance ??= PlPlayerController._internal(videoType);
if (videoType != 'none') { if (videoType != 'none') {
_instance!._playerCount.value += 1; _instance!._playerCount.value += 1;
_videoType.value = videoType; _videoType.value = videoType;
@ -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();
@ -406,7 +406,7 @@ class PlPlayerController {
if (!_listenersInitialized) { if (!_listenersInitialized) {
startListeners(); startListeners();
} }
await _initializePlayer(seekTo: seekTo, duration: _duration.value); await _initializePlayer(duration: _duration.value);
bool autoEnterFullcreen = bool autoEnterFullcreen =
setting.get(SettingBoxKey.enableAutoEnter, defaultValue: false); setting.get(SettingBoxKey.enableAutoEnter, defaultValue: false);
if (autoEnterFullcreen && _isFirstTime) { if (autoEnterFullcreen && _isFirstTime) {
@ -426,6 +426,7 @@ class PlPlayerController {
bool enableHA, bool enableHA,
double? width, double? width,
double? height, double? height,
Duration seekTo,
) async { ) async {
// 每次配置时先移除监听 // 每次配置时先移除监听
removeListeners(); removeListeners();
@ -442,7 +443,7 @@ class PlPlayerController {
configuration: PlayerConfiguration( configuration: PlayerConfiguration(
// 默认缓存 5M 大小 // 默认缓存 5M 大小
bufferSize: bufferSize:
videoType.value == 'live' ? 32 * 1024 * 1024 : 5 * 1024 * 1024, videoType == 'live' ? 32 * 1024 * 1024 : 5 * 1024 * 1024,
), ),
); );
@ -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,
); );
// 音轨 // 音轨
@ -521,7 +523,6 @@ class PlPlayerController {
// 开始播放 // 开始播放
Future _initializePlayer({ Future _initializePlayer({
Duration seekTo = Duration.zero,
Duration? duration, Duration? duration,
}) async { }) async {
getVideoFit(); getVideoFit();
@ -530,9 +531,9 @@ class PlPlayerController {
// } // }
/// 跳转播放 /// 跳转播放
if (seekTo != Duration.zero) { // if (seekTo != Duration.zero) {
await this.seekTo(seekTo); // await this.seekTo(seekTo);
} // }
/// 自动播放 /// 自动播放
if (_autoPlay) { if (_autoPlay) {
@ -540,7 +541,7 @@ class PlPlayerController {
} }
/// 设置倍速 /// 设置倍速
if (videoType.value == 'live') { if (videoType == 'live') {
await setPlaybackSpeed(1.0); await setPlaybackSpeed(1.0);
} else { } else {
if (_playbackSpeed.value != 1.0) { if (_playbackSpeed.value != 1.0) {
@ -932,7 +933,7 @@ class PlPlayerController {
/// 设置长按倍速状态 live模式下禁用 /// 设置长按倍速状态 live模式下禁用
void setDoubleSpeedStatus(bool val) { void setDoubleSpeedStatus(bool val) {
if (videoType.value == 'live') { if (videoType == 'live') {
return; return;
} }
if (controlsLock.value) { if (controlsLock.value) {
@ -976,41 +977,8 @@ class PlPlayerController {
} else { } else {
await landScape(); await landScape();
} }
} else if (isFullScreen.value && !status) {
// bool isValid =
// direction.value == 'vertical' || mode == FullScreenMode.vertical
// ? true
// : false;
// var result = await showDialog(
// context: Get.context!,
// useSafeArea: false,
// builder: (context) => Dialog.fullscreen(
// backgroundColor: Colors.black,
// child: SafeArea(
// // 忽略手机安全区域
// top: isValid,
// left: false,
// right: false,
// bottom: isValid,
// child: PLVideoPlayer(
// controller: this,
// headerControl: headerControl,
// bottomControl: bottomControl,
// danmuWidget: danmuWidget,
// ),
// ),
// ),
// );
// if (result == null) {
// // 退出全屏
// StatusBarControl.setHidden(false, animation: StatusBarAnimation.FADE);
// exitFullScreen();
// await verticalScreen();
// toggleFullScreen(false);
// }
} else if (isFullScreen.value) {
StatusBarControl.setHidden(false, animation: StatusBarAnimation.FADE); StatusBarControl.setHidden(false, animation: StatusBarAnimation.FADE);
// Get.back();
exitFullScreen(); exitFullScreen();
await verticalScreen(); await verticalScreen();
toggleFullScreen(false); toggleFullScreen(false);
@ -1047,7 +1015,7 @@ class PlPlayerController {
if (!_enableHeart) { if (!_enableHeart) {
return false; return false;
} }
if (videoType.value == 'live') { if (videoType == 'live') {
return; return;
} }
// 播放状态变化时,更新 // 播放状态变化时,更新
@ -1146,7 +1114,6 @@ class PlPlayerController {
// _buffered.close(); // _buffered.close();
// _showControls.close(); // _showControls.close();
// _controlsLock.close(); // _controlsLock.close();
// playerStatus.status.close(); // playerStatus.status.close();
// dataStatus.status.close(); // dataStatus.status.close();

View File

@ -334,7 +334,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
color: Colors.white, color: Colors.white,
), ),
), ),
fuc: () => _.triggerFullScreen(), fuc: () => _.triggerFullScreen(status: !_.isFullScreen.value),
), ),
}; };
final List<Widget> list = []; final List<Widget> list = [];
@ -652,7 +652,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
}, },
onDoubleTapDown: (TapDownDetails details) { onDoubleTapDown: (TapDownDetails details) {
// live模式下禁用 锁定时🔒禁用 // live模式下禁用 锁定时🔒禁用
if (_.videoType.value == 'live' || _.controlsLock.value) { if (_.videoType == 'live' || _.controlsLock.value) {
return; return;
} }
final double totalWidth = MediaQuery.sizeOf(context).width; final double totalWidth = MediaQuery.sizeOf(context).width;
@ -679,7 +679,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
/// 水平位置 快进 live模式下禁用 /// 水平位置 快进 live模式下禁用
onHorizontalDragUpdate: (DragUpdateDetails details) { onHorizontalDragUpdate: (DragUpdateDetails details) {
// live模式下禁用 锁定时🔒禁用 // live模式下禁用 锁定时🔒禁用
if (_.videoType.value == 'live' || _.controlsLock.value) { if (_.videoType == 'live' || _.controlsLock.value) {
return; return;
} }
// final double tapPosition = details.localPosition.dx; // final double tapPosition = details.localPosition.dx;
@ -695,7 +695,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
_.onChangedSliderStart(); _.onChangedSliderStart();
}, },
onHorizontalDragEnd: (DragEndDetails details) { onHorizontalDragEnd: (DragEndDetails details) {
if (_.videoType.value == 'live' || _.controlsLock.value) { if (_.videoType == 'live' || _.controlsLock.value) {
return; return;
} }
_.onChangedSliderEnd(); _.onChangedSliderEnd();
@ -826,7 +826,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
return const SizedBox(); return const SizedBox();
} }
if (_.videoType.value == 'live') { if (_.videoType == 'live') {
return const SizedBox(); return const SizedBox();
} }
if (value > max || max <= 0) { if (value > max || max <= 0) {
@ -879,7 +879,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
// 锁 // 锁
Obx( Obx(
() => Visibility( () => Visibility(
visible: _.videoType.value != 'live' && _.isFullScreen.value, visible: _.videoType != 'live' && _.isFullScreen.value,
child: Align( child: Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: FractionalTranslation( child: FractionalTranslation(

View File

@ -26,7 +26,7 @@ class VideoPlayerServiceHandler extends BaseAudioHandler with SeekHandler {
static final List<MediaItem> _item = []; static final List<MediaItem> _item = [];
Box setting = GStrorage.setting; Box setting = GStrorage.setting;
bool enableBackgroundPlay = false; bool enableBackgroundPlay = false;
PlPlayerController player = PlPlayerController.getInstance(videoType: 'none'); PlPlayerController player = PlPlayerController();
VideoPlayerServiceHandler() { VideoPlayerServiceHandler() {
revalidateSetting(); revalidateSetting();

View File

@ -18,7 +18,7 @@ class AudioSessionHandler {
session.configure(const AudioSessionConfiguration.music()); session.configure(const AudioSessionConfiguration.music());
session.interruptionEventStream.listen((event) { session.interruptionEventStream.listen((event) {
final player = PlPlayerController.getInstance(videoType: 'none'); final player = PlPlayerController(videoType: 'none');
if (event.begin) { if (event.begin) {
if (!player.playerStatus.playing) return; if (!player.playerStatus.playing) return;
switch (event.type) { switch (event.type) {
@ -51,7 +51,7 @@ class AudioSessionHandler {
// 耳机拔出暂停 // 耳机拔出暂停
session.becomingNoisyEventStream.listen((_) { session.becomingNoisyEventStream.listen((_) {
final player = PlPlayerController.getInstance(videoType: 'none'); final player = PlPlayerController(videoType: 'none');
if (player.playerStatus.playing) { if (player.playerStatus.playing) {
player.pause(); player.pause();
} }

View File

@ -89,7 +89,7 @@ class ShutdownTimerService {
return; return;
} }
PlPlayerController plPlayerController = PlPlayerController plPlayerController =
PlPlayerController.getInstance(videoType: 'none'); PlPlayerController(videoType: 'none');
if (!exitApp && !waitForPlayingCompleted) { if (!exitApp && !waitForPlayingCompleted) {
if (!plPlayerController.playerStatus.playing) { if (!plPlayerController.playerStatus.playing) {
//仅提示用户 //仅提示用户
@ -124,7 +124,7 @@ class ShutdownTimerService {
} else { } else {
//暂停播放 //暂停播放
PlPlayerController plPlayerController = PlPlayerController plPlayerController =
PlPlayerController.getInstance(videoType: 'none'); PlPlayerController(videoType: 'none');
if (plPlayerController.playerStatus.playing) { if (plPlayerController.playerStatus.playing) {
plPlayerController.pause(); plPlayerController.pause();
waitForPlayingCompleted = true; waitForPlayingCompleted = true;

View File

@ -2,8 +2,8 @@ import 'package:appscheme/appscheme.dart';
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/route_push.dart';
import '../http/search.dart'; import '../http/search.dart';
import '../models/common/search_type.dart';
import 'id_utils.dart'; import 'id_utils.dart';
import 'url_utils.dart'; import 'url_utils.dart';
import 'utils.dart'; import 'utils.dart';
@ -68,7 +68,7 @@ class PiliSchame {
} else if (host == 'bangumi') { } else if (host == 'bangumi') {
if (path.startsWith('/season')) { if (path.startsWith('/season')) {
final String seasonId = path.split('/').last; final String seasonId = path.split('/').last;
_bangumiPush(int.parse(seasonId), null); RoutePush.bangumiPush(int.parse(seasonId), null);
} }
} else if (host == 'opus') { } else if (host == 'opus') {
if (path.startsWith('/detail')) { if (path.startsWith('/detail')) {
@ -126,35 +126,6 @@ class PiliSchame {
} }
} }
// 番剧跳转
static Future<void> _bangumiPush(int? seasonId, int? epId) async {
SmartDialog.showLoading<dynamic>(msg: '获取中...');
try {
var result = await SearchHttp.bangumiInfo(seasonId: seasonId, epId: epId);
if (result['status']) {
var bangumiDetail = result['data'];
final int cid = bangumiDetail.episodes!.first.cid;
final String bvid = IdUtils.av2bv(bangumiDetail.episodes!.first.aid);
final String heroTag = Utils.makeHeroTag(cid);
var epId = bangumiDetail.episodes!.first.id;
SmartDialog.dismiss().then(
(e) => Get.toNamed(
'/video?bvid=$bvid&cid=$cid&epId=$epId',
arguments: <String, dynamic>{
'pic': bangumiDetail.cover,
'heroTag': heroTag,
'videoType': SearchType.media_bangumi,
},
),
);
} else {
SmartDialog.showToast(result['msg']);
}
} catch (e) {
SmartDialog.showToast('番剧获取失败:$e');
}
}
static Future<void> _fullPathPush(SchemeEntity value) async { static Future<void> _fullPathPush(SchemeEntity value) async {
// https://m.bilibili.com/bangumi/play/ss39708 // https://m.bilibili.com/bangumi/play/ss39708
// https | m.bilibili.com | /bangumi/play/ss39708 // https | m.bilibili.com | /bangumi/play/ss39708
@ -167,8 +138,23 @@ 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); Map matchRes = IdUtils.matchAvorBv(input: path);
if (matchRes.containsKey('AV')) {
_videoPush(matchRes['AV']! as int, null);
} else if (matchRes.containsKey('BV')) {
_videoPush(null, matchRes['BV'] as String);
} else {
SmartDialog.showToast('投稿匹配失败');
}
}
if (path.startsWith('/bangumi')) {
if (lastPathSegment.contains('ss')) {
RoutePush.bangumiPush(Utils.matchNum(lastPathSegment).first, null);
}
if (lastPathSegment.contains('ep')) {
RoutePush.bangumiPush(null, Utils.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);
@ -220,9 +206,9 @@ class PiliSchame {
case 'bangumi': case 'bangumi':
print('番剧'); print('番剧');
if (area.startsWith('ep')) { if (area.startsWith('ep')) {
_bangumiPush(null, matchNum(area).first); RoutePush.bangumiPush(null, Utils.matchNum(area).first);
} else if (area.startsWith('ss')) { } else if (area.startsWith('ss')) {
_bangumiPush(matchNum(area).first, null); RoutePush.bangumiPush(Utils.matchNum(area).first, null);
} }
break; break;
case 'video': case 'video':
@ -238,7 +224,7 @@ class PiliSchame {
break; break;
case 'read': case 'read':
print('专栏'); print('专栏');
String id = 'cv${matchNum(query!['id']!).first}'; String id = 'cv${Utils.matchNum(query!['id']!).first}';
Get.toNamed('/htmlRender', parameters: { Get.toNamed('/htmlRender', parameters: {
'url': value.dataString!, 'url': value.dataString!,
'title': '', 'title': '',
@ -254,21 +240,14 @@ class PiliSchame {
} }
} }
static List<int> matchNum(String str) {
final RegExp regExp = RegExp(r'\d+');
final Iterable<Match> matches = regExp.allMatches(str);
return matches.map((Match match) => int.parse(match.group(0)!)).toList();
}
static void _handleEpisodePath(String lastPathSegment, String redirectUrl) { static void _handleEpisodePath(String lastPathSegment, String redirectUrl) {
final String seasonId = _extractIdFromPath(lastPathSegment); final String seasonId = _extractIdFromPath(lastPathSegment);
_bangumiPush(null, matchNum(seasonId).first); RoutePush.bangumiPush(null, Utils.matchNum(seasonId).first);
} }
static void _handleSeasonPath(String lastPathSegment, String redirectUrl) { static void _handleSeasonPath(String lastPathSegment, String redirectUrl) {
final String seasonId = _extractIdFromPath(lastPathSegment); final String seasonId = _extractIdFromPath(lastPathSegment);
_bangumiPush(matchNum(seasonId).first, null); RoutePush.bangumiPush(Utils.matchNum(seasonId).first, null);
} }
static String _extractIdFromPath(String lastPathSegment) { static String _extractIdFromPath(String lastPathSegment) {

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,9 +38,7 @@ 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; final androidInfo = await DeviceInfoPlugin().androidInfo;
if (androidInfo.version.sdkInt <= 32) { if (androidInfo.version.sdkInt <= 32) {
if (!await requestStoragePer()) { if (!await requestStoragePer()) {
@ -85,6 +49,7 @@ class DownloadUtils {
return false; return false;
} }
} }
}
SmartDialog.showLoading(msg: '保存中'); SmartDialog.showLoading(msg: '保存中');
var response = await Dio() var response = await Dio()
@ -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; return true;
} else {
await permissionDialog('保存失败', '相册权限未授权');
return false;
}
} 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),
)
],
),
),
],
),
),
);
}

68
lib/utils/route_push.dart Normal file
View File

@ -0,0 +1,68 @@
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/utils/utils.dart';
class RoutePush {
// 番剧跳转
static Future<void> bangumiPush(int? seasonId, int? epId,
{String? heroTag}) async {
SmartDialog.showLoading<dynamic>(msg: '获取中...');
try {
var result = await SearchHttp.bangumiInfo(seasonId: seasonId, epId: epId);
await SmartDialog.dismiss();
if (result['status']) {
if (result['data'].episodes.isEmpty) {
SmartDialog.showToast('资源获取失败');
return;
}
final BangumiInfoModel bangumiDetail = result['data'];
final EpisodeItem episode = bangumiDetail.episodes!.first;
final int epId = episode.id!;
final int cid = episode.cid!;
final String bvid = episode.bvid!;
final String cover = episode.cover!;
final Map arguments = <String, dynamic>{
'pic': cover,
'videoType': SearchType.media_bangumi,
// 'bangumiItem': bangumiDetail,
};
arguments['heroTag'] = heroTag ?? Utils.makeHeroTag(cid);
Get.toNamed(
'/video?bvid=$bvid&cid=$cid&epId=$epId',
arguments: arguments,
);
} else {
SmartDialog.showToast(result['msg']);
}
} catch (e) {
SmartDialog.showToast('番剧获取失败:$e');
}
}
// 登录跳转
static Future<void> loginPush() async {
await Get.toNamed(
'/webview',
parameters: {
'url': 'https://passport.bilibili.com/h5-app/passport/login',
'type': 'login',
'pageTitle': '登录bilibili',
},
);
}
// 登录跳转
static Future<void> loginRedirectPush() async {
await Get.offAndToNamed(
'/webview',
parameters: {
'url': 'https://passport.bilibili.com/h5-app/passport/login',
'type': 'login',
'pageTitle': '登录bilibili',
},
);
}
}

View File

@ -16,7 +16,7 @@ class UrlUtils {
}; };
try { try {
final response = await dio.get(url); final response = await dio.get(url);
if (response.statusCode == 302) { if (response.statusCode == 302 || response.statusCode == 301) {
redirectUrl = response.headers['location']?.first as String; redirectUrl = response.headers['location']?.first as String;
if (redirectUrl.endsWith('/')) { if (redirectUrl.endsWith('/')) {
redirectUrl = redirectUrl.substring(0, redirectUrl.length - 1); redirectUrl = redirectUrl.substring(0, redirectUrl.length - 1);
@ -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

@ -383,4 +383,11 @@ class Utils {
List<int> randomBytes = generateRandomBytes(minLength, maxLength); List<int> randomBytes = generateRandomBytes(minLength, maxLength);
return base64.encode(randomBytes); return base64.encode(randomBytes);
} }
static List<int> matchNum(String str) {
final RegExp regExp = RegExp(r'\d+');
final Iterable<Match> matches = regExp.allMatches(str);
return matches.map((Match match) => int.parse(match.group(0)!)).toList();
}
} }

View File

@ -69,10 +69,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: audio_service name: audio_service
sha256: a4d989f1225ea9621898d60f23236dcbfc04876fa316086c23c5c4af075dbac4 sha256: "4547c312a94f9cb2c48b60823fb190767cbd63454a83c73049384d5d3cba4650"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.18.12" version: "0.18.13"
audio_service_platform_interface: audio_service_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -85,10 +85,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: audio_service_web name: audio_service_web
sha256: "523e64ddc914c714d53eec2da85bba1074f08cf26c786d4efb322de510815ea7" sha256: "9d7d5ae5f98a5727f2580fad73062f2484f400eef6cef42919413268e62a363e"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.1.1" version: "0.1.2"
audio_session: audio_session:
dependency: "direct main" dependency: "direct main"
description: description:
@ -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: "285f7919bbf4a7d89a62615b14a3766a171ad575"
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: "285f7919bbf4a7d89a62615b14a3766a171ad575"
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: "285f7919bbf4a7d89a62615b14a3766a171ad575"
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.23+1023
environment: environment:
sdk: ">=3.0.0 <4.0.0" sdk: ">=3.0.0 <4.0.0"
@ -90,8 +90,8 @@ dependencies:
media_kit_libs_video: ^1.0.4 media_kit_libs_video: ^1.0.4
# 媒体通知 # 媒体通知
audio_service: ^0.18.12 audio_service: ^0.18.13
audio_session: ^0.1.16 audio_session: ^0.1.18
# 音量、亮度、屏幕控制 # 音量、亮度、屏幕控制
flutter_volume_controller: ^1.3.1 flutter_volume_controller: ^1.3.1
@ -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