|
|
|
@ -3,7 +3,10 @@ import 'package:provider/provider.dart';
@@ -3,7 +3,10 @@ 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'; |
|
|
|
|
import 'dart:io'; // 用于文件操作 |
|
|
|
|
import 'dart:io'; |
|
|
|
|
import 'package:flutter_highlight/flutter_highlight.dart'; |
|
|
|
|
import 'package:flutter_highlight/themes/github.dart'; |
|
|
|
|
import 'package:flutter_highlight/themes/monokai-sublime.dart'; |
|
|
|
|
|
|
|
|
|
class TextTab extends StatefulWidget { |
|
|
|
|
final String tabId; |
|
|
|
@ -17,39 +20,91 @@ class TextTab extends StatefulWidget {
@@ -17,39 +20,91 @@ class TextTab extends StatefulWidget {
|
|
|
|
|
class _TextTabState extends State<TextTab> { |
|
|
|
|
late TextEditingController _controller; |
|
|
|
|
late EditorProvider _provider; |
|
|
|
|
late FocusNode _focusNode; |
|
|
|
|
String _language = 'plaintext'; |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
void initState() { |
|
|
|
|
super.initState(); |
|
|
|
|
_provider = Provider.of<EditorProvider>(context, listen: false); |
|
|
|
|
_controller = TextEditingController(text: _getCurrentContent()); |
|
|
|
|
_focusNode = FocusNode(); |
|
|
|
|
_detectLanguage(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
String _getCurrentContent() { |
|
|
|
|
return _provider.tabs.firstWhere((t) => t.id == widget.tabId).content; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void _detectLanguage() { |
|
|
|
|
final tab = _provider.tabs.firstWhere((t) => t.id == widget.tabId); |
|
|
|
|
final fileName = tab.title.toLowerCase(); |
|
|
|
|
final content = tab.content.trim(); |
|
|
|
|
|
|
|
|
|
if (fileName.endsWith('.dart')) { |
|
|
|
|
_language = 'dart'; |
|
|
|
|
} else if (fileName.endsWith('.java')) { |
|
|
|
|
_language = 'java'; |
|
|
|
|
} else if (fileName.endsWith('.py')) { |
|
|
|
|
_language = 'python'; |
|
|
|
|
} else if (fileName.endsWith('.js')) { |
|
|
|
|
_language = 'javascript'; |
|
|
|
|
} else if (fileName.endsWith('.html')) { |
|
|
|
|
_language = 'html'; |
|
|
|
|
} else if (fileName.endsWith('.css')) { |
|
|
|
|
_language = 'css'; |
|
|
|
|
} else if (fileName.endsWith('.json')) { |
|
|
|
|
_language = 'json'; |
|
|
|
|
} else if (fileName.endsWith('.yaml') || fileName.endsWith('.yml')) { |
|
|
|
|
_language = 'yaml'; |
|
|
|
|
} else if (fileName.endsWith('.xml')) { |
|
|
|
|
_language = 'xml'; |
|
|
|
|
} |
|
|
|
|
// 如果扩展名未识别,检查内容特征 |
|
|
|
|
else if (_isLikelyXml(content)) { |
|
|
|
|
_language = 'xml'; |
|
|
|
|
} else { |
|
|
|
|
_language = 'plaintext'; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
bool _isLikelyXml(String content) { |
|
|
|
|
if (content.isEmpty) return false; |
|
|
|
|
|
|
|
|
|
// 简单检查:内容以'<'开头,以'>'结尾 |
|
|
|
|
final trimmed = content.trim(); |
|
|
|
|
if (!trimmed.startsWith('<') || !trimmed.endsWith('>')) { |
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 更复杂的检查:包含常见的XML标签模式 |
|
|
|
|
final xmlTagRegex = RegExp(r'<[^/>]+>.*<\/[^>]+>|<[^/>]+\/>'); |
|
|
|
|
return xmlTagRegex.hasMatch(content); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
void didUpdateWidget(TextTab oldWidget) { |
|
|
|
|
super.didUpdateWidget(oldWidget); |
|
|
|
|
if (oldWidget.tabId != widget.tabId) { |
|
|
|
|
_controller.text = _getCurrentContent(); |
|
|
|
|
_detectLanguage(); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
void dispose() { |
|
|
|
|
_controller.dispose(); |
|
|
|
|
_focusNode.dispose(); |
|
|
|
|
super.dispose(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
Widget build(BuildContext context) { |
|
|
|
|
final tab = _provider.tabs.firstWhere((t) => t.id == widget.tabId); // Add this line |
|
|
|
|
final tab = _provider.tabs.firstWhere((t) => t.id == widget.tabId); |
|
|
|
|
final isDarkMode = Theme.of(context).brightness == Brightness.dark; |
|
|
|
|
|
|
|
|
|
return Column( |
|
|
|
|
children: [ |
|
|
|
|
// 新增工具条 |
|
|
|
|
Container( |
|
|
|
|
height: 40, |
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 16), |
|
|
|
@ -82,21 +137,64 @@ class _TextTabState extends State<TextTab> {
@@ -82,21 +137,64 @@ class _TextTabState extends State<TextTab> {
|
|
|
|
|
tooltip: '保存到文件', |
|
|
|
|
onPressed: tab.content.isEmpty ? null : () => _saveFile(context, tab.content), |
|
|
|
|
), |
|
|
|
|
DropdownButton<String>( |
|
|
|
|
value: _language, |
|
|
|
|
icon: const Icon(Icons.code, size: 20), |
|
|
|
|
underline: Container(), |
|
|
|
|
onChanged: (String? newValue) { |
|
|
|
|
setState(() { |
|
|
|
|
_language = newValue!; |
|
|
|
|
}); |
|
|
|
|
}, |
|
|
|
|
items: |
|
|
|
|
<String>[ |
|
|
|
|
'plaintext', |
|
|
|
|
'dart', |
|
|
|
|
'java', |
|
|
|
|
'python', |
|
|
|
|
'javascript', |
|
|
|
|
'html', |
|
|
|
|
'css', |
|
|
|
|
'json', |
|
|
|
|
'yaml', |
|
|
|
|
].map<DropdownMenuItem<String>>((String value) { |
|
|
|
|
return DropdownMenuItem<String>(value: value, child: Text(value)); |
|
|
|
|
}).toList(), |
|
|
|
|
), |
|
|
|
|
], |
|
|
|
|
), |
|
|
|
|
], |
|
|
|
|
), |
|
|
|
|
), |
|
|
|
|
// 文本编辑区 |
|
|
|
|
Expanded( |
|
|
|
|
child: TextField( |
|
|
|
|
controller: _controller, |
|
|
|
|
maxLines: null, |
|
|
|
|
onChanged: (text) => _provider.updateContent(widget.tabId, text), |
|
|
|
|
decoration: const InputDecoration( |
|
|
|
|
border: InputBorder.none, |
|
|
|
|
contentPadding: EdgeInsets.all(16), |
|
|
|
|
), |
|
|
|
|
child: Stack( |
|
|
|
|
children: [ |
|
|
|
|
// 高亮显示的背景文本 |
|
|
|
|
HighlightView( |
|
|
|
|
tab.content, |
|
|
|
|
language: _language, |
|
|
|
|
theme: isDarkMode ? monokaiSublimeTheme : githubTheme, |
|
|
|
|
padding: const EdgeInsets.all(16), |
|
|
|
|
textStyle: const TextStyle(fontFamily: 'monospace', fontSize: 14), |
|
|
|
|
), |
|
|
|
|
// 实际可编辑的TextField |
|
|
|
|
TextField( |
|
|
|
|
controller: _controller, |
|
|
|
|
focusNode: _focusNode, |
|
|
|
|
maxLines: null, |
|
|
|
|
onChanged: (text) => _provider.updateContent(widget.tabId, text), |
|
|
|
|
decoration: const InputDecoration( |
|
|
|
|
border: InputBorder.none, |
|
|
|
|
contentPadding: EdgeInsets.all(16), |
|
|
|
|
), |
|
|
|
|
style: const TextStyle( |
|
|
|
|
fontFamily: 'monospace', |
|
|
|
|
fontSize: 14, |
|
|
|
|
color: Colors.transparent, // 使文本透明,只显示高亮背景 |
|
|
|
|
), |
|
|
|
|
cursorColor: Colors.black, // 光标颜色 |
|
|
|
|
), |
|
|
|
|
], |
|
|
|
|
), |
|
|
|
|
), |
|
|
|
|
], |
|
|
|
@ -110,14 +208,12 @@ class _TextTabState extends State<TextTab> {
@@ -110,14 +208,12 @@ class _TextTabState extends State<TextTab> {
|
|
|
|
|
|
|
|
|
|
if (result != null && result.files.single.path != null) { |
|
|
|
|
final file = File(result.files.single.path!); |
|
|
|
|
|
|
|
|
|
// 尝试读取文件内容 |
|
|
|
|
final content = await file.readAsString(); |
|
|
|
|
|
|
|
|
|
// 同时更新Provider和控制器 |
|
|
|
|
_provider.updateContent(widget.tabId, content); |
|
|
|
|
setState(() { |
|
|
|
|
_controller.text = content; |
|
|
|
|
_detectLanguage(); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// 强制刷新文本控制器 |
|
|
|
@ -126,10 +222,8 @@ class _TextTabState extends State<TextTab> {
@@ -126,10 +222,8 @@ class _TextTabState extends State<TextTab> {
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} on FormatException { |
|
|
|
|
// 二进制文件错误 |
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('这不是可读的文本文件'))); |
|
|
|
|
} on FileSystemException catch (e) { |
|
|
|
|
// 文件系统错误 |
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('文件访问错误: ${e.message}'))); |
|
|
|
|
} catch (e) { |
|
|
|
|
if (context.mounted) { |
|
|
|
@ -160,7 +254,7 @@ class _TextTabState extends State<TextTab> {
@@ -160,7 +254,7 @@ class _TextTabState extends State<TextTab> {
|
|
|
|
|
type: FileType.any, |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
if (outputPath == null) return; // 用户取消了 |
|
|
|
|
if (outputPath == null) return; |
|
|
|
|
|
|
|
|
|
final file = File(outputPath); |
|
|
|
|
|
|
|
|
@ -186,7 +280,7 @@ class _TextTabState extends State<TextTab> {
@@ -186,7 +280,7 @@ class _TextTabState extends State<TextTab> {
|
|
|
|
|
), |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
if (shouldOverwrite != true) return; // 用户选择不覆盖 |
|
|
|
|
if (shouldOverwrite != true) return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 写入文件 |
|
|
|
|