57 changed files with 887 additions and 109 deletions
@ -0,0 +1,104 @@
@@ -0,0 +1,104 @@
|
||||
import 'package:flutter/material.dart'; |
||||
import 'package:provider/provider.dart'; |
||||
import 'package:win_text_editor/app/menus/app_menu.dart'; |
||||
import 'package:win_text_editor/app/providers/tab_provider.dart'; |
||||
import 'package:win_text_editor/app/providers/file_provider.dart'; |
||||
import 'package:win_text_editor/app/widgets/editor_pane.dart'; |
||||
import 'package:win_text_editor/app/widgets/file_explorer.dart'; |
||||
import 'package:win_text_editor/app/widgets/console_panel.dart'; // 新增导入 |
||||
|
||||
class AppScaffold extends StatelessWidget { |
||||
const AppScaffold({super.key}); |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
return MultiProvider( |
||||
providers: [ |
||||
ChangeNotifierProvider(create: (_) => FileProvider()), |
||||
ChangeNotifierProvider(create: (_) => TabProvider()), |
||||
], |
||||
child: const Scaffold( |
||||
body: Column( |
||||
children: [ |
||||
// 菜单栏 |
||||
AppMenu(), |
||||
// 主内容区域 |
||||
Expanded(child: _ResizablePanel()), |
||||
], |
||||
), |
||||
), |
||||
); |
||||
} |
||||
} |
||||
|
||||
class _ResizablePanel extends StatefulWidget { |
||||
const _ResizablePanel(); |
||||
|
||||
@override |
||||
State<_ResizablePanel> createState() => _ResizablePanelState(); |
||||
} |
||||
|
||||
class _ResizablePanelState extends State<_ResizablePanel> { |
||||
double _leftWidth = 0.2; // 初始宽度20% |
||||
final double _minWidth = 100; // 最小宽度 |
||||
final double _maxWidth = 400; // 最大宽度 |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
final screenWidth = MediaQuery.of(context).size.width; |
||||
|
||||
return LayoutBuilder( |
||||
builder: (context, constraints) { |
||||
// 计算实际宽度 |
||||
final leftPanelWidth = (_leftWidth * screenWidth).clamp(_minWidth, _maxWidth); |
||||
|
||||
return Column( |
||||
children: [ |
||||
Expanded( |
||||
child: Row( |
||||
children: [ |
||||
// 左侧文件资源管理器 - 添加Material小部件包裹 |
||||
Material( |
||||
elevation: 1, // 轻微阴影分隔 |
||||
child: SizedBox( |
||||
width: leftPanelWidth, |
||||
child: const ClipRect( |
||||
// 确保内容被裁剪 |
||||
child: FileExplorer(), |
||||
), |
||||
), |
||||
), |
||||
// 拖拽手柄 |
||||
GestureDetector( |
||||
behavior: HitTestBehavior.translucent, |
||||
onPanUpdate: (details) { |
||||
setState(() { |
||||
_leftWidth = ((leftPanelWidth + details.delta.dx) / screenWidth).clamp( |
||||
_minWidth / screenWidth, |
||||
_maxWidth / screenWidth, |
||||
); |
||||
}); |
||||
}, |
||||
child: MouseRegion( |
||||
cursor: SystemMouseCursors.resizeLeftRight, |
||||
child: Container(width: 4, color: Colors.grey[300]), |
||||
), |
||||
), |
||||
// 右侧编辑器区域 - 添加Material背景 |
||||
const Expanded( |
||||
child: Material( |
||||
color: Colors.white, |
||||
child: Column(children: [Expanded(child: EditorPane())]), |
||||
), |
||||
), |
||||
], |
||||
), |
||||
), |
||||
// 控制台面板 |
||||
const ConsolePanel(), |
||||
], |
||||
); |
||||
}, |
||||
); |
||||
} |
||||
} |
@ -1,5 +1,5 @@
@@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart'; |
||||
import 'package:win_text_editor/menus/menu_constants.dart'; |
||||
import 'package:win_text_editor/app/menus/menu_constants.dart'; |
||||
|
||||
import 'menu_actions.dart'; |
||||
|
@ -0,0 +1,31 @@
@@ -0,0 +1,31 @@
|
||||
class SyntaxService { |
||||
static String detectFileType(String fileName) { |
||||
final extension = fileName.split('.').last.toLowerCase(); |
||||
|
||||
switch (extension) { |
||||
case 'dart': |
||||
return 'dart'; |
||||
case 'java': |
||||
return 'java'; |
||||
case 'js': |
||||
return 'javascript'; |
||||
case 'py': |
||||
return 'python'; |
||||
case 'html': |
||||
return 'html'; |
||||
case 'css': |
||||
return 'css'; |
||||
case 'json': |
||||
return 'json'; |
||||
case 'xml': |
||||
return 'xml'; |
||||
case 'md': |
||||
return 'markdown'; |
||||
case 'yaml': |
||||
case 'yml': |
||||
return 'yaml'; |
||||
default: |
||||
return 'text'; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,59 @@
@@ -0,0 +1,59 @@
|
||||
// file_utils.dart |
||||
import 'dart:async'; |
||||
import 'dart:io'; |
||||
import 'package:flutter/material.dart'; |
||||
import 'package:file_picker/file_picker.dart'; |
||||
import 'package:win_text_editor/app/providers/logger.dart'; |
||||
|
||||
class FileUtils { |
||||
static Future<String?> pickFile(BuildContext context) async { |
||||
try { |
||||
final result = await FilePicker.platform.pickFiles(type: FileType.any, allowMultiple: false); |
||||
return result?.files.single.path; |
||||
} catch (e) { |
||||
Logger().error('选择文件失败: ${e.toString()}'); |
||||
if (context.mounted) { |
||||
ScaffoldMessenger.of( |
||||
context, |
||||
).showSnackBar(SnackBar(content: Text('选择文件失败: ${e.toString()}'))); |
||||
} |
||||
return null; |
||||
} |
||||
} |
||||
|
||||
static Future<String?> readFileContent( |
||||
BuildContext context, |
||||
String filePath, { |
||||
Duration timeout = const Duration(seconds: 30), |
||||
}) async { |
||||
try { |
||||
final content = await File(filePath).readAsString().timeout( |
||||
timeout, |
||||
onTimeout: () { |
||||
throw TimeoutException('文件加载超时,可能文件过大'); |
||||
}, |
||||
); |
||||
return content; |
||||
} on FormatException { |
||||
if (context.mounted) { |
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('这不是可读的文本文件'))); |
||||
} |
||||
return null; |
||||
} on FileSystemException catch (e) { |
||||
if (context.mounted) { |
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('文件访问错误: ${e.message}'))); |
||||
} |
||||
return null; |
||||
} catch (e) { |
||||
Logger().error('读取文件失败: ${e.toString()}'); |
||||
if (context.mounted) { |
||||
ScaffoldMessenger.of( |
||||
context, |
||||
).showSnackBar(SnackBar(content: Text('读取失败: ${e.toString()}'))); |
||||
} |
||||
return null; |
||||
} |
||||
} |
||||
|
||||
// 移除showLoadingDialog方法,因为现在直接在调用处处理 |
||||
} |
@ -1,7 +1,7 @@
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart'; |
||||
import 'package:provider/provider.dart'; |
||||
import 'package:flutter/services.dart'; // 用于复制到剪贴板 |
||||
import 'package:win_text_editor/framework/controllers/logger.dart'; |
||||
import 'package:win_text_editor/app/providers/logger.dart'; |
||||
|
||||
class ConsolePanel extends StatefulWidget { |
||||
const ConsolePanel({super.key}); |
@ -0,0 +1,84 @@
@@ -0,0 +1,84 @@
|
||||
import 'package:flutter/material.dart'; |
||||
import 'package:provider/provider.dart'; |
||||
import 'package:win_text_editor/app/providers/tab_provider.dart'; |
||||
import 'template_parser_tab.dart'; |
||||
|
||||
class EditorPane extends StatelessWidget { |
||||
const EditorPane({super.key}); |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
final provider = Provider.of<TabProvider>(context); |
||||
|
||||
return Column( |
||||
children: [ |
||||
// 标签栏 |
||||
SizedBox( |
||||
height: 40, |
||||
child: ListView.builder( |
||||
scrollDirection: Axis.horizontal, |
||||
itemCount: provider.tabs.length, |
||||
itemBuilder: (ctx, index) { |
||||
final tab = provider.tabs[index]; |
||||
return _TabItem( |
||||
title: tab.title, |
||||
icon: tab.icon, // 添加图标支持 |
||||
isActive: tab.id == provider.activeTabId, |
||||
onClose: () => provider.closeTab(tab.id), |
||||
onTap: () => provider.setActiveTab(tab.id), |
||||
); |
||||
}, |
||||
), |
||||
), |
||||
// 内容区 |
||||
Expanded( |
||||
child: |
||||
provider.activeTabId != null && provider.tabs.any((t) => t.id == provider.activeTabId) |
||||
? TemplateParserTab(tabId: provider.activeTabId!) |
||||
: const Center(child: Text('无活动标签页')), |
||||
), |
||||
], |
||||
); |
||||
} |
||||
} |
||||
|
||||
class _TabItem extends StatelessWidget { |
||||
final String title; |
||||
final IconData? icon; // 添加图标 |
||||
final bool isActive; |
||||
final VoidCallback onClose; |
||||
final VoidCallback onTap; |
||||
|
||||
const _TabItem({ |
||||
required this.title, |
||||
this.icon, |
||||
required this.isActive, |
||||
required this.onClose, |
||||
required this.onTap, |
||||
}); |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
return GestureDetector( |
||||
onTap: onTap, |
||||
child: Container( |
||||
padding: const EdgeInsets.symmetric(horizontal: 16), |
||||
decoration: BoxDecoration( |
||||
color: isActive ? Colors.blue[100] : Colors.grey[200], |
||||
border: Border( |
||||
bottom: BorderSide(color: isActive ? Colors.blue : Colors.transparent, width: 2), |
||||
), |
||||
), |
||||
child: Row( |
||||
children: [ |
||||
if (icon != null) Icon(icon, size: 16), |
||||
if (icon != null) const SizedBox(width: 4), |
||||
Text(title), |
||||
const SizedBox(width: 8), |
||||
IconButton(icon: const Icon(Icons.close, size: 16), onPressed: onClose), |
||||
], |
||||
), |
||||
), |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,144 @@
@@ -0,0 +1,144 @@
|
||||
import 'package:flutter/material.dart'; |
||||
import 'package:provider/provider.dart'; |
||||
import 'package:win_text_editor/app/providers/tab_provider.dart'; |
||||
import 'package:win_text_editor/app/widgets/text_editor.dart'; |
||||
|
||||
class TemplateParserTab extends StatefulWidget { |
||||
final String tabId; |
||||
|
||||
const TemplateParserTab({super.key, required this.tabId}); |
||||
|
||||
@override |
||||
State<TemplateParserTab> createState() => TemplateParserTabState(); |
||||
} |
||||
|
||||
class TemplateParserTabState extends State<TemplateParserTab> { |
||||
late TabProvider _provider; |
||||
String? _editor1FileName; |
||||
String? _editor2FileName; |
||||
String _editor1Content = ''; |
||||
String _editor2Content = ''; |
||||
int _activeEditorIndex = 0; // 0表示第一个编辑器,1表示第二个 |
||||
final GlobalKey<TextEditorState> _editor1Key = GlobalKey(); |
||||
final GlobalKey<TextEditorState> _editor2Key = GlobalKey(); |
||||
|
||||
// 添加焦点监听 |
||||
void _setupFocusListeners() { |
||||
_editor1Key.currentState?.focusNode.addListener(() { |
||||
if (_editor1Key.currentState?.hasFocus ?? false) { |
||||
setState(() => _activeEditorIndex = 0); |
||||
} |
||||
}); |
||||
|
||||
_editor2Key.currentState?.focusNode.addListener(() { |
||||
if (_editor2Key.currentState?.hasFocus ?? false) { |
||||
setState(() => _activeEditorIndex = 1); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
@override |
||||
void initState() { |
||||
super.initState(); |
||||
_provider = Provider.of<TabProvider>(context, listen: false); |
||||
_provider.registerTextTabController(widget.tabId, this); |
||||
final tab = _provider.getTabById(widget.tabId); |
||||
if (tab != null) { |
||||
_editor1Content = tab.content; |
||||
_editor2Content = tab.content; |
||||
_editor1FileName = tab.fileName; |
||||
_editor2FileName = tab.fileName; |
||||
} |
||||
WidgetsBinding.instance.addPostFrameCallback((_) { |
||||
_setupFocusListeners(); |
||||
}); |
||||
} |
||||
|
||||
// 修改loadFile方法使用_activeEditorIndex |
||||
Future<void> loadFile(BuildContext context, String filePath) async { |
||||
if (_activeEditorIndex == 0) { |
||||
_editor1Key.currentState?.loadFile(context, filePath); |
||||
} else { |
||||
_editor2Key.currentState?.loadFile(context, filePath); |
||||
} |
||||
} |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
final tab = _provider.getTabById(widget.tabId); |
||||
if (tab == null) { |
||||
return const Center(child: Text('选项卡不存在')); |
||||
} |
||||
|
||||
return SingleChildScrollView( |
||||
child: ConstrainedBox( |
||||
constraints: const BoxConstraints(minHeight: 400), |
||||
child: Column( |
||||
mainAxisSize: MainAxisSize.min, |
||||
children: [ |
||||
Flexible( |
||||
child: SizedBox( |
||||
height: MediaQuery.of(context).size.height / 2 - 100, |
||||
child: Container( |
||||
decoration: BoxDecoration( |
||||
border: Border.all( |
||||
color: _activeEditorIndex == 0 ? Colors.blue : Colors.transparent, |
||||
width: 2.0, |
||||
), |
||||
), |
||||
child: TextEditor( |
||||
key: _editor1Key, |
||||
tabId: '${widget.tabId}_1', |
||||
title: '源文本', // 可配置的标题 |
||||
initialContent: _editor1Content, |
||||
fileName: _editor1FileName, |
||||
onContentChanged: (content, fileName) { |
||||
setState(() { |
||||
_editor1Content = content; |
||||
_editor1FileName = fileName; |
||||
}); |
||||
_provider.updateContent(widget.tabId, content, fileName); |
||||
}, |
||||
onFileLoaded: (filePath) { |
||||
// 文件加载处理 |
||||
}, |
||||
), |
||||
), |
||||
), |
||||
), |
||||
Flexible( |
||||
child: SizedBox( |
||||
height: MediaQuery.of(context).size.height / 2 - 100, |
||||
child: Container( |
||||
decoration: BoxDecoration( |
||||
border: Border.all( |
||||
color: _activeEditorIndex == 1 ? Colors.blue : Colors.transparent, |
||||
width: 2.0, |
||||
), |
||||
), |
||||
child: TextEditor( |
||||
key: _editor2Key, |
||||
tabId: '${widget.tabId}_2', |
||||
title: '目标文本', // 可配置的标题 |
||||
initialContent: _editor2Content, |
||||
fileName: _editor2FileName, |
||||
onContentChanged: (content, fileName) { |
||||
setState(() { |
||||
_editor2Content = content; |
||||
_editor2FileName = fileName; |
||||
}); |
||||
_provider.updateContent(widget.tabId, content, fileName); |
||||
}, |
||||
onFileLoaded: (filePath) { |
||||
// 文件加载处理 |
||||
}, |
||||
), |
||||
), |
||||
), |
||||
), |
||||
], |
||||
), |
||||
), |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,328 @@
@@ -0,0 +1,328 @@
|
||||
import 'dart:convert'; |
||||
import 'dart:io'; |
||||
import 'dart:ui'; |
||||
import 'package:flutter/material.dart'; |
||||
import 'package:flutter/services.dart'; |
||||
import 'package:file_picker/file_picker.dart'; |
||||
|
||||
class TextEditor extends StatefulWidget { |
||||
final String tabId; |
||||
final String? initialContent; |
||||
final String? fileName; |
||||
final String title; |
||||
final Function(String, String?)? onContentChanged; |
||||
final Function(String)? onFileLoaded; |
||||
|
||||
const TextEditor({ |
||||
super.key, |
||||
required this.tabId, |
||||
this.initialContent, |
||||
this.fileName, |
||||
this.title = '未命名', |
||||
this.onContentChanged, |
||||
this.onFileLoaded, |
||||
}); |
||||
|
||||
@override |
||||
State<TextEditor> createState() => TextEditorState(); |
||||
} |
||||
|
||||
class TextEditorState extends State<TextEditor> { |
||||
late TextEditingController _controller; |
||||
|
||||
late ScrollController _scrollController; |
||||
bool _isLoading = false; |
||||
static const int maxFileSize = 1024 * 1024; // 1MB |
||||
|
||||
bool get hasFocus => _focusNode.hasFocus; |
||||
FocusNode get _focusNode => focusNode; // 将原来的_focusNode改为focusNode |
||||
late FocusNode focusNode = FocusNode(); // 修改声明方式 |
||||
|
||||
void loadFile(BuildContext context, String filePath) async { |
||||
await _loadFile(context, filePath); |
||||
} |
||||
|
||||
@override |
||||
void initState() { |
||||
super.initState(); |
||||
_controller = TextEditingController(text: widget.initialContent ?? ''); |
||||
focusNode = FocusNode(); |
||||
_scrollController = ScrollController(); |
||||
WidgetsBinding.instance.addPostFrameCallback((_) { |
||||
FocusScope.of(context).requestFocus(focusNode); |
||||
}); |
||||
} |
||||
|
||||
@override |
||||
void didUpdateWidget(TextEditor oldWidget) { |
||||
super.didUpdateWidget(oldWidget); |
||||
if (oldWidget.initialContent != widget.initialContent) { |
||||
_controller.text = widget.initialContent ?? ''; |
||||
} |
||||
} |
||||
|
||||
@override |
||||
void dispose() { |
||||
_controller.dispose(); |
||||
_focusNode.dispose(); |
||||
_scrollController.dispose(); |
||||
super.dispose(); |
||||
} |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
return Column( |
||||
children: [ |
||||
Container( |
||||
height: 40, |
||||
padding: const EdgeInsets.symmetric(horizontal: 16), |
||||
decoration: BoxDecoration( |
||||
color: hasFocus ? Colors.blue[50] : Colors.grey[100], |
||||
border: Border( |
||||
bottom: BorderSide( |
||||
color: hasFocus ? Colors.blue : Colors.grey[300]!, |
||||
width: hasFocus ? 2.0 : 1.0, |
||||
), |
||||
), |
||||
), |
||||
child: Row( |
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
||||
children: [ |
||||
Text( |
||||
'${widget.title}${_controller.text.isEmpty ? '' : ' (${widget.fileName ?? ''}${_controller.text.length}字符)'}', |
||||
style: const TextStyle(fontWeight: FontWeight.bold), |
||||
), |
||||
Row( |
||||
children: [ |
||||
IconButton( |
||||
icon: const Icon(Icons.folder_open, size: 20), |
||||
tooltip: '打开文件', |
||||
onPressed: _isLoading ? null : () => _openFile(context), |
||||
), |
||||
IconButton( |
||||
icon: const Icon(Icons.content_copy, size: 20), |
||||
tooltip: '复制内容', |
||||
onPressed: |
||||
_controller.text.isEmpty |
||||
? null |
||||
: () => _copyToClipboard(context, _controller.text), |
||||
), |
||||
IconButton( |
||||
icon: const Icon(Icons.save, size: 20), |
||||
tooltip: '保存到文件', |
||||
onPressed: |
||||
_controller.text.isEmpty |
||||
? null |
||||
: () => _saveFile(context, _controller.text), |
||||
), |
||||
if (_isLoading) |
||||
const Padding( |
||||
padding: EdgeInsets.only(left: 8), |
||||
child: SizedBox( |
||||
width: 16, |
||||
height: 16, |
||||
child: CircularProgressIndicator(strokeWidth: 2), |
||||
), |
||||
), |
||||
], |
||||
), |
||||
], |
||||
), |
||||
), |
||||
Expanded( |
||||
child: ScrollConfiguration( |
||||
behavior: ScrollConfiguration.of(context).copyWith( |
||||
scrollbars: true, |
||||
dragDevices: {PointerDeviceKind.touch, PointerDeviceKind.mouse}, |
||||
), |
||||
child: SingleChildScrollView( |
||||
controller: _scrollController, |
||||
child: Stack( |
||||
children: [ |
||||
TextField( |
||||
controller: _controller, |
||||
focusNode: _focusNode, |
||||
maxLines: null, |
||||
onChanged: (text) { |
||||
widget.onContentChanged?.call(text, widget.fileName); |
||||
}, |
||||
decoration: const InputDecoration( |
||||
border: InputBorder.none, |
||||
contentPadding: EdgeInsets.all(16), |
||||
), |
||||
style: const TextStyle( |
||||
fontFamily: 'Courier New', |
||||
fontSize: 16, |
||||
color: Colors.black, |
||||
), |
||||
), |
||||
], |
||||
), |
||||
), |
||||
), |
||||
), |
||||
], |
||||
); |
||||
} |
||||
|
||||
Future<void> _openFile(BuildContext context) async { |
||||
final result = await FilePicker.platform.pickFiles(type: FileType.any, allowMultiple: false); |
||||
if (result != null && result.files.single.path != null) { |
||||
await _loadFile(context, result.files.single.path!); |
||||
} |
||||
} |
||||
|
||||
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('已复制到剪贴板'))); |
||||
} |
||||
} |
||||
|
||||
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()}'))); |
||||
} |
||||
} |
||||
} |
||||
|
||||
Future<void> _loadFile(BuildContext context, String filePath) async { |
||||
try { |
||||
setState(() => _isLoading = true); |
||||
final file = File(filePath); |
||||
final fileSize = await file.length(); |
||||
|
||||
// 检查文件大小 |
||||
if (fileSize > maxFileSize) { |
||||
if (context.mounted) { |
||||
ScaffoldMessenger.of( |
||||
context, |
||||
).showSnackBar(const SnackBar(content: Text('文件过大(超过1MB),无法处理'))); |
||||
} |
||||
return; |
||||
} |
||||
|
||||
// 判断当前是否有内容 |
||||
if (_controller.text.isNotEmpty) { |
||||
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.text = ''; |
||||
widget.onContentChanged?.call('', fileName); |
||||
|
||||
// 逐行读取文件 |
||||
final stream = file.openRead(); |
||||
final lines = stream.transform(utf8.decoder).transform(const LineSplitter()); |
||||
|
||||
await for (final line in lines) { |
||||
if (!mounted) break; |
||||
|
||||
setState(() { |
||||
_controller.text += '$line\n'; |
||||
widget.onContentChanged?.call(_controller.text, fileName); |
||||
}); |
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) { |
||||
_scrollController.jumpTo(_scrollController.position.maxScrollExtent); |
||||
}); |
||||
|
||||
await Future.delayed(const Duration(milliseconds: 10)); |
||||
} |
||||
|
||||
widget.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 { |
||||
if (mounted) { |
||||
setState(() => _isLoading = false); |
||||
} |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue