haveno-app/lib/views/screens/link_to_desktop_screen.dart
2024-12-08 06:38:57 +00:00

381 lines
11 KiB
Dart

// Haveno App extends the features of Haveno, supporting mobile devices and more.
// Copyright (C) 2024 Kewbit (https://kewbit.org)
// Source Code: https://git.haveno.com/haveno/haveno-app.git
//
// Author: Kewbit
// Website: https://kewbit.org
// Contact Email: me@kewbit.org
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:haveno_app/services/mobile_manager_service.dart';
import 'package:haveno_app/services/secure_storage_service.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'establish_connection_screen.dart';
class LinkToDesktopScreen extends StatefulWidget {
const LinkToDesktopScreen({super.key});
@override
State<LinkToDesktopScreen> createState() => _LinkToDesktopScreenState();
}
class _LinkToDesktopScreenState extends State<LinkToDesktopScreen> {
final MobileScannerController controller = MobileScannerController();
DateTime? _lastScanTime; // Track the last scan time
final TextEditingController pasteController = TextEditingController();
bool isProcessing = false;
SecureStorageService secureStorageService = SecureStorageService();
@override
void initState() {
super.initState();
pasteController.addListener(_handlePaste);
}
@override
void dispose() {
controller.dispose();
pasteController.dispose();
super.dispose();
}
/// 1. Handle barcode scanning separately
void _handleBarcodeScan(String barcode) {
if (isProcessing) return; // Prevent multiple simultaneous scans
isProcessing = true;
final now = DateTime.now();
if (_lastScanTime != null && now.difference(_lastScanTime!) < const Duration(seconds: 1)) {
// Ignore barcode scans if less than 1 second has passed
isProcessing = false;
return;
}
_lastScanTime = now; // Update the last scan time
try {
_processUri(barcode); // Process the barcode URI
} catch (e) {
print('Error handling barcode: $e');
} finally {
isProcessing = false;
}
}
/// 2. Handle linkage key pasting separately
void _handlePaste() {
Timer(const Duration(milliseconds: 500), () {
final text = pasteController.text.trim(); // Trim whitespace
if (text.isEmpty || isProcessing) return;
isProcessing = true;
try {
final decoded = utf8.decode(base64.decode(text)).trim();
_processUri(decoded); // Process the pasted linkage key
} catch (e) {
_showInvalidUriAlert(); // Show error if URI is invalid
} finally {
isProcessing = false;
}
});
}
/// Process the URI from either barcode or pasted linkage key
void _processUri(String uriString) async {
try {
final Uri onionUri = Uri.parse(uriString);
print("Processed URI: $onionUri");
final mobileManagerService = MobileManagerService();
mobileManagerService.setHavenoDaemonNodeConfig(onionUri).then((daemonConfig) {
if (daemonConfig != null) {
print("Valid daemon config received");
secureStorageService.writeOnboardingStatus(true).then((_) {
if (mounted) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => EstablishConnectionScreen()),
);
}
});
}
}).catchError((e) {
print('Error setting daemon config: $e');
});
} catch (e) {
print('Error processing URI: $e');
_showInvalidUriAlert();
}
}
/// Show alert dialog for invalid linkage key
void _showInvalidUriAlert() {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Invalid Linkage Key'),
content: const Text('The pasted key is not a valid URI.'),
actions: <Widget>[
TextButton(
child: const Text('OK'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}
/// Build barcode overlay for scanner
Widget _buildBarcodeOverlay() {
return ValueListenableBuilder(
valueListenable: controller,
builder: (context, value, child) {
if (!value.isInitialized || !value.isRunning || value.error != null) {
return const SizedBox();
}
return StreamBuilder<BarcodeCapture>(
stream: controller.barcodes,
builder: (context, snapshot) {
final BarcodeCapture? barcodeCapture = snapshot.data;
if (barcodeCapture == null || barcodeCapture.barcodes.isEmpty) {
return const SizedBox();
}
final scannedBarcode = barcodeCapture.barcodes.first;
if (scannedBarcode.rawValue != null) {
_handleBarcodeScan(scannedBarcode.rawValue!);
}
if (scannedBarcode.corners.isEmpty ||
value.size.isEmpty ||
barcodeCapture.size.isEmpty) {
return const SizedBox();
}
return CustomPaint(
painter: BarcodeOverlay(
barcodeCorners: scannedBarcode.corners,
barcodeSize: barcodeCapture.size,
boxFit: BoxFit.contain,
cameraPreviewSize: value.size,
),
);
},
);
},
);
}
Widget _buildScanWindow(Rect scanWindowRect) {
return ValueListenableBuilder(
valueListenable: controller,
builder: (context, value, child) {
if (!value.isInitialized ||
!value.isRunning ||
value.error != null ||
value.size.isEmpty) {
return const SizedBox();
}
return CustomPaint(
painter: ScannerOverlay(scanWindowRect),
);
},
);
}
@override
Widget build(BuildContext context) {
final scanWindow = Rect.fromCenter(
center: MediaQuery.sizeOf(context).center(Offset.zero),
width: 200,
height: 200,
);
return Scaffold(
appBar: AppBar(title: const Text('Link to Desktop')),
backgroundColor: Colors.black,
body: Stack(
fit: StackFit.expand,
children: [
MobileScanner(
fit: BoxFit.contain,
scanWindow: scanWindow,
controller: controller,
),
_buildBarcodeOverlay(),
_buildScanWindow(scanWindow),
Positioned(
bottom: 20,
left: 20,
right: 20,
child: Column(
children: [
const Text(
'Alternatively, paste your linkage key below:',
style: TextStyle(color: Colors.white),
),
const SizedBox(height: 10),
Row(
children: [
Expanded(
child: TextField(
controller: pasteController,
readOnly: true,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Linkage Key',
labelStyle: const TextStyle(color: Colors.white),
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.white),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.blue),
),
),
),
),
IconButton(
icon: const Icon(Icons.paste, color: Colors.white),
onPressed: () async {
ClipboardData? data = await Clipboard.getData('text/plain');
if (data != null) {
pasteController.text = data.text!;
}
},
),
],
),
],
),
),
],
),
);
}
}
class ScannerOverlay extends CustomPainter {
ScannerOverlay(this.scanWindow);
final Rect scanWindow;
@override
void paint(Canvas canvas, Size size) {
final backgroundPath = Path()..addRect(Rect.largest);
final cutoutPath = Path()..addRect(scanWindow);
final backgroundPaint = Paint()
..color = Colors.black.withOpacity(0.5)
..style = PaintingStyle.fill
..blendMode = BlendMode.dstOut;
final backgroundWithCutout = Path.combine(
PathOperation.difference,
backgroundPath,
cutoutPath,
);
canvas.drawPath(backgroundWithCutout, backgroundPaint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}
class BarcodeOverlay extends CustomPainter {
BarcodeOverlay({
required this.barcodeCorners,
required this.barcodeSize,
required this.boxFit,
required this.cameraPreviewSize,
});
final List<Offset> barcodeCorners;
final Size barcodeSize;
final BoxFit boxFit;
final Size cameraPreviewSize;
@override
void paint(Canvas canvas, Size size) {
if (barcodeCorners.isEmpty ||
barcodeSize.isEmpty ||
cameraPreviewSize.isEmpty) {
return;
}
final adjustedSize = applyBoxFit(boxFit, cameraPreviewSize, size);
double verticalPadding = size.height - adjustedSize.destination.height;
double horizontalPadding = size.width - adjustedSize.destination.width;
if (verticalPadding > 0) {
verticalPadding = verticalPadding / 2;
} else {
verticalPadding = 0;
}
if (horizontalPadding > 0) {
horizontalPadding = horizontalPadding / 2;
} else {
horizontalPadding = 0;
}
final double ratioWidth;
final double ratioHeight;
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.iOS) {
ratioWidth = barcodeSize.width / adjustedSize.destination.width;
ratioHeight = barcodeSize.height / adjustedSize.destination.height;
} else {
ratioWidth = cameraPreviewSize.width / adjustedSize.destination.width;
ratioHeight = cameraPreviewSize.height / adjustedSize.destination.height;
}
final List<Offset> adjustedOffset = [
for (final offset in barcodeCorners)
Offset(
offset.dx / ratioWidth + horizontalPadding,
offset.dy / ratioHeight + verticalPadding,
),
];
final cutoutPath = Path()..addPolygon(adjustedOffset, true);
final backgroundPaint = Paint()
..color = Colors.red.withOpacity(0.3)
..style = PaintingStyle.fill
..blendMode = BlendMode.dstOut;
canvas.drawPath(cutoutPath, backgroundPaint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}