Compare commits

...

29 Commits

Author SHA1 Message Date
2ece96df21 fix: 合集顺序播放 2023-11-12 14:07:50 +08:00
c11c5695a2 mod: 合集连播 2023-11-08 23:02:13 +08:00
93581c2932 mod: 播放详情页样式 2023-11-03 23:40:47 +08:00
fd43a8cb31 mod: 历史记录搜索 2023-11-03 23:39:21 +08:00
6f34bacb64 mod: 样式 2023-10-15 16:15:16 +08:00
90314f89ed mod: 还原全屏方式 2023-10-14 17:18:00 +08:00
45c53de2c2 mod: 投稿搜索无内容样式 2023-10-14 17:04:35 +08:00
5c07cb4545 mod: 投稿搜索无内容样式 2023-10-14 16:59:03 +08:00
844053b138 mod: 首次登录时自动加载黑名单 2023-10-14 16:46:41 +08:00
6af8b91f63 fix: 黑名单个数 2023-10-14 16:15:14 +08:00
8aa02f7450 mod: 移除黑名单 2023-10-14 16:07:52 +08:00
38a1f2e1f7 fix: 搜索黑名单问题 2023-10-10 00:01:41 +08:00
b2e1d98f51 fix: 搜索黑名单问题 2023-10-10 00:00:38 +08:00
e19cf92992 mod: 首页布局 2023-10-08 23:16:39 +08:00
80e10aeaad mod: 历史记录删除逻辑 2023-10-08 22:43:44 +08:00
b1a9152a49 mod: 搜索结果拉黑用户逻辑 2023-10-08 22:21:40 +08:00
935b7577b3 mod: 历史记录多选选中样式 2023-10-02 22:12:20 +08:00
fd5c4463d2 mod: 历史记录多选选中样式 2023-10-01 14:27:21 +08:00
7fcbe4dd9d feat: 历史记录多选删除 2023-10-01 10:50:45 +08:00
9a0c9f4021 feat: 按照黑名单对搜索结果进行屏蔽 2023-09-27 23:24:55 +08:00
6b3773a074 mod: 个人主页隐藏背景图 2023-09-27 22:22:23 +08:00
7be8ebaa7e mod: 搜索up主投稿 2023-09-27 22:19:49 +08:00
092b1cee3d mod 2023-09-20 22:53:29 +08:00
252f39e8c7 mod 2023-09-17 20:26:00 +08:00
e631ca04a0 mod: 隐藏签名 2023-09-10 14:52:00 +08:00
3665d6a0f6 mod: 个人中心注释 2023-09-10 14:42:14 +08:00
e3c9e8c13b mod: 移除热搜、搜索提示词 2023-09-09 13:33:30 +08:00
39995bae23 mod: 隐藏番剧、直播搜索 2023-09-09 12:18:38 +08:00
f42d0d01ea mod: custom version 2 2023-09-09 11:46:24 +08:00
55 changed files with 2132 additions and 1386 deletions

View File

@ -1,8 +1,6 @@
PODS: PODS:
- appscheme (1.0.4): - appscheme (1.0.4):
- Flutter - Flutter
- auto_orientation (0.0.1):
- Flutter
- connectivity_plus (0.0.1): - connectivity_plus (0.0.1):
- Flutter - Flutter
- ReachabilitySwift - ReachabilitySwift
@ -54,7 +52,6 @@ PODS:
DEPENDENCIES: DEPENDENCIES:
- appscheme (from `.symlinks/plugins/appscheme/ios`) - appscheme (from `.symlinks/plugins/appscheme/ios`)
- auto_orientation (from `.symlinks/plugins/auto_orientation/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
@ -85,8 +82,6 @@ SPEC REPOS:
EXTERNAL SOURCES: EXTERNAL SOURCES:
appscheme: appscheme:
:path: ".symlinks/plugins/appscheme/ios" :path: ".symlinks/plugins/appscheme/ios"
auto_orientation:
:path: ".symlinks/plugins/auto_orientation/ios"
connectivity_plus: connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios" :path: ".symlinks/plugins/connectivity_plus/ios"
device_info_plus: device_info_plus:
@ -132,7 +127,6 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS: SPEC CHECKSUMS:
appscheme: b1c3f8862331cb20430cf9e0e4af85dbc1572ad8 appscheme: b1c3f8862331cb20430cf9e0e4af85dbc1572ad8
auto_orientation: 102ed811a5938d52c86520ddd7ecd3a126b5d39d
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854

View File

@ -7,6 +7,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/http/user.dart'; import 'package:pilipala/http/user.dart';
import 'package:pilipala/pages/member/index.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';
@ -33,9 +34,10 @@ class VideoCardH extends StatelessWidget {
String heroTag = Utils.makeHeroTag(aid); String heroTag = Utils.makeHeroTag(aid);
return GestureDetector( return GestureDetector(
onLongPress: () { onLongPress: () {
if (longPress != null) { // if (longPress != null) {
longPress!(); // longPress!();
} // }
MemberController().blockUser(videoItem.mid);
}, },
// onLongPressEnd: (details) { // onLongPressEnd: (details) {
// if (longPressEnd != null) { // if (longPressEnd != null) {
@ -188,46 +190,7 @@ class VideoContent extends StatelessWidget {
color: Theme.of(context).colorScheme.outline, color: Theme.of(context).colorScheme.outline,
), ),
), ),
],
),
Row(
children: [
StatView(
theme: 'gray',
view: videoItem.stat.view,
),
const SizedBox(width: 8),
StatDanMu(
theme: 'gray',
danmu: videoItem.stat.danmaku,
),
// Text(
// Utils.dateFormat(videoItem.pubdate!),
// style: TextStyle(
// fontSize: 11,
// color: Theme.of(context).colorScheme.outline),
// )
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,
@ -261,6 +224,20 @@ class VideoContent extends StatelessWidget {
], ],
), ),
), ),
// PopupMenuItem<String>(
// onTap: () async {
// MemberController().blockUser(videoItem.mid);
// },
// value: 'block',
// height: 35,
// child: const Row(
// children: [
// Icon(Icons.block, size: 16),
// SizedBox(width: 6),
// Text('拉黑up', style: TextStyle(fontSize: 13))
// ],
// ),
// ),
], ],
), ),
), ),

View File

@ -48,7 +48,7 @@ class VideoCardV extends StatelessWidget {
arguments: { arguments: {
'pic': videoItem.pic, 'pic': videoItem.pic,
'heroTag': heroTag, 'heroTag': heroTag,
'videoType': SearchType.media_bangumi, // 'videoType': SearchType.media_bangumi,
}, },
), ),
); );

View File

@ -164,6 +164,9 @@ class Api {
// 清空历史记录 // 清空历史记录
static const String clearHistory = '/x/v2/history/clear'; static const String clearHistory = '/x/v2/history/clear';
// 删除某条历史记录
static const String delHistory = '/x/v2/history/delete';
// 热搜 // 热搜
static const String hotSearchList = static const String hotSearchList =
'https://s.search.bilibili.com/main/hotword'; 'https://s.search.bilibili.com/main/hotword';
@ -239,6 +242,9 @@ class Api {
// wts=1689767832 // wts=1689767832
static const String memberArchive = '/x/space/wbi/arc/search'; static const String memberArchive = '/x/space/wbi/arc/search';
// 用户动态搜索
static const String memberDynamicSearch = '/x/space/dynamic/search';
// 用户动态 // 用户动态
static const String memberDynamic = '/x/polymer/web-dynamic/v1/feed/space'; static const String memberDynamic = '/x/polymer/web-dynamic/v1/feed/space';
@ -285,6 +291,9 @@ class Api {
// 黑名单 // 黑名单
static const String blackLst = '/x/relation/blacks'; static const String blackLst = '/x/relation/blacks';
// 移除黑名单
static const String removeBlack = '/x/relation/modify';
// github 获取最新版 // github 获取最新版
static const String latestApp = static const String latestApp =
'https://api.github.com/repos/guozhigq/pilipala/releases/latest'; 'https://api.github.com/repos/guozhigq/pilipala/releases/latest';
@ -294,4 +303,7 @@ class Api {
static const String onlineTotal = '/x/player/online/total'; static const String onlineTotal = '/x/player/online/total';
static const String webDanmaku = '/x/v2/dm/web/seg.so'; static const String webDanmaku = '/x/v2/dm/web/seg.so';
// 搜索历史记录
static const String searchHistory = '/x/web-goblin/history/search';
} }

View File

@ -23,4 +23,31 @@ class BlackHttp {
}; };
} }
} }
// 移除黑名单
static Future removeBlack({required int fid}) async {
var res = await Request().post(
Api.removeBlack,
queryParameters: {
'act': 6,
'csrf': await Request.getCsrf(),
'fid': fid,
'jsonp': 'jsonp',
're_src': 116,
},
);
if (res.data['code'] == 0) {
return {
'status': true,
'data': [],
'msg': '操作成功',
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
} }

View File

@ -65,7 +65,7 @@ class MemberHttp {
int ps = 30, int ps = 30,
int tid = 0, int tid = 0,
int? pn, int? pn,
String keyword = '', String? keyword,
String order = 'pubdate', String order = 'pubdate',
bool orderAvoided = true, bool orderAvoided = true,
}) async { }) async {
@ -74,7 +74,7 @@ class MemberHttp {
'ps': ps, 'ps': ps,
'tid': tid, 'tid': tid,
'pn': pn, 'pn': pn,
'keyword': keyword, 'keyword': keyword ?? '',
'order': order, 'order': order,
'platform': 'web', 'platform': 'web',
'web_location': 1550101, 'web_location': 1550101,
@ -119,4 +119,27 @@ class MemberHttp {
}; };
} }
} }
// 搜索用户动态
static Future memberDynamicSearch({int? pn, int? ps, int? mid}) async {
var res = await Request().get(Api.memberDynamic, data: {
'keyword': '海拔',
'mid': mid,
'pn': pn,
'ps': ps,
'platform': 'web'
});
if (res.data['code'] == 0) {
return {
'status': true,
'data': DynamicsDataModel.fromJson(res.data['data']),
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
} }

View File

@ -1,37 +1,63 @@
import 'dart:convert';
import 'package:hive/hive.dart';
import 'package:pilipala/http/index.dart'; import 'package:pilipala/http/index.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/models/search/hot.dart'; import 'package:pilipala/models/search/hot.dart';
import 'package:pilipala/models/search/result.dart'; import 'package:pilipala/models/search/result.dart';
import 'package:pilipala/models/search/suggest.dart'; import 'package:pilipala/models/search/suggest.dart';
import 'package:pilipala/utils/storage.dart';
class SearchHttp { class SearchHttp {
static Box setting = GStrorage.setting;
static Future hotSearchList() async { static Future hotSearchList() async {
var res = await Request().get(Api.hotSearchList); var res = await Request().get(Api.hotSearchList);
if (res.data['code'] == 0) { if (res.data is String) {
Map<String, dynamic> resultMap = json.decode(res.data);
if (resultMap['code'] == 0) {
return {
'status': true,
'data': HotSearchModel.fromJson(resultMap),
};
}
} else if (res.data is Map<String, dynamic> && res.data['code'] == 0) {
return { return {
'status': true, 'status': true,
'data': HotSearchModel.fromJson(res.data), 'data': HotSearchModel.fromJson(res.data),
}; };
} else {
return {
'status': false,
'data': [],
'msg': '请求错误 🙅',
};
} }
return {
'status': false,
'data': [],
'msg': '请求错误 🙅',
};
} }
// 获取搜索建议 // 获取搜索建议
static Future searchSuggest({required term}) async { static Future searchSuggest({required term}) async {
var res = await Request().get(Api.serachSuggest, var res = await Request().get(Api.serachSuggest,
data: {'term': term, 'main_ver': 'v1', 'highlight': term}); data: {'term': term, 'main_ver': 'v1', 'highlight': term});
if (res.data['code'] == 0) { if (res.data is String) {
res.data['result']['term'] = term; Map<String, dynamic> resultMap = json.decode(res.data);
return { if (resultMap['code'] == 0) {
'status': true, if (resultMap['result'] is Map) {
'data': SearchSuggestModel.fromJson(res.data['result']), resultMap['result']['term'] = term;
}; }
return {
'status': true,
'data': resultMap['result'] is Map
? SearchSuggestModel.fromJson(resultMap['result'])
: [],
};
} else {
return {
'status': false,
'data': [],
'msg': '请求错误 🙅',
};
}
} else { } else {
return { return {
'status': false, 'status': false,
@ -61,29 +87,44 @@ class SearchHttp {
var res = await Request().get(Api.searchByType, data: reqData); var res = await Request().get(Api.searchByType, data: reqData);
if (res.data['code'] == 0 && res.data['data']['numPages'] > 0) { if (res.data['code'] == 0 && res.data['data']['numPages'] > 0) {
Object data; Object data;
switch (searchType) { try {
case SearchType.video: switch (searchType) {
data = SearchVideoModel.fromJson(res.data['data']); case SearchType.video:
break; List<int> blackMidsList =
case SearchType.live_room: setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]);
data = SearchLiveModel.fromJson(res.data['data']); for (var i in res.data['data']['result']) {
break; // 屏蔽推广和拉黑用户
case SearchType.bili_user: i['available'] = !blackMidsList.contains(i['mid']);
data = SearchUserModel.fromJson(res.data['data']); }
break; data = SearchVideoModel.fromJson(res.data['data']);
case SearchType.media_bangumi: break;
data = SearchMBangumiModel.fromJson(res.data['data']); case SearchType.live_room:
break; data = SearchLiveModel.fromJson(res.data['data']);
break;
case SearchType.bili_user:
data = SearchUserModel.fromJson(res.data['data']);
break;
case SearchType.media_bangumi:
data = SearchMBangumiModel.fromJson(res.data['data']);
break;
case SearchType.article:
data = SearchArticleModel.fromJson(res.data['data']);
break;
}
return {
'status': true,
'data': data,
};
} catch (err) {
print(err);
} }
return {
'status': true,
'data': data,
};
} else { } else {
return { return {
'status': false, 'status': false,
'data': [], 'data': [],
'msg': res.data['data']['numPages'] == 0 ? '没有相关数据' : '请求错误 🙅', 'msg': res.data['data'] != null && res.data['data']['numPages'] == 0
? '没有相关数据'
: res.data['message'],
}; };
} }
} }

View File

@ -231,4 +231,39 @@ class UserHttp {
return {'status': false, 'msg': res.data['message']}; return {'status': false, 'msg': res.data['message']};
} }
} }
// 删除历史记录
static Future delHistory(kid) async {
var res = await Request().post(
Api.delHistory,
queryParameters: {
'kid': kid,
'jsonp': 'jsonp',
'csrf': await Request.getCsrf(),
},
);
if (res.data['code'] == 0) {
return {'status': true, 'msg': '已删除'};
} else {
return {'status': false, 'msg': res.data['message']};
}
}
// 搜索历史记录
static Future searchHistory(
{required int pn, required String keyword}) async {
var res = await Request().get(
Api.searchHistory,
data: {
'pn': pn,
'keyword': keyword,
'business': 'all',
},
);
if (res.data['code'] == 0) {
return {'status': true, 'data': HistoryData.fromJson(res.data['data'])};
} else {
return {'status': false, 'msg': res.data['message']};
}
}
} }

View File

@ -12,20 +12,20 @@ enum SearchType {
live_room, live_room,
// 主播live_user // 主播live_user
// live_user, // live_user,
// 专栏article
// article,
// 话题topic // 话题topic
// topic, // topic,
// 用户bili_user // 用户bili_user
bili_user, bili_user,
// 专栏article
article,
// 相簿photo // 相簿photo
// photo // photo
} }
extension SearchTypeExtension on SearchType { extension SearchTypeExtension on SearchType {
String get type => String get type =>
['video', 'media_bangumi', 'live_room', 'bili_user'][index]; ['video', 'media_bangumi', 'live_room', 'bili_user', 'article'][index];
String get label => ['视频', '番剧', '直播间', '用户'][index]; String get label => ['视频', '番剧', '直播间', '用户', '专栏'][index];
} }
// 搜索类型为视频、专栏及相簿时 // 搜索类型为视频、专栏及相簿时

View File

@ -6,6 +6,7 @@ class SearchVideoModel {
List<SearchVideoItemModel>? list; List<SearchVideoItemModel>? list;
SearchVideoModel.fromJson(Map<String, dynamic> json) { SearchVideoModel.fromJson(Map<String, dynamic> json) {
list = json['result'] list = json['result']
.where((e) => e['available'] == true)
.map<SearchVideoItemModel>((e) => SearchVideoItemModel.fromJson(e)) .map<SearchVideoItemModel>((e) => SearchVideoItemModel.fromJson(e))
.toList(); .toList();
} }
@ -17,7 +18,7 @@ class SearchVideoItemModel {
this.id, this.id,
this.cid, this.cid,
// this.author, // this.author,
// this.mid, this.mid,
// this.typeid, // this.typeid,
// this.typename, // this.typename,
this.arcurl, this.arcurl,
@ -47,7 +48,7 @@ class SearchVideoItemModel {
int? id; int? id;
int? cid; int? cid;
// String? author; // String? author;
// String? mid; int? mid;
// String? typeid; // String? typeid;
// String? typename; // String? typename;
String? arcurl; String? arcurl;
@ -80,6 +81,7 @@ class SearchVideoItemModel {
arcurl = json['arcurl']; arcurl = json['arcurl'];
aid = json['aid']; aid = json['aid'];
bvid = json['bvid']; bvid = json['bvid'];
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']);
description = json['description']; description = json['description'];
@ -376,3 +378,75 @@ class SearchMBangumiItemModel {
indexShow = json['index_show']; indexShow = json['index_show'];
} }
} }
class SearchArticleModel {
SearchArticleModel({this.list});
List<SearchArticleItemModel>? list;
SearchArticleModel.fromJson(Map<String, dynamic> json) {
list = json['result'] != null
? json['result']
.map<SearchArticleItemModel>(
(e) => SearchArticleItemModel.fromJson(e))
.toList()
: [];
}
}
class SearchArticleItemModel {
SearchArticleItemModel({
this.pubTime,
this.like,
this.title,
this.subTitle,
this.rankOffset,
this.mid,
this.imageUrls,
this.id,
this.categoryId,
this.view,
this.reply,
this.desc,
this.rankScore,
this.type,
this.templateId,
this.categoryName,
});
int? pubTime;
int? like;
List? title;
String? subTitle;
int? rankOffset;
int? mid;
List? imageUrls;
int? id;
int? categoryId;
int? view;
int? reply;
String? desc;
int? rankScore;
String? type;
int? templateId;
String? categoryName;
SearchArticleItemModel.fromJson(Map<String, dynamic> json) {
pubTime = json['pub_time'];
like = json['like'];
title = Em.regTitle(json['title']);
subTitle = json['title'].replaceAll(RegExp(r'<[^>]*>'), '');
rankOffset = json['rank_offset'];
mid = json['mid'];
imageUrls = json['image_urls'];
id = json['id'];
categoryId = json['category_id'];
view = json['view'];
reply = json['reply'];
desc = json['desc'];
rankScore = json['rank_score'];
type = json['type'];
templateId = json['templateId'];
categoryName = json['category_name'];
}
}

View File

@ -3,17 +3,23 @@ class HistoryData {
this.cursor, this.cursor,
this.tab, this.tab,
this.list, this.list,
this.page,
}); });
Cursor? cursor; Cursor? cursor;
List<HisTabItem>? tab; List<HisTabItem>? tab;
List<HisListItem>? list; List<HisListItem>? list;
Map? page;
HistoryData.fromJson(Map<String, dynamic> json) { HistoryData.fromJson(Map<String, dynamic> json) {
cursor = Cursor.fromJson(json['cursor']); cursor = json['cursor'] != null ? Cursor.fromJson(json['cursor']) : null;
tab = json['tab'].map<HisTabItem>((e) => HisTabItem.fromJson(e)).toList(); tab = json['tab'] != null
list = ? json['tab'].map<HisTabItem>((e) => HisTabItem.fromJson(e)).toList()
json['list'].map<HisListItem>((e) => HisListItem.fromJson(e)).toList(); : [];
list = json['list'] != null
? json['list'].map<HisListItem>((e) => HisListItem.fromJson(e)).toList()
: [];
page = json['page'];
} }
} }
@ -79,6 +85,7 @@ class HisListItem {
this.kid, this.kid,
this.tagName, this.tagName,
this.liveStatus, this.liveStatus,
this.checked,
}); });
String? title; String? title;
@ -105,6 +112,7 @@ class HisListItem {
int? kid; int? kid;
String? tagName; String? tagName;
int? liveStatus; int? liveStatus;
bool? checked;
HisListItem.fromJson(Map<String, dynamic> json) { HisListItem.fromJson(Map<String, dynamic> json) {
title = json['title']; title = json['title'];
@ -131,6 +139,7 @@ class HisListItem {
kid = json['kid']; kid = json['kid'];
tagName = json['tag_name']; tagName = json['tag_name'];
liveStatus = json['live_status']; liveStatus = json['live_status'];
checked = false;
} }
} }

View File

@ -9,6 +9,7 @@ import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/user/fav_folder.dart'; import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/pages/video/detail/reply/index.dart'; import 'package:pilipala/pages/video/detail/reply/index.dart';
import 'package:pilipala/plugin/pl_player/models/play_repeat.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/storage.dart'; import 'package:pilipala/utils/storage.dart';
@ -292,4 +293,31 @@ class BangumiIntroController extends GetxController {
} }
return result; return result;
} }
/// 列表循环或者顺序播放时,自动播放下一个
void nextPlay() {
late List episodes;
if (bangumiDetail.value.episodes != null) {
episodes = bangumiDetail.value.episodes!;
}
VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);
int currentIndex =
episodes.indexWhere((e) => e.cid == videoDetailCtr.cid.value);
int nextIndex = currentIndex + 1;
PlayRepeat platRepeat = videoDetailCtr.plPlayerController.playRepeat;
// 列表循环
if (platRepeat == PlayRepeat.listCycle) {
if (nextIndex == episodes.length - 1) {
nextIndex = 0;
}
}
if (nextIndex <= episodes.length - 1 &&
platRepeat == PlayRepeat.listOrder) {}
int cid = episodes[nextIndex].cid!;
String bvid = episodes[nextIndex].bvid!;
int aid = episodes[nextIndex].aid!;
changeSeasonOrbangu(bvid, cid, aid);
}
} }

View File

@ -62,7 +62,7 @@ class BangumiCardV extends StatelessWidget {
arguments: { arguments: {
'pic': pic, 'pic': pic,
'heroTag': heroTag, 'heroTag': heroTag,
'videoType': SearchType.media_bangumi, // 'videoType': SearchType.media_bangumi,
'bangumiItem': res['data'], 'bangumiItem': res['data'],
}, },
); );

View File

@ -1,4 +1,5 @@
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:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/http_error.dart';
@ -60,7 +61,7 @@ class _BlackListPageState extends State<BlackListPage> {
centerTitle: false, centerTitle: false,
title: Obx( title: Obx(
() => Text( () => Text(
'黑名单管理 ${_blackListController.blackList.length} / 5000', '黑名单管理 - ${_blackListController.total.value}',
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
), ),
@ -104,10 +105,11 @@ class _BlackListPageState extends State<BlackListPage> {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
dense: true, dense: true,
// trailing: TextButton( trailing: TextButton(
// onPressed: () {}, onPressed: () => _blackListController
// child: const Text('移除'), .removeBlack(list[index].mid),
// ), child: const Text('移除'),
),
); );
}, },
), ),
@ -136,6 +138,7 @@ class _BlackListPageState extends State<BlackListPage> {
class BlackListController extends GetxController { class BlackListController extends GetxController {
int currentPage = 1; int currentPage = 1;
int pageSize = 50; int pageSize = 50;
RxInt total = 0.obs;
RxList<BlackListItem> blackList = [BlackListItem()].obs; RxList<BlackListItem> blackList = [BlackListItem()].obs;
Future queryBlacklist({type = 'init'}) async { Future queryBlacklist({type = 'init'}) async {
@ -146,6 +149,7 @@ class BlackListController extends GetxController {
if (result['status']) { if (result['status']) {
if (type == 'init') { if (type == 'init') {
blackList.value = result['data'].list; blackList.value = result['data'].list;
total.value = result['data'].total;
} else { } else {
blackList.addAll(result['data'].list); blackList.addAll(result['data'].list);
} }
@ -154,4 +158,13 @@ class BlackListController extends GetxController {
} }
return result; return result;
} }
Future removeBlack(mid) async {
var result = await BlackHttp.removeBlack(fid: mid);
if (result['status']) {
blackList.removeWhere((e) => e.mid == mid);
total.value = total.value - 1;
SmartDialog.showToast(result['msg']);
}
}
} }

View File

@ -214,7 +214,7 @@ class DynamicsController extends GetxController {
arguments: { arguments: {
'pic': pic, 'pic': pic,
'heroTag': heroTag, 'heroTag': heroTag,
'videoType': SearchType.media_bangumi, // 'videoType': SearchType.media_bangumi,
'bangumiItem': res['data'], 'bangumiItem': res['data'],
}, },
); );

View File

@ -49,8 +49,8 @@ class FavVideoCardH extends StatelessWidget {
Get.toNamed('/video', parameters: parameters, arguments: { Get.toNamed('/video', parameters: parameters, arguments: {
'videoItem': videoItem, 'videoItem': videoItem,
'heroTag': heroTag, 'heroTag': heroTag,
'videoType': // 'videoType':
epId != null ? SearchType.media_bangumi : SearchType.video, // epId != null ? SearchType.media_bangumi : SearchType.video,
}); });
}, },
child: Column( child: Column(
@ -142,21 +142,15 @@ class VideoContent extends StatelessWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
const Spacer(), const Spacer(),
Text(
videoItem.owner.name,
style: TextStyle(
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
Row( Row(
children: [ children: [
StatView( Text(
theme: 'gray', videoItem.owner.name,
view: videoItem.cntInfo['play'], style: TextStyle(
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
), ),
const SizedBox(width: 8),
StatDanMu(theme: 'gray', danmu: videoItem.cntInfo['danmaku']),
const Spacer(), const Spacer(),
SizedBox( SizedBox(
width: 26, width: 26,

View File

@ -27,12 +27,6 @@ Widget followItem({item}) {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 14), style: const TextStyle(fontSize: 14),
), ),
subtitle: Text(
item.sign,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
dense: true,
trailing: const SizedBox(width: 6), trailing: const SizedBox(width: 6),
); );
} }

View File

@ -8,11 +8,13 @@ import 'package:pilipala/utils/storage.dart';
class HistoryController extends GetxController { class HistoryController extends GetxController {
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
RxList<HisListItem> historyList = [HisListItem()].obs; RxList<HisListItem> historyList = <HisListItem>[].obs;
RxBool isLoadingMore = false.obs; RxBool isLoadingMore = false.obs;
RxBool pauseStatus = false.obs; RxBool pauseStatus = false.obs;
Box localCache = GStrorage.localCache; Box localCache = GStrorage.localCache;
RxBool isLoading = false.obs; RxBool isLoading = false.obs;
RxBool enableMultiple = false.obs;
RxInt checkedCount = 0.obs;
@override @override
void onInit() { void onInit() {
@ -121,4 +123,80 @@ class HistoryController extends GetxController {
}, },
); );
} }
// 删除某条历史记录
Future delHistory(kid, business) async {
String resKid = 'archive_$kid';
if (business == 'live') {
resKid = 'live_$kid';
} else if (business.contains('article')) {
resKid = 'article_$kid';
}
var res = await UserHttp.delHistory(resKid);
if (res['status']) {
historyList.removeWhere((e) => e.kid == kid);
SmartDialog.showToast(res['msg']);
}
}
// 删除已看历史记录
Future onDelHistory() async {
/// TODO 优化
List<HisListItem> result =
historyList.where((e) => e.progress == -1).toList();
for (HisListItem i in result) {
String resKid = 'archive_${i.kid}';
await UserHttp.delHistory(resKid);
historyList.removeWhere((e) => e.kid == i.kid);
}
SmartDialog.showToast('操作完成');
}
// 删除选中的记录
Future onDelCheckedHistory() async {
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 {
/// TODO 优化
await SmartDialog.dismiss();
SmartDialog.showLoading(msg: '请求中');
List<HisListItem> result =
historyList.where((e) => e.checked!).toList();
for (HisListItem i in result) {
String str = 'archive';
try {
str = i.history!.business!;
} catch (_) {}
String resKid = '${str}_${i.kid}';
await UserHttp.delHistory(resKid);
historyList.removeWhere((e) => e.kid == i.kid);
}
checkedCount.value = 0;
SmartDialog.dismiss();
enableMultiple.value = false;
},
child: const Text('确认'),
)
],
);
},
);
}
} }

View File

@ -37,6 +37,23 @@ class _HistoryPageState extends State<HistoryPage> {
} }
}, },
); );
_historyController.enableMultiple.listen((p0) {
setState(() {});
});
}
// 选中
onChoose(index) {
_historyController.historyList[index].checked =
!_historyController.historyList[index].checked!;
_historyController.checkedCount.value =
_historyController.historyList.where((item) => item.checked!).length;
_historyController.historyList.refresh();
}
// 更新多选状态
onUpdateMultiple() {
setState(() {});
} }
@override @override
@ -48,44 +65,119 @@ class _HistoryPageState extends State<HistoryPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBarWidget(
titleSpacing: 0, visible: _historyController.enableMultiple.value,
centerTitle: false, child1: AppBar(
title: Text( titleSpacing: 0,
'观看记录', centerTitle: false,
style: Theme.of(context).textTheme.titleMedium, leading: IconButton(
), onPressed: () => Get.back(),
actions: [ icon: const Icon(Icons.arrow_back_outlined),
PopupMenuButton<String>(
onSelected: (String type) {
// 处理菜单项选择的逻辑
switch (type) {
case 'pause':
_historyController.onPauseHistory();
break;
case 'clear':
_historyController.onClearHistory();
break;
default:
}
},
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
PopupMenuItem<String>(
value: 'pause',
child: Obx(
() => Text(!_historyController.pauseStatus.value
? '暂停观看记录'
: '恢复观看记录'),
),
),
const PopupMenuItem<String>(
value: 'clear',
child: Text('清空观看记录'),
),
],
), ),
const SizedBox(width: 6), title: Text(
], '观看记录',
style: Theme.of(context).textTheme.titleMedium,
),
actions: [
// TextButton(
// onPressed: () {
// _historyController.enableMultiple.value = true;
// setState(() {});
// },
// child: const Text('多选'),
// ),
IconButton(
onPressed: () => Get.toNamed('/historySearch'),
icon: const Icon(Icons.search_outlined),
),
PopupMenuButton<String>(
onSelected: (String type) {
// 处理菜单项选择的逻辑
switch (type) {
case 'pause':
_historyController.onPauseHistory();
break;
case 'clear':
_historyController.onClearHistory();
break;
case 'del':
_historyController.onDelHistory();
break;
case 'multiple':
_historyController.enableMultiple.value = true;
setState(() {});
break;
default:
}
},
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
PopupMenuItem<String>(
value: 'pause',
child: Obx(
() => Text(!_historyController.pauseStatus.value
? '暂停观看记录'
: '恢复观看记录'),
),
),
const PopupMenuItem<String>(
value: 'clear',
child: Text('清空观看记录'),
),
const PopupMenuItem<String>(
value: 'del',
child: Text('删除已看记录'),
),
const PopupMenuItem<String>(
value: 'multiple',
child: Text('多选删除'),
),
],
),
const SizedBox(width: 6),
],
),
child2: AppBar(
titleSpacing: 0,
centerTitle: false,
leading: IconButton(
onPressed: () {
_historyController.enableMultiple.value = false;
for (var item in _historyController.historyList) {
item.checked = false;
}
_historyController.checkedCount.value = 0;
setState(() {});
},
icon: const Icon(Icons.close_outlined),
),
title: Obx(
() => Text(
'已选择${_historyController.checkedCount.value}',
style: Theme.of(context).textTheme.titleMedium,
),
),
actions: [
TextButton(
onPressed: () {
for (var item in _historyController.historyList) {
item.checked = true;
}
_historyController.checkedCount.value =
_historyController.historyList.length;
_historyController.historyList.refresh();
},
child: const Text('全选'),
),
TextButton(
onPressed: () => _historyController.onDelCheckedHistory(),
child: Text(
'删除',
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
),
const SizedBox(width: 6),
],
),
), ),
body: RefreshIndicator( body: RefreshIndicator(
onRefresh: () async { onRefresh: () async {
@ -112,6 +204,9 @@ class _HistoryPageState extends State<HistoryPage> {
return HistoryItem( return HistoryItem(
videoItem: videoItem:
_historyController.historyList[index], _historyController.historyList[index],
ctr: _historyController,
onChoose: () => onChoose(index),
onUpdateMultiple: () => onUpdateMultiple(),
); );
}, },
childCount: childCount:
@ -147,6 +242,36 @@ class _HistoryPageState extends State<HistoryPage> {
], ],
), ),
), ),
// bottomNavigationBar: BottomAppBar(),
);
}
}
class AppBarWidget extends StatelessWidget implements PreferredSizeWidget {
const AppBarWidget({
required this.child1,
required this.child2,
required this.visible,
Key? key,
}) : super(key: key);
final PreferredSizeWidget child1;
final PreferredSizeWidget child2;
final bool visible;
@override
Size get preferredSize => child1.preferredSize;
@override
Widget build(BuildContext context) {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
transitionBuilder: (Widget child, Animation<double> animation) {
return ScaleTransition(
scale: animation,
child: child,
);
},
child: !visible ? child1 : child2,
); );
} }
} }

View File

@ -11,12 +11,24 @@ 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/index.dart';
import 'package:pilipala/pages/history_search/index.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/utils.dart'; import 'package:pilipala/utils/utils.dart';
class HistoryItem extends StatelessWidget { class HistoryItem extends StatelessWidget {
final dynamic videoItem; final dynamic videoItem;
const HistoryItem({super.key, required this.videoItem}); final dynamic ctr;
final Function? onChoose;
final Function? onUpdateMultiple;
const HistoryItem({
super.key,
required this.videoItem,
this.ctr,
this.onChoose,
this.onUpdateMultiple,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -25,6 +37,11 @@ class HistoryItem extends StatelessWidget {
String heroTag = Utils.makeHeroTag(aid); String heroTag = Utils.makeHeroTag(aid);
return InkWell( return InkWell(
onTap: () async { onTap: () async {
if (ctr!.enableMultiple.value) {
feedBack();
onChoose!();
return;
}
if (videoItem.history.business.contains('article')) { if (videoItem.history.business.contains('article')) {
int cid = videoItem.history.cid ?? int cid = videoItem.history.cid ??
// videoItem.history.oid ?? // videoItem.history.oid ??
@ -72,7 +89,7 @@ class HistoryItem extends StatelessWidget {
arguments: { arguments: {
'pic': pic, 'pic': pic,
'heroTag': heroTag, 'heroTag': heroTag,
'videoType': SearchType.media_bangumi, // 'videoType': SearchType.media_bangumi,
}, },
); );
} else { } else {
@ -100,7 +117,7 @@ class HistoryItem extends StatelessWidget {
arguments: { arguments: {
'pic': pic, 'pic': pic,
'heroTag': heroTag, 'heroTag': heroTag,
'videoType': SearchType.media_bangumi, // 'videoType': SearchType.media_bangumi,
'bangumiItem': res['data'], 'bangumiItem': res['data'],
}, },
); );
@ -115,6 +132,17 @@ class HistoryItem extends StatelessWidget {
arguments: {'heroTag': heroTag, 'pic': videoItem.cover}); arguments: {'heroTag': heroTag, 'pic': videoItem.cover});
} }
}, },
onLongPress: () {
if (ctr is HistorySearchController) {
return;
}
if (!ctr!.enableMultiple.value) {
feedBack();
ctr!.enableMultiple.value = true;
onChoose!();
onUpdateMultiple!();
}
},
child: Column( child: Column(
children: [ children: [
Padding( Padding(
@ -130,53 +158,110 @@ class HistoryItem extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
AspectRatio( Stack(
aspectRatio: StyleString.aspectRatio, children: [
child: LayoutBuilder( AspectRatio(
builder: (context, boxConstraints) { aspectRatio: StyleString.aspectRatio,
double maxWidth = boxConstraints.maxWidth; child: LayoutBuilder(
double maxHeight = boxConstraints.maxHeight; builder: (context, boxConstraints) {
return Stack( double maxWidth = boxConstraints.maxWidth;
children: [ double maxHeight = boxConstraints.maxHeight;
Hero( return Stack(
tag: heroTag, children: [
child: NetworkImgLayer( Hero(
src: (videoItem.cover != '' tag: heroTag,
? videoItem.cover child: NetworkImgLayer(
: videoItem.covers.first), src: (videoItem.cover != ''
width: maxWidth, ? videoItem.cover
height: maxHeight, : videoItem.covers.first),
width: maxWidth,
height: maxHeight,
),
),
if (!BusinessType
.hiddenDurationType.hiddenDurationType
.contains(videoItem.history.business))
PBadge(
text: videoItem.progress == -1
? '已看完'
: '${Utils.timeFormat(videoItem.progress!)}/${Utils.timeFormat(videoItem.duration!)}',
right: 6.0,
bottom: 6.0,
type: 'gray',
),
// 右上角
if (BusinessType.showBadge.showBadge
.contains(
videoItem.history.business) ||
videoItem.history.business ==
BusinessType.live.type)
PBadge(
text: videoItem.badge,
top: 6.0,
right: 6.0,
bottom: null,
left: null,
),
],
);
},
),
),
Obx(
() => Positioned.fill(
child: AnimatedOpacity(
opacity: ctr!.enableMultiple.value ? 1 : 0,
duration: const Duration(milliseconds: 200),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.black.withOpacity(
ctr!.enableMultiple.value &&
videoItem.checked
? 0.6
: 0),
),
child: Center(
child: SizedBox(
width: 34,
height: 34,
child: AnimatedScale(
scale: videoItem.checked ? 1 : 0,
duration:
const Duration(milliseconds: 250),
curve: Curves.easeInOut,
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(
EdgeInsets.zero),
backgroundColor:
MaterialStateProperty
.resolveWith(
(states) {
return Colors.white
.withOpacity(0.8);
},
),
),
onPressed: () {
feedBack();
onChoose!();
},
icon: Icon(Icons.done_all_outlined,
color: Theme.of(context)
.colorScheme
.primary),
),
),
),
), ),
), ),
if (!BusinessType ),
.hiddenDurationType.hiddenDurationType ),
.contains(videoItem.history.business)) ),
PBadge( ],
text: videoItem.progress == -1
? '已看完'
: '${Utils.timeFormat(videoItem.progress!)}/${Utils.timeFormat(videoItem.duration!)}',
right: 6.0,
bottom: 6.0,
type: 'gray',
),
// 右上角
if (BusinessType.showBadge.showBadge
.contains(videoItem.history.business) ||
videoItem.history.business ==
BusinessType.live.type)
PBadge(
text: videoItem.badge,
top: 6.0,
right: 6.0,
bottom: null,
left: null,
),
],
);
},
),
), ),
VideoContent(videoItem: videoItem) VideoContent(videoItem: videoItem, ctr: ctr)
], ],
), ),
); );
@ -191,7 +276,8 @@ class HistoryItem extends StatelessWidget {
class VideoContent extends StatelessWidget { class VideoContent extends StatelessWidget {
final dynamic videoItem; final dynamic videoItem;
const VideoContent({super.key, required this.videoItem}); final dynamic? ctr;
const VideoContent({super.key, required this.videoItem, this.ctr});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -244,26 +330,26 @@ class VideoContent extends StatelessWidget {
Theme.of(context).textTheme.labelMedium!.fontSize, Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline), color: Theme.of(context).colorScheme.outline),
), ),
if (videoItem.badge != '番剧' && SizedBox(
!videoItem.tagName.contains('动画') && width: 24,
videoItem.history.business != 'live' && height: 24,
!videoItem.history.business.contains('article')) child: PopupMenuButton<String>(
SizedBox( padding: EdgeInsets.zero,
width: 24, tooltip: '功能菜单',
height: 24, icon: Icon(
child: PopupMenuButton<String>( Icons.more_vert_outlined,
padding: EdgeInsets.zero, color: Theme.of(context).colorScheme.outline,
tooltip: '稍后再看', size: 14,
icon: Icon( ),
Icons.more_vert_outlined, position: PopupMenuPosition.under,
color: Theme.of(context).colorScheme.outline, // constraints: const BoxConstraints(maxHeight: 35),
size: 14, onSelected: (String type) {},
), itemBuilder: (BuildContext context) =>
position: PopupMenuPosition.under, <PopupMenuEntry<String>>[
// constraints: const BoxConstraints(maxHeight: 35), if (videoItem.badge != '番剧' &&
onSelected: (String type) {}, !videoItem.tagName.contains('动画') &&
itemBuilder: (BuildContext context) => videoItem.history.business != 'live' &&
<PopupMenuEntry<String>>[ !videoItem.history.business.contains('article'))
PopupMenuItem<String>( PopupMenuItem<String>(
onTap: () async { onTap: () async {
var res = await UserHttp.toViewLater( var res = await UserHttp.toViewLater(
@ -280,9 +366,22 @@ class VideoContent extends StatelessWidget {
], ],
), ),
), ),
], PopupMenuItem<String>(
), onTap: () => ctr!.delHistory(
videoItem.kid, videoItem.history.business),
value: 'pause',
height: 35,
child: const Row(
children: [
Icon(Icons.close_outlined, size: 16),
SizedBox(width: 6),
Text('删除记录', style: TextStyle(fontSize: 13))
],
),
),
],
), ),
),
], ],
), ),
], ],

View File

@ -0,0 +1,91 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/models/user/history.dart';
class HistorySearchController extends GetxController {
final ScrollController scrollController = ScrollController();
Rx<TextEditingController> controller = TextEditingController().obs;
final FocusNode searchFocusNode = FocusNode();
RxString searchKeyWord = ''.obs;
String hintText = '搜索';
RxString loadingStatus = 'init'.obs;
RxString loadingText = '加载中...'.obs;
bool hasRequest = false;
late int mid;
RxString uname = ''.obs;
int pn = 1;
int count = 0;
RxList<HisListItem> historyList = <HisListItem>[].obs;
RxBool enableMultiple = false.obs;
// 清空搜索
void onClear() {
if (searchKeyWord.value.isNotEmpty && controller.value.text != '') {
controller.value.clear();
searchKeyWord.value = '';
} else {
Get.back();
}
}
void onChange(value) {
searchKeyWord.value = value;
}
// 提交搜索内容
void submit() {
loadingStatus.value = 'loading';
if (hasRequest) {
pn = 1;
searchHistories();
}
}
// 搜索视频
Future searchHistories({type = 'init'}) async {
if (type == 'onLoad' && loadingText.value == '没有更多了') {
return;
}
var res = await UserHttp.searchHistory(
pn: pn,
keyword: controller.value.text,
);
if (res['status']) {
if (type == 'init' && pn == 1) {
historyList.value = res['data'].list;
} else {
historyList.addAll(res['data'].list);
}
count = res['data'].page['total'];
if (historyList.length == count) {
loadingText.value = '没有更多了';
}
pn += 1;
hasRequest = true;
}
loadingStatus.value = 'finish';
return res;
}
onLoad() {
searchHistories(type: 'onLoad');
}
Future delHistory(kid, business) async {
String resKid = 'archive_$kid';
if (business == 'live') {
resKid = 'live_$kid';
} else if (business.contains('article')) {
resKid = 'article_$kid';
}
var res = await UserHttp.delHistory(resKid);
if (res['status']) {
historyList.removeWhere((e) => e.kid == kid);
SmartDialog.showToast(res['msg']);
}
loadingStatus.value = 'finish';
}
}

View File

@ -0,0 +1,4 @@
library history_search;
export './controller.dart';
export './view.dart';

View File

@ -0,0 +1,174 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/skeleton/video_card_h.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/no_data.dart';
import 'package:pilipala/pages/history/widgets/item.dart';
import 'controller.dart';
class HistorySearchPage extends StatefulWidget {
const HistorySearchPage({super.key});
@override
State<HistorySearchPage> createState() => _HistorySearchPageState();
}
class _HistorySearchPageState extends State<HistorySearchPage> {
final HistorySearchController _historySearchCtr =
Get.put(HistorySearchController());
late ScrollController scrollController;
@override
void initState() {
super.initState();
scrollController = _historySearchCtr.scrollController;
scrollController.addListener(
() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 300) {
EasyThrottle.throttle('history', const Duration(seconds: 1), () {
_historySearchCtr.onLoad();
});
}
},
);
}
@override
void dispose() {
scrollController.removeListener(() {});
scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
titleSpacing: 0,
actions: [
IconButton(
onPressed: () => _historySearchCtr.submit(),
icon: const Icon(Icons.search_outlined, size: 22)),
const SizedBox(width: 10)
],
title: Obx(
() => TextField(
autofocus: true,
focusNode: _historySearchCtr.searchFocusNode,
controller: _historySearchCtr.controller.value,
textInputAction: TextInputAction.search,
onChanged: (value) => _historySearchCtr.onChange(value),
decoration: InputDecoration(
hintText: _historySearchCtr.hintText,
border: InputBorder.none,
suffixIcon: IconButton(
icon: Icon(
Icons.clear,
size: 22,
color: Theme.of(context).colorScheme.outline,
),
onPressed: () => _historySearchCtr.onClear(),
),
),
onSubmitted: (String value) => _historySearchCtr.submit(),
),
),
),
body: Obx(
() => Column(
children: _historySearchCtr.loadingStatus.value == 'init'
? [const SizedBox()]
: [
Expanded(
child: FutureBuilder(
future: _historySearchCtr.searchHistories(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
return Obx(
() => _historySearchCtr.historyList.isNotEmpty
? ListView.builder(
controller: scrollController,
itemCount:
_historySearchCtr.historyList.length +
1,
itemBuilder: (context, index) {
if (index ==
_historySearchCtr
.historyList.length) {
return Container(
height: MediaQuery.of(context)
.padding
.bottom +
60,
padding: EdgeInsets.only(
bottom: MediaQuery.of(context)
.padding
.bottom),
child: Center(
child: Obx(
() => Text(
_historySearchCtr
.loadingText.value,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.outline,
fontSize: 13),
),
),
),
);
} else {
return HistoryItem(
videoItem: _historySearchCtr
.historyList[index],
ctr: _historySearchCtr,
onChoose: null,
onUpdateMultiple: () => null,
);
;
}
},
)
: _historySearchCtr.loadingStatus.value ==
'loading'
? const SizedBox(child: Text('加载中...'))
: const CustomScrollView(
slivers: <Widget>[
NoData(),
],
),
);
} else {
return CustomScrollView(
slivers: <Widget>[
HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
)
],
);
}
} else {
// 骨架屏
return ListView.builder(
itemCount: 10,
itemBuilder: (context, index) {
return const VideoCardHSkeleton();
},
);
}
},
),
),
],
),
),
);
}
}

View File

@ -1,7 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/http/black.dart';
import 'package:pilipala/models/common/tab_type.dart'; import 'package:pilipala/models/common/tab_type.dart';
import 'package:pilipala/models/user/black.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
class HomeController extends GetxController with GetTickerProviderStateMixin { class HomeController extends GetxController with GetTickerProviderStateMixin {
@ -15,6 +17,13 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
RxBool userLogin = false.obs; RxBool userLogin = false.obs;
RxString userFace = ''.obs; RxString userFace = ''.obs;
var userInfo; var userInfo;
static Box setting = GStrorage.setting;
late List<int> blackMidsList;
int currentPage = 1;
int pageSize = 50;
int total = 0;
List<BlackListItem> blackList = [BlackListItem()];
@override @override
void onInit() { void onInit() {
@ -51,7 +60,33 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
void updateLoginStatus(val) async { void updateLoginStatus(val) async {
userInfo = await userInfoCache.get('userInfoCache'); userInfo = await userInfoCache.get('userInfoCache');
userLogin.value = val ?? false; userLogin.value = val ?? false;
if (val) return; if (val) {
// 获取黑名单
await queryBlacklist();
blackMidsList = blackList.map<int>((e) => e.mid!).toList();
setting.put(SettingBoxKey.blackMidsList, blackMidsList);
return;
}
userFace.value = userInfo != null ? userInfo.face : ''; userFace.value = userInfo != null ? userInfo.face : '';
} }
Future queryBlacklist({type = 'init'}) async {
if (type == 'init') {
currentPage = 1;
}
var result = await BlackHttp.blackList(pn: currentPage, ps: pageSize);
if (result['status']) {
if (type == 'init') {
blackList = result['data'].list;
total = result['data'].total;
} else {
blackList.addAll(result['data'].list);
}
currentPage += 1;
if (blackList.length < total) {
await queryBlacklist(type: 'onLoad');
}
}
return result;
}
} }

View File

@ -8,7 +8,8 @@ import 'package:pilipala/utils/feed_back.dart';
import './controller.dart'; import './controller.dart';
class HomePage extends StatefulWidget { class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key); Function? callFn;
HomePage({Key? key, this.callFn}) : super(key: key);
@override @override
State<HomePage> createState() => _HomePageState(); State<HomePage> createState() => _HomePageState();
@ -25,15 +26,16 @@ class _HomePageState extends State<HomePage>
showUserBottonSheet() { showUserBottonSheet() {
feedBack(); feedBack();
showModalBottomSheet( widget.callFn!();
context: context, // showModalBottomSheet(
builder: (_) => const SizedBox( // context: context,
height: 450, // builder: (_) => const SizedBox(
child: MinePage(), // height: 450,
), // child: MinePage(),
clipBehavior: Clip.hardEdge, // ),
isScrollControlled: true, // clipBehavior: Clip.hardEdge,
); // isScrollControlled: true,
// );
} }
@override @override
@ -50,37 +52,6 @@ class _HomePageState extends State<HomePage>
ctr: _homeController, ctr: _homeController,
callback: showUserBottonSheet, callback: showUserBottonSheet,
), ),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
height: 42,
child: Align(
alignment: Alignment.center,
child: TabBar(
controller: _homeController.tabController,
tabs: [
for (var i in _homeController.tabs) Tab(text: i['label'])
],
isScrollable: true,
dividerColor: Colors.transparent,
enableFeedback: true,
splashBorderRadius: BorderRadius.circular(10),
onTap: (value) {
feedBack();
if (_homeController.initialIndex == value) {
_homeController.tabsCtrList[value]().animateToTop();
}
_homeController.initialIndex = value;
},
),
),
),
Expanded(
child: TabBarView(
controller: _homeController.tabController,
children: _homeController.tabsPageList,
),
),
], ],
), ),
); );
@ -128,8 +99,6 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
), ),
child: Row( child: Row(
children: [ children: [
const Expanded(child: SearchPage()),
const SizedBox(width: 10),
Obx( Obx(
() => ctr!.userLogin.value () => ctr!.userLogin.value
? Stack( ? Stack(
@ -182,6 +151,8 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
), ),
), ),
), ),
const SizedBox(width: 10),
const Expanded(child: SearchPage()),
], ],
), ),
), ),

View File

@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
import 'package:pilipala/pages/media/index.dart';
import 'package:pilipala/pages/mine/index.dart';
class LeftDrawer extends StatefulWidget {
const LeftDrawer({super.key});
@override
State<LeftDrawer> createState() => _LeftDrawerState();
}
class _LeftDrawerState extends State<LeftDrawer> {
@override
Widget build(BuildContext context) {
return Drawer(
width: MediaQuery.of(context).size.width * 0.84,
child: const Column(
children: [
Expanded(child: MinePage()),
Expanded(child: MediaPage()),
],
),
);
}
}

View File

@ -4,53 +4,11 @@ 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:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/pages/dynamics/index.dart'; // import 'package:pilipala/pages/dynamics/index.dart';
import 'package:pilipala/pages/home/view.dart';
import 'package:pilipala/pages/media/index.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';
class MainController extends GetxController { class MainController extends GetxController {
List<Widget> pages = <Widget>[
const HomePage(),
const DynamicsPage(),
const MediaPage(),
];
RxList navigationBars = [
{
'icon': const Icon(
Icons.favorite_outline,
size: 21,
),
'selectIcon': const Icon(
Icons.favorite,
size: 21,
),
'label': "首页",
},
{
'icon': const Icon(
Icons.motion_photos_on_outlined,
size: 21,
),
'selectIcon': const Icon(
Icons.motion_photos_on,
size: 21,
),
'label': "动态",
},
{
'icon': const Icon(
Icons.folder_outlined,
size: 20,
),
'selectIcon': const Icon(
Icons.folder,
size: 21,
),
'label': "媒体库",
}
].obs;
final StreamController<bool> bottomBarStream = final StreamController<bool> bottomBarStream =
StreamController<bool>.broadcast(); StreamController<bool>.broadcast();
Box setting = GStrorage.setting; Box setting = GStrorage.setting;

View File

@ -3,6 +3,7 @@ import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/pages/dynamics/index.dart'; import 'package:pilipala/pages/dynamics/index.dart';
import 'package:pilipala/pages/home/index.dart'; import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/home/widgets/left_drawer.dart';
import 'package:pilipala/pages/media/index.dart'; import 'package:pilipala/pages/media/index.dart';
import 'package:pilipala/utils/event_bus.dart'; import 'package:pilipala/utils/event_bus.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
@ -18,80 +19,16 @@ class MainApp extends StatefulWidget {
class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin { class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
final MainController _mainController = Get.put(MainController()); final MainController _mainController = Get.put(MainController());
final HomeController _homeController = Get.put(HomeController()); final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
final DynamicsController _dynamicController = Get.put(DynamicsController());
final MediaController _mediaController = Get.put(MediaController());
PageController? _pageController;
late AnimationController? _animationController;
late Animation<double>? _fadeAnimation;
late Animation<double>? _slideAnimation;
int selectedIndex = 0; int selectedIndex = 0;
int? _lastSelectTime; //上次点击时间
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 800),
reverseDuration: const Duration(milliseconds: 0),
value: 1,
vsync: this,
);
_fadeAnimation =
Tween<double>(begin: 0.8, end: 1.0).animate(_animationController!);
_slideAnimation =
Tween(begin: 0.8, end: 1.0).animate(_animationController!);
_lastSelectTime = DateTime.now().millisecondsSinceEpoch;
_pageController = PageController(initialPage: selectedIndex);
} }
void setIndex(int value) async { void openDrawer() {
feedBack(); _scaffoldKey.currentState?.openDrawer();
if (selectedIndex != value) {
selectedIndex = value;
_animationController!.reverse().then((_) {
selectedIndex = value;
_animationController!.forward();
});
setState(() {});
}
_pageController!.jumpToPage(value);
var currentPage = _mainController.pages[value];
if (currentPage is HomePage) {
if (_homeController.flag) {
// 单击返回顶部 双击并刷新
if (DateTime.now().millisecondsSinceEpoch - _lastSelectTime! < 500) {
_homeController.onRefresh();
} else {
_homeController.animateToTop();
}
_lastSelectTime = DateTime.now().millisecondsSinceEpoch;
}
_homeController.flag = true;
} else {
_homeController.flag = false;
}
if (currentPage is DynamicsPage) {
if (_dynamicController.flag) {
// 单击返回顶部 双击并刷新
if (DateTime.now().millisecondsSinceEpoch - _lastSelectTime! < 500) {
_dynamicController.onRefresh();
} else {
_dynamicController.animateToTop();
}
_lastSelectTime = DateTime.now().millisecondsSinceEpoch;
}
_dynamicController.flag = true;
} else {
_dynamicController.flag = false;
}
if (currentPage is MediaPage) {
_mediaController.queryFavFolder();
}
} }
@override @override
@ -113,55 +50,10 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
return WillPopScope( return WillPopScope(
onWillPop: () => _mainController.onBackPressed(context), onWillPop: () => _mainController.onBackPressed(context),
child: Scaffold( child: Scaffold(
key: _scaffoldKey,
extendBody: true, extendBody: true,
body: FadeTransition( drawer: const LeftDrawer(),
opacity: _fadeAnimation!, body: HomePage(callFn: openDrawer),
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 0.5),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: _slideAnimation!,
curve: Curves.fastOutSlowIn,
reverseCurve: Curves.linear,
),
),
child: PageView(
physics: const NeverScrollableScrollPhysics(),
controller: _pageController,
onPageChanged: (index) {
selectedIndex = index;
setState(() {});
},
children: _mainController.pages,
),
),
),
bottomNavigationBar: StreamBuilder(
stream: _mainController.bottomBarStream.stream,
initialData: true,
builder: (context, AsyncSnapshot snapshot) {
return AnimatedSlide(
curve: Curves.easeInOutCubicEmphasized,
duration: const Duration(milliseconds: 1000),
offset: Offset(0, snapshot.data ? 0 : 1),
child: NavigationBar(
onDestinationSelected: (value) => setIndex(value),
selectedIndex: selectedIndex,
destinations: <Widget>[
..._mainController.navigationBars.map((e) {
return NavigationDestination(
icon: e['icon'],
selectedIcon: e['selectIcon'],
label: e['label'],
);
}).toList(),
],
),
);
},
),
), ),
); );
} }

View File

@ -1,9 +1,6 @@
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/network_img_layer.dart';
import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/pages/media/index.dart'; import 'package:pilipala/pages/media/index.dart';
import 'package:pilipala/utils/utils.dart';
class MediaPage extends StatefulWidget { class MediaPage extends StatefulWidget {
const MediaPage({super.key}); const MediaPage({super.key});
@ -15,7 +12,6 @@ class MediaPage extends StatefulWidget {
class _MediaPageState extends State<MediaPage> class _MediaPageState extends State<MediaPage>
with AutomaticKeepAliveClientMixin { with AutomaticKeepAliveClientMixin {
late MediaController mediaController; late MediaController mediaController;
late Future _futureBuilderFuture;
@override @override
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
@ -24,13 +20,6 @@ class _MediaPageState extends State<MediaPage>
void initState() { void initState() {
super.initState(); super.initState();
mediaController = Get.put(MediaController()); mediaController = Get.put(MediaController());
_futureBuilderFuture = mediaController.queryFavFolder();
mediaController.userLogin.listen((status) {
setState(() {
_futureBuilderFuture = mediaController.queryFavFolder();
});
});
} }
@override @override
@ -38,22 +27,8 @@ class _MediaPageState extends State<MediaPage>
super.build(context); super.build(context);
Color primary = Theme.of(context).colorScheme.primary; Color primary = Theme.of(context).colorScheme.primary;
return Scaffold( return Scaffold(
appBar: AppBar(toolbarHeight: 30),
body: Column( body: Column(
children: [ children: [
ListTile(
leading: null,
title: Padding(
padding: const EdgeInsets.only(left: 20),
child: Text(
'媒体库',
style: TextStyle(
fontSize: Theme.of(context).textTheme.titleLarge!.fontSize,
fontWeight: FontWeight.bold,
),
),
),
),
for (var i in mediaController.list) ...[ for (var i in mediaController.list) ...[
ListTile( ListTile(
onTap: () => i['onTap'](), onTap: () => i['onTap'](),
@ -74,206 +49,8 @@ class _MediaPageState extends State<MediaPage>
), ),
), ),
], ],
Obx(() => mediaController.userLogin.value
? favFolder(mediaController, context)
: const SizedBox())
], ],
), ),
); );
} }
Widget favFolder(mediaController, context) {
return Column(
children: [
Divider(
height: 35,
color: Theme.of(context).dividerColor.withOpacity(0.1),
),
ListTile(
onTap: () {},
leading: null,
dense: true,
title: Padding(
padding: const EdgeInsets.only(left: 10),
child: Obx(
() => Text.rich(
TextSpan(
children: [
TextSpan(
text: '收藏夹 ',
style: TextStyle(
fontSize:
Theme.of(context).textTheme.titleMedium!.fontSize,
fontWeight: FontWeight.bold),
),
if (mediaController.favFolderData.value.count != null)
TextSpan(
text: mediaController.favFolderData.value.count
.toString(),
style: TextStyle(
fontSize:
Theme.of(context).textTheme.titleSmall!.fontSize,
color: Theme.of(context).colorScheme.primary,
),
),
],
),
),
),
),
trailing: IconButton(
onPressed: () {
setState(() {
_futureBuilderFuture = mediaController.queryFavFolder();
});
},
icon: const Icon(
Icons.refresh,
size: 20,
),
),
),
// const SizedBox(height: 10),
SizedBox(
width: double.infinity,
height: 170 * MediaQuery.of(context).textScaleFactor,
child: FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == null) {
return const SizedBox();
}
Map data = snapshot.data as Map;
if (data['status']) {
List favFolderList =
mediaController.favFolderData.value.list!;
int favFolderCount =
mediaController.favFolderData.value.count!;
bool flag = favFolderCount > favFolderList.length;
return Obx(() => ListView.builder(
itemCount:
mediaController.favFolderData.value.list!.length +
(flag ? 1 : 0),
itemBuilder: (context, index) {
if (flag && index == favFolderList.length) {
return Padding(
padding: const EdgeInsets.only(
right: 14, bottom: 35),
child: Center(
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(
EdgeInsets.zero),
backgroundColor:
MaterialStateProperty.resolveWith(
(states) {
return Theme.of(context)
.colorScheme
.primaryContainer
.withOpacity(0.5);
}),
),
onPressed: () => Get.toNamed('/fav'),
icon: Icon(
Icons.arrow_forward_ios,
size: 18,
color: Theme.of(context)
.colorScheme
.primary,
),
),
));
} else {
return FavFolderItem(
item: mediaController
.favFolderData.value.list![index],
index: index);
}
},
scrollDirection: Axis.horizontal,
));
} else {
return SizedBox(
height: 160,
child: Center(child: Text(data['msg'])),
);
}
} else {
// 骨架屏
return const SizedBox();
}
}),
),
],
);
}
}
class FavFolderItem extends StatelessWidget {
const FavFolderItem({super.key, this.item, this.index});
final FavFolderItemData? item;
final int? index;
@override
Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(item!.fid);
return Container(
margin: EdgeInsets.only(left: index == 0 ? 20 : 0, right: 14),
child: GestureDetector(
onTap: () => Get.toNamed('/favDetail',
arguments: item,
parameters: {'mediaId': item!.id.toString(), 'heroTag': heroTag}),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 12),
Container(
width: 180,
height: 110,
margin: const EdgeInsets.only(bottom: 8),
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Theme.of(context).colorScheme.onInverseSurface,
boxShadow: [
BoxShadow(
color: Theme.of(context).colorScheme.onInverseSurface,
offset: const Offset(4, -12), // 阴影与容器的距离
blurRadius: 0.0, // 高斯的标准偏差与盒子的形状卷积。
spreadRadius: 0.0, // 在应用模糊之前,框应该膨胀的量。
),
],
),
child: LayoutBuilder(
builder: (context, BoxConstraints box) {
return Hero(
tag: heroTag,
child: NetworkImgLayer(
src: item!.cover,
width: box.maxWidth,
height: box.maxHeight,
),
);
},
),
),
Text(
' ${item!.title}',
overflow: TextOverflow.fade,
maxLines: 1,
),
Text(
'${item!.mediaCount}条视频',
style: Theme.of(context)
.textTheme
.labelSmall!
.copyWith(color: Theme.of(context).colorScheme.outline),
)
],
),
),
);
}
} }

View File

@ -19,6 +19,7 @@ class MemberController extends GetxController {
// 投稿列表 // 投稿列表
RxList<VListItemModel>? archiveList = [VListItemModel()].obs; RxList<VListItemModel>? archiveList = [VListItemModel()].obs;
var userInfo; var userInfo;
Box setting = GStrorage.setting;
@override @override
void onInit() { void onInit() {
@ -70,11 +71,11 @@ class MemberController extends GetxController {
builder: (BuildContext context) { builder: (BuildContext context) {
return AlertDialog( return AlertDialog(
title: const Text('提示'), title: const Text('提示'),
content: Text(memberInfo.value.isFollowed! ? '取消关注UP主?' : '关注UP主?'), content: Text(memberInfo.value.isFollowed! ? '取消关注该用户?' : '关注该用户?'),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => SmartDialog.dismiss(), onPressed: () => SmartDialog.dismiss(),
child: const Text('点错了')), child: const Text('取消')),
TextButton( TextButton(
onPressed: () async { onPressed: () async {
await VideoHttp.relationMod( await VideoHttp.relationMod(
@ -95,4 +96,48 @@ class MemberController extends GetxController {
}, },
); );
} }
// 拉黑用户
Future blockUser(int mid) async {
if (userInfoCache.get('userInfoCache') == null) {
SmartDialog.showToast('账号未登录');
return;
}
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 {
var res = await VideoHttp.relationMod(
mid: mid,
act: 5,
reSrc: 11,
);
SmartDialog.dismiss();
if (res['status']) {
List<int> blackMidsList = setting
.get(SettingBoxKey.blackMidsList, defaultValue: [-1]);
blackMidsList.add(mid);
setting.put(SettingBoxKey.blackMidsList, blackMidsList);
}
},
child: const Text('确认'),
)
],
);
},
);
}
} }

View File

@ -102,40 +102,79 @@ class _MemberPageState extends State<MemberPage>
}, },
), ),
actions: [ actions: [
IconButton(onPressed: () {}, icon: const Icon(Icons.more_vert)), IconButton(
onPressed: () => Get.toNamed(
'/memberSearch?mid=${Get.parameters['mid']}&uname=${_memberController.memberInfo.value.name!}'),
icon: const Icon(Icons.search_outlined),
),
// PopupMenuButton(
// icon: const Icon(Icons.more_vert),
// itemBuilder: (BuildContext context) => <PopupMenuEntry>[
// if (_memberController.ownerMid !=
// _memberController.mid) ...[
// PopupMenuItem(
// onTap: () => _memberController.blockUser(),
// child: Row(
// mainAxisSize: MainAxisSize.min,
// children: [
// const Icon(Icons.block, size: 19),
// const SizedBox(width: 10),
// Text(_memberController.attribute.value != 128
// ? '加入黑名单'
// : '移除黑名单'),
// ],
// ),
// )
// ],
// PopupMenuItem(
// onTap: () => _memberController.shareUser(),
// child: Row(
// mainAxisSize: MainAxisSize.min,
// children: [
// const Icon(Icons.share_outlined, size: 19),
// const SizedBox(width: 10),
// Text(_memberController.ownerMid !=
// _memberController.mid
// ? '分享UP主'
// : '分享我的主页'),
// ],
// ),
// ),
// ],
// ),
const SizedBox(width: 4), const SizedBox(width: 4),
], ],
flexibleSpace: FlexibleSpaceBar( flexibleSpace: FlexibleSpaceBar(
background: Stack( background: Stack(
children: [ children: [
if (_memberController.face != null) // if (_memberController.face != null)
Positioned.fill( // Positioned.fill(
bottom: 10, // bottom: 10,
child: Container( // child: Container(
decoration: BoxDecoration( // decoration: BoxDecoration(
image: DecorationImage( // image: DecorationImage(
fit: BoxFit.fitWidth, // fit: BoxFit.fitWidth,
image: NetworkImage(_memberController.face!), // image: NetworkImage(_memberController.face!),
alignment: Alignment.topCenter, // alignment: Alignment.topCenter,
isAntiAlias: true, // isAntiAlias: true,
), // ),
), // ),
foregroundDecoration: BoxDecoration( // foregroundDecoration: BoxDecoration(
gradient: LinearGradient( // gradient: LinearGradient(
colors: [ // colors: [
Theme.of(context) // Theme.of(context)
.colorScheme // .colorScheme
.background // .background
.withOpacity(0.44), // .withOpacity(0.44),
Theme.of(context).colorScheme.background, // Theme.of(context).colorScheme.background,
], // ],
begin: Alignment.topCenter, // begin: Alignment.topCenter,
end: Alignment.bottomCenter, // end: Alignment.bottomCenter,
stops: const [0.0, 0.46], // stops: const [0.0, 0.46],
), // ),
), // ),
), // ),
), // ),
Positioned( Positioned(
left: 0, left: 0,
right: 0, right: 0,
@ -179,109 +218,109 @@ class _MemberPageState extends State<MemberPage>
fontWeight: fontWeight:
FontWeight.bold), FontWeight.bold),
)), )),
const SizedBox(width: 2), // const SizedBox(width: 2),
if (_memberController // if (_memberController
.memberInfo.value.sex == // .memberInfo.value.sex ==
'') // '女')
const Icon( // const Icon(
FontAwesomeIcons.venus, // FontAwesomeIcons.venus,
size: 14, // size: 14,
color: Colors.pink, // color: Colors.pink,
), // ),
if (_memberController // if (_memberController
.memberInfo.value.sex == // .memberInfo.value.sex ==
'') // '男')
const Icon( // const Icon(
FontAwesomeIcons.mars, // FontAwesomeIcons.mars,
size: 14, // size: 14,
color: Colors.blue, // color: Colors.blue,
), // ),
const SizedBox(width: 4), // const SizedBox(width: 4),
Image.asset( // Image.asset(
'assets/images/lv/lv${_memberController.memberInfo.value.level}.png', // 'assets/images/lv/lv${_memberController.memberInfo.value.level}.png',
height: 11, // height: 11,
), // ),
const SizedBox(width: 6), // const SizedBox(width: 6),
if (_memberController.memberInfo // if (_memberController.memberInfo
.value.vip!.status == // .value.vip!.status ==
1 && // 1 &&
_memberController.memberInfo // _memberController.memberInfo
.value.vip!.label![ // .value.vip!.label![
'img_label_uri_hans'] != // 'img_label_uri_hans'] !=
'') ...[ // '') ...[
Image.network( // Image.network(
_memberController.memberInfo // _memberController.memberInfo
.value.vip!.label![ // .value.vip!.label![
'img_label_uri_hans'], // 'img_label_uri_hans'],
height: 20, // height: 20,
), // ),
] else if (_memberController // ] else if (_memberController
.memberInfo // .memberInfo
.value // .value
.vip! // .vip!
.status == // .status ==
1 && // 1 &&
_memberController.memberInfo // _memberController.memberInfo
.value.vip!.label![ // .value.vip!.label![
'img_label_uri_hans_static'] != // 'img_label_uri_hans_static'] !=
'') ...[ // '') ...[
Image.network( // Image.network(
_memberController.memberInfo // _memberController.memberInfo
.value.vip!.label![ // .value.vip!.label![
'img_label_uri_hans_static'], // 'img_label_uri_hans_static'],
height: 20, // height: 20,
), // ),
] // ]
], ],
), ),
if (_memberController.memberInfo.value // if (_memberController.memberInfo.value
.official!['title'] != // .official!['title'] !=
'') ...[ // '') ...[
const SizedBox(height: 6), // const SizedBox(height: 6),
Text.rich( // Text.rich(
maxLines: 2, // maxLines: 2,
TextSpan( // TextSpan(
text: _memberController // text: _memberController
.memberInfo // .memberInfo
.value // .value
.official!['role'] == // .official!['role'] ==
1 // 1
? '个人认证:' // ? '个人认证:'
: '企业认证:', // : '企业认证:',
style: TextStyle( // style: TextStyle(
color: Theme.of(context) // color: Theme.of(context)
.primaryColor, // .primaryColor,
), // ),
children: [ // children: [
TextSpan( // TextSpan(
text: _memberController // text: _memberController
.memberInfo // .memberInfo
.value // .value
.official!['title'], // .official!['title'],
), // ),
], // ],
), // ),
softWrap: true, // softWrap: true,
), // ),
], // ],
const SizedBox(height: 4), // const SizedBox(height: 4),
if (_memberController // if (_memberController
.memberInfo.value.sign != // .memberInfo.value.sign !=
'') // '')
SelectableRegion( // SelectableRegion(
magnifierConfiguration: // magnifierConfiguration:
const TextMagnifierConfiguration(), // const TextMagnifierConfiguration(),
focusNode: FocusNode(), // focusNode: FocusNode(),
selectionControls: // selectionControls:
MaterialTextSelectionControls(), // MaterialTextSelectionControls(),
child: Text( // child: Text(
_memberController // _memberController
.memberInfo.value.sign!, // .memberInfo.value.sign!,
textAlign: TextAlign.left, // textAlign: TextAlign.left,
maxLines: 2, // maxLines: 2,
overflow: TextOverflow.ellipsis, // overflow: TextOverflow.ellipsis,
), // ),
), // ),
], ],
), ),
], ],
@ -308,28 +347,29 @@ class _MemberPageState extends State<MemberPage>
return MediaQuery.of(context).padding.top + kToolbarHeight; return MediaQuery.of(context).padding.top + kToolbarHeight;
}, },
onlyOneScrollInBody: true, onlyOneScrollInBody: true,
body: Column( // body: Column(
children: [ // children: [
SizedBox( // SizedBox(
width: double.infinity, // width: double.infinity,
height: 50, // height: 50,
child: TabBar(controller: _tabController, tabs: const [ // child: TabBar(controller: _tabController, tabs: const [
Tab(text: '主页'), // Tab(text: '主页'),
Tab(text: '动态'), // Tab(text: '动态'),
Tab(text: '投稿'), // Tab(text: '投稿'),
]), // ]),
), // ),
Expanded( // Expanded(
child: TabBarView( // child: TabBarView(
controller: _tabController, // controller: _tabController,
children: const [ // children: const [
Text('主页'), // Text('主页'),
MemberDynamicPanel(), // MemberDynamicPanel(),
ArchivePanel(), // ArchivePanel(),
], // ],
)) // ))
], // ],
), // ),
body: ArchivePanel(),
), ),
); );
} }

View File

@ -76,79 +76,79 @@ Widget profile(ctr, {loadingStatus = false}) {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Padding( // Padding(
padding: const EdgeInsets.only(left: 10, right: 10), // padding: const EdgeInsets.only(left: 10, right: 10),
child: Row( // child: Row(
mainAxisSize: MainAxisSize.max, // mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround, // mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [ // children: [
InkWell( // InkWell(
onTap: () { // onTap: () {
Get.toNamed( // Get.toNamed(
'/follow?mid=${memberInfo.mid}&name=${memberInfo.name}'); // '/follow?mid=${memberInfo.mid}&name=${memberInfo.name}');
}, // },
child: Column( // child: Column(
children: [ // children: [
Text( // Text(
!loadingStatus // !loadingStatus
? ctr.userStat!['following'].toString() // ? ctr.userStat!['following'].toString()
: '-', // : '-',
style: const TextStyle( // style: const TextStyle(
fontWeight: FontWeight.bold), // fontWeight: FontWeight.bold),
), // ),
Text( // Text(
'关注', // '关注',
style: TextStyle( // style: TextStyle(
fontSize: Theme.of(context) // fontSize: Theme.of(context)
.textTheme // .textTheme
.labelMedium! // .labelMedium!
.fontSize), // .fontSize),
) // )
], // ],
), // ),
), // ),
InkWell( // InkWell(
onTap: () { // onTap: () {
Get.toNamed( // Get.toNamed(
'/fan?mid=${memberInfo.mid}&name=${memberInfo.name}'); // '/fan?mid=${memberInfo.mid}&name=${memberInfo.name}');
}, // },
child: Column( // child: Column(
children: [ // children: [
Text( // Text(
!loadingStatus // !loadingStatus
? Utils.numFormat( // ? Utils.numFormat(
ctr.userStat!['follower'], // ctr.userStat!['follower'],
) // )
: '-', // : '-',
style: const TextStyle( // style: const TextStyle(
fontWeight: FontWeight.bold)), // fontWeight: FontWeight.bold)),
Text('粉丝', // Text('粉丝',
style: TextStyle( // style: TextStyle(
fontSize: Theme.of(context) // fontSize: Theme.of(context)
.textTheme // .textTheme
.labelMedium! // .labelMedium!
.fontSize)) // .fontSize))
], // ],
), // ),
), // ),
Column( // Column(
children: [ // children: [
const Text('-', // const Text('-',
style: TextStyle(fontWeight: FontWeight.bold)), // style: TextStyle(fontWeight: FontWeight.bold)),
Text( // Text(
'获赞', // '获赞',
style: TextStyle( // style: TextStyle(
fontSize: Theme.of(context) // fontSize: Theme.of(context)
.textTheme // .textTheme
.labelMedium! // .labelMedium!
.fontSize), // .fontSize),
) // )
], // ],
), // ),
], // ],
), // ),
), // ),
const SizedBox(height: 10), // const SizedBox(height: 10),
if (ctr.ownerMid != ctr.mid) ...[ if (ctr.ownerMid != ctr.mid) ...[
Row( Row(
children: [ children: [

View File

@ -0,0 +1,94 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/member.dart';
import 'package:pilipala/models/member/archive.dart';
class MemberSearchController extends GetxController {
final ScrollController scrollController = ScrollController();
Rx<TextEditingController> controller = TextEditingController().obs;
final FocusNode searchFocusNode = FocusNode();
RxString searchKeyWord = ''.obs;
String hintText = '搜索';
RxString loadingStatus = 'init'.obs;
RxString loadingText = '加载中...'.obs;
bool hasRequest = false;
late int mid;
RxString uname = ''.obs;
int archivePn = 1;
int archiveCount = 0;
RxList<VListItemModel> archiveList = <VListItemModel>[].obs;
int dynamic_pn = 1;
RxList<VListItemModel> dynamicList = <VListItemModel>[].obs;
int ps = 30;
@override
void onInit() {
super.onInit();
mid = int.parse(Get.parameters['mid']!);
uname.value = Get.parameters['uname']!;
}
// 清空搜索
void onClear() {
if (searchKeyWord.value.isNotEmpty && controller.value.text != '') {
controller.value.clear();
searchKeyWord.value = '';
} else {
Get.back();
}
}
void onChange(value) {
searchKeyWord.value = value;
}
// 提交搜索内容
void submit() {
loadingStatus.value = 'loading';
if (hasRequest) {
archivePn = 1;
searchArchives();
}
}
// 搜索视频
Future searchArchives({type = 'init'}) async {
if (type == 'onLoad' && loadingText.value == '没有更多了') {
return;
}
var res = await MemberHttp.memberArchive(
mid: mid,
pn: archivePn,
keyword: controller.value.text,
order: 'pubdate',
);
if (res['status']) {
if (type == 'init' || archivePn == 1) {
archiveList.value = res['data'].list.vlist;
} else {
archiveList.addAll(res['data'].list.vlist);
}
archiveCount = res['data'].page['count'];
if (archiveList.length == archiveCount) {
loadingText.value = '没有更多了';
}
archivePn += 1;
hasRequest = true;
if (res['data'].list.vlist.isEmpty) {
loadingStatus.value = 'finish';
}
}
// loadingStatus.value = 'finish';
return res;
}
// 搜索动态
Future searchDynamic() async {}
//
onLoad() {
searchArchives(type: 'onLoad');
}
}

View File

@ -0,0 +1,4 @@
library member_search;
export './controller.dart';
export './view.dart';

View File

@ -0,0 +1,208 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/skeleton/video_card_h.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/no_data.dart';
import 'package:pilipala/common/widgets/video_card_h.dart';
import 'controller.dart';
class MemberSearchPage extends StatefulWidget {
const MemberSearchPage({super.key});
@override
State<MemberSearchPage> createState() => _MemberSearchPageState();
}
class _MemberSearchPageState extends State<MemberSearchPage>
with SingleTickerProviderStateMixin {
final MemberSearchController _memberSearchCtr =
Get.put(MemberSearchController());
late ScrollController scrollController;
@override
void initState() {
super.initState();
scrollController = _memberSearchCtr.scrollController;
scrollController.addListener(
() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 300) {
EasyThrottle.throttle('history', const Duration(seconds: 1), () {
_memberSearchCtr.onLoad();
});
}
},
);
// _tabController = TabController(length: 2, vsync: this);
}
@override
void dispose() {
// _tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
titleSpacing: 0,
actions: [
IconButton(
onPressed: () => _memberSearchCtr.submit(),
icon: const Icon(CupertinoIcons.search, size: 22)),
const SizedBox(width: 10)
],
title: Obx(
() => TextField(
autofocus: true,
focusNode: _memberSearchCtr.searchFocusNode,
controller: _memberSearchCtr.controller.value,
textInputAction: TextInputAction.search,
onChanged: (value) => _memberSearchCtr.onChange(value),
decoration: InputDecoration(
hintText: _memberSearchCtr.hintText,
border: InputBorder.none,
suffixIcon: IconButton(
icon: Icon(
Icons.clear,
size: 22,
color: Theme.of(context).colorScheme.outline,
),
onPressed: () => _memberSearchCtr.onClear(),
),
),
onSubmitted: (String value) => _memberSearchCtr.submit(),
),
),
),
body: Obx(
() => Column(
children: _memberSearchCtr.loadingStatus.value == 'init'
? [
Expanded(
child: Center(
child: Text('搜索「${_memberSearchCtr.uname.value}」的视频'),
),
),
]
: [
// TabBar(
// controller: _tabController,
// tabs: const [
// Tab(text: "视频"),
// Tab(text: "动态"),
// ],
// ),
Expanded(
child:
// TabBarView(
// controller: _tabController,
// children: [
FutureBuilder(
future: _memberSearchCtr.searchArchives(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
return Obx(
() => _memberSearchCtr.archiveList.isNotEmpty
? ListView.builder(
controller: scrollController,
itemCount:
_memberSearchCtr.archiveList.length +
1,
itemBuilder: (context, index) {
if (index ==
_memberSearchCtr
.archiveList.length) {
return Container(
height: MediaQuery.of(context)
.padding
.bottom +
60,
padding: EdgeInsets.only(
bottom: MediaQuery.of(context)
.padding
.bottom),
child: Center(
child: Obx(
() => Text(
_memberSearchCtr
.loadingText.value,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.outline,
fontSize: 13),
),
),
),
);
} else {
return VideoCardH(
videoItem: _memberSearchCtr
.archiveList[index]);
}
},
)
: _memberSearchCtr.loadingStatus.value ==
'loading'
? ListView.builder(
itemCount: 10,
itemBuilder: (context, index) {
return const VideoCardHSkeleton();
},
)
: CustomScrollView(
slivers: <Widget>[
SliverToBoxAdapter(
child: SizedBox(
height: 400,
child: Center(
child: Text(
'没有搜索到相关内容\n请尝试别的搜索词',
textAlign: TextAlign.center,
style: Theme.of(context)
.textTheme
.titleSmall,
),
),
),
),
],
),
);
} else {
return CustomScrollView(
slivers: <Widget>[
HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
)
],
);
}
} else {
// 骨架屏
return ListView.builder(
itemCount: 10,
itemBuilder: (context, index) {
return const VideoCardHSkeleton();
},
);
}
},
),
// ],
// ),
),
],
),
),
);
}
}

View File

@ -72,38 +72,30 @@ class _MinePageState extends State<MinePage> {
const SizedBox(width: 10), const SizedBox(width: 10),
], ],
), ),
body: LayoutBuilder( body: SingleChildScrollView(
builder: (context, constraint) { physics: const NeverScrollableScrollPhysics(),
return SingleChildScrollView( child: Column(
physics: const NeverScrollableScrollPhysics(), children: [
child: SizedBox( const SizedBox(height: 10),
height: constraint.maxHeight, FutureBuilder(
child: Column( future: _futureBuilderFuture,
children: [ builder: (context, snapshot) {
const SizedBox(height: 10), if (snapshot.connectionState == ConnectionState.done) {
FutureBuilder( if (snapshot.data == null) {
future: _futureBuilderFuture, return const SizedBox();
builder: (context, snapshot) { }
if (snapshot.connectionState == ConnectionState.done) { if (snapshot.data['status']) {
if (snapshot.data == null) { return Obx(() => userInfoBuild(mineController, context));
return const SizedBox(); } else {
} return userInfoBuild(mineController, context);
if (snapshot.data['status']) { }
return Obx( } else {
() => userInfoBuild(mineController, context)); return userInfoBuild(mineController, context);
} else { }
return userInfoBuild(mineController, context); },
}
} else {
return userInfoBuild(mineController, context);
}
},
),
],
),
), ),
); ],
}, ),
), ),
); );
} }
@ -138,85 +130,9 @@ class _MinePageState extends State<MinePage> {
_mineController.userInfo.value.uname ?? '点击头像登录', _mineController.userInfo.value.uname ?? '点击头像登录',
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
const SizedBox(width: 4),
Image.asset(
'assets/images/lv/lv${_mineController.userInfo.value.levelInfo != null ? _mineController.userInfo.value.levelInfo!.currentLevel : '0'}.png',
height: 10,
),
],
),
const SizedBox(height: 5),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text.rich(TextSpan(children: [
TextSpan(
text: '硬币: ',
style:
TextStyle(color: Theme.of(context).colorScheme.outline)),
TextSpan(
text: (_mineController.userInfo.value.money ?? 'pilipala')
.toString(),
style:
TextStyle(color: Theme.of(context).colorScheme.primary)),
]))
], ],
), ),
const SizedBox(height: 25), const SizedBox(height: 25),
if (_mineController.userInfo.value.levelInfo != null) ...[
LayoutBuilder(
builder: (context, BoxConstraints box) {
LevelInfo levelInfo = _mineController.userInfo.value.levelInfo;
return SizedBox(
width: box.maxWidth,
height: 24,
child: Stack(
children: [
Positioned(
top: 0,
right: 0,
bottom: 0,
child: Container(
color: Theme.of(context).colorScheme.primary,
height: 24,
constraints:
const BoxConstraints(minWidth: 100), // 设置最小宽度为100
width: box.maxWidth *
(1 - (levelInfo.currentExp! / levelInfo.nextExp!)),
child: Center(
child: Text(
'${levelInfo.currentExp!}/${levelInfo.nextExp!}',
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimary,
fontSize: 12,
),
),
),
),
),
Positioned(
top: 23,
left: 0,
bottom: 0,
child: Container(
width: box.maxWidth *
(_mineController
.userInfo.value.levelInfo!.currentExp! /
_mineController
.userInfo.value.levelInfo!.nextExp!),
height: 1,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
),
),
),
],
),
);
},
),
],
const SizedBox(height: 30),
Padding( Padding(
padding: const EdgeInsets.only(left: 12, right: 12), padding: const EdgeInsets.only(left: 12, right: 12),
child: LayoutBuilder( child: LayoutBuilder(

View File

@ -27,9 +27,9 @@ class SSearchController extends GetxController {
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
if (setting.get(SettingBoxKey.enableSearchWord, defaultValue: true)) { // if (setting.get(SettingBoxKey.enableSearchWord, defaultValue: true)) {
searchDefault(); // searchDefault();
} // }
// 其他页面跳转过来 // 其他页面跳转过来
if (Get.parameters.keys.isNotEmpty) { if (Get.parameters.keys.isNotEmpty) {
if (Get.parameters['keyword'] != null) { if (Get.parameters['keyword'] != null) {
@ -51,7 +51,7 @@ class SSearchController extends GetxController {
searchSuggestList.value = []; searchSuggestList.value = [];
return; return;
} }
_debouncer.call(() => querySearchSuggest(value)); // _debouncer.call(() => querySearchSuggest(value));
} }
void onClear() { void onClear() {

View File

@ -140,141 +140,18 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
), ),
), ),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Column( child: _history(),
children: [
const SizedBox(height: 12),
// 搜索建议
_searchSuggest(),
// 热搜
Visibility(
visible: _searchController.enableHotKey,
child: hotSearch(_searchController)),
// 搜索历史
_history()
],
),
), ),
); );
}, },
); );
} }
Widget _searchSuggest() {
SSearchController _ssCtr = _searchController;
return Obx(
() => _ssCtr.searchSuggestList.isNotEmpty &&
_ssCtr.searchSuggestList.first.term != null &&
_ssCtr.controller.value.text != ''
? ListView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: _ssCtr.searchSuggestList.length,
itemBuilder: (context, index) {
return InkWell(
customBorder: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
onTap: () => _ssCtr
.onClickKeyword(_ssCtr.searchSuggestList[index].term!),
child: Padding(
padding: const EdgeInsets.only(left: 20, top: 9, bottom: 9),
child: _ssCtr.searchSuggestList[index].textRich,
),
);
},
)
: const SizedBox(),
);
}
Widget hotSearch(ctr) {
return Padding(
padding: const EdgeInsets.fromLTRB(10, 14, 4, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(6, 0, 6, 6),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'大家都在搜',
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontWeight: FontWeight.bold),
),
SizedBox(
height: 34,
child: TextButton.icon(
style: ButtonStyle(
padding: MaterialStateProperty.all(const EdgeInsets.only(
left: 10, top: 6, bottom: 6, right: 10)),
),
onPressed: () => ctr.queryHotSearchList(),
icon: const Icon(Icons.refresh_outlined, size: 18),
label: const Text('刷新'),
),
),
],
),
),
LayoutBuilder(
builder: (context, boxConstraints) {
final double width = boxConstraints.maxWidth;
return FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == null) {
return const SizedBox();
}
Map data = snapshot.data as Map;
if (data['status']) {
return Obx(
() => HotKeyword(
width: width,
hotSearchList: _searchController.hotSearchList.value,
onClick: (keyword) async {
_searchController.searchFocusNode.unfocus();
await Future.delayed(
const Duration(milliseconds: 150));
_searchController.onClickKeyword(keyword);
},
),
);
} else {
return HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
);
}
} else {
// 缓存数据
if (_searchController.hotSearchList.isNotEmpty) {
return HotKeyword(
width: width,
hotSearchList: _searchController.hotSearchList,
);
} else {
return const SizedBox();
}
}
},
);
},
),
],
),
);
}
Widget _history() { Widget _history() {
return Obx( return Obx(
() => Container( () => Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.fromLTRB(10, 25, 6, 0), padding: const EdgeInsets.fromLTRB(10, 4, 6, 0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [

View File

@ -84,12 +84,12 @@ class _SearchPanelState extends State<SearchPanel>
ctr: _searchPanelController, ctr: _searchPanelController,
list: list.value, list: list.value,
); );
case SearchType.media_bangumi: // case SearchType.media_bangumi:
return searchMbangumiPanel(context, ctr, list); // return searchMbangumiPanel(context, ctr, list);
case SearchType.bili_user: case SearchType.bili_user:
return searchUserPanel(context, ctr, list); return searchUserPanel(context, ctr, list);
case SearchType.live_room: // case SearchType.live_room:
return searchLivePanel(context, ctr, list); // return searchLivePanel(context, ctr, list);
default: default:
return const SizedBox(); return const SizedBox();
} }
@ -115,12 +115,12 @@ class _SearchPanelState extends State<SearchPanel>
switch (widget.searchType) { switch (widget.searchType) {
case SearchType.video: case SearchType.video:
return const VideoCardHSkeleton(); return const VideoCardHSkeleton();
case SearchType.media_bangumi: // case SearchType.media_bangumi:
return const MediaBangumiSkeleton(); // return const MediaBangumiSkeleton();
case SearchType.bili_user: case SearchType.bili_user:
return const VideoCardHSkeleton(); return const VideoCardHSkeleton();
case SearchType.live_room: // case SearchType.live_room:
return const VideoCardHSkeleton(); // return const VideoCardHSkeleton();
default: default:
return const VideoCardHSkeleton(); return const VideoCardHSkeleton();
} }

View File

@ -124,7 +124,7 @@ Widget searchMbangumiPanel(BuildContext context, ctr, list) {
arguments: { arguments: {
'pic': pic, 'pic': pic,
'heroTag': heroTag, 'heroTag': heroTag,
'videoType': SearchType.media_bangumi, // 'videoType': SearchType.media_bangumi,
'bangumiItem': res['data'], 'bangumiItem': res['data'],
}, },
); );

View File

@ -38,26 +38,11 @@ Widget searchUserPanel(BuildContext context, ctr, list) {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Row( Text(
children: [ i!.uname,
Text( style: const TextStyle(
i!.uname, fontSize: 14,
style: const TextStyle( ),
fontSize: 14,
),
),
const SizedBox(width: 6),
Image.asset(
'assets/images/lv/lv${i!.level}.png',
height: 11,
),
],
),
Row(
children: [
Text('粉丝:${i.fans} ', style: style),
Text(' 视频:${i.videos}', style: style)
],
), ),
if (i.officialVerify['desc'] != '') if (i.officialVerify['desc'] != '')
Text( Text(

View File

@ -1,4 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:floating/floating.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';
@ -11,6 +13,7 @@ import 'package:pilipala/models/video/play/quality.dart';
import 'package:pilipala/models/video/play/url.dart'; import 'package:pilipala/models/video/play/url.dart';
import 'package:pilipala/models/video/reply/item.dart'; import 'package:pilipala/models/video/reply/item.dart';
import 'package:pilipala/pages/video/detail/replyReply/index.dart'; import 'package:pilipala/pages/video/detail/replyReply/index.dart';
import 'package:pilipala/pages/video/detail/widgets/header_control.dart';
import 'package:pilipala/plugin/pl_player/index.dart'; import 'package:pilipala/plugin/pl_player/index.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';
@ -20,7 +23,7 @@ class VideoDetailController extends GetxController
with GetSingleTickerProviderStateMixin { with GetSingleTickerProviderStateMixin {
/// 路由传参 /// 路由传参
String bvid = Get.parameters['bvid']!; String bvid = Get.parameters['bvid']!;
int cid = int.parse(Get.parameters['cid']!); RxInt cid = int.parse(Get.parameters['cid']!).obs;
RxInt danmakuCid = 0.obs; RxInt danmakuCid = 0.obs;
String heroTag = Get.arguments['heroTag']; String heroTag = Get.arguments['heroTag'];
// 视频详情 // 视频详情
@ -76,6 +79,8 @@ class VideoDetailController extends GetxController
bool enableHeart = true; bool enableHeart = true;
var userInfo; var userInfo;
late bool isFirstTime = true; late bool isFirstTime = true;
Floating? floating;
late PreferredSizeWidget headerControl;
@override @override
void onInit() { void onInit() {
@ -103,7 +108,15 @@ class VideoDetailController extends GetxController
localCache.get(LocalCacheKey.historyPause) == true) { localCache.get(LocalCacheKey.historyPause) == true) {
enableHeart = false; enableHeart = false;
} }
danmakuCid.value = cid; danmakuCid.value = cid.value;
if (Platform.isAndroid) {
floating = Floating();
}
headerControl = HeaderControl(
controller: plPlayerController,
videoDetailCtr: this,
floating: floating,
);
} }
showReplyReplyPanel() { showReplyReplyPanel() {
@ -202,7 +215,7 @@ class VideoDetailController extends GetxController
// 默认1倍速 // 默认1倍速
speed: 1.0, speed: 1.0,
bvid: bvid, bvid: bvid,
cid: cid, cid: cid.value,
enableHeart: enableHeart, enableHeart: enableHeart,
isFirstTime: isFirstTime, isFirstTime: isFirstTime,
autoplay: autoplay, autoplay: autoplay,
@ -211,7 +224,7 @@ class VideoDetailController extends GetxController
// 视频链接 // 视频链接
Future queryVideoUrl() async { Future queryVideoUrl() async {
var result = await VideoHttp.videoUrl(cid: cid, bvid: bvid); var result = await VideoHttp.videoUrl(cid: cid.value, bvid: bvid);
if (result['status']) { if (result['status']) {
data = result['data']; data = result['data'];

View File

@ -11,6 +11,7 @@ import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/models/video_detail_res.dart'; import 'package:pilipala/models/video_detail_res.dart';
import 'package:pilipala/pages/video/detail/controller.dart'; import 'package:pilipala/pages/video/detail/controller.dart';
import 'package:pilipala/pages/video/detail/reply/index.dart'; import 'package:pilipala/pages/video/detail/reply/index.dart';
import 'package:pilipala/plugin/pl_player/models/play_repeat.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/storage.dart'; import 'package:pilipala/utils/storage.dart';
@ -58,10 +59,14 @@ class VideoIntroController extends GetxController {
RxString total = '1'.obs; RxString total = '1'.obs;
Timer? timer; Timer? timer;
bool isPaused = false; bool isPaused = false;
String heroTag = '';
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
try {
heroTag = Get.arguments['heroTag'];
} catch (_) {}
userInfo = userInfoCache.get('userInfoCache'); userInfo = userInfoCache.get('userInfoCache');
if (Get.arguments.isNotEmpty) { if (Get.arguments.isNotEmpty) {
if (Get.arguments.containsKey('videoItem')) { if (Get.arguments.containsKey('videoItem')) {
@ -442,7 +447,7 @@ class VideoIntroController extends GetxController {
VideoDetailController videoDetailCtr = VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']); Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);
videoDetailCtr.bvid = bvid; videoDetailCtr.bvid = bvid;
videoDetailCtr.cid = cid; videoDetailCtr.cid.value = cid;
videoDetailCtr.danmakuCid.value = cid; videoDetailCtr.danmakuCid.value = cid;
videoDetailCtr.queryVideoUrl(); videoDetailCtr.queryVideoUrl();
// 重新请求评论 // 重新请求评论
@ -486,4 +491,45 @@ class VideoIntroController extends GetxController {
} }
super.onClose(); super.onClose();
} }
/// 列表循环或者顺序播放时,自动播放下一个
void nextPlay() {
late List episodes;
bool isPages = false;
if (videoDetail.value.ugcSeason != null) {
UgcSeason ugcSeason = videoDetail.value.ugcSeason!;
List<SectionItem> sections = ugcSeason.sections!;
episodes = [];
for (int i = 0; i < sections.length; i++) {
List<EpisodeItem> episodesList = sections[i].episodes!;
episodes.addAll(episodesList);
}
} else if (videoDetail.value.pages != null) {
isPages = true;
List<Part> pages = videoDetail.value.pages!;
episodes = [];
episodes.addAll(pages);
}
int currentIndex = episodes.indexWhere((e) => e.cid == lastPlayCid.value);
int nextIndex = currentIndex + 1;
VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: heroTag);
PlayRepeat platRepeat = videoDetailCtr.plPlayerController.playRepeat;
// 列表循环
if (nextIndex >= episodes.length) {
if (platRepeat == PlayRepeat.listCycle) {
nextIndex = 0;
}
if (platRepeat == PlayRepeat.listOrder) {
return;
}
}
int cid = episodes[nextIndex].cid!;
String rBvid = isPages ? bvid : episodes[nextIndex].bvid;
int rAid = isPages ? IdUtils.bv2av(bvid) : episodes[nextIndex].aid!;
changeSeasonOrbangu(rBvid, cid, rAid);
}
} }

View File

@ -249,83 +249,83 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
maxLines: 1, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
const SizedBox(width: 20), // const SizedBox(width: 20),
SizedBox( // SizedBox(
width: 34, // width: 34,
height: 34, // height: 34,
child: IconButton( // child: IconButton(
style: ButtonStyle( // style: ButtonStyle(
padding: // padding:
MaterialStateProperty.all(EdgeInsets.zero), // MaterialStateProperty.all(EdgeInsets.zero),
backgroundColor: // backgroundColor:
MaterialStateProperty.resolveWith((states) { // MaterialStateProperty.resolveWith((states) {
return t.highlightColor.withOpacity(0.2); // return t.highlightColor.withOpacity(0.2);
}), // }),
), // ),
onPressed: showIntroDetail, // onPressed: showIntroDetail,
icon: Icon( // icon: Icon(
Icons.more_horiz, // Icons.more_horiz,
color: Theme.of(context).colorScheme.primary, // color: Theme.of(context).colorScheme.primary,
), // ),
), // ),
), // ),
], ],
), ),
), ),
GestureDetector( // GestureDetector(
behavior: HitTestBehavior.translucent, // behavior: HitTestBehavior.translucent,
onTap: () => showIntroDetail(), // onTap: () => showIntroDetail(),
child: Row( // child: Row(
children: [ // children: [
StatView( // StatView(
theme: 'gray', // theme: 'gray',
view: !widget.loadingStatus // view: !widget.loadingStatus
? widget.videoDetail!.stat!.view // ? widget.videoDetail!.stat!.view
: videoItem['stat'].view, // : videoItem['stat'].view,
size: 'medium', // size: 'medium',
), // ),
const SizedBox(width: 10), // const SizedBox(width: 10),
StatDanMu( // StatDanMu(
theme: 'gray', // theme: 'gray',
danmu: !widget.loadingStatus // danmu: !widget.loadingStatus
? widget.videoDetail!.stat!.danmaku // ? widget.videoDetail!.stat!.danmaku
: videoItem['stat'].danmaku, // : videoItem['stat'].danmaku,
size: 'medium', // size: 'medium',
), // ),
const SizedBox(width: 10), // const SizedBox(width: 10),
Text( // Text(
Utils.dateFormat( // Utils.dateFormat(
!widget.loadingStatus // !widget.loadingStatus
? widget.videoDetail!.pubdate // ? widget.videoDetail!.pubdate
: videoItem['pubdate'], // : videoItem['pubdate'],
formatType: 'detail'), // formatType: 'detail'),
style: TextStyle( // style: TextStyle(
fontSize: 12, // fontSize: 12,
color: t.colorScheme.outline, // color: t.colorScheme.outline,
), // ),
), // ),
const SizedBox(width: 10), // const SizedBox(width: 10),
if (videoIntroController.isShowOnlineTotal) // if (videoIntroController.isShowOnlineTotal)
Obx( // Obx(
() => Text( // () => Text(
'${videoIntroController.total.value}人在看', // '${videoIntroController.total.value}人在看',
style: TextStyle( // style: TextStyle(
fontSize: 12, // fontSize: 12,
color: t.colorScheme.outline, // color: t.colorScheme.outline,
), // ),
), // ),
), // ),
], // ],
), // ),
), // ),
const SizedBox(height: 7), // const SizedBox(height: 7),
// 点赞收藏转发 布局样式1 // 点赞收藏转发 布局样式1
SingleChildScrollView( SingleChildScrollView(
padding: const EdgeInsets.only(top: 7, bottom: 7), padding: const EdgeInsets.only(top: 10, bottom: 7),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: actionRow( child: actionRow(
context, context,
@ -382,14 +382,6 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
const SizedBox(width: 10), const SizedBox(width: 10),
Text(owner.name, Text(owner.name,
style: const TextStyle(fontSize: 13)), style: const TextStyle(fontSize: 13)),
const SizedBox(width: 6),
Text(
follower,
style: TextStyle(
fontSize: t.textTheme.labelSmall!.fontSize,
color: outline,
),
),
const Spacer(), const Spacer(),
AnimatedOpacity( AnimatedOpacity(
opacity: loadingStatus ? 0 : 1, opacity: loadingStatus ? 0 : 1,
@ -516,52 +508,52 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
Widget actionRow(BuildContext context, videoIntroController, videoDetailCtr) { Widget actionRow(BuildContext context, videoIntroController, videoDetailCtr) {
return Row(children: [ return Row(children: [
Obx( // Obx(
() => ActionRowItem( // () => ActionRowItem(
icon: const Icon(FontAwesomeIcons.thumbsUp), // icon: const Icon(FontAwesomeIcons.thumbsUp),
onTap: () => videoIntroController.actionLikeVideo(), // onTap: () => videoIntroController.actionLikeVideo(),
selectStatus: videoIntroController.hasLike.value, // selectStatus: videoIntroController.hasLike.value,
loadingStatus: loadingStatus, // loadingStatus: loadingStatus,
text: // text:
!loadingStatus ? widget.videoDetail!.stat!.like!.toString() : '-', // !loadingStatus ? widget.videoDetail!.stat!.like!.toString() : '-',
), // ),
), // ),
const SizedBox(width: 8), // const SizedBox(width: 8),
Obx( // Obx(
() => ActionRowItem( // () => ActionRowItem(
icon: const Icon(FontAwesomeIcons.b), // icon: const Icon(FontAwesomeIcons.b),
onTap: () => videoIntroController.actionCoinVideo(), // onTap: () => videoIntroController.actionCoinVideo(),
selectStatus: videoIntroController.hasCoin.value, // selectStatus: videoIntroController.hasCoin.value,
loadingStatus: loadingStatus, // loadingStatus: loadingStatus,
text: // text:
!loadingStatus ? widget.videoDetail!.stat!.coin!.toString() : '-', // !loadingStatus ? widget.videoDetail!.stat!.coin!.toString() : '-',
), // ),
), // ),
const SizedBox(width: 8), // const SizedBox(width: 8),
Obx( // Obx(
() => ActionRowItem( // () => ActionRowItem(
icon: const Icon(FontAwesomeIcons.heart), // icon: const Icon(FontAwesomeIcons.heart),
onTap: () => showFavBottomSheet(), // onTap: () => showFavBottomSheet(),
onLongPress: () => showFavBottomSheet(type: 'longPress'), // onLongPress: () => showFavBottomSheet(type: 'longPress'),
selectStatus: videoIntroController.hasFav.value, // selectStatus: videoIntroController.hasFav.value,
loadingStatus: loadingStatus, // loadingStatus: loadingStatus,
text: !loadingStatus // text: !loadingStatus
? widget.videoDetail!.stat!.favorite!.toString() // ? widget.videoDetail!.stat!.favorite!.toString()
: '-', // : '-',
), // ),
), // ),
const SizedBox(width: 8), // const SizedBox(width: 8),
ActionRowItem( // ActionRowItem(
icon: const Icon(FontAwesomeIcons.comment), // icon: const Icon(FontAwesomeIcons.comment),
onTap: () { // onTap: () {
videoDetailCtr.tabCtr.animateTo(1); // videoDetailCtr.tabCtr.animateTo(1);
}, // },
selectStatus: false, // selectStatus: false,
loadingStatus: loadingStatus, // loadingStatus: loadingStatus,
text: // text:
!loadingStatus ? widget.videoDetail!.stat!.reply!.toString() : '-', // !loadingStatus ? widget.videoDetail!.stat!.reply!.toString() : '-',
), // ),
const SizedBox(width: 8), // const SizedBox(width: 8),
ActionRowItem( ActionRowItem(
icon: const Icon(FontAwesomeIcons.share), icon: const Icon(FontAwesomeIcons.share),
onTap: () => videoIntroController.actionShareVideo(), onTap: () => videoIntroController.actionShareVideo(),

View File

@ -57,29 +57,13 @@ class IntroDetail extends StatelessWidget {
), ),
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
Row( Text(
children: [ Utils.dateFormat(videoDetail!.pubdate,
StatView( formatType: 'detail'),
theme: 'gray', style: TextStyle(
view: videoDetail!.stat!.view, fontSize: 12,
size: 'medium', color: Theme.of(context).colorScheme.outline,
), ),
const SizedBox(width: 10),
StatDanMu(
theme: 'gray',
danmu: videoDetail!.stat!.danmaku,
size: 'medium',
),
const SizedBox(width: 10),
Text(
Utils.dateFormat(videoDetail!.pubdate,
formatType: 'detail'),
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.outline,
),
),
],
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
SizedBox( SizedBox(

View File

@ -1,6 +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/models/video_detail_res.dart'; import 'package:pilipala/models/video_detail_res.dart';
import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/utils/id_utils.dart'; import 'package:pilipala/utils/id_utils.dart';
class SeasonPanel extends StatefulWidget { class SeasonPanel extends StatefulWidget {
@ -23,11 +24,16 @@ class SeasonPanel extends StatefulWidget {
class _SeasonPanelState extends State<SeasonPanel> { class _SeasonPanelState extends State<SeasonPanel> {
late List<EpisodeItem> episodes; late List<EpisodeItem> episodes;
late int cid;
late int currentIndex; late int currentIndex;
String heroTag = Get.arguments['heroTag'];
late VideoDetailController _videoDetailController;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
cid = widget.cid!;
_videoDetailController = Get.find<VideoDetailController>(tag: heroTag);
/// 根据 cid 找到对应集,找到对应 episodes /// 根据 cid 找到对应集,找到对应 episodes
/// 有多个episodes时只显示其中一个 /// 有多个episodes时只显示其中一个
@ -48,6 +54,11 @@ class _SeasonPanelState extends State<SeasonPanel> {
// .firstWhere((e) => e.seasonId == widget.ugcSeason.id) // .firstWhere((e) => e.seasonId == widget.ugcSeason.id)
// .episodes!; // .episodes!;
currentIndex = episodes.indexWhere((e) => e.cid == widget.cid); currentIndex = episodes.indexWhere((e) => e.cid == widget.cid);
_videoDetailController.cid.listen((p0) {
cid = p0;
setState(() {});
currentIndex = episodes.indexWhere((e) => e.cid == cid);
});
} }
void changeFucCall(item, i) async { void changeFucCall(item, i) async {

View File

@ -20,6 +20,7 @@ import 'package:pilipala/pages/video/detail/controller.dart';
import 'package:pilipala/pages/video/detail/introduction/index.dart'; import 'package:pilipala/pages/video/detail/introduction/index.dart';
import 'package:pilipala/pages/video/detail/related/index.dart'; import 'package:pilipala/pages/video/detail/related/index.dart';
import 'package:pilipala/plugin/pl_player/index.dart'; import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'widgets/app_bar.dart'; import 'widgets/app_bar.dart';
@ -54,6 +55,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
// 自动退出全屏 // 自动退出全屏
late bool autoExitFullcreen; late bool autoExitFullcreen;
Floating? floating; Floating? floating;
late BangumiIntroController bangumiIntroController;
@override @override
void initState() { void initState() {
@ -61,6 +63,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
heroTag = Get.arguments['heroTag']; heroTag = Get.arguments['heroTag'];
videoDetailController = Get.put(VideoDetailController(), tag: heroTag); videoDetailController = Get.put(VideoDetailController(), tag: heroTag);
videoIntroController = Get.put(VideoIntroController(), tag: heroTag); videoIntroController = Get.put(VideoIntroController(), tag: heroTag);
bangumiIntroController = Get.put(BangumiIntroController(), tag: heroTag);
statusBarHeight = localCache.get('statusBarHeight'); statusBarHeight = localCache.get('statusBarHeight');
autoExitFullcreen = autoExitFullcreen =
setting.get(SettingBoxKey.enableAutoExit, defaultValue: false); setting.get(SettingBoxKey.enableAutoExit, defaultValue: false);
@ -99,11 +102,31 @@ class _VideoDetailPageState extends State<VideoDetailPage>
if (autoExitFullcreen) { if (autoExitFullcreen) {
plPlayerController!.triggerFullScreen(status: false); plPlayerController!.triggerFullScreen(status: false);
} }
// 播放完展示控制栏
PiPStatus currentStatus = await floating!.pipStatus; /// 顺序播放 列表循环
if (currentStatus == PiPStatus.disabled) { if (plPlayerController!.playRepeat != PlayRepeat.pause &&
plPlayerController!.onLockControl(false); plPlayerController!.playRepeat != PlayRepeat.singleCycle) {
if (videoDetailController.videoType == SearchType.video) {
videoIntroController.nextPlay();
}
if (videoDetailController.videoType == SearchType.media_bangumi) {
bangumiIntroController.nextPlay();
}
} }
/// 单个循环
if (plPlayerController!.playRepeat == PlayRepeat.singleCycle) {
plPlayerController!.seekTo(Duration.zero);
plPlayerController!.play();
}
// 播放完展示控制栏
try {
PiPStatus currentStatus =
await videoDetailController.floating!.pipStatus;
if (currentStatus == PiPStatus.disabled) {
plPlayerController!.onLockControl(false);
}
} catch (_) {}
} }
} }
@ -347,77 +370,40 @@ class _VideoDetailPageState extends State<VideoDetailPage>
}, },
onlyOneScrollInBody: true, onlyOneScrollInBody: true,
body: Container( body: Container(
key: Key(heroTag),
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.background,
child: Column( child: CustomScrollView(
children: [ key: const PageStorageKey<String>('简介'),
Opacity( slivers: <Widget>[
opacity: 0, if (videoDetailController.videoType ==
child: SizedBox( SearchType.video) ...[
width: double.infinity, const VideoIntroPanel(),
height: 0, ] else
child: Obx( // if (videoDetailController.videoType ==
() => TabBar( // SearchType.media_bangumi) ...[
controller: videoDetailController.tabCtr, // BangumiIntroPanel(
dividerColor: Colors.transparent, // cid: videoDetailController.cid)
indicatorColor: // ],
Theme.of(context).colorScheme.background, // if (videoDetailController.videoType ==
tabs: videoDetailController.tabs // SearchType.video) ...[
.map((String name) => Tab(text: name)) // SliverPersistentHeader(
.toList(), // floating: true,
), // pinned: true,
// delegate: SliverHeaderDelegate(
// height: 50,
// child:
// const MenuRow(loadingStatus: false),
// ),
// ),
// ],
SliverToBoxAdapter(
child: Divider(
indent: 12,
endIndent: 12,
color:
Theme.of(context).dividerColor.withOpacity(0.06),
), ),
), ),
), // const RelatedVideoPanel(),
Expanded(
child: TabBarView(
controller: videoDetailController.tabCtr,
children: [
Builder(
builder: (context) {
return CustomScrollView(
key: const PageStorageKey<String>('简介'),
slivers: <Widget>[
if (videoDetailController.videoType ==
SearchType.video) ...[
const VideoIntroPanel(),
] else if (videoDetailController.videoType ==
SearchType.media_bangumi) ...[
BangumiIntroPanel(
cid: videoDetailController.cid)
],
// if (videoDetailController.videoType ==
// SearchType.video) ...[
// SliverPersistentHeader(
// floating: true,
// pinned: true,
// delegate: SliverHeaderDelegate(
// height: 50,
// child:
// const MenuRow(loadingStatus: false),
// ),
// ),
// ],
SliverToBoxAdapter(
child: Divider(
indent: 12,
endIndent: 12,
color: Theme.of(context)
.dividerColor
.withOpacity(0.06),
),
),
const RelatedVideoPanel(),
],
);
},
),
VideoReplyPanel(
bvid: videoDetailController.bvid,
)
],
),
),
], ],
), ),
), ),

View File

@ -13,6 +13,7 @@ import 'package:pilipala/models/video/play/url.dart';
import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/pages/video/detail/introduction/widgets/menu_row.dart'; import 'package:pilipala/pages/video/detail/introduction/widgets/menu_row.dart';
import 'package:pilipala/plugin/pl_player/index.dart'; import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
class HeaderControl extends StatefulWidget implements PreferredSizeWidget { class HeaderControl extends StatefulWidget implements PreferredSizeWidget {
@ -149,13 +150,14 @@ class _HeaderControlState extends State<HeaderControl> {
'当前解码格式 ${widget.videoDetailCtr!.currentDecodeFormats.description}', '当前解码格式 ${widget.videoDetailCtr!.currentDecodeFormats.description}',
style: subTitleStyle), style: subTitleStyle),
), ),
// ListTile( ListTile(
// onTap: () {}, onTap: () => {Get.back(), showSetRepeat()},
// dense: true, dense: true,
// enabled: false, leading: const Icon(Icons.repeat, size: 20),
// leading: const Icon(Icons.play_circle_outline, size: 20), title: Text('播放顺序', style: titleStyle),
// title: Text('播放设置', style: titleStyle), subtitle: Text(widget.controller!.playRepeat.description,
// ), style: subTitleStyle),
),
ListTile( ListTile(
onTap: () => {Get.back(), showSetDanmaku()}, onTap: () => {Get.back(), showSetDanmaku()},
dense: true, dense: true,
@ -704,6 +706,60 @@ class _HeaderControlState extends State<HeaderControl> {
); );
} }
/// 播放顺序
void showSetRepeat() async {
showModalBottomSheet(
context: context,
elevation: 0,
backgroundColor: Colors.transparent,
builder: (BuildContext context) {
return Container(
width: double.infinity,
height: 250,
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
margin: const EdgeInsets.all(12),
child: Column(
children: [
SizedBox(
height: 45,
child: Center(child: Text('选择播放顺序', style: titleStyle))),
Expanded(
child: Material(
child: ListView(
children: [
for (var i in PlayRepeat.values) ...[
ListTile(
onTap: () {
widget.controller!.setPlayRepeat(i);
Get.back();
},
dense: true,
contentPadding:
const EdgeInsets.only(left: 20, right: 20),
title: Text(i.description),
trailing: widget.controller!.playRepeat == i
? Icon(
Icons.done,
color: Theme.of(context).colorScheme.primary,
)
: const SizedBox(),
)
],
],
),
),
),
],
),
);
},
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final _ = widget.controller!; final _ = widget.controller!;

View File

@ -99,8 +99,6 @@ class WebviewController extends GetxController {
HomeController homeCtr = Get.find<HomeController>(); HomeController homeCtr = Get.find<HomeController>();
homeCtr.updateLoginStatus(true); homeCtr.updateLoginStatus(true);
homeCtr.userFace.value = result['data'].face; homeCtr.userFace.value = result['data'].face;
MediaController mediaCtr = Get.find<MediaController>();
mediaCtr.mid = result['data'].mid;
await LoginUtils.refreshLoginStatus(true); await LoginUtils.refreshLoginStatus(true);
} catch (err) { } catch (err) {
SmartDialog.show(builder: (context) { SmartDialog.show(builder: (context) {

View File

@ -13,6 +13,7 @@ import 'package:media_kit_video/media_kit_video.dart';
import 'package:ns_danmaku/ns_danmaku.dart'; import 'package:ns_danmaku/ns_danmaku.dart';
import 'package:pilipala/http/video.dart'; import 'package:pilipala/http/video.dart';
import 'package:pilipala/plugin/pl_player/index.dart'; import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:screen_brightness/screen_brightness.dart'; import 'package:screen_brightness/screen_brightness.dart';
@ -209,6 +210,9 @@ class PlPlayerController {
late double fontSizeVal; late double fontSizeVal;
late double danmakuSpeedVal; late double danmakuSpeedVal;
// 播放顺序相关
PlayRepeat playRepeat = PlayRepeat.pause;
// 添加一个私有构造函数 // 添加一个私有构造函数
PlPlayerController._() { PlPlayerController._() {
_videoType = videoType; _videoType = videoType;
@ -226,6 +230,12 @@ class PlPlayerController {
// 弹幕速度 // 弹幕速度
danmakuSpeedVal = danmakuSpeedVal =
localCache.get(LocalCacheKey.danmakuSpeed, defaultValue: 4.0); localCache.get(LocalCacheKey.danmakuSpeed, defaultValue: 4.0);
playRepeat = PlayRepeat.values.toList().firstWhere(
(e) =>
e.value ==
videoStorage.get(VideoBoxKey.playRepeat,
defaultValue: PlayRepeat.pause.value),
);
// _playerEventSubs = onPlayerStatusChanged.listen((PlayerStatus status) { // _playerEventSubs = onPlayerStatusChanged.listen((PlayerStatus status) {
// if (status == PlayerStatus.playing) { // if (status == PlayerStatus.playing) {
// WakelockPlus.enable(); // WakelockPlus.enable();
@ -902,6 +912,11 @@ class PlPlayerController {
} }
} }
setPlayRepeat(PlayRepeat type) {
playRepeat = type;
videoStorage.put(VideoBoxKey.playRepeat, type.value);
}
Future<void> dispose({String type = 'single'}) async { Future<void> dispose({String type = 'single'}) async {
// 每次减1最后销毁 // 每次减1最后销毁
if (type == 'single' && playerCount.value > 1) { if (type == 'single' && playerCount.value > 1) {

View File

@ -0,0 +1,25 @@
enum PlayRepeat {
pause,
listOrder,
singleCycle,
listCycle,
}
extension PlayRepeatExtension on PlayRepeat {
static final List<String> _descList = [
'播完暂停',
'顺序播放',
'单个循环',
'列表循环',
];
get description => _descList[index];
static final List<double> _valueList = [
1,
2,
3,
4,
];
get value => _valueList[index];
get defaultValue => _valueList[1];
}

View File

@ -18,6 +18,7 @@ import 'package:pilipala/pages/html/index.dart';
import 'package:pilipala/pages/later/index.dart'; import 'package:pilipala/pages/later/index.dart';
import 'package:pilipala/pages/liveRoom/view.dart'; import 'package:pilipala/pages/liveRoom/view.dart';
import 'package:pilipala/pages/member/index.dart'; import 'package:pilipala/pages/member/index.dart';
import 'package:pilipala/pages/member_search/index.dart';
import 'package:pilipala/pages/preview/index.dart'; import 'package:pilipala/pages/preview/index.dart';
import 'package:pilipala/pages/search/index.dart'; import 'package:pilipala/pages/search/index.dart';
import 'package:pilipala/pages/searchResult/index.dart'; import 'package:pilipala/pages/searchResult/index.dart';
@ -35,6 +36,8 @@ import 'package:pilipala/pages/setting/index.dart';
import 'package:pilipala/pages/media/index.dart'; import 'package:pilipala/pages/media/index.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import '../pages/history_search/index.dart';
Box setting = GStrorage.setting; Box setting = GStrorage.setting;
bool iosTransition = bool iosTransition =
setting.get(SettingBoxKey.iosTransition, defaultValue: false); setting.get(SettingBoxKey.iosTransition, defaultValue: false);
@ -42,7 +45,7 @@ bool iosTransition =
class Routes { class Routes {
static final List<GetPage> getPages = [ static final List<GetPage> getPages = [
// 首页(推荐) // 首页(推荐)
CustomGetPage(name: '/', page: () => const HomePage()), CustomGetPage(name: '/', page: () => HomePage()),
// 热门 // 热门
CustomGetPage(name: '/hot', page: () => const HotPage()), CustomGetPage(name: '/hot', page: () => const HotPage()),
// 视频详情 // 视频详情
@ -86,6 +89,7 @@ class Routes {
CustomGetPage(name: '/liveRoom', page: () => const LiveRoomPage()), CustomGetPage(name: '/liveRoom', page: () => const LiveRoomPage()),
// 用户中心 // 用户中心
CustomGetPage(name: '/member', page: () => const MemberPage()), CustomGetPage(name: '/member', page: () => const MemberPage()),
CustomGetPage(name: '/memberSearch', page: () => const MemberSearchPage()),
// 二级回复 // 二级回复
CustomGetPage( CustomGetPage(
name: '/replyReply', page: () => const VideoReplyReplyPanel()), name: '/replyReply', page: () => const VideoReplyReplyPanel()),
@ -110,6 +114,9 @@ class Routes {
CustomGetPage(name: '/about', page: () => const AboutPage()), CustomGetPage(name: '/about', page: () => const AboutPage()),
// //
CustomGetPage(name: '/htmlRender', page: () => const HtmlRenderPage()), CustomGetPage(name: '/htmlRender', page: () => const HtmlRenderPage()),
// 历史记录搜索
CustomGetPage(
name: '/historySearch', page: () => const HistorySearchPage()),
]; ];
} }

View File

@ -119,7 +119,7 @@ class PiliSchame {
arguments: { arguments: {
'pic': bangumiDetail.cover, 'pic': bangumiDetail.cover,
'heroTag': heroTag, 'heroTag': heroTag,
'videoType': SearchType.media_bangumi, // 'videoType': SearchType.media_bangumi,
}, },
), ),
); );

View File

@ -1,8 +1,6 @@
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/pages/dynamics/index.dart';
import 'package:pilipala/pages/home/index.dart'; import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/media/index.dart';
import 'package:pilipala/pages/mine/index.dart'; import 'package:pilipala/pages/mine/index.dart';
class LoginUtils { class LoginUtils {
@ -17,12 +15,6 @@ class LoginUtils {
MineController mineCtr = Get.find<MineController>(); MineController mineCtr = Get.find<MineController>();
mineCtr.userLogin.value = status; mineCtr.userLogin.value = status;
DynamicsController dynamicsCtr = Get.find<DynamicsController>();
dynamicsCtr.userLogin.value = status;
MediaController mediaCtr = Get.find<MediaController>();
mediaCtr.userLogin.value = status;
} catch (err) { } catch (err) {
SmartDialog.showToast('refreshLoginStatus error: ${err.toString()}'); SmartDialog.showToast('refreshLoginStatus error: ${err.toString()}');
} }

View File

@ -156,4 +156,6 @@ class VideoBoxKey {
static const String videoBrightness = 'videoBrightness'; static const String videoBrightness = 'videoBrightness';
// 倍速 // 倍速
static const String videoSpeed = 'videoSpeed'; static const String videoSpeed = 'videoSpeed';
// 播放顺序
static const String playRepeat = 'playRepeat';
} }