feat: drag and drop

This commit is contained in:
Gringo
2026-03-01 10:20:13 +01:00
parent 4a24d9f9e8
commit 36283231d7
9 changed files with 297 additions and 54 deletions
+58
View File
@@ -1,9 +1,13 @@
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:file_transfer/functions/share_file.dart';
import 'package:file_transfer/models/shared_file.dart';
import 'package:file_transfer/routes.dart';
import 'package:file_transfer/utils/platform_helper.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_dropzone/flutter_dropzone.dart';
import 'package:get/get.dart';
import 'package:mime/mime.dart';
@@ -11,10 +15,64 @@ class HomePageController extends GetxController {
final _sharedFile = Rxn<SharedFile>();
final _isUploading = false.obs;
final _error = RxnString();
final _isDragging = false.obs;
final dropzoneController = Rxn<DropzoneViewController>();
SharedFile? get sharedFile => _sharedFile.value;
bool get isUploading => _isUploading.value;
String? get error => _error.value;
bool get isDragging => _isDragging.value;
bool get isWeb => kIsWeb;
Future<void> handleDroppedFile(dynamic file) async {
_isDragging.value = false;
_isUploading.value = true;
_error.value = null;
try {
String filename;
Uint8List bytes;
if (kIsWeb && file is DropzoneFileInterface) {
// Web: use dropzone controller
filename = await dropzoneController.value!.getFilename(file);
bytes = await dropzoneController.value!.getFileData(file);
} else if (file is String) {
// Desktop: file path
final filePath = file;
final fileObj = File(filePath);
filename = fileObj.uri.pathSegments.last;
bytes = await fileObj.readAsBytes();
} else {
throw Exception('Unsupported file type: ${file.runtimeType}');
}
final mimeType = lookupMimeType(
filename,
headerBytes: bytes.take(8).toList(),
);
final sharedFile = await shareFile(
bytes: bytes,
contentType: mimeType,
filename: filename,
);
_sharedFile.value = sharedFile;
_isUploading.value = false;
} catch (e) {
_error.value = e.toString();
_isUploading.value = false;
}
}
void onDragHover() {
_isDragging.value = true;
}
void onDragLeave() {
_isDragging.value = false;
}
Future<void> pickAndShareFile() async {
_isUploading.value = true;
+186 -54
View File
@@ -1,5 +1,8 @@
import 'package:desktop_drop/desktop_drop.dart';
import 'package:file_transfer/controllers/home_controller.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dropzone/flutter_dropzone.dart';
import 'package:get/get.dart';
class UploadView extends GetView<HomePageController> {
@@ -12,63 +15,192 @@ class UploadView extends GetView<HomePageController> {
return Scaffold(
appBar: AppBar(title: const Text('File Transfer')),
body: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: isSmallScreen ? 16 : 24),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: MediaQuery.of(context).padding.top),
Icon(
Icons.upload_file_rounded,
size: isSmallScreen ? 64 : 80,
color: colorScheme.primary,
),
SizedBox(height: isSmallScreen ? 20 : 24),
Text(
'Select a file to share',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: isSmallScreen ? 16 : 18,
color: colorScheme.onSurface,
),
),
SizedBox(height: isSmallScreen ? 24 : 32),
Obx(
() => FilledButton.icon(
onPressed: controller.isUploading
? null
: controller.pickAndShareFile,
icon: controller.isUploading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.upload_file),
label: Text(
controller.isUploading ? 'Uploading...' : 'Select File',
),
),
),
SizedBox(height: isSmallScreen ? 10 : 12),
OutlinedButton.icon(
onPressed: controller.pasteAndOpenLink,
icon: const Icon(Icons.paste),
label: const Text('Paste Share Link'),
),
Obx(() {
if (controller.error != null) {
return _buildErrorBox(context, isSmallScreen, colorScheme);
}
return const SizedBox.shrink();
}),
],
body: kIsWeb
? _buildWebDropzone(context, isSmallScreen, colorScheme)
: _buildDesktopDropzone(context, isSmallScreen, colorScheme),
);
}
Widget _buildWebDropzone(
BuildContext context,
bool isSmallScreen,
ColorScheme colorScheme,
) {
return Stack(
children: [
Obx(
() => IgnorePointer(
ignoring: !controller.isDragging,
child: Container(
color: controller.isDragging
? colorScheme.primary.withValues(alpha: 0.1)
: Colors.transparent,
child: Center(
child: controller.isDragging
? Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.file_download_rounded,
size: 80,
color: colorScheme.primary,
),
const SizedBox(height: 16),
Text(
'Drop file to upload',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: colorScheme.primary,
),
),
],
)
: null,
),
),
),
),
DropzoneView(
operation: DragOperation.copy,
cursor: CursorType.grab,
onCreated: (DropzoneViewController ctrl) {
controller.dropzoneController.value = ctrl;
},
onHover: () {
controller.onDragHover();
},
onLeave: () {
controller.onDragLeave();
},
onDropFile: (DropzoneFileInterface file) {
controller.handleDroppedFile(file);
},
),
_buildContent(context, isSmallScreen, colorScheme),
],
);
}
Widget _buildDesktopDropzone(
BuildContext context,
bool isSmallScreen,
ColorScheme colorScheme,
) {
return DropTarget(
onDragDone: (event) {
if (event.files.isNotEmpty) {
controller.handleDroppedFile(event.files.first.path);
}
},
onDragEntered: (event) {
controller.onDragHover();
},
onDragExited: (event) {
controller.onDragLeave();
},
child: Stack(
children: [
Obx(
() => IgnorePointer(
ignoring: !controller.isDragging,
child: Container(
color: controller.isDragging
? colorScheme.primary.withValues(alpha: 0.1)
: Colors.transparent,
child: Center(
child: controller.isDragging
? Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.file_download_rounded,
size: 80,
color: colorScheme.primary,
),
const SizedBox(height: 16),
Text(
'Drop file to upload',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: colorScheme.primary,
),
),
],
)
: null,
),
),
),
),
_buildContent(context, isSmallScreen, colorScheme),
],
),
);
}
Widget _buildContent(
BuildContext context,
bool isSmallScreen,
ColorScheme colorScheme,
) {
return SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: isSmallScreen ? 16 : 24),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: MediaQuery.of(context).padding.top),
Icon(
Icons.upload_file_rounded,
size: isSmallScreen ? 64 : 80,
color: colorScheme.primary,
),
SizedBox(height: isSmallScreen ? 20 : 24),
Text(
'Drag & drop a file or select to share',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: isSmallScreen ? 16 : 18,
color: colorScheme.onSurface,
),
),
SizedBox(height: isSmallScreen ? 24 : 32),
Obx(
() => FilledButton.icon(
onPressed: controller.isUploading
? null
: controller.pickAndShareFile,
icon: controller.isUploading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.upload_file),
label: Text(
controller.isUploading ? 'Uploading...' : 'Select File',
),
),
),
SizedBox(height: isSmallScreen ? 10 : 12),
OutlinedButton.icon(
onPressed: controller.pasteAndOpenLink,
icon: const Icon(Icons.paste),
label: const Text('Paste Share Link'),
),
Obx(() {
if (controller.error != null) {
return _buildErrorBox(context, isSmallScreen, colorScheme);
}
return const SizedBox.shrink();
}),
],
),
),
),
);
}
@@ -6,11 +6,15 @@
#include "generated_plugin_registrant.h"
#include <desktop_drop/desktop_drop_plugin.h>
#include <file_saver/file_saver_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <url_launcher_linux/url_launcher_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) file_saver_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSaverPlugin");
file_saver_plugin_register_with_registrar(file_saver_registrar);
+1
View File
@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
desktop_drop
file_saver
flutter_secure_storage_linux
url_launcher_linux
@@ -5,12 +5,14 @@
import FlutterMacOS
import Foundation
import desktop_drop
import file_picker
import file_saver
import flutter_secure_storage_darwin
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin"))
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin"))
FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin"))
+40
View File
@@ -121,6 +121,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.12"
desktop_drop:
dependency: "direct main"
description:
name: desktop_drop
sha256: e70b46b2d61f1af7a81a40d1f79b43c28a879e30a4ef31e87e9c27bea4d784e8
url: "https://pub.dev"
source: hosted
version: "0.7.0"
dio:
dependency: transitive
description:
@@ -206,6 +214,30 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_dropzone:
dependency: "direct main"
description:
name: flutter_dropzone
sha256: d0917c4e366ebb3a98c9c02faac79223d119af5e236967c70e7e4002cdfb20d3
url: "https://pub.dev"
source: hosted
version: "4.2.1"
flutter_dropzone_platform_interface:
dependency: transitive
description:
name: flutter_dropzone_platform_interface
sha256: "96d2c51c86063ba150551c3b40fd26c5a18785bee071c0c751502d28a545df3b"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
flutter_dropzone_web:
dependency: transitive
description:
name: flutter_dropzone_web
sha256: "03b216f9443add6ad1d075ab513a354a47edf3d1b87d830a8ed314f0d3a2beac"
url: "https://pub.dev"
source: hosted
version: "4.2.0"
flutter_lints:
dependency: "direct dev"
description:
@@ -666,6 +698,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
universal_platform:
dependency: transitive
description:
name: universal_platform
sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
unorm_dart:
dependency: transitive
description:
+2
View File
@@ -9,10 +9,12 @@ environment:
dependencies:
crypto: ^3.0.7
cryptography: ^2.9.0
desktop_drop: ^0.7.0
file_picker: ^10.3.10
file_saver: ^0.3.1
flutter:
sdk: flutter
flutter_dropzone: ^4.2.1
get: ^4.7.3
mime: ^2.0.0
ndk: ^0.7.1-dev.19
@@ -6,11 +6,14 @@
#include "generated_plugin_registrant.h"
#include <desktop_drop/desktop_drop_plugin.h>
#include <file_saver/file_saver_plugin.h>
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
DesktopDropPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("DesktopDropPlugin"));
FileSaverPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSaverPlugin"));
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
+1
View File
@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
desktop_drop
file_saver
flutter_secure_storage_windows
url_launcher_windows