diff --git a/android/app/appcenter-post-clone.sh b/android/app/appcenter-post-clone.sh index 01b61a37..f44f5938 100644 --- a/android/app/appcenter-post-clone.sh +++ b/android/app/appcenter-post-clone.sh @@ -37,4 +37,24 @@ echo "APPCENTER_DISTRIBUTION_GROUP_ID_ANDROID=${APPCENTER_DISTRIBUTION_GROUP_ID_ flutter build apk --flavor product --release # copy the APK where AppCenter will find it -mkdir -p android/app/build/outputs/apk/; mv build/app/outputs/apk/product/release/axs-wallet.apk $_ \ No newline at end of file + +mkdir -p android/app/build/outputs/apk/; mv build/app/outputs/apk/product/release/app-product-release.apk $_ + +# copy the AAB where AppCenter will find it +mkdir -p android/app/build/outputs/bundle/; mv build/app/outputs/bundle/googleplayRelease/app-googleplay-release.aab $_ + +# To configure appCenter builds with Waldo UI Automation tool +export WALDO_CLI_BIN=/usr/local/bin +bash -c "$(curl -fLs https://github.com/waldoapp/waldo-go-cli/raw/master/install-waldo.sh)" + +export PATH="$WALDO_CLI_BIN:$PATH" + +# To configure appCenter builds with Waldo UI Automation tool +export WALDO_UPLOAD_TOKEN=$ANDROID_WALDO_UPLOAD_TOKEN + +_build_path=android/app/build/outputs/apk/app-product-release.apk + +waldo upload "$_build_path" + +# /usr/local/bin/waldo upload "$BUILD_PATH" + diff --git a/android/app/build.gradle b/android/app/build.gradle index 7669bae7..05c747d7 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -29,7 +29,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 33 + compileSdkVersion 34 buildToolsVersion "33.0.1" ndkVersion flutter.ndkVersion diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 0795324a..323f9cd0 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -30,7 +30,7 @@ - + diff --git a/assets/flutter_i18n/en.json b/assets/flutter_i18n/en.json index 1a3cb359..9346e225 100644 --- a/assets/flutter_i18n/en.json +++ b/assets/flutter_i18n/en.json @@ -4,6 +4,8 @@ "telegram_secured_storage": "Telegram secured storage", "wechat_secured_storage": "WeChat secured storage", "email_secured_storage": "Email secured storage", + "local_secured_storage": "Local secured storage", + "save_locally": "Save locally", "set_passcode": "Create passcode", "set_passcode_hint": "Enter a 6-digit passcode to unlock your wallet with ease. This passcode can’t be used to recover your wallet.", "passcode_didnt_match": "Passcodes did not match. Try again.", @@ -68,6 +70,7 @@ "share": "Share", "wechat": "WeChat", "email_to_myself_description": "Make sure to email your keys only to yourself.", + "save_locally_description": "Make sure to save your keys locally and then you are good to go.", "email_to_myself": "Email to myself", "security_notice": "Important Security Notice", "ensure_saved_platform": "Please ensure that you have securely saved your key on a trusted platform such as email, Telegram, or WeChat.", @@ -399,6 +402,7 @@ "axs_location_permission_use_case": "AXS Wallet needs access to your location for features such as Wi-Fi hooks while the app is in use or running in the background. Your location data is not saved or shared with any third parties.", "axs_camera_permission_use_case": "AXS Wallet needs access to your camera when the app is in use for services such as RWA minting. Image data is not saved or shared with any third parties.", "axs_photos_permission_use_case": "AXS Wallet needs access to your photos to save images such as RWA. Your photos are not saved or shared with any third parties.", + "axs_storage_permission_use_case": "AXS Wallet needs access to your storage for features such as RWA. Your storage data is not saved or shared with any third parties.", "permission_use_cases": "Permission use cases", "ok_allow": "OK, Allow", "not_now": "Not now", @@ -407,5 +411,6 @@ "remove_dapp": "Remove dApp", "dapp_removal_dialog_text": "Deleting this dApp will remove it from your Home Screen.", "information": "Information", - "ok": "Ok" + "ok": "Ok", + "local_backup": "Local backup" } \ No newline at end of file diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 21fa240c..30e132e8 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -1,154 +1,156 @@ - - BGTaskSchedulerPermittedIdentifiers - - com.transistorsoft.fetch - com.mxc.axswallet.periodicalTasks - com.mxc.axswallet.dappHooksTasks - com.mxc.axswallet.minerAutoClaimTask - - CADisableMinimumFrameDurationOnPhone - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - AXS - CFBundleDocumentTypes - - - CFBundleTypeName - FlSharedLink - LSHandlerRank - Default - LSItemContentTypes - - public.file-url - public.image - public.text - public.url - public.data - - - - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleLocalizations - - en - zh_TW - zh_CN - zh_HK - ko - ja - vi - ru - tr - de - es - pt - tl - id - it - fr - - CFBundleName - axs - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSApplicationQueriesSchemes - - tg - weixin - wechat - googlegmail - x-dispatch - readdle-spark - airmail - ms-outlook - ymail - fastmail - superhuman - protonmail - - LSRequiresIPhoneOS - - LSSupportsOpeningDocumentsInPlace - No - NSAppTransportSecurity - NSAllowsArbitraryLoads + BGTaskSchedulerPermittedIdentifiers + + com.transistorsoft.fetch + com.mxc.axswallet.periodicalTasks + com.mxc.axswallet.dappHooksTasks + com.mxc.axswallet.minerAutoClaimTask + + CADisableMinimumFrameDurationOnPhone - - NSAppleMusicUsageDescription - Would you allow AXS Wallet to use the Media Library? - NSBluetoothPeripheralUsageDescription - Would you allow AXS Wallet to use the Bluetooth? - NSCalendarsUsageDescription - Would you allow AXS Wallet to use the Calendar? - NSCameraUsageDescription - AXS Wallet needs access to your camera when the app is in use for services such as RWA minting. Image data is not saved or shared with any third parties. - NSFaceIDUsageDescription - AXS Wallet needs access to your Biometric data when the app is in use for authentication only. Your Biometric data is not saved or shared with any third parties. - NSLocationAlwaysAndWhenInUseUsageDescription - AXS Wallet needs access to your location for features such as Wi-Fi hooks while the app is in use or running in the background. Your location data is not saved or shared with any third parties. + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + AXS + CFBundleDocumentTypes + + + CFBundleTypeName + FlSharedLink + LSHandlerRank + Default + LSItemContentTypes + + public.file-url + public.image + public.text + public.url + public.data + + + + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleLocalizations + + en + zh_TW + zh_CN + zh_HK + ko + ja + vi + ru + tr + de + es + pt + tl + id + it + fr + + CFBundleName + axs + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSApplicationQueriesSchemes + + tg + weixin + wechat + googlegmail + x-dispatch + readdle-spark + airmail + ms-outlook + ymail + fastmail + superhuman + protonmail + + LSRequiresIPhoneOS + + LSSupportsOpeningDocumentsInPlace + No + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + NSAppleMusicUsageDescription + Would you allow AXS Wallet to use the Media Library? + NSBluetoothPeripheralUsageDescription + Would you allow AXS Wallet to use the Bluetooth? + NSCalendarsUsageDescription + Would you allow AXS Wallet to use the Calendar? + NSCameraUsageDescription + AXS Wallet needs access to your camera when the app is in use for services such as RWA minting. Image data is not saved or shared with any third parties. + NSFaceIDUsageDescription + AXS Wallet needs access to your Biometric data when the app is in use for authentication only. Your Biometric data is not saved or shared with any third parties. + NSLocationAlwaysAndWhenInUseUsageDescription + AXS Wallet needs access to your location for features such as Wi-Fi hooks while the app is in use or running in the background. Your location data is not saved or shared with any third parties. - NSLocationAlwaysUsageDescription - AXS Wallet requires access to your location at all times for services such as Wi-Fi hooks. Your location data is not saved or shared with any third parties. + NSLocationAlwaysUsageDescription + AXS Wallet requires access to your location at all times for services such as Wi-Fi hooks. Your location data is not saved or shared with any third parties. - NSLocationUsageDescription - AXS Wallet needs access to your location to provide services such as Wi-Fi hooks. Your location data is not saved or shared with any third parties. + NSLocationUsageDescription + AXS Wallet needs access to your location to provide services such as Wi-Fi hooks. Your location data is not saved or shared with any third parties. - NSLocationWhenInUseUsageDescription - AXS Wallet needs access to your location when the app is in use for services such as Wi-Fi hooks. Your location data is not saved or shared with any third parties. - NSMotionUsageDescription - Would you allow AXS Wallet to use the Motion? - NSPhotoLibraryAddUsageDescription - AXS Wallet needs access to your photos to save images such as RWA. Your photos are not saved or shared with any third parties. - NSPhotoLibraryUsageDescription - AXS Wallet needs access to your photos when the app is in use for services such as RWA minting. Your photos are not saved or shared with any third parties. - NSSpeechRecognitionUsageDescription - Would you allow AXS Wallet to use the Speech Recognition? - PermissionGroupNotification - Would you allow AXS Wallet to use the Notification? - UIApplicationSupportsIndirectInputEvents - - UIBackgroundModes - - fetch - location - processing - remote-notification - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - + NSLocationWhenInUseUsageDescription + AXS Wallet needs access to your location when the app is in use for services such as Wi-Fi hooks. Your location data is not saved or shared with any third parties. + NSMotionUsageDescription + Would you allow AXS Wallet to use the Motion? + NSPhotoLibraryAddUsageDescription + AXS Wallet needs access to your photos to save images such as RWA. Your photos are not saved or shared with any third parties. + NSPhotoLibraryUsageDescription + AXS Wallet needs access to your photos when the app is in use for services such as RWA minting. Your photos are not saved or shared with any third parties. + NSSpeechRecognitionUsageDescription + Would you allow AXS Wallet to use the Speech Recognition? + PermissionGroupNotification + Would you allow AXS Wallet to use the Notification? + UIApplicationSupportsIndirectInputEvents + + UIBackgroundModes + + fetch + location + processing + remote-notification + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + UISupportsDocumentBrowser + + diff --git a/ios/appcenter-post-clone.sh b/ios/appcenter-post-clone.sh index 7bbc81d8..dc28dc06 100644 --- a/ios/appcenter-post-clone.sh +++ b/ios/appcenter-post-clone.sh @@ -27,9 +27,10 @@ flutter build ios --release --no-codesign ## To configure appCenter builds with Waldo UI Automation tool export WALDO_CLI_BIN=/usr/local/bin - bash -c "$(curl -fLs https://github.com/waldoapp/waldo-go-cli/raw/master/install-waldo.sh)" +export PATH="$WALDO_CLI_BIN:$PATH" + # Remove START-SIM-SEC secion from Podfile sed -i '' "/#-START-SIM-SEC/,/#-END-SIM-SEC/d" 'ios/Podfile' flutter build ios --simulator @@ -38,6 +39,8 @@ flutter build ios --simulator _build_path="build/ios/iphonesimulator/Runner.app" # adjust this as necessary -export WALDO_UPLOAD_TOKEN=7359ffbc47e3005dd303a0161d48b890 +export WALDO_UPLOAD_TOKEN=$IOS_WALDO_UPLOAD_TOKEN + +# waldo auth $IOS_WALDO_UPLOAD_TOKEN -/usr/local/bin/waldo upload "$_build_path" +waldo upload "$_build_path" diff --git a/lib/common/layout/mxc_page.dart b/lib/common/layout/mxc_page.dart index b834045f..f10270a4 100644 --- a/lib/common/layout/mxc_page.dart +++ b/lib/common/layout/mxc_page.dart @@ -31,6 +31,7 @@ abstract class MxcPage extends HookConsumerWidget { this.fixedFooter = false, this.floatingActionButton, this.backgroundColor, + this.backgroundGradient, this.useFooterPadding = true, this.resizeToAvoidBottomInset = true, this.useSplashBackground = false, @@ -55,6 +56,7 @@ abstract class MxcPage extends HookConsumerWidget { bool fixedFooter, Widget? floatingActionButton, Color? backgroundColor, + Gradient? backgroundGradient, bool useFooterPadding, bool resizeToAvoidBottomInset, bool useSplashBackground, @@ -103,6 +105,7 @@ abstract class MxcPage extends HookConsumerWidget { final bool fixedFooter; final Widget? floatingActionButton; final Color? backgroundColor; + final Gradient? backgroundGradient; final bool resizeToAvoidBottomInset; final bool useSplashBackground; @@ -169,6 +172,9 @@ abstract class MxcPage extends HookConsumerWidget { bool get maintainBottomSafeArea => true; Color resolveBackgroundColor(BuildContext context) { + if (backgroundGradient != null) { + return Colors.transparent; + } if (backgroundColor != null) { return backgroundColor!; } @@ -221,40 +227,45 @@ abstract class MxcPage extends HookConsumerWidget { ? Brightness.dark : Brightness.light, ), - child: Scaffold( - backgroundColor: resolveBackgroundColor(context), - extendBodyBehindAppBar: false, - drawer: drawer, - key: scaffoldKey, - resizeToAvoidBottomInset: false, - floatingActionButton: floatingActionButton, - bottomNavigationBar: buildBottomNavigation(context, ref), - floatingActionButtonLocation: - FloatingActionButtonLocation.miniCenterFloat, - body: PresenterHooks( - presenter: presenter, - child: splashLinearBackground( - visiable: useSplashBackground, - child: SafeArea( - bottom: maintainBottomSafeArea, - top: topSafeArea, - child: Column( - children: [ - buildAppBar(context, ref), - Expanded( - child: Padding( - padding: childrenPadding ?? EdgeInsets.zero, - child: content(context, ref), - )), - if (placeBottomInsetFiller) - AnimatedSize( - curve: Curves.easeOutQuad, - duration: const Duration(milliseconds: 275), - child: SizedBox( - height: MediaQuery.of(context).viewInsets.bottom, + child: Container( + decoration: BoxDecoration( + gradient: backgroundGradient, + ), + child: Scaffold( + backgroundColor: resolveBackgroundColor(context), + extendBodyBehindAppBar: false, + drawer: drawer, + key: scaffoldKey, + resizeToAvoidBottomInset: false, + floatingActionButton: floatingActionButton, + bottomNavigationBar: buildBottomNavigation(context, ref), + floatingActionButtonLocation: + FloatingActionButtonLocation.miniCenterFloat, + body: PresenterHooks( + presenter: presenter, + child: splashLinearBackground( + visiable: useSplashBackground, + child: SafeArea( + bottom: maintainBottomSafeArea, + top: topSafeArea, + child: Column( + children: [ + buildAppBar(context, ref), + Expanded( + child: Padding( + padding: childrenPadding ?? EdgeInsets.zero, + child: content(context, ref), + )), + if (placeBottomInsetFiller) + AnimatedSize( + curve: Curves.easeOutQuad, + duration: const Duration(milliseconds: 275), + child: SizedBox( + height: MediaQuery.of(context).viewInsets.bottom, + ), ), - ), - ], + ], + ), ), ), ), diff --git a/lib/common/layout/mxc_page_layer.dart b/lib/common/layout/mxc_page_layer.dart index 60eba807..d9a02288 100644 --- a/lib/common/layout/mxc_page_layer.dart +++ b/lib/common/layout/mxc_page_layer.dart @@ -30,6 +30,7 @@ class MxcPageLayer extends MxcPage { bool fixedFooter = false, Widget? floatingActionButton, Color? backgroundColor, + Gradient? backgroundGradient, bool useFooterPadding = true, bool resizeToAvoidBottomInset = true, bool useSplashBackground = false, @@ -53,6 +54,7 @@ class MxcPageLayer extends MxcPage { fixedFooter: fixedFooter, floatingActionButton: floatingActionButton, backgroundColor: backgroundColor, + backgroundGradient: backgroundGradient, useFooterPadding: useFooterPadding, resizeToAvoidBottomInset: resizeToAvoidBottomInset, useSplashBackground: useSplashBackground, diff --git a/lib/common/layout/mxc_page_regular.dart b/lib/common/layout/mxc_page_regular.dart index 21dc2cfc..89b361b1 100644 --- a/lib/common/layout/mxc_page_regular.dart +++ b/lib/common/layout/mxc_page_regular.dart @@ -26,6 +26,7 @@ class MxcPageRegular extends MxcPage { bool fixedFooter = false, Widget? floatingActionButton, Color? backgroundColor, + Gradient? backgroundGradient, bool useFooterPadding = true, bool resizeToAvoidBottomInset = true, bool useSplashBackground = false, @@ -48,6 +49,7 @@ class MxcPageRegular extends MxcPage { fixedFooter: fixedFooter, floatingActionButton: floatingActionButton, backgroundColor: backgroundColor, + backgroundGradient: backgroundGradient, useFooterPadding: useFooterPadding, resizeToAvoidBottomInset: resizeToAvoidBottomInset, useSplashBackground: useSplashBackground, diff --git a/lib/common/utils/permission.dart b/lib/common/utils/permission.dart index bd3a2512..bac758a8 100644 --- a/lib/common/utils/permission.dart +++ b/lib/common/utils/permission.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:datadashwallet/common/utils/permissions_bottom_sheet.dart'; import 'package:f_logs/f_logs.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; @@ -31,6 +33,9 @@ class PermissionUtils { static Future showUseCaseBottomSheet( Permission permission, BuildContext context) async { + if (Platform.isIOS) { + return true; + } return showPermissionUseCasesBottomSheet(context, permission: permission); } @@ -89,6 +94,31 @@ class PermissionUtils { return status == PermissionStatus.granted; } + static void storagePermissionWrapper(Future Function() func, + Function addError, BuildContext context) async { + final askForPermission = await PermissionUtils.showUseCaseBottomSheet( + Permission.storage, context); + if (askForPermission ?? false) { + final result = await PermissionUtils.askForStoragePermission(); + if (result) { + try { + await func(); + } catch (e, s) { + addError(e, s); + } + } + } + } + + static Future askForStoragePermission() async { + final status = await Permission.manageExternalStorage.request(); + final storageStatus = await Permission.storage.request(); + if (Platform.isIOS) { + return storageStatus == PermissionStatus.granted; + } + return status == PermissionStatus.granted; + } + static Future checkNotificationPermission() async { AuthorizationStatus authorizationStatus = await getNotificationPermission(); diff --git a/lib/common/utils/permissions_bottom_sheet.dart b/lib/common/utils/permissions_bottom_sheet.dart index bda4d982..9ade56a5 100644 --- a/lib/common/utils/permissions_bottom_sheet.dart +++ b/lib/common/utils/permissions_bottom_sheet.dart @@ -79,7 +79,7 @@ String getPermissionUseCaseText(Permission permission) { } else if (Permission.camera == permission) { return 'axs_camera_permission_use_case'; } else if (Permission.storage == permission) { - return 'axs_photos_permission_use_case'; + return 'axs_storage_permission_use_case'; } else if (Permission.photos == permission) { return 'axs_photos_permission_use_case'; } else { diff --git a/lib/core/src/providers/providers_use_cases.dart b/lib/core/src/providers/providers_use_cases.dart index 00a83728..2071c11f 100644 --- a/lib/core/src/providers/providers_use_cases.dart +++ b/lib/core/src/providers/providers_use_cases.dart @@ -42,7 +42,8 @@ final Provider gesturesInstructionUseCaseProvider = ); final Provider dappsOrderUseCaseProvider = Provider( - (ref) => DappsOrderUseCase(ref.watch(datadashCacheProvider).dappsOrderRepository), + (ref) => + DappsOrderUseCase(ref.watch(datadashCacheProvider).dappsOrderRepository), ); final Provider tokenContractUseCaseProvider = Provider( @@ -104,6 +105,10 @@ final Provider authUseCaseProvider = Provider( ), ); +final Provider directoryUseCaseProvider = Provider( + (ref) => DirectoryUseCase(), +); + final Provider accountUseCaseProvider = Provider( (ref) => AccountUseCase( ref.watch(web3RepositoryProvider), diff --git a/lib/features/common/app_nav_bar/app_nav_bar.dart b/lib/features/common/app_nav_bar/app_nav_bar.dart index 47b141e7..76b6f66b 100644 --- a/lib/features/common/app_nav_bar/app_nav_bar.dart +++ b/lib/features/common/app_nav_bar/app_nav_bar.dart @@ -44,7 +44,7 @@ class AppNavBar extends HookConsumerWidget { padding: const EdgeInsets.all(Sizes.space2XSmall), decoration: BoxDecoration( borderRadius: BorderRadius.circular(50), - color: ColorsTheme.of(context).backgroundDisabled, + color: ColorsTheme.of(context).iconPrimary, ), child: GestureDetector( onTap: () => presenter.copy(), @@ -59,7 +59,9 @@ class AppNavBar extends HookConsumerWidget { state.account?.mns ?? MXCFormatter.formatWalletAddress( state.account?.address ?? ''), - style: FontTheme.of(context).subtitle1(), + style: FontTheme.of(context).subtitle1().copyWith( + color: ColorsTheme.of(context).screenBackground, + ), ) ], ), diff --git a/lib/features/dapps/domain/dapp_store_use_case.dart b/lib/features/dapps/domain/dapp_store_use_case.dart index 2fd26dbd..d1846250 100644 --- a/lib/features/dapps/domain/dapp_store_use_case.dart +++ b/lib/features/dapps/domain/dapp_store_use_case.dart @@ -14,12 +14,21 @@ class DappStoreUseCase extends ReactiveUseCase { late final ValueStream> dapps = reactive([]); - loadLocalDApps() async { + Future loadDapps() async { + Future.delayed( + const Duration(seconds: 1), + () => loadLocalDApps(), + ); + + await loadRemoteDApps(); + } + + Future loadLocalDApps() async { final result = await _repository.dappStoreRepository.getAllDappsFromLocal(); update(dapps, result); } - Future getAllDapps() async { + Future loadRemoteDApps() async { final result = await _repository.dappStoreRepository.getAllDapps(); update(dapps, result); diff --git a/lib/features/dapps/helpers/permissions_helpers.dart b/lib/features/dapps/helpers/permissions_helpers.dart index 10d2c64a..fe1a98d5 100644 --- a/lib/features/dapps/helpers/permissions_helpers.dart +++ b/lib/features/dapps/helpers/permissions_helpers.dart @@ -53,7 +53,6 @@ class PermissionsHelper { Future checkPermissionStatusAndRequest( Permission permission, ) async { - final l = await permission.status; if (!(await PermissionUtils.isPermissionGranted(permission)) && !(await PermissionUtils.isPermissionPermanentlyDenied(permission))) { final askForPermission = diff --git a/lib/features/dapps/helpers/reorder_helper.dart b/lib/features/dapps/helpers/reorder_helper.dart index ee16a837..255c20e5 100644 --- a/lib/features/dapps/helpers/reorder_helper.dart +++ b/lib/features/dapps/helpers/reorder_helper.dart @@ -1,7 +1,4 @@ -import 'dart:async'; - import 'package:datadashwallet/features/dapps/presentation/responsive_layout/dapp_utils.dart'; -import 'package:flutter/material.dart'; import 'package:mxc_logic/mxc_logic.dart'; import '../domain/domain.dart'; diff --git a/lib/features/dapps/presentation/dapps_page.dart b/lib/features/dapps/presentation/dapps_page.dart index 057f1c88..5abdd1d5 100644 --- a/lib/features/dapps/presentation/dapps_page.dart +++ b/lib/features/dapps/presentation/dapps_page.dart @@ -1,9 +1,6 @@ import 'package:datadashwallet/common/common.dart'; -import 'package:datadashwallet/core/core.dart'; -import 'package:datadashwallet/features/common/common.dart'; -import 'package:datadashwallet/features/dapps/dapps.dart'; -import 'package:datadashwallet/features/settings/settings.dart'; -import 'package:datadashwallet/features/wallet/wallet.dart'; +import 'package:datadashwallet/features/dapps/presentation/widgets/default_app_bar.dart'; +import 'package:datadashwallet/features/dapps/presentation/widgets/edit_mode_app_bar.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:mxc_ui/mxc_ui.dart'; @@ -11,7 +8,6 @@ import 'package:mxc_ui/mxc_ui.dart'; import 'dapps_presenter.dart'; import 'dapps_state.dart'; import 'responsive_layout/responsive_layout.dart'; -import 'widgets/edit_mode_status_bar.dart'; class DAppsPage extends HookConsumerWidget { const DAppsPage({Key? key}) : super(key: key); @@ -29,41 +25,22 @@ class DAppsPage extends HookConsumerWidget { return MxcPage( layout: LayoutType.column, useContentPadding: false, - childrenPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16), - backgroundColor: ColorsTheme.of(context).screenBackground, + childrenPadding: const EdgeInsets.symmetric( + horizontal: Sizes.spaceSmall, vertical: Sizes.spaceNormal), + backgroundGradient: const LinearGradient( + colors: [ + Color(0xFF0E1629), + Color(0xFF333333), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), presenter: ref.watch(presenter), appBar: Column( children: [ - if (ref.watch(state).isEditMode) ...[ - EditAppsModeStatusBar( - onAdd: dappsPresenter.addBookmark, - onDone: dappsPresenter.changeEditMode, - ), - ], - AppNavBar( - leading: IconButton( - key: const ValueKey('settingsButton'), - icon: const Icon(MxcIcons.settings), - iconSize: 32, - onPressed: () { - Navigator.of(context).push( - route( - const SettingsPage(), - ), - ); - }, - color: ColorsTheme.of(context).iconPrimary, - ), - action: IconButton( - key: const ValueKey('walletButton'), - icon: const Icon(MxcIcons.wallet), - iconSize: 32, - onPressed: () => Navigator.of(context).replaceAll( - route(const WalletPage()), - ), - color: ColorsTheme.of(context).iconPrimary, - ), - ), + ref.watch(state).isEditMode + ? const EditModeAppBar() + : const DefaultAppBar(), ], ), children: const [ diff --git a/lib/features/dapps/presentation/dapps_presenter.dart b/lib/features/dapps/presentation/dapps_presenter.dart index b76c3b69..f64aa16a 100644 --- a/lib/features/dapps/presentation/dapps_presenter.dart +++ b/lib/features/dapps/presentation/dapps_presenter.dart @@ -96,6 +96,8 @@ class DAppsPagePresenter extends CompletePresenter { listen(_chainConfigurationUseCase.selectedNetwork, (value) { if (value != null) { if (state.network != null && state.network!.chainId != value.chainId) { + DappUtils.loadingOnce = true; + notify(() => state.loading = true); reorderHelper.resetDappsMerge(); } notify(() => state.network = value); @@ -157,7 +159,7 @@ class DAppsPagePresenter extends CompletePresenter { void initializeDapps() async { try { - await _dappStoreUseCase.getAllDapps(); + await _dappStoreUseCase.loadDapps(); } catch (e, s) { addError(e, s); } @@ -171,7 +173,8 @@ class DAppsPagePresenter extends CompletePresenter { void addBookmark() async => bookmarksHelper.addBookmark(); - void updateBookmarkFavIcon(Bookmark item) async => bookmarksHelper.updateBookmarkFavIcon(item); + void updateBookmarkFavIcon(Bookmark item) async => + bookmarksHelper.updateBookmarkFavIcon(item); void onPageChage(int index) => notify(() => state.pageIndex = index); diff --git a/lib/features/dapps/presentation/responsive_layout/card_item.dart b/lib/features/dapps/presentation/responsive_layout/card_item.dart deleted file mode 100644 index c90e48dd..00000000 --- a/lib/features/dapps/presentation/responsive_layout/card_item.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; - -class CardCrossAxisCount { - static const int mobile = 4; - static const int tablet = 5; -} - -class CardMainAxisCount { - static const int mobile = 3; - static const int tablet = 4; -} - - -class CardSizes { - static Widget large({required Widget child}) { - return StaggeredGridTile.count( - crossAxisCellCount: 4, - mainAxisCellCount: 2, - child: child, - ); - } - - static Widget medium({required Widget child}) { - return StaggeredGridTile.count( - crossAxisCellCount: 2, - mainAxisCellCount: 2, - child: child, - ); - } - - static Widget small({required Widget child}) { - return StaggeredGridTile.count( - crossAxisCellCount: 1, - mainAxisCellCount: 1, - child: child, - ); - } -} diff --git a/lib/features/dapps/presentation/responsive_layout/dapp_card.dart b/lib/features/dapps/presentation/responsive_layout/dapp_card.dart deleted file mode 100644 index bc5c324c..00000000 --- a/lib/features/dapps/presentation/responsive_layout/dapp_card.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter/material.dart'; -import 'package:mxc_logic/mxc_logic.dart'; -import 'package:mxc_ui/mxc_ui.dart'; - -class DappCard extends StatelessWidget { - const DappCard({ - super.key, - required this.dapp, - this.isEditMode = false, - this.onTap, - this.onLongPress, - this.onRemoveTap, - }); - - final Dapp dapp; - final bool isEditMode; - final VoidCallback? onTap; - final VoidCallback? onLongPress; - final Function(Bookmark?)? onRemoveTap; - - Widget cardBox(BuildContext context) { - final bookmark = dapp is Bookmark ? dapp as Bookmark : null; - if (bookmark != null) { - return Container( - padding: const EdgeInsets.all(2), - width: double.infinity, - height: double.infinity, - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: const BorderRadius.all( - Radius.circular(8), - ), - color: ColorsTheme.of(context).cardBackground, - ), - child: Text( - bookmark.title, - style: FontTheme.of(context).subtitle2().copyWith( - color: ColorsTheme.of(context).textSecondary, - ), - overflow: TextOverflow.ellipsis, - ), - ); - } else { - final icons = dapp.reviewApi!.icons!; - final image = icons.islarge != null && icons.islarge! - ? icons.iconLarge - : icons.iconSmall; - - return ClipRRect( - borderRadius: BorderRadius.circular(22), - child: image!.contains('https') - ? CachedNetworkImage( - imageUrl: image, - fit: BoxFit.cover, - ) - : Image.asset(image)); - } - } - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - onLongPress: onLongPress, - child: Stack( - children: [ - Padding( - padding: const EdgeInsets.all(4), - child: Center( - child: cardBox(context), - ), - ), - if (isEditMode) - Positioned( - top: -2, - left: -2, - child: InkWell( - onTap: onRemoveTap != null - ? () => onRemoveTap!(dapp as Bookmark) - : null, - child: const Icon( - Icons.remove_circle_rounded, - ), - ), - ), - ], - ), - ); - } -} diff --git a/lib/features/dapps/presentation/responsive_layout/dapp_card_layout.dart b/lib/features/dapps/presentation/responsive_layout/dapp_card_layout.dart index c2b20a32..123cc7dc 100644 --- a/lib/features/dapps/presentation/responsive_layout/dapp_card_layout.dart +++ b/lib/features/dapps/presentation/responsive_layout/dapp_card_layout.dart @@ -7,10 +7,10 @@ import 'package:reorderable_grid_view/reorderable_grid_view.dart'; import '../dapps_state.dart'; import '../widgets/dapp_indicator.dart'; -import 'card_item.dart'; +import 'dapps_layout/card_item.dart'; import 'dapp_loading.dart'; import 'dapp_utils.dart'; -import 'new_dapp_card.dart'; +import 'dapps_layout/dapp_card.dart'; class DappCardLayout extends HookConsumerWidget { const DappCardLayout({ @@ -82,6 +82,9 @@ class DappCardLayout extends HookConsumerWidget { }, ), ), + const SizedBox( + height: Sizes.spaceXLarge, + ), DAppIndicator( total: pages, selectedIndex: state.pageIndex, @@ -99,7 +102,7 @@ List getList(List dapps, DAppsPagePresenter actions, final item = dapps[i]; final isBookMark = item is Bookmark; final dappCard = isBookMark - ? NewDAppCard( + ? DAppCard( index: i, width: itemWidth, dapp: item, @@ -107,7 +110,7 @@ List getList(List dapps, DAppsPagePresenter actions, onTap: state.isEditMode ? null : () => actions.openDapp(item.url), mainAxisCount: mainAxisCount, ) - : NewDAppCard( + : DAppCard( index: i, width: itemWidth, dapp: item, diff --git a/lib/features/dapps/presentation/responsive_layout/dapp_loading.dart b/lib/features/dapps/presentation/responsive_layout/dapp_loading.dart index 724cda7e..e3844fd2 100644 --- a/lib/features/dapps/presentation/responsive_layout/dapp_loading.dart +++ b/lib/features/dapps/presentation/responsive_layout/dapp_loading.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:mxc_ui/mxc_ui.dart'; import 'package:shimmer/shimmer.dart'; -import 'card_item.dart'; +import 'dapps_layout/card_item.dart'; class DAppLoading extends StatelessWidget { const DAppLoading({ diff --git a/lib/features/dapps/presentation/responsive_layout/dapp_utils.dart b/lib/features/dapps/presentation/responsive_layout/dapp_utils.dart index 56e9b393..2d1016bf 100644 --- a/lib/features/dapps/presentation/responsive_layout/dapp_utils.dart +++ b/lib/features/dapps/presentation/responsive_layout/dapp_utils.dart @@ -2,7 +2,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:mxc_logic/mxc_logic.dart'; -import 'card_item.dart'; +import 'dapps_layout/card_item.dart'; class DappUtils { static bool loadingOnce = true; @@ -25,7 +25,9 @@ class DappUtils { if (e is Bookmark) { return true; } else { - return (e.store!.chainid == chainId) && + return (!MXCChains.isMXCChains(chainId) + ? MXCChains.isMXCMainnet(e.store!.chainid!) + : e.store!.chainid == chainId) && isSupported(e.app!.supportedPlatforms!); } }).toList(); @@ -44,8 +46,8 @@ class DappUtils { // Sort the DApps list based on the order specified in dappsOrder dapps.sort((a, b) { - final aUrl = a is Bookmark ? a.url : a.app!.url!; - final bUrl = b is Bookmark ? b.url : b.app!.url!; + final aUrl = a is Bookmark ? a.url : a.app!.url!; + final bUrl = b is Bookmark ? b.url : b.app!.url!; int indexA = urlIndices[aUrl] ?? dapps.length; int indexB = urlIndices[bUrl] ?? dapps.length; return dappsOrder.indexOf(aUrl) - dappsOrder.indexOf(bUrl); diff --git a/lib/features/dapps/presentation/responsive_layout/dapps_layout/build_card.dart b/lib/features/dapps/presentation/responsive_layout/dapps_layout/build_card.dart new file mode 100644 index 00000000..c72e5fe9 --- /dev/null +++ b/lib/features/dapps/presentation/responsive_layout/dapps_layout/build_card.dart @@ -0,0 +1,133 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:mxc_logic/mxc_logic.dart'; +import 'package:mxc_ui/mxc_ui.dart'; + +import '../../dapps_presenter.dart'; +import 'card_item.dart'; + +Widget buildCard( + BuildContext context, + Dapp dapp, + int mainAxisCount, + VoidCallback? onTap, + bool isEditMode, + double width, { + double? ratioFactor, + DAppsPagePresenter? actions, + void Function()? shatter, + bool animated = false, +}) { + + final isMobile = mainAxisCount == CardMainAxisCount.mobile; + final imageRatioFactor = (isMobile ? 0.2 : 0.1); + String? image; + if (dapp is Bookmark) { + if ((dapp as Bookmark).image != null) { + image = (dapp as Bookmark).image!; + } else { + actions!.updateBookmarkFavIcon(dapp as Bookmark); + } + } else { + image = dapp.reviewApi!.icon!; + } + final name = dapp is Bookmark ? (dapp as Bookmark).title : dapp.app!.name!; + final imageSize = width * (ratioFactor ?? imageRatioFactor); + return GestureDetector( + onTap: () { + if (animated) { + Navigator.pop(context); + Future.delayed( + const Duration(milliseconds: 500), + () => onTap!(), + ); + } else if (onTap != null) { + onTap(); + } + }, + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Stack( + clipBehavior: Clip.none, + children: [ + Container( + padding: const EdgeInsets.all(Sizes.spaceLarge), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(15)), + gradient: LinearGradient( + colors: [ + ColorsTheme.of(context).textBlack100, + ColorsTheme.of(context).iconBlack200, + ], + begin: AlignmentDirectional.bottomEnd, + end: AlignmentDirectional.topStart, + ), + ), + child: SizedBox( + width: imageSize, + height: imageSize, + child: image == null + ? Icon( + Icons.image_not_supported_rounded, + color: ColorsTheme.of(context).textPrimary, + ) + : image.contains('https') && dapp is Bookmark + ? CachedNetworkImage( + imageUrl: image, + fit: BoxFit.cover, + errorWidget: (context, url, error) { + return Column( + children: [ + Icon( + Icons.image_not_supported_outlined, + color: ColorsTheme.of(context).textError, + ), + const SizedBox( + height: Sizes.spaceXSmall, + ), + ], + ); + }, + ) + : image.contains('https') + ? SvgPicture.network( + image, + ) + : SvgPicture.asset( + image, + ), + ), + ), + if (isEditMode && dapp is Bookmark) + Positioned( + top: -6, + left: -6, + child: GestureDetector( + onTap: () => + actions!.removeBookmarkDialog(dapp as Bookmark, shatter!), + child: const Icon( + Icons.remove_circle_rounded, + ), + ), + ), + ], + ), + const SizedBox( + height: Sizes.space2XSmall, + ), + Text( + name, + style: FontTheme.of(context) + .caption1 + .primary() + .copyWith(fontWeight: FontWeight.w700), + softWrap: false, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ); +} diff --git a/lib/features/dapps/presentation/responsive_layout/dapps_layout/card_item.dart b/lib/features/dapps/presentation/responsive_layout/dapps_layout/card_item.dart new file mode 100644 index 00000000..b259876b --- /dev/null +++ b/lib/features/dapps/presentation/responsive_layout/dapps_layout/card_item.dart @@ -0,0 +1,9 @@ +class CardCrossAxisCount { + static const int mobile = 6; + static const int tablet = 7; +} + +class CardMainAxisCount { + static const int mobile = 4; + static const int tablet = 5; +} diff --git a/lib/features/dapps/presentation/responsive_layout/dapps_layout/context_menu_actions.dart b/lib/features/dapps/presentation/responsive_layout/dapps_layout/context_menu_actions.dart new file mode 100644 index 00000000..a088f053 --- /dev/null +++ b/lib/features/dapps/presentation/responsive_layout/dapps_layout/context_menu_actions.dart @@ -0,0 +1,120 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_i18n/flutter_i18n.dart'; +import 'package:mxc_logic/mxc_logic.dart'; +import 'package:mxc_ui/mxc_ui.dart'; + +import '../../dapps_presenter.dart'; + +getContextMenuActions( + DAppsPagePresenter actions, + BuildContext context, + Dapp dapp, + void Function()? shatter, +) => + dapp is Bookmark? + ? getBookMarkContextMenuAction( + actions, + context, + dapp, + shatter!, + ) + : getDAppMarkContextMenuAction( + actions, + context, + dapp, + ); + +List getDAppMarkContextMenuAction( + DAppsPagePresenter actions, + BuildContext context, + Dapp dapp, +) => + [ + CupertinoContextMenuAction( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + FlutterI18n.translate(context, 'about'), + style: FontTheme.of(context) + .caption1 + .primary() + .copyWith(fontWeight: FontWeight.w700), + ), + Text( + dapp.app!.description!, + style: FontTheme.of(context).caption1.primary(), + ), + ], + )), + CupertinoContextMenuAction( + trailingIcon: Icons.phone_iphone_rounded, + child: Text(FlutterI18n.translate(context, 'edit_home_screen'), + style: FontTheme.of(context).subtitle1()), + onPressed: () => popWrapper(actions.changeEditMode, context)), + CupertinoContextMenuAction( + trailingIcon: Icons.add_circle_outline_rounded, + child: Text(FlutterI18n.translate(context, 'add_new_dapp'), + style: FontTheme.of(context).subtitle1()), + onPressed: () => popWrapper(actions.addBookmark, context)), + ]; + +getBookMarkContextMenuAction( + DAppsPagePresenter actions, + BuildContext context, + Dapp dapp, + void Function() shatter, +) => + [ + CupertinoContextMenuAction( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + FlutterI18n.translate(context, 'about'), + style: FontTheme.of(context) + .caption1 + .primary() + .copyWith(fontWeight: FontWeight.w700), + ), + Text( + getDappAbout(dapp), + style: FontTheme.of(context).caption1.primary(), + ), + ], + )), + CupertinoContextMenuAction( + trailingIcon: Icons.phone_iphone_rounded, + child: Text(FlutterI18n.translate(context, 'edit_home_screen'), + style: FontTheme.of(context).body1()), + onPressed: () => popWrapper(actions.changeEditMode, context)), + CupertinoContextMenuAction( + trailingIcon: Icons.add_circle_outline_rounded, + child: Text(FlutterI18n.translate(context, 'add_new_dapp'), + style: FontTheme.of(context).body1()), + onPressed: () => popWrapper(actions.addBookmark, context)), + CupertinoContextMenuAction( + isDestructiveAction: true, + trailingIcon: Icons.remove_circle_outline_rounded, + onPressed: () => popWrapper(() async { + actions.removeBookmarkDialog(dapp as Bookmark, shatter); + }, context), + child: Text(FlutterI18n.translate(context, 'remove_dapp'), + style: FontTheme.of(context).body1Cl())) + ]; + +void popWrapper(void Function()? func, BuildContext context) { + Navigator.pop(context); + Future.delayed( + const Duration(milliseconds: 500), + () => {if (func != null) func()}, + ); +} + +String getDappAbout( + Dapp dapp, +) { + final dappAbout = dapp is Bookmark ? (dapp).title : dapp.app!.description!; + return dappAbout; +} diff --git a/lib/features/dapps/presentation/responsive_layout/dapps_layout/dapp_card.dart b/lib/features/dapps/presentation/responsive_layout/dapps_layout/dapp_card.dart new file mode 100644 index 00000000..4b957c18 --- /dev/null +++ b/lib/features/dapps/presentation/responsive_layout/dapps_layout/dapp_card.dart @@ -0,0 +1,126 @@ +import 'dart:math'; + +import 'package:datadashwallet/common/components/context_menu_extended.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:mxc_logic/mxc_logic.dart'; +import 'package:reorderable_grid_view/reorderable_grid_view.dart'; +import '../../dapps_presenter.dart'; +import 'build_card.dart'; +import 'context_menu_actions.dart'; +import 'shatter_widget.dart'; +import 'card_item.dart'; + +class DAppCard extends HookConsumerWidget { + final Dapp dapp; + final int index; + final double width; + final bool isEditMode; + final VoidCallback? onTap; + final int mainAxisCount; + const DAppCard({ + super.key, + required this.index, + required this.width, + required this.dapp, + required this.isEditMode, + required this.onTap, + required this.mainAxisCount, + }); + + @override + Widget build( + BuildContext context, + WidgetRef ref, + ) { + final actions = ref.read(appsPagePageContainer.actions); + final dappUrl = dapp is Bookmark ? (dapp as Bookmark).url : dapp.app!.url!; + final isBookMark = dapp is Bookmark; + + final isMobile = mainAxisCount == CardMainAxisCount.mobile; + final imageRatioFactor = (isMobile ? 0.2 : 0.1); + final animatedSize = (isMobile ? 0.25 : 0.15); + final sizeLimit = (imageRatioFactor / animatedSize); + + final animationController = useAnimationController( + duration: const Duration(milliseconds: 75), + lowerBound: -pi / 50, + upperBound: pi / 50, + ); + + if (isEditMode) { + animationController.forward(); + } else { + animationController.stop(); + } + + animationController.addStatusListener((status) { + if (status == AnimationStatus.completed) { + animationController.reverse(); + } else if (status == AnimationStatus.dismissed) { + animationController.forward(); + } + }); + + Widget getCardItem({void Function()? shatter}) { + if (isEditMode) { + return ReorderableItemView( + key: Key(dappUrl), + index: index, + child: AnimatedBuilder( + animation: animationController, + builder: (context, child) { + return Transform.rotate( + angle: animationController.value * + (pi / 8), // Adjust the range of rotation + child: child, + ); + }, + child: SizedBox.expand( + child: buildCard( + context, dapp, mainAxisCount, onTap, isEditMode, width, + shatter: shatter, actions: actions)), + ), + ); + } + return CupertinoContextMenuExtended.builder( + builder: (context, animation) { + return SizedBox( + width: MediaQuery.of(context).size.width / (mainAxisCount), + height: MediaQuery.of(context).size.width / (mainAxisCount), + child: buildCard( + context, + dapp, + mainAxisCount, + onTap, + isEditMode, + width, + ratioFactor: animation.value < sizeLimit + ? null + : (animatedSize * animation.value), + shatter: shatter, + actions: actions, + animated: animation.value != 0.0, + ), + ); + }, + actions: getContextMenuActions( + actions, + context, + dapp, + shatter, + ), + ); + } + + return isBookMark + ? ShatteringWidget( + builder: (shatter) { + return getCardItem(shatter: shatter); + }, + onShatterCompleted: () => actions.removeBookmark(dapp as Bookmark)) + : getCardItem(); + } +} diff --git a/lib/features/dapps/presentation/responsive_layout/shatter_widget.dart b/lib/features/dapps/presentation/responsive_layout/dapps_layout/shatter_widget.dart similarity index 100% rename from lib/features/dapps/presentation/responsive_layout/shatter_widget.dart rename to lib/features/dapps/presentation/responsive_layout/dapps_layout/shatter_widget.dart diff --git a/lib/features/dapps/presentation/responsive_layout/new_dapp_card.dart b/lib/features/dapps/presentation/responsive_layout/new_dapp_card.dart deleted file mode 100644 index 2bbadcc3..00000000 --- a/lib/features/dapps/presentation/responsive_layout/new_dapp_card.dart +++ /dev/null @@ -1,318 +0,0 @@ -import 'dart:async'; -import 'dart:math'; - -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:datadashwallet/features/dapps/presentation/responsive_layout/card_item.dart'; -import 'package:datadashwallet/common/components/context_menu_extended.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:mxc_logic/mxc_logic.dart'; -import 'package:mxc_ui/mxc_ui.dart'; -import 'package:reorderable_grid_view/reorderable_grid_view.dart'; -import '../dapps_presenter.dart'; -import 'shatter_widget.dart'; - -class NewDAppCard extends HookConsumerWidget { - final Dapp dapp; - final int index; - final double width; - final bool isEditMode; - final VoidCallback? onTap; - final int mainAxisCount; - const NewDAppCard({ - super.key, - required this.index, - required this.width, - required this.dapp, - required this.isEditMode, - required this.onTap, - required this.mainAxisCount, - }); - - Widget cardBox( - BuildContext context, { - double? ratioFactor, - DAppsPagePresenter? actions, - void Function()? shatter, - bool animated = false, - }) { - String? image; - if (dapp is Bookmark) { - if ((dapp as Bookmark).image != null) { - image = (dapp as Bookmark).image!; - } else { - actions!.updateBookmarkFavIcon(dapp as Bookmark); - } - } else { - image = dapp.reviewApi!.icon!; - } - final name = dapp is Bookmark ? (dapp as Bookmark).title : dapp.app!.name!; - final imageSize = width * - (ratioFactor ?? - (mainAxisCount == CardMainAxisCount.mobile ? 0.3 : 0.2)); - return GestureDetector( - onTap: () { - if (animated) { - Navigator.pop(context); - Future.delayed( - const Duration(milliseconds: 500), - () => onTap!(), - ); - } else if (onTap != null) { - onTap!(); - } - }, - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Stack( - clipBehavior: Clip.none, - children: [ - Container( - padding: const EdgeInsets.all(Sizes.spaceXLarge), - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(15)), - gradient: LinearGradient( - colors: [ - ColorsTheme.of(context).textBlack100, - ColorsTheme.of(context).iconBlack200, - ], - begin: AlignmentDirectional.bottomEnd, - end: AlignmentDirectional.topStart, - ), - ), - child: SizedBox( - width: imageSize, - height: imageSize, - child: image == null - ? Icon( - Icons.image_not_supported_rounded, - color: ColorsTheme.of(context).textPrimary, - ) - : image.contains('https') && dapp is Bookmark - ? CachedNetworkImage( - imageUrl: image, - fit: BoxFit.cover, - errorWidget: (context, url, error) { - return Column( - children: [ - Icon( - Icons.image_not_supported_outlined, - color: ColorsTheme.of(context).textError, - ), - const SizedBox( - height: Sizes.spaceXSmall, - ), - ], - ); - }, - ) - : image.contains('https') - ? SvgPicture.network( - image, - ) - : SvgPicture.asset( - image, - ), - ), - ), - if (isEditMode && dapp is Bookmark) - Positioned( - top: -6, - left: -6, - child: GestureDetector( - onTap: () => actions! - .removeBookmarkDialog(dapp as Bookmark, shatter!), - child: const Icon( - Icons.remove_circle_rounded, - ), - ), - ), - ], - ), - const SizedBox( - height: Sizes.spaceXSmall, - ), - Text( - name, - style: FontTheme.of(context) - .caption1 - .primary() - .copyWith(fontWeight: FontWeight.w700), - softWrap: false, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ); - } - - @override - Widget build( - BuildContext context, - WidgetRef ref, - ) { - final state = ref.watch(appsPagePageContainer.state); - final actions = ref.read(appsPagePageContainer.actions); - final dapps = state.orderedDapps; - final dappAbout = - dapp is Bookmark ? (dapp as Bookmark).title : dapp.app!.description!; - final dappUrl = dapp is Bookmark ? (dapp as Bookmark).url : dapp.app!.url!; - final isBookMark = dapp is Bookmark; - - final animationController = useAnimationController( - duration: const Duration(milliseconds: 75), - lowerBound: -pi / 50, - upperBound: pi / 50, - ); - - if (isEditMode) { - animationController.forward(); - } else { - animationController.stop(); - } - - animationController.addStatusListener((status) { - if (status == AnimationStatus.completed) { - animationController.reverse(); - } else if (status == AnimationStatus.dismissed) { - animationController.forward(); - } - }); - - List getDAppMarkContextMenuAction() => [ - CupertinoContextMenuAction( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - FlutterI18n.translate(context, 'about'), - style: FontTheme.of(context) - .caption1 - .primary() - .copyWith(fontWeight: FontWeight.w700), - ), - Text( - dapp.app!.description!, - style: FontTheme.of(context).caption1.primary(), - ), - ], - )), - CupertinoContextMenuAction( - trailingIcon: Icons.phone_iphone_rounded, - child: Text(FlutterI18n.translate(context, 'edit_home_screen'), - style: FontTheme.of(context).subtitle1()), - onPressed: () => popWrapper(actions.changeEditMode, context)), - CupertinoContextMenuAction( - trailingIcon: Icons.add_circle_outline_rounded, - child: Text(FlutterI18n.translate(context, 'add_new_dapp'), - style: FontTheme.of(context).subtitle1()), - onPressed: () => popWrapper(actions.addBookmark, context)), - ]; - - getBookMarkContextMenuAction(void Function() shatter) => [ - CupertinoContextMenuAction( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - FlutterI18n.translate(context, 'about'), - style: FontTheme.of(context) - .caption1 - .primary() - .copyWith(fontWeight: FontWeight.w700), - ), - Text( - dappAbout, - style: FontTheme.of(context).caption1.primary(), - ), - ], - )), - CupertinoContextMenuAction( - trailingIcon: Icons.phone_iphone_rounded, - child: Text(FlutterI18n.translate(context, 'edit_home_screen'), - style: FontTheme.of(context).body1()), - onPressed: () => popWrapper(actions.changeEditMode, context)), - CupertinoContextMenuAction( - trailingIcon: Icons.add_circle_outline_rounded, - child: Text(FlutterI18n.translate(context, 'add_new_dapp'), - style: FontTheme.of(context).body1()), - onPressed: () => popWrapper(actions.addBookmark, context)), - CupertinoContextMenuAction( - isDestructiveAction: true, - trailingIcon: Icons.remove_circle_outline_rounded, - onPressed: () => popWrapper(() async { - actions.removeBookmarkDialog(dapp as Bookmark, shatter); - }, context), - child: Text(FlutterI18n.translate(context, 'remove_dapp'), - style: FontTheme.of(context).body1Cl())) - ]; - - final size = (mainAxisCount == CardMainAxisCount.mobile ? 0.5 : 0.3); - final sizeLimit = - (mainAxisCount == CardMainAxisCount.mobile ? 0.6000 : 0.6666); - - Widget getCardItem({void Function()? shatter}) { - final contextMenuActions = dapp is Bookmark? - ? getBookMarkContextMenuAction(shatter!) - : getDAppMarkContextMenuAction(); - if (isEditMode) { - return ReorderableItemView( - key: Key(dappUrl), - index: index, - child: AnimatedBuilder( - animation: animationController, - builder: (context, child) { - return Transform.rotate( - angle: animationController.value * - (pi / 8), // Adjust the range of rotation - child: child, - ); - }, - child: SizedBox.expand( - child: cardBox(context, shatter: shatter, actions: actions)), - ), - ); - } - return CupertinoContextMenuExtended.builder( - builder: (context, animation) { - return SizedBox( - width: MediaQuery.of(context).size.width / - (mainAxisCount - animation.value), - height: MediaQuery.of(context).size.width / - (mainAxisCount - animation.value), - child: cardBox(context, - ratioFactor: animation.value < sizeLimit - ? null - : (size * animation.value), - shatter: shatter, - actions: actions, - animated: animation.value != 0.0), - ); - }, - actions: contextMenuActions, - ); - } - - return isBookMark - ? ShatteringWidget( - builder: (shatter) { - return getCardItem(shatter: shatter); - }, - onShatterCompleted: () => actions.removeBookmark(dapp as Bookmark)) - : getCardItem(); - } -} - -void popWrapper(void Function()? func, BuildContext context) { - Navigator.pop(context); - Future.delayed( - const Duration(milliseconds: 500), - () => {if (func != null) func()}, - ); -} diff --git a/lib/features/dapps/presentation/responsive_layout/responsive_layout.dart b/lib/features/dapps/presentation/responsive_layout/responsive_layout.dart index c6071607..05a806f6 100644 --- a/lib/features/dapps/presentation/responsive_layout/responsive_layout.dart +++ b/lib/features/dapps/presentation/responsive_layout/responsive_layout.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:responsive_builder/responsive_builder.dart'; -import 'card_item.dart'; +import 'dapps_layout/card_item.dart'; import 'dapp_card_layout.dart'; class ResponsiveLayout extends StatelessWidget { diff --git a/lib/features/dapps/presentation/widgets/default_app_bar.dart b/lib/features/dapps/presentation/widgets/default_app_bar.dart new file mode 100644 index 00000000..76d398e4 --- /dev/null +++ b/lib/features/dapps/presentation/widgets/default_app_bar.dart @@ -0,0 +1,39 @@ +import 'package:datadashwallet/core/core.dart'; +import 'package:datadashwallet/features/common/common.dart'; +import 'package:datadashwallet/features/settings/presentation/settings_page.dart'; +import 'package:datadashwallet/features/wallet/presentation/wallet_page.dart'; +import 'package:flutter/material.dart'; +import 'package:mxc_ui/mxc_ui.dart'; + + +class DefaultAppBar extends StatelessWidget { + const DefaultAppBar({super.key}); + + @override + Widget build(BuildContext context) { + return AppNavBar( + leading: IconButton( + key: const ValueKey('settingsButton'), + icon: const Icon(MxcIcons.settings), + iconSize: Sizes.space2XLarge, + onPressed: () { + Navigator.of(context).push( + route( + const SettingsPage(), + ), + ); + }, + color: ColorsTheme.of(context).iconPrimary, + ), + action: IconButton( + key: const ValueKey('walletButton'), + icon: const Icon(MxcIcons.wallet), + iconSize: Sizes.space2XLarge, + onPressed: () => Navigator.of(context).replaceAll( + route(const WalletPage()), + ), + color: ColorsTheme.of(context).iconPrimary, + ), + ); + } +} diff --git a/lib/features/dapps/presentation/widgets/edit_mode_app_bar.dart b/lib/features/dapps/presentation/widgets/edit_mode_app_bar.dart new file mode 100644 index 00000000..37d6a2ec --- /dev/null +++ b/lib/features/dapps/presentation/widgets/edit_mode_app_bar.dart @@ -0,0 +1,65 @@ +import 'package:datadashwallet/features/common/common.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_i18n/flutter_i18n.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:mxc_ui/mxc_ui.dart'; + +import '../dapps_presenter.dart'; + +class EditModeAppBar extends HookConsumerWidget { + const EditModeAppBar({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final dappsPresenter = ref.watch(appsPagePageContainer.actions); + return AppNavBar( + leading: EditModeButton( + onTap: dappsPresenter.addBookmark, + child: Icon( + Icons.add, + size: 20, + color: ColorsTheme.of(context).screenBackground, + ), + ), + action: EditModeButton( + onTap: dappsPresenter.changeEditMode, + child: Text( + FlutterI18n.translate(context, 'done'), + style: FontTheme.of(context).subtitle1().copyWith( + color: ColorsTheme.of(context).screenBackground, + fontWeight: FontWeight.w700), + ), + ), + ); + } +} + +class EditModeButton extends StatelessWidget { + const EditModeButton({ + super.key, + required this.child, + this.onTap, + }); + + final Widget child; + final VoidCallback? onTap; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + width: 56, + height: 22, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all( + Radius.circular(30), + ), + color: ColorsTheme.of(context).iconPrimary, + ), + child: child, + ), + ); + } +} diff --git a/lib/features/dapps/presentation/widgets/edit_mode_status_bar.dart b/lib/features/dapps/presentation/widgets/edit_mode_status_bar.dart deleted file mode 100644 index 25e74b18..00000000 --- a/lib/features/dapps/presentation/widgets/edit_mode_status_bar.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; -import 'package:mxc_ui/mxc_ui.dart'; - -class EditAppsModeStatusBar extends StatelessWidget { - const EditAppsModeStatusBar({ - super.key, - this.onAdd, - this.onDone, - }); - - final VoidCallback? onAdd; - final VoidCallback? onDone; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 5), - child: Row( - children: [ - EditModeButton( - onTap: onAdd, - child: Icon( - Icons.add, - size: 20, - color: ColorsTheme.of(context).iconPrimary, - ), - ), - const Spacer(), - EditModeButton( - onTap: onDone, - child: Text( - FlutterI18n.translate(context, 'done'), - style: FontTheme.of(context).subtitle1().copyWith( - color: ColorsTheme.of(context).textPrimary, - fontWeight: FontWeight.w700), - ), - ) - ], - ), - ); - } -} - -class EditModeButton extends StatelessWidget { - const EditModeButton({ - super.key, - required this.child, - this.onTap, - }); - - final Widget child; - final VoidCallback? onTap; - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - child: Container( - width: 56, - height: 22, - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: const BorderRadius.all( - Radius.circular(30), - ), - color: ColorsTheme.of(context).grey3, - ), - child: child, - ), - ); - } -} diff --git a/lib/features/portfolio/subfeatures/token/send_token/send_crypto/send_crypto_page.dart b/lib/features/portfolio/subfeatures/token/send_token/send_crypto/send_crypto_page.dart index 0f255228..7f13fb4b 100644 --- a/lib/features/portfolio/subfeatures/token/send_token/send_crypto/send_crypto_page.dart +++ b/lib/features/portfolio/subfeatures/token/send_token/send_crypto/send_crypto_page.dart @@ -2,7 +2,6 @@ import 'package:datadashwallet/common/common.dart'; import 'package:datadashwallet/core/core.dart'; import 'package:datadashwallet/features/settings/subfeatures/address_book/address_book.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:mxc_logic/mxc_logic.dart'; diff --git a/lib/features/portfolio/subfeatures/token/send_token/send_crypto/send_crypto_presenter.dart b/lib/features/portfolio/subfeatures/token/send_token/send_crypto/send_crypto_presenter.dart index 2053b8fe..797636a8 100644 --- a/lib/features/portfolio/subfeatures/token/send_token/send_crypto/send_crypto_presenter.dart +++ b/lib/features/portfolio/subfeatures/token/send_token/send_crypto/send_crypto_presenter.dart @@ -5,9 +5,6 @@ import 'package:datadashwallet/common/utils/utils.dart'; import 'package:datadashwallet/core/core.dart'; import 'package:datadashwallet/features/common/common.dart'; import 'package:datadashwallet/features/common/app_nav_bar/app_nav_bar_presenter.dart'; -import 'package:datadashwallet/features/dapps/dapps.dart'; -import 'package:ens_dart/ens_dart.dart'; -import 'package:web3dart/web3dart.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:mxc_logic/mxc_logic.dart'; @@ -149,6 +146,7 @@ class SendCryptoPresenter extends CompletePresenter { } void transactionProcess() async { + loading = true; final amount = amountController.text; final recipient = recipientController.text; String recipientAddress = await getAddress(recipient); diff --git a/lib/features/settings/subfeatures/accounts/portrait.dart b/lib/features/settings/subfeatures/accounts/portrait.dart index 7314e461..c40bd5c3 100644 --- a/lib/features/settings/subfeatures/accounts/portrait.dart +++ b/lib/features/settings/subfeatures/accounts/portrait.dart @@ -13,7 +13,7 @@ class Portrait extends StatelessWidget { @override Widget build(BuildContext context) { return CircleAvatar( - radius: 16, + radius: 14, child: SvgPicture.string( Jdenticon.toSvg(name), fit: BoxFit.contain, diff --git a/lib/features/splash/create_storage/presentation/create_storage_page.dart b/lib/features/splash/create_storage/presentation/create_storage_page.dart index 7901a171..d8846ee0 100644 --- a/lib/features/splash/create_storage/presentation/create_storage_page.dart +++ b/lib/features/splash/create_storage/presentation/create_storage_page.dart @@ -29,6 +29,13 @@ class SplashStoragePage extends SplashBasePage { @override List setButtons(BuildContext context, WidgetRef ref) { + final isEmailAvailable = ref.watch(state).isEmailAppAvailable == true; + final isTelegramAvailable = ref.watch(state).applist['telegram'] == true || + ref.watch(state).applist['telegram_web'] == true; + final isWeChatAvailable = ref.watch(state).applist['weixin'] == true || + ref.watch(state).applist['wechat'] == true; + final isNoneAvailable = + !(isEmailAvailable || isTelegramAvailable || isWeChatAvailable); return [ MxcButton.secondaryWhite( key: const ValueKey('telegramButton'), @@ -36,8 +43,7 @@ class SplashStoragePage extends SplashBasePage { iconSize: 32, titleSize: 18, title: FlutterI18n.translate(context, 'telegram_secured_storage'), - onTap: ref.watch(state).applist['telegram'] == true || - ref.watch(state).applist['telegram_web'] == true + onTap: isTelegramAvailable ? () => Navigator.of(context).push( route.featureDialog( TelegramRecoveryPhrasePage( @@ -53,8 +59,7 @@ class SplashStoragePage extends SplashBasePage { iconSize: 32, titleSize: 18, title: FlutterI18n.translate(context, 'wechat_secured_storage'), - onTap: ref.watch(state).applist['weixin'] == true || - ref.watch(state).applist['wechat'] == true + onTap: isWeChatAvailable ? () => Navigator.of(context).push( route.featureDialog( WechatRecoveryPhrasePage( @@ -70,7 +75,7 @@ class SplashStoragePage extends SplashBasePage { iconSize: 32, titleSize: 18, title: FlutterI18n.translate(context, 'email_secured_storage'), - onTap: ref.watch(state).isEmailAppAvailable == true + onTap: isEmailAvailable ? () => Navigator.of(context).push( route.featureDialog( EmailRecoveryPhrasePage( @@ -80,6 +85,22 @@ class SplashStoragePage extends SplashBasePage { ) : null, ), + !isNoneAvailable + ? MxcButton.secondaryWhite( + key: const ValueKey('localButton'), + icon: Icons.file_download_rounded, + iconSize: 32, + titleSize: 18, + title: FlutterI18n.translate(context, 'local_secured_storage'), + onTap: () => Navigator.of(context).push( + route.featureDialog( + LocalRecoveryPhrasePage( + settingsFlow: settingsFlow, + ), + ), + ), + ) + : Container(), ]; } } diff --git a/lib/features/splash/import_storage/import_storage_page.dart b/lib/features/splash/import_storage/import_storage_page.dart index 392a6d63..06efb43a 100644 --- a/lib/features/splash/import_storage/import_storage_page.dart +++ b/lib/features/splash/import_storage/import_storage_page.dart @@ -25,6 +25,11 @@ class SplashImportStoragePage extends SplashBasePage { @override List setButtons(BuildContext context, WidgetRef ref) { + final isTelegramAvailable = ref.watch(state).applist['telegram'] == true || + ref.watch(state).applist['telegram_web'] == true; + final isWeChatAvailable = ref.watch(state).applist['weixin'] == true || + ref.watch(state).applist['wechat'] == true; + final isNoneAvailable = !(isTelegramAvailable || isWeChatAvailable); return [ MxcButton.secondaryWhite( key: const ValueKey('telegramButton'), @@ -32,8 +37,7 @@ class SplashImportStoragePage extends SplashBasePage { iconSize: 32, titleSize: 18, title: FlutterI18n.translate(context, 'telegram_secured_storage'), - onTap: ref.watch(state).applist['telegram'] == true || - ref.watch(state).applist['telegram_web'] == true + onTap: isTelegramAvailable ? () => ref.read(presenter).openTelegram() : null, ), @@ -43,18 +47,9 @@ class SplashImportStoragePage extends SplashBasePage { iconSize: 32, titleSize: 18, title: FlutterI18n.translate(context, 'wechat_secured_storage'), - onTap: ref.watch(state).applist['weixin'] == true || - ref.watch(state).applist['wechat'] == true - ? () => ref.read(presenter).openWechat() - : null, + onTap: + isWeChatAvailable ? () => ref.read(presenter).openWechat() : null, ), - // MxcButton.secondaryWhite( - // key: const ValueKey('emailButton'), - // icon: MxcIcons.email, - // iconSize: 20, - // title: FlutterI18n.translate(context, 'email_secured_storage'), - // onTap: () => ref.read(presenter).openEmail(), - // ), MxcButton.secondaryWhite( key: const ValueKey('mnemonicButton'), icon: MxcIcons.cloud, @@ -67,6 +62,16 @@ class SplashImportStoragePage extends SplashBasePage { ), ), ), + !isNoneAvailable + ? MxcButton.secondaryWhite( + key: const ValueKey('localButton'), + icon: Icons.file_download_rounded, + iconSize: 32, + titleSize: 18, + title: FlutterI18n.translate(context, 'local_secured_storage'), + onTap: () => ref.read(presenter).openLocalSeedPhrase(), + ) + : Container() ]; } } diff --git a/lib/features/splash/import_storage/import_storage_presenter.dart b/lib/features/splash/import_storage/import_storage_presenter.dart index dccbb8ba..0428c5a4 100644 --- a/lib/features/splash/import_storage/import_storage_presenter.dart +++ b/lib/features/splash/import_storage/import_storage_presenter.dart @@ -1,8 +1,8 @@ import 'package:datadashwallet/core/core.dart'; import 'package:datadashwallet/features/splash/splash.dart'; -import 'package:flutter/material.dart'; +import 'package:mxc_logic/mxc_logic.dart'; +import 'package:open_file_manager/open_file_manager.dart'; import 'package:open_mail_app/open_mail_app.dart'; -import 'package:url_launcher/url_launcher.dart'; final splashImportStorageContainer = PresenterContainer( @@ -13,6 +13,7 @@ class SplashImportStoragePresenter SplashImportStoragePresenter() : super(SplashBaseState()); late final _launcherUseCase = ref.read(launcherUseCaseProvider); + late final _directoryUseCase = ref.read(directoryUseCaseProvider); @override void initState() { @@ -48,4 +49,19 @@ class SplashImportStoragePresenter loading = false; } } + + Future openLocalSeedPhrase() async { + const selectedFolderType = FolderType.download; + + await _directoryUseCase.checkDownloadsDirectoryDirectory(); + + await openFileManager( + androidConfig: AndroidConfig( + folderType: selectedFolderType, + ), + iosConfig: IosConfig( + subFolderPath: 'Downloads', + ), + ); + } } diff --git a/lib/features/splash/secure_recovery_phrase/presentation/email_recovery_phrase_alert/email_recovery_phrase_page.dart b/lib/features/splash/secure_recovery_phrase/presentation/email_recovery_phrase_alert/email_recovery_phrase_page.dart index ab751f58..e23a5bac 100644 --- a/lib/features/splash/secure_recovery_phrase/presentation/email_recovery_phrase_alert/email_recovery_phrase_page.dart +++ b/lib/features/splash/secure_recovery_phrase/presentation/email_recovery_phrase_alert/email_recovery_phrase_page.dart @@ -31,7 +31,7 @@ class EmailRecoveryPhrasePage extends RecoveryPhraseBasePage { ); @override - Color themeColor() => const Color(0xFFE64340); + Color themeColor({BuildContext? context}) => const Color(0xFFE64340); @override Widget buildAlert(BuildContext context) { diff --git a/lib/features/splash/secure_recovery_phrase/presentation/local_seed_phrase_alert/local_recovery_phrase_page.dart b/lib/features/splash/secure_recovery_phrase/presentation/local_seed_phrase_alert/local_recovery_phrase_page.dart new file mode 100644 index 00000000..58ca6c00 --- /dev/null +++ b/lib/features/splash/secure_recovery_phrase/presentation/local_seed_phrase_alert/local_recovery_phrase_page.dart @@ -0,0 +1,86 @@ +import 'package:datadashwallet/features/splash/secure_recovery_phrase/secure_recovery_phrase.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_i18n/flutter_i18n.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:mxc_ui/mxc_ui.dart'; + +import 'local_recovery_phrase_presenter.dart'; +import 'local_recovery_phrase_state.dart'; + +class LocalRecoveryPhrasePage extends RecoveryPhraseBasePage { + const LocalRecoveryPhrasePage({ + Key? key, + this.settingsFlow = false, + }) : super(key: key); + + final bool settingsFlow; + + @override + ProviderBase get presenter => + emailRecoveryPhraseContainer.actions; + + @override + ProviderBase get state => + emailRecoveryPhraseContainer.state; + + @override + Widget icon(BuildContext context) => Icon( + Icons.file_download_rounded, + size: 40, + color: themeColor(context: context), + ); + + @override + Color themeColor({BuildContext? context}) => ColorsTheme.of(context!).primary; + + @override + Widget buildAlert(BuildContext context) { + return Container( + width: double.infinity, + alignment: Alignment.center, + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: ColorsTheme.of(context).cardBackground, + borderRadius: const BorderRadius.all(Radius.circular(10)), + ), + child: Column( + children: [ + Container( + width: 72, + height: 72, + padding: const EdgeInsets.all(18), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: themeColor(context: context), + ), + child: const Icon( + Icons.file_download_rounded, + color: Colors.white, + ), + ), + const SizedBox(height: 12), + Text( + FlutterI18n.translate(context, 'save_locally_description'), + style: FontTheme.of(context).body1().copyWith( + fontWeight: FontWeight.w500, + color: ColorsTheme.of(context).textPrimary, + ), + textAlign: TextAlign.center, + ), + ], + ), + ); + } + + @override + Widget? buildFooter(BuildContext context, WidgetRef ref) => MxcButton.primary( + key: const ValueKey('saveLocallyButton'), + title: FlutterI18n.translate(context, 'save_locally'), + titleColor: ColorsTheme.of(context).textBlack200, + color: themeColor(context: context), + borderColor: themeColor(context: context), + onTap: () => ref.read(presenter).saveLocally( + settingsFlow, + ), + ); +} diff --git a/lib/features/splash/secure_recovery_phrase/presentation/local_seed_phrase_alert/local_recovery_phrase_presenter.dart b/lib/features/splash/secure_recovery_phrase/presentation/local_seed_phrase_alert/local_recovery_phrase_presenter.dart new file mode 100644 index 00000000..10f53451 --- /dev/null +++ b/lib/features/splash/secure_recovery_phrase/presentation/local_seed_phrase_alert/local_recovery_phrase_presenter.dart @@ -0,0 +1,13 @@ +import 'package:datadashwallet/core/core.dart'; +import 'package:datadashwallet/features/splash/secure_recovery_phrase/secure_recovery_phrase.dart'; + +import 'local_recovery_phrase_state.dart'; + +final emailRecoveryPhraseContainer = + PresenterContainer( + () => LocalRecoveryPhrasePresenter()); + +class LocalRecoveryPhrasePresenter + extends RecoveryPhraseBasePresenter { + LocalRecoveryPhrasePresenter() : super(LocalRecoveryPhraseState()); +} diff --git a/lib/features/splash/secure_recovery_phrase/presentation/local_seed_phrase_alert/local_recovery_phrase_state.dart b/lib/features/splash/secure_recovery_phrase/presentation/local_seed_phrase_alert/local_recovery_phrase_state.dart new file mode 100644 index 00000000..8ffe503d --- /dev/null +++ b/lib/features/splash/secure_recovery_phrase/presentation/local_seed_phrase_alert/local_recovery_phrase_state.dart @@ -0,0 +1,10 @@ +import 'package:equatable/equatable.dart'; +import 'package:datadashwallet/features/splash/secure_recovery_phrase/secure_recovery_phrase.dart'; + +class LocalRecoveryPhraseState extends RecoveryPhraseBaseState + with EquatableMixin { + @override + List get props => [ + super.props, + ]; +} diff --git a/lib/features/splash/secure_recovery_phrase/presentation/recovery_phrase_base/recovery_phrase_base_page.dart b/lib/features/splash/secure_recovery_phrase/presentation/recovery_phrase_base/recovery_phrase_base_page.dart index 135097b0..bd967a17 100644 --- a/lib/features/splash/secure_recovery_phrase/presentation/recovery_phrase_base/recovery_phrase_base_page.dart +++ b/lib/features/splash/secure_recovery_phrase/presentation/recovery_phrase_base/recovery_phrase_base_page.dart @@ -14,7 +14,7 @@ abstract class RecoveryPhraseBasePage extends HookConsumerWidget { ProviderBase get state; Widget icon(BuildContext context); - Color themeColor(); + Color themeColor({BuildContext? context}); Widget buildAppBar(BuildContext context, WidgetRef ref) => MxcAppBar.close(text: ''); diff --git a/lib/features/splash/secure_recovery_phrase/presentation/recovery_phrase_base/recovery_phrase_base_presenter.dart b/lib/features/splash/secure_recovery_phrase/presentation/recovery_phrase_base/recovery_phrase_base_presenter.dart index 6dc636d2..b19afe59 100644 --- a/lib/features/splash/secure_recovery_phrase/presentation/recovery_phrase_base/recovery_phrase_base_presenter.dart +++ b/lib/features/splash/secure_recovery_phrase/presentation/recovery_phrase_base/recovery_phrase_base_presenter.dart @@ -3,7 +3,6 @@ import 'dart:io'; import 'package:appinio_social_share/appinio_social_share.dart'; import 'package:datadashwallet/common/common.dart'; import 'package:datadashwallet/core/core.dart'; -import 'package:datadashwallet/features/security/security.dart'; import 'package:datadashwallet/features/splash/secure_recovery_phrase/secure_recovery_phrase.dart'; import 'package:datadashwallet/features/splash/splash.dart'; import 'package:flutter/material.dart'; @@ -136,4 +135,12 @@ abstract class RecoveryPhraseBasePresenter } } } + + void saveLocally(bool settingsFlow) async { + final mnemonic = settingsFlow + ? _accountUseCase.getMnemonic()! + : _authUseCase.generateMnemonic(); + await _authUseCase.saveMnemonicLocally(mnemonic); + nextProcess(settingsFlow, mnemonic); + } } diff --git a/lib/features/splash/secure_recovery_phrase/presentation/telegram_recovery_phrase_alert/telegram_recovery_phrase_page.dart b/lib/features/splash/secure_recovery_phrase/presentation/telegram_recovery_phrase_alert/telegram_recovery_phrase_page.dart index 9d222c7e..94a9bd8b 100644 --- a/lib/features/splash/secure_recovery_phrase/presentation/telegram_recovery_phrase_alert/telegram_recovery_phrase_page.dart +++ b/lib/features/splash/secure_recovery_phrase/presentation/telegram_recovery_phrase_alert/telegram_recovery_phrase_page.dart @@ -35,7 +35,7 @@ class TelegramRecoveryPhrasePage extends RecoveryPhraseBasePage { FlutterI18n.translate(context, 'saved_messages'); @override - Color themeColor() => const Color(0xFF37AEE2); + Color themeColor({BuildContext? context}) => const Color(0xFF37AEE2); @override Widget buildAlert(BuildContext context) { diff --git a/lib/features/splash/secure_recovery_phrase/presentation/wechat_recovery_phrase_alert/wechat_recovery_phrase_page.dart b/lib/features/splash/secure_recovery_phrase/presentation/wechat_recovery_phrase_alert/wechat_recovery_phrase_page.dart index 262a7b36..a7aba134 100644 --- a/lib/features/splash/secure_recovery_phrase/presentation/wechat_recovery_phrase_alert/wechat_recovery_phrase_page.dart +++ b/lib/features/splash/secure_recovery_phrase/presentation/wechat_recovery_phrase_alert/wechat_recovery_phrase_page.dart @@ -37,7 +37,7 @@ class WechatRecoveryPhrasePage extends RecoveryPhraseBasePage { FlutterI18n.translate(context, 'wechat_favorites'); @override - Color themeColor() => const Color(0xFF09BB07); + Color themeColor({BuildContext? context}) => const Color(0xFF09BB07); @override Widget buildAlert(BuildContext context) { diff --git a/lib/features/splash/secure_recovery_phrase/secure_recovery_phrase.dart b/lib/features/splash/secure_recovery_phrase/secure_recovery_phrase.dart index 1e0a27c5..648cb34f 100644 --- a/lib/features/splash/secure_recovery_phrase/secure_recovery_phrase.dart +++ b/lib/features/splash/secure_recovery_phrase/secure_recovery_phrase.dart @@ -5,5 +5,6 @@ export 'presentation/recovery_phrase_base/recovery_phrase_base_state.dart'; export 'presentation/telegram_recovery_phrase_alert/telegram_recovery_phrase_page.dart'; export 'presentation/wechat_recovery_phrase_alert/wechat_recovery_phrase_page.dart'; export 'presentation/email_recovery_phrase_alert/email_recovery_phrase_page.dart'; +export 'presentation/local_seed_phrase_alert/local_recovery_phrase_page.dart'; export 'presentation/widgets/scale_animation.dart'; diff --git a/lib/main.dart b/lib/main.dart index c6646b5a..c794d0be 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -49,6 +49,7 @@ void main() { final isLoggedIn = authUseCase.loggedIn; await Biometric.load(); + Paint.enableDithering = true; final appVersionUseCase = container.read(appVersionUseCaseProvider); await appVersionUseCase.checkLatestVersion(); diff --git a/packages/open_file_manager/.gitignore b/packages/open_file_manager/.gitignore new file mode 100644 index 00000000..96486fd9 --- /dev/null +++ b/packages/open_file_manager/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/packages/open_file_manager/.metadata b/packages/open_file_manager/.metadata new file mode 100644 index 00000000..29c9cb0b --- /dev/null +++ b/packages/open_file_manager/.metadata @@ -0,0 +1,33 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a + channel: stable + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a + base_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a + - platform: android + create_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a + base_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a + - platform: ios + create_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a + base_revision: 6928314d505d2bb4777be05e45d7808a5aa91d2a + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/open_file_manager/CHANGELOG.md b/packages/open_file_manager/CHANGELOG.md new file mode 100644 index 00000000..4eafed0d --- /dev/null +++ b/packages/open_file_manager/CHANGELOG.md @@ -0,0 +1,29 @@ +## 1.0.2 + +- Updated `Flutter linter` & `Dart analysis`. + +## 1.0.1 + +- Added videos in `Preview` + +## 1.0.0 + +- Added support of opening `Recent` folder in `Android` and any `sub-folder` inside app's document in `iOS` + +## 0.0.4 + +- Upload preview + +## 0.0.3 + +- Upgrade platform related files. +- Upgrade kotlin version to "1.9.10" +- Upgrade iOS deployment target to "12" + +## 0.0.2 + +- Update description + +## 0.0.1 + +- Initial release. \ No newline at end of file diff --git a/packages/open_file_manager/LICENSE b/packages/open_file_manager/LICENSE new file mode 100644 index 00000000..170de978 --- /dev/null +++ b/packages/open_file_manager/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Aubergine Solutions Pvt. Ltd. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/open_file_manager/README.md b/packages/open_file_manager/README.md new file mode 100644 index 00000000..ae422403 --- /dev/null +++ b/packages/open_file_manager/README.md @@ -0,0 +1,51 @@ +A flutter plugin to open the default file manager app. + +## Support +| | Android | iOS | +|-------------|---------|---------| +| **Support** | SDK 20+ | iOS 12+ | + +## How it works? + +### Android +The `Android` app can open either `Recent` folder or `Download` folder. +The plugin will show the available file manager apps in the bottom popup and you can select one app to open. +That app will open with the given folder in selected app. + +### iOS +The `iOS` app can open app's document folder and it's sub directory if provided. +Plugin will open the `Files` app in iOS. You need to add the following code snippet in `Info.plist` to view your app's document folder inside `On My iPhone`. +Also, you need to save at least one file to view your app's folder. + +```xml +UISupportsDocumentBrowser + +``` + +## Usage + +It's a very simple to use. Just call the below method and add `config` if required. + + ```dart + import 'package:open_file_manager/open_file_manager.dart' + +openFileManager( + androidConfig: AndroidConfig( + folderType: FolderType.recent, + ), + iosConfig: IosConfig( + // Path is case-sensitive here. + subFolderPath: 'Pictures/Screenshots', + ), +); + ``` + + - If `androidConfig` doesn't provided, Android app will open `Download` folder by default. + - If `iosConfig` doesn't provided, iOS app will open app's document folder by default. + + +## Preview + +https://github.com/nayanAubie/open_file_manager/assets/109264909/57ecd7be-a526-45d8-a138-7ca7dcd255ca + +https://github.com/nayanAubie/open_file_manager/assets/109264909/862bf858-49f1-4c4d-8c8e-22cbafb1d33c diff --git a/packages/open_file_manager/analysis_options.yaml b/packages/open_file_manager/analysis_options.yaml new file mode 100644 index 00000000..999f1b72 --- /dev/null +++ b/packages/open_file_manager/analysis_options.yaml @@ -0,0 +1,66 @@ +include: package:flutter_lints/flutter.yaml + +linter: + rules: + use_enums: true + close_sinks: true + use_colored_box: true + use_raw_strings: true + unnecessary_late: true + avoid_void_async: true + throw_in_finally: true + use_decorated_box: true + use_named_constants: true + prefer_final_locals: true + cascade_invocations: true + directives_ordering: true + flutter_style_todos: true + unnecessary_lambdas: true + secure_pubspec_urls: true + use_super_parameters: true + cancel_subscriptions: true + prefer_single_quotes: true + sort_pub_dependencies: true + parameter_assignments: true + do_not_use_environment: true + avoid_final_parameters: true + unnecessary_statements: true + sized_box_shrink_expand: true + unnecessary_parenthesis: true + prefer_relative_imports: true + unnecessary_raw_strings: true + require_trailing_commas: true + prefer_if_null_operators: true + prefer_final_in_for_each: true + use_test_throws_matchers: true + type_annotate_public_apis: true + omit_local_variable_types: true + use_to_and_as_if_applicable: true + prefer_asserts_with_message: true + avoid_escaping_inner_quotes: true + join_return_with_assignment: true + no_adjacent_strings_in_list: true + unnecessary_await_in_return: true + avoid_double_and_int_checks: true + depend_on_referenced_packages: false + conditional_uri_does_not_exist: true + use_is_even_rather_than_modulo: true + prefer_null_aware_method_calls: true + avoid_redundant_argument_values: true + literal_only_boolean_expressions: true + use_setters_to_change_properties: true + avoid_types_on_closure_parameters: true + prefer_asserts_in_initializer_lists: true + avoid_positional_boolean_parameters: true + avoid_unused_constructor_parameters: true + use_if_null_to_convert_nulls_to_bools: true + prefer_constructors_over_static_methods: true + use_late_for_private_fields_and_variables: true + avoid_field_initializers_in_const_classes: true + no_leading_underscores_for_library_prefixes: true + prefer_function_declarations_over_variables: true + no_leading_underscores_for_local_identifiers: true + avoid_equals_and_hash_code_on_mutable_classes: true + prefer_if_elements_to_conditional_expressions: true + avoid_bool_literals_in_conditional_expressions: true + diff --git a/packages/open_file_manager/android/.gitignore b/packages/open_file_manager/android/.gitignore new file mode 100644 index 00000000..161bdcda --- /dev/null +++ b/packages/open_file_manager/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/packages/open_file_manager/android/build.gradle b/packages/open_file_manager/android/build.gradle new file mode 100644 index 00000000..4c5636b4 --- /dev/null +++ b/packages/open_file_manager/android/build.gradle @@ -0,0 +1,46 @@ +group 'com.aubergine.open_file_manager' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '1.9.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.3.1' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdk 34 + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + minSdkVersion 20 + } +} diff --git a/packages/open_file_manager/android/settings.gradle b/packages/open_file_manager/android/settings.gradle new file mode 100644 index 00000000..25f80433 --- /dev/null +++ b/packages/open_file_manager/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'open_file_manager' diff --git a/packages/open_file_manager/android/src/main/AndroidManifest.xml b/packages/open_file_manager/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..d7599bde --- /dev/null +++ b/packages/open_file_manager/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/packages/open_file_manager/android/src/main/kotlin/com/aubergine/open_file_manager/OpenFileManagerPlugin.kt b/packages/open_file_manager/android/src/main/kotlin/com/aubergine/open_file_manager/OpenFileManagerPlugin.kt new file mode 100644 index 00000000..7cb610a7 --- /dev/null +++ b/packages/open_file_manager/android/src/main/kotlin/com/aubergine/open_file_manager/OpenFileManagerPlugin.kt @@ -0,0 +1,65 @@ +package com.aubergine.open_file_manager + +import android.app.DownloadManager +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Environment +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result + +/** OpenFileManagerPlugin */ +class OpenFileManagerPlugin : FlutterPlugin, MethodCallHandler { + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private lateinit var channel: MethodChannel + private lateinit var context: Context + + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "open_file_manager") + channel.setMethodCallHandler(this) + context = flutterPluginBinding.applicationContext + } + + override fun onMethodCall(call: MethodCall, result: Result) { + when (call.method) { + "openFileManager" -> { + val args = call.arguments as HashMap<*, *>? + openFileManager(result, args?.get("folderType") as String?) + } + + else -> { + result.notImplemented() + } + } + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } + + private fun openFileManager(result: Result, folderType: String?) { + try { + if (folderType == null || folderType == "download") { + val downloadIntent = Intent(DownloadManager.ACTION_VIEW_DOWNLOADS) + downloadIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + context.startActivity(downloadIntent) + result.success(true) + } else if (folderType == "recent") { + val uri = Environment.getExternalStorageDirectory().absolutePath + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) + intent.setDataAndType(Uri.parse(uri), "*/*") + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + context.startActivity(intent) + result.success(true) + } + } catch (e: Exception) { + result.error("$e", "Unable to open the file manager", "") + } + } +} diff --git a/packages/open_file_manager/ios/.gitignore b/packages/open_file_manager/ios/.gitignore new file mode 100644 index 00000000..0c885071 --- /dev/null +++ b/packages/open_file_manager/ios/.gitignore @@ -0,0 +1,38 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig +/Flutter/ephemeral/ +/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/packages/open_file_manager/ios/Assets/.gitkeep b/packages/open_file_manager/ios/Assets/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/packages/open_file_manager/ios/Classes/OpenFileManagerPlugin.h b/packages/open_file_manager/ios/Classes/OpenFileManagerPlugin.h new file mode 100644 index 00000000..1374738b --- /dev/null +++ b/packages/open_file_manager/ios/Classes/OpenFileManagerPlugin.h @@ -0,0 +1,4 @@ +#import + +@interface OpenFileManagerPlugin : NSObject +@end diff --git a/packages/open_file_manager/ios/Classes/OpenFileManagerPlugin.m b/packages/open_file_manager/ios/Classes/OpenFileManagerPlugin.m new file mode 100644 index 00000000..78b1fa59 --- /dev/null +++ b/packages/open_file_manager/ios/Classes/OpenFileManagerPlugin.m @@ -0,0 +1,15 @@ +#import "OpenFileManagerPlugin.h" +#if __has_include() +#import +#else +// Support project import fallback if the generated compatibility header +// is not copied when this plugin is created as a library. +// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 +#import "open_file_manager-Swift.h" +#endif + +@implementation OpenFileManagerPlugin ++ (void)registerWithRegistrar:(NSObject*)registrar { + [SwiftOpenFileManagerPlugin registerWithRegistrar:registrar]; +} +@end diff --git a/packages/open_file_manager/ios/Classes/SwiftOpenFileManagerPlugin.swift b/packages/open_file_manager/ios/Classes/SwiftOpenFileManagerPlugin.swift new file mode 100644 index 00000000..fa764b40 --- /dev/null +++ b/packages/open_file_manager/ios/Classes/SwiftOpenFileManagerPlugin.swift @@ -0,0 +1,31 @@ +import Flutter +import UIKit + +public class SwiftOpenFileManagerPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "open_file_manager", binaryMessenger: registrar.messenger()) + let instance = SwiftOpenFileManagerPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard call.method == "openFileManager" else { + result(FlutterMethodNotImplemented) + return + } + + let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) + let documentsDirectory = paths[0] + + guard let arguments = call.arguments as? [String: Any] else { + result(FlutterError(code: "INVALID_ARGUMENTS", message: "Arguments must be a map", details: nil)) + return + } + + let subFolderPath = arguments["subFolderPath"] as? String + + var path = documentsDirectory.absoluteString.replacingOccurrences(of: "file://", with: "shareddocuments://") + path.append(subFolderPath ?? "") + UIApplication.shared.open(URL(string: path)!) + } +} diff --git a/packages/open_file_manager/ios/open_file_manager.podspec b/packages/open_file_manager/ios/open_file_manager.podspec new file mode 100644 index 00000000..bc7a65f4 --- /dev/null +++ b/packages/open_file_manager/ios/open_file_manager.podspec @@ -0,0 +1,23 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint open_file_manager.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'open_file_manager' + s.version = '0.0.1' + s.summary = 'A flutter plugin to open default file manager app' + s.description = <<-DESC +A flutter plugin to open default file manager app + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.platform = :ios, '12.0' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.swift_version = '5.0' +end diff --git a/packages/open_file_manager/lib/config.dart b/packages/open_file_manager/lib/config.dart new file mode 100644 index 00000000..a1f7dc90 --- /dev/null +++ b/packages/open_file_manager/lib/config.dart @@ -0,0 +1,15 @@ +part of 'open_file_manager.dart'; + +class AndroidConfig { + final FolderType folderType; + + AndroidConfig({required this.folderType}); +} + +class IosConfig { + final String subFolderPath; + + IosConfig({required this.subFolderPath}); +} + +enum FolderType { recent, download } diff --git a/packages/open_file_manager/lib/open_file_manager.dart b/packages/open_file_manager/lib/open_file_manager.dart new file mode 100644 index 00000000..f4d93299 --- /dev/null +++ b/packages/open_file_manager/lib/open_file_manager.dart @@ -0,0 +1,15 @@ +library open_file_manager; + +import 'open_file_manager_platform_interface.dart'; + +part 'config.dart'; + +Future openFileManager({ + AndroidConfig? androidConfig, + IosConfig? iosConfig, +}) { + return OpenFileManagerPlatform.instance.openFileManager( + androidConfig: androidConfig, + iosConfig: iosConfig, + ); +} diff --git a/packages/open_file_manager/lib/open_file_manager_method_channel.dart b/packages/open_file_manager/lib/open_file_manager_method_channel.dart new file mode 100644 index 00000000..0ae1a41a --- /dev/null +++ b/packages/open_file_manager/lib/open_file_manager_method_channel.dart @@ -0,0 +1,30 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'open_file_manager.dart'; +import 'open_file_manager_platform_interface.dart'; + +/// An implementation of [OpenFileManagerPlatform] that uses method channels. +class MethodChannelOpenFileManager extends OpenFileManagerPlatform { + /// The method channel used to interact with the native platform. + @visibleForTesting + final methodChannel = const MethodChannel('open_file_manager'); + + @override + Future openFileManager({ + AndroidConfig? androidConfig, + IosConfig? iosConfig, + }) async { + final data = {}; + if (Platform.isAndroid && androidConfig != null) { + data['folderType'] = androidConfig.folderType.name; + } else if (Platform.isIOS && iosConfig != null) { + data['subFolderPath'] = iosConfig.subFolderPath; + } + final version = + await methodChannel.invokeMethod('openFileManager', data); + return version ?? false; + } +} diff --git a/packages/open_file_manager/lib/open_file_manager_platform_interface.dart b/packages/open_file_manager/lib/open_file_manager_platform_interface.dart new file mode 100644 index 00000000..27ccfa59 --- /dev/null +++ b/packages/open_file_manager/lib/open_file_manager_platform_interface.dart @@ -0,0 +1,33 @@ +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'open_file_manager.dart'; +import 'open_file_manager_method_channel.dart'; + +abstract class OpenFileManagerPlatform extends PlatformInterface { + /// Constructs a OpenFileManagerPlatform. + OpenFileManagerPlatform() : super(token: _token); + + static final Object _token = Object(); + + static OpenFileManagerPlatform _instance = MethodChannelOpenFileManager(); + + /// The default instance of [OpenFileManagerPlatform] to use. + /// + /// Defaults to [MethodChannelOpenFileManager]. + static OpenFileManagerPlatform get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [OpenFileManagerPlatform] when + /// they register themselves. + static set instance(OpenFileManagerPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + Future openFileManager({ + AndroidConfig? androidConfig, + IosConfig? iosConfig, + }) { + throw UnimplementedError('openFileManager() has not been implemented.'); + } +} diff --git a/packages/open_file_manager/pubspec.yaml b/packages/open_file_manager/pubspec.yaml new file mode 100644 index 00000000..f0a669ec --- /dev/null +++ b/packages/open_file_manager/pubspec.yaml @@ -0,0 +1,29 @@ +name: open_file_manager +description: A flutter plugin to open default file manager app. on an Android, it opens Download/Recent folder on file manager app. on iOS, it opens app's document folder in Files app. +version: 1.0.2 +homepage: https://auberginesolutions.com +repository: https://github.com/nayanAubie/open_file_manager +issue_tracker: https://github.com/nayanAubie/open_file_manager/issues + +environment: + sdk: '>=2.19.0 <4.0.0' + flutter: ">=3.0.0" + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: ^2.1.6 + +dev_dependencies: + flutter_lints: ^3.0.1 + flutter_test: + sdk: flutter + +flutter: + plugin: + platforms: + android: + package: com.aubergine.open_file_manager + pluginClass: OpenFileManagerPlugin + ios: + pluginClass: OpenFileManagerPlugin diff --git a/packages/open_file_manager/test/open_file_manager_method_channel_test.dart b/packages/open_file_manager/test/open_file_manager_method_channel_test.dart new file mode 100644 index 00000000..ccbc8e17 --- /dev/null +++ b/packages/open_file_manager/test/open_file_manager_method_channel_test.dart @@ -0,0 +1,24 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:open_file_manager/open_file_manager_method_channel.dart'; + +void main() { + final platform = MethodChannelOpenFileManager(); + const channel = MethodChannel('open_file_manager'); + + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (message) async => '42'); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, null); + }); + + test('getPlatformVersion', () async { + expect(await platform.openFileManager(), '42'); + }); +} diff --git a/packages/open_file_manager/test/open_file_manager_test.dart b/packages/open_file_manager/test/open_file_manager_test.dart new file mode 100644 index 00000000..bccdc654 --- /dev/null +++ b/packages/open_file_manager/test/open_file_manager_test.dart @@ -0,0 +1,31 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:open_file_manager/open_file_manager.dart'; +import 'package:open_file_manager/open_file_manager_method_channel.dart'; +import 'package:open_file_manager/open_file_manager_platform_interface.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +class MockOpenFileManagerPlatform + with MockPlatformInterfaceMixin + implements OpenFileManagerPlatform { + @override + Future openFileManager({ + AndroidConfig? androidConfig, + IosConfig? iosConfig, + }) => + Future.value(true); +} + +void main() { + final initialPlatform = OpenFileManagerPlatform.instance; + + test('$MethodChannelOpenFileManager is the default instance', () { + expect(initialPlatform, isInstanceOf()); + }); + + test('getPlatformVersion', () async { + final fakePlatform = MockOpenFileManagerPlatform(); + OpenFileManagerPlatform.instance = fakePlatform; + + expect(await openFileManager(), true); + }); +} diff --git a/packages/shared b/packages/shared index 4298ce22..05fccdb1 160000 --- a/packages/shared +++ b/packages/shared @@ -1 +1 @@ -Subproject commit 4298ce2223ceca8650e41f9ce1dcd994269ed273 +Subproject commit 05fccdb1178953f02ea2836c020f86869466c17b diff --git a/pubspec.lock b/pubspec.lock index 0031b1b2..c91331bc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -435,10 +435,10 @@ packages: dependency: "direct main" description: name: fl_shared_link - sha256: f9c0da5cdedccad1a51d570a57bc7a7289b345846fe7fab08738f397248f644d + sha256: "65ba4e86d2dc8563f626ed681354b980ef4cfb57fd68c4d7c3cb4fc3a27a4ea3" url: "https://pub.dev" source: hosted - version: "0.0.3" + version: "0.1.0" flutter: dependency: "direct main" description: flutter @@ -576,14 +576,6 @@ packages: url: "https://pub.dev" source: hosted version: "5.2.0" - flutter_staggered_grid_view: - dependency: "direct main" - description: - name: flutter_staggered_grid_view - sha256: "19e7abb550c96fbfeb546b23f3ff356ee7c59a019a651f8f102a4ba9b7349395" - url: "https://pub.dev" - source: hosted - version: "0.7.0" flutter_svg: dependency: "direct main" description: @@ -1015,6 +1007,13 @@ packages: url: "https://pub.dev" source: hosted version: "3.3.2" + open_file_manager: + dependency: "direct main" + description: + path: "packages/open_file_manager" + relative: true + source: path + version: "1.0.2" open_mail_app: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index c34069f8..e1ad42b6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 2.3.8 +version: 2.3.9 environment: sdk: ">=2.19.0 <3.0.0" @@ -49,7 +49,7 @@ dependencies: firebase_core: ^2.22.0 firebase_messaging: ^14.7.4 fl_chart: ^0.62.0 - fl_shared_link: ^0.0.3 + fl_shared_link: ^0.1.0 flutter: sdk: flutter flutter_app_update: @@ -60,7 +60,6 @@ dependencies: flutter_inappwebview: ^5.8.0 flutter_local_notifications: ^16.1.0 flutter_mailer: ^2.0.2 - flutter_staggered_grid_view: ^0.7.0 flutter_svg: ^2.0.1 geolocator: ^10.1.0 h3_flutter: ^0.6.6 @@ -75,6 +74,8 @@ dependencies: path: packages/shared/ui network_info_plus: ^4.1.0 open_file: ^3.3.2 + open_file_manager: + path: packages/open_file_manager open_mail_app: ^0.4.5 package_info_plus: ^4.2.0 path_provider: ^2.0.12