From eb20ed025f57167e20bc9fc680a152401e035d99 Mon Sep 17 00:00:00 2001 From: klockky Date: Sun, 21 Jun 2026 10:47:59 +0000 Subject: [PATCH] =?UTF-8?q?feat(contacts):=20=D1=81=D0=B8=D0=BD=D1=85?= =?UTF-8?q?=D1=80=D0=BE=D0=BD=D0=BD=D1=8B=D0=B9=20=D0=BF=D0=BE=D0=BA=D0=B0?= =?UTF-8?q?=D0=B7=20=D0=BA=D0=B0=D1=80=D1=82=D0=BE=D1=87=D0=BA=D0=B8=20+?= =?UTF-8?q?=20=D1=81=D1=82=D0=B0=D1=82=D1=83=D1=81=20=D0=BE=D0=B1=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/ru/komet/app/BleContactExchange.kt | 13 +++- .../main/kotlin/ru/komet/app/MainActivity.kt | 17 ++++- lib/core/nfc/nfc_exchange_service.dart | 4 +- .../screens/contacts/nfc_exchange_sheet.dart | 74 ++++++++++++++++++- 4 files changed, 100 insertions(+), 8 deletions(-) diff --git a/android/app/src/main/kotlin/ru/komet/app/BleContactExchange.kt b/android/app/src/main/kotlin/ru/komet/app/BleContactExchange.kt index c2cace1..4a4140f 100644 --- a/android/app/src/main/kotlin/ru/komet/app/BleContactExchange.kt +++ b/android/app/src/main/kotlin/ru/komet/app/BleContactExchange.kt @@ -36,6 +36,7 @@ class BleContactExchange(private val context: Context) { } var onReceived: ((Long) -> Unit)? = null + var onSent: ((Long) -> Unit)? = null var onError: ((String) -> Unit)? = null private val main = Handler(Looper.getMainLooper()) @@ -53,6 +54,7 @@ class BleContactExchange(private val context: Context) { @Volatile private var selfId: Long = 0L @Volatile private var selfSession: String = "" + @Volatile private var peerIdForWrite: Long = 0L @Volatile private var connecting = false @Volatile private var running = false @@ -68,8 +70,9 @@ class BleContactExchange(private val context: Context) { startGattServer() } - fun connectTo(peerSession: String) { + fun connectTo(peerSession: String, peerId: Long) { if (!running || connecting) return + peerIdForWrite = peerId startScan(peerSession) } @@ -313,6 +316,10 @@ class BleContactExchange(private val context: Context) { characteristic: BluetoothGattCharacteristic?, status: Int, ) { + if (status == BluetoothGatt.GATT_SUCCESS) { + val id = peerIdForWrite + if (id > 0L) emitSent(id) + } gatt?.disconnect() } } @@ -321,6 +328,10 @@ class BleContactExchange(private val context: Context) { main.post { onReceived?.invoke(id) } } + private fun emitSent(id: Long) { + main.post { onSent?.invoke(id) } + } + private fun emitError(reason: String) { main.post { onError?.invoke(reason) } } diff --git a/android/app/src/main/kotlin/ru/komet/app/MainActivity.kt b/android/app/src/main/kotlin/ru/komet/app/MainActivity.kt index 7cc3421..4b39664 100644 --- a/android/app/src/main/kotlin/ru/komet/app/MainActivity.kt +++ b/android/app/src/main/kotlin/ru/komet/app/MainActivity.kt @@ -43,6 +43,7 @@ class MainActivity : FlutterActivity() { private var ble: BleContactExchange? = null private var pendingSelfId = 0L private var pendingSession = "" + @Volatile private var exchangingEmitted = false private companion object { const val LOG_TAG = "VpnBypass" @@ -212,6 +213,7 @@ class MainActivity : FlutterActivity() { NfcExchange.active = true NfcExchange.onServed = { onNfcServed() } seenPeers.clear() + exchangingEmitted = false pendingSelfId = selfId pendingSession = session nfcCycling = true @@ -258,12 +260,19 @@ class MainActivity : FlutterActivity() { private fun startBle() { val exchange = ble ?: BleContactExchange(applicationContext).also { it.onReceived = { id -> onBleReceived(id) } + it.onSent = { id -> onBleReceived(id) } it.onError = { reason -> onBleError(reason) } ble = it } exchange.start(pendingSelfId, pendingSession) } + private fun emitExchanging() { + if (exchangingEmitted) return + exchangingEmitted = true + nfcEvents?.success(mapOf("event" to "exchanging")) + } + private fun onBleReceived(id: Long) { if (id == NfcExchange.selfId || !seenPeers.add(id)) return nfcEvents?.success(mapOf("event" to "received", "id" to id)) @@ -278,6 +287,7 @@ class MainActivity : FlutterActivity() { if (!NfcExchange.active) return@post nfcCycling = false nfcReaderDisable() + emitExchanging() } } @@ -349,10 +359,9 @@ class MainActivity : FlutterActivity() { if (peer.id == NfcExchange.selfId) return@post nfcCycling = false nfcReaderDisable() - ble?.connectTo(peer.session) - if (seenPeers.add(peer.id)) { - nfcEvents?.success(mapOf("event" to "received", "id" to peer.id)) - } + emitExchanging() + ble?.connectTo(peer.session, peer.id) + nfcHandler.postDelayed({ onBleReceived(peer.id) }, 3000L) } } diff --git a/lib/core/nfc/nfc_exchange_service.dart b/lib/core/nfc/nfc_exchange_service.dart index f5af0e9..f7ea9af 100644 --- a/lib/core/nfc/nfc_exchange_service.dart +++ b/lib/core/nfc/nfc_exchange_service.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:flutter/services.dart'; -enum NfcEventType { received, cancelled, error } +enum NfcEventType { received, exchanging, cancelled, error } class NfcEvent { final NfcEventType type; @@ -64,6 +64,8 @@ class NfcExchangeService { switch (map['event']) { case 'received': return NfcEvent(NfcEventType.received, parsedId); + case 'exchanging': + return const NfcEvent(NfcEventType.exchanging, null); case 'error': return NfcEvent(NfcEventType.error, null, reason: map['reason'] as String?); default: diff --git a/lib/frontend/screens/contacts/nfc_exchange_sheet.dart b/lib/frontend/screens/contacts/nfc_exchange_sheet.dart index 529c03c..5ff5d7a 100644 --- a/lib/frontend/screens/contacts/nfc_exchange_sheet.dart +++ b/lib/frontend/screens/contacts/nfc_exchange_sheet.dart @@ -12,7 +12,17 @@ import '../../../main.dart'; import '../../widgets/custom_notification.dart'; import '../../widgets/komet_avatar.dart'; -enum _Stage { checking, unsupported, disabled, failed, scanning, found, adding, added } +enum _Stage { + checking, + unsupported, + disabled, + failed, + scanning, + exchanging, + found, + adding, + added, +} class NfcExchangeSheet extends StatefulWidget { const NfcExchangeSheet({super.key}); @@ -88,6 +98,12 @@ class _NfcExchangeSheetState extends State } return; } + if (event.type == NfcEventType.exchanging) { + if (mounted && _peerId == null && _stage == _Stage.scanning) { + setState(() => _stage = _Stage.exchanging); + } + return; + } final id = event.id; if (id == null || _peerId != null) return; _peerId = id; @@ -190,7 +206,19 @@ class _NfcExchangeSheetState extends State ], ), const SizedBox(height: 12), - _buildContent(cs), + AnimatedSwitcher( + duration: const Duration(milliseconds: 350), + switchInCurve: Curves.easeOutBack, + switchOutCurve: Curves.easeIn, + transitionBuilder: (child, animation) => FadeTransition( + opacity: animation, + child: ScaleTransition(scale: animation, child: child), + ), + child: KeyedSubtree( + key: ValueKey(_stage == _Stage.found ? 'found' : _stage.name), + child: _buildContent(cs), + ), + ), ], ), ), @@ -216,6 +244,8 @@ class _NfcExchangeSheetState extends State return _message(cs, Symbols.bluetooth_disabled, _failReason); case _Stage.scanning: return _scanning(cs); + case _Stage.exchanging: + return _exchanging(cs); case _Stage.found: case _Stage.adding: case _Stage.added: @@ -280,6 +310,46 @@ class _NfcExchangeSheetState extends State ); } + Widget _exchanging(ColorScheme cs) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 24), + child: Column( + children: [ + SizedBox( + width: 120, + height: 120, + child: AnimatedBuilder( + animation: _pulse, + builder: (context, child) => CustomPaint( + painter: _RadarPainter(_pulse.value, cs.primary), + child: child, + ), + child: Center( + child: Icon(Symbols.sync, color: cs.primary, size: 40), + ), + ), + ), + const SizedBox(height: 22), + Text( + 'Идёт обмен контактами…', + textAlign: TextAlign.center, + style: TextStyle( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 6), + Text( + 'Почти готово', + textAlign: TextAlign.center, + style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13), + ), + ], + ), + ); + } + Widget _foundCard(ColorScheme cs) { final loading = _peerInfo == null && _stage == _Stage.found; return Padding(