mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-23 18:22:39 +03:00
Count UTF8 bytes for byte-limited text fields.
This commit is contained in:
parent
7ee5b82906
commit
662536140a
@ -10,6 +10,7 @@ import 'package:yubico_authenticator/app/logging.dart';
|
|||||||
import '../../app/message.dart';
|
import '../../app/message.dart';
|
||||||
import '../../desktop/models.dart';
|
import '../../desktop/models.dart';
|
||||||
import '../../widgets/responsive_dialog.dart';
|
import '../../widgets/responsive_dialog.dart';
|
||||||
|
import '../../widgets/utf8_text_fields.dart';
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
import '../../fido/models.dart';
|
import '../../fido/models.dart';
|
||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
@ -179,6 +180,8 @@ class _AddFingerprintDialogState extends ConsumerState<AddFingerprintDialog>
|
|||||||
TextFormField(
|
TextFormField(
|
||||||
focusNode: _nameFocus,
|
focusNode: _nameFocus,
|
||||||
maxLength: 15,
|
maxLength: 15,
|
||||||
|
inputFormatters: [limitBytesLength(15)],
|
||||||
|
buildCounter: buildCountersFor(_label),
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
enabled: _fingerprint != null,
|
enabled: _fingerprint != null,
|
||||||
|
@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import '../../app/message.dart';
|
import '../../app/message.dart';
|
||||||
import '../../desktop/models.dart';
|
import '../../desktop/models.dart';
|
||||||
import '../../widgets/responsive_dialog.dart';
|
import '../../widgets/responsive_dialog.dart';
|
||||||
|
import '../../widgets/utf8_text_fields.dart';
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
@ -69,8 +70,9 @@ class _RenameAccountDialogState extends ConsumerState<RenameFingerprintDialog> {
|
|||||||
const Text('This will change the label of the fingerprint.'),
|
const Text('This will change the label of the fingerprint.'),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
initialValue: _label,
|
initialValue: _label,
|
||||||
// TODO: Make this field count UTF-8 bytes instead of characters.
|
|
||||||
maxLength: 15,
|
maxLength: 15,
|
||||||
|
inputFormatters: [limitBytesLength(15)],
|
||||||
|
buildCounter: buildCountersFor(_label),
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
labelText: 'Label',
|
labelText: 'Label',
|
||||||
|
@ -5,14 +5,15 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:yubico_authenticator/app/logging.dart';
|
|
||||||
|
|
||||||
|
import '../../app/logging.dart';
|
||||||
import '../../app/message.dart';
|
import '../../app/message.dart';
|
||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
import '../../app/state.dart';
|
import '../../app/state.dart';
|
||||||
import '../../desktop/models.dart';
|
import '../../desktop/models.dart';
|
||||||
import '../../widgets/file_drop_target.dart';
|
import '../../widgets/file_drop_target.dart';
|
||||||
import '../../widgets/responsive_dialog.dart';
|
import '../../widgets/responsive_dialog.dart';
|
||||||
|
import '../../widgets/utf8_text_fields.dart';
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
import 'utils.dart';
|
import 'utils.dart';
|
||||||
@ -210,6 +211,8 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
autofocus: true,
|
autofocus: true,
|
||||||
enabled: issuerRemaining > 0,
|
enabled: issuerRemaining > 0,
|
||||||
maxLength: max(issuerRemaining, 1),
|
maxLength: max(issuerRemaining, 1),
|
||||||
|
inputFormatters: [limitBytesLength(issuerRemaining)],
|
||||||
|
buildCounter: buildCountersFor(_issuerController.text),
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
labelText: 'Issuer (optional)',
|
labelText: 'Issuer (optional)',
|
||||||
@ -229,6 +232,8 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
key: const Key('name'),
|
key: const Key('name'),
|
||||||
controller: _accountController,
|
controller: _accountController,
|
||||||
maxLength: max(nameRemaining, 1),
|
maxLength: max(nameRemaining, 1),
|
||||||
|
buildCounter: buildCountersFor(_accountController.text),
|
||||||
|
inputFormatters: [limitBytesLength(nameRemaining)],
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
labelText: 'Account name',
|
labelText: 'Account name',
|
||||||
|
@ -7,6 +7,7 @@ import '../../app/message.dart';
|
|||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
import '../../desktop/models.dart';
|
import '../../desktop/models.dart';
|
||||||
import '../../widgets/responsive_dialog.dart';
|
import '../../widgets/responsive_dialog.dart';
|
||||||
|
import '../../widgets/utf8_text_fields.dart';
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
import 'utils.dart';
|
import 'utils.dart';
|
||||||
@ -97,6 +98,8 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
|
|||||||
initialValue: _issuer,
|
initialValue: _issuer,
|
||||||
enabled: issuerRemaining > 0,
|
enabled: issuerRemaining > 0,
|
||||||
maxLength: issuerRemaining > 0 ? issuerRemaining : null,
|
maxLength: issuerRemaining > 0 ? issuerRemaining : null,
|
||||||
|
buildCounter: buildCountersFor(_issuer),
|
||||||
|
inputFormatters: [limitBytesLength(issuerRemaining)],
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
labelText: 'Issuer (optional)',
|
labelText: 'Issuer (optional)',
|
||||||
@ -112,6 +115,8 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
|
|||||||
TextFormField(
|
TextFormField(
|
||||||
initialValue: _account,
|
initialValue: _account,
|
||||||
maxLength: nameRemaining,
|
maxLength: nameRemaining,
|
||||||
|
inputFormatters: [limitBytesLength(nameRemaining)],
|
||||||
|
buildCounter: buildCountersFor(_account),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: 'Account name',
|
labelText: 'Account name',
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:convert';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
@ -18,7 +19,7 @@ Pair<int, int> getRemainingKeySpace(
|
|||||||
// Non-standard TOTP periods are stored as part of this data, as a "D/"- prefix.
|
// Non-standard TOTP periods are stored as part of this data, as a "D/"- prefix.
|
||||||
remaining -= '$period/'.length;
|
remaining -= '$period/'.length;
|
||||||
}
|
}
|
||||||
int issuerSpace = issuer.length;
|
int issuerSpace = utf8.encode(issuer).length;
|
||||||
if (issuer.isNotEmpty) {
|
if (issuer.isNotEmpty) {
|
||||||
// Issuer is separated from name with a ":", if present.
|
// Issuer is separated from name with a ":", if present.
|
||||||
issuerSpace += 1;
|
issuerSpace += 1;
|
||||||
@ -26,7 +27,7 @@ Pair<int, int> getRemainingKeySpace(
|
|||||||
|
|
||||||
return Pair(
|
return Pair(
|
||||||
// Always reserve at least one character for name
|
// Always reserve at least one character for name
|
||||||
remaining - 1 - max(name.length, 1),
|
remaining - 1 - max(utf8.encode(name).length, 1),
|
||||||
remaining - issuerSpace,
|
remaining - issuerSpace,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
21
lib/widgets/utf8_text_fields.dart
Executable file
21
lib/widgets/utf8_text_fields.dart
Executable file
@ -0,0 +1,21 @@
|
|||||||
|
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;
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user