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 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/init.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 {
static Future<LoadingState<FansData>> fans({
static Future<LoadingState<FollowData>> fans({
int? vmid,
int? pn,
int ps = 20,
@@ -21,7 +21,7 @@ class FanHttp {
},
);
if (res.data['code'] == 0) {
return Success(FansData.fromJson(res.data['data']));
return Success(FollowData.fromJson(res.data['data']));
} else {
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(
Api.memberCardInfo,
queryParameters: {
@@ -337,12 +339,9 @@ class MemberHttp {
},
);
if (res.data['code'] == 0) {
return {
'status': true,
'data': MemberCardInfoData.fromJson(res.data['data']),
};
return Success(MemberCardInfoData.fromJson(res.data['data']));
} 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/stat.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/later/data.dart';
import 'package:PiliPlus/models_new/login_log/data.dart';
@@ -496,4 +497,48 @@ class UserHttp {
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 {
late List<FollowItemModel> list;
int? reVersion;
int? total;
FollowData({required this.list, this.reVersion, this.total});
FollowData({required this.list, this.total});
factory FollowData.fromJson(Map<String, dynamic> json) => FollowData(
list:
@@ -13,7 +12,6 @@ class FollowData {
?.map((e) => FollowItemModel.fromJson(e as Map<String, dynamic>))
.toList() ??
<FollowItemModel>[],
reVersion: json['re_version'] as int?,
total: json['total'] as int?,
);
}

View File

@@ -7,12 +7,8 @@ class FollowItemModel extends UpItem {
dynamic tag;
int? special;
String? sign;
int? faceNft;
BaseOfficialVerify? officialVerify;
Vip? vip;
String? nftIcon;
String? recReason;
String? trackId;
String? followTime;
FollowItemModel({
@@ -24,12 +20,8 @@ class FollowItemModel extends UpItem {
super.uname,
super.face,
this.sign,
this.faceNft,
this.officialVerify,
this.vip,
this.nftIcon,
this.recReason,
this.trackId,
this.followTime,
});
@@ -43,7 +35,6 @@ class FollowItemModel extends UpItem {
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(
@@ -52,9 +43,6 @@ class FollowItemModel extends UpItem {
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

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

View File

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

View File

@@ -1,29 +1,35 @@
import 'package:PiliPlus/http/fan.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/video.dart';
import 'package:PiliPlus/models_new/fans/data.dart';
import 'package:PiliPlus/models_new/fans/list.dart';
import 'package:PiliPlus/pages/common/common_list_controller.dart';
import 'package:PiliPlus/models_new/follow/data.dart';
import 'package:PiliPlus/pages/follow_type/controller.dart';
import 'package:PiliPlus/utils/accounts.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
class FansController extends CommonListController<FansData, FansItemModel> {
FansController(this.mid);
int total = 0;
int mid;
class FansController extends FollowTypeController {
FansController(this.showName);
final bool showName;
late final bool isOwner;
@override
void onInit() {
super.onInit();
void init() {
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();
}
@override
List<FansItemModel>? getDataList(FansData response) {
return response.list;
}
@override
Future<LoadingState<FansData>> customGetData() => FanHttp.fans(
Future<LoadingState<FollowData>> customGetData() => FanHttp.fans(
vmid: mid,
pn: page,
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/image/network_img_layer.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/models_new/follow/list.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/services/account_service.dart';
import 'package:PiliPlus/utils/grid.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
@@ -18,160 +11,63 @@ import 'package:get/get.dart';
class FansPage extends StatefulWidget {
const FansPage({
super.key,
this.mid,
this.showName,
this.onSelect,
});
final int? mid;
final bool? showName;
final ValueChanged<UserModel>? onSelect;
@override
State<FansPage> createState() => _FansPageState();
}
class _FansPageState extends State<FansPage> {
late int mid;
String? name;
late bool isOwner;
late FansController _fansController;
class _FansPageState extends FollowTypePageState<FansPage> {
@override
void initState() {
super.initState();
AccountService accountService = Get.find<AccountService>();
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 FansController controller = Get.put(
FansController(widget.showName ?? true),
tag: Utils.generateRandomString(8),
);
late final flag = widget.onSelect == null && controller.isOwner;
Widget _buildBody(
ColorScheme theme,
LoadingState<List<FansItemModel>?> 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) {
_fansController.onLoadMore();
}
return _buildItem(theme, index, response[index]);
},
itemCount: response!.length,
)
: HttpError(onReload: _fansController.onReload),
Error(:var errMsg) => HttpError(
errMsg: errMsg,
onReload: _fansController.onReload,
),
};
}
@override
PreferredSizeWidget? get appBar => widget.showName == false
? null
: AppBar(
title: controller.isOwner
? const Text('我的粉丝')
: Obx(() {
final name = controller.name.value;
if (name != null) return Text('$name的粉丝');
return const SizedBox.shrink();
}),
);
Widget _buildItem(ColorScheme theme, int index, FansItemModel item) {
final isSelect = widget.onSelect != null;
@override
Widget buildItem(int index, FollowItemModel item) {
void onRemove() => showConfirmDialog(
context: context,
title: '确定移除 ${item.uname} ',
onConfirm: () => _fansController.onRemoveFan(index, item.mid!),
onConfirm: () => controller.onRemoveFan(index, item.mid),
);
final flag = !isSelect && isOwner;
return SizedBox(
height: 66,
child: InkWell(
onTap: () {
if (widget.onSelect != null) {
widget.onSelect!(
UserModel(
mid: item.mid!,
name: item.uname!,
avatar: item.face!,
),
);
return;
}
Get.toNamed('/member?mid=${item.mid}');
},
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),
),
],
),
],
),
),
),
return FollowTypeItem(
item: item,
onTap: () {
if (widget.onSelect != null) {
widget.onSelect!(
UserModel(
mid: item.mid,
name: item.uname!,
avatar: item.face!,
),
);
return;
}
Get.toNamed('/member?mid=${item.mid}');
},
onLongPress: flag ? onRemove : null,
onSecondaryTap: flag && !Utils.isMobile ? onRemove : null,
);
}
}

View File

@@ -1,6 +1,7 @@
import 'package:PiliPlus/http/follow.dart';
import 'package:PiliPlus/http/loading_state.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_new/follow/data.dart';
import 'package:PiliPlus/models_new/follow/list.dart';
@@ -14,6 +15,11 @@ class FollowChildController
final FollowController? controller;
final int? tagid;
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;
@@ -21,13 +27,24 @@ class FollowChildController
void onInit() {
super.onInit();
queryData();
if (loadSameFollow) {
_loadSameFollow();
}
}
@override
List<FollowItemModel>? getDataList(FollowData response) {
total = response.total;
return response.list;
}
@override
void checkIsEnd(int length) {
if (total != null && length >= total!) {
isEnd = true;
}
}
@override
bool customHandleResponse(bool isRefresh, Success<FollowData> response) {
if (controller != null) {
@@ -57,4 +74,11 @@ class FollowChildController
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/widgets/button/more_btn.dart';
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
import 'package:PiliPlus/http/loading_state.dart';
@@ -42,7 +43,24 @@ class _FollowChildPageState extends State<FollowChildPage>
@override
Widget build(BuildContext context) {
super.build(context);
final colorScheme = ColorScheme.of(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(
onRefresh: _followController.onRefresh,
child: CustomScrollView(
@@ -55,9 +73,7 @@ class _FollowChildPageState extends State<FollowChildPage>
right: padding.right,
bottom: padding.bottom + 100,
),
sliver: Obx(
() => _buildBody(_followController.loadingState.value),
),
sliver: sliver,
),
],
),
@@ -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
bool get wantKeepAlive =>
widget.onSelect != null || widget.controller?.tabController != null;

View File

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

View File

@@ -31,9 +31,13 @@ class _FollowPageState extends State<FollowPage> {
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
title: Text(
_followController.isOwner ? '我的关注' : '${_followController.name}的关注',
),
title: _followController.isOwner
? const Text('的关注')
: Obx(() {
final name = _followController.name.value;
if (name != null) return Text('$name的关注');
return const SizedBox.shrink();
}),
actions: _followController.isOwner
? [
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/skeleton/video_card_v.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/loading_widget/http_error.dart';
import 'package:PiliPlus/common/widgets/pair.dart';
@@ -251,25 +252,9 @@ class _LivePageState extends CommonPageState<LivePage, LiveController>
),
),
const Spacer(),
GestureDetector(
behavior: HitTestBehavior.opaque,
moreTextButton(
onTap: () => Get.to(const LiveFollowPage()),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'查看更多',
style: TextStyle(
color: theme.colorScheme.outline,
),
),
Icon(
size: 20,
Icons.keyboard_arrow_right_outlined,
color: theme.colorScheme.outline,
),
],
),
color: theme.colorScheme.outline,
),
],
),

View File

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

View File

@@ -1,6 +1,7 @@
import 'dart:math';
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/http/loading_state.dart';
import 'package:PiliPlus/models_new/space/space/data.dart';
@@ -301,7 +302,7 @@ class _MemberHomeState extends State<MemberHome>
],
),
),
GestureDetector(
moreTextButton(
onTap: () {
int index = _ctr.tab2!.indexWhere(
(item) => item.param == param,
@@ -361,25 +362,7 @@ class _MemberHomeState extends State<MemberHome>
SmartDialog.showToast('view $param');
}
},
child: Text.rich(
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),
),
],
),
),
color: color,
),
],
),

View File

@@ -1,6 +1,7 @@
import 'dart:math';
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/loading_widget.dart';
import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
@@ -231,8 +232,8 @@ class _PgcPageState extends CommonPageState<PgcPage, PgcController>
'推荐',
style: theme.textTheme.titleMedium,
),
GestureDetector(
behavior: HitTestBehavior.opaque,
moreTextButton(
padding: const EdgeInsets.symmetric(vertical: 2),
onTap: () {
if (widget.tabType == HomeTabType.bangumi) {
Get.to(const PgcIndexPage());
@@ -291,26 +292,7 @@ class _PgcPageState extends CommonPageState<PgcPage, PgcController>
);
}
},
child: Padding(
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,
),
],
),
),
color: theme.colorScheme.secondary,
),
],
),
@@ -394,34 +376,16 @@ class _PgcPageState extends CommonPageState<PgcPage, PgcController>
() => controller.accountService.isLogin.value
? Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: GestureDetector(
behavior: HitTestBehavior.opaque,
child: moreTextButton(
text: '查看全部',
onTap: () => Get.toNamed(
'/fav',
arguments: widget.tabType == HomeTabType.bangumi
? FavTabType.bangumi.index
: FavTabType.cinema.index,
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
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,
),
],
),
),
padding: const EdgeInsets.symmetric(vertical: 8),
color: theme.colorScheme.secondary,
),
)
: const SizedBox.shrink(),

View File

@@ -157,8 +157,8 @@ class UgcIntroController extends CommonIntroController with ReloadMixin {
return;
}
var result = await MemberHttp.memberCardInfo(mid: mid);
if (result['status']) {
userStat.value = result['data'];
if (result.isSuccess) {
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/follow/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_search/view.dart';
import 'package:PiliPlus/pages/home/view.dart';
@@ -223,6 +225,8 @@ class Routes {
),
CustomGetPage(name: '/audio', page: () => const AudioPage()),
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();
return false;
} else if (host.contains('space.bilibili.com')) {
String? sid =
uri.queryParameters['sid'] ??
void toType({
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);
if (sid != null) {
SubDetailPage.toSubDetailPage(int.parse(sid));
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) {
PageUtils.toDupNamed('/member?mid=$mid', off: off);
toType(mid: mid, type: type);
return true;
}
launchURL();