import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../l10n/generated/app_localizations.dart'; import '../src/rust/api.dart' as rust; /// Server connection form. class ConnectForm extends StatefulWidget { /// Construct a connect form. const ConnectForm({ super.key, required this.hostCtl, required this.nickCtl, required this.passwordCtl, required this.onConnect, required this.onAddBookmark, }); /// Server host controller. final TextEditingController hostCtl; /// Nickname controller. final TextEditingController nickCtl; /// Server password controller. final TextEditingController passwordCtl; /// Called when the user submits a connection. final VoidCallback onConnect; /// Called when the user saves the current form as a bookmark. final VoidCallback onAddBookmark; @override State createState() => _ConnectFormState(); } class _ConnectFormState extends State { final FocusNode _hostFocus = FocusNode(); final FocusNode _nickFocus = FocusNode(); final FocusNode _passwordFocus = FocusNode(); void _onTapOutside(PointerDownEvent _) { FocusManager.instance.primaryFocus?.unfocus(); } @override void dispose() { _hostFocus.dispose(); _nickFocus.dispose(); _passwordFocus.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final l10n = AppL10n.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ TextField( controller: widget.hostCtl, focusNode: _hostFocus, onTapOutside: _onTapOutside, keyboardType: TextInputType.url, textCapitalization: TextCapitalization.none, textInputAction: TextInputAction.next, autocorrect: false, enableSuggestions: false, inputFormatters: [ FilteringTextInputFormatter.deny(RegExp(r'\s')), TextInputFormatter.withFunction( (oldValue, newValue) => newValue.copyWith( text: newValue.text.toLowerCase(), selection: newValue.selection, ), ), ], decoration: InputDecoration( labelText: l10n.fieldServerHost, hintText: 'host[:port]', prefixIcon: const Icon(Icons.dns_outlined), border: const OutlineInputBorder(), ), ), const SizedBox(height: 8), TextField( controller: widget.nickCtl, focusNode: _nickFocus, onTapOutside: _onTapOutside, textInputAction: TextInputAction.next, autocorrect: false, enableSuggestions: false, decoration: InputDecoration( labelText: l10n.fieldNickname, border: const OutlineInputBorder(), ), ), const SizedBox(height: 8), TextField( controller: widget.passwordCtl, focusNode: _passwordFocus, onTapOutside: _onTapOutside, obscureText: true, textInputAction: TextInputAction.done, autocorrect: false, enableSuggestions: false, decoration: InputDecoration( labelText: l10n.fieldServerPassword, helperText: l10n.fieldServerPasswordHelp, border: const OutlineInputBorder(), ), ), const SizedBox(height: 16), LayoutBuilder( builder: (context, constraints) { const stackedActionsMaxWidth = 400.0; final connectButton = FilledButton.icon( icon: const Icon(Icons.login), label: Text(l10n.connectAction), onPressed: widget.onConnect, ); final bookmarkButton = OutlinedButton.icon( icon: const Icon(Icons.bookmark_add_outlined), label: Text(l10n.bookmarkAddAction), onPressed: widget.onAddBookmark, ); if (constraints.maxWidth <= stackedActionsMaxWidth) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ connectButton, const SizedBox(height: 8), bookmarkButton, ], ); } return Row( children: [ Expanded(child: connectButton), const SizedBox(width: 8), Flexible(child: bookmarkButton), ], ); }, ), ], ); } } /// Saved bookmark list. class BookmarkList extends StatelessWidget { /// Construct a bookmark list. const BookmarkList({ super.key, required this.bookmarks, required this.onConnect, required this.onDelete, }); /// Saved bookmarks. final List bookmarks; /// Connect to a bookmark. final ValueChanged onConnect; /// Delete a bookmark. final ValueChanged onDelete; @override Widget build(BuildContext context) { final l10n = AppL10n.of(context); final theme = Theme.of(context); if (bookmarks.isEmpty) { return Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Text(l10n.bookmarksEmpty, style: theme.textTheme.bodySmall), ); } return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text(l10n.bookmarksHeading, style: theme.textTheme.titleSmall), const SizedBox(height: 4), for (final b in bookmarks) Card( margin: const EdgeInsets.symmetric(vertical: 4), child: ListTile( title: Text(b.displayName), subtitle: Text('${b.host} — ${b.nickname}'), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( icon: const Icon(Icons.login), tooltip: l10n.connectAction, onPressed: () => onConnect(b), ), IconButton( icon: const Icon(Icons.delete_outline), tooltip: l10n.bookmarkDeleteAction, onPressed: () => onDelete(b), ), ], ), ), ), ], ); } }