20 changed files with 1425 additions and 202 deletions
@ -0,0 +1,3 @@ |
|||||||
|
description: This file stores settings for Dart & Flutter DevTools. |
||||||
|
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states |
||||||
|
extensions: |
@ -0,0 +1,89 @@ |
|||||||
|
import 'package:flutter/material.dart'; |
||||||
|
import 'package:provider/provider.dart'; |
||||||
|
import 'package:win_text_editor/app/menus/app_menu.dart'; |
||||||
|
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'; |
||||||
|
|
||||||
|
class AppScaffold extends StatelessWidget { |
||||||
|
const AppScaffold({super.key}); |
||||||
|
|
||||||
|
@override |
||||||
|
Widget build(BuildContext context) { |
||||||
|
return MultiProvider( |
||||||
|
providers: [ |
||||||
|
ChangeNotifierProvider(create: (_) => FileProvider()), |
||||||
|
ChangeNotifierProvider(create: (_) => EditorProvider()), |
||||||
|
], |
||||||
|
child: const Scaffold( |
||||||
|
body: Column( |
||||||
|
children: [ |
||||||
|
// 菜单栏 |
||||||
|
AppMenu(), |
||||||
|
// 主内容区域 |
||||||
|
Expanded( |
||||||
|
child: _ResizablePanel(), |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class _ResizablePanel extends StatefulWidget { |
||||||
|
const _ResizablePanel(); |
||||||
|
|
||||||
|
@override |
||||||
|
State<_ResizablePanel> createState() => _ResizablePanelState(); |
||||||
|
} |
||||||
|
|
||||||
|
class _ResizablePanelState extends State<_ResizablePanel> { |
||||||
|
double _leftWidth = 0.2; // 初始宽度20% |
||||||
|
final double _minWidth = 100; // 最小宽度 |
||||||
|
final double _maxWidth = 400; // 最大宽度 |
||||||
|
|
||||||
|
@override |
||||||
|
Widget build(BuildContext context) { |
||||||
|
final screenWidth = MediaQuery.of(context).size.width; |
||||||
|
|
||||||
|
return LayoutBuilder( |
||||||
|
builder: (context, constraints) { |
||||||
|
// 计算实际宽度 |
||||||
|
final leftPanelWidth = (_leftWidth * screenWidth).clamp(_minWidth, _maxWidth); |
||||||
|
|
||||||
|
return 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], |
||||||
|
), |
||||||
|
), |
||||||
|
), |
||||||
|
// 右侧编辑器区域 |
||||||
|
Expanded( |
||||||
|
child: const EditorPane(), |
||||||
|
), |
||||||
|
], |
||||||
|
); |
||||||
|
}, |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,67 @@ |
|||||||
|
import 'package:flutter/material.dart'; |
||||||
|
import 'package:win_text_editor/app/menus/menu_constants.dart'; |
||||||
|
|
||||||
|
import 'menu_actions.dart'; |
||||||
|
|
||||||
|
class AppMenu extends StatelessWidget { |
||||||
|
const AppMenu({super.key}); |
||||||
|
|
||||||
|
@override |
||||||
|
Widget build(BuildContext context) { |
||||||
|
return Container( |
||||||
|
height: 30, |
||||||
|
color: Colors.grey[200], |
||||||
|
child: Row( |
||||||
|
children: [ |
||||||
|
_buildMenuButton(context, '文件', _buildFileMenuItems()), |
||||||
|
_buildMenuButton(context, '编辑', _buildEditMenuItems()), |
||||||
|
_buildMenuButton(context, '窗口', _buildWindowMenuItems()), |
||||||
|
_buildMenuButton(context, '帮助', _buildHelpMenuItems()), |
||||||
|
], |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
Widget _buildMenuButton(BuildContext context, String label, List<PopupMenuEntry<String>> items) { |
||||||
|
return PopupMenuButton<String>( |
||||||
|
offset: const Offset(0, 30), |
||||||
|
child: Padding(padding: const EdgeInsets.symmetric(horizontal: 12.0), child: Text(label)), |
||||||
|
itemBuilder: (context) => items, |
||||||
|
onSelected: (value) => MenuActions.handleMenuAction(value, context), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
List<PopupMenuEntry<String>> _buildFileMenuItems() { |
||||||
|
return [ |
||||||
|
const PopupMenuItem<String>(value: MenuConstants.openFolder, child: Text('打开文件夹...')), |
||||||
|
const PopupMenuItem<String>(value: MenuConstants.save, child: Text('保存')), |
||||||
|
const PopupMenuItem<String>(value: MenuConstants.saveAs, child: Text('另存为...')), |
||||||
|
const PopupMenuItem<String>(value: MenuConstants.exit, child: Text('退出')), |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
List<PopupMenuEntry<String>> _buildEditMenuItems() { |
||||||
|
return [ |
||||||
|
const PopupMenuItem<String>(value: MenuConstants.undo, child: Text('撤销')), |
||||||
|
const PopupMenuItem<String>(value: MenuConstants.redo, child: Text('重做')), |
||||||
|
const PopupMenuItem<String>(value: MenuConstants.cut, child: Text('剪切')), |
||||||
|
const PopupMenuItem<String>(value: MenuConstants.copy, child: Text('复制')), |
||||||
|
const PopupMenuItem<String>(value: MenuConstants.paste, child: Text('粘贴')), |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
List<PopupMenuEntry<String>> _buildWindowMenuItems() { |
||||||
|
return [ |
||||||
|
const PopupMenuItem<String>(value: MenuConstants.minimize, child: Text('最小化')), |
||||||
|
const PopupMenuItem<String>(value: MenuConstants.maximize, child: Text('最大化')), |
||||||
|
const PopupMenuItem<String>(value: MenuConstants.close, child: Text('关闭')), |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
List<PopupMenuEntry<String>> _buildHelpMenuItems() { |
||||||
|
return [ |
||||||
|
const PopupMenuItem<String>(value: MenuConstants.about, child: Text('关于')), |
||||||
|
const PopupMenuItem<String>(value: MenuConstants.help, child: Text('帮助')), |
||||||
|
]; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,33 @@ |
|||||||
|
import 'package:flutter/material.dart'; |
||||||
|
import 'package:file_picker/file_picker.dart'; |
||||||
|
import 'package:provider/provider.dart'; |
||||||
|
import 'package:win_text_editor/app/menus/menu_constants.dart'; |
||||||
|
import 'package:win_text_editor/app/providers/file_provider.dart'; |
||||||
|
import 'dart:io'; |
||||||
|
|
||||||
|
class MenuActions { |
||||||
|
static Future<void> handleMenuAction(String value, BuildContext context) async { |
||||||
|
switch (value) { |
||||||
|
case MenuConstants.openFolder: |
||||||
|
await _openFolder(context); |
||||||
|
break; |
||||||
|
case MenuConstants.exit: |
||||||
|
_exitApplication(); |
||||||
|
break; |
||||||
|
// 其他菜单项可以在这里添加处理逻辑 |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
static Future<void> _openFolder(BuildContext context) async { |
||||||
|
final fileProvider = Provider.of<FileProvider>(context, listen: false); |
||||||
|
final String? selectedDirectory = await FilePicker.platform.getDirectoryPath(); |
||||||
|
|
||||||
|
if (selectedDirectory != null) { |
||||||
|
await fileProvider.loadDirectory(selectedDirectory); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
static void _exitApplication() { |
||||||
|
exit(0); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,29 @@ |
|||||||
|
class MenuConstants { |
||||||
|
// 菜单项类型 |
||||||
|
static const String fileMenu = 'file'; |
||||||
|
static const String editMenu = 'edit'; |
||||||
|
static const String windowMenu = 'window'; |
||||||
|
static const String helpMenu = 'help'; |
||||||
|
|
||||||
|
// 文件菜单项 |
||||||
|
static const String openFolder = 'open_folder'; |
||||||
|
static const String save = 'save'; |
||||||
|
static const String saveAs = 'save_as'; |
||||||
|
static const String exit = 'exit'; |
||||||
|
|
||||||
|
// 编辑菜单项 |
||||||
|
static const String undo = 'undo'; |
||||||
|
static const String redo = 'redo'; |
||||||
|
static const String cut = 'cut'; |
||||||
|
static const String copy = 'copy'; |
||||||
|
static const String paste = 'paste'; |
||||||
|
|
||||||
|
// 窗口菜单项 |
||||||
|
static const String minimize = 'minimize'; |
||||||
|
static const String maximize = 'maximize'; |
||||||
|
static const String close = 'close'; |
||||||
|
|
||||||
|
// 帮助菜单项 |
||||||
|
static const String about = 'about'; |
||||||
|
static const String help = 'help'; |
||||||
|
} |
@ -0,0 +1,15 @@ |
|||||||
|
class EditorTab { |
||||||
|
final String id; |
||||||
|
final String title; |
||||||
|
final String path; |
||||||
|
String content; |
||||||
|
final String fileType; |
||||||
|
|
||||||
|
EditorTab({ |
||||||
|
required this.id, |
||||||
|
required this.title, |
||||||
|
required this.path, |
||||||
|
required this.content, |
||||||
|
required this.fileType, |
||||||
|
}); |
||||||
|
} |
@ -0,0 +1,127 @@ |
|||||||
|
import 'package:flutter/foundation.dart'; |
||||||
|
import 'package:flutter/material.dart'; |
||||||
|
|
||||||
|
class FileNode { |
||||||
|
final String name; |
||||||
|
final String path; |
||||||
|
final bool isDirectory; |
||||||
|
final bool isRoot; |
||||||
|
final int depth; |
||||||
|
List<FileNode> children; |
||||||
|
bool isExpanded; |
||||||
|
|
||||||
|
FileNode({ |
||||||
|
required this.name, |
||||||
|
required this.path, |
||||||
|
required this.isDirectory, |
||||||
|
this.isRoot = false, |
||||||
|
this.depth = 0, |
||||||
|
this.isExpanded = false, |
||||||
|
List<FileNode>? children, |
||||||
|
}) : children = children ?? []; |
||||||
|
|
||||||
|
// 获取文件图标数据 |
||||||
|
IconData get iconData { |
||||||
|
if (isDirectory) { |
||||||
|
return Icons.folder; |
||||||
|
} |
||||||
|
|
||||||
|
final ext = name.split('.').last.toLowerCase(); |
||||||
|
|
||||||
|
// 常见文件类型图标映射 |
||||||
|
switch (ext) { |
||||||
|
case 'pdf': |
||||||
|
return Icons.picture_as_pdf; |
||||||
|
case 'doc': |
||||||
|
case 'docx': |
||||||
|
return Icons.article; |
||||||
|
case 'xls': |
||||||
|
case 'xlsx': |
||||||
|
return Icons.table_chart; |
||||||
|
case 'ppt': |
||||||
|
case 'pptx': |
||||||
|
return Icons.slideshow; |
||||||
|
case 'txt': |
||||||
|
return Icons.text_snippet; |
||||||
|
case 'dart': |
||||||
|
return Icons.code; |
||||||
|
case 'js': |
||||||
|
return Icons.javascript; |
||||||
|
case 'java': |
||||||
|
return Icons.coffee; |
||||||
|
case 'py': |
||||||
|
return Icons.data_object; |
||||||
|
case 'html': |
||||||
|
return Icons.html; |
||||||
|
case 'css': |
||||||
|
return Icons.css; |
||||||
|
case 'json': |
||||||
|
return Icons.data_array; |
||||||
|
case 'png': |
||||||
|
case 'jpg': |
||||||
|
case 'jpeg': |
||||||
|
case 'gif': |
||||||
|
return Icons.image; |
||||||
|
case 'mp3': |
||||||
|
case 'wav': |
||||||
|
return Icons.audiotrack; |
||||||
|
case 'mp4': |
||||||
|
case 'avi': |
||||||
|
case 'mov': |
||||||
|
return Icons.videocam; |
||||||
|
case 'zip': |
||||||
|
case 'rar': |
||||||
|
case '7z': |
||||||
|
return Icons.archive; |
||||||
|
default: |
||||||
|
return Icons.insert_drive_file; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 获取构建好的图标组件 |
||||||
|
Widget get icon { |
||||||
|
return Icon(iconData, color: isDirectory ? Colors.amber[700] : Colors.blue); |
||||||
|
} |
||||||
|
|
||||||
|
FileNode copyWith({ |
||||||
|
String? name, |
||||||
|
String? path, |
||||||
|
bool? isDirectory, |
||||||
|
bool? isRoot, |
||||||
|
int? depth, // 添加depth参数 |
||||||
|
List<FileNode>? children, |
||||||
|
bool? isExpanded, |
||||||
|
}) { |
||||||
|
return FileNode( |
||||||
|
name: name ?? this.name, |
||||||
|
path: path ?? this.path, |
||||||
|
isDirectory: isDirectory ?? this.isDirectory, |
||||||
|
isRoot: isRoot ?? this.isRoot, |
||||||
|
depth: depth ?? this.depth, // 保留原有depth或使用新值 |
||||||
|
children: children ?? this.children, |
||||||
|
isExpanded: isExpanded ?? this.isExpanded, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
bool operator ==(Object other) { |
||||||
|
if (identical(this, other)) return true; |
||||||
|
return other is FileNode && |
||||||
|
other.name == name && |
||||||
|
other.path == path && |
||||||
|
other.isDirectory == isDirectory && |
||||||
|
other.isRoot == isRoot && |
||||||
|
listEquals(other.children, children) && |
||||||
|
other.isExpanded == isExpanded; |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
int get hashCode { |
||||||
|
return name.hashCode ^ |
||||||
|
path.hashCode ^ |
||||||
|
isDirectory.hashCode ^ |
||||||
|
isRoot.hashCode ^ |
||||||
|
children.hashCode ^ |
||||||
|
isExpanded.hashCode; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,97 @@ |
|||||||
|
import 'package:flutter/material.dart'; |
||||||
|
import 'package:win_text_editor/app/models/editor_tab.dart'; |
||||||
|
import 'package:win_text_editor/app/services/file_service.dart'; |
||||||
|
import 'package:win_text_editor/app/services/syntax_service.dart'; |
||||||
|
|
||||||
|
class EditorProvider with ChangeNotifier { |
||||||
|
final List<EditorTab> _openTabs = []; |
||||||
|
int _activeTabIndex = 0; |
||||||
|
int _currentLayout = 0; // 0=平铺, 1=层叠, 2=单页 |
||||||
|
|
||||||
|
List<EditorTab> get openTabs => _openTabs; |
||||||
|
int get activeTabIndex => _activeTabIndex; |
||||||
|
int get currentLayout => _currentLayout; |
||||||
|
|
||||||
|
Future<void> openFile(String filePath) async { |
||||||
|
try { |
||||||
|
// 检查是否已经打开 |
||||||
|
final existingIndex = _openTabs.indexWhere((tab) => tab.path == filePath); |
||||||
|
if (existingIndex != -1) { |
||||||
|
_activeTabIndex = existingIndex; |
||||||
|
notifyListeners(); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// 读取文件内容 |
||||||
|
final content = await FileService.readFile(filePath); |
||||||
|
final fileName = filePath.split('/').last; |
||||||
|
final fileType = SyntaxService.detectFileType(fileName); |
||||||
|
|
||||||
|
// 创建新标签页 |
||||||
|
final newTab = EditorTab( |
||||||
|
id: DateTime.now().millisecondsSinceEpoch.toString(), |
||||||
|
title: fileName, |
||||||
|
path: filePath, |
||||||
|
content: content, |
||||||
|
fileType: fileType, |
||||||
|
); |
||||||
|
|
||||||
|
_openTabs.add(newTab); |
||||||
|
_activeTabIndex = _openTabs.length - 1; |
||||||
|
notifyListeners(); |
||||||
|
} catch (e) { |
||||||
|
debugPrint('Error opening file: $e'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void closeTab(String tabId) { |
||||||
|
final index = _openTabs.indexWhere((tab) => tab.id == tabId); |
||||||
|
if (index != -1) { |
||||||
|
_openTabs.removeAt(index); |
||||||
|
if (_activeTabIndex >= index && _activeTabIndex > 0) { |
||||||
|
_activeTabIndex--; |
||||||
|
} |
||||||
|
notifyListeners(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void setActiveTab(int index) { |
||||||
|
if (index >= 0 && index < _openTabs.length) { |
||||||
|
_activeTabIndex = index; |
||||||
|
notifyListeners(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Future<void> saveFile(String tabId) async { |
||||||
|
final index = _openTabs.indexWhere((tab) => tab.id == tabId); |
||||||
|
if (index != -1) { |
||||||
|
final tab = _openTabs[index]; |
||||||
|
await FileService.writeFile(tab.path, tab.content); |
||||||
|
// 可以添加保存成功的提示 |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Future<void> copyToClipboard(String tabId) async { |
||||||
|
final index = _openTabs.indexWhere((tab) => tab.id == tabId); |
||||||
|
if (index != -1) { |
||||||
|
final tab = _openTabs[index]; |
||||||
|
// 这里需要实现复制到剪贴板的逻辑 |
||||||
|
// 可以使用 clipboard package |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void changeLayout(int layout) { |
||||||
|
if (layout >= 0 && layout <= 2) { |
||||||
|
_currentLayout = layout; |
||||||
|
notifyListeners(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void updateTabContent(String tabId, String newContent) { |
||||||
|
final index = _openTabs.indexWhere((tab) => tab.id == tabId); |
||||||
|
if (index != -1) { |
||||||
|
_openTabs[index].content = newContent; |
||||||
|
notifyListeners(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,227 @@ |
|||||||
|
import 'dart:io'; |
||||||
|
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/services/file_service.dart'; |
||||||
|
|
||||||
|
class FileProvider with ChangeNotifier { |
||||||
|
List<FileNode> _fileNodes = []; |
||||||
|
bool _isLoading = false; |
||||||
|
String _searchQuery = ''; |
||||||
|
String? _currentRootPath; // 跟踪当前根路径 |
||||||
|
|
||||||
|
bool get isLoading => _isLoading; |
||||||
|
bool get hasRoot => _fileNodes.isNotEmpty && _fileNodes[0].isRoot; |
||||||
|
|
||||||
|
// 移除构造函数的_initFileTree调用 |
||||||
|
FileProvider(); |
||||||
|
|
||||||
|
// 新增方法:手动设置根路径 |
||||||
|
Future<void> setRootPath(String path) async { |
||||||
|
_currentRootPath = path; |
||||||
|
await _loadRootDirectory(); |
||||||
|
} |
||||||
|
|
||||||
|
List<FileNode> get fileNodes => |
||||||
|
_searchQuery.isEmpty ? _fileNodes : _fileNodes.where((node) => _filterNode(node)).toList(); |
||||||
|
|
||||||
|
bool _filterNode(FileNode node) { |
||||||
|
if (node.name.toLowerCase().contains(_searchQuery.toLowerCase())) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
return node.children.any(_filterNode); |
||||||
|
} |
||||||
|
|
||||||
|
void searchFiles(String query) { |
||||||
|
_searchQuery = query; |
||||||
|
notifyListeners(); |
||||||
|
} |
||||||
|
|
||||||
|
void toggleExpand(FileNode node) { |
||||||
|
node.isExpanded = !node.isExpanded; |
||||||
|
notifyListeners(); |
||||||
|
} |
||||||
|
|
||||||
|
Future<void> pickAndOpenFile() async { |
||||||
|
final result = await FilePicker.platform.pickFiles(); |
||||||
|
if (result != null && result.files.single.path != null) { |
||||||
|
// 这里需要与编辑器提供者交互来打开文件 |
||||||
|
debugPrint('File selected: ${result.files.single.path}'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Future<void> loadDirectory(String path) async { |
||||||
|
_isLoading = true; |
||||||
|
notifyListeners(); |
||||||
|
|
||||||
|
try { |
||||||
|
final directory = Directory(path); |
||||||
|
final rootNode = FileNode( |
||||||
|
name: directory.path.split(Platform.pathSeparator).last, |
||||||
|
path: directory.path, |
||||||
|
isDirectory: true, |
||||||
|
isRoot: true, // 添加根节点标识 |
||||||
|
children: await FileService.buildFileTree(directory.path), |
||||||
|
); |
||||||
|
_fileNodes = [rootNode]; // 将根节点作为唯一顶层节点 |
||||||
|
} catch (e) { |
||||||
|
debugPrint('Error loading directory: $e'); |
||||||
|
_fileNodes = []; |
||||||
|
} |
||||||
|
|
||||||
|
_isLoading = false; |
||||||
|
notifyListeners(); |
||||||
|
} |
||||||
|
|
||||||
|
Future<void> _loadRootDirectory() async { |
||||||
|
if (_currentRootPath == null) return; |
||||||
|
|
||||||
|
_isLoading = true; |
||||||
|
notifyListeners(); |
||||||
|
|
||||||
|
try { |
||||||
|
_fileNodes = [ |
||||||
|
FileNode( |
||||||
|
name: _currentRootPath!.split(Platform.pathSeparator).last, |
||||||
|
path: _currentRootPath!, |
||||||
|
isDirectory: true, |
||||||
|
isRoot: true, |
||||||
|
depth: 0, // 根节点深度为0 |
||||||
|
children: [], // 初始为空,不加载内容 |
||||||
|
), |
||||||
|
]; |
||||||
|
} catch (e) { |
||||||
|
debugPrint('Error loading root directory: $e'); |
||||||
|
_fileNodes = []; |
||||||
|
} |
||||||
|
|
||||||
|
_isLoading = false; |
||||||
|
notifyListeners(); |
||||||
|
} |
||||||
|
|
||||||
|
Future<void> loadRootDirectory(String path) async { |
||||||
|
_isLoading = true; |
||||||
|
notifyListeners(); |
||||||
|
|
||||||
|
try { |
||||||
|
_fileNodes = [ |
||||||
|
FileNode( |
||||||
|
name: path.split(Platform.pathSeparator).last, |
||||||
|
path: path, |
||||||
|
isDirectory: true, |
||||||
|
isRoot: true, |
||||||
|
children: [], // 初始为空 |
||||||
|
), |
||||||
|
]; |
||||||
|
} catch (e) { |
||||||
|
debugPrint('Error loading root: $e'); |
||||||
|
_fileNodes = []; |
||||||
|
} |
||||||
|
|
||||||
|
_isLoading = false; |
||||||
|
notifyListeners(); |
||||||
|
} |
||||||
|
|
||||||
|
Future<void> toggleDirectory(FileNode dirNode) async { |
||||||
|
if (dirNode.children.isEmpty) { |
||||||
|
// 首次点击:加载内容 |
||||||
|
_isLoading = true; |
||||||
|
notifyListeners(); |
||||||
|
|
||||||
|
try { |
||||||
|
dirNode.children = await FileService.listDirectory(dirNode.path); |
||||||
|
dirNode.isExpanded = true; |
||||||
|
} catch (e) { |
||||||
|
debugPrint('Error loading dir: $e'); |
||||||
|
dirNode.children = []; |
||||||
|
} |
||||||
|
|
||||||
|
_isLoading = false; |
||||||
|
notifyListeners(); |
||||||
|
} else { |
||||||
|
// 已加载过:只切换展开状态 |
||||||
|
dirNode.isExpanded = !dirNode.isExpanded; |
||||||
|
notifyListeners(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Future<void> loadDirectoryContents(FileNode dirNode) async { |
||||||
|
if (dirNode.children.isNotEmpty && dirNode.isExpanded) { |
||||||
|
// 如果已经加载过且是展开状态,只切换展开状态 |
||||||
|
dirNode.isExpanded = !dirNode.isExpanded; |
||||||
|
notifyListeners(); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
_isLoading = true; |
||||||
|
notifyListeners(); |
||||||
|
|
||||||
|
try { |
||||||
|
final contents = await FileService.listDirectory( |
||||||
|
dirNode.path, |
||||||
|
parentDepth: dirNode.depth, // 确保传递父节点深度 |
||||||
|
); |
||||||
|
|
||||||
|
final updatedNode = dirNode.copyWith( |
||||||
|
children: contents, |
||||||
|
isExpanded: true, |
||||||
|
// 不需要设置 depth,因为 copyWith 会自动保留原值 |
||||||
|
); |
||||||
|
|
||||||
|
_replaceNodeInTree(dirNode, updatedNode); |
||||||
|
} catch (e) { |
||||||
|
debugPrint('Error loading directory contents: $e'); |
||||||
|
final updatedNode = dirNode.copyWith(children: []); |
||||||
|
_replaceNodeInTree(dirNode, updatedNode); |
||||||
|
} |
||||||
|
|
||||||
|
_isLoading = false; |
||||||
|
notifyListeners(); |
||||||
|
} |
||||||
|
|
||||||
|
void _replaceNodeInTree(FileNode oldNode, FileNode newNode) { |
||||||
|
for (int i = 0; i < _fileNodes.length; i++) { |
||||||
|
if (_fileNodes[i] == oldNode) { |
||||||
|
_fileNodes[i] = newNode; |
||||||
|
return; |
||||||
|
} |
||||||
|
_replaceNodeInChildren(_fileNodes[i], oldNode, newNode); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void _replaceNodeInChildren(FileNode parent, FileNode oldNode, FileNode newNode) { |
||||||
|
for (int i = 0; i < parent.children.length; i++) { |
||||||
|
if (parent.children[i] == oldNode) { |
||||||
|
parent.children[i] = newNode; |
||||||
|
return; |
||||||
|
} |
||||||
|
_replaceNodeInChildren(parent.children[i], oldNode, newNode); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Future<void> refreshFileTree({bool loadContent = false}) async { |
||||||
|
_isLoading = true; |
||||||
|
notifyListeners(); |
||||||
|
|
||||||
|
try { |
||||||
|
final rootDir = await getApplicationDocumentsDirectory(); |
||||||
|
_fileNodes = [ |
||||||
|
FileNode( |
||||||
|
name: rootDir.path.split(Platform.pathSeparator).last, |
||||||
|
path: rootDir.path, |
||||||
|
isDirectory: true, |
||||||
|
isRoot: true, |
||||||
|
// 初始不加载内容 |
||||||
|
children: loadContent ? await FileService.listDirectory(rootDir.path) : [], |
||||||
|
), |
||||||
|
]; |
||||||
|
} catch (e) { |
||||||
|
debugPrint('Error refreshing file tree: $e'); |
||||||
|
_fileNodes = []; |
||||||
|
} |
||||||
|
|
||||||
|
_isLoading = false; |
||||||
|
notifyListeners(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,60 @@ |
|||||||
|
import 'dart:io'; |
||||||
|
import 'package:win_text_editor/app/models/file_node.dart'; |
||||||
|
|
||||||
|
class FileService { |
||||||
|
/// 延时加载目录内容(不递归) |
||||||
|
static Future<List<FileNode>> listDirectory(String path, {int parentDepth = 0}) async { |
||||||
|
final directory = Directory(path); |
||||||
|
final List<FileNode> nodes = []; |
||||||
|
|
||||||
|
if (await directory.exists()) { |
||||||
|
final entities = directory.listSync(); |
||||||
|
for (final entity in entities) { |
||||||
|
nodes.add( |
||||||
|
FileNode( |
||||||
|
name: entity.path.split(Platform.pathSeparator).last, |
||||||
|
path: entity.path, |
||||||
|
isDirectory: entity is Directory, |
||||||
|
depth: parentDepth + 1, // 关键修复:基于父节点深度+1 |
||||||
|
children: [], |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
return nodes; |
||||||
|
} |
||||||
|
|
||||||
|
/// 递归构建完整文件树(原方法保留备用) |
||||||
|
static Future<List<FileNode>> buildFileTree(String rootPath) async { |
||||||
|
final rootDirectory = Directory(rootPath); |
||||||
|
final List<FileNode> nodes = []; |
||||||
|
|
||||||
|
if (await rootDirectory.exists()) { |
||||||
|
final entities = rootDirectory.listSync(); |
||||||
|
|
||||||
|
for (final entity in entities) { |
||||||
|
final node = FileNode( |
||||||
|
name: entity.path.split(Platform.pathSeparator).last, |
||||||
|
path: entity.path, |
||||||
|
isDirectory: entity is Directory, |
||||||
|
); |
||||||
|
|
||||||
|
if (entity is Directory) { |
||||||
|
node.children.addAll(await buildFileTree(entity.path)); |
||||||
|
} |
||||||
|
|
||||||
|
nodes.add(node); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return nodes; |
||||||
|
} |
||||||
|
|
||||||
|
static Future<String> readFile(String filePath) async { |
||||||
|
return await File(filePath).readAsString(); |
||||||
|
} |
||||||
|
|
||||||
|
static Future<void> writeFile(String filePath, String content) async { |
||||||
|
await File(filePath).writeAsString(content); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,31 @@ |
|||||||
|
class SyntaxService { |
||||||
|
static String detectFileType(String fileName) { |
||||||
|
final extension = fileName.split('.').last.toLowerCase(); |
||||||
|
|
||||||
|
switch (extension) { |
||||||
|
case 'dart': |
||||||
|
return 'dart'; |
||||||
|
case 'java': |
||||||
|
return 'java'; |
||||||
|
case 'js': |
||||||
|
return 'javascript'; |
||||||
|
case 'py': |
||||||
|
return 'python'; |
||||||
|
case 'html': |
||||||
|
return 'html'; |
||||||
|
case 'css': |
||||||
|
return 'css'; |
||||||
|
case 'json': |
||||||
|
return 'json'; |
||||||
|
case 'xml': |
||||||
|
return 'xml'; |
||||||
|
case 'md': |
||||||
|
return 'markdown'; |
||||||
|
case 'yaml': |
||||||
|
case 'yml': |
||||||
|
return 'yaml'; |
||||||
|
default: |
||||||
|
return 'text'; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,97 @@ |
|||||||
|
import 'package:flutter/material.dart'; |
||||||
|
import 'package:provider/provider.dart'; |
||||||
|
import 'package:win_text_editor/app/providers/editor_provider.dart'; |
||||||
|
import 'package:win_text_editor/app/widgets/tab_bar.dart'; |
||||||
|
|
||||||
|
import '../models/editor_tab.dart'; |
||||||
|
|
||||||
|
class EditorPane extends StatelessWidget { |
||||||
|
const EditorPane({super.key}); |
||||||
|
|
||||||
|
@override |
||||||
|
Widget build(BuildContext context) { |
||||||
|
final editorProvider = Provider.of<EditorProvider>(context); |
||||||
|
|
||||||
|
return Column( |
||||||
|
children: [ |
||||||
|
const EditorTabBar(), |
||||||
|
Expanded( |
||||||
|
child: IndexedStack( |
||||||
|
index: editorProvider.currentLayout, |
||||||
|
children: [ |
||||||
|
// 平铺布局 |
||||||
|
GridView.builder( |
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2), |
||||||
|
itemCount: editorProvider.openTabs.length, |
||||||
|
itemBuilder: (context, index) { |
||||||
|
return EditorTabContent(tab: editorProvider.openTabs[index]); |
||||||
|
}, |
||||||
|
), |
||||||
|
// 层叠布局 |
||||||
|
Stack( |
||||||
|
children: |
||||||
|
editorProvider.openTabs.map((tab) { |
||||||
|
return Positioned( |
||||||
|
top: 20.0 * editorProvider.openTabs.indexOf(tab), |
||||||
|
left: 20.0 * editorProvider.openTabs.indexOf(tab), |
||||||
|
right: 20.0 * editorProvider.openTabs.indexOf(tab), |
||||||
|
bottom: 20.0 * editorProvider.openTabs.indexOf(tab), |
||||||
|
child: EditorTabContent(tab: tab), |
||||||
|
); |
||||||
|
}).toList(), |
||||||
|
), |
||||||
|
// 单页布局 |
||||||
|
if (editorProvider.openTabs.isNotEmpty) |
||||||
|
EditorTabContent(tab: editorProvider.openTabs[editorProvider.activeTabIndex]), |
||||||
|
], |
||||||
|
), |
||||||
|
), |
||||||
|
], |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class EditorTabContent extends StatelessWidget { |
||||||
|
final EditorTab tab; |
||||||
|
|
||||||
|
const EditorTabContent({super.key, required this.tab}); |
||||||
|
|
||||||
|
@override |
||||||
|
Widget build(BuildContext context) { |
||||||
|
final editorProvider = Provider.of<EditorProvider>(context, listen: false); |
||||||
|
|
||||||
|
return Card( |
||||||
|
margin: const EdgeInsets.all(8.0), |
||||||
|
child: Column( |
||||||
|
children: [ |
||||||
|
Padding( |
||||||
|
padding: const EdgeInsets.all(8.0), |
||||||
|
child: Row( |
||||||
|
children: [ |
||||||
|
Text(tab.title), |
||||||
|
const Spacer(), |
||||||
|
IconButton( |
||||||
|
icon: const Icon(Icons.content_copy), |
||||||
|
onPressed: () => editorProvider.copyToClipboard(tab.id), |
||||||
|
), |
||||||
|
IconButton( |
||||||
|
icon: const Icon(Icons.save), |
||||||
|
onPressed: () => editorProvider.saveFile(tab.id), |
||||||
|
), |
||||||
|
IconButton( |
||||||
|
icon: const Icon(Icons.close), |
||||||
|
onPressed: () => editorProvider.closeTab(tab.id), |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
), |
||||||
|
Expanded( |
||||||
|
child: SingleChildScrollView( |
||||||
|
child: Container(padding: const EdgeInsets.all(8.0), child: Text(tab.content)), |
||||||
|
), |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,207 @@ |
|||||||
|
import 'package:file_picker/file_picker.dart'; |
||||||
|
import 'package:flutter/material.dart'; |
||||||
|
import 'package:provider/provider.dart'; |
||||||
|
|
||||||
|
import '../models/file_node.dart'; |
||||||
|
import '../providers/file_provider.dart'; |
||||||
|
import '../providers/editor_provider.dart'; |
||||||
|
|
||||||
|
class FileExplorer extends StatefulWidget { |
||||||
|
const FileExplorer({super.key}); |
||||||
|
|
||||||
|
@override |
||||||
|
State<FileExplorer> createState() => _FileExplorerState(); |
||||||
|
} |
||||||
|
|
||||||
|
class _FileExplorerState extends State<FileExplorer> { |
||||||
|
final ScrollController _scrollController = ScrollController(); // 添加这行 |
||||||
|
|
||||||
|
@override |
||||||
|
void dispose() { |
||||||
|
_scrollController.dispose(); // 记得释放资源 |
||||||
|
super.dispose(); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
void initState() { |
||||||
|
super.initState(); |
||||||
|
} |
||||||
|
|
||||||
|
Future<void> _promptForDirectory(BuildContext context) async { |
||||||
|
final fileProvider = Provider.of<FileProvider>(context, listen: false); |
||||||
|
final String? selectedDirectory = await FilePicker.platform.getDirectoryPath(); |
||||||
|
|
||||||
|
if (selectedDirectory != null) { |
||||||
|
await fileProvider.setRootPath(selectedDirectory); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
Widget build(BuildContext context) { |
||||||
|
final fileProvider = Provider.of<FileProvider>(context); |
||||||
|
|
||||||
|
return Container( |
||||||
|
color: Colors.grey[200], |
||||||
|
child: Column( |
||||||
|
children: [ |
||||||
|
Padding( |
||||||
|
padding: const EdgeInsets.all(4.0), |
||||||
|
child: Row( |
||||||
|
children: [ |
||||||
|
Expanded( |
||||||
|
child: TextField( |
||||||
|
decoration: const InputDecoration( |
||||||
|
hintText: 'Search files...', |
||||||
|
prefixIcon: Icon(Icons.search), |
||||||
|
isDense: true, |
||||||
|
), |
||||||
|
onChanged: (value) => fileProvider.searchFiles(value), |
||||||
|
), |
||||||
|
), |
||||||
|
IconButton( |
||||||
|
icon: const Icon(Icons.folder_open), // 改为文件夹打开图标 |
||||||
|
onPressed: () => _promptForDirectory(context), |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
), |
||||||
|
Expanded( |
||||||
|
child: Scrollbar( |
||||||
|
controller: _scrollController, // 添加控制器绑定 |
||||||
|
thumbVisibility: true, // 始终显示滚动条 |
||||||
|
interactive: true, // 启用滚动条拖动交互 |
||||||
|
child: |
||||||
|
fileProvider.isLoading |
||||||
|
? const Center(child: CircularProgressIndicator()) |
||||||
|
: fileProvider.fileNodes.isEmpty |
||||||
|
? Center( |
||||||
|
child: Column( |
||||||
|
mainAxisAlignment: MainAxisAlignment.center, |
||||||
|
children: [ |
||||||
|
const Text('No directory selected'), |
||||||
|
TextButton( |
||||||
|
onPressed: () => _promptForDirectory(context), |
||||||
|
child: const Text('Open Folder'), |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
) |
||||||
|
: ListView.builder( |
||||||
|
controller: _scrollController, // 确保ListView使用相同的控制器 |
||||||
|
itemCount: _countVisibleNodes(fileProvider.fileNodes), |
||||||
|
itemBuilder: (context, index) { |
||||||
|
final node = _getVisibleNode(fileProvider.fileNodes, index); |
||||||
|
return _FileNodeWidget( |
||||||
|
key: ValueKey(node.path), |
||||||
|
node: node, |
||||||
|
onTap: () async { |
||||||
|
if (node.isDirectory) { |
||||||
|
await fileProvider.loadDirectoryContents(node); |
||||||
|
} |
||||||
|
}, |
||||||
|
); |
||||||
|
}, |
||||||
|
), |
||||||
|
), |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 辅助方法:计算所有可见节点数量 |
||||||
|
int _countVisibleNodes(List<FileNode> nodes) { |
||||||
|
int count = 0; |
||||||
|
for (final node in nodes) { |
||||||
|
count++; |
||||||
|
if (node.isDirectory && node.isExpanded) { |
||||||
|
count += _countVisibleNodes(node.children); |
||||||
|
} |
||||||
|
} |
||||||
|
return count; |
||||||
|
} |
||||||
|
|
||||||
|
// 辅助方法:根据索引获取可见节点 |
||||||
|
FileNode _getVisibleNode(List<FileNode> nodes, int index) { |
||||||
|
int current = 0; |
||||||
|
for (final node in nodes) { |
||||||
|
if (current == index) return node; |
||||||
|
current++; |
||||||
|
if (node.isDirectory && node.isExpanded) { |
||||||
|
final childCount = _countVisibleNodes(node.children); |
||||||
|
if (index - current < childCount) { |
||||||
|
return _getVisibleNode(node.children, index - current); |
||||||
|
} |
||||||
|
current += childCount; |
||||||
|
} |
||||||
|
} |
||||||
|
throw Exception('Index out of bounds: $index (max: ${current - 1})'); |
||||||
|
} |
||||||
|
|
||||||
|
class _FileNodeWidget extends StatelessWidget { |
||||||
|
final FileNode node; |
||||||
|
final VoidCallback onTap; |
||||||
|
|
||||||
|
const _FileNodeWidget({ |
||||||
|
Key? key, // 声明 key 参数 |
||||||
|
required this.node, |
||||||
|
required this.onTap, |
||||||
|
}) : 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); |
||||||
|
} |
||||||
|
}, |
||||||
|
child: Container( |
||||||
|
padding: const EdgeInsets.symmetric(vertical: 0), // 缩小垂直间距 |
||||||
|
child: ListTile( |
||||||
|
dense: true, // 启用紧凑模式 |
||||||
|
visualDensity: const VisualDensity( |
||||||
|
vertical: -4, // 负值减少高度,正值增加高度 |
||||||
|
), |
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 2), // 调整水平内边距 |
||||||
|
minVerticalPadding: 0, // 最小垂直内边距设为0 |
||||||
|
leading: _buildLeadingWidget(context), |
||||||
|
title: Text( |
||||||
|
node.name, |
||||||
|
style: Theme.of(context).textTheme.bodyMedium, // 使用标准文本样式 |
||||||
|
), |
||||||
|
), |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
Widget _buildLeadingWidget(BuildContext context) { |
||||||
|
return Row( |
||||||
|
mainAxisSize: MainAxisSize.min, |
||||||
|
children: [ |
||||||
|
// Indentation lines |
||||||
|
...List.generate(node.depth, (index) { |
||||||
|
return Padding( |
||||||
|
padding: const EdgeInsets.only(left: 6, right: 6.0), |
||||||
|
child: Container(width: 1.0, height: 32.0, color: Colors.grey[500]), |
||||||
|
); |
||||||
|
}), |
||||||
|
node.isDirectory |
||||||
|
? Icon( |
||||||
|
node.isExpanded ? Icons.expand_more : Icons.chevron_right, |
||||||
|
color: Colors.cyan[200], |
||||||
|
size: 20, // 缩小图标尺寸 |
||||||
|
) |
||||||
|
: const Icon(null, size: 2), |
||||||
|
node.isDirectory |
||||||
|
? Icon(Icons.folder, color: Colors.cyan[500], size: 16) |
||||||
|
: Icon(Icons.file_open, color: Colors.amber[700], size: 16), |
||||||
|
], |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,83 @@ |
|||||||
|
import 'package:flutter/material.dart'; |
||||||
|
import 'package:provider/provider.dart'; |
||||||
|
import 'package:win_text_editor/app/providers/editor_provider.dart'; |
||||||
|
|
||||||
|
import '../providers/file_provider.dart'; |
||||||
|
|
||||||
|
class EditorTabBar extends StatelessWidget { |
||||||
|
const EditorTabBar({super.key}); |
||||||
|
|
||||||
|
@override |
||||||
|
Widget build(BuildContext context) { |
||||||
|
final editorProvider = Provider.of<EditorProvider>(context); |
||||||
|
|
||||||
|
return Container( |
||||||
|
height: 40, |
||||||
|
color: Colors.grey[300], |
||||||
|
child: Row( |
||||||
|
children: [ |
||||||
|
// 布局切换按钮 |
||||||
|
PopupMenuButton<int>( |
||||||
|
icon: const Icon(Icons.grid_view), |
||||||
|
itemBuilder: |
||||||
|
(context) => [ |
||||||
|
const PopupMenuItem(value: 0, child: Text('平铺布局')), |
||||||
|
const PopupMenuItem(value: 1, child: Text('层叠布局')), |
||||||
|
const PopupMenuItem(value: 2, child: Text('单页布局')), |
||||||
|
], |
||||||
|
onSelected: (value) => editorProvider.changeLayout(value), |
||||||
|
), |
||||||
|
// 标签页 |
||||||
|
Expanded( |
||||||
|
child: ListView.builder( |
||||||
|
scrollDirection: Axis.horizontal, |
||||||
|
itemCount: editorProvider.openTabs.length, |
||||||
|
itemBuilder: (context, index) { |
||||||
|
final tab = editorProvider.openTabs[index]; |
||||||
|
return InkWell( |
||||||
|
onTap: () => editorProvider.setActiveTab(index), |
||||||
|
child: Container( |
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0), |
||||||
|
decoration: BoxDecoration( |
||||||
|
color: |
||||||
|
editorProvider.activeTabIndex == index ? Colors.white : Colors.grey[200], |
||||||
|
border: Border( |
||||||
|
bottom: BorderSide( |
||||||
|
color: |
||||||
|
editorProvider.activeTabIndex == index |
||||||
|
? Colors.blue |
||||||
|
: Colors.transparent, |
||||||
|
width: 2.0, |
||||||
|
), |
||||||
|
), |
||||||
|
), |
||||||
|
child: Center( |
||||||
|
child: Row( |
||||||
|
children: [ |
||||||
|
Text(tab.title), |
||||||
|
const SizedBox(width: 8), |
||||||
|
IconButton( |
||||||
|
icon: const Icon(Icons.close, size: 16), |
||||||
|
onPressed: () => editorProvider.closeTab(tab.id), |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
), |
||||||
|
), |
||||||
|
); |
||||||
|
}, |
||||||
|
), |
||||||
|
), |
||||||
|
// 打开文件按钮 |
||||||
|
IconButton( |
||||||
|
icon: const Icon(Icons.add), |
||||||
|
onPressed: () async { |
||||||
|
final fileProvider = Provider.of<FileProvider>(context, listen: false); |
||||||
|
await fileProvider.pickAndOpenFile(); |
||||||
|
}, |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -1,122 +1,38 @@ |
|||||||
import 'package:flutter/material.dart'; |
import 'package:flutter/material.dart'; |
||||||
|
import 'package:window_manager/window_manager.dart'; |
||||||
|
import 'app/app.dart'; |
||||||
|
|
||||||
|
void main() async { |
||||||
|
WidgetsFlutterBinding.ensureInitialized(); |
||||||
|
|
||||||
|
// 配置窗口 |
||||||
|
await windowManager.ensureInitialized(); |
||||||
|
WindowOptions windowOptions = const WindowOptions( |
||||||
|
size: Size(1200, 800), |
||||||
|
center: true, |
||||||
|
title: 'Win Text Editor', |
||||||
|
); |
||||||
|
windowManager.waitUntilReadyToShow(windowOptions, () async { |
||||||
|
await windowManager.show(); |
||||||
|
await windowManager.focus(); |
||||||
|
}); |
||||||
|
|
||||||
void main() { |
|
||||||
runApp(const MyApp()); |
runApp(const MyApp()); |
||||||
} |
} |
||||||
|
|
||||||
class MyApp extends StatelessWidget { |
class MyApp extends StatelessWidget { |
||||||
const MyApp({super.key}); |
const MyApp({super.key}); |
||||||
|
|
||||||
// This widget is the root of your application. |
|
||||||
@override |
@override |
||||||
Widget build(BuildContext context) { |
Widget build(BuildContext context) { |
||||||
return MaterialApp( |
return MaterialApp( |
||||||
title: 'Flutter Demo', |
title: '文本编辑器', |
||||||
|
debugShowCheckedModeBanner: false, |
||||||
theme: ThemeData( |
theme: ThemeData( |
||||||
// This is the theme of your application. |
primarySwatch: Colors.blue, |
||||||
// |
visualDensity: VisualDensity.adaptivePlatformDensity, |
||||||
// TRY THIS: Try running your application with "flutter run". You'll see |
|
||||||
// the application has a purple toolbar. Then, without quitting the app, |
|
||||||
// try changing the seedColor in the colorScheme below to Colors.green |
|
||||||
// and then invoke "hot reload" (save your changes or press the "hot |
|
||||||
// reload" button in a Flutter-supported IDE, or press "r" if you used |
|
||||||
// the command line to start the app). |
|
||||||
// |
|
||||||
// Notice that the counter didn't reset back to zero; the application |
|
||||||
// state is not lost during the reload. To reset the state, use hot |
|
||||||
// restart instead. |
|
||||||
// |
|
||||||
// This works for code too, not just values: Most code changes can be |
|
||||||
// tested with just a hot reload. |
|
||||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), |
|
||||||
), |
), |
||||||
home: const MyHomePage(title: 'Flutter Demo Home Page'), |
home: const AppScaffold(), |
||||||
); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
class MyHomePage extends StatefulWidget { |
|
||||||
const MyHomePage({super.key, required this.title}); |
|
||||||
|
|
||||||
// This widget is the home page of your application. It is stateful, meaning |
|
||||||
// that it has a State object (defined below) that contains fields that affect |
|
||||||
// how it looks. |
|
||||||
|
|
||||||
// This class is the configuration for the state. It holds the values (in this |
|
||||||
// case the title) provided by the parent (in this case the App widget) and |
|
||||||
// used by the build method of the State. Fields in a Widget subclass are |
|
||||||
// always marked "final". |
|
||||||
|
|
||||||
final String title; |
|
||||||
|
|
||||||
@override |
|
||||||
State<MyHomePage> createState() => _MyHomePageState(); |
|
||||||
} |
|
||||||
|
|
||||||
class _MyHomePageState extends State<MyHomePage> { |
|
||||||
int _counter = 0; |
|
||||||
|
|
||||||
void _incrementCounter() { |
|
||||||
setState(() { |
|
||||||
// This call to setState tells the Flutter framework that something has |
|
||||||
// changed in this State, which causes it to rerun the build method below |
|
||||||
// so that the display can reflect the updated values. If we changed |
|
||||||
// _counter without calling setState(), then the build method would not be |
|
||||||
// called again, and so nothing would appear to happen. |
|
||||||
_counter++; |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
@override |
|
||||||
Widget build(BuildContext context) { |
|
||||||
// This method is rerun every time setState is called, for instance as done |
|
||||||
// by the _incrementCounter method above. |
|
||||||
// |
|
||||||
// The Flutter framework has been optimized to make rerunning build methods |
|
||||||
// fast, so that you can just rebuild anything that needs updating rather |
|
||||||
// than having to individually change instances of widgets. |
|
||||||
return Scaffold( |
|
||||||
appBar: AppBar( |
|
||||||
// TRY THIS: Try changing the color here to a specific color (to |
|
||||||
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar |
|
||||||
// change color while the other colors stay the same. |
|
||||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary, |
|
||||||
// Here we take the value from the MyHomePage object that was created by |
|
||||||
// the App.build method, and use it to set our appbar title. |
|
||||||
title: Text(widget.title), |
|
||||||
), |
|
||||||
body: Center( |
|
||||||
// Center is a layout widget. It takes a single child and positions it |
|
||||||
// in the middle of the parent. |
|
||||||
child: Column( |
|
||||||
// Column is also a layout widget. It takes a list of children and |
|
||||||
// arranges them vertically. By default, it sizes itself to fit its |
|
||||||
// children horizontally, and tries to be as tall as its parent. |
|
||||||
// |
|
||||||
// Column has various properties to control how it sizes itself and |
|
||||||
// how it positions its children. Here we use mainAxisAlignment to |
|
||||||
// center the children vertically; the main axis here is the vertical |
|
||||||
// axis because Columns are vertical (the cross axis would be |
|
||||||
// horizontal). |
|
||||||
// |
|
||||||
// TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint" |
|
||||||
// action in the IDE, or press "p" in the console), to see the |
|
||||||
// wireframe for each widget. |
|
||||||
mainAxisAlignment: MainAxisAlignment.center, |
|
||||||
children: <Widget>[ |
|
||||||
const Text('You have pushed the button this many times:'), |
|
||||||
Text( |
|
||||||
'$_counter', |
|
||||||
style: Theme.of(context).textTheme.headlineMedium, |
|
||||||
), |
|
||||||
], |
|
||||||
), |
|
||||||
), |
|
||||||
floatingActionButton: FloatingActionButton( |
|
||||||
onPressed: _incrementCounter, |
|
||||||
tooltip: 'Increment', |
|
||||||
child: const Icon(Icons.add), |
|
||||||
), // This trailing comma makes auto-formatting nicer for build methods. |
|
||||||
); |
); |
||||||
} |
} |
||||||
} |
} |
||||||
|
Loading…
Reference in new issue