diff --git a/lib/common/widgets/badge.dart b/lib/common/widgets/badge.dart index b6be50e38..6eb745745 100644 --- a/lib/common/widgets/badge.dart +++ b/lib/common/widgets/badge.dart @@ -77,6 +77,9 @@ class PBadge extends StatelessWidget { case PBadgeType.free: bgColor = theme.freeColor; color = Colors.white; + case PBadgeType.shop: + bgColor = theme.secondaryContainer.withValues(alpha: 0.5); + color = theme.onSurfaceVariant; } late EdgeInsets paddingStyle = const EdgeInsets.symmetric( diff --git a/lib/http/api.dart b/lib/http/api.dart index b55b1cd9a..34360cfdd 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -959,4 +959,7 @@ class Api { static const String bgmRecommend = '/x/copyright-music-publicity/bgm/recommend_list'; + + static const String spaceShop = + '${HttpString.mallBaseUrl}/community-hub/small_shop/feed/tab/item'; } diff --git a/lib/http/constants.dart b/lib/http/constants.dart index 46b547967..0f1f06975 100644 --- a/lib/http/constants.dart +++ b/lib/http/constants.dart @@ -9,6 +9,7 @@ class HttpString { static const String dynamicShareBaseUrl = 'https://t.bilibili.com'; static const String spaceBaseUrl = 'https://space.bilibili.com'; static const String accountBaseUrl = 'https://account.bilibili.com'; + static const String mallBaseUrl = 'https://mall.bilibili.com'; static const String sponsorBlockBaseUrl = 'https://www.bsbsb.top'; } diff --git a/lib/http/live.dart b/lib/http/live.dart index 5164f56bb..484e1aa05 100644 --- a/lib/http/live.dart +++ b/lib/http/live.dart @@ -168,7 +168,7 @@ class LiveHttp { bool? moduleSelect, }) async { final params = { - if (isLogin) 'access_key': Accounts.main.accessKey, + 'access_key': ?Accounts.main.accessKey, 'appkey': Constants.appKey, 'channel': 'master', 'actionKey': 'appkey', @@ -250,7 +250,7 @@ class LiveHttp { String? sortType, }) async { final params = { - if (isLogin) 'access_key': Accounts.main.accessKey, + 'access_key': ?Accounts.main.accessKey, 'appkey': Constants.appKey, 'actionKey': 'appkey', 'channel': 'master', @@ -316,7 +316,7 @@ class LiveHttp { required bool isLogin, }) async { final params = { - if (isLogin) 'access_key': Accounts.main.accessKey, + 'access_key': ?Accounts.main.accessKey, 'appkey': Constants.appKey, 'actionKey': 'appkey', 'build': 8430300, @@ -355,7 +355,7 @@ class LiveHttp { required bool isLogin, }) async { final params = { - if (isLogin) 'access_key': Accounts.main.accessKey, + 'access_key': ?Accounts.main.accessKey, 'appkey': Constants.appKey, 'actionKey': 'appkey', 'build': 8430300, @@ -435,7 +435,7 @@ class LiveHttp { required parentid, }) async { final params = { - if (isLogin) 'access_key': Accounts.main.accessKey, + 'access_key': ?Accounts.main.accessKey, 'appkey': Constants.appKey, 'actionKey': 'appkey', 'build': 8430300, @@ -478,7 +478,7 @@ class LiveHttp { required LiveSearchType type, }) async { final params = { - if (isLogin) 'access_key': Accounts.main.accessKey, + 'access_key': ?Accounts.main.accessKey, 'appkey': Constants.appKey, 'actionKey': 'appkey', 'build': 8430300, diff --git a/lib/http/member.dart b/lib/http/member.dart index f23efca1c..c0899723c 100644 --- a/lib/http/member.dart +++ b/lib/http/member.dart @@ -23,8 +23,10 @@ import 'package:PiliPlus/models_new/space/space_audio/data.dart'; import 'package:PiliPlus/models_new/space/space_cheese/data.dart'; import 'package:PiliPlus/models_new/space/space_opus/data.dart'; import 'package:PiliPlus/models_new/space/space_season_series/item.dart'; +import 'package:PiliPlus/models_new/space/space_shop/data.dart'; import 'package:PiliPlus/models_new/upower_rank/data.dart'; import 'package:PiliPlus/utils/accounts.dart'; +import 'package:PiliPlus/utils/app_sign.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/wbi_sign.dart'; import 'package:dio/dio.dart'; @@ -779,4 +781,34 @@ class MemberHttp { return Error(res.data['message']); } } + + static Future> spaceShop({ + required int mid, + }) async { + final params = { + 'access_key': ?Accounts.main.accessKey, + 'actionKey': 'appkey', + 'build': 8430300, + 'mVersion': 309, + 'mallVersion': 8430300, + 'statistics': Constants.statisticsApp, + }; + AppSign.appSign(params); + var res = await Request().post( + Api.spaceShop, + queryParameters: params, + data: { + "from": "cps_productTab_$mid", + "searchAfter": 0, + "msource": "cps_productTab_$mid", + "pageSize": 8, + "upMid": mid.toString(), + }, + ); + if (res.data['code'] == 0) { + return Success(SpaceShopData.fromJson(res.data['data'])); + } else { + return Error(res.data['message']); + } + } } diff --git a/lib/models/common/badge_type.dart b/lib/models/common/badge_type.dart index 65994173a..20bc3fd3a 100644 --- a/lib/models/common/badge_type.dart +++ b/lib/models/common/badge_type.dart @@ -8,6 +8,7 @@ enum PBadgeType { line_primary, line_secondary, free, + shop, } enum PBadgeSize { medium, small } diff --git a/lib/models/common/member/tab_type.dart b/lib/models/common/member/tab_type.dart index 8cbb2e35f..3ad376621 100644 --- a/lib/models/common/member/tab_type.dart +++ b/lib/models/common/member/tab_type.dart @@ -1,3 +1,5 @@ +import 'package:PiliPlus/utils/storage_pref.dart'; + enum MemberTabType { def('默认'), home('主页'), @@ -5,9 +7,15 @@ enum MemberTabType { contribute('投稿'), favorite('收藏'), bangumi('番剧'), - cheese('课堂'); + cheese('课堂'), + shop('小店'); + + static bool showMemberShop = Pref.showMemberShop; static bool contains(String type) { + if (type == shop.name && !showMemberShop) { + return false; + } for (var e in MemberTabType.values) { if (e.name == type) { return true; diff --git a/lib/models_new/space/space_shop/below_label.dart b/lib/models_new/space/space_shop/below_label.dart new file mode 100644 index 000000000..a016458f5 --- /dev/null +++ b/lib/models_new/space/space_shop/below_label.dart @@ -0,0 +1,44 @@ +class BelowLabel { + int? tagType; + String? title; + String? titleDayColor1; + String? titleDayColor2; + String? titleNightColor1; + String? titleNightColor2; + int? cornerRadius; + int? useBoard; + String? backDayColor1; + String? backDayColor2; + String? backNightColor1; + String? backNightColor2; + + BelowLabel({ + this.tagType, + this.title, + this.titleDayColor1, + this.titleDayColor2, + this.titleNightColor1, + this.titleNightColor2, + this.cornerRadius, + this.useBoard, + this.backDayColor1, + this.backDayColor2, + this.backNightColor1, + this.backNightColor2, + }); + + factory BelowLabel.fromJson(Map json) => BelowLabel( + tagType: json['tagType'] as int?, + title: json['title'] as String?, + titleDayColor1: json['titleDayColor1'] as String?, + titleDayColor2: json['titleDayColor2'] as String?, + titleNightColor1: json['titleNightColor1'] as String?, + titleNightColor2: json['titleNightColor2'] as String?, + cornerRadius: json['cornerRadius'] as int?, + useBoard: json['useBoard'] as int?, + backDayColor1: json['backDayColor1'] as String?, + backDayColor2: json['backDayColor2'] as String?, + backNightColor1: json['backNightColor1'] as String?, + backNightColor2: json['backNightColor2'] as String?, + ); +} diff --git a/lib/models_new/space/space_shop/benefit_info.dart b/lib/models_new/space/space_shop/benefit_info.dart new file mode 100644 index 000000000..374ab2d61 --- /dev/null +++ b/lib/models_new/space/space_shop/benefit_info.dart @@ -0,0 +1,13 @@ +class BenefitInfo { + String? prefix; + String? amount; + dynamic suffix; + + BenefitInfo({this.prefix, this.amount, this.suffix}); + + factory BenefitInfo.fromJson(Map json) => BenefitInfo( + prefix: json['prefix'] as String?, + amount: json['amount'] as String?, + suffix: json['suffix'] as dynamic, + ); +} diff --git a/lib/models_new/space/space_shop/cover.dart b/lib/models_new/space/space_shop/cover.dart new file mode 100644 index 000000000..be2fe0096 --- /dev/null +++ b/lib/models_new/space/space_shop/cover.dart @@ -0,0 +1,17 @@ +class Cover { + String? url; + String? imgWh; + int? height; + int? width; + dynamic size; + + Cover({this.url, this.imgWh, this.height, this.width, this.size}); + + factory Cover.fromJson(Map json) => Cover( + url: json['url'] as String?, + imgWh: json['imgWH'] as String?, + height: json['height'] as int?, + width: json['width'] as int?, + size: json['size'] as dynamic, + ); +} diff --git a/lib/models_new/space/space_shop/data.dart b/lib/models_new/space/space_shop/data.dart new file mode 100644 index 000000000..16f4f4d56 --- /dev/null +++ b/lib/models_new/space/space_shop/data.dart @@ -0,0 +1,30 @@ +import 'package:PiliPlus/models_new/space/space_shop/item.dart'; + +class SpaceShopData { + List? data; + bool? showMoreTab; + String? clickUrl; + String? showMoreDesc; + bool? haveNextPage; + int? nextSearchAfter; + + SpaceShopData({ + this.data, + this.showMoreTab, + this.clickUrl, + this.showMoreDesc, + this.haveNextPage, + this.nextSearchAfter, + }); + + factory SpaceShopData.fromJson(Map json) => SpaceShopData( + data: (json['data'] as List?) + ?.map((e) => SpaceShopItem.fromJson(e as Map)) + .toList(), + showMoreTab: json['showMoreTab'] as bool?, + clickUrl: json['clickUrl'] as String?, + showMoreDesc: json['showMoreDesc'] as String?, + haveNextPage: json['haveNextPage'] as bool?, + nextSearchAfter: json['nextSearchAfter'] as int?, + ); +} diff --git a/lib/models_new/space/space_shop/item.dart b/lib/models_new/space/space_shop/item.dart new file mode 100644 index 000000000..c3f93a92d --- /dev/null +++ b/lib/models_new/space/space_shop/item.dart @@ -0,0 +1,144 @@ +import 'package:PiliPlus/models_new/space/space_shop/below_label.dart'; +import 'package:PiliPlus/models_new/space/space_shop/benefit_info.dart'; +import 'package:PiliPlus/models_new/space/space_shop/cover.dart'; +import 'package:PiliPlus/models_new/space/space_shop/net_price.dart'; +import 'package:PiliPlus/models_new/space/space_shop/report_params.dart'; +import 'package:PiliPlus/models_new/space/space_shop/source_desc.dart'; +import 'package:PiliPlus/models_new/space/space_shop/source_front_tag.dart'; +import 'package:PiliPlus/utils/extension.dart'; + +class SpaceShopItem { + String? contentId; + int? contentType; + dynamic contentSubType; + dynamic trackId; + Cover? cover; + String? title; + dynamic subTitle; + String? cardUrl; + List? belowLabels; + dynamic topRightLabels; + dynamic bottomRightLabels; + dynamic topLeftLabels; + dynamic bottomLeftLabels; + List? titleFrontLabels; + dynamic priceBehindLabels; + NetPrice? netPrice; + dynamic userInteractInfos; + List? benefitInfos; + ReportParams? reportParams; + dynamic ichibanItem; + bool? isMarketItem; + dynamic remainBoxStr; + dynamic surpriseTips; + String? outSchemaUrl; + int? itemCode; + dynamic merchantId; + int? itemSource; + String? itemSourceName; + SourceDesc? sourceDesc; + SourceFrontTag? sourceFrontTag; + List? openWhiteList; + bool? sellOut; + int? status; + bool? preSaleEnd; + bool? preSaleNotStart; + int? jumpType; + dynamic lrpriceStr; + + SpaceShopItem({ + this.contentId, + this.contentType, + this.contentSubType, + this.trackId, + this.cover, + this.title, + this.subTitle, + this.cardUrl, + this.belowLabels, + this.topRightLabels, + this.bottomRightLabels, + this.topLeftLabels, + this.bottomLeftLabels, + this.titleFrontLabels, + this.priceBehindLabels, + this.netPrice, + this.userInteractInfos, + this.benefitInfos, + this.reportParams, + this.ichibanItem, + this.isMarketItem, + this.remainBoxStr, + this.surpriseTips, + this.outSchemaUrl, + this.itemCode, + this.merchantId, + this.itemSource, + this.itemSourceName, + this.sourceDesc, + this.sourceFrontTag, + this.openWhiteList, + this.sellOut, + this.status, + this.preSaleEnd, + this.preSaleNotStart, + this.jumpType, + this.lrpriceStr, + }); + + factory SpaceShopItem.fromJson(Map json) => SpaceShopItem( + contentId: json['contentId'] as String?, + contentType: json['contentType'] as int?, + contentSubType: json['contentSubType'] as dynamic, + trackId: json['trackId'] as dynamic, + cover: json['cover'] == null + ? null + : Cover.fromJson(json['cover'] as Map), + title: json['title'] as String?, + subTitle: json['subTitle'] as dynamic, + cardUrl: json['cardUrl'] as String?, + belowLabels: (json['belowLabels'] as List?) + ?.map((e) => BelowLabel.fromJson(e as Map)) + .toList(), + topRightLabels: json['topRightLabels'] as dynamic, + bottomRightLabels: json['bottomRightLabels'] as dynamic, + topLeftLabels: json['topLeftLabels'] as dynamic, + bottomLeftLabels: json['bottomLeftLabels'] as dynamic, + titleFrontLabels: json['titleFrontLabels'] as List?, + priceBehindLabels: json['priceBehindLabels'] as dynamic, + netPrice: json['netPrice'] == null + ? null + : NetPrice.fromJson(json['netPrice'] as Map), + userInteractInfos: json['userInteractInfos'] as dynamic, + benefitInfos: (json['benefitInfos'] as List?) + ?.map((e) => BenefitInfo.fromJson(e as Map)) + .toList(), + reportParams: json['reportParams'] == null + ? null + : ReportParams.fromJson(json['reportParams'] as Map), + ichibanItem: json['ichibanItem'] as dynamic, + isMarketItem: json['isMarketItem'] as bool?, + remainBoxStr: json['remainBoxStr'] as dynamic, + surpriseTips: json['surpriseTips'] as dynamic, + outSchemaUrl: json['outSchemaUrl'] as String?, + itemCode: json['itemCode'] as int?, + merchantId: json['merchantId'] as dynamic, + itemSource: json['itemSource'] as int?, + itemSourceName: json['itemSourceName'] as String?, + sourceDesc: json['sourceDesc'] == null + ? null + : SourceDesc.fromJson(json['sourceDesc'] as Map), + sourceFrontTag: json['sourceFrontTag'] == null + ? null + : SourceFrontTag.fromJson( + json['sourceFrontTag'] as Map, + ), + openWhiteList: (json['openWhiteList'] as List?)?.fromCast(), + sellOut: json['sellOut'] as bool?, + status: json['status'] as int?, + preSaleEnd: json['preSaleEnd'] as bool?, + preSaleNotStart: json['preSaleNotStart'] as bool?, + jumpType: json['jumpType'] as int?, + lrpriceStr: json['lrpriceStr'] as dynamic, + ); +} diff --git a/lib/models_new/space/space_shop/net_price.dart b/lib/models_new/space/space_shop/net_price.dart new file mode 100644 index 000000000..1f6cc123a --- /dev/null +++ b/lib/models_new/space/space_shop/net_price.dart @@ -0,0 +1,13 @@ +class NetPrice { + String? netPrice; + String? priceSymbol; + String? pricePrefix; + + NetPrice({this.netPrice, this.priceSymbol, this.pricePrefix}); + + factory NetPrice.fromJson(Map json) => NetPrice( + netPrice: json['netPrice'] as String?, + priceSymbol: json['priceSymbol'] as String?, + pricePrefix: json['pricePrefix'] as String?, + ); +} diff --git a/lib/models_new/space/space_shop/report_params.dart b/lib/models_new/space/space_shop/report_params.dart new file mode 100644 index 000000000..41a20f346 --- /dev/null +++ b/lib/models_new/space/space_shop/report_params.dart @@ -0,0 +1,11 @@ +class ReportParams { + String? trail; + String? trackId; + + ReportParams({this.trail, this.trackId}); + + factory ReportParams.fromJson(Map json) => ReportParams( + trail: json['trail'] as String?, + trackId: json['track_id'] as String?, + ); +} diff --git a/lib/models_new/space/space_shop/source_desc.dart b/lib/models_new/space/space_shop/source_desc.dart new file mode 100644 index 000000000..659fff6c2 --- /dev/null +++ b/lib/models_new/space/space_shop/source_desc.dart @@ -0,0 +1,44 @@ +class SourceDesc { + int? tagType; + String? title; + String? titleDayColor1; + String? titleDayColor2; + String? titleNightColor1; + String? titleNightColor2; + int? cornerRadius; + int? useBoard; + String? backDayColor1; + String? backDayColor2; + String? backNightColor1; + String? backNightColor2; + + SourceDesc({ + this.tagType, + this.title, + this.titleDayColor1, + this.titleDayColor2, + this.titleNightColor1, + this.titleNightColor2, + this.cornerRadius, + this.useBoard, + this.backDayColor1, + this.backDayColor2, + this.backNightColor1, + this.backNightColor2, + }); + + factory SourceDesc.fromJson(Map json) => SourceDesc( + tagType: json['tagType'] as int?, + title: json['title'] as String?, + titleDayColor1: json['titleDayColor1'] as String?, + titleDayColor2: json['titleDayColor2'] as String?, + titleNightColor1: json['titleNightColor1'] as String?, + titleNightColor2: json['titleNightColor2'] as String?, + cornerRadius: json['cornerRadius'] as int?, + useBoard: json['useBoard'] as int?, + backDayColor1: json['backDayColor1'] as String?, + backDayColor2: json['backDayColor2'] as String?, + backNightColor1: json['backNightColor1'] as String?, + backNightColor2: json['backNightColor2'] as String?, + ); +} diff --git a/lib/models_new/space/space_shop/source_front_tag.dart b/lib/models_new/space/space_shop/source_front_tag.dart new file mode 100644 index 000000000..a79c6e236 --- /dev/null +++ b/lib/models_new/space/space_shop/source_front_tag.dart @@ -0,0 +1,46 @@ +class SourceFrontTag { + int? tagType; + String? title; + String? titleDayColor1; + String? titleDayColor2; + String? titleNightColor1; + String? titleNightColor2; + int? cornerRadius; + int? useBoard; + String? backDayColor1; + String? backDayColor2; + String? backNightColor1; + String? backNightColor2; + + SourceFrontTag({ + this.tagType, + this.title, + this.titleDayColor1, + this.titleDayColor2, + this.titleNightColor1, + this.titleNightColor2, + this.cornerRadius, + this.useBoard, + this.backDayColor1, + this.backDayColor2, + this.backNightColor1, + this.backNightColor2, + }); + + factory SourceFrontTag.fromJson(Map json) { + return SourceFrontTag( + tagType: json['tagType'] as int?, + title: json['title'] as String?, + titleDayColor1: json['titleDayColor1'] as String?, + titleDayColor2: json['titleDayColor2'] as String?, + titleNightColor1: json['titleNightColor1'] as String?, + titleNightColor2: json['titleNightColor2'] as String?, + cornerRadius: json['cornerRadius'] as int?, + useBoard: json['useBoard'] as int?, + backDayColor1: json['backDayColor1'] as String?, + backDayColor2: json['backDayColor2'] as String?, + backNightColor1: json['backNightColor1'] as String?, + backNightColor2: json['backNightColor2'] as String?, + ); + } +} diff --git a/lib/pages/member/view.dart b/lib/pages/member/view.dart index 38b245f8f..2b9e34fc1 100644 --- a/lib/pages/member/view.dart +++ b/lib/pages/member/view.dart @@ -17,6 +17,7 @@ import 'package:PiliPlus/pages/member_dynamics/view.dart'; import 'package:PiliPlus/pages/member_favorite/view.dart'; import 'package:PiliPlus/pages/member_home/view.dart'; import 'package:PiliPlus/pages/member_pgc/view.dart'; +import 'package:PiliPlus/pages/member_shop/view.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:flutter/material.dart'; @@ -319,6 +320,10 @@ class _MemberPageState extends State { heroTag: _heroTag, mid: _mid, ), + 'shop' => MemberShop( + heroTag: _heroTag, + mid: _mid, + ), _ => Center(child: Text(item.title ?? '')), }; }).toList(), diff --git a/lib/pages/member_opus/view.dart b/lib/pages/member_opus/view.dart index d568da2df..db01c8b30 100644 --- a/lib/pages/member_opus/view.dart +++ b/lib/pages/member_opus/view.dart @@ -57,7 +57,7 @@ class _MemberOpusState extends State top: widget.isSingle ? 12 : 0, left: StyleString.safeSpace, right: StyleString.safeSpace, - bottom: bottom + 90, + bottom: bottom + 100, ), sliver: Obx(() => _buildBody(_controller.loadingState.value)), ), diff --git a/lib/pages/member_shop/controller.dart b/lib/pages/member_shop/controller.dart new file mode 100644 index 000000000..99adefb69 --- /dev/null +++ b/lib/pages/member_shop/controller.dart @@ -0,0 +1,35 @@ +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/http/member.dart'; +import 'package:PiliPlus/models_new/space/space_shop/data.dart'; +import 'package:PiliPlus/models_new/space/space_shop/item.dart'; +import 'package:PiliPlus/pages/common/common_list_controller.dart'; + +class MemberShopController + extends CommonListController { + MemberShopController(this.mid); + + final int mid; + + @override + void onInit() { + super.onInit(); + queryData(); + } + + bool? showMoreTab; + String? clickUrl; + String? showMoreDesc; + + @override + List? getDataList(SpaceShopData response) { + isEnd = response.haveNextPage == false; + showMoreTab = response.showMoreTab; + clickUrl = response.clickUrl; + showMoreDesc = response.showMoreDesc; + return response.data; + } + + @override + Future> customGetData() => + MemberHttp.spaceShop(mid: mid); +} diff --git a/lib/pages/member_shop/view.dart b/lib/pages/member_shop/view.dart new file mode 100644 index 000000000..cdcc1274d --- /dev/null +++ b/lib/pages/member_shop/view.dart @@ -0,0 +1,139 @@ +import 'package:PiliPlus/common/constants.dart'; +import 'package:PiliPlus/common/skeleton/space_opus.dart'; +import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; +import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models_new/space/space_shop/item.dart'; +import 'package:PiliPlus/pages/member_shop/controller.dart'; +import 'package:PiliPlus/pages/member_shop/widgets/item.dart'; +import 'package:PiliPlus/utils/extension.dart'; +import 'package:PiliPlus/utils/grid.dart'; +import 'package:PiliPlus/utils/waterfall.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:waterfall_flow/waterfall_flow.dart' + hide SliverWaterfallFlowDelegateWithMaxCrossAxisExtent; + +class MemberShop extends StatefulWidget { + const MemberShop({ + super.key, + required this.heroTag, + required this.mid, + }); + + final String? heroTag; + final int mid; + + @override + State createState() => _MemberShopState(); +} + +class _MemberShopState extends State + with AutomaticKeepAliveClientMixin { + late final _controller = Get.put( + MemberShopController(widget.mid), + tag: widget.heroTag, + ); + + @override + Widget build(BuildContext context) { + super.build(context); + return refreshIndicator( + onRefresh: _controller.onRefresh, + child: CustomScrollView( + physics: const AlwaysScrollableScrollPhysics(), + slivers: [ + SliverPadding( + padding: EdgeInsets.only( + top: 12, + left: StyleString.safeSpace, + right: StyleString.safeSpace, + bottom: MediaQuery.viewPaddingOf(context).bottom + 100, + ), + sliver: Obx(() => _buildBody(_controller.loadingState.value)), + ), + ], + ), + ); + } + + @override + bool get wantKeepAlive => true; + + late double _maxWidth; + + late final gridDelegate = SliverWaterfallFlowDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: Grid.smallCardWidth, + mainAxisSpacing: StyleString.safeSpace, + crossAxisSpacing: StyleString.safeSpace, + callback: (value) => _maxWidth = value, + ); + + Widget _buildBody(LoadingState?> loadingState) { + switch (loadingState) { + case Loading(): + return SliverWaterfallFlow( + gridDelegate: gridDelegate, + delegate: SliverChildBuilderDelegate( + (context, index) => const SpaceOpusSkeleton(), + childCount: 10, + ), + ); + case Success(:var response): + if (response.isNullOrEmpty) { + return HttpError(onReload: _controller.onReload); + } + Widget sliver = SliverWaterfallFlow( + gridDelegate: gridDelegate, + delegate: SliverChildBuilderDelegate( + (_, index) { + return MemberShopItem( + item: response[index], + maxWidth: _maxWidth, + ); + }, + childCount: response!.length, + ), + ); + if (_controller.showMoreTab == true) { + sliver = SliverMainAxisGroup( + slivers: [ + sliver, + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only(top: 25), + child: Center( + child: FilledButton.tonal( + onPressed: () { + if (_controller.clickUrl case String clickUrl) { + final url = Uri.parse( + clickUrl, + ).queryParameters['url']; + if (url case String url) { + Get.toNamed( + '/webview', + parameters: {'url': url}, + ); + } + } + }, + style: FilledButton.styleFrom( + visualDensity: VisualDensity.compact, + ), + child: Text(_controller.showMoreDesc ?? ''), + ), + ), + ), + ), + ], + ); + } + return sliver; + case Error(:var errMsg): + return HttpError( + errMsg: errMsg, + onReload: _controller.onReload, + ); + } + } +} diff --git a/lib/pages/member_shop/widgets/item.dart b/lib/pages/member_shop/widgets/item.dart new file mode 100644 index 000000000..dceb775ad --- /dev/null +++ b/lib/pages/member_shop/widgets/item.dart @@ -0,0 +1,123 @@ +import 'package:PiliPlus/common/widgets/badge.dart'; +import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; +import 'package:PiliPlus/models/common/badge_type.dart'; +import 'package:PiliPlus/models_new/space/space_shop/item.dart'; +import 'package:PiliPlus/models_new/space/space_shop/net_price.dart'; +import 'package:PiliPlus/utils/extension.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class MemberShopItem extends StatelessWidget { + const MemberShopItem({ + super.key, + required this.item, + required this.maxWidth, + }); + + final SpaceShopItem item; + final double maxWidth; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final belowLabels = item.belowLabels?.map((e) => e.title).join('|'); + return Card( + clipBehavior: Clip.hardEdge, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(6)), + ), + child: InkWell( + onTap: () { + if (item.cardUrl case String cardUrl) { + Get.toNamed('/webview', parameters: {'url': cardUrl}); + } + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + NetworkImgLayer( + radius: 0, + src: item.cover?.url, + width: maxWidth, + height: maxWidth, + ), + Padding( + padding: const EdgeInsets.all(8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.title!, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + if (belowLabels?.isNotEmpty == true) + Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: PBadge( + text: belowLabels, + type: PBadgeType.shop, + size: PBadgeSize.small, + isStack: false, + fontSize: 10, + padding: const EdgeInsets.symmetric( + horizontal: 3, + vertical: 2, + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (item.netPrice case NetPrice netPrice) + Text.rich( + style: TextStyle(color: colorScheme.vipColor), + TextSpan( + children: [ + if (netPrice.pricePrefix?.isNotEmpty == true) + TextSpan( + text: '${netPrice.pricePrefix} ', + style: const TextStyle(fontSize: 11), + ), + TextSpan( + text: + '${netPrice.priceSymbol}${netPrice.netPrice}', + style: const TextStyle( + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + if (item.benefitInfos?.isNotEmpty == true) + Text( + item.benefitInfos! + .map( + (e) => + '${e.prefix ?? ''}${e.amount ?? ''}${e.suffix ?? ''}', + ) + .join('|'), + style: TextStyle( + fontSize: 11, + color: colorScheme.outline, + ), + ), + ], + ), + if (item.itemSourceName?.isNotEmpty == true) + Text( + '来自${item.itemSourceName}', + style: TextStyle( + fontSize: 11, + color: colorScheme.freeColor, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/setting/models/extra_settings.dart b/lib/pages/setting/models/extra_settings.dart index 795cc6273..f6d2685d6 100644 --- a/lib/pages/setting/models/extra_settings.dart +++ b/lib/pages/setting/models/extra_settings.dart @@ -921,6 +921,14 @@ List get extraSettings => [ } }, ), + SettingsModel( + settingsType: SettingsType.sw1tch, + title: '显示UP主页小店TAB', + leading: const Icon(Icons.shop_outlined), + setKey: SettingBoxKey.showMemberShop, + defaultVal: false, + onChanged: (value) => MemberTabType.showMemberShop = value, + ), SettingsModel( settingsType: SettingsType.sw1tch, onTap: () { diff --git a/lib/pages/webdav/view.dart b/lib/pages/webdav/view.dart index c8ebb1c51..a1233e83e 100644 --- a/lib/pages/webdav/view.dart +++ b/lib/pages/webdav/view.dart @@ -46,7 +46,7 @@ class _WebDavSettingPageState extends State { top: 20, left: padding.left + 20, right: padding.right + 20, - bottom: padding.bottom + 90, + bottom: padding.bottom + 100, ), children: [ TextField( diff --git a/lib/utils/storage_key.dart b/lib/utils/storage_key.dart index 5c58305e1..3547d7447 100644 --- a/lib/utils/storage_key.dart +++ b/lib/utils/storage_key.dart @@ -133,7 +133,8 @@ class SettingBoxKey { quickFavId = 'quickFavId', showFsScreenshotBtn = 'showFsScreenshotBtn', showFsLockBtn = 'showFsLockBtn', - silentDownImg = 'silentDownImg'; + silentDownImg = 'silentDownImg', + showMemberShop = 'showMemberShop'; static const String subtitlePreferenceV2 = 'subtitlePreferenceV2', enableDragSubtitle = 'enableDragSubtitle', diff --git a/lib/utils/storage_pref.dart b/lib/utils/storage_pref.dart index 6a8b43124..1355b997c 100644 --- a/lib/utils/storage_pref.dart +++ b/lib/utils/storage_pref.dart @@ -804,4 +804,7 @@ class Pref { } return buvid; } + + static bool get showMemberShop => + _setting.get(SettingBoxKey.showMemberShop, defaultValue: false); }