file_picker_writable 2.1.0+1 file_picker_writable: ^2.1.0+1 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.
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({Key? key}) : super(key: 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({Key? key, required this.appDataBloc}) : super(key: key);
final AppDataBloc appDataBloc;
@override
MainScreenState createState() => MainScreenState();
}
class MainScreenState extends State<MainScreen> {
AppDataBloc get _appDataBloc => widget.appDataBloc;
@override
void initState() {
super.initState();
final state = FilePickerWritable().init();
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'),
),
],
),
...?(!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({
Key? key,
required this.fileInfo,
required this.appDataBloc,
}) : super(key: key);
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),
),
ButtonBar(
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);
// ignore: use_build_context_synchronously
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({Key? key, this.titleText, required this.bodyText})
: super(key: key);
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';
// ignore: use_build_context_synchronously
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);
}
}