Files
Qlyra/lib/frontend/screens/chats/profile_action_sheets.dart
T

399 lines
12 KiB
Dart

import 'package:flutter/material.dart';
import '../contacts/contact_sheet_common.dart';
import '../../../core/config/app_fonts.dart';
class ConfirmChoice {
final bool confirmed;
final bool checked;
const ConfirmChoice({required this.confirmed, required this.checked});
static const cancelled = ConfirmChoice(confirmed: false, checked: false);
}
Future<ConfirmChoice> showBlurredConfirm(
BuildContext context, {
required String title,
required String message,
required String confirmLabel,
required String cancelLabel,
bool destructive = false,
String? checkboxLabel,
bool checkboxInitial = false,
}) async {
final result = await showBlurredCard<ConfirmChoice>(
context,
(_) => _ConfirmCard(
title: title,
message: message,
confirmLabel: confirmLabel,
cancelLabel: cancelLabel,
destructive: destructive,
checkboxLabel: checkboxLabel,
checkboxInitial: checkboxInitial,
),
);
return result ?? ConfirmChoice.cancelled;
}
Future<void> showComplaintCard(
BuildContext context, {
required String title,
required String subtitle,
required String sendLabel,
required String closeLabel,
required String emptyLabel,
required Future<List<({int id, String title})>> Function() loadReasons,
required Future<bool> Function(int reasonId) onSend,
}) {
return showBlurredCard<void>(
context,
(_) => _ComplaintCard(
title: title,
subtitle: subtitle,
sendLabel: sendLabel,
closeLabel: closeLabel,
emptyLabel: emptyLabel,
loadReasons: loadReasons,
onSend: onSend,
),
);
}
class _CardShell extends StatelessWidget {
final Widget child;
const _CardShell({required this.child});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final width = MediaQuery.sizeOf(context).width;
return Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Material(
color: Colors.transparent,
child: Container(
width: width > 420 ? 380 : double.infinity,
decoration: BoxDecoration(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(22),
),
clipBehavior: Clip.antiAlias,
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 18, 20, 16),
child: child,
),
),
),
),
);
}
}
class _ConfirmCard extends StatefulWidget {
final String title;
final String message;
final String confirmLabel;
final String cancelLabel;
final bool destructive;
final String? checkboxLabel;
final bool checkboxInitial;
const _ConfirmCard({
required this.title,
required this.message,
required this.confirmLabel,
required this.cancelLabel,
required this.destructive,
required this.checkboxLabel,
required this.checkboxInitial,
});
@override
State<_ConfirmCard> createState() => _ConfirmCardState();
}
class _ConfirmCardState extends State<_ConfirmCard> {
late bool _checked = widget.checkboxInitial;
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final checkboxLabel = widget.checkboxLabel;
return _CardShell(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.title,
style: TextStyle(
color: cs.onSurface,
fontSize: 18,
fontWeight: FontWeight.w700,
fontFamily: displayFontOf(context),
),
),
const SizedBox(height: 8),
Text(
widget.message,
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 14,
height: 1.35,
),
),
if (checkboxLabel != null) ...[
const SizedBox(height: 12),
InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () => setState(() => _checked = !_checked),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
Checkbox(
value: _checked,
visualDensity: VisualDensity.compact,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
onChanged: (v) => setState(() => _checked = v ?? false),
),
const SizedBox(width: 8),
Expanded(
child: Text(
checkboxLabel,
style: TextStyle(color: cs.onSurface, fontSize: 15),
),
),
],
),
),
),
],
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () =>
Navigator.of(context).pop(ConfirmChoice.cancelled),
child: Text(
widget.cancelLabel,
style: TextStyle(color: cs.onSurfaceVariant),
),
),
const SizedBox(width: 8),
FilledButton.tonal(
style: widget.destructive
? FilledButton.styleFrom(
backgroundColor: cs.errorContainer,
foregroundColor: cs.onErrorContainer,
)
: null,
onPressed: () => Navigator.of(
context,
).pop(ConfirmChoice(confirmed: true, checked: _checked)),
child: Text(widget.confirmLabel),
),
],
),
],
),
);
}
}
class _ComplaintCard extends StatefulWidget {
final String title;
final String subtitle;
final String sendLabel;
final String closeLabel;
final String emptyLabel;
final Future<List<({int id, String title})>> Function() loadReasons;
final Future<bool> Function(int reasonId) onSend;
const _ComplaintCard({
required this.title,
required this.subtitle,
required this.sendLabel,
required this.closeLabel,
required this.emptyLabel,
required this.loadReasons,
required this.onSend,
});
@override
State<_ComplaintCard> createState() => _ComplaintCardState();
}
class _ComplaintCardState extends State<_ComplaintCard> {
List<({int id, String title})>? _reasons;
int? _selected;
bool _sending = false;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
List<({int id, String title})> loaded;
try {
loaded = await widget.loadReasons();
} catch (_) {
loaded = const [];
}
if (!mounted) return;
setState(() => _reasons = loaded);
}
Future<void> _send() async {
final reasonId = _selected;
if (reasonId == null || _sending) return;
setState(() => _sending = true);
final ok = await widget.onSend(reasonId);
if (!mounted) return;
if (ok) {
Navigator.of(context).pop();
} else {
setState(() => _sending = false);
}
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final reasons = _reasons;
return _CardShell(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.title,
style: TextStyle(
color: cs.onSurface,
fontSize: 18,
fontWeight: FontWeight.w700,
fontFamily: displayFontOf(context),
),
),
const SizedBox(height: 6),
Text(
widget.subtitle,
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 14),
),
const SizedBox(height: 12),
if (reasons == null)
const Padding(
padding: EdgeInsets.symmetric(vertical: 28),
child: Center(
child: SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
)
else if (reasons.isEmpty)
Padding(
padding: const EdgeInsets.symmetric(vertical: 22),
child: Center(
child: Text(
widget.emptyLabel,
textAlign: TextAlign.center,
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 14),
),
),
)
else
ConstrainedBox(
constraints: BoxConstraints(
maxHeight: MediaQuery.sizeOf(context).height * 0.42,
),
child: SingleChildScrollView(
child: RadioGroup<int>(
groupValue: _selected,
onChanged: (v) {
if (_sending) return;
setState(() => _selected = v);
},
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
for (final reason in reasons)
InkWell(
borderRadius: BorderRadius.circular(12),
onTap: _sending
? null
: () => setState(() => _selected = reason.id),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
children: [
Radio<int>(
value: reason.id,
visualDensity: VisualDensity.compact,
materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap,
),
const SizedBox(width: 8),
Expanded(
child: Text(
reason.title,
style: TextStyle(
color: cs.onSurface,
fontSize: 15,
),
),
),
],
),
),
),
],
),
),
),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: _sending ? null : () => Navigator.of(context).pop(),
child: Text(
widget.closeLabel,
style: TextStyle(color: cs.onSurfaceVariant),
),
),
const SizedBox(width: 8),
FilledButton.tonal(
style: FilledButton.styleFrom(
backgroundColor: cs.errorContainer,
foregroundColor: cs.onErrorContainer,
),
onPressed: _selected == null || _sending ? null : _send,
child: _sending
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Text(widget.sendLabel),
),
],
),
],
),
);
}
}