handle relation url

Closes #1566

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-10-15 18:25:15 +08:00
parent 32ce2b87db
commit c9de79532a
31 changed files with 634 additions and 393 deletions

View File

@@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
Widget moreTextButton({
String text = '查看更多',
required VoidCallback onTap,
EdgeInsets? padding,
Color? color,
}) {
Widget child = Text.rich(
style: TextStyle(color: color, height: 1),
strutStyle: const StrutStyle(leading: 0, height: 1),
TextSpan(
children: [
TextSpan(text: text),
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Icon(
size: 22,
color: color,
Icons.keyboard_arrow_right,
),
),
],
),
);
if (padding != null) {
child = Padding(padding: padding, child: child);
}
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: child,
);
}

View File

@@ -966,4 +966,8 @@ class Api {
static const String danmakuRecall = '/x/dm/recall'; static const String danmakuRecall = '/x/dm/recall';
static const String danmakuEditState = '/x/v2/dm/edit/state'; static const String danmakuEditState = '/x/v2/dm/edit/state';
static const String followedUp = '/x/relation/followings/followed_upper';
static const String sameFollowing = '/x/relation/same/followings';
} }

View File

@@ -1,10 +1,10 @@
import 'package:PiliPlus/http/api.dart'; import 'package:PiliPlus/http/api.dart';
import 'package:PiliPlus/http/init.dart'; import 'package:PiliPlus/http/init.dart';
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models_new/fans/data.dart'; import 'package:PiliPlus/models_new/follow/data.dart';
class FanHttp { class FanHttp {
static Future<LoadingState<FansData>> fans({ static Future<LoadingState<FollowData>> fans({
int? vmid, int? vmid,
int? pn, int? pn,
int ps = 20, int ps = 20,
@@ -21,7 +21,7 @@ class FanHttp {
}, },
); );
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return Success(FansData.fromJson(res.data['data'])); return Success(FollowData.fromJson(res.data['data']));
} else { } else {
return Error(res.data['message']); return Error(res.data['message']);
} }

View File

@@ -328,7 +328,9 @@ class MemberHttp {
} }
} }
static Future memberCardInfo({int? mid}) async { static Future<LoadingState<MemberCardInfoData>> memberCardInfo({
int? mid,
}) async {
var res = await Request().get( var res = await Request().get(
Api.memberCardInfo, Api.memberCardInfo,
queryParameters: { queryParameters: {
@@ -337,12 +339,9 @@ class MemberHttp {
}, },
); );
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return { return Success(MemberCardInfoData.fromJson(res.data['data']));
'status': true,
'data': MemberCardInfoData.fromJson(res.data['data']),
};
} else { } else {
return {'status': false, 'msg': res.data['message']}; return Error(res.data['message']);
} }
} }

View File

@@ -5,6 +5,7 @@ import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/user/info.dart'; import 'package:PiliPlus/models/user/info.dart';
import 'package:PiliPlus/models/user/stat.dart'; import 'package:PiliPlus/models/user/stat.dart';
import 'package:PiliPlus/models_new/coin_log/data.dart'; import 'package:PiliPlus/models_new/coin_log/data.dart';
import 'package:PiliPlus/models_new/follow/data.dart';
import 'package:PiliPlus/models_new/history/data.dart'; import 'package:PiliPlus/models_new/history/data.dart';
import 'package:PiliPlus/models_new/later/data.dart'; import 'package:PiliPlus/models_new/later/data.dart';
import 'package:PiliPlus/models_new/login_log/data.dart'; import 'package:PiliPlus/models_new/login_log/data.dart';
@@ -496,4 +497,48 @@ class UserHttp {
return Error(res.data['message']); return Error(res.data['message']);
} }
} }
static Future<LoadingState<FollowData>> followedUp({
required Object mid,
required int pn,
}) async {
final res = await Request().get(
Api.followedUp,
queryParameters: {
'csrf': Accounts.main.csrf,
'pn': pn,
'vmid': mid,
'web_location': 333.789,
'x-bili-device-req-json':
'{"platform":"web","device":"pc","spmid":"333.789"}',
},
);
if (res.data['code'] == 0) {
return Success(FollowData.fromJson(res.data['data']));
} else {
return Error(res.data['message']);
}
}
static Future<LoadingState<FollowData>> sameFollowing({
required Object mid,
int? pn,
}) async {
final res = await Request().get(
Api.sameFollowing,
queryParameters: {
'csrf': Accounts.main.csrf,
'pn': ?pn,
'vmid': mid,
'web_location': 333.789,
'x-bili-device-req-json':
'{"platform":"web","device":"pc","spmid":"333.789"}',
},
);
if (res.data['code'] == 0) {
return Success(FollowData.fromJson(res.data['data']));
} else {
return Error(res.data['message']);
}
}
} }

View File

@@ -1,19 +0,0 @@
import 'package:PiliPlus/models_new/fans/list.dart';
class FansData {
List<FansItemModel>? list;
String? offset;
int? reVersion;
int? total;
FansData({this.list, this.offset, this.reVersion, this.total});
factory FansData.fromJson(Map<String, dynamic> json) => FansData(
list: (json['list'] as List<dynamic>?)
?.map((e) => FansItemModel.fromJson(e as Map<String, dynamic>))
.toList(),
offset: json['offset'] as String?,
reVersion: json['re_version'] as int?,
total: json['total'] as int?,
);
}

View File

@@ -1,61 +0,0 @@
import 'package:PiliPlus/models/model_avatar.dart';
class FansItemModel {
int? mid;
int? attribute;
int? mtime;
dynamic tag;
int? special;
String? uname;
String? face;
String? sign;
int? faceNft;
BaseOfficialVerify? officialVerify;
Vip? vip;
String? nftIcon;
String? recReason;
String? trackId;
String? followTime;
FansItemModel({
this.mid,
this.attribute,
this.mtime,
this.tag,
this.special,
this.uname,
this.face,
this.sign,
this.faceNft,
this.officialVerify,
this.vip,
this.nftIcon,
this.recReason,
this.trackId,
this.followTime,
});
factory FansItemModel.fromJson(Map<String, dynamic> json) => FansItemModel(
mid: json['mid'] as int?,
attribute: json['attribute'] as int?,
mtime: json['mtime'] as int?,
tag: json['tag'] as dynamic,
special: json['special'] as int?,
uname: json['uname'] as String?,
face: json['face'] as String?,
sign: json['sign'] as String?,
faceNft: json['face_nft'] as int?,
officialVerify: json['official_verify'] == null
? null
: BaseOfficialVerify.fromJson(
json['official_verify'] as Map<String, dynamic>,
),
vip: json['vip'] == null
? null
: Vip.fromJson(json['vip'] as Map<String, dynamic>),
nftIcon: json['nft_icon'] as String?,
recReason: json['rec_reason'] as String?,
trackId: json['track_id'] as String?,
followTime: json['follow_time'] as String?,
);
}

View File

@@ -2,10 +2,9 @@ import 'package:PiliPlus/models_new/follow/list.dart';
class FollowData { class FollowData {
late List<FollowItemModel> list; late List<FollowItemModel> list;
int? reVersion;
int? total; int? total;
FollowData({required this.list, this.reVersion, this.total}); FollowData({required this.list, this.total});
factory FollowData.fromJson(Map<String, dynamic> json) => FollowData( factory FollowData.fromJson(Map<String, dynamic> json) => FollowData(
list: list:
@@ -13,7 +12,6 @@ class FollowData {
?.map((e) => FollowItemModel.fromJson(e as Map<String, dynamic>)) ?.map((e) => FollowItemModel.fromJson(e as Map<String, dynamic>))
.toList() ?? .toList() ??
<FollowItemModel>[], <FollowItemModel>[],
reVersion: json['re_version'] as int?,
total: json['total'] as int?, total: json['total'] as int?,
); );
} }

View File

@@ -7,12 +7,8 @@ class FollowItemModel extends UpItem {
dynamic tag; dynamic tag;
int? special; int? special;
String? sign; String? sign;
int? faceNft;
BaseOfficialVerify? officialVerify; BaseOfficialVerify? officialVerify;
Vip? vip; Vip? vip;
String? nftIcon;
String? recReason;
String? trackId;
String? followTime; String? followTime;
FollowItemModel({ FollowItemModel({
@@ -24,12 +20,8 @@ class FollowItemModel extends UpItem {
super.uname, super.uname,
super.face, super.face,
this.sign, this.sign,
this.faceNft,
this.officialVerify, this.officialVerify,
this.vip, this.vip,
this.nftIcon,
this.recReason,
this.trackId,
this.followTime, this.followTime,
}); });
@@ -43,7 +35,6 @@ class FollowItemModel extends UpItem {
uname: json['uname'] as String?, uname: json['uname'] as String?,
face: json['face'] as String?, face: json['face'] as String?,
sign: json['sign'] as String?, sign: json['sign'] as String?,
faceNft: json['face_nft'] as int?,
officialVerify: json['official_verify'] == null officialVerify: json['official_verify'] == null
? null ? null
: BaseOfficialVerify.fromJson( : BaseOfficialVerify.fromJson(
@@ -52,9 +43,6 @@ class FollowItemModel extends UpItem {
vip: json['vip'] == null vip: json['vip'] == null
? null ? null
: Vip.fromJson(json['vip'] as Map<String, dynamic>), : Vip.fromJson(json['vip'] as Map<String, dynamic>),
nftIcon: json['nft_icon'] as String?,
recReason: json['rec_reason'] as String?,
trackId: json['track_id'] as String?,
followTime: json['follow_time'] as String?, followTime: json['follow_time'] as String?,
); );
} }

View File

@@ -1,19 +1,13 @@
class RejectPage { class RejectPage {
String? title; String? title;
String? text; String? text;
String? img; String? img;
RejectPage({this.title, this.text, this.img}); RejectPage({this.title, this.text, this.img});
factory RejectPage.fromJson(Map<String, dynamic> json) => RejectPage( factory RejectPage.fromJson(Map<String, dynamic> json) => RejectPage(
title: json['title'] as String?, title: json['title'] as String?,
text: json['text'] as String?, text: json['text'] as String?,
img: json['img'] as String?, img: json['img'] as String?,
); );
Map<String, dynamic> toJson() => {
'title': title,
'text': text,
'img': img,
};
} }

View File

@@ -72,7 +72,7 @@ class _ContactPageState extends State<ContactPage>
onSelect: widget.isFromSelect ? onSelect : null, onSelect: widget.isFromSelect ? onSelect : null,
), ),
FansPage( FansPage(
mid: accountService.mid, showName: false,
onSelect: widget.isFromSelect ? onSelect : null, onSelect: widget.isFromSelect ? onSelect : null,
), ),
], ],

View File

@@ -1,29 +1,35 @@
import 'package:PiliPlus/http/fan.dart'; import 'package:PiliPlus/http/fan.dart';
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/video.dart'; import 'package:PiliPlus/http/video.dart';
import 'package:PiliPlus/models_new/fans/data.dart'; import 'package:PiliPlus/models_new/follow/data.dart';
import 'package:PiliPlus/models_new/fans/list.dart'; import 'package:PiliPlus/pages/follow_type/controller.dart';
import 'package:PiliPlus/pages/common/common_list_controller.dart'; import 'package:PiliPlus/utils/accounts.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
class FansController extends CommonListController<FansData, FansItemModel> { class FansController extends FollowTypeController {
FansController(this.mid); FansController(this.showName);
int total = 0; final bool showName;
int mid; late final bool isOwner;
@override @override
void onInit() { void init() {
super.onInit(); final ownerMid = Accounts.main.mid;
final mid = Get.parameters['mid'];
this.mid = mid != null ? int.parse(mid) : ownerMid;
isOwner = ownerMid == this.mid;
if (showName && !isOwner) {
final name = Get.parameters['name'];
this.name = RxnString(name);
if (name == null) {
queryUserName();
}
}
queryData(); queryData();
} }
@override @override
List<FansItemModel>? getDataList(FansData response) { Future<LoadingState<FollowData>> customGetData() => FanHttp.fans(
return response.list;
}
@override
Future<LoadingState<FansData>> customGetData() => FanHttp.fans(
vmid: mid, vmid: mid,
pn: page, pn: page,
orderType: 'attention', orderType: 'attention',

View File

@@ -1,16 +1,9 @@
import 'package:PiliPlus/common/skeleton/msg_feed_top.dart';
import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart';
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/models_new/follow/list.dart';
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
import 'package:PiliPlus/common/widgets/view_sliver_safe_area.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/common/image_type.dart';
import 'package:PiliPlus/models_new/fans/list.dart';
import 'package:PiliPlus/pages/fan/controller.dart'; import 'package:PiliPlus/pages/fan/controller.dart';
import 'package:PiliPlus/pages/follow_type/view.dart';
import 'package:PiliPlus/pages/follow_type/widgets/item.dart';
import 'package:PiliPlus/pages/share/view.dart' show UserModel; import 'package:PiliPlus/pages/share/view.dart' show UserModel;
import 'package:PiliPlus/services/account_service.dart';
import 'package:PiliPlus/utils/grid.dart';
import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -18,160 +11,63 @@ import 'package:get/get.dart';
class FansPage extends StatefulWidget { class FansPage extends StatefulWidget {
const FansPage({ const FansPage({
super.key, super.key,
this.mid, this.showName,
this.onSelect, this.onSelect,
}); });
final int? mid; final bool? showName;
final ValueChanged<UserModel>? onSelect; final ValueChanged<UserModel>? onSelect;
@override @override
State<FansPage> createState() => _FansPageState(); State<FansPage> createState() => _FansPageState();
} }
class _FansPageState extends State<FansPage> { class _FansPageState extends FollowTypePageState<FansPage> {
late int mid;
String? name;
late bool isOwner;
late FansController _fansController;
@override @override
void initState() { late final FansController controller = Get.put(
super.initState(); FansController(widget.showName ?? true),
AccountService accountService = Get.find<AccountService>(); tag: Utils.generateRandomString(8),
late final mid = Get.parameters['mid'];
this.mid =
widget.mid ?? (mid != null ? int.parse(mid) : accountService.mid);
isOwner = this.mid == accountService.mid;
name = Get.parameters['name'] ?? accountService.name.value;
_fansController = Get.put(
FansController(this.mid),
tag: Utils.makeHeroTag(this.mid),
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context).colorScheme;
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: widget.mid != null
? null
: AppBar(title: Text(isOwner ? '我的粉丝' : '$name的粉丝')),
body: refreshIndicator(
onRefresh: _fansController.onRefresh,
child: CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
controller: _fansController.scrollController,
slivers: [
ViewSliverSafeArea(
sliver: Obx(
() => _buildBody(theme, _fansController.loadingState.value),
),
),
],
),
),
);
}
late final gridDelegate = SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: Grid.smallCardWidth * 2,
mainAxisExtent: 66,
); );
late final flag = widget.onSelect == null && controller.isOwner;
Widget _buildBody( @override
ColorScheme theme, PreferredSizeWidget? get appBar => widget.showName == false
LoadingState<List<FansItemModel>?> loadingState, ? null
) { : AppBar(
return switch (loadingState) { title: controller.isOwner
Loading() => SliverGrid.builder( ? const Text('我的粉丝')
gridDelegate: gridDelegate, : Obx(() {
itemBuilder: (context, index) => const MsgFeedTopSkeleton(), final name = controller.name.value;
itemCount: 16, if (name != null) return Text('$name的粉丝');
), return const SizedBox.shrink();
Success(:var response) => }),
response?.isNotEmpty == true );
? SliverGrid.builder(
gridDelegate: gridDelegate,
itemBuilder: (context, index) {
if (index == response.length - 1) {
_fansController.onLoadMore();
}
return _buildItem(theme, index, response[index]);
},
itemCount: response!.length,
)
: HttpError(onReload: _fansController.onReload),
Error(:var errMsg) => HttpError(
errMsg: errMsg,
onReload: _fansController.onReload,
),
};
}
Widget _buildItem(ColorScheme theme, int index, FansItemModel item) { @override
final isSelect = widget.onSelect != null; Widget buildItem(int index, FollowItemModel item) {
void onRemove() => showConfirmDialog( void onRemove() => showConfirmDialog(
context: context, context: context,
title: '确定移除 ${item.uname} ', title: '确定移除 ${item.uname} ',
onConfirm: () => _fansController.onRemoveFan(index, item.mid!), onConfirm: () => controller.onRemoveFan(index, item.mid),
); );
final flag = !isSelect && isOwner; return FollowTypeItem(
return SizedBox( item: item,
height: 66, onTap: () {
child: InkWell( if (widget.onSelect != null) {
onTap: () { widget.onSelect!(
if (widget.onSelect != null) { UserModel(
widget.onSelect!( mid: item.mid,
UserModel( name: item.uname!,
mid: item.mid!, avatar: item.face!,
name: item.uname!, ),
avatar: item.face!, );
), return;
); }
return; Get.toNamed('/member?mid=${item.mid}');
} },
Get.toNamed('/member?mid=${item.mid}'); onLongPress: flag ? onRemove : null,
}, onSecondaryTap: flag && !Utils.isMobile ? onRemove : null,
onLongPress: flag ? onRemove : null,
onSecondaryTap: flag && !Utils.isMobile ? onRemove : null,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
),
child: Row(
spacing: 10,
children: [
NetworkImgLayer(
width: 45,
height: 45,
type: ImageType.avatar,
src: item.face,
),
Column(
spacing: 3,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.uname!,
style: const TextStyle(fontSize: 14),
),
if (item.sign != null)
Text(
item.sign!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 13, color: theme.outline),
),
],
),
],
),
),
),
); );
} }
} }

View File

@@ -1,6 +1,7 @@
import 'package:PiliPlus/http/follow.dart'; import 'package:PiliPlus/http/follow.dart';
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/member.dart'; import 'package:PiliPlus/http/member.dart';
import 'package:PiliPlus/http/user.dart';
import 'package:PiliPlus/models/common/follow_order_type.dart'; import 'package:PiliPlus/models/common/follow_order_type.dart';
import 'package:PiliPlus/models_new/follow/data.dart'; import 'package:PiliPlus/models_new/follow/data.dart';
import 'package:PiliPlus/models_new/follow/list.dart'; import 'package:PiliPlus/models_new/follow/list.dart';
@@ -14,6 +15,11 @@ class FollowChildController
final FollowController? controller; final FollowController? controller;
final int? tagid; final int? tagid;
final int mid; final int mid;
int? total;
late final loadSameFollow = controller?.isOwner == false;
late final Rx<LoadingState<List<FollowItemModel>?>> sameState =
LoadingState<List<FollowItemModel>?>.loading().obs;
late final Rx<FollowOrderType> orderType = FollowOrderType.def.obs; late final Rx<FollowOrderType> orderType = FollowOrderType.def.obs;
@@ -21,13 +27,24 @@ class FollowChildController
void onInit() { void onInit() {
super.onInit(); super.onInit();
queryData(); queryData();
if (loadSameFollow) {
_loadSameFollow();
}
} }
@override @override
List<FollowItemModel>? getDataList(FollowData response) { List<FollowItemModel>? getDataList(FollowData response) {
total = response.total;
return response.list; return response.list;
} }
@override
void checkIsEnd(int length) {
if (total != null && length >= total!) {
isEnd = true;
}
}
@override @override
bool customHandleResponse(bool isRefresh, Success<FollowData> response) { bool customHandleResponse(bool isRefresh, Success<FollowData> response) {
if (controller != null) { if (controller != null) {
@@ -57,4 +74,11 @@ class FollowChildController
orderType: orderType.value.type, orderType: orderType.value.type,
); );
} }
Future<void> _loadSameFollow() async {
final res = await UserHttp.sameFollowing(mid: mid);
if (res.isSuccess) {
sameState.value = Success(res.data.list);
}
}
} }

View File

@@ -1,4 +1,5 @@
import 'package:PiliPlus/common/skeleton/msg_feed_top.dart'; import 'package:PiliPlus/common/skeleton/msg_feed_top.dart';
import 'package:PiliPlus/common/widgets/button/more_btn.dart';
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
@@ -42,7 +43,24 @@ class _FollowChildPageState extends State<FollowChildPage>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
final colorScheme = ColorScheme.of(context);
final padding = MediaQuery.viewPaddingOf(context); final padding = MediaQuery.viewPaddingOf(context);
Widget sliver = Obx(
() => _buildBody(_followController.loadingState.value),
);
if (_followController.loadSameFollow) {
sliver = SliverMainAxisGroup(
slivers: [
Obx(
() => _buildSameFollowing(
colorScheme,
_followController.sameState.value,
),
),
sliver,
],
);
}
Widget child = refreshIndicator( Widget child = refreshIndicator(
onRefresh: _followController.onRefresh, onRefresh: _followController.onRefresh,
child: CustomScrollView( child: CustomScrollView(
@@ -55,9 +73,7 @@ class _FollowChildPageState extends State<FollowChildPage>
right: padding.right, right: padding.right,
bottom: padding.bottom + 100, bottom: padding.bottom + 100,
), ),
sliver: Obx( sliver: sliver,
() => _buildBody(_followController.loadingState.value),
),
), ),
], ],
), ),
@@ -122,6 +138,68 @@ class _FollowChildPageState extends State<FollowChildPage>
}; };
} }
Widget _buildSameFollowing(
ColorScheme colorScheme,
LoadingState<List<FollowItemModel>?> state,
) {
return switch (state) {
Success(:var response) =>
response?.isNotEmpty == true
? SliverMainAxisGroup(
slivers: [
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(
left: 16,
right: 16,
bottom: 6,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'我们的共同关注',
style: TextStyle(
color: colorScheme.onSurfaceVariant,
),
),
moreTextButton(
onTap: () => Get.toNamed(
'/sameFollowing?mid=${_followController.mid}&name=${widget.controller?.name.value}',
),
color: colorScheme.outline,
),
],
),
),
),
SliverList.builder(
itemCount: response!.length,
itemBuilder: (_, index) =>
FollowItem(item: response[index]),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(
left: 16,
top: 16,
bottom: 6,
),
child: Text(
'全部关注',
style: TextStyle(
color: colorScheme.onSurfaceVariant,
),
),
),
),
],
)
: const SliverToBoxAdapter(),
_ => const SliverToBoxAdapter(),
};
}
@override @override
bool get wantKeepAlive => bool get wantKeepAlive =>
widget.onSelect != null || widget.controller?.tabController != null; widget.onSelect != null || widget.controller?.tabController != null;

View File

@@ -1,16 +1,15 @@
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/member.dart'; import 'package:PiliPlus/http/member.dart';
import 'package:PiliPlus/models/member/tags.dart'; import 'package:PiliPlus/models/member/tags.dart';
import 'package:PiliPlus/services/account_service.dart';
import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/accounts.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
class FollowController extends GetxController with GetTickerProviderStateMixin { class FollowController extends GetxController with GetTickerProviderStateMixin {
late int mid; late final int mid;
String? name; late final RxnString name;
late bool isOwner; late final bool isOwner;
late final Rx<LoadingState> followState = LoadingState.loading().obs; late final Rx<LoadingState> followState = LoadingState.loading().obs;
late final RxList<MemberTagItemModel> tabs = <MemberTagItemModel>[].obs; late final RxList<MemberTagItemModel> tabs = <MemberTagItemModel>[].obs;
@@ -19,16 +18,26 @@ class FollowController extends GetxController with GetTickerProviderStateMixin {
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
int ownerMid = Accounts.main.mid; final ownerMid = Accounts.main.mid;
final mid = Get.parameters['mid']; final mid = Get.parameters['mid'];
this.mid = mid != null ? int.parse(mid) : ownerMid; this.mid = mid != null ? int.parse(mid) : ownerMid;
isOwner = ownerMid == this.mid; isOwner = ownerMid == this.mid;
name = Get.parameters['name'] ?? Get.find<AccountService>().name.value;
if (isOwner) { if (isOwner) {
queryFollowUpTags(); queryFollowUpTags();
} else {
final name = Get.parameters['name'];
this.name = RxnString(name);
if (name == null) {
_queryUserName();
}
} }
} }
Future<void> _queryUserName() async {
final res = await MemberHttp.memberCardInfo(mid: mid);
name.value = res.dataOrNull?.card?.name;
}
Future<void> queryFollowUpTags() async { Future<void> queryFollowUpTags() async {
var res = await MemberHttp.followUpTags(); var res = await MemberHttp.followUpTags();
if (res.isSuccess) { if (res.isSuccess) {

View File

@@ -31,9 +31,13 @@ class _FollowPageState extends State<FollowPage> {
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
appBar: AppBar( appBar: AppBar(
title: Text( title: _followController.isOwner
_followController.isOwner ? '我的关注' : '${_followController.name}的关注', ? const Text('的关注')
), : Obx(() {
final name = _followController.name.value;
if (name != null) return Text('$name的关注');
return const SizedBox.shrink();
}),
actions: _followController.isOwner actions: _followController.isOwner
? [ ? [
IconButton( IconButton(

View File

@@ -0,0 +1,50 @@
import 'package:PiliPlus/http/member.dart';
import 'package:PiliPlus/models_new/follow/data.dart';
import 'package:PiliPlus/models_new/follow/list.dart';
import 'package:PiliPlus/pages/common/common_list_controller.dart';
import 'package:PiliPlus/utils/accounts.dart';
import 'package:get/get.dart';
abstract class FollowTypeController
extends CommonListController<FollowData, FollowItemModel> {
late final int mid;
late final RxnString name;
RxInt total = 0.obs;
@override
void onInit() {
super.onInit();
init();
}
void init() {
final ownerMid = Accounts.main.mid;
final mid = Get.parameters['mid'];
this.mid = mid != null ? int.parse(mid) : ownerMid;
final name = Get.parameters['name'];
this.name = RxnString(name);
if (name == null) {
queryUserName();
}
queryData();
}
Future<void> queryUserName() async {
final res = await MemberHttp.memberCardInfo(mid: mid);
name.value = res.dataOrNull?.card?.name;
}
@override
List<FollowItemModel>? getDataList(FollowData response) {
total.value = response.total ?? 0;
return response.list;
}
@override
void checkIsEnd(int length) {
if (length >= total.value) {
isEnd = true;
}
}
}

View File

@@ -0,0 +1,10 @@
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/user.dart';
import 'package:PiliPlus/models_new/follow/data.dart';
import 'package:PiliPlus/pages/follow_type/controller.dart';
class FollowSameController extends FollowTypeController {
@override
Future<LoadingState<FollowData>> customGetData() =>
UserHttp.sameFollowing(mid: mid, pn: page);
}

View File

@@ -0,0 +1,30 @@
import 'package:PiliPlus/pages/follow_type/follow_same/controller.dart';
import 'package:PiliPlus/pages/follow_type/view.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class FollowSamePage extends StatefulWidget {
const FollowSamePage({super.key});
@override
State<FollowSamePage> createState() => _FollowSamePageState();
}
class _FollowSamePageState extends FollowTypePageState<FollowSamePage> {
@override
final controller = Get.put(
FollowSameController(),
tag: Utils.generateRandomString(8),
);
@override
PreferredSizeWidget get appBar => AppBar(
title: Obx(
() {
final name = controller.name.value;
return Text('${name == null ? '' : '我与$name的'}共同关注');
},
),
);
}

View File

@@ -0,0 +1,10 @@
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/user.dart';
import 'package:PiliPlus/models_new/follow/data.dart';
import 'package:PiliPlus/pages/follow_type/controller.dart';
class FollowedController extends FollowTypeController {
@override
Future<LoadingState<FollowData>> customGetData() =>
UserHttp.followedUp(mid: mid, pn: page);
}

View File

@@ -0,0 +1,29 @@
import 'package:PiliPlus/pages/follow_type/followed/controller.dart';
import 'package:PiliPlus/pages/follow_type/view.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class FollowedPage extends StatefulWidget {
const FollowedPage({super.key});
@override
State<FollowedPage> createState() => _FollowedPageState();
}
class _FollowedPageState extends FollowTypePageState<FollowedPage> {
@override
final controller = Get.put(
FollowedController(),
tag: Utils.generateRandomString(8),
);
@override
PreferredSizeWidget get appBar => AppBar(
title: Obx(
() => Text(
'我关注的${controller.total.value}人也关注了${controller.name.value ?? 'TA'}',
),
),
);
}

View File

@@ -0,0 +1,77 @@
import 'package:PiliPlus/common/skeleton/msg_feed_top.dart';
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
import 'package:PiliPlus/common/widgets/view_sliver_safe_area.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models_new/follow/list.dart';
import 'package:PiliPlus/pages/follow/widgets/follow_item.dart';
import 'package:PiliPlus/pages/follow_type/controller.dart';
import 'package:PiliPlus/utils/grid.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
abstract class FollowTypePageState<T extends StatefulWidget> extends State<T> {
FollowTypeController get controller;
PreferredSizeWidget? get appBar;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context).colorScheme;
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: appBar,
body: refreshIndicator(
onRefresh: controller.onRefresh,
child: CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
controller: controller.scrollController,
slivers: [
ViewSliverSafeArea(
sliver: Obx(
() => _buildBody(theme, controller.loadingState.value),
),
),
],
),
),
);
}
late final gridDelegate = SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: Grid.smallCardWidth * 2,
mainAxisExtent: 66,
);
Widget _buildBody(
ColorScheme theme,
LoadingState<List<FollowItemModel>?> loadingState,
) {
return switch (loadingState) {
Loading() => SliverGrid.builder(
gridDelegate: gridDelegate,
itemBuilder: (context, index) => const MsgFeedTopSkeleton(),
itemCount: 16,
),
Success(:var response) =>
response?.isNotEmpty == true
? SliverGrid.builder(
gridDelegate: gridDelegate,
itemBuilder: (context, index) {
if (index == response.length - 1) {
controller.onLoadMore();
}
return buildItem(index, response[index]);
},
itemCount: response!.length,
)
: HttpError(onReload: controller.onReload),
Error(:var errMsg) => HttpError(
errMsg: errMsg,
onReload: controller.onReload,
),
};
}
Widget buildItem(int index, FollowItemModel item) => FollowItem(item: item);
}

View File

@@ -0,0 +1,71 @@
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/models/common/image_type.dart';
import 'package:PiliPlus/models_new/follow/list.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class FollowTypeItem extends StatelessWidget {
const FollowTypeItem({
super.key,
required this.item,
this.onTap,
this.onLongPress,
this.onSecondaryTap,
});
final FollowItemModel item;
final VoidCallback? onTap;
final VoidCallback? onLongPress;
final VoidCallback? onSecondaryTap;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 66,
child: InkWell(
onTap: onTap ?? () => Get.toNamed('/member?mid=${item.mid}'),
onLongPress: onLongPress,
onSecondaryTap: onSecondaryTap,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
),
child: Row(
spacing: 10,
children: [
NetworkImgLayer(
width: 45,
height: 45,
type: ImageType.avatar,
src: item.face,
),
Expanded(
child: Column(
spacing: 3,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.uname!,
style: const TextStyle(fontSize: 14),
),
if (item.sign case final sign?)
Text(
sign,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 13,
color: ColorScheme.of(context).outline,
),
),
],
),
),
],
),
),
),
);
}
}

View File

@@ -1,6 +1,7 @@
import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/skeleton/video_card_v.dart'; import 'package:PiliPlus/common/skeleton/video_card_v.dart';
import 'package:PiliPlus/common/widgets/button/icon_button.dart'; import 'package:PiliPlus/common/widgets/button/icon_button.dart';
import 'package:PiliPlus/common/widgets/button/more_btn.dart';
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
import 'package:PiliPlus/common/widgets/pair.dart'; import 'package:PiliPlus/common/widgets/pair.dart';
@@ -251,25 +252,9 @@ class _LivePageState extends CommonPageState<LivePage, LiveController>
), ),
), ),
const Spacer(), const Spacer(),
GestureDetector( moreTextButton(
behavior: HitTestBehavior.opaque,
onTap: () => Get.to(const LiveFollowPage()), onTap: () => Get.to(const LiveFollowPage()),
child: Row( color: theme.colorScheme.outline,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'查看更多',
style: TextStyle(
color: theme.colorScheme.outline,
),
),
Icon(
size: 20,
Icons.keyboard_arrow_right_outlined,
color: theme.colorScheme.outline,
),
],
),
), ),
], ],
), ),

View File

@@ -652,17 +652,9 @@ class UserInfoCard extends StatelessWidget {
const SizedBox(width: 10), const SizedBox(width: 10),
], ],
); );
if (item.jumpUrl?.isNotEmpty == true) { return GestureDetector(
return GestureDetector( onTap: () => Get.toNamed('/followed?mid=${card.mid}&name=${card.name}'),
onTap: () { child: child,
final isDark = Get.isDarkMode; );
PageUtils.handleWebview(
'${item.jumpUrl}&native.theme=${isDark ? 2 : 1}&night=${isDark ? 1 : 0}',
);
},
child: child,
);
}
return child;
} }
} }

View File

@@ -1,6 +1,7 @@
import 'dart:math'; import 'dart:math';
import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/button/more_btn.dart';
import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart';
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models_new/space/space/data.dart'; import 'package:PiliPlus/models_new/space/space/data.dart';
@@ -301,7 +302,7 @@ class _MemberHomeState extends State<MemberHome>
], ],
), ),
), ),
GestureDetector( moreTextButton(
onTap: () { onTap: () {
int index = _ctr.tab2!.indexWhere( int index = _ctr.tab2!.indexWhere(
(item) => item.param == param, (item) => item.param == param,
@@ -361,25 +362,7 @@ class _MemberHomeState extends State<MemberHome>
SmartDialog.showToast('view $param'); SmartDialog.showToast('view $param');
} }
}, },
child: Text.rich( color: color,
TextSpan(
children: [
TextSpan(
text: '查看更多',
style: TextStyle(color: color),
),
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Icon(
Icons.arrow_forward_ios,
size: 14,
color: color,
),
style: TextStyle(fontSize: 13, color: color),
),
],
),
),
), ),
], ],
), ),

View File

@@ -1,6 +1,7 @@
import 'dart:math'; import 'dart:math';
import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/button/more_btn.dart';
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart';
import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
@@ -231,8 +232,8 @@ class _PgcPageState extends CommonPageState<PgcPage, PgcController>
'推荐', '推荐',
style: theme.textTheme.titleMedium, style: theme.textTheme.titleMedium,
), ),
GestureDetector( moreTextButton(
behavior: HitTestBehavior.opaque, padding: const EdgeInsets.symmetric(vertical: 2),
onTap: () { onTap: () {
if (widget.tabType == HomeTabType.bangumi) { if (widget.tabType == HomeTabType.bangumi) {
Get.to(const PgcIndexPage()); Get.to(const PgcIndexPage());
@@ -291,26 +292,7 @@ class _PgcPageState extends CommonPageState<PgcPage, PgcController>
); );
} }
}, },
child: Padding( color: theme.colorScheme.secondary,
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'查看更多',
strutStyle: const StrutStyle(leading: 0, height: 1),
style: TextStyle(
height: 1,
color: theme.colorScheme.secondary,
),
),
Icon(
Icons.chevron_right,
color: theme.colorScheme.secondary,
),
],
),
),
), ),
], ],
), ),
@@ -394,34 +376,16 @@ class _PgcPageState extends CommonPageState<PgcPage, PgcController>
() => controller.accountService.isLogin.value () => controller.accountService.isLogin.value
? Padding( ? Padding(
padding: const EdgeInsets.symmetric(horizontal: 10), padding: const EdgeInsets.symmetric(horizontal: 10),
child: GestureDetector( child: moreTextButton(
behavior: HitTestBehavior.opaque, text: '查看全部',
onTap: () => Get.toNamed( onTap: () => Get.toNamed(
'/fav', '/fav',
arguments: widget.tabType == HomeTabType.bangumi arguments: widget.tabType == HomeTabType.bangumi
? FavTabType.bangumi.index ? FavTabType.bangumi.index
: FavTabType.cinema.index, : FavTabType.cinema.index,
), ),
child: Padding( padding: const EdgeInsets.symmetric(vertical: 8),
padding: const EdgeInsets.symmetric(vertical: 8), color: theme.colorScheme.secondary,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'查看全部',
strutStyle: const StrutStyle(leading: 0, height: 1),
style: TextStyle(
height: 1,
color: theme.colorScheme.secondary,
),
),
Icon(
Icons.chevron_right,
color: theme.colorScheme.secondary,
),
],
),
),
), ),
) )
: const SizedBox.shrink(), : const SizedBox.shrink(),

View File

@@ -157,8 +157,8 @@ class UgcIntroController extends CommonIntroController with ReloadMixin {
return; return;
} }
var result = await MemberHttp.memberCardInfo(mid: mid); var result = await MemberHttp.memberCardInfo(mid: mid);
if (result['status']) { if (result.isSuccess) {
userStat.value = result['data']; userStat.value = result.data;
} }
} }
} }

View File

@@ -16,6 +16,8 @@ import 'package:PiliPlus/pages/fav_detail/view.dart';
import 'package:PiliPlus/pages/fav_search/view.dart'; import 'package:PiliPlus/pages/fav_search/view.dart';
import 'package:PiliPlus/pages/follow/view.dart'; import 'package:PiliPlus/pages/follow/view.dart';
import 'package:PiliPlus/pages/follow_search/view.dart'; import 'package:PiliPlus/pages/follow_search/view.dart';
import 'package:PiliPlus/pages/follow_type/follow_same/view.dart';
import 'package:PiliPlus/pages/follow_type/followed/view.dart';
import 'package:PiliPlus/pages/history/view.dart'; import 'package:PiliPlus/pages/history/view.dart';
import 'package:PiliPlus/pages/history_search/view.dart'; import 'package:PiliPlus/pages/history_search/view.dart';
import 'package:PiliPlus/pages/home/view.dart'; import 'package:PiliPlus/pages/home/view.dart';
@@ -223,6 +225,8 @@ class Routes {
), ),
CustomGetPage(name: '/audio', page: () => const AudioPage()), CustomGetPage(name: '/audio', page: () => const AudioPage()),
CustomGetPage(name: '/mainReply', page: () => const MainReplyPage()), CustomGetPage(name: '/mainReply', page: () => const MainReplyPage()),
CustomGetPage(name: '/followed', page: () => const FollowedPage()),
CustomGetPage(name: '/sameFollowing', page: () => const FollowSamePage()),
]; ];
} }

View File

@@ -633,16 +633,53 @@ abstract class PiliScheme {
launchURL(); launchURL();
return false; return false;
} else if (host.contains('space.bilibili.com')) { } else if (host.contains('space.bilibili.com')) {
String? sid = void toType({
uri.queryParameters['sid'] ?? required String mid,
required String? type,
}) {
switch (type) {
case 'follow':
Get.toNamed('/follow?mid=$mid');
break;
case 'fans':
Get.toNamed('/fan?mid=$mid');
break;
case 'followed':
Get.toNamed('/followed?mid=$mid');
break;
default:
PageUtils.toDupNamed('/member?mid=$mid', off: off);
}
}
late final queryParameters = uri.queryParameters;
// space.bilibili.com/h5/follow?mid={{mid}}&type={{type}}
if (path.startsWith('/h5/follow')) {
final mid = queryParameters['mid'];
final type = queryParameters['type'];
if (mid != null) {
toType(mid: mid, type: type);
return true;
}
}
// space.bilibili.com/{{uid}}/lists/{{season_id}}
// space.bilibili.com/{{uid}}/lists?sid={{season_id}}
// space.bilibili.com/{{uid}}/channel/collectiondetail?sid={{season_id}}
final sid =
queryParameters['sid'] ??
RegExp(r'lists/(\d+)').firstMatch(path)?.group(1); RegExp(r'lists/(\d+)').firstMatch(path)?.group(1);
if (sid != null) { if (sid != null) {
SubDetailPage.toSubDetailPage(int.parse(sid)); SubDetailPage.toSubDetailPage(int.parse(sid));
return true; return true;
} }
String? mid = uriDigitRegExp.firstMatch(path)?.group(1);
// space.bilibili.com/{{mid}}/relation/{{type}}
final mid = uriDigitRegExp.firstMatch(path)?.group(1);
final type = RegExp(r'relation/([a-z]+)').firstMatch(path)?.group(1);
if (mid != null) { if (mid != null) {
PageUtils.toDupNamed('/member?mid=$mid', off: off); toType(mid: mid, type: type);
return true; return true;
} }
launchURL(); launchURL();