feat: drag and drop files

This commit is contained in:
Tien Do Nam
2022-12-30 22:39:46 +01:00
parent 9135a59c49
commit fa90816cff
15 changed files with 175 additions and 63 deletions
+4
View File
@@ -1,3 +1,7 @@
## 1.2.0
- feat: drag and drop files
## 1.1.0 (2022-12-30)
- feat(android): add media picker
+2 -1
View File
@@ -45,7 +45,8 @@
},
"nearbyDevices": "Nearby devices",
"thisDevice": "This Device",
"help": "Please ensure that the desired target is also in the same wifi network."
"help": "Please ensure that the desired target is also in the same wifi network.",
"placeItems": "Place items to share."
},
"settingsTab": {
"title": "Settings",
+2 -1
View File
@@ -45,7 +45,8 @@
},
"nearbyDevices": "Geräte in der Nähe",
"thisDevice": "Dieses Gerät",
"help": "Bitte stelle sicher, dass sich das gewünschte Ziel auch im selben WLAN-Netzwerk befindet."
"help": "Bitte stelle sicher, dass sich das gewünschte Ziel auch im selben WLAN-Netzwerk befindet.",
"placeItems": "Dateien ablegen, um zu teilen."
},
"settingsTab": {
"title": "Einstellungen",
+1 -1
View File
@@ -7,7 +7,7 @@ import 'package:wechat_assets_picker/wechat_assets_picker.dart';
part 'cross_file.freezed.dart';
/// Common file model to merge file_picker and image_picker results.
/// Common file model to avoid any third party libraries in the core logic.
@freezed
class CrossFile with _$CrossFile {
const factory CrossFile({
+66 -22
View File
@@ -1,3 +1,4 @@
import 'package:desktop_drop/desktop_drop.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:localsend_app/gen/strings.g.dart';
@@ -5,6 +6,7 @@ import 'package:localsend_app/pages/tabs/receive_tab.dart';
import 'package:localsend_app/pages/tabs/send_tab.dart';
import 'package:localsend_app/pages/tabs/settings_tab.dart';
import 'package:localsend_app/provider/network/server_provider.dart';
import 'package:localsend_app/provider/selected_files_provider.dart';
import 'package:localsend_app/provider/settings_provider.dart';
import 'package:localsend_app/theme.dart';
import 'package:localsend_app/util/snackbar.dart';
@@ -41,6 +43,8 @@ class _HomePageState extends ConsumerState<HomePage> {
final _pageController = PageController();
_Tab _currentTab = _Tab.receive;
bool _dragAndDropIndicator = false;
@override
void initState() {
super.initState();
@@ -64,32 +68,72 @@ class _HomePageState extends ConsumerState<HomePage> {
}
}
void _goToPage(int index) {
setState(() {
_currentTab = _Tab.values[index];
_pageController.jumpToPage(_currentTab.index);
});
}
@override
Widget build(BuildContext context) {
Translations.of(context); // rebuild on locale change
return Scaffold(
body: SafeArea(
child: PageView(
controller: _pageController,
physics: const NeverScrollableScrollPhysics(),
children: const [
ReceiveTab(),
SendTab(),
SettingsTab(),
],
return DropTarget(
onDragEntered: (_) {
setState(() {
_dragAndDropIndicator = true;
});
},
onDragExited: (_) {
setState(() {
_dragAndDropIndicator = false;
});
},
onDragDone: (event) {
ref.read(selectedFilesProvider.notifier).addFiles(
files: event.files,
converter: CrossFileConverters.convertXFile,
);
_goToPage(_Tab.send.index);
},
child: Scaffold(
body: SafeArea(
child: Stack(
children: [
PageView(
controller: _pageController,
physics: const NeverScrollableScrollPhysics(),
children: const [
ReceiveTab(),
SendTab(),
SettingsTab(),
],
),
if (_dragAndDropIndicator)
Container(
width: double.infinity,
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.file_download, size: 128),
const SizedBox(height: 30),
Text(t.sendTab.placeItems, style: Theme.of(context).textTheme.headline6),
],
),
),
],
),
),
bottomNavigationBar: NavigationBar(
selectedIndex: _currentTab.index,
onDestinationSelected: _goToPage,
destinations: _Tab.values.map((tab) {
return NavigationDestination(icon: Icon(tab.icon), label: tab.label);
}).toList(),
),
),
bottomNavigationBar: NavigationBar(
selectedIndex: _currentTab.index,
onDestinationSelected: (index) {
setState(() {
_currentTab = _Tab.values[index];
_pageController.jumpToPage(_currentTab.index);
});
},
destinations: _Tab.values.map((tab) {
return NavigationDestination(icon: Icon(tab.icon), label: tab.label);
}).toList(),
),
);
}
+2 -4
View File
@@ -35,7 +35,7 @@ class SelectedFilesPage extends ConsumerWidget {
),
ElevatedButton(
onPressed: () {
ref.read(selectedFilesProvider.notifier).state = [];
ref.read(selectedFilesProvider.notifier).reset();
context.popUntilRoot();
},
child: Text(t.selectedFilesPage.deleteAll),
@@ -65,9 +65,7 @@ class SelectedFilesPage extends ConsumerWidget {
),
IconButton(
onPressed: () {
final newList = [...selectedFiles];
newList.removeAt(index);
ref.read(selectedFilesProvider.notifier).state = newList;
ref.read(selectedFilesProvider.notifier).removeAt(index);
},
icon: const Icon(Icons.delete),
),
+71 -1
View File
@@ -1,4 +1,74 @@
import 'package:file_picker/file_picker.dart' as file_picker;
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.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:wechat_assets_picker/wechat_assets_picker.dart';
final selectedFilesProvider = StateProvider<List<CrossFile>>((ref) => []);
final selectedFilesProvider = StateNotifierProvider<SelectedFilesNotifier, List<CrossFile>>((ref) {
return SelectedFilesNotifier();
});
class SelectedFilesNotifier extends StateNotifier<List<CrossFile>> {
SelectedFilesNotifier() : super([]);
Future<void> addFiles<T>({
required Iterable<T> files,
required Future<CrossFile> Function(T) converter,
}) async {
final tempList = [...state];
for (final file in files) {
// we do it sequential because there are bugs
// https://github.com/fluttercandies/flutter_photo_manager/issues/589
tempList.add(await converter(file));
}
state = List.unmodifiable(tempList);
}
void removeAt(int index) {
state = List.unmodifiable([...state]..removeAt(index));
}
void reset() {
state = List.empty(growable: false);
}
}
/// Utility functions to convert third party models to [CrossFile].
class CrossFileConverters {
static Future<CrossFile> convertPlatformFile(file_picker.PlatformFile file) async {
return CrossFile(
name: file.name,
fileType: file.name.guessFileType(),
size: file.size,
asset: null,
path: kIsWeb ? null : file.path,
bytes: kIsWeb ? () async => 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(),
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(),
asset: null,
path: kIsWeb ? null : file.path,
bytes: kIsWeb ? () => file.readAsBytes() : null,
);
}
}
+8 -33
View File
@@ -1,12 +1,8 @@
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:localsend_app/gen/strings.g.dart';
import 'package:localsend_app/model/cross_file.dart';
import 'package:localsend_app/model/file_type.dart' as model;
import 'package:localsend_app/provider/selected_files_provider.dart';
import 'package:localsend_app/util/file_path_helper.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
enum FilePickerOption {
@@ -30,24 +26,14 @@ enum FilePickerOption {
required BuildContext context,
required WidgetRef ref,
}) async {
final selectedFiles = ref.read(selectedFilesProvider);
switch (this) {
case FilePickerOption.file:
final result = await FilePicker.platform.pickFiles(allowMultiple: true);
if (result != null) {
ref.read(selectedFilesProvider.notifier).state = [
...selectedFiles,
...result.files.map((f) {
return CrossFile(
name: f.name,
fileType: f.name.guessFileType(),
size: f.size,
asset: null,
path: kIsWeb ? null : f.path,
bytes: kIsWeb ? () async => f.bytes! : null,
);
}),
];
ref.read(selectedFilesProvider.notifier).addFiles(
files: result.files,
converter: CrossFileConverters.convertPlatformFile,
);
}
break;
case FilePickerOption.media:
@@ -58,21 +44,10 @@ enum FilePickerOption {
),
);
if (result != null) {
ref.read(selectedFilesProvider.notifier).state = [
...selectedFiles,
...await Future.wait(result.map((asset) async {
final file = (await asset.originFile)!;
return CrossFile(
name: await asset.titleAsync,
fileType: asset.type == AssetType.video ? model.FileType.video : model.FileType.image,
size: await file.length(),
asset: asset,
path: file.path,
bytes: null,
);
})),
];
print(result);
ref.read(selectedFilesProvider.notifier).addFiles(
files: result,
converter: CrossFileConverters.convertAssetEntity,
);
}
return;
}
@@ -6,11 +6,15 @@
#include "generated_plugin_registrant.h"
#include <desktop_drop/desktop_drop_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
#include <window_manager/window_manager_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) desktop_drop_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopDropPlugin");
desktop_drop_plugin_register_with_registrar(desktop_drop_registrar);
g_autoptr(FlPluginRegistrar) screen_retriever_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin");
screen_retriever_plugin_register_with_registrar(screen_retriever_registrar);
+1
View File
@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
desktop_drop
screen_retriever
url_launcher_linux
window_manager
@@ -5,6 +5,7 @@
import FlutterMacOS
import Foundation
import desktop_drop
import device_info_plus
import network_info_plus
import package_info_plus
@@ -16,6 +17,7 @@ import url_launcher_macos
import window_manager
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
NetworkInfoPlusPlugin.register(with: registry.registrar(forPlugin: "NetworkInfoPlusPlugin"))
FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
+7
View File
@@ -211,6 +211,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.8"
desktop_drop:
dependency: "direct main"
description:
name: desktop_drop
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.0"
device_info_plus:
dependency: "direct main"
description:
+1
View File
@@ -11,6 +11,7 @@ environment:
dependencies:
basic_utils: 5.4.2
collection: 1.16.0
desktop_drop: 0.4.0
device_info_plus: 8.0.0
dio: 4.0.6
file_picker: 5.2.4
@@ -6,6 +6,7 @@
#include "generated_plugin_registrant.h"
#include <desktop_drop/desktop_drop_plugin.h>
#include <network_info_plus/network_info_plus_windows_plugin.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h>
@@ -13,6 +14,8 @@
#include <window_manager/window_manager_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
DesktopDropPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("DesktopDropPlugin"));
NetworkInfoPlusWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("NetworkInfoPlusWindowsPlugin"));
PermissionHandlerWindowsPluginRegisterWithRegistrar(
+1
View File
@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
desktop_drop
network_info_plus
permission_handler_windows
screen_retriever