feat: use native media picker on iOS and Android

This commit is contained in:
Tien Do Nam
2023-10-11 18:08:38 +02:00
parent fc85b629ba
commit 7c658babae
11 changed files with 360 additions and 262 deletions
+7 -6
View File
@@ -17,6 +17,7 @@ import 'package:localsend_app/provider/window_dimensions_provider.dart';
import 'package:localsend_app/theme.dart';
import 'package:localsend_app/util/api_route_builder.dart';
import 'package:localsend_app/util/native/cache_helper.dart';
import 'package:localsend_app/util/native/cross_file_converters.dart';
import 'package:localsend_app/util/native/platform_check.dart';
import 'package:localsend_app/util/native/tray_helper.dart';
import 'package:localsend_app/util/ui/snackbar.dart';
@@ -168,17 +169,17 @@ Future<void> postInit(BuildContext context, Ref ref, bool appStart, void Functio
if (appStart && !hasInitialShare && (checkPlatformWithGallery() || checkPlatformCanReceiveShareIntent())) {
// Clear cache on every app start.
// If we received a share intent, then don't clear it, otherwise the shared file will be lost.
clearCache();
ref.dispatch(ClearCacheAction());
}
}
Future<void> _handleSharedIntent(SharedMedia payload, Ref ref) async {
final message = payload.content;
if (message != null && message.trim().isNotEmpty) {
ref.notifier(selectedSendingFilesProvider).addMessage(message);
ref.redux(selectedSendingFilesProvider).dispatch(AddMessageAction(message: message));
}
await ref.notifier(selectedSendingFilesProvider).addFiles(
files: payload.attachments?.where((a) => a != null).cast<SharedAttachment>() ?? <SharedAttachment>[],
converter: CrossFileConverters.convertSharedAttachment,
);
await ref.redux(selectedSendingFilesProvider).dispatchAsync(AddFilesAction(
files: payload.attachments?.where((a) => a != null).cast<SharedAttachment>() ?? <SharedAttachment>[],
converter: CrossFileConverters.convertSharedAttachment,
));
}
+9 -7
View File
@@ -5,6 +5,7 @@ import 'package:localsend_app/model/file_type.dart';
import 'package:localsend_app/provider/apk_provider.dart';
import 'package:localsend_app/provider/selection/selected_sending_files_provider.dart';
import 'package:localsend_app/util/file_size_helper.dart';
import 'package:localsend_app/util/native/cross_file_converters.dart';
import 'package:localsend_app/util/ui/nav_bar_padding.dart';
import 'package:localsend_app/widget/file_thumbnail.dart';
import 'package:localsend_app/widget/responsive_list_view.dart';
@@ -19,10 +20,15 @@ class ApkPickerPage extends StatefulWidget {
State<ApkPickerPage> createState() => _ApkPickerPageState();
}
class _ApkPickerPageState extends State<ApkPickerPage> {
class _ApkPickerPageState extends State<ApkPickerPage> with Refena {
void _pickApp(Application app) {
// ignore: discarded_futures
ref.redux(selectedSendingFilesProvider).dispatchAsync(AddFilesAction(files: [app], converter: CrossFileConverters.convertApplication));
context.pop();
}
@override
Widget build(BuildContext context) {
final ref = context.ref;
final apkParams = ref.watch(apkSearchParamProvider);
final apkAsync = ref.watch(apkProvider);
@@ -101,11 +107,7 @@ class _ApkPickerPageState extends State<ApkPickerPage> {
return Padding(
padding: const EdgeInsets.only(bottom: 10),
child: InkWell(
onTap: () {
// ignore: discarded_futures
ref.notifier(selectedSendingFilesProvider).addFiles(files: [app], converter: CrossFileConverters.convertApplication);
context.pop();
},
onTap: () => _pickApp(app),
customBorder: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
+4 -3
View File
@@ -10,6 +10,7 @@ import 'package:localsend_app/pages/tabs/settings_tab.dart';
import 'package:localsend_app/provider/selection/selected_sending_files_provider.dart';
import 'package:localsend_app/provider/ui/home_tab_provider.dart';
import 'package:localsend_app/theme.dart';
import 'package:localsend_app/util/native/cross_file_converters.dart';
import 'package:localsend_app/widget/responsive_builder.dart';
import 'package:refena_flutter/refena_flutter.dart';
@@ -96,13 +97,13 @@ class _HomePageState extends State<HomePage> with Refena {
onDragDone: (event) async {
if (event.files.length == 1 && Directory(event.files.first.path).existsSync()) {
// user dropped a directory
await ref.notifier(selectedSendingFilesProvider).addDirectory(event.files.first.path);
await ref.redux(selectedSendingFilesProvider).dispatchAsync(AddDirectoryAction(event.files.first.path));
} else {
// user dropped one or more files
await ref.notifier(selectedSendingFilesProvider).addFiles(
await ref.redux(selectedSendingFilesProvider).dispatchAsync(AddFilesAction(
files: event.files,
converter: CrossFileConverters.convertXFile,
);
));
}
_goToPage(HomeTab.send.index);
},
+3 -4
View File
@@ -48,7 +48,7 @@ class SelectedFilesPage extends StatelessWidget {
),
ElevatedButton(
onPressed: () {
ref.notifier(selectedSendingFilesProvider).reset();
ref.redux(selectedSendingFilesProvider).dispatch(ClearSelectionAction());
context.popUntilRoot();
},
child: Text(t.selectedFilesPage.deleteAll),
@@ -110,8 +110,7 @@ class SelectedFilesPage extends StatelessWidget {
final result =
await showDialog<String>(context: context, builder: (_) => MessageInputDialog(initialText: message));
if (result != null) {
ref.notifier(selectedSendingFilesProvider).removeAt(index);
ref.notifier(selectedSendingFilesProvider).addMessage(result, index: index);
ref.redux(selectedSendingFilesProvider).dispatch(UpdateMessageAction(message: result, index: index));
}
},
child: const Icon(Icons.edit),
@@ -122,7 +121,7 @@ class SelectedFilesPage extends StatelessWidget {
),
onPressed: () {
final currCount = ref.read(selectedSendingFilesProvider).length;
ref.notifier(selectedSendingFilesProvider).removeAt(index);
ref.redux(selectedSendingFilesProvider).dispatch(RemoveSelectedFileAction(index));
if (currCount == 1) {
context.popUntilRoot();
}
+8 -4
View File
@@ -103,10 +103,10 @@ class _SendTabState extends State<SendTab> with Refena {
icon: option.icon,
label: option.label,
filled: false,
onTap: () async => option.select(
onTap: () async => ref.dispatchAsync(PickAction(
option: option,
context: context,
ref: ref,
),
)),
),
);
}),
@@ -164,7 +164,11 @@ class _SendTabState extends State<SendTab> with Refena {
),
onPressed: () async {
if (options.length == 1) {
await options.first.select(context: context, ref: ref); // open directly
// open directly
await ref.dispatchAsync(PickAction(
option: options.first,
context: context,
));
return;
}
await AddFileDialog.open(
+1 -1
View File
@@ -396,7 +396,7 @@ class SendNotifier extends Notifier<Map<String, SendSessionState>> {
state = state.removeSession(ref, sessionId);
if (sessionState.status == SessionStatus.finished && ref.read(settingsProvider).sendMode == SendMode.single) {
// clear selected files
ref.notifier(selectedSendingFilesProvider).reset();
ref.redux(selectedSendingFilesProvider).dispatch(ClearSelectionAction());
}
}
@@ -1,10 +1,6 @@
import 'dart:convert' show utf8;
import 'dart:io';
import 'package:device_apps/device_apps.dart';
import 'package:file_picker/file_picker.dart' as file_picker;
import 'package:flutter/foundation.dart';
import 'package:image_picker/image_picker.dart';
import 'package:localsend_app/model/cross_file.dart';
import 'package:localsend_app/model/file_type.dart';
import 'package:localsend_app/util/file_path_helper.dart';
@@ -12,30 +8,33 @@ import 'package:localsend_app/util/native/cache_helper.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart' as p;
import 'package:refena_flutter/refena_flutter.dart';
import 'package:share_handler/share_handler.dart';
import 'package:uuid/uuid.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
final _logger = Logger('SelectedSendingFiles');
const _uuid = Uuid();
/// Manages files selected for sending.
/// Will stay alive even after a session has been completed to send the same files to another device.
final selectedSendingFilesProvider = NotifierProvider<SelectedSendingFilesNotifier, List<CrossFile>>((ref) {
final selectedSendingFilesProvider = ReduxProvider<SelectedSendingFilesNotifier, List<CrossFile>>((ref) {
return SelectedSendingFilesNotifier();
});
class SelectedSendingFilesNotifier extends Notifier<List<CrossFile>> {
SelectedSendingFilesNotifier();
class SelectedSendingFilesNotifier extends ReduxNotifier<List<CrossFile>> {
@override
List<CrossFile> init() => [];
}
class AddMessageAction extends ReduxAction<SelectedSendingFilesNotifier, List<CrossFile>> {
final String message;
final int? index;
AddMessageAction({
required this.message,
this.index,
});
@override
List<CrossFile> init() {
return [];
}
/// Add a simple message
/// Internally, the message will be stored into [CrossFile.bytes] as UTF-8
void addMessage(String message, {int? index}) {
List<CrossFile> reduce() {
final List<int> bytes = utf8.encode(message);
final file = CrossFile(
name: '${_uuid.v4()}.txt',
@@ -46,30 +45,62 @@ class SelectedSendingFilesNotifier extends Notifier<List<CrossFile>> {
path: null,
bytes: bytes,
);
state = List.unmodifiable([
return List.unmodifiable([
...state,
]..insert(index ?? state.length, file));
}
}
Future<void> addFiles<T>({
required Iterable<T> files,
required Future<CrossFile> Function(T) converter,
}) async {
class UpdateMessageAction extends ReduxAction<SelectedSendingFilesNotifier, List<CrossFile>> {
final String message;
final int index;
UpdateMessageAction({
required this.message,
required this.index,
});
@override
List<CrossFile> reduce() {
dispatch(RemoveSelectedFileAction(index));
dispatch(AddMessageAction(message: message, index: index));
return state;
}
}
class AddFilesAction<T> extends AsyncReduxAction<SelectedSendingFilesNotifier, List<CrossFile>> {
final Iterable<T> files;
final Future<CrossFile> Function(T) converter;
AddFilesAction({
required this.files,
required this.converter,
});
@override
Future<List<CrossFile>> reduce() async {
final newFiles = <CrossFile>[];
for (final file in files) {
// we do it sequential because there are bugs
// https://github.com/fluttercandies/flutter_photo_manager/issues/589
newFiles.add(await converter(file));
}
state = List.unmodifiable([
return List.unmodifiable([
...state,
...newFiles,
]);
}
}
/// Adds files inside the directory recursively.
/// If [includeDirectory] is true, the directory itself will be added as part of each file path.
Future<void> addDirectory(String directoryPath) async {
/// Adds files inside the directory recursively.
class AddDirectoryAction extends AsyncReduxAction<SelectedSendingFilesNotifier, List<CrossFile>> {
final String directoryPath;
AddDirectoryAction(this.directoryPath);
@override
Future<List<CrossFile>> reduce() async {
_logger.info('Reading files in $directoryPath');
final newFiles = <CrossFile>[];
final directoryName = p.basename(directoryPath);
@@ -89,88 +120,40 @@ class SelectedSendingFilesNotifier extends Notifier<List<CrossFile>> {
newFiles.add(file);
}
}
state = List.unmodifiable([
return List.unmodifiable([
...state,
...newFiles,
]);
}
}
void removeAt(int index) {
state = List.unmodifiable([...state]..removeAt(index));
class RemoveSelectedFileAction extends ReduxAction<SelectedSendingFilesNotifier, List<CrossFile>> with GlobalActions {
final int index;
RemoveSelectedFileAction(this.index);
@override
List<CrossFile> reduce() {
return List<CrossFile>.unmodifiable([...state]..removeAt(index));
}
@override
void after() {
if (state.isEmpty) {
clearCache();
global.dispatch(ClearCacheAction());
}
}
void reset() {
state = List.empty(growable: false);
clearCache();
}
}
/// Utility functions to convert third party models to common [CrossFile] model.
class CrossFileConverters {
static Future<CrossFile> convertPlatformFile(file_picker.PlatformFile file) async {
return CrossFile(
name: file.name,
fileType: file.name.guessFileType(),
size: file.size,
thumbnail: null,
asset: null,
path: kIsWeb ? null : file.path,
bytes: kIsWeb ? file.bytes! : null,
);
class ClearSelectionAction extends ReduxAction<SelectedSendingFilesNotifier, List<CrossFile>> with GlobalActions {
@override
List<CrossFile> reduce() {
return List.empty(growable: false);
}
static Future<CrossFile> convertAssetEntity(AssetEntity asset) async {
final file = (await asset.originFile)!;
return CrossFile(
name: await asset.titleAsync,
fileType: asset.type == AssetType.video ? FileType.video : FileType.image,
size: await file.length(),
thumbnail: null,
asset: asset,
path: file.path,
bytes: null,
);
}
static Future<CrossFile> convertXFile(XFile file) async {
return CrossFile(
name: file.name,
fileType: file.name.guessFileType(),
size: await file.length(),
thumbnail: null,
asset: null,
path: kIsWeb ? null : file.path,
bytes: kIsWeb ? await file.readAsBytes() : null, // we can fetch it now because in Web it is already there
);
}
static Future<CrossFile> convertSharedAttachment(SharedAttachment attachment) async {
final file = File(attachment.path);
final fileName = attachment.path.fileName;
return CrossFile(
name: fileName,
fileType: fileName.guessFileType(),
size: await file.length(),
thumbnail: null,
asset: null,
path: file.path,
bytes: null,
);
}
static Future<CrossFile> convertApplication(Application app) async {
final file = File(app.apkFilePath);
return CrossFile(
name: '${app.appName.trim()} - v${app.versionName}.apk',
fileType: FileType.apk,
thumbnail: app is ApplicationWithIcon ? app.icon : null,
size: await file.length(),
asset: null,
path: app.apkFilePath,
bytes: null,
);
@override
void after() {
global.dispatch(ClearCacheAction());
}
}
+24 -20
View File
@@ -5,32 +5,36 @@ import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:localsend_app/util/native/platform_check.dart';
import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart';
import 'package:refena_flutter/refena_flutter.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
final _logger = Logger('CacheHelper');
/// Clears the cache.
/// It is written in a "fire-and-forget" way, so we don't need to wait until everything is cleared.
void clearCache() {
FilePicker.platform.clearTemporaryFiles().then((_) {}).catchError((error) {
_logger.info('Failed to clear file picker cache', error);
});
class ClearCacheAction extends GlobalAction {
@override
void reduce() {}
PhotoManager.clearFileCache().catchError((error) => _logger.info('Failed to clear photo manager cache', error));
if (checkPlatform([TargetPlatform.iOS, TargetPlatform.android])) {
getTemporaryDirectory().then((cacheDir) {
cacheDir.list().listen((event) {
if (event is File) {
event.delete().then((_) {}).catchError((error) {
_logger.info('Failed to delete file', error);
});
}
});
}).catchError((error) {
_logger.info('Failed to get temporary directory', error);
@override
void after() {
FilePicker.platform.clearTemporaryFiles().then((_) {}).catchError((error) {
ref.message('Failed to clear file picker cache: $error');
});
PhotoManager.clearFileCache().catchError((error) => ref.message('Failed to clear photo manager cache: $error'));
if (checkPlatform([TargetPlatform.iOS, TargetPlatform.android])) {
getTemporaryDirectory().then((cacheDir) {
cacheDir.list().listen((event) {
if (event is File) {
event.delete().then((_) {}).catchError((error) {
ref.message('Failed to delete file: $error');
});
}
});
}).catchError((error) {
ref.message('Failed to get temporary directory: $error');
});
}
}
}
@@ -0,0 +1,78 @@
import 'dart:io';
import 'package:device_apps/device_apps.dart';
import 'package:file_picker/file_picker.dart' as file_picker;
import 'package:flutter/foundation.dart';
import 'package:image_picker/image_picker.dart';
import 'package:localsend_app/model/cross_file.dart';
import 'package:localsend_app/model/file_type.dart';
import 'package:localsend_app/util/file_path_helper.dart';
import 'package:share_handler/share_handler.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
/// Utility functions to convert third party models to common [CrossFile] model.
class CrossFileConverters {
static Future<CrossFile> convertPlatformFile(file_picker.PlatformFile file) async {
return CrossFile(
name: file.name,
fileType: file.name.guessFileType(),
size: file.size,
thumbnail: null,
asset: null,
path: kIsWeb ? null : file.path,
bytes: kIsWeb ? file.bytes! : null,
);
}
static Future<CrossFile> convertAssetEntity(AssetEntity asset) async {
final file = (await asset.originFile)!;
return CrossFile(
name: await asset.titleAsync,
fileType: asset.type == AssetType.video ? FileType.video : FileType.image,
size: await file.length(),
thumbnail: null,
asset: asset,
path: file.path,
bytes: null,
);
}
static Future<CrossFile> convertXFile(XFile file) async {
return CrossFile(
name: file.name,
fileType: file.name.guessFileType(),
size: await file.length(),
thumbnail: null,
asset: null,
path: kIsWeb ? null : file.path,
bytes: kIsWeb ? await file.readAsBytes() : null, // we can fetch it now because in Web it is already there
);
}
static Future<CrossFile> convertSharedAttachment(SharedAttachment attachment) async {
final file = File(attachment.path);
final fileName = attachment.path.fileName;
return CrossFile(
name: fileName,
fileType: fileName.guessFileType(),
size: await file.length(),
thumbnail: null,
asset: null,
path: file.path,
bytes: null,
);
}
static Future<CrossFile> convertApplication(Application app) async {
final file = File(app.apkFilePath);
return CrossFile(
name: '${app.appName.trim()} - v${app.versionName}.apk',
fileType: FileType.apk,
thumbnail: app is ApplicationWithIcon ? app.icon : null,
size: await file.length(),
asset: null,
path: app.apkFilePath,
bytes: null,
);
}
}
+146 -117
View File
@@ -4,14 +4,14 @@ import 'package:file_picker/file_picker.dart';
import 'package:file_selector/file_selector.dart' as file_selector;
import 'package:file_selector/file_selector.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:localsend_app/gen/strings.g.dart';
import 'package:localsend_app/pages/apk_picker_page.dart';
import 'package:localsend_app/provider/selection/selected_sending_files_provider.dart';
import 'package:localsend_app/theme.dart';
import 'package:localsend_app/util/native/cross_file_converters.dart';
import 'package:localsend_app/util/native/pick_directory_path.dart';
import 'package:localsend_app/util/native/platform_check.dart';
import 'package:localsend_app/util/sleep.dart';
import 'package:localsend_app/util/ui/asset_picker_translated_text_delegate.dart';
import 'package:localsend_app/widget/dialogs/loading_dialog.dart';
import 'package:localsend_app/widget/dialogs/message_input_dialog.dart';
import 'package:localsend_app/widget/dialogs/no_permission_dialog.dart';
@@ -20,7 +20,6 @@ import 'package:pasteboard/pasteboard.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:refena_flutter/refena_flutter.dart';
import 'package:routerino/routerino.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
final _logger = Logger('FilePickerHelper');
@@ -66,9 +65,9 @@ enum FilePickerOption {
];
} else if (checkPlatform([TargetPlatform.android])) {
// On android, the file app is most powerful.
// It actually also allows to pick media files.
return [
FilePickerOption.file,
FilePickerOption.media,
FilePickerOption.text,
FilePickerOption.folder,
FilePickerOption.app,
@@ -83,137 +82,167 @@ enum FilePickerOption {
];
}
}
}
Future<void> select({
required BuildContext context,
required Ref ref,
}) async {
switch (this) {
class PickAction extends AsyncGlobalAction {
final FilePickerOption option;
final BuildContext context;
PickAction({
required this.option,
required this.context,
});
@override
Future<void> reduce() async {
switch (option) {
case FilePickerOption.file:
if (checkPlatform([TargetPlatform.android])) {
// On android, the files are copied to the cache which takes some time.
// ignore: unawaited_futures
showDialog(
context: context,
barrierDismissible: false,
builder: (_) => const LoadingDialog(),
);
}
try {
if (checkPlatform([TargetPlatform.android])) {
// We also need to use the file_picker package because file_selector does not expose the raw path.
final result = await FilePicker.platform.pickFiles(allowMultiple: true);
if (result != null) {
await ref.notifier(selectedSendingFilesProvider).addFiles(
files: result.files,
converter: CrossFileConverters.convertPlatformFile,
);
}
} else {
final result = await file_selector.openFiles();
if (result.isNotEmpty) {
await ref.notifier(selectedSendingFilesProvider).addFiles<file_selector.XFile>(
files: result,
converter: CrossFileConverters.convertXFile,
);
}
}
} catch (e) {
// ignore: use_build_context_synchronously
await showDialog(context: context, builder: (_) => const NoPermissionDialog());
_logger.warning('Failed to pick files', e);
} finally {
// ignore: use_build_context_synchronously
Routerino.context.popUntilRoot(); // remove loading dialog
}
await _pickFiles(context, ref);
break;
case FilePickerOption.folder:
if (checkPlatform([TargetPlatform.android])) {
try {
await Permission.manageExternalStorage.request();
} catch (e) {
_logger.warning('Failed to request manageExternalStorage permission', e);
}
}
// ignore: use_build_context_synchronously
if (!context.mounted) {
return;
}
// ignore: unawaited_futures
showDialog(
context: context,
barrierDismissible: false,
builder: (_) => const LoadingDialog(),
);
await sleepAsync(200); // Wait for the dialog to be shown
try {
final directoryPath = await pickDirectoryPath();
if (directoryPath != null) {
await ref.notifier(selectedSendingFilesProvider).addDirectory(directoryPath);
}
} catch (e) {
_logger.warning('Failed to pick directory', e);
// ignore: use_build_context_synchronously
await showDialog(context: context, builder: (_) => const NoPermissionDialog());
} finally {
// ignore: use_build_context_synchronously
Routerino.context.popUntilRoot(); // remove loading dialog
}
await _pickFolder(context, ref);
break;
case FilePickerOption.media:
// ignore: use_build_context_synchronously
final oldBrightness = Theme.of(context).brightness;
// ignore: use_build_context_synchronously
final List<AssetEntity>? result = await AssetPicker.pickAssets(
context,
pickerConfig: const AssetPickerConfig(maxAssets: 999, textDelegate: TranslatedAssetPickerTextDelegate()),
);
WidgetsBinding.instance.addPostFrameCallback((_) async {
// restore brightness for Android
await sleepAsync(500);
if (context.mounted) {
await updateSystemOverlayStyleWithBrightness(oldBrightness);
}
});
if (result != null) {
await ref.notifier(selectedSendingFilesProvider).addFiles(
files: result,
converter: CrossFileConverters.convertAssetEntity,
);
}
await _pickMedia(context, ref);
break;
case FilePickerOption.text:
// ignore: use_build_context_synchronously
final result = await showDialog<String>(context: context, builder: (_) => const MessageInputDialog());
if (result != null) {
ref.notifier(selectedSendingFilesProvider).addMessage(result);
}
await _pickText(context, ref);
break;
case FilePickerOption.clipboard:
// ignore: use_build_context_synchronously
late List<String> files = [];
await Pasteboard.files().then((value) => {for (final file in value) files.add(file)});
if (files.isNotEmpty) {
await ref.notifier(selectedSendingFilesProvider).addFiles<file_selector.XFile>(
files: files.map((e) => XFile(e)).toList(),
converter: CrossFileConverters.convertXFile,
);
} else {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(t.general.noItemInClipboard),
));
}
await _pickClipboard(context, ref);
break;
case FilePickerOption.app:
// Currently, only Android APK
// ignore: use_build_context_synchronously
await context.push(() => const ApkPickerPage());
await _pickApp(context);
break;
}
}
@override
String toString() {
return 'PickAction(option: $option)';
}
}
Future<void> _pickFiles(BuildContext context, Ref ref) async {
if (checkPlatform([TargetPlatform.android])) {
// On android, the files are copied to the cache which takes some time.
// ignore: unawaited_futures
showDialog(
context: context,
barrierDismissible: false,
builder: (_) => const LoadingDialog(),
);
}
try {
if (checkPlatform([TargetPlatform.android])) {
// We also need to use the file_picker package because file_selector does not expose the raw path.
final result = await FilePicker.platform.pickFiles(allowMultiple: true);
if (result != null) {
await ref.redux(selectedSendingFilesProvider).dispatchAsync(AddFilesAction(
files: result.files,
converter: CrossFileConverters.convertPlatformFile,
));
}
} else {
final result = await file_selector.openFiles();
if (result.isNotEmpty) {
await ref.redux(selectedSendingFilesProvider).dispatchAsync(AddFilesAction(
files: result,
converter: CrossFileConverters.convertXFile,
));
}
}
} catch (e) {
// ignore: use_build_context_synchronously
await showDialog(context: context, builder: (_) => const NoPermissionDialog());
_logger.warning('Failed to pick files', e);
} finally {
// ignore: use_build_context_synchronously
Routerino.context.popUntilRoot(); // remove loading dialog
}
}
Future<void> _pickFolder(BuildContext context, Ref ref) async {
if (checkPlatform([TargetPlatform.android])) {
try {
await Permission.manageExternalStorage.request();
} catch (e) {
_logger.warning('Failed to request manageExternalStorage permission', e);
}
}
if (!context.mounted) {
return;
}
// ignore: unawaited_futures
showDialog(
context: context,
barrierDismissible: false,
builder: (_) => const LoadingDialog(),
);
await sleepAsync(200); // Wait for the dialog to be shown
try {
final directoryPath = await pickDirectoryPath();
if (directoryPath != null) {
await ref.redux(selectedSendingFilesProvider).dispatchAsync(AddDirectoryAction(directoryPath));
}
} catch (e) {
_logger.warning('Failed to pick directory', e);
// ignore: use_build_context_synchronously
await showDialog(context: context, builder: (_) => const NoPermissionDialog());
} finally {
// ignore: use_build_context_synchronously
Routerino.context.popUntilRoot(); // remove loading dialog
}
}
Future<void> _pickMedia(BuildContext context, Ref ref) async {
final result = await ImagePicker().pickMultipleMedia(
imageQuality: null,
requestFullMetadata: true,
);
if (result.isEmpty) {
ref.message('No media selected');
return;
}
await ref.redux(selectedSendingFilesProvider).dispatchAsync(AddFilesAction(
files: result,
converter: CrossFileConverters.convertXFile,
));
}
Future<void> _pickText(BuildContext context, Ref ref) async {
final result = await showDialog<String>(context: context, builder: (_) => const MessageInputDialog());
if (result != null) {
ref.redux(selectedSendingFilesProvider).dispatch(AddMessageAction(message: result));
}
}
Future<void> _pickClipboard(BuildContext context, Ref ref) async {
late List<String> files = [];
await Pasteboard.files().then((value) => {for (final file in value) files.add(file)});
if (files.isNotEmpty) {
await ref.redux(selectedSendingFilesProvider).dispatchAsync(AddFilesAction(
files: files.map((e) => XFile(e)).toList(),
converter: CrossFileConverters.convertXFile,
));
} else {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(t.general.noItemInClipboard),
));
}
}
Future<void> _pickApp(BuildContext context) async {
// Currently, only Android APK
await context.push(() => const ApkPickerPage());
}
+1 -4
View File
@@ -62,10 +62,7 @@ class AddFileDialog extends StatelessWidget {
filled: true,
onTap: () async {
context.popUntilRoot();
await option.select(
context: context,
ref: RefenaScope.defaultRef,
);
await context.ref.dispatchAsync(PickAction(option: option, context: context));
},
);
}),