Merge branch 'design'

This commit is contained in:
guozhigq
2023-11-27 00:43:53 +08:00
38 changed files with 1550 additions and 696 deletions

View File

@ -1,37 +1,5 @@
import 'package:flutter/material.dart';
// Widget pBadge(
// text,
// context,
// double? top,
// double? right,
// double? bottom,
// double? left, {
// type = 'primary',
// }) {
// Color bgColor = Theme.of(context).colorScheme.primary;
// Color color = Theme.of(context).colorScheme.onPrimary;
// if (type == 'gray') {
// bgColor = Colors.black54.withOpacity(0.4);
// color = Colors.white;
// }
// return Positioned(
// top: top,
// left: left,
// right: right,
// bottom: bottom,
// child: Container(
// padding: const EdgeInsets.symmetric(vertical: 1, horizontal: 6),
// decoration:
// BoxDecoration(borderRadius: BorderRadius.circular(4), color: bgColor),
// child: Text(
// text,
// style: TextStyle(fontSize: 11, color: color),
// ),
// ),
// );
// }
class PBadge extends StatelessWidget {
final String? text;
final double? top;

View File

@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
class ContentContainer extends StatelessWidget {
final Widget? contentWidget;
final Widget? bottomWidget;
final bool isScrollable;
final Clip? childClipBehavior;
const ContentContainer(
{Key? key,
this.contentWidget,
this.bottomWidget,
this.isScrollable = true,
this.childClipBehavior})
: super(key: key);
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return SingleChildScrollView(
clipBehavior: childClipBehavior ?? Clip.hardEdge,
physics: isScrollable ? null : NeverScrollableScrollPhysics(),
child: ConstrainedBox(
constraints: constraints.copyWith(
minHeight: constraints.maxHeight,
maxHeight: double.infinity,
),
child: IntrinsicHeight(
child: Column(
children: <Widget>[
if (contentWidget != null)
Expanded(
child: contentWidget!,
)
else
Spacer(),
if (bottomWidget != null) bottomWidget!,
],
),
),
),
);
},
);
}
}

View File

@ -17,6 +17,10 @@ class VideoCardH extends StatelessWidget {
final Function()? longPress;
final Function()? longPressEnd;
final String source;
final bool showOwner;
final bool showView;
final bool showDanmaku;
final bool showPubdate;
const VideoCardH({
Key? key,
@ -24,6 +28,10 @@ class VideoCardH extends StatelessWidget {
this.longPress,
this.longPressEnd,
this.source = 'normal',
this.showOwner = true,
this.showView = true,
this.showDanmaku = true,
this.showPubdate = false,
}) : super(key: key);
@override
@ -103,7 +111,14 @@ class VideoCardH extends StatelessWidget {
},
),
),
VideoContent(videoItem: videoItem, source: source)
VideoContent(
videoItem: videoItem,
source: source,
showOwner: showOwner,
showView: showView,
showDanmaku: showDanmaku,
showPubdate: showPubdate,
)
],
),
);
@ -119,8 +134,20 @@ class VideoContent extends StatelessWidget {
// ignore: prefer_typing_uninitialized_variables
final videoItem;
final String source;
const VideoContent(
{super.key, required this.videoItem, this.source = 'normal'});
final bool showOwner;
final bool showView;
final bool showDanmaku;
final bool showPubdate;
const VideoContent({
super.key,
required this.videoItem,
this.source = 'normal',
this.showOwner = true,
this.showView = true,
this.showDanmaku = true,
this.showPubdate = false,
});
@override
Widget build(BuildContext context) {
@ -179,34 +206,40 @@ class VideoContent extends StatelessWidget {
// ),
// ),
// const SizedBox(height: 4),
Row(
children: [
Text(
videoItem.owner.name,
style: TextStyle(
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
if (showPubdate)
Text(
Utils.dateFormat(videoItem.pubdate!),
style: TextStyle(
fontSize: 11, color: Theme.of(context).colorScheme.outline),
),
if (showOwner)
Row(
children: [
Text(
videoItem.owner.name,
style: TextStyle(
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
),
],
),
],
),
Row(
children: [
StatView(
theme: 'gray',
view: videoItem.stat.view,
),
const SizedBox(width: 8),
StatDanMu(
theme: 'gray',
danmu: videoItem.stat.danmaku,
),
// Text(
// Utils.dateFormat(videoItem.pubdate!),
// style: TextStyle(
// fontSize: 11,
// color: Theme.of(context).colorScheme.outline),
// )
if (showView) ...[
StatView(
theme: 'gray',
view: videoItem.stat.view,
),
const SizedBox(width: 8),
],
if (showDanmaku)
StatDanMu(
theme: 'gray',
danmu: videoItem.stat.danmaku,
),
const Spacer(),
// SizedBox(
// width: 20,

View File

@ -215,7 +215,7 @@ class Api {
// 粉丝
// vmid 用户id pn 页码 ps 每页个数最大50 order: desc
// order_type 排序规则 最近访问传空,最常访问传 attention
static const String fans = 'https://api.bilibili.com/x/relation/fans';
static const String fans = '/x/relation/fans';
// 直播
// ?page=1&page_size=30&platform=web
@ -372,4 +372,36 @@ class Api {
/// local_id
static const getWebKey =
'https://passport.bilibili.com/x/passport-login/web/key';
/// 置顶视频
static const getTopVideoApi = '/x/space/top/arc';
/// 主页 - 最近投币的视频
/// vmid
/// gaia_source = main_web
/// web_location
/// w_rid
/// wts
static const getRecentCoinVideoApi = '/x/space/coin/video';
/// 最近点赞的视频
static const getRecentLikeVideoApi = '/x/space/like/video';
/// 最近追番
static const getRecentBangumiApi = '/x/space/bangumi/follow/list';
/// 用户专栏
static const getMemberSeasonsApi = '/x/polymer/web-space/home/seasons_series';
/// 获赞数 播放数
static const getMemberViewApi = '/x/space/upstat';
/// 查询某个专栏
/// mid
/// season_id
/// sort_reverse
/// page_num
/// page_size
static const getSeasonDetailApi =
'/x/polymer/web-space/seasons_archives_list';
}

View File

@ -1,8 +1,12 @@
import 'dart:ffi';
import 'package:pilipala/http/index.dart';
import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/models/follow/result.dart';
import 'package:pilipala/models/member/archive.dart';
import 'package:pilipala/models/member/coin.dart';
import 'package:pilipala/models/member/info.dart';
import 'package:pilipala/models/member/seasons.dart';
import 'package:pilipala/models/member/tags.dart';
import 'package:pilipala/utils/wbi_sign.dart';
@ -215,4 +219,144 @@ class MemberHttp {
};
}
}
// 获取up置顶
static Future getTopVideo(String? vmid) async {
var res = await Request().get(Api.getTopVideoApi);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data']
.map<MemberTagItemModel>((e) => MemberTagItemModel.fromJson(e))
.toList()
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
// 获取uo专栏
static Future getMemberSeasons(int? mid, int? pn, int? ps) async {
var res = await Request().get(Api.getMemberSeasonsApi, data: {
'mid': mid,
'page_num': pn,
'page_size': ps,
});
if (res.data['code'] == 0) {
return {
'status': true,
'data': MemberSeasonsDataModel.fromJson(res.data['data']['items_lists'])
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
// 最近投币
static Future getRecentCoinVideo({required int mid}) async {
Map params = await WbiSign().makSign({
'mid': mid,
'gaia_source': 'main_web',
'web_location': 333.999,
});
var res = await Request().get(
Api.getRecentCoinVideoApi,
data: {
'vmid': mid,
'gaia_source': 'main_web',
'web_location': 333.999,
'w_rid': params['w_rid'],
'wts': params['wts'],
},
);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data']
.map<MemberCoinsDataModel>((e) => MemberCoinsDataModel.fromJson(e))
.toList(),
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
// 最近点赞
static Future getRecentLikeVideo({required int mid}) async {
Map params = await WbiSign().makSign({
'mid': mid,
'gaia_source': 'main_web',
'web_location': 333.999,
});
var res = await Request().get(
Api.getRecentLikeVideoApi,
data: {
'vmid': mid,
'gaia_source': 'main_web',
'web_location': 333.999,
'w_rid': params['w_rid'],
'wts': params['wts'],
},
);
if (res.data['code'] == 0) {
return {
'status': true,
'data': MemberSeasonsDataModel.fromJson(res.data['data']['items_lists'])
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
// 查看某个专栏
static Future getSeasonDetail({
required int mid,
required int seasonId,
bool sortReverse = false,
required int pn,
required int ps,
}) async {
var res = await Request().get(
Api.getSeasonDetailApi,
data: {
'mid': mid,
'season_id': seasonId,
'sort_reverse': sortReverse,
'page_num': pn,
'page_size': ps,
},
);
if (res.data['code'] == 0) {
try {
return {
'status': true,
'data': MemberSeasonsList.fromJson(res.data['data'])
};
} catch (err) {
print(err);
}
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
}

View File

@ -0,0 +1,89 @@
class MemberCoinsDataModel {
MemberCoinsDataModel({
this.aid,
this.bvid,
this.cid,
this.coins,
this.copyright,
this.ctime,
this.desc,
this.duration,
this.owner,
this.pic,
this.pubLocation,
this.pubdate,
this.resourceType,
this.state,
this.subtitle,
this.time,
this.title,
this.tname,
this.videos,
this.view,
this.danmaku,
});
int? aid;
String? bvid;
int? cid;
int? coins;
int? copyright;
int? ctime;
String? desc;
int? duration;
Owner? owner;
String? pic;
String? pubLocation;
int? pubdate;
String? resourceType;
int? state;
String? subtitle;
int? time;
String? title;
String? tname;
int? videos;
int? view;
int? danmaku;
MemberCoinsDataModel.fromJson(Map<String, dynamic> json) {
aid = json['aid'];
bvid = json['bvid'];
cid = json['cid'];
coins = json['coins'];
copyright = json['copyright'];
ctime = json['ctime'];
desc = json['desc'];
duration = json['duration'];
owner = Owner.fromJson(json['owner']);
pic = json['pic'];
pubLocation = json['pub_location'];
pubdate = json['pubdate'];
resourceType = json['resource_type'];
state = json['state'];
subtitle = json['subtitle'];
time = json['time'];
title = json['title'];
tname = json['tname'];
videos = json['videos'];
view = json['stat']['view'];
danmaku = json['stat']['danmaku'];
}
}
class Owner {
Owner({
this.mid,
this.name,
this.face,
});
int? mid;
String? name;
String? face;
Owner.fromJson(Map<String, dynamic> json) {
mid = json['mid'];
name = json['name'];
face = json['face'];
}
}

View File

@ -0,0 +1,108 @@
class MemberSeasonsDataModel {
MemberSeasonsDataModel({
this.page,
this.seasonsList,
});
Map? page;
List<MemberSeasonsList>? seasonsList;
MemberSeasonsDataModel.fromJson(Map<String, dynamic> json) {
page = json['page'];
seasonsList = json['seasons_list'] != null
? json['seasons_list']
.map<MemberSeasonsList>((e) => MemberSeasonsList.fromJson(e))
.toList()
: [];
}
}
class MemberSeasonsList {
MemberSeasonsList({
this.archives,
this.meta,
this.recentAids,
this.page,
});
List<MemberArchiveItem>? archives;
MamberMeta? meta;
List? recentAids;
Map? page;
MemberSeasonsList.fromJson(Map<String, dynamic> json) {
archives = json['archives'] != null
? json['archives']
.map<MemberArchiveItem>((e) => MemberArchiveItem.fromJson(e))
.toList()
: [];
meta = MamberMeta.fromJson(json['meta']);
page = json['page'];
}
}
class MemberArchiveItem {
MemberArchiveItem({
this.aid,
this.bvid,
this.ctime,
this.duration,
this.pic,
this.cover,
this.pubdate,
this.view,
this.title,
});
int? aid;
String? bvid;
int? ctime;
int? duration;
String? pic;
String? cover;
int? pubdate;
int? view;
String? title;
MemberArchiveItem.fromJson(Map<String, dynamic> json) {
aid = json['aid'];
bvid = json['bvid'];
ctime = json['ctime'];
duration = json['duration'];
pic = json['pic'];
cover = json['pic'];
pubdate = json['pubdate'];
view = json['stat']['view'];
title = json['title'];
}
}
class MamberMeta {
MamberMeta({
this.cover,
this.description,
this.mid,
this.name,
this.ptime,
this.seasonId,
this.total,
});
String? cover;
String? description;
int? mid;
String? name;
int? ptime;
int? seasonId;
int? total;
MamberMeta.fromJson(Map<String, dynamic> json) {
cover = json['cover'];
description = json['description'];
mid = json['mid'];
name = json['name'];
ptime = json['ptime'];
seasonId = json['season_id'];
total = json['total'];
}
}

View File

@ -95,8 +95,12 @@ class _PlDanmakuState extends State<PlDanmaku> {
// 根据position判断是否有已缓存弹幕。没有则请求对应段
int segIndex = (currentPosition / (6 * 60 * 1000)).ceil();
segIndex = segIndex < 1 ? 1 : segIndex;
if (ctr.dmSegList[segIndex - 1].elems.isEmpty &&
!ctr.hasrequestSeg.contains(segIndex - 1)) {
print('🌹🌹: ${segIndex}');
print('🌹🌹: ${ctr.dmSegList.length}');
print('🌹🌹: ${ctr.hasrequestSeg.contains(segIndex - 1)}');
if (segIndex - 1 >= ctr.dmSegList.length ||
(ctr.dmSegList[segIndex - 1].elems.isEmpty &&
!ctr.hasrequestSeg.contains(segIndex - 1))) {
ctr.hasrequestSeg.add(segIndex - 1);
ctr.currentSegIndex = segIndex;
EasyThrottle.throttle('follow', const Duration(seconds: 1), () {

View File

@ -1,4 +0,0 @@
library archive_panel;
export './controller.dart';
export 'index.dart';

View File

@ -1,240 +0,0 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:loading_more_list/loading_more_list.dart';
import 'package:pilipala/common/widgets/video_card_h.dart';
import 'package:pilipala/models/member/archive.dart';
import 'package:pilipala/pages/member/archive/index.dart';
import 'package:pilipala/utils/utils.dart';
import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart';
class ArchivePanel extends StatefulWidget {
final int? mid;
const ArchivePanel({super.key, this.mid});
@override
State<ArchivePanel> createState() => _ArchivePanelState();
}
class _ArchivePanelState extends State<ArchivePanel>
with AutomaticKeepAliveClientMixin {
DateTime lastRefreshTime = DateTime.now();
late final LoadMoreListSource source;
late final ArchiveController _archiveController;
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
print('🐶🐶: ${widget.mid}');
_archiveController = Get.put(ArchiveController(widget.mid),
tag: Utils.makeHeroTag(widget.mid));
source = LoadMoreListSource(_archiveController);
}
@override
Widget build(BuildContext context) {
super.build(context);
return PullToRefreshNotification(
onRefresh: () async {
await Future.delayed(const Duration(seconds: 1));
return true;
},
maxDragOffset: 50,
child: GlowNotificationWidget(
Column(
children: <Widget>[
// 下拉刷新指示器
// PullToRefreshContainer(
// (PullToRefreshScrollNotificationInfo? info) {
// return PullToRefreshHeader(info, lastRefreshTime);
// },
// ),
Padding(
padding:
const EdgeInsets.only(left: 14, top: 8, bottom: 8, right: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('排序方式'),
SizedBox(
height: 35,
width: 85,
child: TextButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () {
// _archiveController.order = 'click';
// _archiveController.pn = 1;
_archiveController.toggleSort();
source.refresh(true);
// LoadMoreListSource().loadData();
},
child: Obx(
() => AnimatedSwitcher(
duration: const Duration(milliseconds: 400),
transitionBuilder:
(Widget child, Animation<double> animation) {
return ScaleTransition(
scale: animation, child: child);
},
child: Text(
_archiveController.currentOrder['label']!,
key: ValueKey<String>(
_archiveController.currentOrder['label']!),
),
),
),
),
),
],
),
),
Expanded(
child: LoadingMoreList<VListItemModel>(
ListConfig<VListItemModel>(
sourceList: source,
itemBuilder:
(BuildContext c, VListItemModel item, int index) {
if (index == 0) {
return Column(
children: [
const SizedBox(height: 6),
VideoCardH(videoItem: item)
],
);
} else {
return VideoCardH(videoItem: item);
}
},
indicatorBuilder: _buildIndicator,
),
),
)
],
),
showGlowLeading: false,
),
);
}
Widget _buildIndicator(BuildContext context, IndicatorStatus status) {
TextStyle style =
TextStyle(fontSize: 13, color: Theme.of(context).colorScheme.outline);
Widget? widget;
switch (status) {
case IndicatorStatus.none:
widget = Container(height: 0.0);
break;
case IndicatorStatus.loadingMoreBusying:
widget = Text('加载中...', style: style);
widget = _setbackground(false, widget, height: 60.0);
break;
case IndicatorStatus.fullScreenBusying:
widget = Text('加载中...', style: style);
widget = _setbackground(true, widget);
break;
case IndicatorStatus.error:
/// TODO 异常逻辑
widget = Text('没有更多了', style: style);
widget = _setbackground(false, widget);
widget = GestureDetector(
onTap: () {},
child: widget,
);
break;
case IndicatorStatus.fullScreenError:
/// TODO 异常逻辑
widget = Text('没有更多了', style: style);
widget = _setbackground(true, widget);
widget = GestureDetector(
onTap: () {},
child: widget,
);
break;
case IndicatorStatus.noMoreLoad:
widget = Text('没有更多了', style: style);
widget = _setbackground(false, widget, height: 60.0);
break;
case IndicatorStatus.empty:
widget = Text('用户没有投稿', style: style);
widget = _setbackground(true, widget);
break;
}
return widget;
}
Widget _setbackground(bool full, Widget widget, {double height = 100}) {
widget = Padding(
padding: height == double.infinity
? EdgeInsets.zero
: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
child: Container(
width: double.infinity,
height: height,
color: Theme.of(context).colorScheme.background,
alignment: Alignment.center,
child: widget,
),
);
return widget;
}
Widget getIndicator(BuildContext context) {
final TargetPlatform platform = Theme.of(context).platform;
return platform == TargetPlatform.iOS
? const CupertinoActivityIndicator(
animating: true,
radius: 16.0,
)
: CircularProgressIndicator(
strokeWidth: 2.0,
valueColor:
AlwaysStoppedAnimation<Color>(Theme.of(context).primaryColor),
);
}
}
class LoadMoreListSource extends LoadingMoreBase<VListItemModel> {
late ArchiveController ctr;
LoadMoreListSource(this.ctr);
bool forceRefresh = false;
@override
Future<bool> loadData([bool isloadMoreAction = false]) async {
bool isSuccess = false;
var res = await ctr.getMemberArchive();
if (res['status']) {
if (ctr.pn == 2) {
clear();
}
addAll(res['data'].list.vlist);
}
if (length < res['data'].page['count']) {
isSuccess = true;
} else {
isSuccess = false;
}
return isSuccess;
}
@override
Future<bool> refresh([bool clearBeforeRequest = false]) async {
// _hasMore = true;
// pageindex = 1;
// //force to refresh list when you don't want clear list before request
// //for the case, if your list already has 20 items.
forceRefresh = !clearBeforeRequest;
var result = await super.refresh(clearBeforeRequest);
forceRefresh = false;
return result;
}
}

View File

@ -6,6 +6,7 @@ import 'package:pilipala/http/member.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/http/video.dart';
import 'package:pilipala/models/member/archive.dart';
import 'package:pilipala/models/member/coin.dart';
import 'package:pilipala/models/member/info.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:share_plus/share_plus.dart';
@ -20,9 +21,10 @@ class MemberController extends GetxController {
late int ownerMid;
// 投稿列表
RxList<VListItemModel>? archiveList = [VListItemModel()].obs;
var userInfo;
dynamic userInfo;
RxInt attribute = (-1).obs;
RxString attributeText = '关注'.obs;
RxList<MemberCoinsDataModel> recentCoinsList = <MemberCoinsDataModel>[].obs;
@override
void onInit() {
@ -55,14 +57,6 @@ class MemberController extends GetxController {
return res;
}
// Future getMemberCardInfo() async {
// var res = await MemberHttp.memberCardInfo(mid: mid);
// if (res['status']) {
// print(userStat);
// }
// return res;
// }
// 关注/取关up
Future actionRelationMod() async {
if (userInfo == null) {
@ -173,4 +167,35 @@ class MemberController extends GetxController {
void shareUser() {
Share.share('${memberInfo.value.name} - https://space.bilibili.com/$mid');
}
// 请求专栏
Future getMemberSeasons() async {
if (userInfo == null) return;
var res = await MemberHttp.getMemberSeasons(mid, 1, 10);
if (!res['status']) {
SmartDialog.showToast("用户专栏请求异常:${res['msg']}");
}
return res;
}
// 请求投币视频
Future getRecentCoinVideo() async {
if (userInfo == null) return;
var res = await MemberHttp.getRecentCoinVideo(mid: mid);
recentCoinsList.value = res['data'];
return res;
}
// 跳转查看动态
void pushDynamicsPage() => Get.toNamed('/memberDynamics?mid=$mid');
// 跳转查看投稿
void pushArchivesPage() => Get.toNamed('/memberArchive?mid=$mid');
// 跳转查看专栏
void pushSeasonsPage() {}
// 跳转查看最近投币
void pushRecentCoinsPage() async {
if (recentCoinsList.isNotEmpty) {}
}
}

View File

@ -1,31 +0,0 @@
import 'package:get/get.dart';
import 'package:pilipala/http/member.dart';
class MemberDynamicPanelController extends GetxController {
MemberDynamicPanelController(this.mid);
int? mid;
String offset = '';
int count = 0;
bool hasMore = true;
@override
void onInit() {
super.onInit();
mid ??= int.parse(Get.parameters['mid']!);
}
Future getMemberDynamic() async {
if (!hasMore) {
return {'status': false};
}
var res = await MemberHttp.memberDynamic(
offset: offset,
mid: mid,
);
if (res['status']) {
offset = res['data'].offset;
hasMore = res['data'].hasMore;
}
return res;
}
}

View File

@ -1,152 +0,0 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:loading_more_list/loading_more_list.dart';
import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/pages/dynamics/widgets/dynamic_panel.dart';
import 'package:pilipala/utils/utils.dart';
import 'controller.dart';
class MemberDynamicPanel extends StatefulWidget {
final int? mid;
const MemberDynamicPanel({super.key, this.mid});
@override
State<MemberDynamicPanel> createState() => _MemberDynamicPanelState();
}
class _MemberDynamicPanelState extends State<MemberDynamicPanel>
with AutomaticKeepAliveClientMixin {
DateTime lastRefreshTime = DateTime.now();
late final LoadMoreListSource source;
late final MemberDynamicPanelController _dynamicController;
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
_dynamicController = Get.put(MemberDynamicPanelController(widget.mid),
tag: Utils.makeHeroTag(widget.mid));
source = LoadMoreListSource(_dynamicController);
}
@override
Widget build(BuildContext context) {
super.build(context);
return LoadingMoreList<DynamicItemModel>(
ListConfig<DynamicItemModel>(
sourceList: source,
itemBuilder: (BuildContext c, DynamicItemModel item, int index) {
return DynamicPanel(item: item);
},
indicatorBuilder: _buildIndicator,
),
);
}
Widget _buildIndicator(BuildContext context, IndicatorStatus status) {
TextStyle style =
TextStyle(fontSize: 13, color: Theme.of(context).colorScheme.outline);
Widget? widget;
switch (status) {
case IndicatorStatus.none:
widget = Container(height: 0.0);
break;
case IndicatorStatus.loadingMoreBusying:
widget = Text('加载中...', style: style);
widget = _setbackground(false, widget, height: 60.0);
break;
case IndicatorStatus.fullScreenBusying:
widget = Text('加载中...', style: style);
widget = _setbackground(true, widget);
break;
case IndicatorStatus.error:
/// TODO 异常逻辑
widget = Text('没有更多了', style: style);
widget = _setbackground(false, widget);
widget = GestureDetector(
onTap: () {},
child: widget,
);
break;
case IndicatorStatus.fullScreenError:
/// TODO 异常逻辑
widget = Text('没有更多了', style: style);
widget = _setbackground(true, widget);
widget = GestureDetector(
onTap: () {},
child: widget,
);
break;
case IndicatorStatus.noMoreLoad:
widget = Text('没有更多了', style: style);
widget = _setbackground(false, widget, height: 60.0);
break;
case IndicatorStatus.empty:
widget = Text('用户没有投稿', style: style);
widget = _setbackground(true, widget);
break;
}
return widget;
}
Widget _setbackground(bool full, Widget widget, {double height = 100}) {
widget = Padding(
padding: height == double.infinity
? EdgeInsets.zero
: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
child: Container(
width: double.infinity,
height: height,
color: Theme.of(context).colorScheme.background,
alignment: Alignment.center,
child: widget,
),
);
return widget;
}
Widget getIndicator(BuildContext context) {
final TargetPlatform platform = Theme.of(context).platform;
return platform == TargetPlatform.iOS
? const CupertinoActivityIndicator(
animating: true,
radius: 16.0,
)
: CircularProgressIndicator(
strokeWidth: 2.0,
valueColor:
AlwaysStoppedAnimation<Color>(Theme.of(context).primaryColor),
);
}
}
class LoadMoreListSource extends LoadingMoreBase<DynamicItemModel> {
late MemberDynamicPanelController ctr;
LoadMoreListSource(this.ctr);
@override
Future<bool> loadData([bool isloadMoreAction = false]) async {
bool isSuccess = false;
var res = await ctr.getMemberDynamic();
if (res['status']) {
addAll(res['data'].items);
}
try {
if (res['data'].hasMore) {
isSuccess = true;
} else {
isSuccess = false;
}
} catch (_) {}
return isSuccess;
}
}

View File

@ -1,16 +1,16 @@
import 'dart:async';
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/pages/member/archive/view.dart';
import 'package:pilipala/pages/member/dynamic/index.dart';
import 'package:pilipala/pages/member/index.dart';
import 'package:pilipala/utils/utils.dart';
import 'widgets/conis.dart';
import 'widgets/profile.dart';
import 'widgets/seasons.dart';
class MemberPage extends StatefulWidget {
const MemberPage({super.key});
@ -23,9 +23,10 @@ class _MemberPageState extends State<MemberPage>
with SingleTickerProviderStateMixin {
late String heroTag;
late MemberController _memberController;
Future? _futureBuilderFuture;
late Future _futureBuilderFuture;
late Future _memberSeasonsFuture;
late Future _memberCoinsFuture;
final ScrollController _extendNestCtr = ScrollController();
late TabController _tabController;
final StreamController<bool> appbarStream = StreamController<bool>();
late int mid;
@ -35,12 +36,13 @@ class _MemberPageState extends State<MemberPage>
mid = int.parse(Get.parameters['mid']!);
heroTag = Get.arguments['heroTag'] ?? Utils.makeHeroTag(mid);
_memberController = Get.put(MemberController(), tag: heroTag);
_tabController = TabController(length: 3, vsync: this, initialIndex: 2);
_futureBuilderFuture = _memberController.getInfo();
_memberSeasonsFuture = _memberController.getMemberSeasons();
_memberCoinsFuture = _memberController.getRecentCoinVideo();
_extendNestCtr.addListener(
() {
double offset = _extendNestCtr.position.pixels;
if (offset > 230) {
if (offset > 100) {
appbarStream.add(true);
} else {
appbarStream.add(false);
@ -59,183 +61,222 @@ class _MemberPageState extends State<MemberPage>
Widget build(BuildContext context) {
return Scaffold(
primary: true,
body: ExtendedNestedScrollView(
controller: _extendNestCtr,
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
pinned: false,
primary: true,
elevation: 0,
scrolledUnderElevation: 1,
forceElevated: innerBoxIsScrolled,
expandedHeight: 290,
titleSpacing: 0,
title: StreamBuilder(
stream: appbarStream.stream,
initialData: false,
builder: (context, AsyncSnapshot snapshot) {
return AnimatedOpacity(
opacity: snapshot.data ? 1 : 0,
curve: Curves.easeOut,
duration: const Duration(milliseconds: 500),
child: Row(
children: [
Row(
children: [
Obx(
() => NetworkImgLayer(
width: 35,
height: 35,
type: 'avatar',
src: _memberController.face.value,
),
body: Column(
children: [
AppBar(
title: StreamBuilder(
stream: appbarStream.stream,
initialData: false,
builder: (context, AsyncSnapshot snapshot) {
return AnimatedOpacity(
opacity: snapshot.data ? 1 : 0,
curve: Curves.easeOut,
duration: const Duration(milliseconds: 500),
child: Row(
children: [
Row(
children: [
Obx(
() => NetworkImgLayer(
width: 35,
height: 35,
type: 'avatar',
src: _memberController.face.value,
),
const SizedBox(width: 10),
Obx(
() => Text(
_memberController.memberInfo.value.name ?? '',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onBackground,
fontSize: 14),
),
),
const SizedBox(width: 10),
Obx(
() => Text(
_memberController.memberInfo.value.name ?? '',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onBackground,
fontSize: 14),
),
],
)
],
),
);
},
),
actions: [
IconButton(
onPressed: () => Get.toNamed(
'/memberSearch?mid=${Get.parameters['mid']}&uname=${_memberController.memberInfo.value.name!}'),
icon: const Icon(Icons.search_outlined),
),
PopupMenuButton(
icon: const Icon(Icons.more_vert),
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
if (_memberController.ownerMid !=
_memberController.mid) ...[
PopupMenuItem(
onTap: () => _memberController.blockUser(),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.block, size: 19),
const SizedBox(width: 10),
Text(_memberController.attribute.value != 128
? '加入黑名单'
: '移除黑名单'),
],
),
),
],
)
],
),
);
},
),
actions: [
IconButton(
onPressed: () => Get.toNamed(
'/memberSearch?mid=${Get.parameters['mid']}&uname=${_memberController.memberInfo.value.name!}'),
icon: const Icon(Icons.search_outlined),
),
PopupMenuButton(
icon: const Icon(Icons.more_vert),
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
if (_memberController.ownerMid != _memberController.mid) ...[
PopupMenuItem(
onTap: () => _memberController.shareUser(),
onTap: () => _memberController.blockUser(),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.share_outlined, size: 19),
const Icon(Icons.block, size: 19),
const SizedBox(width: 10),
Text(_memberController.ownerMid !=
_memberController.mid
? '分享UP主'
: '分享我的主页'),
Text(_memberController.attribute.value != 128
? '加入黑名单'
: '移除黑名单'),
],
),
),
)
],
PopupMenuItem(
onTap: () => _memberController.shareUser(),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.share_outlined, size: 19),
const SizedBox(width: 10),
Text(_memberController.ownerMid != _memberController.mid
? '分享UP主'
: '分享我的主页'),
],
),
),
],
),
const SizedBox(width: 4),
],
),
Expanded(
child: SingleChildScrollView(
controller: _extendNestCtr,
child: Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom + 20,
),
const SizedBox(width: 4),
],
flexibleSpace: FlexibleSpaceBar(
background: Stack(
child: Column(
children: [
profileWidget(),
/// 动态链接
ListTile(
onTap: _memberController.pushDynamicsPage,
title: const Text('Ta的动态'),
trailing:
const Icon(Icons.arrow_forward_outlined, size: 19),
),
/// 视频
ListTile(
onTap: _memberController.pushArchivesPage,
title: const Text('Ta的投稿'),
trailing:
const Icon(Icons.arrow_forward_outlined, size: 19),
),
/// 专栏
ListTile(
onTap: () {},
title: const Text('Ta的专栏'),
),
MediaQuery.removePadding(
removeTop: true,
removeBottom: true,
context: context,
child: Padding(
padding: const EdgeInsets.only(
left: StyleString.safeSpace,
right: StyleString.safeSpace,
),
child: FutureBuilder(
future: _memberSeasonsFuture,
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.done) {
if (snapshot.data == null) {
return const SizedBox();
}
if (snapshot.data['status']) {
Map data = snapshot.data as Map;
if (data['data'].seasonsList.isEmpty) {
return commenWidget('用户没有设置专栏');
} else {
return MemberSeasonsPanel(data: data['data']);
}
} else {
// 请求错误
return const SizedBox();
}
} else {
return const SizedBox();
}
},
),
),
),
/// 收藏
/// 追番
/// 最近投币
Obx(
() => _memberController.face.value != ''
? Positioned.fill(
bottom: 10,
child: Container(
decoration: BoxDecoration(
image: DecorationImage(
fit: BoxFit.fitWidth,
image: NetworkImage(
_memberController.face.value),
alignment: Alignment.topCenter,
isAntiAlias: true,
),
),
foregroundDecoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Theme.of(context)
.colorScheme
.background
.withOpacity(0.44),
Theme.of(context).colorScheme.background,
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
stops: const [0.0, 0.46],
),
),
),
() => _memberController.recentCoinsList.isNotEmpty
? ListTile(
onTap: () {},
title: const Text('最近投币的视频'),
// trailing: const Icon(Icons.arrow_forward_outlined,
// size: 19),
)
: const SizedBox(),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
height: 20,
child: Container(
color: Theme.of(context).colorScheme.background,
MediaQuery.removePadding(
removeTop: true,
removeBottom: true,
context: context,
child: Padding(
padding: const EdgeInsets.only(
left: StyleString.safeSpace,
right: StyleString.safeSpace,
),
child: FutureBuilder(
future: _memberCoinsFuture,
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.done) {
if (snapshot.data == null) {
return const SizedBox();
}
if (snapshot.data['status']) {
Map data = snapshot.data as Map;
return MemberCoinsPanel(data: data['data']);
} else {
// 请求错误
return const SizedBox();
}
} else {
return const SizedBox();
}
},
),
),
),
profileWidget(),
// 最近点赞
// ListTile(
// onTap: () {},
// title: const Text('最近点赞的视频'),
// trailing:
// const Icon(Icons.arrow_forward_outlined, size: 19),
// ),
],
),
),
),
];
},
pinnedHeaderSliverHeightBuilder: () {
return MediaQuery.of(context).padding.top + kToolbarHeight;
},
onlyOneScrollInBody: true,
body: Column(
children: [
SizedBox(
width: double.infinity,
height: 50,
child: TabBar(controller: _tabController, tabs: const [
Tab(text: '主页'),
Tab(text: '动态'),
Tab(text: '投稿'),
]),
),
Expanded(
child: TabBarView(
controller: _tabController,
children: [
const Text('主页'),
MemberDynamicPanel(mid: mid),
ArchivePanel(mid: mid),
],
))
],
),
),
],
),
);
}
Widget profileWidget() {
return Padding(
padding: const EdgeInsets.only(left: 18, right: 18),
padding: const EdgeInsets.only(left: 18, right: 18, bottom: 20),
child: FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
@ -250,7 +291,7 @@ class _MemberPageState extends State<MemberPage>
crossAxisAlignment: CrossAxisAlignment.start,
children: [
profile(_memberController),
const SizedBox(height: 14),
const SizedBox(height: 20),
Row(
children: [
Flexible(
@ -260,7 +301,7 @@ class _MemberPageState extends State<MemberPage>
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.bodyLarge!
.titleMedium!
.copyWith(fontWeight: FontWeight.bold),
)),
const SizedBox(width: 2),
@ -332,29 +373,11 @@ class _MemberPageState extends State<MemberPage>
softWrap: true,
),
],
const SizedBox(height: 4),
const SizedBox(height: 6),
if (_memberController.memberInfo.value.sign != '')
SelectableText(
_memberController.memberInfo.value.sign!,
maxLines: _memberController
.memberInfo.value.official!['title'] !=
''
? 1
: 2,
style: const TextStyle(
overflow: TextOverflow.ellipsis),
onTap: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
content: SelectableText(_memberController
.memberInfo.value.sign!),
);
},
);
},
)
),
],
),
],
@ -371,4 +394,22 @@ class _MemberPageState extends State<MemberPage>
),
);
}
Widget commenWidget(msg) {
return Padding(
padding: const EdgeInsets.only(
top: 20,
bottom: 30,
),
child: Center(
child: Text(
msg,
style: Theme.of(context)
.textTheme
.labelMedium!
.copyWith(color: Theme.of(context).colorScheme.outline),
),
),
);
}
}

View File

@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/models/member/coin.dart';
import 'package:pilipala/pages/member_coin/widgets/item.dart';
class MemberCoinsPanel extends StatelessWidget {
final List<MemberCoinsDataModel>? data;
const MemberCoinsPanel({super.key, this.data});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, boxConstraints) {
return GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, // Use a fixed count for GridView
crossAxisSpacing: StyleString.safeSpace,
mainAxisSpacing: StyleString.safeSpace,
childAspectRatio: 0.94,
),
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: data!.length,
itemBuilder: (context, i) {
return MemberCoinsItem(coinItem: data![i]);
},
);
},
);
}
}

View File

@ -11,7 +11,7 @@ Widget profile(ctr, {loadingStatus = false}) {
return Builder(
builder: ((context) {
return Padding(
padding: EdgeInsets.only(top: 3 * MediaQuery.of(context).padding.top),
padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top - 20),
child: Row(
children: [
Hero(
@ -78,7 +78,8 @@ Widget profile(ctr, {loadingStatus = false}) {
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(left: 10, right: 10),
padding:
const EdgeInsets.only(top: 10, left: 10, right: 10),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,

View File

@ -0,0 +1,85 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/models/member/seasons.dart';
import 'package:pilipala/pages/member_seasons/widgets/item.dart';
class MemberSeasonsPanel extends StatelessWidget {
final MemberSeasonsDataModel? data;
const MemberSeasonsPanel({super.key, this.data});
@override
Widget build(BuildContext context) {
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: data!.seasonsList!.length,
itemBuilder: (context, index) {
MemberSeasonsList item = data!.seasonsList![index];
return Padding(
padding: const EdgeInsets.only(bottom: 12, right: 4),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 12, left: 4),
child: Row(
children: [
Text(
item.meta!.name!,
maxLines: 1,
style: Theme.of(context).textTheme.titleSmall!,
),
const SizedBox(width: 10),
PBadge(
stack: 'relative',
size: 'small',
text: item.meta!.total.toString(),
),
const Spacer(),
SizedBox(
width: 35,
height: 35,
child: IconButton(
onPressed: () => Get.toNamed(
'/memberSeasons?mid=${item.meta!.mid}&seasonId=${item.meta!.seasonId}'),
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
icon: const Icon(
Icons.arrow_forward,
size: 20,
),
),
)
],
),
),
LayoutBuilder(
builder: (context, boxConstraints) {
return GridView.builder(
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, // Use a fixed count for GridView
crossAxisSpacing: StyleString.safeSpace,
mainAxisSpacing: StyleString.safeSpace,
childAspectRatio: 0.94,
),
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: item.archives!.length,
itemBuilder: (context, i) {
return MemberSeasonsItem(seasonItem: item.archives![i]);
},
);
},
),
],
),
);
},
);
}
}

View File

@ -1,9 +1,11 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/member.dart';
import 'package:pilipala/models/member/archive.dart';
class ArchiveController extends GetxController {
ArchiveController(this.mid);
int? mid;
class MemberArchiveController extends GetxController {
final ScrollController scrollController = ScrollController();
late int mid;
int pn = 1;
int count = 0;
RxMap<String, String> currentOrder = <String, String>{}.obs;
@ -12,20 +14,27 @@ class ArchiveController extends GetxController {
{'type': 'click', 'label': '最多播放'},
{'type': 'stow', 'label': '最多收藏'},
];
RxList<VListItemModel> archivesList = <VListItemModel>[].obs;
@override
void onInit() {
super.onInit();
mid ??= int.parse(Get.parameters['mid']!);
print('🐶🐶: $mid');
mid = int.parse(Get.parameters['mid']!);
currentOrder.value = orderList.first;
}
// 稿
Future getMemberArchive() async {
Future getMemberArchive(type) async {
if (type == 'onRefresh') {
pn = 1;
}
var res = await MemberHttp.memberArchive(
mid: mid, pn: pn, order: currentOrder['type']!);
mid: mid,
pn: pn,
order: currentOrder['type']!,
);
if (res['status']) {
archivesList.addAll(res['data'].list.vlist);
count = res['data'].page['count'];
pn += 1;
}
@ -34,11 +43,16 @@ class ArchiveController extends GetxController {
toggleSort() async {
pn = 1;
int index = orderList.indexOf(currentOrder.value);
int index = orderList.indexOf(currentOrder);
if (index == orderList.length - 1) {
currentOrder.value = orderList.first;
} else {
currentOrder.value = orderList[index + 1];
}
}
//
Future onLoad() async {
getMemberArchive('onLoad');
}
}

View File

@ -0,0 +1,4 @@
library member_archive;
export './controller.dart';
export './view.dart';

View File

@ -0,0 +1,120 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/video_card_h.dart';
import 'controller.dart';
class MemberArchivePage extends StatefulWidget {
const MemberArchivePage({super.key});
@override
State<MemberArchivePage> createState() => _MemberArchivePageState();
}
class _MemberArchivePageState extends State<MemberArchivePage> {
final MemberArchiveController _memberArchivesController =
Get.put(MemberArchiveController());
late Future _futureBuilderFuture;
late ScrollController scrollController;
@override
void initState() {
super.initState();
_futureBuilderFuture =
_memberArchivesController.getMemberArchive('onRefresh');
scrollController = _memberArchivesController.scrollController;
scrollController.addListener(
() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
EasyThrottle.throttle(
'member_archives', const Duration(milliseconds: 500), () {
_memberArchivesController.onLoad();
});
}
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('他的投稿'),
// actions: [
// Obx(
// () => PopupMenuButton<String>(
// padding: EdgeInsets.zero,
// tooltip: '投稿排序',
// icon: Icon(
// Icons.more_vert_outlined,
// color: Theme.of(context).colorScheme.outline,
// ),
// position: PopupMenuPosition.under,
// onSelected: (String type) {},
// itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
// for (var i in _memberArchivesController.orderList) ...[
// PopupMenuItem<String>(
// onTap: () {},
// value: _memberArchivesController.currentOrder['label'],
// child: Row(
// mainAxisSize: MainAxisSize.min,
// children: [
// Text(i['label']!),
// if (_memberArchivesController.currentOrder['label'] ==
// i['label']) ...[
// const SizedBox(width: 10),
// const Icon(Icons.done, size: 20),
// ],
// ],
// ),
// ),
// ]
// ],
// ),
// ),
// ],
),
body: CustomScrollView(
controller: _memberArchivesController.scrollController,
slivers: [
FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data != null) {
Map data = snapshot.data as Map;
List list = _memberArchivesController.archivesList;
if (data['status']) {
return Obx(
() => list.isNotEmpty
? SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return VideoCardH(
videoItem: list[index],
showOwner: false,
showPubdate: true,
);
},
childCount: list.length,
),
)
: const SliverToBoxAdapter(),
);
} else {
return const SliverToBoxAdapter();
}
} else {
return const SliverToBoxAdapter();
}
} else {
return const SliverToBoxAdapter();
}
},
),
],
),
);
}
}

View File

@ -0,0 +1,3 @@
import 'package:get/get.dart';
class MemberCoinController extends GetxController {}

View File

@ -1,4 +1,4 @@
library dynamic_panel;
library member_coin;
export './controller.dart';
export './view.dart';

View File

@ -0,0 +1,15 @@
import 'package:flutter/material.dart';
class MemberCoinPage extends StatefulWidget {
const MemberCoinPage({super.key});
@override
State<MemberCoinPage> createState() => _MemberCoinPageState();
}
class _MemberCoinPageState extends State<MemberCoinPage> {
@override
Widget build(BuildContext context) {
return Scaffold();
}
}

View File

@ -0,0 +1,95 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/common/widgets/stat/view.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/member/coin.dart';
import 'package:pilipala/utils/utils.dart';
class MemberCoinsItem extends StatelessWidget {
final MemberCoinsDataModel coinItem;
const MemberCoinsItem({
Key? key,
required this.coinItem,
}) : super(key: key);
@override
Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(coinItem.aid);
return Card(
elevation: 0,
clipBehavior: Clip.hardEdge,
margin: EdgeInsets.zero,
child: InkWell(
onTap: () async {
int cid =
await SearchHttp.ab2c(aid: coinItem.aid, bvid: coinItem.bvid);
Get.toNamed('/video?bvid=${coinItem.bvid}&cid=$cid',
arguments: {'videoItem': coinItem, 'heroTag': heroTag});
},
child: Column(
children: [
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(builder: (context, boxConstraints) {
double maxWidth = boxConstraints.maxWidth;
double maxHeight = boxConstraints.maxHeight;
return Stack(
children: [
NetworkImgLayer(
src: coinItem.pic,
width: maxWidth,
height: maxHeight,
),
if (coinItem.duration != null)
PBadge(
bottom: 6,
right: 6,
type: 'gray',
text: Utils.timeFormat(coinItem.duration),
)
],
);
}),
),
Padding(
padding: const EdgeInsets.fromLTRB(5, 6, 0, 0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
coinItem.title!,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Row(
children: [
StatView(
view: coinItem.view,
theme: 'gray',
),
const Spacer(),
Text(
Utils.CustomStamp_str(
timestamp: coinItem.pubdate, date: 'MM-DD'),
style: TextStyle(
fontSize: 11,
color: Theme.of(context).colorScheme.outline,
),
),
const SizedBox(width: 6)
],
),
],
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/member.dart';
import 'package:pilipala/models/dynamics/result.dart';
class MemberDynamicsController extends GetxController {
final ScrollController scrollController = ScrollController();
late int mid;
String offset = '';
int count = 0;
bool hasMore = true;
RxList<DynamicItemModel> dynamicsList = <DynamicItemModel>[].obs;
@override
void onInit() {
super.onInit();
mid = int.parse(Get.parameters['mid']!);
}
Future getMemberDynamic(type) async {
if (type == 'onRefresh') {
offset = '';
dynamicsList.clear();
}
var res = await MemberHttp.memberDynamic(
offset: offset,
mid: mid,
);
if (res['status']) {
dynamicsList.addAll(res['data'].items);
offset = res['data'].offset;
hasMore = res['data'].hasMore;
}
return res;
}
// 上拉加载
Future onLoad() async {
getMemberDynamic('onLoad');
}
}

View File

@ -0,0 +1,4 @@
library member_dynamics;
export './controller.dart';
export './view.dart';

View File

@ -0,0 +1,86 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/pages/member_dynamics/index.dart';
import '../dynamics/widgets/dynamic_panel.dart';
class MemberDynamicsPage extends StatefulWidget {
const MemberDynamicsPage({super.key});
@override
State<MemberDynamicsPage> createState() => _MemberDynamicsPageState();
}
class _MemberDynamicsPageState extends State<MemberDynamicsPage> {
final MemberDynamicsController _memberDynamicController =
Get.put(MemberDynamicsController());
late Future _futureBuilderFuture;
late ScrollController scrollController;
@override
void initState() {
super.initState();
_futureBuilderFuture =
_memberDynamicController.getMemberDynamic('onRefresh');
scrollController = _memberDynamicController.scrollController;
scrollController.addListener(
() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
EasyThrottle.throttle(
'member_dynamics', const Duration(milliseconds: 500), () {
_memberDynamicController.onLoad();
});
}
},
);
}
@override
void dispose() {
_memberDynamicController.scrollController.removeListener(() {});
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('他的动态'),
),
body: CustomScrollView(
controller: _memberDynamicController.scrollController,
slivers: [
FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
List list = _memberDynamicController.dynamicsList;
if (data['status']) {
return Obx(
() => list.isNotEmpty
? SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return DynamicPanel(item: list[index]);
},
childCount: list.length,
),
)
: const SliverToBoxAdapter(),
);
} else {
return const SliverToBoxAdapter();
}
} else {
return const SliverToBoxAdapter();
}
},
),
],
),
);
}
}

View File

@ -0,0 +1,3 @@
import 'package:get/get.dart';
class MemberLikeController extends GetxController {}

View File

@ -0,0 +1,4 @@
library member_like;
export './controller.dart';
export './view.dart';

View File

@ -0,0 +1,15 @@
import 'package:flutter/material.dart';
class MemberLikePage extends StatefulWidget {
const MemberLikePage({super.key});
@override
State<MemberLikePage> createState() => _MemberLikePageState();
}
class _MemberLikePageState extends State<MemberLikePage> {
@override
Widget build(BuildContext context) {
return Scaffold();
}
}

View File

@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/member.dart';
import 'package:pilipala/models/member/seasons.dart';
class MemberSeasonsController extends GetxController {
final ScrollController scrollController = ScrollController();
late int mid;
late int seasonId;
int pn = 1;
int ps = 30;
int count = 0;
RxList<MemberArchiveItem> seasonsList = <MemberArchiveItem>[].obs;
late Map page;
@override
void onInit() {
super.onInit();
mid = int.parse(Get.parameters['mid']!);
seasonId = int.parse(Get.parameters['seasonId']!);
}
// 获取专栏详情
Future getSeasonDetail(type) async {
if (type == 'onRefresh') {
pn = 1;
}
var res = await MemberHttp.getSeasonDetail(
mid: mid,
seasonId: seasonId,
pn: pn,
ps: ps,
sortReverse: false,
);
if (res['status']) {
seasonsList.addAll(res['data'].archives);
page = res['data'].page;
pn += 1;
}
return res;
}
// 上拉加载
Future onLoad() async {
getSeasonDetail('onLoad');
}
}

View File

@ -0,0 +1,4 @@
library member_seasons;
export 'controller.dart';
export 'view.dart';

View File

@ -0,0 +1,103 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'controller.dart';
import 'widgets/item.dart';
class MemberSeasonsPage extends StatefulWidget {
const MemberSeasonsPage({super.key});
@override
State<MemberSeasonsPage> createState() => _MemberSeasonsPageState();
}
class _MemberSeasonsPageState extends State<MemberSeasonsPage> {
final MemberSeasonsController _memberSeasonsController =
Get.put(MemberSeasonsController());
late Future _futureBuilderFuture;
late ScrollController scrollController;
@override
void initState() {
super.initState();
_futureBuilderFuture =
_memberSeasonsController.getSeasonDetail('onRefresh');
scrollController = _memberSeasonsController.scrollController;
scrollController.addListener(
() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
EasyThrottle.throttle(
'member_archives', const Duration(milliseconds: 500), () {
_memberSeasonsController.onLoad();
});
}
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('他的专栏'),
),
body: Padding(
padding: const EdgeInsets.only(
left: StyleString.safeSpace,
right: StyleString.safeSpace,
),
child: SingleChildScrollView(
controller: _memberSeasonsController.scrollController,
child: FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data != null) {
Map data = snapshot.data as Map;
List list = _memberSeasonsController.seasonsList;
if (data['status']) {
return Obx(
() => list.isNotEmpty
? LayoutBuilder(
builder: (context, boxConstraints) {
return GridView.builder(
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: StyleString.safeSpace,
mainAxisSpacing: StyleString.safeSpace,
childAspectRatio: 0.94,
),
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: _memberSeasonsController
.seasonsList.length,
itemBuilder: (context, i) {
return MemberSeasonsItem(
seasonItem: _memberSeasonsController
.seasonsList[i],
);
},
);
},
)
: const SizedBox(),
);
} else {
return const SizedBox();
}
} else {
return const SizedBox();
}
} else {
return const SizedBox();
}
},
),
),
),
);
}
}

View File

@ -0,0 +1,98 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/common/widgets/stat/view.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/utils/utils.dart';
class MemberSeasonsItem extends StatelessWidget {
final dynamic seasonItem;
const MemberSeasonsItem({
Key? key,
required this.seasonItem,
}) : super(key: key);
@override
Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(seasonItem.aid);
return Card(
elevation: 0,
clipBehavior: Clip.hardEdge,
margin: EdgeInsets.zero,
child: InkWell(
onTap: () async {
int cid =
await SearchHttp.ab2c(aid: seasonItem.aid, bvid: seasonItem.bvid);
Get.toNamed('/video?bvid=${seasonItem.bvid}&cid=$cid',
arguments: {'videoItem': seasonItem, 'heroTag': heroTag});
},
child: Column(
children: [
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(builder: (context, boxConstraints) {
double maxWidth = boxConstraints.maxWidth;
double maxHeight = boxConstraints.maxHeight;
return Stack(
children: [
Hero(
tag: heroTag,
child: NetworkImgLayer(
src: seasonItem.pic,
width: maxWidth,
height: maxHeight,
),
),
if (seasonItem.duration != null)
PBadge(
bottom: 6,
right: 6,
type: 'gray',
text: Utils.timeFormat(seasonItem.duration),
)
],
);
}),
),
Padding(
padding: const EdgeInsets.fromLTRB(5, 6, 0, 0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
seasonItem.title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Row(
children: [
StatView(
view: seasonItem.view,
theme: 'gray',
),
const Spacer(),
Text(
Utils.CustomStamp_str(
timestamp: seasonItem.pubdate, date: 'MM-DD'),
style: TextStyle(
fontSize: 11,
color: Theme.of(context).colorScheme.outline,
),
),
const SizedBox(width: 6)
],
),
],
),
),
],
),
),
);
}
}

View File

@ -76,6 +76,7 @@ class VideoIntroController extends GetxController {
if (Get.arguments.containsKey('videoItem')) {
preRender = true;
var args = Get.arguments['videoItem'];
var keys = Get.arguments.keys.toList();
videoItem!['pic'] = args.pic;
if (args.title is String) {
videoItem!['title'] = args.title;
@ -86,11 +87,9 @@ class VideoIntroController extends GetxController {
}
videoItem!['title'] = str;
}
if (args.stat != null) {
videoItem!['stat'] = args.stat;
}
videoItem!['pubdate'] = args.pubdate;
videoItem!['owner'] = args.owner;
videoItem!['stat'] = keys.contains('stat') && args.stat;
videoItem!['pubdate'] = keys.contains('pubdate') && args.pubdate;
videoItem!['owner'] = keys.contains('owner') && args.owner;
}
}
userLogin = userInfo != null;

View File

@ -247,7 +247,7 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
padding: const EdgeInsets.only(
left: StyleString.safeSpace, right: StyleString.safeSpace, top: 10),
sliver: SliverToBoxAdapter(
child: !loadingStatus || videoItem.isNotEmpty
child: !loadingStatus
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -277,7 +277,7 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
children: [
StatView(
theme: 'gray',
view: !widget.loadingStatus
view: !loadingStatus
? widget.videoDetail!.stat!.view
: videoItem['stat'].view,
size: 'medium',
@ -285,7 +285,7 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
const SizedBox(width: 10),
StatDanMu(
theme: 'gray',
danmu: !widget.loadingStatus
danmu: !loadingStatus
? widget.videoDetail!.stat!.danmaku
: videoItem['stat'].danmaku,
size: 'medium',
@ -293,7 +293,7 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
const SizedBox(width: 10),
Text(
Utils.dateFormat(
!widget.loadingStatus
!loadingStatus
? widget.videoDetail!.pubdate
: videoItem['pubdate'],
formatType: 'detail'),

View File

@ -21,7 +21,12 @@ import 'package:pilipala/pages/later/index.dart';
import 'package:pilipala/pages/liveRoom/view.dart';
import 'package:pilipala/pages/login/index.dart';
import 'package:pilipala/pages/member/index.dart';
import 'package:pilipala/pages/member_archive/index.dart';
import 'package:pilipala/pages/member_coin/index.dart';
import 'package:pilipala/pages/member_dynamics/index.dart';
import 'package:pilipala/pages/member_like/index.dart';
import 'package:pilipala/pages/member_search/index.dart';
import 'package:pilipala/pages/member_seasons/index.dart';
import 'package:pilipala/pages/preview/index.dart';
import 'package:pilipala/pages/search/index.dart';
import 'package:pilipala/pages/searchResult/index.dart';
@ -125,6 +130,19 @@ class Routes {
CustomGetPage(name: '/favSearch', page: () => const FavSearchPage()),
// 登录页面
CustomGetPage(name: '/loginPage', page: () => const LoginPage()),
// 用户动态
CustomGetPage(
name: '/memberDynamics', page: () => const MemberDynamicsPage()),
// 用户投稿
CustomGetPage(
name: '/memberArchive', page: () => const MemberArchivePage()),
// 用户最近投币
CustomGetPage(name: '/memberCoin', page: () => const MemberCoinPage()),
// 用户最近喜欢
CustomGetPage(name: '/memberLike', page: () => const MemberLikePage()),
// 用户专栏
CustomGetPage(
name: '/memberSeasons', page: () => const MemberSeasonsPage()),
];
}

View File

@ -146,7 +146,7 @@ class Utils {
int.parse(MM) == DateTime.now().month) {
// 当天
if (int.parse(DD) == DateTime.now().day) {
return date.split(' ')[1];
return '今天';
}
}
return date;