feat(contacts): синхронный показ карточки + статус обмена

This commit is contained in:
klockky
2026-06-21 10:47:59 +00:00
parent 6b082a81fe
commit eb20ed025f
4 changed files with 100 additions and 8 deletions
@@ -36,6 +36,7 @@ class BleContactExchange(private val context: Context) {
} }
var onReceived: ((Long) -> Unit)? = null var onReceived: ((Long) -> Unit)? = null
var onSent: ((Long) -> Unit)? = null
var onError: ((String) -> Unit)? = null var onError: ((String) -> Unit)? = null
private val main = Handler(Looper.getMainLooper()) 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 selfId: Long = 0L
@Volatile private var selfSession: String = "" @Volatile private var selfSession: String = ""
@Volatile private var peerIdForWrite: Long = 0L
@Volatile private var connecting = false @Volatile private var connecting = false
@Volatile private var running = false @Volatile private var running = false
@@ -68,8 +70,9 @@ class BleContactExchange(private val context: Context) {
startGattServer() startGattServer()
} }
fun connectTo(peerSession: String) { fun connectTo(peerSession: String, peerId: Long) {
if (!running || connecting) return if (!running || connecting) return
peerIdForWrite = peerId
startScan(peerSession) startScan(peerSession)
} }
@@ -313,6 +316,10 @@ class BleContactExchange(private val context: Context) {
characteristic: BluetoothGattCharacteristic?, characteristic: BluetoothGattCharacteristic?,
status: Int, status: Int,
) { ) {
if (status == BluetoothGatt.GATT_SUCCESS) {
val id = peerIdForWrite
if (id > 0L) emitSent(id)
}
gatt?.disconnect() gatt?.disconnect()
} }
} }
@@ -321,6 +328,10 @@ class BleContactExchange(private val context: Context) {
main.post { onReceived?.invoke(id) } main.post { onReceived?.invoke(id) }
} }
private fun emitSent(id: Long) {
main.post { onSent?.invoke(id) }
}
private fun emitError(reason: String) { private fun emitError(reason: String) {
main.post { onError?.invoke(reason) } main.post { onError?.invoke(reason) }
} }
@@ -43,6 +43,7 @@ class MainActivity : FlutterActivity() {
private var ble: BleContactExchange? = null private var ble: BleContactExchange? = null
private var pendingSelfId = 0L private var pendingSelfId = 0L
private var pendingSession = "" private var pendingSession = ""
@Volatile private var exchangingEmitted = false
private companion object { private companion object {
const val LOG_TAG = "VpnBypass" const val LOG_TAG = "VpnBypass"
@@ -212,6 +213,7 @@ class MainActivity : FlutterActivity() {
NfcExchange.active = true NfcExchange.active = true
NfcExchange.onServed = { onNfcServed() } NfcExchange.onServed = { onNfcServed() }
seenPeers.clear() seenPeers.clear()
exchangingEmitted = false
pendingSelfId = selfId pendingSelfId = selfId
pendingSession = session pendingSession = session
nfcCycling = true nfcCycling = true
@@ -258,12 +260,19 @@ class MainActivity : FlutterActivity() {
private fun startBle() { private fun startBle() {
val exchange = ble ?: BleContactExchange(applicationContext).also { val exchange = ble ?: BleContactExchange(applicationContext).also {
it.onReceived = { id -> onBleReceived(id) } it.onReceived = { id -> onBleReceived(id) }
it.onSent = { id -> onBleReceived(id) }
it.onError = { reason -> onBleError(reason) } it.onError = { reason -> onBleError(reason) }
ble = it ble = it
} }
exchange.start(pendingSelfId, pendingSession) exchange.start(pendingSelfId, pendingSession)
} }
private fun emitExchanging() {
if (exchangingEmitted) return
exchangingEmitted = true
nfcEvents?.success(mapOf("event" to "exchanging"))
}
private fun onBleReceived(id: Long) { private fun onBleReceived(id: Long) {
if (id == NfcExchange.selfId || !seenPeers.add(id)) return if (id == NfcExchange.selfId || !seenPeers.add(id)) return
nfcEvents?.success(mapOf("event" to "received", "id" to id)) nfcEvents?.success(mapOf("event" to "received", "id" to id))
@@ -278,6 +287,7 @@ class MainActivity : FlutterActivity() {
if (!NfcExchange.active) return@post if (!NfcExchange.active) return@post
nfcCycling = false nfcCycling = false
nfcReaderDisable() nfcReaderDisable()
emitExchanging()
} }
} }
@@ -349,10 +359,9 @@ class MainActivity : FlutterActivity() {
if (peer.id == NfcExchange.selfId) return@post if (peer.id == NfcExchange.selfId) return@post
nfcCycling = false nfcCycling = false
nfcReaderDisable() nfcReaderDisable()
ble?.connectTo(peer.session) emitExchanging()
if (seenPeers.add(peer.id)) { ble?.connectTo(peer.session, peer.id)
nfcEvents?.success(mapOf("event" to "received", "id" to peer.id)) nfcHandler.postDelayed({ onBleReceived(peer.id) }, 3000L)
}
} }
} }
+3 -1
View File
@@ -3,7 +3,7 @@ import 'dart:io';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
enum NfcEventType { received, cancelled, error } enum NfcEventType { received, exchanging, cancelled, error }
class NfcEvent { class NfcEvent {
final NfcEventType type; final NfcEventType type;
@@ -64,6 +64,8 @@ class NfcExchangeService {
switch (map['event']) { switch (map['event']) {
case 'received': case 'received':
return NfcEvent(NfcEventType.received, parsedId); return NfcEvent(NfcEventType.received, parsedId);
case 'exchanging':
return const NfcEvent(NfcEventType.exchanging, null);
case 'error': case 'error':
return NfcEvent(NfcEventType.error, null, reason: map['reason'] as String?); return NfcEvent(NfcEventType.error, null, reason: map['reason'] as String?);
default: default:
@@ -12,7 +12,17 @@ import '../../../main.dart';
import '../../widgets/custom_notification.dart'; import '../../widgets/custom_notification.dart';
import '../../widgets/komet_avatar.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 { class NfcExchangeSheet extends StatefulWidget {
const NfcExchangeSheet({super.key}); const NfcExchangeSheet({super.key});
@@ -88,6 +98,12 @@ class _NfcExchangeSheetState extends State<NfcExchangeSheet>
} }
return; return;
} }
if (event.type == NfcEventType.exchanging) {
if (mounted && _peerId == null && _stage == _Stage.scanning) {
setState(() => _stage = _Stage.exchanging);
}
return;
}
final id = event.id; final id = event.id;
if (id == null || _peerId != null) return; if (id == null || _peerId != null) return;
_peerId = id; _peerId = id;
@@ -190,7 +206,19 @@ class _NfcExchangeSheetState extends State<NfcExchangeSheet>
], ],
), ),
const SizedBox(height: 12), 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<NfcExchangeSheet>
return _message(cs, Symbols.bluetooth_disabled, _failReason); return _message(cs, Symbols.bluetooth_disabled, _failReason);
case _Stage.scanning: case _Stage.scanning:
return _scanning(cs); return _scanning(cs);
case _Stage.exchanging:
return _exchanging(cs);
case _Stage.found: case _Stage.found:
case _Stage.adding: case _Stage.adding:
case _Stage.added: case _Stage.added:
@@ -280,6 +310,46 @@ class _NfcExchangeSheetState extends State<NfcExchangeSheet>
); );
} }
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) { Widget _foundCard(ColorScheme cs) {
final loading = _peerInfo == null && _stage == _Stage.found; final loading = _peerInfo == null && _stage == _Stage.found;
return Padding( return Padding(