feat(client): 断开连接前弹二次确认框(避免误触瞬断)

连接键在 on 态点击不再直接断开:先弹危险操作确认框(告知连接已加密,
确认才 toggle 断开);off/connecting 态行为不变直接 toggle。
新增 l10n disconnectTitle/Body/Action(en/zh/es/ja/ko/ru 六语)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEVUXAbFT6bF1Qw27RHWoD
This commit is contained in:
wangjia
2026-09-05 19:08:01 +08:00
parent def3bb34b8
commit 1cc52075e9
8 changed files with 82 additions and 2 deletions
+3
View File
@@ -343,6 +343,9 @@ abstract class AppText {
String get capOn;
String get connectNow; // 连接键 off 态圆内文字
String get secure; // 连接键圆内「已加密」
String get disconnectTitle; // 断开确认弹窗标题
String get disconnectBody; // 断开确认弹窗正文
String get disconnectAction; // 断开确认弹窗「断开」按钮
// ── 底部 / 侧栏导航 ──
String get tabConnect;
+6
View File
@@ -27,6 +27,12 @@ class StringsEn extends AppText {
@override
String get secure => 'SECURE';
@override
String get disconnectTitle => 'Disconnect?';
@override
String get disconnectBody => 'Your connection is encrypted. Are you sure you want to disconnect?';
@override
String get disconnectAction => 'Disconnect';
@override
String get tabConnect => 'Connect';
@override
String get tabServers => 'Servers';
+6
View File
@@ -27,6 +27,12 @@ class StringsEs extends AppText {
@override
String get secure => 'SEGURO';
@override
String get disconnectTitle => '¿Desconectar?';
@override
String get disconnectBody => 'Tu conexión está cifrada. ¿Seguro que quieres desconectar?';
@override
String get disconnectAction => 'Desconectar';
@override
String get tabConnect => 'Conectar';
@override
String get tabServers => 'Servidores';
+6
View File
@@ -27,6 +27,12 @@ class StringsJa extends AppText {
@override
String get secure => 'セキュア';
@override
String get disconnectTitle => '切断しますか?';
@override
String get disconnectBody => '接続は暗号化されています。切断してもよろしいですか?';
@override
String get disconnectAction => '切断';
@override
String get tabConnect => '接続';
@override
String get tabServers => 'サーバー';
+6
View File
@@ -27,6 +27,12 @@ class StringsKo extends AppText {
@override
String get secure => '보안';
@override
String get disconnectTitle => '연결 해제하시겠습니까?';
@override
String get disconnectBody => '연결이 암호화되어 있습니다. 정말 연결을 해제하시겠습니까?';
@override
String get disconnectAction => '연결 해제';
@override
String get tabConnect => '연결';
@override
String get tabServers => '서버';
+6
View File
@@ -27,6 +27,12 @@ class StringsRu extends AppText {
@override
String get secure => 'ЗАЩИЩЕНО';
@override
String get disconnectTitle => 'Отключиться?';
@override
String get disconnectBody => 'Ваше соединение зашифровано. Действительно отключиться?';
@override
String get disconnectAction => 'Отключить';
@override
String get tabConnect => 'Подключение';
@override
String get tabServers => 'Серверы';
+6
View File
@@ -27,6 +27,12 @@ class StringsZh extends AppText {
@override
String get secure => '已加密';
@override
String get disconnectTitle => '断开连接?';
@override
String get disconnectBody => '当前连接已加密。确定要断开吗?';
@override
String get disconnectAction => '断开';
@override
String get tabConnect => '连接';
@override
String get tabServers => '节点';
+43 -2
View File
@@ -21,6 +21,47 @@ class ConnectPage extends ConsumerWidget {
final bool isWide;
final VoidCallback onOpenNodes;
// 连接键点击:off/connecting 直接 toggle;on 态(已连接)先弹二次确认再断开,
// 避免误触瞬断加密连接。
Future<void> _handleToggle(BuildContext context, WidgetRef ref, VpnPhase phase, AppText t) async {
final notifier = ref.read(connectionProvider.notifier);
if (phase != VpnPhase.on) {
notifier.toggle();
return;
}
final c = context.pangolin;
final ok = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: c.surface,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(PangolinRadius.xl)),
title: Row(children: [
Container(
width: 34,
height: 34,
decoration: BoxDecoration(color: c.dangerSubtle, shape: BoxShape.circle),
child: Icon(PangolinIcons.alertTriangle, size: 18, color: c.danger)),
const SizedBox(width: 12),
Expanded(
child: Text(t.disconnectTitle,
overflow: TextOverflow.ellipsis,
style: PangolinText.body.copyWith(color: c.fg1, fontWeight: FontWeight.w700))),
]),
content: Text(t.disconnectBody, style: PangolinText.sm.copyWith(color: c.fg2, height: 1.5)),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: Text(t.devCancel, style: PangolinText.sm.copyWith(color: c.fg2, fontWeight: FontWeight.w600))),
TextButton(
onPressed: () => Navigator.pop(ctx, true),
child: Text(t.disconnectAction,
style: PangolinText.sm.copyWith(color: c.danger, fontWeight: FontWeight.w700))),
],
),
);
if (ok == true) notifier.toggle();
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final c = context.pangolin;
@@ -53,7 +94,7 @@ class ConnectPage extends ConsumerWidget {
elapsed: conn.elapsed,
offLabel: t.connectNow,
secureLabel: t.secure,
onTap: () => ref.read(connectionProvider.notifier).toggle(),
onTap: () => _handleToggle(context, ref, conn.phase, t),
);
final captionWidget = Column(mainAxisSize: MainAxisSize.min, children: [
@@ -94,7 +135,7 @@ class ConnectPage extends ConsumerWidget {
offLabel: t.connectNow,
secureLabel: t.secure,
size: 176,
onTap: () => ref.read(connectionProvider.notifier).toggle(),
onTap: () => _handleToggle(context, ref, conn.phase, t),
),
const SizedBox(height: 24),
Text(caption,