You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

320 lines
11 KiB

import 'dart:ui';
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';
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;
const TextTab({super.key, required this.tabId});
@override
State<TextTab> createState() => _TextTabState();
}
class _TextTabState extends State<TextTab> {
late TextEditingController _controller;
late EditorProvider _provider;
late FocusNode _focusNode;
late ScrollController _scrollController;
String _language = 'plaintext';
@override
void initState() {
super.initState();
_provider = Provider.of<EditorProvider>(context, listen: false);
_controller = TextEditingController(text: _getCurrentContent());
_focusNode = FocusNode();
_scrollController = ScrollController(); // 添加这行
_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();
_scrollController.dispose(); // 添加这行
super.dispose();
}
@override
Widget build(BuildContext context) {
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),
decoration: BoxDecoration(
color: Colors.grey[100],
border: Border(bottom: BorderSide(color: Colors.grey[300]!)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'源文本${tab.content.isEmpty ? '' : ' (${tab.content.length}字符)'}',
style: const TextStyle(fontWeight: FontWeight.bold),
),
Row(
children: [
IconButton(
icon: const Icon(Icons.folder_open, size: 20),
tooltip: '打开文件',
onPressed: () => _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),
),
DropdownButton<String>(
value: _language,
icon: const Icon(Icons.code, size: 20),
underline: Container(),
onChanged: (String? newValue) {
setState(() {
_language = newValue!;
});
},
items:
<String>[
'xml',
'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: ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(
scrollbars: true,
dragDevices: {PointerDeviceKind.touch, PointerDeviceKind.mouse},
),
child: SingleChildScrollView(
controller: _scrollController, // 添加滚动控制器
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,
scrollController: _scrollController, // 使用同一个滚动控制器
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,
),
],
),
),
),
),
],
);
}
// _openFile方法现在需要更新控制器
Future<void> _openFile(BuildContext context) async {
try {
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();
_provider.updateContent(widget.tabId, content);
setState(() {
_controller.text = content;
_detectLanguage();
});
// 强制刷新文本控制器
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('已加载: ${file.path}')));
}
}
} 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) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('读取失败: ${e.toString()}')));
}
}
}
// 复制到剪贴板
Future<void> _copyToClipboard(BuildContext context, String content) async {
await Clipboard.setData(ClipboardData(text: content));
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('已复制到剪贴板')));
}
}
// 保存文件模拟功能
// 保存文件功能
Future<void> _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<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 (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()}')));
}
}
}
}