diff --git a/README.md b/README.md index 970de55f..6f456ff0 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ ## feat +- [x] 修改头像/用户名/签名/性别/生日 - [x] 创建/编辑/删除收藏夹 - [x] 评论楼中楼查看对话 - [x] 评论楼中楼定位点击查看的评论 diff --git a/lib/http/init.dart b/lib/http/init.dart index 9e0c118a..b8457295 100644 --- a/lib/http/init.dart +++ b/lib/http/init.dart @@ -188,7 +188,7 @@ class Request { /* * get请求 */ - get(url, {data, options, cancelToken, extra}) async { + Future get(url, {data, options, cancelToken, extra}) async { Response response; options ??= Options(); ResponseType resType = ResponseType.json; diff --git a/lib/pages/dynamics/detail/view.dart b/lib/pages/dynamics/detail/view.dart index 17f72a37..b7e5698b 100644 --- a/lib/pages/dynamics/detail/view.dart +++ b/lib/pages/dynamics/detail/view.dart @@ -193,7 +193,8 @@ class _DynamicDetailPageState extends State return Scaffold( appBar: AppBar( elevation: 0, - scrolledUnderElevation: 1, + scrolledUnderElevation: 0, + backgroundColor: Theme.of(context).colorScheme.surface, centerTitle: false, titleSpacing: 0, title: StreamBuilder( diff --git a/lib/pages/member/new/controller.dart b/lib/pages/member/new/controller.dart index a03e0529..730b304b 100644 --- a/lib/pages/member/new/controller.dart +++ b/lib/pages/member/new/controller.dart @@ -117,7 +117,9 @@ class MemberControllerNew extends CommonController } void onFollow(BuildContext context) async { - if (relation.value == -1) { + if (mid == ownerMid) { + Get.toNamed('/editProfile'); + } else if (relation.value == -1) { _onBlock(); } else { if (ownerMid == null) { diff --git a/lib/pages/member/new/member_page.dart b/lib/pages/member/new/member_page.dart index 4e82d74e..397800fc 100644 --- a/lib/pages/member/new/member_page.dart +++ b/lib/pages/member/new/member_page.dart @@ -231,7 +231,8 @@ class _MemberPageNewState extends State ], ), ), - if (_userController.ownerMid != null) ...[ + if (_userController.ownerMid != null && + _userController.mid != _userController.ownerMid) ...[ const PopupMenuDivider(), PopupMenuItem( onTap: () { @@ -303,6 +304,7 @@ class _MemberPageNewState extends State padding: EdgeInsets.only( bottom: (_userController.tab2?.length ?? 0) > 1 ? 48 : 0), child: UserInfoCard( + isOwner: _userController.mid == _userController.ownerMid, relation: _userController.relation.value, isFollow: _userController.isFollow.value, card: userState.response.card, diff --git a/lib/pages/member/new/widget/edit_profile_page.dart b/lib/pages/member/new/widget/edit_profile_page.dart new file mode 100644 index 00000000..6919dd13 --- /dev/null +++ b/lib/pages/member/new/widget/edit_profile_page.dart @@ -0,0 +1,509 @@ +import 'package:PiliPalaX/common/constants.dart'; +import 'package:PiliPalaX/common/widgets/http_error.dart'; +import 'package:PiliPalaX/http/constants.dart'; +import 'package:PiliPalaX/http/index.dart'; +import 'package:PiliPalaX/http/loading_state.dart'; +import 'package:PiliPalaX/utils/storage.dart'; +import 'package:PiliPalaX/utils/utils.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:dio/dio.dart'; +import 'package:easy_debounce/easy_throttle.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart' hide FormData, MultipartFile; +import 'package:image_cropper/image_cropper.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:intl/intl.dart'; + +enum ProfileType { uname, sign, sex, birthday } + +class EditProfilePage extends StatefulWidget { + const EditProfilePage({super.key}); + + @override + State createState() => _EditProfilePageState(); +} + +class _EditProfilePageState extends State { + LoadingState _loadingState = LoadingState.loading(); + late final _textController = TextEditingController(); + late final _imagePicker = ImagePicker(); + + @override + void initState() { + super.initState(); + _getInfo(); + } + + @override + void dispose() { + _textController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('账号资料'), + ), + body: _buildBody(_loadingState), + ); + } + + _getInfo() async { + Map data = { + 'access_key': GStorage.localCache + .get(LocalCacheKey.accessKey, defaultValue: {})['value'], + 'appkey': Constants.appKey, + 'build': '1462100', + 'c_locale': 'zh_CN', + 'channel': 'yingyongbao', + 'mobi_app': 'android_hd', + 'platform': 'android', + 's_locale': 'zh_CN', + 'statistics': Constants.statistics, + 'ts': (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString(), + }; + String sign = Utils.appSign( + data, + Constants.appKey, + Constants.appSec, + ); + data['sign'] = sign; + Request() + .get( + '${HttpString.appBaseUrl}/x/v2/account/myinfo', + data: data, + ) + .then((data) { + setState(() { + if (data.data['code'] == 0) { + _loadingState = LoadingState.success(data.data['data']); + } else { + _loadingState = LoadingState.error(data.data['message']); + } + }); + }); + } + + Widget get _divider => Divider( + height: 1, + color: Theme.of(context).dividerColor.withOpacity(0.15), + ); + + Widget get _divider1 => Divider( + thickness: 16, + color: Theme.of(context).dividerColor.withOpacity(0.25), + ); + + Widget _buildBody(LoadingState loadingState) { + switch (loadingState) { + case Error(): + return CustomScrollView( + shrinkWrap: true, + slivers: [ + HttpError( + errMsg: loadingState.errMsg, + fn: _getInfo, + ), + ], + ); + case Success(): + return SingleChildScrollView( + child: Column( + children: [ + _item( + title: '头像', + widget: Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: ClipOval( + child: CachedNetworkImage( + imageUrl: loadingState.response['face'], + ), + ), + ), + onTap: () { + EasyThrottle.throttle( + 'imagePicker', const Duration(milliseconds: 500), + () async { + _pickImg(); + }); + }, + ), + _divider, + _item( + title: '昵称', + text: loadingState.response['name'], + onTap: () { + if (loadingState.response['coins'] < 6) { + SmartDialog.showToast('硬币不足'); + } else { + _editDialog( + type: ProfileType.uname, + title: '昵称', + text: loadingState.response['name'], + ); + } + }, + ), + _divider, + _item( + title: '性别', + text: _sex(loadingState.response['sex']), + onTap: () { + showDialog( + context: context, + builder: (_) => _sexDialog(loadingState.response['sex']), + ); + }, + ), + _divider, + _item( + title: '出生年月', + text: loadingState.response['birthday'], + onTap: () { + showDatePicker( + context: context, + initialDate: + DateTime.parse(loadingState.response['birthday']), + firstDate: DateTime(1900, 1, 1), + lastDate: DateTime.now(), + ).then((date) { + if (date != null) { + _update( + type: ProfileType.birthday, + datum: DateFormat('yyyy-MM-dd').format(date), + ); + } + }); + }, + ), + _divider, + _item( + title: '个性签名', + text: loadingState.response['sign'].isEmpty + ? '无' + : loadingState.response['sign'], + onTap: () { + _editDialog( + type: ProfileType.sign, + title: '个性签名', + text: loadingState.response['sign'], + ); + }, + ), + _divider1, + _item( + title: '头像挂件', + onTap: () => Utils.launchURL( + 'https://www.bilibili.com/h5/mall/pendant/home'), + ), + _divider1, + _item( + title: 'UID', + needIcon: false, + text: loadingState.response['mid'].toString(), + onTap: () => + Utils.copyText(loadingState.response['mid'].toString()), + ), + // _divider, + // _item( + // title: '二维码名片', + // widget: Icon( + // Icons.qr_code, + // color: Theme.of(context).colorScheme.outline, + // ), + // onTap: () {}, + // ), + _divider1, + _item( + title: '哔哩哔哩认证', + onTap: () => Utils.launchURL( + 'https://account.bilibili.com/official/mobile/home'), + ), + _divider, + ], + ), + ); + } + return Center( + child: CircularProgressIndicator(), + ); + } + + Widget _sexDialog(int current) { + return AlertDialog( + clipBehavior: Clip.hardEdge, + contentPadding: const EdgeInsets.symmetric(vertical: 12), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _sexDialogItem(1, current, '男'), + _sexDialogItem(0, current, '保密'), + _sexDialogItem(2, current, '女'), + ], + ), + ); + } + + Widget _sexDialogItem( + int sex, + int current, + String text, + ) { + return ListTile( + dense: true, + enabled: current != sex, + title: Padding( + padding: const EdgeInsets.only(left: 10), + child: Text( + text, + style: const TextStyle(fontSize: 14), + ), + ), + trailing: current == sex ? const Icon(size: 22, Icons.check) : null, + onTap: () { + Get.back(); + _update(type: ProfileType.sex, datum: sex); + }, + ); + } + + void _editDialog({ + required ProfileType type, + required String title, + required String text, + }) { + _textController.text = text; + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text( + '修改$title', + style: TextStyle(fontSize: 18), + ), + content: TextField( + controller: _textController, + minLines: type == ProfileType.uname ? 1 : 4, + maxLines: type == ProfileType.uname ? 1 : 4, + autofocus: true, + style: TextStyle(fontSize: 14), + textInputAction: + type == ProfileType.sign ? TextInputAction.newline : null, + inputFormatters: [ + LengthLimitingTextInputFormatter( + type == ProfileType.uname ? 16 : 70), + ], + decoration: InputDecoration( + hintText: text, + hintStyle: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.outline, + ), + ), + ), + actions: [ + TextButton( + onPressed: Get.back, + child: Text( + '取消', + style: TextStyle(color: Theme.of(context).colorScheme.outline), + ), + ), + TextButton( + onPressed: () { + if (_textController.text == text) { + SmartDialog.showToast('与原$title相同'); + } else { + _update(type: type); + } + }, + child: const Text('确定'), + ), + ], + ); + }, + ).then((_) { + _textController.clear(); + }); + } + + _update({ + required ProfileType type, + dynamic datum, + }) async { + Map data = { + 'access_key': GStorage.localCache + .get(LocalCacheKey.accessKey, defaultValue: {})['value'], + 'appkey': Constants.appKey, + 'build': '1462100', + 'c_locale': 'zh_CN', + 'channel': 'yingyongbao', + 'mobi_app': 'android_hd', + 'platform': 'android', + 's_locale': 'zh_CN', + 'statistics': Constants.statistics, + 'ts': (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString(), + if (type == ProfileType.uname) + 'uname': _textController.text + else if (type == ProfileType.sign) + 'user_sign': _textController.text + else if (type == ProfileType.birthday) + 'birthday': datum + else if (type == ProfileType.sex) + 'sex': datum.toString(), + }; + String sign = Utils.appSign( + data, + Constants.appKey, + Constants.appSec, + ); + data['sign'] = sign; + Request() + .post( + '/x/member/app/${type.name}/update', + data: data, + options: Options( + contentType: Headers.formUrlEncodedContentType, + ), + ) + .then((data) { + if (data.data['code'] == 0) { + if (type == ProfileType.uname) { + (_loadingState as Success).response['name'] = _textController.text; + (_loadingState as Success).response['coins'] -= 6; + } else if (type == ProfileType.sign) { + (_loadingState as Success).response['sign'] = _textController.text; + } else if (type == ProfileType.birthday) { + (_loadingState as Success).response['birthday'] = datum; + } else if (type == ProfileType.sex) { + (_loadingState as Success).response['sex'] = datum; + } + SmartDialog.showToast('修改成功'); + setState(() {}); + } else { + SmartDialog.showToast(data.data['message']); + } + }); + } + + String _sex(int sex) { + return switch (sex) { + 0 => '保密', + 1 => '男', + 2 => '女', + _ => '未知', + }; + } + + Widget _item({ + required String title, + Widget? widget, + String? text, + GestureTapCallback? onTap, + bool needIcon = true, + }) { + return ListTile( + onTap: onTap, + dense: title != '头像', + leading: Text( + title, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + ), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (text != null) + Text( + text, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Theme.of(context).colorScheme.outline, + ), + ) + else if (widget != null) + widget, + if (needIcon) + Icon( + Icons.keyboard_arrow_right, + color: Theme.of(context).colorScheme.outline, + ) + ], + ), + ); + } + + void _pickImg() async { + XFile? pickedFile = await _imagePicker.pickImage( + source: ImageSource.gallery, + imageQuality: 100, + ); + if (pickedFile != null && mounted) { + CroppedFile? croppedFile = await ImageCropper().cropImage( + sourcePath: pickedFile.path, + uiSettings: [ + AndroidUiSettings( + toolbarTitle: '裁剪', + toolbarColor: Theme.of(context).colorScheme.primary, + toolbarWidgetColor: Colors.white, + aspectRatioPresets: [ + CropAspectRatioPresetCustom(), + ], + lockAspectRatio: true, + hideBottomControls: true, + cropStyle: CropStyle.circle, + initAspectRatio: CropAspectRatioPresetCustom(), + ), + IOSUiSettings( + title: '裁剪', + aspectRatioPresets: [ + CropAspectRatioPresetCustom(), + ], + cropStyle: CropStyle.circle, + aspectRatioLockEnabled: true, + resetAspectRatioEnabled: false, + aspectRatioPickerButtonHidden: true, + ), + ], + ); + if (croppedFile != null) { + Request() + .post( + '/x/member/web/face/update', + queryParameters: { + 'csrf': await Request.getCsrf(), + }, + data: FormData.fromMap({ + 'dopost': 'save', + 'DisplayRank': 10000, + 'face': await MultipartFile.fromFile(croppedFile.path), + }), + ) + .then((data) { + if (data.data['code'] == 0) { + (_loadingState as Success).response['face'] = data.data['data']; + SmartDialog.showToast('修改成功'); + setState(() {}); + } else { + SmartDialog.showToast(data.data['message']); + } + }); + } + } + } +} + +class CropAspectRatioPresetCustom implements CropAspectRatioPresetData { + @override + (int, int)? get data => (1, 1); + + @override + String get name => '1x1 (customized)'; +} diff --git a/lib/pages/member/new/widget/user_info_card.dart b/lib/pages/member/new/widget/user_info_card.dart index cef563f9..dca1d33e 100644 --- a/lib/pages/member/new/widget/user_info_card.dart +++ b/lib/pages/member/new/widget/user_info_card.dart @@ -12,6 +12,7 @@ import 'package:get/get.dart'; class UserInfoCard extends StatelessWidget { const UserInfoCard({ super.key, + required this.isOwner, required this.card, required this.images, required this.relation, @@ -19,6 +20,7 @@ class UserInfoCard extends StatelessWidget { required this.onFollow, }); + final bool isOwner; final int relation; final bool isFollow; final space.Card card; @@ -173,37 +175,39 @@ class UserInfoCard extends StatelessWidget { Row( mainAxisSize: MainAxisSize.min, children: [ - IconButton.outlined( - onPressed: () { - if (GStorage.userInfo.get('userInfoCache') != null) { - Get.toNamed( - '/whisperDetail', - parameters: { - 'talkerId': card.mid ?? '', - 'name': card.name ?? '', - 'face': card.face ?? '', - 'mid': card.mid ?? '', - }, - ); - } - }, - icon: const Icon(Icons.mail_outline, size: 21), - style: IconButton.styleFrom( - side: BorderSide( - width: 1.0, - color: Theme.of(context) - .colorScheme - .outline - .withOpacity(0.5), - ), - padding: EdgeInsets.zero, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - visualDensity: const VisualDensity( - horizontal: -2, - vertical: -2, + if (!isOwner) + IconButton.outlined( + onPressed: () { + if (GStorage.userInfo.get('userInfoCache') != + null) { + Get.toNamed( + '/whisperDetail', + parameters: { + 'talkerId': card.mid ?? '', + 'name': card.name ?? '', + 'face': card.face ?? '', + 'mid': card.mid ?? '', + }, + ); + } + }, + icon: const Icon(Icons.mail_outline, size: 21), + style: IconButton.styleFrom( + side: BorderSide( + width: 1.0, + color: Theme.of(context) + .colorScheme + .outline + .withOpacity(0.5), + ), + padding: EdgeInsets.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + visualDensity: const VisualDensity( + horizontal: -2, + vertical: -2, + ), ), ), - ), const SizedBox(width: 10), FilledButton.tonal( onPressed: onFollow, @@ -236,13 +240,15 @@ class UserInfoCard extends StatelessWidget { ), ), TextSpan( - text: relation == -1 - ? '移除黑名单' - : relation == 2 - ? ' 特别关注' - : isFollow - ? ' 已关注' - : '关注', + text: isOwner + ? '编辑资料' + : relation == -1 + ? '移除黑名单' + : relation == 2 + ? ' 特别关注' + : isFollow + ? ' 已关注' + : '关注', ) ], ), diff --git a/lib/pages/video/detail/introduction/widgets/create_fav_page.dart b/lib/pages/video/detail/introduction/widgets/create_fav_page.dart index 9fcf4df1..e2a9f753 100644 --- a/lib/pages/video/detail/introduction/widgets/create_fav_page.dart +++ b/lib/pages/video/detail/introduction/widgets/create_fav_page.dart @@ -286,7 +286,7 @@ class _CreateFavPageState extends State { : null, ), inputFormatters: [ - LengthLimitingTextInputFormatter(21), + LengthLimitingTextInputFormatter(20), ], decoration: InputDecoration( isDense: true, @@ -332,11 +332,11 @@ class _CreateFavPageState extends State { controller: _introController, style: TextStyle(fontSize: 14), inputFormatters: [ - LengthLimitingTextInputFormatter(201), + LengthLimitingTextInputFormatter(200), ], decoration: InputDecoration( isDense: true, - hintText: '简介', + hintText: '可填写简介', hintStyle: TextStyle( fontSize: 14, color: Theme.of(context).colorScheme.outline, diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index 1b641da4..c83e2520 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -1,6 +1,7 @@ // ignore_for_file: must_be_immutable import 'package:PiliPalaX/pages/member/new/member_page.dart'; +import 'package:PiliPalaX/pages/member/new/widget/edit_profile_page.dart'; import 'package:PiliPalaX/pages/setting/sponsor_block_page.dart'; import 'package:PiliPalaX/pages/video/detail/introduction/widgets/create_fav_page.dart'; import 'package:PiliPalaX/pages/webview/webview_page.dart'; @@ -188,6 +189,7 @@ class Routes { CustomGetPage(name: '/danmakuBlock', page: () => const DanmakuBlockPage()), CustomGetPage(name: '/sponsorBlock', page: () => const SponsorBlockPage()), CustomGetPage(name: '/createFav', page: () => const CreateFavPage()), + CustomGetPage(name: '/editProfile', page: () => const EditProfilePage()), ]; }