diff --git a/ios/Podfile.lock b/ios/Podfile.lock index d899801..69bbf64 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -6,6 +6,8 @@ PODS: - Flutter - integration_test (0.0.1): - Flutter + - package_info_plus (0.4.5): + - Flutter - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS @@ -17,6 +19,7 @@ DEPENDENCIES: - Flutter (from `Flutter`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - integration_test (from `.symlinks/plugins/integration_test/ios`) + - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - share_plus (from `.symlinks/plugins/share_plus/ios`) @@ -29,6 +32,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_secure_storage/ios" integration_test: :path: ".symlinks/plugins/integration_test/ios" + package_info_plus: + :path: ".symlinks/plugins/package_info_plus/ios" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" share_plus: @@ -39,6 +44,7 @@ SPEC CHECKSUMS: Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 integration_test: 13825b8a9334a850581300559b8839134b124670 + package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad diff --git a/lib/coins/abstract.dart b/lib/coins/abstract.dart index 66c2477..471e217 100644 --- a/lib/coins/abstract.dart +++ b/lib/coins/abstract.dart @@ -2,9 +2,9 @@ import 'dart:core'; import 'package:cup_cake/coins/monero/coin.dart'; import 'package:cup_cake/l10n/app_localizations.dart'; +import 'package:cup_cake/utils/config.dart'; import 'package:cup_cake/view_model/barcode_scanner_view_model.dart'; import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:path/path.dart' as p; @@ -65,7 +65,7 @@ class CoinWalletInfo { Map toJson() { return { "typeIndex": type.index, - if (kDebugMode) "typeIndex__debug": type.toString(), + if (config.debug) "typeIndex__debug": type.toString(), "walletName": p.basename(walletName), }; } diff --git a/lib/coins/monero/coin.dart b/lib/coins/monero/coin.dart index 4aed9c6..bb66c42 100644 --- a/lib/coins/monero/coin.dart +++ b/lib/coins/monero/coin.dart @@ -6,7 +6,6 @@ import 'package:cup_cake/utils/config.dart'; import 'package:cup_cake/utils/filesystem.dart'; import 'package:cup_cake/views/open_wallet.dart'; import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:monero/monero.dart' as monero; import 'package:path/path.dart' as p; @@ -22,7 +21,7 @@ class Monero implements Coin { monero.isLibOk(); return true; } catch (e) { - if (kDebugMode) { + if (config.debug) { print("monero.dart: isLibOk failed: $e"); return false; } diff --git a/lib/coins/monero/wallet.dart b/lib/coins/monero/wallet.dart index 56b1e31..138acdf 100644 --- a/lib/coins/monero/wallet.dart +++ b/lib/coins/monero/wallet.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:cup_cake/coins/abstract.dart'; import 'package:cup_cake/coins/monero/coin.dart'; import 'package:cup_cake/l10n/app_localizations.dart'; +import 'package:cup_cake/utils/config.dart'; import 'package:cup_cake/utils/null_if_empty.dart'; import 'package:cup_cake/utils/secure_storage.dart'; import 'package:cup_cake/view_model/barcode_scanner_view_model.dart'; @@ -11,7 +12,6 @@ import 'package:cup_cake/view_model/urqr_view_model.dart'; import 'package:cup_cake/views/unconfirmed_transaction.dart'; import 'package:cup_cake/views/urqr.dart'; import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; import 'package:path/path.dart' as p; import 'package:monero/monero.dart' as monero; import 'package:polyseed/polyseed.dart'; @@ -70,10 +70,10 @@ class MoneroWallet implements CoinWallet { Future exportKeyImagesUR(BuildContext context) async { final allImages = monero.Wallet_exportKeyImagesUR(wptr, - max_fragment_length: 130, all: true) + max_fragment_length: config.maxFragmentLength, all: true) .split("\n"); final someImages = monero.Wallet_exportKeyImagesUR(wptr, - max_fragment_length: 130, all: false) + max_fragment_length: config.maxFragmentLength, all: false) .split("\n"); await AnimatedURPage.staticPush( context, @@ -137,8 +137,9 @@ class MoneroWallet implements CoinWallet { destMap: destMap, fee: fee, confirmCallback: (BuildContext context) async { - final signedTx = - monero.UnsignedTransaction_signUR(txptr, 130).split("\n"); + final signedTx = monero.UnsignedTransaction_signUR( + txptr, config.maxFragmentLength) + .split("\n"); var status = monero.Wallet_status(wptr); if (status != 0) { final error = monero.Wallet_errorString(wptr); @@ -281,14 +282,17 @@ class MoneroWallet implements CoinWallet { "restoreHeight": monero.Wallet_getRefreshFromBlockHeight(wptr), }), ), - if (kDebugMode) - ...List.generate(secrets.keys.length, (index) { - final key = secrets.keys.elementAt(index); - return WalletSeedDetail( - type: WalletSeedDetailType.text, - name: key, - value: secrets[key] ?? "unknown"); - }), + if (config.debug) + ...List.generate( + secrets.keys.length, + (index) { + final key = secrets.keys.elementAt(index); + return WalletSeedDetail( + type: WalletSeedDetailType.text, + name: key, + value: secrets[key] ?? "unknown"); + }, + ), ]; } } diff --git a/lib/utils/config.dart b/lib/utils/config.dart index 3e917b1..d480c2f 100644 --- a/lib/utils/config.dart +++ b/lib/utils/config.dart @@ -11,16 +11,25 @@ class CupcakeConfig { required this.lastWallet, required this.initialSetupComplete, required this.walletMigrationLevel, + required this.msForQrCode, + required this.maxFragmentLength, + required this.debug, }); CoinWalletInfo? lastWallet; bool initialSetupComplete; int walletMigrationLevel; + int msForQrCode; + int maxFragmentLength; + bool debug; factory CupcakeConfig.fromJson(Map json) { return CupcakeConfig( lastWallet: CoinWalletInfo.fromJson(json['lastWallet']), initialSetupComplete: json['initialSetupComplete'] ?? false, walletMigrationLevel: json['walletMigrationLevel'] ?? 0, + msForQrCode: json['msForQrCode'] ?? 1000 ~/ 3.5, + maxFragmentLength: json['maxFragmentLength'] ?? 130, + debug: json['debug'] ?? false, ); } @@ -29,6 +38,9 @@ class CupcakeConfig { 'lastWallet': lastWallet, 'initialSetupComplete': initialSetupComplete, 'walletMigrationLevel': walletMigrationLevel, + 'msForQrCode': msForQrCode, + 'maxFragmentLength': maxFragmentLength, + 'debug': debug, }; } diff --git a/lib/view_model/create_wallet_view_model.dart b/lib/view_model/create_wallet_view_model.dart index c33365b..9dc1e50 100644 --- a/lib/view_model/create_wallet_view_model.dart +++ b/lib/view_model/create_wallet_view_model.dart @@ -12,7 +12,6 @@ import 'package:cup_cake/view_model/new_wallet_info_view_model.dart'; import 'package:cup_cake/views/new_wallet_info.dart'; import 'package:cup_cake/gen/assets.gen.dart'; import 'package:cup_cake/views/wallet_home.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:share_plus/share_plus.dart'; @@ -416,7 +415,7 @@ class FlutterSecureStorageValueOutcome implements ValueOutcome { return; } if (!canWrite) { - if (kDebugMode) { + if (config.debug) { throw Exception( "DEBUG_ONLY: canWrite is false but we tried to flush the value"); } diff --git a/lib/view_model/settings_view_model.dart b/lib/view_model/settings_view_model.dart index 78a722d..37bf249 100644 --- a/lib/view_model/settings_view_model.dart +++ b/lib/view_model/settings_view_model.dart @@ -1,3 +1,4 @@ +import 'package:cup_cake/utils/config.dart'; import 'package:cup_cake/view_model/abstract.dart'; class SettingsViewModel extends ViewModel { @@ -5,4 +6,6 @@ class SettingsViewModel extends ViewModel { @override String get screenName => "Settings"; + + CupcakeConfig get appConfig => config; } diff --git a/lib/views/create_wallet.dart b/lib/views/create_wallet.dart index f0af609..04753cb 100644 --- a/lib/views/create_wallet.dart +++ b/lib/views/create_wallet.dart @@ -1,11 +1,11 @@ import 'package:cup_cake/gen/assets.gen.dart'; import 'package:cup_cake/utils/call_throwable.dart'; +import 'package:cup_cake/utils/config.dart'; import 'package:cup_cake/view_model/create_wallet_view_model.dart'; import 'package:cup_cake/views/abstract.dart'; import 'package:cup_cake/views/initial_setup_screen.dart'; import 'package:cup_cake/widgets/form_builder.dart'; import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; // ignore: must_be_immutable @@ -145,7 +145,7 @@ class CreateWallet extends AbstractView { bool isFormBad(List form) { for (var element in form) { if (!element.isOk) { - if (kDebugMode) { + if (config.debug) { print("${element.label} is not valid: "); } return true; diff --git a/lib/views/settings.dart b/lib/views/settings.dart new file mode 100644 index 0000000..2a25bd4 --- /dev/null +++ b/lib/views/settings.dart @@ -0,0 +1,228 @@ +import 'package:cup_cake/utils/alert.dart'; +import 'package:cup_cake/utils/config.dart'; +import 'package:cup_cake/view_model/settings_view_model.dart'; +import 'package:cup_cake/views/abstract.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +class SettingsView extends AbstractView { + SettingsView({super.key}); + + @override + SettingsViewModel get viewModel => SettingsViewModel(); + + static staticPush(BuildContext context) { + return Navigator.of(context).push( + CupertinoPageRoute( + builder: (BuildContext context) => SettingsView(), + ), + ); + } + + Future postUpdate(BuildContext context) async { + viewModel.appConfig.save(); + markNeedsBuild(context); + } + + @override + Widget? body(BuildContext context) { + return Column( + children: [ + if (config.debug) + BooleanConfigElement( + title: "Debug", + subtitleEnabled: "Debug options are enabled", + subtitleDisabled: "Debug options are disabled", + value: viewModel.appConfig.debug, + onChange: (bool value) { + viewModel.appConfig.debug = value; + postUpdate(context); + }), + if (config.debug) + BooleanConfigElement( + title: "Initial config done", + subtitleEnabled: "Initial setup has been completed", + subtitleDisabled: "Initial setup has not been completed", + value: viewModel.appConfig.initialSetupComplete, + onChange: (bool value) { + viewModel.appConfig.initialSetupComplete = value; + postUpdate(context); + }), + IntegerConfigElement( + title: "Milliseconds for qr code", + hint: + "How many milliseconds should one QR code last before switching to next one", + value: viewModel.appConfig.msForQrCode, + onChange: (int value) { + viewModel.appConfig.msForQrCode = value; + postUpdate(context); + }), + IntegerConfigElement( + title: "Max fragment density", + hint: + "How many characters of data should fit within a single QR code", + value: viewModel.appConfig.maxFragmentLength, + onChange: (int value) { + viewModel.appConfig.maxFragmentLength = value; + postUpdate(context); + }, + ), + const VersionWidget(), + ], + ); + } +} + +class VersionWidget extends StatefulWidget { + const VersionWidget({super.key}); + + @override + State createState() => _VersionWidgetState(); +} + +class _VersionWidgetState extends State { + Future showWidget(BuildContext context) async { + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + + String appName = packageInfo.appName; + String packageName = packageInfo.packageName; + String version = packageInfo.version; + String buildNumber = packageInfo.buildNumber; + showAboutDialog( + context: context, + applicationName: appName, + applicationVersion: "$version+$buildNumber"); + } + + List easterEgg = [ + "¯\\_(ツ)_/¯", // Shrug + "( ͡° ͜ʖ ͡°)", // Lenny Face + "(╯°□°)╯︵ ┻━┻", // Table Flip + "┬─┬ ノ( ゜-゜ノ)", // Table Unflip + "ಠ_ಠ", // Disapproval Look + "(ಥ﹏ಥ)", // Crying + "ʕ•ᴥ•ʔ", // Bear Hug + "( ^_^)o自自o(^_^ )", // Cheers! + "✨🌟✨", // Sparkles + // New ASCII-only additions + "+(._.)+", // Robot Face + "<(o_o<)", // Kirby Dance + "(>'-')> <('-'<)", // High Five! + "d(^_^)b", // Thumbs Up + "(* ^ ω ^)", // Cute Smiling Face + "(\\__/)", // Bunny + "(^._.^)ノ", // Cat waving + "ʕ •́؈•̀ ₎", // Angry Bear + "/ᐠ。‸。ᐟ\\", // Kitty Frustrated + "| (• ◡•)|", // Simple Smiling Face + "<('o'<)", // Happy Kirby + "(-'-)=o", // Fighting Stance + "/╲/( •̀ ω •́ )╯", // Action Pose + "ʕง•ᴥ•ʔง", // Flexing Bear + "(.❛ ᴗ ❛.)", // Smiling Face + "(╥_╥)", // Sad Crying + "(ง'̀-'́)ง", // Fight Me! + "(ノಠ益ಠ)ノ", // Rage Flip + ]; + + String? subtitle; + + Future _debugTrigger() async { + if (easterEgg.isEmpty) { + if (config.debug) return; + config.debug = true; + config.save(); + setState(() { + subtitle = "debug options enabled"; + }); + Navigator.of(context).pop(); + return; + } + easterEgg.shuffle(); + setState(() { + subtitle = easterEgg.removeAt(0); + }); + } + + @override + Widget build(BuildContext context) { + return ListTile( + title: const Text("About the app"), + subtitle: subtitle == null ? null : Text(subtitle ?? "..."), + onTap: subtitle != null ? _debugTrigger : () => showWidget(context), + onLongPress: _debugTrigger, + ); + } +} + +class IntegerConfigElement extends StatelessWidget { + IntegerConfigElement({ + super.key, + required this.title, + required this.hint, + required this.value, + required this.onChange, + }); + + final String title; + final String? hint; + final int value; + final Function(int val) onChange; + late final ctrl = TextEditingController(text: value.toString()); + @override + Widget build(BuildContext context) { + return ListTile( + title: Text(title), + onLongPress: () { + config.debug = true; + config.save(); + }, + subtitle: TextField( + controller: ctrl, + onSubmitted: (String value) { + final i = int.tryParse(value); + if (i == null) return; + onChange(i); + }, + ), + trailing: hint == null + ? null + : IconButton( + icon: const Icon(Icons.info), + onPressed: () { + showAlertWidget( + context: context, title: title, body: [Text(hint ?? "")]); + }, + ), + ); + } +} + +class BooleanConfigElement extends StatelessWidget { + const BooleanConfigElement({ + super.key, + required this.title, + required this.subtitleEnabled, + required this.subtitleDisabled, + required this.value, + required this.onChange, + }); + + final String title; + final String subtitleEnabled; + final String subtitleDisabled; + final bool value; + final Function(bool val) onChange; + @override + Widget build(BuildContext context) { + return CheckboxListTile( + title: Text(title), + subtitle: Text(value ? subtitleEnabled : subtitleDisabled), + value: value, + onChanged: (bool? value) { + onChange(value == true); + }, + ); + } +} diff --git a/lib/views/urqr.dart b/lib/views/urqr.dart index 20b3c10..52d5e0e 100644 --- a/lib/views/urqr.dart +++ b/lib/views/urqr.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:cup_cake/utils/config.dart'; import 'package:cup_cake/view_model/urqr_view_model.dart'; import 'package:cup_cake/views/abstract.dart'; import 'package:flutter/cupertino.dart'; @@ -72,8 +73,6 @@ class URQR extends StatefulWidget { _URQRState createState() => _URQRState(); } -const urFrameTime = 1000 ~/ 5; - class _URQRState extends State { Timer? t; int frame = 0; @@ -81,7 +80,7 @@ class _URQRState extends State { void initState() { super.initState(); setState(() { - t = Timer.periodic(const Duration(milliseconds: urFrameTime), (timer) { + t = Timer.periodic(Duration(milliseconds: config.msForQrCode), (timer) { _nextFrame(); }); }); diff --git a/lib/views/widgets/drawer_element.dart b/lib/views/widgets/drawer_element.dart index 7d0eb80..2a1dc6a 100644 --- a/lib/views/widgets/drawer_element.dart +++ b/lib/views/widgets/drawer_element.dart @@ -4,6 +4,7 @@ import 'package:cup_cake/l10n/app_localizations.dart'; import 'package:cup_cake/utils/call_throwable.dart'; import 'package:cup_cake/views/home_screen.dart'; import 'package:cup_cake/views/security_backup.dart'; +import 'package:cup_cake/views/settings.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:cup_cake/gen/assets.gen.dart'; @@ -23,7 +24,6 @@ class DrawerElement extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - //padding: EdgeInsets.only(top: 5, left: 15, bottom: 5), margin: const EdgeInsets.only(top: 5, left: 20, bottom: 5), child: TextButton( style: ButtonStyle( @@ -32,7 +32,7 @@ class DrawerElement extends StatelessWidget { const RoundedRectangleBorder( side: BorderSide( //color: Colors.black12, - width: 100), + width: 1), borderRadius: BorderRadius.only( topLeft: Radius.circular(20), bottomLeft: Radius.circular(20), @@ -70,11 +70,11 @@ class DrawerElements extends StatelessWidget { final CoinWallet coinWallet; Future _walletList(BuildContext context) async { - HomeScreen.staticPush(context, openLastWallet: false); + await HomeScreen.staticPush(context, openLastWallet: false); } Future _securityBackup(BuildContext context) async { - SecurityBackup.staticPush(context, coinWallet); + await SecurityBackup.staticPush(context, coinWallet); } Future _exportKeyImages(BuildContext context) async { @@ -85,7 +85,7 @@ class DrawerElements extends StatelessWidget { } Future _otherSettings(BuildContext context) async { - throw UnimplementedError("Other settings are not implemented"); + await SettingsView.staticPush(context); } @override diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index d385c23..1609ce9 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,12 +7,14 @@ import Foundation import fast_scanner import flutter_secure_storage_macos +import package_info_plus import path_provider_foundation import share_plus func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 045988d..ceae8ff 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -590,6 +590,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: a75164ade98cb7d24cfd0a13c6408927c6b217fa60dee5a7ff5c116a58f28918 + url: "https://pub.dev" + source: hosted + version: "8.0.2" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: ac1f4a4847f1ade8e6a87d1f39f5d7c67490738642e2542f559ec38c37489a66 + url: "https://pub.dev" + source: hosted + version: "3.0.1" path: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 224207c..7d518ee 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -59,6 +59,7 @@ dependencies: polyseed: ^0.0.6 flutter_secure_storage: ^9.2.2 crypto: ^3.0.3 + package_info_plus: ^8.0.2 dev_dependencies: flutter_test: