diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index d5995a0bb..7ca69aca4 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,5 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lib/bademagic_module/usb/payload_builder.dart b/lib/bademagic_module/usb/payload_builder.dart new file mode 100644 index 000000000..35d9bb460 --- /dev/null +++ b/lib/bademagic_module/usb/payload_builder.dart @@ -0,0 +1,27 @@ +import 'package:badgemagic/bademagic_module/bluetooth/datagenerator.dart'; + +/// Builds USB CDC payloads from Badge data. +/// Same format as BLE, but split into 64-byte chunks. +class PayloadBuilder { + final DataTransferManager manager; + + PayloadBuilder({required this.manager}); + + Future>> buildPayloads() async { + // Generate the raw payload using the existing BLE generator + final rawChunks = await manager.generateDataChunk(); + + // Flatten because BLE uses 16-byte chunks + final flat = rawChunks.expand((c) => c).toList(); + + // Split into 64-byte chunks for USB CDC + final usbChunks = >[]; + for (int i = 0; i < flat.length; i += 64) { + usbChunks.add( + flat.sublist(i, i + 64 > flat.length ? flat.length : i + 64), + ); + } + + return usbChunks; + } +} diff --git a/lib/bademagic_module/usb/usb_cdc.dart b/lib/bademagic_module/usb/usb_cdc.dart new file mode 100644 index 000000000..82092200f --- /dev/null +++ b/lib/bademagic_module/usb/usb_cdc.dart @@ -0,0 +1,105 @@ +import 'dart:typed_data'; +import 'package:badgemagic/constants.dart'; +import 'package:logger/logger.dart'; +import 'package:usb_serial/usb_serial.dart'; + +/// Wrapper around usb_serial for BadgeMagic devices +class UsbCdc { + final logger = Logger(); + UsbPort? _port; + + /// Open the first available FOSSASIA USB device + Future openDevice() async { + final devices = await listDevices(); + + // Filter for FOSSASIA badges - BOTH MODES + final fossasiaDevices = devices + .where((device) => + (device.vid == normalVendorId && device.pid == normalProductId) || + (device.vid == bootloaderVendorId && + device.pid == bootloaderProductId)) + .toList(); + + if (fossasiaDevices.isEmpty) { + logger.e("No FOSSASIA badge found. Available devices: $devices"); + return false; + } + + final device = fossasiaDevices.first; + logger.d("Found FOSSASIA device: ${device.vid}:${device.pid}"); + + // BOOTLOADER DETECTION + if (device.vid == bootloaderVendorId && device.pid == bootloaderProductId) { + logger.e("Device is in bootloader mode - cannot transfer data"); + throw Exception( + "Device is in bootloader mode. Please disconnect, then connect without holding any buttons."); + } + + _port = await device.create(); + if (_port == null) { + logger.e("Failed to create USB port"); + return false; + } + + final opened = await _port!.open(); + if (!opened) { + logger.e("Failed to open USB port"); + return false; + } + + await _port!.setPortParameters( + 115200, + UsbPort.DATABITS_8, + UsbPort.STOPBITS_1, + UsbPort.PARITY_NONE, + ); + + logger.d("USB device opened successfully"); + return true; + } + + /// Write data to the USB port + Future write(List data) async { + if (_port == null) throw Exception("USB port not open"); + + try { + await _port!.write(Uint8List.fromList(data)); + logger.d("USB chunk written: ${data.length} bytes"); + } catch (e) { + logger.e("Failed to write USB chunk: $e"); + rethrow; + } + } + + /// Close the USB port + Future close() async { + try { + await _port?.close(); + logger.d("USB port closed"); + } catch (e) { + logger.e("Error closing USB port: $e"); + } + } + + /// List connected USB devices with better logging + Future> listDevices() async { + try { + final devices = await UsbSerial.listDevices(); + logger.d( + "USB devices found: ${devices.map((d) => 'VID:${d.vid?.toRadixString(16)} PID:${d.pid?.toRadixString(16)}').toList()}"); + return devices; + } catch (e) { + logger.e("Error listing USB devices: $e"); + return []; + } + } + + /// Helper to check if any FOSSASIA device is connected + Future isFossasiaDeviceConnected() async { + final devices = await listDevices(); + return devices.any((device) => + (device.vid == normalVendorId && device.pid == normalProductId) || + (device.vid == bootloaderVendorId && + device.pid == bootloaderProductId)); + } +} diff --git a/lib/bademagic_module/usb/usb_scan_state.dart b/lib/bademagic_module/usb/usb_scan_state.dart new file mode 100644 index 000000000..cc2e6f1b5 --- /dev/null +++ b/lib/bademagic_module/usb/usb_scan_state.dart @@ -0,0 +1,45 @@ +// usb_scan_state.dart +import 'dart:async'; +import 'package:badgemagic/bademagic_module/bluetooth/base_ble_state.dart'; +import 'package:badgemagic/bademagic_module/usb/payload_builder.dart'; +import 'package:badgemagic/bademagic_module/usb/usb_cdc.dart'; +import 'package:badgemagic/bademagic_module/bluetooth/completed_state.dart'; +import 'package:badgemagic/bademagic_module/usb/usb_write_state.dart'; + +/// USB scan state (mirrors ScanState for BLE) +class UsbScanState extends NormalBleState { + final PayloadBuilder builder; + + UsbScanState({required this.builder}); + + @override + Future processState() async { + final usb = UsbCdc(); + toast.showToast("Searching for USB device..."); + + try { + final devices = + await usb.listDevices(); // wrapper for UsbSerial.listDevices() + + final fossasiaDevices = devices + .where((device) => device.vid == 0x0416 && device.pid == 0x5020) + .toList(); + + if (fossasiaDevices.isEmpty) { + toast.showErrorToast("No FOSSASIA badge found"); + return CompletedState( + isSuccess: false, message: "No FOSSASIA USB device found"); + } + + toast.showToast("USB device found. Preparing transfer..."); + + // Directly pass to UsbWriteState + final writeState = UsbWriteState(builder: builder); + return await writeState.process(); + } catch (e) { + logger.e("USB scan error: $e"); + toast.showErrorToast("USB scan failed: $e"); + return CompletedState(isSuccess: false, message: "USB scan failed: $e"); + } + } +} diff --git a/lib/bademagic_module/usb/usb_write_state.dart b/lib/bademagic_module/usb/usb_write_state.dart new file mode 100644 index 000000000..5800a5681 --- /dev/null +++ b/lib/bademagic_module/usb/usb_write_state.dart @@ -0,0 +1,76 @@ +import 'package:badgemagic/bademagic_module/bluetooth/base_ble_state.dart'; +import 'package:badgemagic/bademagic_module/bluetooth/completed_state.dart'; +import 'package:badgemagic/bademagic_module/usb/payload_builder.dart'; +import 'package:badgemagic/bademagic_module/usb/usb_cdc.dart'; +import 'dart:io' show Platform; +import 'package:flutter/services.dart'; // for MissingPluginException + +class UsbWriteState extends NormalBleState { + final PayloadBuilder builder; + + UsbWriteState({required this.builder}); + + @override + Future processState() async { + // Unsupported platforms (macOS, Web, etc.) + if (!Platform.isAndroid) { + toast.showErrorToast("USB transfer not supported on this platform"); + return CompletedState(isSuccess: false, message: "Unsupported platform"); + } + + final usb = UsbCdc(); + try { + bool opened; + try { + opened = await usb.openDevice(); + } on MissingPluginException catch (_) { + toast.showErrorToast( + "USB plugin not available. Please ensure the plugin is installed and platform supports USB.", + ); + throw Exception("USB plugin missing or not registered"); + } + + if (!opened) { + toast.showErrorToast("No BadgeMagic USB device found"); + throw Exception("No USB device connected"); + } + + final dataChunks = await builder.buildPayloads(); + logger.d("USB payload chunks: ${dataChunks.length}"); + + for (final chunk in dataChunks) { + bool success = false; + for (int attempt = 1; attempt <= 3; attempt++) { + try { + await usb.write(chunk); + logger.d("USB chunk written: $chunk"); + success = true; + break; + } catch (e) { + logger.e("USB write failed (attempt $attempt/3): $e"); + } + } + if (!success) { + toast.showErrorToast("Failed to transfer data over USB"); + throw Exception("USB transfer failed"); + } + await Future.delayed(const Duration(milliseconds: 20)); + } + + toast.showToast("USB transfer completed successfully"); + return CompletedState(isSuccess: true, message: "USB transfer complete"); + } catch (e) { + logger.e("USB transfer error: $e"); + if (e.toString().contains("MissingPluginException")) { + // Already handled above; no need to show extra toast + } else if (e.toString().contains("No USB device connected")) { + // Already shown toast above + } else { + toast.showErrorToast("USB transfer failed: ${e.toString()}"); + } + throw e; + } finally { + await usb.close(); + } + } +} diff --git a/lib/constants.dart b/lib/constants.dart index 113ab2e39..dc293e8ea 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -8,6 +8,11 @@ const drawBadgeScreen = "bm_db_screen"; const savedClipartScreen = "bm_sc_screen"; const savedBadgeScreen = "bm_sb_screen"; +enum ConnectionType { + bluetooth, + usb, +} + //Colors used in the app // Primary Colors const Color colorPrimary = Color(0xFFD32F2F); @@ -24,6 +29,12 @@ const Color mdGrey400 = Color(0xFFBDBDBD); const Color dividerColor = Color(0xFFE0E0E0); const Color drawerHeaderTitle = Color(0xFFFFFFFF); +// USB Configuration +const int normalVendorId = 4348; // 0x10FC - Normal mode +const int normalProductId = 55200; // 0x55E0 - Normal mode +const int bootloaderVendorId = 1046; // 0x0416 - Bootloader mode +const int bootloaderProductId = 20512; // 0x5020 - Bootloader mode + //path to all the animation assets used const String animation = 'assets/animations/ic_anim_animation.gif'; const String aniLeft = 'assets/animations/ic_anim_left.gif'; diff --git a/lib/main.dart b/lib/main.dart index f953de1a2..7545ac57b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -16,6 +16,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:provider/provider.dart'; import 'globals/globals.dart' as globals; import 'services/localization_service.dart'; +import 'package:badgemagic/providers/transfer_provider.dart'; Future main() async { setupLocator(); @@ -47,7 +48,9 @@ Future main() async { ChangeNotifierProvider( create: (context) => getIt()), ChangeNotifierProvider( - create: (_) => getIt(), + create: (_) => getIt()), + ChangeNotifierProvider( + create: (context) => TransferProvider(), // Create a new instance ), ], child: const MyApp(), diff --git a/lib/providers/animation_badge_provider.dart b/lib/providers/animation_badge_provider.dart index 366a1de4a..6474938e0 100644 --- a/lib/providers/animation_badge_provider.dart +++ b/lib/providers/animation_badge_provider.dart @@ -1,4 +1,9 @@ import 'dart:async'; +import 'dart:io'; +import 'package:badgemagic/bademagic_module/bluetooth/datagenerator.dart'; +import 'package:badgemagic/bademagic_module/usb/payload_builder.dart'; +import 'package:badgemagic/bademagic_module/usb/usb_write_state.dart'; +import 'package:badgemagic/bademagic_module/utils/toast_utils.dart'; import 'package:badgemagic/providers/badge_message_provider.dart'; import 'package:badgemagic/providers/imageprovider.dart'; import 'package:badgemagic/providers/speed_dial_provider.dart'; @@ -271,51 +276,83 @@ class AnimationBadgeProvider extends ChangeNotifier { required bool marquee, required bool invert, required BuildContext context, + required ConnectionType connectionType, }) async { + // DEFENSIVE CHECK - though UI should prevent this + if (connectionType == ConnectionType.usb && !Platform.isAndroid) { + ToastUtils().showToast("USB transfer is only available on Android"); + return; + } final int aniIndex = getAnimationIndex() ?? 0; final int selectedSpeed = speedDialProvider.getOuterValue(); - if (aniIndex == 9) { - // Pacman - await transferPacmanAnimation(badgeData, selectedSpeed); - } else if (aniIndex == 10) { - await transferChevronAnimation(badgeData, selectedSpeed); - } else if (aniIndex == 11) { - await transferDiamondAnimation(badgeData, selectedSpeed); - } else if (aniIndex == 12) { - await transferBrokenHeartsAnimation(badgeData, selectedSpeed); - } else if (aniIndex == 13) { - await transferCupidAnimation(badgeData, selectedSpeed); - setAnimationMode(CupidAnimation()); - _animationIndex = 0; - if (_timer == null || !_timer!.isActive) startTimer(); - } else if (aniIndex == 14) { - await transferFeetAnimation(badgeData, selectedSpeed); - } else if (aniIndex == 15) { - await transferFishAnimation(badgeData, selectedSpeed); - } else if (aniIndex == 16) { - await transferDiagonalAnimation(badgeData, selectedSpeed); - } else if (aniIndex == 17) { - await transferEmergencyAnimation(badgeData, selectedSpeed); - } else if (aniIndex == 18) { - await transferBeatingHeartsAnimation(badgeData, selectedSpeed); - } else if (aniIndex == 19) { - await transferFireworksAnimation(badgeData, selectedSpeed); - } else if (aniIndex == 20) { - await transferEqualizerAnimation(badgeData, selectedSpeed); - } else if (aniIndex == 21) { - await transferCycleAnimation(badgeData, selectedSpeed); - } else { - await badgeData.checkAndTransfer( - inlineImageProvider.getController().text, - flash, - marquee, - invert, - selectedSpeed, - modeValueMap[aniIndex], - null, - false, - context, - ); + if (connectionType == ConnectionType.bluetooth) { + if (aniIndex == 9) { + // Pacman + await transferPacmanAnimation(badgeData, selectedSpeed); + } else if (aniIndex == 10) { + await transferChevronAnimation(badgeData, selectedSpeed); + } else if (aniIndex == 11) { + await transferDiamondAnimation(badgeData, selectedSpeed); + } else if (aniIndex == 12) { + await transferBrokenHeartsAnimation(badgeData, selectedSpeed); + } else if (aniIndex == 13) { + await transferCupidAnimation(badgeData, selectedSpeed); + setAnimationMode(CupidAnimation()); + _animationIndex = 0; + if (_timer == null || !_timer!.isActive) startTimer(); + } else if (aniIndex == 14) { + await transferFeetAnimation(badgeData, selectedSpeed); + } else if (aniIndex == 15) { + await transferFishAnimation(badgeData, selectedSpeed); + } else if (aniIndex == 16) { + await transferDiagonalAnimation(badgeData, selectedSpeed); + } else if (aniIndex == 17) { + await transferEmergencyAnimation(badgeData, selectedSpeed); + } else if (aniIndex == 18) { + await transferBeatingHeartsAnimation(badgeData, selectedSpeed); + } else if (aniIndex == 19) { + await transferFireworksAnimation(badgeData, selectedSpeed); + } else if (aniIndex == 20) { + await transferEqualizerAnimation(badgeData, selectedSpeed); + } else if (aniIndex == 21) { + await transferCycleAnimation(badgeData, selectedSpeed); + } else { + await badgeData.checkAndTransfer( + inlineImageProvider.getController().text, + flash, + marquee, + invert, + selectedSpeed, + modeValueMap[aniIndex], + null, + false, + context, + ); + } + } else if (connectionType == ConnectionType.usb) { + if (inlineImageProvider.getController().text.trim().isEmpty) { + ToastUtils().showToast("Error: Please enter a message"); + return; + } + try { + final data = await badgeData.generateData( + inlineImageProvider.getController().text, + flash, + marquee, + invert, + speedMap[selectedSpeed], + modeValueMap[aniIndex], + null, + ); + + final manager = DataTransferManager(data); + final builder = PayloadBuilder(manager: manager); + final usbWriteState = UsbWriteState(builder: builder); + + await usbWriteState.process(); // Toasts handled inside UsbWriteState + } catch (e) { + logger.e("USB transfer error: $e"); + } } } } diff --git a/lib/providers/transfer_provider.dart b/lib/providers/transfer_provider.dart new file mode 100644 index 000000000..0c5a703fa --- /dev/null +++ b/lib/providers/transfer_provider.dart @@ -0,0 +1,33 @@ +// lib/providers/transfer_provider.dart +import 'package:flutter/foundation.dart'; +import 'package:badgemagic/constants.dart'; + +class TransferProvider with ChangeNotifier { + ConnectionType? _selectedMethod; + bool _showTray = false; + + ConnectionType? get selectedMethod => _selectedMethod; + bool get showTray => _showTray; + + void openTray() { + _showTray = true; + notifyListeners(); + } + + void closeTray() { + _showTray = false; + notifyListeners(); + } + + void selectMethod(ConnectionType method) { + _selectedMethod = method; + _showTray = false; + notifyListeners(); + } + + void reset() { + _selectedMethod = null; + _showTray = false; + notifyListeners(); + } +} diff --git a/lib/view/homescreen.dart b/lib/view/homescreen.dart index fe461f1a1..f0cde2b7d 100644 --- a/lib/view/homescreen.dart +++ b/lib/view/homescreen.dart @@ -18,12 +18,14 @@ import 'package:badgemagic/providers/font_provider.dart'; import 'package:badgemagic/providers/imageprovider.dart'; import 'package:badgemagic/providers/saved_badge_provider.dart'; import 'package:badgemagic/providers/speed_dial_provider.dart'; +import 'package:badgemagic/providers/transfer_provider.dart'; import 'package:badgemagic/services/localization_service.dart'; import 'package:badgemagic/view/special_text_field.dart'; import 'package:badgemagic/view/widgets/common_scaffold_widget.dart'; import 'package:badgemagic/view/widgets/homescreentabs.dart'; -import 'package:badgemagic/view/widgets/transitiontab.dart'; import 'package:badgemagic/view/widgets/save_badge_dialog.dart'; +import 'package:badgemagic/view/widgets/transfer_method_tray.dart'; +import 'package:badgemagic/view/widgets/transitiontab.dart'; import 'package:badgemagic/view/widgets/speedial.dart'; import 'package:badgemagic/view/widgets/vectorview.dart'; import 'package:badgemagic/virtualbadge/view/animated_badge.dart'; @@ -487,19 +489,32 @@ class _HomeScreenState extends State height: 32.h, child: GestureDetector( onTap: () async { - await animationProvider - .handleAnimationTransfer( - badgeData: badgeData, - inlineImageProvider: inlineImageProvider, - speedDialProvider: speedDialProvider, - flash: animationProvider - .isEffectActive(FlashEffect()), - marquee: animationProvider - .isEffectActive(MarqueeEffect()), - invert: animationProvider - .isEffectActive(InvertLEDEffect()), - context: context, - ); + // Platform-specific transfer logic + if (Theme.of(context).platform == + TargetPlatform.android) { + // Android: Show transfer tray + final transferProvider = + Provider.of(context, + listen: false); + transferProvider.openTray(); + } else { + // Other platforms: Direct BLE transfer + await animationProvider + .handleAnimationTransfer( + badgeData: badgeData, + inlineImageProvider: inlineImageProvider, + speedDialProvider: speedDialProvider, + flash: animationProvider + .isEffectActive(FlashEffect()), + marquee: animationProvider + .isEffectActive(MarqueeEffect()), + invert: animationProvider + .isEffectActive(InvertLEDEffect()), + context: context, + connectionType: ConnectionType + .bluetooth, // Always BLE for non-Android + ); + } }, child: Container( alignment: Alignment.center, @@ -527,7 +542,6 @@ class _HomeScreenState extends State "Please enter a message"); return; } - if (widget.savedBadgeFilename != null) { // Update existing badge SavedBadgeProvider savedBadgeProvider = @@ -538,7 +552,6 @@ class _HomeScreenState extends State baseFilename = baseFilename.substring( 0, baseFilename.length - 5); } - await savedBadgeProvider .updateBadgeData( baseFilename, @@ -554,7 +567,6 @@ class _HomeScreenState extends State .getAnimationIndex() ?? 1, ); - ToastUtils().showToast( "Badge Updated Successfully"); Navigator.pushNamedAndRemoveUntil( @@ -599,20 +611,35 @@ class _HomeScreenState extends State Expanded( child: GestureDetector( onTap: () async { - await animationProvider - .handleAnimationTransfer( - badgeData: badgeData, - inlineImageProvider: - inlineImageProvider, - speedDialProvider: speedDialProvider, - flash: animationProvider - .isEffectActive(FlashEffect()), - marquee: animationProvider - .isEffectActive(MarqueeEffect()), - invert: animationProvider - .isEffectActive(InvertLEDEffect()), - context: context, - ); + // Platform-specific transfer logic + if (Theme.of(context).platform == + TargetPlatform.android) { + // Android: Show transfer tray + final transferProvider = + Provider.of( + context, + listen: false); + transferProvider.openTray(); + } else { + // Other platforms: Direct BLE transfer + await animationProvider + .handleAnimationTransfer( + badgeData: badgeData, + inlineImageProvider: + inlineImageProvider, + speedDialProvider: speedDialProvider, + flash: animationProvider + .isEffectActive(FlashEffect()), + marquee: animationProvider + .isEffectActive(MarqueeEffect()), + invert: + animationProvider.isEffectActive( + InvertLEDEffect()), + context: context, + connectionType: ConnectionType + .bluetooth, // Always BLE for non-Android + ); + } }, child: Container( height: 32.h, @@ -634,6 +661,46 @@ class _HomeScreenState extends State }, ), ), + Consumer( + builder: (context, transferProvider, _) { + if (transferProvider.showTray) { + return Positioned.fill( + child: GestureDetector( + onTap: () { + transferProvider.closeTray(); + }, + child: Container( + color: Colors.black.withOpacity(0.3), + ), + ), + ); + } + return const SizedBox.shrink(); + }, + ), + +// Transfer Method Tray (only slides up on Android) + Consumer( + builder: (context, transferProvider, _) { + return AnimatedPositioned( + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + bottom: transferProvider.showTray ? 0 : -300, + left: 0, + right: 0, + child: TransferMethodTray( + onMethodSelected: (method) async { + transferProvider.selectMethod(method); + await _handleTransfer( + context, method, animationProvider); + }, + onCancel: () { + transferProvider.closeTray(); + }, + ), + ); + }, + ), ], ), ), @@ -690,6 +757,31 @@ class _HomeScreenState extends State ); } + Future _handleTransfer( + BuildContext context, + ConnectionType method, + AnimationBadgeProvider animationProvider, + ) async { + try { + await animationProvider.handleAnimationTransfer( + badgeData: badgeData, + inlineImageProvider: inlineImageProvider, + speedDialProvider: speedDialProvider, + flash: animationProvider.isEffectActive(FlashEffect()), + marquee: animationProvider.isEffectActive(MarqueeEffect()), + invert: animationProvider.isEffectActive(InvertLEDEffect()), + context: context, + connectionType: method, + ); + } catch (e) { + ToastUtils().showToast("Transfer failed: ${e.toString()}"); + } finally { + final transferProvider = + Provider.of(context, listen: false); + transferProvider.reset(); + } + } + @override bool get wantKeepAlive => true; } diff --git a/lib/view/widgets/transfer_method_tray.dart b/lib/view/widgets/transfer_method_tray.dart new file mode 100644 index 000000000..ec568bdc8 --- /dev/null +++ b/lib/view/widgets/transfer_method_tray.dart @@ -0,0 +1,123 @@ +import 'package:flutter/material.dart'; +import 'package:badgemagic/constants.dart'; // Assuming you have colors defined + +class TransferMethodTray extends StatelessWidget { + final Function(ConnectionType) onMethodSelected; + final VoidCallback onCancel; + + const TransferMethodTray({ + Key? key, + required this.onMethodSelected, + required this.onCancel, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + height: MediaQuery.of(context).size.height / 3.8, + decoration: BoxDecoration( + color: Theme.of(context).dialogBackgroundColor, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 8, + offset: const Offset(0, -2), + ), + ], + ), + child: Column( + children: [ + // Drag handle + Container( + width: 40, + height: 4, + margin: const EdgeInsets.symmetric(vertical: 8), + decoration: BoxDecoration( + color: Colors.grey[400], + borderRadius: BorderRadius.circular(2), + ), + ), + + const Text( + 'Choose Transfer Method', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + + const SizedBox(height: 16), + + // Transfer options + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildTransferOption( + context, + icon: Icons.bluetooth, + label: 'Bluetooth', + type: ConnectionType.bluetooth, + ), + _buildTransferOption( + context, + icon: Icons.usb, + label: 'USB', + type: ConnectionType.usb, + ), + ], + ), + + const Spacer(), + + // Cancel button + TextButton( + onPressed: onCancel, + child: const Text( + 'Cancel', + style: TextStyle(color: colorPrimaryDark), + ), + ), + ], + ), + ); + } + + Widget _buildTransferOption( + BuildContext context, { + required IconData icon, + required String label, + required ConnectionType type, + }) { + return GestureDetector( + onTap: () => onMethodSelected(type), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 60, + height: 60, + decoration: BoxDecoration( + color: colorPrimary.withOpacity(0.1), + shape: BoxShape.circle, + border: Border.all(color: colorPrimary, width: 2), + ), + child: Icon( + icon, + size: 30, + color: colorPrimary, + ), + ), + const SizedBox(height: 8), + Text( + label, + style: const TextStyle(fontSize: 14), + ), + ], + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index a7cc98e93..5c60a72bf 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -801,6 +801,15 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.4" + usb_serial: + dependency: "direct main" + description: + path: "." + ref: "886d3a6b2e6831020586c99af6ae9d1b3bf35115" + resolved-ref: "886d3a6b2e6831020586c99af6ae9d1b3bf35115" + url: "https://github.com/tushar11kh/usbserial.git" + source: git + version: "0.5.1" uuid: dependency: "direct main" description: @@ -907,4 +916,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.8.0 <4.0.0" - flutter: ">=3.35.3" + flutter: ">=3.35.5" diff --git a/pubspec.yaml b/pubspec.yaml index 6053abf41..37a4d051a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -55,6 +55,10 @@ dependencies: google_fonts: ^6.3.2 url_launcher: ^6.3.2 image: ^4.5.4 + usb_serial: + git: + url: https://github.com/tushar11kh/usbserial.git + ref: 886d3a6b2e6831020586c99af6ae9d1b3bf35115 dev_dependencies: flutter_test: