Compare commits

...

21 Commits

Author SHA1 Message Date
569277572a Merge pull request #536 from KoolShow/fix_seekto_regexp
fix: 含有小时的时间无法跳转
2024-02-12 18:11:26 +08:00
19b84571c1 Merge branch 'main' into fix_seekto_regexp 2024-02-12 18:11:15 +08:00
0812b8339e Merge branch 'feature-replyItem' 2024-02-12 17:55:08 +08:00
b817a0c807 修正正则表达式以匹配含小时的时间 2024-02-12 17:20:18 +08:00
3da70d7e27 Merge branch 'fix-replyRepeat' 2024-02-12 16:55:56 +08:00
5e59db85be fix: 评论笔记跳转 issues #472 2024-02-12 16:51:05 +08:00
89026e671c mod: 收藏视频相关 issues #51 #534 2024-02-12 10:07:29 +08:00
1c8e7e53a5 Merge branch 'fix-replyRepeat' 2024-02-11 23:20:11 +08:00
b264427be6 fix: 切换合集评论不刷新 issues #326 #525 2024-02-11 23:07:44 +08:00
d5134f972d Merge branch 'feature-liveRoomRender' 2024-02-11 18:48:24 +08:00
e2fd01a6d5 fix: video Storage初始化 2024-02-11 18:47:51 +08:00
289cc99bc2 mod 2024-02-11 09:10:45 +08:00
3d5ebe7e99 fix: 视频详情页评论重复请求 2024-02-10 19:57:10 +08:00
d9964d37a4 Merge branch 'fix-favBangumiPushError' 2024-02-10 19:24:54 +08:00
5da39a9c52 Merge branch 'feature-cacheManage' 2024-02-10 19:22:49 +08:00
44a162762c fix: 评论页面路由跳转 issues #405 2024-02-09 23:24:26 +08:00
10b928474b mod 2024-02-08 22:46:39 +08:00
c39e91073b feat: 应用内缓存清理 2024-02-07 22:57:30 +08:00
d258474a5a mod: 直播页面内容更新 2024-02-07 22:23:29 +08:00
083739e562 mod: 收藏卡片内容修改 2024-02-06 11:13:33 +08:00
71ccb9c0e5 fix: 收藏国创跳转异常 2024-02-06 11:01:36 +08:00
23 changed files with 804 additions and 241 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -233,6 +233,10 @@ class Api {
static const String liveRoomInfo =
'${HttpString.liveBaseUrl}/xlive/web-room/v2/index/getRoomPlayInfo';
// 直播间详情 H5
static const String liveRoomInfoH5 =
'${HttpString.liveBaseUrl}/xlive/web-room/v1/index/getH5InfoByRoom';
// 用户信息 需要Wbi签名
// https://api.bilibili.com/x/space/wbi/acc/info?mid=503427686&token=&platform=web&web_location=1550101&w_rid=d709892496ce93e3d94d6d37c95bde91&wts=1689301482
static const String memberInfo = '/x/space/wbi/acc/info';

View File

@ -1,5 +1,6 @@
import '../models/live/item.dart';
import '../models/live/room_info.dart';
import '../models/live/room_info_h5.dart';
import 'api.dart';
import 'init.dart';
@ -46,4 +47,22 @@ class LiveHttp {
};
}
}
static Future liveRoomInfoH5({roomId, qn}) async {
var res = await Request().get(Api.liveRoomInfoH5, data: {
'room_id': roomId,
});
if (res.data['code'] == 0) {
return {
'status': true,
'data': RoomInfoH5Model.fromJson(res.data['data'])
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
}

View File

@ -51,7 +51,7 @@ class VideoHttp {
(i['owner'] != null &&
!blackMidsList.contains(i['owner']['mid']))) {
RecVideoItemModel videoItem = RecVideoItemModel.fromJson(i);
if (!RecommendFilter.filter(videoItem)){
if (!RecommendFilter.filter(videoItem)) {
list.add(videoItem);
}
}
@ -98,7 +98,7 @@ class VideoHttp {
(i['args'] != null &&
!blackMidsList.contains(i['args']['up_mid']))) {
RecVideoItemAppModel videoItem = RecVideoItemAppModel.fromJson(i);
if (!RecommendFilter.filter(videoItem)){
if (!RecommendFilter.filter(videoItem)) {
list.add(videoItem);
}
}
@ -217,7 +217,7 @@ class VideoHttp {
List<HotVideoItemModel> list = [];
for (var i in res.data['data']) {
HotVideoItemModel videoItem = HotVideoItemModel.fromJson(i);
if (!RecommendFilter.filter(videoItem, relatedVideos: true)){
if (!RecommendFilter.filter(videoItem, relatedVideos: true)) {
list.add(videoItem);
}
}
@ -322,7 +322,7 @@ class VideoHttp {
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {'status': false, 'data': []};
return {'status': false, 'data': [], 'msg': res.data['message']};
}
}

View File

@ -25,7 +25,6 @@ import 'package:pilipala/utils/recommend_filter.dart';
import 'package:catcher_2/catcher_2.dart';
import './services/loggeer.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
MediaKit.ensureInitialized();
@ -63,7 +62,6 @@ void main() async {
},
);
// 小白条、导航栏沉浸
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
@ -72,7 +70,6 @@ void main() async {
statusBarColor: Colors.transparent,
));
Data.init();
GStrorage.lazyInit();
PiliSchame.init();
});
}

View File

@ -0,0 +1,130 @@
class RoomInfoH5Model {
RoomInfoH5Model({
this.roomInfo,
this.anchorInfo,
this.isRoomFeed,
this.watchedShow,
this.likeInfoV3,
this.blockInfo,
});
RoomInfo? roomInfo;
AnchorInfo? anchorInfo;
int? isRoomFeed;
Map? watchedShow;
LikeInfoV3? likeInfoV3;
Map? blockInfo;
RoomInfoH5Model.fromJson(Map<String, dynamic> json) {
roomInfo = RoomInfo.fromJson(json['room_info']);
anchorInfo = AnchorInfo.fromJson(json['anchor_info']);
isRoomFeed = json['is_room_feed'];
watchedShow = json['watched_show'];
likeInfoV3 = LikeInfoV3.fromJson(json['like_info_v3']);
blockInfo = json['block_info'];
}
}
class RoomInfo {
RoomInfo({
this.uid,
this.roomId,
this.title,
this.cover,
this.description,
this.liveStatus,
this.liveStartTime,
this.areaId,
this.areaName,
this.parentAreaId,
this.parentAreaName,
this.online,
this.background,
this.appBackground,
this.liveId,
});
int? uid;
int? roomId;
String? title;
String? cover;
String? description;
int? liveStatus;
int? liveStartTime;
int? areaId;
String? areaName;
int? parentAreaId;
String? parentAreaName;
int? online;
String? background;
String? appBackground;
String? liveId;
RoomInfo.fromJson(Map<String, dynamic> json) {
uid = json['uid'];
roomId = json['room_id'];
title = json['title'];
cover = json['cover'];
description = json['description'];
liveStatus = json['liveS_satus'];
liveStartTime = json['live_start_time'];
areaId = json['area_id'];
areaName = json['area_name'];
parentAreaId = json['parent_area_id'];
parentAreaName = json['parent_area_name'];
online = json['online'];
background = json['background'];
appBackground = json['app_background'];
liveId = json['live_id'];
}
}
class AnchorInfo {
AnchorInfo({
this.baseInfo,
this.relationInfo,
});
BaseInfo? baseInfo;
RelationInfo? relationInfo;
AnchorInfo.fromJson(Map<String, dynamic> json) {
baseInfo = BaseInfo.fromJson(json['base_info']);
relationInfo = RelationInfo.fromJson(json['relation_info']);
}
}
class BaseInfo {
BaseInfo({
this.uname,
this.face,
});
String? uname;
String? face;
BaseInfo.fromJson(Map<String, dynamic> json) {
uname = json['uname'];
face = json['face'];
}
}
class RelationInfo {
RelationInfo({this.attention});
int? attention;
RelationInfo.fromJson(Map<String, dynamic> json) {
attention = json['attention'];
}
}
class LikeInfoV3 {
LikeInfoV3({this.totalLikes});
int? totalLikes;
LikeInfoV3.fromJson(Map<String, dynamic> json) {
totalLikes = json['total_likes'];
}
}

View File

@ -7,6 +7,7 @@ import 'package:pilipala/http/index.dart';
import 'package:pilipala/models/github/latest.dart';
import 'package:pilipala/utils/utils.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../utils/cache_manage.dart';
class AboutPage extends StatefulWidget {
const AboutPage({super.key});
@ -17,6 +18,19 @@ class AboutPage extends StatefulWidget {
class _AboutPageState extends State<AboutPage> {
final AboutController _aboutController = Get.put(AboutController());
String cacheSize = '';
@override
void initState() {
super.initState();
// 读取缓存占用
getCacheSize();
}
Future<void> getCacheSize() async {
final res = await CacheManage().loadApplicationCache();
setState(() => cacheSize = res);
}
@override
Widget build(BuildContext context) {
@ -138,6 +152,17 @@ class _AboutPageState extends State<AboutPage> {
title: const Text('错误日志'),
trailing: Icon(Icons.arrow_forward_ios, size: 16, color: outline),
),
ListTile(
onTap: () async {
var cleanStatus = await CacheManage().clearCacheAll();
if (cleanStatus) {
getCacheSize();
}
},
title: const Text('清除缓存'),
subtitle: Text('图片及网络缓存 $cacheSize', style: subTitleStyle),
trailing: Icon(Icons.arrow_forward_ios, size: 16, color: outline),
),
],
),
),

View File

@ -9,6 +9,7 @@ import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import '../../../common/widgets/badge.dart';
// 收藏视频卡片 - 水平布局
class FavVideoCardH extends StatelessWidget {
@ -27,7 +28,9 @@ class FavVideoCardH extends StatelessWidget {
onTap: () async {
// int? seasonId;
String? epId;
if (videoItem.ogv != null && videoItem.ogv['type_name'] == '番剧') {
if (videoItem.ogv != null &&
(videoItem.ogv['type_name'] == '番剧' ||
videoItem.ogv['type_name'] == '国创')) {
videoItem.cid = await SearchHttp.ab2c(bvid: bvid);
// seasonId = videoItem.ogv['season_id'];
epId = videoItem.epId;
@ -84,22 +87,21 @@ class FavVideoCardH extends StatelessWidget {
height: maxHeight,
),
),
Positioned(
right: 4,
bottom: 4,
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 1, horizontal: 6),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: Colors.black54.withOpacity(0.4)),
child: Text(
Utils.timeFormat(videoItem.duration!),
style: const TextStyle(
fontSize: 11, color: Colors.white),
),
PBadge(
text: Utils.timeFormat(videoItem.duration!),
right: 6.0,
bottom: 6.0,
type: 'gray',
),
if (videoItem.ogv != null) ...[
PBadge(
text: videoItem.ogv['type_name'],
top: 6.0,
right: 6.0,
bottom: null,
left: null,
),
)
],
],
);
},
@ -128,86 +130,107 @@ class VideoContent extends StatelessWidget {
return Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(10, 2, 6, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
child: Stack(
children: [
Text(
videoItem.title,
textAlign: TextAlign.start,
style: const TextStyle(
fontWeight: FontWeight.w500,
letterSpacing: 0.3,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const Spacer(),
Text(
Utils.dateFormat(videoItem.ctime!),
style: TextStyle(
fontSize: 11, color: Theme.of(context).colorScheme.outline),
),
Text(
videoItem.owner.name,
style: TextStyle(
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
Row(
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
StatView(
theme: 'gray',
view: videoItem.cntInfo['play'],
Text(
videoItem.title,
textAlign: TextAlign.start,
style: const TextStyle(
fontWeight: FontWeight.w500,
letterSpacing: 0.3,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(width: 8),
StatDanMu(theme: 'gray', danmu: videoItem.cntInfo['danmaku']),
const Spacer(),
SizedBox(
width: 26,
height: 26,
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () {
showDialog(
context: Get.context!,
builder: (context) {
return AlertDialog(
title: const Text('提示'),
content: const Text('要取消收藏吗?'),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.outline),
)),
TextButton(
onPressed: () async {
await callFn!();
Get.back();
},
child: const Text('确定取消'),
)
],
);
},
);
},
icon: Icon(
Icons.clear_outlined,
if (videoItem.ogv != null) ...[
Text(
videoItem.intro,
style: TextStyle(
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
size: 18,
),
),
],
const Spacer(),
Text(
Utils.dateFormat(videoItem.favTime),
style: TextStyle(
fontSize: 11,
color: Theme.of(context).colorScheme.outline),
),
if (videoItem.owner.name != '') ...[
Text(
videoItem.owner.name,
style: TextStyle(
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
],
Padding(
padding: const EdgeInsets.only(top: 2),
child: Row(
children: [
StatView(
theme: 'gray',
view: videoItem.cntInfo['play'],
),
const SizedBox(width: 8),
StatDanMu(
theme: 'gray', danmu: videoItem.cntInfo['danmaku']),
const Spacer(),
],
),
),
],
),
Positioned(
right: 0,
bottom: -4,
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () {
showDialog(
context: Get.context!,
builder: (context) {
return AlertDialog(
title: const Text('提示'),
content: const Text('要取消收藏吗?'),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text(
'取消',
style: TextStyle(
color:
Theme.of(context).colorScheme.outline),
)),
TextButton(
onPressed: () async {
await callFn!();
Get.back();
},
child: const Text('确定取消'),
)
],
);
},
);
},
icon: Icon(
Icons.clear_outlined,
color: Theme.of(context).colorScheme.outline,
size: 18,
),
),
),
],
),
),

View File

@ -184,18 +184,32 @@ class VideoStat extends StatelessWidget {
tileMode: TileMode.mirror,
),
),
child: RichText(
maxLines: 1,
textAlign: TextAlign.justify,
softWrap: false,
text: TextSpan(
style: const TextStyle(fontSize: 11, color: Colors.white),
children: [
TextSpan(text: liveItem!.areaName!),
TextSpan(text: liveItem!.watchedShow!['text_small']),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
liveItem!.areaName!,
style: const TextStyle(fontSize: 11, color: Colors.white),
),
Text(
liveItem!.watchedShow!['text_small'],
style: const TextStyle(fontSize: 11, color: Colors.white),
),
],
),
// child: RichText(
// maxLines: 1,
// textAlign: TextAlign.justify,
// softWrap: false,
// text: TextSpan(
// style: const TextStyle(fontSize: 11, color: Colors.white),
// children: [
// TextSpan(text: liveItem!.areaName!),
// TextSpan(text: liveItem!.watchedShow!['text_small']),
// ],
// ),
// ),
);
}
}

View File

@ -4,6 +4,8 @@ import 'package:pilipala/http/live.dart';
import 'package:pilipala/models/live/room_info.dart';
import 'package:pilipala/plugin/pl_player/index.dart';
import '../../models/live/room_info_h5.dart';
class LiveRoomController extends GetxController {
String cover = '';
late int roomId;
@ -21,6 +23,7 @@ class LiveRoomController extends GetxController {
// controlsStyle: ControlsStyle.live,
// enabledButtons: const EnabledButtons(pip: true),
// );
Rx<RoomInfoH5Model> roomInfoH5 = RoomInfoH5Model().obs;
@override
void onInit() {
@ -37,10 +40,11 @@ class LiveRoomController extends GetxController {
}
}
queryLiveInfo();
queryLiveInfoH5();
}
playerInit(source) {
plPlayerController.setDataSource(
playerInit(source) async {
await plPlayerController.setDataSource(
DataSource(
videoSource: source,
audioSource: null,
@ -66,7 +70,8 @@ class LiveRoomController extends GetxController {
String videoUrl = (item.urlInfo?.first.host)! +
item.baseUrl! +
item.urlInfo!.first.extra!;
playerInit(videoUrl);
await playerInit(videoUrl);
return res;
}
}
@ -80,4 +85,12 @@ class LiveRoomController extends GetxController {
volumeOff.value = true;
}
}
Future queryLiveInfoH5() async {
var res = await LiveHttp.liveRoomInfoH5(roomId: roomId);
if (res['status']) {
roomInfoH5.value = res['data'];
}
return res;
}
}

View File

@ -19,6 +19,8 @@ class LiveRoomPage extends StatefulWidget {
class _LiveRoomPageState extends State<LiveRoomPage> {
final LiveRoomController _liveRoomController = Get.put(LiveRoomController());
PlPlayerController? plPlayerController;
late Future? _futureBuilder;
late Future? _futureBuilderFuture;
bool isShowCover = true;
bool isPlay = true;
@ -39,6 +41,8 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
if (Platform.isAndroid) {
floating = Floating();
}
_futureBuilder = _liveRoomController.queryLiveInfoH5();
_futureBuilderFuture = _liveRoomController.queryLiveInfo();
}
@override
@ -52,57 +56,123 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
@override
Widget build(BuildContext context) {
Widget videoPlayerPanel = FutureBuilder(
future: _futureBuilderFuture,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData && snapshot.data['status']) {
return PLVideoPlayer(
controller: plPlayerController!,
bottomControl: BottomControl(
controller: plPlayerController,
liveRoomCtr: _liveRoomController,
floating: floating,
),
);
} else {
return const SizedBox();
}
},
);
Widget childWhenDisabled = Scaffold(
primary: true,
appBar: PreferredSize(
preferredSize: Size.fromHeight(
MediaQuery.of(context).orientation == Orientation.portrait ? 56 : 0,
),
child: AppBar(
centerTitle: false,
titleSpacing: 0,
title: _liveRoomController.liveItem != null
? Row(
children: [
NetworkImgLayer(
width: 34,
height: 34,
type: 'avatar',
src: _liveRoomController.liveItem.face,
),
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_liveRoomController.liveItem.uname,
style: const TextStyle(fontSize: 14),
),
const SizedBox(height: 1),
if (_liveRoomController.liveItem.watchedShow != null)
Text(
_liveRoomController
.liveItem.watchedShow['text_large'] ??
'',
style: const TextStyle(fontSize: 12)),
],
),
],
)
: const SizedBox(),
// actions: [
// SizedBox(
// height: 34,
// child: ElevatedButton(onPressed: () {}, child: const Text('关注')),
// ),
// const SizedBox(width: 12),
// ],
),
),
body: Column(
backgroundColor: Colors.black,
body: Stack(
children: [
Stack(
// Obx(
// () => Positioned.fill(
// child: Opacity(
// opacity: 0.8,
// child: _liveRoomController
// .roomInfoH5.value.roomInfo?.appBackground !=
// '' &&
// _liveRoomController
// .roomInfoH5.value.roomInfo?.appBackground !=
// null
// ? NetworkImgLayer(
// width: Get.width,
// height: Get.height,
// src: _liveRoomController
// .roomInfoH5.value.roomInfo?.appBackground ??
// '',
// )
// : Image.asset(
// 'assets/images/live/default_bg.webp',
// width: Get.width,
// height: Get.height,
// ),
// ),
// ),
// ),
Positioned.fill(
child: Opacity(
opacity: 0.8,
child: Image.asset(
'assets/images/live/default_bg.webp',
width: Get.width,
height: Get.height,
),
),
),
Column(
children: [
AppBar(
centerTitle: false,
titleSpacing: 0,
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
toolbarHeight:
MediaQuery.of(context).orientation == Orientation.portrait
? 56
: 0,
title: FutureBuilder(
future: _futureBuilder,
builder: (context, snapshot) {
if (snapshot.data == null) {
return const SizedBox();
}
Map data = snapshot.data as Map;
if (data['status']) {
return Obx(
() => Row(
children: [
NetworkImgLayer(
width: 34,
height: 34,
type: 'avatar',
src: _liveRoomController
.roomInfoH5.value.anchorInfo!.baseInfo!.face,
),
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_liveRoomController.roomInfoH5.value
.anchorInfo!.baseInfo!.uname!,
style: const TextStyle(fontSize: 14),
),
const SizedBox(height: 1),
if (_liveRoomController
.roomInfoH5.value.watchedShow !=
null)
Text(
_liveRoomController.roomInfoH5.value
.watchedShow!['text_large'] ??
'',
style: const TextStyle(fontSize: 12),
),
],
),
],
),
);
} else {
return const SizedBox();
}
},
),
),
PopScope(
canPop: plPlayerController?.isFullScreen.value != true,
onPopInvoked: (bool didPop) {
@ -120,55 +190,19 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
Orientation.landscape
? Get.size.height
: Get.size.width * 9 / 16,
child: plPlayerController!.videoPlayerController != null
? PLVideoPlayer(
controller: plPlayerController!,
bottomControl: BottomControl(
controller: plPlayerController,
liveRoomCtr: _liveRoomController,
floating: floating,
),
)
: const SizedBox(),
child: videoPlayerPanel,
),
),
// if (_liveRoomController.liveItem != null &&
// _liveRoomController.liveItem.cover != null)
// Visibility(
// visible: isShowCover,
// child: Positioned(
// top: 0,
// left: 0,
// right: 0,
// child: NetworkImgLayer(
// type: 'emote',
// src: _liveRoomController.liveItem.cover,
// width: Get.size.width,
// height: videoHeight,
// ),
// ),
// ),
],
),
],
),
);
Widget childWhenEnabled = AspectRatio(
aspectRatio: 16 / 9,
child: plPlayerController!.videoPlayerController != null
? PLVideoPlayer(
controller: plPlayerController!,
bottomControl: BottomControl(
controller: plPlayerController,
liveRoomCtr: _liveRoomController,
),
)
: const SizedBox(),
);
if (Platform.isAndroid) {
return PiPSwitcher(
childWhenDisabled: childWhenDisabled,
childWhenEnabled: childWhenEnabled,
childWhenEnabled: videoPlayerPanel,
floating: floating,
);
} else {
return childWhenDisabled;

View File

@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/video_card_h.dart';
import 'package:pilipala/utils/utils.dart';
import '../../common/constants.dart';
import 'controller.dart';
class MemberArchivePage extends StatefulWidget {
@ -26,8 +25,7 @@ class _MemberArchivePageState extends State<MemberArchivePage> {
final String heroTag = Utils.makeHeroTag(mid);
_memberArchivesController =
Get.put(MemberArchiveController(), tag: heroTag);
_futureBuilderFuture =
_memberArchivesController.getMemberArchive('onRefresh');
_futureBuilderFuture = _memberArchivesController.getMemberArchive('init');
scrollController = _memberArchivesController.scrollController;
scrollController.addListener(
() {

View File

@ -19,6 +19,7 @@ import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/utils/video_utils.dart';
import 'package:screen_brightness/screen_brightness.dart';
import '../../../utils/id_utils.dart';
import 'widgets/header_control.dart';
class VideoDetailController extends GetxController
@ -61,7 +62,7 @@ class VideoDetailController extends GetxController
Box localCache = GStrorage.localCache;
Box setting = GStrorage.setting;
int oid = 0;
RxInt oid = 0.obs;
// 评论id 请求楼中楼评论使用
int fRpid = 0;
@ -135,13 +136,14 @@ class VideoDetailController extends GetxController
defaultValue: VideoDecodeFormats.values.last.code);
cacheAudioQa = setting.get(SettingBoxKey.defaultAudioQa,
defaultValue: AudioQuality.hiRes.code);
oid.value = IdUtils.bv2av(Get.parameters['bvid']!);
}
showReplyReplyPanel() {
PersistentBottomSheetController? ctr =
scaffoldKey.currentState?.showBottomSheet((BuildContext context) {
return VideoReplyReplyPanel(
oid: oid,
oid: oid.value,
rpid: fRpid,
closePanel: () => {
fRpid = 0,

View File

@ -298,7 +298,6 @@ class VideoIntroController extends GetxController {
await queryVideoInFolder();
int defaultFolderId = favFolderData.value.list!.first.id!;
int favStatus = favFolderData.value.list!.first.favState!;
print('favStatus: $favStatus');
var result = await VideoHttp.favVideo(
aid: IdUtils.bv2av(bvid),
addIds: favStatus == 0 ? '$defaultFolderId' : '',
@ -310,6 +309,8 @@ class VideoIntroController extends GetxController {
await queryHasFavVideo();
SmartDialog.showToast('✅ 操作成功');
}
} else {
SmartDialog.showToast(result['msg']);
}
return;
}
@ -340,6 +341,8 @@ class VideoIntroController extends GetxController {
await queryHasFavVideo();
SmartDialog.showToast('✅ 操作成功');
}
} else {
SmartDialog.showToast(result['msg']);
}
}
@ -476,6 +479,7 @@ class VideoIntroController extends GetxController {
final VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: heroTag);
videoDetailCtr.bvid = bvid;
videoDetailCtr.oid.value = aid;
videoDetailCtr.cid.value = cid;
videoDetailCtr.danmakuCid.value = cid;
videoDetailCtr.queryVideoUrl();

View File

@ -53,9 +53,13 @@ class VideoReplyController extends GetxController {
}
Future queryReplyList({type = 'init'}) async {
if (isLoadingMore) {
return;
}
isLoadingMore = true;
if (type == 'init') {
currentPage = 0;
noMore.value = '';
}
if (noMore.value == '没有更多了') {
return;

View File

@ -16,11 +16,13 @@ import 'widgets/reply_item.dart';
class VideoReplyPanel extends StatefulWidget {
final String? bvid;
final int? oid;
final int rpid;
final String? replyLevel;
const VideoReplyPanel({
this.bvid,
this.oid,
this.rpid = 0,
this.replyLevel,
super.key,
@ -48,16 +50,17 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
@override
void initState() {
super.initState();
int oid = widget.bvid != null ? IdUtils.bv2av(widget.bvid!) : 0;
// int oid = widget.bvid != null ? IdUtils.bv2av(widget.bvid!) : 0;
heroTag = Get.arguments['heroTag'];
replyLevel = widget.replyLevel ?? '1';
if (replyLevel == '2') {
_videoReplyController = Get.put(
VideoReplyController(oid, widget.rpid.toString(), replyLevel),
VideoReplyController(widget.oid, widget.rpid.toString(), replyLevel),
tag: widget.rpid.toString());
} else {
_videoReplyController =
Get.put(VideoReplyController(oid, '', replyLevel), tag: heroTag);
_videoReplyController = Get.put(
VideoReplyController(widget.oid, '', replyLevel),
tag: heroTag);
}
fabAnimationCtr = AnimationController(
@ -75,7 +78,8 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 300) {
EasyThrottle.throttle('replylist', const Duration(seconds: 2), () {
EasyThrottle.throttle('replylist', const Duration(milliseconds: 200),
() {
_videoReplyController.onLoad();
});
}
@ -110,7 +114,7 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
final VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: heroTag);
if (replyItem != null) {
videoDetailCtr.oid = replyItem.oid;
videoDetailCtr.oid.value = replyItem.oid;
videoDetailCtr.fRpid = replyItem.rpid!;
videoDetailCtr.firstFloor = replyItem;
videoDetailCtr.showReplyReplyPanel();

View File

@ -1,7 +1,6 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/common/widgets/badge.dart';
@ -12,10 +11,9 @@ import 'package:pilipala/pages/preview/index.dart';
import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/pages/video/detail/reply_new/index.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/url_utils.dart';
import 'package:pilipala/utils/utils.dart';
import 'zan.dart';
Box setting = GStrorage.setting;
@ -541,7 +539,6 @@ InlineSpan buildContent(
// fReplyItem 父级回复内容,用作二楼回复(回复详情)展示
final content = replyItem.content;
final List<InlineSpan> spanChilds = <InlineSpan>[];
bool hasMatchMember = false;
// 投票
if (content.vote.isNotEmpty) {
@ -591,7 +588,7 @@ InlineSpan buildContent(
if (patternStr.isNotEmpty) {
patternStr += "|";
}
patternStr += r'(\b\d{1,2}[:]\d{2}\b)';
patternStr += r'(\b(?:\d+[:])?[0-5]?[0-9][:][0-5]?[0-9]\b)';
final RegExp pattern = RegExp(patternStr);
List<String> matchedStrs = [];
void addPlainTextSpan(str) {
@ -639,7 +636,9 @@ InlineSpan buildContent(
},
),
);
} else if (RegExp(r'^\b[0-9]{1,2}[:][0-9]{2}\b$').hasMatch(matchStr)) {
} else if (RegExp(r'^\b(?:\d+[:])?[0-5]?[0-9][:][0-5]?[0-9]\b$').hasMatch(matchStr)) {
matchStr = matchStr.replaceAll('', ':');
spanChilds.add(
TextSpan(
text: ' $matchStr ',
@ -650,7 +649,6 @@ InlineSpan buildContent(
..onTap = () {
// 跳转到指定位置
try {
matchStr = matchStr.replaceAll('', ':');
SmartDialog.showToast('跳转至:$matchStr');
Get.find<VideoDetailController>(tag: Get.arguments['heroTag'])
.plPlayerController
@ -692,16 +690,54 @@ InlineSpan buildContent(
color: Theme.of(context).colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () {
..onTap = () async {
final String title = content.jumpUrl[matchStr]['title'];
if (appUrlSchema == '') {
final String str = Uri.parse(matchStr).pathSegments[0];
final Map matchRes = IdUtils.matchAvorBv(input: str);
final List matchKeys = matchRes.keys.toList();
if (matchKeys.isNotEmpty) {
if (matchKeys.first == 'BV') {
final String redirectUrl =
await UrlUtils.parseRedirectUrl(matchStr);
final String pathSegment = Uri.parse(redirectUrl).path;
final String lastPathSegment =
pathSegment.split('/').last;
if (lastPathSegment.startsWith('BV')) {
UrlUtils.matchUrlPush(
lastPathSegment,
title,
redirectUrl,
);
} else {
Get.toNamed(
'/webview',
parameters: {
'url': redirectUrl,
'type': 'url',
'pageTitle': title
},
);
}
} else {
if (appUrlSchema.startsWith('bilibili://search')) {
Get.toNamed('/searchResult',
parameters: {'keyword': title});
} else if (matchStr.startsWith('https://b23.tv')) {
final String redirectUrl =
await UrlUtils.parseRedirectUrl(matchStr);
final String pathSegment = Uri.parse(redirectUrl).path;
final String lastPathSegment =
pathSegment.split('/').last;
if (lastPathSegment.startsWith('BV')) {
UrlUtils.matchUrlPush(
lastPathSegment,
title,
redirectUrl,
);
} else {
Get.toNamed(
'/searchResult',
parameters: {'keyword': matchRes['BV']},
'/webview',
parameters: {
'url': redirectUrl,
'type': 'url',
'pageTitle': title
},
);
}
} else {
@ -710,16 +746,10 @@ InlineSpan buildContent(
parameters: {
'url': matchStr,
'type': 'url',
'pageTitle': ''
'pageTitle': title
},
);
}
} else {
if (appUrlSchema.startsWith('bilibili://search')) {
Get.toNamed('/searchResult', parameters: {
'keyword': content.jumpUrl[matchStr]['title']
});
}
}
},
)
@ -739,6 +769,47 @@ InlineSpan buildContent(
},
);
if (content.jumpUrl.keys.isNotEmpty) {
List<String> unmatchedItems = content.jumpUrl.keys
.toList()
.where((item) => !content.message.contains(item))
.toList();
if (unmatchedItems.isNotEmpty) {
for (int i = 0; i < unmatchedItems.length; i++) {
String patternStr = unmatchedItems[i];
spanChilds.addAll(
[
if (content.jumpUrl[patternStr]?['prefix_icon'] != null) ...[
WidgetSpan(
child: Image.network(
content.jumpUrl[patternStr]['prefix_icon'],
height: 19,
color: Theme.of(context).colorScheme.primary,
),
)
],
TextSpan(
text: content.jumpUrl[patternStr]['title'],
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () {
Get.toNamed(
'/webview',
parameters: {
'url': patternStr,
'type': 'url',
'pageTitle': content.jumpUrl[patternStr]['title']
},
);
},
)
],
);
}
}
}
// 图片渲染
if (content.pictures.isNotEmpty) {
final List<String> picList = <String>[];
@ -753,11 +824,15 @@ InlineSpan buildContent(
builder: (BuildContext context, BoxConstraints box) {
double maxHeight = box.maxWidth * 0.6; // 设置最大高度
// double width = (box.maxWidth / 2).truncateToDouble();
double height = ((box.maxWidth /
2 *
pictureItem['img_height'] /
pictureItem['img_width']))
.truncateToDouble();
double height = 100;
try {
height = ((box.maxWidth /
2 *
pictureItem['img_height'] /
pictureItem['img_width']))
.truncateToDouble();
} catch (_) {}
return GestureDetector(
onTap: () {
showDialog(

View File

@ -570,8 +570,11 @@ class _VideoDetailPageState extends State<VideoDetailPage>
);
},
),
VideoReplyPanel(
bvid: videoDetailController.bvid,
Obx(
() => VideoReplyPanel(
bvid: videoDetailController.bvid,
oid: videoDetailController.oid.value,
),
)
],
),

View File

@ -586,6 +586,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
),
/// 进度条 live模式下禁用
Obx(
() {
final int value = _.sliderPositionSeconds.value;
@ -609,7 +610,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
}
if (_.videoType.value == 'live') {
return nil;
return const SizedBox();
}
if (value > max || max <= 0) {
return nil;

154
lib/utils/cache_manage.dart Normal file
View File

@ -0,0 +1,154 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
class CacheManage {
CacheManage._internal();
static final CacheManage cacheManage = CacheManage._internal();
factory CacheManage() => cacheManage;
// 获取缓存目录
Future<String> loadApplicationCache() async {
/// clear all of image in memory
// clearMemoryImageCache();
/// get ImageCache
// var res = getMemoryImageCache();
// 缓存大小
double cacheSize = 0;
// cached_network_image directory
Directory tempDirectory = await getTemporaryDirectory();
// get_storage directory
Directory docDirectory = await getApplicationDocumentsDirectory();
// 获取缓存大小
if (tempDirectory.existsSync()) {
double value = await getTotalSizeOfFilesInDir(tempDirectory);
cacheSize += value;
}
/// 获取缓存大小 dioCache
if (docDirectory.existsSync()) {
double value = 0;
String dioCacheFileName =
'${docDirectory.path}${Platform.pathSeparator}DioCache.db';
var dioCacheFile = File(dioCacheFileName);
if (dioCacheFile.existsSync()) {
value = await getTotalSizeOfFilesInDir(dioCacheFile);
}
cacheSize += value;
}
return formatSize(cacheSize);
}
// 循环计算文件的大小(递归)
Future<double> getTotalSizeOfFilesInDir(final FileSystemEntity file) async {
if (file is File) {
int length = await file.length();
return double.parse(length.toString());
}
if (file is Directory) {
final List<FileSystemEntity> children = file.listSync();
double total = 0;
for (final FileSystemEntity child in children) {
total += await getTotalSizeOfFilesInDir(child);
}
return total;
}
return 0;
}
// 缓存大小格式转换
String formatSize(double value) {
List<String> unitArr = ['B', 'K', 'M', 'G'];
int index = 0;
while (value > 1024) {
index++;
value = value / 1024;
}
String size = value.toStringAsFixed(2);
return size + unitArr[index];
}
// 清除缓存
Future<bool> clearCacheAll() async {
bool cleanStatus = await SmartDialog.show(
useSystem: true,
animationType: SmartAnimationType.centerFade_otherSlide,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('提示'),
content: const Text('该操作将清除图片及网络请求缓存数据,确认清除?'),
actions: [
TextButton(
onPressed: (() => {SmartDialog.dismiss()}),
child: Text(
'取消',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
),
),
TextButton(
onPressed: () async {
SmartDialog.dismiss();
SmartDialog.showLoading(msg: '正在清除...');
try {
// 清除缓存 图片缓存
await clearLibraryCache();
Timer(const Duration(milliseconds: 500), () {
SmartDialog.dismiss().then((res) {
SmartDialog.showToast('清除完成');
});
});
} catch (err) {
SmartDialog.dismiss();
SmartDialog.showToast(err.toString());
}
},
child: const Text('确认'),
)
],
);
},
).then((res) {
return true;
});
return cleanStatus;
}
/// 清除 Documents 目录下的 DioCache.db
Future clearApplicationCache() async {
Directory directory = await getApplicationDocumentsDirectory();
if (directory.existsSync()) {
String dioCacheFileName =
'${directory.path}${Platform.pathSeparator}DioCache.db';
var dioCacheFile = File(dioCacheFileName);
if (dioCacheFile.existsSync()) {
dioCacheFile.delete();
}
}
}
// 清除 Library/Caches 目录及文件缓存
Future clearLibraryCache() async {
var appDocDir = await getTemporaryDirectory();
if (appDocDir.existsSync()) {
await appDocDir.delete(recursive: true);
}
}
/// 递归方式删除目录及文件
Future deleteDirectory(FileSystemEntity file) async {
if (file is Directory) {
final List<FileSystemEntity> children = file.listSync();
for (final FileSystemEntity child in children) {
await deleteDirectory(child);
}
}
await file.delete();
}
}

View File

@ -42,6 +42,8 @@ class GStrorage {
return deletedEntries > 10;
},
);
// 视频设置
video = await Hive.openBox('video');
}
static void regAdapter() {
@ -52,11 +54,6 @@ class GStrorage {
Hive.registerAdapter(HotSearchItemAdapter());
}
static Future<void> lazyInit() async {
// 视频设置
video = await Hive.openBox('video');
}
static Future<void> close() async {
// user.compact();
// user.close();

61
lib/utils/url_utils.dart Normal file
View File

@ -0,0 +1,61 @@
import 'package:dio/dio.dart';
import 'package:get/get.dart';
import '../http/search.dart';
import 'id_utils.dart';
import 'utils.dart';
class UrlUtils {
// 302重定向路由截取
static Future<String> parseRedirectUrl(String url) async {
late String redirectUrl;
final dio = Dio();
dio.options.followRedirects = false;
dio.options.validateStatus = (status) {
return status == 200 || status == 301 || status == 302;
};
final response = await dio.get(url);
if (response.statusCode == 302) {
redirectUrl = response.headers['location']?.first as String;
if (redirectUrl.endsWith('/')) {
redirectUrl = redirectUrl.substring(0, redirectUrl.length - 1);
}
} else {
if (url.endsWith('/')) {
url = url.substring(0, url.length - 1);
}
return url;
}
return redirectUrl;
}
// 匹配url路由跳转
static matchUrlPush(
String pathSegment,
String title,
String redirectUrl,
) async {
final Map matchRes = IdUtils.matchAvorBv(input: pathSegment);
if (matchRes.containsKey('BV')) {
final String bv = matchRes['BV'];
final int cid = await SearchHttp.ab2c(bvid: bv);
final String heroTag = Utils.makeHeroTag(bv);
await Get.toNamed(
'/video?bvid=$bv&cid=$cid',
arguments: <String, String?>{
'pic': '',
'heroTag': heroTag,
},
);
} else {
await Get.toNamed(
'/webview',
parameters: {
'url': redirectUrl,
'type': 'url',
'pageTitle': title,
},
);
}
}
}

View File

@ -188,6 +188,7 @@ flutter:
- assets/images/
- assets/images/lv/
- assets/images/logo/
- assets/images/live/
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware