Surface/lib/widgets/account/status_creation.dart

250 lines
9.5 KiB
Dart

import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/user.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/widgets/account/status.dart';
import 'package:island/widgets/alert.dart';
import 'package:material_symbols_icons/symbols.dart';
class AccountStatusCreationSheet extends HookConsumerWidget {
final SnAccountStatus? initialStatus;
const AccountStatusCreationSheet({super.key, this.initialStatus});
@override
Widget build(BuildContext context, WidgetRef ref) {
final attitude = useState<int>(initialStatus?.attitude ?? 1);
final isInvisible = useState(initialStatus?.isInvisible ?? false);
final isNotDisturb = useState(initialStatus?.isNotDisturb ?? false);
final clearedAt = useState<DateTime?>(initialStatus?.clearedAt);
final labelController = useTextEditingController(
text: initialStatus?.label ?? '',
);
final submitting = useState(false);
Future<void> clearStatus() async {
try {
submitting.value = true;
final user = ref.watch(userInfoProvider);
final apiClient = ref.read(apiClientProvider);
await apiClient.delete('/accounts/me/statuses');
if (!context.mounted) return;
ref.invalidate(accountStatusProvider(user.value!.name));
Navigator.pop(context);
} catch (err) {
showErrorAlert(err);
} finally {
submitting.value = false;
}
}
Future<void> submitStatus() async {
try {
submitting.value = true;
final user = ref.watch(userInfoProvider);
final apiClient = ref.read(apiClientProvider);
await apiClient.request(
'/accounts/me/statuses',
data: {
'attitude': attitude.value,
'is_invisible': isInvisible.value,
'is_not_disturb': isNotDisturb.value,
'cleared_at': clearedAt.value?.toIso8601String(),
if (labelController.text.isNotEmpty) 'label': labelController.text,
},
options: Options(method: initialStatus == null ? 'POST' : 'PATCH'),
);
if (user.hasValue) {
ref.invalidate(accountStatusProvider(user.value!.name));
}
if (!context.mounted) return;
Navigator.pop(context);
} catch (err) {
showErrorAlert(err);
} finally {
submitting.value = false;
}
}
return Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.8,
),
child: Column(
children: [
Padding(
padding: EdgeInsets.only(top: 16, left: 20, right: 16, bottom: 12),
child: Row(
children: [
Text(
initialStatus == null
? 'statusCreate'.tr()
: 'statusUpdate'.tr(),
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w600,
letterSpacing: -0.5,
),
),
const Spacer(),
TextButton.icon(
onPressed:
submitting.value
? null
: () {
submitStatus();
},
icon: const Icon(Symbols.upload),
label: Text(initialStatus == null ? 'create' : 'update').tr(),
style: ButtonStyle(
visualDensity: VisualDensity(
horizontal: VisualDensity.minimumDensity,
),
foregroundColor: WidgetStatePropertyAll(
Theme.of(context).colorScheme.onSurface,
),
),
),
if (initialStatus != null)
IconButton(
icon: const Icon(Symbols.delete),
onPressed: submitting.value ? null : () => clearStatus(),
style: IconButton.styleFrom(
minimumSize: const Size(36, 36),
),
),
IconButton(
icon: const Icon(Symbols.close),
onPressed: () => Navigator.pop(context),
style: IconButton.styleFrom(minimumSize: const Size(36, 36)),
),
],
),
),
const Divider(height: 1),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Gap(24),
TextField(
controller: labelController,
decoration: InputDecoration(
labelText: 'statusLabel'.tr(),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
),
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 24),
Text(
'statusAttitude'.tr(),
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
SegmentedButton(
segments: [
ButtonSegment(
value: 0,
icon: const Icon(Symbols.sentiment_satisfied),
label: Text('attitudePositive'.tr()),
),
ButtonSegment(
value: 1,
icon: const Icon(Symbols.sentiment_stressed),
label: Text('attitudeNeutral'.tr()),
),
ButtonSegment(
value: 2,
icon: const Icon(Symbols.sentiment_sad),
label: Text('attitudeNegative'.tr()),
),
],
selected: {attitude.value},
onSelectionChanged: (Set<int> newSelection) {
attitude.value = newSelection.first;
},
),
const Gap(12),
SwitchListTile(
title: Text('statusInvisible'.tr()),
subtitle: Text('statusInvisibleDescription'.tr()),
value: isInvisible.value,
contentPadding: EdgeInsets.symmetric(horizontal: 8),
onChanged: (bool value) {
isInvisible.value = value;
},
),
SwitchListTile(
title: Text('statusNotDisturb'.tr()),
subtitle: Text('statusNotDisturbDescription'.tr()),
value: isNotDisturb.value,
contentPadding: EdgeInsets.symmetric(horizontal: 8),
onChanged: (bool value) {
isNotDisturb.value = value;
},
),
const SizedBox(height: 24),
Text(
'statusClearTime'.tr(),
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
ListTile(
title: Text(
clearedAt.value == null
? 'statusNoAutoClear'.tr()
: DateFormat.yMMMd().add_jm().format(
clearedAt.value!,
),
),
trailing: const Icon(Symbols.schedule),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(
color: Theme.of(context).colorScheme.outline,
),
),
onTap: () async {
final now = DateTime.now();
final date = await showDatePicker(
context: context,
initialDate: now,
firstDate: now,
lastDate: now.add(const Duration(days: 365)),
);
if (date == null) return;
if (!context.mounted) return;
final time = await showTimePicker(
context: context,
initialTime: TimeOfDay.now(),
);
if (time == null) return;
clearedAt.value = DateTime(
date.year,
date.month,
date.day,
time.hour,
time.minute,
);
},
),
Gap(MediaQuery.of(context).padding.bottom + 24),
],
),
),
),
],
),
);
}
}