mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-04-20 22:13:48 +00:00
adds a cusomizable widget that counts bytes and limits to max uses new widget in chat and channel message inputs remove prefix icon felt cluttery
125 lines
4.2 KiB
Dart
125 lines
4.2 KiB
Dart
import 'dart:convert';
|
||
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter/services.dart';
|
||
|
||
import '../helpers/utf8_length_limiter.dart';
|
||
|
||
/// A [TextField] that displays a live UTF-8 byte counter.
|
||
///
|
||
/// The counter appears below the field once the user starts typing and changes
|
||
/// colour as the limit is approached (orange at 70 %, error-red at 90 %).
|
||
///
|
||
/// All standard [TextField] behaviour (focus nodes, input actions, decoration
|
||
/// overrides, etc.) is forwarded so the widget can be dropped into any screen.
|
||
class ByteCountedTextField extends StatelessWidget {
|
||
/// Maximum number of UTF-8 bytes allowed.
|
||
final int maxBytes;
|
||
|
||
/// Controller for the text field.
|
||
final TextEditingController controller;
|
||
|
||
/// Optional focus node forwarded to the inner [TextField].
|
||
final FocusNode? focusNode;
|
||
|
||
/// Hint text shown when the field is empty.
|
||
final String? hintText;
|
||
|
||
/// Keyboard action button (defaults to [TextInputAction.send]).
|
||
final TextInputAction textInputAction;
|
||
|
||
/// Called when the user submits via the keyboard action button.
|
||
final ValueChanged<String>? onSubmitted;
|
||
|
||
/// Additional [TextInputFormatter]s applied *before* the byte limiter.
|
||
final List<TextInputFormatter> extraFormatters;
|
||
|
||
/// Text capitalisation forwarded to the inner [TextField].
|
||
final TextCapitalization textCapitalization;
|
||
|
||
/// Optional full [InputDecoration] override. When provided, [hintText] is
|
||
/// ignored – set it inside the decoration instead.
|
||
final InputDecoration? decoration;
|
||
|
||
/// Ratio (0–1) at which the counter turns the warning colour (default 0.7).
|
||
final double warningThreshold;
|
||
|
||
/// Ratio (0–1) at which the counter turns the error colour (default 0.9).
|
||
final double errorThreshold;
|
||
|
||
/// Whether to hide the counter when the field is empty (default `true`).
|
||
final bool hideCounterWhenEmpty;
|
||
|
||
const ByteCountedTextField({
|
||
super.key,
|
||
required this.maxBytes,
|
||
required this.controller,
|
||
this.focusNode,
|
||
this.hintText,
|
||
this.textInputAction = TextInputAction.send,
|
||
this.onSubmitted,
|
||
this.extraFormatters = const [],
|
||
this.textCapitalization = TextCapitalization.sentences,
|
||
this.decoration,
|
||
this.warningThreshold = 0.7,
|
||
this.errorThreshold = 0.9,
|
||
this.hideCounterWhenEmpty = true,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return ValueListenableBuilder<TextEditingValue>(
|
||
valueListenable: controller,
|
||
builder: (context, value, _) {
|
||
final usedBytes = utf8.encode(value.text).length;
|
||
final ratio = maxBytes > 0 ? usedBytes / maxBytes : 0.0;
|
||
final showCounter = !(hideCounterWhenEmpty && value.text.isEmpty);
|
||
|
||
final counterColor = ratio > errorThreshold
|
||
? Theme.of(context).colorScheme.error
|
||
: ratio > warningThreshold
|
||
? Colors.orange
|
||
: Theme.of(context).colorScheme.onSurfaceVariant;
|
||
|
||
return Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: [
|
||
TextField(
|
||
controller: controller,
|
||
focusNode: focusNode,
|
||
inputFormatters: [
|
||
...extraFormatters,
|
||
Utf8LengthLimitingTextInputFormatter(maxBytes),
|
||
],
|
||
textCapitalization: textCapitalization,
|
||
decoration:
|
||
decoration ??
|
||
InputDecoration(
|
||
hintText: hintText,
|
||
border: const OutlineInputBorder(),
|
||
contentPadding: const EdgeInsets.symmetric(
|
||
horizontal: 16,
|
||
vertical: 12,
|
||
),
|
||
),
|
||
textInputAction: textInputAction,
|
||
onSubmitted: onSubmitted,
|
||
),
|
||
if (showCounter)
|
||
Padding(
|
||
padding: const EdgeInsets.only(top: 4, right: 4),
|
||
child: Align(
|
||
alignment: Alignment.centerRight,
|
||
child: Text(
|
||
'$usedBytes / $maxBytes',
|
||
style: TextStyle(fontSize: 11, color: counterColor),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
},
|
||
);
|
||
}
|
||
}
|