web archive

Signed-off-by: dom <githubaccount56556@proton.me>
This commit is contained in:
dom
2026-03-23 18:28:38 +08:00
parent 2220372e4f
commit b4b3764e5f
39 changed files with 1005 additions and 306 deletions

View File

@@ -22,7 +22,7 @@ import 'package:intl/intl.dart';
// 视频卡片 - 垂直布局 // 视频卡片 - 垂直布局
class VideoCardV extends StatelessWidget { class VideoCardV extends StatelessWidget {
final BaseRecVideoItemModel videoItem; final BaseRcmdVideoItemModel videoItem;
final VoidCallback? onRemove; final VoidCallback? onRemove;
const VideoCardV({ const VideoCardV({
@@ -229,7 +229,7 @@ class VideoCardV extends StatelessWidget {
value: videoItem.stat.danmu, value: videoItem.stat.danmu,
), ),
], ],
if (videoItem is RecVideoItemModel) ...[ if (videoItem is RcmdVideoItemModel) ...[
const Spacer(), const Spacer(),
Text.rich( Text.rich(
maxLines: 1, maxLines: 1,
@@ -248,7 +248,7 @@ class VideoCardV extends StatelessWidget {
const SizedBox(width: 2), const SizedBox(width: 2),
], ],
// deprecated // deprecated
// else if (videoItem is RecVideoItemAppModel && // else if (videoItem is RcmdVideoItemAppModel &&
// videoItem.desc != null && // videoItem.desc != null &&
// videoItem.desc!.contains(' · ')) ...[ // videoItem.desc!.contains(' · ')) ...[
// const Spacer(), // const Spacer(),

View File

@@ -135,7 +135,7 @@ class VideoPopupMenu extends StatelessWidget {
SmartDialog.showToast("请退出账号后重新登录"); SmartDialog.showToast("请退出账号后重新登录");
return; return;
} }
if (videoItem case final RecVideoItemAppModel item) { if (videoItem case final RcmdVideoItemAppModel item) {
ThreePoint? tp = item.threePoint; ThreePoint? tp = item.threePoint;
if (tp == null) { if (tp == null) {
SmartDialog.showToast("未能获取threePoint"); SmartDialog.showToast("未能获取threePoint");

View File

@@ -649,14 +649,9 @@ abstract final class Api {
/// mid /// mid
static const getMemberViewApi = '/x/space/upstat'; static const getMemberViewApi = '/x/space/upstat';
/// 查询某个专栏 static const seasonArchives = '/x/polymer/web-space/seasons_archives_list';
/// mid
/// season_id static const seriesArchives = '/x/series/archives';
/// sort_reverse
/// page_num
/// page_size
static const getSeasonDetailApi =
'/x/polymer/web-space/seasons_archives_list';
/// 获取未读动态数 /// 获取未读动态数
static const getUnreadDynamic = '/x/web-interface/dynamic/entrance'; static const getUnreadDynamic = '/x/web-interface/dynamic/entrance';

3
lib/http/error_msg.dart Normal file
View File

@@ -0,0 +1,3 @@
const errorMsg = {
-352: '风控校验失败,请检查登录状态',
};

View File

@@ -1,4 +1,5 @@
import 'package:PiliPlus/http/api.dart'; import 'package:PiliPlus/http/api.dart';
import 'package:PiliPlus/http/error_msg.dart';
import 'package:PiliPlus/http/init.dart'; import 'package:PiliPlus/http/init.dart';
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models_new/follow/data.dart'; import 'package:PiliPlus/models_new/follow/data.dart';
@@ -23,7 +24,7 @@ abstract final class FanHttp {
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return Success(FollowData.fromJson(res.data['data'])); return Success(FollowData.fromJson(res.data['data']));
} else { } else {
return Error(res.data['message']); return Error(errorMsg[res.data['code']] ?? res.data['message']);
} }
} }
} }

View File

@@ -1,4 +1,5 @@
import 'package:PiliPlus/http/api.dart'; import 'package:PiliPlus/http/api.dart';
import 'package:PiliPlus/http/error_msg.dart';
import 'package:PiliPlus/http/init.dart'; import 'package:PiliPlus/http/init.dart';
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models_new/follow/data.dart'; import 'package:PiliPlus/models_new/follow/data.dart';
@@ -23,7 +24,7 @@ abstract final class FollowHttp {
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return Success(FollowData.fromJson(res.data['data'])); return Success(FollowData.fromJson(res.data['data']));
} else { } else {
return Error(res.data['message']); return Error(errorMsg[res.data['code']] ?? res.data['message']);
} }
} }
} }

View File

@@ -4,9 +4,14 @@ import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/http/api.dart'; import 'package:PiliPlus/http/api.dart';
import 'package:PiliPlus/http/browser_ua.dart'; import 'package:PiliPlus/http/browser_ua.dart';
import 'package:PiliPlus/http/constants.dart'; import 'package:PiliPlus/http/constants.dart';
import 'package:PiliPlus/http/error_msg.dart';
import 'package:PiliPlus/http/init.dart'; import 'package:PiliPlus/http/init.dart';
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/common/member/archive_order_type_app.dart';
import 'package:PiliPlus/models/common/member/archive_order_type_web.dart';
import 'package:PiliPlus/models/common/member/archive_sort_type_app.dart';
import 'package:PiliPlus/models/common/member/contribute_type.dart'; import 'package:PiliPlus/models/common/member/contribute_type.dart';
import 'package:PiliPlus/models/common/member/web_ss_type.dart';
import 'package:PiliPlus/models/dynamics/result.dart'; import 'package:PiliPlus/models/dynamics/result.dart';
import 'package:PiliPlus/models/member/info.dart'; import 'package:PiliPlus/models/member/info.dart';
import 'package:PiliPlus/models/member/tags.dart'; import 'package:PiliPlus/models/member/tags.dart';
@@ -14,6 +19,7 @@ import 'package:PiliPlus/models_new/follow/data.dart';
import 'package:PiliPlus/models_new/follow/list.dart'; import 'package:PiliPlus/models_new/follow/list.dart';
import 'package:PiliPlus/models_new/member/coin_like_arc/data.dart'; import 'package:PiliPlus/models_new/member/coin_like_arc/data.dart';
import 'package:PiliPlus/models_new/member/search_archive/data.dart'; import 'package:PiliPlus/models_new/member/search_archive/data.dart';
import 'package:PiliPlus/models_new/member/season_web/data.dart';
import 'package:PiliPlus/models_new/member_card_info/data.dart'; import 'package:PiliPlus/models_new/member_card_info/data.dart';
import 'package:PiliPlus/models_new/space/space/data.dart'; import 'package:PiliPlus/models_new/space/space/data.dart';
import 'package:PiliPlus/models_new/space/space_archive/data.dart'; import 'package:PiliPlus/models_new/space/space_archive/data.dart';
@@ -113,8 +119,8 @@ abstract final class MemberHttp {
required ContributeType type, required ContributeType type,
required int? mid, required int? mid,
String? aid, String? aid,
String? order, ArchiveOrderTypeApp? order,
String? sort, ArchiveSortTypeApp? sort,
int? pn, int? pn,
int? next, int? next,
int? seasonId, int? seasonId,
@@ -135,9 +141,9 @@ abstract final class MemberHttp {
'next': ?next, 'next': ?next,
'season_id': ?seasonId, 'season_id': ?seasonId,
'series_id': ?seriesId, 'series_id': ?seriesId,
'qn': type == ContributeType.video ? 80 : 32, 'qn': type == .video ? 80 : 32,
'order': ?order, 'order': ?order?.name,
'sort': ?sort, 'sort': ?sort?.name,
'include_cursor': ?includeCursor, 'include_cursor': ?includeCursor,
'statistics': Constants.statisticsApp, 'statistics': Constants.statisticsApp,
'vmid': mid, 'vmid': mid,
@@ -341,12 +347,12 @@ abstract final class MemberHttp {
static Future<LoadingState<SearchArchiveData>> searchArchive({ static Future<LoadingState<SearchArchiveData>> searchArchive({
required Object mid, required Object mid,
int tid = 0, // e.g. pugv: 196
int ps = 30, int ps = 30,
int tid = 0, required int pn,
int? pn,
String? keyword, String? keyword,
String order = 'pubdate', String? specialType, // e.g. 'charging'
bool orderAvoided = true, ArchiveOrderTypeWeb order = .pubdate,
}) async { }) async {
String dmImgStr = Utils.base64EncodeRandomString(16, 64); String dmImgStr = Utils.base64EncodeRandomString(16, 64);
String dmCoverImgStr = Utils.base64EncodeRandomString(32, 128); String dmCoverImgStr = Utils.base64EncodeRandomString(32, 128);
@@ -354,12 +360,13 @@ abstract final class MemberHttp {
'mid': mid, 'mid': mid,
'ps': ps, 'ps': ps,
'tid': tid, 'tid': tid,
'pn': ?pn, 'pn': pn,
'keyword': ?keyword, 'keyword': ?keyword,
'order': order, 'special_type': ?specialType,
'order': order.name,
'platform': 'web', 'platform': 'web',
'web_location': 1550101, 'web_location': 333.1387,
'order_avoided': orderAvoided, 'order_avoided': true,
'dm_img_list': '[]', 'dm_img_list': '[]',
'dm_img_str': dmImgStr, 'dm_img_str': dmImgStr,
'dm_cover_img_str': dmCoverImgStr, 'dm_cover_img_str': dmCoverImgStr,
@@ -379,10 +386,50 @@ abstract final class MemberHttp {
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return Success(SearchArchiveData.fromJson(res.data['data'])); return Success(SearchArchiveData.fromJson(res.data['data']));
} else { } else {
Map errMap = const { return Error(errorMsg[res.data['code']] ?? res.data['message']);
-352: '风控校验失败,请检查登录状态', }
}; }
return Error(errMap[res.data['code']] ?? res.data['message']);
static Future<LoadingState<SeasonWebData>> seasonSeriesWeb({
required WebSsType type,
required Object mid,
required Object id,
int ps = 30,
required int pn,
ArchiveSortTypeApp sort = .desc,
}) async {
final res = await Request().get(
type.api,
queryParameters: switch (type) {
.season => {
'mid': mid,
'season_id': id,
'sort_reverse': sort == .asc,
'page_size': ps,
'page_num': pn,
'web_location': 333.1387,
},
.series => {
'mid': mid,
'series_id': id,
'sort': sort.name,
'ps': ps,
'pn': pn,
'web_location': 333.1387,
},
},
options: Options(
headers: {
HttpHeaders.userAgentHeader: BrowserUa.pc,
HttpHeaders.refererHeader: '${HttpString.spaceBaseUrl}/$mid',
'origin': HttpString.spaceBaseUrl,
},
),
);
if (res.data['code'] == 0) {
return Success(SeasonWebData.fromJson(res.data['data']));
} else {
return Error(errorMsg[res.data['code']] ?? res.data['message']);
} }
} }
@@ -430,10 +477,7 @@ abstract final class MemberHttp {
return Error('$e\n\n$s'); return Error('$e\n\n$s');
} }
} else { } else {
Map errMap = const { return Error(errorMsg[res.data['code']] ?? res.data['message']);
-352: '风控校验失败,请检查登录状态',
};
return Error(errMap[res.data['code']] ?? res.data['message']);
} }
} }
@@ -544,7 +588,7 @@ abstract final class MemberHttp {
), ),
); );
} else { } else {
return Error(res.data['message']); return Error(errorMsg[res.data['code']] ?? res.data['message']);
} }
} }

View File

@@ -49,7 +49,7 @@ abstract final class VideoHttp {
static bool enableFilter = zoneRegExp.pattern.isNotEmpty; static bool enableFilter = zoneRegExp.pattern.isNotEmpty;
// 首页推荐视频 // 首页推荐视频
static Future<LoadingState<List<RecVideoItemModel>>> rcmdVideoList({ static Future<LoadingState<List<RcmdVideoItemModel>>> rcmdVideoList({
required int ps, required int ps,
required int freshIdx, required int freshIdx,
}) async { }) async {
@@ -66,13 +66,13 @@ abstract final class VideoHttp {
}), }),
); );
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
List<RecVideoItemModel> list = <RecVideoItemModel>[]; List<RcmdVideoItemModel> list = <RcmdVideoItemModel>[];
for (final i in res.data['data']['item']) { for (final i in res.data['data']['item']) {
//过滤掉live与ad以及拉黑用户 //过滤掉live与ad以及拉黑用户
if (i['goto'] == 'av' && if (i['goto'] == 'av' &&
(i['owner'] != null && (i['owner'] != null &&
!GlobalData().blackMids.contains(i['owner']['mid']))) { !GlobalData().blackMids.contains(i['owner']['mid']))) {
RecVideoItemModel videoItem = RecVideoItemModel.fromJson(i); RcmdVideoItemModel videoItem = RcmdVideoItemModel.fromJson(i);
if (!RecommendFilter.filter(videoItem)) { if (!RecommendFilter.filter(videoItem)) {
list.add(videoItem); list.add(videoItem);
} }
@@ -85,7 +85,7 @@ abstract final class VideoHttp {
} }
// 添加额外的loginState变量模拟未登录状态 // 添加额外的loginState变量模拟未登录状态
static Future<LoadingState<List<RecVideoItemAppModel>>> rcmdVideoListApp({ static Future<LoadingState<List<RcmdVideoItemAppModel>>> rcmdVideoListApp({
required int freshIdx, required int freshIdx,
}) async { }) async {
final params = { final params = {
@@ -139,7 +139,7 @@ abstract final class VideoHttp {
), ),
); );
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
List<RecVideoItemAppModel> list = <RecVideoItemAppModel>[]; List<RcmdVideoItemAppModel> list = <RcmdVideoItemAppModel>[];
for (final i in res.data['data']['items']) { for (final i in res.data['data']['items']) {
// 屏蔽推广和拉黑用户 // 屏蔽推广和拉黑用户
if (i['card_goto'] != 'ad_av' && if (i['card_goto'] != 'ad_av' &&
@@ -152,7 +152,7 @@ abstract final class VideoHttp {
zoneRegExp.hasMatch(i['args']['tname'])) { zoneRegExp.hasMatch(i['args']['tname'])) {
continue; continue;
} }
RecVideoItemAppModel videoItem = RecVideoItemAppModel.fromJson(i); RcmdVideoItemAppModel videoItem = RcmdVideoItemAppModel.fromJson(i);
if (!RecommendFilter.filter(videoItem)) { if (!RecommendFilter.filter(videoItem)) {
list.add(videoItem); list.add(videoItem);
} }

View File

@@ -0,0 +1,11 @@
import 'package:PiliPlus/models/common/enum_with_label.dart';
enum ArchiveOrderTypeApp with EnumWithLabel {
pubdate('最新发布'),
click('最多播放'),
;
@override
final String label;
const ArchiveOrderTypeApp(this.label);
}

View File

@@ -0,0 +1,12 @@
import 'package:PiliPlus/models/common/enum_with_label.dart';
enum ArchiveOrderTypeWeb with EnumWithLabel {
pubdate('最新发布'),
click('最多播放'),
stow('最多收藏'),
;
@override
final String label;
const ArchiveOrderTypeWeb(this.label);
}

View File

@@ -0,0 +1,11 @@
import 'package:PiliPlus/models/common/enum_with_label.dart';
enum ArchiveSortTypeApp with EnumWithLabel {
desc('默认'),
asc('倒序'),
;
@override
final String label;
const ArchiveSortTypeApp(this.label);
}

View File

@@ -0,0 +1,10 @@
import 'package:PiliPlus/http/api.dart';
enum WebSsType {
season(Api.seasonArchives),
series(Api.seriesArchives),
;
final String api;
const WebSsType(this.api);
}

View File

@@ -3,14 +3,14 @@ import 'package:PiliPlus/models/model_video.dart';
import 'package:PiliPlus/utils/id_utils.dart'; import 'package:PiliPlus/utils/id_utils.dart';
import 'package:PiliPlus/utils/num_utils.dart'; import 'package:PiliPlus/utils/num_utils.dart';
class RecVideoItemAppModel extends BaseRecVideoItemModel { class RcmdVideoItemAppModel extends BaseRcmdVideoItemModel {
int? get id => aid; int? get id => aid;
String? talkBack; String? talkBack;
String? cardType; String? cardType;
ThreePoint? threePoint; ThreePoint? threePoint;
RecVideoItemAppModel.fromJson(Map<String, dynamic> json) { RcmdVideoItemAppModel.fromJson(Map<String, dynamic> json) {
aid = json['player_args']?['aid'] ?? int.tryParse(json['param'] ?? '0'); aid = json['player_args']?['aid'] ?? int.tryParse(json['param'] ?? '0');
bvid = json['bvid'] ?? IdUtils.av2bv(aid!); bvid = json['bvid'] ?? IdUtils.av2bv(aid!);
cid = json['player_args']?['cid']; cid = json['player_args']?['cid'];

View File

@@ -5,7 +5,7 @@ import 'package:PiliPlus/models_new/video/video_detail/dimension.dart';
import 'package:PiliPlus/pages/common/multi_select/base.dart'; import 'package:PiliPlus/pages/common/multi_select/base.dart';
// 稍后再看, 排行榜等网页返回也使用该类 // 稍后再看, 排行榜等网页返回也使用该类
class HotVideoItemModel extends BaseRecVideoItemModel with MultiSelectData { class HotVideoItemModel extends BaseRcmdVideoItemModel with MultiSelectData {
int? videos; int? videos;
int? tid; int? tid;
String? tname; String? tname;

View File

@@ -1,7 +1,7 @@
import 'package:PiliPlus/models/model_owner.dart'; import 'package:PiliPlus/models/model_owner.dart';
import 'package:PiliPlus/models/model_video.dart'; import 'package:PiliPlus/models/model_video.dart';
abstract class BaseRecVideoItemModel extends BaseVideoItemModel { abstract class BaseRcmdVideoItemModel extends BaseVideoItemModel {
String? goto; String? goto;
String? uri; String? uri;
String? rcmdReason; String? rcmdReason;
@@ -11,8 +11,8 @@ abstract class BaseRecVideoItemModel extends BaseVideoItemModel {
String? pgcBadge; String? pgcBadge;
} }
class RecVideoItemModel extends BaseRecVideoItemModel { class RcmdVideoItemModel extends BaseRcmdVideoItemModel {
RecVideoItemModel.fromJson(Map<String, dynamic> json) { RcmdVideoItemModel.fromJson(Map<String, dynamic> json) {
aid = json["id"]; aid = json["id"];
bvid = json["bvid"]; bvid = json["bvid"];
cid = json["cid"]; cid = json["cid"];

View File

@@ -1,22 +1,13 @@
import 'package:PiliPlus/models_new/member/search_archive/episodic_button.dart';
import 'package:PiliPlus/models_new/member/search_archive/list.dart'; import 'package:PiliPlus/models_new/member/search_archive/list.dart';
import 'package:PiliPlus/models_new/member/search_archive/page.dart'; import 'package:PiliPlus/models_new/member/search_archive/page.dart';
class SearchArchiveData { class SearchArchiveData {
SearchArchiveList? list; SearchArchiveList? list;
Page? page; Page? page;
EpisodicButton? episodicButton;
bool? isRisk;
int? gaiaResType;
dynamic gaiaData;
SearchArchiveData({ SearchArchiveData({
this.list, this.list,
this.page, this.page,
this.episodicButton,
this.isRisk,
this.gaiaResType,
this.gaiaData,
}); });
factory SearchArchiveData.fromJson(Map<String, dynamic> json) => factory SearchArchiveData.fromJson(Map<String, dynamic> json) =>
@@ -27,13 +18,5 @@ class SearchArchiveData {
page: json['page'] == null page: json['page'] == null
? null ? null
: Page.fromJson(json['page'] as Map<String, dynamic>), : Page.fromJson(json['page'] as Map<String, dynamic>),
episodicButton: json['episodic_button'] == null
? null
: EpisodicButton.fromJson(
json['episodic_button'] as Map<String, dynamic>,
),
isRisk: json['is_risk'] as bool?,
gaiaResType: json['gaia_res_type'] as int?,
gaiaData: json['gaia_data'] as dynamic,
); );
} }

View File

@@ -1,13 +0,0 @@
class EpisodicButton {
String? text;
String? uri;
EpisodicButton({this.text, this.uri});
factory EpisodicButton.fromJson(Map<String, dynamic> json) {
return EpisodicButton(
text: json['text'] as String?,
uri: json['uri'] as String?,
);
}
}

View File

@@ -1,14 +1,26 @@
import 'package:PiliPlus/models_new/member/search_archive/slist.dart';
import 'package:PiliPlus/models_new/member/search_archive/vlist.dart'; import 'package:PiliPlus/models_new/member/search_archive/vlist.dart';
class SearchArchiveList { class SearchArchiveList {
List<ListTag>? tags;
List<VListItemModel>? vlist; List<VListItemModel>? vlist;
SearchArchiveList({this.vlist}); SearchArchiveList.fromJson(Map<String, dynamic> json) {
vlist = (json['vlist'] as List<dynamic>?)
factory SearchArchiveList.fromJson(Map<String, dynamic> json) => ?.map((e) => VListItemModel.fromJson(e as Map<String, dynamic>))
SearchArchiveList( .toList();
vlist: (json['vlist'] as List<dynamic>?) tags = (json['slist'] as List<dynamic>?)
?.map((e) => VListItemModel.fromJson(e as Map<String, dynamic>)) ?.map((e) => ListTag.fromJson(e as Map<String, dynamic>))
.toList(), .toList();
); (json['tlist'] as Map<String, dynamic>?)?.forEach((k, v) {
if (k == '196') {
if (tags == null) {
tags = [ListTag.fromJson(v)];
} else {
tags!.add(ListTag.fromJson(v));
}
return;
}
});
}
} }

View File

@@ -0,0 +1,15 @@
class ListTag {
int? tid;
int? count;
String? name;
String? specialType;
ListTag({this.tid, this.count, this.name, this.specialType});
factory ListTag.fromJson(Map<String, dynamic> json) => ListTag(
tid: json['tid'] as int?,
count: json['count'] as int?,
name: json['name'] as String?,
specialType: json['special_type'] as String?,
);
}

View File

@@ -2,31 +2,16 @@ import 'package:PiliPlus/models/model_video.dart';
import 'package:PiliPlus/utils/duration_utils.dart'; import 'package:PiliPlus/utils/duration_utils.dart';
class VListItemModel extends BaseVideoItemModel { class VListItemModel extends BaseVideoItemModel {
int? comment;
int? typeid;
String? subtitle;
String? copyright;
int? review;
bool? hideClick;
bool? isChargingSrc;
VListItemModel.fromJson(Map<String, dynamic> json) { VListItemModel.fromJson(Map<String, dynamic> json) {
comment = json['comment'];
typeid = json['typeid'];
cover = json['pic']; cover = json['pic'];
subtitle = json['subtitle'];
desc = json['description']; desc = json['description'];
copyright = json['copyright'];
title = json['title']; title = json['title'];
review = json['review'];
pubdate = json['created']; pubdate = json['created'];
if (json['length'] != null) { if (json['length'] != null) {
duration = DurationUtils.parseDuration(json['length']); duration = DurationUtils.parseDuration(json['length']);
} }
aid = json['aid']; aid = json['aid'];
bvid = json['bvid']; bvid = json['bvid'];
hideClick = json['hide_click'];
isChargingSrc = json['is_charging_arc'];
stat = VListStat.fromJson(json); stat = VListStat.fromJson(json);
owner = VListOwner.fromJson(json); owner = VListOwner.fromJson(json);
} }

View File

@@ -0,0 +1,28 @@
import 'package:PiliPlus/models/model_video.dart';
class SeasonArchive extends BaseVideoItemModel {
SeasonArchive.fromJson(Map<String, dynamic> json) {
aid = json['aid'];
bvid = json['bvid'];
cover = json['pic'];
title = json['title'];
pubdate = json['pubdate'];
duration = json['duration'];
stat = ArchiveStat.fromJson(json['stat']);
owner = ArchiveOwner.fromJson(json);
}
}
class ArchiveOwner extends BaseOwner {
ArchiveOwner.fromJson(Map<String, dynamic> json) {
mid = json['upMid'];
name = '';
}
}
class ArchiveStat extends BaseStat {
ArchiveStat.fromJson(Map<String, dynamic> json) {
view = json['view'];
danmu = json['danmaku'];
}
}

View File

@@ -0,0 +1,18 @@
import 'package:PiliPlus/models_new/member/season_web/archive.dart';
import 'package:PiliPlus/models_new/member/season_web/page.dart';
class SeasonWebData {
List<SeasonArchive>? archives;
Page? page;
SeasonWebData({this.archives, this.page});
factory SeasonWebData.fromJson(Map<String, dynamic> json) => SeasonWebData(
archives: (json['archives'] as List<dynamic>?)
?.map((e) => SeasonArchive.fromJson(e as Map<String, dynamic>))
.toList(),
page: json['page'] == null
? null
: Page.fromJson(json['page'] as Map<String, dynamic>),
);
}

View File

@@ -0,0 +1,13 @@
class Page {
int? pageNum;
int? pageSize;
int? total;
Page({this.pageNum, this.pageSize, this.total});
factory Page.fromJson(Map<String, dynamic> json) => Page(
pageNum: json['page_num'] ?? json['num'],
pageSize: json['page_size'] ?? json['size'],
total: json['total'] as int?,
);
}

View File

@@ -43,6 +43,8 @@ class MemberController extends CommonDataController<SpaceData, SpaceData?>
bool? hasSeasonOrSeries; bool? hasSeasonOrSeries;
late bool hasCharge = false;
final fromViewAid = Get.parameters['from_view_aid']; final fromViewAid = Get.parameters['from_view_aid'];
final key = GlobalKey<ExtendedNestedScrollViewState>(); final key = GlobalKey<ExtendedNestedScrollViewState>();
@@ -58,6 +60,7 @@ class MemberController extends CommonDataController<SpaceData, SpaceData?>
final data = response.response; final data = response.response;
username = data.card?.name ?? ''; username = data.card?.name ?? '';
isFollowed = data.card?.relation?.isFollowed; isFollowed = data.card?.relation?.isFollowed;
hasCharge = (data.elec?.total ?? 0) > 0;
if (data.relation == -1) { if (data.relation == -1) {
relation.value = 128; relation.value = 128;
} else { } else {

View File

@@ -17,12 +17,15 @@ import 'package:PiliPlus/pages/member/controller.dart';
import 'package:PiliPlus/pages/member/widget/medal_wall.dart'; import 'package:PiliPlus/pages/member/widget/medal_wall.dart';
import 'package:PiliPlus/pages/member/widget/user_info_card.dart'; import 'package:PiliPlus/pages/member/widget/user_info_card.dart';
import 'package:PiliPlus/pages/member_cheese/view.dart'; import 'package:PiliPlus/pages/member_cheese/view.dart';
import 'package:PiliPlus/pages/member_contribute/controller.dart';
import 'package:PiliPlus/pages/member_contribute/view.dart'; import 'package:PiliPlus/pages/member_contribute/view.dart';
import 'package:PiliPlus/pages/member_dynamics/view.dart'; import 'package:PiliPlus/pages/member_dynamics/view.dart';
import 'package:PiliPlus/pages/member_favorite/view.dart'; import 'package:PiliPlus/pages/member_favorite/view.dart';
import 'package:PiliPlus/pages/member_home/view.dart'; import 'package:PiliPlus/pages/member_home/view.dart';
import 'package:PiliPlus/pages/member_pgc/view.dart'; import 'package:PiliPlus/pages/member_pgc/view.dart';
import 'package:PiliPlus/pages/member_shop/view.dart'; import 'package:PiliPlus/pages/member_shop/view.dart';
import 'package:PiliPlus/pages/member_video_web/archive/view.dart';
import 'package:PiliPlus/pages/member_video_web/season_series/view.dart';
import 'package:PiliPlus/utils/date_utils.dart'; import 'package:PiliPlus/utils/date_utils.dart';
import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart';
import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/utils.dart';
@@ -215,22 +218,35 @@ class _MemberPageState extends State<MemberPage> {
], ],
), ),
), ),
PopupMenuItem( if (_userController.hasCharge)
onTap: () => Get.toNamed( PopupMenuItem(
'/upowerRank', onTap: () => Get.toNamed(
parameters: { '/upowerRank',
'mid': _userController.mid.toString(), parameters: {
}, 'mid': _userController.mid.toString(),
},
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.electric_bolt, size: 19),
SizedBox(width: 10),
Text('充电排行榜'),
],
),
), ),
child: const Row( if (Get.isRegistered<MemberContributeCtr>(tag: _heroTag))
mainAxisSize: MainAxisSize.min, PopupMenuItem(
children: [ onTap: _toWebArchive,
Icon(Icons.electric_bolt, size: 19), child: const Row(
SizedBox(width: 10), mainAxisSize: MainAxisSize.min,
Text('充电排行榜'), children: [
], Icon(Icons.extension_outlined, size: 19),
SizedBox(width: 10),
Text('网页投稿'),
],
),
), ),
),
if (_userController.account.isLogin) if (_userController.account.isLogin)
if (_userController.mid == _userController.account.mid) ...[ if (_userController.mid == _userController.account.mid) ...[
if ((_userController if ((_userController
@@ -456,4 +472,29 @@ class _MemberPageState extends State<MemberPage> {
res.toast(); res.toast();
} }
} }
void _toWebArchive() {
try {
final ctr = Get.find<MemberContributeCtr>(tag: _heroTag);
final item = ctr.items?[ctr.tabController?.index ?? 0];
if (item != null) {
final id = item.seasonId ?? item.seriesId;
if (id != null) {
MemberSSWeb.toMemberSSWeb(
type: item.seasonId != null ? .season : .series,
id: id,
mid: _mid,
name: _userController.username ?? '',
);
return;
}
}
MemberVideoWeb.toMemberVideoWeb(
mid: _mid,
name: _userController.username ?? '',
);
} catch (e) {
SmartDialog.showToast(e.toString());
}
}
} }

View File

@@ -62,7 +62,6 @@ class MemberSearchChildController extends CommonListController {
mid: controller.mid, mid: controller.mid,
pn: page, pn: page,
keyword: controller.editingController.text, keyword: controller.editingController.text,
order: 'pubdate',
), ),
MemberSearchType.dynamic => MemberHttp.dynSearch( MemberSearchType.dynamic => MemberHttp.dynSearch(
mid: controller.mid, mid: controller.mid,

View File

@@ -71,47 +71,43 @@ class _MemberSearchChildPageState extends State<MemberSearchChildPage>
Loading() => _buildLoading, Loading() => _buildLoading,
Success(:final response) => Success(:final response) =>
response != null && response.isNotEmpty response != null && response.isNotEmpty
? Builder( ? switch (widget.searchType) {
builder: (context) { MemberSearchType.archive => SliverGrid.builder(
return switch (widget.searchType) { gridDelegate: gridDelegate,
MemberSearchType.archive => SliverGrid.builder( itemBuilder: (context, index) {
gridDelegate: gridDelegate, if (index == response.length - 1) {
itemBuilder: (context, index) { _controller.onLoadMore();
if (index == response.length - 1) { }
_controller.onLoadMore(); return VideoCardH(
} videoItem: response[index],
return VideoCardH( );
videoItem: response[index], },
); itemCount: response.length,
}, ),
itemCount: response.length, MemberSearchType.dynamic =>
), GlobalData().dynamicsWaterfallFlow
MemberSearchType.dynamic => ? SliverWaterfallFlow(
GlobalData().dynamicsWaterfallFlow gridDelegate: dynGridDelegate,
? SliverWaterfallFlow( delegate: SliverChildBuilderDelegate(
gridDelegate: dynGridDelegate, (_, index) {
delegate: SliverChildBuilderDelegate( if (index == response.length - 1) {
(_, index) { _controller.onLoadMore();
if (index == response.length - 1) { }
_controller.onLoadMore(); return DynamicPanel(item: response[index]);
} },
return DynamicPanel(item: response[index]); childCount: response.length,
}, ),
childCount: response.length, )
), : SliverList.builder(
) itemBuilder: (context, index) {
: SliverList.builder( if (index == response.length - 1) {
itemBuilder: (context, index) { _controller.onLoadMore();
if (index == response.length - 1) { }
_controller.onLoadMore(); return DynamicPanel(item: response[index]);
} },
return DynamicPanel(item: response[index]); itemCount: response.length,
}, ),
itemCount: response.length, }
),
};
},
)
: HttpError(onReload: _controller.onReload), : HttpError(onReload: _controller.onReload),
Error(:final errMsg) => HttpError( Error(:final errMsg) => HttpError(
errMsg: errMsg, errMsg: errMsg,

View File

@@ -2,6 +2,8 @@ import 'package:PiliPlus/common/widgets/scroll_physics.dart';
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/member.dart'; import 'package:PiliPlus/http/member.dart';
import 'package:PiliPlus/http/search.dart'; import 'package:PiliPlus/http/search.dart';
import 'package:PiliPlus/models/common/member/archive_order_type_app.dart';
import 'package:PiliPlus/models/common/member/archive_sort_type_app.dart';
import 'package:PiliPlus/models/common/member/contribute_type.dart'; import 'package:PiliPlus/models/common/member/contribute_type.dart';
import 'package:PiliPlus/models/common/video/source_type.dart'; import 'package:PiliPlus/models/common/video/source_type.dart';
import 'package:PiliPlus/models_new/space/space_archive/data.dart'; import 'package:PiliPlus/models_new/space/space_archive/data.dart';
@@ -23,17 +25,18 @@ class MemberVideoCtr
required this.seriesId, required this.seriesId,
this.username, this.username,
this.title, this.title,
}); }) : isVideo = type == .video;
ContributeType type; final ContributeType type;
final bool isVideo;
int? seasonId; int? seasonId;
int? seriesId; int? seriesId;
final int mid; final int mid;
late RxString order = 'pubdate'.obs; late ArchiveOrderTypeApp order = .pubdate;
late RxString sort = 'desc'.obs; late ArchiveSortTypeApp sort = .desc;
RxInt count = (-1).obs; int? count;
int? next; int? next;
Rx<EpisodicButton> episodicButton = EpisodicButton().obs; EpisodicButton? episodicButton;
final String? username; final String? username;
final String? title; final String? title;
@@ -65,7 +68,7 @@ class MemberVideoCtr
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
if (type == ContributeType.video) { if (isVideo) {
fromViewAid = Get.parameters['from_view_aid']; fromViewAid = Get.parameters['from_view_aid'];
} }
page = 0; page = 0;
@@ -78,24 +81,18 @@ class MemberVideoCtr
Success<SpaceArchiveData> response, Success<SpaceArchiveData> response,
) { ) {
final data = response.response; final data = response.response;
episodicButton episodicButton = data.episodicButton;
..value = data.episodicButton ?? EpisodicButton()
..refresh();
next = data.next; next = data.next;
if (page == 0 || isLoadPrevious) { if (page == 0 || isLoadPrevious) {
hasPrev = data.hasPrev; hasPrev = data.hasPrev;
} }
if (page == 0 || !isLoadPrevious) { if (page == 0 || !isLoadPrevious) {
if ((type == ContributeType.video if ((isVideo ? data.hasNext == false : data.next == 0) ||
? data.hasNext == false
: data.next == 0) ||
data.item.isNullOrEmpty) { data.item.isNullOrEmpty) {
isEnd = true; isEnd = true;
} }
} }
count.value = type == ContributeType.season count = type == .season ? data.item?.length : data.count;
? (data.item?.length ?? -1)
: (data.count ?? -1);
if (page != 0) { if (page != 0) {
if (loadingState.value case Success(:final response)) { if (loadingState.value case Success(:final response)) {
data.item ??= <SpaceArchiveItem>[]; data.item ??= <SpaceArchiveItem>[];
@@ -118,18 +115,18 @@ class MemberVideoCtr
MemberHttp.spaceArchive( MemberHttp.spaceArchive(
type: type, type: type,
mid: mid, mid: mid,
aid: type == ContributeType.video aid: isVideo
? isLoadPrevious ? isLoadPrevious
? firstAid ? firstAid
: lastAid : lastAid
: null, : null,
order: type == ContributeType.video ? order.value : null, order: isVideo ? order : null,
sort: type == ContributeType.video sort: isVideo
? isLoadPrevious ? isLoadPrevious
? 'asc' ? .asc
: null : null
: sort.value, : sort,
pn: type == ContributeType.charging ? page : null, pn: type == .charging ? page : null,
next: next, next: next,
seasonId: seasonId, seasonId: seasonId,
seriesId: seriesId, seriesId: seriesId,
@@ -138,17 +135,17 @@ class MemberVideoCtr
void queryBySort() { void queryBySort() {
if (isLoading) return; if (isLoading) return;
if (type == ContributeType.video) { if (isVideo) {
isLocating.value = false; isLocating.value = false;
order.value = order.value == 'pubdate' ? 'click' : 'pubdate'; order = order == .pubdate ? .click : .pubdate;
} else { } else {
sort.value = sort.value == 'desc' ? 'asc' : 'desc'; sort = sort == .desc ? .asc : .desc;
} }
onReload(); onReload();
} }
Future<void> toViewPlayAll() async { Future<void> toViewPlayAll() async {
final episodicButton = this.episodicButton.value; final episodicButton = this.episodicButton!;
if (episodicButton.text == '继续播放' && if (episodicButton.text == '继续播放' &&
episodicButton.uri?.isNotEmpty == true) { episodicButton.uri?.isNotEmpty == true) {
final params = Uri.parse(episodicButton.uri!).queryParameters; final params = Uri.parse(episodicButton.uri!).queryParameters;
@@ -167,7 +164,7 @@ class MemberVideoCtr
'oid': oid, 'oid': oid,
'favTitle': 'favTitle':
'$username: ${title ?? episodicButton.text ?? '播放全部'}', '$username: ${title ?? episodicButton.text ?? '播放全部'}',
if (seriesId == null) 'count': count.value, if (seriesId == null) 'count': ?count,
if (seasonId != null || seriesId != null) if (seasonId != null || seriesId != null)
'mediaType': params['page_type'], 'mediaType': params['page_type'],
'desc': params['desc'] == '1', 'desc': params['desc'] == '1',
@@ -190,9 +187,7 @@ class MemberVideoCtr
bool desc = seasonId != null ? false : true; bool desc = seasonId != null ? false : true;
desc = desc =
(seasonId != null || seriesId != null) && (seasonId != null || seriesId != null) &&
(type == ContributeType.video (isVideo ? order == .click : sort == .asc)
? order.value == 'click'
: sort.value == 'asc')
? !desc ? !desc
: desc; : desc;
PageUtils.toVideoPage( PageUtils.toVideoPage(
@@ -206,14 +201,13 @@ class MemberVideoCtr
'oid': IdUtils.bv2av(element.bvid!), 'oid': IdUtils.bv2av(element.bvid!),
'favTitle': 'favTitle':
'$username: ${title ?? episodicButton.text ?? '播放全部'}', '$username: ${title ?? episodicButton.text ?? '播放全部'}',
if (seriesId == null) 'count': count.value, if (seriesId == null) 'count': ?count,
if (seasonId != null || seriesId != null) if (seasonId != null || seriesId != null)
'mediaType': Uri.parse( 'mediaType': Uri.parse(
episodicButton.uri!, episodicButton.uri!,
).queryParameters['page_type'], ).queryParameters['page_type'],
'desc': desc, 'desc': desc,
if (type == ContributeType.video) if (isVideo) 'sortField': order == .click ? 2 : 1,
'sortField': order.value == 'click' ? 2 : 1,
}, },
); );
break; break;

View File

@@ -111,8 +111,7 @@ class _MemberVideoState extends State<MemberVideo>
], ],
), ),
); );
if (widget.type == ContributeType.video && if (_controller.isVideo && _controller.fromViewAid?.isNotEmpty == true) {
_controller.fromViewAid?.isNotEmpty == true) {
if (_index == null) { if (_index == null) {
_scrollController = _scrollController =
PrimaryScrollController.of(this.context) PrimaryScrollController.of(this.context)
@@ -174,85 +173,11 @@ class _MemberVideoState extends State<MemberVideo>
response != null && response.isNotEmpty response != null && response.isNotEmpty
? SliverMainAxisGroup( ? SliverMainAxisGroup(
slivers: [ slivers: [
SliverFloatingHeaderWidget( _buildHeader(theme),
backgroundColor: theme.colorScheme.surface,
child: Padding(
padding: const EdgeInsets.fromLTRB(14, 2.5, 8, 2.5),
child: Row(
children: [
Obx(
() {
final count = _controller.count.value;
return Text(
count != -1 ? '$count视频' : '',
style: const TextStyle(fontSize: 13),
);
},
),
Obx(
() {
final episodicButton =
_controller.episodicButton.value;
return episodicButton.uri?.isNotEmpty == true
? Padding(
padding: EdgeInsets.only(
left: _controller.count.value != -1
? 6
: 0,
),
child: TextButton.icon(
style: Style.buttonStyle,
onPressed: _controller.toViewPlayAll,
icon: Icon(
Icons.play_circle_outline_rounded,
size: 16,
color: theme.colorScheme.secondary,
),
label: Text(
episodicButton.text ?? '播放全部',
style: TextStyle(
fontSize: 13,
color: theme.colorScheme.secondary,
),
),
),
)
: const SizedBox.shrink();
},
),
const Spacer(),
TextButton.icon(
style: Style.buttonStyle,
onPressed: _controller.queryBySort,
icon: Icon(
Icons.sort,
size: 16,
color: theme.colorScheme.secondary,
),
label: Obx(
() => Text(
widget.type == ContributeType.video
? _controller.order.value == 'pubdate'
? '最新发布'
: '最多播放'
: _controller.sort.value == 'desc'
? '默认'
: '倒序',
style: TextStyle(
fontSize: 13,
color: theme.colorScheme.secondary,
),
),
),
),
],
),
),
),
SliverGrid.builder( SliverGrid.builder(
gridDelegate: gridDelegate, gridDelegate: gridDelegate,
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (widget.type != ContributeType.season && if (widget.type != .season &&
index == response.length - 1) { index == response.length - 1) {
_controller.onLoadMore(); _controller.onLoadMore();
} }
@@ -272,4 +197,79 @@ class _MemberVideoState extends State<MemberVideo>
), ),
}; };
} }
Widget _buildHeader(ThemeData theme) {
return SliverFloatingHeaderWidget(
backgroundColor: theme.colorScheme.surface,
child: Padding(
padding: const EdgeInsets.fromLTRB(14, 2.5, 8, 2.5),
child: Row(
children: [
?_buildCount(),
?_buildEpisodeBtn(theme),
const Spacer(),
_buildSortBtn(theme),
],
),
),
);
}
Widget? _buildCount() {
final count = _controller.count;
if (count != null) {
return Text(
'$count视频',
style: const TextStyle(fontSize: 13),
);
}
return null;
}
Widget? _buildEpisodeBtn(ThemeData theme) {
final episodicButton = _controller.episodicButton;
if (episodicButton?.uri?.isNotEmpty ?? false) {
return Padding(
padding: EdgeInsets.only(
left: _controller.count != null ? 6 : 0,
),
child: TextButton.icon(
style: Style.buttonStyle,
onPressed: _controller.toViewPlayAll,
icon: Icon(
Icons.play_circle_outline_rounded,
size: 16,
color: theme.colorScheme.secondary,
),
label: Text(
episodicButton?.text ?? '播放全部',
style: TextStyle(
fontSize: 13,
color: theme.colorScheme.secondary,
),
),
),
);
}
return null;
}
Widget _buildSortBtn(ThemeData theme) {
return TextButton.icon(
style: Style.buttonStyle,
onPressed: _controller.queryBySort,
icon: Icon(
Icons.sort,
size: 16,
color: theme.colorScheme.secondary,
),
label: Text(
_controller.isVideo ? _controller.order.label : _controller.sort.label,
style: TextStyle(
fontSize: 13,
color: theme.colorScheme.secondary,
),
),
);
}
} }

View File

@@ -0,0 +1,63 @@
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/member.dart';
import 'package:PiliPlus/models/common/member/archive_order_type_web.dart';
import 'package:PiliPlus/models_new/member/search_archive/data.dart';
import 'package:PiliPlus/models_new/member/search_archive/slist.dart';
import 'package:PiliPlus/models_new/member/search_archive/vlist.dart';
import 'package:PiliPlus/pages/member_video_web/base/controller.dart';
import 'package:get/get.dart';
class MemberVideoWebCtr
extends
BaseVideoWebCtr<
SearchArchiveData,
VListItemModel,
ArchiveOrderTypeWeb
> {
int? _totalCount;
@override
final Rx<ArchiveOrderTypeWeb> order = Rx(.pubdate);
int tid = 0;
String? specialType;
List<ListTag>? tags;
@override
List<VListItemModel>? getDataList(SearchArchiveData response) {
return response.list?.vlist;
}
@override
bool customHandleResponse(
bool isRefresh,
Success<SearchArchiveData> response,
) {
if (isRefresh) {
final data = response.response;
if (data.page?.count case final count?) {
if (tid == 0 && specialType == null) {
_totalCount = count;
}
this.count = count;
totalPage = (count / ps).ceil();
}
final tags = data.list?.tags;
if (tags?.isNotEmpty ?? false) {
this.tags = tags!
..insert(0, ListTag(tid: 0, name: '全部类型', count: _totalCount));
}
}
return false;
}
@override
Future<LoadingState<SearchArchiveData>> customGetData() =>
MemberHttp.searchArchive(
mid: mid,
ps: ps,
pn: page,
order: order.value,
tid: tid,
specialType: specialType,
);
}

View File

@@ -0,0 +1,87 @@
import 'package:PiliPlus/common/widgets/self_sized_horizontal_list.dart';
import 'package:PiliPlus/common/widgets/sliver/sliver_pinned_header.dart';
import 'package:PiliPlus/models/common/member/archive_order_type_web.dart';
import 'package:PiliPlus/models_new/member/search_archive/data.dart';
import 'package:PiliPlus/models_new/member/search_archive/vlist.dart';
import 'package:PiliPlus/pages/member_video_web/archive/controller.dart';
import 'package:PiliPlus/pages/member_video_web/base/view.dart';
import 'package:PiliPlus/pages/search/widgets/search_text.dart';
import 'package:PiliPlus/utils/grid.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class MemberVideoWeb extends StatefulWidget {
const MemberVideoWeb({super.key});
@override
State<MemberVideoWeb> createState() => _MemberVideoWebState();
static Future<void>? toMemberVideoWeb({
required Object mid,
required String name,
}) {
return Get.toNamed(
'/videoWeb',
arguments: {
'mid': mid,
'name': name,
},
);
}
}
class _MemberVideoWebState
extends
BaseVideoWebState<
MemberVideoWeb,
SearchArchiveData,
VListItemModel,
ArchiveOrderTypeWeb
>
with GridMixin {
@override
late final MemberVideoWebCtr controller;
@override
void initState() {
super.initState();
controller = Get.put(MemberVideoWebCtr(), tag: name);
}
@override
List<ArchiveOrderTypeWeb> get values => ArchiveOrderTypeWeb.values;
@override
Widget? buildTags(ColorScheme colorScheme) {
if (controller.tags case final tags?) {
return SliverPinnedHeader(
backgroundColor: colorScheme.surface,
child: SelfSizedHorizontalList(
itemCount: tags.length,
padding: const .fromLTRB(10, 0, 10, 8),
itemBuilder: (context, index) {
final item = tags[index];
final isCurr = controller.specialType != null
? item.specialType == controller.specialType
: item.tid == controller.tid;
return SearchText(
padding: const .symmetric(horizontal: 8, vertical: 4),
text: '${item.name!} ${item.count}',
bgColor: isCurr ? colorScheme.secondaryContainer : null,
textColor: isCurr ? colorScheme.onSecondaryContainer : null,
onTap: (_) {
if (isCurr) return;
controller
..tid = item.tid ?? 0
..specialType = item.specialType
..onReload();
},
);
},
separatorBuilder: (_, _) => const SizedBox(width: 10),
),
);
}
return null;
}
}

View File

@@ -0,0 +1,50 @@
import 'package:PiliPlus/common/widgets/scroll_physics.dart' show ReloadMixin;
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/pages/common/common_list_controller.dart';
import 'package:get/get.dart';
const int ps = 30;
abstract class BaseVideoWebCtr<R, T, V> extends CommonListController<R, T>
with ReloadMixin {
final Object mid = Get.arguments['mid'];
int? totalPage;
int? count;
Rx<V> get order;
@override
void onInit() {
super.onInit();
queryData();
}
@override
void checkIsEnd(int length) {
if (totalPage != null && page >= totalPage!) {
isEnd = true;
} else if (count != null && length >= count!) {
isEnd = true;
}
}
void queryBySort(V value) {
if (isLoading) return;
order.value = value;
onReload();
}
void jumpToPage(int page) {
isEnd = false;
reload = true;
this.page = page;
loadingState.value = LoadingState.loading();
queryData();
}
@override
Future<void> onReload() {
reload = true;
return super.onReload();
}
}

View File

@@ -0,0 +1,217 @@
import 'package:PiliPlus/common/widgets/button/icon_button.dart';
import 'package:PiliPlus/common/widgets/dialog/dialog.dart';
import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart';
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
import 'package:PiliPlus/common/widgets/sliver/sliver_pinned_header.dart';
import 'package:PiliPlus/common/widgets/video_card/video_card_h.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/common/enum_with_label.dart';
import 'package:PiliPlus/models/model_video.dart';
import 'package:PiliPlus/pages/member_video_web/base/controller.dart';
import 'package:PiliPlus/pages/search/widgets/search_text.dart';
import 'package:PiliPlus/utils/grid.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
abstract class BaseVideoWebState<
S extends StatefulWidget,
R,
T extends BaseVideoItemModel,
V extends EnumWithLabel
>
extends State<S>
with GridMixin {
late final String name;
BaseVideoWebCtr<R, T, V> get controller;
@override
void initState() {
super.initState();
final args = Get.arguments;
name = args['name'];
}
List<V> get values;
@override
Widget build(BuildContext context) {
final colorScheme = ColorScheme.of(context);
return Scaffold(
appBar: AppBar(
title: Text(name),
actions: [
Obx(
() {
final order = controller.order.value;
return PopupMenuButton<V>(
tooltip: '排序',
icon: const Icon(Icons.sort),
initialValue: order,
onSelected: controller.queryBySort,
itemBuilder: (_) => values
.map((e) => PopupMenuItem(value: e, child: Text(e.label)))
.toList(),
);
},
),
const SizedBox(width: 6),
],
),
body: refreshIndicator(
onRefresh: controller.onRefresh,
child: CustomScrollView(
physics: ReloadScrollPhysics(controller: controller),
slivers: [
SliverPadding(
padding: .only(
bottom: MediaQuery.viewPaddingOf(context).bottom + 100,
),
sliver: Obx(
() => buildBody(colorScheme, controller.loadingState.value),
),
),
],
),
),
);
}
Widget buildBody(
ColorScheme colorScheme,
LoadingState<List<T>?> loadingState,
) {
return switch (loadingState) {
Loading() => gridSkeleton,
Success(:final response) =>
response != null && response.isNotEmpty
? SliverMainAxisGroup(
slivers: [
buildHeader(colorScheme),
?buildTags(colorScheme),
SliverGrid.builder(
gridDelegate: gridDelegate,
itemCount: response.length,
itemBuilder: (context, index) {
if (index == response.length - 1) {
controller.onLoadMore();
}
return VideoCardH(videoItem: response[index]);
},
),
],
)
: HttpError(onReload: controller.onReload),
Error(:final errMsg) => HttpError(
errMsg: errMsg,
onReload: controller.onReload,
),
};
}
Widget? buildTags(ColorScheme colorScheme) => null;
Widget buildHeader(ColorScheme colorScheme) {
return SliverPinnedHeader(
backgroundColor: colorScheme.surface,
child: Padding(
padding: const .fromLTRB(14, 0, 8, 4),
child: Stack(
alignment: .centerLeft,
children: [
?buildCount(),
Center(child: buildPageBtn(colorScheme)),
],
),
),
);
}
Widget? buildCount() {
final count = controller.count;
if (count == null) return null;
return Text(
'$count 视频',
style: const TextStyle(height: 1),
strutStyle: const StrutStyle(leading: 0, height: 1),
);
}
Widget? buildPageBtn(ColorScheme colorScheme) {
final totalPage = controller.totalPage;
if (totalPage == null) return null;
final page = controller.page - 1;
final canBackward = page > 1;
final canForward = page < totalPage;
const size = 30.0;
const iconSize = 24.0;
final backwardBtn = iconButton(
size: size,
iconSize: iconSize,
tooltip: canBackward ? '上一页' : null,
icon: const Icon(Icons.keyboard_arrow_left),
onPressed: canBackward ? () => controller.jumpToPage(page - 1) : null,
);
final forwardBtn = iconButton(
size: size,
iconSize: iconSize,
tooltip: canForward ? '下一页' : null,
icon: const Icon(Icons.keyboard_arrow_right),
onPressed: canForward ? () => controller.jumpToPage(page + 1) : null,
);
final pageIndicator = SearchText(
height: 1,
text: '$page / $totalPage',
borderRadius: const .all(.circular(4)),
padding: const .symmetric(horizontal: 10, vertical: 5),
onTap: (_) => showJumpDialog(page),
);
return Row(
spacing: 6,
mainAxisSize: .min,
children: [
backwardBtn,
pageIndicator,
forwardBtn,
],
);
}
void showJumpDialog(int page) {
var pageStr = page.toString();
void onSubmit([_]) {
try {
controller.jumpToPage(int.parse(pageStr));
} catch (e) {
SmartDialog.showToast(e.toString());
}
}
showConfirmDialog(
context: context,
title: const Text('跳至: '),
content: TextFormField(
autofocus: true,
initialValue: pageStr,
onChanged: (value) => pageStr = value,
decoration: const InputDecoration(
labelText: '页数',
border: OutlineInputBorder(),
),
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
onFieldSubmitted: (_) {
Get.back();
onSubmit();
},
),
onConfirm: onSubmit,
);
}
}

View File

@@ -0,0 +1,55 @@
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/member.dart';
import 'package:PiliPlus/models/common/member/archive_sort_type_app.dart';
import 'package:PiliPlus/models/common/member/web_ss_type.dart';
import 'package:PiliPlus/models_new/member/season_web/archive.dart';
import 'package:PiliPlus/models_new/member/season_web/data.dart';
import 'package:PiliPlus/pages/member_video_web/base/controller.dart';
import 'package:get/get.dart';
class MemberSSWebCtr
extends BaseVideoWebCtr<SeasonWebData, SeasonArchive, ArchiveSortTypeApp> {
@override
final Rx<ArchiveSortTypeApp> order = Rx(.desc);
late final WebSsType _type;
late final Object _id;
@override
void onInit() {
final args = Get.arguments;
_type = args['type'];
_id = args['id'];
super.onInit();
}
@override
List<SeasonArchive>? getDataList(SeasonWebData response) {
return response.archives;
}
@override
bool customHandleResponse(
bool isRefresh,
Success<SeasonWebData> response,
) {
if (isRefresh) {
final data = response.response;
if (data.page?.total case final total?) {
count = total;
totalPage = (total / ps).ceil();
}
}
return false;
}
@override
Future<LoadingState<SeasonWebData>> customGetData() =>
MemberHttp.seasonSeriesWeb(
type: _type,
mid: mid,
id: _id,
ps: ps,
pn: page,
sort: order.value,
);
}

View File

@@ -0,0 +1,55 @@
import 'package:PiliPlus/models/common/member/archive_sort_type_app.dart';
import 'package:PiliPlus/models/common/member/web_ss_type.dart';
import 'package:PiliPlus/models_new/member/season_web/archive.dart';
import 'package:PiliPlus/models_new/member/season_web/data.dart';
import 'package:PiliPlus/pages/member_video_web/base/view.dart';
import 'package:PiliPlus/pages/member_video_web/season_series/controller.dart';
import 'package:PiliPlus/utils/grid.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class MemberSSWeb extends StatefulWidget {
const MemberSSWeb({super.key});
@override
State<MemberSSWeb> createState() => _MemberSSWebState();
static Future<void>? toMemberSSWeb({
required WebSsType type,
required Object id,
required Object mid,
required String name,
}) {
return Get.toNamed(
'/ssWeb',
arguments: {
'type': type,
'id': id,
'mid': mid,
'name': name,
},
);
}
}
class _MemberSSWebState
extends
BaseVideoWebState<
MemberSSWeb,
SeasonWebData,
SeasonArchive,
ArchiveSortTypeApp
>
with GridMixin {
@override
late final MemberSSWebCtr controller;
@override
void initState() {
super.initState();
controller = Get.put(MemberSSWebCtr(), tag: name);
}
@override
List<ArchiveSortTypeApp> get values => ArchiveSortTypeApp.values;
}

View File

@@ -1,6 +1,6 @@
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/member.dart'; import 'package:PiliPlus/http/member.dart';
import 'package:PiliPlus/models/common/member/contribute_type.dart'; import 'package:PiliPlus/models/common/member/archive_order_type_app.dart';
import 'package:PiliPlus/models/member/info.dart'; import 'package:PiliPlus/models/member/info.dart';
import 'package:PiliPlus/models_new/space/space_archive/data.dart'; import 'package:PiliPlus/models_new/space/space_archive/data.dart';
import 'package:PiliPlus/models_new/space/space_archive/item.dart'; import 'package:PiliPlus/models_new/space/space_archive/item.dart';
@@ -55,7 +55,7 @@ class HorizontalMemberPageController
@override @override
bool customHandleResponse(bool isRefresh, Success response) { bool customHandleResponse(bool isRefresh, Success response) {
SpaceArchiveData data = response.response; SpaceArchiveData data = response.response;
count.value = data.count ?? -1; count = data.count;
if (isRefresh) { if (isRefresh) {
if (isLoadPrevious) { if (isLoadPrevious) {
hasPrev = data.hasPrev ?? false; hasPrev = data.hasPrev ?? false;
@@ -83,8 +83,8 @@ class HorizontalMemberPageController
String? currAid; String? currAid;
String? firstAid; String? firstAid;
String? lastAid; String? lastAid;
RxString order = 'pubdate'.obs; ArchiveOrderTypeApp order = .pubdate;
RxInt count = (-1).obs; int? count;
bool isLoadPrevious = false; bool isLoadPrevious = false;
bool hasPrev = true; bool hasPrev = true;
bool hasNext = true; bool hasNext = true;
@@ -92,15 +92,15 @@ class HorizontalMemberPageController
@override @override
Future<LoadingState<SpaceArchiveData>> customGetData() => Future<LoadingState<SpaceArchiveData>> customGetData() =>
MemberHttp.spaceArchive( MemberHttp.spaceArchive(
type: ContributeType.video, type: .video,
mid: mid, mid: mid,
aid: page == 1 aid: page == 1
? currAid ? currAid
: isLoadPrevious : isLoadPrevious
? firstAid ? firstAid
: lastAid, : lastAid,
order: order.value, order: order,
sort: page != 1 && isLoadPrevious ? 'asc' : null, sort: page != 1 && isLoadPrevious ? .asc : null,
pn: null, pn: null,
next: null, next: null,
seasonId: null, seasonId: null,
@@ -131,7 +131,7 @@ class HorizontalMemberPageController
void queryBySort() { void queryBySort() {
if (isLoading) return; if (isLoading) return;
order.value = order.value == 'pubdate' ? 'click' : 'pubdate'; order = order == .pubdate ? .click : .pubdate;
onReload(); onReload();
} }
} }

View File

@@ -128,40 +128,45 @@ class _HorizontalMemberPageState extends State<HorizontalMemberPage> {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Obx( ?_buildCount(),
() { _buildSortBtn(theme),
final count = _controller.count.value;
return Text(
count != -1 ? '$count视频' : '',
style: const TextStyle(fontSize: 13),
);
},
),
TextButton.icon(
style: Style.buttonStyle,
onPressed: () => _controller
..lastAid = widget.videoDetailController.aid.toString()
..queryBySort(),
icon: Icon(
Icons.sort,
size: 16,
color: theme.colorScheme.secondary,
),
label: Obx(
() => Text(
_controller.order.value == 'pubdate' ? '最新发布' : '最多播放',
style: TextStyle(
fontSize: 13,
color: theme.colorScheme.secondary,
),
),
),
),
], ],
), ),
); );
} }
Widget? _buildCount() {
final count = _controller.count;
if (count != null) {
return Text(
'$count视频',
style: const TextStyle(fontSize: 13),
);
}
return null;
}
Widget _buildSortBtn(ThemeData theme) {
return TextButton.icon(
style: Style.buttonStyle,
onPressed: () => _controller
..lastAid = widget.videoDetailController.aid.toString()
..queryBySort(),
icon: Icon(
Icons.sort,
size: 16,
color: theme.colorScheme.secondary,
),
label: Text(
_controller.order.label,
style: TextStyle(
fontSize: 13,
color: theme.colorScheme.secondary,
),
),
);
}
Widget _buildVideoList( Widget _buildVideoList(
ThemeData theme, ThemeData theme,
LoadingState<List<SpaceArchiveItem>?> loadingState, LoadingState<List<SpaceArchiveItem>?> loadingState,

View File

@@ -37,6 +37,8 @@ import 'package:PiliPlus/pages/member_dynamics/view.dart';
import 'package:PiliPlus/pages/member_profile/view.dart'; import 'package:PiliPlus/pages/member_profile/view.dart';
import 'package:PiliPlus/pages/member_search/view.dart'; import 'package:PiliPlus/pages/member_search/view.dart';
import 'package:PiliPlus/pages/member_upower_rank/view.dart'; import 'package:PiliPlus/pages/member_upower_rank/view.dart';
import 'package:PiliPlus/pages/member_video_web/archive/view.dart';
import 'package:PiliPlus/pages/member_video_web/season_series/view.dart';
import 'package:PiliPlus/pages/msg_feed_top/at_me/view.dart'; import 'package:PiliPlus/pages/msg_feed_top/at_me/view.dart';
import 'package:PiliPlus/pages/msg_feed_top/like_detail/view.dart'; import 'package:PiliPlus/pages/msg_feed_top/like_detail/view.dart';
import 'package:PiliPlus/pages/msg_feed_top/like_me/view.dart'; import 'package:PiliPlus/pages/msg_feed_top/like_me/view.dart';
@@ -192,5 +194,7 @@ class Routes {
GetPage(name: '/download', page: () => const DownloadPage()), GetPage(name: '/download', page: () => const DownloadPage()),
GetPage(name: '/dlna', page: () => const DLNAPage()), GetPage(name: '/dlna', page: () => const DLNAPage()),
GetPage(name: '/myReply', page: () => const MyReply()), GetPage(name: '/myReply', page: () => const MyReply()),
GetPage(name: '/videoWeb', page: () => const MemberVideoWeb()),
GetPage(name: '/ssWeb', page: () => const MemberSSWeb()),
]; ];
} }

View File

@@ -31,7 +31,8 @@ abstract final class ApiType {
Api.onlineTotal, Api.onlineTotal,
Api.dynamicDetail, Api.dynamicDetail,
Api.aiConclusion, Api.aiConclusion,
Api.getSeasonDetailApi, Api.seasonArchives,
Api.seriesArchives,
Api.liveRoomDmToken, Api.liveRoomDmToken,
Api.liveRoomDmPrefetch, Api.liveRoomDmPrefetch,
Api.superChatMsg, Api.superChatMsg,