Directory Bookmarks
A Flutter plugin for cross-platform directory bookmarking and secure file operations. This plugin provides a consistent API for handling directory access and file operations, with special support for platform-specific security features.
Platform Support
Platform | Status | Implementation Details |
---|---|---|
macOS | Supported | Security-scoped bookmarks for persistent directory access |
Android | In Development | Storage Access Framework (partial implementation) |
iOS | Planned | Will use security-scoped bookmarks |
Windows | Planned | Future implementation |
Linux | Planned | Future implementation |
Note: Currently, this package is primarily focused on macOS support. Using it on other platforms will result in unsupported platform errors. We are actively working on expanding platform support.
Features
- Secure Directory Access: Platform-specific secure directory access mechanisms
- macOS: Security-scoped bookmarks
- Android: Storage Access Framework
- Directory Bookmarking: Save and restore access to user-selected directories
- File Operations: Read, write, and list files in bookmarked directories
- Persistent Access: Maintain access to directories across app restarts
- Permission Handling: Built-in permission management and verification
- Resource Management: Automatic cleanup of system resources
Getting Started
Add the package to your pubspec.yaml:
dependencies:
directory_bookmarks: ^0.1.0
Platform-Specific Setup
macOS (Supported)
- Enable App Sandbox and required entitlements in your macOS app. Add the following to your entitlements files:
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.files.bookmarks.app-scope</key>
<true/>
- Register the plugin in your
AppDelegate.swift
:
import directory_bookmarks
class AppDelegate: FlutterAppDelegate {
override func applicationDidFinishLaunching(_ notification: Notification) {
guard let mainWindow = mainFlutterWindow else { return }
guard let controller = mainWindow.contentViewController as? FlutterViewController else { return }
DirectoryBookmarksPlugin.register(with: controller.registrar(forPlugin: "DirectoryBookmarksPlugin"))
super.applicationDidFinishLaunching(notification)
}
}
Android (In Development)
Note: Android support is currently in development. The implementation is partial and may not work as expected.
Add the following permissions to your AndroidManifest.xml
:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
Other Platforms (Planned)
Support for iOS, Windows, and Linux is planned for future releases. Using this package on these platforms will currently result in an UnsupportedError.
API Reference
Directory Bookmark Operations
saveBookmark(String directoryPath, {Map<String, dynamic>? metadata})
: Save a directory bookmark with optional metadataresolveBookmark()
: Resolve and return the current directory bookmark information
File Operations
saveFile(String fileName, List<int> data)
: Save raw data to a file in the bookmarked directorysaveStringToFile(String fileName, String content)
: Save text content to a filesaveBytesToFile(String fileName, Uint8List bytes)
: Save binary data to a filereadFile(String fileName)
: Read raw data from a filereadStringFromFile(String fileName)
: Read text content from a filereadBytesFromFile(String fileName)
: Read binary data from a filelistFiles()
: List all files in the bookmarked directory
Directory Operations
// Create a directory in the bookmarked location
final success = await DirectoryBookmarkHandler.createDirectory('images');
// Create nested directories
await DirectoryBookmarkHandler.createDirectory('images/thumbnails');
// Save a file in a subdirectory (creates directories if they don't exist)
final imageBytes = await File('path/to/image.jpg').readAsBytes();
await DirectoryBookmarkHandler.saveBytesToFileInPath(
'images/thumbnails/image1.jpg',
imageBytes,
);
// Save text file in subdirectory
await DirectoryBookmarkHandler.saveStringToFileInPath(
'docs/notes/note1.txt',
'Hello, World!',
);
// List files in a specific subdirectory
final imageFiles = await DirectoryBookmarkHandler.listFilesInPath('images');
if (imageFiles != null) {
for (final file in imageFiles) {
print('Image file: $file');
}
}
Example: Image Gallery with Subdirectories
class ImageGalleryWithFolders extends StatefulWidget {
const ImageGalleryWithFolders({super.key});
@override
State<ImageGalleryWithFolders> createState() => _ImageGalleryWithFoldersState();
}
class _ImageGalleryWithFoldersState extends State<ImageGalleryWithFolders> {
String _currentPath = '';
List<String> _items = [];
String? _errorMessage;
@override
void initState() {
super.initState();
_loadCurrentDirectory();
}
Future<void> _loadCurrentDirectory() async {
try {
final files = await DirectoryBookmarkHandler.listFilesInPath(_currentPath);
if (files != null) {
setState(() {
_items = files;
_errorMessage = null;
});
}
} catch (e) {
setState(() {
_errorMessage = 'Error loading directory: $e';
});
}
}
Future<void> _createNewFolder(String folderName) async {
try {
final path = _currentPath.isEmpty
? folderName
: '$_currentPath/$folderName';
final success = await DirectoryBookmarkHandler.createDirectory(path);
if (success) {
_loadCurrentDirectory();
}
} catch (e) {
setState(() {
_errorMessage = 'Error creating folder: $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();
}
@override
Widget build(BuildContext context) {
if (_errorMessage != null) {
return Center(child: Text(_errorMessage!));
}
return 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,
),
Text('Current path: ${_currentPath.isEmpty ? '/' : _currentPath}'),
IconButton(
icon: const Icon(Icons.create_new_folder),
onPressed: () async {
final name = await showDialog<String>(
context: context,
builder: (context) => NewFolderDialog(),
);
if (name != null) {
_createNewFolder(name);
}
},
),
],
),
),
// 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 : Icons.image),
title: Text(item),
onTap: isDirectory
? () => _navigateToFolder(item)
: null,
);
},
),
),
],
);
}
}
class NewFolderDialog extends StatelessWidget {
final controller = TextEditingController();
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Create New Folder'),
content: TextField(
controller: controller,
decoration: const InputDecoration(
labelText: 'Folder Name',
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.pop(context, controller.text),
child: const Text('Create'),
),
],
);
}
}
Writing Files
// Save any type of file (base method)
final fileData = await File('path/to/source/file').readAsBytes();
final success = await DirectoryBookmarkHandler.saveFile(
'destination.file',
fileData,
);
// Save text files
final textSuccess = await DirectoryBookmarkHandler.saveStringToFile(
'example.txt',
'Hello, World!',
);
// Save binary files (images, PDFs, etc.)
final imageBytes = await File('path/to/image.jpg').readAsBytes();
final imageSuccess = await DirectoryBookmarkHandler.saveBytesToFile(
'image.jpg',
imageBytes,
);
Reading Files
// Read any type of file (base method)
final fileData = await DirectoryBookmarkHandler.readFile('myfile.dat');
if (fileData != null) {
// Use the file data (List<int>)
}
// Read text files
final textContent = await DirectoryBookmarkHandler.readStringFromFile('example.txt');
if (textContent != null) {
print('File content: $textContent');
}
// Read binary files
final imageBytes = await DirectoryBookmarkHandler.readBytesFromFile('image.jpg');
if (imageBytes != null) {
// Use the image bytes (Uint8List)
final image = Image.memory(imageBytes);
}
Example: Copying a File to Bookmarked Directory
import 'package:directory_bookmarks/directory_bookmarks.dart';
import 'package:file_picker/file_picker.dart';
Future<void> copyFileToBookmark() async {
try {
// Pick a file to copy
final result = await FilePicker.platform.pickFiles();
if (result == null) return;
final file = result.files.first;
if (file.bytes == null) return;
// Save to bookmarked directory
final success = await DirectoryBookmarkHandler.saveBytesToFile(
file.name,
file.bytes!,
);
if (success) {
print('File copied successfully');
} else {
print('Failed to copy file');
}
} catch (e) {
print('Error copying file: $e');
}
}
Permission Management
hasWritePermission()
: Check if write permission is granted for the bookmarked directoryrequestWritePermission()
: Request write permission for the bookmarked directory
Usage
Basic Example
import 'package:directory_bookmarks/directory_bookmarks.dart';
void main() async {
// Check platform support
if (!(defaultTargetPlatform == TargetPlatform.macOS ||
defaultTargetPlatform == TargetPlatform.android)) {
print('Platform not supported');
return;
}
try {
// Select and bookmark a directory
final path = await FilePicker.platform.getDirectoryPath(
dialogTitle: 'Select a directory to bookmark',
);
if (path == null) {
print('No directory selected');
return;
}
// Save the bookmark
final success = await DirectoryBookmarkHandler.saveBookmark(
path,
metadata: {'lastAccessed': DateTime.now().toIso8601String()},
);
if (success) {
print('Directory bookmarked successfully');
} else {
print('Failed to bookmark directory');
return;
}
// Resolve the bookmark
final bookmark = await DirectoryBookmarkHandler.resolveBookmark();
if (bookmark != null) {
print('Bookmarked directory: ${bookmark.path}');
// Check write permission
final hasPermission = await DirectoryBookmarkHandler.hasWritePermission();
if (!hasPermission) {
print('No write permission');
return;
}
// List files
final files = await DirectoryBookmarkHandler.listFiles();
if (files != null) {
print('Files in directory: $files');
}
// Write a file
final writeSuccess = await DirectoryBookmarkHandler.saveStringToFile(
'test.txt',
'Hello, World!',
);
if (writeSuccess) {
print('File written successfully');
}
// Read the file
final content = await DirectoryBookmarkHandler.readStringFromFile('test.txt');
if (content != null) {
print('File content: $content');
}
}
} catch (e) {
print('Error: $e');
}
}
Error Handling
The plugin includes comprehensive error handling:
try {
// Check platform support first
if (!(defaultTargetPlatform == TargetPlatform.macOS ||
defaultTargetPlatform == TargetPlatform.android)) {
print('Platform ${defaultTargetPlatform.name} is not supported yet');
return;
}
// Try to resolve existing bookmark
final bookmark = await DirectoryBookmarkHandler.resolveBookmark();
if (bookmark == null) {
print('No bookmark found, selecting new directory...');
final path = await FilePicker.platform.getDirectoryPath();
if (path == null) {
print('No directory selected');
return;
}
final success = await DirectoryBookmarkHandler.saveBookmark(path);
if (!success) {
print('Failed to bookmark directory');
return;
}
}
// Check permissions
if (!await DirectoryBookmarkHandler.hasWritePermission()) {
print('No write permission for bookmarked directory');
return;
}
// Perform file operations...
} on PlatformException catch (e) {
print('Platform error: ${e.message}');
} catch (e) {
print('Unexpected error: $e');
}
Features and bugs
Please file feature requests and bugs at the issue tracker.
Contributing
Contributions are welcome! Please read our contributing guidelines to get started.
License
This project is licensed under the MIT License - see the LICENSE file for details.