Skip to content

Commit 9fd5057

Browse files
committed
refactor(settings): debounce save and migrate SA JSON editor
- Migrate Service Account JSON editor to PlainTextCodeEditor - Sync CodeLineEditingController content safely (IME-aware) - Debounce config writes (400ms) and flush on blur/dispose
1 parent c52c8f0 commit 9fd5057

File tree

1 file changed

+81
-38
lines changed

1 file changed

+81
-38
lines changed

lib/desktop/desktop_settings_page.dart

Lines changed: 81 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'package:flutter/cupertino.dart';
33
import 'package:provider/provider.dart';
44
import 'package:flutter/rendering.dart';
55
import 'package:flutter/services.dart';
6+
import 'package:re_editor/re_editor.dart';
67
import 'dart:math' as math;
78
import 'dart:convert';
89
import 'dart:typed_data';
@@ -17,6 +18,8 @@ import 'model_fetch_dialog.dart' show showModelFetchDialog;
1718
import 'widgets/desktop_select_dropdown.dart';
1819
import '../shared/widgets/ios_switch.dart';
1920
import '../shared/widgets/ios_checkbox.dart';
21+
import '../shared/widgets/input_height_constraints.dart';
22+
import '../shared/widgets/plain_text_code_editor.dart';
2023
// Desktop assistants panel dependencies
2124
import '../features/assistant/pages/assistant_settings_edit_page.dart'
2225
show showAssistantDesktopDialog; // dialog opener only
@@ -27,6 +30,7 @@ import '../utils/sandbox_path_resolver.dart';
2730
import 'dart:io' show Directory, File, Platform;
2831
import '../utils/app_directories.dart';
2932
import 'package:characters/characters.dart';
33+
import '../utils/re_editor_utils.dart';
3034
import '../features/provider/pages/multi_key_manager_page.dart';
3135
import '../features/model/widgets/model_detail_sheet.dart';
3236
import 'add_provider_dialog.dart' show showDesktopAddProviderDialog;
@@ -2018,8 +2022,10 @@ class _DesktopProviderDetailPaneState
20182022
final TextEditingController _baseUrlCtrl = TextEditingController();
20192023
final TextEditingController _locationCtrl = TextEditingController();
20202024
final TextEditingController _projectIdCtrl = TextEditingController();
2021-
final TextEditingController _saJsonCtrl = TextEditingController();
2025+
final CodeLineEditingController _saJsonCtrl = CodeLineEditingController();
20222026
final TextEditingController _apiPathCtrl = TextEditingController();
2027+
Timer? _saJsonSaveTimer;
2028+
String _lastSavedSaJson = '';
20232029

20242030
void _syncCtrl(TextEditingController c, String newText) {
20252031
final v = c.value;
@@ -2033,17 +2039,50 @@ class _DesktopProviderDetailPaneState
20332039
}
20342040
}
20352041

2042+
void _syncCodeCtrl(CodeLineEditingController c, String newText) {
2043+
c.setTextSafely(newText);
2044+
}
2045+
20362046
void _syncControllersFromConfig(ProviderConfig cfg) {
20372047
_syncCtrl(_apiKeyCtrl, cfg.apiKey);
20382048
_syncCtrl(_baseUrlCtrl, cfg.baseUrl);
20392049
_syncCtrl(_apiPathCtrl, cfg.chatPath ?? '/chat/completions');
20402050
_syncCtrl(_locationCtrl, cfg.location ?? '');
20412051
_syncCtrl(_projectIdCtrl, cfg.projectId ?? '');
2042-
_syncCtrl(_saJsonCtrl, cfg.serviceAccountJson ?? '');
2052+
_syncCodeCtrl(_saJsonCtrl, cfg.serviceAccountJson ?? '');
2053+
_lastSavedSaJson = cfg.serviceAccountJson ?? '';
2054+
}
2055+
2056+
Future<void> _saveSaJsonNow(SettingsProvider sp) async {
2057+
final text = _saJsonCtrl.text;
2058+
if (text == _lastSavedSaJson) return;
2059+
_lastSavedSaJson = text;
2060+
final old = sp.getProviderConfig(widget.providerKey, defaultName: widget.displayName);
2061+
await sp.setProviderConfig(widget.providerKey, old.copyWith(serviceAccountJson: text));
2062+
}
2063+
2064+
void _scheduleSaJsonSave(SettingsProvider sp) {
2065+
_saJsonSaveTimer?.cancel();
2066+
_saJsonSaveTimer = Timer(const Duration(milliseconds: 400), () {
2067+
_saJsonSaveTimer = null;
2068+
if (!mounted) return;
2069+
_saveSaJsonNow(sp);
2070+
});
2071+
}
2072+
2073+
void _flushSaJsonSave(SettingsProvider sp) {
2074+
_saJsonSaveTimer?.cancel();
2075+
_saJsonSaveTimer = null;
2076+
_saveSaJsonNow(sp);
20432077
}
20442078

20452079
@override
20462080
void dispose() {
2081+
try {
2082+
final sp = context.read<SettingsProvider>();
2083+
_flushSaJsonSave(sp);
2084+
} catch (_) {}
2085+
_saJsonSaveTimer?.cancel();
20472086
_filterCtrl.dispose();
20482087
_searchFocus.dispose();
20492088
_apiKeyCtrl.dispose();
@@ -2663,43 +2702,47 @@ class _DesktopProviderDetailPaneState
26632702
bold: true,
26642703
),
26652704
const SizedBox(height: 6),
2666-
ConstrainedBox(
2667-
constraints: const BoxConstraints(minHeight: 120),
2668-
child: Focus(
2669-
onFocusChange: (has) async {
2670-
if (!has) {
2671-
final v = _saJsonCtrl.text;
2672-
final old = sp.getProviderConfig(
2673-
widget.providerKey,
2674-
defaultName: widget.displayName,
2675-
);
2676-
await sp.setProviderConfig(
2677-
widget.providerKey,
2678-
old.copyWith(serviceAccountJson: v),
2679-
);
2680-
}
2681-
},
2682-
child: TextField(
2683-
controller: _saJsonCtrl,
2684-
maxLines: null,
2685-
minLines: 6,
2686-
onChanged: (v) async {
2687-
if (_saJsonCtrl.value.composing.isValid) return;
2688-
final old = sp.getProviderConfig(
2689-
widget.providerKey,
2690-
defaultName: widget.displayName,
2691-
);
2692-
await sp.setProviderConfig(
2693-
widget.providerKey,
2694-
old.copyWith(serviceAccountJson: v),
2695-
);
2696-
},
2697-
style: const TextStyle(fontSize: 14),
2698-
decoration: _inputDecoration(context).copyWith(
2699-
hintText: '{\n "type": "service_account", ...\n}',
2705+
Builder(
2706+
builder: (innerCtx) {
2707+
final rawMaxSaJsonHeight = computeInputMaxHeight(
2708+
context: innerCtx,
2709+
reservedHeight: 260,
2710+
softCapFraction: 0.6,
2711+
minHeight: 120,
2712+
);
2713+
// TODO: computeInputMaxHeight already enforces minHeight; remove this redundant clamp (can be simplified to math.max) and keep the height logic consistent.
2714+
final maxSaJsonHeight = rawMaxSaJsonHeight < 120 ? 120.0 : rawMaxSaJsonHeight;
2715+
return ConstrainedBox(
2716+
constraints: BoxConstraints(minHeight: 120, maxHeight: maxSaJsonHeight),
2717+
child: Focus(
2718+
onFocusChange: (has) async {
2719+
if (!has) {
2720+
await _saveSaJsonNow(sp);
2721+
}
2722+
},
2723+
child: Container(
2724+
decoration: BoxDecoration(
2725+
color: Theme.of(context).brightness == Brightness.dark ? Colors.white10 : const Color(0xFFF7F7F9),
2726+
borderRadius: BorderRadius.circular(10),
2727+
border: Border.all(color: cs.outlineVariant.withOpacity(0.12), width: 0.6),
2728+
),
2729+
clipBehavior: Clip.antiAlias,
2730+
child: PlainTextCodeEditor(
2731+
controller: _saJsonCtrl,
2732+
autofocus: false,
2733+
hint: '{\n "type": "service_account", ...\n}',
2734+
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
2735+
fontSize: 14,
2736+
fontHeight: 1.4,
2737+
onChanged: (value) async {
2738+
if (_saJsonCtrl.isComposing) return;
2739+
_scheduleSaJsonSave(sp);
2740+
},
2741+
),
2742+
),
27002743
),
2701-
),
2702-
),
2744+
);
2745+
},
27032746
),
27042747
const SizedBox(height: 8),
27052748
Align(

0 commit comments

Comments
 (0)