Files
buzz/mobile/lib/features/profile/profile_text_edit_sheet.dart
klopez4212 cfcaef4e9f Update mobile toast animations (#8855)
Updates mobile toast animations and styling.



| Sign in | Retry | Copied |
| --- | --- | --- |
| ![Reconnect to
Builderlab](https://github.com/user-attachments/assets/5298b507-df5b-49da-a021-03f337e997fa)
| ![Save error with
Retry](https://github.com/user-attachments/assets/492c8929-4f91-4342-943b-a72b96547992)
| ![Copied
confirmation](https://github.com/user-attachments/assets/864fd629-7414-415e-81ef-5f7d3d5333a4)
|

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-10-31 07:50:00 -07:00

166 lines
5.7 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:buzz/shared/theme/buzz_icons.dart';
import '../../shared/theme/theme.dart';
import '../../shared/widgets/buzz_sheet_header.dart';
import '../../shared/widgets/ios_glass_navigation_button.dart';
import 'profile_provider.dart';
/// Flutter fallback sheet for editing one profile text field.
class ProfileTextEditSheet extends HookWidget {
/// Creates a profile text editor.
const ProfileTextEditSheet({
super.key,
required this.title,
required this.initialValue,
required this.hintText,
required this.multiline,
required this.onSave,
});
/// The heading displayed in the sheet header.
final String title;
/// The value loaded into the text field when the sheet opens.
final String initialValue;
/// Placeholder text displayed while the text field is empty.
final String hintText;
/// Whether the text field accepts and displays multiple lines.
final bool multiline;
/// Persists the submitted value before the sheet closes.
final Future<void> Function(String value) onSave;
@override
Widget build(BuildContext context) {
final controller = useTextEditingController(text: initialValue);
useListenable(controller);
final isSaving = useState(false);
final error = useState<String?>(null);
final hasChanges = controller.text.trim() != initialValue.trim();
Future<void> closeAfterSave() async {
isSaving.value = false;
await WidgetsBinding.instance.endOfFrame;
if (context.mounted) Navigator.of(context).pop();
}
Future<void> save() async {
if (!hasChanges || isSaving.value) return;
isSaving.value = true;
error.value = null;
try {
await onSave(controller.text);
if (context.mounted) await closeAfterSave();
} on ProfileCommunityChangedException {
if (context.mounted) await closeAfterSave();
} catch (_) {
if (context.mounted) {
error.value = 'Couldn’t save changes';
}
} finally {
if (context.mounted) isSaving.value = false;
}
}
final closeButton = Theme.of(context).platform == TargetPlatform.iOS
? IosGlassNavigationButton(
key: const ValueKey('profile-field-close'),
icon: IosGlassNavigationIcon.close,
semanticLabel: 'Close sheet',
onPressed: isSaving.value
? null
: () => Navigator.of(context).pop(),
width: 44,
height: 44,
foregroundColor: context.colors.primary,
)
: SizedBox.square(
dimension: 44,
child: IconButton(
key: const ValueKey('profile-field-close'),
tooltip: 'Close sheet',
onPressed: isSaving.value
? null
: () => Navigator.of(context).pop(),
style: IconButton.styleFrom(
padding: EdgeInsets.zero,
backgroundColor: context.colors.surfaceContainerHighest,
foregroundColor: context.colors.primary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(Radii.dialog),
),
),
icon: const Icon(BuzzIcons.x, size: 24),
),
);
return PopScope<void>(
canPop: !isSaving.value,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
BuzzSheetHeader(title: title, trailing: closeButton),
Flexible(
child: SafeArea(
top: false,
child: SingleChildScrollView(
key: const ValueKey('profile-field-scroll-view'),
padding: EdgeInsets.fromLTRB(
Grid.gutter,
Grid.xxs,
Grid.gutter,
MediaQuery.viewInsetsOf(context).bottom + Grid.xs,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
key: const ValueKey('profile-field-input'),
controller: controller,
autofocus: true,
enabled: !isSaving.value,
minLines: multiline ? 4 : 1,
maxLines: multiline ? 6 : 1,
textCapitalization: TextCapitalization.sentences,
textInputAction: multiline
? TextInputAction.newline
: TextInputAction.done,
onSubmitted: multiline ? null : (_) => unawaited(save()),
decoration: InputDecoration(hintText: hintText),
),
if (error.value != null) ...[
const SizedBox(height: Grid.xxs),
Semantics(
liveRegion: true,
child: Text(
error.value!,
style: context.textTheme.bodySmall?.copyWith(
color: context.colors.error,
),
),
),
],
const SizedBox(height: Grid.xs),
FilledButton(
key: const ValueKey('profile-field-save'),
onPressed: hasChanges && !isSaving.value ? save : null,
child: Text(isSaving.value ? 'Saving…' : 'Save'),
),
],
),
),
),
),
],
),
);
}
}