import 'dart:math'; import 'package:flutter/material.dart'; import 'package:get/get_rx/src/rx_types/rx_types.dart'; import 'package:proxypin/network/util/logger.dart'; import 'package:proxypin/ui/component/search/search_field.dart'; class SearchTextController extends ValueNotifier with WidgetsBindingObserver { SearchTextController() : super(SearchSettings.empty) { patternController.addListener(_onPatternControllerChanged); WidgetsBinding.instance.addObserver(this); // 添加监听器 } final patternController = TextEditingController(); RxInt currentMatchIndex = RxInt(0); RxInt totalMatchCount = RxInt(0); OverlayEntry? _searchPopup; double? overlayTop; double? overlayRight; bool shouldSearch() { return isSearchOverlayVisible && patternController.text.isNotEmpty; } void toggleCaseSensitivity() { value = value.copyWith(isCaseSensitive: !value.isCaseSensitive); } void toggleIsRegExp() { value = value.copyWith(isRegExp: !value.isRegExp); } void _onPatternControllerChanged() { value = value.copyWith(pattern: patternController.text, currentMatchIndex: 0); if (value.pattern.isEmpty) { currentMatchIndex.value = 0; totalMatchCount.value = 0; } } void updateMatchCount(int count) { totalMatchCount.value = count; if (count == 0) { currentMatchIndex.value = 0; return; } if (currentMatchIndex.value >= count) { currentMatchIndex.value = count - 1; } } void movePrevious() { if (totalMatchCount.value == 0) return; if (currentMatchIndex.value == 0) { currentMatchIndex.value = totalMatchCount.value - 1; } else { currentMatchIndex.value--; } value = value.copyWith(currentMatchIndex: currentMatchIndex.value); } void moveNext() { if (totalMatchCount.value == 0) return; if (currentMatchIndex.value >= totalMatchCount.value - 1) { currentMatchIndex.value = 0; } else { currentMatchIndex.value++; } value = value.copyWith(currentMatchIndex: currentMatchIndex.value); } void closeSearch() { removeSearchOverlay(); } void updateOverlayPosition(double top, double right) { overlayTop = top; overlayRight = right; if (_searchPopup != null) { _searchPopup!.markNeedsBuild(); } } @override void didChangeMetrics() { super.didChangeMetrics(); if (!isSearchOverlayVisible) { return; } // 检测键盘弹出并调整位置 var view = WidgetsBinding.instance.platformDispatcher.views.first; final bottomInset = MediaQueryData.fromView(view).viewInsets.bottom; if (bottomInset == 0 || overlayTop == null) { // 键盘收起 return; } var screenHeight = MediaQueryData.fromView(view).size.height; final currentHeight = screenHeight - bottomInset; if (overlayTop! + 50 > currentHeight) { // 如果被键盘遮挡 updateOverlayPosition(max(currentHeight - 120, 120), overlayRight!); // 移动到键��上方 } } @override void dispose() { WidgetsBinding.instance.removeObserver(this); // 移除监听器 logger.d('Disposing SearchTextController'); super.didChangeMetrics(); removeSearchOverlay(); patternController.dispose(); totalMatchCount.close(); currentMatchIndex.close(); super.dispose(); } bool get isSearchOverlayVisible => _searchPopup != null; void showSearchOverlay(BuildContext context, {double? top, double? right}) { if (_searchPopup != null) { return; } _searchPopup = _buildSearchOverlay(context, top: top, right: right); Overlay.of(context).insert(_searchPopup!); } void removeSearchOverlay() { _searchPopup?.remove(); _searchPopup = null; } OverlayEntry _buildSearchOverlay(BuildContext context, {double? top, double? right}) { overlayTop = top ?? overlayTop; overlayRight = right ?? overlayRight; return OverlayEntry( builder: (context) { return Positioned( top: overlayTop, right: overlayRight, child: Actions(actions: { DismissIntent: CallbackAction(onInvoke: (intent) { closeSearch(); return null; }), }, child: SearchField(searchController: this)), ); }, ); } } class SearchSettings { const SearchSettings({ required this.isCaseSensitive, required this.isRegExp, required this.pattern, this.currentMatchIndex = 0, }); final bool isCaseSensitive; final bool isRegExp; final String pattern; final int currentMatchIndex; static const empty = SearchSettings( isCaseSensitive: false, isRegExp: false, pattern: '', ); SearchSettings copyWith({ bool? isCaseSensitive, bool? isRegExp, String? pattern, int? currentMatchIndex, }) { return SearchSettings( isCaseSensitive: isCaseSensitive ?? this.isCaseSensitive, isRegExp: isRegExp ?? this.isRegExp, pattern: pattern ?? this.pattern, currentMatchIndex: currentMatchIndex ?? this.currentMatchIndex, ); } @override bool operator ==(Object other) => identical(this, other) || other is SearchSettings && runtimeType == other.runtimeType && isCaseSensitive == other.isCaseSensitive && isRegExp == other.isRegExp && pattern == other.pattern && currentMatchIndex == other.currentMatchIndex; @override int get hashCode => isCaseSensitive.hashCode ^ isRegExp.hashCode ^ pattern.hashCode ^ currentMatchIndex.hashCode; }