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'; import 'package:win_text_editor/app/providers/editor_provider.dart'; import 'package:file_picker/file_picker.dart'; class TextTab extends StatefulWidget { final String tabId; const TextTab({super.key, required this.tabId}); @override State createState() => TextTabState(); } class TextTabState extends State { late TextEditingController _controller; late EditorProvider _provider; late FocusNode _focusNode; late ScrollController _scrollController; bool _isLoading = false; static const int maxFileSize = 1024 * 1024; // 1MB @override void initState() { super.initState(); _provider = Provider.of(context, listen: false); _provider.registerTextTabController(widget.tabId, this); _controller = TextEditingController(text: _getCurrentContent()); _focusNode = FocusNode(); _scrollController = ScrollController(); WidgetsBinding.instance.addPostFrameCallback((_) { FocusScope.of(context).requestFocus(_focusNode); }); } String _getCurrentContent() { return _provider.tabs.firstWhere((t) => t.id == widget.tabId).content; } @override void didUpdateWidget(TextTab oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.tabId != widget.tabId) { _controller.text = _getCurrentContent(); } } @override void dispose() { _controller.dispose(); _focusNode.dispose(); _scrollController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { 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: [ Container( height: 40, padding: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( color: Colors.grey[100], border: Border(bottom: BorderSide(color: Colors.grey[300]!)), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '源文本${tab.content.isEmpty ? '' : ' ($fileNameText${tab.content.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: tab.content.isEmpty ? null : () => _copyToClipboard(context, tab.content), ), IconButton( icon: const Icon(Icons.save, size: 20), 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), ), ), ], ), ], ), ), 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) => _provider.updateContent(widget.tabId, text, tab.fileName), decoration: const InputDecoration( border: InputBorder.none, contentPadding: EdgeInsets.all(16), ), style: TextStyle( fontFamily: 'monospace', fontSize: 14, color: Theme.of(context).textTheme.bodyLarge?.color, ), ), ], ), ), ), ), ], ); } Future _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 _copyToClipboard(BuildContext context, String content) async { await Clipboard.setData(ClipboardData(text: content)); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('已复制到剪贴板'))); } } Future _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( 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 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( 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); } } } }