mirror of
https://github.com/localsend/localsend.git
synced 2026-08-07 07:14:52 +00:00
feat: drag and drop files
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
## 1.2.0
|
||||
|
||||
- feat: drag and drop files
|
||||
|
||||
## 1.1.0 (2022-12-30)
|
||||
|
||||
- feat(android): add media picker
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
desktop_drop
|
||||
network_info_plus
|
||||
permission_handler_windows
|
||||
screen_retriever
|
||||
|
||||
Reference in New Issue
Block a user