|
|
|
@ -1,11 +1,11 @@
@@ -1,11 +1,11 @@
|
|
|
|
|
import 'dart:convert'; |
|
|
|
|
import 'dart:ui'; |
|
|
|
|
|
|
|
|
|
import 'dart:io'; |
|
|
|
|
import 'package:flutter/material.dart'; |
|
|
|
|
import 'package:provider/provider.dart'; |
|
|
|
|
import 'package:flutter/services.dart'; // 复制功能需要 |
|
|
|
|
import 'package:flutter/services.dart'; |
|
|
|
|
import 'package:win_text_editor/app/providers/editor_provider.dart'; |
|
|
|
|
import 'package:file_picker/file_picker.dart'; |
|
|
|
|
import 'dart:io'; |
|
|
|
|
|
|
|
|
|
class TextTab extends StatefulWidget { |
|
|
|
|
final String tabId; |
|
|
|
@ -21,8 +21,9 @@ class _TextTabState extends State<TextTab> {
@@ -21,8 +21,9 @@ class _TextTabState extends State<TextTab> {
|
|
|
|
|
late EditorProvider _provider; |
|
|
|
|
late FocusNode _focusNode; |
|
|
|
|
late ScrollController _scrollController; |
|
|
|
|
bool _isLoading = false; |
|
|
|
|
static const int maxFileSize = 10 * 1024 * 1024; // 10MB |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
@override |
|
|
|
|
void initState() { |
|
|
|
|
super.initState(); |
|
|
|
@ -51,7 +52,7 @@ class _TextTabState extends State<TextTab> {
@@ -51,7 +52,7 @@ class _TextTabState extends State<TextTab> {
|
|
|
|
|
void dispose() { |
|
|
|
|
_controller.dispose(); |
|
|
|
|
_focusNode.dispose(); |
|
|
|
|
_scrollController.dispose(); // 添加这行 |
|
|
|
|
_scrollController.dispose(); |
|
|
|
|
super.dispose(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -80,7 +81,7 @@ class _TextTabState extends State<TextTab> {
@@ -80,7 +81,7 @@ class _TextTabState extends State<TextTab> {
|
|
|
|
|
IconButton( |
|
|
|
|
icon: const Icon(Icons.folder_open, size: 20), |
|
|
|
|
tooltip: '打开文件', |
|
|
|
|
onPressed: () => _openFile(context), |
|
|
|
|
onPressed: _isLoading ? null : () => _openFile(context), |
|
|
|
|
), |
|
|
|
|
IconButton( |
|
|
|
|
icon: const Icon(Icons.content_copy, size: 20), |
|
|
|
@ -93,6 +94,15 @@ class _TextTabState extends State<TextTab> {
@@ -93,6 +94,15 @@ class _TextTabState extends State<TextTab> {
|
|
|
|
|
tooltip: '保存到文件', |
|
|
|
|
onPressed: tab.content.isEmpty ? null : () => _saveFile(context, tab.content), |
|
|
|
|
), |
|
|
|
|
if (_isLoading) |
|
|
|
|
const Padding( |
|
|
|
|
padding: EdgeInsets.only(left: 8), |
|
|
|
|
child: SizedBox( |
|
|
|
|
width: 16, |
|
|
|
|
height: 16, |
|
|
|
|
child: CircularProgressIndicator(strokeWidth: 2), |
|
|
|
|
), |
|
|
|
|
), |
|
|
|
|
], |
|
|
|
|
), |
|
|
|
|
], |
|
|
|
@ -105,10 +115,9 @@ class _TextTabState extends State<TextTab> {
@@ -105,10 +115,9 @@ class _TextTabState extends State<TextTab> {
|
|
|
|
|
dragDevices: {PointerDeviceKind.touch, PointerDeviceKind.mouse}, |
|
|
|
|
), |
|
|
|
|
child: SingleChildScrollView( |
|
|
|
|
controller: _scrollController, // 添加滚动控制器 |
|
|
|
|
controller: _scrollController, |
|
|
|
|
child: Stack( |
|
|
|
|
children: [ |
|
|
|
|
// 实际可编辑的TextField |
|
|
|
|
TextField( |
|
|
|
|
controller: _controller, |
|
|
|
|
focusNode: _focusNode, |
|
|
|
@ -121,7 +130,7 @@ class _TextTabState extends State<TextTab> {
@@ -121,7 +130,7 @@ class _TextTabState extends State<TextTab> {
|
|
|
|
|
style: TextStyle( |
|
|
|
|
fontFamily: 'monospace', |
|
|
|
|
fontSize: 14, |
|
|
|
|
color: Theme.of(context).textTheme.bodyLarge?.color, // 使用主题文本颜色 |
|
|
|
|
color: Theme.of(context).textTheme.bodyLarge?.color, |
|
|
|
|
), |
|
|
|
|
), |
|
|
|
|
], |
|
|
|
@ -135,34 +144,74 @@ class _TextTabState extends State<TextTab> {
@@ -135,34 +144,74 @@ 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 content = await file.readAsString(); |
|
|
|
|
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()); |
|
|
|
|
|
|
|
|
|
// 更新provider和控制器 |
|
|
|
|
_provider.updateContent(widget.tabId, content, result.files.first.name); |
|
|
|
|
_controller.text = content; // 确保更新控制器 |
|
|
|
|
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 { |
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('这不是可读的文本文件'))); |
|
|
|
|
if (context.mounted) { |
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('这不是可读的文本文件'))); |
|
|
|
|
} |
|
|
|
|
} on FileSystemException catch (e) { |
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('文件访问错误: ${e.message}'))); |
|
|
|
|
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); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 复制到剪贴板 |
|
|
|
|
Future<void> _copyToClipboard(BuildContext context, String content) async { |
|
|
|
|
await Clipboard.setData(ClipboardData(text: content)); |
|
|
|
|
if (context.mounted) { |
|
|
|
@ -170,11 +219,8 @@ class _TextTabState extends State<TextTab> {
@@ -170,11 +219,8 @@ class _TextTabState extends State<TextTab> {
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 保存文件模拟功能 |
|
|
|
|
// 保存文件功能 |
|
|
|
|
Future<void> _saveFile(BuildContext context, String content) async { |
|
|
|
|
try { |
|
|
|
|
// 让用户选择保存位置 |
|
|
|
|
String? outputPath = await FilePicker.platform.saveFile( |
|
|
|
|
dialogTitle: '保存文件', |
|
|
|
|
fileName: 'untitled.txt', |
|
|
|
@ -186,9 +232,7 @@ class _TextTabState extends State<TextTab> {
@@ -186,9 +232,7 @@ class _TextTabState extends State<TextTab> {
|
|
|
|
|
|
|
|
|
|
final file = File(outputPath); |
|
|
|
|
|
|
|
|
|
// 检查文件是否存在 |
|
|
|
|
if (await file.exists()) { |
|
|
|
|
// 如果存在,显示确认对话框 |
|
|
|
|
final shouldOverwrite = await showDialog<bool>( |
|
|
|
|
context: context, |
|
|
|
|
builder: |
|
|
|
@ -211,7 +255,6 @@ class _TextTabState extends State<TextTab> {
@@ -211,7 +255,6 @@ class _TextTabState extends State<TextTab> {
|
|
|
|
|
if (shouldOverwrite != true) return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 写入文件 |
|
|
|
|
await file.writeAsString(content); |
|
|
|
|
|
|
|
|
|
if (context.mounted) { |
|
|
|
|