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)

  1. 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/>
  1. 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 metadata
  • resolveBookmark(): 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 directory
  • saveStringToFile(String fileName, String content): Save text content to a file
  • saveBytesToFile(String fileName, Uint8List bytes): Save binary data to a file
  • readFile(String fileName): Read raw data from a file
  • readStringFromFile(String fileName): Read text content from a file
  • readBytesFromFile(String fileName): Read binary data from a file
  • listFiles(): 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');
  }
}
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 directory
  • requestWritePermission(): 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.