refa: dir

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-05-03 13:57:47 +08:00
parent 57fa8b4f3e
commit 7f70ee5045
260 changed files with 748 additions and 967 deletions

View File

@@ -1,81 +0,0 @@
import 'package:PiliPlus/common/widgets/loading_widget.dart';
import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/space_article/item.dart';
import 'package:PiliPlus/pages/member/content/member_contribute/content/article/member_article_ctr.dart';
import 'package:PiliPlus/pages/member/content/member_contribute/content/article/widget/item.dart';
import 'package:PiliPlus/utils/grid.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class MemberArticle extends StatefulWidget {
const MemberArticle({
super.key,
required this.heroTag,
required this.mid,
});
final String? heroTag;
final int mid;
@override
State<MemberArticle> createState() => _MemberArticleState();
}
class _MemberArticleState extends State<MemberArticle>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
late final _controller = Get.put(
MemberArticleCtr(mid: widget.mid),
tag: widget.heroTag,
);
@override
Widget build(BuildContext context) {
super.build(context);
return Obx(() => _buildBody(_controller.loadingState.value));
}
_buildBody(LoadingState<List<Item>?> loadingState) {
return switch (loadingState) {
Loading() => loadingWidget,
Success() => loadingState.response?.isNotEmpty == true
? refreshIndicator(
onRefresh: () async {
await _controller.onRefresh();
},
child: CustomScrollView(
slivers: [
SliverPadding(
padding: EdgeInsets.only(
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,
),
),
),
],
),
)
: scrollErrorWidget(
onReload: _controller.onReload,
),
Error() => scrollErrorWidget(
errMsg: loadingState.errMsg,
onReload: _controller.onReload,
),
};
}
}

View File

@@ -1,43 +0,0 @@
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/member.dart';
import 'package:PiliPlus/models/space_article/item.dart';
import 'package:PiliPlus/models/space_article/data.dart';
import 'package:PiliPlus/pages/common/common_list_controller.dart';
class MemberArticleCtr extends CommonListController<Data, Item> {
MemberArticleCtr({
required this.mid,
});
final int mid;
int count = -1;
@override
void onInit() {
super.onInit();
queryData();
}
@override
List<Item>? getDataList(Data response) {
return response.item;
}
@override
void checkIsEnd(int length) {
if (length >= count) {
isEnd = true;
}
}
@override
bool customHandleResponse(bool isRefresh, Success<Data> response) {
count = response.response.count ?? -1;
return false;
}
@override
Future<LoadingState<Data>> customGetData() =>
MemberHttp.spaceArticle(mid: mid, page: currentPage);
}

View File

@@ -1,111 +0,0 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/image_save.dart';
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
import 'package:PiliPlus/common/widgets/stat/stat.dart';
import 'package:PiliPlus/models/space_article/item.dart';
import 'package:PiliPlus/utils/app_scheme.dart';
import 'package:flutter/material.dart';
class MemberArticleItem extends StatelessWidget {
const MemberArticleItem({super.key, required this.item});
final Item item;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final outline = theme.colorScheme.outline;
return Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
if (item.uri?.isNotEmpty == true) {
PiliScheme.routePushFromUrl(item.uri!);
}
},
onLongPress: () {
imageSaveDialog(
title: item.title,
cover: item.originImageUrls?.firstOrNull,
);
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: StyleString.safeSpace,
vertical: 5,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (item.originImageUrls?.firstOrNull?.isNotEmpty == true) ...[
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(
builder:
(BuildContext context, BoxConstraints boxConstraints) {
return NetworkImgLayer(
src: item.originImageUrls!.first,
width: boxConstraints.maxWidth,
height: boxConstraints.maxHeight,
);
},
),
),
const SizedBox(width: 10),
],
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (item.title?.isNotEmpty == true) ...[
Expanded(
child: Text(
item.title!,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: theme.textTheme.bodyMedium!.fontSize,
height: 1.42,
letterSpacing: 0.3,
),
),
),
],
Text(
'${item.publishTimeText}',
style: TextStyle(
fontSize: 12,
height: 1,
color: outline,
),
),
const SizedBox(height: 3),
Row(
children: [
StatView(
context: context,
value: item.stats?.view ?? 0,
goto: 'picture',
textColor: outline,
),
const SizedBox(width: 16),
StatView(
context: context,
goto: 'reply',
value: item.stats?.reply ?? 0,
textColor: outline,
),
],
),
],
),
),
],
),
),
),
);
}
}

View File

@@ -1,27 +0,0 @@
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

@@ -1,98 +0,0 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/loading_widget.dart';
import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/space_archive/item.dart';
import 'package:PiliPlus/pages/bangumi/widgets/bangumi_card_v_member_home.dart';
import 'package:PiliPlus/pages/member/content/member_contribute/content/bangumi/member_bangumi_ctr.dart';
import 'package:PiliPlus/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<List<Item>?> loadingState) {
return switch (loadingState) {
Loading() => loadingWidget,
Success() => loadingState.response?.isNotEmpty == true
? 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 +
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,
),
),
),
],
),
)
: scrollErrorWidget(
onReload: _controller.onReload,
),
Error() => scrollErrorWidget(
errMsg: loadingState.errMsg,
onReload: _controller.onReload,
),
};
}
}

View File

@@ -1,56 +0,0 @@
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/member.dart';
import 'package:PiliPlus/models/space_archive/data.dart';
import 'package:PiliPlus/models/space_archive/item.dart';
import 'package:PiliPlus/pages/common/common_list_controller.dart';
import 'package:PiliPlus/pages/member/content/member_contribute/member_contribute.dart'
show ContributeType;
import 'package:PiliPlus/pages/member/controller.dart';
import 'package:get/get.dart';
import 'package:PiliPlus/models/space/data.dart' as space;
class MemberBangumiCtr extends CommonListController<Data, Item> {
MemberBangumiCtr({
required this.mid,
required this.heroTag,
});
final int mid;
final String? heroTag;
int? count;
late final _ctr = Get.find<MemberControllerNew>(tag: heroTag);
@override
void onInit() {
super.onInit();
dynamic response = (_ctr.loadingState.value as Success).response;
if (response is space.Data) {
currentPage = 2;
dynamic res = response.season;
loadingState.value = LoadingState.success(res.item);
count = res.count;
isEnd = res.item!.length >= count;
} else {
queryData();
}
}
@override
List<Item>? getDataList(Data response) {
return response.item;
}
@override
void checkIsEnd(int length) {
if (count != null && length >= count!) {
isEnd = true;
}
}
@override
Future<LoadingState<Data>> customGetData() => MemberHttp.spaceArchive(
type: ContributeType.bangumi,
mid: mid,
pn: currentPage,
);
}

View File

@@ -1,157 +0,0 @@
import 'package:PiliPlus/common/widgets/loading_widget.dart';
import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/space_fav/datum.dart';
import 'package:PiliPlus/models/space_fav/list.dart';
import 'package:PiliPlus/pages/member/content/member_contribute/content/favorite/member_favorite_ctr.dart';
import 'package:PiliPlus/pages/member/content/member_contribute/content/favorite/widget/item.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) {
final theme = Theme.of(context);
return switch (loadingState) {
Loading() => loadingWidget,
Success() => (loadingState.response as List?)?.isNotEmpty == true
? refreshIndicator(
onRefresh: () async {
await _controller.onRefresh();
},
child: CustomScrollView(
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,
),
)
],
),
)
: scrollErrorWidget(
onReload: _controller.onReload,
),
Error() => scrollErrorWidget(
errMsg: loadingState.errMsg,
onReload: _controller.onReload,
),
};
}
_buildItem(ThemeData theme, Datum data, bool isFirst) {
return Theme(
data: theme.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.colorScheme.outline,
),
),
],
),
),
controlAffinity: ListTileControlAffinity.leading,
children: [
...(data.mediaListResponse?.list as List<FavList>).map(
(item) => SizedBox(
height: 98,
child: MemberFavItem(
item: item,
callback: (res) {
if (res == true) {
_controller.first.value.mediaListResponse?.list
?.remove(item);
_controller.first.refresh();
} else {
Future.delayed(const Duration(milliseconds: 100), () {
_controller.onRefresh();
});
}
},
),
),
),
Obx(
() => (isFirst
? _controller.firstEnd.value
: _controller.secondEnd.value)
? const SizedBox.shrink()
: _buildLoadMoreItem(theme, isFirst),
),
],
),
);
}
_buildLoadMoreItem(ThemeData theme, bool isFirst) {
return ListTile(
dense: true,
onTap: () {
if (isFirst) {
_controller.userfavFolder();
} else {
_controller.userSubFolder();
}
},
title: Text(
'查看更多内容',
textAlign: TextAlign.center,
style: TextStyle(
color: theme.colorScheme.primary,
),
),
);
}
}

View File

@@ -1,110 +0,0 @@
import 'package:PiliPlus/http/api.dart';
import 'package:PiliPlus/http/init.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/member.dart';
import 'package:PiliPlus/models/space_fav/datum.dart';
import 'package:PiliPlus/models/space_fav/list.dart';
import 'package:PiliPlus/pages/common/common_data_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
class MemberFavoriteCtr extends CommonDataController {
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(bool isRefresh, 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) {
debugPrint(e.toString());
}
loadingState.value = response;
return true;
}
Future userfavFolder() async {
var res = await Request().get(Api.userFavFolder, queryParameters: {
'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, queryParameters: {
'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

@@ -1,151 +0,0 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/badge.dart';
import 'package:PiliPlus/common/widgets/image_save.dart';
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
import 'package:PiliPlus/models/space_fav/list.dart';
import 'package:PiliPlus/models/user/sub_folder.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class MemberFavItem extends StatelessWidget {
const MemberFavItem({super.key, required this.item, this.callback});
final FavList item;
final ValueChanged<bool?>? callback;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Material(
color: Colors.transparent,
child: InkWell(
onTap: () async {
if (item.state == 1) {
// invalid
return;
}
if (item.type == 0) {
dynamic res = await Get.toNamed(
'/favDetail',
parameters: {
'mediaId': item.id.toString(),
'heroTag': Utils.makeHeroTag(item.id),
},
);
callback?.call(res);
} else {
Get.toNamed(
'/subDetail',
arguments: SubFolderItemData(
type: item.type,
title: item.title,
cover: item.cover,
upper: Upper(
mid: item.upper?.mid,
name: item.upper?.name,
face: item.upper?.face,
),
mediaCount: item.mediaCount,
viewCount: item.viewCount,
),
parameters: {
'id': item.id.toString(),
'heroTag': Utils.makeHeroTag(item.id),
},
);
}
},
onLongPress: () {
imageSaveDialog(
title: item.title,
cover: item.cover,
);
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: StyleString.safeSpace,
vertical: 5,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(
builder: (context, boxConstraints) {
return Stack(
clipBehavior: Clip.none,
children: [
NetworkImgLayer(
src: item.cover,
width: boxConstraints.maxWidth,
height: boxConstraints.maxHeight,
),
if (item.type == 21)
PBadge(
right: 3,
bottom: 3,
text: '合集',
bold: false,
size: 'small',
)
else if (item.type == 0 || item.type == 11)
Positioned(
right: 3,
bottom: 3,
child: Container(
padding: const EdgeInsets.all(5),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: theme.colorScheme.primary,
),
child: Icon(
Icons.video_library_outlined,
size: 12,
color: theme.colorScheme.onPrimary,
),
),
),
],
);
},
),
),
const SizedBox(width: 10),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.title ?? '',
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const Spacer(),
Text(
item.type == 0
? '${item.mediaCount}个内容 · ${Utils.isPublicFavText(item.attr ?? 0)}'
: item.type == 11
? '${item.mediaCount}个内容 · ${item.upper?.name}'
: item.type == 21
? '创建者: ${item.upper?.name}\n${item.mediaCount}个视频 · ${Utils.numFormat(item.viewCount)}播放'
: '${item.mediaCount}个内容',
style: TextStyle(
fontSize: 12,
color: theme.colorScheme.outline,
),
),
],
),
),
],
),
),
),
);
}
}

View File

@@ -1,40 +0,0 @@
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/member.dart';
import 'package:PiliPlus/pages/common/common_list_controller.dart';
class SeasonSeriesController extends CommonListController {
SeasonSeriesController(this.mid);
final int mid;
int? count;
@override
void onInit() {
super.onInit();
queryData();
}
@override
List? getDataList(response) {
return ((response['seasons_list'] as List?) ?? []) +
((response['series_list'] as List?) ?? []);
}
@override
void checkIsEnd(int length) {
if (count != null && length >= count!) {
isEnd = true;
}
}
@override
bool customHandleResponse(bool isRefresh, Success response) {
count = response.response['page']?['total'];
return false;
}
@override
Future<LoadingState> customGetData() => MemberHttp.seasonSeriesList(
mid: mid,
pn: currentPage,
);
}

View File

@@ -1,111 +0,0 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/loading_widget.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/pages/member/content/member_contribute/content/season_series/controller.dart';
import 'package:PiliPlus/pages/member/content/member_contribute/content/season_series/widget/season_series_card.dart';
import 'package:PiliPlus/pages/member/content/member_contribute/content/video/member_video.dart';
import 'package:PiliPlus/pages/member/content/member_contribute/member_contribute.dart';
import 'package:PiliPlus/utils/grid.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class SeasonSeriesPage extends StatefulWidget {
const SeasonSeriesPage({
super.key,
required this.mid,
this.heroTag,
});
final int mid;
final String? heroTag;
@override
State<SeasonSeriesPage> createState() => _SeasonSeriesPageState();
}
class _SeasonSeriesPageState extends State<SeasonSeriesPage>
with AutomaticKeepAliveClientMixin {
late final _controller = Get.put(
SeasonSeriesController(widget.mid),
tag: widget.heroTag,
);
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context);
return Obx(() => _buildBody(_controller.loadingState.value));
}
Widget _buildBody(LoadingState<List<dynamic>?> loadingState) {
return switch (loadingState) {
Loading() => loadingWidget,
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,
),
),
),
],
)
: scrollErrorWidget(
onReload: () {
_controller.onReload();
},
),
Error() => scrollErrorWidget(
errMsg: loadingState.errMsg,
onReload: () {
_controller.onReload();
},
),
};
}
}

View File

@@ -1,102 +0,0 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/badge.dart';
import 'package:PiliPlus/common/widgets/image_save.dart';
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart';
class SeasonSeriesCard extends StatelessWidget {
const SeasonSeriesCard({
super.key,
required this.item,
required this.onTap,
});
final dynamic item;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return InkWell(
onLongPress: () {
imageSaveDialog(
title: item['meta']['name'],
cover: item['meta']['cover'],
);
},
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: StyleString.safeSpace,
vertical: 5,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints boxConstraints) {
final double maxWidth = boxConstraints.maxWidth;
final double maxHeight = boxConstraints.maxHeight;
return Stack(
clipBehavior: Clip.none,
children: [
NetworkImgLayer(
src: item['meta']['cover'],
width: maxWidth,
height: maxHeight,
),
PBadge(
text:
'${item['meta']['season_id'] != null ? '合集' : '列表'}: ${item['meta']['total']}',
bottom: 6.0,
right: 6.0,
),
],
);
},
),
),
const SizedBox(width: 10),
videoContent(context),
],
),
),
);
}
Widget videoContent(context) {
final theme = Theme.of(context);
return Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item['meta']['name'],
textAlign: TextAlign.start,
style: TextStyle(
fontSize: theme.textTheme.bodyMedium!.fontSize,
height: 1.42,
letterSpacing: 0.3,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const Spacer(),
Text(
Utils.dateFormat(item['meta']['ptime']),
maxLines: 1,
style: TextStyle(
fontSize: 12,
height: 1,
color: theme.colorScheme.outline,
overflow: TextOverflow.clip,
),
),
const Spacer(),
],
),
);
}
}

View File

@@ -1,230 +0,0 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart';
import 'package:PiliPlus/common/widgets/loading_widget.dart';
import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
import 'package:PiliPlus/common/widgets/video_card_h_member_video.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/space_archive/item.dart';
import 'package:PiliPlus/pages/member/content/member_contribute/content/video/member_video_ctr.dart';
import 'package:PiliPlus/pages/member/content/member_contribute/member_contribute.dart'
show ContributeType;
import 'package:PiliPlus/pages/member/controller.dart';
import 'package:PiliPlus/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,
this.title,
});
final ContributeType type;
final String? heroTag;
final int mid;
final int? seasonId;
final int? seriesId;
final String? title;
@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,
username: Get.find<MemberControllerNew>(tag: widget.heroTag).username,
title: widget.title,
),
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<List<Item>?> loadingState) {
final theme = Theme.of(context);
return switch (loadingState) {
Loading() => loadingWidget,
Success() => loadingState.response?.isNotEmpty == true
? Stack(
clipBehavior: Clip.none,
children: [
refreshIndicator(
onRefresh: () async {
await _controller.onRefresh();
},
child: CustomScrollView(
physics: PositionRetainedScrollPhysics(
shouldRetain: _controller.isLocating == true,
parent: 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'
? '默认'
: '倒序',
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 Item 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('定位至上次观看'),
),
),
),
],
)
: scrollErrorWidget(
onReload: _controller.onReload,
),
Error() => scrollErrorWidget(
errMsg: loadingState.errMsg,
onReload: _controller.onReload,
),
};
}
}

View File

@@ -1,224 +0,0 @@
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/member.dart';
import 'package:PiliPlus/http/search.dart';
import 'package:PiliPlus/models/space_archive/data.dart';
import 'package:PiliPlus/models/space_archive/episodic_button.dart';
import 'package:PiliPlus/models/space_archive/item.dart';
import 'package:PiliPlus/pages/common/common_list_controller.dart';
import 'package:PiliPlus/pages/member/content/member_contribute/member_contribute.dart'
show ContributeType;
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/id_utils.dart';
import 'package:PiliPlus/utils/page_utils.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
class MemberVideoCtr extends CommonListController<Data, Item> {
MemberVideoCtr({
required this.type,
required this.mid,
required this.seasonId,
required this.seriesId,
this.username,
this.title,
});
ContributeType type;
int? seasonId;
int? seriesId;
final int mid;
late RxString order = 'pubdate'.obs;
late RxString sort = 'desc'.obs;
RxInt count = (-1).obs;
int? next;
Rx<EpisodicButton> episodicButton = EpisodicButton().obs;
final String? username;
final String? title;
String? firstAid;
String? lastAid;
String? fromViewAid;
bool? isLocating;
bool? isLoadPrevious;
bool? hasPrev;
@override
Future onRefresh() async {
if (isLocating == true) {
if (hasPrev == true) {
isLoadPrevious = true;
await queryData();
}
} else {
firstAid = null;
lastAid = null;
next = null;
isEnd = false;
currentPage = 0;
await queryData();
}
}
@override
void onInit() {
super.onInit();
if (type == ContributeType.video) {
fromViewAid = Get.parameters['from_view_aid'];
}
currentPage = 0;
queryData();
}
@override
bool customHandleResponse(bool isRefresh, Success<Data> response) {
Data data = response.response;
episodicButton.value = data.episodicButton ?? EpisodicButton();
episodicButton.refresh();
next = data.next;
if (currentPage == 0 || isLoadPrevious == true) {
hasPrev = data.hasPrev;
}
if (currentPage == 0 || isLoadPrevious != true) {
if ((type == ContributeType.video
? data.hasNext == false
: data.next == 0) ||
data.item.isNullOrEmpty) {
isEnd = true;
}
}
count.value = type == ContributeType.season
? (data.item?.length ?? -1)
: (data.count ?? -1);
if (currentPage != 0 && loadingState.value is Success) {
data.item ??= <Item>[];
if (isLoadPrevious == true) {
data.item!.addAll((loadingState.value as Success).response);
} else {
data.item!.insertAll(0, (loadingState.value as Success).response);
}
}
firstAid = data.item?.firstOrNull?.param;
lastAid = data.item?.lastOrNull?.param;
isLoadPrevious = null;
loadingState.value = LoadingState.success(data.item);
return true;
}
@override
Future<LoadingState<Data>> customGetData() => MemberHttp.spaceArchive(
type: type,
mid: mid,
aid: type == ContributeType.video
? isLoadPrevious == true
? firstAid
: lastAid
: null,
order: type == ContributeType.video ? order.value : null,
sort: type == ContributeType.video
? isLoadPrevious == true
? 'asc'
: null
: sort.value,
pn: type == ContributeType.charging ? currentPage : null,
next: next,
seasonId: seasonId,
seriesId: seriesId,
includeCursor: isLocating == true && currentPage == 0 ? true : null,
);
queryBySort() {
if (type == ContributeType.video) {
isLocating = null;
order.value = order.value == 'pubdate' ? 'click' : 'pubdate';
} else {
sort.value = sort.value == 'desc' ? 'asc' : 'desc';
}
onReload();
}
void toViewPlayAll() async {
if (loadingState.value is Success) {
List<Item>? list = (loadingState.value as Success).response;
if (list.isNullOrEmpty) return;
if (episodicButton.value.text == '继续播放') {
dynamic oid = RegExp(r'oid=([\d]+)')
.firstMatch('${episodicButton.value.uri}')
?.group(1);
dynamic bvid = IdUtils.av2bv(int.tryParse(oid) ?? 0);
dynamic cid = await SearchHttp.ab2c(aid: oid, bvid: bvid);
PageUtils.toVideoPage(
'bvid=$bvid&cid=$cid',
arguments: {
'heroTag': Utils.makeHeroTag(oid),
'sourceType': 'archive',
'mediaId': seasonId ?? seriesId ?? mid,
'oid': oid,
'favTitle':
'$username: ${title ?? episodicButton.value.text ?? '播放全部'}',
if (seriesId == null) 'count': count.value,
if (seasonId != null || seriesId != null)
'mediaType': RegExp(r'page_type=([\d]+)')
.firstMatch('${episodicButton.value.uri}')
?.group(1),
'desc': RegExp(r'desc=([\d]+)')
.firstMatch('${episodicButton.value.uri}')
?.group(1) ==
'1',
'sortField': RegExp(r'sort_field=([\d]+)')
.firstMatch('${episodicButton.value.uri}')
?.group(1),
'isContinuePlaying': true,
},
);
return;
}
for (Item element in list!) {
if (element.cid == null) {
continue;
} else {
if (element.bvid != list.first.bvid) {
SmartDialog.showToast('已跳过不支持播放的视频');
}
bool desc = seasonId != null ? false : true;
desc = (seasonId != null || seriesId != null) &&
(type == ContributeType.video
? order.value == 'click'
: sort.value == 'asc')
? desc.not
: desc;
PageUtils.toVideoPage(
'bvid=${element.bvid}&cid=${element.cid}',
arguments: {
'videoItem': element,
'heroTag': Utils.makeHeroTag(element.bvid),
'sourceType': 'archive',
'mediaId': seasonId ?? seriesId ?? mid,
'oid': IdUtils.bv2av(element.bvid!),
'favTitle':
'$username: ${title ?? episodicButton.value.text ?? '播放全部'}',
if (seriesId == null) 'count': count.value,
if (seasonId != null || seriesId != null)
'mediaType': RegExp(r'page_type=([\d]+)')
.firstMatch('${episodicButton.value.uri}')
?.group(1),
'desc': desc,
if (type == ContributeType.video)
'sortField': order.value == 'click' ? 2 : 1,
},
);
break;
}
}
}
}
@override
Future onReload() {
isLocating = null;
return super.onReload();
}
}

View File

@@ -1,131 +0,0 @@
import 'package:PiliPlus/pages/member/content/member_contribute/content/article/member_article.dart';
import 'package:PiliPlus/pages/member/content/member_contribute/content/audio/member_audio.dart';
import 'package:PiliPlus/pages/member/content/member_contribute/content/season_series/season_series_page.dart';
import 'package:PiliPlus/pages/member/content/member_contribute/content/video/member_video.dart';
import 'package:PiliPlus/pages/member/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);
final theme = Theme.of(context);
return _controller.tabs != null
? Column(
children: [
SizedBox(
width: double.infinity,
child: TabBar(
overlayColor: WidgetStateProperty.all(Colors.transparent),
splashFactory: NoSplash.splashFactory,
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.colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(20),
),
indicatorSize: TabBarIndicatorSize.tab,
labelStyle: TabBarTheme.of(context)
.labelStyle
?.copyWith(fontSize: 14) ??
const TextStyle(fontSize: 14),
labelColor: theme.colorScheme.onSecondaryContainer,
unselectedLabelColor: theme.colorScheme.outline,
),
),
Expanded(
child: Material(
color: Colors.transparent,
child: TabBarView(
physics: const NeverScrollableScrollPhysics(),
controller: _controller.tabController,
children: _controller.items!.map(_getPageFromType).toList(),
),
),
),
],
)
: _controller.items?.isNotEmpty == true
? _getPageFromType(_controller.items!.first)
: const SizedBox.shrink();
}
Widget _getPageFromType(item) {
return switch (item.param) {
'video' => MemberVideo(
type: ContributeType.video,
heroTag: widget.heroTag,
mid: widget.mid,
title: item.title,
),
'charging_video' => MemberVideo(
type: ContributeType.charging,
heroTag: widget.heroTag,
mid: widget.mid,
title: item.title,
),
'article' => MemberArticle(
heroTag: widget.heroTag,
mid: widget.mid,
),
'audio' => MemberAudio(heroTag: widget.heroTag),
'season_video' => MemberVideo(
type: ContributeType.season,
heroTag: widget.heroTag,
mid: widget.mid,
seasonId: item.seasonId,
title: item.title,
),
'series' => MemberVideo(
type: ContributeType.series,
heroTag: widget.heroTag,
mid: widget.mid,
seriesId: item.seriesId,
title: item.title,
),
'ugcSeason' => SeasonSeriesPage(
mid: widget.mid,
heroTag: widget.heroTag,
),
_ => Center(child: Text(item.title!))
};
}
}

View File

@@ -1,64 +0,0 @@
import 'dart:math';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/space/tab2.dart';
import 'package:PiliPlus/pages/common/common_data_controller.dart';
import 'package:PiliPlus/pages/member/controller.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../../models/space/item.dart';
class MemberContributeCtr extends CommonDataController
with GetSingleTickerProviderStateMixin {
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) {
items = contribute.items;
if (contribute.items!.length > 1) {
// show if exist
if (_ctr.hasSeasonOrSeries == true) {
items!.add(
Item(
param: 'ugcSeason',
title: '全部合集/列表',
),
);
}
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();
}
@override
void onClose() {
tabController?.dispose();
super.onClose();
}
}

View File

@@ -1,338 +0,0 @@
import 'dart:math';
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/loading_widget.dart';
import 'package:PiliPlus/common/widgets/video_card_v_member_home.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/space/data.dart';
import 'package:PiliPlus/models/space/item.dart';
import 'package:PiliPlus/pages/bangumi/widgets/bangumi_card_v_member_home.dart';
import 'package:PiliPlus/pages/member/content/member_contribute/content/article/widget/item.dart';
import 'package:PiliPlus/pages/member/content/member_contribute/member_contribute_ctr.dart';
import 'package:PiliPlus/pages/member/content/member_home/widget/fav_item.dart';
import 'package:PiliPlus/pages/member/controller.dart';
import 'package:PiliPlus/pages/member_coin/index.dart';
import 'package:PiliPlus/pages/member_like/index.dart';
import 'package:PiliPlus/utils/grid.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.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) {
final isVertical = context.orientation == Orientation.portrait;
return switch (loadingState) {
Loading() => loadingWidget,
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.smallCardWidth,
childAspectRatio: StyleString.aspectRatio,
mainAxisExtent:
MediaQuery.textScalerOf(context).scale(55),
),
delegate: SliverChildBuilderDelegate(
(context, index) {
return VideoCardVMemberHome(
videoItem:
loadingState.response.archive.item[index],
);
},
childCount: min(isVertical ? 4 : 8,
loadingState.response.archive.item.length),
),
),
),
],
if (loadingState.response?.favourite2?.item?.isNotEmpty ==
true) ...[
_videoHeader(
title: '收藏',
param: 'favorite',
count: loadingState.response.favourite2.count,
),
SliverToBoxAdapter(
child: SizedBox(
height: 98,
child: MemberFavItem(
item: loadingState.response.favourite2.item.first,
),
),
),
],
if (loadingState.response?.coinArchive?.item?.isNotEmpty ==
true) ...[
_videoHeader(
title: '最近投币的视频',
param: 'coinArchive',
count: loadingState.response.coinArchive.count,
),
SliverPadding(
padding: const EdgeInsets.symmetric(
horizontal: StyleString.safeSpace,
),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithExtentAndRatio(
mainAxisSpacing: StyleString.cardSpace,
crossAxisSpacing: StyleString.cardSpace,
maxCrossAxisExtent: Grid.smallCardWidth,
childAspectRatio: StyleString.aspectRatio,
mainAxisExtent:
MediaQuery.textScalerOf(context).scale(55),
),
delegate: SliverChildBuilderDelegate(
(context, index) {
return VideoCardVMemberHome(
videoItem:
loadingState.response.coinArchive.item[index],
);
},
childCount: min(isVertical ? 2 : 4,
loadingState.response.coinArchive.item.length),
),
),
),
],
if (loadingState.response?.likeArchive?.item?.isNotEmpty ==
true) ...[
_videoHeader(
title: '最近点赞的视频',
param: 'likeArchive',
count: loadingState.response.likeArchive.count,
),
SliverPadding(
padding: const EdgeInsets.symmetric(
horizontal: StyleString.safeSpace,
),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithExtentAndRatio(
mainAxisSpacing: StyleString.cardSpace,
crossAxisSpacing: StyleString.cardSpace,
maxCrossAxisExtent: Grid.smallCardWidth,
childAspectRatio: StyleString.aspectRatio,
mainAxisExtent:
MediaQuery.textScalerOf(context).scale(55),
),
delegate: SliverChildBuilderDelegate(
(context, index) {
return VideoCardVMemberHome(
videoItem:
loadingState.response.likeArchive.item[index],
);
},
childCount: min(isVertical ? 2 : 4,
loadingState.response.likeArchive.item.length),
),
),
),
],
if (loadingState.response?.article?.item?.isNotEmpty ==
true) ...[
_videoHeader(
title: '专栏',
param: 'contribute',
param1: 'article',
count: loadingState.response.article.count,
),
SliverGrid(
gridDelegate: Grid.videoCardHDelegate(context),
delegate: SliverChildBuilderDelegate(
(context, index) {
return MemberArticleItem(
item: loadingState.response.article.item[index],
);
},
childCount: isVertical
? 1
: loadingState.response.article.item.length,
),
),
],
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,
crossAxisSpacing: StyleString.cardSpace,
maxCrossAxisExtent: Grid.smallCardWidth / 3 * 2,
childAspectRatio: 0.75,
mainAxisExtent:
MediaQuery.textScalerOf(context).scale(52),
),
delegate: SliverChildBuilderDelegate(
(context, index) {
return BangumiCardVMemberHome(
bangumiItem:
loadingState.response.season.item[index],
);
},
childCount: min(isVertical ? 3 : 6,
loadingState.response.season.item.length),
),
),
),
],
SliverToBoxAdapter(
child: SizedBox(
height: 80 + MediaQuery.of(context).padding.bottom,
),
),
],
)
: errorWidget(),
Error() => errorWidget(),
};
}
Widget _videoHeader({
required String title,
required String param,
String? param1,
required int count,
}) {
final color = Theme.of(context).colorScheme.outline;
return 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: color),
),
],
),
),
GestureDetector(
onTap: () {
int index =
_ctr.tab2!.indexWhere((item) => item.param == param);
if (index != -1) {
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;
}
debugPrint('initialized');
} catch (e) {
_ctr.contributeInitialIndex.value = index1;
debugPrint('not initialized');
}
}
_ctr.tabController?.animateTo(index);
} else {
if (param == 'coinArchive') {
Get.to(MemberCoinPage(
mid: _ctr.mid,
name: _ctr.username,
));
return;
}
if (param == 'likeArchive') {
Get.to(MemberLikePage(
mid: _ctr.mid,
name: _ctr.username,
));
return;
}
// else TODO
SmartDialog.showToast('view $param');
}
},
child: Text.rich(
TextSpan(
children: [
TextSpan(
text: '查看更多',
style: TextStyle(color: color),
),
WidgetSpan(
alignment: PlaceholderAlignment.top,
child: Icon(
Icons.arrow_forward_ios,
size: 14,
color: color,
),
style: TextStyle(fontSize: 13, color: color),
),
],
),
),
),
],
),
),
);
}
}

View File

@@ -1,84 +0,0 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/image_save.dart';
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class MemberFavItem extends StatelessWidget {
const MemberFavItem({super.key, required this.item});
final dynamic item;
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
if (item['type'] == 2) {
Get.toNamed(
'/favDetail',
parameters: {
'mediaId': item['media_id'].toString(),
'heroTag': Utils.makeHeroTag(item['media_id']),
},
);
}
},
onLongPress: () {
imageSaveDialog(
title: item['title'],
cover: item['cover'],
);
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: StyleString.safeSpace,
vertical: 5,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(
builder: (context, boxConstraints) {
return NetworkImgLayer(
src: item['cover'],
width: boxConstraints.maxWidth,
height: boxConstraints.maxHeight,
);
},
),
),
const SizedBox(width: 10),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item['title'] ?? '',
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const Spacer(),
Text(
'${item['count']}个内容 · ${item['is_public'] == 1 ? '私密' : '公开'}',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.outline,
),
),
],
),
),
],
),
),
),
);
}
}

View File

@@ -21,7 +21,7 @@ extension MemberTabTypeExt on MemberTabType {
String get title => ['默认', '首页', '动态', '投稿', '收藏', '番剧'][index];
}
class MemberControllerNew extends CommonDataController<Data, dynamic>
class MemberControllerNew extends CommonDataController<SpaceData, dynamic>
with GetTickerProviderStateMixin {
MemberControllerNew({required this.mid});
int mid;
@@ -61,8 +61,8 @@ class MemberControllerNew extends CommonDataController<Data, dynamic>
}
@override
bool customHandleResponse(bool isRefresh, Success<Data> response) {
Data data = response.response;
bool customHandleResponse(bool isRefresh, Success<SpaceData> response) {
SpaceData data = response.response;
username = data.card?.name ?? '';
isFollowed = data.card?.relation?.isFollowed;
if (data.relation == -1) {
@@ -133,7 +133,7 @@ class MemberControllerNew extends CommonDataController<Data, dynamic>
Tab2(
title: '投稿',
param: 'contribute',
items: [Item(title: '视频', param: 'video')],
items: [SpaceItem(title: '视频', param: 'video')],
),
Tab2(title: '收藏', param: 'favorite'),
Tab2(title: '追番', param: 'bangumi'),
@@ -151,7 +151,7 @@ class MemberControllerNew extends CommonDataController<Data, dynamic>
}
@override
Future<LoadingState<Data>> customGetData() => MemberHttp.space(
Future<LoadingState<SpaceData>> customGetData() => MemberHttp.space(
mid: mid,
fromViewAid: fromViewAid,
);

View File

@@ -4,10 +4,10 @@ import 'package:PiliPlus/common/widgets/radio_widget.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/member.dart';
import 'package:PiliPlus/models/space/data.dart';
import 'package:PiliPlus/pages/member/content/member_contribute/content/bangumi/member_bangumi.dart';
import 'package:PiliPlus/pages/member/content/member_contribute/content/favorite/member_favorite.dart';
import 'package:PiliPlus/pages/member/content/member_contribute/member_contribute.dart';
import 'package:PiliPlus/pages/member/content/member_home/member_home.dart';
import 'package:PiliPlus/pages/member_pgc/view.dart';
import 'package:PiliPlus/pages/member_favorite/view.dart';
import 'package:PiliPlus/pages/member_contribute/view.dart';
import 'package:PiliPlus/pages/member_home/view.dart';
import 'package:PiliPlus/pages/member/controller.dart';
import 'package:PiliPlus/pages/member/widget/user_info_card.dart';
import 'package:PiliPlus/pages/member_dynamics/view.dart';
@@ -19,14 +19,14 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
class MemberPageNew extends StatefulWidget {
const MemberPageNew({super.key});
class MemberPage extends StatefulWidget {
const MemberPage({super.key});
@override
State<MemberPageNew> createState() => _MemberPageNewState();
State<MemberPage> createState() => _MemberPageState();
}
class _MemberPageNewState extends State<MemberPageNew> {
class _MemberPageState extends State<MemberPage> {
late final int _mid;
late final String _heroTag;
late final MemberControllerNew _userController;
@@ -271,7 +271,7 @@ class _MemberPageNewState extends State<MemberPageNew> {
Widget _buildUserInfo(LoadingState userState, [bool isV = true]) {
return switch (userState) {
Loading() => const CircularProgressIndicator(),
Success() => userState.response is Data
Success() => userState.response is SpaceData
? Obx(
() => UserInfoCard(
isV: isV,

View File

@@ -1,504 +0,0 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/loading_widget.dart';
import 'package:PiliPlus/http/constants.dart';
import 'package:PiliPlus/http/index.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/page_utils.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:dio/dio.dart';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart' hide FormData, MultipartFile;
import 'package:image_cropper/image_cropper.dart';
import 'package:image_picker/image_picker.dart';
import 'package:intl/intl.dart';
import 'package:mime/mime.dart';
enum ProfileType { uname, sign, sex, birthday }
class EditProfilePage extends StatefulWidget {
const EditProfilePage({super.key});
@override
State<EditProfilePage> createState() => _EditProfilePageState();
}
class _EditProfilePageState extends State<EditProfilePage> {
LoadingState _loadingState = LoadingState.loading();
late final _textController = TextEditingController();
late final _imagePicker = ImagePicker();
@override
void initState() {
super.initState();
_getInfo();
}
@override
void dispose() {
_textController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(title: const Text('账号资料')),
body: _buildBody(theme, _loadingState),
);
}
_getInfo() async {
Map<String, String> data = {
'build': '1462100',
'c_locale': 'zh_CN',
'channel': 'yingyongbao',
'mobi_app': 'android_hd',
'platform': 'android',
's_locale': 'zh_CN',
'statistics': Constants.statistics,
};
Request()
.get('${HttpString.appBaseUrl}/x/v2/account/myinfo',
queryParameters: data)
.then((data) {
setState(() {
if (data.data['code'] == 0) {
_loadingState = LoadingState.success(data.data['data']);
} else {
_loadingState = LoadingState.error(data.data['message']);
}
});
});
}
Widget _buildBody(ThemeData theme, LoadingState loadingState) {
late final divider = Divider(
height: 1,
color: theme.dividerColor.withOpacity(0.1),
);
late final divider1 = Divider(
thickness: 16,
color: theme.dividerColor.withOpacity(0.1),
);
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']),
),
),
),
onTap: () {
EasyThrottle.throttle(
'imagePicker', const Duration(milliseconds: 500),
() async {
_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: 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(
errMsg: loadingState.errMsg,
onReload: _getInfo,
),
};
}
Widget _sexDialog(int current) {
return AlertDialog(
clipBehavior: Clip.hardEdge,
contentPadding: const EdgeInsets.symmetric(vertical: 12),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
_sexDialogItem(1, current, ''),
_sexDialogItem(0, current, '保密'),
_sexDialogItem(2, current, ''),
],
),
);
}
Widget _sexDialogItem(
int sex,
int current,
String text,
) {
return ListTile(
dense: true,
enabled: current != sex,
title: Padding(
padding: const EdgeInsets.only(left: 10),
child: Text(
text,
style: const TextStyle(fontSize: 14),
),
),
trailing: current == sex ? const Icon(size: 22, Icons.check) : null,
onTap: () {
Get.back();
_update(type: ProfileType.sex, datum: sex);
},
);
}
void _editDialog({
required ProfileType type,
required String title,
required String text,
}) {
_textController.text = text;
showDialog(
context: context,
builder: (BuildContext context) {
final theme = Theme.of(context);
return AlertDialog(
title: Text('修改$title'),
content: TextField(
controller: _textController,
minLines: type == ProfileType.uname ? 1 : 4,
maxLines: type == ProfileType.uname ? 1 : 4,
autofocus: true,
style: TextStyle(fontSize: 14),
textInputAction:
type == ProfileType.sign ? TextInputAction.newline : null,
inputFormatters: [
LengthLimitingTextInputFormatter(
type == ProfileType.uname ? 16 : 70),
],
decoration: InputDecoration(
hintText: text,
hintStyle: TextStyle(
fontSize: 14,
color: theme.colorScheme.outline,
),
),
),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(color: theme.colorScheme.outline),
),
),
TextButton(
onPressed: () {
if (_textController.text == text) {
SmartDialog.showToast('与原$title相同');
} else {
_update(type: type);
}
},
child: const Text('确定'),
),
],
);
},
).then((_) {
_textController.clear();
});
}
_update({
required ProfileType type,
dynamic datum,
}) async {
final accessKey = Accounts.main.accessKey;
if (accessKey.isNullOrEmpty) {
SmartDialog.showToast('请退出账号后重新登录');
return;
}
Map<String, String> data = {
'access_key': accessKey!,
'build': '1462100',
'c_locale': 'zh_CN',
'channel': 'yingyongbao',
'mobi_app': 'android_hd',
'platform': 'android',
's_locale': 'zh_CN',
'statistics': Constants.statistics,
'ts': (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString(),
if (type == ProfileType.uname)
'uname': _textController.text
else if (type == ProfileType.sign)
'user_sign': _textController.text
else if (type == ProfileType.birthday)
'birthday': datum
else if (type == ProfileType.sex)
'sex': datum.toString(),
};
Utils.appSign(data);
Request()
.post(
'/x/member/app/${type.name}/update',
data: data,
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
)
.then((data) {
if (data.data['code'] == 0) {
if (type == ProfileType.uname) {
(_loadingState as Success).response['name'] = _textController.text;
(_loadingState as Success).response['coins'] -= 6;
} else if (type == ProfileType.sign) {
(_loadingState as Success).response['sign'] = _textController.text;
} else if (type == ProfileType.birthday) {
(_loadingState as Success).response['birthday'] = datum;
} else if (type == ProfileType.sex) {
(_loadingState as Success).response['sex'] = datum;
}
SmartDialog.showToast('修改成功');
setState(() {});
if (type == ProfileType.uname || type == ProfileType.sign) {
Get.back();
}
} else {
SmartDialog.showToast(data.data['message']);
}
});
}
String _sex(int sex) {
return switch (sex) {
0 => '保密',
1 => '',
2 => '',
_ => '未知',
};
}
Widget _item({
required ThemeData theme,
required String title,
Widget? widget,
String? text,
GestureTapCallback? onTap,
bool needIcon = true,
}) {
return ListTile(
onTap: onTap,
dense: title != '头像',
leading: Text(
title,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (text != null)
Text(
text,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: theme.colorScheme.outline,
),
)
else if (widget != null)
widget,
if (needIcon)
Icon(
Icons.keyboard_arrow_right,
color: theme.colorScheme.outline,
)
],
),
);
}
void _pickImg(ThemeData theme) async {
try {
XFile? pickedFile = await _imagePicker.pickImage(
source: ImageSource.gallery,
imageQuality: 100,
);
if (pickedFile != null && mounted) {
String? mimeType =
lookupMimeType(pickedFile.path)?.split('/').getOrNull(1);
if (mimeType == 'gif') {
SmartDialog.showToast('不能选GIF');
return;
}
CroppedFile? croppedFile = await ImageCropper().cropImage(
sourcePath: pickedFile.path,
uiSettings: [
AndroidUiSettings(
toolbarTitle: '裁剪',
toolbarColor: theme.colorScheme.secondaryContainer,
toolbarWidgetColor: theme.colorScheme.onSecondaryContainer,
aspectRatioPresets: [
CropAspectRatioPresetCustom(),
],
lockAspectRatio: true,
hideBottomControls: true,
cropStyle: CropStyle.circle,
initAspectRatio: CropAspectRatioPresetCustom(),
),
IOSUiSettings(
title: '裁剪',
aspectRatioPresets: [
CropAspectRatioPresetCustom(),
],
cropStyle: CropStyle.circle,
aspectRatioLockEnabled: true,
resetAspectRatioEnabled: false,
aspectRatioPickerButtonHidden: true,
),
],
);
if (croppedFile != null) {
Request()
.post(
'/x/member/web/face/update',
queryParameters: {
'csrf': Accounts.main.csrf,
},
data: FormData.fromMap({
'dopost': 'save',
'DisplayRank': 10000,
'face': await MultipartFile.fromFile(croppedFile.path),
}),
)
.then((data) {
if (data.data['code'] == 0) {
(_loadingState as Success).response['face'] = data.data['data'];
SmartDialog.showToast('修改成功');
setState(() {});
} else {
SmartDialog.showToast(data.data['message']);
}
});
}
}
} catch (e) {
SmartDialog.showToast(e.toString());
}
}
}
class CropAspectRatioPresetCustom implements CropAspectRatioPresetData {
@override
(int, int)? get data => (1, 1);
@override
String get name => '1x1 (customized)';
}

View File

@@ -2,8 +2,8 @@ import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/avatar.dart';
import 'package:PiliPlus/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart'
show SourceModel;
import 'package:PiliPlus/models/space/card.dart' as space;
import 'package:PiliPlus/models/space/images.dart' as space;
import 'package:PiliPlus/models/space/card.dart';
import 'package:PiliPlus/models/space/images.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/page_utils.dart';
import 'package:PiliPlus/utils/storage.dart';
@@ -29,8 +29,8 @@ class UserInfoCard extends StatelessWidget {
final bool isV;
final bool isOwner;
final int relation;
final space.Card card;
final space.Images images;
final SpaceCard card;
final SpaceImages images;
final VoidCallback onFollow;
final dynamic live;
final int? silence;