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 [...]
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.