Browse Source
# Conflicts: # win_text_editor/lib/app/providers/editor_provider.dart # win_text_editor/lib/app/widgets/file_explorer.dart # win_text_editor/lib/app/widgets/text_tab.dartmaster
3 changed files with 208 additions and 2 deletions
@ -0,0 +1,142 @@ |
|||||||
|
import 'dart:math'; |
||||||
|
|
||||||
|
import 'package:flutter/material.dart'; |
||||||
|
import 'package:win_text_editor/app/providers/logger.dart'; |
||||||
|
|
||||||
|
class EditorProvider with ChangeNotifier { |
||||||
|
final List<EditorTab> _tabs = []; |
||||||
|
String? _activeTabId; |
||||||
|
bool _isLoading = false; |
||||||
|
|
||||||
|
List<EditorTab> get tabs => _tabs; |
||||||
|
String? get activeTabId => _activeTabId; |
||||||
|
bool get isLoading => _isLoading; |
||||||
|
|
||||||
|
int _templateTabCounter = 1; |
||||||
|
|
||||||
|
final Map<String, bool> _tabLoadingStates = {}; |
||||||
|
final Map<String, int> _tabLoadedChunks = {}; |
||||||
|
|
||||||
|
bool isTabLoading(String tabId) => _tabLoadingStates[tabId] ?? false; |
||||||
|
int getLoadedChunks(String tabId) => _tabLoadedChunks[tabId] ?? 0; |
||||||
|
|
||||||
|
EditorTab? get activeTab { |
||||||
|
if (_activeTabId == null) return null; |
||||||
|
try { |
||||||
|
return _tabs.firstWhere((tab) => tab.id == _activeTabId); |
||||||
|
} catch (e) { |
||||||
|
Logger().error("找不到活动选项卡: $_activeTabId", source: 'EditorProvider'); |
||||||
|
return null; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void addTab() { |
||||||
|
final tabId = DateTime.now().millisecondsSinceEpoch.toString(); |
||||||
|
_tabs.add( |
||||||
|
EditorTab( |
||||||
|
id: tabId, |
||||||
|
title: '模板解析[$_templateTabCounter]', |
||||||
|
chunks: [], // 显式初始化 |
||||||
|
content: '', |
||||||
|
), |
||||||
|
); |
||||||
|
_templateTabCounter++; |
||||||
|
_activeTabId = tabId; |
||||||
|
notifyListeners(); |
||||||
|
} |
||||||
|
|
||||||
|
void closeTab(String tabId) { |
||||||
|
Logger().info('关闭选项卡: $tabId'); |
||||||
|
_tabs.removeWhere((tab) => tab.id == tabId); |
||||||
|
if (_activeTabId == tabId) { |
||||||
|
_activeTabId = _tabs.isNotEmpty ? _tabs.last.id : null; |
||||||
|
} |
||||||
|
notifyListeners(); |
||||||
|
} |
||||||
|
|
||||||
|
void setActiveTab(String tabId) { |
||||||
|
Logger().info('设置活动选项卡: $tabId'); |
||||||
|
_activeTabId = tabId; |
||||||
|
notifyListeners(); |
||||||
|
} |
||||||
|
|
||||||
|
Future<void> updateContent(String tabId, String content, String? name) async { |
||||||
|
_isLoading = true; |
||||||
|
notifyListeners(); |
||||||
|
|
||||||
|
try { |
||||||
|
final index = _tabs.indexWhere((t) => t.id == tabId); |
||||||
|
if (index == -1) throw Exception("Tab not found"); |
||||||
|
|
||||||
|
_tabs[index] = EditorTab( |
||||||
|
id: _tabs[index].id, |
||||||
|
title: _tabs[index].title, |
||||||
|
chunks: content.split('\n'), // 同步更新 chunks |
||||||
|
fileName: name ?? _tabs[index].fileName, |
||||||
|
content: content, |
||||||
|
); |
||||||
|
} finally { |
||||||
|
_isLoading = false; |
||||||
|
notifyListeners(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 在 EditorProvider 中修改文件加载逻辑 |
||||||
|
Future<void> updateContentInChunks(String tabId, String fullContent, String? fileName) async { |
||||||
|
final lines = fullContent.split('\n'); |
||||||
|
const linesPerChunk = 1000; // 每1000行一个块 |
||||||
|
|
||||||
|
_tabLoadingStates[tabId] = true; |
||||||
|
notifyListeners(); |
||||||
|
|
||||||
|
try { |
||||||
|
// 清空现有内容 |
||||||
|
final index = _tabs.indexWhere((t) => t.id == tabId); |
||||||
|
_tabs[index] = EditorTab( |
||||||
|
id: tabId, |
||||||
|
title: fileName ?? _tabs[index].title, |
||||||
|
chunks: [], |
||||||
|
fileName: fileName, |
||||||
|
); |
||||||
|
|
||||||
|
// 分块加载行 |
||||||
|
for (int i = 0; i < lines.length; i += linesPerChunk) { |
||||||
|
if (!_tabLoadingStates[tabId]!) break; // 检查是否取消 |
||||||
|
|
||||||
|
final chunkLines = lines.sublist(i, min(i + linesPerChunk, lines.length)); |
||||||
|
_tabs[index].chunks!.addAll(chunkLines); |
||||||
|
|
||||||
|
notifyListeners(); |
||||||
|
await Future.delayed(Duration.zero); // 让UI更新 |
||||||
|
} |
||||||
|
} finally { |
||||||
|
_tabLoadingStates.remove(tabId); |
||||||
|
notifyListeners(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void cancelLoading(String tabId) { |
||||||
|
_tabLoadingStates[tabId] = false; |
||||||
|
notifyListeners(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class EditorTab { |
||||||
|
final String id; |
||||||
|
String title; |
||||||
|
List<String> chunks; // 移除可空标记,改为非空 |
||||||
|
String? fileName; |
||||||
|
bool isHighlightEnabled; |
||||||
|
String content; |
||||||
|
|
||||||
|
EditorTab({ |
||||||
|
required this.id, |
||||||
|
required this.title, |
||||||
|
List<String>? chunks, // 参数可选,但类内部非空 |
||||||
|
this.fileName, |
||||||
|
this.isHighlightEnabled = true, |
||||||
|
this.content = '', |
||||||
|
}) : chunks = chunks ?? []; // 默认值为空列表 |
||||||
|
|
||||||
|
String get fullContent => chunks.join('\n'); // 用换行符连接 |
||||||
|
} |
@ -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方法,因为现在直接在调用处处理 |
||||||
|
} |
Loading…
Reference in new issue