import 'package:dio/dio.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:surface/providers/sn_network.dart'; import 'package:surface/types/account.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/loading_indicator.dart'; final Map kPresetStatus = { 'online': ( const Icon(Symbols.circle, color: Colors.green, fill: 1), 'accountStatusOnline'.tr(), null, ), 'silent': ( const Icon(Symbols.do_not_disturb_on, color: Colors.red), 'accountStatusSilent'.tr(), 'accountStatusSilentDesc'.tr(), ), 'invisible': ( const Icon(Symbols.circle, color: Colors.grey), 'accountStatusInvisible'.tr(), 'accountStatusInvisibleDesc'.tr(), ), }; class AccountStatusActionPopup extends StatefulWidget { final SnAccountStatusInfo? currentStatus; const AccountStatusActionPopup({super.key, this.currentStatus}); @override State createState() => _AccountStatusActionPopupState(); } class _AccountStatusActionPopupState extends State { bool _isBusy = false; Future setStatus( String type, String? label, int attitude, { bool isUpdate = false, bool isSilent = false, bool isInvisible = false, DateTime? clearAt, }) async { setState(() => _isBusy = true); final sn = context.read(); final payload = { 'type': type, 'label': label, 'attitude': attitude, 'is_no_disturb': isSilent, 'is_invisible': isInvisible, 'clear_at': clearAt?.toUtc().toIso8601String() }; try { await sn.client.request( '/cgi/id/users/me/status', data: payload, options: Options(method: isUpdate ? 'PUT' : 'POST'), ); if (!mounted) return; Navigator.pop(context, true); } catch (err) { if (!mounted) return; context.showErrorDialog(err); } finally { setState(() => _isBusy = false); } } Future _clearStatus() async { if (_isBusy) return; setState(() => _isBusy = true); try { final sn = context.read(); await sn.client.delete('/cgi/id/users/me/status'); if (!mounted) return; Navigator.of(context).pop(true); } catch (err) { if (!mounted) return; context.showErrorDialog(err); } finally { setState(() => _isBusy = false); } } @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ const Icon(Symbols.mood, size: 24), const Gap(16), Text('accountChangeStatus', style: Theme.of(context).textTheme.titleLarge) .tr(), ], ).padding(horizontal: 20, top: 16, bottom: 12), LoadingIndicator(isActive: _isBusy), SizedBox( height: 48, child: ListView( padding: EdgeInsets.symmetric(horizontal: 18), scrollDirection: Axis.horizontal, children: kPresetStatus.entries .map( (x) => StyledWidget(ActionChip( avatar: x.value.$1, label: Text(x.value.$2), tooltip: x.value.$3, onPressed: _isBusy ? null : () { setStatus( x.key, x.value.$2, 0, isInvisible: x.key == 'invisible', isSilent: x.key == 'silent', ); }, )).padding(right: 6), ) .toList(), ), ), const Gap(16), const Divider(thickness: 0.3, height: 0.3), ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 24), leading: widget.currentStatus != null ? const Icon(Icons.edit) : const Icon(Icons.add), title: Text('accountCustomStatus').tr(), subtitle: Text('accountCustomStatusDescription').tr(), onTap: _isBusy ? null : () async { final val = await showDialog( context: context, builder: (context) => _AccountStatusEditorDialog( currentStatus: widget.currentStatus, ), ); if (val == true && context.mounted) { Navigator.of(context).pop(true); } }, ), if (widget.currentStatus != null) ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 24), leading: const Icon(Icons.clear), title: Text('accountClearStatus').tr(), subtitle: Text('accountClearStatusDescription').tr(), onTap: _isBusy ? null : () { _clearStatus(); }, ), ], ); } } class _AccountStatusEditorDialog extends StatefulWidget { final SnAccountStatusInfo? currentStatus; const _AccountStatusEditorDialog({this.currentStatus}); @override State<_AccountStatusEditorDialog> createState() => _AccountStatusEditorDialogState(); } class _AccountStatusEditorDialogState extends State<_AccountStatusEditorDialog> { bool _isBusy = false; final TextEditingController _labelController = TextEditingController(); final TextEditingController _clearAtController = TextEditingController(); int _attitude = 0; bool _isSilent = false; bool _isInvisible = false; DateTime? _clearAt; Future _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; if (!mounted) return; final TimeOfDay? pickedTime = await showTimePicker( context: context, initialTime: TimeOfDay.now(), ); if (pickedTime == null) return; if (!mounted) 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 { if (_isBusy) return; setState(() => _isBusy = true); try { final sn = context.read(); await sn.client.request( '/cgi/id/users/me/status', data: { 'type': 'custom', 'label': _labelController.text, 'attitude': _attitude, 'is_no_disturb': _isSilent, 'is_invisible': _isInvisible, 'clear_at': _clearAt?.toUtc().toIso8601String(), }, options: Options( method: widget.currentStatus?.status != null ? 'PUT' : 'POST', ), ); if (!mounted) return; Navigator.of(context).pop(true); } catch (err) { if (!mounted) return; context.showErrorDialog(err); } finally { setState(() => _isBusy = false); } } void _syncWidget() { if (widget.currentStatus?.status != null) { _clearAt = widget.currentStatus!.status!.clearAt; if (_clearAt != null) { _clearAtController.text = DateFormat('y/M/d HH:mm').format(_clearAt!); } _labelController.text = widget.currentStatus!.status!.label; _attitude = widget.currentStatus!.status!.attitude; _isInvisible = widget.currentStatus!.status!.isInvisible; _isSilent = widget.currentStatus!.status!.isNoDisturb; } } @override void initState() { _syncWidget(); super.initState(); } @override Widget build(BuildContext context) { return AlertDialog( title: Text('accountCustomStatus').tr(), content: Column( mainAxisSize: MainAxisSize.min, children: [ LoadingIndicator(isActive: _isBusy), TextField( controller: _labelController, decoration: InputDecoration( isDense: true, prefixIcon: const Icon(Icons.label), border: const OutlineInputBorder(), labelText: 'fieldAccountStatusLabel'.tr(), ), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), ), const Gap(8), TextField( controller: _clearAtController, readOnly: true, decoration: InputDecoration( isDense: true, prefixIcon: const Icon(Icons.event_busy), border: const OutlineInputBorder(), labelText: 'fieldAccountStatusClearAt'.tr(), ), onTap: () => _selectClearAt(), ), const Gap(8), SingleChildScrollView( scrollDirection: Axis.horizontal, child: Wrap( spacing: 6, runSpacing: 0, children: [ ChoiceChip( avatar: Icon( Symbols.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( Symbols.contrast, color: Theme.of(context).colorScheme.onSurfaceVariant, ), selected: _attitude == 0, label: Text('accountStatusNeutral'.tr()), onSelected: (val) { if (val) setState(() => _attitude = 0); }, ), ChoiceChip( avatar: Icon( Symbols.circle, color: Theme.of(context).colorScheme.onSurfaceVariant, ), selected: _attitude == 1, label: Text('accountStatusPositive'.tr()), onSelected: (val) { if (val) setState(() => _attitude = 1); }, ), ], ), ), const Gap(4), SingleChildScrollView( scrollDirection: Axis.horizontal, child: Wrap( spacing: 6, runSpacing: 0, children: [ ChoiceChip( selected: _isSilent, label: Text('accountStatusSilent').tr(), onSelected: (val) { setState(() => _isSilent = 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('dialogCancel').tr(), ), TextButton( onPressed: _isBusy ? null : () => _applyStatus(), child: Text('dialogConfirm').tr(), ), ], ); } }