diff --git a/lib/pages/monkey/monkey_view.dart b/lib/pages/monkey/monkey_view.dart index f8f9d3f4d..ab39021cb 100644 --- a/lib/pages/monkey/monkey_view.dart +++ b/lib/pages/monkey/monkey_view.dart @@ -4,8 +4,8 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; -import 'package:permission_handler/permission_handler.dart'; import '../../notifications/show_flush_bar.dart'; import '../../providers/global/wallets_provider.dart'; @@ -14,6 +14,7 @@ import '../../themes/coin_icon_provider.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/assets.dart'; import '../../utilities/show_loading.dart'; +import '../../utilities/stack_file_system.dart'; import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; @@ -30,10 +31,7 @@ import '../../widgets/desktop/secondary_button.dart'; import '../../widgets/stack_dialog.dart'; class MonkeyView extends ConsumerStatefulWidget { - const MonkeyView({ - super.key, - required this.walletId, - }); + const MonkeyView({super.key, required this.walletId}); static const String routeName = "/monkey"; static const double navBarHeight = 65.0; @@ -56,7 +54,7 @@ class _MonkeyViewState extends ConsumerState { Future _getDocsDir() async { try { if (Platform.isAndroid) { - return Directory("/storage/emulated/0/Documents"); + return await StackFileSystem.wtfAndroidDocumentsPath(); } return await getApplicationDocumentsDirectory(); @@ -72,21 +70,17 @@ class _MonkeyViewState extends ConsumerState { bool isPNG = false, bool overwrite = false, }) async { - if (Platform.isAndroid) { - await Permission.storage.request(); - } - final dir = await _getDocsDir(); if (dir == null) { throw Exception("Failed to get documents directory to save monKey image"); } - final address = await ref - .read(pWallets) - .getWallet(walletId) - .getCurrentReceivingAddress(); - final docPath = dir.path; - String filePath = "$docPath/monkey_$address"; + final address = + await ref + .read(pWallets) + .getWallet(walletId) + .getCurrentReceivingAddress(); + String filePath = path.join(dir.path, "monkey_$address"); filePath += isPNG ? ".png" : ".svg"; @@ -119,273 +113,261 @@ class _MonkeyViewState extends ConsumerState { return Background( child: ConditionalParent( condition: isDesktop, - builder: (child) => DesktopScaffold( - appBar: DesktopAppBar( - background: Theme.of(context).extension()!.popupBG, - leading: Expanded( - child: Row( - children: [ - const SizedBox( - width: 32, - ), - AppBarIconButton( - size: 32, - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, - shadows: const [], - icon: SvgPicture.asset( - Assets.svg.arrowLeft, - width: 18, - height: 18, - color: Theme.of(context) - .extension()! - .topNavIconPrimary, - ), - onPressed: Navigator.of(context).pop, - ), - const SizedBox( - width: 15, - ), - SvgPicture.asset( - Assets.svg.monkey, - width: 32, - height: 32, - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - const SizedBox( - width: 12, + builder: + (child) => DesktopScaffold( + appBar: DesktopAppBar( + background: Theme.of(context).extension()!.popupBG, + leading: Expanded( + child: Row( + children: [ + const SizedBox(width: 32), + AppBarIconButton( + size: 32, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, + shadows: const [], + icon: SvgPicture.asset( + Assets.svg.arrowLeft, + width: 18, + height: 18, + color: + Theme.of( + context, + ).extension()!.topNavIconPrimary, + ), + onPressed: Navigator.of(context).pop, + ), + const SizedBox(width: 15), + SvgPicture.asset( + Assets.svg.monkey, + width: 32, + height: 32, + color: + Theme.of( + context, + ).extension()!.textSubtitle1, + ), + const SizedBox(width: 12), + Text("MonKey", style: STextStyles.desktopH3(context)), + ], ), - Text( - "MonKey", - style: STextStyles.desktopH3(context), + ), + trailing: RawMaterialButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(1000), ), - ], - ), - ), - trailing: RawMaterialButton( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(1000), - ), - onPressed: () { - showDialog( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return DesktopDialog( - maxHeight: double.infinity, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + onPressed: () { + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return DesktopDialog( + maxHeight: double.infinity, + child: Column( children: [ - Padding( - padding: const EdgeInsets.only(left: 32), - child: Text( - "About MonKeys", - style: STextStyles.desktopH3(context), - ), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "About MonKeys", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], ), - const DesktopDialogCloseButton(), - ], - ), - Text( - "A MonKey is a visual representation of your Banano address.", - style: - STextStyles.desktopTextMedium(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark3, - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Padding( - padding: const EdgeInsets.all( - 32, - ), - child: PrimaryButton( - width: 272.5, - label: "OK", - onPressed: () { - Navigator.of(context).pop(); - }, + Text( + "A MonKey is a visual representation of your Banano address.", + style: STextStyles.desktopTextMedium( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark3, ), ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Padding( + padding: const EdgeInsets.all(32), + child: PrimaryButton( + width: 272.5, + label: "OK", + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + ], + ), ], ), - ], - ), + ); + }, ); }, - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 19, - horizontal: 32, - ), - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.circleQuestion, - width: 20, - height: 20, - color: Theme.of(context) - .extension()! - .customTextButtonEnabledText, - ), - const SizedBox( - width: 8, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 19, + horizontal: 32, ), - Text( - "What is MonKey?", - style: - STextStyles.desktopMenuItemSelected(context).copyWith( - color: Theme.of(context) - .extension()! - .customTextButtonEnabledText, - ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.circleQuestion, + width: 20, + height: 20, + color: + Theme.of(context) + .extension()! + .customTextButtonEnabledText, + ), + const SizedBox(width: 8), + Text( + "What is MonKey?", + style: STextStyles.desktopMenuItemSelected( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .customTextButtonEnabledText, + ), + ), + ], ), - ], + ), ), + useSpacers: false, + isCompactHeight: true, ), + body: child, ), - useSpacers: false, - isCompactHeight: true, - ), - body: child, - ), child: ConditionalParent( condition: !isDesktop, - builder: (child) => Scaffold( - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "MonKey", - style: STextStyles.navBarTitle(context), - ), - actions: [ - AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - icon: SvgPicture.asset( - Assets.svg.circleQuestion, - ), + builder: + (child) => Scaffold( + appBar: AppBar( + leading: AppBarBackButton( onPressed: () { - showDialog( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return const StackOkDialog( - title: "About MonKeys", - message: - "A MonKey is a visual representation of your Banano address.", - ); - }, - ); + Navigator.of(context).pop(); }, ), + title: Text( + "MonKey", + style: STextStyles.navBarTitle(context), + ), + actions: [ + AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + icon: SvgPicture.asset(Assets.svg.circleQuestion), + onPressed: () { + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return const StackOkDialog( + title: "About MonKeys", + message: + "A MonKey is a visual representation of your Banano address.", + ); + }, + ); + }, + ), + ), + ], ), - ], - ), - body: child, - ), + body: child, + ), child: ConditionalParent( condition: isDesktop, - builder: (child) => SizedBox( - width: 318, - child: child, - ), + builder: (child) => SizedBox(width: 318, child: child), child: ConditionalParent( condition: imageBytes != null, - builder: (_) => Column( - children: [ - isDesktop - ? const SizedBox( - height: 50, - ) - : const Spacer( - flex: 1, - ), - if (imageBytes != null) - SizedBox( - width: 300, - height: 300, - child: SvgPicture.memory(Uint8List.fromList(imageBytes!)), - ), - isDesktop - ? const SizedBox( - height: 50, - ) - : const Spacer( - flex: 1, + builder: + (_) => Column( + children: [ + isDesktop + ? const SizedBox(height: 50) + : const Spacer(flex: 1), + if (imageBytes != null) + SizedBox( + width: 300, + height: 300, + child: SvgPicture.memory( + Uint8List.fromList(imageBytes!), + ), ), - Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - SecondaryButton( - label: "Save as SVG", - onPressed: () async { - bool didError = false; - await showLoading( - whileFuture: Future.wait([ - _saveMonKeyToFile( - bytes: Uint8List.fromList( - (wallet as BananoWallet) - .getMonkeyImageBytes()!, - ), - ), - Future.delayed( - const Duration(seconds: 2), - ), - ]), - context: context, - rootNavigator: Util.isDesktop, - message: "Saving MonKey svg", - onException: (e) { - didError = true; - String msg = e.toString(); - while (msg.isNotEmpty && - msg.startsWith("Exception:")) { - msg = msg.substring(10).trim(); - } - showFloatingFlushBar( - type: FlushBarType.warning, - message: msg, + isDesktop + ? const SizedBox(height: 50) + : const Spacer(flex: 1), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + SecondaryButton( + label: "Save as SVG", + onPressed: () async { + bool didError = false; + await showLoading( + whileFuture: Future.wait([ + _saveMonKeyToFile( + bytes: Uint8List.fromList( + (wallet as BananoWallet) + .getMonkeyImageBytes()!, + ), + ), + Future.delayed( + const Duration(seconds: 2), + ), + ]), context: context, + rootNavigator: Util.isDesktop, + message: "Saving MonKey svg", + onException: (e) { + didError = true; + String msg = e.toString(); + while (msg.isNotEmpty && + msg.startsWith("Exception:")) { + msg = msg.substring(10).trim(); + } + showFloatingFlushBar( + type: FlushBarType.warning, + message: msg, + context: context, + ); + }, ); - }, - ); - if (!didError && mounted) { - await showFloatingFlushBar( - type: FlushBarType.success, - message: - "SVG MonKey image saved to $_monkeyPath", - context: context, - ); - } - }, - ), - const SizedBox(height: 12), - SecondaryButton( - label: "Download as PNG", - onPressed: () async { - bool didError = false; - await showLoading( - whileFuture: Future.wait([ - wallet.getCurrentReceivingAddress().then( + if (!didError && mounted) { + await showFloatingFlushBar( + type: FlushBarType.success, + message: + "SVG MonKey image saved to $_monkeyPath", + context: context, + ); + } + }, + ), + const SizedBox(height: 12), + SecondaryButton( + label: "Download as PNG", + onPressed: () async { + bool didError = false; + await showLoading( + whileFuture: Future.wait([ + wallet.getCurrentReceivingAddress().then( (address) async => await ref .read(pMonKeyService) .fetchMonKey( @@ -395,80 +377,73 @@ class _MonkeyViewState extends ConsumerState { .then( (monKeyBytes) async => await _saveMonKeyToFile( - bytes: monKeyBytes, - isPNG: true, - ), + bytes: monKeyBytes, + isPNG: true, + ), ), ), - Future.delayed( - const Duration(seconds: 2), - ), - ]), - context: context, - rootNavigator: Util.isDesktop, - message: "Downloading MonKey png", - onException: (e) { - didError = true; - String msg = e.toString(); - while (msg.isNotEmpty && - msg.startsWith("Exception:")) { - msg = msg.substring(10).trim(); - } - showFloatingFlushBar( - type: FlushBarType.warning, - message: msg, + Future.delayed( + const Duration(seconds: 2), + ), + ]), context: context, + rootNavigator: Util.isDesktop, + message: "Downloading MonKey png", + onException: (e) { + didError = true; + String msg = e.toString(); + while (msg.isNotEmpty && + msg.startsWith("Exception:")) { + msg = msg.substring(10).trim(); + } + showFloatingFlushBar( + type: FlushBarType.warning, + message: msg, + context: context, + ); + }, ); - }, - ); - if (!didError && mounted) { - await showFloatingFlushBar( - type: FlushBarType.success, - message: - "PNG MonKey image saved to $_monkeyPath", - context: context, - ); - } - }, + if (!didError && mounted) { + await showFloatingFlushBar( + type: FlushBarType.success, + message: + "PNG MonKey image saved to $_monkeyPath", + context: context, + ); + } + }, + ), + ], ), - ], - ), + ), + // child, + ], ), - // child, - ], - ), child: Column( children: [ isDesktop - ? const SizedBox( - height: 100, - ) - : const Spacer( - flex: 4, - ), + ? const SizedBox(height: 100) + : const Spacer(flex: 4), Center( child: Column( children: [ Opacity( opacity: 0.2, child: SvgPicture.file( - File( - ref.watch(coinIconProvider(coin)), - ), + File(ref.watch(coinIconProvider(coin))), width: 200, height: 200, ), ), - const SizedBox( - height: 70, - ), + const SizedBox(height: 70), Text( "You do not have a MonKey yet. \nFetch yours now!", style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark3, + color: + Theme.of( + context, + ).extension()!.textDark3, ), textAlign: TextAlign.center, ), @@ -476,12 +451,8 @@ class _MonkeyViewState extends ConsumerState { ), ), isDesktop - ? const SizedBox( - height: 50, - ) - : const Spacer( - flex: 6, - ), + ? const SizedBox(height: 50) + : const Spacer(flex: 6), Padding( padding: const EdgeInsets.all(16.0), child: PrimaryButton( @@ -490,16 +461,14 @@ class _MonkeyViewState extends ConsumerState { await showLoading( whileFuture: Future.wait([ wallet.getCurrentReceivingAddress().then( - (address) async => await ref - .read(pMonKeyService) - .fetchMonKey(address: address!.value) - .then( - (monKeyBytes) async => - await _updateWalletMonKey( - monKeyBytes, - ), - ), - ), + (address) async => await ref + .read(pMonKeyService) + .fetchMonKey(address: address!.value) + .then( + (monKeyBytes) async => + await _updateWalletMonKey(monKeyBytes), + ), + ), Future.delayed(const Duration(seconds: 2)), ]), context: context, diff --git a/lib/pages/ordinals/ordinal_details_view.dart b/lib/pages/ordinals/ordinal_details_view.dart index 02e0ee074..40ba0333e 100644 --- a/lib/pages/ordinals/ordinal_details_view.dart +++ b/lib/pages/ordinals/ordinal_details_view.dart @@ -5,8 +5,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; -import 'package:permission_handler/permission_handler.dart'; import '../../models/isar/models/blockchain_data/utxo.dart'; import '../../models/isar/ordinal.dart'; @@ -21,6 +21,7 @@ import '../../utilities/amount/amount_formatter.dart'; import '../../utilities/assets.dart'; import '../../utilities/constants.dart'; import '../../utilities/show_loading.dart'; +import '../../utilities/stack_file_system.dart'; import '../../utilities/text_styles.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; import '../../widgets/background.dart'; @@ -92,9 +93,7 @@ class _OrdinalDetailsViewState extends ConsumerState { title: "Inscription number", data: widget.ordinal.inscriptionNumber.toString(), ), - const SizedBox( - height: _spacing, - ), + const SizedBox(height: _spacing), _DetailsItemWCopy( title: "Inscription ID", data: widget.ordinal.inscriptionId, @@ -103,37 +102,32 @@ class _OrdinalDetailsViewState extends ConsumerState { // height: _spacing, // ), // // todo: add utxo status - const SizedBox( - height: _spacing, - ), + const SizedBox(height: _spacing), _DetailsItemWCopy( title: "Amount", - data: utxo == null - ? "ERROR" - : ref.watch(pAmountFormatter(coin)).format( - Amount( - rawValue: BigInt.from(utxo!.value), - fractionDigits: coin.fractionDigits, - ), - ), - ), - const SizedBox( - height: _spacing, + data: + utxo == null + ? "ERROR" + : ref + .watch(pAmountFormatter(coin)) + .format( + Amount( + rawValue: BigInt.from(utxo!.value), + fractionDigits: coin.fractionDigits, + ), + ), ), + const SizedBox(height: _spacing), _DetailsItemWCopy( title: "Owner address", data: utxo?.address ?? "ERROR", ), - const SizedBox( - height: _spacing, - ), + const SizedBox(height: _spacing), _DetailsItemWCopy( title: "Transaction ID", data: widget.ordinal.utxoTXID, ), - const SizedBox( - height: _spacing, - ), + const SizedBox(height: _spacing), ], ), ), @@ -145,11 +139,7 @@ class _OrdinalDetailsViewState extends ConsumerState { } class _DetailsItemWCopy extends StatelessWidget { - const _DetailsItemWCopy({ - super.key, - required this.title, - required this.data, - }); + const _DetailsItemWCopy({super.key, required this.title, required this.data}); final String title; final String data; @@ -163,10 +153,7 @@ class _DetailsItemWCopy extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - title, - style: STextStyles.itemSubtitle(context), - ), + Text(title, style: STextStyles.itemSubtitle(context)), GestureDetector( onTap: () async { await Clipboard.setData(ClipboardData(text: data)); @@ -184,20 +171,20 @@ class _DetailsItemWCopy extends StatelessWidget { children: [ SvgPicture.asset( Assets.svg.copy, - color: Theme.of(context) - .extension()! - .infoItemIcons, + color: + Theme.of( + context, + ).extension()!.infoItemIcons, width: 12, ), - const SizedBox( - width: 6, - ), + const SizedBox(width: 6), Text( "Copy", style: STextStyles.infoSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .infoItemIcons, + color: + Theme.of( + context, + ).extension()!.infoItemIcons, ), ), ], @@ -205,13 +192,8 @@ class _DetailsItemWCopy extends StatelessWidget { ), ], ), - const SizedBox( - height: 4, - ), - SelectableText( - data, - style: STextStyles.itemSubtitle12(context), - ), + const SizedBox(height: 4), + SelectableText(data, style: STextStyles.itemSubtitle12(context)), ], ), ); @@ -235,9 +217,10 @@ class _OrdinalImageGroup extends ConsumerWidget { final response = await client.get( url: Uri.parse(ordinal.content), - proxyInfo: ref.read(prefsChangeNotifierProvider).useTor - ? ref.read(pTorService).getProxyInfo() - : null, + proxyInfo: + ref.read(prefsChangeNotifierProvider).useTor + ? ref.read(pTorService).getProxyInfo() + : null, ); if (response.code != 200) { @@ -248,16 +231,14 @@ class _OrdinalImageGroup extends ConsumerWidget { final bytes = response.bodyBytes; - if (Platform.isAndroid) { - await Permission.storage.request(); - } - - final dir = Platform.isAndroid - ? Directory("/storage/emulated/0/Documents") - : await getApplicationDocumentsDirectory(); - - final docPath = dir.path; - final filePath = "$docPath/ordinal_${ordinal.inscriptionNumber}.png"; + final dir = + Platform.isAndroid + ? await StackFileSystem.wtfAndroidDocumentsPath() + : await getApplicationDocumentsDirectory(); + final filePath = path.join( + dir.path, + "ordinal_${ordinal.inscriptionNumber}.png", + ); final File imgFile = File(filePath); @@ -299,9 +280,7 @@ class _OrdinalImageGroup extends ConsumerWidget { ), ), ), - const SizedBox( - height: _spacing, - ), + const SizedBox(height: _spacing), Row( children: [ Expanded( @@ -311,9 +290,10 @@ class _OrdinalImageGroup extends ConsumerWidget { Assets.svg.arrowDown, width: 10, height: 12, - color: Theme.of(context) - .extension()! - .buttonTextSecondary, + color: + Theme.of( + context, + ).extension()!.buttonTextSecondary, ), buttonHeight: ButtonHeight.l, iconSpacing: 4, diff --git a/lib/pages/pinpad_views/lock_screen_view.dart b/lib/pages/pinpad_views/lock_screen_view.dart index f3552cf82..7a21a99c3 100644 --- a/lib/pages/pinpad_views/lock_screen_view.dart +++ b/lib/pages/pinpad_views/lock_screen_view.dart @@ -204,7 +204,10 @@ class _LockscreenViewState extends ConsumerState { final hasDuressPin = (await _secureStore.read(key: kDuressPinKey)) != null; - ref.read(pDuress.notifier).state = hasDuressPin; + ref.read(pDuress.notifier).state = + hasDuressPin && + ref.read(prefsChangeNotifierProvider).biometricsDuress; + canTryUnlock = true; } else { if (ref.read(pDuress)) { diff --git a/lib/pages/settings_views/global_settings_view/advanced_views/logging_settings_view.dart b/lib/pages/settings_views/global_settings_view/advanced_views/logging_settings_view.dart index dc6c24013..eb4eb5536 100644 --- a/lib/pages/settings_views/global_settings_view/advanced_views/logging_settings_view.dart +++ b/lib/pages/settings_views/global_settings_view/advanced_views/logging_settings_view.dart @@ -11,21 +11,28 @@ import 'dart:async'; import 'dart:io'; +import 'package:archive/archive_io.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; // import 'package:flutter_libmonero/git_versions.dart' as MONERO_VERSIONS; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; +import 'package:share_plus/share_plus.dart'; import '../../../../app_config.dart'; import '../../../../providers/global/prefs_provider.dart'; import '../../../../themes/stack_colors.dart'; import '../../../../utilities/assets.dart'; import '../../../../utilities/logger.dart'; +import '../../../../utilities/show_loading.dart'; +import '../../../../utilities/stack_file_system.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../widgets/background.dart'; import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../../widgets/desktop/primary_button.dart'; +import '../../../../widgets/desktop/secondary_button.dart'; import '../../../../widgets/log_level_preference_widget.dart'; import '../../../../widgets/rounded_white_container.dart'; import '../../../../widgets/stack_dialog.dart'; @@ -45,15 +52,14 @@ class _LoggingSettingsViewState extends ConsumerState { bool _lock = false; Future _edit() async { - final currentPath = ref.read(prefsChangeNotifierProvider).logsPath ?? + final currentPath = + ref.read(prefsChangeNotifierProvider).logsPath ?? Logging.instance.logsDirPath; final newPath = await _pickDir(context, currentPath); // test if has permission to write if (newPath != null) { - final file = File( - "$newPath${Platform.pathSeparator}._test", - ); + final file = File("$newPath${Platform.pathSeparator}._test"); if (!file.existsSync()) { file.createSync(); file.deleteSync(); @@ -67,7 +73,7 @@ class _LoggingSettingsViewState extends ConsumerState { setState(() { fileLocationController.text = ref.read(prefsChangeNotifierProvider).logsPath ?? - Logging.instance.logsDirPath; + Logging.instance.logsDirPath; }); } } @@ -88,13 +94,82 @@ class _LoggingSettingsViewState extends ConsumerState { return chosenPath; } + Future _exportHelper() async { + final logsDir = await StackFileSystem.applicationLogsDirectory( + ref.read(prefsChangeNotifierProvider), + ); + + final files = logsDir + .listSync(recursive: false) + .whereType() + .where((f) => f.path.endsWith('.txt')); + + if (files.isEmpty) { + throw Exception("No logs found in ${logsDir.path}"); + } + + final archive = Archive(); + + for (final file in files) { + final bytes = await file.readAsBytes(); + final fileName = path.basename(file.path); + archive.addFile(ArchiveFile(fileName, bytes.length, bytes)); + } + + if (archive.isEmpty) { + throw Exception("Failed to add log files to archive"); + } + + // Write zip to a temp location + final tempDir = await getTemporaryDirectory(); + final zipPath = path.join(tempDir.path, 'logs.zip'); + final zipFile = File(zipPath); + await zipFile.writeAsBytes(ZipEncoder().encode(archive)!); + + await Share.shareXFiles([ + XFile(zipFile.path), + ], text: "${AppConfig.appName} logs"); + } + + bool _exportLock = false; + Future _androidExportLogs() async { + if (_exportLock) { + return; + } + _exportLock = true; + try { + await showLoading( + whileFuture: _exportHelper(), + context: context, + message: "Exporting logs...", + onException: (e) => throw e, + ); + } catch (e, s) { + Logging.instance.e("Failed to export logs", error: e, stackTrace: s); + if (mounted) { + unawaited( + showDialog( + context: context, + builder: + (context) => StackOkDialog( + title: "Failed to export logs", + message: e.toString(), + ), + ), + ); + } + } finally { + _exportLock = false; + } + } + @override void initState() { super.initState(); fileLocationController = TextEditingController(); fileLocationController.text = ref.read(prefsChangeNotifierProvider).logsPath ?? - Logging.instance.logsDirPath; + Logging.instance.logsDirPath; } @override @@ -114,18 +189,11 @@ class _LoggingSettingsViewState extends ConsumerState { Navigator.of(context).pop(); }, ), - title: Text( - "Logging", - style: STextStyles.navBarTitle(context), - ), + title: Text("Logging", style: STextStyles.navBarTitle(context)), ), body: SafeArea( child: Padding( - padding: const EdgeInsets.only( - top: 12, - left: 16, - right: 16, - ), + padding: const EdgeInsets.only(top: 12, left: 16, right: 16), child: Column( children: [ Row( @@ -137,9 +205,7 @@ class _LoggingSettingsViewState extends ConsumerState { ), ], ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), TextField( autocorrect: false, enableSuggestions: false, @@ -151,27 +217,22 @@ class _LoggingSettingsViewState extends ConsumerState { suffixIcon: UnconstrainedBox( child: Row( children: [ - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), SvgPicture.asset( Assets.svg.folder, - color: Theme.of(context) - .extension()! - .textDark3, + color: + Theme.of( + context, + ).extension()!.textDark3, width: 16, height: 16, ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), ], ), ), ), - key: const Key( - "logsDirPathLocationControllerKey", - ), + key: const Key("logsDirPathLocationControllerKey"), readOnly: true, toolbarOptions: const ToolbarOptions( copy: true, @@ -181,13 +242,9 @@ class _LoggingSettingsViewState extends ConsumerState { ), onChanged: (newValue) {}, ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), const LogLevelPreferenceWidget(), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), Row( children: [ Expanded( @@ -201,10 +258,16 @@ class _LoggingSettingsViewState extends ConsumerState { ), ], ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), const Spacer(), + + if (Platform.isAndroid) + SecondaryButton( + label: "Export logs", + onPressed: _androidExportLogs, + ), + if (Platform.isAndroid) const SizedBox(height: 16), + PrimaryButton( label: "Select log save location", onPressed: () async { @@ -222,9 +285,9 @@ class _LoggingSettingsViewState extends ConsumerState { ); if (context.mounted) { final String err; - if (e - .toString() - .contains("OS Error: Operation not permitted")) { + if (e.toString().contains( + "OS Error: Operation not permitted", + )) { err = "Cannot use chosen location"; } else { err = e.toString(); @@ -233,10 +296,11 @@ class _LoggingSettingsViewState extends ConsumerState { unawaited( showDialog( context: context, - builder: (context) => StackOkDialog( - title: "Failed to change logs path", - message: err, - ), + builder: + (context) => StackOkDialog( + title: "Failed to change logs path", + message: err, + ), ), ); } @@ -245,9 +309,7 @@ class _LoggingSettingsViewState extends ConsumerState { } }, ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), ], ), ), diff --git a/lib/pages/settings_views/global_settings_view/security_views/security_view.dart b/lib/pages/settings_views/global_settings_view/security_views/security_view.dart index 7ccfd2650..b5aac24a9 100644 --- a/lib/pages/settings_views/global_settings_view/security_views/security_view.dart +++ b/lib/pages/settings_views/global_settings_view/security_views/security_view.dart @@ -489,7 +489,7 @@ class _SecurityViewState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - "Duress uses biometrics", + "Biometrics opens duress", style: STextStyles.titleBold12(context), textAlign: TextAlign.left, ), diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart index 001279117..9954cb0b7 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart @@ -12,10 +12,11 @@ import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; +import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; -import 'package:permission_handler/permission_handler.dart'; import '../../../../../app_config.dart'; +import '../../../../../utilities/stack_file_system.dart'; import '../../../../../utilities/util.dart'; class SWBFileSystem { @@ -29,13 +30,9 @@ class SWBFileSystem { Future prepareStorage() async { if (Platform.isAndroid) { - await Permission.storage.request(); - } - rootPath = (await getApplicationDocumentsDirectory()); - //todo: check if print needed - // debugPrint(rootPath!.absolute.toString()); - if (Platform.isAndroid) { - rootPath = Directory("/storage/emulated/0/"); + rootPath = await StackFileSystem.wtfAndroidDocumentsPath(); + } else { + rootPath = await getApplicationDocumentsDirectory(); } //todo: check if print needed // debugPrint(rootPath!.absolute.toString()); @@ -45,14 +42,11 @@ class SWBFileSystem { if (Platform.isIOS) { sampleFolder = Directory(rootPath!.path); - } else if (Platform.isAndroid) { - sampleFolder = Directory('${rootPath!.path}Documents/$dirName'); - } else if (Platform.isLinux) { - sampleFolder = Directory('${rootPath!.path}/$dirName'); - } else if (Platform.isWindows) { - sampleFolder = Directory('${rootPath!.path}/$dirName'); - } else if (Platform.isMacOS) { - sampleFolder = Directory('${rootPath!.path}/$dirName'); + } else if (Platform.isAndroid || + Platform.isLinux || + Platform.isWindows || + Platform.isMacOS) { + sampleFolder = Directory(path.join(rootPath!.path, dirName)); } try { @@ -86,9 +80,10 @@ class SWBFileSystem { if (Platform.isIOS) { chosenPath = startPath?.path; } else { - final String path = Platform.isWindows - ? startPath!.path.replaceAll("/", "\\") - : startPath!.path; + final String path = + Platform.isWindows + ? startPath!.path.replaceAll("/", "\\") + : startPath!.path; chosenPath = await FilePicker.platform.getDirectoryPath( dialogTitle: "Choose Backup location", initialDirectory: path, diff --git a/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart b/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart index 4aff4afb0..aea4a8f02 100644 --- a/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart +++ b/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart @@ -3,8 +3,8 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; -import 'package:permission_handler/permission_handler.dart'; import '../../models/isar/models/blockchain_data/utxo.dart'; import '../../models/isar/ordinal.dart'; @@ -21,6 +21,7 @@ import '../../utilities/assets.dart'; import '../../utilities/constants.dart'; import '../../utilities/prefs.dart'; import '../../utilities/show_loading.dart'; +import '../../utilities/stack_file_system.dart'; import '../../utilities/text_styles.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../widgets/desktop/desktop_app_bar.dart'; @@ -56,9 +57,10 @@ class _DesktopOrdinalDetailsViewState final response = await client.get( url: Uri.parse(widget.ordinal.content), - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, + proxyInfo: + Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, ); if (response.code != 200) { @@ -69,16 +71,15 @@ class _DesktopOrdinalDetailsViewState final bytes = response.bodyBytes; - if (Platform.isAndroid) { - await Permission.storage.request(); - } - - final dir = Platform.isAndroid - ? Directory("/storage/emulated/0/Documents") - : await getApplicationDocumentsDirectory(); + final dir = + Platform.isAndroid + ? await StackFileSystem.wtfAndroidDocumentsPath() + : await getApplicationDocumentsDirectory(); - final docPath = dir.path; - final filePath = "$docPath/ordinal_${widget.ordinal.inscriptionNumber}.png"; + final filePath = path.join( + dir.path, + "ordinal_${widget.ordinal.inscriptionNumber}.png", + ); final File imgFile = File(filePath); @@ -105,32 +106,27 @@ class _DesktopOrdinalDetailsViewState child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - const SizedBox( - width: 32, - ), + const SizedBox(width: 32), AppBarIconButton( size: 32, - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, shadows: const [], icon: SvgPicture.asset( Assets.svg.arrowLeft, width: 18, height: 18, - color: Theme.of(context) - .extension()! - .topNavIconPrimary, + color: + Theme.of( + context, + ).extension()!.topNavIconPrimary, ), onPressed: Navigator.of(context).pop, ), - const SizedBox( - width: 18, - ), - Text( - "Ordinal details", - style: STextStyles.desktopH3(context), - ), + const SizedBox(width: 18), + Text("Ordinal details", style: STextStyles.desktopH3(context)), ], ), ), @@ -138,11 +134,7 @@ class _DesktopOrdinalDetailsViewState isCompactHeight: true, ), body: Padding( - padding: const EdgeInsets.only( - left: 24, - top: 24, - right: 24, - ), + padding: const EdgeInsets.only(left: 24, top: 24, right: 24), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -154,7 +146,8 @@ class _DesktopOrdinalDetailsViewState Constants.size.circularBorderRadius, ), child: Image.network( - widget.ordinal + widget + .ordinal .content, // Use the preview URL as the image source fit: BoxFit.cover, filterQuality: @@ -162,9 +155,7 @@ class _DesktopOrdinalDetailsViewState ), ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Expanded( child: SingleChildScrollView( child: Padding( @@ -187,9 +178,7 @@ class _DesktopOrdinalDetailsViewState ], ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), // PrimaryButton( // width: 150, // label: "Send", @@ -224,9 +213,10 @@ class _DesktopOrdinalDetailsViewState Assets.svg.arrowDown, width: 13, height: 18, - color: Theme.of(context) - .extension()! - .buttonTextSecondary, + color: + Theme.of(context) + .extension()! + .buttonTextSecondary, ), buttonHeight: ButtonHeight.l, iconSpacing: 8, @@ -264,9 +254,7 @@ class _DesktopOrdinalDetailsViewState ], ), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), RoundedWhiteContainer( padding: const EdgeInsets.all(16), child: Column( @@ -288,25 +276,28 @@ class _DesktopOrdinalDetailsViewState const _Divider(), Consumer( builder: (context, ref, _) { - final coin = ref - .watch(pWallets) - .getWallet(widget.walletId) - .info - .coin; + final coin = + ref + .watch(pWallets) + .getWallet(widget.walletId) + .info + .coin; return _DetailsItemWCopy( title: "Amount", - data: utxo == null - ? "ERROR" - : ref - .watch(pAmountFormatter(coin)) - .format( - Amount( - rawValue: - BigInt.from(utxo!.value), - fractionDigits: - coin.fractionDigits, - ), - ), + data: + utxo == null + ? "ERROR" + : ref + .watch(pAmountFormatter(coin)) + .format( + Amount( + rawValue: BigInt.from( + utxo!.value, + ), + fractionDigits: + coin.fractionDigits, + ), + ), ); }, ), @@ -341,9 +332,7 @@ class _Divider extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.symmetric( - vertical: 16, - ), + padding: const EdgeInsets.symmetric(vertical: 16), child: Container( height: 1, color: Theme.of(context).extension()!.backgroundAppBar, @@ -353,11 +342,7 @@ class _Divider extends StatelessWidget { } class _DetailsItemWCopy extends StatelessWidget { - const _DetailsItemWCopy({ - super.key, - required this.title, - required this.data, - }); + const _DetailsItemWCopy({super.key, required this.title, required this.data}); final String title; final String data; @@ -370,22 +355,12 @@ class _DetailsItemWCopy extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - title, - style: STextStyles.itemSubtitle(context), - ), - IconCopyButton( - data: data, - ), + Text(title, style: STextStyles.itemSubtitle(context)), + IconCopyButton(data: data), ], ), - const SizedBox( - height: 4, - ), - SelectableText( - data, - style: STextStyles.itemSubtitle12(context), - ), + const SizedBox(height: 4), + SelectableText(data, style: STextStyles.itemSubtitle12(context)), ], ); } diff --git a/lib/providers/global/duress_provider.dart b/lib/providers/global/duress_provider.dart index f6e84011c..22ef4072d 100644 --- a/lib/providers/global/duress_provider.dart +++ b/lib/providers/global/duress_provider.dart @@ -1,4 +1,4 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -final pDuress = StateProvider((ref) => true); +final pDuress = StateProvider((ref) => false); diff --git a/lib/utilities/stack_file_system.dart b/lib/utilities/stack_file_system.dart index 4f9bff090..7a254dffe 100644 --- a/lib/utilities/stack_file_system.dart +++ b/lib/utilities/stack_file_system.dart @@ -12,7 +12,6 @@ import 'dart:io'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; -import 'package:permission_handler/permission_handler.dart'; import '../app_config.dart'; import 'prefs.dart'; @@ -215,13 +214,11 @@ abstract class StackFileSystem { // TODO check this is correct for macos logsDir = Directory(path.join(appDocsDir.path, logsDirName)); } else if (Platform.isAndroid) { - await Permission.storage.request(); - final ext = await getExternalStorageDirectory(); - final rootPath = path.dirname( - path.dirname(path.dirname(path.dirname(ext!.path))), - ); - final logsDirPath = path.join(rootPath, "Documents", logsDirName); - logsDir = Directory(logsDirPath); + // final dir = await wtfAndroidDocumentsPath(); + // final logsDirPath = path.join(dir.path, logsDirName); + // logsDir = Directory(logsDirPath); + + logsDir = Directory(path.join(appDocsDir.path, "logs")); } else { throw Exception("Unsupported Platform"); } @@ -232,4 +229,18 @@ abstract class StackFileSystem { return logsDir; } + + static Future wtfAndroidDocumentsPath() async { + const base = "/storage/emulated/"; + final rootDir = await applicationRootDirectory(); + final parts = rootDir.path.replaceFirst("/data/user/", "").split("/"); + if (parts.isNotEmpty) { + final id = int.tryParse(parts.first); + + if (id != null) { + return Directory(path.join(base, id.toString(), "Documents")); + } + } + throw Exception("Unsupported Android flavor"); + } } diff --git a/scripts/app_config/templates/android/app/src/main/AndroidManifest.xml b/scripts/app_config/templates/android/app/src/main/AndroidManifest.xml index 07f7a3ef0..d10488cc9 100644 --- a/scripts/app_config/templates/android/app/src/main/AndroidManifest.xml +++ b/scripts/app_config/templates/android/app/src/main/AndroidManifest.xml @@ -5,12 +5,6 @@ android:name="android.permission.INTERNET"/> - - -