From f4355ce4f20eeae47c39f38d623865ecc0d40342 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Thu, 19 Mar 2026 11:32:23 +0800 Subject: [PATCH] Polish gateway access panel layout --- lib/widgets/gateway_connect_dialog.dart | 202 +++++++++++++++++------- 1 file changed, 143 insertions(+), 59 deletions(-) diff --git a/lib/widgets/gateway_connect_dialog.dart b/lib/widgets/gateway_connect_dialog.dart index cd6ccb68..f223ec81 100644 --- a/lib/widgets/gateway_connect_dialog.dart +++ b/lib/widgets/gateway_connect_dialog.dart @@ -65,10 +65,13 @@ class _GatewayConnectDialogState extends State { Widget build(BuildContext context) { final theme = Theme.of(context); final palette = context.palette; + final horizontalPadding = widget.compact ? 20.0 : 24.0; + final verticalPadding = widget.compact ? 18.0 : 22.0; final dialogTitleStyle = theme.textTheme.headlineSmall?.copyWith( - fontSize: widget.compact ? 17 : 18, - height: 20 / 17, - letterSpacing: -0.12, + fontSize: widget.compact ? 24 : 22, + height: widget.compact ? 28 / 24 : 26 / 22, + letterSpacing: -0.28, + fontWeight: FontWeight.w700, ); final supportingCopyStyle = theme.textTheme.bodyMedium?.copyWith( fontSize: 12, @@ -99,7 +102,12 @@ class _GatewayConnectDialogState extends State { ), ), child: SingleChildScrollView( - padding: const EdgeInsets.all(AppSpacing.page), + padding: EdgeInsets.fromLTRB( + horizontalPadding, + verticalPadding, + horizontalPadding, + verticalPadding, + ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, @@ -131,7 +139,7 @@ class _GatewayConnectDialogState extends State { ), const SizedBox(height: AppSpacing.section), _StatusBanner(controller: widget.controller), - const SizedBox(height: AppSpacing.section), + const SizedBox(height: 14), if (_mode == 'setup') ...[ TextField( controller: _setupCodeController, @@ -146,6 +154,8 @@ class _GatewayConnectDialogState extends State { ), ), ] else ...[ + _FormSectionLabel(label: appText('连接目标', 'Connection Target')), + const SizedBox(height: 8), DropdownButtonFormField( initialValue: _connectionMode, decoration: InputDecoration( @@ -173,15 +183,17 @@ class _GatewayConnectDialogState extends State { }); }, ), - const SizedBox(height: AppSpacing.section), + const SizedBox(height: 12), TextField( controller: _hostController, decoration: InputDecoration(labelText: appText('主机', 'Host')), ), - const SizedBox(height: AppSpacing.section), + const SizedBox(height: 12), Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( + flex: 3, child: TextField( controller: _portController, keyboardType: TextInputType.number, @@ -190,15 +202,13 @@ class _GatewayConnectDialogState extends State { ), ), ), - const SizedBox(width: AppSpacing.section), + const SizedBox(width: 12), Expanded( - child: SwitchListTile.adaptive( - contentPadding: EdgeInsets.zero, + flex: 2, + child: _TlsToggleCard( value: _tls, - title: Text( - appText('TLS', 'TLS'), - style: floatingFieldLabelStyle, - ), + label: appText('TLS', 'TLS'), + enabled: _connectionMode != RuntimeConnectionMode.local, onChanged: _connectionMode == RuntimeConnectionMode.local ? null : (value) => setState(() => _tls = value), @@ -207,7 +217,9 @@ class _GatewayConnectDialogState extends State { ], ), ], - const SizedBox(height: AppSpacing.section), + const SizedBox(height: 14), + _FormSectionLabel(label: appText('凭证', 'Credentials')), + const SizedBox(height: 8), TextField( controller: _tokenController, obscureText: _obscureSharedToken, @@ -236,7 +248,7 @@ class _GatewayConnectDialogState extends State { onChanged: (_) => setState(() {}), ), if (willUseStoredGatewayToken || typedGatewayToken.isNotEmpty) ...[ - const SizedBox(height: AppSpacing.section), + const SizedBox(height: 10), _SharedTokenStatusCard( hasStoredGatewayToken: hasStoredGatewayToken, storedGatewayTokenMask: storedGatewayTokenMask, @@ -253,7 +265,7 @@ class _GatewayConnectDialogState extends State { : null, ), ], - const SizedBox(height: AppSpacing.section), + const SizedBox(height: 12), TextField( controller: _passwordController, obscureText: true, @@ -262,14 +274,11 @@ class _GatewayConnectDialogState extends State { hintText: appText('可选:共享密码', 'Optional shared password'), ), ), - const SizedBox(height: AppSpacing.section), - Wrap( - spacing: AppSpacing.section, - runSpacing: AppSpacing.section, - alignment: WrapAlignment.end, + const SizedBox(height: 16), + Row( children: [ if (widget.controller.connection.status == - RuntimeConnectionStatus.connected) + RuntimeConnectionStatus.connected) ...[ OutlinedButton.icon( onPressed: _submitting ? null @@ -283,13 +292,17 @@ class _GatewayConnectDialogState extends State { icon: const Icon(Icons.link_off_rounded), label: Text(appText('断开连接', 'Disconnect')), ), - FilledButton.icon( - onPressed: _submitting ? null : _submit, - icon: const Icon(Icons.wifi_tethering_rounded), - label: Text( - _submitting - ? appText('连接中…', 'Connecting…') - : appText('连接', 'Connect'), + const SizedBox(width: 10), + ], + Expanded( + child: FilledButton.icon( + onPressed: _submitting ? null : _submit, + icon: const Icon(Icons.wifi_tethering_rounded), + label: Text( + _submitting + ? appText('连接中…', 'Connecting…') + : appText('连接', 'Connect'), + ), ), ), ], @@ -409,10 +422,10 @@ class _SharedTokenStatusCard extends StatelessWidget { ); return Container( width: double.infinity, - padding: const EdgeInsets.all(AppSpacing.section), + padding: const EdgeInsets.fromLTRB(12, 10, 12, 10), decoration: BoxDecoration( - color: theme.colorScheme.surfaceContainerHighest, - border: Border.all(color: theme.dividerColor), + color: palette.surfaceSecondary.withValues(alpha: 0.92), + border: Border.all(color: palette.strokeSoft), borderRadius: BorderRadius.circular(AppRadius.card), ), child: Row( @@ -457,53 +470,65 @@ class _StatusBanner extends StatelessWidget { final palette = context.palette; final connection = controller.connection; final tone = switch (connection.status) { - RuntimeConnectionStatus.connected => theme.colorScheme.primaryContainer, + RuntimeConnectionStatus.connected => palette.accentMuted, RuntimeConnectionStatus.error => theme.colorScheme.errorContainer, - RuntimeConnectionStatus.connecting => - theme.colorScheme.secondaryContainer, - RuntimeConnectionStatus.offline => - theme.colorScheme.surfaceContainerHighest, + RuntimeConnectionStatus.connecting => palette.surfaceSecondary, + RuntimeConnectionStatus.offline => palette.surfaceSecondary, + }; + final statusColor = switch (connection.status) { + RuntimeConnectionStatus.connected => palette.success, + RuntimeConnectionStatus.error => palette.danger, + RuntimeConnectionStatus.connecting => palette.accent, + RuntimeConnectionStatus.offline => palette.textSecondary, }; return Container( width: double.infinity, - padding: const EdgeInsets.all(AppSpacing.section), + padding: const EdgeInsets.fromLTRB(14, 14, 14, 14), decoration: BoxDecoration( color: tone, - border: Border.all(color: theme.dividerColor), + border: Border.all(color: palette.strokeSoft), borderRadius: BorderRadius.circular(AppRadius.card), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - connection.status.label, - style: theme.textTheme.titleMedium?.copyWith( - fontSize: 12, - height: 14 / 12, - ), + Row( + children: [ + Container( + width: 10, + height: 10, + decoration: BoxDecoration( + color: statusColor, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 8), + Text( + connection.status.label, + style: theme.textTheme.titleMedium?.copyWith( + fontSize: 14, + height: 16 / 14, + fontWeight: FontWeight.w700, + ), + ), + ], ), - const SizedBox(height: AppSpacing.compact), + const SizedBox(height: 8), Text( connection.remoteAddress ?? 'No active gateway target', style: theme.textTheme.bodyMedium?.copyWith( - fontSize: 12, - height: 16 / 12, + fontSize: 13, + height: 18 / 13, color: palette.textSecondary, ), ), - const SizedBox(height: AppSpacing.section), - Text( - appText('认证诊断', 'Auth Diagnostics'), - style: theme.textTheme.labelLarge?.copyWith( - fontSize: 12, - height: 14 / 12, - ), - ), - const SizedBox(height: AppSpacing.compact), + const SizedBox(height: 12), + _FormSectionLabel(label: appText('认证诊断', 'Auth Diagnostics')), + const SizedBox(height: 6), Text( connection.connectAuthSummary, style: theme.textTheme.bodySmall?.copyWith( - fontSize: 12, + fontSize: 13, height: 16 / 12, color: palette.textSecondary, ), @@ -565,3 +590,62 @@ class _StatusBanner extends StatelessWidget { ); } } + +class _FormSectionLabel extends StatelessWidget { + const _FormSectionLabel({required this.label}); + + final String label; + + @override + Widget build(BuildContext context) { + final palette = context.palette; + return Text( + label, + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: palette.textMuted, + letterSpacing: 0.32, + ), + ); + } +} + +class _TlsToggleCard extends StatelessWidget { + const _TlsToggleCard({ + required this.value, + required this.label, + required this.enabled, + required this.onChanged, + }); + + final bool value; + final String label; + final bool enabled; + final ValueChanged? onChanged; + + @override + Widget build(BuildContext context) { + final palette = context.palette; + return Container( + constraints: const BoxConstraints(minHeight: AppSizes.inputHeight), + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: palette.surfacePrimary.withValues(alpha: 0.92), + borderRadius: BorderRadius.circular(AppRadius.input), + border: Border.all(color: palette.strokeSoft), + ), + child: Row( + children: [ + Expanded( + child: Text( + label, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: enabled ? palette.textSecondary : palette.textMuted, + ), + ), + ), + Switch.adaptive(value: value, onChanged: enabled ? onChanged : null), + ], + ), + ); + } +}