diff --git a/lib/fido/views/add_fingerprint_dialog.dart b/lib/fido/views/add_fingerprint_dialog.dart index d0618490..ae25a783 100755 --- a/lib/fido/views/add_fingerprint_dialog.dart +++ b/lib/fido/views/add_fingerprint_dialog.dart @@ -10,7 +10,7 @@ import 'package:yubico_authenticator/app/logging.dart'; import '../../app/message.dart'; import '../../desktop/models.dart'; import '../../widgets/responsive_dialog.dart'; -import '../../widgets/utf8_text_fields.dart'; +import '../../widgets/utf8_utils.dart'; import '../state.dart'; import '../../fido/models.dart'; import '../../app/models.dart'; @@ -181,7 +181,7 @@ class _AddFingerprintDialogState extends ConsumerState focusNode: _nameFocus, maxLength: 15, inputFormatters: [limitBytesLength(15)], - buildCounter: buildCountersFor(_label), + buildCounter: buildByteCounterFor(_label), autofocus: true, decoration: InputDecoration( enabled: _fingerprint != null, diff --git a/lib/fido/views/rename_fingerprint_dialog.dart b/lib/fido/views/rename_fingerprint_dialog.dart index a2f5f57b..200482f8 100755 --- a/lib/fido/views/rename_fingerprint_dialog.dart +++ b/lib/fido/views/rename_fingerprint_dialog.dart @@ -4,7 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../app/message.dart'; import '../../desktop/models.dart'; import '../../widgets/responsive_dialog.dart'; -import '../../widgets/utf8_text_fields.dart'; +import '../../widgets/utf8_utils.dart'; import '../models.dart'; import '../state.dart'; import '../../app/models.dart'; @@ -72,7 +72,7 @@ class _RenameAccountDialogState extends ConsumerState { initialValue: _label, maxLength: 15, inputFormatters: [limitBytesLength(15)], - buildCounter: buildCountersFor(_label), + buildCounter: buildByteCounterFor(_label), decoration: const InputDecoration( border: OutlineInputBorder(), labelText: 'Label', diff --git a/lib/oath/views/add_account_page.dart b/lib/oath/views/add_account_page.dart index 101deb47..e5ff81e0 100755 --- a/lib/oath/views/add_account_page.dart +++ b/lib/oath/views/add_account_page.dart @@ -13,7 +13,7 @@ import '../../app/state.dart'; import '../../desktop/models.dart'; import '../../widgets/file_drop_target.dart'; import '../../widgets/responsive_dialog.dart'; -import '../../widgets/utf8_text_fields.dart'; +import '../../widgets/utf8_utils.dart'; import '../models.dart'; import '../state.dart'; import 'utils.dart'; @@ -212,7 +212,7 @@ class _OathAddAccountPageState extends ConsumerState { enabled: issuerRemaining > 0, maxLength: max(issuerRemaining, 1), inputFormatters: [limitBytesLength(issuerRemaining)], - buildCounter: buildCountersFor(_issuerController.text), + buildCounter: buildByteCounterFor(_issuerController.text), decoration: const InputDecoration( border: OutlineInputBorder(), labelText: 'Issuer (optional)', @@ -232,7 +232,7 @@ class _OathAddAccountPageState extends ConsumerState { key: const Key('name'), controller: _accountController, maxLength: max(nameRemaining, 1), - buildCounter: buildCountersFor(_accountController.text), + buildCounter: buildByteCounterFor(_accountController.text), inputFormatters: [limitBytesLength(nameRemaining)], decoration: const InputDecoration( border: OutlineInputBorder(), diff --git a/lib/oath/views/rename_account_dialog.dart b/lib/oath/views/rename_account_dialog.dart index 21303dae..9053d0a0 100755 --- a/lib/oath/views/rename_account_dialog.dart +++ b/lib/oath/views/rename_account_dialog.dart @@ -7,7 +7,7 @@ import '../../app/message.dart'; import '../../app/models.dart'; import '../../desktop/models.dart'; import '../../widgets/responsive_dialog.dart'; -import '../../widgets/utf8_text_fields.dart'; +import '../../widgets/utf8_utils.dart'; import '../models.dart'; import '../state.dart'; import 'utils.dart'; @@ -98,7 +98,7 @@ class _RenameAccountDialogState extends ConsumerState { initialValue: _issuer, enabled: issuerRemaining > 0, maxLength: issuerRemaining > 0 ? issuerRemaining : null, - buildCounter: buildCountersFor(_issuer), + buildCounter: buildByteCounterFor(_issuer), inputFormatters: [limitBytesLength(issuerRemaining)], decoration: const InputDecoration( border: OutlineInputBorder(), @@ -116,7 +116,7 @@ class _RenameAccountDialogState extends ConsumerState { initialValue: _account, maxLength: nameRemaining, inputFormatters: [limitBytesLength(nameRemaining)], - buildCounter: buildCountersFor(_account), + buildCounter: buildByteCounterFor(_account), decoration: InputDecoration( border: const OutlineInputBorder(), labelText: 'Account name', diff --git a/lib/oath/views/utils.dart b/lib/oath/views/utils.dart index 62634c4d..e4c4a32c 100755 --- a/lib/oath/views/utils.dart +++ b/lib/oath/views/utils.dart @@ -1,6 +1,6 @@ -import 'dart:convert'; import 'dart:math'; +import '../../widgets/utf8_utils.dart'; import '../models.dart'; import '../../core/models.dart'; @@ -19,7 +19,7 @@ Pair getRemainingKeySpace( // Non-standard TOTP periods are stored as part of this data, as a "D/"- prefix. remaining -= '$period/'.length; } - int issuerSpace = utf8.encode(issuer).length; + int issuerSpace = byteLength(issuer); if (issuer.isNotEmpty) { // Issuer is separated from name with a ":", if present. issuerSpace += 1; @@ -27,7 +27,7 @@ Pair getRemainingKeySpace( return Pair( // Always reserve at least one character for name - remaining - 1 - max(utf8.encode(name).length, 1), + remaining - 1 - max(byteLength(name), 1), remaining - issuerSpace, ); } diff --git a/lib/widgets/utf8_text_fields.dart b/lib/widgets/utf8_text_fields.dart deleted file mode 100755 index d6ccdb06..00000000 --- a/lib/widgets/utf8_text_fields.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -int _byteLength(String value) => utf8.encode(value).length; - -InputCounterWidgetBuilder buildCountersFor(String currentValue) => - (context, {required currentLength, required isFocused, maxLength}) => Text( - maxLength != null ? '${_byteLength(currentValue)}/$maxLength' : '', - style: Theme.of(context).textTheme.caption, - ); - -TextInputFormatter limitBytesLength(int maxByteLength) => - TextInputFormatter.withFunction((oldValue, newValue) { - final newLength = _byteLength(newValue.text); - if (newLength <= maxByteLength) { - return newValue; - } - return oldValue; - }); diff --git a/lib/widgets/utf8_utils.dart b/lib/widgets/utf8_utils.dart new file mode 100755 index 00000000..336b34ef --- /dev/null +++ b/lib/widgets/utf8_utils.dart @@ -0,0 +1,29 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +/// Get the number of bytes used by a String when encoded to UTF-8. +int byteLength(String value) => utf8.encode(value).length; + +/// Builds a counter widget showing number of bytes used/available. +/// +/// Set this as a [TextField.buildCounter] callback to show the number of bytes +/// used rather than number of characters. [currentValue] should always match +/// the input text value to measure. +InputCounterWidgetBuilder buildByteCounterFor(String currentValue) => + (context, {required currentLength, required isFocused, maxLength}) => Text( + maxLength != null ? '${byteLength(currentValue)}/$maxLength' : '', + style: Theme.of(context).textTheme.caption, + ); + +/// Limits the input in length based on the byte length when encoded. +/// This is generally used together with [buildByteCounterFor]. +TextInputFormatter limitBytesLength(int maxByteLength) => + TextInputFormatter.withFunction((oldValue, newValue) { + final newLength = byteLength(newValue.text); + if (newLength <= maxByteLength) { + return newValue; + } + return oldValue; + });