Skip to content

Instantly share code, notes, and snippets.

@rubywai
Last active March 22, 2025 12:44
Show Gist options
  • Save rubywai/c7db6717ebe321f11dc4a810b57a9206 to your computer and use it in GitHub Desktop.
Save rubywai/c7db6717ebe321f11dc4a810b57a9206 to your computer and use it in GitHub Desktop.
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