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

@@ -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;
}