persistent_file_access 0.1.1 copy "persistent_file_access: ^0.1.1" to clipboard
persistent_file_access: ^0.1.1 copied to clipboard

Persist and restore access to user-selected files and directories across app restarts.

example/lib/main.dart

import 'dart:async';
import 'dart:io';

import 'package:file_selector/file_selector.dart';
import 'package:flutter/material.dart';
import 'package:persistent_file_access/persistent_file_access.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final access = await PersistentFileAccessSet.open(
    'persistent-file-access-example',
  );
  runApp(PersistentFileAccessExample(access: access));
}

class PersistentFileAccessExample extends StatefulWidget {
  final PersistentFileAccessSet access;

  const PersistentFileAccessExample({required this.access, super.key});

  @override
  State<PersistentFileAccessExample> createState() =>
      _PersistentFileAccessExampleState();
}

class _PersistentFileAccessExampleState
    extends State<PersistentFileAccessExample> {
  StreamSubscription<PersistentAccessSnapshot>? _snapshotSubscription;
  String? _message;
  var _busy = false;

  @override
  void initState() {
    super.initState();
    _snapshotSubscription = widget.access.snapshots.listen((final _) {
      if (mounted) {
        setState(() {});
      }
    });
  }

  @override
  void dispose() {
    unawaited(_snapshotSubscription?.cancel());
    unawaited(_closeAccess());
    super.dispose();
  }

  Future<void> _closeAccess() async {
    try {
      await widget.access.close();
    } catch (error, stackTrace) {
      FlutterError.reportError(
        FlutterErrorDetails(
          exception: error,
          stack: stackTrace,
          library: 'persistent_file_access example',
          context: ErrorDescription('while closing persistent file access'),
        ),
      );
    }
  }

  Future<void> _chooseFile() async {
    setState(() => _busy = true);
    try {
      final selection = await openFile();
      if (mounted && selection != null) {
        await _addEntity(File(selection.path));
      }
    } catch (error) {
      if (mounted) {
        setState(() => _message = 'Could not choose a file: $error');
      }
    } finally {
      if (mounted) {
        setState(() => _busy = false);
      }
    }
  }

  Future<void> _chooseDirectory() async {
    setState(() => _busy = true);
    try {
      final selectedPath = await getDirectoryPath();
      if (mounted && selectedPath != null) {
        await _addEntity(Directory(selectedPath));
      }
    } catch (error) {
      if (mounted) {
        setState(() => _message = 'Could not choose a folder: $error');
      }
    } finally {
      if (mounted) {
        setState(() => _busy = false);
      }
    }
  }

  Future<void> _addEntity(final FileSystemEntity entity) async {
    try {
      final result = await widget.access.addDetailed(entity);
      if (mounted) {
        setState(() {
          _message = switch (result.disposition) {
            PersistentFileAccessAddDisposition.added
                when result.entry.isAvailable =>
              'Access saved.',
            PersistentFileAccessAddDisposition.added =>
              'Access saved, but the item is currently unavailable.',
            PersistentFileAccessAddDisposition.replaced => 'Access updated.',
            PersistentFileAccessAddDisposition.recovered => 'Access recovered.',
            PersistentFileAccessAddDisposition.alreadyPresent
                when !result.entry.isAvailable =>
              'The saved item is still unavailable.',
            PersistentFileAccessAddDisposition.alreadyPresent =>
              'That item is already saved.',
          };
        });
      }
    } catch (error) {
      if (mounted) {
        setState(() => _message = 'Could not save access: $error');
      }
    }
  }

  Future<void> _removeEntry(final PersistentAccessEntry entry) async {
    setState(() => _busy = true);
    try {
      final removed = await widget.access.removeEntry(entry);
      if (mounted) {
        setState(() {
          _message =
              removed ? 'Saved access removed.' : 'Item already removed.';
        });
      }
    } catch (error) {
      if (mounted) {
        setState(() => _message = 'Could not remove access: $error');
      }
    } finally {
      if (mounted) {
        setState(() => _busy = false);
      }
    }
  }

  Future<void> _retryEntry(final PersistentAccessEntry entry) async {
    setState(() => _busy = true);
    try {
      final retried = await widget.access.retryEntry(entry);
      if (mounted) {
        setState(() {
          _message = switch (retried?.status) {
            PersistentAccessStatus.available => 'Access recovered.',
            PersistentAccessStatus.temporarilyUnavailable =>
              'The item is still unavailable.',
            PersistentAccessStatus.invalid => 'The saved access is invalid.',
            null => 'Item already removed.',
          };
        });
      }
    } catch (error) {
      if (mounted) {
        setState(() => _message = 'Could not retry access: $error');
      }
    } finally {
      if (mounted) {
        setState(() => _busy = false);
      }
    }
  }

  Future<void> _inspectEntry(final PersistentAccessEntry entry) async {
    setState(() => _busy = true);
    try {
      final message = await widget.access.withAccess(entry, (
        final entity,
      ) async {
        if (entity is File) {
          return 'Read ${await entity.length()} bytes.';
        }
        final count = await (entity as Directory).list().length;
        return 'Read $count folder entries.';
      });
      if (mounted) {
        setState(() => _message = message);
      }
    } catch (error) {
      if (mounted) {
        setState(() => _message = 'Could not access the item: $error');
      }
    } finally {
      if (mounted) {
        setState(() => _busy = false);
      }
    }
  }

  @override
  Widget build(final BuildContext context) {
    final entries = widget.access.snapshot.entries;
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Persistent File Access')),
        body: Padding(
          padding: const EdgeInsets.all(24),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              Wrap(
                spacing: 12,
                runSpacing: 12,
                children: [
                  FilledButton.icon(
                    onPressed: _busy ? null : _chooseFile,
                    icon: const Icon(Icons.insert_drive_file_outlined),
                    label: const Text('Choose file'),
                  ),
                  FilledButton.tonalIcon(
                    onPressed: _busy ? null : _chooseDirectory,
                    icon: const Icon(Icons.folder_open_outlined),
                    label: const Text('Choose folder'),
                  ),
                ],
              ),
              if (_busy) ...[
                const SizedBox(height: 12),
                const LinearProgressIndicator(),
              ],
              if (_message case final message?) ...[
                const SizedBox(height: 12),
                Text(message),
              ],
              const SizedBox(height: 24),
              Expanded(
                child: ListView(
                  children: [
                    for (final entry in entries)
                      ListTile(
                        leading: Icon(
                          entry.kind == PersistentAccessEntityKind.directory
                              ? Icons.folder_outlined
                              : Icons.insert_drive_file_outlined,
                        ),
                        title: Text(entry.lastKnownPath ?? 'Unknown path'),
                        subtitle: Text(entry.status.name),
                        trailing: Row(
                          mainAxisSize: MainAxisSize.min,
                          children: [
                            if (entry.isAvailable)
                              IconButton(
                                tooltip: 'Access item',
                                icon: const Icon(Icons.visibility_outlined),
                                onPressed:
                                    _busy ? null : () => _inspectEntry(entry),
                              ),
                            if (!entry.isAvailable)
                              IconButton(
                                tooltip: 'Retry access',
                                icon: const Icon(Icons.refresh),
                                onPressed:
                                    _busy ? null : () => _retryEntry(entry),
                              ),
                            IconButton(
                              tooltip: 'Remove saved access',
                              icon: const Icon(Icons.delete_outline),
                              onPressed:
                                  _busy ? null : () => _removeEntry(entry),
                            ),
                          ],
                        ),
                      ),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
2
likes
160
points
0
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Persist and restore access to user-selected files and directories across app restarts.

Repository (GitHub)
View/report issues

Topics

#files #persistence #macos #windows #bookmarks

License

BSD-3-Clause (license)

Dependencies

crypto, flutter, macos_security_scoped_bookmarks, path, persistent_set

More

Packages that depend on persistent_file_access