directory_bookmarks 0.1.2
directory_bookmarks: ^0.1.2 copied to clipboard
A Flutter plugin for cross-platform directory bookmarking and secure file operations. Provides a consistent API for handling directory access and file operations, with special support for platform-spe [...]
import 'dart:async';
import 'dart:io';
import 'package:directory_bookmarks/directory_bookmarks.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:path/path.dart' as path;
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Directory Bookmarks Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'Directory Bookmarks Example'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
String _currentPath = '';
List<String> _items = [];
String? _errorMessage;
StreamSubscription<FileSystemEvent>? _directoryWatcher;
BookmarkData? _bookmark;
@override
void initState() {
super.initState();
_initializeBookmark();
}
@override
void dispose() {
_directoryWatcher?.cancel();
super.dispose();
}
Future<void> _initializeBookmark() async {
try {
final bookmark = await DirectoryBookmarkHandler.resolveBookmark();
if (bookmark == null) {
final directoryPath = await FilePicker.platform.getDirectoryPath(
dialogTitle: 'Select a directory to bookmark',
);
if (directoryPath != null) {
await DirectoryBookmarkHandler.saveBookmark(directoryPath);
_bookmark = await DirectoryBookmarkHandler.resolveBookmark();
}
} else {
_bookmark = bookmark;
}
_setupDirectoryWatcher();
_loadCurrentDirectory();
} catch (e) {
setState(() {
_errorMessage = 'Error initializing bookmark: $e';
});
}
}
Future<void> _setupDirectoryWatcher() async {
_directoryWatcher?.cancel();
try {
if (_bookmark != null) {
final directory = Directory(_bookmark!.path);
_directoryWatcher = directory.watch(recursive: true).listen((event) {
_loadCurrentDirectory();
});
}
} catch (e) {
debugPrint('Error setting up directory watcher: $e');
}
}
Future<void> _loadCurrentDirectory() async {
if (_bookmark == null) return;
try {
final files = _currentPath.isEmpty
? await DirectoryBookmarkHandler.listFiles()
: await DirectoryBookmarkHandler.listFilesInPath(_currentPath);
if (files != null) {
setState(() {
_items = files;
_errorMessage = null;
});
}
} catch (e) {
setState(() {
_errorMessage = 'Error loading directory: $e';
});
}
}
Future<void> _createNewFolder() async {
final name = await showDialog<String>(
context: context,
builder: (context) => const NewFolderDialog(),
);
if (name != null && name.isNotEmpty) {
try {
final folderPath = _currentPath.isEmpty ? name : '$_currentPath/$name';
final success =
await DirectoryBookmarkHandler.createDirectory(folderPath);
if (success) {
_loadCurrentDirectory();
}
} catch (e) {
setState(() {
_errorMessage = 'Error creating folder: $e';
});
}
}
}
Future<void> _pickAndSaveFile() async {
try {
final result = await FilePicker.platform.pickFiles();
if (result != null && result.files.isNotEmpty) {
final file = result.files.first;
if (file.path != null) {
final fileName = path.basename(file.path!);
final targetPath =
_currentPath.isEmpty ? fileName : '$_currentPath/$fileName';
final bytes = await File(file.path!).readAsBytes();
await DirectoryBookmarkHandler.saveBytesToFileInPath(
targetPath,
bytes,
);
_loadCurrentDirectory();
}
}
} catch (e) {
setState(() {
_errorMessage = 'Error saving file: $e';
});
}
}
Future<void> _navigateToFolder(String folderName) async {
setState(() {
_currentPath =
_currentPath.isEmpty ? folderName : '$_currentPath/$folderName';
});
_loadCurrentDirectory();
}
Future<void> _navigateUp() async {
if (_currentPath.isEmpty) return;
final lastSlash = _currentPath.lastIndexOf('/');
setState(() {
_currentPath =
lastSlash == -1 ? '' : _currentPath.substring(0, lastSlash);
});
_loadCurrentDirectory();
}
Future<void> _createNewTextFile() async {
final result = await showDialog<Map<String, String>>(
context: context,
builder: (context) => const NewTextFileDialog(),
);
if (result != null) {
try {
// Build the complete file path
final subPath =
result['path']?.isNotEmpty == true ? result['path']! : '';
final filePath = [
if (_currentPath.isNotEmpty) _currentPath,
if (subPath.isNotEmpty) subPath,
result['name']!,
].join('/');
// Create subdirectories if needed
if (subPath.isNotEmpty) {
final dirPath = [
if (_currentPath.isNotEmpty) _currentPath,
subPath,
].join('/');
final dirCreated =
await DirectoryBookmarkHandler.createDirectory(dirPath);
if (!dirCreated) {
throw Exception('Failed to create subdirectories');
}
}
// Save the file
final success = await DirectoryBookmarkHandler.saveStringToFileInPath(
filePath,
result['content']!,
);
if (success) {
_loadCurrentDirectory();
} else {
throw Exception('Failed to save file');
}
} catch (e) {
setState(() {
_errorMessage = 'Error creating file: $e';
});
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: _errorMessage != null
? Center(child: Text(_errorMessage!))
: Column(
children: [
// Directory navigation bar
Container(
padding: const EdgeInsets.all(8),
child: Row(
children: [
IconButton(
icon: const Icon(Icons.arrow_upward),
onPressed: _currentPath.isEmpty ? null : _navigateUp,
tooltip: 'Go up one level',
),
Expanded(
child: Text(
'Current path: ${_currentPath.isEmpty ? '/' : _currentPath}',
overflow: TextOverflow.ellipsis,
),
),
IconButton(
icon: const Icon(Icons.create_new_folder),
onPressed: _createNewFolder,
tooltip: 'Create new folder',
),
IconButton(
icon: const Icon(Icons.note_add),
onPressed: _createNewTextFile,
tooltip: 'Create new text file',
),
IconButton(
icon: const Icon(Icons.folder),
onPressed: _pickAndSaveFile,
tooltip: 'Select folder',
),
],
),
),
// Directory contents
Expanded(
child: ListView.builder(
itemCount: _items.length,
itemBuilder: (context, index) {
final item = _items[index];
final isDirectory = !item.contains('.');
return ListTile(
leading: Icon(
isDirectory ? Icons.folder : _getFileIcon(item),
color: isDirectory ? Colors.amber : null,
),
title: Text(item),
onTap:
isDirectory ? () => _navigateToFolder(item) : null,
);
},
),
),
],
),
);
}
IconData _getFileIcon(String fileName) {
final extension = fileName.split('.').last.toLowerCase();
switch (extension) {
case 'jpg':
case 'jpeg':
case 'png':
case 'gif':
return Icons.image;
case 'pdf':
return Icons.picture_as_pdf;
case 'txt':
case 'md':
case 'json':
return Icons.description;
default:
return Icons.insert_drive_file;
}
}
}
class NewFolderDialog extends StatefulWidget {
const NewFolderDialog({super.key});
@override
State<NewFolderDialog> createState() => _NewFolderDialogState();
}
class _NewFolderDialogState extends State<NewFolderDialog> {
final _controller = TextEditingController();
bool _isValid = false;
@override
void initState() {
super.initState();
_controller.addListener(() {
setState(() {
_isValid = _controller.text.isNotEmpty &&
!_controller.text.contains('/') &&
!_controller.text.contains('\\');
});
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Create New Folder'),
content: TextField(
controller: _controller,
autofocus: true,
decoration: const InputDecoration(
labelText: 'Folder Name',
hintText: 'Enter folder name',
helperText: 'Folder name cannot contain / or \\',
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
TextButton(
onPressed:
_isValid ? () => Navigator.pop(context, _controller.text) : null,
child: const Text('Create'),
),
],
);
}
}
class NewTextFileDialog extends StatefulWidget {
const NewTextFileDialog({super.key});
@override
State<NewTextFileDialog> createState() => _NewTextFileDialogState();
}
class _NewTextFileDialogState extends State<NewTextFileDialog> {
final _nameController = TextEditingController();
final _pathController = TextEditingController();
final _contentController = TextEditingController();
bool _isValid = false;
@override
void initState() {
super.initState();
_nameController.addListener(_validateInput);
_pathController.addListener(_validateInput);
}
@override
void dispose() {
_nameController.dispose();
_pathController.dispose();
_contentController.dispose();
super.dispose();
}
void _validateInput() {
setState(() {
final fileName = _nameController.text;
final path = _pathController.text;
_isValid = fileName.isNotEmpty &&
!fileName.contains('/') &&
!fileName.contains('\\') &&
(path.isEmpty ||
(path.split('/').every((part) =>
part.isNotEmpty &&
!part.contains('\\') &&
part != '.' &&
part != '..')));
});
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Create New Text File'),
content: SizedBox(
width: 400,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: _nameController,
autofocus: true,
decoration: const InputDecoration(
labelText: 'File Name',
hintText: 'Enter file name (e.g., notes.txt)',
helperText: 'File name cannot contain / or \\',
),
),
const SizedBox(height: 16),
TextField(
controller: _pathController,
decoration: const InputDecoration(
labelText: 'Subdirectory (Optional)',
hintText: 'Enter subdirectory path (e.g., docs/notes)',
helperText: 'Use forward slashes (/) to separate directories',
),
),
const SizedBox(height: 16),
TextField(
controller: _contentController,
maxLines: 5,
decoration: const InputDecoration(
labelText: 'File Content',
hintText: 'Enter file content',
border: OutlineInputBorder(),
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
TextButton(
onPressed: _isValid
? () => Navigator.pop(context, {
'name': _nameController.text,
'path': _pathController.text,
'content': _contentController.text,
})
: null,
child: const Text('Create'),
),
],
);
}
}