250 lines
9.5 KiB
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),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|