Browse Source

当个编辑窗内容加载与保存功能完成。

master
hejl 2 months ago
parent
commit
c491b2c17e
  1. 55
      win_text_editor/lib/app/app.dart
  2. 34
      win_text_editor/lib/app/providers/editor_provider.dart
  3. 15
      win_text_editor/lib/app/providers/file_provider.dart
  4. 86
      win_text_editor/lib/app/providers/logger.dart
  5. 130
      win_text_editor/lib/app/widgets/console_panel.dart
  6. 134
      win_text_editor/lib/app/widgets/file_explorer.dart
  7. 60
      win_text_editor/lib/app/widgets/text_tab.dart
  8. 12
      win_text_editor/lib/main.dart

55
win_text_editor/lib/app/app.dart

@ -5,6 +5,7 @@ import 'package:win_text_editor/app/providers/editor_provider.dart'; @@ -5,6 +5,7 @@ import 'package:win_text_editor/app/providers/editor_provider.dart';
import 'package:win_text_editor/app/providers/file_provider.dart';
import 'package:win_text_editor/app/widgets/editor_pane.dart';
import 'package:win_text_editor/app/widgets/file_explorer.dart';
import 'package:win_text_editor/app/widgets/console_panel.dart'; //
class AppScaffold extends StatelessWidget {
const AppScaffold({super.key});
@ -22,9 +23,7 @@ class AppScaffold extends StatelessWidget { @@ -22,9 +23,7 @@ class AppScaffold extends StatelessWidget {
//
AppMenu(),
//
Expanded(
child: _ResizablePanel(),
),
Expanded(child: _ResizablePanel()),
],
),
),
@ -53,34 +52,36 @@ class _ResizablePanelState extends State<_ResizablePanel> { @@ -53,34 +52,36 @@ class _ResizablePanelState extends State<_ResizablePanel> {
//
final leftPanelWidth = (_leftWidth * screenWidth).clamp(_minWidth, _maxWidth);
return Row(
return Column(
children: [
Expanded(
child: Row(
children: [
//
SizedBox(
width: leftPanelWidth,
child: const FileExplorer(),
),
//
GestureDetector(
behavior: HitTestBehavior.translucent,
onPanUpdate: (details) {
setState(() {
_leftWidth = ((leftPanelWidth + details.delta.dx) / screenWidth)
.clamp(_minWidth / screenWidth, _maxWidth / screenWidth);
});
},
child: MouseRegion(
cursor: SystemMouseCursors.resizeLeftRight,
child: Container(
width: 4,
color: Colors.grey[300],
SizedBox(width: leftPanelWidth, child: const FileExplorer()),
//
GestureDetector(
behavior: HitTestBehavior.translucent,
onPanUpdate: (details) {
setState(() {
_leftWidth = ((leftPanelWidth + details.delta.dx) / screenWidth).clamp(
_minWidth / screenWidth,
_maxWidth / screenWidth,
);
});
},
child: MouseRegion(
cursor: SystemMouseCursors.resizeLeftRight,
child: Container(width: 4, color: Colors.grey[300]),
),
),
//
const Expanded(child: Column(children: [Expanded(child: EditorPane())])),
],
),
),
),
//
Expanded(
child: const EditorPane(),
),
//
const ConsolePanel(),
],
);
},

34
win_text_editor/lib/app/providers/editor_provider.dart

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:win_text_editor/app/providers/logger.dart';
class EditorProvider with ChangeNotifier {
final List<EditorTab> _tabs = [];
@ -7,20 +8,25 @@ class EditorProvider with ChangeNotifier { @@ -7,20 +8,25 @@ class EditorProvider with ChangeNotifier {
List<EditorTab> get tabs => _tabs;
String? get activeTabId => _activeTabId;
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: '文档 ${_tabs.length + 1}',
content: '', //
),
);
_tabs.add(EditorTab(id: tabId, title: '文档 ${_tabs.length + 1}', content: ''));
_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;
@ -29,19 +35,27 @@ class EditorProvider with ChangeNotifier { @@ -29,19 +35,27 @@ class EditorProvider with ChangeNotifier {
}
void setActiveTab(String tabId) {
Logger().info('设置活动选项卡: $tabId');
_activeTabId = tabId;
notifyListeners();
}
void updateContent(String tabId, String content) {
final tab = _tabs.firstWhere((t) => t.id == tabId);
tab.content = content;
Logger().debug("更新选项卡内容: $tabId, 长度: ${content.length}");
try {
final tab = _tabs.firstWhere((t) => t.id == tabId);
tab.content = content;
Logger().debug("内容更新成功,新长度: ${tab.content.length}");
notifyListeners();
} catch (e) {
Logger().error("更新内容失败: ${e.toString()}", source: 'EditorProvider');
}
}
}
class EditorTab {
final String id;
final String title;
String title;
String content;
EditorTab({required this.id, required this.title, required this.content});

15
win_text_editor/lib/app/providers/file_provider.dart

@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; @@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart';
import 'package:path_provider/path_provider.dart';
import 'package:win_text_editor/app/models/file_node.dart';
import 'package:win_text_editor/app/providers/logger.dart';
import 'package:win_text_editor/app/services/file_service.dart';
class FileProvider with ChangeNotifier {
@ -47,7 +48,7 @@ class FileProvider with ChangeNotifier { @@ -47,7 +48,7 @@ class FileProvider with ChangeNotifier {
final result = await FilePicker.platform.pickFiles();
if (result != null && result.files.single.path != null) {
//
debugPrint('File selected: ${result.files.single.path}');
Logger().info('File selected: ${result.files.single.path}');
}
}
@ -66,7 +67,7 @@ class FileProvider with ChangeNotifier { @@ -66,7 +67,7 @@ class FileProvider with ChangeNotifier {
);
_fileNodes = [rootNode]; //
} catch (e) {
debugPrint('Error loading directory: $e');
Logger().error('Error loading directory: $e');
_fileNodes = [];
}
@ -92,7 +93,7 @@ class FileProvider with ChangeNotifier { @@ -92,7 +93,7 @@ class FileProvider with ChangeNotifier {
),
];
} catch (e) {
debugPrint('Error loading root directory: $e');
Logger().error('Error loading root directory: $e');
_fileNodes = [];
}
@ -115,7 +116,7 @@ class FileProvider with ChangeNotifier { @@ -115,7 +116,7 @@ class FileProvider with ChangeNotifier {
),
];
} catch (e) {
debugPrint('Error loading root: $e');
Logger().error('Error loading root: $e');
_fileNodes = [];
}
@ -133,7 +134,7 @@ class FileProvider with ChangeNotifier { @@ -133,7 +134,7 @@ class FileProvider with ChangeNotifier {
dirNode.children = await FileService.listDirectory(dirNode.path);
dirNode.isExpanded = true;
} catch (e) {
debugPrint('Error loading dir: $e');
Logger().error('Error loading directory: $e');
dirNode.children = [];
}
@ -171,7 +172,7 @@ class FileProvider with ChangeNotifier { @@ -171,7 +172,7 @@ class FileProvider with ChangeNotifier {
_replaceNodeInTree(dirNode, updatedNode);
} catch (e) {
debugPrint('Error loading directory contents: $e');
Logger().error('Error loading directory contents: $e');
final updatedNode = dirNode.copyWith(children: []);
_replaceNodeInTree(dirNode, updatedNode);
}
@ -217,7 +218,7 @@ class FileProvider with ChangeNotifier { @@ -217,7 +218,7 @@ class FileProvider with ChangeNotifier {
),
];
} catch (e) {
debugPrint('Error refreshing file tree: $e');
Logger().error('Error refreshing file tree: $e');
_fileNodes = [];
}

86
win_text_editor/lib/app/providers/logger.dart

@ -0,0 +1,86 @@ @@ -0,0 +1,86 @@
import 'package:flutter/foundation.dart';
enum LogLevel { debug, info, warning, error }
class LogEntry {
final DateTime timestamp;
final LogLevel level;
final String message;
final String? source;
LogEntry({required this.level, required this.message, this.source}) : timestamp = DateTime.now();
@override
String toString() {
final levelStr = level.toString().split('.').last.toUpperCase();
final timeStr = timestamp.toIso8601String();
final sourceStr = source != null ? ' [$source]' : '';
return '$timeStr $levelStr$sourceStr: $message';
}
}
class Logger with ChangeNotifier {
static final Logger _instance = Logger._internal();
final List<LogEntry> _logs = [];
bool _showTimestamps = true;
bool _showSource = true;
LogLevel _minimumLevel = LogLevel.debug;
factory Logger() {
return _instance;
}
Logger._internal();
List<LogEntry> get logs => List.unmodifiable(_logs);
bool get showTimestamps => _showTimestamps;
bool get showSource => _showSource;
LogLevel get minimumLevel => _minimumLevel;
void setMinimumLevel(LogLevel level) {
_minimumLevel = level;
notifyListeners();
}
void setShowTimestamps(bool show) {
_showTimestamps = show;
notifyListeners();
}
void setShowSource(bool show) {
_showSource = show;
notifyListeners();
}
void _addLog(LogEntry entry) {
print('Adding log: $entry');
if (entry.level.index >= _minimumLevel.index) {
_logs.add(entry);
notifyListeners();
if (kDebugMode) {
print(entry.toString());
}
}
}
void debug(String message, {String? source}) {
_addLog(LogEntry(level: LogLevel.debug, message: message, source: source));
}
void info(String message, {String? source}) {
_addLog(LogEntry(level: LogLevel.info, message: message, source: source));
}
void warning(String message, {String? source}) {
_addLog(LogEntry(level: LogLevel.warning, message: message, source: source));
}
void error(String message, {String? source}) {
_addLog(LogEntry(level: LogLevel.error, message: message, source: source));
}
void clear() {
_logs.clear();
notifyListeners();
}
}

130
win_text_editor/lib/app/widgets/console_panel.dart

@ -0,0 +1,130 @@ @@ -0,0 +1,130 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter/services.dart'; //
import 'package:win_text_editor/app/providers/logger.dart';
class ConsolePanel extends StatefulWidget {
const ConsolePanel({super.key});
@override
State<ConsolePanel> createState() => _ConsolePanelState();
}
class _ConsolePanelState extends State<ConsolePanel> {
double _height = 100;
final double _minHeight = 50;
final double _maxHeight = 300;
final ScrollController _scrollController = ScrollController();
String? _selectedLog; //
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
Color _getLogColor(LogLevel level) {
switch (level) {
case LogLevel.error:
return Colors.red[400]!;
case LogLevel.warning:
return Colors.orange[400]!;
case LogLevel.info:
return Colors.blue[400]!;
case LogLevel.debug:
return Colors.grey;
}
}
//
void _showContextMenu(BuildContext context, Offset position, String logContent) async {
final RenderBox overlay = Overlay.of(context).context.findRenderObject() as RenderBox;
await showMenu(
context: context,
position: RelativeRect.fromRect(
Rect.fromPoints(position, position),
Offset.zero & overlay.size,
),
items: [
PopupMenuItem(
child: const Text('复制'),
onTap: () async {
await Clipboard.setData(ClipboardData(text: logContent));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('已复制到剪贴板'), duration: Duration(seconds: 1)),
);
},
),
PopupMenuItem(
child: const Text('清除日志'),
onTap: () {
Provider.of<Logger>(context, listen: false).clear();
},
),
],
);
}
@override
Widget build(BuildContext context) {
final logger = Provider.of<Logger>(context);
final consoleHeight = _height.clamp(_minHeight, _maxHeight);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_scrollController.hasClients) {
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
}
});
return Column(
children: [
//
GestureDetector(
behavior: HitTestBehavior.translucent,
onPanUpdate: (details) {
setState(() {
_height = (_height - details.delta.dy).clamp(_minHeight, _maxHeight);
});
},
child: MouseRegion(
cursor: SystemMouseCursors.resizeUpDown,
child: Container(height: 4, color: Colors.grey[300]),
),
),
//
SizedBox(
height: consoleHeight,
child: Container(
color: Colors.grey[100],
padding: const EdgeInsets.all(8),
child: ListView.builder(
controller: _scrollController,
itemCount: logger.logs.length,
itemBuilder: (context, index) {
final log = logger.logs[index];
final logContent = log.toString();
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: GestureDetector(
onSecondaryTapDown: (details) {
_showContextMenu(context, details.globalPosition, logContent);
},
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: SelectableText(
//
logContent,
style: TextStyle(fontFamily: 'monospace', color: _getLogColor(log.level)),
),
),
),
);
},
),
),
),
],
);
}
}

134
win_text_editor/lib/app/widgets/file_explorer.dart

@ -1,6 +1,9 @@ @@ -1,6 +1,9 @@
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:win_text_editor/app/providers/logger.dart';
import '../models/file_node.dart';
import '../providers/file_provider.dart';
@ -55,10 +58,82 @@ class _FileExplorerState extends State<FileExplorer> { @@ -55,10 +58,82 @@ class _FileExplorerState extends State<FileExplorer> {
if (node.isDirectory) {
await fileProvider.loadDirectoryContents(node);
} else {
//
//
// final editorProvider = Provider.of<EditorProvider>(context, listen: false);
// editorProvider.openFile(node.path);
// Handle file opening
print("No active tab found");
_openFileInEditor(context, node);
}
}
Future<void> _openFileInEditor(BuildContext context, FileNode node) async {
try {
Logger().info('尝试打开文件: ${node.path}');
final editorProvider = Provider.of<EditorProvider>(context, listen: false);
final content = await File(node.path).readAsString();
Logger().debug('文件内容读取成功,长度: ${content.length}');
Logger().debug('当前活动选项卡ID: ${editorProvider.activeTabId}');
Logger().debug('现有选项卡数量: ${editorProvider.tabs.length}');
//
if (editorProvider.activeTabId == null) {
Logger().info('没有活动选项卡,创建新选项卡');
editorProvider.addTab();
}
//
Logger().info('准备更新选项卡内容');
editorProvider.updateContent(editorProvider.activeTabId!, content);
//
final activeTab = editorProvider.activeTab;
if (activeTab != null) {
activeTab.title = node.name;
Logger().debug('更新选项卡标题为: ${node.name}');
editorProvider.notifyListeners();
}
if (context.mounted) {
Logger().debug('已加载: ${node.name}');
}
} on FormatException {
Logger().warning('文件格式异常: ${node.path}');
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('这不是可读的文本文件')));
}
} catch (e) {
Logger().error('打开文件失败: ${e.toString()}', source: 'FileExplorer');
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('读取失败: ${e.toString()}')));
}
}
}
int getMaxDepth(List<FileNode> nodes) {
int maxDepth = 0;
for (final node in nodes) {
if (node.isDirectory && node.isExpanded) {
maxDepth = max(maxDepth, getMaxDepth(node.children) + 1);
}
}
return maxDepth;
}
//
double calculateTotalWidth(BuildContext context, FileProvider fileProvider) {
final maxDepth = getMaxDepth(fileProvider.fileNodes);
return maxDepth * 200 + MediaQuery.of(context).size.width * 0.5;
}
void _handleNodeDoubleTap(BuildContext context, FileNode node) async {
if (!node.isDirectory) {
//
Logger().info('双击打开文件: ${node.path}', source: 'FileExplorer');
await _openFileInEditor(context, node);
} else {
//
Logger().debug('双击文件夹: ${node.path}', source: 'FileExplorer');
}
}
@ -90,7 +165,7 @@ class _FileExplorerState extends State<FileExplorer> { @@ -90,7 +165,7 @@ class _FileExplorerState extends State<FileExplorer> {
thumbVisibility: true,
scrollbarOrientation: ScrollbarOrientation.bottom, //
child: SizedBox(
width: _calculateTotalWidth(context, fileProvider), //
width: calculateTotalWidth(context, fileProvider), //
child: ListView.builder(
//
controller: _verticalScrollController,
@ -101,6 +176,7 @@ class _FileExplorerState extends State<FileExplorer> { @@ -101,6 +176,7 @@ class _FileExplorerState extends State<FileExplorer> {
key: ValueKey(node.path),
node: node,
onTap: () => _handleNodeTap(context, node),
onDoubleTap: () => _handleNodeDoubleTap(context, node),
);
},
),
@ -113,22 +189,6 @@ class _FileExplorerState extends State<FileExplorer> { @@ -113,22 +189,6 @@ class _FileExplorerState extends State<FileExplorer> {
),
);
}
//
double _calculateTotalWidth(BuildContext context, FileProvider fileProvider) {
final maxDepth = _getMaxDepth(fileProvider.fileNodes);
return maxDepth * 200 + MediaQuery.of(context).size.width * 0.5;
}
int _getMaxDepth(List<FileNode> nodes) {
int maxDepth = 0;
for (final node in nodes) {
if (node.isDirectory && node.isExpanded) {
maxDepth = max(maxDepth, _getMaxDepth(node.children) + 1);
}
}
return maxDepth;
}
}
//
@ -163,39 +223,31 @@ FileNode _getVisibleNode(List<FileNode> nodes, int index) { @@ -163,39 +223,31 @@ FileNode _getVisibleNode(List<FileNode> nodes, int index) {
class _FileNodeWidget extends StatelessWidget {
final FileNode node;
final VoidCallback onTap;
final VoidCallback onDoubleTap; //
const _FileNodeWidget({
Key? key, // key
Key? key,
required this.node,
required this.onTap,
}) : super(key: key); //
required this.onDoubleTap, //
}) : super(key: key);
@override
Widget build(BuildContext context) {
//final editorProvider = Provider.of<EditorProvider>(context, listen: false);
return InkWell(
onTap: () {
if (node.isDirectory) {
onTap();
} else {
//editorProvider.openFile(node.path);
}
},
onTap: onTap,
onDoubleTap: onDoubleTap, //
child: Container(
padding: const EdgeInsets.symmetric(vertical: 0), //
padding: const EdgeInsets.symmetric(vertical: 0),
child: ListTile(
dense: true, //
visualDensity: const VisualDensity(
vertical: -4, //
),
contentPadding: const EdgeInsets.symmetric(horizontal: 2), //
minVerticalPadding: 0, // 0
dense: true,
visualDensity: const VisualDensity(vertical: -4),
contentPadding: const EdgeInsets.symmetric(horizontal: 2),
minVerticalPadding: 0,
leading: _buildLeadingWidget(context),
title: Text(
node.name,
style: Theme.of(context).textTheme.bodyMedium, // 使
),
title: Text(node.name, style: Theme.of(context).textTheme.bodyMedium),
),
),
);

60
win_text_editor/lib/app/widgets/text_tab.dart

@ -149,12 +149,62 @@ class _TextTabState extends State<TextTab> { @@ -149,12 +149,62 @@ class _TextTabState extends State<TextTab> {
}
//
//
Future<void> _saveFile(BuildContext context, String content) async {
// 使
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('已保存 ${content.length} 个字符')));
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()}')));
}
}
}
}

12
win_text_editor/lib/main.dart

@ -1,6 +1,8 @@ @@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:window_manager/window_manager.dart';
import 'app/app.dart';
import 'app/providers/logger.dart'; //
void main() async {
WidgetsFlutterBinding.ensureInitialized();
@ -17,7 +19,15 @@ void main() async { @@ -17,7 +19,15 @@ void main() async {
await windowManager.focus();
});
runApp(const MyApp());
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => Logger()), // Logger提供者
// Provider
],
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {

Loading…
Cancel
Save