Files
sm-emby-share/lib/features/account/widgets/otp_code_input.dart
T

190 lines
5.1 KiB
Dart
Raw Normal View History

2026-07-14 11:11:36 +08:00
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class OtpCodeInput extends StatefulWidget {
final int length;
final ValueChanged<String> onChanged;
final ValueChanged<String>? onCompleted;
final bool enabled;
final bool autofocus;
final Color foreground;
final Color accent;
const OtpCodeInput({
super.key,
this.length = 6,
required this.onChanged,
this.onCompleted,
this.enabled = true,
this.autofocus = true,
this.foreground = Colors.white,
this.accent = const Color(0xFF1677FF),
});
@override
State<OtpCodeInput> createState() => _OtpCodeInputState();
}
class _OtpCodeInputState extends State<OtpCodeInput> {
late final List<TextEditingController> _controllers;
late final List<FocusNode> _nodes;
@override
void initState() {
super.initState();
_controllers = List.generate(widget.length, (_) => TextEditingController());
_nodes = List.generate(widget.length, (_) {
final node = FocusNode();
node.addListener(() {
if (node.hasFocus) {
final i = _nodes.indexOf(node);
final c = _controllers[i];
c.selection = TextSelection(
baseOffset: 0,
extentOffset: c.text.length,
);
}
if (mounted) setState(() {});
});
return node;
});
}
@override
void dispose() {
for (final c in _controllers) {
c.dispose();
}
for (final n in _nodes) {
n.dispose();
}
super.dispose();
}
String get _value => _controllers.map((c) => c.text).join();
void _emit() {
final v = _value;
widget.onChanged(v);
if (v.length == widget.length) widget.onCompleted?.call(v);
}
void _spread(int startIndex, String digits) {
var idx = startIndex;
for (final ch in digits.split('')) {
if (idx >= widget.length) break;
_controllers[idx].text = ch;
idx++;
}
final focusIndex = idx >= widget.length ? widget.length - 1 : idx;
_nodes[focusIndex].requestFocus();
_emit();
}
void _onChanged(int index, String raw) {
final digits = raw.replaceAll(RegExp(r'\D'), '');
if (digits.isEmpty) {
_controllers[index].text = '';
_emit();
return;
}
if (digits.length == 1) {
final c = _controllers[index];
c.text = digits;
c.selection = TextSelection.collapsed(offset: c.text.length);
if (index < widget.length - 1) _nodes[index + 1].requestFocus();
_emit();
} else {
_spread(index, digits);
}
}
KeyEventResult _onKey(int index, KeyEvent event) {
if (event is KeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.backspace &&
_controllers[index].text.isEmpty &&
index > 0) {
_controllers[index - 1].text = '';
_nodes[index - 1].requestFocus();
_emit();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(widget.length, (i) {
final focused = _nodes[i].hasFocus;
final filled = _controllers[i].text.isNotEmpty;
final borderColor = focused
? widget.accent
: widget.foreground.withValues(alpha: filled ? 0.45 : 0.18);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 5),
child: SizedBox(
width: 46,
height: 56,
child: Focus(
canRequestFocus: false,
skipTraversal: true,
onKeyEvent: (_, event) => _onKey(i, event),
child: TextField(
controller: _controllers[i],
focusNode: _nodes[i],
enabled: widget.enabled,
autofocus: widget.autofocus && i == 0,
textAlign: TextAlign.center,
keyboardType: TextInputType.number,
cursorColor: widget.accent,
showCursor: true,
style: TextStyle(
color: widget.foreground,
fontSize: 22,
fontWeight: FontWeight.w600,
),
decoration: InputDecoration(
counterText: '',
filled: true,
fillColor: widget.foreground.withValues(alpha: 0.06),
contentPadding: EdgeInsets.zero,
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: borderColor, width: 1.2),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: widget.accent, width: 1.6),
),
disabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: borderColor, width: 1.2),
),
),
onChanged: (v) => _onChanged(i, v),
),
),
),
);
}),
);
}
}