mirror of
https://github.com/wanghongenpin/proxypin.git
synced 2026-04-27 22:49:53 +08:00
203 lines
5.5 KiB
Dart
203 lines
5.5 KiB
Dart
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<SearchSettings> 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!); // 移动到键<E588B0><E994AE>上方
|
||
}
|
||
}
|
||
|
||
@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<DismissIntent>(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;
|
||
}
|