ask for note at end of connection (#13499)

Signed-off-by: 21pages <sunboeasy@gmail.com>
This commit is contained in:
21pages
2025-11-13 23:35:40 +08:00
committed by GitHub
parent 13ee3e907d
commit 296c6df462
72 changed files with 932 additions and 200 deletions

View File

@@ -7,20 +7,29 @@ import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:flutter_hbb/utils/http_service.dart' as http;
import '../../common.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
import 'address_book.dart';
void clientClose(SessionID sessionId, OverlayDialogManager dialogManager) {
msgBox(sessionId, 'info', 'Close', 'Are you sure to close the connection?',
'', dialogManager);
void clientClose(SessionID sessionId, FFI ffi) async {
if (allowAskForNoteAtEndOfConnection(ffi, true)) {
if (await showConnEndAuditDialogCloseCanceled(ffi: ffi)) {
return;
}
closeConnection();
} else {
msgBox(sessionId, 'info', 'Close', 'Are you sure to close the connection?',
'', ffi.dialogManager);
}
}
abstract class ValidationRule {
@@ -1509,56 +1518,71 @@ showSetOSAccount(
});
}
Widget buildNoteTextField({
required TextEditingController controller,
required VoidCallback onEscape,
}) {
final focusNode = FocusNode(
onKey: (FocusNode node, RawKeyEvent evt) {
if (evt.logicalKey.keyLabel == 'Enter') {
if (evt is RawKeyDownEvent) {
int pos = controller.selection.base.offset;
controller.text =
'${controller.text.substring(0, pos)}\n${controller.text.substring(pos)}';
controller.selection =
TextSelection.fromPosition(TextPosition(offset: pos + 1));
}
return KeyEventResult.handled;
}
if (evt.logicalKey.keyLabel == 'Esc') {
if (evt is RawKeyDownEvent) {
onEscape();
}
return KeyEventResult.handled;
} else {
return KeyEventResult.ignored;
}
},
);
return TextField(
autofocus: true,
keyboardType: TextInputType.multiline,
textInputAction: TextInputAction.newline,
decoration: InputDecoration(
hintText: translate('input note here'),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: EdgeInsets.all(12),
),
minLines: 5,
maxLines: null,
maxLength: 256,
controller: controller,
focusNode: focusNode,
).workaroundFreezeLinuxMint();
}
showAuditDialog(FFI ffi) async {
final controller = TextEditingController(text: ffi.auditNote);
final controller = TextEditingController(
text: bind.sessionGetLastAuditNote(sessionId: ffi.sessionId));
ffi.dialogManager.show((setState, close, context) {
submit() {
var text = controller.text;
bind.sessionSendNote(sessionId: ffi.sessionId, note: text);
ffi.auditNote = text;
close();
}
late final focusNode = FocusNode(
onKey: (FocusNode node, RawKeyEvent evt) {
if (evt.logicalKey.keyLabel == 'Enter') {
if (evt is RawKeyDownEvent) {
int pos = controller.selection.base.offset;
controller.text =
'${controller.text.substring(0, pos)}\n${controller.text.substring(pos)}';
controller.selection =
TextSelection.fromPosition(TextPosition(offset: pos + 1));
}
return KeyEventResult.handled;
}
if (evt.logicalKey.keyLabel == 'Esc') {
if (evt is RawKeyDownEvent) {
close();
}
return KeyEventResult.handled;
} else {
return KeyEventResult.ignored;
}
},
);
return CustomAlertDialog(
title: Text(translate('Note')),
content: SizedBox(
width: 250,
height: 120,
child: TextField(
autofocus: true,
keyboardType: TextInputType.multiline,
textInputAction: TextInputAction.newline,
decoration: const InputDecoration.collapsed(
hintText: 'input note here',
),
maxLines: null,
maxLength: 256,
child: buildNoteTextField(
controller: controller,
focusNode: focusNode,
).workaroundFreezeLinuxMint()),
onEscape: close,
)),
actions: [
dialogButton('Cancel', onPressed: close, isOutline: true),
dialogButton('OK', onPressed: submit)
@@ -1569,6 +1593,223 @@ showAuditDialog(FFI ffi) async {
});
}
bool allowAskForNoteAtEndOfConnection(FFI? ffi, bool closedByControlling) {
if (ffi == null) {
return false;
}
return mainGetLocalBoolOptionSync(kOptionAllowAskForNoteAtEndOfConnection) &&
bind
.sessionGetAuditServerSync(sessionId: ffi.sessionId, typ: "conn")
.isNotEmpty &&
bind.sessionGetAuditGuid(sessionId: ffi.sessionId).isNotEmpty &&
bind.sessionGetLastAuditNote(sessionId: ffi.sessionId).isEmpty &&
(!closedByControlling ||
bind.willSessionCloseCloseSession(sessionId: ffi.sessionId));
}
// return value: close canceled
// true: return
// false: go on
Future<bool> desktopTryShowTabAuditDialogCloseCancelled(
{required String id, required DesktopTabController tabController}) async {
try {
final page =
tabController.state.value.tabs.firstWhere((tab) => tab.key == id).page;
final ffi = (page as dynamic).ffi;
final res = await showConnEndAuditDialogCloseCanceled(ffi: ffi);
return res;
} catch (e) {
debugPrint('Failed to show audit dialog: $e');
return false;
}
}
// return value:
// true: return
// false: go on
Future<bool> showConnEndAuditDialogCloseCanceled(
{required FFI ffi, String? type, String? title, String? text}) async {
final res = await _showConnEndAuditDialogCloseCanceled(
ffi: ffi, type: type, title: title, text: text);
if (res == true) {
return true;
}
return false;
}
// return value:
// true: return
// false / null: go on
Future<bool?> _showConnEndAuditDialogCloseCanceled({
required FFI ffi,
String? type,
String? title,
String? text,
}) async {
final closedByControlling = type == null;
final showDialog = allowAskForNoteAtEndOfConnection(ffi, closedByControlling);
if (!showDialog) {
return false;
}
ffi.dialogManager.dismissAll();
Future<void> updateAuditNoteByGuid(String auditGuid, String note) async {
debugPrint('Updating audit note for GUID: $auditGuid, note: $note');
try {
final apiServer = await bind.mainGetApiServer();
if (apiServer.isEmpty) {
debugPrint('API server is empty, cannot update audit note');
return;
}
final url = '$apiServer/api/audit';
var headers = getHttpHeaders();
headers['Content-Type'] = "application/json";
final body = jsonEncode({
'guid': auditGuid,
'note': note,
});
final response = await http.put(
Uri.parse(url),
headers: headers,
body: body,
);
if (response.statusCode == 200) {
debugPrint('Successfully updated audit note for GUID: $auditGuid');
} else {
debugPrint(
'Failed to update audit note. Status: ${response.statusCode}, Body: ${response.body}');
}
} catch (e) {
debugPrint('Error updating audit note: $e');
}
}
final controller = TextEditingController();
bool askForNote =
mainGetLocalBoolOptionSync(kOptionAllowAskForNoteAtEndOfConnection);
final isOptFixed = isOptionFixed(kOptionAllowAskForNoteAtEndOfConnection);
bool isInProgress = false;
return await ffi.dialogManager.show<bool>((setState, close, context) {
cancel() {
close(true);
}
set() async {
if (isInProgress) return;
setState(() {
isInProgress = true;
});
var text = controller.text;
if (text.isNotEmpty) {
await updateAuditNoteByGuid(
bind.sessionGetAuditGuid(sessionId: ffi.sessionId), text)
.timeout(const Duration(seconds: 6), onTimeout: () {
debugPrint('updateAuditNoteByGuid timeout after 6s');
});
}
// Save the "ask for note" preference
if (!isOptFixed) {
await mainSetLocalBoolOption(
kOptionAllowAskForNoteAtEndOfConnection, askForNote);
}
}
submit() async {
await set();
close(false);
}
final buttons = [
dialogButton('OK', onPressed: isInProgress ? null : submit)
];
if (type == 'relay-hint' || type == 'relay-hint2') {
buttons.add(dialogButton('Retry', onPressed: () async {
await set();
close(true);
ffi.ffiModel.reconnect(ffi.dialogManager, ffi.sessionId, false);
}));
if (type == 'relay-hint2') {
buttons.add(dialogButton('Connect via relay', onPressed: () async {
await set();
close(true);
ffi.ffiModel.reconnect(ffi.dialogManager, ffi.sessionId, true);
}));
}
}
if (closedByControlling) {
buttons.add(dialogButton('Cancel',
onPressed: isInProgress ? null : cancel, isOutline: true));
}
Widget content;
if (closedByControlling) {
content = SelectionArea(
child: msgboxContent(
'info', 'Close', 'Are you sure to close the connection?'));
} else {
content =
SelectionArea(child: msgboxContent(type, title ?? '', text ?? ''));
}
return CustomAlertDialog(
title: null,
content: SizedBox(
width: 350,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
content,
const SizedBox(height: 16),
SizedBox(
height: 120,
child: buildNoteTextField(
controller: controller,
onEscape: cancel,
),
),
if (!isOptFixed) ...[
const SizedBox(height: 8),
InkWell(
onTap: () {
setState(() {
askForNote = !askForNote;
});
},
child: Row(
children: [
Checkbox(
value: askForNote,
onChanged: (value) {
setState(() {
askForNote = value ?? false;
});
},
),
Expanded(
child: Text(
translate('note-at-conn-end-tip'),
style: const TextStyle(fontSize: 13),
),
),
],
),
),
],
if (isInProgress)
const LinearProgressIndicator().marginOnly(top: 4),
],
)),
actions: buttons,
onSubmit: submit,
onCancel: cancel,
);
});
}
void showConfirmSwitchSidesDialog(
SessionID sessionId, String id, OverlayDialogManager dialogManager) async {
dialogManager.show((setState, close, context) {