CakeWallet/lib/entities/qr_scanner.dart
tuxsudo df88914628
New themes (#2239)
* Add theme base v2

* Initial new theme base files

* Typos

* Fixes

* Update theme files

* feat: Migrate to Material 3 Theming

Foundation, Theme Data Refactor, and First Extension Cleanup Batch.

This commit completes the first major phase of migrating to Material 3 theming by setting up the foundations for material 3 integration and begins the initial migration, removing custom theme extensions, updating theme data, and refactoring all relevant widget and page theming to use Material 3’s built-in color and typography tokens.

These changes:
- Lays the groundwork for Material 3 theming by restructuring the app’s theme configuration to use Material 3’s ColorScheme and TextTheme as the primary sources of color and typography throughout the app.
- Refactors the core theme config files by removing legacy custom color roles ensuring all color definitions now map directly to Material 3’s role.
- Begins the first batch migration of custom theme extensions (InfoTheme, PlaceholderTheme, KeyboardTheme, PinCodeTheme) and updates all affected widgets and pages to use Material 3 color and typography tokens instead of the custom properties.
- Cleans up the codebase by deleting the files of the initial set of migrated extensions and eliminating all related imports and usages.

* feat: Migrate to Material 3 Theming.

This change:
- Updates the themes README.md file to reflect the current structure and give more information based on the first major phase that was completed.

* feat: Migrate to Material 3 Theming

Deleting previous theme extensions

* feat: Migrate to Material 3 Theming

Another batch of migrations from existing extensions

* feat: Migration to Material 3 Theming

Third Migration batch for theme extensions

* fwat: Migration to Material 3 Theming

Final Migration batch for previous theme extensions

* Update onboarding hero

* Update button radius

* Add surfaceContainer to light theme

* feat(themes): Migrate to Material 3 Theming

This change:
- Adds new set of hero images
- Modifies the core structure for the themes
- Add missing color tokens to the theme classes
- Adds a CustomThemeColor class for custom color tokens
- Modifies the themelist to have a fall back for previous theme implementation
- Adds localization for some texts
- Modifies the flow for loading the theme on app start
- Add a WidgetsBindingObserver that listens for changes in the device theme and updates the app theme when there is a change
- Registers the themeStore as a Singleton for codebase wide use

* feat(themes): Migrate to Material 3 theming

This change:
- Migrates UI flows across the app to the new themes
- Confirms styling and typography of components across the app uses the new themes
- Remove instances of Palette use
- Switch TextStyles across the app to use theme text styles

* feat(themes): Migrate to Material 3 Theming.

This change:
- Adjusts bottomsheets styling and removes duplicate close button
- Removes more themedata extensions from the previous implementation

* - Remove outlines from cards and dock
- Update menu colors
- Update padding/divider size for cards

* - Update PIN screen
- Fix navigation dock shadow
- Update wallet screen colors

* Update border radius --skip-ci

* feat(themes): Migrate to Material 3 Theming.

This change:
- Adds gradient backgrounds to the dashbaord and balance cards.
- Migrates the input fields across the app to BaseTextFormFields.
- Removes dependence of input fields on individual styling, focusing instead on using theme defined InputDecoration styling with adjustments on individual components where needed.
- Applies new theme styling to BaseTextFormField, AddressTextField and CurrencyAmountTextField.

* - Switch some hero images to PNG
- Fix nagivation_dock shadow
- Minor fixes

* feat: Add fallback to previous underline styling in central widgets

This change:
- Adds a fallback to CurrencyAmountTextField, AddressTextField, and BaseTextFormField, allowing them use the previous theme styling.
- Adds localization for new texts

* feat(themes): Update warning box colors for dark and light themes

* feat(themes): Relaod themes when user restores from backup, ensuring the user previous theme preference is used.

* feat(themes): Handle themes logic during restore from backup

This change:
- Refactors theme loading logic to handle backup restore edgecase
- Refreshes the theme based on the user saved preference during restore from backup flow

* Fix card gradients and spacing

* Fix even more radiuses
Test new icons for navigation_dock.dart

* Update onboarding flow backgrounds
Fix swap icon clipping
Fix some text colors
Add more hero images

* Fix incorrect color for light theme

* Fix more hero images and cleanup

* Update text field icons
Fix info box CTA colors
Fix sync indicator colors

* Update toggle colors
Update dark theme colors (minor)
Update crypto_balance_widget.dart icon

* Update page transitions in router.dart
Fix some colors

* feat(themes): Display label by default for filled textfields

* feat(themes): Refactor theme handling across various components

This change:
- Fixes issue with themeMode resetting to system mode when app is restarted causing a UI glitch
- Updates theme checks from `currentTheme.type == ThemeType.dark` to `currentTheme.isDark` for consistency
- Adjusts UI components to use the theme directly from the themeStore

* feat(themes): Add animating tagline to the create pin welcome screen

* Revert text fields label temporarily, fix a couple colors, and cleanup some images

---------

Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>
Co-authored-by: Blazebrain <davidadegoke16@gmail.com>
2025-05-25 23:11:45 +03:00

374 lines
10 KiB
Dart

import 'dart:math';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:fast_scanner/fast_scanner.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
var isQrScannerShown = false;
Future<String?> presentQRScanner(BuildContext context) async {
isQrScannerShown = true;
try {
final result = await Navigator.of(context).push<String>(
MaterialPageRoute(
builder: (context) {
return BarcodeScannerSimple();
},
),
);
isQrScannerShown = false;
return result;
} catch (e) {
isQrScannerShown = false;
rethrow;
}
}
// https://github.com/MrCyjaneK/fast_scanner/blob/master/example/lib/barcode_scanner_simple.dart
class BarcodeScannerSimple extends StatefulWidget {
const BarcodeScannerSimple({super.key});
@override
State<BarcodeScannerSimple> createState() => _BarcodeScannerSimpleState();
}
class _BarcodeScannerSimpleState extends State<BarcodeScannerSimple> {
Barcode? _barcode;
bool popped = false;
List<String> urCodes = [];
late var ur = URQRToURQRData(urCodes);
void _handleBarcode(BarcodeCapture barcodes) {
try {
_handleBarcodeInternal(barcodes);
} catch (e) {
showPopUp<void>(
context: context,
builder: (context) {
return AlertWithOneAction(
alertTitle: S.of(context).error,
alertContent: S.of(context).error_dialog_content,
buttonText: 'ok',
buttonAction: () {
Navigator.of(context).pop();
},
);
},
);
printV(e);
}
}
void _handleBarcodeInternal(BarcodeCapture barcodes) {
for (final barcode in barcodes.barcodes) {
// don't handle unknown QR codes
if (barcode.rawValue?.trim().isEmpty ?? false == false) continue;
if (barcode.rawValue!.startsWith("ur:")) {
if (urCodes.contains(barcode.rawValue)) continue;
setState(() {
urCodes.add(barcode.rawValue!);
ur = URQRToURQRData(urCodes);
});
if (ur.progress == 1) {
setState(() {
popped = true;
});
SchedulerBinding.instance.addPostFrameCallback((_) {
Navigator.of(context).pop(ur.inputs.join("\n"));
});
}
;
}
}
if (urCodes.isNotEmpty) return;
if (mounted) {
setState(() {
_barcode = barcodes.barcodes.firstOrNull;
});
if (_barcode != null && popped != true) {
setState(() {
popped = true;
});
Navigator.of(context).pop(_barcode!.rawValue ?? _barcode!.rawBytes);
}
}
}
final MobileScannerController ctrl = MobileScannerController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Scan'),
actions: [
SwitchCameraButton(controller: ctrl),
ToggleFlashlightButton(controller: ctrl),
],
),
backgroundColor: Theme.of(context).colorScheme.surface,
body: Stack(
children: [
MobileScanner(
onDetect: _handleBarcode,
controller: ctrl,
),
if (ur.inputs.length != 0)
Center(
child: Text(
"${ur.inputs.length}/${ur.count}",
style: Theme.of(context)
.textTheme
.displayLarge
?.copyWith(color: Theme.of(context).colorScheme.onSurface),
),
),
SizedBox(
child: Center(
child: SizedBox(
width: 250,
height: 250,
child: CustomPaint(
painter: ProgressPainter(
urQrProgress: URQrProgress(
expectedPartCount: ur.count - 1,
processedPartsCount: ur.inputs.length,
receivedPartIndexes: _urParts(),
percentage: ur.progress,
),
),
),
),
),
),
],
),
);
}
List<int> _urParts() {
List<int> l = [];
for (var inp in ur.inputs) {
try {
l.add(int.parse(inp.split("/")[1].split("-")[0]));
} catch (e) {}
}
return l;
}
}
class ToggleFlashlightButton extends StatelessWidget {
const ToggleFlashlightButton({required this.controller, super.key});
final MobileScannerController controller;
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: controller,
builder: (context, state, child) {
if (!state.isInitialized || !state.isRunning) {
return const SizedBox.shrink();
}
switch (state.torchState) {
case TorchState.auto:
return IconButton(
iconSize: 32.0,
icon: const Icon(Icons.flash_auto),
onPressed: () async {
await controller.toggleTorch();
},
);
case TorchState.off:
return IconButton(
iconSize: 32.0,
icon: const Icon(Icons.flash_off),
onPressed: () async {
await controller.toggleTorch();
},
);
case TorchState.on:
return IconButton(
iconSize: 32.0,
icon: const Icon(Icons.flash_on),
onPressed: () async {
await controller.toggleTorch();
},
);
case TorchState.unavailable:
return Icon(
Icons.no_flash,
color: Theme.of(context).colorScheme.onSurfaceVariant,
);
}
},
);
}
}
class SwitchCameraButton extends StatelessWidget {
const SwitchCameraButton({required this.controller, super.key});
final MobileScannerController controller;
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: controller,
builder: (context, state, child) {
if (!state.isInitialized || !state.isRunning) {
return const SizedBox.shrink();
}
final int? availableCameras = state.availableCameras;
if (availableCameras != null && availableCameras < 2) {
return const SizedBox.shrink();
}
final Widget icon;
switch (state.cameraDirection) {
case CameraFacing.front:
icon = const Icon(Icons.camera_front);
case CameraFacing.back:
icon = const Icon(Icons.camera_rear);
}
return IconButton(
iconSize: 32.0,
icon: icon,
onPressed: () async {
await controller.switchCamera();
},
);
},
);
}
}
class URQRData {
URQRData(
{required this.tag,
required this.str,
required this.progress,
required this.count,
required this.error,
required this.inputs});
final String tag;
final String str;
final double progress;
final int count;
final String error;
final List<String> inputs;
Map<String, dynamic> toJson() {
return {
"tag": tag,
"str": str,
"progress": progress,
"count": count,
"error": error,
"inputs": inputs,
};
}
}
URQRData URQRToURQRData(List<String> urqr_) {
final urqr = urqr_.toSet().toList();
urqr.sort((s1, s2) {
final s1s = s1.split("/");
final s1frameStr = s1s[1].split("-");
final s1curFrame = int.parse(s1frameStr[0]);
final s2s = s2.split("/");
final s2frameStr = s2s[1].split("-");
final s2curFrame = int.parse(s2frameStr[0]);
return s1curFrame - s2curFrame;
});
String tag = '';
int count = 0;
String bw = '';
for (var elm in urqr) {
final s = elm.substring(elm.indexOf(":") + 1); // strip down ur: prefix
final s2 = s.split("/");
tag = s2[0];
final frameStr = s2[1].split("-");
// final curFrame = int.parse(frameStr[0]);
count = int.parse(frameStr[1]);
final byteWords = s2[2];
bw += byteWords;
}
String? error;
return URQRData(
tag: tag,
str: bw,
progress: count == 0 ? 0 : (urqr.length / count),
count: count,
error: error ?? "",
inputs: urqr,
);
}
class ProgressPainter extends CustomPainter {
final URQrProgress urQrProgress;
ProgressPainter({required this.urQrProgress});
@override
void paint(Canvas canvas, Size size) {
final c = Offset(size.width / 2.0, size.height / 2.0);
final radius = size.width * 0.9;
final rect = Rect.fromCenter(center: c, width: radius, height: radius);
const fullAngle = 360.0;
var startAngle = 0.0;
for (int i = 0; i < urQrProgress.expectedPartCount.toInt(); i++) {
var sweepAngle = (1 / urQrProgress.expectedPartCount) * fullAngle * pi / 180.0;
drawSector(
canvas, urQrProgress.receivedPartIndexes.contains(i), rect, startAngle, sweepAngle);
startAngle += sweepAngle;
}
}
void drawSector(Canvas canvas, bool isActive, Rect rect, double startAngle, double sweepAngle) {
final paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 8
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round
..color = isActive ? const Color(0xffff6600) : Colors.white70;
canvas.drawArc(rect, startAngle, sweepAngle, false, paint);
}
@override
bool shouldRepaint(covariant ProgressPainter oldDelegate) {
return urQrProgress != oldDelegate.urQrProgress;
}
}
class URQrProgress {
int expectedPartCount;
int processedPartsCount;
List<int> receivedPartIndexes;
double percentage;
URQrProgress({
required this.expectedPartCount,
required this.processedPartsCount,
required this.receivedPartIndexes,
required this.percentage,
});
bool equals(URQrProgress? progress) {
if (progress == null) {
return false;
}
return processedPartsCount == progress.processedPartsCount;
}
}