feat: space opus

Closes #833

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-05-08 22:39:29 +08:00
parent bcd0d63db7
commit 2d75d89825
28 changed files with 1107 additions and 511 deletions

View File

@@ -261,13 +261,6 @@ class _MemberPageState extends State<MemberPage> {
);
}
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<MemberPage> {
behavior: HitTestBehavior.opaque,
child: const SizedBox(height: 56, width: double.infinity),
),
Error() => _errorWidget(userState.errMsg),
Error() => scrollErrorWidget(
errMsg: userState.errMsg,
onReload: _userController.onReload,
),
};
}
}

View File

@@ -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<MemberArticle>
@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<List<SpaceArticleItem>?> 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,
),

View File

@@ -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<MemberContribute>
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,

View File

@@ -58,9 +58,13 @@ class _MemberDynamicsPageState extends State<MemberDynamicsPage>
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<MemberDynamicsPage>
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,
),

View File

@@ -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<MemberFavorite>
@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,
),

View File

@@ -222,8 +222,8 @@ class _MemberHomeState extends State<MemberHome>
),
],
)
: errorWidget(),
Error() => errorWidget(),
: scrollErrorWidget(),
Error() => scrollErrorWidget(),
};
}

View File

@@ -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<SpaceOpusData, SpaceOpusItemModel> {
MemberOpusController({
required this.heroTag,
required this.mid,
});
final String? heroTag;
final int mid;
String offset = '';
Rx<SpaceTabFilter> type =
SpaceTabFilter(text: "全部图文", meta: "all", tabName: "图文").obs;
List<SpaceTabFilter>? filter;
@override
void onInit() {
super.onInit();
filter = Get.find<MemberController>(tag: heroTag)
.tab2
?.firstWhereOrNull((e) => e.param == 'contribute')
?.items
?.firstWhereOrNull((e) => e.param == 'opus')
?.filter;
queryData();
}
@override
Future<void> onRefresh() {
offset = '';
return super.onRefresh();
}
@override
List<SpaceOpusItemModel>? getDataList(SpaceOpusData response) {
offset = response.offset ?? '';
if (response.hasMore == false) {
isEnd = true;
}
return response.items;
}
@override
Future<LoadingState<SpaceOpusData>> customGetData() => MemberHttp.spaceOpus(
hostMid: mid,
page: currentPage,
offset: offset,
type: type.value.meta,
);
}

View File

@@ -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<MemberOpus> createState() => _MemberOpusState();
}
class _MemberOpusState extends State<MemberOpus>
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<List<SpaceOpusItemModel>?> 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;
}

View File

@@ -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,
),
),
],
),
),
);
}
}

View File

@@ -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<MemberBangumi>
@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<List<SpaceArchiveItem>?> 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,
),

View File

@@ -90,124 +90,122 @@ class _EditProfilePageState extends State<EditProfilePage> {
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,
),

View File

@@ -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<SeasonSeriesPage>
@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<List<dynamic>?> 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,
),
};
}

View File

@@ -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<MemberVideo>
@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<List<SpaceArchiveItem>?> loadingState) {
final theme = Theme.of(context);
Widget _buildBody(
ThemeData theme, LoadingState<List<SpaceArchiveItem>?> 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,
),

View File

@@ -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),