Files
PiliPlus/lib/utils/request_utils.dart
dom 07843a5e77 drop
Signed-off-by: dom <githubaccount56556@proton.me>
2026-05-08 21:27:01 +08:00

605 lines
20 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:PiliPlus/common/widgets/dialog/dialog.dart';
import 'package:PiliPlus/grpc/bilibili/im/type.pbenum.dart';
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
show ReplyInfo;
import 'package:PiliPlus/grpc/im.dart';
import 'package:PiliPlus/http/dynamics.dart';
import 'package:PiliPlus/http/fav.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/member.dart';
import 'package:PiliPlus/http/user.dart';
import 'package:PiliPlus/http/validate.dart';
import 'package:PiliPlus/http/video.dart';
import 'package:PiliPlus/models/dynamics/result.dart';
import 'package:PiliPlus/models/login/model.dart';
import 'package:PiliPlus/models_new/fav/fav_detail/media.dart';
import 'package:PiliPlus/models_new/later/list.dart';
import 'package:PiliPlus/models_new/relation/data.dart';
import 'package:PiliPlus/pages/common/multi_select/base.dart';
import 'package:PiliPlus/pages/dynamics_tab/controller.dart';
import 'package:PiliPlus/pages/fav_detail/controller.dart'
show BaseFavController;
import 'package:PiliPlus/pages/group_panel/view.dart';
import 'package:PiliPlus/pages/login/geetest/geetest_webview_dialog.dart';
import 'package:PiliPlus/pages/webview/view.dart';
import 'package:PiliPlus/utils/accounts.dart';
import 'package:PiliPlus/utils/extension/context_ext.dart';
import 'package:PiliPlus/utils/extension/size_ext.dart';
import 'package:PiliPlus/utils/extension/string_ext.dart';
import 'package:PiliPlus/utils/extension/theme_ext.dart';
import 'package:PiliPlus/utils/platform_utils.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/utils/storage_key.dart';
import 'package:PiliPlus/utils/theme_utils.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/foundation.dart' show kDebugMode;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show LengthLimitingTextInputFormatter;
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
abstract final class RequestUtils {
static Future<void> syncHistoryStatus() async {
final account = Accounts.history;
if (!account.isLogin) {
return;
}
final res = await UserHttp.historyStatus(account: account);
if (res case Success(:final response)) {
GStorage.localCache.put(LocalCacheKey.historyPause, response);
}
}
// 1小视频已弃用
// 2相簿
// 3纯文字
// 4直播此类型不常用见分享其他内容消息
// 5视频
// 6专栏
// 7番剧id 为 season_id
// 8音乐
// 9国产动画id 为 AV 号)
// 10图片
// 11动态
// 16番剧id 为 epid
// 17番剧
// https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/message/private_msg_content.md
static Future<bool> pmShare({
required int receiverId,
required Map content,
String? message,
}) async {
final ownerMid = Accounts.main.mid;
final contentRes = await ImGrpc.sendMsg(
senderUid: ownerMid,
receiverId: receiverId,
content: jsonEncode(content),
msgType: content['source'] is String
? MsgType.EN_MSG_TYPE_COMMON_SHARE_CARD
: MsgType.EN_MSG_TYPE_SHARE_V2,
);
if (contentRes.isSuccess) {
if (message?.isNotEmpty == true) {
final msgRes = await ImGrpc.sendMsg(
senderUid: ownerMid,
receiverId: receiverId,
content: jsonEncode({"content": message}),
msgType: MsgType.EN_MSG_TYPE_TEXT,
);
return msgRes.isSuccess;
} else {
return true;
}
} else {
return false;
}
}
static Future<void> createFavTag(
BuildContext context,
ValueChanged<({int tagid, String tagName})> onSuccess,
) async {
String tagName = '';
final onCreate = await showConfirmDialog(
context: context,
title: const Text('新建分组'),
content: TextFormField(
autofocus: true,
initialValue: tagName,
onChanged: (value) => tagName = value,
inputFormatters: [
LengthLimitingTextInputFormatter(16),
],
decoration: const InputDecoration(border: OutlineInputBorder()),
),
);
if (onCreate) {
final res = await MemberHttp.createFollowTag(tagName);
if (res case Success(:final response)) {
onSuccess((tagid: response, tagName: tagName));
SmartDialog.showToast('创建成功');
} else {
res.toast();
}
}
}
static Future<void> actionRelationMod({
required BuildContext context,
required dynamic mid,
required bool isFollow,
required ValueChanged<int>? afterMod,
RelationData? followStatus,
}) async {
if (mid == null) {
return;
}
if (!isFollow) {
final res = await VideoHttp.relationMod(
mid: mid,
act: 1,
reSrc: 11,
);
if (res.isSuccess) {
SmartDialog.showToast('关注成功');
afterMod?.call(2);
} else {
res.toast();
}
} else {
if (followStatus?.tag == null) {
final res = await UserHttp.userRelation(mid);
if (res case Success(:final response)) {
followStatus = response;
} else {
res.toast();
return;
}
}
if (context.mounted) {
bool isSpecialFollowed = followStatus!.special == 1;
String text = isSpecialFollowed ? '移除特别关注' : '加入特别关注';
showDialog(
context: context,
builder: (context) => AlertDialog(
clipBehavior: Clip.hardEdge,
contentPadding: const EdgeInsets.symmetric(vertical: 12),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
dense: true,
onTap: () async {
Get.back();
final res = await MemberHttp.specialAction(
fid: mid,
isAdd: !isSpecialFollowed,
);
if (res.isSuccess) {
SmartDialog.showToast('$text成功');
afterMod?.call(isSpecialFollowed ? 2 : -10);
} else {
res.toast();
}
},
title: Text(
text,
style: const TextStyle(fontSize: 14),
),
),
ListTile(
dense: true,
onTap: () async {
Get.back();
final result = await showModalBottomSheet<Set<int>>(
context: context,
useSafeArea: true,
isScrollControlled: true,
constraints: BoxConstraints(
maxWidth: min(640, context.mediaQueryShortestSide),
),
builder: (BuildContext context) {
final maxChildSize =
PlatformUtils.isMobile &&
!context.mediaQuerySize.isPortrait
? 1.0
: 0.7;
return DraggableScrollableSheet(
minChildSize: 0,
maxChildSize: 1,
snap: true,
expand: false,
snapSizes: [maxChildSize],
initialChildSize: maxChildSize,
builder: (context, scrollController) {
return GroupPanel(
mid: mid,
tags: followStatus!.tag,
scrollController: scrollController,
);
},
);
},
);
if (result != null) {
followStatus!.tag = result.toList();
afterMod?.call(result.contains(-10) ? -10 : 2);
}
},
title: const Text(
'设置分组',
style: TextStyle(fontSize: 14),
),
),
ListTile(
dense: true,
onTap: () async {
Get.back();
final res = await VideoHttp.relationMod(
mid: mid,
act: 2,
reSrc: 11,
);
if (res.isSuccess) {
SmartDialog.showToast('取消关注成功');
afterMod?.call(0);
} else {
res.toast();
}
},
title: const Text(
'取消关注',
style: TextStyle(fontSize: 14),
),
),
],
),
),
);
}
}
}
static ReplyInfo replyCast(Map res) {
Map? emote = res['content']['emote'];
emote?.forEach((key, value) {
value['size'] = value['meta']['size'];
});
return ReplyInfo.create()..mergeFromProto3Json(
res
..['content'].remove('members')
..['id'] = res['rpid']
..['member']['name'] = res['member']['uname']
..['member']['face'] = res['member']['avatar']
..['member']['level'] = res['member']['level_info']['current_level']
..['member']['vipStatus'] = res['member']['vip']['vipStatus']
..['member']['vipType'] = res['member']['vip']['vipType']
..['member']['officialVerifyType'] =
res['member']['official_verify']['type']
..['content']['emotes'] = emote,
ignoreUnknownFields: true,
);
}
// static Future<dynamic> getWwebid(mid) async {
// try {
// final response = await Request().get(
// '${HttpString.spaceBaseUrl}/$mid/dynamic',
// options: Options(
// extra: {'account': AnonymousAccount()},
// ),
// );
// dom.Document document = html_parser.parse(response.data);
// dom.Element? scriptElement =
// document.querySelector('script#__RENDER_DATA__');
// return jsonDecode(
// Uri.decodeComponent(scriptElement?.text ?? ''))['access_id'];
// } catch (e) {
// if (kDebugMode) debugPrint('failed to get wwebid: $e');
// return null;
// }
// }
static Future<void> insertCreatedDyn(dynamic id) async {
if (id != null) {
try {
await Future.delayed(const Duration(milliseconds: 450));
final res = await DynamicsHttp.dynamicDetail(id: id);
if (res case final Success<DynamicItemModel> e) {
final ctr = Get.find<DynamicsTabController>(tag: 'all');
if (ctr.loadingState.value case Success(:final response?)) {
response.insert(0, e.response);
ctr.loadingState.refresh();
return;
}
ctr.loadingState.value = Success([e.response]);
}
} catch (e) {
if (kDebugMode) debugPrint('create dyn $e');
}
}
}
static Future<void> checkCreatedDyn({
dynamic id,
String? dynText,
bool isManual = false,
}) async {
try {
if (id != null) {
if (!isManual) {
await Future.delayed(const Duration(seconds: 5));
}
final res = await DynamicsHttp.dynamicDetail(
id: id,
clearCookie: true,
);
final isSuccess = res.isSuccess;
final theme = ThemeUtils.theme;
final actions = [
if (!isSuccess)
TextButton(
onPressed: () {
Get.back();
Utils.copyText('https://www.bilibili.com/opus/$id');
WebViewPage.toWebView(
'https://www.bilibili.com/h5/comment/appeal?${ThemeUtils.themeUrl(theme.isDark)}',
);
},
child: const Text('申诉'),
),
if (!isManual)
TextButton(
onPressed: Get.back,
child: Text(
'关闭',
style: TextStyle(color: theme.colorScheme.outline),
),
),
];
showDialog(
context: Get.context!,
barrierDismissible: isManual,
builder: (context) => AlertDialog(
title: const Text('动态检查结果'),
content: SelectableText(
'${isSuccess ? '无账号状态下找到了你的动态,动态正常!' : '你的动态被shadow ban仅自己可见'}${dynText != null ? ' \n\n动态内容: $dynText' : ''}',
),
actions: actions.isEmpty ? null : actions,
),
);
}
} catch (e) {
if (kDebugMode) debugPrint('check dyn error: $e');
}
}
// 动态点赞
static Future<void> onLikeDynamic(
DynamicItemModel item,
bool uiStatus,
VoidCallback onSuccess,
) async {
final like = item.modules.moduleStat?.like;
final status = like?.status ?? false;
if (status ^ uiStatus) {
SmartDialog.showToast(status ? '点赞成功' : '取消赞');
onSuccess();
return;
}
final res = await DynamicsHttp.thumbDynamic(
dynamicId: item.idStr!,
up: status ? 2 : 1, // 1 已点赞 2 不喜欢 0 未操作
);
if (res.isSuccess) {
SmartDialog.showToast(status ? '取消赞' : '点赞成功');
like
?..count = (like.count ?? 0) + (status ? -1 : 1)
..status = !status;
onSuccess();
} else {
res.toast();
}
}
static void onCopyOrMove<T extends MultiSelectData>({
required BuildContext context,
required bool isCopy,
required CommonMultiSelectMixin<T> ctr,
required dynamic mediaId,
required dynamic mid,
}) {
FavHttp.allFavFolders(mid).then((res) {
if (!context.mounted) return;
if (res case Success(:final response)) {
final list = response.list;
if (list == null || list.isEmpty) return;
int? checkedId;
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('${isCopy ? '复制' : '移动'}'),
contentPadding: const EdgeInsets.only(top: 5),
content: SingleChildScrollView(
child: RadioGroup(
onChanged: (value) {
checkedId = value;
(context as Element).markNeedsBuild();
},
groupValue: checkedId,
child: Column(
children: list.map((item) {
return RadioListTile<int>(
dense: true,
title: Text(item.title),
value: item.id,
);
}).toList(),
),
),
),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
),
),
),
TextButton(
onPressed: () {
if (checkedId != null) {
final removeList = ctr.allChecked.toSet();
SmartDialog.showLoading();
FavHttp.copyOrMoveFav(
isCopy: isCopy,
isFav: ctr is BaseFavController,
srcMediaId: mediaId,
tarMediaId: checkedId,
resources: removeList
.map(
(e) => switch (e) {
LaterItemModel _ => e.aid,
FavDetailItemModel _ => '${e.id}:${e.type}',
_ => throw UnsupportedError(e.toString()),
},
)
.join(','),
mid: isCopy ? mid : null,
).then((res) {
if (res.isSuccess) {
ctr.handleSelect(checked: false);
if (!isCopy) {
ctr.loadingState
..value.data!.removeWhere(removeList.contains)
..refresh();
}
SmartDialog.dismiss();
SmartDialog.showToast('${isCopy ? '复制' : '移动'}成功');
Get.back();
} else {
SmartDialog.dismiss();
res.toast();
}
});
}
},
child: const Text('确认'),
),
],
);
},
);
} else {
res.toast();
}
});
}
static Future<void> validate(
String vVoucher,
ValueChanged<String> onSuccess,
) async {
final res = await ValidateHttp.gaiaVgateRegister(vVoucher);
if (!res.isSuccess) {
res.toast();
return;
}
final resData = res.data;
if (resData == null) {
SmartDialog.showToast("null data");
return;
}
CaptchaDataModel captchaData = CaptchaDataModel();
final geetest = resData['geetest'];
String? gt = geetest?['gt'];
String? challenge = geetest?['challenge'];
captchaData.token = resData['token'];
bool isGeeArgumentValid() {
return gt?.isNotEmpty == true &&
challenge?.isNotEmpty == true &&
captchaData.token?.isNotEmpty == true;
}
if (!isGeeArgumentValid()) {
SmartDialog.showToast("参数为空");
return;
}
Future<void> gaiaVgateValidate() async {
final res = await ValidateHttp.gaiaVgateValidate(
challenge: captchaData.geetest?.challenge,
seccode: captchaData.seccode,
token: captchaData.token,
validate: captchaData.validate,
);
if (res case Success(:final response?)) {
if (response['is_valid'] == 1) {
final griskId = response['grisk_id'];
if (griskId is String) {
onSuccess(griskId);
}
} else {
SmartDialog.showToast('invalid');
}
} else {
res.toast();
}
}
final json = await showDialog<Map<String, dynamic>>(
context: Get.context!,
builder: (context) => GeetestWebviewDialog(gt!, challenge!),
);
if (json != null) {
captchaData
..validate = json['geetest_validate']
..seccode = json['geetest_seccode']
..geetest = GeetestData(
challenge: json['geetest_challenge'],
gt: gt!,
);
gaiaVgateValidate();
}
}
static Future<void> showUserRealName(String mid) async {
final res = await UserHttp.getUserRealName(mid);
if (res case Success(:final response)) {
final show = !response.name.isNullOrEmpty;
showDialog(
context: Get.context!,
builder: (context) => AlertDialog(
title: SelectableText(
show ? response.name! : response.rejectPage?.title ?? '',
),
content: show ? null : Text(response.rejectPage?.text ?? ''),
actions: [
TextButton(
onPressed: Get.back,
child: const Text('关闭'),
),
],
),
);
} else {
res.toast();
}
}
}