Refactor member page (#3)

* refactor: member page

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip
This commit is contained in:
bggRGjQaUbCoE
2024-10-17 11:18:59 +08:00
committed by GitHub
parent 270bf0a4b6
commit f77088b870
205 changed files with 70072 additions and 119 deletions

View File

@@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
class MemberArticle extends StatefulWidget {
const MemberArticle({
super.key,
required this.heroTag,
});
final String? heroTag;
@override
State<MemberArticle> createState() => _MemberArticleState();
}
class _MemberArticleState extends State<MemberArticle>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context);
return Center(
child: Text('Article'),
);
}
}

View File

@@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
class MemberAudio extends StatefulWidget {
const MemberAudio({
super.key,
required this.heroTag,
});
final String? heroTag;
@override
State<MemberAudio> createState() => _MemberAudioState();
}
class _MemberAudioState extends State<MemberAudio>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context);
return Center(
child: Text('Audio'),
);
}
}

View File

@@ -0,0 +1,100 @@
import 'package:PiliPalaX/common/constants.dart';
import 'package:PiliPalaX/common/widgets/http_error.dart';
import 'package:PiliPalaX/http/loading_state.dart';
import 'package:PiliPalaX/pages/bangumi/widgets/bangumi_card_v_member_home.dart';
import 'package:PiliPalaX/pages/member/new/content/member_contribute/content/bangumi/member_bangumi_ctr.dart';
import 'package:PiliPalaX/utils/grid.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class MemberBangumi extends StatefulWidget {
const MemberBangumi({
super.key,
required this.heroTag,
required this.mid,
});
final String? heroTag;
final int mid;
@override
State<MemberBangumi> createState() => _MemberBangumiState();
}
class _MemberBangumiState extends State<MemberBangumi>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
late final _controller = Get.put(
MemberBangumiCtr(
heroTag: widget.heroTag,
mid: widget.mid,
),
tag: widget.heroTag,
);
@override
Widget build(BuildContext context) {
super.build(context);
return Obx(() => _buildBody(_controller.loadingState.value));
}
_buildBody(LoadingState loadingState) {
return loadingState is Success
? RefreshIndicator(
onRefresh: () async {
await _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,
),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithExtentAndRatio(
mainAxisSpacing: StyleString.cardSpace - 2,
crossAxisSpacing: StyleString.cardSpace,
maxCrossAxisExtent: Grid.maxRowWidth / 3 * 2,
childAspectRatio: 0.65,
mainAxisExtent:
MediaQuery.textScalerOf(context).scale(60),
),
delegate: SliverChildBuilderDelegate(
(context, index) {
if (index == loadingState.response.length - 1) {
_controller.onLoadMore();
}
return BangumiCardVMemberHome(
bangumiItem: loadingState.response[index],
);
},
childCount: loadingState.response.length,
),
),
),
],
),
)
: loadingState is Error
? Center(
child: CustomScrollView(
shrinkWrap: true,
slivers: [
HttpError(
errMsg: loadingState.errMsg,
fn: _controller.onReload,
),
],
),
)
: Center(
child: CircularProgressIndicator(),
);
}
}

View File

@@ -0,0 +1,62 @@
import 'package:PiliPalaX/http/loading_state.dart';
import 'package:PiliPalaX/http/member.dart';
import 'package:PiliPalaX/models/space_archive/data.dart';
import 'package:PiliPalaX/pages/common/common_controller.dart';
import 'package:PiliPalaX/pages/member/new/content/member_contribute/member_contribute.dart'
show ContributeType;
import 'package:PiliPalaX/pages/member/new/controller.dart';
import 'package:get/get.dart';
class MemberBangumiCtr extends CommonController {
MemberBangumiCtr({
required this.mid,
required this.heroTag,
});
final int mid;
final String? heroTag;
bool isEnd = false;
late final int count;
late final _ctr = Get.find<MemberControllerNew>(tag: heroTag);
@override
void onInit() {
super.onInit();
currentPage = 2;
dynamic res = (_ctr.loadingState.value as Success).response.season;
loadingState.value = LoadingState.success(res.item);
count = res.count;
isEnd = res.item!.length >= count;
}
@override
Future onRefresh() async {
isEnd = false;
currentPage = 1;
await queryData();
}
@override
Future queryData([bool isRefresh = true]) {
if (isEnd) return Future.value();
return super.queryData(isRefresh);
}
@override
bool customHandleResponse(Success response) {
Data data = response.response;
if (currentPage != 1 && loadingState.value is Success) {
data.item?.insertAll(0, (loadingState.value as Success).response);
}
isEnd = data.item!.length >= count;
loadingState.value = LoadingState.success(data.item);
return true;
}
@override
Future<LoadingState> customGetData() => MemberHttp.spaceArchive(
type: ContributeType.bangumi,
mid: mid,
pn: currentPage,
);
}

View File

@@ -0,0 +1,261 @@
import 'package:PiliPalaX/common/constants.dart';
import 'package:PiliPalaX/common/widgets/badge.dart';
import 'package:PiliPalaX/common/widgets/http_error.dart';
import 'package:PiliPalaX/common/widgets/network_img_layer.dart';
import 'package:PiliPalaX/http/loading_state.dart';
import 'package:PiliPalaX/models/space_fav/datum.dart';
import 'package:PiliPalaX/models/space_fav/list.dart';
import 'package:PiliPalaX/models/user/sub_folder.dart';
import 'package:PiliPalaX/pages/member/new/content/member_contribute/content/favorite/member_favorite_ctr.dart';
import 'package:PiliPalaX/utils/app_scheme.dart';
import 'package:PiliPalaX/utils/utils.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class MemberFavorite extends StatefulWidget {
const MemberFavorite({
super.key,
required this.heroTag,
required this.mid,
});
final String? heroTag;
final int mid;
@override
State<MemberFavorite> createState() => _MemberFavoriteState();
}
class _MemberFavoriteState extends State<MemberFavorite>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
late final _controller = Get.put(
MemberFavoriteCtr(mid: widget.mid),
tag: widget.heroTag,
);
@override
Widget build(BuildContext context) {
super.build(context);
return Obx(() => _buildBody(_controller.loadingState.value));
}
_buildBody(LoadingState loadingState) {
return loadingState is Success
? RefreshIndicator(
onRefresh: () async {
await _controller.onRefresh();
},
child: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Obx(
() => _controller.first.value.mediaListResponse?.list
?.isNotEmpty ==
true
? _buildItem(_controller.first.value, true)
: const SizedBox.shrink(),
),
),
SliverToBoxAdapter(
child: Obx(
() => _controller.second.value.mediaListResponse?.list
?.isNotEmpty ==
true
? _buildItem(_controller.second.value, false)
: const SizedBox.shrink(),
),
),
SliverToBoxAdapter(
child: SizedBox(
height: 12 + MediaQuery.of(context).padding.bottom,
),
)
],
),
)
: loadingState is Error
? Center(
child: CustomScrollView(
shrinkWrap: true,
slivers: [
HttpError(
errMsg: loadingState.errMsg,
fn: _controller.onReload,
),
],
),
)
: Center(
child: CircularProgressIndicator(),
);
}
_buildItem(Datum data, bool isFirst) {
return Theme(
data: Theme.of(context).copyWith(
dividerColor: Colors.transparent,
),
child: ExpansionTile(
dense: true,
initiallyExpanded: true,
title: Text.rich(
TextSpan(
children: [
TextSpan(
text: data.name,
style: TextStyle(fontSize: 14),
),
TextSpan(
text: ' ${data.mediaListResponse?.count}',
style: TextStyle(
fontSize: 13,
color: Theme.of(context).colorScheme.outline,
),
),
],
),
),
controlAffinity: ListTileControlAffinity.leading,
children: [
...(data.mediaListResponse?.list as List<FavList>).map(
(item1) => ListTile(
onTap: () {
if (item1.state == 1) {
// invalid
return;
}
if (item1.type == 0) {
Get.toNamed(
'/favDetail',
parameters: {
'mediaId': item1.id.toString(),
'heroTag': widget.heroTag ?? '',
},
);
} else if (item1.type == 21) {
PiliScheme.routePush(Uri.parse(item1.link ?? ''));
} else if (item1.type == 11) {
Get.toNamed(
'/subDetail',
arguments: SubFolderItemData(
type: 11,
title: item1.title,
cover: item1.cover,
upper: Upper(
mid: item1.upper?.mid,
name: item1.upper?.name,
face: item1.upper?.face,
),
mediaCount: item1.mediaCount,
viewCount: item1.viewCount,
),
parameters: {
'heroTag': widget.heroTag ?? '',
'id': item1.id.toString(),
},
);
}
},
leading: Container(
margin: const EdgeInsets.symmetric(vertical: 6),
child: LayoutBuilder(
builder: (_, constraints) {
return Stack(
children: [
NetworkImgLayer(
radius: 6,
src: item1.cover,
width:
constraints.maxHeight * StyleString.aspectRatio,
height: constraints.maxHeight,
),
if (item1.type == 21)
PBadge(
right: 3,
bottom: 3,
text: '合集',
bold: false,
size: 'small',
)
else if (item1.type == 0 || item1.type == 11)
Positioned(
right: 3,
bottom: 3,
child: Container(
padding: const EdgeInsets.all(5),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.primary,
),
child: Icon(
Icons.video_library_outlined,
size: 12,
color: Theme.of(context).colorScheme.onPrimary,
),
),
),
],
);
},
),
),
title: Text(
item1.title ?? '',
style: TextStyle(
fontSize: 14,
),
),
subtitle: Text(
item1.type == 0
? '${item1.mediaCount}个内容 · ${[
0,
2,
22
].contains(item1.attr) ? '公开' : '私密'}'
: item1.type == 11
? '${item1.mediaCount}个内容 · ${item1.upper?.name}'
: item1.type == 21
? '创建者: ${item1.upper?.name}\n${item1.mediaCount}个视频 · ${Utils.numFormat(item1.viewCount)}播放'
: '${item1.mediaCount}个内容',
style: TextStyle(
fontSize: 13,
color: Theme.of(context).colorScheme.outline,
),
),
),
),
Obx(
() => (isFirst
? _controller.firstEnd.value
: _controller.secondEnd.value)
? const SizedBox.shrink()
: _buildLoadMoreItem(isFirst),
),
],
),
);
}
_buildLoadMoreItem(bool isFirst) {
return ListTile(
dense: true,
onTap: () {
if (isFirst) {
_controller.userfavFolder();
} else {
_controller.userSubFolder();
}
},
title: Text(
'查看更多内容',
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
),
);
}
}

View File

@@ -0,0 +1,111 @@
import 'package:PiliPalaX/http/api.dart';
import 'package:PiliPalaX/http/init.dart';
import 'package:PiliPalaX/http/loading_state.dart';
import 'package:PiliPalaX/http/member.dart';
import 'package:PiliPalaX/models/space_fav/datum.dart';
import 'package:PiliPalaX/models/space_fav/list.dart';
import 'package:PiliPalaX/models/space_fav/space_fav.dart';
import 'package:PiliPalaX/models/user/fav_folder.dart';
import 'package:PiliPalaX/pages/common/common_controller.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
class MemberFavoriteCtr extends CommonController {
MemberFavoriteCtr({
required this.mid,
});
final int mid;
Rx<Datum> first = Datum().obs;
Rx<Datum> second = Datum().obs;
RxBool firstEnd = true.obs;
RxBool secondEnd = true.obs;
late int page = 2;
late int page1 = 2;
@override
void onInit() {
super.onInit();
queryData();
}
@override
Future onRefresh() {
page = 2;
page1 = 2;
return super.onRefresh();
}
@override
bool customHandleResponse(Success response) {
try {
List<Datum> res = response.response;
first.value = res.first;
second.value = res[1];
firstEnd.value = (res.first.mediaListResponse?.count ?? -1) <=
(res.first.mediaListResponse?.list?.length ?? -1);
secondEnd.value = (res[1].mediaListResponse?.count ?? -1) <=
(res[1].mediaListResponse?.list?.length ?? -1);
} catch (e) {
print(e.toString());
}
loadingState.value = response;
return true;
}
Future userfavFolder() async {
var res = await Request().get(Api.userFavFolder, data: {
'pn': page,
'ps': 20,
'up_mid': mid,
});
if (res.data['code'] == 0) {
page++;
firstEnd.value = res.data['data']['has_more'] == false;
if (res.data['data'] != null) {
List<FavList> list = (res.data['data']['list'] as List<dynamic>?)
?.map((item) => FavList.fromJson(item))
.toList() ??
<FavList>[];
first.value.mediaListResponse?.list?.addAll(list);
first.refresh();
} else {
firstEnd.value = true;
}
} else {
SmartDialog.showToast(res.data['message'] ?? '账号未登录');
}
}
Future userSubFolder() async {
var res = await Request().get(Api.userSubFolder, data: {
'up_mid': mid,
'ps': 20,
'pn': page1,
'platform': 'web',
});
if (res.data['code'] == 0) {
page++;
secondEnd.value = res.data['data']['has_more'] == false;
if (res.data['data'] != null) {
List<FavList> list = (res.data['data']['list'] as List<dynamic>?)
?.map((item) => FavList.fromJson(item))
.toList() ??
<FavList>[];
second.value.mediaListResponse?.list?.addAll(list);
second.refresh();
} else {
secondEnd.value = true;
}
} else {
SmartDialog.showToast(res.data['message'] ?? '账号未登录');
}
}
@override
Future<LoadingState> customGetData() => MemberHttp.spaceFav(mid: mid);
}

View File

@@ -0,0 +1,156 @@
import 'package:PiliPalaX/common/constants.dart';
import 'package:PiliPalaX/common/widgets/http_error.dart';
import 'package:PiliPalaX/common/widgets/video_card_h_member_video.dart';
import 'package:PiliPalaX/http/loading_state.dart';
import 'package:PiliPalaX/pages/member/new/content/member_contribute/content/video/member_video_ctr.dart';
import 'package:PiliPalaX/pages/member/new/content/member_contribute/member_contribute.dart'
show ContributeType;
import 'package:PiliPalaX/pages/video/detail/reply/view.dart';
import 'package:PiliPalaX/utils/grid.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class MemberVideo extends StatefulWidget {
const MemberVideo({
super.key,
required this.type,
required this.heroTag,
required this.mid,
this.seasonId,
this.seriesId,
});
final ContributeType type;
final String? heroTag;
final int mid;
final int? seasonId;
final int? seriesId;
@override
State<MemberVideo> createState() => _MemberVideoState();
}
class _MemberVideoState extends State<MemberVideo>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
late final _controller = Get.put(
MemberVideoCtr(
type: widget.type,
mid: widget.mid,
seasonId: widget.seasonId,
seriesId: widget.seriesId,
),
tag:
'${widget.heroTag}${widget.type.name}${widget.seasonId}${widget.seriesId}',
);
@override
Widget build(BuildContext context) {
super.build(context);
return Obx(() => _buildBody(_controller.loadingState.value));
}
_buildBody(LoadingState loadingState) {
return loadingState is Success
? RefreshIndicator(
onRefresh: () async {
await _controller.onRefresh();
},
child: CustomScrollView(
slivers: [
SliverPersistentHeader(
pinned: false,
floating: true,
delegate: MySliverPersistentHeaderDelegate(
child: Container(
height: 40,
padding: const EdgeInsets.fromLTRB(12, 0, 6, 0),
color: Theme.of(context).colorScheme.surface,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Obx(
() => Text(
_controller.count.value != -1
? '${_controller.count.value}视频'
: '',
style: const TextStyle(fontSize: 13),
),
),
SizedBox(
height: 35,
child: TextButton.icon(
onPressed: _controller.queryBySort,
icon: const Icon(Icons.sort, size: 16),
label: Obx(
() => Text(
widget.type == ContributeType.video
? _controller.order.value == 'pubdate'
? '最新发布'
: '最多播放'
: _controller.sort.value == 'desc'
? '默认'
: '倒序',
style: const TextStyle(fontSize: 13),
),
),
),
)
],
),
),
),
),
SliverPadding(
// 单列布局 EdgeInsets.zero
padding: EdgeInsets.fromLTRB(
StyleString.safeSpace,
StyleString.safeSpace - 5,
StyleString.safeSpace,
MediaQuery.of(context).padding.bottom + 10,
),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithExtentAndRatio(
mainAxisSpacing: StyleString.safeSpace,
crossAxisSpacing: StyleString.safeSpace,
maxCrossAxisExtent: Grid.maxRowWidth * 2,
childAspectRatio: StyleString.aspectRatio * 2.4,
mainAxisExtent: 0,
),
delegate: SliverChildBuilderDelegate(
(context, index) {
if (widget.type != ContributeType.season &&
index == loadingState.response.length - 1) {
_controller.onLoadMore();
}
return VideoCardHMemberVideo(
videoItem: loadingState.response[index],
showPubdate: true,
);
},
childCount: loadingState.response.length,
),
),
),
],
),
)
: loadingState is Error
? Center(
child: CustomScrollView(
shrinkWrap: true,
slivers: [
HttpError(
errMsg: loadingState.errMsg,
fn: _controller.onReload,
),
],
),
)
: Center(
child: CircularProgressIndicator(),
);
}
}

View File

@@ -0,0 +1,90 @@
import 'package:PiliPalaX/http/loading_state.dart';
import 'package:PiliPalaX/http/member.dart';
import 'package:PiliPalaX/models/space_archive/data.dart';
import 'package:PiliPalaX/pages/common/common_controller.dart';
import 'package:PiliPalaX/pages/member/new/content/member_contribute/member_contribute.dart'
show ContributeType;
import 'package:get/get.dart';
class MemberVideoCtr extends CommonController {
MemberVideoCtr({
required this.type,
required this.mid,
required this.seasonId,
required this.seriesId,
});
ContributeType type;
int? seasonId;
int? seriesId;
final int mid;
String? aid;
RxString order = 'pubdate'.obs;
RxString sort = 'desc'.obs;
bool isEnd = false;
RxInt count = (-1).obs;
int? next;
@override
Future onRefresh() async {
isEnd = false;
aid = null;
next = null;
currentPage = 0;
await queryData();
}
@override
void onInit() {
super.onInit();
currentPage = 0;
queryData();
}
@override
Future queryData([bool isRefresh = true]) {
if (isEnd) return Future.value();
return super.queryData(isRefresh);
}
@override
bool customHandleResponse(Success response) {
Data data = response.response;
next = data.next;
aid = data.item?.lastOrNull?.param;
isEnd =
type == ContributeType.video ? data.hasNext == false : data.next == 0;
if (currentPage == 0) {
count.value = type == ContributeType.season
? (data.item?.length ?? -1)
: (data.count ?? -1);
} else if (loadingState.value is Success) {
data.item?.insertAll(0, (loadingState.value as Success).response);
}
loadingState.value = LoadingState.success(data.item);
return true;
}
@override
Future<LoadingState> customGetData() => MemberHttp.spaceArchive(
type: type,
mid: mid,
aid: type == ContributeType.video ? aid : null,
order: order.value,
sort: sort.value,
pn: type == ContributeType.charging ? currentPage : null,
next: next,
seasonId: seasonId,
seriesId: seriesId,
);
queryBySort() {
if (type == ContributeType.video) {
order.value = order.value == 'pubdate' ? 'click' : 'pubdate';
} else {
sort.value = sort.value == 'desc' ? 'asc' : 'desc';
}
loadingState.value = LoadingState.loading();
onRefresh();
}
}

View File

@@ -0,0 +1,120 @@
import 'package:PiliPalaX/pages/member/new/content/member_contribute/content/article/member_article.dart';
import 'package:PiliPalaX/pages/member/new/content/member_contribute/content/audio/member_audio.dart';
import 'package:PiliPalaX/pages/member/new/content/member_contribute/content/video/member_video.dart';
import 'package:PiliPalaX/pages/member/new/content/member_contribute/member_contribute_ctr.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
enum ContributeType { video, charging, season, series, bangumi }
class MemberContribute extends StatefulWidget {
const MemberContribute({
super.key,
this.heroTag,
this.initialIndex,
required this.mid,
});
final String? heroTag;
final int? initialIndex;
final int mid;
@override
State<MemberContribute> createState() => _MemberContributeState();
}
class _MemberContributeState extends State<MemberContribute>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
late final _controller = Get.put(
MemberContributeCtr(
heroTag: widget.heroTag,
initialIndex: widget.initialIndex,
),
tag: widget.heroTag,
);
@override
Widget build(BuildContext context) {
super.build(context);
return _controller.tabs != null
? Column(
children: [
Container(
width: double.infinity,
color: Theme.of(context).colorScheme.surface,
child: Theme(
data: ThemeData(
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
),
child: TabBar(
padding: const EdgeInsets.symmetric(horizontal: 8),
isScrollable: true,
tabs: _controller.tabs!,
tabAlignment: TabAlignment.start,
controller: _controller.tabController,
dividerHeight: 0,
indicatorWeight: 0,
indicatorPadding:
const EdgeInsets.symmetric(horizontal: 3, vertical: 8),
indicator: BoxDecoration(
color: Theme.of(context).colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(20),
),
indicatorSize: TabBarIndicatorSize.tab,
labelStyle: const TextStyle(fontSize: 14),
labelColor:
Theme.of(context).colorScheme.onSecondaryContainer,
unselectedLabelColor: Theme.of(context).colorScheme.outline,
),
),
),
Expanded(
child: TabBarView(
physics: const NeverScrollableScrollPhysics(),
controller: _controller.tabController,
children: _controller.items!
.map(
(item) => switch (item.param) {
'video' => MemberVideo(
type: ContributeType.video,
heroTag: widget.heroTag,
mid: widget.mid,
),
'charging_video' => MemberVideo(
type: ContributeType.charging,
heroTag: widget.heroTag,
mid: widget.mid,
),
'article' => MemberArticle(heroTag: widget.heroTag),
'audio' => MemberAudio(heroTag: widget.heroTag),
'season_video' => MemberVideo(
type: ContributeType.season,
heroTag: widget.heroTag,
mid: widget.mid,
seasonId: item.seasonId,
),
'series' => MemberVideo(
type: ContributeType.series,
heroTag: widget.heroTag,
mid: widget.mid,
seriesId: item.seriesId,
),
_ => Center(child: Text(item.title!))
},
)
.toList(),
),
),
],
)
: MemberVideo(
type: ContributeType.video,
heroTag: widget.heroTag,
mid: widget.mid,
);
}
}

View File

@@ -0,0 +1,48 @@
import 'dart:math';
import 'package:PiliPalaX/http/loading_state.dart';
import 'package:PiliPalaX/models/space/tab2.dart';
import 'package:PiliPalaX/pages/common/common_controller.dart';
import 'package:PiliPalaX/pages/member/new/controller.dart';
import 'package:PiliPalaX/utils/extension.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../../../models/space/item.dart';
class MemberContributeCtr extends CommonController
with GetTickerProviderStateMixin {
MemberContributeCtr({
required this.heroTag,
required this.initialIndex,
});
final String? heroTag;
final int? initialIndex;
TabController? tabController;
List<Tab>? tabs;
late final _ctr = Get.find<MemberControllerNew>(tag: heroTag);
List<Item>? items;
@override
void onInit() {
super.onInit();
Tab2 contribute =
_ctr.tab2!.firstWhere((item) => item.param == 'contribute');
if (contribute.items?.isNullOrEmpty == false &&
contribute.items!.length > 1) {
items = contribute.items;
tabs = items!.map((item) => Tab(text: item.title)).toList();
tabController = TabController(
vsync: this,
length: items!.length,
initialIndex: max(0, initialIndex ?? 0),
);
}
}
@override
Future<LoadingState> customGetData() {
throw UnimplementedError();
}
}

View File

@@ -0,0 +1,75 @@
import 'package:PiliPalaX/common/widgets/http_error.dart';
import 'package:PiliPalaX/http/loading_state.dart';
import 'package:PiliPalaX/pages/dynamics/widgets/dynamic_panel_grpc.dart';
import 'package:PiliPalaX/pages/member/new/content/member_dynamic/member_dynamic_ctr.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class MemberDynamic extends StatefulWidget {
const MemberDynamic({
super.key,
required this.mid,
});
final int mid;
@override
State<MemberDynamic> createState() => _MemberDynamicState();
}
class _MemberDynamicState extends State<MemberDynamic>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
late final _controller = Get.put(MemberDynamicCtr(mid: widget.mid));
@override
Widget build(BuildContext context) {
super.build(context);
return ListView.separated(
itemCount: 100,
itemBuilder: (_, index) {
return ListTile(title: Text(index.toString()));
},
separatorBuilder: (_, index) => const SizedBox(height: 10),
);
return Obx(() => _buildBody(_controller.loadingState.value));
}
_buildBody(LoadingState loadingState) {
return loadingState is Success
? RefreshIndicator(
onRefresh: () async {
await _controller.onRefresh();
},
child: ListView.separated(
itemCount: loadingState.response.length,
itemBuilder: (_, index) {
if (index == loadingState.response.length - 1) {
_controller.onLoadMore();
}
return DynamicPanelGrpc(
item: loadingState.response[index],
);
},
separatorBuilder: (_, index) => const SizedBox(height: 10),
),
)
: loadingState is Error
? Center(
child: CustomScrollView(
shrinkWrap: true,
slivers: [
HttpError(
errMsg: loadingState.errMsg,
fn: _controller.onReload,
),
],
),
)
: Center(
child: CircularProgressIndicator(),
);
}
}

View File

@@ -0,0 +1,48 @@
import 'package:PiliPalaX/grpc/app/dynamic/v2/dynamic.pb.dart';
import 'package:PiliPalaX/http/loading_state.dart';
import 'package:PiliPalaX/http/member.dart';
import 'package:PiliPalaX/pages/common/common_controller.dart';
class MemberDynamicCtr extends CommonController {
MemberDynamicCtr({
required this.mid,
});
int mid;
bool isEnd = false;
@override
void onInit() {
super.onInit();
// queryData();
}
@override
Future onRefresh() {
isEnd = false;
return super.onRefresh();
}
@override
Future queryData([bool isRefresh = true]) {
if (isEnd) return Future.value();
return super.queryData(isRefresh);
}
@override
bool customHandleResponse(Success response) {
DynSpaceRsp res = response.response;
isEnd = !res.hasMore;
if (currentPage != 1) {
res.list.insertAll(
0, (loadingState.value as Success?)?.response ?? <DynamicItem>[]);
}
loadingState.value = LoadingState.success(res.list);
return true;
}
@override
Future<LoadingState> customGetData() => MemberHttp.spaceDynamic(
mid: mid,
page: currentPage,
);
}

View File

@@ -0,0 +1,214 @@
import 'dart:math';
import 'package:PiliPalaX/common/constants.dart';
import 'package:PiliPalaX/common/widgets/video_card_v_member_home.dart';
import 'package:PiliPalaX/http/loading_state.dart';
import 'package:PiliPalaX/models/space/data.dart';
import 'package:PiliPalaX/models/space/item.dart';
import 'package:PiliPalaX/pages/bangumi/widgets/bangumi_card_v_member_home.dart';
import 'package:PiliPalaX/pages/member/new/content/member_contribute/member_contribute_ctr.dart';
import 'package:PiliPalaX/pages/member/new/controller.dart';
import 'package:PiliPalaX/utils/grid.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class MemberHome extends StatefulWidget {
const MemberHome({super.key, this.heroTag});
final String? heroTag;
@override
State<MemberHome> createState() => _MemberHomeState();
}
class _MemberHomeState extends State<MemberHome>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
late final _ctr = Get.find<MemberControllerNew>(tag: widget.heroTag);
@override
Widget build(BuildContext context) {
super.build(context);
return _buildBody(_ctr.loadingState.value);
}
Widget _buildBody(LoadingState loadingState) {
return loadingState is Success && loadingState.response is Data
? CustomScrollView(
slivers: [
if (loadingState.response?.archive?.item?.isNotEmpty == true) ...[
_videoHeader(
title: '视频',
param: 'contribute',
param1: 'video',
count: loadingState.response.archive.count,
),
SliverPadding(
padding: const EdgeInsets.symmetric(
horizontal: StyleString.safeSpace,
),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithExtentAndRatio(
mainAxisSpacing: StyleString.cardSpace,
crossAxisSpacing: StyleString.cardSpace,
maxCrossAxisExtent: Grid.maxRowWidth,
childAspectRatio: StyleString.aspectRatio,
mainAxisExtent:
MediaQuery.textScalerOf(context).scale(90),
),
delegate: SliverChildBuilderDelegate(
(context, index) {
return VideoCardVMemberHome(
videoItem: loadingState.response.archive.item[index],
);
},
childCount:
min(4, loadingState.response.archive.item.length),
),
),
),
],
if (loadingState.response?.article?.item?.isNotEmpty == true) ...[
_videoHeader(
title: '专栏',
param: 'contribute',
param1: 'article',
count: loadingState.response.article.count,
),
// TODO
],
if (loadingState.response?.audios?.item?.isNotEmpty == true) ...[
_videoHeader(
title: '音频',
param: 'contribute',
param1: 'audio',
count: loadingState.response.audios.count,
),
// TODO
],
if (loadingState.response?.season?.item?.isNotEmpty == true) ...[
_videoHeader(
title: '追番',
param: 'bangumi',
count: loadingState.response.season.count,
),
SliverPadding(
padding: const EdgeInsets.symmetric(
horizontal: StyleString.safeSpace,
),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithExtentAndRatio(
mainAxisSpacing: StyleString.cardSpace - 2,
crossAxisSpacing: StyleString.cardSpace,
maxCrossAxisExtent: Grid.maxRowWidth / 3 * 2,
childAspectRatio: 0.65,
mainAxisExtent:
MediaQuery.textScalerOf(context).scale(60),
),
delegate: SliverChildBuilderDelegate(
(context, index) {
return BangumiCardVMemberHome(
bangumiItem: loadingState.response.season.item[index],
);
},
childCount:
min(3, loadingState.response.season.item.length),
),
),
),
],
SliverToBoxAdapter(
child: SizedBox(
height: 12 + MediaQuery.of(context).padding.bottom,
),
),
],
)
: const SizedBox.shrink();
}
Widget _videoHeader({
required String title,
required String param,
String? param1,
required int count,
}) =>
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text.rich(
TextSpan(
children: [
TextSpan(text: '$title '),
TextSpan(
text: count.toString(),
style: TextStyle(
fontSize: 13,
color: Theme.of(context).colorScheme.outline,
),
),
],
),
),
GestureDetector(
onTap: () {
int index =
_ctr.tab2!.indexWhere((item) => item.param == param);
if (['video', 'article', 'audio'].contains(param1)) {
List<Item> items = _ctr.tab2!
.firstWhere((item) => item.param == param)
.items!;
int index1 =
items.indexWhere((item) => item.param == param1);
try {
final contributeCtr =
Get.find<MemberContributeCtr>(tag: widget.heroTag);
// contributeCtr.tabController?.animateTo(index1);
if (contributeCtr.tabController?.index != index1) {
contributeCtr.tabController?.index = index1;
}
print('initialized');
} catch (e) {
_ctr.contributeInitialIndex.value = index1;
print('not initialized');
}
}
if (index != -1) {
_ctr.tabController.animateTo(index);
}
},
child: Text.rich(
TextSpan(
children: [
TextSpan(
text: '查看更多',
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
),
),
WidgetSpan(
alignment: PlaceholderAlignment.top,
child: Icon(
Icons.arrow_forward_ios,
size: 14,
color: Theme.of(context).colorScheme.outline,
),
style: TextStyle(
fontSize: 13,
color: Theme.of(context).colorScheme.outline,
),
),
],
),
),
),
],
),
),
);
}