mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 19:07:17 +01:00
data_transfer_flutter 0.2.0: exportTextFile + mime types
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzxdWXTUM9dpnCS2wtFkx4
This commit is contained in:
parent
4c417472ec
commit
bbb7f0f0c0
@ -11,7 +11,7 @@ dependencies:
|
|||||||
data_transfer_flutter:
|
data_transfer_flutter:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/kuhyx/utils
|
url: https://github.com/kuhyx/utils
|
||||||
ref: data_transfer_flutter-v0.1.0
|
ref: data_transfer_flutter-v0.2.0
|
||||||
path: data_transfer_flutter
|
path: data_transfer_flutter
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -26,6 +26,7 @@ final map = await pickJson(); // null when cancelled
|
|||||||
use the system "Save as" document picker (`file_picker` `saveFile`, i.e.
|
use the system "Save as" document picker (`file_picker` `saveFile`, i.e.
|
||||||
ACTION_CREATE_DOCUMENT), desktop a save dialog (`file_selector`), web a
|
ACTION_CREATE_DOCUMENT), desktop a save dialog (`file_selector`), web a
|
||||||
browser download. Returns false when cancelled.
|
browser download. Returns false when cancelled.
|
||||||
|
- `exportTextFile` does the same for any text (`text/csv`, `text/calendar`).
|
||||||
- `shareJson` is an optional second action (share sheet). It is never the
|
- `shareJson` is an optional second action (share sheet). It is never the
|
||||||
only way out: a stock phone's share sheet has no local save target.
|
only way out: a stock phone's share sheet has no local save target.
|
||||||
- `pickJson` / `pickJsonText` use the system open-document picker.
|
- `pickJson` / `pickJsonText` use the system open-document picker.
|
||||||
|
|||||||
@ -9,3 +9,19 @@ const jsonTypeGroup = XTypeGroup(
|
|||||||
uniformTypeIdentifiers: ['public.json'],
|
uniformTypeIdentifiers: ['public.json'],
|
||||||
mimeTypes: ['application/json'],
|
mimeTypes: ['application/json'],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// The save-dialog filter for [fileName] holding [mimeType] data.
|
||||||
|
///
|
||||||
|
/// Keyed on the file's own extension so a CSV export is not offered under a
|
||||||
|
/// JSON filter; a name with no extension accepts any type.
|
||||||
|
XTypeGroup saveTypeGroup(String fileName, String mimeType) {
|
||||||
|
final dot = fileName.lastIndexOf('.');
|
||||||
|
final extension = dot > 0 && dot < fileName.length - 1
|
||||||
|
? fileName.substring(dot + 1).toLowerCase()
|
||||||
|
: null;
|
||||||
|
return XTypeGroup(
|
||||||
|
label: extension?.toUpperCase(),
|
||||||
|
extensions: extension == null ? null : [extension],
|
||||||
|
mimeTypes: [mimeType],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -9,14 +9,22 @@ abstract interface class DataTransferPlatform {
|
|||||||
/// Android/iOS: the system "Save as" document picker (device storage).
|
/// Android/iOS: the system "Save as" document picker (device storage).
|
||||||
/// Desktop: a save dialog. Web: a browser download. Returns false when the
|
/// Desktop: a save dialog. Web: a browser download. Returns false when the
|
||||||
/// user cancelled.
|
/// user cancelled.
|
||||||
Future<bool> saveText({required String fileName, required String text});
|
Future<bool> saveText({
|
||||||
|
required String fileName,
|
||||||
|
required String text,
|
||||||
|
String mimeType = 'application/json',
|
||||||
|
});
|
||||||
|
|
||||||
/// Offers [text] as [fileName] to the share sheet, where the host has one.
|
/// Offers [text] as [fileName] to the share sheet, where the host has one.
|
||||||
///
|
///
|
||||||
/// A second action only: a share sheet can route the data to cloud apps, so
|
/// A second action only: a share sheet can route the data to cloud apps, so
|
||||||
/// it never replaces [saveText]. Hosts without a sheet fall back to
|
/// it never replaces [saveText]. Hosts without a sheet fall back to
|
||||||
/// [saveText].
|
/// [saveText].
|
||||||
Future<bool> shareText({required String fileName, required String text});
|
Future<bool> shareText({
|
||||||
|
required String fileName,
|
||||||
|
required String text,
|
||||||
|
String mimeType = 'application/json',
|
||||||
|
});
|
||||||
|
|
||||||
/// Opens the system open-document picker and returns the file's text, or
|
/// Opens the system open-document picker and returns the file's text, or
|
||||||
/// null when the user cancelled.
|
/// null when the user cancelled.
|
||||||
|
|||||||
@ -12,11 +12,19 @@ import 'package:share_plus/share_plus.dart';
|
|||||||
|
|
||||||
/// Writes [bytes] through the system "Save as" picker; false on cancel.
|
/// Writes [bytes] through the system "Save as" picker; false on cancel.
|
||||||
typedef DocumentSaver =
|
typedef DocumentSaver =
|
||||||
Future<bool> Function({required String fileName, required Uint8List bytes});
|
Future<bool> Function({
|
||||||
|
required String fileName,
|
||||||
|
required Uint8List bytes,
|
||||||
|
required String mimeType,
|
||||||
|
});
|
||||||
|
|
||||||
/// Offers [text] as a file named [fileName] to the share sheet.
|
/// Offers [text] as a file named [fileName] to the share sheet.
|
||||||
typedef SheetSharer =
|
typedef SheetSharer =
|
||||||
Future<bool> Function({required String fileName, required String text});
|
Future<bool> Function({
|
||||||
|
required String fileName,
|
||||||
|
required String text,
|
||||||
|
required String mimeType,
|
||||||
|
});
|
||||||
|
|
||||||
/// The platform for hosts that have `dart:io`.
|
/// The platform for hosts that have `dart:io`.
|
||||||
DataTransferPlatform createDefaultPlatform() => IoDataTransferPlatform();
|
DataTransferPlatform createDefaultPlatform() => IoDataTransferPlatform();
|
||||||
@ -46,16 +54,18 @@ class IoDataTransferPlatform implements DataTransferPlatform {
|
|||||||
Future<bool> saveText({
|
Future<bool> saveText({
|
||||||
required String fileName,
|
required String fileName,
|
||||||
required String text,
|
required String text,
|
||||||
|
String mimeType = 'application/json',
|
||||||
}) async {
|
}) async {
|
||||||
if (_isMobile) {
|
if (_isMobile) {
|
||||||
return await _documentSaver(
|
return await _documentSaver(
|
||||||
fileName: fileName,
|
fileName: fileName,
|
||||||
bytes: Uint8List.fromList(utf8.encode(text)),
|
bytes: Uint8List.fromList(utf8.encode(text)),
|
||||||
|
mimeType: mimeType,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final location = await getSaveLocation(
|
final location = await getSaveLocation(
|
||||||
suggestedName: fileName,
|
suggestedName: fileName,
|
||||||
acceptedTypeGroups: const [jsonTypeGroup],
|
acceptedTypeGroups: [saveTypeGroup(fileName, mimeType)],
|
||||||
);
|
);
|
||||||
if (location == null) return false;
|
if (location == null) return false;
|
||||||
await File(location.path).writeAsString(text);
|
await File(location.path).writeAsString(text);
|
||||||
@ -63,10 +73,13 @@ class IoDataTransferPlatform implements DataTransferPlatform {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> shareText({required String fileName, required String text}) =>
|
Future<bool> shareText({
|
||||||
_isMobile
|
required String fileName,
|
||||||
? _sheetSharer(fileName: fileName, text: text)
|
required String text,
|
||||||
: saveText(fileName: fileName, text: text);
|
String mimeType = 'application/json',
|
||||||
|
}) => _isMobile
|
||||||
|
? _sheetSharer(fileName: fileName, text: text, mimeType: mimeType)
|
||||||
|
: saveText(fileName: fileName, text: text, mimeType: mimeType);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> pickText() async {
|
Future<String?> pickText() async {
|
||||||
@ -81,11 +94,12 @@ class IoDataTransferPlatform implements DataTransferPlatform {
|
|||||||
Future<bool> _saveViaDocumentPicker({
|
Future<bool> _saveViaDocumentPicker({
|
||||||
required String fileName,
|
required String fileName,
|
||||||
required Uint8List bytes,
|
required Uint8List bytes,
|
||||||
|
required String mimeType,
|
||||||
}) async {
|
}) async {
|
||||||
final saved = await FilePicker.saveFile(
|
final saved = await FilePicker.saveFile(
|
||||||
fileName: fileName,
|
fileName: fileName,
|
||||||
bytes: bytes,
|
bytes: bytes,
|
||||||
mimeType: 'application/json',
|
mimeType: mimeType,
|
||||||
);
|
);
|
||||||
return saved != null;
|
return saved != null;
|
||||||
}
|
}
|
||||||
@ -93,12 +107,13 @@ Future<bool> _saveViaDocumentPicker({
|
|||||||
Future<bool> _shareViaSheet({
|
Future<bool> _shareViaSheet({
|
||||||
required String fileName,
|
required String fileName,
|
||||||
required String text,
|
required String text,
|
||||||
|
required String mimeType,
|
||||||
}) async {
|
}) async {
|
||||||
final dir = await getTemporaryDirectory();
|
final dir = await getTemporaryDirectory();
|
||||||
final file = File(p.join(dir.path, fileName));
|
final file = File(p.join(dir.path, fileName));
|
||||||
await file.writeAsString(text);
|
await file.writeAsString(text);
|
||||||
await SharePlus.instance.share(
|
await SharePlus.instance.share(
|
||||||
ShareParams(files: [XFile(file.path, mimeType: 'application/json')]),
|
ShareParams(files: [XFile(file.path, mimeType: mimeType)]),
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,20 +16,24 @@ class WebDataTransferPlatform implements DataTransferPlatform {
|
|||||||
Future<bool> saveText({
|
Future<bool> saveText({
|
||||||
required String fileName,
|
required String fileName,
|
||||||
required String text,
|
required String text,
|
||||||
|
String mimeType = 'application/json',
|
||||||
}) async {
|
}) async {
|
||||||
// `saveTo` on web clicks a synthetic anchor: the path is ignored and the
|
// `saveTo` on web clicks a synthetic anchor: the path is ignored and the
|
||||||
// browser's download UI decides where the file lands.
|
// browser's download UI decides where the file lands.
|
||||||
await XFile.fromData(
|
await XFile.fromData(
|
||||||
utf8.encode(text),
|
utf8.encode(text),
|
||||||
mimeType: 'application/json',
|
mimeType: mimeType,
|
||||||
name: fileName,
|
name: fileName,
|
||||||
).saveTo('');
|
).saveTo('');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> shareText({required String fileName, required String text}) =>
|
Future<bool> shareText({
|
||||||
saveText(fileName: fileName, text: text);
|
required String fileName,
|
||||||
|
required String text,
|
||||||
|
String mimeType = 'application/json',
|
||||||
|
}) => saveText(fileName: fileName, text: text, mimeType: mimeType);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> pickText() async {
|
Future<String?> pickText() async {
|
||||||
|
|||||||
@ -21,7 +21,19 @@ Future<bool> exportJson({
|
|||||||
|
|
||||||
/// [exportJson] for an already-encoded JSON string.
|
/// [exportJson] for an already-encoded JSON string.
|
||||||
Future<bool> exportJsonText({required String fileName, required String text}) =>
|
Future<bool> exportJsonText({required String fileName, required String text}) =>
|
||||||
DataTransfer.platform.saveText(fileName: fileName, text: text);
|
exportTextFile(fileName: fileName, text: text);
|
||||||
|
|
||||||
|
/// Saves any text file ([mimeType], e.g. `text/csv`) to device storage, with
|
||||||
|
/// the same host behaviour as [exportJson]. False when cancelled.
|
||||||
|
Future<bool> exportTextFile({
|
||||||
|
required String fileName,
|
||||||
|
required String text,
|
||||||
|
String mimeType = 'application/json',
|
||||||
|
}) => DataTransfer.platform.saveText(
|
||||||
|
fileName: fileName,
|
||||||
|
text: text,
|
||||||
|
mimeType: mimeType,
|
||||||
|
);
|
||||||
|
|
||||||
/// Optional second action: offers [json] to the share sheet.
|
/// Optional second action: offers [json] to the share sheet.
|
||||||
Future<bool> shareJson({
|
Future<bool> shareJson({
|
||||||
|
|||||||
@ -4,7 +4,7 @@ description: >-
|
|||||||
Flutter apps: system Save-as on Android/iOS (never cloud-only), a save
|
Flutter apps: system Save-as on Android/iOS (never cloud-only), a save
|
||||||
dialog on desktop, a download on web, plus a versioned export envelope.
|
dialog on desktop, a download on web, plus a versioned export envelope.
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 0.1.0
|
version: 0.2.0
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.12.2
|
sdk: ^3.12.2
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import 'dart:convert';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:data_transfer_flutter/src/json_type_group.dart';
|
||||||
import 'package:data_transfer_flutter/src/platform_io.dart';
|
import 'package:data_transfer_flutter/src/platform_io.dart';
|
||||||
import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
|
import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
@ -46,6 +47,17 @@ void main() {
|
|||||||
group('desktop', () {
|
group('desktop', () {
|
||||||
final desktop = IoDataTransferPlatform(isMobile: false);
|
final desktop = IoDataTransferPlatform(isMobile: false);
|
||||||
|
|
||||||
|
test('the save dialog is typed by the file name and mime type', () {
|
||||||
|
final group = saveTypeGroup('punchme.csv', 'text/csv');
|
||||||
|
|
||||||
|
expect(group.extensions, ['csv']);
|
||||||
|
expect(group.mimeTypes, ['text/csv']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a name without an extension accepts any type', () {
|
||||||
|
expect(saveTypeGroup('README', 'text/plain').extensions, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
test('save writes to the chosen path and suggests the name', () async {
|
test('save writes to the chosen path and suggests the name', () async {
|
||||||
final path = '${temp.path}/out.json';
|
final path = '${temp.path}/out.json';
|
||||||
selector.saveLocation = FileSaveLocation(path);
|
selector.saveLocation = FileSaveLocation(path);
|
||||||
@ -91,20 +103,28 @@ void main() {
|
|||||||
'save hands UTF-8 bytes to the document saver, not file_selector',
|
'save hands UTF-8 bytes to the document saver, not file_selector',
|
||||||
() async {
|
() async {
|
||||||
String? name;
|
String? name;
|
||||||
|
String? mime;
|
||||||
List<int>? captured;
|
List<int>? captured;
|
||||||
final mobile = IoDataTransferPlatform(
|
final mobile = IoDataTransferPlatform(
|
||||||
isMobile: true,
|
isMobile: true,
|
||||||
documentSaver: ({required fileName, required bytes}) async {
|
documentSaver:
|
||||||
name = fileName;
|
({required fileName, required bytes, required mimeType}) async {
|
||||||
captured = bytes;
|
name = fileName;
|
||||||
return true;
|
mime = mimeType;
|
||||||
},
|
captured = bytes;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
final saved = await mobile.saveText(fileName: 'm.json', text: 'zażółć');
|
final saved = await mobile.saveText(
|
||||||
|
fileName: 'm.json',
|
||||||
|
text: 'zażółć',
|
||||||
|
mimeType: 'text/csv',
|
||||||
|
);
|
||||||
|
|
||||||
expect(saved, isTrue);
|
expect(saved, isTrue);
|
||||||
expect(name, 'm.json');
|
expect(name, 'm.json');
|
||||||
|
expect(mime, 'text/csv');
|
||||||
expect(utf8.decode(captured!), 'zażółć');
|
expect(utf8.decode(captured!), 'zażółć');
|
||||||
expect(selector.saveRequests, isEmpty);
|
expect(selector.saveRequests, isEmpty);
|
||||||
},
|
},
|
||||||
@ -113,7 +133,9 @@ void main() {
|
|||||||
test('a cancelled document picker reports false', () async {
|
test('a cancelled document picker reports false', () async {
|
||||||
final mobile = IoDataTransferPlatform(
|
final mobile = IoDataTransferPlatform(
|
||||||
isMobile: true,
|
isMobile: true,
|
||||||
documentSaver: ({required fileName, required bytes}) async => false,
|
documentSaver:
|
||||||
|
({required fileName, required bytes, required mimeType}) async =>
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(await mobile.saveText(fileName: 'm.json', text: '{}'), isFalse);
|
expect(await mobile.saveText(fileName: 'm.json', text: '{}'), isFalse);
|
||||||
@ -121,16 +143,20 @@ void main() {
|
|||||||
|
|
||||||
test('share goes through the share sheet seam', () async {
|
test('share goes through the share sheet seam', () async {
|
||||||
(String, String)? seen;
|
(String, String)? seen;
|
||||||
|
String? sharedMime;
|
||||||
final mobile = IoDataTransferPlatform(
|
final mobile = IoDataTransferPlatform(
|
||||||
isMobile: true,
|
isMobile: true,
|
||||||
sheetSharer: ({required fileName, required text}) async {
|
sheetSharer:
|
||||||
seen = (fileName, text);
|
({required fileName, required text, required mimeType}) async {
|
||||||
return true;
|
seen = (fileName, text);
|
||||||
},
|
sharedMime = mimeType;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(await mobile.shareText(fileName: 's.json', text: '1'), isTrue);
|
expect(await mobile.shareText(fileName: 's.json', text: '1'), isTrue);
|
||||||
expect(seen, ('s.json', '1'));
|
expect(seen, ('s.json', '1'));
|
||||||
|
expect(sharedMime, 'application/json');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,9 @@ class FakePlatform implements DataTransferPlatform {
|
|||||||
/// `(fileName, text)` of every [saveText] call.
|
/// `(fileName, text)` of every [saveText] call.
|
||||||
final saves = <(String, String)>[];
|
final saves = <(String, String)>[];
|
||||||
|
|
||||||
|
/// The mime type of every [saveText] call, in order.
|
||||||
|
final saveMimes = <String>[];
|
||||||
|
|
||||||
/// `(fileName, text)` of every [shareText] call.
|
/// `(fileName, text)` of every [shareText] call.
|
||||||
final shares = <(String, String)>[];
|
final shares = <(String, String)>[];
|
||||||
|
|
||||||
@ -18,8 +21,10 @@ class FakePlatform implements DataTransferPlatform {
|
|||||||
Future<bool> saveText({
|
Future<bool> saveText({
|
||||||
required String fileName,
|
required String fileName,
|
||||||
required String text,
|
required String text,
|
||||||
|
String mimeType = 'application/json',
|
||||||
}) async {
|
}) async {
|
||||||
saves.add((fileName, text));
|
saves.add((fileName, text));
|
||||||
|
saveMimes.add(mimeType);
|
||||||
return accept;
|
return accept;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,6 +32,7 @@ class FakePlatform implements DataTransferPlatform {
|
|||||||
Future<bool> shareText({
|
Future<bool> shareText({
|
||||||
required String fileName,
|
required String fileName,
|
||||||
required String text,
|
required String text,
|
||||||
|
String mimeType = 'application/json',
|
||||||
}) async {
|
}) async {
|
||||||
shares.add((fileName, text));
|
shares.add((fileName, text));
|
||||||
return accept;
|
return accept;
|
||||||
|
|||||||
@ -45,6 +45,26 @@ void main() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('exportTextFile', () {
|
||||||
|
test('saves non-JSON text with its own mime type', () async {
|
||||||
|
final saved = await exportTextFile(
|
||||||
|
fileName: 'a.csv',
|
||||||
|
text: 'a,b\n',
|
||||||
|
mimeType: 'text/csv',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(saved, isTrue);
|
||||||
|
expect(fake.saves.single, ('a.csv', 'a,b\n'));
|
||||||
|
expect(fake.saveMimes.single, 'text/csv');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('exportJsonText keeps the JSON mime type', () async {
|
||||||
|
await exportJsonText(fileName: 'a.json', text: '{}');
|
||||||
|
|
||||||
|
expect(fake.saveMimes.single, 'application/json');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
group('shareJson', () {
|
group('shareJson', () {
|
||||||
test('uses the share action, not save', () async {
|
test('uses the share action, not save', () async {
|
||||||
final shared = await shareJson(fileName: 'c.json', json: {'k': 2});
|
final shared = await shareJson(fileName: 'c.json', json: {'k': 2});
|
||||||
|
|||||||
@ -5,7 +5,7 @@ description: >-
|
|||||||
injected closures -- no keystore or platform plugin access lives in this
|
injected closures -- no keystore or platform plugin access lives in this
|
||||||
package itself.
|
package itself.
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 0.5.0
|
version: 0.5.1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.12.2
|
sdk: ^3.12.2
|
||||||
@ -19,7 +19,7 @@ dependencies:
|
|||||||
data_transfer_flutter:
|
data_transfer_flutter:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/kuhyx/utils
|
url: https://github.com/kuhyx/utils
|
||||||
ref: data_transfer_flutter-v0.1.0
|
ref: data_transfer_flutter-v0.2.0
|
||||||
path: data_transfer_flutter
|
path: data_transfer_flutter
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|||||||
@ -85,6 +85,7 @@ class _FakePlatform implements DataTransferPlatform {
|
|||||||
Future<bool> saveText({
|
Future<bool> saveText({
|
||||||
required String fileName,
|
required String fileName,
|
||||||
required String text,
|
required String text,
|
||||||
|
String mimeType = 'application/json',
|
||||||
}) async {
|
}) async {
|
||||||
saved.add((fileName, text));
|
saved.add((fileName, text));
|
||||||
return true;
|
return true;
|
||||||
@ -94,6 +95,7 @@ class _FakePlatform implements DataTransferPlatform {
|
|||||||
Future<bool> shareText({
|
Future<bool> shareText({
|
||||||
required String fileName,
|
required String fileName,
|
||||||
required String text,
|
required String text,
|
||||||
|
String mimeType = 'application/json',
|
||||||
}) async => true;
|
}) async => true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user