directory_bookmarks 0.1.2 copy "directory_bookmarks: ^0.1.2" to clipboard
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 [...]

example/lib/main.dart

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'),
        ),
      ],
    );
  }
}
2
likes
130
points
65
downloads

Publisher

unverified uploader

Weekly Downloads

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-specific security features.

Repository (GitHub)
View/report issues
Contributing

Topics

#files #storage #directory #bookmarks #security

Documentation

Documentation
API reference

License

MIT (license)

Dependencies

file_picker, flutter, path, path_provider, permission_handler

More

Packages that depend on directory_bookmarks