diff --git a/lib/common/widgets/self_sized_horizontal_list.dart b/lib/common/widgets/self_sized_horizontal_list.dart index 5e19347a..19a94efb 100644 --- a/lib/common/widgets/self_sized_horizontal_list.dart +++ b/lib/common/widgets/self_sized_horizontal_list.dart @@ -39,7 +39,13 @@ class _SelfSizedHorizontalListState extends State { WidgetsBinding.instance.addPostFrameCallback((v) => setState(() {})); } if (widget.itemCount == 0) return const SizedBox(); - if (isInit) return Container(key: infoKey, child: widget.childBuilder(0)); + if (isInit) { + return Container( + key: infoKey, + padding: widget.padding, + child: widget.childBuilder(0), + ); + } return SizedBox( height: height, diff --git a/lib/http/api.dart b/lib/http/api.dart index 0a82cd6d..c0d605c0 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -718,4 +718,8 @@ class Api { /// 我的关注 - 正在直播 static const String getFollowingLive = '${HttpString.liveBaseUrl}/xlive/web-ucenter/user/following'; + + static const String pgcIndexCondition = '/pgc/season/index/condition'; + + static const String pgcIndexResult = '/pgc/season/index/result'; } diff --git a/lib/http/bangumi.dart b/lib/http/bangumi.dart index cdfedbe5..e58e2a2e 100644 --- a/lib/http/bangumi.dart +++ b/lib/http/bangumi.dart @@ -1,9 +1,55 @@ import 'package:PiliPlus/http/loading_state.dart'; import '../models/bangumi/list.dart'; +import '../models/bangumi/pgc_index/condition.dart'; import 'index.dart'; class BangumiHttp { + static Future pgcIndexResult({ + required int page, + required Map params, + seasonType, + type, + indexType, + }) async { + dynamic res = await Request().get( + Api.pgcIndexResult, + queryParameters: { + ...params, + if (seasonType != null) 'season_type': seasonType, + if (type != null) 'type': type, + if (indexType != null) 'index_type': indexType, + 'page': page, + 'pagesize': 21, + }, + ); + if (res.data['code'] == 0) { + return LoadingState.success(res.data['data']); + } else { + return LoadingState.error(res.data['message']); + } + } + + static Future pgcIndexCondition({ + seasonType, + type, + indexType, + }) async { + dynamic res = await Request().get( + Api.pgcIndexCondition, + queryParameters: { + if (seasonType != null) 'season_type': seasonType, + if (type != null) 'type': type, + if (indexType != null) 'index_type': indexType, + }, + ); + if (res.data['code'] == 0) { + return LoadingState.success(Condition.fromJson(res.data['data'])); + } else { + return LoadingState.error(res.data['message']); + } + } + static Future bangumiList({ int? page, int? indexType, diff --git a/lib/models/bangumi/pgc_index/condition.dart b/lib/models/bangumi/pgc_index/condition.dart new file mode 100644 index 00000000..5a39954e --- /dev/null +++ b/lib/models/bangumi/pgc_index/condition.dart @@ -0,0 +1,70 @@ +class Condition { + List? filter; + List? order; + + Condition({ + this.filter, + this.order, + }); + + Condition.fromJson(Map json) { + filter = (json['filter'] as List?) + ?.map((item) => Filter.fromJson(item)) + .toList(); + order = + (json['order'] as List?)?.map((item) => Order.fromJson(item)).toList(); + } +} + +class Order { + String? field; + String? name; + String? sort; + + Order({ + this.field, + this.name, + this.sort, + }); + + Order.fromJson(Map json) { + field = json['field']; + name = json['name']; + sort = json['sort']; + } +} + +class Filter { + String? field; + String? name; + List? values; + + Filter({ + this.field, + this.name, + this.values, + }); + + Filter.fromJson(Map json) { + field = json['field']; + name = json['name']; + values = (json['values'] as List?) + ?.map((item) => Values.fromJson(item)) + .toList(); + } +} + +class Values { + String? keyword; + String? name; + + Values({ + this.keyword, + this.name, + }); + + Values.fromJson(Map json) { + keyword = json['keyword']; + name = json['name']; + } +} diff --git a/lib/pages/bangumi/pgc_index/pgc_index_controller.dart b/lib/pages/bangumi/pgc_index/pgc_index_controller.dart new file mode 100644 index 00000000..45081d84 --- /dev/null +++ b/lib/pages/bangumi/pgc_index/pgc_index_controller.dart @@ -0,0 +1,69 @@ +import 'package:PiliPlus/http/bangumi.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models/bangumi/pgc_index/condition.dart'; +import 'package:PiliPlus/pages/common/common_controller.dart'; +import 'package:get/get.dart' hide Condition; + +class PgcIndexController extends CommonController { + PgcIndexController(this.indexType); + int? indexType; + Rx conditionState = LoadingState.loading().obs; + + late final RxBool isExpand = false.obs; + + RxMap indexParams = {}.obs; + + @override + void onInit() { + super.onInit(); + getPgcIndexCondition(); + } + + Future getPgcIndexCondition() async { + dynamic res = await BangumiHttp.pgcIndexCondition( + seasonType: indexType == null ? 1 : null, + type: 0, + indexType: indexType, + ); + if (res is Success) { + Condition data = res.response; + if (data.order?.isNotEmpty == true) { + indexParams['order'] = data.order!.first.field; + } + if (data.filter?.isNotEmpty == true) { + for (Filter item in data.filter!) { + indexParams['${item.field}'] = item.values?.firstOrNull?.keyword; + } + } + queryData(); + } + conditionState.value = res; + } + + @override + Future customGetData() => BangumiHttp.pgcIndexResult( + page: currentPage, + params: indexParams, + seasonType: indexType == null ? 1 : null, + type: 0, + indexType: indexType, + ); + + @override + bool customHandleResponse(Success response) { + if (response.response['has_next'] == null || + response.response['has_next'] == 0) { + isEnd = true; + } + if (response.response['list'] == null || + (response.response['list'] as List?)?.isEmpty == true) { + isEnd = true; + } + if (currentPage != 1 && loadingState.value is Success) { + response.response['list'] + ?.insertAll(0, (loadingState.value as Success).response); + } + loadingState.value = LoadingState.success(response.response['list']); + return true; + } +} diff --git a/lib/pages/bangumi/pgc_index/pgc_index_page.dart b/lib/pages/bangumi/pgc_index/pgc_index_page.dart new file mode 100644 index 00000000..377c9028 --- /dev/null +++ b/lib/pages/bangumi/pgc_index/pgc_index_page.dart @@ -0,0 +1,237 @@ +import 'package:PiliPlus/common/constants.dart'; +import 'package:PiliPlus/common/widgets/http_error.dart'; +import 'package:PiliPlus/common/widgets/loading_widget.dart'; +import 'package:PiliPlus/common/widgets/self_sized_horizontal_list.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/pages/bangumi/pgc_index/pgc_index_controller.dart'; +import 'package:PiliPlus/pages/bangumi/widgets/bangumi_card_v_pgc_index.dart'; +import 'package:PiliPlus/pages/search/widgets/search_text.dart'; +import 'package:PiliPlus/utils/extension.dart'; +import 'package:PiliPlus/utils/grid.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart' hide Condition; + +import '../../../models/bangumi/pgc_index/condition.dart'; + +class PgcIndexPage extends StatefulWidget { + const PgcIndexPage({super.key, this.indexType}); + + final int? indexType; + + @override + State createState() => _PgcIndexPageState(); +} + +class _PgcIndexPageState extends State { + late final _ctr = Get.put( + PgcIndexController(widget.indexType), + tag: '${widget.indexType}', + ); + + @override + Widget build(BuildContext context) { + return widget.indexType == null + ? Scaffold( + appBar: AppBar(title: const Text('索引')), + body: Obx(() => _buildBody(_ctr.conditionState.value)), + ) + : Obx(() => _buildBody(_ctr.conditionState.value)); + } + + Widget _buildBody(LoadingState loadingState) { + return switch (loadingState) { + Loading() => loadingWidget, + Success() => Builder(builder: (context) { + Condition data = loadingState.response; + int count = (data.order?.isNotEmpty == true ? 1 : 0) + + (data.filter?.length ?? 0); + if (count == 0) return const SizedBox.shrink(); + return CustomScrollView( + slivers: [ + if (widget.indexType != null) + SliverToBoxAdapter(child: const SizedBox(height: 12)), + SliverToBoxAdapter( + child: AnimatedSize( + curve: Curves.easeInOut, + alignment: Alignment.topCenter, + duration: const Duration(milliseconds: 200), + child: count > 5 + ? Obx(() => _buildSortWidget(count, data)) + : _buildSortWidget(count, data), + ), + ), + SliverPadding( + padding: EdgeInsets.only( + left: StyleString.safeSpace, + right: StyleString.safeSpace, + top: 12, + bottom: MediaQuery.paddingOf(context).bottom + 80, + ), + sliver: Obx(() => _buildList(_ctr.loadingState.value)), + ), + ], + ); + }), + Error() => scrollErrorWidget( + errMsg: loadingState.errMsg, + callback: () { + _ctr.conditionState.value = LoadingState.loading(); + _ctr.getPgcIndexCondition(); + }, + ), + LoadingState() => throw UnimplementedError(), + }; + } + + Widget _buildSortWidget(count, data) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...List.generate( + count > 5 + ? _ctr.isExpand.value + ? count + : count ~/ 2 + : count, + (index) { + List? item = data.order?.isNotEmpty == true + ? index == 0 + ? data.order + : data.filter![index - 1].values + : data.filter![index].values; + return item?.isNotEmpty == true + ? Padding( + padding: EdgeInsets.only( + top: index == 0 ? 0 : 10, + ), + child: SelfSizedHorizontalList( + gapSize: 12, + padding: const EdgeInsets.symmetric( + horizontal: 12, + ), + childBuilder: (childIndex) => Obx( + () => SearchText( + bgColor: (item[childIndex] is Order + ? _ctr.indexParams['order'] + : _ctr.indexParams[data + .filter![ + data.order?.isNotEmpty == true + ? index - 1 + : index] + .field]) == + (item[childIndex] is Order + ? item[childIndex].field + : item[childIndex].keyword) + ? Theme.of(context) + .colorScheme + .secondaryContainer + : Colors.transparent, + textColor: (item[childIndex] is Order + ? _ctr.indexParams['order'] + : _ctr.indexParams[data + .filter![ + data.order?.isNotEmpty == true + ? index - 1 + : index] + .field]) == + (item[childIndex] is Order + ? item[childIndex].field + : item[childIndex].keyword) + ? Theme.of(context) + .colorScheme + .onSecondaryContainer + : Theme.of(context) + .colorScheme + .onSurfaceVariant, + text: item[childIndex].name, + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 3, + ), + onTap: (_) { + String name = item[childIndex] is Order + ? 'order' + : data + .filter![data.order?.isNotEmpty == true + ? index - 1 + : index] + .field!; + _ctr.indexParams[name] = + (item[childIndex] is Order + ? item[childIndex].field + : item[childIndex].keyword); + _ctr.onReload(); + }, + ), + ), + itemCount: item!.length, + ), + ) + : const SizedBox.shrink(); + }, + ), + if (count > 5) ...[ + const SizedBox(height: 8), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + _ctr.isExpand.value = _ctr.isExpand.value.not; + }, + child: Container( + width: double.infinity, + alignment: Alignment.center, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + _ctr.isExpand.value ? '收起' : '展开', + style: TextStyle( + color: Theme.of(context).colorScheme.outline, + ), + ), + Icon( + _ctr.isExpand.value + ? Icons.keyboard_arrow_up + : Icons.keyboard_arrow_down, + color: Theme.of(context).colorScheme.outline, + ), + ], + ), + ), + ), + ], + ], + ); + + Widget _buildList(LoadingState loadingState) { + return switch (loadingState) { + Loading() => HttpError(errMsg: '加载中'), + Success() => (loadingState.response as List?)?.isNotEmpty == true + ? SliverGrid( + gridDelegate: SliverGridDelegateWithExtentAndRatio( + mainAxisSpacing: StyleString.cardSpace, + crossAxisSpacing: StyleString.cardSpace, + maxCrossAxisExtent: Grid.smallCardWidth / 3 * 2, + childAspectRatio: 0.75, + mainAxisExtent: MediaQuery.textScalerOf(context).scale(50), + ), + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + if (index == loadingState.response.length - 1) { + _ctr.onLoadMore(); + } + return BangumiCardVPgcIndex( + bangumiItem: loadingState.response[index]); + }, + childCount: loadingState.response.length, + ), + ) + : HttpError(callback: _ctr.onReload), + Error() => HttpError( + errMsg: loadingState.errMsg, + callback: _ctr.onReload, + ), + LoadingState() => throw UnimplementedError(), + }; + } +} diff --git a/lib/pages/bangumi/view.dart b/lib/pages/bangumi/view.dart index 8748037f..78077f35 100644 --- a/lib/pages/bangumi/view.dart +++ b/lib/pages/bangumi/view.dart @@ -4,6 +4,7 @@ import 'package:PiliPlus/common/widgets/loading_widget.dart'; import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/tab_type.dart'; +import 'package:PiliPlus/pages/bangumi/pgc_index/pgc_index_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:get/get.dart'; @@ -128,7 +129,12 @@ class _BangumiPageState extends State ), SliverToBoxAdapter( child: Padding( - padding: const EdgeInsets.only(top: 10, bottom: 10, left: 16), + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + left: 16, + right: 10, + ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -136,6 +142,56 @@ class _BangumiPageState extends State '推荐', style: Theme.of(context).textTheme.titleMedium, ), + GestureDetector( + onTap: () { + if (widget.tabType == TabType.bangumi) { + Get.to(PgcIndexPage()); + } else { + List titles = const ['全部', '电影', '电视剧', '纪录片', '综艺']; + List types = const [102, 2, 5, 3, 7]; + Get.to( + Scaffold( + appBar: AppBar(title: const Text('索引')), + body: DefaultTabController( + length: types.length, + child: Column( + children: [ + TabBar( + tabs: titles + .map((title) => Tab(text: title)) + .toList()), + Expanded( + child: TabBarView( + children: types + .map((type) => + PgcIndexPage(indexType: type)) + .toList()), + ) + ], + ), + ), + ), + ); + } + }, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '查看更多', + strutStyle: StrutStyle(leading: 0, height: 1), + style: TextStyle( + height: 1, + color: Theme.of(context).colorScheme.secondary, + ), + ), + Icon( + Icons.chevron_right, + color: Theme.of(context).colorScheme.secondary, + ), + ], + ), + ), ], ), ), diff --git a/lib/pages/bangumi/widgets/bangumi_card_v_pgc_index.dart b/lib/pages/bangumi/widgets/bangumi_card_v_pgc_index.dart new file mode 100644 index 00000000..e32ac715 --- /dev/null +++ b/lib/pages/bangumi/widgets/bangumi_card_v_pgc_index.dart @@ -0,0 +1,127 @@ +import 'package:PiliPlus/common/widgets/image_save.dart'; +import 'package:flutter/material.dart'; +import 'package:PiliPlus/common/constants.dart'; +import 'package:PiliPlus/common/widgets/badge.dart'; +import 'package:PiliPlus/utils/utils.dart'; +import 'package:PiliPlus/common/widgets/network_img_layer.dart'; + +// 视频卡片 - 垂直布局 +class BangumiCardVPgcIndex extends StatelessWidget { + const BangumiCardVPgcIndex({ + super.key, + required this.bangumiItem, + }); + + final dynamic bangumiItem; + + @override + Widget build(BuildContext context) { + return Card( + clipBehavior: Clip.hardEdge, + margin: EdgeInsets.zero, + child: InkWell( + onLongPress: () => imageSaveDialog( + context: context, + title: bangumiItem['title'], + cover: bangumiItem['cover'], + ), + onTap: () { + Utils.viewBangumi(seasonId: bangumiItem['season_id']); + }, + child: Column( + children: [ + ClipRRect( + borderRadius: StyleString.mdRadius, + child: AspectRatio( + aspectRatio: 0.75, + child: LayoutBuilder(builder: (context, boxConstraints) { + final double maxWidth = boxConstraints.maxWidth; + final double maxHeight = boxConstraints.maxHeight; + return Stack( + children: [ + NetworkImgLayer( + src: bangumiItem['cover'], + width: maxWidth, + height: maxHeight, + ), + if (bangumiItem['badge'] != null && + bangumiItem['badge'] != '') + PBadge( + text: bangumiItem['badge'], + top: 6, + right: 6, + bottom: null, + left: null, + ), + if (bangumiItem['order'] != null && + bangumiItem['order'] != '') + PBadge( + text: bangumiItem['order'], + top: null, + right: null, + bottom: 6, + left: 6, + type: 'gray', + ), + ], + ); + }), + ), + ), + bagumiContent(context) + ], + ), + ), + ); + } + + Widget bagumiContent(context) { + return Expanded( + child: Padding( + // 多列 + padding: const EdgeInsets.fromLTRB(4, 5, 0, 3), + // 单列 + // padding: const EdgeInsets.fromLTRB(14, 10, 4, 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Expanded( + child: Text( + bangumiItem['title'], + textAlign: TextAlign.start, + style: const TextStyle( + letterSpacing: 0.3, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + )), + ], + ), + const SizedBox(height: 1), + if (bangumiItem['index_show'] != null) + Text( + bangumiItem['index_show'], + maxLines: 1, + style: TextStyle( + fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, + color: Theme.of(context).colorScheme.outline, + ), + ), + // if (bangumiItem.progress != null) + // Text( + // bangumiItem.progress, + // maxLines: 1, + // style: TextStyle( + // fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, + // color: Theme.of(context).colorScheme.outline, + // ), + // ), + ], + ), + ), + ); + } +}