Files
proxypin/lib/ui/component/search/search_controller.dart

203 lines
5.5 KiB
Dart
Raw Permalink Blame History

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;
}