diff --git a/lib/common/constants.dart b/lib/common/constants.dart index 1af6bcffb..deae5d47b 100644 --- a/lib/common/constants.dart +++ b/lib/common/constants.dart @@ -26,6 +26,12 @@ class Constants { '{"appId":5,"platform":3,"version":"1.46.2","abtest":""}'; // 请求时会自动encodeComponent + // app + static const String userAgentApp = + 'Mozilla/5.0 BiliDroid/8.43.0 (bbcallen@gmail.com) os/android model/android mobi_app/android build/8430300 channel/bili innerVer/8430300 osVer/15 network/2'; + static const String statisticsApp = + '{"appId":5,"platform":3,"version":"8.43.0","abtest":""}'; + static const urlPattern = r'https?://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]'; diff --git a/lib/common/skeleton/space_opus.dart b/lib/common/skeleton/space_opus.dart new file mode 100644 index 000000000..0ecbcf272 --- /dev/null +++ b/lib/common/skeleton/space_opus.dart @@ -0,0 +1,48 @@ +import 'package:PiliPlus/common/skeleton/skeleton.dart'; +import 'package:PiliPlus/utils/utils.dart'; +import 'package:flutter/material.dart'; + +class SpaceOpusSkeleton extends StatelessWidget { + const SpaceOpusSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + final surface = Theme.of(context).colorScheme.onInverseSurface; + return Skeleton( + child: Card( + clipBehavior: Clip.hardEdge, + margin: EdgeInsets.zero, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(6)), + ), + child: LayoutBuilder( + builder: (context, constraints) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: (0.68 + 0.82 * Utils.random.nextDouble()) * + constraints.maxWidth, + color: surface, + ), + Container( + height: 10, + color: surface, + margin: const EdgeInsets.all(10), + width: constraints.maxWidth * 0.7, + ), + Container( + height: 10, + color: surface, + margin: + const EdgeInsets.only(left: 10, right: 10, bottom: 10), + width: constraints.maxWidth, + ), + ], + ); + }, + ), + ), + ); + } +} diff --git a/lib/common/skeleton/video_card_h.dart b/lib/common/skeleton/video_card_h.dart index 9030b8afc..e3efed202 100644 --- a/lib/common/skeleton/video_card_h.dart +++ b/lib/common/skeleton/video_card_h.dart @@ -22,7 +22,7 @@ class VideoCardHSkeleton extends StatelessWidget { aspectRatio: StyleString.aspectRatio, child: LayoutBuilder( builder: (context, boxConstraints) { - return Container( + return DecoratedBox( decoration: BoxDecoration( color: color, borderRadius: StyleString.mdRadius, diff --git a/lib/common/skeleton/video_card_v.dart b/lib/common/skeleton/video_card_v.dart index 398fabee5..265ba58c8 100644 --- a/lib/common/skeleton/video_card_v.dart +++ b/lib/common/skeleton/video_card_v.dart @@ -15,7 +15,7 @@ class VideoCardVSkeleton extends StatelessWidget { aspectRatio: StyleString.aspectRatio, child: LayoutBuilder( builder: (context, boxConstraints) { - return Container( + return DecoratedBox( decoration: BoxDecoration( color: color, borderRadius: StyleString.mdRadius, diff --git a/lib/grpc/grpc_repo.dart b/lib/grpc/grpc_repo.dart index f10c9be85..4cdcc4e43 100644 --- a/lib/grpc/grpc_repo.dart +++ b/lib/grpc/grpc_repo.dart @@ -3,6 +3,7 @@ import 'dart:typed_data'; import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/grpc/bilibili/app/dynamic/v1.pb.dart'; +import 'package:PiliPlus/grpc/bilibili/app/dynamic/v2.pb.dart'; import 'package:PiliPlus/grpc/bilibili/app/im/v1.pb.dart'; import 'package:PiliPlus/grpc/bilibili/community/service/dm/v1.pb.dart'; import 'package:PiliPlus/grpc/bilibili/im/interfaces/v1.pb.dart'; @@ -32,8 +33,12 @@ class GrpcUrl { // static const popular = '/bilibili.app.show.v1.Popular/Index'; // dynamic - static const dynRed = '/bilibili.app.dynamic.v1.Dynamic/DynRed'; - // static const dynSpace = '/bilibili.app.dynamic.v2.Dynamic/DynSpace'; + static const dynV1 = '/bilibili.app.dynamic.v1.Dynamic'; + static const dynV2 = '/bilibili.app.dynamic.v2.Dynamic'; + static const opusV2 = '/bilibili.app.dynamic.v2.Opus'; + static const dynRed = '$dynV1/DynRed'; + static const opusSpaceFlow = '$opusV2/OpusSpaceFlow'; + // static const dynSpace = '$dynV2/DynSpace'; // danmaku static const dmSegMobile = '/bilibili.community.service.dm.v1.DM/DmSegMobile'; @@ -397,4 +402,23 @@ class GrpcRepo { ClearUnreadReply.fromBuffer, ); } + + static Future opusSpaceFlow({ + required int hostMid, + String? next, + required String filterType, + }) { + return _request( + GrpcUrl.opusSpaceFlow, + OpusSpaceFlowReq( + hostMid: Int64(hostMid), + pagination: Pagination( + pageSize: 20, + next: next, + ), + filterType: filterType, + ), + OpusSpaceFlowResp.fromBuffer, + ); + } } diff --git a/lib/http/api.dart b/lib/http/api.dart index 88cd499ec..5a33e8608 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -827,4 +827,6 @@ class Api { '${HttpString.appBaseUrl}/x/topic/web/details/top'; static const String topicFeed = '/x/polymer/web-dynamic/v1/feed/topic'; + + static const String spaceOpus = '/x/polymer/web-dynamic/v1/opus/feed/space'; } diff --git a/lib/http/member.dart b/lib/http/member.dart index b81e18353..2e521b48b 100644 --- a/lib/http/member.dart +++ b/lib/http/member.dart @@ -2,6 +2,9 @@ import 'dart:convert'; import 'dart:io'; import 'package:PiliPlus/common/constants.dart'; +import 'package:PiliPlus/grpc/bilibili/app/dynamic/v2.pb.dart' + show OpusSpaceFlowResp; +import 'package:PiliPlus/grpc/grpc_repo.dart'; import 'package:PiliPlus/http/api.dart'; import 'package:PiliPlus/http/constants.dart'; import 'package:PiliPlus/http/init.dart'; @@ -18,6 +21,7 @@ import 'package:PiliPlus/models/space/data.dart'; import 'package:PiliPlus/models/space_archive/data.dart'; import 'package:PiliPlus/models/space_article/data.dart'; import 'package:PiliPlus/models/space_fav/space_fav.dart'; +import 'package:PiliPlus/models/space_opus/data.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/wbi_sign.dart'; @@ -65,15 +69,15 @@ class MemberHttp { required int page, }) async { Map data = { - 'build': '1462100', + 'build': '8430300', 'c_locale': 'zh_CN', - 'channel': 'yingyongbao', - 'mobi_app': 'android_hd', + 'channel': 'bili', + 'mobi_app': 'android', 'platform': 'android', - 'pn': '$page', + 'pn': page.toString(), 'ps': '10', 's_locale': 'zh_CN', - 'statistics': Constants.statistics, + 'statistics': Constants.statisticsApp, 'vmid': mid.toString(), }; dynamic res = await Request().get( @@ -82,7 +86,7 @@ class MemberHttp { options: Options( headers: { 'bili-http-engine': 'cronet', - 'user-agent': Constants.userAgent, + 'user-agent': Constants.userAgentApp, }, ), ); @@ -97,13 +101,13 @@ class MemberHttp { required int mid, }) async { Map data = { - 'build': '1462100', + 'build': '8430300', 'c_locale': 'zh_CN', - 'channel': 'yingyongbao', - 'mobi_app': 'android_hd', + 'channel': 'bili', + 'mobi_app': 'android', 'platform': 'android', 's_locale': 'zh_CN', - 'statistics': Constants.statistics, + 'statistics': Constants.statisticsApp, 'up_mid': mid.toString(), }; dynamic res = await Request().get( @@ -112,7 +116,7 @@ class MemberHttp { options: Options( headers: { 'bili-http-engine': 'cronet', - 'user-agent': Constants.userAgent, + 'user-agent': Constants.userAgentApp, }, ), ); @@ -156,10 +160,10 @@ class MemberHttp { }) async { Map data = { if (aid != null) 'aid': aid.toString(), - 'build': '1462100', + 'build': '8430300', 'c_locale': 'zh_CN', - 'channel': 'yingyongbao', - 'mobi_app': 'android_hd', + 'channel': 'bili', + 'mobi_app': 'android', 'platform': 'android', 's_locale': 'zh_CN', 'ps': '20', @@ -171,7 +175,7 @@ class MemberHttp { if (order != null) 'order': order, if (sort != null) 'sort': sort, if (includeCursor != null) 'include_cursor': includeCursor.toString(), - 'statistics': Constants.statistics, + 'statistics': Constants.statisticsApp, 'vmid': mid.toString(), }; dynamic res = await Request().get( @@ -186,7 +190,7 @@ class MemberHttp { options: Options( headers: { 'bili-http-engine': 'cronet', - 'user-agent': Constants.userAgent, + 'user-agent': Constants.userAgentApp, }, ), ); @@ -213,13 +217,13 @@ class MemberHttp { 'cid': cid.toString(), 'contain': contain.toString(), 'index': index.toString(), - 'build': '1462100', + 'build': '8430300', 'c_locale': 'zh_CN', - 'channel': 'yingyongbao', - 'mobi_app': 'android_hd', + 'channel': 'bili', + 'mobi_app': 'android', 'platform': 'android', 's_locale': 'zh_CN', - 'statistics': Constants.statistics, + 'statistics': Constants.statisticsApp, 'vmid': mid.toString(), }; dynamic res = await Request().get( @@ -228,7 +232,7 @@ class MemberHttp { options: Options( headers: { 'bili-http-engine': 'cronet', - 'user-agent': Constants.userAgent, + 'user-agent': Constants.userAgentApp, }, ), ); @@ -244,14 +248,14 @@ class MemberHttp { dynamic fromViewAid, }) async { Map data = { - 'build': '1462100', + 'build': '8430300', 'c_locale': 'zh_CN', - 'channel': 'yingyongbao', - 'mobi_app': 'android_hd', + 'channel': 'bili', + 'mobi_app': 'android', 'platform': 'android', 's_locale': 'zh_CN', if (fromViewAid != null) 'from_view_aid': fromViewAid, - 'statistics': Constants.statistics, + 'statistics': Constants.statisticsApp, 'vmid': mid.toString(), }; dynamic res = await Request().get( @@ -260,7 +264,7 @@ class MemberHttp { options: Options( headers: { 'bili-http-engine': 'cronet', - 'user-agent': Constants.userAgent, + 'user-agent': Constants.userAgentApp, }, ), ); @@ -843,4 +847,44 @@ class MemberHttp { return LoadingState.error(res.data['message']); } } + + static Future> opusSpaceFlow({ + required int hostMid, + String? next, + required String filterType, + }) async { + var res = await GrpcRepo.opusSpaceFlow( + hostMid: hostMid, + next: next, + filterType: filterType, + ); + if (res['status']) { + return LoadingState.success(res['data']); + } else { + return LoadingState.error(res['msg']); + } + } + + static Future> spaceOpus({ + required int hostMid, + required int page, + String offset = '', + String type = 'all', + }) async { + var res = await Request().get( + Api.spaceOpus, + queryParameters: await WbiSign.makSign({ + 'host_mid': hostMid, + 'page': page, + 'offset': offset, + 'type': type, + 'web_location': 333.1387, + }), + ); + if (res.data['code'] == 0) { + return LoadingState.success(SpaceOpusData.fromJson(res.data['data'])); + } else { + return LoadingState.error(res.data['message']); + } + } } diff --git a/lib/models/space/filter.dart b/lib/models/space/filter.dart new file mode 100644 index 000000000..3d6b17518 --- /dev/null +++ b/lib/models/space/filter.dart @@ -0,0 +1,31 @@ +class SpaceTabFilter { + SpaceTabFilter({ + this.text, + required this.meta, + this.tabName, + }); + + String? text; + late String meta; + String? tabName; + + SpaceTabFilter.fromJson(Map json) { + text = json['text']; + meta = json['meta'] ?? 'all'; + tabName = json['tab_ame']; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other is SpaceTabFilter) { + return meta == other.meta; + } + return false; + } + + @override + int get hashCode => meta.hashCode; +} diff --git a/lib/models/space/item.dart b/lib/models/space/item.dart index 47ac9c494..737ae1d76 100644 --- a/lib/models/space/item.dart +++ b/lib/models/space/item.dart @@ -1,3 +1,4 @@ +import 'package:PiliPlus/models/space/filter.dart'; import 'package:json_annotation/json_annotation.dart'; part 'item.g.dart'; @@ -56,6 +57,7 @@ class SpaceItem { int? iconType; @JsonKey(name: 'publish_time_text') String? publishTimeText; + List? filter; SpaceItem({ this.title, @@ -92,6 +94,7 @@ class SpaceItem { this.viewContent, this.iconType, this.publishTimeText, + this.filter, }); factory SpaceItem.fromJson(Map json) => _$ItemFromJson(json); diff --git a/lib/models/space/item.g.dart b/lib/models/space/item.g.dart index 11abbf26d..f0a5dfc92 100644 --- a/lib/models/space/item.g.dart +++ b/lib/models/space/item.g.dart @@ -43,6 +43,9 @@ SpaceItem _$ItemFromJson(Map json) => SpaceItem( viewContent: json['view_content'] as String?, iconType: (json['icon_type'] as num?)?.toInt(), publishTimeText: json['publish_time_text'] as String?, + filter: (json['filter'] as List?) + ?.map((e) => SpaceTabFilter.fromJson(e)) + .toList(), ); Map _$ItemToJson(SpaceItem instance) => { diff --git a/lib/models/space_opus/cover.dart b/lib/models/space_opus/cover.dart new file mode 100644 index 000000000..0fc04f549 --- /dev/null +++ b/lib/models/space_opus/cover.dart @@ -0,0 +1,27 @@ +import 'package:flutter/foundation.dart'; + +class Cover { + int? height; + String? url; + int? width; + late double ratio; + + Cover({this.height, this.url, this.width, required this.ratio}); + + Cover.fromJson(Map json) { + height = json['height'] as int?; + url = json['url'] as String?; + width = json['width'] as int?; + if (height != null && width != null) { + ratio = clampDouble(height! / width!, 0.68, 2.7); + } else { + ratio = 1; + } + } + + Map toJson() => { + 'height': height, + 'url': url, + 'width': width, + }; +} diff --git a/lib/models/space_opus/data.dart b/lib/models/space_opus/data.dart new file mode 100644 index 000000000..66cb46cbe --- /dev/null +++ b/lib/models/space_opus/data.dart @@ -0,0 +1,26 @@ +import 'package:PiliPlus/models/space_opus/item.dart'; + +class SpaceOpusData { + bool? hasMore; + List? items; + String? offset; + int? updateNum; + + SpaceOpusData({this.hasMore, this.items, this.offset, this.updateNum}); + + factory SpaceOpusData.fromJson(Map json) => SpaceOpusData( + hasMore: json['has_more'] as bool?, + items: (json['items'] as List?) + ?.map((e) => SpaceOpusItemModel.fromJson(e as Map)) + .toList(), + offset: json['offset'] as String?, + updateNum: json['update_num'] as int?, + ); + + Map toJson() => { + 'has_more': hasMore, + 'items': items?.map((e) => e.toJson()).toList(), + 'offset': offset, + 'update_num': updateNum, + }; +} diff --git a/lib/models/space_opus/item.dart b/lib/models/space_opus/item.dart new file mode 100644 index 000000000..1a4e715df --- /dev/null +++ b/lib/models/space_opus/item.dart @@ -0,0 +1,34 @@ +import 'package:PiliPlus/models/space_opus/cover.dart'; +import 'package:PiliPlus/models/space_opus/stat.dart'; + +class SpaceOpusItemModel { + String? content; + String? jumpUrl; + String? opusId; + Stat? stat; + Cover? cover; + + SpaceOpusItemModel( + {this.content, this.jumpUrl, this.opusId, this.stat, this.cover}); + + factory SpaceOpusItemModel.fromJson(Map json) => + SpaceOpusItemModel( + content: json['content'] as String?, + jumpUrl: json['jump_url'] as String?, + opusId: json['opus_id'] as String?, + stat: json['stat'] == null + ? null + : Stat.fromJson(json['stat'] as Map), + cover: json['cover'] == null + ? null + : Cover.fromJson(json['cover'] as Map), + ); + + Map toJson() => { + 'content': content, + 'jump_url': jumpUrl, + 'opus_id': opusId, + 'stat': stat?.toJson(), + 'cover': cover?.toJson(), + }; +} diff --git a/lib/models/space_opus/stat.dart b/lib/models/space_opus/stat.dart new file mode 100644 index 000000000..726f15e52 --- /dev/null +++ b/lib/models/space_opus/stat.dart @@ -0,0 +1,13 @@ +class Stat { + String? like; + + Stat({this.like}); + + factory Stat.fromJson(Map json) => Stat( + like: json['like'] as String?, + ); + + Map toJson() => { + 'like': like, + }; +} diff --git a/lib/pages/member/view.dart b/lib/pages/member/view.dart index 55b2b50ff..79f37b675 100644 --- a/lib/pages/member/view.dart +++ b/lib/pages/member/view.dart @@ -261,13 +261,6 @@ class _MemberPageState extends State { ); } - Widget _errorWidget(msg) { - return errorWidget( - errMsg: msg, - onReload: _userController.onReload, - ); - } - Widget _buildUserInfo(LoadingState userState, [bool isV = true]) { return switch (userState) { Loading() => const CircularProgressIndicator(), @@ -290,7 +283,10 @@ class _MemberPageState extends State { behavior: HitTestBehavior.opaque, child: const SizedBox(height: 56, width: double.infinity), ), - Error() => _errorWidget(userState.errMsg), + Error() => scrollErrorWidget( + errMsg: userState.errMsg, + onReload: _userController.onReload, + ), }; } } diff --git a/lib/pages/member_article/view.dart b/lib/pages/member_article/view.dart index bf9f378bc..126b14d9d 100644 --- a/lib/pages/member_article/view.dart +++ b/lib/pages/member_article/view.dart @@ -1,4 +1,5 @@ -import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; +import 'package:PiliPlus/common/skeleton/video_card_h.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/space_article/item.dart'; @@ -35,44 +36,51 @@ class _MemberArticleState extends State @override Widget build(BuildContext context) { super.build(context); - return Obx(() => _buildBody(_controller.loadingState.value)); + return refreshIndicator( + onRefresh: _controller.onRefresh, + child: CustomScrollView( + slivers: [ + SliverPadding( + padding: EdgeInsets.only( + top: 7, + bottom: MediaQuery.paddingOf(context).bottom + 80, + ), + sliver: Obx(() => _buildBody(_controller.loadingState.value))) + ], + ), + ); } Widget _buildBody(LoadingState?> loadingState) { return switch (loadingState) { - Loading() => loadingWidget, + Loading() => SliverGrid( + gridDelegate: Grid.videoCardHDelegate(context), + delegate: SliverChildBuilderDelegate( + (context, index) { + return const VideoCardHSkeleton(); + }, + childCount: 10, + ), + ), Success() => loadingState.response?.isNotEmpty == true - ? refreshIndicator( - onRefresh: _controller.onRefresh, - child: CustomScrollView( - slivers: [ - SliverPadding( - padding: EdgeInsets.only( - top: 7, - bottom: MediaQuery.paddingOf(context).bottom + 80, - ), - sliver: SliverGrid( - gridDelegate: Grid.videoCardHDelegate(context), - delegate: SliverChildBuilderDelegate( - (context, index) { - if (index == loadingState.response!.length - 1) { - _controller.onLoadMore(); - } - return MemberArticleItem( - item: loadingState.response![index], - ); - }, - childCount: loadingState.response!.length, - ), - ), - ), - ], + ? SliverGrid( + gridDelegate: Grid.videoCardHDelegate(context), + delegate: SliverChildBuilderDelegate( + (context, index) { + if (index == loadingState.response!.length - 1) { + _controller.onLoadMore(); + } + return MemberArticleItem( + item: loadingState.response![index], + ); + }, + childCount: loadingState.response!.length, ), ) - : scrollErrorWidget( + : HttpError( onReload: _controller.onReload, ), - Error() => scrollErrorWidget( + Error() => HttpError( errMsg: loadingState.errMsg, onReload: _controller.onReload, ), diff --git a/lib/pages/member_contribute/view.dart b/lib/pages/member_contribute/view.dart index 5232db8e6..fc6817af7 100644 --- a/lib/pages/member_contribute/view.dart +++ b/lib/pages/member_contribute/view.dart @@ -2,6 +2,7 @@ import 'package:PiliPlus/models/common/member/contribute_type.dart'; import 'package:PiliPlus/pages/member_article/view.dart'; import 'package:PiliPlus/pages/member_audio/view.dart'; import 'package:PiliPlus/pages/member_contribute/controller.dart'; +import 'package:PiliPlus/pages/member_opus/view.dart'; import 'package:PiliPlus/pages/member_season_series/view.dart'; import 'package:PiliPlus/pages/member_video/view.dart'; import 'package:flutter/material.dart'; @@ -105,6 +106,10 @@ class _MemberContributeState extends State heroTag: widget.heroTag, mid: widget.mid, ), + 'opus' => MemberOpus( + heroTag: widget.heroTag, + mid: widget.mid, + ), 'audio' => MemberAudio(heroTag: widget.heroTag), 'season_video' => MemberVideo( type: ContributeType.season, diff --git a/lib/pages/member_dynamics/view.dart b/lib/pages/member_dynamics/view.dart index 8af70a651..1b33be5dc 100644 --- a/lib/pages/member_dynamics/view.dart +++ b/lib/pages/member_dynamics/view.dart @@ -58,9 +58,13 @@ class _MemberDynamicsPageState extends State child: CustomScrollView( physics: const AlwaysScrollableScrollPhysics(), slivers: [ - Obx( - () => _buildContent(_memberDynamicController.loadingState.value), - ) + SliverPadding( + padding: EdgeInsets.only( + bottom: MediaQuery.paddingOf(context).bottom + 80, + ), + sliver: Obx(() => + _buildContent(_memberDynamicController.loadingState.value)), + ), ], ), ); @@ -69,52 +73,47 @@ class _MemberDynamicsPageState extends State return switch (loadingState) { Loading() => DynamicsTabPage.dynSkeleton(dynamicsWaterfallFlow), Success() => loadingState.response?.isNotEmpty == true - ? SliverPadding( - padding: EdgeInsets.only( - bottom: MediaQuery.paddingOf(context).bottom + 80, - ), - sliver: dynamicsWaterfallFlow - ? SliverWaterfallFlow.extent( - maxCrossAxisExtent: Grid.smallCardWidth * 2, - crossAxisSpacing: StyleString.safeSpace, - lastChildLayoutTypeBuilder: (index) { - if (index == loadingState.response!.length - 1) { - _memberDynamicController.onLoadMore(); - } - return index == loadingState.response!.length - ? LastChildLayoutType.foot - : LastChildLayoutType.none; - }, - children: loadingState.response! - .map((item) => DynamicPanel( - item: item, - onRemove: _memberDynamicController.onRemove, - onSetTop: _memberDynamicController.onSetTop)) - .toList(), - ) - : SliverCrossAxisGroup( - slivers: [ - const SliverFillRemaining(), - SliverConstrainedCrossAxis( - maxExtent: Grid.smallCardWidth * 2, - sliver: SliverList.builder( - itemBuilder: (context, index) { - if (index == loadingState.response!.length - 1) { - _memberDynamicController.onLoadMore(); - } - return DynamicPanel( - item: loadingState.response![index], - onRemove: _memberDynamicController.onRemove, - onSetTop: _memberDynamicController.onSetTop, - ); - }, - itemCount: loadingState.response!.length, - ), - ), - const SliverFillRemaining(), - ], + ? dynamicsWaterfallFlow + ? SliverWaterfallFlow.extent( + maxCrossAxisExtent: Grid.smallCardWidth * 2, + crossAxisSpacing: StyleString.safeSpace, + lastChildLayoutTypeBuilder: (index) { + if (index == loadingState.response!.length - 1) { + _memberDynamicController.onLoadMore(); + } + return index == loadingState.response!.length + ? LastChildLayoutType.foot + : LastChildLayoutType.none; + }, + children: loadingState.response! + .map((item) => DynamicPanel( + item: item, + onRemove: _memberDynamicController.onRemove, + onSetTop: _memberDynamicController.onSetTop)) + .toList(), + ) + : SliverCrossAxisGroup( + slivers: [ + const SliverFillRemaining(), + SliverConstrainedCrossAxis( + maxExtent: Grid.smallCardWidth * 2, + sliver: SliverList.builder( + itemBuilder: (context, index) { + if (index == loadingState.response!.length - 1) { + _memberDynamicController.onLoadMore(); + } + return DynamicPanel( + item: loadingState.response![index], + onRemove: _memberDynamicController.onRemove, + onSetTop: _memberDynamicController.onSetTop, + ); + }, + itemCount: loadingState.response!.length, + ), ), - ) + const SliverFillRemaining(), + ], + ) : HttpError( onReload: _memberDynamicController.onReload, ), diff --git a/lib/pages/member_favorite/view.dart b/lib/pages/member_favorite/view.dart index a32c98d13..f0d787272 100644 --- a/lib/pages/member_favorite/view.dart +++ b/lib/pages/member_favorite/view.dart @@ -1,10 +1,12 @@ -import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; +import 'package:PiliPlus/common/skeleton/video_card_h.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/space_fav/datum.dart'; import 'package:PiliPlus/models/space_fav/list.dart'; import 'package:PiliPlus/pages/member_favorite/controller.dart'; import 'package:PiliPlus/pages/member_favorite/widget/item.dart'; +import 'package:PiliPlus/utils/grid.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -35,40 +37,48 @@ class _MemberFavoriteState extends State @override Widget build(BuildContext context) { super.build(context); - return Obx(() => _buildBody(_controller.loadingState.value)); + final theme = Theme.of(context); + return refreshIndicator( + onRefresh: _controller.onRefresh, + child: CustomScrollView( + slivers: [Obx(() => _buildBody(theme, _controller.loadingState.value))], + ), + ); } - Widget _buildBody(LoadingState loadingState) { - final theme = Theme.of(context); + Widget _buildBody(ThemeData theme, LoadingState loadingState) { return switch (loadingState) { - Loading() => loadingWidget, + Loading() => SliverGrid( + gridDelegate: Grid.videoCardHDelegate(context), + delegate: SliverChildBuilderDelegate( + (context, index) { + return const VideoCardHSkeleton(); + }, + childCount: 10, + ), + ), Success() => (loadingState.response as List?)?.isNotEmpty == true - ? refreshIndicator( - onRefresh: _controller.onRefresh, - child: CustomScrollView( - slivers: [ - SliverToBoxAdapter( - child: Obx( - () => _buildItem(theme, _controller.first.value, true), - ), + ? SliverMainAxisGroup( + slivers: [ + SliverToBoxAdapter( + child: Obx( + () => _buildItem(theme, _controller.first.value, true)), + ), + SliverToBoxAdapter( + child: Obx( + () => _buildItem(theme, _controller.second.value, false)), + ), + SliverToBoxAdapter( + child: SizedBox( + height: 80 + MediaQuery.of(context).padding.bottom, ), - SliverToBoxAdapter( - child: Obx( - () => _buildItem(theme, _controller.second.value, false), - ), - ), - SliverToBoxAdapter( - child: SizedBox( - height: 80 + MediaQuery.of(context).padding.bottom, - ), - ) - ], - ), + ), + ], ) - : scrollErrorWidget( + : HttpError( onReload: _controller.onReload, ), - Error() => scrollErrorWidget( + Error() => HttpError( errMsg: loadingState.errMsg, onReload: _controller.onReload, ), diff --git a/lib/pages/member_home/view.dart b/lib/pages/member_home/view.dart index 118d6a951..44a47a2d4 100644 --- a/lib/pages/member_home/view.dart +++ b/lib/pages/member_home/view.dart @@ -222,8 +222,8 @@ class _MemberHomeState extends State ), ], ) - : errorWidget(), - Error() => errorWidget(), + : scrollErrorWidget(), + Error() => scrollErrorWidget(), }; } diff --git a/lib/pages/member_opus/controller.dart b/lib/pages/member_opus/controller.dart new file mode 100644 index 000000000..67ff86494 --- /dev/null +++ b/lib/pages/member_opus/controller.dart @@ -0,0 +1,59 @@ +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/http/member.dart'; +import 'package:PiliPlus/models/space/filter.dart'; +import 'package:PiliPlus/models/space_opus/data.dart'; +import 'package:PiliPlus/models/space_opus/item.dart'; +import 'package:PiliPlus/pages/common/common_list_controller.dart'; +import 'package:PiliPlus/pages/member/controller.dart'; +import 'package:get/get.dart'; + +class MemberOpusController + extends CommonListController { + MemberOpusController({ + required this.heroTag, + required this.mid, + }); + + final String? heroTag; + final int mid; + + String offset = ''; + Rx type = + SpaceTabFilter(text: "全部图文", meta: "all", tabName: "图文").obs; + List? filter; + + @override + void onInit() { + super.onInit(); + filter = Get.find(tag: heroTag) + .tab2 + ?.firstWhereOrNull((e) => e.param == 'contribute') + ?.items + ?.firstWhereOrNull((e) => e.param == 'opus') + ?.filter; + queryData(); + } + + @override + Future onRefresh() { + offset = ''; + return super.onRefresh(); + } + + @override + List? getDataList(SpaceOpusData response) { + offset = response.offset ?? ''; + if (response.hasMore == false) { + isEnd = true; + } + return response.items; + } + + @override + Future> customGetData() => MemberHttp.spaceOpus( + hostMid: mid, + page: currentPage, + offset: offset, + type: type.value.meta, + ); +} diff --git a/lib/pages/member_opus/view.dart b/lib/pages/member_opus/view.dart new file mode 100644 index 000000000..0321e99b4 --- /dev/null +++ b/lib/pages/member_opus/view.dart @@ -0,0 +1,154 @@ +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/space_opus/item.dart'; +import 'package:PiliPlus/pages/member_opus/controller.dart'; +import 'package:PiliPlus/pages/member_opus/widgets/space_opus_item.dart'; +import 'package:PiliPlus/utils/grid.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:waterfall_flow/waterfall_flow.dart'; + +class MemberOpus extends StatefulWidget { + const MemberOpus({ + super.key, + required this.heroTag, + required this.mid, + }); + + final String? heroTag; + final int mid; + + @override + State createState() => _MemberOpusState(); +} + +class _MemberOpusState extends State + with AutomaticKeepAliveClientMixin { + late final _controller = Get.put( + MemberOpusController( + mid: widget.mid, + heroTag: widget.heroTag, + ), + tag: widget.heroTag, + ); + + @override + Widget build(BuildContext context) { + super.build(context); + return Stack( + children: [ + refreshIndicator( + onRefresh: _controller.onRefresh, + child: CustomScrollView( + slivers: [ + SliverPadding( + padding: EdgeInsets.only( + left: StyleString.safeSpace, + right: StyleString.safeSpace, + bottom: MediaQuery.paddingOf(context).bottom + 90, + ), + sliver: Obx(() => _buildBody(_controller.loadingState.value)), + ), + ], + ), + ), + if (_controller.filter?.isNotEmpty == true) + Positioned( + right: 16, + bottom: MediaQuery.paddingOf(context).bottom + 16, + child: FloatingActionButton.extended( + onPressed: () { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + clipBehavior: Clip.hardEdge, + contentPadding: const EdgeInsets.symmetric(vertical: 12), + content: Column( + mainAxisSize: MainAxisSize.min, + children: _controller.filter! + .map( + (e) => ListTile( + onTap: () { + if (e == _controller.type.value) { + return; + } + Get.back(); + _controller + ..type.value = e + ..onReload(); + }, + tileColor: e == _controller.type.value + ? Theme.of(context) + .colorScheme + .onInverseSurface + : null, + dense: true, + title: Text( + e.text ?? e.tabName!, + style: const TextStyle(fontSize: 14), + ), + ), + ) + .toList(), + ), + ); + }, + ); + }, + label: Row( + children: [ + const Icon(size: 20, Icons.sort), + Obx( + () => Text(_controller.type.value.text ?? + _controller.type.value.tabName!), + ), + ], + ), + ), + ), + ], + ); + } + + Widget _buildBody(LoadingState?> loadingState) { + return switch (loadingState) { + Loading() => SliverWaterfallFlow.extent( + maxCrossAxisExtent: Grid.smallCardWidth, + mainAxisSpacing: StyleString.safeSpace, + crossAxisSpacing: StyleString.safeSpace, + children: List.generate(10, (_) => const SpaceOpusSkeleton()), + ), + Success() => loadingState.response?.isNotEmpty == true + ? SliverWaterfallFlow.extent( + maxCrossAxisExtent: Grid.smallCardWidth, + mainAxisSpacing: StyleString.safeSpace, + crossAxisSpacing: StyleString.safeSpace, + lastChildLayoutTypeBuilder: (index) { + if (index == loadingState.response!.length - 1) { + _controller.onLoadMore(); + } + return index == loadingState.response!.length + ? LastChildLayoutType.foot + : LastChildLayoutType.none; + }, + children: loadingState.response! + .map((item) => SpaceOpusItem(item: item)) + .toList(), + ) + : HttpError( + onReload: _controller.onReload, + ), + Error() => HttpError( + errMsg: loadingState.errMsg, + onReload: _controller.onReload, + ), + }; + } + + @override + bool get wantKeepAlive => true; +} diff --git a/lib/pages/member_opus/widgets/space_opus_item.dart b/lib/pages/member_opus/widgets/space_opus_item.dart new file mode 100644 index 000000000..c20ce5021 --- /dev/null +++ b/lib/pages/member_opus/widgets/space_opus_item.dart @@ -0,0 +1,95 @@ +import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; +import 'package:PiliPlus/common/widgets/stat/stat.dart'; +import 'package:PiliPlus/models/space_opus/item.dart'; +import 'package:PiliPlus/utils/page_utils.dart'; +import 'package:flutter/material.dart'; + +class SpaceOpusItem extends StatelessWidget { + const SpaceOpusItem({ + super.key, + required this.item, + }); + + final SpaceOpusItemModel item; + + @override + Widget build(BuildContext context) { + final hasPic = item.cover?.url?.isNotEmpty == true; + return Card( + clipBehavior: Clip.hardEdge, + margin: EdgeInsets.zero, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(6)), + ), + child: InkWell( + onTap: () { + PageUtils.pushDynFromId(id: item.opusId!); + }, + borderRadius: const BorderRadius.all(Radius.circular(6)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (hasPic) + Stack( + children: [ + LayoutBuilder( + builder: (context, constraints) { + return NetworkImgLayer( + width: constraints.maxWidth, + height: constraints.maxWidth * item.cover!.ratio, + src: item.cover!.url, + type: 'emote', + quality: 60, + ); + }, + ), + Positioned( + left: 0, + bottom: 0, + right: 0, + child: Container( + height: 45, + alignment: Alignment.bottomLeft, + padding: const EdgeInsets.only(left: 8, bottom: 4), + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.transparent, Colors.black54], + ), + ), + child: StatView( + context: context, + value: item.stat?.like ?? 0, + goto: 'like', + ), + ), + ), + ], + ), + if (item.content?.isNotEmpty == true) + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 10), + child: Text( + item.content!, + maxLines: hasPic ? 4 : 6, + overflow: TextOverflow.ellipsis, + ), + ), + if (!hasPic) + Padding( + padding: const EdgeInsets.only(left: 8, bottom: 8, right: 8), + child: StatView( + context: context, + value: item.stat?.like ?? 0, + goto: 'like', + textColor: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/member_pgc/view.dart b/lib/pages/member_pgc/view.dart index b32aabfd6..5e7684c30 100644 --- a/lib/pages/member_pgc/view.dart +++ b/lib/pages/member_pgc/view.dart @@ -1,5 +1,5 @@ import 'package:PiliPlus/common/constants.dart'; -import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.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/space_archive/item.dart'; @@ -39,55 +39,54 @@ class _MemberBangumiState extends State @override Widget build(BuildContext context) { super.build(context); - return Obx(() => _buildBody(_controller.loadingState.value)); + return refreshIndicator( + onRefresh: _controller.onRefresh, + child: CustomScrollView( + slivers: [ + SliverPadding( + padding: EdgeInsets.only( + left: StyleString.safeSpace, + right: StyleString.safeSpace, + top: StyleString.safeSpace, + bottom: StyleString.safeSpace + + MediaQuery.of(context).padding.bottom + + 80, + ), + sliver: Obx( + () => _buildBody(_controller.loadingState.value), + ), + ), + ], + ), + ); } Widget _buildBody(LoadingState?> loadingState) { return switch (loadingState) { - Loading() => loadingWidget, + Loading() => const SliverToBoxAdapter(), Success() => loadingState.response?.isNotEmpty == true - ? refreshIndicator( - onRefresh: _controller.onRefresh, - child: CustomScrollView( - slivers: [ - SliverPadding( - padding: EdgeInsets.only( - left: StyleString.safeSpace, - right: StyleString.safeSpace, - top: StyleString.safeSpace, - bottom: StyleString.safeSpace + - MediaQuery.of(context).padding.bottom + - 80, - ), - sliver: SliverGrid( - gridDelegate: SliverGridDelegateWithExtentAndRatio( - mainAxisSpacing: StyleString.cardSpace, - crossAxisSpacing: StyleString.cardSpace, - maxCrossAxisExtent: Grid.smallCardWidth / 3 * 2, - childAspectRatio: 0.75, - mainAxisExtent: - MediaQuery.textScalerOf(context).scale(52), - ), - delegate: SliverChildBuilderDelegate( - (context, index) { - if (index == loadingState.response!.length - 1) { - _controller.onLoadMore(); - } - return BangumiCardVMemberHome( - bangumiItem: loadingState.response![index], - ); - }, - childCount: loadingState.response!.length, - ), - ), - ), - ], + ? SliverGrid( + gridDelegate: SliverGridDelegateWithExtentAndRatio( + mainAxisSpacing: StyleString.cardSpace, + crossAxisSpacing: StyleString.cardSpace, + maxCrossAxisExtent: Grid.smallCardWidth / 3 * 2, + childAspectRatio: 0.75, + mainAxisExtent: MediaQuery.textScalerOf(context).scale(52), + ), + delegate: SliverChildBuilderDelegate( + (context, index) { + if (index == loadingState.response!.length - 1) { + _controller.onLoadMore(); + } + return BangumiCardVMemberHome( + bangumiItem: loadingState.response![index], + ); + }, + childCount: loadingState.response!.length, ), ) - : scrollErrorWidget( - onReload: _controller.onReload, - ), - Error() => scrollErrorWidget( + : HttpError(onReload: _controller.onReload), + Error() => HttpError( errMsg: loadingState.errMsg, onReload: _controller.onReload, ), diff --git a/lib/pages/member_profile/view.dart b/lib/pages/member_profile/view.dart index 8dcfe20b7..d41b3c750 100644 --- a/lib/pages/member_profile/view.dart +++ b/lib/pages/member_profile/view.dart @@ -90,124 +90,122 @@ class _EditProfilePageState extends State { return switch (loadingState) { Loading() => loadingWidget, - Success() => SingleChildScrollView( - child: Column( - children: [ - _item( - theme: theme, - title: '头像', - widget: Padding( - padding: const EdgeInsets.symmetric(vertical: 5), - child: ClipOval( - child: CachedNetworkImage( - imageUrl: - Utils.thumbnailImgUrl(loadingState.response['face']), - ), + Success() => ListView( + children: [ + _item( + theme: theme, + title: '头像', + widget: Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: ClipOval( + child: CachedNetworkImage( + imageUrl: + Utils.thumbnailImgUrl(loadingState.response['face']), ), ), - onTap: () { - EasyThrottle.throttle( - 'imagePicker', const Duration(milliseconds: 500), () { - _pickImg(theme); - }); - }, ), - divider, - _item( - theme: theme, - title: '昵称', - text: loadingState.response['name'], - onTap: () { - if (loadingState.response['coins'] < 6) { - SmartDialog.showToast('硬币不足'); - } else { - _editDialog( - type: ProfileType.uname, - title: '昵称', - text: loadingState.response['name'], + onTap: () { + EasyThrottle.throttle( + 'imagePicker', const Duration(milliseconds: 500), () { + _pickImg(theme); + }); + }, + ), + divider, + _item( + theme: theme, + title: '昵称', + text: loadingState.response['name'], + onTap: () { + if (loadingState.response['coins'] < 6) { + SmartDialog.showToast('硬币不足'); + } else { + _editDialog( + type: ProfileType.uname, + title: '昵称', + text: loadingState.response['name'], + ); + } + }, + ), + divider, + _item( + theme: theme, + title: '性别', + text: _sex(loadingState.response['sex']), + onTap: () { + showDialog( + context: context, + builder: (context_) => + _sexDialog(loadingState.response['sex']), + ); + }, + ), + divider, + _item( + theme: theme, + title: '出生年月', + text: loadingState.response['birthday'], + onTap: () { + showDatePicker( + context: context, + initialDate: + DateTime.parse(loadingState.response['birthday']), + firstDate: DateTime(1900, 1, 1), + lastDate: DateTime.now(), + ).then((date) { + if (date != null) { + _update( + type: ProfileType.birthday, + datum: DateFormat('yyyy-MM-dd').format(date), ); } - }, - ), - divider, - _item( - theme: theme, - title: '性别', - text: _sex(loadingState.response['sex']), - onTap: () { - showDialog( - context: context, - builder: (context_) => - _sexDialog(loadingState.response['sex']), - ); - }, - ), - divider, - _item( - theme: theme, - title: '出生年月', - text: loadingState.response['birthday'], - onTap: () { - showDatePicker( - context: context, - initialDate: - DateTime.parse(loadingState.response['birthday']), - firstDate: DateTime(1900, 1, 1), - lastDate: DateTime.now(), - ).then((date) { - if (date != null) { - _update( - type: ProfileType.birthday, - datum: DateFormat('yyyy-MM-dd').format(date), - ); - } - }); - }, - ), - divider, - _item( - theme: theme, - title: '个性签名', - text: loadingState.response['sign'].isEmpty - ? '无' - : loadingState.response['sign'], - onTap: () { - _editDialog( - type: ProfileType.sign, - title: '个性签名', - text: loadingState.response['sign'], - ); - }, - ), - divider1, - _item( - theme: theme, - title: '头像挂件', - onTap: () => PageUtils.launchURL( - 'https://www.bilibili.com/h5/mall/pendant/home'), - ), - divider1, - _item( - theme: theme, - title: 'UID', - needIcon: false, - text: loadingState.response['mid'].toString(), - onTap: () => - Utils.copyText(loadingState.response['mid'].toString()), - ), - divider1, - _item( - theme: theme, - title: '哔哩哔哩认证', - onTap: () => PageUtils.launchURL( - 'https://account.bilibili.com/official/mobile/home'), - ), - divider, - SizedBox(height: 25 + MediaQuery.paddingOf(context).bottom), - ], - ), + }); + }, + ), + divider, + _item( + theme: theme, + title: '个性签名', + text: loadingState.response['sign'].isEmpty + ? '无' + : loadingState.response['sign'], + onTap: () { + _editDialog( + type: ProfileType.sign, + title: '个性签名', + text: loadingState.response['sign'], + ); + }, + ), + divider1, + _item( + theme: theme, + title: '头像挂件', + onTap: () => PageUtils.launchURL( + 'https://www.bilibili.com/h5/mall/pendant/home'), + ), + divider1, + _item( + theme: theme, + title: 'UID', + needIcon: false, + text: loadingState.response['mid'].toString(), + onTap: () => + Utils.copyText(loadingState.response['mid'].toString()), + ), + divider1, + _item( + theme: theme, + title: '哔哩哔哩认证', + onTap: () => PageUtils.launchURL( + 'https://account.bilibili.com/official/mobile/home'), + ), + divider, + SizedBox(height: 25 + MediaQuery.paddingOf(context).bottom), + ], ), - Error() => errorWidget( + Error() => scrollErrorWidget( errMsg: loadingState.errMsg, onReload: _getInfo, ), diff --git a/lib/pages/member_season_series/view.dart b/lib/pages/member_season_series/view.dart index 7968224d0..096eecc72 100644 --- a/lib/pages/member_season_series/view.dart +++ b/lib/pages/member_season_series/view.dart @@ -1,5 +1,6 @@ import 'package:PiliPlus/common/constants.dart'; -import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; +import 'package:PiliPlus/common/skeleton/video_card_h.dart'; +import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/member/contribute_type.dart'; import 'package:PiliPlus/pages/member_season_series/controller.dart'; @@ -36,75 +37,81 @@ class _SeasonSeriesPageState extends State @override Widget build(BuildContext context) { super.build(context); - return Obx(() => _buildBody(_controller.loadingState.value)); + return CustomScrollView( + slivers: [ + SliverPadding( + padding: EdgeInsets.only( + top: StyleString.safeSpace - 5, + bottom: MediaQuery.paddingOf(context).bottom + 80, + ), + sliver: Obx( + () => _buildBody(_controller.loadingState.value), + ), + ), + ], + ); } Widget _buildBody(LoadingState?> loadingState) { return switch (loadingState) { - Loading() => loadingWidget, + Loading() => SliverGrid( + gridDelegate: Grid.videoCardHDelegate(context), + delegate: SliverChildBuilderDelegate( + (context, index) { + return const VideoCardHSkeleton(); + }, + childCount: 10, + ), + ), Success() => loadingState.response?.isNotEmpty == true - ? CustomScrollView( - slivers: [ - SliverPadding( - padding: EdgeInsets.only( - top: StyleString.safeSpace - 5, - bottom: MediaQuery.paddingOf(context).bottom + 80, - ), - sliver: SliverGrid( - gridDelegate: Grid.videoCardHDelegate(context), - delegate: SliverChildBuilderDelegate( - (context, index) { - if (index == loadingState.response!.length - 1) { - _controller.onLoadMore(); - } - dynamic item = loadingState.response![index]; - return SeasonSeriesCard( - item: item, - onTap: () { - bool isSeason = item['meta']['season_id'] != null; - dynamic id = isSeason - ? item['meta']['season_id'] - : item['meta']['series_id']; - Get.to( - Scaffold( - appBar: AppBar( - title: Text(item['meta']['name']), - ), - body: SafeArea( - top: false, - bottom: false, - child: MemberVideo( - type: isSeason - ? ContributeType.season - : ContributeType.series, - heroTag: widget.heroTag, - mid: widget.mid, - seasonId: isSeason ? id : null, - seriesId: isSeason ? null : id, - title: item['meta']['name'], - ), - ), - ), - ); - }, - ); - }, - childCount: loadingState.response!.length, - ), - ), - ), - ], + ? SliverGrid( + gridDelegate: Grid.videoCardHDelegate(context), + delegate: SliverChildBuilderDelegate( + (context, index) { + if (index == loadingState.response!.length - 1) { + _controller.onLoadMore(); + } + dynamic item = loadingState.response![index]; + return SeasonSeriesCard( + item: item, + onTap: () { + bool isSeason = item['meta']['season_id'] != null; + dynamic id = isSeason + ? item['meta']['season_id'] + : item['meta']['series_id']; + Get.to( + Scaffold( + appBar: AppBar( + title: Text(item['meta']['name']), + ), + body: SafeArea( + top: false, + bottom: false, + child: MemberVideo( + type: isSeason + ? ContributeType.season + : ContributeType.series, + heroTag: widget.heroTag, + mid: widget.mid, + seasonId: isSeason ? id : null, + seriesId: isSeason ? null : id, + title: item['meta']['name'], + ), + ), + ), + ); + }, + ); + }, + childCount: loadingState.response!.length, + ), ) - : scrollErrorWidget( - onReload: () { - _controller.onReload(); - }, + : HttpError( + onReload: _controller.onReload, ), - Error() => scrollErrorWidget( + Error() => HttpError( errMsg: loadingState.errMsg, - onReload: () { - _controller.onReload(); - }, + onReload: _controller.onReload, ), }; } diff --git a/lib/pages/member_video/view.dart b/lib/pages/member_video/view.dart index c8d334368..f6fd3a313 100644 --- a/lib/pages/member_video/view.dart +++ b/lib/pages/member_video/view.dart @@ -1,8 +1,7 @@ -import 'package:PiliPlus/common/constants.dart'; +import 'package:PiliPlus/common/skeleton/video_card_h.dart'; import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart'; -import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; +import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; -import 'package:PiliPlus/common/widgets/scroll_physics.dart'; import 'package:PiliPlus/common/widgets/video_card/video_card_h_member_video.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/member/contribute_type.dart'; @@ -56,170 +55,176 @@ class _MemberVideoState extends State @override Widget build(BuildContext context) { super.build(context); - return Obx(() => _buildBody(_controller.loadingState.value)); + final theme = Theme.of(context); + return Stack( + clipBehavior: Clip.none, + children: [ + refreshIndicator( + onRefresh: _controller.onRefresh, + child: CustomScrollView( + // physics: PositionRetainedScrollPhysics( + // shouldRetain: _controller.isLocating == true, + // parent: const ClampingScrollPhysics(), + // ), + slivers: [ + SliverPadding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).padding.bottom + 80, + ), + sliver: Obx( + () => _buildBody(theme, _controller.loadingState.value)), + ), + ], + ), + ), + if (widget.type == ContributeType.video && + _controller.fromViewAid?.isNotEmpty == true && + _controller.isLocating != true) + Positioned( + right: 15, + bottom: 15, + child: SafeArea( + top: false, + left: false, + child: FloatingActionButton.extended( + onPressed: () { + _controller + ..isLocating = true + ..lastAid = _controller.fromViewAid + ..currentPage = 0 + ..loadingState.value = LoadingState.loading() + ..queryData(); + }, + label: const Text('定位至上次观看'), + ), + ), + ), + ], + ); } - Widget _buildBody(LoadingState?> loadingState) { - final theme = Theme.of(context); + Widget _buildBody( + ThemeData theme, LoadingState?> loadingState) { return switch (loadingState) { - Loading() => loadingWidget, + Loading() => SliverGrid( + gridDelegate: Grid.videoCardHDelegate(context), + delegate: SliverChildBuilderDelegate( + (context, index) { + return const VideoCardHSkeleton(); + }, + childCount: 10, + ), + ), Success() => loadingState.response?.isNotEmpty == true - ? Stack( - clipBehavior: Clip.none, - children: [ - refreshIndicator( - onRefresh: _controller.onRefresh, - child: CustomScrollView( - physics: PositionRetainedScrollPhysics( - shouldRetain: _controller.isLocating == true, - parent: const ClampingScrollPhysics(), - ), - slivers: [ - SliverPersistentHeader( - pinned: false, - floating: true, - delegate: CustomSliverPersistentHeaderDelegate( - extent: 40, - bgColor: theme.colorScheme.surface, - child: SizedBox( - height: 40, - child: Row( - children: [ - const SizedBox(width: 8), - Obx( - () => Padding( - padding: const EdgeInsets.only(left: 6), - child: Text( - _controller.count.value != -1 - ? '共${_controller.count.value}视频' - : '', - style: const TextStyle(fontSize: 13), - ), - ), - ), - Obx( - () => _controller.episodicButton.value.uri != - null - ? Container( - height: 35, - padding: EdgeInsets.only( - left: - _controller.count.value != -1 - ? 6 - : 0), - child: TextButton.icon( - onPressed: - _controller.toViewPlayAll, - icon: Icon( - Icons.play_circle_outline_rounded, - size: 16, - color: - theme.colorScheme.secondary, - ), - label: Text( - _controller.episodicButton.value - .text ?? - '播放全部', - style: TextStyle( - fontSize: 13, - color: - theme.colorScheme.secondary, - ), - ), - ), - ) - : const SizedBox.shrink(), - ), - const Spacer(), - SizedBox( - height: 35, - child: TextButton.icon( - 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' - ? '默认' - : '倒序', + ? SliverMainAxisGroup( + slivers: [ + SliverPersistentHeader( + pinned: false, + floating: true, + delegate: CustomSliverPersistentHeaderDelegate( + extent: 40, + bgColor: theme.colorScheme.surface, + child: SizedBox( + height: 40, + child: Row( + children: [ + const SizedBox(width: 8), + Obx( + () => Padding( + padding: const EdgeInsets.only(left: 6), + child: Text( + _controller.count.value != -1 + ? '共${_controller.count.value}视频' + : '', + style: const TextStyle(fontSize: 13), + ), + ), + ), + Obx( + () => _controller.episodicButton.value.uri != null + ? Container( + height: 35, + padding: EdgeInsets.only( + left: _controller.count.value != -1 + ? 6 + : 0), + child: TextButton.icon( + onPressed: _controller.toViewPlayAll, + icon: Icon( + Icons.play_circle_outline_rounded, + size: 16, + color: theme.colorScheme.secondary, + ), + label: Text( + _controller.episodicButton.value.text ?? + '播放全部', style: TextStyle( fontSize: 13, color: theme.colorScheme.secondary, ), ), ), + ) + : const SizedBox.shrink(), + ), + const Spacer(), + SizedBox( + height: 35, + child: TextButton.icon( + 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, ), ), - const SizedBox(width: 8), - ], + ), ), ), - ), - ), - SliverPadding( - padding: EdgeInsets.only( - top: StyleString.safeSpace - 5, - bottom: MediaQuery.of(context).padding.bottom + 80, - ), - sliver: SliverGrid( - gridDelegate: Grid.videoCardHDelegate(context), - delegate: SliverChildBuilderDelegate( - (context, index) { - if (widget.type != ContributeType.season && - index == loadingState.response!.length - 1) { - _controller.onLoadMore(); - } - final SpaceArchiveItem item = - loadingState.response![index]; - return VideoCardHMemberVideo( - key: ValueKey('${item.param}'), - videoItem: item, - fromViewAid: _controller.fromViewAid, - ); - }, - childCount: loadingState.response!.length, - ), - ), - ), - ], - ), - ), - if (widget.type == ContributeType.video && - _controller.fromViewAid?.isNotEmpty == true && - _controller.isLocating != true) - Positioned( - right: 15, - bottom: 15, - child: SafeArea( - top: false, - left: false, - child: FloatingActionButton.extended( - onPressed: () { - _controller - ..isLocating = true - ..lastAid = _controller.fromViewAid - ..currentPage = 0 - ..loadingState.value = LoadingState.loading() - ..queryData(); - }, - label: const Text('定位至上次观看'), + const SizedBox(width: 8), + ], ), ), ), + ), + SliverGrid( + gridDelegate: Grid.videoCardHDelegate(context), + delegate: SliverChildBuilderDelegate( + (context, index) { + if (widget.type != ContributeType.season && + index == loadingState.response!.length - 1) { + _controller.onLoadMore(); + } + final SpaceArchiveItem item = + loadingState.response![index]; + return VideoCardHMemberVideo( + key: ValueKey('${item.param}'), + videoItem: item, + fromViewAid: _controller.fromViewAid, + ); + }, + childCount: loadingState.response!.length, + ), + ), ], ) - : scrollErrorWidget( + : HttpError( onReload: _controller.onReload, ), - Error() => scrollErrorWidget( + Error() => HttpError( errMsg: loadingState.errMsg, onReload: _controller.onReload, ), diff --git a/lib/pages/whisper_detail/view.dart b/lib/pages/whisper_detail/view.dart index 37bdad0b2..5215e99b2 100644 --- a/lib/pages/whisper_detail/view.dart +++ b/lib/pages/whisper_detail/view.dart @@ -102,12 +102,12 @@ class _WhisperDetailPageState children: [ Expanded( child: Listener( - child: Obx(() => - _buildBody(_whisperDetailController.loadingState.value)), onPointerDown: (event) { - // Hide panel when touch ListView. hidePanel(); }, + behavior: HitTestBehavior.opaque, + child: Obx(() => + _buildBody(_whisperDetailController.loadingState.value)), ), ), _buildInputView(theme),