file_picker_writable 2.2.0
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.
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,
),
],
],
),
),
),
);
}
}