feat: searching in editor page (#756)

* feat: searching in editor page
Fixes #734

* opt.: editor searching ui
This commit is contained in:
lollipopkit🏳️‍⚧️
2025-05-14 05:09:32 +08:00
committed by GitHub
parent f6b3ec2a62
commit 4d45d01074
4 changed files with 256 additions and 6 deletions

View File

@@ -14,6 +14,7 @@ import 'package:server_box/data/store/setting.dart';
import 'package:server_box/view/widget/two_line_text.dart'; import 'package:server_box/view/widget/two_line_text.dart';
import 'package:re_editor/re_editor.dart'; import 'package:re_editor/re_editor.dart';
import 'package:server_box/view/widget/code_find_panel_view.dart';
enum EditorPageRetType { path, text } enum EditorPageRetType { path, text }
@@ -67,6 +68,7 @@ class _EditorPageState extends State<EditorPage> {
final _focusNode = FocusNode(); final _focusNode = FocusNode();
late CodeLineEditingController _controller; late CodeLineEditingController _controller;
late CodeFindController _findController;
late Map<String, TextStyle> _codeTheme; late Map<String, TextStyle> _codeTheme;
String? _langCode; String? _langCode;
@@ -74,14 +76,17 @@ class _EditorPageState extends State<EditorPage> {
@override @override
void dispose() { void dispose() {
super.dispose();
_controller.dispose(); _controller.dispose();
_findController.dispose();
_focusNode.dispose(); _focusNode.dispose();
super.dispose();
} }
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_controller = CodeLineEditingController();
_findController = CodeFindController(_controller);
_init(); _init();
} }
@@ -120,6 +125,11 @@ class _EditorPageState extends State<EditorPage> {
down: l10n.editor, down: l10n.editor,
), ),
actions: [ actions: [
IconButton(
icon: const Icon(Icons.search),
tooltip: libL10n.search,
onPressed: () => _findController.findMode(),
),
PopupMenuButton<String>( PopupMenuButton<String>(
icon: const Icon(Icons.language), icon: const Icon(Icons.language),
tooltip: libL10n.language, tooltip: libL10n.language,
@@ -150,6 +160,7 @@ class _EditorPageState extends State<EditorPage> {
color: _EditorReTheme.getBackground(_codeTheme), color: _EditorReTheme.getBackground(_codeTheme),
child: CodeEditor( child: CodeEditor(
controller: _controller, controller: _controller,
findController: _findController,
wordWrap: Stores.setting.editorSoftWrap.fetch(), wordWrap: Stores.setting.editorSoftWrap.fetch(),
focusNode: _focusNode, focusNode: _focusNode,
indicatorBuilder: (context, editingController, chunkController, notifier) { indicatorBuilder: (context, editingController, chunkController, notifier) {
@@ -162,7 +173,8 @@ class _EditorPageState extends State<EditorPage> {
], ],
); );
}, },
// findBuilder: (context, controller, readOnly) => CodeFindPanelView(controller: controller, readOnly: readOnly), findBuilder: (context, controller, readOnly) =>
CodeFindPanelView(controller: controller, readOnly: readOnly),
// toolbarController: const ContextMenuControllerImpl(), // toolbarController: const ContextMenuControllerImpl(),
), ),
); );
@@ -175,7 +187,6 @@ extension on _EditorPageState {
if (Stores.setting.editorHighlight.fetch()) { if (Stores.setting.editorHighlight.fetch()) {
_langCode = widget.args?.langCode ?? Highlights.getCode(widget.args?.path); _langCode = widget.args?.langCode ?? Highlights.getCode(widget.args?.path);
} }
_controller = CodeLineEditingController();
if (_langCode == null) { if (_langCode == null) {
_setupCtrl(); _setupCtrl();
} else { } else {

View File

@@ -0,0 +1,239 @@
import 'dart:math';
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:re_editor/re_editor.dart';
const EdgeInsetsGeometry _kDefaultFindMargin = EdgeInsets.only(right: 10);
const double _kDefaultFindPanelWidth = 360;
const double _kDefaultFindPanelHeight = 36;
const double _kDefaultReplacePanelHeight = _kDefaultFindPanelHeight * 2;
const double _kDefaultFindIconSize = 16;
const double _kDefaultFindIconWidth = 30;
const double _kDefaultFindIconHeight = 30;
const double _kDefaultFindInputFontSize = 13;
const double _kDefaultFindResultFontSize = 12;
const EdgeInsetsGeometry _kDefaultFindPadding = EdgeInsets.only(left: 5, right: 5, top: 2.5, bottom: 2.5);
const EdgeInsetsGeometry _kDefaultFindInputContentPadding = EdgeInsets.only(left: 5, right: 5);
class CodeFindPanelView extends StatelessWidget implements PreferredSizeWidget {
final CodeFindController controller;
final EdgeInsetsGeometry margin;
final bool readOnly;
final Color? iconColor;
final Color? iconSelectedColor;
final double iconSize;
final double inputFontSize;
final double resultFontSize;
final Color? inputTextColor;
final Color? resultFontColor;
final EdgeInsetsGeometry padding;
final InputDecoration decoration;
const CodeFindPanelView({
super.key,
required this.controller,
this.margin = _kDefaultFindMargin,
required this.readOnly,
this.iconSelectedColor,
this.iconColor,
this.iconSize = _kDefaultFindIconSize,
this.inputFontSize = _kDefaultFindInputFontSize,
this.resultFontSize = _kDefaultFindResultFontSize,
this.inputTextColor,
this.resultFontColor,
this.padding = _kDefaultFindPadding,
this.decoration = const InputDecoration(
filled: true,
contentPadding: _kDefaultFindInputContentPadding,
border: OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(0)), gapPadding: 0),
),
});
@override
Size get preferredSize {
final value = controller.value;
final height = value == null
? 0.0
: (value.replaceMode ? _kDefaultReplacePanelHeight : _kDefaultFindPanelHeight) + margin.vertical;
return Size(double.infinity, height);
}
@override
Widget build(BuildContext context) {
if (controller.value == null) return UIs.placeholder;
return Container(
margin: margin,
alignment: Alignment.topRight,
height: preferredSize.height,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SizedBox(
width: _kDefaultFindPanelWidth,
child: Column(
children: [
_buildFindInputView(context),
if (controller.value!.replaceMode) _buildReplaceInputView(context),
],
),
),
),
);
}
Widget _buildFindInputView(BuildContext context) {
final CodeFindValue value = controller.value!;
final String result;
if (value.result == null) {
result = libL10n.empty;
} else {
final index = value.result?.index;
final count = value.result?.matches.length;
result = '$index/$count';
}
return Row(
children: [
SizedBox(
width: _kDefaultFindPanelWidth / 1.75,
height: _kDefaultFindPanelHeight,
child: Stack(
alignment: Alignment.center,
children: [
_buildTextField(
context: context,
controller: controller.findInputController,
focusNode: controller.findInputFocusNode,
iconsWidth: _kDefaultFindIconWidth * 1.5,
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
_buildCheckText(
context: context,
text: 'Aa',
checked: value.option.caseSensitive,
onPressed: controller.toggleCaseSensitive,
),
_buildCheckText(
context: context,
text: '.*',
checked: value.option.regex,
onPressed: controller.toggleRegex,
)
],
)
],
)),
Text(result, style: TextStyle(color: resultFontColor, fontSize: resultFontSize)),
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
_buildIconButton(
onPressed: value.result == null ? null : controller.previousMatch,
icon: Icons.arrow_upward,
tooltip: libL10n.previous,
),
_buildIconButton(
onPressed: value.result == null ? null : controller.nextMatch,
icon: Icons.arrow_downward,
tooltip: libL10n.next,
),
_buildIconButton(
onPressed: controller.close,
icon: Icons.close,
tooltip: libL10n.close,
)
],
),
)
],
);
}
Widget _buildReplaceInputView(BuildContext context) {
final CodeFindValue value = controller.value!;
return Row(
children: [
SizedBox(
width: _kDefaultFindPanelWidth / 1.75,
height: _kDefaultFindPanelHeight,
child: _buildTextField(
context: context,
controller: controller.replaceInputController,
focusNode: controller.replaceInputFocusNode,
),
),
_buildIconButton(
onPressed: value.result == null ? null : controller.replaceMatch,
icon: Icons.done,
tooltip: libL10n.replace,
),
_buildIconButton(
onPressed: value.result == null ? null : controller.replaceAllMatches,
icon: Icons.done_all,
tooltip: libL10n.replaceAll,
)
],
);
}
Widget _buildTextField({
required BuildContext context,
required TextEditingController controller,
required FocusNode focusNode,
double iconsWidth = 0,
}) {
return Padding(
padding: padding,
child: TextField(
maxLines: 1,
focusNode: focusNode,
style: TextStyle(color: inputTextColor, fontSize: inputFontSize),
decoration: decoration.copyWith(
contentPadding:
(decoration.contentPadding ?? EdgeInsets.zero).add(EdgeInsets.only(right: iconsWidth))),
controller: controller,
),
);
}
Widget _buildCheckText({
required BuildContext context,
required String text,
required bool checked,
required VoidCallback onPressed,
}) {
final Color selectedColor = iconSelectedColor ?? Theme.of(context).primaryColor;
return GestureDetector(
onTap: onPressed,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: SizedBox(
width: _kDefaultFindIconWidth * 0.75,
child: Text(
text,
style: TextStyle(
color: checked ? selectedColor : iconColor,
fontSize: inputFontSize,
),
),
),
),
);
}
Widget _buildIconButton({required IconData icon, VoidCallback? onPressed, String? tooltip}) {
return IconButton(
onPressed: onPressed,
icon: Icon(
icon,
size: iconSize,
),
constraints: const BoxConstraints(maxWidth: _kDefaultFindIconWidth, maxHeight: _kDefaultFindIconHeight),
tooltip: tooltip,
splashRadius: max(_kDefaultFindIconWidth, _kDefaultFindIconHeight) / 2,
);
}
}

View File

@@ -534,8 +534,8 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: "v1.0.283" ref: "v1.0.285"
resolved-ref: "6e25f9f00bbf5c2e2900009f9f2e28454050c4ec" resolved-ref: "5bcde3c934290093bc468d24c6f308da8fb8dd4e"
url: "https://github.com/lppcg/fl_lib" url: "https://github.com/lppcg/fl_lib"
source: git source: git
version: "0.0.1" version: "0.0.1"

View File

@@ -63,7 +63,7 @@ dependencies:
fl_lib: fl_lib:
git: git:
url: https://github.com/lppcg/fl_lib url: https://github.com/lppcg/fl_lib
ref: v1.0.283 ref: v1.0.285
dependency_overrides: dependency_overrides:
# webdav_client_plus: # webdav_client_plus: