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'; @@ -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/providers/file_provider.dart';
import 'package:win_text_editor/app/providers/editor_provider.dart';
import 'package:collection/collection.dart';
import 'dart:io';
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 {
switch (value) {
case MenuConstants.openFolder:
await _openFolder(context);
break;
case MenuConstants.templateParser:
_openTemplateParser(context);
await _openTemplateParser(context);
break;
case MenuConstants.exit:
_exitApplication();
@ -31,8 +38,26 @@ class MenuActions { @@ -31,8 +38,26 @@ class MenuActions {
}
}
static void _openTemplateParser(BuildContext context) {
Provider.of<EditorProvider>(context, listen: false).addTab();
static Future<void> _openTemplateParser(BuildContext context) async {
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() {

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

@ -0,0 +1,19 @@ @@ -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 @@ @@ -1,5 +1,7 @@
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/widgets/text_tab.dart';
class EditorProvider with ChangeNotifier {
final List<EditorTab> _tabs = [];
@ -8,7 +10,15 @@ class EditorProvider with ChangeNotifier { @@ -8,7 +10,15 @@ class EditorProvider with ChangeNotifier {
List<EditorTab> get tabs => _tabs;
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 {
if (_activeTabId == null) return null;
@ -20,14 +30,34 @@ class EditorProvider with ChangeNotifier { @@ -20,14 +30,34 @@ class EditorProvider with ChangeNotifier {
}
}
void addTab() {
final tabId = DateTime.now().millisecondsSinceEpoch.toString();
_tabs.add(EditorTab(id: tabId, title: '模板解析[$_templateTabCounter]', content: ''));
_templateTabCounter++;
_activeTabId = tabId;
Future<void> addTab({
String title = '未命名',
String? type,
IconData? icon,
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();
}
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) {
Logger().info('关闭选项卡: $tabId');
_tabs.removeWhere((tab) => tab.id == tabId);
@ -57,13 +87,33 @@ class EditorProvider with ChangeNotifier { @@ -57,13 +87,33 @@ class EditorProvider with ChangeNotifier {
Logger().error("更新内容失败: ${e.toString()}", source: 'EditorProvider');
}
}
}
class EditorTab {
final String id;
String title;
String content;
String? fileName;
Future<void> requestLoadFile(BuildContext context, String filePath) async {
if (_activeTabId == null) {
Logger().warning("没有活动选项卡,无法加载文件");
return;
}
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> { @@ -15,7 +15,6 @@ class _ConsolePanelState extends State<ConsolePanel> {
final double _minHeight = 50;
final double _maxHeight = 300;
final ScrollController _scrollController = ScrollController();
String? _selectedLog; //
@override
void dispose() {

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

@ -22,6 +22,7 @@ class EditorPane extends StatelessWidget { @@ -22,6 +22,7 @@ class EditorPane extends StatelessWidget {
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),
@ -32,7 +33,7 @@ class EditorPane extends StatelessWidget { @@ -32,7 +33,7 @@ class EditorPane extends StatelessWidget {
//
Expanded(
child:
provider.activeTabId != null
provider.activeTabId != null && provider.tabs.any((t) => t.id == provider.activeTabId)
? TextTab(tabId: provider.activeTabId!)
: const Center(child: Text('无活动标签页')),
),
@ -43,12 +44,14 @@ class EditorPane extends StatelessWidget { @@ -43,12 +44,14 @@ class EditorPane extends StatelessWidget {
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,
@ -68,6 +71,8 @@ class _TabItem extends StatelessWidget { @@ -68,6 +71,8 @@ class _TabItem extends StatelessWidget {
),
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),

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

@ -1,13 +1,11 @@ @@ -1,13 +1,11 @@
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.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 '../models/file_node.dart';
import '../providers/file_provider.dart';
import '../providers/editor_provider.dart';
import 'dart:math';
@ -57,46 +55,22 @@ class _FileExplorerState extends State<FileExplorer> { @@ -57,46 +55,22 @@ class _FileExplorerState extends State<FileExplorer> {
final fileProvider = Provider.of<FileProvider>(context, listen: false);
if (node.isDirectory) {
await fileProvider.loadDirectoryContents(node);
} else {
// Handle file opening
print("No active tab found");
_openFileInEditor(context, node);
}
//
}
Future<void> _openFileInEditor(BuildContext context, FileNode node) async {
try {
Logger().info('尝试打开文件: ${node.path}');
final editorProvider = Provider.of<EditorProvider>(context, listen: false);
final content = await File(node.path).readAsString();
Logger().debug('文件内容读取成功,长度: ${content.length}');
Logger().debug('当前活动选项卡ID: ${editorProvider.activeTabId}');
Logger().debug('现有选项卡数量: ${editorProvider.tabs.length}');
//
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()}')));
if (!node.isDirectory) {
try {
final editorProvider = Provider.of<EditorProvider>(context, listen: false);
await editorProvider.requestLoadFile(context, node.path);
} catch (e) {
Logger().error("打开文件失败: ${e.toString()}");
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 @@ @@ -1,6 +1,7 @@
import 'dart:convert';
import 'dart:ui';
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter/services.dart';
@ -13,21 +14,22 @@ class TextTab extends StatefulWidget { @@ -13,21 +14,22 @@ class TextTab extends StatefulWidget {
const TextTab({super.key, required this.tabId});
@override
State<TextTab> createState() => _TextTabState();
State<TextTab> createState() => TextTabState();
}
class _TextTabState extends State<TextTab> {
class TextTabState extends State<TextTab> {
late TextEditingController _controller;
late EditorProvider _provider;
late FocusNode _focusNode;
late ScrollController _scrollController;
bool _isLoading = false;
static const int maxFileSize = 10 * 1024 * 1024; // 10MB
static const int maxFileSize = 1024 * 1024; // 1MB
@override
void initState() {
super.initState();
_provider = Provider.of<EditorProvider>(context, listen: false);
_provider.registerTextTabController(widget.tabId, this);
_controller = TextEditingController(text: _getCurrentContent());
_focusNode = FocusNode();
_scrollController = ScrollController();
@ -58,7 +60,13 @@ class _TextTabState extends State<TextTab> { @@ -58,7 +60,13 @@ class _TextTabState extends State<TextTab> {
@override
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(
children: [
@ -73,7 +81,7 @@ class _TextTabState extends State<TextTab> { @@ -73,7 +81,7 @@ class _TextTabState extends State<TextTab> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
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),
),
Row(
@ -143,72 +151,9 @@ class _TextTabState extends State<TextTab> { @@ -143,72 +151,9 @@ class _TextTabState extends State<TextTab> {
}
Future<void> _openFile(BuildContext context) async {
try {
setState(() => _isLoading = true);
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);
}
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!);
}
}
@ -272,4 +217,99 @@ class _TextTabState extends State<TextTab> { @@ -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: @@ -74,7 +74,7 @@ packages:
source: hosted
version: "1.1.2"
collection:
dependency: transitive
dependency: "direct main"
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"

3
win_text_editor/pubspec.yaml

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

Loading…
Cancel
Save