Browse Source

文件加载正常

master
hejl 2 months ago
parent
commit
940aa9466b
  1. 31
      win_text_editor/lib/app/menus/menu_actions.dart
  2. 19
      win_text_editor/lib/app/models/tab_model.dart
  3. 76
      win_text_editor/lib/app/providers/editor_provider.dart
  4. 1
      win_text_editor/lib/app/widgets/console_panel.dart
  5. 7
      win_text_editor/lib/app/widgets/editor_pane.dart
  6. 52
      win_text_editor/lib/app/widgets/file_explorer.dart
  7. 182
      win_text_editor/lib/app/widgets/text_tab.dart
  8. 2
      win_text_editor/pubspec.lock
  9. 3
      win_text_editor/pubspec.yaml

31
win_text_editor/lib/app/menus/menu_actions.dart

@ -4,16 +4,23 @@ import 'package:provider/provider.dart';
import 'package:win_text_editor/app/menus/menu_constants.dart'; import 'package:win_text_editor/app/menus/menu_constants.dart';
import 'package:win_text_editor/app/providers/file_provider.dart'; import 'package:win_text_editor/app/providers/file_provider.dart';
import 'package:win_text_editor/app/providers/editor_provider.dart'; import 'package:win_text_editor/app/providers/editor_provider.dart';
import 'package:collection/collection.dart';
import 'dart:io'; import 'dart:io';
class MenuActions { class MenuActions {
//
static const String templateParserTabType = "template_parser";
static const String templateParserTabTitle = "模板解析";
static const IconData templateParserTabIcon = Icons.auto_awesome_mosaic;
static const String templateParserDefaultContent = "";
static Future<void> handleMenuAction(String value, BuildContext context) async { static Future<void> handleMenuAction(String value, BuildContext context) async {
switch (value) { switch (value) {
case MenuConstants.openFolder: case MenuConstants.openFolder:
await _openFolder(context); await _openFolder(context);
break; break;
case MenuConstants.templateParser: case MenuConstants.templateParser:
_openTemplateParser(context); await _openTemplateParser(context);
break; break;
case MenuConstants.exit: case MenuConstants.exit:
_exitApplication(); _exitApplication();
@ -31,8 +38,26 @@ class MenuActions {
} }
} }
static void _openTemplateParser(BuildContext context) { static Future<void> _openTemplateParser(BuildContext context) async {
Provider.of<EditorProvider>(context, listen: false).addTab(); final editorProvider = Provider.of<EditorProvider>(context, listen: false);
// 使 firstWhereOrNull
final existingTab = editorProvider.tabs.firstWhereOrNull(
(tab) => tab.type == templateParserTabType,
);
if (existingTab != null) {
//
editorProvider.setActiveTab(existingTab.id);
} else {
//
await editorProvider.addTab(
title: templateParserTabTitle,
type: templateParserTabType,
icon: templateParserTabIcon,
content: templateParserDefaultContent,
);
}
} }
static void _exitApplication() { static void _exitApplication() {

19
win_text_editor/lib/app/models/tab_model.dart

@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
class EditorTab {
final String id;
final String title;
final String? type; //
final IconData? icon; //
String content;
String? fileName;
EditorTab({
required this.id,
required this.title,
this.type,
this.icon,
this.content = '',
this.fileName,
});
}

76
win_text_editor/lib/app/providers/editor_provider.dart

@ -1,5 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:win_text_editor/app/models/tab_model.dart';
import 'package:win_text_editor/app/providers/logger.dart'; import 'package:win_text_editor/app/providers/logger.dart';
import 'package:win_text_editor/app/widgets/text_tab.dart';
class EditorProvider with ChangeNotifier { class EditorProvider with ChangeNotifier {
final List<EditorTab> _tabs = []; final List<EditorTab> _tabs = [];
@ -8,7 +10,15 @@ class EditorProvider with ChangeNotifier {
List<EditorTab> get tabs => _tabs; List<EditorTab> get tabs => _tabs;
String? get activeTabId => _activeTabId; String? get activeTabId => _activeTabId;
int _templateTabCounter = 1; final Map<String, TextTabState> _tabControllers = {};
void registerTextTabController(String tabId, TextTabState controller) {
_tabControllers[tabId] = controller;
}
void unregisterTextTabController(String tabId) {
_tabControllers.remove(tabId);
}
EditorTab? get activeTab { EditorTab? get activeTab {
if (_activeTabId == null) return null; if (_activeTabId == null) return null;
@ -20,14 +30,34 @@ class EditorProvider with ChangeNotifier {
} }
} }
void addTab() { Future<void> addTab({
final tabId = DateTime.now().millisecondsSinceEpoch.toString(); String title = '未命名',
_tabs.add(EditorTab(id: tabId, title: '模板解析[$_templateTabCounter]', content: '')); String? type,
_templateTabCounter++; IconData? icon,
_activeTabId = tabId; String content = '',
}) async {
final newTab = EditorTab(
id: DateTime.now().millisecondsSinceEpoch.toString(),
title: title,
type: type,
icon: icon,
content: content,
);
_tabs.add(newTab);
_activeTabId = newTab.id;
notifyListeners(); notifyListeners();
} }
EditorTab? getTabById(String tabId) {
try {
return _tabs.firstWhere((tab) => tab.id == tabId);
} catch (e) {
Logger().error("找不到选项卡: ${tabId}", source: 'EditorProvider');
return null;
}
}
void closeTab(String tabId) { void closeTab(String tabId) {
Logger().info('关闭选项卡: $tabId'); Logger().info('关闭选项卡: $tabId');
_tabs.removeWhere((tab) => tab.id == tabId); _tabs.removeWhere((tab) => tab.id == tabId);
@ -57,13 +87,33 @@ class EditorProvider with ChangeNotifier {
Logger().error("更新内容失败: ${e.toString()}", source: 'EditorProvider'); Logger().error("更新内容失败: ${e.toString()}", source: 'EditorProvider');
} }
} }
}
class EditorTab { Future<void> requestLoadFile(BuildContext context, String filePath) async {
final String id; if (_activeTabId == null) {
String title; Logger().warning("没有活动选项卡,无法加载文件");
String content; return;
String? fileName; }
final textTabState = _tabControllers[_activeTabId];
if (textTabState == null) {
Logger().warning("找不到 TextTab 状态");
return;
}
if (!textTabState.mounted) {
Logger().warning("TextTab 状态组件未挂载");
return;
}
EditorTab({required this.id, required this.title, required this.content, this.fileName}); try {
await textTabState.loadFile(context, filePath);
} catch (e) {
Logger().error("加载文件失败: ${e.toString()}");
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('加载文件失败: ${e.toString()}')));
}
}
}
} }

1
win_text_editor/lib/app/widgets/console_panel.dart

@ -15,7 +15,6 @@ class _ConsolePanelState extends State<ConsolePanel> {
final double _minHeight = 50; final double _minHeight = 50;
final double _maxHeight = 300; final double _maxHeight = 300;
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
String? _selectedLog; //
@override @override
void dispose() { void dispose() {

7
win_text_editor/lib/app/widgets/editor_pane.dart

@ -22,6 +22,7 @@ class EditorPane extends StatelessWidget {
final tab = provider.tabs[index]; final tab = provider.tabs[index];
return _TabItem( return _TabItem(
title: tab.title, title: tab.title,
icon: tab.icon, //
isActive: tab.id == provider.activeTabId, isActive: tab.id == provider.activeTabId,
onClose: () => provider.closeTab(tab.id), onClose: () => provider.closeTab(tab.id),
onTap: () => provider.setActiveTab(tab.id), onTap: () => provider.setActiveTab(tab.id),
@ -32,7 +33,7 @@ class EditorPane extends StatelessWidget {
// //
Expanded( Expanded(
child: child:
provider.activeTabId != null provider.activeTabId != null && provider.tabs.any((t) => t.id == provider.activeTabId)
? TextTab(tabId: provider.activeTabId!) ? TextTab(tabId: provider.activeTabId!)
: const Center(child: Text('无活动标签页')), : const Center(child: Text('无活动标签页')),
), ),
@ -43,12 +44,14 @@ class EditorPane extends StatelessWidget {
class _TabItem extends StatelessWidget { class _TabItem extends StatelessWidget {
final String title; final String title;
final IconData? icon; //
final bool isActive; final bool isActive;
final VoidCallback onClose; final VoidCallback onClose;
final VoidCallback onTap; final VoidCallback onTap;
const _TabItem({ const _TabItem({
required this.title, required this.title,
this.icon,
required this.isActive, required this.isActive,
required this.onClose, required this.onClose,
required this.onTap, required this.onTap,
@ -68,6 +71,8 @@ class _TabItem extends StatelessWidget {
), ),
child: Row( child: Row(
children: [ children: [
if (icon != null) Icon(icon, size: 16),
if (icon != null) const SizedBox(width: 4),
Text(title), Text(title),
const SizedBox(width: 8), const SizedBox(width: 8),
IconButton(icon: const Icon(Icons.close, size: 16), onPressed: onClose), IconButton(icon: const Icon(Icons.close, size: 16), onPressed: onClose),

52
win_text_editor/lib/app/widgets/file_explorer.dart

@ -1,13 +1,11 @@
import 'dart:io';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:win_text_editor/app/providers/editor_provider.dart';
import 'package:win_text_editor/app/providers/logger.dart'; import 'package:win_text_editor/app/providers/logger.dart';
import '../models/file_node.dart'; import '../models/file_node.dart';
import '../providers/file_provider.dart'; import '../providers/file_provider.dart';
import '../providers/editor_provider.dart';
import 'dart:math'; import 'dart:math';
@ -57,46 +55,22 @@ class _FileExplorerState extends State<FileExplorer> {
final fileProvider = Provider.of<FileProvider>(context, listen: false); final fileProvider = Provider.of<FileProvider>(context, listen: false);
if (node.isDirectory) { if (node.isDirectory) {
await fileProvider.loadDirectoryContents(node); await fileProvider.loadDirectoryContents(node);
} else {
// Handle file opening
print("No active tab found");
_openFileInEditor(context, node);
} }
//
} }
Future<void> _openFileInEditor(BuildContext context, FileNode node) async { Future<void> _openFileInEditor(BuildContext context, FileNode node) async {
try { if (!node.isDirectory) {
Logger().info('尝试打开文件: ${node.path}'); try {
final editorProvider = Provider.of<EditorProvider>(context, listen: false); final editorProvider = Provider.of<EditorProvider>(context, listen: false);
final content = await File(node.path).readAsString(); await editorProvider.requestLoadFile(context, node.path);
} catch (e) {
Logger().debug('文件内容读取成功,长度: ${content.length}'); Logger().error("打开文件失败: ${e.toString()}");
Logger().debug('当前活动选项卡ID: ${editorProvider.activeTabId}'); if (context.mounted) {
Logger().debug('现有选项卡数量: ${editorProvider.tabs.length}'); ScaffoldMessenger.of(
context,
// ).showSnackBar(SnackBar(content: Text('打开文件失败: ${e.toString()}')));
if (editorProvider.activeTabId == null) { }
Logger().info('没有活动选项卡,创建新选项卡');
editorProvider.addTab();
}
//
Logger().info('准备更新选项卡内容');
editorProvider.updateContent(editorProvider.activeTabId!, content, node.name);
if (context.mounted) {
Logger().debug('已加载: ${node.name}');
}
} on FormatException {
Logger().warning('文件格式异常: ${node.path}');
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('这不是可读的文本文件')));
}
} catch (e) {
Logger().error('打开文件失败: ${e.toString()}', source: 'FileExplorer');
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('读取失败: ${e.toString()}')));
} }
} }
} }

182
win_text_editor/lib/app/widgets/text_tab.dart

@ -1,6 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:ui'; import 'dart:ui';
import 'dart:io'; import 'dart:io';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -13,21 +14,22 @@ class TextTab extends StatefulWidget {
const TextTab({super.key, required this.tabId}); const TextTab({super.key, required this.tabId});
@override @override
State<TextTab> createState() => _TextTabState(); State<TextTab> createState() => TextTabState();
} }
class _TextTabState extends State<TextTab> { class TextTabState extends State<TextTab> {
late TextEditingController _controller; late TextEditingController _controller;
late EditorProvider _provider; late EditorProvider _provider;
late FocusNode _focusNode; late FocusNode _focusNode;
late ScrollController _scrollController; late ScrollController _scrollController;
bool _isLoading = false; bool _isLoading = false;
static const int maxFileSize = 10 * 1024 * 1024; // 10MB static const int maxFileSize = 1024 * 1024; // 1MB
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_provider = Provider.of<EditorProvider>(context, listen: false); _provider = Provider.of<EditorProvider>(context, listen: false);
_provider.registerTextTabController(widget.tabId, this);
_controller = TextEditingController(text: _getCurrentContent()); _controller = TextEditingController(text: _getCurrentContent());
_focusNode = FocusNode(); _focusNode = FocusNode();
_scrollController = ScrollController(); _scrollController = ScrollController();
@ -58,7 +60,13 @@ class _TextTabState extends State<TextTab> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final tab = _provider.tabs.firstWhere((t) => t.id == widget.tabId); final tab = _provider.tabs.firstWhereOrNull((t) => t.id == widget.tabId);
if (tab == null) {
return const Center(child: Text('选项卡不存在'));
}
String fileNameText =
tab.fileName != null && tab.fileName!.isNotEmpty ? '${tab.fileName},' : '';
return Column( return Column(
children: [ children: [
@ -73,7 +81,7 @@ class _TextTabState extends State<TextTab> {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
'源文本${tab.content.isEmpty ? '' : ' (${tab.fileName!.isEmpty ? '' : '${tab.fileName},'}${tab.content.length})'}', '源文本${tab.content.isEmpty ? '' : ' ($fileNameText${tab.content.length}字符)'}',
style: const TextStyle(fontWeight: FontWeight.bold), style: const TextStyle(fontWeight: FontWeight.bold),
), ),
Row( Row(
@ -143,72 +151,9 @@ class _TextTabState extends State<TextTab> {
} }
Future<void> _openFile(BuildContext context) async { Future<void> _openFile(BuildContext context) async {
try { final result = await FilePicker.platform.pickFiles(type: FileType.any, allowMultiple: false);
setState(() => _isLoading = true); if (result != null && result.files.single.path != null) {
await loadFile(context, result.files.single.path!);
final result = await FilePicker.platform.pickFiles(type: FileType.any, allowMultiple: false);
if (result != null && result.files.single.path != null) {
final file = File(result.files.single.path!);
final fileSize = await file.length();
//
if (fileSize > maxFileSize) {
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('文件过大(超过10MB),无法处理')));
}
return;
}
//
_provider.updateContent(widget.tabId, '', result.files.first.name);
_controller.text = '';
//
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';
_provider.updateContent(widget.tabId, _controller.text, result.files.first.name);
});
//
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
});
//
await Future.delayed(const Duration(milliseconds: 10));
}
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);
}
} }
} }
@ -272,4 +217,99 @@ class _TextTabState extends State<TextTab> {
} }
} }
} }
//
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;
}
//
final activeTab = _provider.getTabById(widget.tabId);
if (activeTab != null && activeTab.content.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;
//
_provider.updateContent(widget.tabId, '', _fileName);
_controller.text = '';
//
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';
_provider.updateContent(widget.tabId, _controller.text, _fileName);
});
//
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
});
//
await Future.delayed(const Duration(milliseconds: 10));
}
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);
}
}
}
} }

2
win_text_editor/pubspec.lock

@ -74,7 +74,7 @@ packages:
source: hosted source: hosted
version: "1.1.2" version: "1.1.2"
collection: collection:
dependency: transitive dependency: "direct main"
description: description:
name: collection name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"

3
win_text_editor/pubspec.yaml

@ -16,8 +16,7 @@ dependencies:
bitsdojo_window: ^0.1.1+2 bitsdojo_window: ^0.1.1+2
flutter_syntax_view: ^4.1.7 flutter_syntax_view: ^4.1.7
expandable: ^5.0.1 expandable: ^5.0.1
collection: ^1.17.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

Loading…
Cancel
Save