Files
buzz/mobile/lib/shared/widgets/text_editor.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

170 lines
5.3 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 'package:buzz/shared/widgets/feedback_snackbar.dart';
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import '../theme/theme.dart';
import 'modal_presentation.dart';
import 'ios_text_editor.dart';
/// Opens the shared native iOS text form, with a Flutter sheet fallback.
Future<void> showTextEditor({
required BuildContext context,
required String title,
required String initialValue,
required String hintText,
required Future<void> Function(String value) onSave,
bool multiline = false,
String keyPrefix = 'text-editor',
bool Function(Object error)? shouldRetryOnError,
}) async {
if (defaultTargetPlatform == TargetPlatform.iOS) {
final sheetTheme = utilitySurfaceThemeData(Theme.of(context));
try {
await IosTextEditor.presentUntilSaved(
title: title,
initialValue: initialValue,
placeholder: hintText,
multiline: multiline,
brightness: sheetTheme.brightness,
pageBackgroundColor: sheetTheme.colorScheme.surface,
containerBackgroundColor:
sheetTheme.colorScheme.surfaceContainerHighest,
onSave: onSave,
shouldRetryOnError: shouldRetryOnError,
canPresent: () =>
context.mounted && (ModalRoute.of(context)?.isCurrent ?? true),
onSaveError: () {
if (context.mounted && (ModalRoute.of(context)?.isCurrent ?? true)) {
_showSaveError(context);
}
},
);
return;
} on MissingPluginException {
// Previews and older builds retain the complete Flutter fallback.
} on PlatformException {
// A temporary native presentation failure should not block editing.
}
}
if (!context.mounted) return;
await showBuzzModalBottomSheet<void>(
context: context,
title: title,
isScrollControlled: true,
requestFocus: true,
builder: (_) => _TextEditSheet(
initialValue: initialValue,
hintText: hintText,
multiline: multiline,
onSave: onSave,
shouldRetryOnError: shouldRetryOnError,
keyPrefix: keyPrefix,
),
);
}
void _showSaveError(BuildContext context) {
ScaffoldMessenger.of(context).showFeedbackSnackBar(
const SnackBar(content: Text('Couldn’t save changes')),
kind: ToastKind.error,
);
}
class _TextEditSheet extends HookWidget {
const _TextEditSheet({
required this.initialValue,
required this.hintText,
required this.multiline,
required this.onSave,
this.shouldRetryOnError,
required this.keyPrefix,
});
final String keyPrefix;
final String initialValue;
final String hintText;
final bool multiline;
final Future<void> Function(String value) onSave;
final bool Function(Object error)? shouldRetryOnError;
@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> save() async {
if (!hasChanges || isSaving.value) return;
isSaving.value = true;
error.value = null;
try {
await onSave(controller.text);
if (context.mounted) Navigator.of(context).pop();
} catch (saveError) {
if (!context.mounted) return;
if (shouldRetryOnError?.call(saveError) == false) {
Navigator.of(context).pop();
return;
}
error.value = 'Couldn’t save changes';
} finally {
if (context.mounted) isSaving.value = false;
}
}
return SafeArea(
top: false,
child: SingleChildScrollView(
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: ValueKey('$keyPrefix-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),
Text(
error.value!,
style: context.textTheme.bodySmall?.copyWith(
color: context.colors.error,
),
),
],
const SizedBox(height: Grid.xs),
FilledButton(
key: ValueKey('$keyPrefix-save'),
onPressed: hasChanges && !isSaving.value ? save : null,
child: Text(isSaving.value ? 'Saving…' : 'Save'),
),
],
),
),
);
}
}