Last active
March 22, 2025 12:44
-
-
Save rubywai/c7db6717ebe321f11dc4a810b57a9206 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:io'; | |
import 'package:flutter/material.dart'; | |
import 'package:path_provider/path_provider.dart'; | |
void main() { | |
runApp(const MyApp()); | |
} | |
class MyApp extends StatelessWidget { | |
const MyApp({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
title: 'File Explorer', | |
theme: ThemeData( | |
primarySwatch: Colors.blue, | |
useMaterial3: true, | |
), | |
home: const FileExplorerScreen(), | |
); | |
} | |
} | |
class FileItem { | |
final String name; | |
final bool isFolder; | |
final int size; | |
final DateTime createdAt; | |
FileItem({ | |
required this.name, | |
required this.isFolder, | |
required this.size, | |
required this.createdAt, | |
}); | |
} | |
class FileExplorerScreen extends StatefulWidget { | |
const FileExplorerScreen({Key? key}) : super(key: key); | |
@override | |
State<FileExplorerScreen> createState() => _FileExplorerScreenState(); | |
} | |
class _FileExplorerScreenState extends State<FileExplorerScreen> { | |
final List<FileItem> _items = []; | |
String _currentPath = 'Home'; | |
Future<int> _getFolderSize(String folderName) async { | |
int size = 0; | |
String path = await _getCachedStorageDirectory(); | |
Directory directory = Directory('$path/$folderName'); | |
for (final entity in directory.listSync(recursive: true)) { | |
int entitySize = entity.statSync().size; | |
size += entitySize; | |
} | |
return size; | |
} | |
String _formatBytes(int bytes) { | |
if (bytes < 1024) return '$bytes B'; | |
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB'; | |
if (bytes < 1024 * 1024 * 1024) { | |
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB'; | |
} | |
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB'; | |
} | |
void _showFileContents(FileItem file) async { | |
String path = await _getCachedStorageDirectory(); | |
File textFile = File('$path/${file.name}'); | |
Navigator.push( | |
context, | |
MaterialPageRoute( | |
builder: (context) => FileViewScreen( | |
fileName: file.name, | |
text: textFile.readAsStringSync(), | |
onSaved: (String text) async { | |
textFile.writeAsStringSync(text); | |
await _refreshFileOrFolder(); | |
setState(() {}); | |
}, | |
), | |
), | |
); | |
} | |
void _handleRename(FileItem item) { | |
final controller = TextEditingController(text: item.name); | |
showDialog( | |
context: context, | |
builder: (context) => AlertDialog( | |
title: Text('Rename ${item.isFolder ? 'Folder' : 'File'}'), | |
content: TextField( | |
controller: controller, | |
decoration: const InputDecoration( | |
labelText: 'New Name', | |
), | |
autofocus: true, | |
), | |
actions: [ | |
TextButton( | |
onPressed: () => Navigator.pop(context), | |
child: const Text('Cancel'), | |
), | |
TextButton( | |
onPressed: () async { | |
String path = await _getCachedStorageDirectory(); | |
if (item.name == controller.text.trim()) { | |
return; | |
} | |
if (item.isFolder) { | |
Directory directory = Directory("$path/${item.name}"); | |
directory.renameSync('$path/${controller.text}'); | |
} else { | |
File file = File("$path/${item.name}"); | |
file.renameSync('$path/${controller.text}'); | |
} | |
await _refreshFileOrFolder(); | |
setState(() {}); | |
if (context.mounted) { | |
Navigator.pop(context); | |
ScaffoldMessenger.of(context).showSnackBar( | |
SnackBar(content: Text('Renamed to ${controller.text}')), | |
); | |
} | |
}, | |
child: const Text('Rename'), | |
), | |
], | |
), | |
); | |
} | |
void _handleDelete(FileItem item) { | |
showDialog( | |
context: context, | |
builder: (context) => AlertDialog( | |
title: Text('Delete ${item.isFolder ? 'Folder' : 'File'}'), | |
content: Text('Are you sure you want to delete "${item.name}"?'), | |
actions: [ | |
TextButton( | |
onPressed: () => Navigator.pop(context), | |
child: const Text('Cancel'), | |
), | |
TextButton( | |
onPressed: () async { | |
String path = await _getCachedStorageDirectory(); | |
if (item.isFolder) { | |
Directory directory = Directory("$path/${item.name}"); | |
directory.deleteSync(); | |
} else { | |
File file = File("$path/${item.name}"); | |
file.deleteSync(); | |
} | |
await _refreshFileOrFolder(); | |
setState(() {}); | |
if (context.mounted) { | |
Navigator.pop(context); | |
ScaffoldMessenger.of(context).showSnackBar( | |
SnackBar(content: Text('Deleted ${item.name}')), | |
); | |
} | |
}, | |
child: const Text('Delete'), | |
), | |
], | |
), | |
); | |
} | |
Future<String> _getCachedStorageDirectory() async { | |
final cacheDirectory = await getTemporaryDirectory(); | |
return cacheDirectory.path; | |
} | |
Future<void> _refreshFileOrFolder() async { | |
String path = await _getCachedStorageDirectory(); | |
Directory cachedDirectory = Directory(path); | |
List<FileSystemEntity> entities = cachedDirectory.listSync(); | |
_items.clear(); | |
for (FileSystemEntity entity in entities) { | |
String name = entity.path.split('/').last; | |
DateTime createDate = entity.statSync().changed; | |
if (entity is Directory) { | |
int size = await _getFolderSize(name); | |
FileItem folder = FileItem( | |
name: name, | |
isFolder: true, | |
size: size, | |
createdAt: createDate, | |
); | |
_items.add(folder); | |
} else { | |
final size = entity.statSync().size; | |
FileItem file = FileItem( | |
name: name, | |
isFolder: false, | |
size: size, | |
createdAt: createDate, | |
); | |
_items.add(file); | |
} | |
} | |
} | |
void _createNew(bool isFolder) { | |
final controller = TextEditingController(); | |
showDialog( | |
context: context, | |
builder: (context) => AlertDialog( | |
title: Text('Create New ${isFolder ? 'Folder' : 'File'}'), | |
content: TextField( | |
controller: controller, | |
decoration: InputDecoration( | |
labelText: isFolder ? 'Folder Name' : 'File Name', | |
), | |
autofocus: true, | |
), | |
actions: [ | |
TextButton( | |
onPressed: () => Navigator.pop(context), | |
child: const Text('Cancel'), | |
), | |
TextButton( | |
onPressed: () async { | |
if (controller.text.trim().isNotEmpty) { | |
try { | |
String path = await _getCachedStorageDirectory(); | |
if (isFolder) { | |
Directory cachedDirectory = | |
Directory('$path/${controller.text}'); | |
cachedDirectory.createSync(); | |
} else { | |
File file = File("$path/${controller.text}"); | |
file.createSync(); | |
} | |
} catch (e) { | |
if (context.mounted) { | |
ScaffoldMessenger.of(context).showSnackBar( | |
SnackBar( | |
content: Text('Folder create failed'), | |
), | |
); | |
} | |
} finally { | |
await _refreshFileOrFolder(); | |
setState(() {}); | |
if (context.mounted) { | |
Navigator.pop(context); | |
} | |
} | |
} | |
}, | |
child: const Text('Create'), | |
), | |
], | |
), | |
); | |
} | |
@override | |
void initState() { | |
super.initState(); | |
_refreshFileOrFolder(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar( | |
title: Text(_currentPath), | |
actions: [ | |
IconButton( | |
icon: const Icon(Icons.create_new_folder), | |
onPressed: () => _createNew(true), | |
tooltip: 'New Folder', | |
), | |
IconButton( | |
icon: const Icon(Icons.note_add), | |
onPressed: () => _createNew(false), | |
tooltip: 'New File', | |
), | |
], | |
), | |
body: ListView.builder( | |
itemCount: _items.length, | |
itemBuilder: (context, index) { | |
final item = _items[index]; | |
DateTime createDate = item.createdAt; | |
String formattedDate = | |
"${createDate.day}/${createDate.month}/${createDate.year} ${createDate.hour}:${createDate.minute}"; | |
return ListTile( | |
leading: Icon( | |
item.isFolder ? Icons.folder : Icons.insert_drive_file, | |
color: | |
item.isFolder ? Colors.amber.shade300 : Colors.blue.shade300, | |
), | |
title: Text(item.name), | |
subtitle: Text(_formatBytes(item.size)), | |
trailing: Row( | |
mainAxisSize: MainAxisSize.min, | |
children: [ | |
Text(formattedDate), | |
MenuAnchor( | |
builder: (context, controller, child) { | |
return IconButton( | |
icon: const Icon(Icons.more_vert), | |
onPressed: () { | |
if (controller.isOpen) { | |
controller.close(); | |
} else { | |
controller.open(); | |
} | |
}, | |
); | |
}, | |
menuChildren: [ | |
MenuItemButton( | |
child: const Text('Rename'), | |
onPressed: () => _handleRename(item), | |
), | |
MenuItemButton( | |
child: const Text('Delete'), | |
onPressed: () => _handleDelete(item), | |
), | |
], | |
), | |
], | |
), | |
onTap: () { | |
if (item.isFolder) { | |
// In a real app, navigate to the folder's contents | |
setState(() { | |
_currentPath = '${_currentPath}/${item.name}'; | |
}); | |
} else { | |
_showFileContents(item); | |
} | |
}, | |
onLongPress: () {}, | |
); | |
}, | |
), | |
); | |
} | |
} | |
class FileViewScreen extends StatefulWidget { | |
final String fileName; | |
final String text; | |
final Function(String) onSaved; | |
const FileViewScreen({ | |
Key? key, | |
required this.fileName, | |
required this.text, | |
required this.onSaved, | |
}) : super(key: key); | |
@override | |
State<FileViewScreen> createState() => _FileViewScreenState(); | |
} | |
class _FileViewScreenState extends State<FileViewScreen> { | |
final TextEditingController _textController = TextEditingController(); | |
@override | |
void initState() { | |
super.initState(); | |
_textController.text = widget.text; | |
} | |
@override | |
void dispose() { | |
_textController.dispose(); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar( | |
title: Text(widget.fileName), | |
actions: [ | |
IconButton( | |
icon: const Icon(Icons.save), | |
onPressed: () { | |
widget.onSaved(_textController.text); | |
Navigator.pop(context); | |
ScaffoldMessenger.of(context).showSnackBar( | |
const SnackBar(content: Text('File saved')), | |
); | |
}, | |
), | |
], | |
), | |
body: Padding( | |
padding: const EdgeInsets.all(16.0), | |
child: TextField( | |
controller: _textController, | |
maxLines: null, | |
expands: true, | |
decoration: const InputDecoration( | |
border: InputBorder.none, | |
), | |
), | |
), | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment