file_picker_writable 2.2.0 copy "file_picker_writable: ^2.2.0" to clipboard
file_picker_writable: ^2.2.0 copied to clipboard

Flutter plugin to allow users to open files/documents which can be read, and written back at a later time as well as creating new files on external media.

example/lib/main.dart

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';

import 'package:convert/convert.dart';
import 'package:file_picker_writable/file_picker_writable.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:logging_appenders/logging_appenders.dart';
import 'package:simple_json_persistence/simple_json_persistence.dart';

final _logger = Logger('main');

Future<void> main() async {
  Logger.root.level = Level.ALL;
  PrintAppender().attachToLogger(Logger.root);

  runApp(const MyApp());
}

class AppDataBloc {
  final store = SimpleJsonPersistence.getForTypeWithDefault(
    (json) => AppData.fromJson(json),
    defaultCreator: () => AppData(files: []),
    name: 'AppData',
  );
}

class AppData implements HasToJson {
  AppData({required this.files});
  final List<FileInfo> files;

  static AppData fromJson(Map<String, dynamic> json) => AppData(
      files: (json['files'] as List<dynamic>)
          .where((dynamic element) => element != null)
          .map((dynamic e) => FileInfo.fromJson(e as Map<String, dynamic>))
          .toList());

  @override
  Map<String, dynamic> toJson() => <String, dynamic>{
        'files': files,
      };

  AppData copyWith({required List<FileInfo> files}) => AppData(files: files);
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  MyAppState createState() => MyAppState();
}

class MyAppState extends State<MyApp> {
  final AppDataBloc _appDataBloc = AppDataBloc();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MainScreen(
        appDataBloc: _appDataBloc,
      ),
    );
  }
}

class MainScreen extends StatefulWidget {
  const MainScreen({super.key, required this.appDataBloc});
  final AppDataBloc appDataBloc;

  @override
  MainScreenState createState() => MainScreenState();
}

class MainScreenState extends State<MainScreen> {
  AppDataBloc get _appDataBloc => widget.appDataBloc;
  late final FilePickerState _pickerState;

  @override
  void initState() {
    super.initState();
    final state = FilePickerWritable().init();
    _pickerState = state;
    state.registerFileOpenHandler((fileInfo, file) async {
      _logger.fine('got file info. we are mounted:$mounted');
      if (!mounted) {
        return false;
      }
      await SimpleAlertDialog.readFileContentsAndShowDialog(
        fileInfo,
        file,
        context,
        bodyTextPrefix: 'Should open file from external app.\n\n'
            'fileName: ${fileInfo.fileName}\n'
            'uri: ${fileInfo.uri}\n\n\n',
      );
      return true;
    });
    state.registerUriHandler((uri) {
      SimpleAlertDialog(
        titleText: 'Handling Uri',
        bodyText: 'Got a uri to handle: $uri',
      ).show(context);
      return true;
    });
    state.registerErrorEventHandler((errorEvent) async {
      _logger.fine('Handling error event, mounted: $mounted');
      if (!mounted) {
        return false;
      }
      await SimpleAlertDialog(
        titleText: 'Received error event',
        bodyText: errorEvent.message,
      ).show(context);
      return true;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('File Picker Example'),
      ),
      body: SingleChildScrollView(
        child: SizedBox(
          width: double.infinity,
          child: StreamBuilder<AppData>(
            stream: _appDataBloc.store.onValueChangedAndLoad,
            builder: (context, snapshot) => Column(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: <Widget>[
                Wrap(
                  children: <Widget>[
                    ElevatedButton(
                      onPressed: _openFilePicker,
                      child: const Text('Open File Picker'),
                    ),
                    const SizedBox(width: 32),
                    ElevatedButton(
                      onPressed: _openFilePickerForCreate,
                      child: const Text('Create New File'),
                    ),
                    const SizedBox(width: 32),
                    ElevatedButton(
                      onPressed: FilePickerWritable().disposeAllIdentifiers,
                      child: const Text('Dispose All IDs'),
                    ),
                  ],
                ),
                DropTargetDemo(pickerState: _pickerState),
                ...?(!snapshot.hasData
                    ? null
                    : snapshot.data!.files.map((fileInfo) => FileInfoDisplay(
                          fileInfo: fileInfo,
                          appDataBloc: _appDataBloc,
                        ))),
              ],
            ),
          ),
        ),
      ),
    );
  }

  Future<void> _openFilePicker() async {
    final fileInfo =
        await FilePickerWritable().openFile((fileInfo, file) async {
      _logger.fine('Got picker result: $fileInfo');
      final data = await _appDataBloc.store.load();
      await _appDataBloc.store
          .save(data.copyWith(files: data.files + [fileInfo]));
      return fileInfo;
    });
    if (fileInfo == null) {
      _logger.fine('User cancelled.');
    }
  }

  Future<void> _openFilePickerForCreate() async {
    final rand = Random().nextInt(10000000);
    final fileInfo = await FilePickerWritable().openFileForCreate(
      fileName: 'newfile.$rand.codeux',
      writer: (file) async {
        final content = 'File created at ${DateTime.now()}\n\n';
        await file.writeAsString(content);
      },
    );
    if (fileInfo == null) {
      _logger.info('User canceled.');
      return;
    }
    final data = await _appDataBloc.store.load();
    await _appDataBloc.store
        .save(data.copyWith(files: data.files + [fileInfo]));
  }
}

class FileInfoDisplay extends StatelessWidget {
  const FileInfoDisplay({
    super.key,
    required this.fileInfo,
    required this.appDataBloc,
  });

  final AppDataBloc appDataBloc;
  final FileInfo fileInfo;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Card(
        elevation: 2,
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            children: <Widget>[
              const Text('Selected File:'),
              Text(
                fileInfo.fileName ?? 'null',
                maxLines: 4,
                overflow: TextOverflow.ellipsis,
                style: theme.textTheme.bodySmall?.apply(fontSizeFactor: 0.75),
              ),
              Text(
                fileInfo.identifier,
                maxLines: 1,
                overflow: TextOverflow.ellipsis,
              ),
              Text(
                'uri:${fileInfo.uri}',
                style: theme.textTheme.bodyMedium
                    ?.apply(fontSizeFactor: 0.7)
                    .copyWith(fontWeight: FontWeight.bold),
              ),
              Text(
                'fileName: ${fileInfo.fileName}',
                style: theme.textTheme.bodyMedium
                    ?.apply(fontSizeFactor: 0.7)
                    .copyWith(fontWeight: FontWeight.bold),
              ),
              OverflowBar(
                alignment: MainAxisAlignment.end,
                children: <Widget>[
                  TextButton(
                    onPressed: () async {
                      try {
                        await FilePickerWritable().readFile(
                            identifier: fileInfo.identifier,
                            reader: (fileInfo, file) async {
                              await SimpleAlertDialog
                                  .readFileContentsAndShowDialog(
                                      fileInfo, file, context);
                            });
                      } on Exception catch (e) {
                        if (!context.mounted) {
                          return;
                        }
                        await SimpleAlertDialog.showErrorDialog(e, context);
                      }
                    },
                    child: const Text('Read'),
                  ),
                  TextButton(
                    onPressed: () async {
                      await FilePickerWritable().writeFile(
                          identifier: fileInfo.identifier,
                          writer: (file) async {
                            final content =
                                'New Content written at ${DateTime.now()}.\n\n';
                            await file.writeAsString(content);
                            if (!context.mounted) {
                              return;
                            }
                            await SimpleAlertDialog(
                              bodyText: 'Written: $content',
                            ).show(context);
                          });
                    },
                    child: const Text('Overwrite'),
                  ),
                  IconButton(
                    onPressed: () async {
                      try {
                        await FilePickerWritable()
                            .disposeIdentifier(fileInfo.identifier);
                      } on Exception catch (e) {
                        if (!context.mounted) {
                          return;
                        }
                        await SimpleAlertDialog.showErrorDialog(e, context);
                      }
                      final appData = await appDataBloc.store.load();
                      await appDataBloc.store.save(appData.copyWith(
                          files: appData.files
                              .where((element) => element != fileInfo)
                              .toList()));
                    },
                    icon: const Icon(Icons.remove_circle_outline),
                  ),
                ],
              )
            ],
          ),
        ),
      ),
    );
  }
}

class SimpleAlertDialog extends StatelessWidget {
  const SimpleAlertDialog(
      {super.key, this.titleText, required this.bodyText});
  final String? titleText;
  final String bodyText;

  Future<void> show(BuildContext context) =>
      showDialog<void>(context: context, builder: (context) => this);

  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      scrollable: true,
      title: titleText == null ? null : Text(titleText!),
      content: Text(bodyText),
      actions: <Widget>[
        TextButton(
            child: const Text('Ok'),
            onPressed: () {
              Navigator.of(context).pop();
            }),
      ],
    );
  }

  static Future<void> readFileContentsAndShowDialog(
    FileInfo fi,
    File file,
    BuildContext context, {
    String bodyTextPrefix = '',
  }) async {
    final dataList = await file.openRead(0, 64).toList();
    final data = dataList.expand((element) => element).toList();
    final hexString = hex.encode(data);
    final utf8String = utf8.decode(data, allowMalformed: true);
    final fileContentExample = 'hexString: $hexString\n\nutf8: $utf8String';

    if (!context.mounted) {
      return;
    }
    await SimpleAlertDialog(
      titleText: 'Read first ${data.length} bytes of file',
      bodyText: '$bodyTextPrefix $fileContentExample',
    ).show(context);
  }

  static Future<void> showErrorDialog(Exception e, BuildContext context) async {
    await SimpleAlertDialog(
      titleText: 'Error',
      bodyText: e.toString(),
    ).show(context);
  }
}

class DropTargetDemo extends StatefulWidget {
  const DropTargetDemo({super.key, required this.pickerState});
  final FilePickerState pickerState;

  @override
  DropTargetDemoState createState() => DropTargetDemoState();
}

class DropTargetDemoState extends State<DropTargetDemo> {
  bool _hovering = false;
  final List<String> _drops = [];

  @override
  void initState() {
    super.initState();
    widget.pickerState.registerDropHandler(_onDrop);
    widget.pickerState.registerDropHoverHandler(_onHover);
  }

  @override
  void dispose() {
    widget.pickerState.removeDropHandler(_onDrop);
    widget.pickerState.removeDropHoverHandler(_onHover);
    super.dispose();
  }

  Future<bool> _onDrop(DropEvent drop) async {
    final summaries = <String>[];
    for (final item in drop.items) {
      final bytes = await item.file.readAsBytes();
      summaries.add(
          '${item.fileInfo.fileName ?? 'unnamed'} (${bytes.length} bytes)');
      _logger.fine('Drop: ${item.fileInfo}');
    }
    if (!mounted) {
      return true;
    }
    setState(() {
      _drops.addAll(summaries);
    });
    return true;
  }

  void _onHover(bool entered) {
    _logger.fine('Drop hover: $entered');
    if (_hovering == entered) return;
    setState(() {
      _hovering = entered;
    });
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Card(
        elevation: 2,
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            children: <Widget>[
              const Text('Drop target (Android)'),
              const SizedBox(height: 8),
              AnimatedContainer(
                duration: const Duration(milliseconds: 150),
                width: double.infinity,
                padding: const EdgeInsets.all(24.0),
                decoration: BoxDecoration(
                  border: Border.all(
                    color: _hovering
                        ? theme.colorScheme.primary
                        : theme.colorScheme.outline,
                    width: _hovering ? 3.0 : 1.0,
                  ),
                  borderRadius: BorderRadius.circular(8.0),
                ),
                child: Text(
                  _hovering
                      ? 'Release to drop!'
                      : 'Drop files anywhere in the window.',
                  textAlign: TextAlign.center,
                ),
              ),
              ..._drops.map((summary) => Text(
                    summary,
                    style: theme.textTheme.bodySmall,
                  )),
              if (_drops.isEmpty)
                ...[
                  Text(
                    'No drops yet.',
                    style: theme.textTheme.bodySmall,
                  ),
                ],
            ],
          ),
        ),
      ),
    );
  }
}
14
likes
160
points
214
downloads

Documentation

API reference

Publisher

verified publishercodeux.design

Weekly Downloads

Flutter plugin to allow users to open files/documents which can be read, and written back at a later time as well as creating new files on external media.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

convert, flutter, logging, path, path_provider, synchronized

More

Packages that depend on file_picker_writable

Packages that implement file_picker_writable