diff --git a/lib/providers/account_status.dart b/lib/providers/account_status.dart index bd1ebb3..ed02f10 100644 --- a/lib/providers/account_status.dart +++ b/lib/providers/account_status.dart @@ -60,6 +60,7 @@ class StatusProvider extends GetConnect { final payload = { 'type': type, 'label': label, + 'attitude': attitude, 'is_no_disturb': isSilent, 'is_invisible': isInvisible, 'clear_at': clearAt?.toUtc().toIso8601String() diff --git a/lib/screens/account/personalize.dart b/lib/screens/account/personalize.dart index a1dc6dd..e6b2471 100644 --- a/lib/screens/account/personalize.dart +++ b/lib/screens/account/personalize.dart @@ -44,7 +44,7 @@ class _PersonalizeScreenState extends State { if (picked != null && picked != _birthday) { setState(() { _birthday = picked; - _birthdayController.text = DateFormat('yyyy-MM-dd').format(_birthday!); + _birthdayController.text = DateFormat('y/M/d').format(_birthday!); }); } } diff --git a/lib/translations.dart b/lib/translations.dart index 31dc4b3..27e0cd0 100644 --- a/lib/translations.dart +++ b/lib/translations.dart @@ -226,6 +226,11 @@ class SolianMessages extends Translations { 'accountStatusInvisibleDesc': 'Will show as offline, but all features still remain normal', 'accountStatusOffline': 'Offline', 'accountLastSeenAt': 'Last seen at @date ago', + 'accountStatusLabel': 'Status Text', + 'accountStatusClearAt': 'Clear At', + 'accountStatusNegative': 'Negative', + 'accountStatusNeutral': 'Neutral', + 'accountStatusPositive': 'Positive', }, 'zh_CN': { 'hide': '隐藏', @@ -435,6 +440,11 @@ class SolianMessages extends Translations { 'accountStatusInvisibleDesc': '将会在他人界面显示离线,但不影响功能使用', 'accountStatusOffline': '离线', 'accountLastSeenAt': '最后上线于 @date 前', + 'accountStatusLabel': '状态文字', + 'accountStatusClearAt': '清除状态于', + 'accountStatusNegative': '负面', + 'accountStatusNeutral': '中性', + 'accountStatusPositive': '积极', } }; } diff --git a/lib/widgets/account/account_heading.dart b/lib/widgets/account/account_heading.dart index 69bef5a..ae0aa7b 100644 --- a/lib/widgets/account/account_heading.dart +++ b/lib/widgets/account/account_heading.dart @@ -32,13 +32,13 @@ class AccountHeadingWidget extends StatelessWidget { this.onEditStatus, }); - void showStatusAction(BuildContext context, bool hasStatus) { + void showStatusAction(BuildContext context, Status? current) { if (onEditStatus == null) return; showModalBottomSheet( useRootNavigator: true, context: context, - builder: (context) => AccountStatusAction(hasStatus: hasStatus), + builder: (context) => AccountStatusAction(currentStatus: current), ).then((val) { if (val == true) onEditStatus!(); }); @@ -129,10 +129,7 @@ class AccountHeadingWidget extends StatelessWidget { ], ), onTap: () { - showStatusAction( - context, - snapshot.data!.body['status'] != null, - ); + showStatusAction(context, status.status); }, ); }, diff --git a/lib/widgets/account/account_status_action.dart b/lib/widgets/account/account_status_action.dart index 2d6ff50..cedcbc1 100644 --- a/lib/widgets/account/account_status_action.dart +++ b/lib/widgets/account/account_status_action.dart @@ -1,13 +1,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:get/get.dart'; +import 'package:intl/intl.dart'; import 'package:solian/exts.dart'; +import 'package:solian/models/account_status.dart'; import 'package:solian/providers/account_status.dart'; class AccountStatusAction extends StatefulWidget { - final bool hasStatus; + final Status? currentStatus; - const AccountStatusAction({super.key, this.hasStatus = false}); + const AccountStatusAction({super.key, this.currentStatus}); @override State createState() => _AccountStatusActionState(); @@ -18,7 +20,7 @@ class _AccountStatusActionState extends State { @override Widget build(BuildContext context) { - final StatusProvider controller = Get.find(); + final StatusProvider provider = Get.find(); return SizedBox( child: Column( @@ -48,7 +50,7 @@ class _AccountStatusActionState extends State { : () async { setState(() => _isBusy = true); try { - await controller.setStatus( + await provider.setStatus( x.key, x.value.$2, 0, @@ -70,13 +72,25 @@ class _AccountStatusActionState extends State { .paddingSymmetric(vertical: 16), ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 24), - leading: widget.hasStatus + leading: widget.currentStatus != null ? const Icon(Icons.edit) : const Icon(Icons.add), title: Text('accountCustomStatus'.tr), - onTap: _isBusy ? null : () {}, + onTap: _isBusy + ? null + : () async { + final val = await showDialog( + context: context, + builder: (context) => AccountStatusEditorDialog( + currentStatus: widget.currentStatus, + ), + ); + if (val == true) { + Navigator.of(context).pop(true); + } + }, ), - if (widget.hasStatus) + if (widget.currentStatus != null) ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 24), leading: const Icon(Icons.clear), @@ -86,7 +100,7 @@ class _AccountStatusActionState extends State { : () async { setState(() => _isBusy = true); try { - await controller.clearStatus(); + await provider.clearStatus(); Navigator.of(context).pop(true); } catch (e) { context.showErrorDialog(e); @@ -99,3 +113,211 @@ class _AccountStatusActionState extends State { ); } } + +class AccountStatusEditorDialog extends StatefulWidget { + final Status? currentStatus; + + const AccountStatusEditorDialog({super.key, this.currentStatus}); + + @override + State createState() => + _AccountStatusEditorDialogState(); +} + +class _AccountStatusEditorDialogState extends State { + bool _isBusy = false; + + int _attitude = 0; + DateTime? _clearAt; + bool _isInvisible = false; + bool _isSlient = false; + + final _labelController = TextEditingController(); + final _clearAtController = TextEditingController(); + + void selectClearAt() async { + final DateTime? pickedDate = await showDatePicker( + context: context, + initialDate: _clearAt?.toLocal() ?? DateTime.now(), + firstDate: DateTime.now(), + lastDate: DateTime.now().add(const Duration(days: 365)), + ); + if (pickedDate == null) return; + final TimeOfDay? pickedTime = await showTimePicker( + context: context, + initialTime: TimeOfDay.now(), + ); + if (pickedTime == null) return; + + final picked = pickedDate.copyWith( + hour: pickedTime.hour, + minute: pickedTime.minute, + ); + setState(() { + _clearAt = picked; + _clearAtController.text = DateFormat('y/M/d HH:mm').format(_clearAt!); + }); + } + + Future applyStatus() async { + final StatusProvider provider = Get.find(); + + setState(() => _isBusy = true); + try { + await provider.setStatus( + 'custom', + _labelController.value.text, + _attitude, + clearAt: _clearAt, + isSilent: _isSlient, + isInvisible: _isInvisible, + isUpdate: widget.currentStatus != null, + ); + Navigator.pop(context, true); + } catch (e) { + context.showErrorDialog(e); + } + setState(() => _isBusy = false); + } + + void syncWidget() { + if (widget.currentStatus != null) { + _clearAt = widget.currentStatus!.clearAt; + _clearAtController.text = DateFormat('y/M/d HH:mm').format(_clearAt!); + _labelController.text = widget.currentStatus!.label; + _attitude = widget.currentStatus!.attitude; + _isInvisible = widget.currentStatus!.isInvisible; + _isSlient = widget.currentStatus!.isNoDisturb; + } + } + + @override + void initState() { + syncWidget(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text('accountCustomStatus'.tr), + content: Container( + constraints: const BoxConstraints(minWidth: 400), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (_isBusy) + ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(8)), + child: const LinearProgressIndicator().animate().scaleX(), + ), + const SizedBox(height: 18), + TextField( + controller: _labelController, + decoration: InputDecoration( + isDense: true, + prefixIcon: const Icon(Icons.label), + border: const OutlineInputBorder(), + labelText: 'accountStatusLabel'.tr, + ), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), + ), + const SizedBox(height: 16), + TextField( + controller: _clearAtController, + readOnly: true, + decoration: InputDecoration( + isDense: true, + prefixIcon: const Icon(Icons.event_busy), + border: const OutlineInputBorder(), + labelText: 'accountStatusClearAt'.tr, + ), + onTap: () => selectClearAt(), + ), + const SizedBox(height: 5), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Wrap( + spacing: 6, + runSpacing: 0, + children: [ + ChoiceChip( + avatar: Icon( + Icons.radio_button_unchecked, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + selected: _attitude == 2, + label: Text('accountStatusNegative'.tr), + onSelected: (val) { + if (val) setState(() => _attitude = 2); + }, + ), + ChoiceChip( + avatar: Icon( + Icons.contrast, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + selected: _attitude == 0, + label: Text('accountStatusNeutral'.tr), + onSelected: (val) { + if (val) setState(() => _attitude = 0); + }, + ), + ChoiceChip( + avatar: Icon( + Icons.circle, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + selected: _attitude == 1, + label: Text('accountStatusPositive'.tr), + onSelected: (val) { + if (val) setState(() => _attitude = 1); + }, + ), + ], + ), + ), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Wrap( + spacing: 6, + runSpacing: 0, + children: [ + ChoiceChip( + selected: _isSlient, + label: Text('accountStatusSilent'.tr), + onSelected: (val) { + setState(() => _isSlient = val); + }, + ), + ChoiceChip( + selected: _isInvisible, + label: Text('accountStatusInvisible'.tr), + onSelected: (val) { + setState(() => _isInvisible = val); + }, + ), + ], + ), + ), + ], + ), + ), + actions: [ + TextButton( + style: TextButton.styleFrom( + foregroundColor: + Theme.of(context).colorScheme.onSurface.withOpacity(0.8), + ), + onPressed: _isBusy ? null : () => Navigator.pop(context), + child: Text('cancel'.tr), + ), + TextButton( + onPressed: _isBusy ? null : () => applyStatus(), + child: Text('apply'.tr), + ), + ], + ); + } +}