9 changed files with 292 additions and 352 deletions
@ -0,0 +1,67 @@ |
|||||||
|
import 'package:flutter/material.dart'; |
||||||
|
|
||||||
|
class EditorToolbar extends StatelessWidget { |
||||||
|
final String title; |
||||||
|
final String text; |
||||||
|
final bool isLoading; |
||||||
|
final VoidCallback onOpenFile; |
||||||
|
final VoidCallback onCopyToClipboard; |
||||||
|
final VoidCallback onSaveFile; |
||||||
|
|
||||||
|
const EditorToolbar({ |
||||||
|
super.key, |
||||||
|
required this.title, |
||||||
|
required this.text, |
||||||
|
required this.isLoading, |
||||||
|
required this.onOpenFile, |
||||||
|
required this.onCopyToClipboard, |
||||||
|
required this.onSaveFile, |
||||||
|
}); |
||||||
|
|
||||||
|
@override |
||||||
|
Widget build(BuildContext context) { |
||||||
|
return Container( |
||||||
|
height: 40, |
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16), |
||||||
|
decoration: BoxDecoration(color: Colors.grey[100]), |
||||||
|
child: Row( |
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
||||||
|
children: [ |
||||||
|
Text(title, style: const TextStyle(fontWeight: FontWeight.bold)), |
||||||
|
_buildActionButtons(context), |
||||||
|
], |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
Widget _buildActionButtons(BuildContext context) { |
||||||
|
return Row( |
||||||
|
children: [ |
||||||
|
IconButton( |
||||||
|
icon: const Icon(Icons.folder_open, size: 20), |
||||||
|
tooltip: '打开文件', |
||||||
|
onPressed: isLoading ? null : onOpenFile, |
||||||
|
), |
||||||
|
IconButton( |
||||||
|
icon: const Icon(Icons.content_copy, size: 20), |
||||||
|
tooltip: '复制内容', |
||||||
|
onPressed: text.isEmpty ? null : onCopyToClipboard, |
||||||
|
), |
||||||
|
IconButton( |
||||||
|
icon: const Icon(Icons.save, size: 20), |
||||||
|
tooltip: '保存到文件', |
||||||
|
onPressed: text.isEmpty ? null : onSaveFile, |
||||||
|
), |
||||||
|
if (isLoading) |
||||||
|
const Padding( |
||||||
|
padding: EdgeInsets.only(left: 8), |
||||||
|
child: SizedBox( |
||||||
|
width: 16, |
||||||
|
height: 16, |
||||||
|
child: CircularProgressIndicator(strokeWidth: 2), |
||||||
|
), |
||||||
|
), |
||||||
|
], |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -1,163 +0,0 @@ |
|||||||
import 'dart:convert'; |
|
||||||
import 'dart:io'; |
|
||||||
import 'package:flutter/material.dart'; |
|
||||||
import 'package:flutter/services.dart'; |
|
||||||
import 'package:file_picker/file_picker.dart'; |
|
||||||
import 'text_editor_controller.dart'; |
|
||||||
|
|
||||||
class TextEditorActions { |
|
||||||
static Future<void> openFile(BuildContext context, TextEditorController controller) async { |
|
||||||
final result = await FilePicker.platform.pickFiles(type: FileType.any, allowMultiple: false); |
|
||||||
|
|
||||||
if (result != null && result.files.single.path != null) { |
|
||||||
await _loadFile(context, controller, result.files.single.path!); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
static Future<void> copyToClipboard(BuildContext context, String content) async { |
|
||||||
await Clipboard.setData(ClipboardData(text: content)); |
|
||||||
if (context.mounted) { |
|
||||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('已复制到剪贴板'))); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
static Future<void> saveFile(BuildContext context, String content) async { |
|
||||||
try { |
|
||||||
String? outputPath = await FilePicker.platform.saveFile( |
|
||||||
dialogTitle: '保存文件', |
|
||||||
fileName: 'untitled.txt', |
|
||||||
allowedExtensions: ['txt'], |
|
||||||
type: FileType.any, |
|
||||||
); |
|
||||||
|
|
||||||
if (outputPath == null) return; |
|
||||||
|
|
||||||
final file = File(outputPath); |
|
||||||
|
|
||||||
if (await file.exists()) { |
|
||||||
final shouldOverwrite = await showDialog<bool>( |
|
||||||
context: context, |
|
||||||
builder: |
|
||||||
(context) => AlertDialog( |
|
||||||
title: const Text('文件已存在'), |
|
||||||
content: const Text('要覆盖现有文件吗?'), |
|
||||||
actions: [ |
|
||||||
TextButton( |
|
||||||
onPressed: () => Navigator.pop(context, false), |
|
||||||
child: const Text('取消'), |
|
||||||
), |
|
||||||
TextButton( |
|
||||||
onPressed: () => Navigator.pop(context, true), |
|
||||||
child: const Text('覆盖'), |
|
||||||
), |
|
||||||
], |
|
||||||
), |
|
||||||
); |
|
||||||
|
|
||||||
if (shouldOverwrite != true) return; |
|
||||||
} |
|
||||||
|
|
||||||
await file.writeAsString(content); |
|
||||||
|
|
||||||
if (context.mounted) { |
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('已保存到: ${file.path}'))); |
|
||||||
} |
|
||||||
} on FileSystemException catch (e) { |
|
||||||
if (context.mounted) { |
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('保存失败: ${e.message}'))); |
|
||||||
} |
|
||||||
} catch (e) { |
|
||||||
if (context.mounted) { |
|
||||||
ScaffoldMessenger.of( |
|
||||||
context, |
|
||||||
).showSnackBar(SnackBar(content: Text('保存失败: ${e.toString()}'))); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
static Future<void> _loadFile( |
|
||||||
BuildContext context, |
|
||||||
TextEditorController controller, |
|
||||||
String filePath, |
|
||||||
) async { |
|
||||||
try { |
|
||||||
controller.setLoading(true); |
|
||||||
final file = File(filePath); |
|
||||||
final fileSize = await file.length(); |
|
||||||
|
|
||||||
if (fileSize > controller.maxFileSize) { |
|
||||||
if (context.mounted) { |
|
||||||
ScaffoldMessenger.of( |
|
||||||
context, |
|
||||||
).showSnackBar(const SnackBar(content: Text('文件过大(超过1MB),无法处理'))); |
|
||||||
} |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
if (!controller.isEmpty) { |
|
||||||
final confirm = await showDialog<bool>( |
|
||||||
context: context, |
|
||||||
builder: |
|
||||||
(context) => AlertDialog( |
|
||||||
title: const Text('确认提示'), |
|
||||||
content: const Text('确认是否打开新的文件?打开后将覆盖当前内容'), |
|
||||||
actions: [ |
|
||||||
TextButton( |
|
||||||
onPressed: () => Navigator.pop(context, false), |
|
||||||
child: const Text('取消'), |
|
||||||
), |
|
||||||
TextButton( |
|
||||||
onPressed: () => Navigator.pop(context, true), |
|
||||||
child: const Text('确认'), |
|
||||||
), |
|
||||||
], |
|
||||||
), |
|
||||||
); |
|
||||||
|
|
||||||
if (confirm != true) return; |
|
||||||
} |
|
||||||
|
|
||||||
final fileName = file.path.split('\\').last; |
|
||||||
controller.updateContent(''); |
|
||||||
controller.onContentChanged?.call('', fileName); |
|
||||||
|
|
||||||
final stream = file.openRead(); |
|
||||||
final lines = stream.transform(utf8.decoder).transform(const LineSplitter()); |
|
||||||
|
|
||||||
await for (final line in lines) { |
|
||||||
if (!controller.textController.hasListeners) break; |
|
||||||
|
|
||||||
controller.textController.text += '$line\n'; |
|
||||||
controller.onContentChanged?.call(controller.content, fileName); |
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) { |
|
||||||
controller.scrollController.jumpTo(controller.scrollController.position.maxScrollExtent); |
|
||||||
}); |
|
||||||
|
|
||||||
await Future.delayed(const Duration(milliseconds: 10)); |
|
||||||
} |
|
||||||
|
|
||||||
controller.onFileLoaded?.call(file.path); |
|
||||||
|
|
||||||
if (context.mounted) { |
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('已加载: ${file.path}'))); |
|
||||||
} |
|
||||||
} on FormatException { |
|
||||||
if (context.mounted) { |
|
||||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('这不是可读的文本文件'))); |
|
||||||
} |
|
||||||
} on FileSystemException catch (e) { |
|
||||||
if (context.mounted) { |
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('文件访问错误: ${e.message}'))); |
|
||||||
} |
|
||||||
} catch (e) { |
|
||||||
if (context.mounted) { |
|
||||||
ScaffoldMessenger.of( |
|
||||||
context, |
|
||||||
).showSnackBar(SnackBar(content: Text('读取失败: ${e.toString()}'))); |
|
||||||
} |
|
||||||
} finally { |
|
||||||
controller.setLoading(false); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,42 +0,0 @@ |
|||||||
import 'package:flutter/material.dart'; |
|
||||||
|
|
||||||
class TextEditorController { |
|
||||||
final TextEditingController textController; |
|
||||||
final FocusNode focusNode; |
|
||||||
final ScrollController scrollController; |
|
||||||
final Function(String, String?)? onContentChanged; |
|
||||||
final Function(String)? onFileLoaded; |
|
||||||
|
|
||||||
bool _isLoading = false; |
|
||||||
static const int _maxFileSize = 1024 * 1024; // 1MB |
|
||||||
|
|
||||||
TextEditorController({String? initialContent, this.onContentChanged, this.onFileLoaded}) |
|
||||||
: textController = TextEditingController(text: initialContent ?? ''), |
|
||||||
focusNode = FocusNode(), |
|
||||||
scrollController = ScrollController(); |
|
||||||
|
|
||||||
bool get isLoading => _isLoading; |
|
||||||
bool get hasFocus => focusNode.hasFocus; |
|
||||||
bool get isEmpty => textController.text.isEmpty; |
|
||||||
String get content => textController.text; |
|
||||||
int get contentLength => textController.text.length; |
|
||||||
int get maxFileSize => _maxFileSize; |
|
||||||
|
|
||||||
void updateContent(String content) { |
|
||||||
textController.text = content; |
|
||||||
} |
|
||||||
|
|
||||||
void handleContentChanged(String text) { |
|
||||||
onContentChanged?.call(text, null); |
|
||||||
} |
|
||||||
|
|
||||||
void setLoading(bool loading) { |
|
||||||
_isLoading = loading; |
|
||||||
} |
|
||||||
|
|
||||||
void dispose() { |
|
||||||
textController.dispose(); |
|
||||||
focusNode.dispose(); |
|
||||||
scrollController.dispose(); |
|
||||||
} |
|
||||||
} |
|
Loading…
Reference in new issue