diff --git a/.github/workflows/auto_test_android.yaml b/.github/workflows/auto_test_android.yaml index d82c6e2554..fe18795751 100644 --- a/.github/workflows/auto_test_android.yaml +++ b/.github/workflows/auto_test_android.yaml @@ -14,6 +14,10 @@ jobs: - name: Checkout code uses: actions/checkout@v3 # Set build number + - uses: subosito/flutter-action@v2 + with: + flutter-version: "3.22.1" + channel: stable - name: Set env run: | echo "FLUTTER_VERSION_NAME=0.100.0" >> $GITHUB_ENV diff --git a/.github/workflows/auto_test_ios.yaml b/.github/workflows/auto_test_ios.yaml index b106e019bf..3682793263 100644 --- a/.github/workflows/auto_test_ios.yaml +++ b/.github/workflows/auto_test_ios.yaml @@ -14,6 +14,10 @@ jobs: - name: Checkout code uses: actions/checkout@v3 # Set build number + - uses: subosito/flutter-action@v2 + with: + flutter-version: "3.22.1" + channel: stable - name: Set env run: | echo "FLUTTER_VERSION_NAME=0.100.0" >> $GITHUB_ENV diff --git a/.github/workflows/bmvn_build_appcenter_android.yaml b/.github/workflows/bmvn_build_appcenter_android.yaml index a177ab58df..155db59e14 100644 --- a/.github/workflows/bmvn_build_appcenter_android.yaml +++ b/.github/workflows/bmvn_build_appcenter_android.yaml @@ -30,7 +30,7 @@ on: description: Using testnet (Default staging) jobs: build: - runs-on: [ self-hosted, macm2, build-ci-local-bmvn, android ] + runs-on: [self-hosted, macm2, build-ci-local-bmvn, android] steps: - name: Extract Build number for Testnet id: extract-version-testnet @@ -57,7 +57,7 @@ jobs: java-version: "17" - uses: subosito/flutter-action@v2 with: - flutter-version: "3.19.0" + flutter-version: "3.22.1" channel: stable - name: Set env run: | diff --git a/.github/workflows/bmvn_build_appcenter_ios.yaml b/.github/workflows/bmvn_build_appcenter_ios.yaml index ca607cf26c..679368eeaa 100644 --- a/.github/workflows/bmvn_build_appcenter_ios.yaml +++ b/.github/workflows/bmvn_build_appcenter_ios.yaml @@ -32,13 +32,13 @@ on: jobs: fastlane-deploy: - runs-on: [ self-hosted, macm2, build-ci-local-bmvn, ios ] + runs-on: [self-hosted, macm2, build-ci-local-bmvn, ios] steps: # Set up Flutter. - name: Clone Flutter repository with master channel uses: subosito/flutter-action@v2 with: - flutter-version: "3.19.0" + flutter-version: "3.22.1" channel: stable # - uses: maxim-lobanov/setup-xcode@v1 # with: diff --git a/.github/workflows/ios-release-appcenter.yaml b/.github/workflows/ios-release-appcenter.yaml index 392ca59405..e2a08a38fd 100644 --- a/.github/workflows/ios-release-appcenter.yaml +++ b/.github/workflows/ios-release-appcenter.yaml @@ -23,7 +23,7 @@ jobs: - name: Clone Flutter repository with master channel uses: subosito/flutter-action@v2 with: - flutter-version: "3.19.0" + flutter-version: "3.22.1" channel: stable - uses: maxim-lobanov/setup-xcode@v1 with: diff --git a/.github/workflows/ios-release-testflight.yaml b/.github/workflows/ios-release-testflight.yaml index bf9c2450aa..244fd28583 100644 --- a/.github/workflows/ios-release-testflight.yaml +++ b/.github/workflows/ios-release-testflight.yaml @@ -18,7 +18,7 @@ jobs: - name: Clone Flutter repository with master channel uses: subosito/flutter-action@v2 with: - flutter-version: "3.19.0" + flutter-version: "3.22.1" channel: stable - uses: maxim-lobanov/setup-xcode@v1 with: diff --git a/README.md b/README.md index d70f4474d4..700d3196ce 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ samples, guidance on mobile development, and a full API reference. - [App Store](https://apps.apple.com/us/app/feral-file/id1544022728) - [Google Play](https://play.google.com/store/apps/details?id=com.bitmark.autonomy_client&pli=) -[Release Notes](https://github.com/bitmark-inc/autonomy-apps/tree/main/release_notes/production) +[Release Notes](https://github.com/bitmark-inc/feral-file-docs/blob/master/app/release_notes/production/changelog.md) ## Contributing diff --git a/assets b/assets index 90b3a6a9ea..71655929f8 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 90b3a6a9eaf9586165535ef20d6e8be68ec25aca +Subproject commit 71655929f8a0762ea663a973224863273ca8872e diff --git a/ios/Runner-Inhouse-Info.plist b/ios/Runner-Inhouse-Info.plist index 9e5b627866..1a70126ac1 100644 --- a/ios/Runner-Inhouse-Info.plist +++ b/ios/Runner-Inhouse-Info.plist @@ -39,7 +39,7 @@ tezos autonomy-wc autonomy-app - feralfile + feralfile @@ -59,6 +59,7 @@ _googlecast._tcp _YOUR_APP_ID._googlecast._tcp + _feralFileCanvas._tcp NSCameraUsageDescription QR code scanning requires camera access. diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 60924ce267..f618f954fb 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -44,7 +44,7 @@ tezos autonomy-wc autonomy-app - feralfile + feralfile @@ -64,6 +64,7 @@ _googlecast._tcp _YOUR_APP_ID._googlecast._tcp + _feralFileCanvas._tcp NSCameraUsageDescription QR code scanning requires camera access. diff --git a/ios/fastlane/provisioning_profiles/Autonomy_Inhouse_Adhoc.mobileprovision b/ios/fastlane/provisioning_profiles/Autonomy_Inhouse_Adhoc.mobileprovision index 9b847156ff..fb5af6c52e 100644 Binary files a/ios/fastlane/provisioning_profiles/Autonomy_Inhouse_Adhoc.mobileprovision and b/ios/fastlane/provisioning_profiles/Autonomy_Inhouse_Adhoc.mobileprovision differ diff --git a/ios/fastlane/provisioning_profiles/Autonomy_Inhouse_Adhoc_Notification.mobileprovision b/ios/fastlane/provisioning_profiles/Autonomy_Inhouse_Adhoc_Notification.mobileprovision index 9940c41738..9fc78d903e 100644 Binary files a/ios/fastlane/provisioning_profiles/Autonomy_Inhouse_Adhoc_Notification.mobileprovision and b/ios/fastlane/provisioning_profiles/Autonomy_Inhouse_Adhoc_Notification.mobileprovision differ diff --git a/lib/common/environment.dart b/lib/common/environment.dart index 3dcf625974..1a87ac097c 100644 --- a/lib/common/environment.dart +++ b/lib/common/environment.dart @@ -43,6 +43,8 @@ class Environment { ? connectWebsocketTestnetURL : connectWebsocketMainnetURL; + static String get tvCastApiUrl => dotenv.env['TV_CAST_API_URL'] ?? ''; + static String get tokenWebviewPrefix => dotenv.env['TOKEN_WEBVIEW_PREFIX'] ?? ''; @@ -151,9 +153,6 @@ class Environment { static String get tzktTestnetURL => dotenv.env['TZKT_TESTNET_URL'] ?? ''; - static String get autonomyAirdropURL => - dotenv.env['AUTONOMY_AIRDROP_URL'] ?? ''; - static String get autonomyAirDropContractAddress => dotenv.env['AUTONOMY_AIRDROP_CONTRACT_ADDRESS'] ?? ''; @@ -187,6 +186,8 @@ class Environment { static String get sentryDSN => cachedSecretEnv['SENTRY_DSN'] ?? ''; static String get onesignalAppID => cachedSecretEnv['ONESIGNAL_APP_ID'] ?? ''; + + static String get tvKey => cachedSecretEnv['TV_API_KEY'] ?? ''; } class Secret { diff --git a/lib/common/injector.dart b/lib/common/injector.dart index be36ed4652..8ca03357a6 100644 --- a/lib/common/injector.dart +++ b/lib/common/injector.dart @@ -7,13 +7,9 @@ // ignore_for_file: cascade_invocations -import 'dart:math'; - import 'package:autonomy_flutter/common/environment.dart'; import 'package:autonomy_flutter/database/app_database.dart'; import 'package:autonomy_flutter/database/cloud_database.dart'; -import 'package:autonomy_flutter/gateway/activation_api.dart'; -import 'package:autonomy_flutter/gateway/airdrop_api.dart'; import 'package:autonomy_flutter/gateway/announcement_api.dart'; import 'package:autonomy_flutter/gateway/autonomy_api.dart'; import 'package:autonomy_flutter/gateway/branch_api.dart'; @@ -26,25 +22,28 @@ import 'package:autonomy_flutter/gateway/iap_api.dart'; import 'package:autonomy_flutter/gateway/merchandise_api.dart'; import 'package:autonomy_flutter/gateway/postcard_api.dart'; import 'package:autonomy_flutter/gateway/pubdoc_api.dart'; +import 'package:autonomy_flutter/gateway/source_exhibition_api.dart'; +import 'package:autonomy_flutter/gateway/tv_cast_api.dart'; import 'package:autonomy_flutter/gateway/tzkt_api.dart'; import 'package:autonomy_flutter/screen/bloc/connections/connections_bloc.dart'; import 'package:autonomy_flutter/screen/bloc/identity/identity_bloc.dart'; +import 'package:autonomy_flutter/screen/bloc/subscription/subscription_bloc.dart'; import 'package:autonomy_flutter/screen/chat/chat_bloc.dart'; import 'package:autonomy_flutter/screen/collection_pro/collection_pro_bloc.dart'; +import 'package:autonomy_flutter/screen/detail/preview/canvas_device_bloc.dart'; +import 'package:autonomy_flutter/screen/exhibitions/exhibitions_bloc.dart'; import 'package:autonomy_flutter/screen/interactive_postcard/claim_empty_postcard/claim_empty_postcard_bloc.dart'; import 'package:autonomy_flutter/screen/playlists/add_new_playlist/add_new_playlist_bloc.dart'; import 'package:autonomy_flutter/screen/playlists/edit_playlist/edit_playlist_bloc.dart'; import 'package:autonomy_flutter/screen/playlists/view_playlist/view_playlist_bloc.dart'; import 'package:autonomy_flutter/screen/predefined_collection/predefined_collection_bloc.dart'; import 'package:autonomy_flutter/service/account_service.dart'; -import 'package:autonomy_flutter/service/activation_service.dart'; import 'package:autonomy_flutter/service/address_service.dart'; -import 'package:autonomy_flutter/service/airdrop_service.dart'; import 'package:autonomy_flutter/service/audit_service.dart'; import 'package:autonomy_flutter/service/auth_service.dart'; import 'package:autonomy_flutter/service/autonomy_service.dart'; import 'package:autonomy_flutter/service/backup_service.dart'; -import 'package:autonomy_flutter/service/canvas_client_service.dart'; +import 'package:autonomy_flutter/service/canvas_client_service_v2.dart'; import 'package:autonomy_flutter/service/chat_auth_service.dart'; import 'package:autonomy_flutter/service/chat_service.dart'; import 'package:autonomy_flutter/service/client_token_service.dart'; @@ -53,16 +52,20 @@ import 'package:autonomy_flutter/service/configuration_service.dart'; import 'package:autonomy_flutter/service/currency_service.dart'; import 'package:autonomy_flutter/service/customer_support_service.dart'; import 'package:autonomy_flutter/service/deeplink_service.dart'; +import 'package:autonomy_flutter/service/device_info_service.dart'; import 'package:autonomy_flutter/service/domain_service.dart'; import 'package:autonomy_flutter/service/ethereum_service.dart'; import 'package:autonomy_flutter/service/feralfile_service.dart'; import 'package:autonomy_flutter/service/hive_service.dart'; +import 'package:autonomy_flutter/service/hive_store_service.dart'; import 'package:autonomy_flutter/service/iap_service.dart'; import 'package:autonomy_flutter/service/keychain_service.dart'; import 'package:autonomy_flutter/service/merchandise_service.dart'; import 'package:autonomy_flutter/service/metric_client_service.dart'; import 'package:autonomy_flutter/service/mix_panel_client_service.dart'; import 'package:autonomy_flutter/service/navigation_service.dart'; +import 'package:autonomy_flutter/service/network_issue_manager.dart'; +import 'package:autonomy_flutter/service/network_service.dart'; import 'package:autonomy_flutter/service/notification_service.dart'; import 'package:autonomy_flutter/service/pending_token_service.dart'; import 'package:autonomy_flutter/service/playlist_service.dart'; @@ -74,15 +77,14 @@ import 'package:autonomy_flutter/service/tezos_service.dart'; import 'package:autonomy_flutter/service/versions_service.dart'; import 'package:autonomy_flutter/service/wc2_service.dart'; import 'package:autonomy_flutter/util/au_file_service.dart'; -import 'package:autonomy_flutter/util/constants.dart'; import 'package:autonomy_flutter/util/dio_interceptors.dart'; import 'package:autonomy_flutter/util/dio_util.dart'; import 'package:autonomy_flutter/util/log.dart'; import 'package:dio/dio.dart'; -import 'package:dio_smart_retry/dio_smart_retry.dart'; +import 'package:feralfile_app_tv_proto/feralfile_app_tv_proto.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:get_it/get_it.dart'; -import 'package:http/http.dart'; +import 'package:http/http.dart' as http; import 'package:logging/logging.dart'; import 'package:nft_collection/data/api/indexer_api.dart'; import 'package:nft_collection/graphql/clients/indexer_client.dart'; @@ -91,7 +93,6 @@ import 'package:nft_collection/services/indexer_service.dart'; import 'package:nft_collection/services/tokens_service.dart'; import 'package:sentry_dio/sentry_dio.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:tezart/tezart.dart'; import 'package:web3dart/web3dart.dart'; final injector = GetIt.instance; @@ -127,6 +128,7 @@ Future setup() async { migrateV15ToV16, migrateV16ToV17, migrateV17ToV18, + migrateV18ToV19, ]).build(); final cloudDB = await $FloorCloudDatabase @@ -134,6 +136,11 @@ Future setup() async { .addMigrations(cloudDatabaseMigrations) .build(); + injector.registerLazySingleton(() => NavigationService()); + + injector + .registerLazySingleton(() => NetworkIssueManager()); + final BaseOptions dioOptions = BaseOptions( followRedirects: true, connectTimeout: const Duration(seconds: 3), @@ -163,31 +170,19 @@ Future setup() async { final authenticatedDio = Dio(); // Authenticated dio instance for AU servers authenticatedDio.interceptors.add(AutonomyAuthInterceptor()); authenticatedDio.interceptors.add(LoggingInterceptor()); + authenticatedDio.interceptors.add(ConnectingExceptionInterceptor()); (authenticatedDio.transformer as SyncTransformer).jsonDecodeCallback = parseJson; - dio.interceptors.add(RetryInterceptor( - dio: dio, - logPrint: (message) { - log.warning('[request retry] $message'); - }, - retryDelays: const [ - // set delays between retries - Duration(seconds: 1), - Duration(seconds: 2), - Duration(seconds: 3), - ], - )); authenticatedDio.addSentry(); authenticatedDio.options = dioOptions; + injector.registerLazySingleton(() => NetworkService()); // Services final auditService = AuditServiceImpl(cloudDB); injector.registerSingleton( ConfigurationServiceImpl(sharedPreferences)); - - injector.registerLazySingleton(() => Client()); - injector.registerLazySingleton(() => NavigationService()); + injector.registerLazySingleton(() => http.Client()); injector.registerLazySingleton( () => AutonomyServiceImpl(injector(), injector())); injector @@ -223,6 +218,8 @@ Future setup() async { injector.registerLazySingleton(() => BranchApi(dio)); injector.registerLazySingleton( () => PubdocAPI(dio, baseUrl: Environment.pubdocURL)); + injector.registerLazySingleton( + () => SourceExhibitionAPI(dio, baseUrl: Environment.pubdocURL)); injector.registerLazySingleton( () => RemoteConfigServiceImpl(injector())); injector.registerLazySingleton( @@ -254,6 +251,8 @@ Future setup() async { injector.registerLazySingleton( () => IAPServiceImpl(injector(), injector())); + injector.registerLazySingleton(() => + TvCastApi(tvCastDio(dioOptions), baseUrl: Environment.tvCastApiUrl)); injector.registerLazySingleton(() => Wc2Service( injector(), injector(), @@ -269,7 +268,13 @@ Future setup() async { injector.registerLazySingleton( () => CustomerSupportServiceImpl( mainnetDB.draftCustomerSupportDao, - CustomerSupportApi(authenticatedDio, + CustomerSupportApi( + customerSupportDio( + dioOptions.copyWith( + connectTimeout: const Duration(seconds: 10), + receiveTimeout: const Duration(seconds: 10), + ), + ), baseUrl: Environment.customerSupportURL), injector(), mainnetDB.announcementDao, @@ -298,11 +303,6 @@ Future setup() async { injector.registerLazySingleton( () => ClientTokenService(injector(), injector(), injector(), injector())); - - final tezosNodeClientURL = Environment.appTestnetConfig - ? Environment.tezosNodeClientTestnetURL - : publicTezosNodes[Random().nextInt(publicTezosNodes.length)]; - injector.registerLazySingleton(() => TezartClient(tezosNodeClientURL)); injector.registerLazySingleton(() => FeralFileApi( feralFileDio(dioOptions), baseUrl: Environment.feralFileAPIURL)); @@ -319,17 +319,22 @@ Future setup() async { injector.registerLazySingleton( () => IndexerService(indexerClient)); - injector.registerLazySingleton(() => - EthereumServiceImpl(injector(), injector(), injector(), injector())); + injector.registerLazySingleton(() => EthereumServiceImpl( + injector(), injector(), injector(), injector(), injector())); injector.registerLazySingleton(() => HiveServiceImpl()); injector .registerLazySingleton(() => TezosServiceImpl(injector())); injector.registerLazySingleton(() => mainnetDB); injector.registerLazySingleton( () => PlayListServiceImp(injector(), injector(), injector(), injector())); + injector.registerLazySingleton(() => DeviceInfoService()); - injector.registerLazySingleton( - () => CanvasClientService(injector())); + injector.registerLazySingleton>( + () => HiveStoreObjectServiceImpl()); + await injector>() + .init('local.canvas_device'); + injector.registerLazySingleton(() => + CanvasClientServiceV2(injector(), injector(), injector(), injector())); injector.registerLazySingleton( () => PostcardServiceImpl( @@ -348,36 +353,9 @@ Future setup() async { injector(), )); - injector.registerLazySingleton( - () => AirdropService( - injector(), - injector(), - injector(), - injector(), - injector(), - injector(), - injector(), - injector(), - ), - ); - - injector.registerLazySingleton(() => ActivationService( - injector(), - injector(), - injector(), - )); - injector .registerLazySingleton(() => NotificationService()); - injector.registerLazySingleton(() => AirdropApi( - airdropDio(dioOptions.copyWith(followRedirects: true)), - baseUrl: Environment.autonomyAirdropURL)); - - injector.registerLazySingleton(() => ActivationApi( - airdropDio(dioOptions.copyWith(followRedirects: true)), - baseUrl: Environment.autonomyActivationURL)); - injector.registerLazySingleton(() => FeralFileServiceImpl( injector(), injector(), @@ -391,10 +369,6 @@ Future setup() async { injector(), injector(), injector(), - injector(), - injector(), - injector(), - injector(), )); injector.registerLazySingleton(() => PendingTokenService( @@ -424,4 +398,10 @@ Future setup() async { injector(), injector(), )); + injector.registerLazySingleton( + () => CanvasDeviceBloc(injector())); + injector + .registerLazySingleton(() => ExhibitionBloc(injector())); + injector.registerLazySingleton( + () => SubscriptionBloc(injector())); } diff --git a/lib/database/app_database.dart b/lib/database/app_database.dart index ed0509e5a2..10c4df8c23 100644 --- a/lib/database/app_database.dart +++ b/lib/database/app_database.dart @@ -8,14 +8,12 @@ import 'dart:async'; import 'package:autonomy_flutter/database/dao/announcement_dao.dart'; -import 'package:autonomy_flutter/database/dao/canvas_device_dao.dart'; import 'package:autonomy_flutter/database/dao/draft_customer_support_dao.dart'; import 'package:autonomy_flutter/database/dao/identity_dao.dart'; import 'package:autonomy_flutter/database/entity/announcement_local.dart'; import 'package:autonomy_flutter/database/entity/draft_customer_support.dart'; import 'package:autonomy_flutter/database/entity/identity.dart'; import 'package:autonomy_flutter/util/log.dart'; -import 'package:feralfile_app_tv_proto/feralfile_app_tv_proto.dart'; import 'package:floor/floor.dart'; import 'package:nft_collection/models/token.dart'; import 'package:sqflite/sqflite.dart' as sqflite; @@ -25,12 +23,10 @@ import 'package:sqflite/sqflite.dart' as sqflite; part 'app_database.g.dart'; // the generated code will be there @TypeConverters([DateTimeConverter, TokenOwnersConverter]) -@Database(version: 17, entities: [ +@Database(version: 19, entities: [ Identity, DraftCustomerSupport, AnnouncementLocal, - CanvasDevice, - Scene, ]) abstract class AppDatabase extends FloorDatabase { IdentityDao get identityDao; @@ -39,16 +35,10 @@ abstract class AppDatabase extends FloorDatabase { AnnouncementLocalDao get announcementDao; - CanvasDeviceDao get canvasDeviceDao; - - SceneDao get sceneDao; - Future removeAll() async { await identityDao.removeAll(); await draftCustomerSupportDao.removeAll(); await announcementDao.removeAll(); - await canvasDeviceDao.removeAll(); - await sceneDao.removeAll(); } } @@ -164,3 +154,8 @@ final migrateV17ToV18 = Migration(17, 18, (database) async { await database.execute('DROP TABLE IF EXISTS Followee;'); log.info('Migrated App database from version 17 to 18'); }); + +final migrateV18ToV19 = Migration(18, 19, (database) async { + await database.execute('DROP TABLE IF EXISTS CanvasDevice;'); + await database.execute('DROP TABLE IF EXISTS Scene;'); +}); diff --git a/lib/database/app_database.g.dart b/lib/database/app_database.g.dart index 4a7d5ceb0a..65bbc68c4c 100644 --- a/lib/database/app_database.g.dart +++ b/lib/database/app_database.g.dart @@ -67,17 +67,13 @@ class _$AppDatabase extends AppDatabase { AnnouncementLocalDao? _announcementDaoInstance; - CanvasDeviceDao? _canvasDeviceDaoInstance; - - SceneDao? _sceneDaoInstance; - Future open( String path, List migrations, [ Callback? callback, ]) async { final databaseOptions = sqflite.OpenDatabaseOptions( - version: 17, + version: 19, onConfigure: (database) async { await database.execute('PRAGMA foreign_keys = ON'); await callback?.onConfigure?.call(database); @@ -98,10 +94,6 @@ class _$AppDatabase extends AppDatabase { 'CREATE TABLE IF NOT EXISTS `DraftCustomerSupport` (`uuid` TEXT NOT NULL, `issueID` TEXT NOT NULL, `type` TEXT NOT NULL, `data` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `reportIssueType` TEXT NOT NULL, `mutedMessages` TEXT NOT NULL, PRIMARY KEY (`uuid`))'); await database.execute( 'CREATE TABLE IF NOT EXISTS `AnnouncementLocal` (`announcementContextId` TEXT NOT NULL, `title` TEXT NOT NULL, `body` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `announceAt` INTEGER NOT NULL, `type` TEXT NOT NULL, `unread` INTEGER NOT NULL, PRIMARY KEY (`announcementContextId`))'); - await database.execute( - 'CREATE TABLE IF NOT EXISTS `CanvasDevice` (`id` TEXT NOT NULL, `ip` TEXT NOT NULL, `port` INTEGER NOT NULL, `name` TEXT NOT NULL, `isConnecting` INTEGER NOT NULL, `playingSceneId` TEXT, PRIMARY KEY (`id`))'); - await database.execute( - 'CREATE TABLE IF NOT EXISTS `Scene` (`id` TEXT NOT NULL, `deviceId` TEXT NOT NULL, `isPlaying` INTEGER NOT NULL, `metadata` TEXT NOT NULL, PRIMARY KEY (`id`))'); await callback?.onCreate?.call(database, version); }, @@ -125,17 +117,6 @@ class _$AppDatabase extends AppDatabase { return _announcementDaoInstance ??= _$AnnouncementLocalDao(database, changeListener); } - - @override - CanvasDeviceDao get canvasDeviceDao { - return _canvasDeviceDaoInstance ??= - _$CanvasDeviceDao(database, changeListener); - } - - @override - SceneDao get sceneDao { - return _sceneDaoInstance ??= _$SceneDao(database, changeListener); - } } class _$IdentityDao extends IdentityDao { @@ -438,198 +419,6 @@ class _$AnnouncementLocalDao extends AnnouncementLocalDao { } } -class _$CanvasDeviceDao extends CanvasDeviceDao { - _$CanvasDeviceDao( - this.database, - this.changeListener, - ) : _queryAdapter = QueryAdapter(database), - _canvasDeviceInsertionAdapter = InsertionAdapter( - database, - 'CanvasDevice', - (CanvasDevice item) => { - 'id': item.id, - 'ip': item.ip, - 'port': item.port, - 'name': item.name, - 'isConnecting': item.isConnecting ? 1 : 0, - 'playingSceneId': item.playingSceneId - }), - _canvasDeviceUpdateAdapter = UpdateAdapter( - database, - 'CanvasDevice', - ['id'], - (CanvasDevice item) => { - 'id': item.id, - 'ip': item.ip, - 'port': item.port, - 'name': item.name, - 'isConnecting': item.isConnecting ? 1 : 0, - 'playingSceneId': item.playingSceneId - }), - _canvasDeviceDeletionAdapter = DeletionAdapter( - database, - 'CanvasDevice', - ['id'], - (CanvasDevice item) => { - 'id': item.id, - 'ip': item.ip, - 'port': item.port, - 'name': item.name, - 'isConnecting': item.isConnecting ? 1 : 0, - 'playingSceneId': item.playingSceneId - }); - - final sqflite.DatabaseExecutor database; - - final StreamController changeListener; - - final QueryAdapter _queryAdapter; - - final InsertionAdapter _canvasDeviceInsertionAdapter; - - final UpdateAdapter _canvasDeviceUpdateAdapter; - - final DeletionAdapter _canvasDeviceDeletionAdapter; - - @override - Future> getCanvasDevices() async { - return _queryAdapter.queryList('SELECT * FROM CanvasDevice', - mapper: (Map row) => CanvasDevice( - id: row['id'] as String, - ip: row['ip'] as String, - port: row['port'] as int, - name: row['name'] as String, - isConnecting: (row['isConnecting'] as int) != 0, - playingSceneId: row['playingSceneId'] as String?)); - } - - @override - Future removeAll() async { - await _queryAdapter.queryNoReturn('DELETE FROM CanvasDevice'); - } - - @override - Future insertCanvasDevice(CanvasDevice canvasDevice) async { - await _canvasDeviceInsertionAdapter.insert( - canvasDevice, OnConflictStrategy.replace); - } - - @override - Future insertCanvasDevices(List canvasDevices) async { - await _canvasDeviceInsertionAdapter.insertList( - canvasDevices, OnConflictStrategy.replace); - } - - @override - Future updateCanvasDevice(CanvasDevice canvasDevice) async { - await _canvasDeviceUpdateAdapter.update( - canvasDevice, OnConflictStrategy.abort); - } - - @override - Future deleteCanvasDevice(CanvasDevice canvasDevice) async { - await _canvasDeviceDeletionAdapter.delete(canvasDevice); - } -} - -class _$SceneDao extends SceneDao { - _$SceneDao( - this.database, - this.changeListener, - ) : _queryAdapter = QueryAdapter(database), - _sceneInsertionAdapter = InsertionAdapter( - database, - 'Scene', - (Scene item) => { - 'id': item.id, - 'deviceId': item.deviceId, - 'isPlaying': item.isPlaying ? 1 : 0, - 'metadata': item.metadata - }), - _sceneUpdateAdapter = UpdateAdapter( - database, - 'Scene', - ['id'], - (Scene item) => { - 'id': item.id, - 'deviceId': item.deviceId, - 'isPlaying': item.isPlaying ? 1 : 0, - 'metadata': item.metadata - }); - - final sqflite.DatabaseExecutor database; - - final StreamController changeListener; - - final QueryAdapter _queryAdapter; - - final InsertionAdapter _sceneInsertionAdapter; - - final UpdateAdapter _sceneUpdateAdapter; - - @override - Future> getScenes() async { - return _queryAdapter.queryList('SELECT * FROM Scene', - mapper: (Map row) => Scene( - id: row['id'] as String, - deviceId: row['deviceId'] as String, - metadata: row['metadata'] as String, - isPlaying: (row['isPlaying'] as int) != 0)); - } - - @override - Future> getScenesByDeviceId(String deviceId) async { - return _queryAdapter.queryList('SELECT * FROM Scene WHERE deviceId = ?1', - mapper: (Map row) => Scene( - id: row['id'] as String, - deviceId: row['deviceId'] as String, - metadata: row['metadata'] as String, - isPlaying: (row['isPlaying'] as int) != 0), - arguments: [deviceId]); - } - - @override - Future getSceneById(String id) async { - return _queryAdapter.query('SELECT * FROM Scene WHERE id = ?1', - mapper: (Map row) => Scene( - id: row['id'] as String, - deviceId: row['deviceId'] as String, - metadata: row['metadata'] as String, - isPlaying: (row['isPlaying'] as int) != 0), - arguments: [id]); - } - - @override - Future updateSceneMetadata( - String id, - String metadata, - ) async { - await _queryAdapter.queryNoReturn( - 'UPDATE Scene SET metadata = ?2 WHERE id = ?1', - arguments: [id, metadata]); - } - - @override - Future removeAll() async { - await _queryAdapter.queryNoReturn('DELETE FROM Scene'); - } - - @override - Future insertScene(Scene scene) async { - await _sceneInsertionAdapter.insert(scene, OnConflictStrategy.replace); - } - - @override - Future insertScenes(List scenes) async { - await _sceneInsertionAdapter.insertList(scenes, OnConflictStrategy.replace); - } - - @override - Future updateScene(Scene scene) async { - await _sceneUpdateAdapter.update(scene, OnConflictStrategy.abort); - } -} - // ignore_for_file: unused_element final _dateTimeConverter = DateTimeConverter(); final _tokenOwnersConverter = TokenOwnersConverter(); diff --git a/lib/database/cloud_database.g.dart b/lib/database/cloud_database.g.dart index 72fa370ecd..0f823bf508 100644 --- a/lib/database/cloud_database.g.dart +++ b/lib/database/cloud_database.g.dart @@ -723,6 +723,29 @@ class _$WalletAddressDao extends WalletAddressDao { await _queryAdapter.queryNoReturn('DELETE FROM WalletAddress'); } + @override + Future deleteAddressesByPersona(String uuid) async { + await _queryAdapter.queryNoReturn( + 'DELETE FROM WalletAddress WHERE uuid = ?1', + arguments: [uuid]); + } + + @override + Future> getAddressesByPersona(String uuid) async { + return _queryAdapter.queryList( + 'SELECT * FROM WalletAddress WHERE uuid = ?1', + mapper: (Map row) => WalletAddress( + address: row['address'] as String, + uuid: row['uuid'] as String, + index: row['index'] as int, + cryptoType: row['cryptoType'] as String, + createdAt: _dateTimeConverter.decode(row['createdAt'] as int), + isHidden: (row['isHidden'] as int) != 0, + name: row['name'] as String?, + accountOrder: row['accountOrder'] as int?), + arguments: [uuid]); + } + @override Future insertAddress(WalletAddress address) async { await _walletAddressInsertionAdapter.insert( diff --git a/lib/database/dao/address_dao.dart b/lib/database/dao/address_dao.dart index f563b3df32..aa59ca09c4 100644 --- a/lib/database/dao/address_dao.dart +++ b/lib/database/dao/address_dao.dart @@ -46,4 +46,12 @@ abstract class WalletAddressDao { @Query('DELETE FROM WalletAddress') Future removeAll(); + + // deleteAddresses by persona + @Query('DELETE FROM WalletAddress WHERE uuid = :uuid') + Future deleteAddressesByPersona(String uuid); + + // get addresses by persona + @Query('SELECT * FROM WalletAddress WHERE uuid = :uuid') + Future> getAddressesByPersona(String uuid); } diff --git a/lib/database/dao/canvas_device_dao.dart b/lib/database/dao/canvas_device_dao.dart deleted file mode 100644 index 63f29bb92f..0000000000 --- a/lib/database/dao/canvas_device_dao.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:feralfile_app_tv_proto/feralfile_app_tv_proto.dart'; -import 'package:floor/floor.dart'; - -@dao -abstract class CanvasDeviceDao { - @Query('SELECT * FROM CanvasDevice') - Future> getCanvasDevices(); - - @Insert(onConflict: OnConflictStrategy.replace) - Future insertCanvasDevice(CanvasDevice canvasDevice); - - @Insert(onConflict: OnConflictStrategy.replace) - Future insertCanvasDevices(List canvasDevices); - - @update - Future updateCanvasDevice(CanvasDevice canvasDevice); - - @delete - Future deleteCanvasDevice(CanvasDevice canvasDevice); - - @Query('DELETE FROM CanvasDevice') - Future removeAll(); -} - -@dao -abstract class SceneDao { - @Query('SELECT * FROM Scene') - Future> getScenes(); - - @Query('SELECT * FROM Scene WHERE deviceId = :deviceId') - Future> getScenesByDeviceId(String deviceId); - - @Query('SELECT * FROM Scene WHERE id = :id') - Future getSceneById(String id); - - @Query('UPDATE Scene SET metadata = :metadata WHERE id = :id') - Future updateSceneMetadata(String id, String metadata); - - @Insert(onConflict: OnConflictStrategy.replace) - Future insertScene(Scene scene); - - @Insert(onConflict: OnConflictStrategy.replace) - Future insertScenes(List scenes); - - @update - Future updateScene(Scene scene); - - @Query('DELETE FROM Scene') - Future removeAll(); -} diff --git a/lib/gateway/activation_api.dart b/lib/gateway/activation_api.dart deleted file mode 100644 index 36f69c1572..0000000000 --- a/lib/gateway/activation_api.dart +++ /dev/null @@ -1,80 +0,0 @@ -import 'package:dio/dio.dart'; -import 'package:retrofit/retrofit.dart'; - -part 'activation_api.g.dart'; - -@RestApi(baseUrl: "") -abstract class ActivationApi { - factory ActivationApi(Dio dio, {String baseUrl}) = _ActivationApi; - - @GET("/v1/activation/{activation_id}") - Future getActivation( - @Path("activation_id") String activationId); - - @POST("/v1/activation/claim") - Future claim(@Body() ActivationClaimRequest body); -} - -class ActivationInfo { - String name; - String description; - String blockchain; - String contractAddress; - String tokenID; - - ActivationInfo(this.name, this.description, this.blockchain, - this.contractAddress, this.tokenID); - - factory ActivationInfo.fromJson(Map json) { - return ActivationInfo( - json['name'], - json['description'], - json['blockchain'], - json['contractAddress'], - json['tokenID'], - ); - } - - Map toJson() => { - 'name': name, - 'description': description, - 'blockchain': blockchain, - 'contractAddress': contractAddress, - 'tokenID': tokenID, - }; -} - -class ActivationClaimRequest { - String activationID; - String address; - String airdropTOTPPasscode; - - ActivationClaimRequest( - {required this.activationID, - required this.address, - required this.airdropTOTPPasscode}); - - Map toJson() => { - 'activationID': activationID, - 'address': address, - 'airdropTOTPPasscode': airdropTOTPPasscode, - }; - - factory ActivationClaimRequest.fromJson(Map json) { - return ActivationClaimRequest( - activationID: json['activationID'], - address: json['address'], - airdropTOTPPasscode: json['airdropTOTPPasscode'], - ); - } -} - -class ActivationClaimResponse { - ActivationClaimResponse(); - - factory ActivationClaimResponse.fromJson(Map json) { - return ActivationClaimResponse(); - } - - Map toJson() => {}; -} diff --git a/lib/gateway/airdrop_api.dart b/lib/gateway/airdrop_api.dart deleted file mode 100644 index c19a023934..0000000000 --- a/lib/gateway/airdrop_api.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:autonomy_flutter/model/ff_account.dart'; -import 'package:autonomy_flutter/service/airdrop_service.dart'; -import 'package:dio/dio.dart'; -import 'package:retrofit/retrofit.dart'; - -part 'airdrop_api.g.dart'; - -@RestApi(baseUrl: '') -abstract class AirdropApi { - factory AirdropApi(Dio dio, {String baseUrl}) = _AirdropApi; - - @POST('/v1/claim/request') - Future requestClaim( - @Body() AirdropRequestClaimRequest body); - - @POST('/v1/claim') - Future claim(@Body() AirdropClaimRequest body); - - @GET('/v1/claim/{share_code}') - Future claimShare( - @Path('share_code') String shareCode); - - @POST('/v1/share/{token_id}') - Future share( - @Path('token_id') String tokenId, @Body() AirdropShareRequest body); - - @POST('/v1/feralfile-claim') - Future feralfileClaim( - @Body() FeralFileTokenClaimRequest body); -} diff --git a/lib/gateway/airdrop_api.g.dart b/lib/gateway/airdrop_api.g.dart deleted file mode 100644 index e7ba06cdf9..0000000000 --- a/lib/gateway/airdrop_api.g.dart +++ /dev/null @@ -1,194 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'airdrop_api.dart'; - -// ************************************************************************** -// RetrofitGenerator -// ************************************************************************** - -// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers - -class _AirdropApi implements AirdropApi { - _AirdropApi( - this._dio, { - this.baseUrl, - }); - - final Dio _dio; - - String? baseUrl; - - @override - Future requestClaim( - AirdropRequestClaimRequest body) async { - const _extra = {}; - final queryParameters = {}; - final _headers = {}; - final _data = {}; - _data.addAll(body.toJson()); - final _result = await _dio.fetch>( - _setStreamType(Options( - method: 'POST', - headers: _headers, - extra: _extra, - ) - .compose( - _dio.options, - '/v1/claim/request', - queryParameters: queryParameters, - data: _data, - ) - .copyWith( - baseUrl: _combineBaseUrls( - _dio.options.baseUrl, - baseUrl, - )))); - final value = AirdropRequestClaimResponse.fromJson(_result.data!); - return value; - } - - @override - Future claim(AirdropClaimRequest body) async { - const _extra = {}; - final queryParameters = {}; - final _headers = {}; - final _data = {}; - _data.addAll(body.toJson()); - final _result = await _dio - .fetch>(_setStreamType(Options( - method: 'POST', - headers: _headers, - extra: _extra, - ) - .compose( - _dio.options, - '/v1/claim', - queryParameters: queryParameters, - data: _data, - ) - .copyWith( - baseUrl: _combineBaseUrls( - _dio.options.baseUrl, - baseUrl, - )))); - final value = TokenClaimResponse.fromJson(_result.data!); - return value; - } - - @override - Future claimShare(String shareCode) async { - const _extra = {}; - final queryParameters = {}; - final _headers = {}; - final Map? _data = null; - final _result = await _dio.fetch>( - _setStreamType(Options( - method: 'GET', - headers: _headers, - extra: _extra, - ) - .compose( - _dio.options, - '/v1/claim/${shareCode}', - queryParameters: queryParameters, - data: _data, - ) - .copyWith( - baseUrl: _combineBaseUrls( - _dio.options.baseUrl, - baseUrl, - )))); - final value = AirdropClaimShareResponse.fromJson(_result.data!); - return value; - } - - @override - Future share( - String tokenId, - AirdropShareRequest body, - ) async { - const _extra = {}; - final queryParameters = {}; - final _headers = {}; - final _data = {}; - _data.addAll(body.toJson()); - final _result = await _dio.fetch>( - _setStreamType(Options( - method: 'POST', - headers: _headers, - extra: _extra, - ) - .compose( - _dio.options, - '/v1/share/${tokenId}', - queryParameters: queryParameters, - data: _data, - ) - .copyWith( - baseUrl: _combineBaseUrls( - _dio.options.baseUrl, - baseUrl, - )))); - final value = AirdropShareResponse.fromJson(_result.data!); - return value; - } - - @override - Future feralfileClaim( - FeralFileTokenClaimRequest body) async { - const _extra = {}; - final queryParameters = {}; - final _headers = {}; - final _data = {}; - _data.addAll(body.toJson()); - final _result = await _dio - .fetch>(_setStreamType(Options( - method: 'POST', - headers: _headers, - extra: _extra, - ) - .compose( - _dio.options, - '/v1/feralfile-claim', - queryParameters: queryParameters, - data: _data, - ) - .copyWith( - baseUrl: _combineBaseUrls( - _dio.options.baseUrl, - baseUrl, - )))); - final value = TokenClaimResponse.fromJson(_result.data!); - return value; - } - - RequestOptions _setStreamType(RequestOptions requestOptions) { - if (T != dynamic && - !(requestOptions.responseType == ResponseType.bytes || - requestOptions.responseType == ResponseType.stream)) { - if (T == String) { - requestOptions.responseType = ResponseType.plain; - } else { - requestOptions.responseType = ResponseType.json; - } - } - return requestOptions; - } - - String _combineBaseUrls( - String dioBaseUrl, - String? baseUrl, - ) { - if (baseUrl == null || baseUrl.trim().isEmpty) { - return dioBaseUrl; - } - - final url = Uri.parse(baseUrl); - - if (url.isAbsolute) { - return url.toString(); - } - - return Uri.parse(dioBaseUrl).resolveUri(url).toString(); - } -} diff --git a/lib/gateway/feralfile_api.dart b/lib/gateway/feralfile_api.dart index 0bb2c0d044..19698fb86d 100644 --- a/lib/gateway/feralfile_api.dart +++ b/lib/gateway/feralfile_api.dart @@ -6,8 +6,9 @@ // import 'package:autonomy_flutter/model/ff_account.dart'; +import 'package:autonomy_flutter/model/ff_artwork.dart'; import 'package:autonomy_flutter/model/ff_exhibition.dart'; -import 'package:autonomy_flutter/model/ff_exhibition_artworks_response.dart'; +import 'package:autonomy_flutter/model/ff_list_response.dart'; import 'package:autonomy_flutter/model/ff_series.dart'; import 'package:dio/dio.dart'; import 'package:retrofit/retrofit.dart'; @@ -20,7 +21,8 @@ abstract class FeralFileApi { @GET('/api/exhibitions/{exhibitionId}') Future getExhibition( - @Path('exhibitionId') String exhibitionId); + @Path('exhibitionId') String exhibitionId, + {@Query('includeFirstArtwork') bool includeFirstArtwork = false}); @GET('/api/series/{seriesId}') Future getSeries({ @@ -40,12 +42,6 @@ abstract class FeralFileApi { @Query('includeUniqueFilePath') bool includeUniqueFilePath = true, }); - @POST('/api/series/{seriesId}/claim') - Future claimSeries( - @Path('seriesId') String seriesId, - @Body() Map body, - ); - @GET('/api/exhibitions/{exhibitionID}/revenue-setting/resale') Future getResaleInfo( @Path('exhibitionID') String exhibitionID); @@ -69,13 +65,24 @@ abstract class FeralFileApi { @GET('/api/exhibitions/featured') Future getFeaturedExhibition(); + @GET('/api/artworks/featured') + Future getFeaturedArtworks({ + @Query('includeArtist') bool includeArtist = true, + }); + + @GET('/api/exhibitions/upcoming') + Future getUpcomingExhibition(); + @GET('/api/artworks') - Future getListArtworks({ + Future> getListArtworks({ @Query('exhibitionID') String? exhibitionId, @Query('seriesID') String? seriesId, + @Query('offset') int? offset = 0, + @Query('limit') int? limit = 1, @Query('includeActiveSwap') bool includeActiveSwap = true, @Query('sortBy') String sortBy = 'index', @Query('sortOrder') String sortOrder = 'ASC', + @Query('isViewable') bool? isViewable, }); @POST('/api/web3/messages/action') @@ -122,6 +129,22 @@ class FFListSeriesResponse { }; } +class FFListArtworksResponse { + List result; + + FFListArtworksResponse({required this.result}); + + factory FFListArtworksResponse.fromJson(Map json) => + FFListArtworksResponse( + result: + (json['result'] as List).map((e) => Artwork.fromJson(e)).toList(), + ); + + Map toJson() => { + 'result': result, + }; +} + class FeralFileResponse { T result; diff --git a/lib/gateway/feralfile_api.g.dart b/lib/gateway/feralfile_api.g.dart index 4072e04129..0c33cac0df 100644 --- a/lib/gateway/feralfile_api.g.dart +++ b/lib/gateway/feralfile_api.g.dart @@ -19,9 +19,14 @@ class _FeralFileApi implements FeralFileApi { String? baseUrl; @override - Future getExhibition(String exhibitionId) async { + Future getExhibition( + String exhibitionId, { + bool includeFirstArtwork = false, + }) async { const _extra = {}; - final queryParameters = {}; + final queryParameters = { + r'includeFirstArtwork': includeFirstArtwork + }; final _headers = {}; final Map? _data = null; final _result = await _dio @@ -123,37 +128,6 @@ class _FeralFileApi implements FeralFileApi { return value; } - @override - Future claimSeries( - String seriesId, - Map body, - ) async { - const _extra = {}; - final queryParameters = {}; - final _headers = {}; - final _data = {}; - _data.addAll(body); - final _result = await _dio - .fetch>(_setStreamType(Options( - method: 'POST', - headers: _headers, - extra: _extra, - ) - .compose( - _dio.options, - '/api/series/${seriesId}/claim', - queryParameters: queryParameters, - data: _data, - ) - .copyWith( - baseUrl: _combineBaseUrls( - _dio.options.baseUrl, - baseUrl, - )))); - final value = TokenClaimResponse.fromJson(_result.data!); - return value; - } - @override Future getResaleInfo(String exhibitionID) async { const _extra = {}; @@ -283,26 +257,87 @@ class _FeralFileApi implements FeralFileApi { } @override - Future getListArtworks({ + Future getFeaturedArtworks( + {bool includeArtist = true}) async { + const _extra = {}; + final queryParameters = {r'includeArtist': includeArtist}; + final _headers = {}; + final Map? _data = null; + final _result = await _dio.fetch>( + _setStreamType(Options( + method: 'GET', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/artworks/featured', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = FFListArtworksResponse.fromJson(_result.data!); + return value; + } + + @override + Future getUpcomingExhibition() async { + const _extra = {}; + final queryParameters = {}; + final _headers = {}; + final Map? _data = null; + final _result = await _dio + .fetch>(_setStreamType(Options( + method: 'GET', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/exhibitions/upcoming', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = ExhibitionResponse.fromJson(_result.data!); + return value; + } + + @override + Future> getListArtworks({ String? exhibitionId, String? seriesId, + int? offset = 0, + int? limit = 1, bool includeActiveSwap = true, String sortBy = 'index', String sortOrder = 'ASC', + bool? isViewable, }) async { const _extra = {}; final queryParameters = { r'exhibitionID': exhibitionId, r'seriesID': seriesId, + r'offset': offset, + r'limit': limit, r'includeActiveSwap': includeActiveSwap, r'sortBy': sortBy, r'sortOrder': sortOrder, + r'isViewable': isViewable, }; queryParameters.removeWhere((k, v) => v == null); final _headers = {}; final Map? _data = null; - final _result = await _dio - .fetch>(_setStreamType(Options( + final _result = await _dio.fetch>( + _setStreamType>(Options( method: 'GET', headers: _headers, extra: _extra, @@ -318,7 +353,8 @@ class _FeralFileApi implements FeralFileApi { _dio.options.baseUrl, baseUrl, )))); - final value = ArtworksResponse.fromJson(_result.data!); + final value = FeralFileListResponse.fromJson( + _result.data!, Artwork.fromJson); return value; } diff --git a/lib/gateway/iap_api.dart b/lib/gateway/iap_api.dart index 49275a6e0a..5f6e6a808a 100644 --- a/lib/gateway/iap_api.dart +++ b/lib/gateway/iap_api.dart @@ -14,9 +14,9 @@ import 'package:retrofit/retrofit.dart'; part 'iap_api.g.dart'; -@RestApi(baseUrl: "") +@RestApi(baseUrl: '') abstract class IAPApi { - static const authenticationPath = "/apis/v1/auth"; + static const authenticationPath = '/apis/v1/auth'; factory IAPApi(Dio dio, {String baseUrl}) = _IAPApi; @@ -24,36 +24,36 @@ abstract class IAPApi { Future auth(@Body() Map body); @MultiPart() - @POST("/apis/v1/premium/profile-data") + @POST('/apis/v1/premium/profile-data') Future uploadProfile( - @Header("requester") String requester, - @Part(name: "filename") String filename, - @Part(name: "appVersion") String appVersion, - @Part(name: "data") File data, + @Header('requester') String requester, + @Part(name: 'filename') String filename, + @Part(name: 'appVersion') String appVersion, + @Part(name: 'data') File data, ); - @GET("/apis/v1/premium/profile-data/versions") + @GET('/apis/v1/premium/profile-data/versions') Future getProfileVersions( - @Header("requester") String requester, - @Query("filename") String filename, + @Header('requester') String requester, + @Query('filename') String filename, ); - @GET("/apis/v1/premium/profile-data") + @GET('/apis/v1/premium/profile-data') Future getProfileData( - @Header("requester") String requester, - @Query("filename") String filename, - @Query("appVersion") String version, + @Header('requester') String requester, + @Query('filename') String filename, + @Query('appVersion') String version, ); - @DELETE("/apis/v1/premium/profile-data") + @DELETE('/apis/v1/premium/profile-data') Future deleteAllProfiles( - @Header("requester") String requester, + @Header('requester') String requester, ); - @DELETE("/apis/v1/me") + @DELETE('/apis/v1/me') Future deleteUserData(); - @POST("/apis/v1/me/identity-hash") + @POST('/apis/v1/me/identity-hash') Future generateIdentityHash( @Body() Map body); } diff --git a/lib/gateway/source_exhibition_api.dart b/lib/gateway/source_exhibition_api.dart new file mode 100644 index 0000000000..a17cefbe99 --- /dev/null +++ b/lib/gateway/source_exhibition_api.dart @@ -0,0 +1,46 @@ +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// Copyright © 2022 Bitmark. All rights reserved. +// Use of this source code is governed by the BSD-2-Clause Plus Patent License +// that can be found in the LICENSE file. +// + +import 'dart:convert'; + +import 'package:autonomy_flutter/model/ff_exhibition.dart'; +import 'package:autonomy_flutter/model/ff_series.dart'; +import 'package:dio/dio.dart'; +import 'package:retrofit/retrofit.dart'; + +part 'source_exhibition_api.g.dart'; + +@RestApi(baseUrl: '') +abstract class SourceExhibitionAPI { + factory SourceExhibitionAPI(Dio dio, {String baseUrl}) = _SourceExhibitionAPI; + + @GET('/source_exhibition/exhibition.json') + Future getSourceExhibition(); + + @GET('/source_exhibition/series.json') + Future getSourceSeries(); +} + +extension SourceExhibitionAPIHelper on SourceExhibitionAPI { + Future getSourceExhibitionInfo() async { + final value = await getSourceExhibition(); + return Exhibition.fromJson(jsonDecode(value)); + } + + Future> getSourceExhibitionSeries() async { + try { + final value = await getSourceSeries(); + final List series = (jsonDecode(value) as List?) + ?.map((element) => FFSeries.fromJson(element)) + .toList() ?? + []; + return series.map((e) => e.copyWith(artwork: e.artworks!.first)).toList(); + } catch (e) { + return []; + } + } +} diff --git a/lib/gateway/activation_api.g.dart b/lib/gateway/source_exhibition_api.g.dart similarity index 55% rename from lib/gateway/activation_api.g.dart rename to lib/gateway/source_exhibition_api.g.dart index 3f32b83f71..d429f10143 100644 --- a/lib/gateway/activation_api.g.dart +++ b/lib/gateway/source_exhibition_api.g.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'activation_api.dart'; +part of 'source_exhibition_api.dart'; // ************************************************************************** // RetrofitGenerator @@ -8,8 +8,8 @@ part of 'activation_api.dart'; // ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers -class _ActivationApi implements ActivationApi { - _ActivationApi( +class _SourceExhibitionAPI implements SourceExhibitionAPI { + _SourceExhibitionAPI( this._dio, { this.baseUrl, }); @@ -19,57 +19,54 @@ class _ActivationApi implements ActivationApi { String? baseUrl; @override - Future getActivation(String activationId) async { + Future getSourceExhibition() async { const _extra = {}; final queryParameters = {}; final _headers = {}; final Map? _data = null; - final _result = await _dio - .fetch>(_setStreamType(Options( + final _result = await _dio.fetch(_setStreamType(Options( method: 'GET', headers: _headers, extra: _extra, ) - .compose( - _dio.options, - '/v1/activation/${activationId}', - queryParameters: queryParameters, - data: _data, - ) - .copyWith( - baseUrl: _combineBaseUrls( - _dio.options.baseUrl, - baseUrl, - )))); - final value = ActivationInfo.fromJson(_result.data!); + .compose( + _dio.options, + '/source_exhibition/exhibition.json', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = _result.data!; return value; } @override - Future claim(ActivationClaimRequest body) async { + Future getSourceSeries() async { const _extra = {}; final queryParameters = {}; final _headers = {}; - final _data = {}; - _data.addAll(body.toJson()); - final _result = await _dio.fetch>( - _setStreamType(Options( - method: 'POST', + final Map? _data = null; + final _result = await _dio.fetch(_setStreamType(Options( + method: 'GET', headers: _headers, extra: _extra, ) - .compose( - _dio.options, - '/v1/activation/claim', - queryParameters: queryParameters, - data: _data, - ) - .copyWith( - baseUrl: _combineBaseUrls( - _dio.options.baseUrl, - baseUrl, - )))); - final value = ActivationClaimResponse.fromJson(_result.data!); + .compose( + _dio.options, + '/source_exhibition/series.json', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = _result.data!; return value; } diff --git a/lib/gateway/tv_cast_api.dart b/lib/gateway/tv_cast_api.dart new file mode 100644 index 0000000000..afc68a6c52 --- /dev/null +++ b/lib/gateway/tv_cast_api.dart @@ -0,0 +1,16 @@ +import 'package:dio/dio.dart'; +import 'package:retrofit/retrofit.dart'; + +part 'tv_cast_api.g.dart'; + +@RestApi(baseUrl: '') +abstract class TvCastApi { + factory TvCastApi(Dio dio, {String baseUrl}) = _TvCastApi; + + @GET('/api/cast') + Future request({ + @Query('locationID') required String locationId, + @Query('topicID') required String topicId, + @Body() required Map body, + }); +} diff --git a/lib/gateway/tv_cast_api.g.dart b/lib/gateway/tv_cast_api.g.dart new file mode 100644 index 0000000000..53d82fc33c --- /dev/null +++ b/lib/gateway/tv_cast_api.g.dart @@ -0,0 +1,84 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'tv_cast_api.dart'; + +// ************************************************************************** +// RetrofitGenerator +// ************************************************************************** + +// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers + +class _TvCastApi implements TvCastApi { + _TvCastApi( + this._dio, { + this.baseUrl, + }); + + final Dio _dio; + + String? baseUrl; + + @override + Future request({ + required String locationId, + required String topicId, + required Map body, + }) async { + const _extra = {}; + final queryParameters = { + r'locationID': locationId, + r'topicID': topicId, + }; + final _headers = {}; + final _data = {}; + _data.addAll(body); + final _result = await _dio.fetch(_setStreamType(Options( + method: 'GET', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/cast', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = _result.data; + return value; + } + + RequestOptions _setStreamType(RequestOptions requestOptions) { + if (T != dynamic && + !(requestOptions.responseType == ResponseType.bytes || + requestOptions.responseType == ResponseType.stream)) { + if (T == String) { + requestOptions.responseType = ResponseType.plain; + } else { + requestOptions.responseType = ResponseType.json; + } + } + return requestOptions; + } + + String _combineBaseUrls( + String dioBaseUrl, + String? baseUrl, + ) { + if (baseUrl == null || baseUrl.trim().isEmpty) { + return dioBaseUrl; + } + + final url = Uri.parse(baseUrl); + + if (url.isAbsolute) { + return url.toString(); + } + + return Uri.parse(dioBaseUrl).resolveUri(url).toString(); + } +} diff --git a/lib/main.dart b/lib/main.dart index 3676dc6918..74d79c8728 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -21,15 +21,18 @@ import 'package:autonomy_flutter/model/eth_pending_tx_amount.dart'; import 'package:autonomy_flutter/screen/app_router.dart'; import 'package:autonomy_flutter/service/configuration_service.dart'; import 'package:autonomy_flutter/service/deeplink_service.dart'; +import 'package:autonomy_flutter/service/device_info_service.dart'; import 'package:autonomy_flutter/service/iap_service.dart'; import 'package:autonomy_flutter/service/metric_client_service.dart'; import 'package:autonomy_flutter/service/navigation_service.dart'; import 'package:autonomy_flutter/service/notification_service.dart'; import 'package:autonomy_flutter/service/remote_config_service.dart'; import 'package:autonomy_flutter/util/au_file_service.dart'; +import 'package:autonomy_flutter/util/canvas_device_adapter.dart'; import 'package:autonomy_flutter/util/custom_route_observer.dart'; import 'package:autonomy_flutter/util/device.dart'; import 'package:autonomy_flutter/util/error_handler.dart'; +import 'package:autonomy_flutter/util/john_gerrard_helper.dart'; import 'package:autonomy_flutter/util/log.dart'; import 'package:autonomy_flutter/util/route_ext.dart'; import 'package:autonomy_flutter/util/style.dart'; @@ -130,7 +133,8 @@ Future runFeralFileApp() async { void _registerHiveAdapter() { Hive ..registerAdapter(EthereumPendingTxAmountAdapter()) - ..registerAdapter(EthereumPendingTxListAdapter()); + ..registerAdapter(EthereumPendingTxListAdapter()) + ..registerAdapter(CanvasDeviceAdapter()); } Future _setupApp() async { @@ -139,6 +143,8 @@ Future _setupApp() async { await DeviceInfo.instance.init(); await LibAukDart.migrate(); + await injector().init(); + final metricClient = injector.get(); await metricClient.initService(); await injector().loadConfigs(); @@ -153,6 +159,7 @@ Future _setupApp() async { await disableLandscapeMode(); final isPremium = await injector.get().isSubscribed(); await injector().setPremium(isPremium); + await JohnGerrardHelper.updateJohnGerrardLatestRevealIndex(); runApp( EasyLocalization( @@ -167,9 +174,7 @@ Future _setupApp() async { Sentry.configureScope((scope) async { final deviceID = await getDeviceID(); - if (deviceID != null) { - scope.setUser(SentryUser(id: deviceID)); - } + scope.setUser(SentryUser(id: deviceID)); }); //safe delay to wait for onboarding finished diff --git a/lib/model/add_ethereum_chain.dart b/lib/model/add_ethereum_chain.dart deleted file mode 100644 index 75d2e50b3b..0000000000 --- a/lib/model/add_ethereum_chain.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'package:autonomy_flutter/util/string_ext.dart'; - -class AddEthereumChainParameter { - final String chainId; - final List? blockExplorerUrls; - final String? chainName; - final List? iconUrls; - final NativeCurrency? nativeCurrency; - final List rpcUrls; - - AddEthereumChainParameter({ - required this.chainId, - required this.rpcUrls, - this.blockExplorerUrls, - this.chainName, - this.iconUrls, - this.nativeCurrency, - }); - - factory AddEthereumChainParameter.fromJson(Map json) => - AddEthereumChainParameter( - chainId: json['chainId'], - blockExplorerUrls: json['blockExplorerUrls'] != null - ? List.from(json['blockExplorerUrls']) - : null, - chainName: json['chainName'], - iconUrls: json['iconUrls'] != null - ? List.from(json['iconUrls']) - : null, - nativeCurrency: json['nativeCurrency'] != null - ? NativeCurrency.fromJson(json['nativeCurrency']) - : null, - rpcUrls: List.from(json['rpcUrls']), // Not null - ); - - bool get isValid { - final chainIdRegExp = RegExp(r'^0x[0-9a-fA-F]+$'); - - if (chainIdRegExp.hasMatch(chainId) && - _isValidUrlList(rpcUrls, allowNullAndEmpty: false) && - _isValidUrlList(iconUrls) && - _isValidUrlList(blockExplorerUrls) && - (nativeCurrency == null || - (nativeCurrency!.name.isNotEmpty && - nativeCurrency!.symbol.isNotEmpty && - nativeCurrency!.decimals > 0))) { - return true; - } - - return false; - } - - bool _isValidUrlList(List? urls, {bool allowNullAndEmpty = true}) { - if (urls == null || urls.isEmpty) { - return allowNullAndEmpty; - } - for (String url in urls) { - if (!url.isValidUrl() || url.isInvalidRPCScheme()) { - return false; - } - } - return true; - } - - String get chainNet { - switch (int.parse(chainId.substring('0x'.length), radix: 16)) { - case 1: - return 'Mainnet'; - case 5: - return 'Goerli'; - case 11155111: - return 'Sepolia'; - default: - return 'unknown'; - } - } -} - -class NativeCurrency { - final String name; - final String symbol; - final int decimals; - - NativeCurrency({ - required this.name, - required this.symbol, - required this.decimals, - }); - - factory NativeCurrency.fromJson(Map json) => NativeCurrency( - name: json['name'], - symbol: json['symbol'], - decimals: json['decimals'], - ); -} diff --git a/lib/model/customer_support.dart b/lib/model/customer_support.dart index 93ede8ac1e..fa4c0ef995 100644 --- a/lib/model/customer_support.dart +++ b/lib/model/customer_support.dart @@ -30,6 +30,8 @@ class Issue implements ChatThread { int rating; @JsonKey(name: 'last_message') Message? lastMessage; + @JsonKey(name: 'first_message') + Message? firstMessage; // only on local @JsonKey(includeFromJson: false, includeToJson: false) DraftCustomerSupport? draft; @@ -47,6 +49,7 @@ class Issue implements ChatThread { required this.total, required this.unread, required this.lastMessage, + required this.firstMessage, this.draft, required this.rating, this.announcementID, @@ -56,16 +59,13 @@ class Issue implements ChatThread { Map toJson() => _$IssueToJson(this); - String get reportIssueType { - return ReportIssueType.getList - .firstWhereOrNull((element) => tags.contains(element)) ?? - ""; - } + String get reportIssueType => + ReportIssueType.getList + .firstWhereOrNull((element) => tags.contains(element)) ?? + ''; @override - String getListTitle() { - return ReportIssueType.toTitle(reportIssueType); - } + String getListTitle() => ReportIssueType.toTitle(reportIssueType); } @JsonSerializable() diff --git a/lib/model/customer_support.g.dart b/lib/model/customer_support.g.dart index 8b4fba8038..179800d2a7 100644 --- a/lib/model/customer_support.g.dart +++ b/lib/model/customer_support.g.dart @@ -17,6 +17,9 @@ Issue _$IssueFromJson(Map json) => Issue( lastMessage: json['last_message'] == null ? null : Message.fromJson(json['last_message'] as Map), + firstMessage: json['first_message'] == null + ? null + : Message.fromJson(json['first_message'] as Map), rating: json['rating'] as int, announcementID: json['announcement_context_id'] as String?, ); @@ -31,6 +34,7 @@ Map _$IssueToJson(Issue instance) => { 'unread': instance.unread, 'rating': instance.rating, 'last_message': instance.lastMessage, + 'first_message': instance.firstMessage, 'announcement_context_id': instance.announcementID, }; diff --git a/lib/model/ff_account.dart b/lib/model/ff_account.dart index bb0e2d3de2..470b0ab6d9 100644 --- a/lib/model/ff_account.dart +++ b/lib/model/ff_account.dart @@ -5,9 +5,7 @@ // that can be found in the LICENSE file. // -import 'package:autonomy_flutter/model/ff_series.dart'; import 'package:json_annotation/json_annotation.dart'; -import 'package:nft_rendering/nft_rendering.dart'; part 'ff_account.g.dart'; @@ -72,88 +70,6 @@ class FFContract { Map toJson() => _$FFContractToJson(this); } -@JsonSerializable() -class AirdropInfo { - final String contractAddress; - final String blockchain; - final int remainAmount; - final String? seriesId; // TODO: rename? - final String? seriesTitle; - final String? artist; - final String? gifter; - final DateTime? startedAt; - final DateTime? endedAt; - final String? twitterCaption; - - AirdropInfo( - this.contractAddress, - this.blockchain, - this.remainAmount, - this.seriesId, - this.seriesTitle, - this.artist, - this.gifter, - this.startedAt, - this.endedAt, - this.twitterCaption, - ); - - factory AirdropInfo.fromJson(Map json) => - _$AirdropInfoFromJson(json); - - Map toJson() => _$AirdropInfoToJson(this); - - bool get isAirdropStarted => startedAt?.isBefore(DateTime.now()) == true; -} - -@JsonSerializable() -class TokenClaimResponse { - final TokenClaimResult result; - - TokenClaimResponse(this.result); - - factory TokenClaimResponse.fromJson(Map json) => - _$TokenClaimResponseFromJson(json); - - Map toJson() => _$TokenClaimResponseToJson(this); - - @override - String toString() => 'TokenClaimResponse{result: $result}'; -} - -@JsonSerializable() -class TokenClaimResult { - final String id; - final String claimerID; - final String exhibitionID; - final String seriesID; - final String artworkID; - final String txID; - final Map? metadata; - - TokenClaimResult( - this.id, - this.claimerID, - this.exhibitionID, - this.artworkID, - this.txID, - this.seriesID, - this.metadata, - ); - - factory TokenClaimResult.fromJson(Map json) => - _$TokenClaimResultFromJson(json); - - Map toJson() => _$TokenClaimResultToJson(this); - - @override - String toString() => 'TokenClaimResult{id: $id, ' - 'claimerID: $claimerID, ' - 'exhibitionID: $exhibitionID, ' - 'artworkID: $artworkID, ' - 'txID: $txID}'; -} - @JsonSerializable() class FeralfileError { final int code; @@ -214,248 +130,6 @@ class FeralFileResaleInfo { Map toJson() => _$FeralFileResaleInfoToJson(this); } -@JsonSerializable() -class ArtworkResponse { - final Artwork result; - - ArtworkResponse(this.result); - - factory ArtworkResponse.fromJson(Map json) => - _$ArtworkResponseFromJson(json); - - Map toJson() => _$ArtworkResponseToJson(this); -} - -@JsonSerializable() -class Artwork { - final String id; - final String seriesID; - final int index; - final String name; - final String category; - final String ownerAccountID; - final bool? virgin; - final bool? burned; - final String blockchainStatus; - final bool isExternal; - final String thumbnailURI; - final String previewURI; - final Map metadata; - final DateTime mintedAt; - final DateTime createdAt; - final DateTime updatedAt; - final bool? isArchived; - final FFSeries? series; - final ArtworkSwap? swap; - - Artwork( - this.id, - this.seriesID, - this.index, - this.name, - this.category, - this.ownerAccountID, - this.virgin, - this.burned, - this.blockchainStatus, - this.isExternal, - this.thumbnailURI, - this.previewURI, - this.metadata, - this.mintedAt, - this.createdAt, - this.updatedAt, - this.isArchived, - this.series, - this.swap); - - factory Artwork.fromJson(Map json) => - _$ArtworkFromJson(json); - - Map toJson() => _$ArtworkToJson(this); - - // copyWith method - Artwork copyWith({ - String? id, - String? seriesID, - int? index, - String? name, - String? category, - String? ownerAccountID, - bool? virgin, - bool? burned, - String? blockchainStatus, - bool? isExternal, - String? thumbnailURI, - String? previewURI, - Map? metadata, - DateTime? mintedAt, - DateTime? createdAt, - DateTime? updatedAt, - bool? isArchived, - FFSeries? series, - ArtworkSwap? swap, - }) => - Artwork( - id ?? this.id, - seriesID ?? this.seriesID, - index ?? this.index, - name ?? this.name, - category ?? this.category, - ownerAccountID ?? this.ownerAccountID, - virgin ?? this.virgin, - burned ?? this.burned, - blockchainStatus ?? this.blockchainStatus, - isExternal ?? this.isExternal, - thumbnailURI ?? this.thumbnailURI, - previewURI ?? this.previewURI, - metadata ?? this.metadata, - mintedAt ?? this.mintedAt, - createdAt ?? this.createdAt, - updatedAt ?? this.updatedAt, - isArchived ?? this.isArchived, - series ?? this.series, - swap ?? this.swap, - ); - - static Artwork createFake( - String thumbNailURI, String previewURI, String medium) => - Artwork( - 'id', - 'seriesID', - 0, - 'name', - 'category', - 'ownerAccountID', - false, - false, - 'blockchainStatus', - false, - thumbNailURI, - previewURI, - {}, - DateTime.now(), - DateTime.now(), - DateTime.now(), - false, - FFSeries( - 'id', - 'artistID', - 'assetID', - 'title', - 'slug', - medium, - 'description', - 'thumbnailURI', - 'exhibitionID', - {}, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - ), - null, - ); -} - -class ArtworkSwap { - final String id; - final String artworkID; - final String seriesID; - final String? paymentID; - final double? fee; - final String currency; - final int artworkIndex; - final String ownerAccount; - final String status; - final String contractName; - final String contractAddress; - final String recipientAddress; - final String? ipfsCid; - final String? token; - final String blockchainType; - final DateTime createdAt; - final DateTime updatedAt; - final DateTime expiredAt; - - // Constructor - ArtworkSwap({ - required this.id, - required this.artworkID, - required this.seriesID, - required this.currency, - required this.artworkIndex, - required this.ownerAccount, - required this.status, - required this.contractName, - required this.contractAddress, - required this.recipientAddress, - required this.blockchainType, - required this.createdAt, - required this.updatedAt, - required this.expiredAt, - this.ipfsCid, - this.token, - this.paymentID, - this.fee, - }); - - // Factory method to create an ArtworkSwap instance from JSON - factory ArtworkSwap.fromJson(Map json) => ArtworkSwap( - id: json['id'], - artworkID: json['artworkID'], - seriesID: json['seriesID'], - paymentID: json['paymentID'], - fee: json['fee']?.toDouble(), - currency: json['currency'], - artworkIndex: json['artworkIndex'], - ownerAccount: json['ownerAccount'], - status: json['status'], - contractName: json['contractName'], - contractAddress: json['contractAddress'], - recipientAddress: json['recipientAddress'], - ipfsCid: json['ipfsCid'], - token: json['token'], - blockchainType: json['blockchainType'], - createdAt: DateTime.parse(json['createdAt']), - updatedAt: DateTime.parse(json['updatedAt']), - expiredAt: DateTime.parse(json['expiredAt']), - ); - - // Method to convert ArtworkSwap instance to JSON - Map toJson() => { - 'id': id, - 'artworkID': artworkID, - 'seriesID': seriesID, - 'paymentID': paymentID, - 'fee': fee, - 'currency': currency, - 'artworkIndex': artworkIndex, - 'ownerAccount': ownerAccount, - 'status': status, - 'contractName': contractName, - 'contractAddress': contractAddress, - 'recipientAddress': recipientAddress, - 'ipfsCid': ipfsCid, - 'token': token, - 'blockchainType': blockchainType, - 'createdAt': createdAt.toIso8601String(), - 'updatedAt': updatedAt.toIso8601String(), - 'expiredAt': expiredAt.toIso8601String(), - }; -} - class FileAssetMetadata { final String urlOverwrite; @@ -514,62 +188,3 @@ class FileInfo { 'updatedAt': updatedAt, }; } - -enum FeralfileMediumTypes { - unknown, - image, - video, - software, - pdf, - audio, - model, - animatedGif, - txt, - ; - - static FeralfileMediumTypes fromString(String type) { - switch (type) { - case 'image': - return FeralfileMediumTypes.image; - case 'video': - return FeralfileMediumTypes.video; - case 'software': - return FeralfileMediumTypes.software; - case 'pdf': - return FeralfileMediumTypes.pdf; - case 'audio': - return FeralfileMediumTypes.audio; - case '3d': - return FeralfileMediumTypes.model; - case 'animated gif': - return FeralfileMediumTypes.animatedGif; - case 'txt': - return FeralfileMediumTypes.txt; - default: - return FeralfileMediumTypes.unknown; - } - } - - String get toRenderingType { - switch (this) { - case FeralfileMediumTypes.image: - return RenderingType.image; - case FeralfileMediumTypes.video: - return RenderingType.video; - case FeralfileMediumTypes.software: - return RenderingType.webview; - case FeralfileMediumTypes.pdf: - return RenderingType.pdf; - case FeralfileMediumTypes.audio: - return RenderingType.audio; - case FeralfileMediumTypes.model: - return RenderingType.modelViewer; - case FeralfileMediumTypes.animatedGif: - return RenderingType.gif; - case FeralfileMediumTypes.txt: - return RenderingType.webview; - default: - return RenderingType.webview; - } - } -} diff --git a/lib/model/ff_account.g.dart b/lib/model/ff_account.g.dart index 4be72c8696..b9f8069cec 100644 --- a/lib/model/ff_account.g.dart +++ b/lib/model/ff_account.g.dart @@ -51,69 +51,6 @@ Map _$FFContractToJson(FFContract instance) => 'address': instance.address, }; -AirdropInfo _$AirdropInfoFromJson(Map json) => AirdropInfo( - json['contractAddress'] as String, - json['blockchain'] as String, - json['remainAmount'] as int, - json['seriesId'] as String?, - json['seriesTitle'] as String?, - json['artist'] as String?, - json['gifter'] as String?, - json['startedAt'] == null - ? null - : DateTime.parse(json['startedAt'] as String), - json['endedAt'] == null - ? null - : DateTime.parse(json['endedAt'] as String), - json['twitterCaption'] as String?, - ); - -Map _$AirdropInfoToJson(AirdropInfo instance) => - { - 'contractAddress': instance.contractAddress, - 'blockchain': instance.blockchain, - 'remainAmount': instance.remainAmount, - 'seriesId': instance.seriesId, - 'seriesTitle': instance.seriesTitle, - 'artist': instance.artist, - 'gifter': instance.gifter, - 'startedAt': instance.startedAt?.toIso8601String(), - 'endedAt': instance.endedAt?.toIso8601String(), - 'twitterCaption': instance.twitterCaption, - }; - -TokenClaimResponse _$TokenClaimResponseFromJson(Map json) => - TokenClaimResponse( - TokenClaimResult.fromJson(json['result'] as Map), - ); - -Map _$TokenClaimResponseToJson(TokenClaimResponse instance) => - { - 'result': instance.result, - }; - -TokenClaimResult _$TokenClaimResultFromJson(Map json) => - TokenClaimResult( - json['id'] as String, - json['claimerID'] as String, - json['exhibitionID'] as String, - json['artworkID'] as String, - json['txID'] as String, - json['seriesID'] as String, - json['metadata'] as Map?, - ); - -Map _$TokenClaimResultToJson(TokenClaimResult instance) => - { - 'id': instance.id, - 'claimerID': instance.claimerID, - 'exhibitionID': instance.exhibitionID, - 'seriesID': instance.seriesID, - 'artworkID': instance.artworkID, - 'txID': instance.txID, - 'metadata': instance.metadata, - }; - FeralfileError _$FeralfileErrorFromJson(Map json) => FeralfileError( json['code'] as int, @@ -162,61 +99,3 @@ Map _$FeralFileResaleInfoToJson( 'createdAt': instance.createdAt.toIso8601String(), 'updatedAt': instance.updatedAt.toIso8601String(), }; - -ArtworkResponse _$ArtworkResponseFromJson(Map json) => - ArtworkResponse( - Artwork.fromJson(json['result'] as Map), - ); - -Map _$ArtworkResponseToJson(ArtworkResponse instance) => - { - 'result': instance.result, - }; - -Artwork _$ArtworkFromJson(Map json) => Artwork( - json['id'] as String, - json['seriesID'] as String, - json['index'] as int, - json['name'] as String, - json['category'] as String, - json['ownerAccountID'] as String, - json['virgin'] as bool?, - json['burned'] as bool?, - json['blockchainStatus'] as String, - json['isExternal'] as bool, - json['thumbnailURI'] as String, - json['previewURI'] as String, - json['metadata'] as Map, - DateTime.parse(json['mintedAt'] as String), - DateTime.parse(json['createdAt'] as String), - DateTime.parse(json['updatedAt'] as String), - json['isArchived'] as bool?, - json['series'] == null - ? null - : FFSeries.fromJson(json['series'] as Map), - json['swap'] == null - ? null - : ArtworkSwap.fromJson(json['swap'] as Map), - ); - -Map _$ArtworkToJson(Artwork instance) => { - 'id': instance.id, - 'seriesID': instance.seriesID, - 'index': instance.index, - 'name': instance.name, - 'category': instance.category, - 'ownerAccountID': instance.ownerAccountID, - 'virgin': instance.virgin, - 'burned': instance.burned, - 'blockchainStatus': instance.blockchainStatus, - 'isExternal': instance.isExternal, - 'thumbnailURI': instance.thumbnailURI, - 'previewURI': instance.previewURI, - 'metadata': instance.metadata, - 'mintedAt': instance.mintedAt.toIso8601String(), - 'createdAt': instance.createdAt.toIso8601String(), - 'updatedAt': instance.updatedAt.toIso8601String(), - 'isArchived': instance.isArchived, - 'series': instance.series, - 'swap': instance.swap, - }; diff --git a/lib/model/ff_artwork.dart b/lib/model/ff_artwork.dart new file mode 100644 index 0000000000..77c70e3130 --- /dev/null +++ b/lib/model/ff_artwork.dart @@ -0,0 +1,346 @@ +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// Copyright © 2022 Bitmark. All rights reserved. +// Use of this source code is governed by the BSD-2-Clause Plus Patent License +// that can be found in the LICENSE file. +// + +import 'package:autonomy_flutter/model/ff_series.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:nft_rendering/nft_rendering.dart'; + +part 'ff_artwork.g.dart'; + +@JsonSerializable() +class ArtworkResponse { + final Artwork result; + + ArtworkResponse(this.result); + + factory ArtworkResponse.fromJson(Map json) => + _$ArtworkResponseFromJson(json); + + Map toJson() => _$ArtworkResponseToJson(this); +} + +@JsonSerializable() +class Artwork { + final String id; + final String seriesID; + final int index; + final String name; + final String? category; + final String? ownerAccountID; + final bool? virgin; + final bool? burned; + final String? blockchainStatus; + final bool? isExternal; + final String thumbnailURI; + final String previewURI; + final Map? metadata; + final DateTime? mintedAt; + final DateTime? createdAt; + final DateTime? updatedAt; + final bool? isArchived; + final FFSeries? series; + final ArtworkSwap? swap; + final List? artworkAttributes; + + Artwork( + this.id, + this.seriesID, + this.index, + this.name, + this.category, + this.ownerAccountID, + this.virgin, + this.burned, + this.blockchainStatus, + this.isExternal, + this.thumbnailURI, + this.previewURI, + this.metadata, + this.mintedAt, + this.createdAt, + this.updatedAt, + this.isArchived, + this.series, + this.swap, + this.artworkAttributes); + + factory Artwork.fromJson(Map json) => Artwork( + json['id'] as String, + json['seriesID'] as String, + json['index'] as int, + json['name'] as String, + json['category'] as String?, + json['ownerAccountID'] as String?, + json['virgin'] as bool?, + json['burned'] as bool?, + json['blockchainStatus'] as String?, + json['isExternal'] as bool?, + json['thumbnailURI'] as String, + json['previewURI'] as String, + json['metadata'] as Map?, + json['mintedAt'] == null || (json['mintedAt'] as String).isEmpty + ? null + : DateTime.parse(json['mintedAt'] as String), + json['createdAt'] == null + ? null + : DateTime.parse(json['createdAt'] as String), + json['updatedAt'] == null + ? null + : DateTime.parse(json['updatedAt'] as String), + json['isArchived'] as bool?, + json['series'] == null + ? null + : FFSeries.fromJson(json['series'] as Map), + json['swap'] == null + ? null + : ArtworkSwap.fromJson(json['swap'] as Map), + json['artworkAttributes'] == null + ? null + : (json['artworkAttributes'] as List) + .map( + (e) => ArtworkAttribute.fromJson(e as Map)) + .toList(), + ); + + Map toJson() => _$ArtworkToJson(this); + + // copyWith method + Artwork copyWith({ + String? id, + String? seriesID, + int? index, + String? name, + String? category, + String? ownerAccountID, + bool? virgin, + bool? burned, + String? blockchainStatus, + bool? isExternal, + String? thumbnailURI, + String? previewURI, + Map? metadata, + DateTime? mintedAt, + DateTime? createdAt, + DateTime? updatedAt, + bool? isArchived, + FFSeries? series, + ArtworkSwap? swap, + List? artworkAttributes, + }) => + Artwork( + id ?? this.id, + seriesID ?? this.seriesID, + index ?? this.index, + name ?? this.name, + category ?? this.category, + ownerAccountID ?? this.ownerAccountID, + virgin ?? this.virgin, + burned ?? this.burned, + blockchainStatus ?? this.blockchainStatus, + isExternal ?? this.isExternal, + thumbnailURI ?? this.thumbnailURI, + previewURI ?? this.previewURI, + metadata ?? this.metadata, + mintedAt ?? this.mintedAt, + createdAt ?? this.createdAt, + updatedAt ?? this.updatedAt, + isArchived ?? this.isArchived, + series ?? this.series, + swap ?? this.swap, + artworkAttributes ?? this.artworkAttributes, + ); +} + +class ArtworkSwap { + final String id; + final String artworkID; + final String seriesID; + final String? paymentID; + final double? fee; + final String currency; + final int artworkIndex; + final String ownerAccount; + final String status; + final String contractName; + final String contractAddress; + final String recipientAddress; + final String? ipfsCid; + final String? token; + final String blockchainType; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime expiredAt; + + // Constructor + ArtworkSwap({ + required this.id, + required this.artworkID, + required this.seriesID, + required this.currency, + required this.artworkIndex, + required this.ownerAccount, + required this.status, + required this.contractName, + required this.contractAddress, + required this.recipientAddress, + required this.blockchainType, + required this.createdAt, + required this.updatedAt, + required this.expiredAt, + this.ipfsCid, + this.token, + this.paymentID, + this.fee, + }); + + // Factory method to create an ArtworkSwap instance from JSON + factory ArtworkSwap.fromJson(Map json) => ArtworkSwap( + id: json['id'], + artworkID: json['artworkID'], + seriesID: json['seriesID'], + paymentID: json['paymentID'], + fee: json['fee']?.toDouble(), + currency: json['currency'], + artworkIndex: json['artworkIndex'], + ownerAccount: json['ownerAccount'], + status: json['status'], + contractName: json['contractName'], + contractAddress: json['contractAddress'], + recipientAddress: json['recipientAddress'], + ipfsCid: json['ipfsCid'], + token: json['token'], + blockchainType: json['blockchainType'], + createdAt: DateTime.parse(json['createdAt']), + updatedAt: DateTime.parse(json['updatedAt']), + expiredAt: DateTime.parse(json['expiredAt']), + ); + + // Method to convert ArtworkSwap instance to JSON + Map toJson() => { + 'id': id, + 'artworkID': artworkID, + 'seriesID': seriesID, + 'paymentID': paymentID, + 'fee': fee, + 'currency': currency, + 'artworkIndex': artworkIndex, + 'ownerAccount': ownerAccount, + 'status': status, + 'contractName': contractName, + 'contractAddress': contractAddress, + 'recipientAddress': recipientAddress, + 'ipfsCid': ipfsCid, + 'token': token, + 'blockchainType': blockchainType, + 'createdAt': createdAt.toIso8601String(), + 'updatedAt': updatedAt.toIso8601String(), + 'expiredAt': expiredAt.toIso8601String(), + }; +} + +@JsonSerializable() +class ArtworkAttribute { + final String id; + final String artworkID; + final String seriesID; + final int index; + final double percentage; + final String traitType; + final String value; + + ArtworkAttribute({ + required this.id, + required this.artworkID, + required this.seriesID, + required this.index, + required this.percentage, + required this.traitType, + required this.value, + }); + + factory ArtworkAttribute.fromJson(Map json) => + _$ArtworkAttributeFromJson(json); +} + +enum FeralfileMediumTypes { + unknown, + image, + video, + software, + pdf, + audio, + model, + animatedGif, + txt, + ; + + static FeralfileMediumTypes fromString(String type) { + switch (type) { + case 'image': + return FeralfileMediumTypes.image; + case 'video': + return FeralfileMediumTypes.video; + case 'software': + return FeralfileMediumTypes.software; + case 'pdf': + return FeralfileMediumTypes.pdf; + case 'audio': + return FeralfileMediumTypes.audio; + case '3d': + return FeralfileMediumTypes.model; + case 'animated gif': + return FeralfileMediumTypes.animatedGif; + case 'txt': + return FeralfileMediumTypes.txt; + default: + return FeralfileMediumTypes.unknown; + } + } + + String get toRenderingType { + switch (this) { + case FeralfileMediumTypes.image: + return RenderingType.image; + case FeralfileMediumTypes.video: + return RenderingType.video; + case FeralfileMediumTypes.software: + return RenderingType.webview; + case FeralfileMediumTypes.pdf: + return RenderingType.pdf; + case FeralfileMediumTypes.audio: + return RenderingType.audio; + case FeralfileMediumTypes.model: + return RenderingType.modelViewer; + case FeralfileMediumTypes.animatedGif: + return RenderingType.gif; + case FeralfileMediumTypes.txt: + return RenderingType.webview; + default: + return RenderingType.webview; + } + } +} + +// Support for John Gerrard show +class BeforeMintingArtworkInfo { + final int index; + final String viewableAt; + final String artworkTitle; + + BeforeMintingArtworkInfo({ + required this.index, + required this.viewableAt, + required this.artworkTitle, + }); + + factory BeforeMintingArtworkInfo.fromJson(Map json) => + BeforeMintingArtworkInfo( + index: json['index'], + viewableAt: json['viewableAt'], + artworkTitle: json['artworkTitle'], + ); +} diff --git a/lib/model/ff_artwork.g.dart b/lib/model/ff_artwork.g.dart new file mode 100644 index 0000000000..ef7fe7fbb7 --- /dev/null +++ b/lib/model/ff_artwork.g.dart @@ -0,0 +1,97 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'ff_artwork.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ArtworkResponse _$ArtworkResponseFromJson(Map json) => + ArtworkResponse( + Artwork.fromJson(json['result'] as Map), + ); + +Map _$ArtworkResponseToJson(ArtworkResponse instance) => + { + 'result': instance.result, + }; + +Artwork _$ArtworkFromJson(Map json) => Artwork( + json['id'] as String, + json['seriesID'] as String, + json['index'] as int, + json['name'] as String, + json['category'] as String?, + json['ownerAccountID'] as String?, + json['virgin'] as bool?, + json['burned'] as bool?, + json['blockchainStatus'] as String?, + json['isExternal'] as bool?, + json['thumbnailURI'] as String, + json['previewURI'] as String, + json['metadata'] as Map?, + json['mintedAt'] == null + ? null + : DateTime.parse(json['mintedAt'] as String), + json['createdAt'] == null + ? null + : DateTime.parse(json['createdAt'] as String), + json['updatedAt'] == null + ? null + : DateTime.parse(json['updatedAt'] as String), + json['isArchived'] as bool?, + json['series'] == null + ? null + : FFSeries.fromJson(json['series'] as Map), + json['swap'] == null + ? null + : ArtworkSwap.fromJson(json['swap'] as Map), + (json['artworkAttributes'] as List?) + ?.map((e) => ArtworkAttribute.fromJson(e as Map)) + .toList(), + ); + +Map _$ArtworkToJson(Artwork instance) => { + 'id': instance.id, + 'seriesID': instance.seriesID, + 'index': instance.index, + 'name': instance.name, + 'category': instance.category, + 'ownerAccountID': instance.ownerAccountID, + 'virgin': instance.virgin, + 'burned': instance.burned, + 'blockchainStatus': instance.blockchainStatus, + 'isExternal': instance.isExternal, + 'thumbnailURI': instance.thumbnailURI, + 'previewURI': instance.previewURI, + 'metadata': instance.metadata, + 'mintedAt': instance.mintedAt?.toIso8601String(), + 'createdAt': instance.createdAt?.toIso8601String(), + 'updatedAt': instance.updatedAt?.toIso8601String(), + 'isArchived': instance.isArchived, + 'series': instance.series, + 'swap': instance.swap, + 'artworkAttributes': instance.artworkAttributes, + }; + +ArtworkAttribute _$ArtworkAttributeFromJson(Map json) => + ArtworkAttribute( + id: json['id'] as String, + artworkID: json['artworkID'] as String, + seriesID: json['seriesID'] as String, + index: json['index'] as int, + percentage: (json['percentage'] as num).toDouble(), + traitType: json['traitType'] as String, + value: json['value'] as String, + ); + +Map _$ArtworkAttributeToJson(ArtworkAttribute instance) => + { + 'id': instance.id, + 'artworkID': instance.artworkID, + 'seriesID': instance.seriesID, + 'index': instance.index, + 'percentage': instance.percentage, + 'traitType': instance.traitType, + 'value': instance.value, + }; diff --git a/lib/model/ff_exhibition.dart b/lib/model/ff_exhibition.dart index 185b04996a..cea4fd79e3 100644 --- a/lib/model/ff_exhibition.dart +++ b/lib/model/ff_exhibition.dart @@ -1,14 +1,20 @@ import 'package:autonomy_flutter/common/environment.dart'; import 'package:autonomy_flutter/model/ff_account.dart'; +import 'package:autonomy_flutter/model/ff_artwork.dart'; import 'package:autonomy_flutter/model/ff_series.dart'; import 'package:autonomy_flutter/model/ff_user.dart'; +import 'package:autonomy_flutter/util/constants.dart'; +import 'package:autonomy_flutter/util/exhibition_ext.dart'; +import 'package:autonomy_flutter/util/string_ext.dart'; import 'package:collection/collection.dart'; +import 'package:easy_localization/easy_localization.dart'; class Exhibition { final String id; final String title; final String slug; final DateTime exhibitionStartAt; + final int? previewDuration; final String noteTitle; final String noteBrief; @@ -23,7 +29,7 @@ class Exhibition { final List? contracts; final FFArtist? partner; final String type; - final List? resources; + final List? posts; final int status; Exhibition({ @@ -31,6 +37,7 @@ class Exhibition { required this.title, required this.slug, required this.exhibitionStartAt, + required this.previewDuration, required this.noteTitle, required this.noteBrief, required this.note, @@ -44,7 +51,7 @@ class Exhibition { this.contracts, this.partner, this.curator, - this.resources, + this.posts, }); factory Exhibition.fromJson(Map json) => Exhibition( @@ -52,6 +59,7 @@ class Exhibition { title: json['title'] as String, slug: json['slug'] as String, exhibitionStartAt: DateTime.parse(json['exhibitionStartAt'] as String), + previewDuration: json['previewDuration'] as int?, noteTitle: json['noteTitle'] as String, noteBrief: json['noteBrief'] as String, note: json['note'] as String, @@ -66,7 +74,7 @@ class Exhibition { contracts: (json['contracts'] as List?) ?.map((e) => FFContract.fromJson(e as Map)) .toList(), - mintBlockchain: json['mintBlockchain'] as String, + mintBlockchain: (json['mintBlockchain'] ?? '') as String, partner: json['partner'] == null ? null : FFArtist.fromJson(json['partner'] as Map), @@ -74,8 +82,8 @@ class Exhibition { curator: json['curator'] == null ? null : FFCurator.fromJson(json['curator'] as Map), - resources: (json['resources'] as List?) - ?.map((e) => ExhibitionEvent.fromJson(e as Map)) + posts: (json['posts'] as List?) + ?.map((e) => Post.fromJson(e as Map)) .toList(), status: json['status'] as int, ); @@ -85,6 +93,7 @@ class Exhibition { 'title': title, 'slug': slug, 'exhibitionStartAt': exhibitionStartAt.toIso8601String(), + 'previewDuration': previewDuration, 'noteTitle': noteTitle, 'noteBrief': noteBrief, 'note': note, @@ -97,7 +106,7 @@ class Exhibition { 'partner': partner?.toJson(), 'type': type, 'curator': curator?.toJson(), - 'resources': resources?.map((e) => e.toJson()).toList(), + 'posts': posts?.map((e) => e.toJson()).toList(), 'status': status, }; @@ -114,6 +123,7 @@ class Exhibition { String? title, String? slug, DateTime? exhibitionStartAt, + int? previewDuration, String? noteTitle, String? noteBrief, String? note, @@ -126,7 +136,7 @@ class Exhibition { List? contracts, FFArtist? partner, String? type, - List? resources, + List? posts, int? status, }) => Exhibition( @@ -134,6 +144,7 @@ class Exhibition { title: title ?? this.title, slug: slug ?? this.slug, exhibitionStartAt: exhibitionStartAt ?? this.exhibitionStartAt, + previewDuration: previewDuration ?? this.previewDuration, noteTitle: noteTitle ?? this.noteTitle, noteBrief: noteBrief ?? this.noteBrief, note: note ?? this.note, @@ -146,19 +157,21 @@ class Exhibition { contracts: contracts ?? this.contracts, partner: partner ?? this.partner, type: type ?? this.type, - resources: resources ?? this.resources, + posts: posts ?? this.posts, status: status ?? this.status, ); } class ExhibitionResponse { - final Exhibition result; + final Exhibition? result; ExhibitionResponse(this.result); factory ExhibitionResponse.fromJson(Map json) => ExhibitionResponse( - Exhibition.fromJson(json['result'] as Map), + json['result'] == null + ? null + : Exhibition.fromJson(json['result'] as Map), ); Map toJson() => { @@ -194,82 +207,139 @@ class ExhibitionDetail { ); } -class ExhibitionEvent { +class Post { final String id; - final String exhibitionID; final String type; + final String slug; final String title; - final DateTime? dateTime; - final String? description; - final Map? links; + final String content; + final int? displayIndex; + final String? coverURI; final DateTime createdAt; final DateTime updatedAt; - final MediaUri? mediaUri; + final DateTime? dateTime; + final String? description; + final String? author; + final String? exhibitionID; + final Exhibition? exhibition; - ExhibitionEvent({ + Post({ required this.id, - required this.exhibitionID, required this.type, + required this.slug, required this.title, + required this.content, + required this.coverURI, required this.createdAt, required this.updatedAt, this.dateTime, this.description, - this.links, - this.mediaUri, + this.author, + this.displayIndex, + this.exhibitionID, + this.exhibition, }); - factory ExhibitionEvent.fromJson(Map json) => - ExhibitionEvent( + factory Post.fromJson(Map json) => Post( id: json['id'], - exhibitionID: json['exhibitionID'], type: json['type'], + slug: json['slug'], title: json['title'], - dateTime: DateTime.tryParse(json['dateTime'] ?? ''), - description: json['description'], - links: Map.from(json['links'] ?? {}), + content: json['content'], + coverURI: json['coverURI'], createdAt: DateTime.parse(json['createdAt']), updatedAt: DateTime.parse(json['updatedAt']), - mediaUri: json['mediaUri'] == null + dateTime: + json['dateTime'] == null ? null : DateTime.parse(json['dateTime']), + description: json['description'], + author: json['author'], + displayIndex: json['displayIndex'], + exhibitionID: json['exhibitionID'], + exhibition: json['exhibition'] == null ? null - : MediaUri.fromJson(json['mediaUri'] as Map), + : Exhibition.fromJson(json['exhibition']), ); - // toJson Map toJson() => { 'id': id, - 'exhibitionID': exhibitionID, 'type': type, + 'slug': slug, 'title': title, - 'dateTime': dateTime?.toIso8601String(), - 'description': description, - 'links': links, + 'content': content, + 'coverURI': coverURI, 'createdAt': createdAt.toIso8601String(), 'updatedAt': updatedAt.toIso8601String(), - 'mediaUri': mediaUri?.toJson(), + 'dateTime': dateTime?.toIso8601String(), + 'description': description, + 'author': author, + 'displayIndex': displayIndex, + 'exhibitionID': exhibitionID, + 'exhibition': exhibition?.toJson(), }; } -class MediaUri { - final String url; - final String type; - final String? title; +class CustomExhibitionNote { + final String title; + final String content; + final bool? canReadMore; - MediaUri({ - required this.url, - required this.type, - this.title, - }); + CustomExhibitionNote( + {required this.title, required this.content, this.canReadMore}); - factory MediaUri.fromJson(Map json) => MediaUri( - url: json['url'], - type: json['type'], + factory CustomExhibitionNote.fromJson(Map json) => + CustomExhibitionNote( title: json['title'], + content: json['content'], + canReadMore: json['canReadMore'] ?? false, ); +} - Map toJson() => { - 'url': url, - 'type': type, - 'title': title, - }; +enum MediaType { + image, + video, +} + +extension PostExt on Post { + MediaType? get mediaType { + if (coverURI == null) { + return null; + } + final url = Uri.parse(coverURI!); + if (YOUTUBE_DOMAINS.any((domain) => url.host.contains(domain))) { + return MediaType.video; + } + return MediaType.image; + } + + String get displayType => + type == 'close-up' ? 'close_up'.tr() : type.capitalize(); + + List get thumbnailUrls { + if (coverURI == null) { + return []; + } + if (mediaType == MediaType.image) { + return [getFFUrl(coverURI!)]; + } else { + final List thumbUrls = []; + final videoId = Uri.parse(coverURI!).queryParameters['v']; + for (var variant in YOUTUBE_VARIANTS) { + final url = 'https://img.youtube.com/vi/$videoId/$variant.jpg'; + thumbUrls.add(url); + } + return thumbUrls; + } + } + + String get previewUrl { + if (coverURI == null) { + return ''; + } + if (mediaType == MediaType.image) { + return getFFUrl(coverURI!); + } else { + final videoId = Uri.parse(coverURI!).queryParameters['v']; + return 'https://www.youtube.com/embed/$videoId?autoplay=1&loop=1&controls=0'; + } + } } diff --git a/lib/model/ff_exhibition_artworks_response.dart b/lib/model/ff_exhibition_artworks_response.dart deleted file mode 100644 index f2941209c7..0000000000 --- a/lib/model/ff_exhibition_artworks_response.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:autonomy_flutter/model/ff_account.dart'; - -class ArtworksResponse { - final List result; - - ArtworksResponse({ - required this.result, - }); - - factory ArtworksResponse.fromJson(Map json) => - ArtworksResponse( - result: (json['result'] as List) - .map((e) => Artwork.fromJson(e as Map)) - .toList(), - ); - - Map toJson() => { - 'result': result.map((e) => e.toJson()).toList(), - }; -} diff --git a/lib/model/ff_list_response.dart b/lib/model/ff_list_response.dart new file mode 100644 index 0000000000..115fc273fd --- /dev/null +++ b/lib/model/ff_list_response.dart @@ -0,0 +1,59 @@ +class FeralFileListResponse { + final List result; + final Paging paging; + + FeralFileListResponse({required this.result, required this.paging}); + + factory FeralFileListResponse.fromJson( + Map json, + T Function(Map) fromJson, + ) => + FeralFileListResponse( + result: (json['result'] as List) + .map((e) => fromJson(e as Map)) + .toList(), + paging: Paging.fromJson(json['paging']), + ); + + Map toJson(Map Function(T) toJson) => { + 'result': result.map((e) => toJson(e)).toList(), + 'paging': paging.toJson(), + }; + + FeralFileListResponse copyWith({ + List? result, + Paging? paging, + }) => + FeralFileListResponse( + result: result ?? this.result, + paging: paging ?? this.paging, + ); +} + +class Paging { + final int offset; + final int limit; + final int total; + + Paging({ + required this.offset, + required this.limit, + required this.total, + }); + + factory Paging.fromJson(Map json) => Paging( + offset: json['offset'], + limit: json['limit'], + total: json['total'], + ); + + Map toJson() => { + 'offset': offset, + 'limit': limit, + 'total': total, + }; +} + +extension PagingExtension on Paging { + bool get shouldLoadMore => offset + limit < total; +} diff --git a/lib/model/ff_series.dart b/lib/model/ff_series.dart index 839483fdb0..710454c669 100644 --- a/lib/model/ff_series.dart +++ b/lib/model/ff_series.dart @@ -1,16 +1,15 @@ -import 'package:autonomy_flutter/common/environment.dart'; import 'package:autonomy_flutter/model/ff_account.dart'; +import 'package:autonomy_flutter/model/ff_artwork.dart'; import 'package:autonomy_flutter/model/ff_exhibition.dart'; import 'package:autonomy_flutter/model/ff_user.dart'; import 'package:autonomy_flutter/service/feralfile_service.dart'; -import 'package:collection/collection.dart'; class FFSeries { final String id; final String artistID; final String? assetID; final String title; - final String slug; + final String? slug; final String medium; final String? description; final String? thumbnailURI; @@ -21,9 +20,9 @@ class FFSeries { final FFSeriesSettings? settings; final FFArtist? artist; final Exhibition? exhibition; - final AirdropInfo? airdropInfo; final DateTime? createdAt; final DateTime? updatedAt; + final DateTime? mintedAt; final FileInfo? originalFile; final FileInfo? previewFile; final Artwork? artwork; @@ -31,6 +30,7 @@ class FFSeries { final String? uniqueThumbnailPath; final String? uniquePreviewPath; final String? onchainID; + final List? artworks; FFSeries( this.id, @@ -46,8 +46,8 @@ class FFSeries { this.settings, this.artist, this.exhibition, - this.airdropInfo, this.createdAt, + this.mintedAt, this.displayIndex, this.featuringIndex, this.updatedAt, @@ -58,15 +58,11 @@ class FFSeries { this.uniqueThumbnailPath, this.uniquePreviewPath, this.onchainID, + this.artworks, ); int get maxEdition => settings?.maxArtwork ?? -1; - FFContract? get contract => exhibition?.contracts - ?.firstWhereOrNull((e) => e.address == airdropInfo?.contractAddress); - - String getThumbnailURL() => '${Environment.feralFileAssetURL}/$thumbnailURI'; - bool get isAirdropSeries => settings?.isAirdrop == true; factory FFSeries.fromJson(Map json) => FFSeries( @@ -74,7 +70,7 @@ class FFSeries { json['artistID'] as String, json['assetID'] as String?, json['title'] as String, - json['slug'] as String, + json['slug'] as String?, json['medium'] as String, json['description'] as String?, json['thumbnailURI'] as String?, @@ -90,12 +86,12 @@ class FFSeries { json['exhibition'] == null ? null : Exhibition.fromJson(json['exhibition'] as Map), - json['airdropInfo'] == null - ? null - : AirdropInfo.fromJson(json['airdropInfo'] as Map), json['createdAt'] == null ? null : DateTime.parse(json['createdAt'] as String), + json['mintedAt'] == null + ? null + : DateTime.parse(json['mintedAt'] as String), json['displayIndex'] as int?, json['featuringIndex'] as int?, json['updatedAt'] == null @@ -114,6 +110,11 @@ class FFSeries { json['uniqueThumbnailPath'] as String?, json['uniquePreviewPath'] as String?, json['onchainID'] as String?, + json['artworks'] == null + ? null + : (json['artworks'] as List) + .map((e) => Artwork.fromJson(e as Map)) + .toList(), ); Map toJson() => { @@ -132,9 +133,9 @@ class FFSeries { 'settings': settings, 'artist': artist, 'exhibition': exhibition, - 'airdropInfo': airdropInfo, 'createdAt': createdAt?.toIso8601String(), 'updatedAt': updatedAt?.toIso8601String(), + 'mintedAt': mintedAt?.toIso8601String(), 'originalFile': originalFile?.toJson(), 'previewFile': previewFile?.toJson(), 'artwork': artwork?.toJson(), @@ -143,6 +144,63 @@ class FFSeries { 'uniquePreviewPath': uniquePreviewPath, 'onchainID': onchainID, }; + + FFSeries copyWith({ + String? id, + String? artistID, + String? assetID, + String? title, + String? slug, + String? medium, + String? description, + String? thumbnailURI, + String? exhibitionID, + Map? metadata, + int? displayIndex, + int? featuringIndex, + FFSeriesSettings? settings, + FFArtist? artist, + Exhibition? exhibition, + DateTime? createdAt, + DateTime? mintedAt, + DateTime? updatedAt, + FileInfo? originalFile, + FileInfo? previewFile, + Artwork? artwork, + String? externalSource, + String? uniqueThumbnailPath, + String? uniquePreviewPath, + String? onchainID, + List? artworks, + }) => + FFSeries( + id ?? this.id, + artistID ?? this.artistID, + assetID ?? this.assetID, + title ?? this.title, + slug ?? this.slug, + medium ?? this.medium, + description ?? this.description, + thumbnailURI ?? this.thumbnailURI, + exhibitionID ?? this.exhibitionID, + metadata ?? this.metadata, + settings ?? this.settings, + artist ?? this.artist, + exhibition ?? this.exhibition, + createdAt ?? this.createdAt, + mintedAt ?? this.mintedAt, + displayIndex ?? this.displayIndex, + featuringIndex ?? this.featuringIndex, + updatedAt ?? this.updatedAt, + originalFile ?? this.originalFile, + previewFile ?? this.previewFile, + artwork ?? this.artwork, + externalSource ?? this.externalSource, + uniqueThumbnailPath ?? this.uniqueThumbnailPath, + uniquePreviewPath ?? this.uniquePreviewPath, + onchainID ?? this.onchainID, + artworks ?? this.artworks, + ); } class FFSeriesResponse { @@ -166,8 +224,19 @@ class FFSeriesSettings { final int maxArtwork; final String? saleModel; ArtworkModel? artworkModel; + int artistReservation; + int publisherProof; + int promotionalReservation; + bool? tradeSeries; + bool? transferToCurator; - FFSeriesSettings(this.saleModel, this.maxArtwork, {this.artworkModel}); + FFSeriesSettings(this.saleModel, this.maxArtwork, + {this.artworkModel, + this.artistReservation = 0, + this.publisherProof = 0, + this.promotionalReservation = 0, + this.tradeSeries = false, + this.transferToCurator = false}); factory FFSeriesSettings.fromJson(Map json) => FFSeriesSettings( @@ -176,6 +245,11 @@ class FFSeriesSettings { artworkModel: json['artworkModel'] == null ? null : ArtworkModel.fromString(json['artworkModel'] as String), + artistReservation: json['artistReservation'] as int? ?? 0, + publisherProof: json['publisherProof'] as int? ?? 0, + promotionalReservation: json['promotionalReservation'] as int? ?? 0, + tradeSeries: json['tradeSeries'] as bool?, + transferToCurator: json['transferToCurator'] as bool?, ); Map toJson() => { diff --git a/lib/model/ff_user.dart b/lib/model/ff_user.dart index 7d378c8cd4..920ec8477d 100644 --- a/lib/model/ff_user.dart +++ b/lib/model/ff_user.dart @@ -25,7 +25,7 @@ class FFUser { class FFArtist { final String id; final String alias; - final String slug; + final String? slug; final bool? verified; final bool? isArtist; final String? fullName; @@ -48,7 +48,7 @@ class FFArtist { factory FFArtist.fromJson(Map json) => FFArtist( json['ID'] as String, json['alias'] as String, - json['slug'] as String, + json['slug'] as String?, json['verified'] as bool?, json['isArtist'] as bool?, json['fullName'] as String?, @@ -72,7 +72,7 @@ class FFArtist { class FFCurator extends FFUser { final String? email; - final String avatarUri; + final String? avatarUri; FFCurator({ required super.id, @@ -91,13 +91,17 @@ class FFCurator extends FFUser { id: json['ID'], alias: json['alias'], slug: json['slug'], - email: json['email'], - avatarUri: json['avatarURI'], - fullName: json['fullName'], - type: json['type'], - metadata: json['metadata'], - createdAt: DateTime.tryParse(json['createdAt']), - updatedAt: DateTime.tryParse(json['updatedAt']), + email: json['email'] as String?, + avatarUri: json['avatarURI'] as String?, + fullName: json['fullName'] as String?, + type: json['type'] as String?, + metadata: json['metadata'] as Map?, + createdAt: json['createdAt'] != null + ? DateTime.tryParse(json['createdAt']) + : null, + updatedAt: json['updatedAt'] != null + ? DateTime.tryParse(json['updatedAt']) + : null, ); Map toJson() => { diff --git a/lib/model/project.dart b/lib/model/project.dart new file mode 100644 index 0000000000..6da34e2550 --- /dev/null +++ b/lib/model/project.dart @@ -0,0 +1,13 @@ +class ProjectInfo { + final String title; + final String route; + final dynamic delegate; + final dynamic arguments; + + ProjectInfo({ + required this.title, + required this.route, + required this.delegate, + this.arguments, + }); +} diff --git a/lib/model/tap_navigate.dart b/lib/model/tap_navigate.dart deleted file mode 100644 index ef07fda6c4..0000000000 --- a/lib/model/tap_navigate.dart +++ /dev/null @@ -1,7 +0,0 @@ -class TapNavigate { - final String title; - final String route; - final dynamic arguments; - - TapNavigate({required this.title, required this.route, this.arguments}); -} diff --git a/lib/screen/app_router.dart b/lib/screen/app_router.dart index a67b057bdf..702dc4d802 100644 --- a/lib/screen/app_router.dart +++ b/lib/screen/app_router.dart @@ -11,7 +11,6 @@ import 'package:autonomy_flutter/database/cloud_database.dart'; import 'package:autonomy_flutter/database/entity/connection.dart'; import 'package:autonomy_flutter/model/connection_request_args.dart'; import 'package:autonomy_flutter/model/ff_exhibition.dart'; -import 'package:autonomy_flutter/model/ff_series.dart'; import 'package:autonomy_flutter/model/play_list_model.dart'; import 'package:autonomy_flutter/model/postcard_claim.dart'; import 'package:autonomy_flutter/screen/account/access_method_page.dart'; @@ -30,16 +29,9 @@ import 'package:autonomy_flutter/screen/bloc/tezos/tezos_bloc.dart'; import 'package:autonomy_flutter/screen/bloc/usdc/usdc_bloc.dart'; import 'package:autonomy_flutter/screen/bug_bounty_page.dart'; import 'package:autonomy_flutter/screen/chat/chat_thread_page.dart'; -import 'package:autonomy_flutter/screen/claim/activation/activation_token_detail_page.dart'; -import 'package:autonomy_flutter/screen/claim/activation/claim_activation_page.dart'; -import 'package:autonomy_flutter/screen/claim/activation/preview_activation_claim.dart'; -import 'package:autonomy_flutter/screen/claim/airdrop/claim_airdrop_page.dart'; -import 'package:autonomy_flutter/screen/claim/claim_token_page.dart'; -import 'package:autonomy_flutter/screen/claim/preview_token_claim.dart'; -import 'package:autonomy_flutter/screen/claim/select_account_page.dart'; -import 'package:autonomy_flutter/screen/claim/token_detail_page.dart'; import 'package:autonomy_flutter/screen/cloud/cloud_android_page.dart'; import 'package:autonomy_flutter/screen/cloud/cloud_page.dart'; +import 'package:autonomy_flutter/screen/collection_pro/artists_list_page/artists_list_page.dart'; import 'package:autonomy_flutter/screen/connection/connection_details_page.dart'; import 'package:autonomy_flutter/screen/connection/persona_connections_page.dart'; import 'package:autonomy_flutter/screen/customer_support/merchandise_order/merchandise_orders_page.dart'; @@ -56,9 +48,10 @@ import 'package:autonomy_flutter/screen/detail/preview/keyboard_control_page.dar import 'package:autonomy_flutter/screen/detail/preview/touchpad_page.dart'; import 'package:autonomy_flutter/screen/detail/preview_primer.dart'; import 'package:autonomy_flutter/screen/detail/royalty/royalty_bloc.dart'; +import 'package:autonomy_flutter/screen/exhibition_custom_note/exhibition_custom_note_page.dart'; import 'package:autonomy_flutter/screen/exhibition_details/exhibition_detail_bloc.dart'; import 'package:autonomy_flutter/screen/exhibition_details/exhibition_detail_page.dart'; -import 'package:autonomy_flutter/screen/exhibition_note/exhibition_note_page.dart'; +import 'package:autonomy_flutter/screen/featured_works_page/featured_works_page.dart'; import 'package:autonomy_flutter/screen/feralfile_artwork_preview/feralfile_artwork_preview_page.dart'; import 'package:autonomy_flutter/screen/feralfile_series/feralfile_series_bloc.dart'; import 'package:autonomy_flutter/screen/feralfile_series/feralfile_series_page.dart'; @@ -130,7 +123,6 @@ import 'package:autonomy_flutter/screen/tezos_beacon/tb_sign_message_page.dart'; import 'package:autonomy_flutter/screen/wallet/wallet_page.dart'; import 'package:autonomy_flutter/screen/wallet_connect/send/wc_send_transaction_bloc.dart'; import 'package:autonomy_flutter/screen/wallet_connect/send/wc_send_transaction_page.dart'; -import 'package:autonomy_flutter/screen/wallet_connect/v2/add_ethereum_chain_page.dart'; import 'package:autonomy_flutter/screen/wallet_connect/v2/wc2_permission_page.dart'; import 'package:autonomy_flutter/screen/wallet_connect/wc_connect_page.dart'; import 'package:autonomy_flutter/screen/wallet_connect/wc_sign_message_page.dart'; @@ -186,9 +178,6 @@ class AppRouter { static const githubDocPage = 'github_doc_page'; static const sendArtworkPage = 'send_artwork_page'; static const sendArtworkReviewPage = 'send_artwork_review_page'; - static const claimFeralfileTokenPage = 'claim_feralfile_token_page'; - static const claimSelectAccountPage = 'claim_select_account_page'; - static const airdropTokenDetailPage = 'airdrop_token_detail_page'; static const wc2ConnectPage = 'wc2_connect_page'; static const wc2PermissionPage = 'wc2_permission_page'; static const preferencesPage = 'preferences_page'; @@ -210,9 +199,6 @@ class AppRouter { static const canvasHelpPage = 'canvas_help_page'; static const keyboardControlPage = 'keyboard_control_page'; static const touchPadPage = 'touch_pad_page'; - static const claimAirdropPage = 'claim_airdrop_page'; - static const activationTokenDetailPage = 'activation_token_detail_page'; - static const claimActivationPage = 'claim_activation_page'; static const postcardLeaderboardPage = 'postcard_leaderboard_page'; static const postcardLocationExplain = 'postcard_location_explain'; static const predefinedCollectionPage = 'predefined_collection_page'; @@ -239,14 +225,11 @@ class AppRouter { static const collectionPage = 'collection_page'; static const organizePage = 'organize_page'; static const exhibitionsPage = 'exhibitions_page'; - static const autonomyAirdropTokenPreviewPage = - 'autonomy_airdrop_token_detail_page'; - static const exhibitionNotePage = 'exhibition_note_page'; - static const activationTokenPreviewPage = 'activation_token_preview_page'; - static const feralfileAirdropTokenPreviewPage = - 'feralfile_airdrop_token_preview_page'; static const projectsList = 'projects_list'; static const addEthereumChainPage = 'add_ethereum_chain_page'; + static const artistsListPage = 'artists_list_page'; + static const exhibitionCustomNote = 'exhibition_custom_note'; + static const featuredWorksPage = 'featured_works_page'; static Route onGenerateRoute(RouteSettings settings) { final ethereumBloc = EthereumBloc(injector(), injector()); @@ -260,6 +243,8 @@ class AppRouter { ); final connectionsBloc = injector(); final identityBloc = IdentityBloc(injector(), injector()); + final canvasDeviceBloc = injector(); + final postcardDetailBloc = PostcardDetailBloc( injector(), injector(), @@ -275,15 +260,18 @@ class AppRouter { injector(), injector(), injector(), + injector(), ); switch (settings.name) { - case addEthereumChainPage: - return CupertinoPageRoute( + case artistsListPage: + return PageTransition( + type: PageTransitionType.fade, + curve: Curves.easeIn, + duration: const Duration(milliseconds: 250), settings: settings, - builder: (context) => AddEthereumChainPage( - payload: settings.arguments! as AddEthereumChainPagePayload, - ), + child: ArtistsListPage( + payload: settings.arguments! as ArtistsListPagePayload), ); case projectsList: return PageTransition( @@ -300,9 +288,7 @@ class AppRouter { return CupertinoPageRoute( settings: settings, builder: (context) => BlocProvider( - create: (_) => CanvasDeviceBloc( - injector(), - ), + create: (_) => canvasDeviceBloc, child: ViewPlaylistScreen( payload: settings.arguments! as ViewPlaylistScreenPayload, ), @@ -369,6 +355,7 @@ class AppRouter { BlocProvider( create: (_) => personaBloc, ), + BlocProvider(create: (_) => canvasDeviceBloc), BlocProvider(lazy: false, create: (_) => connectionsBloc), ], child: HomeNavigationPage( @@ -394,6 +381,7 @@ class AppRouter { create: (_) => personaBloc, ), BlocProvider(lazy: false, create: (_) => connectionsBloc), + BlocProvider(create: (_) => canvasDeviceBloc), ], child: HomeNavigationPage( key: homePageKey, @@ -645,9 +633,7 @@ class AppRouter { create: (_) => identityBloc, ), BlocProvider( - create: (_) => CanvasDeviceBloc( - injector(), - ), + create: (_) => canvasDeviceBloc, ), BlocProvider(create: (_) => postcardDetailBloc), ], @@ -727,8 +713,10 @@ class AppRouter { injector(), injector(), injector(), - injector(), )), + BlocProvider( + create: (_) => canvasDeviceBloc, + ), ], child: ArtworkDetailPage( payload: settings.arguments! as ArtworkDetailPayload))); @@ -860,10 +848,16 @@ class AppRouter { ], child: const HiddenArtworksPage(), )); + case momaPostcardPage: return CupertinoPageRoute( settings: settings, builder: (context) => const MoMAPostcardPage()); + case featuredWorksPage: + return CupertinoPageRoute( + settings: settings, + builder: (context) => const FeaturedWorksPage()); + case exhibitionDetailPage: return CupertinoPageRoute( settings: settings, @@ -873,9 +867,7 @@ class AppRouter { create: (_) => ExhibitionDetailBloc(injector()), ), BlocProvider( - create: (_) => CanvasDeviceBloc( - injector(), - ), + create: (_) => canvasDeviceBloc, ), ], child: ExhibitionDetailPage( @@ -945,35 +937,6 @@ class AppRouter { payload: settings.arguments! as SendArtworkReviewPayload), )); - case claimFeralfileTokenPage: - final payload = settings.arguments! as ClaimTokenPagePayload; - return CupertinoPageRoute( - settings: settings, - builder: (context) => ClaimTokenPage( - payload: payload, - )); - - case airdropTokenDetailPage: - return CupertinoPageRoute( - settings: settings, - builder: (context) => BlocProvider( - create: (_) => RoyaltyBloc(injector()), - child: TokenDetailPage( - series: settings.arguments! as FFSeries, - ), - )); - - case claimSelectAccountPage: - final payload = settings.arguments! as SelectAddressPagePayload; - return CupertinoPageRoute( - settings: settings, - builder: (context) => BlocProvider.value( - value: accountsBloc, - child: SelectAccountPage( - payload: payload, - ), - )); - case wc2ConnectPage: return CupertinoPageRoute( settings: settings, @@ -1019,7 +982,9 @@ class AppRouter { create: (_) => personaBloc, ), ], - child: const WalletPage(), + child: WalletPage( + payload: settings.arguments as WalletPagePayload?, + ), )); case preferencesPage: return CupertinoPageRoute( @@ -1137,38 +1102,6 @@ class AppRouter { payload: payload, ); }); - case claimAirdropPage: - return CupertinoPageRoute( - settings: settings, - builder: (context) => BlocProvider.value( - value: accountsBloc, - child: ClaimAirdropPage( - payload: settings.arguments! as ClaimAirdropPagePayload, - ), - ), - ); - - case activationTokenDetailPage: - return CupertinoPageRoute( - settings: settings, - builder: (context) => BlocProvider.value( - value: accountsBloc, - child: ActivationTokenDetailPage( - assetToken: settings.arguments! as AssetToken, - ), - ), - ); - - case claimActivationPage: - return CupertinoPageRoute( - settings: settings, - builder: (context) => BlocProvider.value( - value: accountsBloc, - child: ClaimActivationPage( - payload: settings.arguments! as ClaimActivationPagePayload, - ), - ), - ); case postcardLeaderboardPage: return PageTransition( @@ -1216,31 +1149,14 @@ class AppRouter { playList: settings.arguments! as PlayListModel, ), ); - case autonomyAirdropTokenPreviewPage: - return MaterialPageRoute( - settings: settings, - builder: (context) => PreviewTokenClaim( - series: settings.arguments! as FFSeries, - ), - ); - case exhibitionNotePage: - return MaterialPageRoute( - builder: (context) => ExhibitionNotePage( - exhibition: settings.arguments! as Exhibition, - ), - ); - case activationTokenPreviewPage: - return MaterialPageRoute( - builder: (context) => PreviewActivationTokenPage( - assetToken: settings.arguments! as AssetToken, - ), - ); - case feralfileAirdropTokenPreviewPage: + + case exhibitionCustomNote: return MaterialPageRoute( - builder: (context) => PreviewTokenClaim( - series: settings.arguments! as FFSeries, + builder: (context) => ExhibitionCustomNotePage( + info: settings.arguments! as CustomExhibitionNote, ), ); + default: throw Exception('Invalid route: ${settings.name}'); } diff --git a/lib/screen/bloc/router/router_bloc.dart b/lib/screen/bloc/router/router_bloc.dart index 09312d4865..02a0147959 100644 --- a/lib/screen/bloc/router/router_bloc.dart +++ b/lib/screen/bloc/router/router_bloc.dart @@ -32,7 +32,7 @@ class RouterBloc extends AuBloc { final AuditService _auditService; final SettingsDataService _settingsDataService; - Future hasAccounts() async { + Future _hasAccounts() async { final personas = await _cloudDB.personaDao.getPersonas(); final connections = await _cloudDB.connectionDao.getUpdatedLinkedAccounts(); return personas.isNotEmpty || connections.isNotEmpty; @@ -60,6 +60,10 @@ class RouterBloc extends AuBloc { // Check and restore full accounts from cloud if existing await migrationUtil.migrationFromKeychain(); await _accountService.androidRestoreKeys(); + final hasAccount = await _hasAccounts(); + if (!hasAccount) { + await _configurationService.setDoneOnboarding(hasAccount); + } if (_configurationService.isDoneOnboarding()) { emit(RouterState(onboardingStep: OnboardingStep.dashboard)); @@ -69,7 +73,7 @@ class RouterBloc extends AuBloc { //Soft delay 1s waiting for database synchronizing await Future.delayed(const Duration(seconds: 1)); - if (await hasAccounts()) { + if (hasAccount) { unawaited(_configurationService.setOldUser()); final backupVersion = await _backupService .fetchBackupVersion(await _accountService.getDefaultAccount()); diff --git a/lib/screen/bloc/subscription/subscription_bloc.dart b/lib/screen/bloc/subscription/subscription_bloc.dart new file mode 100644 index 0000000000..1d2c627c82 --- /dev/null +++ b/lib/screen/bloc/subscription/subscription_bloc.dart @@ -0,0 +1,15 @@ +import 'package:autonomy_flutter/au_bloc.dart'; +import 'package:autonomy_flutter/screen/bloc/subscription/subscription_state.dart'; +import 'package:autonomy_flutter/service/iap_service.dart'; + +class SubscriptionBloc extends AuBloc { + final IAPService _iapService; + SubscriptionBloc(this._iapService) : super(SubscriptionState()) { + on((event, emit) async { + final isSubscribed = await _iapService.isSubscribed(); + emit(state.copyWith( + isSubscribed: isSubscribed, + )); + }); + } +} diff --git a/lib/screen/bloc/subscription/subscription_state.dart b/lib/screen/bloc/subscription/subscription_state.dart new file mode 100644 index 0000000000..d588d97457 --- /dev/null +++ b/lib/screen/bloc/subscription/subscription_state.dart @@ -0,0 +1,18 @@ +class SubscriptionEvent {} + +class GetSubscriptionEvent extends SubscriptionEvent {} + +class SubscriptionState { + SubscriptionState({ + this.isSubscribed = false, + }); + + final bool isSubscribed; + + SubscriptionState copyWith({ + bool? isSubscribed, + }) => + SubscriptionState( + isSubscribed: isSubscribed ?? this.isSubscribed, + ); +} diff --git a/lib/screen/claim/activation/activation_token_detail_page.dart b/lib/screen/claim/activation/activation_token_detail_page.dart deleted file mode 100644 index e461d61bd0..0000000000 --- a/lib/screen/claim/activation/activation_token_detail_page.dart +++ /dev/null @@ -1,156 +0,0 @@ -// -// SPDX-License-Identifier: BSD-2-Clause-Patent -// Copyright © 2022 Bitmark. All rights reserved. -// Use of this source code is governed by the BSD-2-Clause Plus Patent License -// that can be found in the LICENSE file. -// - -import 'package:autonomy_flutter/util/asset_token_ext.dart'; -import 'package:autonomy_flutter/util/style.dart'; -import 'package:autonomy_flutter/view/artwork_common_widget.dart'; -import 'package:autonomy_flutter/view/responsive.dart'; -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; -import 'package:nft_collection/models/asset_token.dart'; - -class ActivationTokenDetailPage extends StatefulWidget { - final AssetToken assetToken; - - const ActivationTokenDetailPage({ - Key? key, - required this.assetToken, - }) : super(key: key); - - @override - State createState() => - _ActivationTokenDetailPageState(); -} - -class _ActivationTokenDetailPageState extends State { - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final assetToken = widget.assetToken; - final artistName = assetToken.artistName ?? ""; - return Scaffold( - appBar: _appBar( - context, - onBack: () => Navigator.of(context).pop(), - ), - backgroundColor: theme.colorScheme.primary, - body: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 16.0), - Padding( - padding: ResponsiveLayout.getPadding, - child: Text( - assetToken.title ?? "", - style: theme.primaryTextTheme.displayLarge, - ), - ), - const SizedBox(height: 8.0), - Padding( - padding: ResponsiveLayout.getPadding, - child: Text( - "by".tr(args: [artistName]).trim(), - style: theme.primaryTextTheme.headlineMedium - ?.copyWith(fontSize: 18), - ), - ), - const SizedBox(height: 15.0), - // Show artwork here. - CachedNetworkImage( - imageUrl: assetToken.getPreviewUrl() ?? "", - fit: BoxFit.fitWidth, - ), - const SizedBox(height: 24.0), - Padding( - padding: ResponsiveLayout.getPadding, - child: HtmlWidget( - customStylesBuilder: auHtmlStyle, - assetToken.description ?? '', - textStyle: theme.primaryTextTheme.bodyLarge, - ), - ), - const SizedBox(height: 40.0), - Padding( - padding: ResponsiveLayout.getPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Theme( - data: theme.copyWith(textTheme: theme.primaryTextTheme), - child: artworkDetailsMetadataSection( - context, - widget.assetToken, - artistName, - ), - ), - const SizedBox(height: 40.0), - Theme( - data: theme.copyWith(textTheme: theme.primaryTextTheme), - child: artworkDetailsRightSection( - context, - widget.assetToken, - ), - ), - const SizedBox(height: 40.0), - ], - ), - ) - ], - ), - )); - } - - AppBar _appBar( - BuildContext context, { - required void Function() onBack, - }) { - final theme = Theme.of(context); - return AppBar( - systemOverlayStyle: SystemUiOverlayStyle( - statusBarColor: theme.colorScheme.secondary, - statusBarIconBrightness: Brightness.dark, - statusBarBrightness: Brightness.light, - ), - leading: const SizedBox(), - leadingWidth: 0.0, - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: onBack, - child: Padding( - padding: const EdgeInsets.fromLTRB(0, 7, 18, 8), - child: Row( - children: [ - SvgPicture.asset( - 'assets/images/nav-arrow-left.svg', - colorFilter: - const ColorFilter.mode(Colors.white, BlendMode.srcIn), - ), - const SizedBox(width: 7), - Text( - "BACK", - style: theme.primaryTextTheme.labelLarge, - ), - ], - ), - ), - ), - ], - ), - backgroundColor: Colors.transparent, - shadowColor: Colors.transparent, - elevation: 0, - ); - } -} diff --git a/lib/screen/claim/activation/claim_activation_page.dart b/lib/screen/claim/activation/claim_activation_page.dart deleted file mode 100644 index 6675f2076c..0000000000 --- a/lib/screen/claim/activation/claim_activation_page.dart +++ /dev/null @@ -1,342 +0,0 @@ -// ignore_for_file: discarded_futures, unawaited_futures - -import 'package:auto_size_text/auto_size_text.dart'; -import 'package:autonomy_flutter/common/injector.dart'; -import 'package:autonomy_flutter/gateway/activation_api.dart'; -import 'package:autonomy_flutter/main.dart'; -import 'package:autonomy_flutter/model/otp.dart'; -import 'package:autonomy_flutter/screen/app_router.dart'; -import 'package:autonomy_flutter/screen/claim/select_account_page.dart'; -import 'package:autonomy_flutter/screen/detail/artwork_detail_page.dart'; -import 'package:autonomy_flutter/service/account_service.dart'; -import 'package:autonomy_flutter/service/activation_service.dart'; -import 'package:autonomy_flutter/service/configuration_service.dart'; -import 'package:autonomy_flutter/service/metric_client_service.dart'; -import 'package:autonomy_flutter/util/wallet_utils.dart'; -import 'package:autonomy_flutter/view/primary_button.dart'; -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:feralfile_app_theme/feral_file_app_theme.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.dart'; -import 'package:marqueer/marqueer.dart'; -import 'package:nft_collection/models/asset_token.dart'; -import 'package:nft_collection/nft_collection.dart'; - -class ClaimActivationPagePayload { - final AssetToken assetToken; - final String activationID; - final Otp otp; - - ClaimActivationPagePayload({ - required this.assetToken, - required this.activationID, - required this.otp, - }); -} - -class ClaimActivationPage extends StatefulWidget { - final ClaimActivationPagePayload payload; - - const ClaimActivationPage({ - required this.payload, - super.key, - }); - - @override - State createState() => _ClaimActivationPageState(); -} - -class _ClaimActivationPageState extends State { - bool _processing = false; - - final _metricClient = injector.get(); - final _accountService = injector(); - final _activationService = injector(); - final _configService = injector(); - - @override - Widget build(BuildContext context) { - final assetToken = widget.payload.assetToken; - final artistName = widget.payload.assetToken.artistName!; - String gifter = 'Gitfer'; - String giftIntro = 'you_can_receive_free_gift'.tr(); - if (gifter.trim().isNotEmpty) { - giftIntro += " ${'from'.tr().toLowerCase()} "; - } - final theme = Theme.of(context); - return Scaffold( - backgroundColor: theme.colorScheme.primary, - body: Container( - padding: const EdgeInsets.fromLTRB(14, 28, 14, 40), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: NotificationListener( - onNotification: (overScroll) { - overScroll.disallowIndicator(); - return false; - }, - child: ListView( - padding: const EdgeInsets.all(0), - shrinkWrap: true, - children: [ - const SizedBox( - height: 24, - ), - FittedBox( - child: GestureDetector( - behavior: HitTestBehavior.opaque, - child: Transform.translate( - offset: const Offset(1, 0), - child: Container( - color: Colors.white, - child: Column( - children: [ - const SizedBox( - height: 10, - ), - SizedBox( - width: MediaQuery.of(context).size.width, - height: 20, - child: Marqueer( - direction: MarqueerDirection.ltr, - pps: 30, - child: Text( - 'gift_edition'.tr(), - style: theme.textTheme.ppMori400Black14, - ), - ), - ), - Padding( - padding: const EdgeInsets.symmetric( - vertical: 45, - horizontal: 75, - ), - child: Container( - color: Colors.black, - child: CachedNetworkImage( - fit: BoxFit.cover, - imageUrl: assetToken.previewURL!, - width: 225, - height: 225, - ), - ), - ), - SizedBox( - width: MediaQuery.of(context).size.width, - height: 30, - child: Marqueer( - pps: 30, - child: Text( - 'gift_edition'.tr().toUpperCase(), - style: theme.textTheme.ppMori400Black14, - ), - ), - ), - ], - ), - ), - ), - onTap: () {}, - ), - ), - Padding( - padding: const EdgeInsets.all(15), - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - behavior: HitTestBehavior.opaque, - child: AutoSizeText( - assetToken.title!, - style: theme.textTheme.ppMori400White14, - maxFontSize: 14, - minFontSize: 14, - maxLines: 2, - ), - onTap: () { - Navigator.pushNamed( - context, - AppRouter.activationTokenPreviewPage, - arguments: widget.payload.assetToken, - ); - }, - ), - Text( - 'by'.tr(args: [artistName]), - style: theme.textTheme.ppMori400White14, - ), - ], - ), - ), - const SizedBox( - width: 10, - ), - SvgPicture.asset( - 'assets/images/penrose_moma.svg', - colorFilter: ColorFilter.mode( - theme.colorScheme.secondary, BlendMode.srcIn), - height: 27, - ), - ], - ), - ), - Divider( - color: theme.colorScheme.secondary, - ), - const SizedBox( - height: 30, - ), - RichText( - maxLines: 5, - overflow: TextOverflow.ellipsis, - text: TextSpan( - text: giftIntro, - style: theme.textTheme.ppMori400White14, - children: [ - TextSpan( - text: gifter, - style: theme.primaryTextTheme.ppMori700White14, - ), - TextSpan( - text: '.', - style: theme.primaryTextTheme.ppMori400White14, - ), - ], - ), - ), - const SizedBox( - height: 30, - ), - PrimaryButton( - text: 'accept_ownership'.tr(), - enabled: !_processing, - isProcessing: _processing, - onTap: () async { - setState(() { - _processing = true; - }); - final blockchain = widget.payload.assetToken.blockchain; - final addresses = - await _accountService.getAddress(blockchain); - - String? address; - if (addresses.isEmpty) { - final defaultPersona = - await _accountService.getOrCreateDefaultPersona(); - final walletAddress = - await defaultPersona.insertNextAddress( - blockchain.toLowerCase() == 'tezos' - ? WalletType.Tezos - : WalletType.Ethereum); - await _configService.setDoneOnboarding(true); - _metricClient.mixPanelClient.initIfDefaultAccount(); - await _configService.setPendingSettings(true); - address = walletAddress.first.address; - } else if (addresses.length == 1) { - address = addresses.first; - } else { - if (mounted) { - final response = - await Navigator.of(context).pushNamed( - AppRouter.claimSelectAccountPage, - arguments: SelectAddressPagePayload( - blockchain: blockchain, - ), - ); - address = response as String?; - } - } - - if (address != null && mounted) { - _claimActivation( - context: context, - activationID: widget.payload.activationID, - receiveAddress: address, - otp: widget.payload.otp, - assetToken: widget.payload.assetToken, - ); - } else { - setState(() { - _processing = false; - }); - } - }, - ), - const SizedBox( - height: 30, - ), - Text( - 'accept_ownership_desc'.tr(), - style: theme.primaryTextTheme.ppMori400White14, - ), - const SizedBox( - height: 16, - ), - ], - ), - ), - ), - const SizedBox( - height: 10, - ), - OutlineButton( - text: 'decline'.tr(), - enabled: !_processing, - color: theme.colorScheme.primary, - onTap: () { - memoryValues.branchDeeplinkData.value = null; - Navigator.of(context).pop(false); - }, - ), - ], - ), - ), - ); - } - - Future _claimActivation( - {required BuildContext context, - required String activationID, - required String receiveAddress, - required AssetToken assetToken, - required Otp otp}) async { - try { - await _activationService.claimActivation( - request: ActivationClaimRequest( - activationID: activationID, - address: receiveAddress, - airdropTOTPPasscode: otp.code, - ), - assetToken: assetToken, - ); - } catch (e) { - setState(() { - _processing = false; - }); - return; - } - setState(() { - _processing = false; - }); - if (mounted) { - Navigator.of(context).pushNamedAndRemoveUntil( - AppRouter.homePage, - (route) => false, - ); - NftCollectionBloc.eventController - .add(GetTokensByOwnerEvent(pageKey: PageKey.init())); - final token = widget.payload.assetToken; - const caption = ''; - Navigator.of(context).pushNamed(AppRouter.artworkDetailsPage, - arguments: ArtworkDetailPayload( - [ArtworkIdentity(token.id, receiveAddress)], 0, - twitterCaption: caption)); - } - } -} diff --git a/lib/screen/claim/activation/preview_activation_claim.dart b/lib/screen/claim/activation/preview_activation_claim.dart deleted file mode 100644 index cabef1a3fb..0000000000 --- a/lib/screen/claim/activation/preview_activation_claim.dart +++ /dev/null @@ -1,173 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:after_layout/after_layout.dart'; -import 'package:autonomy_flutter/screen/app_router.dart'; -import 'package:autonomy_flutter/util/style.dart'; -import 'package:autonomy_flutter/view/responsive.dart'; -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:feralfile_app_theme/feral_file_app_theme.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:nft_collection/models/asset_token.dart'; -import 'package:shake/shake.dart'; - -class PreviewActivationTokenPage extends StatefulWidget { - final AssetToken assetToken; - - const PreviewActivationTokenPage({ - required this.assetToken, - super.key, - }); - - @override - State createState() => - _PreviewActivationTokenPageState(); -} - -class _PreviewActivationTokenPageState extends State - with AfterLayoutMixin, WidgetsBindingObserver { - bool isFullScreen = false; - ShakeDetector? _detector; - - @override - void dispose() { - super.dispose(); - WidgetsBinding.instance.removeObserver(this); - _detector?.stopListening(); - if (Platform.isAndroid) { - unawaited(SystemChrome.setEnabledSystemUIMode( - SystemUiMode.manual, - overlays: [SystemUiOverlay.top, SystemUiOverlay.bottom], - )); - } - } - - @override - void afterFirstLayout(BuildContext context) { - // Calling the same function "after layout" to resolve the issue. - _detector = ShakeDetector.autoStart( - onPhoneShake: () { - setState(() { - isFullScreen = false; - }); - unawaited(SystemChrome.setEnabledSystemUIMode( - SystemUiMode.manual, - overlays: SystemUiOverlay.values, - )); - }, - ); - - _detector?.startListening(); - - WidgetsBinding.instance.addObserver(this); - } - - @override - Widget build(BuildContext context) { - final safeAreaTop = MediaQuery.of(context).padding.top; - final theme = Theme.of(context); - final assetToken = widget.assetToken; - final artist = assetToken.artistName; - return Scaffold( - backgroundColor: theme.colorScheme.primary, - body: SafeArea( - top: false, - bottom: false, - left: !isFullScreen, - right: !isFullScreen, - child: Column( - children: [ - Visibility( - visible: !isFullScreen, - child: Container( - color: theme.colorScheme.primary, - height: safeAreaTop + 52, - padding: EdgeInsets.fromLTRB(15, safeAreaTop, 15, 0), - child: Row( - children: [ - Expanded( - child: GestureDetector( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SvgPicture.asset( - 'assets/images/iconInfo.svg', - colorFilter: ColorFilter.mode( - theme.colorScheme.secondary, - BlendMode.srcIn), - ), - const SizedBox( - width: 15, - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - assetToken.title ?? '', - overflow: TextOverflow.ellipsis, - style: ResponsiveLayout.isMobile - ? theme.textTheme.atlasWhiteBold12 - : theme.textTheme.atlasWhiteBold14, - ), - Row( - children: [ - const SizedBox(height: 4), - Text( - 'by'.tr(args: [artist ?? '']).trim(), - overflow: TextOverflow.ellipsis, - style: theme - .primaryTextTheme.headlineSmall, - ) - ], - ), - ], - ), - ), - const SizedBox(width: 5), - ], - ), - onTap: () { - unawaited(Navigator.of(context).pushNamed( - AppRouter.activationTokenDetailPage, - arguments: assetToken, - )); - }, - ), - ), - IconButton( - onPressed: () { - setState(() { - isFullScreen = true; - }); - }, - icon: Icon( - Icons.fullscreen, - color: theme.colorScheme.secondary, - size: 32, - ), - ), - IconButton( - onPressed: () => Navigator.of(context).pop(), - icon: closeIcon(color: theme.colorScheme.secondary), - tooltip: 'CloseArtwork', - ) - ], - ), - ), - ), - Expanded( - child: CachedNetworkImage( - imageUrl: assetToken.thumbnailURL!, - fit: BoxFit.contain, - ), - ) - ], - ), - )); - } -} diff --git a/lib/screen/claim/airdrop/claim_airdrop_page.dart b/lib/screen/claim/airdrop/claim_airdrop_page.dart deleted file mode 100644 index 65947108dc..0000000000 --- a/lib/screen/claim/airdrop/claim_airdrop_page.dart +++ /dev/null @@ -1,394 +0,0 @@ -import 'dart:async'; - -import 'package:auto_size_text/auto_size_text.dart'; -import 'package:autonomy_flutter/common/injector.dart'; -import 'package:autonomy_flutter/main.dart'; -import 'package:autonomy_flutter/model/ff_series.dart'; -import 'package:autonomy_flutter/screen/app_router.dart'; -import 'package:autonomy_flutter/screen/claim/claim_token_page.dart'; -import 'package:autonomy_flutter/screen/claim/select_account_page.dart'; -import 'package:autonomy_flutter/screen/detail/artwork_detail_page.dart'; -import 'package:autonomy_flutter/service/account_service.dart'; -import 'package:autonomy_flutter/service/airdrop_service.dart'; -import 'package:autonomy_flutter/service/configuration_service.dart'; -import 'package:autonomy_flutter/service/metric_client_service.dart'; -import 'package:autonomy_flutter/util/constants.dart'; -import 'package:autonomy_flutter/util/feralfile_extension.dart'; -import 'package:autonomy_flutter/util/string_ext.dart'; -import 'package:autonomy_flutter/util/style.dart'; -import 'package:autonomy_flutter/util/wallet_utils.dart'; -import 'package:autonomy_flutter/view/primary_button.dart'; -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:feralfile_app_theme/feral_file_app_theme.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.dart'; -import 'package:marqueer/marqueer.dart'; -import 'package:nft_collection/nft_collection.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class ClaimAirdropPagePayload { - final String claimID; - final String shareCode; - final FFSeries series; - final bool allowViewOnlyClaim; - - ClaimAirdropPagePayload({ - required this.claimID, - required this.shareCode, - required this.series, - this.allowViewOnlyClaim = false, - }); -} - -class ClaimAirdropPage extends StatefulWidget { - final ClaimAirdropPagePayload payload; - - const ClaimAirdropPage({ - required this.payload, - super.key, - }); - - @override - State createState() => _ClaimAirdropPageState(); -} - -class _ClaimAirdropPageState extends State { - bool _processing = false; - - final _metricClient = injector.get(); - final _airdropService = injector(); - final _accountService = injector(); - final _configService = injector(); - - @override - Widget build(BuildContext context) { - final artwork = widget.payload.series; - final artist = artwork.artist; - final artistName = artist != null ? artist.getDisplayName() : ''; - final artworkThumbnail = artwork.getThumbnailURL(); - String gifter = - artwork.airdropInfo?.gifter?.replaceAll(' ', '\u00A0') ?? ''; - String giftIntro = 'you_can_receive_free_gift'.tr(); - if (gifter.trim().isNotEmpty) { - giftIntro += " ${'from'.tr().toLowerCase()} "; - } - final theme = Theme.of(context); - return Scaffold( - backgroundColor: theme.colorScheme.primary, - body: Container( - padding: const EdgeInsets.fromLTRB(14, 28, 14, 40), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: NotificationListener( - onNotification: (overScroll) { - overScroll.disallowIndicator(); - return false; - }, - child: ListView( - padding: const EdgeInsets.all(0), - shrinkWrap: true, - children: [ - const SizedBox( - height: 24, - ), - FittedBox( - child: GestureDetector( - behavior: HitTestBehavior.opaque, - child: Transform.translate( - offset: const Offset(1, 0), - child: Container( - color: Colors.white, - child: Column( - children: [ - const SizedBox( - height: 10, - ), - SizedBox( - width: MediaQuery.of(context).size.width, - height: 20, - child: Marqueer( - direction: MarqueerDirection.ltr, - pps: 30, - child: Text( - 'gift_edition'.tr(), - style: theme.textTheme.ppMori400Black14, - ), - ), - ), - Padding( - padding: const EdgeInsets.symmetric( - vertical: 45, - horizontal: 75, - ), - child: Container( - color: Colors.black, - child: CachedNetworkImage( - fit: BoxFit.cover, - imageUrl: artworkThumbnail, - width: 225, - height: 225, - ), - ), - ), - SizedBox( - width: MediaQuery.of(context).size.width, - height: 30, - child: Marqueer( - pps: 30, - child: Text( - 'gift_edition'.tr().toUpperCase(), - style: theme.textTheme.ppMori400Black14, - ), - ), - ), - ], - ), - ), - ), - onTap: () async { - await Navigator.pushNamed( - context, - AppRouter.autonomyAirdropTokenPreviewPage, - arguments: widget.payload.series, - ); - }, - ), - ), - Padding( - padding: const EdgeInsets.all(15), - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - behavior: HitTestBehavior.opaque, - child: AutoSizeText( - artwork.title, - style: theme.textTheme.ppMori400White14, - maxFontSize: 14, - minFontSize: 14, - maxLines: 2, - ), - onTap: () async { - await Navigator.pushNamed( - context, - AppRouter.autonomyAirdropTokenPreviewPage, - arguments: widget.payload.series, - ); - }, - ), - Text( - 'by'.tr(args: [artistName]), - style: theme.textTheme.ppMori400White14, - ), - ], - ), - ), - const SizedBox( - width: 10, - ), - SvgPicture.asset( - 'assets/images/penrose_moma.svg', - colorFilter: ColorFilter.mode( - theme.colorScheme.secondary, BlendMode.srcIn), - height: 27, - ), - ], - ), - ), - Divider( - color: theme.colorScheme.secondary, - ), - const SizedBox( - height: 30, - ), - RichText( - maxLines: 5, - overflow: TextOverflow.ellipsis, - text: TextSpan( - text: giftIntro, - style: theme.textTheme.ppMori400White14, - children: [ - TextSpan( - text: gifter, - style: theme.primaryTextTheme.ppMori700White14, - ), - TextSpan( - text: '.', - style: theme.primaryTextTheme.ppMori400White14, - ), - ], - ), - ), - const SizedBox( - height: 30, - ), - PrimaryButton( - text: 'accept_ownership'.tr(), - enabled: !_processing, - isProcessing: _processing, - onTap: () async { - setState(() { - _processing = true; - }); - final blockchain = widget - .payload.series.exhibition?.mintBlockchain - .capitalize() ?? - 'Tezos'; - final addresses = await _accountService.getAddress( - blockchain, - withViewOnly: widget.payload.allowViewOnlyClaim); - - String? address; - if (addresses.isEmpty) { - final defaultPersona = - await _accountService.getOrCreateDefaultPersona(); - final walletAddress = - await defaultPersona.insertNextAddress( - blockchain.toLowerCase() == 'tezos' - ? WalletType.Tezos - : WalletType.Ethereum); - await _configService.setDoneOnboarding(true); - unawaited(_metricClient.mixPanelClient - .initIfDefaultAccount()); - await _configService.setPendingSettings(true); - address = walletAddress.first.address; - } else if (addresses.length == 1) { - address = addresses.first; - } else { - if (mounted) { - final response = - await Navigator.of(context).pushNamed( - AppRouter.claimSelectAccountPage, - arguments: SelectAddressPagePayload( - blockchain: blockchain, - withViewOnly: true, - ), - ); - address = response as String?; - } - } - - if (address != null && mounted) { - await _claimToken( - context: context, - claimID: widget.payload.claimID, - shareCode: widget.payload.shareCode, - seriesId: widget.payload.series.id, - receiveAddress: address, - ); - } else { - setState(() { - _processing = false; - }); - } - }, - ), - const SizedBox( - height: 30, - ), - Text( - 'accept_ownership_desc'.tr(), - style: theme.primaryTextTheme.ppMori400White14, - ), - const SizedBox( - height: 16, - ), - RichText( - text: TextSpan( - text: 'airdrop_accept_privacy_policy'.tr(), - style: theme.textTheme.ppMori400Grey12, - children: [ - TextSpan( - text: 'airdrop_privacy_policy'.tr(), - style: makeLinkStyle( - theme.textTheme.ppMori400Grey12, - ), - recognizer: TapGestureRecognizer() - ..onTap = () { - _openFFArtistCollector(); - }), - TextSpan( - text: '.', - style: theme.primaryTextTheme.bodyLarge - ?.copyWith(fontSize: 14), - ), - ], - ), - ), - ], - ), - ), - ), - const SizedBox( - height: 10, - ), - OutlineButton( - text: 'decline'.tr(), - enabled: !_processing, - color: theme.colorScheme.primary, - onTap: () { - memoryValues.branchDeeplinkData.value = null; - Navigator.of(context).pop(false); - }, - ), - ], - ), - ), - ); - } - - Future _claimToken( - {required BuildContext context, - required String claimID, - required String shareCode, - required String seriesId, - required String receiveAddress}) async { - ClaimResponse? claimRespone; - try { - claimRespone = await _airdropService.claimGift( - claimID: claimID, - shareCode: shareCode, - seriesId: seriesId, - receivingAddress: receiveAddress); - unawaited(_configService.setAlreadyClaimedAirdrop(seriesId, true)); - } catch (e) { - setState(() { - _processing = false; - }); - return; - } - setState(() { - _processing = false; - }); - if (mounted) { - await Navigator.of(context).pushNamedAndRemoveUntil( - AppRouter.homePage, - (route) => false, - ); - NftCollectionBloc.eventController - .add(GetTokensByOwnerEvent(pageKey: PageKey.init())); - final token = claimRespone.token; - final caption = claimRespone.airdropInfo.twitterCaption; - if (mounted) { - await Navigator.of(context).pushNamed(AppRouter.artworkDetailsPage, - arguments: ArtworkDetailPayload( - [ArtworkIdentity(token.id, token.owner)], 0, - twitterCaption: caption ?? '')); - } - } - } - - void _openFFArtistCollector() { - String uri = (widget.payload.series.exhibition?.id == null) - ? FF_ARTIST_COLLECTOR - : '$FF_ARTIST_COLLECTOR/${widget.payload.series.exhibition?.id}'; - unawaited(launchUrl(Uri.parse(uri), mode: LaunchMode.externalApplication)); - } -} diff --git a/lib/screen/claim/claim_token_page.dart b/lib/screen/claim/claim_token_page.dart deleted file mode 100644 index 4547d3c1c3..0000000000 --- a/lib/screen/claim/claim_token_page.dart +++ /dev/null @@ -1,428 +0,0 @@ -import 'dart:async'; - -import 'package:auto_size_text/auto_size_text.dart'; -import 'package:autonomy_flutter/common/injector.dart'; -import 'package:autonomy_flutter/main.dart'; -import 'package:autonomy_flutter/model/ff_account.dart'; -import 'package:autonomy_flutter/model/ff_series.dart'; -import 'package:autonomy_flutter/model/otp.dart'; -import 'package:autonomy_flutter/screen/app_router.dart'; -import 'package:autonomy_flutter/screen/claim/select_account_page.dart'; -import 'package:autonomy_flutter/screen/detail/artwork_detail_page.dart'; -import 'package:autonomy_flutter/screen/home/home_navigation_page.dart'; -import 'package:autonomy_flutter/service/account_service.dart'; -import 'package:autonomy_flutter/service/configuration_service.dart'; -import 'package:autonomy_flutter/service/feralfile_service.dart'; -import 'package:autonomy_flutter/service/metric_client_service.dart'; -import 'package:autonomy_flutter/service/navigation_service.dart'; -import 'package:autonomy_flutter/util/constants.dart'; -import 'package:autonomy_flutter/util/dio_exception_ext.dart'; -import 'package:autonomy_flutter/util/feralfile_extension.dart'; -import 'package:autonomy_flutter/util/log.dart'; -import 'package:autonomy_flutter/util/string_ext.dart'; -import 'package:autonomy_flutter/util/style.dart'; -import 'package:autonomy_flutter/util/wallet_utils.dart'; -import 'package:autonomy_flutter/view/primary_button.dart'; -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:dio/dio.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:feralfile_app_theme/feral_file_app_theme.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.dart'; -import 'package:marqueer/marqueer.dart'; -import 'package:nft_collection/models/asset_token.dart'; -import 'package:nft_collection/nft_collection.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class ClaimTokenPagePayload { - final FFSeries series; - final Otp? otp; - final bool allowViewOnlyClaim; - final Future Function({required String receiveAddress})? - claimFunction; - - ClaimTokenPagePayload({ - required this.series, - required this.claimFunction, - this.otp, - this.allowViewOnlyClaim = false, - }); -} - -class ClaimTokenPage extends StatefulWidget { - final ClaimTokenPagePayload payload; - - const ClaimTokenPage({ - required this.payload, - super.key, - }); - - @override - State createState() => _ClaimTokenPageState(); -} - -class _ClaimTokenPageState extends State { - bool _processing = false; - - final metricClient = injector.get(); - final configurationService = injector.get(); - final _navigationService = injector.get(); - - @override - Widget build(BuildContext context) { - final artwork = widget.payload.series; - final artist = artwork.artist; - final artistName = artist != null ? artist.getDisplayName() : ''; - final artworkThumbnail = artwork.getThumbnailURL(); - String gifter = - artwork.airdropInfo?.gifter?.replaceAll(' ', '\u00A0') ?? ''; - String giftIntro = 'you_can_receive_free_gift'.tr(); - if (gifter.trim().isNotEmpty) { - giftIntro += " ${'from'.tr().toLowerCase()} "; - } - final theme = Theme.of(context); - return Scaffold( - backgroundColor: theme.colorScheme.primary, - body: Container( - padding: const EdgeInsets.fromLTRB(14, 28, 14, 40), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: NotificationListener( - onNotification: (overScroll) { - overScroll.disallowIndicator(); - return false; - }, - child: ListView( - padding: const EdgeInsets.all(0), - shrinkWrap: true, - children: [ - const SizedBox( - height: 24, - ), - FittedBox( - child: GestureDetector( - behavior: HitTestBehavior.opaque, - child: Transform.translate( - offset: const Offset(1, 0), - child: Container( - color: Colors.white, - child: Column( - children: [ - const SizedBox( - height: 10, - ), - SizedBox( - width: MediaQuery.of(context).size.width, - height: 20, - child: Marqueer( - direction: MarqueerDirection.ltr, - pps: 30, - child: Text( - 'gift_edition'.tr().toUpperCase(), - style: theme.textTheme.ppMori400Black14, - ), - ), - ), - Padding( - padding: const EdgeInsets.symmetric( - vertical: 45, - horizontal: 75, - ), - child: Container( - color: Colors.black, - child: CachedNetworkImage( - fit: BoxFit.cover, - imageUrl: artworkThumbnail, - width: 225, - height: 225, - ), - ), - ), - SizedBox( - width: MediaQuery.of(context).size.width, - height: 30, - child: Marqueer( - pps: 30, - child: Text( - 'gift_edition'.tr().toUpperCase(), - style: theme.textTheme.ppMori400Black14, - ), - ), - ), - ], - ), - ), - ), - onTap: () async { - await Navigator.pushNamed( - context, - AppRouter.feralfileAirdropTokenPreviewPage, - arguments: widget.payload.series, - ); - }, - ), - ), - Padding( - padding: const EdgeInsets.all(15), - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - behavior: HitTestBehavior.opaque, - child: AutoSizeText( - artwork.title, - style: theme.textTheme.ppMori400White14, - maxFontSize: 14, - minFontSize: 14, - maxLines: 2, - ), - onTap: () async { - await Navigator.pushNamed( - context, - AppRouter - .feralfileAirdropTokenPreviewPage, - arguments: widget.payload.series, - ); - }, - ), - Text( - 'by'.tr(args: [artistName]), - style: theme.textTheme.ppMori400White14, - ), - ], - ), - ), - const SizedBox( - width: 10, - ), - SvgPicture.asset( - 'assets/images/penrose_moma.svg', - colorFilter: ColorFilter.mode( - theme.colorScheme.secondary, BlendMode.srcIn), - height: 27, - ), - ], - ), - ), - Divider( - color: theme.colorScheme.secondary, - ), - const SizedBox( - height: 30, - ), - RichText( - maxLines: 5, - overflow: TextOverflow.ellipsis, - text: TextSpan( - text: giftIntro, - style: theme.textTheme.ppMori400White14, - children: [ - TextSpan( - text: gifter, - style: theme.primaryTextTheme.ppMori700White14, - ), - TextSpan( - text: '.', - style: theme.primaryTextTheme.ppMori400White14, - ), - ], - ), - ), - const SizedBox( - height: 30, - ), - PrimaryButton( - text: 'accept_ownership'.tr(), - enabled: !_processing, - isProcessing: _processing, - onTap: () async { - setState(() { - _processing = true; - }); - final blockchain = widget - .payload.series.exhibition?.mintBlockchain - .capitalize() ?? - 'Tezos'; - final accountService = injector(); - final addresses = await accountService.getAddress( - blockchain, - withViewOnly: widget.payload.allowViewOnlyClaim); - - String? address; - if (addresses.isEmpty) { - final defaultPersona = - await accountService.getOrCreateDefaultPersona(); - final walletAddress = - await defaultPersona.insertNextAddress( - blockchain.toLowerCase() == 'tezos' - ? WalletType.Tezos - : WalletType.Ethereum); - - final configService = - injector(); - await configService.setDoneOnboarding(true); - unawaited(injector() - .mixPanelClient - .initIfDefaultAccount()); - await configService.setPendingSettings(true); - address = walletAddress.first.address; - } else if (addresses.length == 1) { - address = addresses.first; - } else { - if (!mounted) { - return; - } - if (mounted) { - final response = - await Navigator.of(context).pushNamed( - AppRouter.claimSelectAccountPage, - arguments: SelectAddressPagePayload( - blockchain: blockchain, - withViewOnly: widget.payload.allowViewOnlyClaim, - ), - ); - address = response as String?; - } - } - if (address != null && mounted) { - unawaited(_claimToken(context, address)); - } else { - setState(() { - _processing = false; - }); - } - }, - ), - const SizedBox( - height: 30, - ), - Text( - 'accept_ownership_desc'.tr(), - style: theme.primaryTextTheme.ppMori400White14, - ), - const SizedBox( - height: 16, - ), - RichText( - text: TextSpan( - text: 'airdrop_accept_privacy_policy'.tr(), - style: theme.textTheme.ppMori400Grey12, - children: [ - TextSpan( - text: 'airdrop_privacy_policy'.tr(), - style: makeLinkStyle( - theme.textTheme.ppMori400Grey12, - ), - recognizer: TapGestureRecognizer() - ..onTap = () { - _openFFArtistCollector(); - }), - TextSpan( - text: '.', - style: theme.primaryTextTheme.bodyLarge - ?.copyWith(fontSize: 14), - ), - ], - ), - ), - ], - ), - ), - ), - const SizedBox( - height: 10, - ), - OutlineButton( - text: 'decline'.tr(), - enabled: !_processing, - color: theme.colorScheme.primary, - onTap: () { - memoryValues.branchDeeplinkData.value = null; - Navigator.of(context).pop(false); - }, - ), - ], - ), - ), - ); - } - - Future _claimToken(BuildContext context, String receiveAddress) async { - ClaimResponse? claimResponse; - final ffService = injector(); - try { - if (widget.payload.claimFunction != null) { - claimResponse = - await widget.payload.claimFunction!(receiveAddress: receiveAddress); - } else { - claimResponse = await ffService.claimToken( - seriesId: widget.payload.series.id, - address: receiveAddress, - otp: widget.payload.otp, - ); - } - unawaited(configurationService.setAlreadyClaimedAirdrop( - widget.payload.series.id, true)); - memoryValues.branchDeeplinkData.value = null; - } catch (e) { - log.info('[ClaimTokenPage] Claim token failed. $e'); - if (mounted) { - if (e is DioException && e.isClaimPassLimit) { - await _navigationService.showFeralFileClaimTokenPassLimit( - series: widget.payload.series); - } else { - await _navigationService.showClaimTokenError( - e, - series: widget.payload.series, - ); - } - } - memoryValues.branchDeeplinkData.value = null; - } - setState(() { - _processing = false; - }); - if (mounted) { - unawaited(Navigator.of(context).pushNamedAndRemoveUntil( - AppRouter.homePage, - (route) => false, - arguments: const HomeNavigationPagePayload( - startedTab: HomeNavigatorTab.collection, - ), - )); - NftCollectionBloc.eventController - .add(GetTokensByOwnerEvent(pageKey: PageKey.init())); - final token = claimResponse?.token; - final caption = claimResponse?.airdropInfo.twitterCaption; - if (token == null) { - return; - } - if (mounted) { - unawaited(Navigator.of(context).pushNamed(AppRouter.artworkDetailsPage, - arguments: ArtworkDetailPayload( - [ArtworkIdentity(token.id, token.owner)], 0, - twitterCaption: caption ?? ''))); - } - } - } - - void _openFFArtistCollector() { - String uri = (widget.payload.series.exhibition?.id == null) - ? FF_ARTIST_COLLECTOR - : '$FF_ARTIST_COLLECTOR/${widget.payload.series.exhibition?.id}'; - unawaited(launchUrl(Uri.parse(uri), mode: LaunchMode.externalApplication)); - } -} - -class ClaimResponse { - AssetToken token; - AirdropInfo airdropInfo; - - ClaimResponse({required this.token, required this.airdropInfo}); -} diff --git a/lib/screen/claim/preview_token_claim.dart b/lib/screen/claim/preview_token_claim.dart deleted file mode 100644 index f31218a1bf..0000000000 --- a/lib/screen/claim/preview_token_claim.dart +++ /dev/null @@ -1,178 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:after_layout/after_layout.dart'; -import 'package:autonomy_flutter/model/ff_series.dart'; -import 'package:autonomy_flutter/screen/app_router.dart'; -import 'package:autonomy_flutter/util/feralfile_extension.dart'; -import 'package:autonomy_flutter/util/style.dart'; -import 'package:autonomy_flutter/view/responsive.dart'; -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:feralfile_app_theme/feral_file_app_theme.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:shake/shake.dart'; - -class PreviewTokenClaim extends StatefulWidget { - final FFSeries series; - - const PreviewTokenClaim({ - required this.series, - super.key, - }); - - @override - State createState() => _PreviewTokenClaimState(); -} - -class _PreviewTokenClaimState extends State - with AfterLayoutMixin, WidgetsBindingObserver { - bool isFullScreen = false; - ShakeDetector? _detector; - - @override - void dispose() { - super.dispose(); - WidgetsBinding.instance.removeObserver(this); - _detector?.stopListening(); - if (Platform.isAndroid) { - unawaited(SystemChrome.setEnabledSystemUIMode( - SystemUiMode.manual, - overlays: [SystemUiOverlay.top, SystemUiOverlay.bottom], - )); - } - } - - @override - void afterFirstLayout(BuildContext context) { - // Calling the same function "after layout" to resolve the issue. - _detector = ShakeDetector.autoStart( - onPhoneShake: () { - setState(() { - isFullScreen = false; - }); - unawaited(SystemChrome.setEnabledSystemUIMode( - SystemUiMode.manual, - overlays: SystemUiOverlay.values, - )); - }, - ); - - _detector?.startListening(); - - WidgetsBinding.instance.addObserver(this); - } - - @override - Widget build(BuildContext context) { - final safeAreaTop = MediaQuery.of(context).padding.top; - final theme = Theme.of(context); - final artwork = widget.series; - final artist = artwork.artist; - return Scaffold( - backgroundColor: theme.colorScheme.primary, - body: SafeArea( - top: false, - bottom: false, - left: !isFullScreen, - right: !isFullScreen, - child: Column( - children: [ - Visibility( - visible: !isFullScreen, - child: Container( - color: theme.colorScheme.primary, - height: safeAreaTop + 52, - padding: EdgeInsets.fromLTRB(15, safeAreaTop, 15, 0), - child: Row( - children: [ - Expanded( - child: GestureDetector( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SvgPicture.asset( - 'assets/images/iconInfo.svg', - colorFilter: ColorFilter.mode( - theme.colorScheme.secondary, - BlendMode.srcIn), - ), - const SizedBox( - width: 15, - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - artwork.title, - overflow: TextOverflow.ellipsis, - style: ResponsiveLayout.isMobile - ? theme.textTheme.atlasWhiteBold12 - : theme.textTheme.atlasWhiteBold14, - ), - Row( - children: [ - const SizedBox(height: 4), - Text( - 'by'.tr(args: [ - if (artist != null) - artist.getDisplayName() - else - '' - ]).trim(), - overflow: TextOverflow.ellipsis, - style: theme - .primaryTextTheme.headlineSmall, - ) - ], - ), - ], - ), - ), - const SizedBox(width: 5), - ], - ), - onTap: () async { - await Navigator.of(context).pushNamed( - AppRouter.airdropTokenDetailPage, - arguments: artwork, - ); - }, - ), - ), - IconButton( - onPressed: () { - setState(() { - isFullScreen = true; - }); - }, - icon: Icon( - Icons.fullscreen, - color: theme.colorScheme.secondary, - size: 32, - ), - ), - IconButton( - onPressed: () => Navigator.of(context).pop(), - icon: closeIcon(color: theme.colorScheme.secondary), - tooltip: 'CloseArtwork', - ) - ], - ), - ), - ), - Expanded( - child: CachedNetworkImage( - imageUrl: artwork.getThumbnailURL(), - fit: BoxFit.contain, - ), - ) - ], - ), - )); - } -} diff --git a/lib/screen/claim/select_account_page.dart b/lib/screen/claim/select_account_page.dart deleted file mode 100644 index 5926d37803..0000000000 --- a/lib/screen/claim/select_account_page.dart +++ /dev/null @@ -1,142 +0,0 @@ -import 'package:autonomy_flutter/main.dart'; -import 'package:autonomy_flutter/screen/bloc/accounts/accounts_bloc.dart'; -import 'package:autonomy_flutter/util/style.dart'; -import 'package:autonomy_flutter/view/back_appbar.dart'; -import 'package:autonomy_flutter/view/list_address_account.dart'; -import 'package:autonomy_flutter/view/primary_button.dart'; -import 'package:autonomy_flutter/view/responsive.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:feralfile_app_theme/feral_file_app_theme.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class SelectAddressPagePayload { - final String? blockchain; - final bool withViewOnly; - - SelectAddressPagePayload({ - this.blockchain, - this.withViewOnly = false, - }); -} - -class SelectAccountPage extends StatefulWidget { - final SelectAddressPagePayload payload; - - const SelectAccountPage({ - required this.payload, - super.key, - }); - - @override - State createState() => _SelectAccountPageState(); -} - -class _SelectAccountPageState extends State with RouteAware { - String? _selectedAddress; - late final bool _isTezos; - - @override - void initState() { - _isTezos = widget.payload.blockchain?.toLowerCase() == 'tezos'; - _callAccountEvent(); - super.initState(); - } - - @override - void didChangeDependencies() { - routeObserver.subscribe(this, ModalRoute.of(context)!); - super.didChangeDependencies(); - } - - @override - void dispose() { - routeObserver.unsubscribe(this); - super.dispose(); - } - - @override - void didPopNext() { - _callAccountEvent(); - super.didPopNext(); - } - - void _callAccountEvent() { - context.read().add(GetCategorizedAccountsEvent( - getEth: !_isTezos, - getTezos: _isTezos, - includeLinkedAccount: widget.payload.withViewOnly)); - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - return Scaffold( - appBar: getBackAppBar( - context, - onBack: () { - Navigator.of(context).pop(); - }, - title: 'gift_edition'.tr(), - ), - body: Container( - padding: const EdgeInsets.only(bottom: 32), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - addTitleSpace(), - Padding( - padding: ResponsiveLayout.pageHorizontalEdgeInsets, - child: Text( - 'where_do_want_to_receive_gift'.tr(), - style: theme.textTheme.ppMori700Black24, - ), - ), - const SizedBox(height: 60), - Padding( - padding: ResponsiveLayout.pageHorizontalEdgeInsets, - child: Text( - 'claim_airdrop_select_account_desc'.tr(args: [ - widget.payload.blockchain ?? 'Tezos', - widget.payload.blockchain ?? 'Tezos', - ]), - style: theme.textTheme.ppMori400Black14, - ), - ), - const SizedBox(height: 30), - Expanded( - child: SingleChildScrollView(child: _buildAddressList()), - ), - Padding( - padding: ResponsiveLayout.pageHorizontalEdgeInsets, - child: PrimaryAsyncButton( - text: 'h_confirm'.tr(), - enabled: _selectedAddress != null, - onTap: () async { - Navigator.pop(context, _selectedAddress); - }, - ), - ), - ], - ), - ), - ); - } - - Widget _buildAddressList() => BlocBuilder( - builder: (context, state) { - final accounts = state.accounts ?? []; - void select(Account value) { - setState(() { - _selectedAddress = value.accountNumber; - }); - } - - return ListAccountConnect( - accounts: accounts, - onSelectEth: !_isTezos ? select : null, - onSelectTez: _isTezos ? select : null, - ); - }, - ); -} diff --git a/lib/screen/claim/token_detail_page.dart b/lib/screen/claim/token_detail_page.dart deleted file mode 100644 index 15b456707d..0000000000 --- a/lib/screen/claim/token_detail_page.dart +++ /dev/null @@ -1,154 +0,0 @@ -// -// SPDX-License-Identifier: BSD-2-Clause-Patent -// Copyright © 2022 Bitmark. All rights reserved. -// Use of this source code is governed by the BSD-2-Clause Plus Patent License -// that can be found in the LICENSE file. -// - -import 'package:autonomy_flutter/model/ff_series.dart'; -import 'package:autonomy_flutter/util/feralfile_extension.dart'; -import 'package:autonomy_flutter/util/style.dart'; -import 'package:autonomy_flutter/view/artwork_common_widget.dart'; -import 'package:autonomy_flutter/view/responsive.dart'; -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; - -class TokenDetailPage extends StatefulWidget { - final FFSeries series; - - const TokenDetailPage({ - required this.series, - super.key, - }); - - @override - State createState() => _TokenDetailPageState(); -} - -class _TokenDetailPageState extends State { - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final series = widget.series; - final artist = series.artist; - final contract = series.contract; - return Scaffold( - appBar: _appBar( - context, - onBack: () => Navigator.of(context).pop(), - ), - backgroundColor: theme.colorScheme.primary, - body: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 16), - Padding( - padding: ResponsiveLayout.getPadding, - child: Text( - series.title, - style: theme.primaryTextTheme.displayLarge, - ), - ), - const SizedBox(height: 8), - Padding( - padding: ResponsiveLayout.getPadding, - child: Text( - 'by'.tr(args: [artist?.getDisplayName() ?? '']).trim(), - style: theme.primaryTextTheme.headlineMedium - ?.copyWith(fontSize: 18), - ), - ), - const SizedBox(height: 15), - // Show artwork here. - CachedNetworkImage( - imageUrl: series.getThumbnailURL(), - fit: BoxFit.fitWidth, - ), - const SizedBox(height: 24), - Padding( - padding: ResponsiveLayout.getPadding, - child: HtmlWidget( - customStylesBuilder: auHtmlStyle, - series.description ?? '', - textStyle: theme.primaryTextTheme.bodyLarge, - ), - ), - const SizedBox(height: 40), - Padding( - padding: ResponsiveLayout.getPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Theme( - data: theme.copyWith(textTheme: theme.primaryTextTheme), - child: FeralfileArtworkDetailsMetadataSection( - series: widget.series, - ), - ), - const SizedBox(height: 40), - Theme( - data: theme.copyWith(textTheme: theme.primaryTextTheme), - child: ArtworkRightWidget( - contract: contract, - exhibitionID: widget.series.exhibition?.id, - ), - ), - const SizedBox(height: 40), - ], - ), - ) - ], - ), - )); - } - - AppBar _appBar( - BuildContext context, { - required void Function() onBack, - }) { - final theme = Theme.of(context); - return AppBar( - systemOverlayStyle: SystemUiOverlayStyle( - statusBarColor: theme.colorScheme.secondary, - statusBarIconBrightness: Brightness.dark, - statusBarBrightness: Brightness.light, - ), - leading: const SizedBox(), - leadingWidth: 0, - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: onBack, - child: Padding( - padding: const EdgeInsets.fromLTRB(0, 7, 18, 8), - child: Row( - children: [ - SvgPicture.asset( - 'assets/images/nav-arrow-left.svg', - colorFilter: - const ColorFilter.mode(Colors.white, BlendMode.srcIn), - ), - const SizedBox(width: 7), - Text( - 'BACK', - style: theme.primaryTextTheme.labelLarge, - ), - ], - ), - ), - ), - ], - ), - backgroundColor: Colors.transparent, - shadowColor: Colors.transparent, - elevation: 0, - ); - } -} diff --git a/lib/screen/cloud/cloud_android_page.dart b/lib/screen/cloud/cloud_android_page.dart index 9c36d2769c..b245a7787d 100644 --- a/lib/screen/cloud/cloud_android_page.dart +++ b/lib/screen/cloud/cloud_android_page.dart @@ -189,7 +189,6 @@ class _CloudAndroidPageState extends State void _continue(BuildContext context) { if (injector().isDoneOnboarding()) { Navigator.of(context).popUntil((route) => - route.settings.name == AppRouter.claimSelectAccountPage || route.settings.name == AppRouter.tbConnectPage || route.settings.name == AppRouter.wc2ConnectPage || route.settings.name == AppRouter.homePage || diff --git a/lib/screen/cloud/cloud_page.dart b/lib/screen/cloud/cloud_page.dart index ba1d990dcd..114a24aa58 100644 --- a/lib/screen/cloud/cloud_page.dart +++ b/lib/screen/cloud/cloud_page.dart @@ -151,7 +151,6 @@ class CloudPage extends StatelessWidget { void _continue(BuildContext context) { if (injector().isDoneOnboarding()) { Navigator.of(context).popUntil((route) => - route.settings.name == AppRouter.claimSelectAccountPage || route.settings.name == AppRouter.tbConnectPage || route.settings.name == AppRouter.wc2ConnectPage || route.settings.name == AppRouter.homePage || diff --git a/lib/screen/collection_pro/artists_list_page/artists_list_page.dart b/lib/screen/collection_pro/artists_list_page/artists_list_page.dart new file mode 100644 index 0000000000..3c714724a5 --- /dev/null +++ b/lib/screen/collection_pro/artists_list_page/artists_list_page.dart @@ -0,0 +1,161 @@ +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// Copyright © 2022 Bitmark. All rights reserved. +// Use of this source code is governed by the BSD-2-Clause Plus Patent License +// that can be found in the LICENSE file. +// +import 'dart:async'; +import 'dart:math'; + +import 'package:autonomy_flutter/screen/predefined_collection/predefined_collection_screen.dart'; +import 'package:autonomy_flutter/util/string_ext.dart'; +import 'package:autonomy_flutter/util/style.dart'; +import 'package:autonomy_flutter/view/back_appbar.dart'; +import 'package:autonomy_flutter/view/header.dart'; +import 'package:autonomy_flutter/view/paging_bar.dart'; +import 'package:autonomy_flutter/view/predefined_collection/predefined_collection_item.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:feralfile_app_theme/feral_file_app_theme.dart'; +import 'package:flutter/material.dart'; +import 'package:nft_collection/models/predefined_collection_model.dart'; + +class ArtistsListPage extends StatefulWidget { + final ArtistsListPagePayload payload; + + const ArtistsListPage({required this.payload, super.key}); + + @override + State createState() => _ArtistsListPageState(); +} + +class _ArtistsListPageState extends State { + final ScrollController _scrollController = ScrollController(); + final List _items = []; + final ValueNotifier _selectedCharacter = ValueNotifier(null); + final _itemHeight = PredefinedCollectionItem.height + 1; + static const int _scrollDuration = 500; + static const int _scrollLag = 10; + bool _isDragging = false; + + @override + void initState() { + super.initState(); + _items.addAll(widget.payload.listPredefinedCollectionByArtist); + + _selectedCharacter.value = _items.first.name.firstSearchCharacter; + + _scrollController.addListener(_scrollListener); + } + + void _scrollListener() { + if (_isDragging) { + return; + } + double offset = _scrollController.offset; + final targetIndex = (offset / _itemHeight).floor(); + if (targetIndex < 0 || targetIndex >= _items.length) { + return; + } + final selectedCharacter = _items[targetIndex].name.firstSearchCharacter; + _selectedCharacter.value = selectedCharacter; + } + + @override + void dispose() { + _scrollController + ..removeListener(_scrollListener) + ..dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => Scaffold( + backgroundColor: AppColor.primaryBlack, + appBar: _getAppBar(context), + body: _body(context), + ); + + AppBar _getAppBar(BuildContext context) => getFFAppBar( + context, + onBack: () => Navigator.pop(context), + title: HeaderView( + title: 'artists'.tr(), + padding: EdgeInsets.zero, + ), + action: const SizedBox(), + ); + + Widget _body(BuildContext context) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: Column( + children: [ + ValueListenableBuilder( + valueListenable: _selectedCharacter, + builder: (context, value, child) => PagingBar( + onTap: (a) async { + final index = _items.indexWhere((element) => + element.name.firstSearchCharacter == a); + if (index == -1) { + final nearestIndex = _items.lastIndexWhere( + (element) => + element.name.firstSearchCharacter + .compareSearchKey(a) < + 0); + if (nearestIndex == -1) { + await _scrollTo(0); + } else { + await _scrollTo(nearestIndex * _itemHeight); + } + } else { + await _scrollTo(index * _itemHeight); + } + Future.delayed(const Duration(milliseconds: _scrollLag), + () { + _selectedCharacter.value = a; + }); + }, + onDragEnd: () { + Future.delayed( + const Duration( + milliseconds: _scrollDuration + _scrollLag), + () { + _isDragging = false; + }); + }, + onDragging: () { + _isDragging = true; + }, + selectedCharacter: value, + )), + Expanded( + child: ListView.separated( + controller: _scrollController, + itemCount: _items.length, + itemBuilder: (context, index) { + final predefinedCollection = _items[index]; + return PredefinedCollectionItem( + predefinedCollection: predefinedCollection, + type: PredefinedCollectionType.artist, + searchStr: '', + ); + }, + separatorBuilder: (BuildContext context, int index) => + addOnlyDivider(color: AppColor.auGreyBackground), + )), + ], + ), + ); + + Future _scrollTo(double offset) async { + await _scrollController.animateTo( + min(_scrollController.position.maxScrollExtent, offset), + duration: const Duration(milliseconds: _scrollDuration), + curve: Curves.easeIn); + } +} + +class ArtistsListPagePayload { + final List listPredefinedCollectionByArtist; + + ArtistsListPagePayload(this.listPredefinedCollectionByArtist); +} diff --git a/lib/screen/collection_pro/collection_pro_screen.dart b/lib/screen/collection_pro/collection_pro_screen.dart index c4b9301242..650f27a5ac 100644 --- a/lib/screen/collection_pro/collection_pro_screen.dart +++ b/lib/screen/collection_pro/collection_pro_screen.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:math'; import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/main.dart'; @@ -7,24 +8,28 @@ import 'package:autonomy_flutter/model/play_list_model.dart'; import 'package:autonomy_flutter/model/shared_postcard.dart'; import 'package:autonomy_flutter/screen/app_router.dart'; import 'package:autonomy_flutter/screen/bloc/identity/identity_bloc.dart'; +import 'package:autonomy_flutter/screen/collection_pro/artists_list_page/artists_list_page.dart'; import 'package:autonomy_flutter/screen/collection_pro/collection_pro_bloc.dart'; import 'package:autonomy_flutter/screen/collection_pro/collection_pro_state.dart'; import 'package:autonomy_flutter/screen/detail/artwork_detail_page.dart'; import 'package:autonomy_flutter/screen/playlists/list_playlists/list_playlists.dart'; import 'package:autonomy_flutter/screen/playlists/view_playlist/view_playlist.dart'; import 'package:autonomy_flutter/screen/predefined_collection/predefined_collection_screen.dart'; +import 'package:autonomy_flutter/screen/wallet/wallet_page.dart'; import 'package:autonomy_flutter/service/configuration_service.dart'; import 'package:autonomy_flutter/service/playlist_service.dart'; import 'package:autonomy_flutter/service/versions_service.dart'; +import 'package:autonomy_flutter/util/asset_token_ext.dart'; import 'package:autonomy_flutter/util/collection_ext.dart'; -import 'package:autonomy_flutter/util/medium_category_ext.dart'; import 'package:autonomy_flutter/util/predefined_collection_ext.dart'; import 'package:autonomy_flutter/util/string_ext.dart'; import 'package:autonomy_flutter/util/style.dart'; -import 'package:autonomy_flutter/view/artwork_common_widget.dart'; import 'package:autonomy_flutter/view/galery_thumbnail_item.dart'; +import 'package:autonomy_flutter/view/get_started_banner.dart'; import 'package:autonomy_flutter/view/header.dart'; +import 'package:autonomy_flutter/view/predefined_collection/predefined_collection_item.dart'; import 'package:autonomy_flutter/view/search_bar.dart'; +import 'package:autonomy_flutter/view/title_text.dart'; import 'package:collection/collection.dart'; import 'package:crypto/crypto.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -58,11 +63,15 @@ class CollectionProState extends State List _listPredefinedCollectionByMedium = []; List _works = []; late bool _isLoaded; + late bool _showGetStartedBanner = false; + final _configurationService = injector(); + static const _maxArtistsView = 30; @override void initState() { WidgetsBinding.instance.addObserver(this); _isLoaded = false; + _showGetStartedBanner = _configurationService.getShowAddAddressBanner(); searchStr = ValueNotifier(''); searchStr.addListener(() { loadCollection(); @@ -156,17 +165,18 @@ class CollectionProState extends State ) .toList() .filterByName(searchStr.value) - ..sort((a, b) => - a.name!.toLowerCase().compareTo(b.name!.toLowerCase())); + ..sort((a, b) => a.name.compareSearchKey(b.name)); setState(() { _listPredefinedCollectionByArtist = listPredefinedCollectionByArtist; }); }, builder: (context, identityState) { - final isEmptyView = _isLoaded && - searchStr.value.isNotEmpty && - _isEmptyCollection(); + final isEmptyView = !_isLoaded || + (_isEmptyCollection() && searchStr.value.isEmpty); + final isSearchEmptyView = _isLoaded && + _isEmptyCollection() && + searchStr.value.isNotEmpty; return CustomScrollView( controller: _scrollController, shrinkWrap: true, @@ -177,17 +187,18 @@ class CollectionProState extends State SliverToBoxAdapter( child: _pageHeader(context), ), - SliverToBoxAdapter( - child: ValueListenableBuilder( - valueListenable: searchStr, - builder: (BuildContext context, String value, - Widget? child) => - CollectionSection( - key: _collectionSectionKey, - filterString: value, + if (!isEmptyView) + SliverToBoxAdapter( + child: ValueListenableBuilder( + valueListenable: searchStr, + builder: (BuildContext context, String value, + Widget? child) => + CollectionSection( + key: _collectionSectionKey, + filterString: value, + ), ), ), - ), if (isEmptyView) ...[ SliverToBoxAdapter( child: Visibility( @@ -199,6 +210,17 @@ class CollectionProState extends State ), ), ), + ] else if (isSearchEmptyView) ...[ + SliverToBoxAdapter( + child: Visibility( + visible: isSearchEmptyView, + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 15), + child: _searchEmptyView(context), + ), + ), + ), ] else ...[ const SliverToBoxAdapter( child: SizedBox(height: 60), @@ -229,8 +251,10 @@ class CollectionProState extends State SliverList( delegate: SliverChildBuilderDelegate( _predefinedCollectionByArtistBuilder, - childCount: - _listPredefinedCollectionByArtist.length + 1, + childCount: min( + _listPredefinedCollectionByArtist.length, + _maxArtistsView) + + 1, ), ), const SliverToBoxAdapter( @@ -261,7 +285,7 @@ class CollectionProState extends State return isEmpty; } - Widget _emptyView(BuildContext context) { + Widget _searchEmptyView(BuildContext context) { final theme = Theme.of(context); return Text( 'no_results'.tr(), @@ -269,9 +293,61 @@ class CollectionProState extends State ); } + Widget _emptyView(BuildContext context) { + final theme = Theme.of(context); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'see_your_collection'.tr(), + style: theme.textTheme.ppMori400White14, + ), + const SizedBox(height: 15), + if (_showGetStartedBanner) + GetStartedBanner( + onClose: () async { + await _hideGetStartedBanner(); + }, + title: 'add_collection_from_address'.tr(), + onGetStarted: _onGetStarted, + ) + ], + ); + } + + Future _hideGetStartedBanner() async { + setState(() { + _showGetStartedBanner = false; + }); + await _configurationService.setShowPostcardBanner(false); + } + + Future _onGetStarted() async { + await Navigator.of(context).pushNamed(AppRouter.walletPage, + arguments: const WalletPagePayload(openAddAddress: true)); + } + Widget _predefinedCollectionByArtistBuilder(BuildContext context, int index) { const type = PredefinedCollectionType.artist; - return _predefinedCollectionBuilder(context, index, type); + final isSearching = searchStr.value.isNotEmpty; + final numberOfArtists = _listPredefinedCollectionByArtist.length; + final displaySeeAll = + !isSearching && index == 0 && numberOfArtists > _maxArtistsView; + final Widget? action = displaySeeAll + ? GestureDetector( + onTap: () async { + await Navigator.of(context).pushNamed(AppRouter.artistsListPage, + arguments: ArtistsListPagePayload( + _listPredefinedCollectionByArtist)); + }, + child: Text( + 'see_all'.tr(), + style: Theme.of(context).textTheme.ppMori400White14.copyWith( + decoration: TextDecoration.underline, + ), + )) + : null; + return _predefinedCollectionBuilder(context, index, type, action: action); } Widget _predefinedCollectionByMediumBuilder(BuildContext context, int index) { @@ -286,11 +362,14 @@ class CollectionProState extends State return Padding( padding: padding, child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ HeaderView(title: 'works'.tr(), padding: EdgeInsets.zero), const SizedBox( height: 30, ), + if (searchStr.value.isNotEmpty && _works.isEmpty) + _searchEmptyView(context), ], ), ); @@ -326,22 +405,25 @@ class CollectionProState extends State Widget _predefinedCollectionBuilder( BuildContext context, int index, - PredefinedCollectionType type, - ) { + PredefinedCollectionType type, { + Widget? action, + }) { final sep = addOnlyDivider(color: AppColor.auGreyBackground); const padding = EdgeInsets.symmetric(horizontal: 15); if (index == 0) { return Padding( padding: padding, child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - _predefinedCollectionHeader( - context, - type, - ), + _predefinedCollectionHeader(context, type, action: action), const SizedBox( height: 30, ), + if (searchStr.value.isNotEmpty && + _listPredefinedCollectionByArtist.isEmpty && + type == PredefinedCollectionType.artist) + _searchEmptyView(context) ], ), ); @@ -351,10 +433,10 @@ class CollectionProState extends State padding: const EdgeInsets.symmetric(horizontal: 15), child: Column( children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 15), - child: _predefinedCollectionitem( - context, predefinedCollection, type, searchStr.value), + PredefinedCollectionItem( + predefinedCollection: predefinedCollection, + type: type, + searchStr: searchStr.value, ), sep, ], @@ -363,97 +445,16 @@ class CollectionProState extends State } } - Widget _predefinedCollectionitem( - BuildContext context, - PredefinedCollectionModel predefinedCollection, - PredefinedCollectionType type, - String searchStr) { - final theme = Theme.of(context); - var title = predefinedCollection.name ?? predefinedCollection.id; - if (predefinedCollection.name == predefinedCollection.id) { - title = title.maskOnly(5); - } - final titleStyle = theme.textTheme.ppMori400White14; - return GestureDetector( - onTap: () async { - await Navigator.pushNamed( - context, - AppRouter.predefinedCollectionPage, - arguments: PredefinedCollectionScreenPayload( - type: type, - predefinedCollection: predefinedCollection, - filterStr: searchStr, - ), - ); - }, - child: Container( - color: Colors.transparent, - child: Row( - children: [ - _predefinedCollectionIcon( - predefinedCollection, - type, - ), - const SizedBox(width: 33), - Expanded( - child: Text( - title, - style: titleStyle, - overflow: TextOverflow.ellipsis, - ), - ), - Text('${predefinedCollection.total}', - style: theme.textTheme.ppMori400Grey14), - ], - ), - ), - ); - } - - Widget _predefinedCollectionIcon( - PredefinedCollectionModel predefinedCollection, - PredefinedCollectionType type) { - switch (type) { - case PredefinedCollectionType.medium: - return Container( - padding: const EdgeInsets.symmetric(horizontal: 10), - width: 44, - height: 44, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: AppColor.auGreyBackground, - ), - child: SvgPicture.asset( - MediumCategoryExt.icon(predefinedCollection.id), - width: 22, - colorFilter: - const ColorFilter.mode(AppColor.white, BlendMode.srcIn), - ), - ); - case PredefinedCollectionType.artist: - final compactedAssetTokens = predefinedCollection.compactedAssetToken; - return SizedBox( - width: 42, - height: 42, - child: tokenGalleryThumbnailWidget(context, compactedAssetTokens, 100, - usingThumbnailID: false, - galleryThumbnailPlaceholder: Container( - width: 42, - height: 42, - color: AppColor.auLightGrey, - )), - ); - } - } - Widget _predefinedCollectionHeader( - BuildContext context, PredefinedCollectionType type) { + BuildContext context, PredefinedCollectionType type, + {Widget? action}) { final title = type == PredefinedCollectionType.medium ? 'medium'.tr() : 'artists'.tr(); return HeaderView( title: title, padding: EdgeInsets.zero, + action: action, ); } @@ -481,36 +482,43 @@ class CollectionProState extends State }, ), ) - : Align( - alignment: Alignment.bottomLeft, - child: ActionBar( - searchBar: AuSearchBar( - onChanged: (text) {}, - onSearch: (text) { - setState(() { - searchStr.value = text; - }); - }, - onClear: (text) { - setState(() { - searchStr.value = text; - }); - }, + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Align( + alignment: Alignment.bottomLeft, + child: ActionBar( + searchBar: AuSearchBar( + onChanged: (text) {}, + onSearch: (text) { + setState(() { + searchStr.value = text; + }); + }, + onClear: (text) { + setState(() { + searchStr.value = text; + }); + }, + ), + onCancel: () { + setState(() { + searchStr.value = ''; + isShowSearchBar = false; + }); + }, + ), ), - onCancel: () { - setState(() { - searchStr.value = ''; - isShowSearchBar = false; - }); - }, - ), + const SizedBox(height: 20), + if (searchStr.value.isEmpty) TitleText(title: 'organize'.tr()), + ], ), ); } Widget _artworkItem(BuildContext context, CompactedAssetToken token) { final theme = Theme.of(context); - final title = token.title ?? ''; + final title = token.displayTitle ?? ''; final artistName = token.artistTitle ?? token.artistID ?? ''; return GestureDetector( onTap: () async { diff --git a/lib/screen/customer_support/support_list_page.dart b/lib/screen/customer_support/support_list_page.dart index 248ef4cc48..483eb7120c 100644 --- a/lib/screen/customer_support/support_list_page.dart +++ b/lib/screen/customer_support/support_list_page.dart @@ -105,7 +105,8 @@ class _SupportListPageState extends State case Issue: final issue = chatThread as Issue; final status = issue.status; - final lastMessage = getLastMessage(issue); + final lastMessage = + _getDisplayMessage(issue, issue.lastMessage); final isRated = (lastMessage.contains(STAR_RATING) || lastMessage.contains(RATING_MESSAGE_START)) && issue.rating > 0; @@ -194,11 +195,38 @@ class _SupportListPageState extends State const SizedBox(height: 17), Padding( padding: const EdgeInsets.only(right: 14), - child: Text( - getPreviewMessage(issue), - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.ppMori400Black14, + child: Row( + children: [ + if (issue.status == 'closed') ...[ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 4, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(64), + border: Border.all( + color: AppColor.auQuickSilver, + ), + ), + child: Text( + 'resolved'.tr(), + style: theme.textTheme.ppMori400FFQuickSilver12, + ), + ), + const SizedBox( + width: 14, + ) + ], + Expanded( + child: Text( + getPreviewMessage(issue), + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.ppMori400Black14, + ), + ), + ], ), ), if (hasDivider) @@ -212,21 +240,14 @@ class _SupportListPageState extends State } String getPreviewMessage(Issue issue) { - final lastMessage = getLastMessage(issue); if (issue.status == 'closed') { - if (lastMessage.contains(RATING_MESSAGE_START)) { - return lastMessage.substring(RATING_MESSAGE_START.length); - } - if (lastMessage.contains(STAR_RATING)) { - return 'care_to_share'.tr(); - } - return 'rate_issue'.tr(); + return _getDisplayMessage(issue, issue.firstMessage); + } else { + return _getDisplayMessage(issue, issue.lastMessage); } - return lastMessage; } - String getLastMessage(Issue issue) { - var lastMessage = issue.lastMessage; + String _getDisplayMessage(Issue issue, Message? message) { if (issue.draft != null) { final draft = issue.draft!; final draftData = draft.draftData; @@ -244,7 +265,7 @@ class _SupportListPageState extends State .toList(); } - lastMessage = Message( + message = Message( id: random.nextInt(100000), read: true, from: 'did:key:user', @@ -254,17 +275,17 @@ class _SupportListPageState extends State ); } - if (lastMessage == null) { + if (message == null) { return ''; } - if (lastMessage.filteredMessage.isNotEmpty) { - return lastMessage.filteredMessage; + if (message.filteredMessage.isNotEmpty) { + return message.filteredMessage; } - if (lastMessage.attachments.isEmpty) { + if (message.attachments.isEmpty) { return ''; } - final attachment = lastMessage.attachments.last; + final attachment = message.attachments.last; final attachmentTitle = ReceiveAttachment.extractSizeAndRealTitle(attachment.title)[1]; if (attachment.contentType.contains('image')) { diff --git a/lib/screen/customer_support/support_thread_page.dart b/lib/screen/customer_support/support_thread_page.dart index 30356ec2b8..3a5790e374 100644 --- a/lib/screen/customer_support/support_thread_page.dart +++ b/lib/screen/customer_support/support_thread_page.dart @@ -18,17 +18,14 @@ import 'package:autonomy_flutter/main.dart'; import 'package:autonomy_flutter/model/customer_support.dart' as app; import 'package:autonomy_flutter/model/customer_support.dart'; import 'package:autonomy_flutter/model/pair.dart'; -import 'package:autonomy_flutter/screen/app_router.dart'; -import 'package:autonomy_flutter/screen/claim/airdrop/claim_airdrop_page.dart'; -import 'package:autonomy_flutter/service/airdrop_service.dart'; import 'package:autonomy_flutter/service/audit_service.dart'; +import 'package:autonomy_flutter/service/auth_service.dart'; import 'package:autonomy_flutter/service/customer_support_service.dart'; import 'package:autonomy_flutter/service/feralfile_service.dart'; -import 'package:autonomy_flutter/util/announcement_ext.dart'; import 'package:autonomy_flutter/util/constants.dart'; import 'package:autonomy_flutter/util/datetime_ext.dart'; +import 'package:autonomy_flutter/util/jwt.dart'; import 'package:autonomy_flutter/util/log.dart' as log_util; -import 'package:autonomy_flutter/util/log.dart'; import 'package:autonomy_flutter/util/string_ext.dart'; import 'package:autonomy_flutter/util/ui_helper.dart'; import 'package:autonomy_flutter/view/back_appbar.dart'; @@ -45,7 +42,6 @@ import 'package:flutter_chat_ui/flutter_chat_ui.dart'; import 'package:flutter_rating_bar/flutter_rating_bar.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:nft_collection/models/asset_token.dart'; import 'package:uuid/uuid.dart'; abstract class SupportThreadPayload { @@ -139,9 +135,10 @@ class _SupportThreadPageState extends State final _askReviewMessengerID = const Uuid().v4(); final _announcementMessengerID = const Uuid().v4(); final _customerSupportService = injector(); - final _airdropService = injector(); final _feralFileService = injector(); + String? _userId; + types.TextMessage get _introMessenger => types.TextMessage( author: _bitmark, id: _introMessengerID, @@ -162,13 +159,6 @@ class _SupportThreadPageState extends State createdAt: DateTime.now().millisecondsSinceEpoch, ); - types.CustomMessage get _askReviewMessenger => types.CustomMessage( - author: _bitmark, - id: _askReviewMessengerID, - metadata: {'status': 'careToShare', 'content': 'care_to_share'.tr()}, - createdAt: DateTime.now().millisecondsSinceEpoch, - ); - types.CustomMessage get _announcementMessenger => types.CustomMessage( id: _announcementMessengerID, author: _bitmark, @@ -177,6 +167,7 @@ class _SupportThreadPageState extends State @override void initState() { + unawaited(_getUserId()); unawaited(_fetchCustomerSupportAvailability()); unawaited(injector().processMessages()); injector() @@ -234,6 +225,16 @@ class _SupportThreadPageState extends State } } + Future _getUserId() async { + if (_userId != null) { + return _userId!; + } + final jwt = await injector().getAuthToken(); + final data = parseJwt(jwt.jwtToken); + _userId = data['sub'] ?? ''; + return _userId!; + } + @override void afterFirstLayout(BuildContext context) { final payload = widget.payload; @@ -316,44 +317,36 @@ class _SupportThreadPageState extends State Widget build(BuildContext context) { List messages = _draftMessages + _messages; ////// this convert rating messages to customMessage type, then convert the string messages to rating bars - for (int i = 0; i < messages.length; i++) { - if (_isRating(messages[i])) { - final ratingMessengerID = const Uuid().v4(); - final ratingMessenger = types.CustomMessage( - id: ratingMessengerID, - author: _user, - metadata: { - 'status': 'rating', - 'rating': messages[i].metadata!['rating'], - }, - ); - messages[i] = ratingMessenger; + if (messages.isNotEmpty) { + for (int i = 0; i < messages.length; i++) { + if (_isRating(messages[i])) { + final ratingMessengerID = const Uuid().v4(); + final ratingMessenger = types.CustomMessage( + id: ratingMessengerID, + author: _user, + metadata: { + 'status': 'rating', + 'rating': messages[i].metadata!['rating'], + }, + ); + messages[i] = ratingMessenger; + } } - } - if (_status == 'closed' || _status == 'clickToReopen') { - final ratingIndex = _firstRatingIndex(messages); - messages - ..insert(ratingIndex + 1, _resolvedMessenger) - ..insert(ratingIndex + 1, _askRatingMessenger); - if (ratingIndex > -1 && _status == 'closed') { - messages.insert(ratingIndex, _askReviewMessenger); - } - } + messages.removeWhere((element) => + messages.indexOf(element) != 0 && _isRatingMessage(element)); - for (int i = 0; i < messages.length; i++) { - if (_isRatingMessage(messages[i])) { - if (messages[i + 1] != _askRatingMessenger) { + if (_status == 'closed' || _status == 'clickToReopen') { + final ratingIndex = + messages.indexWhere((element) => _isRatingMessage(element)); + if (messages[ratingIndex + 1] != _askRatingMessenger) { messages - ..insert(i + 1, _resolvedMessenger) - ..insert(i + 1, _askRatingMessenger); - } - if (i > 0 && _isCustomerSupportMessage(messages[i - 1])) { - messages.insert(i, _askReviewMessenger); - i++; + ..insert(ratingIndex + 1, _resolvedMessenger) + ..insert(ratingIndex + 1, _askRatingMessenger); } } } + if (widget.payload.announcement != null) { messages.add(_announcementMessenger); } else if (_issueID == null || messages.isNotEmpty) { @@ -387,55 +380,18 @@ class _SupportThreadPageState extends State }).toList(), onSendPressed: _handleSendPressed, user: _user, - listBottomWidget: - (widget.payload.announcement?.isMemento6 == true) - ? FutureBuilder( - future: _airdropService - // ignore: discarded_futures - .getTokenByContract(momaMementoContractAddresses), - builder: (context, snapshot) { - final token = snapshot.data as AssetToken?; - return Padding( - padding: const EdgeInsets.only( - left: 18, right: 18, bottom: 15), - child: PrimaryAsyncButton( - text: 'claim_your_gift'.tr(), - enabled: token != null, - onTap: () async { - if (token == null) { - return; - } - try { - final response = await _airdropService - .claimRequestGift(token); - final series = await _feralFileService - .getSeries(response.seriesID); - if (!mounted) { - return; - } - unawaited(Navigator.of(context).pushNamed( - AppRouter.claimAirdropPage, - arguments: ClaimAirdropPagePayload( - claimID: response.claimID, - series: series, - shareCode: ''))); - } catch (e) { - log.info('Claim your gift tap $e'); - } - }, - ), - ); - }) - : null, customBottomWidget: !isCustomerSupportAvailable ? const SizedBox() - : !_isRated && _status == 'closed' - ? MyRatingBar( - submit: (String messageType, - DraftCustomerSupportData data, - {bool isRating = false}) => - // ignore: discarded_futures - _submit(messageType, data, isRating: isRating)) + : _status == 'closed' + ? _isRated + ? const SizedBox() + : MyRatingBar( + submit: (String messageType, + DraftCustomerSupportData data, + {bool isRating = false}) => + // ignore: discarded_futures + _submit(messageType, data, + isRating: isRating)) : Column( children: [ if (_isFileAttached) debugLogView(), @@ -523,25 +479,6 @@ class _SupportThreadPageState extends State return false; } - bool _isCustomerSupportMessage(types.Message message) { - if (message is types.TextMessage) { - return message.text.contains(RATING_MESSAGE_START); - } - return false; - } - - int _firstRatingIndex(List messages) { - for (int i = 0; i < messages.length; i++) { - if (_isRatingMessage(messages[i])) { - return i; - } - if (!_isCustomerSupportMessage(messages[i])) { - return -1; - } - } - return -1; - } - Widget _ratingBar(int rating) { if (rating == 0) { return const SizedBox(); @@ -703,7 +640,7 @@ class _SupportThreadPageState extends State const SizedBox(height: 20), TextButton( onPressed: () { - if (_status == 'close') { + if (_status == 'closed') { setState(() { _status = 'clickToReopen'; }); @@ -779,7 +716,7 @@ class _SupportThreadPageState extends State return; } final issueDetails = await _customerSupportService.getDetails(_issueID!); - + await _getUserId(); final parsedMessages = (await Future.wait( issueDetails.messages.map((e) => _convertChatMessage(e, null)))) .expand((i) => i) @@ -1031,7 +968,9 @@ class _SupportThreadPageState extends State Map metadata = {}; if (message is app.Message) { id = tempID ?? '${message.id}'; - author = message.from.contains('did:key') ? _user : _bitmark; + author = (message.from == _userId || message.from.contains('did:key')) + ? _user + : _bitmark; status = types.Status.delivered; createdAt = message.timestamp; text = message.filteredMessage; diff --git a/lib/screen/detail/artwork_detail_bloc.dart b/lib/screen/detail/artwork_detail_bloc.dart index 2b5212e242..a4b581b69e 100644 --- a/lib/screen/detail/artwork_detail_bloc.dart +++ b/lib/screen/detail/artwork_detail_bloc.dart @@ -9,7 +9,6 @@ import 'dart:async'; import 'package:autonomy_flutter/au_bloc.dart'; import 'package:autonomy_flutter/screen/detail/artwork_detail_state.dart'; -import 'package:autonomy_flutter/service/airdrop_service.dart'; import 'package:autonomy_flutter/util/asset_token_ext.dart'; import 'package:autonomy_flutter/util/log.dart'; import 'package:http/http.dart' as http; @@ -24,7 +23,6 @@ class ArtworkDetailBloc extends AuBloc { final ProvenanceDao _provenanceDao; final IndexerService _indexerService; final TokenDao _tokenDao; - final AirdropService _airdropService; final IndexerApi _indexerApi; ArtworkDetailBloc( @@ -33,7 +31,6 @@ class ArtworkDetailBloc extends AuBloc { this._provenanceDao, this._indexerService, this._tokenDao, - this._airdropService, this._indexerApi, ) : super(ArtworkDetailState(provenances: [])) { on((event, emit) async { @@ -92,21 +89,8 @@ class ArtworkDetailBloc extends AuBloc { } } } - if (assetToken != null && assetToken.isAirdropToken) { - add(ArtworkDetailGetAirdropDeeplink(assetToken: state.assetToken!)); - } await _indexHistory(event.identity.id); }); - on((event, emit) async { - String deeplink = ''; - try { - deeplink = await _airdropService.shareAirdrop(event.assetToken) ?? ''; - } catch (error) { - log.info('ArtworkDetailGetAirdropDeeplink: share airdrop error', - error.toString()); - } - emit(state.copyWith(airdropDeeplink: deeplink)); - }); } Future _indexHistory(String tokenId) async { diff --git a/lib/screen/detail/artwork_detail_page.dart b/lib/screen/detail/artwork_detail_page.dart index bca6f064d0..6198e03e9f 100644 --- a/lib/screen/detail/artwork_detail_page.dart +++ b/lib/screen/detail/artwork_detail_page.dart @@ -11,40 +11,47 @@ import 'dart:collection'; import 'package:after_layout/after_layout.dart'; import 'package:autonomy_flutter/common/environment.dart'; import 'package:autonomy_flutter/common/injector.dart'; -import 'package:autonomy_flutter/model/play_control_model.dart'; +import 'package:autonomy_flutter/main.dart'; +import 'package:autonomy_flutter/model/play_list_model.dart'; import 'package:autonomy_flutter/model/sent_artwork.dart'; import 'package:autonomy_flutter/screen/app_router.dart'; import 'package:autonomy_flutter/screen/bloc/accounts/accounts_bloc.dart'; import 'package:autonomy_flutter/screen/bloc/identity/identity_bloc.dart'; import 'package:autonomy_flutter/screen/detail/artwork_detail_bloc.dart'; import 'package:autonomy_flutter/screen/detail/artwork_detail_state.dart'; +import 'package:autonomy_flutter/screen/detail/preview/canvas_device_bloc.dart'; +import 'package:autonomy_flutter/screen/detail/preview/keyboard_control_page.dart'; import 'package:autonomy_flutter/screen/detail/preview_detail/preview_detail_widget.dart'; import 'package:autonomy_flutter/screen/gallery/gallery_page.dart'; import 'package:autonomy_flutter/screen/irl_screen/webview_irl_screen.dart'; import 'package:autonomy_flutter/screen/settings/crypto/send_artwork/send_artwork_page.dart'; -import 'package:autonomy_flutter/service/airdrop_service.dart'; import 'package:autonomy_flutter/service/configuration_service.dart'; import 'package:autonomy_flutter/service/feralfile_service.dart'; import 'package:autonomy_flutter/service/settings_data_service.dart'; import 'package:autonomy_flutter/util/asset_token_ext.dart'; import 'package:autonomy_flutter/util/au_icons.dart'; import 'package:autonomy_flutter/util/constants.dart'; +import 'package:autonomy_flutter/util/feral_file_custom_tab.dart'; import 'package:autonomy_flutter/util/file_helper.dart'; import 'package:autonomy_flutter/util/log.dart'; +import 'package:autonomy_flutter/util/playlist_ext.dart'; import 'package:autonomy_flutter/util/string_ext.dart'; import 'package:autonomy_flutter/util/style.dart'; import 'package:autonomy_flutter/util/ui_helper.dart'; import 'package:autonomy_flutter/util/wallet_storage_ext.dart'; import 'package:autonomy_flutter/view/artwork_common_widget.dart'; import 'package:autonomy_flutter/view/back_appbar.dart'; -import 'package:autonomy_flutter/view/external_link.dart'; -import 'package:autonomy_flutter/view/postcard_button.dart'; +import 'package:autonomy_flutter/view/cast_button.dart'; import 'package:autonomy_flutter/view/primary_button.dart'; import 'package:autonomy_flutter/view/responsive.dart'; +import 'package:autonomy_flutter/view/stream_common_widget.dart'; +import 'package:backdrop/backdrop.dart'; import 'package:dio/dio.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:feralfile_app_theme/feral_file_app_theme.dart'; +import 'package:feralfile_app_tv_proto/feralfile_app_tv_proto.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; @@ -52,9 +59,11 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:nft_collection/models/asset_token.dart'; import 'package:nft_collection/models/provenance.dart'; import 'package:nft_collection/nft_collection.dart'; -import 'package:share_plus/share_plus.dart'; +import 'package:nft_collection/services/tokens_service.dart'; +import 'package:shake/shake.dart'; import 'package:social_share/social_share.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; part 'artwork_detail_page.g.dart'; @@ -68,21 +77,43 @@ class ArtworkDetailPage extends StatefulWidget { } class _ArtworkDetailPageState extends State - with AfterLayoutMixin { - late ScrollController _scrollController; + with + AfterLayoutMixin, + RouteAware, + SingleTickerProviderStateMixin, + WidgetsBindingObserver { + ScrollController? _scrollController; late bool withSharing; ValueNotifier downloadProgress = ValueNotifier(0); HashSet _accountNumberHash = HashSet.identity(); AssetToken? currentAsset; - final _airdropService = injector.get(); final _feralfileService = injector.get(); + final _focusNode = FocusNode(); + bool _isInfoExpand = false; + static const _infoShrinkPosition = 0.001; + static const _infoExpandPosition = 0.29; + late ArtworkDetailBloc _bloc; + late CanvasDeviceBloc _canvasDeviceBloc; + late AnimationController _animationController; + double? _appBarBottomDy; + bool _isFullScreen = false; + ShakeDetector? _detector; @override void initState() { - _scrollController = ScrollController(); super.initState(); - context.read().add(ArtworkDetailGetInfoEvent( + _animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 300), + reverseDuration: const Duration(milliseconds: 300), + value: _infoShrinkPosition, + upperBound: _infoExpandPosition, + ); + _infoShrink(); + _bloc = context.read(); + _canvasDeviceBloc = injector.get(); + _bloc.add(ArtworkDetailGetInfoEvent( widget.payload.identities[widget.payload.currentIndex], useIndexer: widget.payload.useIndexer)); context.read().add(FetchAllAddressesEvent()); @@ -91,7 +122,22 @@ class _ArtworkDetailPageState extends State } @override - void afterFirstLayout(BuildContext context) {} + void afterFirstLayout(BuildContext context) { + WidgetsBinding.instance.addObserver(this); + _appBarBottomDy ??= MediaQuery.of(context).padding.top + kToolbarHeight; + _detector = ShakeDetector.autoStart( + onPhoneShake: () async { + await _exitFullScreen(); + }, + ); + _detector?.startListening(); + } + + @override + void didChangeDependencies() { + routeObserver.subscribe(this, ModalRoute.of(context)!); + super.didChangeDependencies(); + } Future _manualShare( String caption, String url, List hashTags) async { @@ -166,55 +212,41 @@ class _ArtworkDetailPageState extends State return UIHelper.showDialog(context, 'share_the_new'.tr(), content); } - Future _shareMemento(BuildContext context, AssetToken asset) async { - final deeplink = await _airdropService.shareAirdrop(asset); - if (deeplink == null) { - if (!context.mounted) { - return; - } - context - .read() - .add(ArtworkDetailGetAirdropDeeplink(assetToken: asset)); - unawaited(UIHelper.showAirdropCannotShare(context)); - return; - } - try { - final shareMessage = 'memento_6_share_message'.tr(namedArgs: { - 'deeplink': deeplink, - }); - await Share.share(shareMessage); - } catch (e) { - if (e is DioException) { - if (context.mounted) { - unawaited(UIHelper.showSharePostcardFailed(context, e)); - } - } - } + @override + void dispose() { + _scrollController?.dispose(); + _animationController.dispose(); + _focusNode.dispose(); + unawaited(disableLandscapeMode()); + unawaited(WakelockPlus.disable()); + _detector?.stopListening(); + routeObserver.unsubscribe(this); + WidgetsBinding.instance.removeObserver(this); + unawaited(SystemChrome.setEnabledSystemUIMode( + SystemUiMode.manual, + overlays: SystemUiOverlay.values, + )); + super.dispose(); } - Widget _sendMemento6(BuildContext context, AssetToken asset) { - final deeplink = context.watch().state.airdropDeeplink; - final canSend = deeplink != null && deeplink.isNotEmpty; - if (!canSend) { - return const SizedBox(); - } - return PostcardButton( - text: 'send_memento'.tr(), - onTap: () { - unawaited(_shareMemento(context, asset)); - }, - ); + void _infoShrink() { + setState(() { + _isInfoExpand = false; + }); + _animationController.animateTo(_infoShrinkPosition); } - @override - void dispose() { - _scrollController.dispose(); - super.dispose(); + void _infoExpand() { + _scrollController?.jumpTo(0); + _scrollController ??= ScrollController(); + setState(() { + _isInfoExpand = true; + }); + _animationController.animateTo(_infoExpandPosition); } @override Widget build(BuildContext context) { - final theme = Theme.of(context); final hasKeyboard = currentAsset?.medium == 'software' || currentAsset?.medium == 'other' || currentAsset?.medium == null; @@ -251,24 +283,190 @@ class _ArtworkDetailPageState extends State final artistName = asset.artistName?.toIdentityOrMask(identityState.identityMap); - var subTitle = ''; - if (artistName != null && artistName.isNotEmpty) { - subTitle = artistName; - } - - final editionSubTitle = getEditionSubTitle(asset); - - return Scaffold( - backgroundColor: theme.colorScheme.primary, - resizeToAvoidBottomInset: !hasKeyboard, - appBar: AppBar( - systemOverlayStyle: systemUiOverlayDarkStyle, - leadingWidth: 0, - leading: const SizedBox(), - centerTitle: false, - backgroundColor: Colors.transparent, - title: ArtworkDetailsHeader( - title: asset.title ?? '', + return BlocBuilder( + bloc: _canvasDeviceBloc, + builder: (context, canvasState) => Stack( + children: [ + BackdropScaffold( + backgroundColor: AppColor.primaryBlack, + resizeToAvoidBottomInset: !hasKeyboard, + appBar: _isFullScreen + ? null + : PreferredSize( + preferredSize: const Size.fromHeight(kToolbarHeight), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 14), + child: AppBar( + systemOverlayStyle: systemUiOverlayDarkStyle, + leadingWidth: 44, + leading: Semantics( + label: 'BACK', + child: IconButton( + onPressed: () => Navigator.pop(context), + constraints: const BoxConstraints( + maxWidth: 34, + maxHeight: 34, + ), + icon: SvgPicture.asset( + 'assets/images/ff_back_dark.svg', + ), + padding: const EdgeInsets.all(0), + ), + ), + centerTitle: false, + backgroundColor: Colors.transparent, + actions: [ + FFCastButton( + displayKey: _getDisplayKey(asset), + onDeviceSelected: (device) { + if (widget.payload.playlist == null) { + final artwork = PlayArtworkV2( + token: CastAssetToken(id: asset.id), + duration: 0, + ); + _canvasDeviceBloc.add( + CanvasDeviceCastListArtworkEvent( + device, [artwork])); + } else { + final playlist = widget.payload.playlist!; + final listTokenIds = playlist.tokenIDs; + if (listTokenIds == null) { + log.info('Playlist tokenIds is null'); + return; + } + + final duration = + speedValues.values.first.inMilliseconds; + final listPlayArtwork = listTokenIds + .rotateListByItem(asset.id) + .map((e) => PlayArtworkV2( + token: CastAssetToken(id: e), + duration: duration)) + .toList(); + _canvasDeviceBloc.add( + CanvasDeviceChangeControlDeviceEvent( + device, listPlayArtwork)); + } + }, + ), + ], + ), + ), + ), + backLayer: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: ArtworkPreviewWidget( + focusNode: _focusNode, + useIndexer: widget.payload.useIndexer, + identity: widget + .payload.identities[widget.payload.currentIndex], + ), + ), + if (!_isFullScreen) + const Padding( + padding: EdgeInsets.symmetric(vertical: 18), + child: ArtworkDetailsHeader( + title: 'I', + subTitle: 'I', + color: Colors.transparent, + ), + ), + ], + ), + reverseAnimationCurve: Curves.ease, + frontLayer: _isFullScreen + ? const SizedBox() + : _infoContent(context, identityState, state, artistName), + frontLayerBackgroundColor: + _isFullScreen ? Colors.transparent : AppColor.primaryBlack, + backLayerBackgroundColor: AppColor.primaryBlack, + animationController: _animationController, + revealBackLayerAtStart: true, + frontLayerScrim: Colors.transparent, + backLayerScrim: Colors.transparent, + subHeaderAlwaysActive: false, + frontLayerShape: const BeveledRectangleBorder(), + subHeader: _isFullScreen + ? null + : DecoratedBox( + decoration: + const BoxDecoration(color: AppColor.primaryBlack), + child: GestureDetector( + onVerticalDragEnd: (details) { + final dy = details.primaryVelocity ?? 0; + if (dy <= 0) { + _infoExpand(); + } else { + _infoShrink(); + } + }, + child: _infoHeader(context, asset, artistName, + state.isViewOnly, canvasState), + ), + ), + ), + if (_isInfoExpand && !_isFullScreen) + Positioned( + top: _appBarBottomDy ?? 80, + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: _infoShrink, + child: Container( + color: Colors.transparent, + height: MediaQuery.of(context).size.height / 2 - + (_appBarBottomDy ?? 80), + width: MediaQuery.of(context).size.width, + ), + ), + ), + ], + ), + ); + } else { + return const SizedBox(); + } + }); + } + + String _getDisplayKey(AssetToken asset) { + final playlistDisplayKey = widget.payload.playlist?.displayKey; + if (playlistDisplayKey != null) { + return playlistDisplayKey; + } + return asset.id.hashCode.toString(); + } + + Widget _artworkInfoIcon() => Semantics( + label: 'artworkInfoIcon', + child: GestureDetector( + onTap: () { + _isInfoExpand ? _infoShrink() : _infoExpand(); + }, + child: SvgPicture.asset( + !_isInfoExpand + ? 'assets/images/info_white.svg' + : 'assets/images/info_white_active.svg', + width: 22, + height: 22, + ), + ), + ); + + Widget _infoHeader(BuildContext context, AssetToken asset, String? artistName, + bool isViewOnly, CanvasDeviceState canvasState) { + var subTitle = ''; + if (artistName != null && artistName.isNotEmpty) { + subTitle = artistName; + } + return Padding( + padding: const EdgeInsets.fromLTRB(15, 15, 15, 20), + child: Row( + children: [ + Expanded( + child: ArtworkDetailsHeader( + title: asset.displayTitle ?? '', subTitle: subTitle, onSubTitleTap: asset.artistID != null ? () => unawaited( @@ -280,137 +478,105 @@ class _ArtworkDetailPageState extends State ))) : null, ), - actions: [ - Semantics( - label: 'externalLink', - child: ExternalLink( - link: asset.secondaryMarketURL, - color: AppColor.white, - ), - ), - if (widget.payload.useIndexer) - const SizedBox() - else - Semantics( - label: 'artworkDotIcon', - child: IconButton( - onPressed: () => unawaited(_showArtworkOptionsDialog( - context, asset, state.isViewOnly)), - constraints: const BoxConstraints( - maxWidth: 44, - maxHeight: 44, - ), - icon: SvgPicture.asset( - 'assets/images/more_circle.svg', - width: 22, - ), - ), - ), - Semantics( - label: 'close_icon', - child: IconButton( - onPressed: () => Navigator.pop(context), - constraints: const BoxConstraints( - maxWidth: 44, - maxHeight: 44, - ), - icon: Icon( - AuIcon.close, - color: theme.colorScheme.secondary, - size: 20, - ), - ), - ) - ], ), - body: SingleChildScrollView( - controller: _scrollController, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 40, - ), - Hero( - tag: 'detail_${asset.id}', - child: _ArtworkView( - payload: widget.payload, - token: asset, + _artworkInfoIcon(), + if (!widget.payload.useIndexer) + Semantics( + label: 'artworkDotIcon', + child: Padding( + padding: const EdgeInsets.only(left: 20), + child: GestureDetector( + onTap: () async => _showArtworkOptionsDialog( + context, asset, isViewOnly, canvasState), + child: SvgPicture.asset( + 'assets/images/more_circle.svg', + width: 22, ), ), - _sendMemento6(context, asset), - Visibility( - visible: - checkWeb3ContractAddress.contains(asset.contractAddress), - child: Align( - alignment: Alignment.centerRight, - child: Padding( - padding: - const EdgeInsets.only(left: 16, right: 16, top: 40), - child: OutlineButton( - color: Colors.transparent, - text: 'web3_glossary'.tr(), - onTap: () { - unawaited(Navigator.pushNamed( - context, AppRouter.previewPrimerPage, - arguments: asset)); - }, - ), - ), - ), - ), - const SizedBox( - height: 40, + ), + ), + ], + ), + ); + } + + Widget _infoContent(BuildContext context, IdentityState identityState, + ArtworkDetailState state, String? artistName) { + final theme = Theme.of(context); + final asset = state.assetToken!; + final editionSubTitle = getEditionSubTitle(asset); + return SingleChildScrollView( + controller: _scrollController, + child: SizedBox( + width: double.infinity, + child: Column( + children: [ + Visibility( + visible: checkWeb3ContractAddress.contains(asset.contractAddress), + child: Padding( + padding: const EdgeInsets.only(left: 16, right: 16, bottom: 20), + child: OutlineButton( + color: Colors.transparent, + text: 'web3_glossary'.tr(), + onTap: () { + unawaited(Navigator.pushNamed( + context, AppRouter.previewPrimerPage, + arguments: asset)); + }, ), - Visibility( - visible: editionSubTitle.isNotEmpty, - child: Padding( - padding: ResponsiveLayout.getPadding, - child: Text( - editionSubTitle, - style: theme.textTheme.ppMori400Grey14, - ), + ), + ), + Visibility( + visible: editionSubTitle.isNotEmpty, + child: Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: ResponsiveLayout.getPadding, + child: Text( + editionSubTitle, + style: theme.textTheme.ppMori400Grey14, ), ), - debugInfoWidget(context, currentAsset), - const SizedBox(height: 16), - Padding( - padding: ResponsiveLayout.getPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Semantics( - label: 'Desc', - child: HtmlWidget( - customStylesBuilder: auHtmlStyle, - asset.description ?? '', - textStyle: theme.textTheme.ppMori400White14, - ), - ), - const SizedBox(height: 40), - artworkDetailsMetadataSection(context, asset, artistName), - if (asset.fungible) ...[ - tokenOwnership(context, asset, - identityState.identityMap[asset.owner] ?? ''), - ] else ...[ - if (state.provenances.isNotEmpty) - _provenanceView(context, state.provenances) - else - const SizedBox() - ], - artworkDetailsRightSection(context, asset), - const SizedBox(height: 80), - ], + ), + ), + debugInfoWidget(context, currentAsset), + Padding( + padding: ResponsiveLayout.getPadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Semantics( + label: 'Desc', + child: HtmlWidget( + customStylesBuilder: auHtmlStyle, + asset.description ?? '', + textStyle: theme.textTheme.ppMori400White14, + ), ), - ) - ], + const SizedBox(height: 40), + artworkDetailsMetadataSection(context, asset, artistName), + if (asset.fungible) ...[ + tokenOwnership(context, asset, + identityState.identityMap[asset.owner] ?? ''), + ] else ...[ + if (state.provenances.isNotEmpty) + _provenanceView(context, state.provenances) + else + const SizedBox() + ], + artworkDetailsRightSection(context, asset), + const SizedBox(height: 80), + ], + ), ), - ), - ); - } else { - return const SizedBox(); - } - }); + SizedBox( + height: MediaQuery.of(context).size.height * 0.5 - + (_appBarBottomDy ?? 80), + ), + ], + ), + ), + ); } Widget _provenanceView(BuildContext context, List provenances) => @@ -432,38 +598,108 @@ class _ArtworkDetailPageState extends State .getTempStorageHiddenTokenIDs() .contains(token.id); - Future _showArtworkOptionsDialog( - BuildContext context, AssetToken asset, bool isViewOnly) async { + Future _showArtworkOptionsDialog(BuildContext context, AssetToken asset, + bool isViewOnly, CanvasDeviceState canvasDeviceState) async { final owner = await asset.getOwnerWallet(); final ownerWallet = owner?.first; final addressIndex = owner?.second; final irlUrl = asset.irlTapLink; - + final showKeyboard = (asset.medium == 'software' || + asset.medium == 'other' || + (asset.medium?.isEmpty ?? true) || + canvasDeviceState + .lastSelectedActiveDeviceForKey(_getDisplayKey(asset)) != + null) && + !asset.isPostcard; if (!context.mounted) { return; } final isHidden = _isHidden(asset); + _focusNode.unfocus(); unawaited(UIHelper.showDrawerAction( context, options: [ + OptionItem( + title: 'full_screen'.tr(), + icon: SvgPicture.asset('assets/images/fullscreen_icon.svg'), + onTap: () { + Navigator.of(context).pop(); + _setFullScreen(); + }), + if (showKeyboard) + OptionItem( + title: 'interact'.tr(), + icon: SvgPicture.asset('assets/images/keyboard_icon.svg'), + onTap: () { + Navigator.of(context).pop(); + final castingDevice = canvasDeviceState + .lastSelectedActiveDeviceForKey(_getDisplayKey(asset)); + if (castingDevice != null) { + unawaited(Navigator.of(context).pushNamed( + AppRouter.keyboardControlPage, + arguments: KeyboardControlPagePayload( + asset, + [castingDevice], + ), + )); + } else { + FocusScope.of(context).requestFocus(_focusNode); + } + }, + ), if (!isViewOnly && irlUrl != null) OptionItem( title: irlUrl.first, - icon: const Icon(AuIcon.microphone), - onTap: () { - unawaited( - Navigator.pushNamed( - context, - AppRouter.irlWebView, - arguments: IRLWebScreenPayload(irlUrl.second), - ), + icon: const Icon( + AuIcon.microphone, + color: AppColor.white, + ), + onTap: () async { + await Navigator.popAndPushNamed( + context, + AppRouter.irlWebView, + arguments: IRLWebScreenPayload(irlUrl.second), ); }, ), + if (asset.secondaryMarketURL.isNotEmpty) + OptionItem( + title: 'view_on_'.tr(args: [asset.secondaryMarketName]), + icon: SvgPicture.asset( + 'assets/images/external_link_white.svg', + width: 18, + height: 18, + ), + onTap: () async { + final browser = FeralFileBrowser(); + await browser.openUrl(asset.secondaryMarketURL); + }, + ), + OptionItem( + title: isHidden ? 'unhide_aw'.tr() : 'hide_aw'.tr(), + icon: SvgPicture.asset('assets/images/hide_artwork_white.svg'), + onTap: () async { + await injector() + .updateTempStorageHiddenTokenIDs([asset.id], !isHidden); + unawaited(injector().backup()); + + if (!context.mounted) { + return; + } + NftCollectionBloc.eventController.add(ReloadEvent()); + Navigator.of(context).pop(); + unawaited(UIHelper.showHideArtworkResultDialog(context, !isHidden, + onOK: () { + Navigator.of(context).popUntil((route) => + route.settings.name == AppRouter.homePage || + route.settings.name == AppRouter.homePageNoTransition); + })); + }, + ), if (asset.shouldShowDownloadArtwork && !isViewOnly) OptionItem( title: 'download_artwork'.tr(), - icon: SvgPicture.asset('assets/images/download_artwork.svg'), + icon: SvgPicture.asset('assets/images/download_artwork_white.svg'), iconOnDisable: SvgPicture.asset( 'assets/images/download_artwork.svg', colorFilter: const ColorFilter.mode( @@ -521,31 +757,10 @@ class _ArtworkDetailPageState extends State } }, ), - OptionItem( - title: isHidden ? 'unhide_aw'.tr() : 'hide_aw'.tr(), - icon: const Icon(AuIcon.hidden_artwork), - onTap: () async { - await injector() - .updateTempStorageHiddenTokenIDs([asset.id], !isHidden); - unawaited(injector().backup()); - - if (!context.mounted) { - return; - } - NftCollectionBloc.eventController.add(ReloadEvent()); - Navigator.of(context).pop(); - unawaited(UIHelper.showHideArtworkResultDialog(context, !isHidden, - onOK: () { - Navigator.of(context).popUntil((route) => - route.settings.name == AppRouter.homePage || - route.settings.name == AppRouter.homePageNoTransition); - })); - }, - ), if (ownerWallet != null && asset.isTransferable) ...[ OptionItem( title: 'send_artwork'.tr(), - icon: SvgPicture.asset('assets/images/Send.svg'), + icon: SvgPicture.asset('assets/images/send_white.svg'), onTap: () async { final payload = await Navigator.of(context).popAndPushNamed( AppRouter.sendArtworkPage, @@ -595,82 +810,72 @@ class _ArtworkDetailPageState extends State }, ), ], + OptionItem( + title: 'refresh_metadata'.tr(), + icon: SvgPicture.asset( + 'assets/images/refresh_metadata_white.svg', + width: 20, + height: 20, + ), + onTap: () async { + await injector().fetchManualTokens([asset.id]); + if (!context.mounted) { + return; + } + Navigator.of(context).pop(); + await Navigator.of(context).pushReplacementNamed( + AppRouter.artworkDetailsPage, + arguments: widget.payload.copyWith()); + }, + ), + OptionItem.emptyOptionItem, ], )); } -} -class _ArtworkView extends StatelessWidget { - const _ArtworkView({ - required this.payload, - required this.token, - }); - - final ArtworkDetailPayload payload; - final AssetToken token; + Future _setFullScreen() async { + unawaited(_openSnackBar(context)); + if (_isInfoExpand) { + _infoShrink(); + } + await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); + unawaited(WakelockPlus.enable()); + setState(() { + _isFullScreen = true; + }); + } - @override - Widget build(BuildContext context) { - final mimeType = token.getMimeType; - switch (mimeType) { - case 'image': - case 'gif': - case 'audio': - case 'video': - return Stack( - children: [ - AbsorbPointer( - child: Center( - child: IntrinsicHeight( - child: ArtworkPreviewWidget( - identity: payload.identities[payload.currentIndex], - isMute: true, - useIndexer: payload.useIndexer, - ), - ), - ), - ), - Positioned.fill( - child: GestureDetector( - onTap: () { - unawaited(Navigator.of(context).pushNamed( - AppRouter.artworkPreviewPage, - arguments: payload)); - }, - child: Container( - color: Colors.transparent, - ), - ), - ), - ], - ); + Future _exitFullScreen() async { + await SystemChrome.setEnabledSystemUIMode( + SystemUiMode.manual, + overlays: SystemUiOverlay.values, + ); + unawaited(WakelockPlus.disable()); + setState(() { + _isFullScreen = false; + }); + } - default: - return AspectRatio( - aspectRatio: 1, - child: Stack( - children: [ - Center( - child: ArtworkPreviewWidget( - identity: payload.identities[payload.currentIndex], - isMute: true, - useIndexer: payload.useIndexer, - ), - ), - GestureDetector( - onTap: () { - unawaited(Navigator.of(context).pushNamed( - AppRouter.artworkPreviewPage, - arguments: payload)); - }, - child: Container( - color: Colors.transparent, - ), - ), - ], + Future _openSnackBar(BuildContext context) async { + final theme = Theme.of(context); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Container( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 10), + decoration: BoxDecoration( + color: AppColor.feralFileHighlight.withOpacity(0.9), + borderRadius: BorderRadius.circular(64), ), - ); - } + child: Text( + 'shake_exit'.tr(), + textAlign: TextAlign.center, + style: theme.textTheme.ppMori600Black12, + ), + ), + backgroundColor: Colors.transparent, + elevation: 0, + ), + ); } } @@ -678,7 +883,7 @@ class ArtworkDetailPayload { final Key? key; final List identities; final int currentIndex; - final PlayControlModel? playControl; + final PlayListModel? playlist; final String? twitterCaption; final bool useIndexer; // set true when navigate from discover/gallery page @@ -686,7 +891,7 @@ class ArtworkDetailPayload { this.identities, this.currentIndex, { this.twitterCaption, - this.playControl, + this.playlist, this.useIndexer = false, this.key, }); @@ -694,14 +899,14 @@ class ArtworkDetailPayload { ArtworkDetailPayload copyWith( {List? ids, int? currentIndex, - PlayControlModel? playControl, + PlayListModel? playlist, String? twitterCaption, bool? useIndexer}) => ArtworkDetailPayload( ids ?? identities, currentIndex ?? this.currentIndex, twitterCaption: twitterCaption ?? this.twitterCaption, - playControl: playControl ?? this.playControl, + playlist: playlist ?? this.playlist, useIndexer: useIndexer ?? this.useIndexer, ); } diff --git a/lib/screen/detail/artwork_detail_state.dart b/lib/screen/detail/artwork_detail_state.dart index 38d21ef3fe..34b12d8f63 100644 --- a/lib/screen/detail/artwork_detail_state.dart +++ b/lib/screen/detail/artwork_detail_state.dart @@ -18,41 +18,29 @@ class ArtworkDetailGetInfoEvent extends ArtworkDetailEvent { ArtworkDetailGetInfoEvent(this.identity, {this.useIndexer = false}); } -class ArtworkDetailGetAirdropDeeplink extends ArtworkDetailEvent { - final AssetToken assetToken; - - ArtworkDetailGetAirdropDeeplink({required this.assetToken}); -} - class ArtworkDetailState { AssetToken? assetToken; List provenances; Map owners; - String? airdropDeeplink; bool isViewOnly; ArtworkDetailState({ - this.assetToken, required this.provenances, + this.assetToken, this.owners = const {}, - this.airdropDeeplink, this.isViewOnly = true, }); //copyWith - ArtworkDetailState copyWith({ - AssetToken? assetToken, - List? provenances, - Map? owners, - String? airdropDeeplink, - bool? isViewOnly, - }) { - return ArtworkDetailState( - assetToken: assetToken ?? this.assetToken, - provenances: provenances ?? this.provenances, - owners: owners ?? this.owners, - airdropDeeplink: airdropDeeplink ?? this.airdropDeeplink, - isViewOnly: isViewOnly ?? this.isViewOnly, - ); - } + ArtworkDetailState copyWith( + {AssetToken? assetToken, + List? provenances, + Map? owners, + bool? isViewOnly}) => + ArtworkDetailState( + assetToken: assetToken ?? this.assetToken, + provenances: provenances ?? this.provenances, + owners: owners ?? this.owners, + isViewOnly: isViewOnly ?? this.isViewOnly, + ); } diff --git a/lib/screen/detail/preview/artwork_preview_bloc.dart b/lib/screen/detail/preview/artwork_preview_bloc.dart index 1ddfe49a5e..a5b3b63198 100644 --- a/lib/screen/detail/preview/artwork_preview_bloc.dart +++ b/lib/screen/detail/preview/artwork_preview_bloc.dart @@ -68,12 +68,5 @@ class ArtworkPreviewBloc // ignore this error } }); - - on((event, emit) async { - if (state is ArtworkPreviewLoadedState) { - final currentState = state as ArtworkPreviewLoadedState; - emit(currentState.copyWith(isFullScreen: event.isFullscreen)); - } - }); } } diff --git a/lib/screen/detail/preview/artwork_preview_page.dart b/lib/screen/detail/preview/artwork_preview_page.dart index 66e04d953b..ad837a5123 100644 --- a/lib/screen/detail/preview/artwork_preview_page.dart +++ b/lib/screen/detail/preview/artwork_preview_page.dart @@ -11,24 +11,13 @@ import 'dart:io'; import 'package:after_layout/after_layout.dart'; import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/main.dart'; -import 'package:autonomy_flutter/screen/app_router.dart'; -import 'package:autonomy_flutter/screen/bloc/identity/identity_bloc.dart'; import 'package:autonomy_flutter/screen/detail/artwork_detail_page.dart'; import 'package:autonomy_flutter/screen/detail/preview/artwork_preview_bloc.dart'; import 'package:autonomy_flutter/screen/detail/preview/artwork_preview_state.dart'; -import 'package:autonomy_flutter/screen/detail/preview/canvas_device_bloc.dart'; -import 'package:autonomy_flutter/screen/detail/preview/keyboard_control_page.dart'; import 'package:autonomy_flutter/screen/detail/preview_detail/preview_detail_widget.dart'; import 'package:autonomy_flutter/service/metric_client_service.dart'; -import 'package:autonomy_flutter/util/asset_token_ext.dart'; -import 'package:autonomy_flutter/util/au_icons.dart'; import 'package:autonomy_flutter/util/string_ext.dart'; import 'package:autonomy_flutter/util/style.dart'; -import 'package:autonomy_flutter/util/ui_helper.dart'; -import 'package:autonomy_flutter/view/artwork_common_widget.dart'; -import 'package:autonomy_flutter/view/back_appbar.dart'; -import 'package:autonomy_flutter/view/canvas_device_view.dart'; -import 'package:autonomy_flutter/view/cast_button.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:feralfile_app_theme/feral_file_app_theme.dart'; import 'package:flutter/material.dart'; @@ -36,7 +25,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:nft_collection/models/asset_token.dart'; import 'package:nft_rendering/nft_rendering.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:shake/shake.dart'; @@ -58,32 +46,30 @@ class _ArtworkPreviewPageState extends State WidgetsBindingObserver { late PageController controller; late ArtworkPreviewBloc _bloc; - late CanvasDeviceBloc _canvasDeviceBloc; ShakeDetector? _detector; - final keyboardManagerKey = GlobalKey(); final _focusNode = FocusNode(); INFTRenderingWidget? _renderingWidget; - List tokens = []; + List _tokens = []; late int initialPage; final metricClient = injector.get(); @override void initState() { - tokens = List.from(widget.payload.identities); - final initialTokenID = tokens[widget.payload.currentIndex]; - initialPage = tokens.indexOf(initialTokenID); + super.initState(); + _tokens = List.from(widget.payload.identities); + final initialTokenID = _tokens[widget.payload.currentIndex]; + initialPage = _tokens.indexOf(initialTokenID); controller = PageController(initialPage: initialPage); _bloc = context.read(); - _canvasDeviceBloc = context.read(); - final currentIdentity = tokens[initialPage]; + final currentIdentity = _tokens[initialPage]; _bloc.add(ArtworkPreviewGetAssetTokenEvent(currentIdentity, useIndexer: widget.payload.useIndexer)); - super.initState(); + unawaited(_setFullScreen()); } @override @@ -122,14 +108,11 @@ class _ArtworkPreviewPageState extends State @override void afterFirstLayout(BuildContext context) { + unawaited(_openSnackBar(context)); // Calling the same function "after layout" to resolve the issue. _detector = ShakeDetector.autoStart( - onPhoneShake: () { - _bloc.add(ChangeFullScreen()); - unawaited(SystemChrome.setEnabledSystemUIMode( - SystemUiMode.manual, - overlays: SystemUiOverlay.values, - )); + onPhoneShake: () async { + Navigator.of(context).pop(); }, ); @@ -138,38 +121,12 @@ class _ArtworkPreviewPageState extends State WidgetsBinding.instance.addObserver(this); } - Future _moveToInfo(AssetToken? assetToken) async { - if (assetToken == null) { - return; - } - keyboardManagerKey.currentState?.hideKeyboard(); - - final currentIndex = tokens.indexWhere((element) => - element.id == assetToken.id && element.owner == assetToken.owner); - if (currentIndex == initialPage) { - Navigator.of(context).pop(); - return; - } - - unawaited(disableLandscapeMode()); - - unawaited(WakelockPlus.disable()); - - unawaited(Navigator.of(context).pushNamed( - AppRouter.artworkDetailsPage, - arguments: widget.payload.copyWith( - currentIndex: currentIndex, - ids: tokens, - ), - )); + Future _setFullScreen() async { + await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); } - void onClickFullScreen(AssetToken? assetToken) { + Future _openSnackBar(BuildContext context) async { final theme = Theme.of(context); - _bloc.add(ChangeFullScreen(isFullscreen: true)); - unawaited( - SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky)); - ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Container( @@ -190,233 +147,61 @@ class _ArtworkPreviewPageState extends State ); } - Future _onCastTap(AssetToken? assetToken) async { - keyboardManagerKey.currentState?.hideKeyboard(); - await UIHelper.showFlexibleDialog( - context, - BlocProvider.value( - value: _canvasDeviceBloc, - child: CanvasDeviceView( - sceneId: assetToken?.id ?? '', - onClose: () { - Navigator.of(context).pop(); - }, - ), - ), - isDismissible: true, - ); - unawaited(_fetchDevice(assetToken?.id ?? '')); - } - @override Widget build(BuildContext context) { final theme = Theme.of(context); - return BlocConsumer( - builder: (context, state) { - AssetToken? assetToken; - bool isFullScreen = false; - if (state is ArtworkPreviewLoadedState) { - assetToken = state.assetToken; - isFullScreen = state.isFullScreen; - } - final hasKeyboard = assetToken?.medium == 'software' || - assetToken?.medium == 'other' || - assetToken?.medium == null; - final hideArtist = assetToken?.isPostcard ?? false; - final identityState = context.watch().state; - final artistName = - assetToken?.artistName?.toIdentityOrMask(identityState.identityMap); - unawaited(_fetchDevice(assetToken?.id ?? '')); - var subTitle = ''; - if (artistName != null && artistName.isNotEmpty) { - subTitle = artistName; - } - return Scaffold( - appBar: isFullScreen - ? null - : AppBar( - systemOverlayStyle: systemUiOverlayDarkStyle, - backgroundColor: theme.colorScheme.primary, - leadingWidth: 0, - centerTitle: false, - automaticallyImplyLeading: false, - title: GestureDetector( - onTap: () async => _moveToInfo(assetToken), - child: ArtworkDetailsHeader( - title: assetToken?.title ?? '', - subTitle: subTitle, - hideArtist: hideArtist, - )), - actions: [ - IconButton( - onPressed: () => Navigator.pop(context), - constraints: const BoxConstraints( - maxWidth: 44, - maxHeight: 44, - ), - icon: Icon( - AuIcon.close, - color: theme.colorScheme.secondary, - size: 20, - ), - tooltip: 'close_icon', - ) - ], - ), + return BlocBuilder( + builder: (context, states) => PopScope( + onPopInvoked: (_) async { + await SystemChrome.setEnabledSystemUIMode( + SystemUiMode.manual, + overlays: SystemUiOverlay.values, + ); + }, + child: Scaffold( backgroundColor: theme.colorScheme.primary, - resizeToAvoidBottomInset: !hasKeyboard, body: SafeArea( top: false, bottom: false, - left: !isFullScreen, - right: !isFullScreen, + left: false, + right: false, child: Column( children: [ Expanded( child: PageView.builder( physics: const NeverScrollableScrollPhysics(), onPageChanged: (value) { - final currentId = tokens[value]; + final currentId = _tokens[value]; _bloc.add(ArtworkPreviewGetAssetTokenEvent(currentId, useIndexer: widget.payload.useIndexer)); - keyboardManagerKey.currentState?.hideKeyboard(); }, controller: controller, - itemCount: tokens.length, - itemBuilder: (context, index) => - BlocBuilder( - bloc: _canvasDeviceBloc, - builder: (context, state) { - final isCasting = state.isCasting; - if (isCasting) { - return const Center( - child: CurrentlyCastingArtwork(), - ); - } - if (tokens[index].id.isPostcardId) { - return PostcardPreviewWidget( - identity: tokens[index], - useIndexer: widget.payload.useIndexer, - ); - } - return ArtworkPreviewWidget( - identity: tokens[index], - onLoaded: ( - {InAppWebViewController? webViewController, - int? time}) {}, - focusNode: _focusNode, + itemCount: _tokens.length, + itemBuilder: (context, index) { + if (_tokens[index].id.isPostcardId) { + return PostcardPreviewWidget( + identity: _tokens[index], useIndexer: widget.payload.useIndexer, ); - }, - ), + } + return ArtworkPreviewWidget( + identity: _tokens[index], + onLoaded: ( + {InAppWebViewController? webViewController, + int? time}) {}, + focusNode: _focusNode, + useIndexer: widget.payload.useIndexer, + ); + }, ), ), - Visibility( - visible: !isFullScreen, - child: BlocBuilder( - bloc: _canvasDeviceBloc, - builder: (context, state) { - final isCasting = state.isCasting; - final playingDevice = state.playingDevice; - return Container( - color: theme.colorScheme.primary, - child: Padding( - padding: const EdgeInsets.only( - top: 15, - bottom: 30, - right: 20, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - const SizedBox( - width: 20, - ), - Visibility( - visible: (assetToken?.medium == 'software' || - assetToken?.medium == 'other' || - (assetToken?.medium?.isEmpty ?? - true) || - isCasting) && - assetToken?.isPostcard != true, - child: KeyboardManagerWidget( - key: keyboardManagerKey, - focusNode: _focusNode, - onTap: isCasting - ? () { - unawaited( - Navigator.of(context).pushNamed( - AppRouter.keyboardControlPage, - arguments: - KeyboardControlPagePayload( - assetToken!, - playingDevice, - ), - ), - ); - } - : () { - FocusScope.of(context) - .requestFocus(_focusNode); - }, - ), - ), - const SizedBox( - width: 20, - ), - CastButton( - onCastTap: () async => _onCastTap(assetToken), - isCasting: isCasting, - ), - const SizedBox( - width: 20, - ), - GestureDetector( - onTap: isCasting - ? null - : () => onClickFullScreen(assetToken), - child: Semantics( - label: 'fullscreen_icon', - child: SvgPicture.asset( - 'assets/images/fullscreen_icon.svg', - colorFilter: ColorFilter.mode( - isCasting - ? AppColor.disabledColor - : AppColor.white, - BlendMode.srcIn), - ), - ), - ), - ], - ), - ), - ); - }), - ), ], ), ), - ); - }, - listener: (context, state) { - AssetToken? assetToken; - if (state is ArtworkPreviewLoadedState) { - assetToken = state.assetToken; - } - if (assetToken != null) { - unawaited(assetToken.sendViewArtworkEvent()); - } - final identitiesList = [ - assetToken?.artistName ?? '', - ]; - context.read().add(GetIdentityEvent(identitiesList)); - }, + ), + ), ); } - - Future _fetchDevice(String tokenID) async { - _canvasDeviceBloc.add(CanvasDeviceGetDevicesEvent(tokenID, syncAll: false)); - } } class KeyboardManagerWidget extends StatefulWidget { diff --git a/lib/screen/detail/preview/artwork_preview_state.dart b/lib/screen/detail/preview/artwork_preview_state.dart index 4f10d74797..69966796ce 100644 --- a/lib/screen/detail/preview/artwork_preview_state.dart +++ b/lib/screen/detail/preview/artwork_preview_state.dart @@ -21,26 +21,15 @@ abstract class ArtworkPreviewState { ArtworkPreviewState(); } -class ChangeFullScreen extends ArtworkPreviewEvent { - bool isFullscreen; - - ChangeFullScreen({this.isFullscreen = false}); -} - class ArtworkPreviewLoadingState extends ArtworkPreviewState { ArtworkPreviewLoadingState(); } class ArtworkPreviewLoadedState extends ArtworkPreviewState { AssetToken? assetToken; - bool isFullScreen; - ArtworkPreviewLoadedState({this.assetToken, this.isFullScreen = false}); + ArtworkPreviewLoadedState({this.assetToken}); - ArtworkPreviewLoadedState copyWith( - {AssetToken? assetToken, bool? isFullScreen}) { - return ArtworkPreviewLoadedState( - assetToken: assetToken ?? this.assetToken, - isFullScreen: isFullScreen ?? this.isFullScreen); - } + ArtworkPreviewLoadedState copyWith({AssetToken? assetToken}) => + ArtworkPreviewLoadedState(assetToken: assetToken ?? this.assetToken); } diff --git a/lib/screen/detail/preview/canvas_device_bloc.dart b/lib/screen/detail/preview/canvas_device_bloc.dart index 628b993c5c..e86a66e56e 100644 --- a/lib/screen/detail/preview/canvas_device_bloc.dart +++ b/lib/screen/detail/preview/canvas_device_bloc.dart @@ -1,78 +1,151 @@ +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// Copyright © 2022 Bitmark. All rights reserved. +// Use of this source code is governed by the BSD-2-Clause Plus Patent License +// that can be found in the LICENSE file. +// + +import 'dart:async'; + import 'package:autonomy_flutter/au_bloc.dart'; -import 'package:autonomy_flutter/model/play_list_model.dart'; -import 'package:autonomy_flutter/service/canvas_client_service.dart'; +import 'package:autonomy_flutter/service/canvas_client_service_v2.dart'; +import 'package:autonomy_flutter/util/cast_request_ext.dart'; +import 'package:autonomy_flutter/util/constants.dart'; +import 'package:autonomy_flutter/util/device_status_ext.dart'; +import 'package:autonomy_flutter/util/log.dart'; import 'package:collection/collection.dart'; import 'package:feralfile_app_tv_proto/feralfile_app_tv_proto.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:rxdart/transformers.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:web3dart/json_rpc.dart'; abstract class CanvasDeviceEvent {} class CanvasDeviceGetDevicesEvent extends CanvasDeviceEvent { - final String sceneId; - final bool syncAll; + final bool retry; - // constructor - CanvasDeviceGetDevicesEvent(this.sceneId, {this.syncAll = true}); + CanvasDeviceGetDevicesEvent({this.retry = false}); } -class CanvasDeviceAddEvent extends CanvasDeviceEvent { - final DeviceState device; +class CanvasDeviceAppendDeviceEvent extends CanvasDeviceEvent { + final CanvasDevice device; - CanvasDeviceAddEvent(this.device); + CanvasDeviceAppendDeviceEvent(this.device); } -class CanvasDeviceCastSingleEvent extends CanvasDeviceEvent { +class CanvasDeviceRotateEvent extends CanvasDeviceEvent { final CanvasDevice device; - final String tokenId; + final bool clockwise; - CanvasDeviceCastSingleEvent(this.device, this.tokenId); + CanvasDeviceRotateEvent(this.device, {this.clockwise = true}); } -class CanvasDeviceCastCollectionEvent extends CanvasDeviceEvent { +/* +* Version V2 +*/ + +class CanvasDeviceDisconnectEvent extends CanvasDeviceEvent { + final List devices; + final bool callRPC; + + CanvasDeviceDisconnectEvent(this.devices, {this.callRPC = true}); +} + +class CanvasDeviceOnRPCErrorEvent extends CanvasDeviceEvent { final CanvasDevice device; - final PlayListModel playlist; - CanvasDeviceCastCollectionEvent(this.device, this.playlist); + CanvasDeviceOnRPCErrorEvent(this.device); } -class CanvasDeviceUnCastingEvent extends CanvasDeviceEvent { +class CanvasDeviceCastListArtworkEvent extends CanvasDeviceEvent { final CanvasDevice device; - final bool isCollection; + final List artwork; - CanvasDeviceUnCastingEvent(this.device, this.isCollection); + CanvasDeviceCastListArtworkEvent(this.device, this.artwork); } -class CanvasDeviceRotateEvent extends CanvasDeviceEvent { +class CanvasDeviceChangeControlDeviceEvent extends CanvasDeviceEvent { + final CanvasDevice newDevice; + final List artwork; + + CanvasDeviceChangeControlDeviceEvent(this.newDevice, this.artwork); +} + +class CanvasDevicePauseCastingEvent extends CanvasDeviceEvent { final CanvasDevice device; - final bool clockwise; - CanvasDeviceRotateEvent(this.device, {this.clockwise = true}); + CanvasDevicePauseCastingEvent(this.device); +} + +class CanvasDeviceResumeCastingEvent extends CanvasDeviceEvent { + final CanvasDevice device; + + CanvasDeviceResumeCastingEvent(this.device); +} + +class CanvasDeviceNextArtworkEvent extends CanvasDeviceEvent { + final CanvasDevice device; + + CanvasDeviceNextArtworkEvent(this.device); +} + +class CanvasDevicePreviousArtworkEvent extends CanvasDeviceEvent { + final CanvasDevice device; + + CanvasDevicePreviousArtworkEvent(this.device); +} + +class CanvasDeviceUpdateDurationEvent extends CanvasDeviceEvent { + final CanvasDevice device; + final List artwork; + + CanvasDeviceUpdateDurationEvent(this.device, this.artwork); +} + +class CanvasDeviceCastExhibitionEvent extends CanvasDeviceEvent { + final CanvasDevice device; + final CastExhibitionRequest castRequest; + + CanvasDeviceCastExhibitionEvent(this.device, this.castRequest); } class CanvasDeviceState { final List devices; - final String sceneId; - final bool isConnectError; - final bool isLoaded; + final Map canvasDeviceStatus; + final Map lastSelectedActiveDeviceMap; + + // final String sceneId; + final RPCError? rpcError; CanvasDeviceState({ required this.devices, - required this.isLoaded, - this.sceneId = '', - this.isConnectError = false, - }); + Map? canvasDeviceStatus, + Map? lastSelectedActiveDeviceMap, + this.rpcError, + }) : canvasDeviceStatus = canvasDeviceStatus ?? {}, + lastSelectedActiveDeviceMap = lastSelectedActiveDeviceMap ?? {}; - CanvasDeviceState copyWith({ - List? devices, - String? sceneId, - bool? isConnectError, - bool? isLoaded, - }) => + CanvasDeviceState copyWith( + {List? devices, + Map? controllingDeviceStatus, + Map? lastActiveDevice, + RPCError? rpcError}) => CanvasDeviceState( - devices: devices ?? this.devices, - sceneId: sceneId ?? this.sceneId, - isConnectError: isConnectError ?? false, - isLoaded: isLoaded ?? this.isLoaded, - ); + devices: devices ?? this.devices, + canvasDeviceStatus: controllingDeviceStatus ?? canvasDeviceStatus, + lastSelectedActiveDeviceMap: + lastActiveDevice ?? lastSelectedActiveDeviceMap, + rpcError: rpcError ?? this.rpcError); + + CanvasDeviceState updateOnCast( + {required CanvasDevice device, required String displayKey}) { + lastSelectedActiveDeviceMap.removeWhere((key, value) => value == device); + lastSelectedActiveDeviceMap[displayKey] = device; + return copyWith( + lastActiveDevice: lastSelectedActiveDeviceMap, + ); + } CanvasDeviceState replaceDeviceState( {required CanvasDevice device, required DeviceState deviceState}) { @@ -85,40 +158,74 @@ class CanvasDeviceState { return copyWith(devices: newDeviceState); } - List get playingDevice => devices - .map((e) { - if (e.status == DeviceStatus.playing) { - return e.device; - } - }) - .whereNotNull() - .toList(); + CanvasDevice? lastSelectedActiveDeviceForKey(String key) { + final lastActiveDevice = lastSelectedActiveDeviceMap[key]; + if (lastActiveDevice != null) { + if (isDeviceAlive(lastActiveDevice)) { + return lastActiveDevice; + } else { + lastSelectedActiveDeviceMap.remove(key); + } + } + final activeDevice = _activeDeviceForKey(key); + if (activeDevice != null) { + lastSelectedActiveDeviceMap[key] = activeDevice; + } + return activeDevice; + } + + Duration? castingSpeed(String key) { + CanvasDevice? lastActiveDevice = lastSelectedActiveDeviceForKey(key); + final lastActiveDeviceStatus = + canvasDeviceStatus[lastActiveDevice?.deviceId]; + final durationInMilisecond = + lastActiveDeviceStatus?.artworks.first.duration; + if (durationInMilisecond != null) { + return Duration(milliseconds: durationInMilisecond); + } + return null; + } + + CheckDeviceStatusReply? statusOf(CanvasDevice device) => + canvasDeviceStatus[device.deviceId]; - bool get isCasting => - devices.firstWhereOrNull((deviceState) => - deviceState.status == DeviceStatus.playing && - deviceState.device.playingSceneId == sceneId) != - null; + bool isDeviceAlive(CanvasDevice device) { + final status = statusOf(device); + return status != null; + } + + CanvasDevice? _activeDeviceForKey(String key) { + final id = canvasDeviceStatus.entries + .firstWhereOrNull((element) => element.value.playingArtworkKey == key) + ?.key; + return devices + .firstWhereOrNull((element) => element.device.deviceId == id) + ?.device; + } } class DeviceState { final CanvasDevice device; - DeviceStatus status; + final Duration? duration; + final bool? isPlaying; // constructor DeviceState({ required this.device, - this.status = DeviceStatus.connected, + this.duration, + this.isPlaying, }); // DeviceState copyWith({ CanvasDevice? device, - DeviceStatus? status, + Duration? duration, + bool? isPlaying, }) => DeviceState( device: device ?? this.device, - status: status ?? this.status, + duration: duration ?? this.duration, + isPlaying: isPlaying ?? this.isPlaying, ); } @@ -129,109 +236,261 @@ enum DeviceStatus { error, } +EventTransformer debounceSequential(Duration duration) => + (events, mapper) => events.throttleTime(duration).asyncExpand(mapper); + class CanvasDeviceBloc extends AuBloc { - final CanvasClientService _canvasClientService; + final CanvasClientServiceV2 _canvasClientServiceV2; + + final Map _deviceRetryCount = {}; // constructor - CanvasDeviceBloc(this._canvasClientService) - : super(CanvasDeviceState(devices: [], isLoaded: false)) { - on((event, emit) async { - emit(CanvasDeviceState( - devices: state.devices, - sceneId: event.sceneId, - isLoaded: state.devices.isNotEmpty)); - final devices = await _canvasClientService.getConnectingDevices( - doSync: event.syncAll); - emit(state.copyWith( - devices: devices - .map((e) => DeviceState( - device: e, - status: e.isConnecting && e.playingSceneId != null - ? DeviceStatus.playing - : DeviceStatus.connected)) - .toList(), - isLoaded: true)); - }); + CanvasDeviceBloc(this._canvasClientServiceV2) + : super(CanvasDeviceState(devices: [])) { + on( + (event, emit) async { + log.info('CanvasDeviceBloc: adding devices'); + try { + final devices = await _canvasClientServiceV2.scanDevices(); + + Map? controllingDeviceStatus = {}; + + controllingDeviceStatus = devices.controllingDevices; - on((event, emit) async { + final newState = state.copyWith( + devices: devices.map((e) => DeviceState(device: e.first)).toList(), + controllingDeviceStatus: controllingDeviceStatus, + ); + log.info('CanvasDeviceBloc: get devices: ${newState.devices.length}, ' + 'controllingDeviceStatus: ${newState.canvasDeviceStatus}'); + emit(newState); + } catch (e) { + log.info('CanvasDeviceBloc: error while get devices: $e'); + unawaited(Sentry.captureException(e)); + emit(state.copyWith()); + } + }, + transformer: debounceSequential(const Duration(seconds: 5)), + ); + + on((event, emit) async { final newState = state.copyWith( devices: state.devices ..removeWhere( - (element) => element.device.id == event.device.device.id) - ..add(DeviceState(device: event.device.device))); + (element) => element.device.deviceId == event.device.deviceId) + ..add(DeviceState(device: event.device))); emit(newState); }); - on((event, emit) async { + on((event, emit) async { final device = event.device; try { - emit(state.replaceDeviceState( - device: device, - deviceState: - DeviceState(device: device, status: DeviceStatus.loading))); - final connected = await _canvasClientService.connectToDevice(device); - if (!connected) { - throw Exception('Failed to connect to device'); + await _canvasClientServiceV2.rotateCanvas(device, + clockwise: event.clockwise); + } catch (_) {} + }); + + /* + * Version V2 + */ + + on((event, emit) async { + final devices = event.devices; + await Future.forEach(devices, (device) async { + try { + log.info('CanvasDeviceBloc: disconnect device: ' + '${device.deviceId}, ${device.name}, ${device.deviceId}'); + if (event.callRPC) { + await _canvasClientServiceV2.disconnectDevice(device); + } + add(CanvasDeviceGetDevicesEvent()); + } catch (e) { + log.info('CanvasDeviceBloc: error while disconnect device: $e'); } + }); + emit(state.copyWith(controllingDeviceStatus: {})); + }); + + on((event, emit) async { + final controllingDevice = event.device; + final numberOfRetry = _deviceRetryCount[controllingDevice.deviceId] ?? 0; + log.info('CanvasDeviceBloc: retry connect to device: $numberOfRetry'); + if (numberOfRetry < maxRetryCount) { + await Future.delayed(const Duration(milliseconds: 500)); + _deviceRetryCount[controllingDevice.deviceId] = numberOfRetry + 1; + final isSuccess = + await _canvasClientServiceV2.connectToDevice(controllingDevice); + log.info('CanvasDeviceBloc: retry connect to device: $isSuccess'); + if (isSuccess) { + _deviceRetryCount.remove(controllingDevice.deviceId); + } else { + add(CanvasDeviceDisconnectEvent([controllingDevice])); + } + } else { + add(CanvasDeviceDisconnectEvent([controllingDevice])); + } + }); + + on((event, emit) async { + final device = event.device; + try { final ok = - await _canvasClientService.castSingleArtwork(device, event.tokenId); + await _canvasClientServiceV2.castListArtwork(device, event.artwork); if (!ok) { throw Exception('Failed to cast to device'); } + final currentDeviceState = state.devices.firstWhereOrNull( + (element) => element.device.deviceId == device.deviceId); + if (currentDeviceState == null) { + throw Exception('Device not found'); + } + final status = + await _canvasClientServiceV2.getDeviceCastingStatus(device); + final newStatus = state.canvasDeviceStatus; + newStatus[device.deviceId] = status; + final displayKey = event.artwork.playArtworksHashCode.toString(); + emit( + state + .updateOnCast(device: device, displayKey: displayKey) + .replaceDeviceState( + device: device, + deviceState: currentDeviceState.copyWith(isPlaying: true)) + .copyWith(controllingDeviceStatus: newStatus), + ); + } catch (_) { emit(state.replaceDeviceState( - device: device, - deviceState: - DeviceState(device: device, status: DeviceStatus.playing))); + device: device, deviceState: DeviceState(device: device))); + } + }); + + on((event, emit) async { + final device = event.device; + try { + final ok = await _canvasClientServiceV2.castExhibition( + device, event.castRequest); + if (!ok) { + throw Exception('Failed to cast to device'); + } + final currentDeviceState = state.devices.firstWhereOrNull( + (element) => element.device.deviceId == device.deviceId); + if (currentDeviceState == null) { + throw Exception('Device not found'); + } + final status = + await _canvasClientServiceV2.getDeviceCastingStatus(device); + final newStatus = state.canvasDeviceStatus; + newStatus[device.deviceId] = status; + final displayKey = event.castRequest.displayKey; + emit( + state + .updateOnCast(device: device, displayKey: displayKey) + .replaceDeviceState( + device: device, + deviceState: currentDeviceState.copyWith(isPlaying: true)) + .copyWith(controllingDeviceStatus: newStatus), + ); } catch (_) { emit(state.replaceDeviceState( - device: device, - deviceState: - DeviceState(device: device, status: DeviceStatus.error))); + device: device, deviceState: DeviceState(device: device))); } }); - on((event, emit) async { + on((event, emit) async { final device = event.device; try { + final currentDeviceState = state.devices.firstWhereOrNull( + (element) => element.device.deviceId == device.deviceId); + if (currentDeviceState == null) { + throw Exception('Device not found'); + } + await _canvasClientServiceV2.nextArtwork(device); emit(state.replaceDeviceState( device: device, - deviceState: - DeviceState(device: device, status: DeviceStatus.loading))); - final connected = await _canvasClientService.connectToDevice(device); - if (!connected) { - throw Exception('Failed to connect to device'); - } - final ok = - await _canvasClientService.castCollection(device, event.playlist); - if (!ok) { - throw Exception('Failed to cast to device'); + deviceState: currentDeviceState.copyWith(isPlaying: true))); + } catch (_) {} + }); + + on((event, emit) async { + final device = event.device; + try { + final currentDeviceState = state.devices.firstWhereOrNull( + (element) => element.device.deviceId == device.deviceId); + if (currentDeviceState == null) { + throw Exception('Device not found'); } + await _canvasClientServiceV2.previousArtwork(device); emit(state.replaceDeviceState( device: device, - deviceState: - DeviceState(device: device, status: DeviceStatus.playing))); - } catch (_) { + deviceState: currentDeviceState.copyWith(isPlaying: true))); + } catch (_) {} + }); + + on((event, emit) async { + final device = event.device; + try { + final currentDeviceState = state.devices.firstWhereOrNull( + (element) => element.device.deviceId == device.deviceId); + if (currentDeviceState == null) { + throw Exception('Device not found'); + } + await _canvasClientServiceV2.pauseCasting(device); emit(state.replaceDeviceState( device: device, - deviceState: - DeviceState(device: device, status: DeviceStatus.error))); - } + deviceState: currentDeviceState.copyWith(isPlaying: false))); + } catch (_) {} }); - on((event, emit) async { + on((event, emit) async { final device = event.device; try { - await _canvasClientService.uncastSingleArtwork(device); + final currentDeviceState = state.devices.firstWhereOrNull( + (element) => element.device.deviceId == device.deviceId); + if (currentDeviceState == null) { + throw Exception('Device not found'); + } + await _canvasClientServiceV2.resumeCasting(device); emit(state.replaceDeviceState( - device: device, deviceState: DeviceState(device: device))); + device: device, + deviceState: currentDeviceState.copyWith(isPlaying: true))); } catch (_) {} }); - on((event, emit) async { + on((event, emit) async { + add(CanvasDeviceCastListArtworkEvent(event.newDevice, event.artwork)); + }); + + on((event, emit) async { final device = event.device; + final artworks = event.artwork; try { - await _canvasClientService.rotateCanvas(device, - clockwise: event.clockwise); + final response = + await _canvasClientServiceV2.updateDuration(device, artworks); + final currentDeviceState = state.devices.firstWhereOrNull( + (element) => element.device.deviceId == device.deviceId); + if (currentDeviceState == null) { + throw Exception('Device not found'); + } + final controllingStatus = state.canvasDeviceStatus[device.deviceId]; + if (controllingStatus == null) { + throw Exception('Device not found'); + } + final newControllingStatus = CheckDeviceStatusReply(artworks: artworks) + ..startTime = response.startTime + ..connectedDevice = controllingStatus.connectedDevice; + + final controllingDeviceStatus = + state.canvasDeviceStatus.map((key, value) { + if (key == device.deviceId) { + return MapEntry(key, newControllingStatus); + } + return MapEntry(key, value); + }); + + emit(state + .copyWith(controllingDeviceStatus: controllingDeviceStatus) + .replaceDeviceState( + device: device, + deviceState: currentDeviceState.copyWith(isPlaying: true))); } catch (_) {} }); } diff --git a/lib/screen/detail/preview/keyboard_control_page.dart b/lib/screen/detail/preview/keyboard_control_page.dart index 98c5861208..c3920dcf6a 100644 --- a/lib/screen/detail/preview/keyboard_control_page.dart +++ b/lib/screen/detail/preview/keyboard_control_page.dart @@ -5,7 +5,7 @@ import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/main.dart'; import 'package:autonomy_flutter/screen/app_router.dart'; import 'package:autonomy_flutter/screen/detail/preview/touchpad_page.dart'; -import 'package:autonomy_flutter/service/canvas_client_service.dart'; +import 'package:autonomy_flutter/service/canvas_client_service_v2.dart'; import 'package:autonomy_flutter/util/style.dart'; import 'package:autonomy_flutter/view/artwork_common_widget.dart'; import 'package:autonomy_flutter/view/responsive.dart'; @@ -157,7 +157,7 @@ class _KeyboardControlPageState extends State final code = text[text.length - 1]; _textController.text = ''; final devices = widget.payload.devices; - await injector() + await injector() .sendKeyBoard(devices, code.codeUnitAt(0)); }, ), diff --git a/lib/screen/detail/preview_detail/preview_detail_widget.dart b/lib/screen/detail/preview_detail/preview_detail_widget.dart index f10f435520..e12100625f 100644 --- a/lib/screen/detail/preview_detail/preview_detail_widget.dart +++ b/lib/screen/detail/preview_detail/preview_detail_widget.dart @@ -16,6 +16,7 @@ import 'package:autonomy_flutter/screen/interactive_postcard/postcard_detail_blo import 'package:autonomy_flutter/screen/interactive_postcard/postcard_detail_state.dart'; import 'package:autonomy_flutter/screen/interactive_postcard/postcard_view_widget.dart'; import 'package:autonomy_flutter/util/asset_token_ext.dart'; +import 'package:autonomy_flutter/util/custom_route_observer.dart'; import 'package:autonomy_flutter/view/artwork_common_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -75,13 +76,17 @@ class _ArtworkPreviewWidgetState extends State @override void didPopNext() { - _renderingWidget?.didPopNext(); + if (!CustomRouteObserver.onIgnoreBackLayerPopUp) { + _renderingWidget?.didPopNext(); + } super.didPopNext(); } @override void didPushNext() { - unawaited(_renderingWidget?.clearPrevious()); + if (!CustomRouteObserver.onIgnoreBackLayerPopUp) { + unawaited(_renderingWidget?.clearPrevious()); + } super.didPushNext(); } @@ -106,7 +111,7 @@ class _ArtworkPreviewWidgetState extends State builder: (context, state) { switch (state.runtimeType) { case ArtworkPreviewDetailLoadingState: - return const CircularProgressIndicator(); + return previewPlaceholder(); case ArtworkPreviewDetailLoadedState: final assetToken = (state as ArtworkPreviewDetailLoadedState).assetToken; diff --git a/lib/screen/detail/preview_primer.dart b/lib/screen/detail/preview_primer.dart index c14a92694b..2f202c766f 100644 --- a/lib/screen/detail/preview_primer.dart +++ b/lib/screen/detail/preview_primer.dart @@ -136,7 +136,7 @@ class _PreviewPrimerPageState extends State crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - token.title ?? '', + token.displayTitle ?? '', style: theme.textTheme.ppMori400White16, maxLines: 1, overflow: TextOverflow.ellipsis, diff --git a/lib/screen/exhibition_note/exhibition_note_page.dart b/lib/screen/exhibition_custom_note/exhibition_custom_note_page.dart similarity index 69% rename from lib/screen/exhibition_note/exhibition_note_page.dart rename to lib/screen/exhibition_custom_note/exhibition_custom_note_page.dart index b821fc9707..dbdc9b94d5 100644 --- a/lib/screen/exhibition_note/exhibition_note_page.dart +++ b/lib/screen/exhibition_custom_note/exhibition_custom_note_page.dart @@ -1,13 +1,13 @@ import 'package:autonomy_flutter/model/ff_exhibition.dart'; import 'package:autonomy_flutter/view/back_appbar.dart'; -import 'package:autonomy_flutter/view/note_view.dart'; +import 'package:autonomy_flutter/view/custom_note.dart'; import 'package:feralfile_app_theme/feral_file_app_theme.dart'; import 'package:flutter/material.dart'; -class ExhibitionNotePage extends StatelessWidget { - const ExhibitionNotePage({required this.exhibition, super.key}); +class ExhibitionCustomNotePage extends StatelessWidget { + const ExhibitionCustomNotePage({required this.info, super.key}); - final Exhibition exhibition; + final CustomExhibitionNote info; @override Widget build(BuildContext context) => Scaffold( @@ -19,8 +19,8 @@ class ExhibitionNotePage extends StatelessWidget { body: Padding( padding: const EdgeInsets.fromLTRB(14, 0, 14, 20), child: SingleChildScrollView( - child: ExhibitionNoteView( - exhibition: exhibition, + child: ExhibitionCustomNote( + info: info, isFull: true, ), ), diff --git a/lib/screen/exhibition_details/exhibition_detail_bloc.dart b/lib/screen/exhibition_details/exhibition_detail_bloc.dart index 69ee240ff4..76430fec51 100644 --- a/lib/screen/exhibition_details/exhibition_detail_bloc.dart +++ b/lib/screen/exhibition_details/exhibition_detail_bloc.dart @@ -1,10 +1,9 @@ // create exhibition_detail bloc import 'package:autonomy_flutter/au_bloc.dart'; -import 'package:autonomy_flutter/model/ff_account.dart'; -import 'package:autonomy_flutter/model/ff_exhibition.dart'; import 'package:autonomy_flutter/screen/exhibition_details/exhibition_detail_state.dart'; import 'package:autonomy_flutter/service/feralfile_service.dart'; +import 'package:autonomy_flutter/util/exhibition_ext.dart'; class ExhibitionDetailBloc extends AuBloc { @@ -13,15 +12,17 @@ class ExhibitionDetailBloc ExhibitionDetailBloc(this._feralFileService) : super(ExhibitionDetailState()) { on((event, emit) async { - final result = await Future.wait([ - _feralFileService.getExhibition(event.exhibitionId), - _feralFileService.getExhibitionArtworks(event.exhibitionId, - withSeries: true) - ]); - final exhibitionDetail = ExhibitionDetail( - exhibition: result[0] as Exhibition, - artworks: result[1] as List); - emit(state.copyWith(exhibitionDetail: exhibitionDetail)); + final exhibition = await _feralFileService + .getExhibition(event.exhibitionId, includeFirstArtwork: true); + final listSeries = exhibition.series ?? []; + if (exhibition.isJohnGerrardShow && listSeries.isNotEmpty) { + final firstViewableArtwork = await _feralFileService + .getFirstViewableArtwork(listSeries.first.id); + listSeries.first = + listSeries.first.copyWith(artwork: firstViewableArtwork); + } + + emit(state.copyWith(exhibition: exhibition.copyWith(series: listSeries))); }); } } diff --git a/lib/screen/exhibition_details/exhibition_detail_page.dart b/lib/screen/exhibition_details/exhibition_detail_page.dart index 12e988da39..7122d45b3e 100644 --- a/lib/screen/exhibition_details/exhibition_detail_page.dart +++ b/lib/screen/exhibition_details/exhibition_detail_page.dart @@ -3,26 +3,27 @@ import 'dart:async'; import 'package:after_layout/after_layout.dart'; import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/model/ff_exhibition.dart'; -import 'package:autonomy_flutter/model/play_control_model.dart'; -import 'package:autonomy_flutter/model/play_list_model.dart'; -import 'package:autonomy_flutter/screen/app_router.dart'; +import 'package:autonomy_flutter/model/pair.dart'; import 'package:autonomy_flutter/screen/detail/preview/canvas_device_bloc.dart'; import 'package:autonomy_flutter/screen/exhibition_details/exhibition_detail_bloc.dart'; import 'package:autonomy_flutter/screen/exhibition_details/exhibition_detail_state.dart'; +import 'package:autonomy_flutter/screen/exhibitions/exhibitions_bloc.dart'; import 'package:autonomy_flutter/service/metric_client_service.dart'; import 'package:autonomy_flutter/util/constants.dart'; import 'package:autonomy_flutter/util/exhibition_ext.dart'; -import 'package:autonomy_flutter/util/ui_helper.dart'; +import 'package:autonomy_flutter/util/john_gerrard_helper.dart'; +import 'package:autonomy_flutter/util/log.dart'; import 'package:autonomy_flutter/view/back_appbar.dart'; -import 'package:autonomy_flutter/view/canvas_device_view.dart'; import 'package:autonomy_flutter/view/cast_button.dart'; -import 'package:autonomy_flutter/view/event_view.dart'; +import 'package:autonomy_flutter/view/custom_note.dart'; import 'package:autonomy_flutter/view/exhibition_detail_last_page.dart'; import 'package:autonomy_flutter/view/exhibition_detail_preview.dart'; import 'package:autonomy_flutter/view/ff_artwork_preview.dart'; import 'package:autonomy_flutter/view/note_view.dart'; -import 'package:easy_localization/easy_localization.dart'; +import 'package:autonomy_flutter/view/post_view.dart'; +import 'package:carousel_slider/carousel_slider.dart'; import 'package:feralfile_app_theme/feral_file_app_theme.dart'; +import 'package:feralfile_app_tv_proto/feralfile_app_tv_proto.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; @@ -39,97 +40,121 @@ class ExhibitionDetailPage extends StatefulWidget { class _ExhibitionDetailPageState extends State with AfterLayoutMixin { late final ExhibitionDetailBloc _exBloc; - late final CanvasDeviceBloc _canvasDeviceBloc; + late bool isUpcomingExhibition; + final _metricClientService = injector(); + final _canvasDeviceBloc = injector(); late final PageController _controller; int _currentIndex = 0; + int _carouselIndex = 0; @override void initState() { super.initState(); + final exhibitionBloc = injector(); + isUpcomingExhibition = exhibitionBloc.state.upcomingExhibition != null && + exhibitionBloc.state.upcomingExhibition!.id == + widget.payload.exhibitions[widget.payload.index].id; _exBloc = context.read(); - _canvasDeviceBloc = context.read(); _exBloc.add(GetExhibitionDetailEvent( widget.payload.exhibitions[widget.payload.index].id)); - _controller = PageController(); } @override Widget build(BuildContext context) => BlocConsumer( - builder: (context, state) => Scaffold( - appBar: _getAppBar(context, state.exhibitionDetail), - backgroundColor: AppColor.primaryBlack, - body: _body(context, state), - ), - listener: (context, state) {}, - ); + builder: (context, state) => Scaffold( + appBar: _getAppBar(context, state.exhibition), + backgroundColor: AppColor.primaryBlack, + body: _body(context, state), + ), + listener: (context, state) {}, + listenWhen: (previous, current) { + if (previous.exhibition == null && current.exhibition != null) { + _stream(current.exhibition!); + } + return true; + }); Widget _body(BuildContext context, ExhibitionDetailState state) { - final exhibitionDetail = state.exhibitionDetail; - if (exhibitionDetail == null) { + final exhibition = state.exhibition; + if (exhibition == null) { return const Center( child: CircularProgressIndicator(), ); } - final viewingArtworks = exhibitionDetail.representArtworks; - final itemCount = viewingArtworks.length + 3; - return Stack( + final itemCount = + isUpcomingExhibition ? 3 : ((exhibition.series?.length ?? 0) + 3); + return Column( children: [ - PageView.builder( - controller: _controller, - onPageChanged: (index) { - setState(() { - _currentIndex = index; - }); - }, - scrollDirection: Axis.vertical, - itemCount: itemCount, - itemBuilder: (context, index) { - if (index == itemCount - 1) { - return ExhibitionDetailLastPage( - startOver: () => setState(() { - _currentIndex = 0; - _controller.jumpToPage(0); - }), - nextPayload: widget.payload.next(), - ); - } + Expanded( + child: PageView.builder( + controller: _controller, + onPageChanged: (index) { + setState(() { + _currentIndex = index; + }); + _stream(exhibition); + }, + scrollDirection: Axis.vertical, + itemCount: itemCount, + itemBuilder: (context, index) { + if (index == itemCount - 1) { + return ExhibitionDetailLastPage( + startOver: () => setState(() { + _currentIndex = 0; + _controller.jumpToPage(0); + }), + nextPayload: widget.payload.next(), + ); + } - switch (index) { - case 0: - return _getPreviewPage(exhibitionDetail.exhibition); - case 1: - return _notePage(exhibitionDetail.exhibition); - default: - final seriesIndex = index - 2; - final series = exhibitionDetail.exhibition.series!.firstWhere( - (element) => - element.id == viewingArtworks[seriesIndex].seriesID); - return Padding( - padding: const EdgeInsets.only(bottom: 40), - child: FeralFileArtworkPreview( - payload: FeralFileArtworkPreviewPayload( - artwork: viewingArtworks[seriesIndex], - series: series, + switch (index) { + case 0: + return _getPreviewPage(exhibition); + case 1: + return _notePage(exhibition); + default: + final seriesIndex = index - 2; + final series = exhibition.sortedSeries[seriesIndex]; + final artwork = series.artwork; + if (artwork == null) { + return const SizedBox(); + } + return Padding( + padding: const EdgeInsets.only(bottom: 40), + child: FeralFileArtworkPreview( + payload: FeralFileArtworkPreviewPayload( + artwork: artwork.copyWith(series: series), + ), ), - ), - ); - } - }, - ), - if (_currentIndex == 0 || _currentIndex == 1) - Align( - alignment: Alignment.bottomCenter, - child: _nextButton(), + ); + } + }, ), + ), + if (_currentIndex == 0 || _currentIndex == 1) _nextButton() ], ); } + void _stream(Exhibition exhibition) { + log.info('onPageChanged: $_currentIndex'); + final displayKey = exhibition.displayKey; + final lastSelectedDevice = + _canvasDeviceBloc.state.lastSelectedActiveDeviceForKey(displayKey); + if (lastSelectedDevice != null) { + final request = _getCastExhibitionRequest(exhibition); + log.info('onPageChanged: request: $request'); + _canvasDeviceBloc.add( + CanvasDeviceCastExhibitionEvent(lastSelectedDevice, request), + ); + } + } + Widget _getPreviewPage(Exhibition exhibition) => Column( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ @@ -144,6 +169,7 @@ class _ExhibitionDetailPageState extends State child: RotatedBox( quarterTurns: 3, child: IconButton( + padding: const EdgeInsets.all(0), onPressed: () async => _controller.nextPage( duration: const Duration(milliseconds: 300), curve: Curves.easeIn), @@ -154,99 +180,96 @@ class _ExhibitionDetailPageState extends State ), ); - Widget _notePage(Exhibition exhibition) { - const horizontalPadding = 14.0; - const peekWidth = 50.0; - final width = - MediaQuery.sizeOf(context).width - horizontalPadding * 2 - peekWidth; - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 14), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - Padding( - padding: const EdgeInsets.only(right: 14), - child: ExhibitionNoteView( + Widget _notePage(Exhibition exhibition) => LayoutBuilder( + builder: (context, constraints) => Center( + child: CarouselSlider( + items: [ + ExhibitionNoteView( exhibition: exhibition, - width: width, - onReadMore: () async { - await Navigator.pushNamed( - context, - AppRouter.exhibitionNotePage, - arguments: exhibition, - ); - }, ), + if (exhibition.isJohnGerrardShow) + ...JohnGerrardHelper.customNote.map( + (info) => ExhibitionCustomNote( + info: info, + ), + ), + ...exhibition.posts?.where((post) => post.coverURI != null).map( + (e) => ExhibitionPostView( + post: e, + exhibitionID: exhibition.id, + ), + ) ?? + [] + ], + options: CarouselOptions( + aspectRatio: constraints.maxWidth / constraints.maxHeight, + viewportFraction: 0.76, + enableInfiniteScroll: false, + enlargeCenterPage: true, + initialPage: _carouselIndex, + onPageChanged: (index, reason) { + _carouselIndex = index; + _stream(exhibition); + }, ), - ...exhibition.resources?.map((e) => ExhibitionEventView( - exhibitionEvent: e, - width: width, - )) ?? - [] - ], + ), ), - ), - ); - } + ); - AppBar _getAppBar( - BuildContext buildContext, ExhibitionDetail? exhibitionDetail) => + AppBar _getAppBar(BuildContext buildContext, Exhibition? exhibition) => getFFAppBar( buildContext, onBack: () => Navigator.pop(buildContext), - action: exhibitionDetail == null || - exhibitionDetail.exhibition.status != 4 - ? null - : Padding( + action: exhibition != null + ? Padding( padding: const EdgeInsets.only(right: 14, bottom: 10, top: 10), child: FFCastButton( - text: _currentIndex == 0 ? 'stream_to_device'.tr() : null, - onCastTap: () async { - await _onCastTap(buildContext, exhibitionDetail); + displayKey: exhibition.id, + onDeviceSelected: (device) async { + final request = _getCastExhibitionRequest(exhibition); + _canvasDeviceBloc.add( + CanvasDeviceCastExhibitionEvent(device, request), + ); }, ), - ), + ) + : null, ); - Future _onCastTap( - BuildContext context, ExhibitionDetail exhibitionDetail) async { - if (exhibitionDetail.artworks == null || - exhibitionDetail.artworks!.isEmpty) { - return; + Pair _getCurrentCatalogInfo( + Exhibition exhibition) { + ExhibitionCatalog? catalog; + String? catalogId; + switch (_currentIndex) { + case 0: + catalog = ExhibitionCatalog.home; + case 1: + if (_carouselIndex == 0) { + catalog = ExhibitionCatalog.curatorNote; + } else { + catalog = ExhibitionCatalog.resource; + catalogId = exhibition.posts![_carouselIndex - 1].id; + } + default: + catalog = ExhibitionCatalog.artwork; + final seriesIndex = _currentIndex - 2; + final currentArtwork = exhibition.series?[seriesIndex].artwork?.id; + catalogId = currentArtwork; } - final tokenIds = exhibitionDetail.artworks - ?.map((e) => exhibitionDetail.getArtworkTokenId(e)!) - .toList(); - final sceneId = exhibitionDetail.exhibition.id; - final playlistModel = PlayListModel( - name: exhibitionDetail.exhibition.title, - id: sceneId, - thumbnailURL: exhibitionDetail.exhibition.coverUrl, - tokenIDs: tokenIds, - playControlModel: PlayControlModel(timer: 30), - ); - await UIHelper.showFlexibleDialog( - context, - BlocProvider.value( - value: _canvasDeviceBloc, - child: CanvasDeviceView( - sceneId: sceneId, - isCollection: true, - playlist: playlistModel, - onClose: () { - Navigator.of(context).pop(); - }, - ), - ), - isDismissible: true, - ); - await _fetchDevice(sceneId); + return Pair(catalog, catalogId); } - Future _fetchDevice(String exhibitionId) async { - _canvasDeviceBloc - .add(CanvasDeviceGetDevicesEvent(exhibitionId, syncAll: false)); + CastExhibitionRequest _getCastExhibitionRequest(Exhibition exhibition) { + final exhibitionId = exhibition.id; + final catalogInfo = _getCurrentCatalogInfo(exhibition); + final catalog = catalogInfo.first; + final catalogId = catalogInfo.second; + CastExhibitionRequest request = CastExhibitionRequest( + exhibitionId: exhibitionId, + catalog: catalog, + catalogId: catalogId, + ); + return request; } @override diff --git a/lib/screen/exhibition_details/exhibition_detail_state.dart b/lib/screen/exhibition_details/exhibition_detail_state.dart index 851dd902ca..86f9d7c0be 100644 --- a/lib/screen/exhibition_details/exhibition_detail_state.dart +++ b/lib/screen/exhibition_details/exhibition_detail_state.dart @@ -9,12 +9,10 @@ class GetExhibitionDetailEvent extends ExhibitionDetailEvent { } class ExhibitionDetailState { - ExhibitionDetailState({this.exhibitionDetail}); + ExhibitionDetailState({this.exhibition}); - final ExhibitionDetail? exhibitionDetail; + final Exhibition? exhibition; - ExhibitionDetailState copyWith({ExhibitionDetail? exhibitionDetail}) => - ExhibitionDetailState( - exhibitionDetail: exhibitionDetail ?? this.exhibitionDetail, - ); + ExhibitionDetailState copyWith({Exhibition? exhibition}) => + ExhibitionDetailState(exhibition: exhibition ?? this.exhibition); } diff --git a/lib/screen/exhibitions/exhibitions_bloc.dart b/lib/screen/exhibitions/exhibitions_bloc.dart index 8d83764e41..1938869f55 100644 --- a/lib/screen/exhibitions/exhibitions_bloc.dart +++ b/lib/screen/exhibitions/exhibitions_bloc.dart @@ -1,17 +1,95 @@ import 'package:autonomy_flutter/au_bloc.dart'; +import 'package:autonomy_flutter/model/ff_exhibition.dart'; import 'package:autonomy_flutter/screen/exhibitions/exhibitions_state.dart'; import 'package:autonomy_flutter/service/feralfile_service.dart'; +import 'package:autonomy_flutter/util/exhibition_ext.dart'; +import 'package:autonomy_flutter/util/log.dart'; class ExhibitionBloc extends AuBloc { final FeralFileService _feralFileService; + static const limit = 25; + Exhibition? _sourceExhibition; + ExhibitionBloc(this._feralFileService) : super(ExhibitionsState()) { on((event, emit) async { - final featuredExhibition = await _feralFileService.getAllExhibitions( - limit: 1, - withSeries: true, - ); - emit(state.copyWith(exhibitions: featuredExhibition)); + if (state.allExhibitionIds.isNotEmpty) { + return; + } + final result = await Future.wait([ + _feralFileService.getUpcomingExhibition(), + _feralFileService.getFeaturedExhibition(), + _feralFileService.getAllExhibitions(limit: limit), + _feralFileService.getSourceExhibition(), + ]); + final upcomingExhibition = result[0] as Exhibition?; + final featuredExhibition = result[1]! as Exhibition; + final allExhibitions = result[2]! as List; + final sourceExhibition = result[3]! as Exhibition; + log.info('[ExhibitionBloc] getAllExhibitionsEvent:' + ' pro ${allExhibitions.length}'); + var pastExhibitions = allExhibitions + .where((exhibition) => + exhibition.id != featuredExhibition.id && + exhibition.id != upcomingExhibition?.id) + .toList(); + pastExhibitions = + _addSourceExhibitionIfNeeded(pastExhibitions, sourceExhibition); + _sourceExhibition = sourceExhibition; + emit(state.copyWith( + currentPage: 1, + upcomingExhibition: upcomingExhibition, + featuredExhibition: featuredExhibition, + pastExhibitions: pastExhibitions, + )); + add(GetNextPageEvent(isLoop: true)); }); + + on( + (event, emit) async { + log.info('[ExhibitionBloc] getNextPageEvent:' + 'offset ${state.currentPage * limit}'); + List exhibitions = + await _feralFileService.getAllExhibitions( + limit: limit, + offset: state.currentPage * limit, + ); + final resultLength = exhibitions.length; + log.info('[ExhibitionBloc] getNextPageEvent: $resultLength'); + + exhibitions.removeWhere( + (element) => state.allExhibitionIds.contains(element.id)); + if (_sourceExhibition != null && + (state.pastExhibitions ?? []).isNotEmpty) { + exhibitions = + _addSourceExhibitionIfNeeded(exhibitions, _sourceExhibition!); + } + emit(state.copyWith( + pastExhibitions: [...?state.pastExhibitions, ...exhibitions], + currentPage: state.currentPage + 1, + )); + if (event.isLoop && resultLength == limit) { + add(GetNextPageEvent(isLoop: true)); + } + }, + ); + } + + List _addSourceExhibitionIfNeeded( + List exhibitions, Exhibition sourceExhibition) { + final isExistSourceExhibition = + exhibitions.any((exhibition) => exhibition.id == sourceExhibition.id); + if (isExistSourceExhibition) { + return exhibitions; + } + final lastExhibition = exhibitions.last; + if (lastExhibition.exhibitionViewAt + .isBefore(sourceExhibition.exhibitionViewAt)) { + log.info('[ExhibitionBloc] inserted Source Exhibition'); + exhibitions + ..add(sourceExhibition) + ..sort((a, b) => b.exhibitionViewAt.compareTo(a.exhibitionViewAt)); + } + return exhibitions; } } diff --git a/lib/screen/exhibitions/exhibitions_page.dart b/lib/screen/exhibitions/exhibitions_page.dart index 14c96a13c3..a724ad76fa 100644 --- a/lib/screen/exhibitions/exhibitions_page.dart +++ b/lib/screen/exhibitions/exhibitions_page.dart @@ -1,23 +1,32 @@ import 'dart:async'; +import 'package:auto_size_text/auto_size_text.dart'; import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/main.dart'; import 'package:autonomy_flutter/model/ff_exhibition.dart'; import 'package:autonomy_flutter/screen/app_router.dart'; +import 'package:autonomy_flutter/screen/bloc/subscription/subscription_bloc.dart'; +import 'package:autonomy_flutter/screen/bloc/subscription/subscription_state.dart'; import 'package:autonomy_flutter/screen/exhibition_details/exhibition_detail_page.dart'; import 'package:autonomy_flutter/screen/exhibitions/exhibitions_bloc.dart'; import 'package:autonomy_flutter/screen/exhibitions/exhibitions_state.dart'; +import 'package:autonomy_flutter/service/iap_service.dart'; import 'package:autonomy_flutter/service/navigation_service.dart'; +import 'package:autonomy_flutter/util/constants.dart'; import 'package:autonomy_flutter/util/exhibition_ext.dart'; +import 'package:autonomy_flutter/util/style.dart'; import 'package:autonomy_flutter/view/back_appbar.dart'; import 'package:autonomy_flutter/view/header.dart'; +import 'package:autonomy_flutter/view/primary_button.dart'; import 'package:cached_network_image/cached_network_image.dart'; -import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:feralfile_app_theme/feral_file_app_theme.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:sentry/sentry.dart'; class ExhibitionsPage extends StatefulWidget { const ExhibitionsPage({super.key}); @@ -28,16 +37,23 @@ class ExhibitionsPage extends StatefulWidget { class ExhibitionsPageState extends State with RouteAware { late ExhibitionBloc _exhibitionBloc; + late SubscriptionBloc _subscriptionBloc; late ScrollController _controller; final _navigationService = injector(); + final _iapService = injector(); + static const _padding = 14.0; + static const _exhibitionInfoDivideWidth = 20.0; + String? _autoOpenExhibitionId; // initState @override void initState() { super.initState(); _controller = ScrollController(); - _exhibitionBloc = context.read(); + _exhibitionBloc = injector(); + _subscriptionBloc = injector(); _exhibitionBloc.add(GetAllExhibitionsEvent()); + _subscriptionBloc.add(GetSubscriptionEvent()); } void scrollToTop() { @@ -67,6 +83,36 @@ class ExhibitionsPageState extends State with RouteAware { void refreshExhibitions() { _exhibitionBloc.add(GetAllExhibitionsEvent()); + _subscriptionBloc.add(GetSubscriptionEvent()); + } + + void setAutoOpenExhibition(String exhibitionId) { + setState(() { + _autoOpenExhibitionId = exhibitionId; + }); + if (_exhibitionBloc.state.allExhibitions.isNotEmpty) { + _openExhibition(context, exhibitionId); + } + } + + void _openExhibition(BuildContext context, String exhibitionId) { + final listExhibitions = _exhibitionBloc.state.allExhibitions; + final index = + listExhibitions.indexWhere((element) => element.id == exhibitionId); + if (index < 0) { + unawaited(Sentry.captureMessage('Exhibition not found: $exhibitionId')); + } else { + unawaited( + _navigationService.navigateTo( + AppRouter.exhibitionDetailPage, + arguments: ExhibitionDetailPayload( + exhibitions: listExhibitions, + index: index, + ), + ), + ); + } + _autoOpenExhibitionId = null; } @override @@ -88,139 +134,319 @@ class ExhibitionsPageState extends State with RouteAware { title: 'exhibitions'.tr(), ), ), - SliverToBoxAdapter(child: _listExhibitions(context)) + _listExhibitions(context), ], ), ); - Widget _exhibitionItem( - BuildContext context, ExhibitionDetail exhibitionDetail, int index) { + Widget _exhibitionItem({ + required BuildContext context, + required List viewableExhibitions, + required Exhibition exhibition, + required bool isFeaturedExhibition, + }) { final theme = Theme.of(context); final screenWidth = MediaQuery.sizeOf(context).width; - const double padding = 14; - final estimatedHeight = (screenWidth - padding * 2) / 16 * 9; - final exhibition = exhibitionDetail.exhibition; - return Container( - padding: const EdgeInsets.symmetric(horizontal: padding), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('current_exhibition'.tr(), - style: theme.textTheme.ppMori400White14), - if (exhibition.isFreeToStream) - Text('free_to_stream'.tr(), style: theme.textTheme.ppMori400Grey14), - const SizedBox(height: 18), - Column( - children: [ - GestureDetector( - child: ClipRRect( - borderRadius: BorderRadius.circular(10), - child: CachedNetworkImage( - imageUrl: exhibition.coverUrl, - placeholder: (context, url) => SizedBox( - height: estimatedHeight, - child: const Center( - child: CircularProgressIndicator( - color: Colors.white, - backgroundColor: AppColor.auQuickSilver, - strokeWidth: 2, + final estimatedHeight = (screenWidth - _padding * 2) / 16 * 9; + final estimatedWidth = screenWidth - _padding * 2; + final index = viewableExhibitions.indexOf(exhibition); + final titleStyle = theme.textTheme.ppMori400White16; + final subTitleStyle = theme.textTheme.ppMori400Grey12; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + children: [ + GestureDetector( + onTap: () async { + if (exhibition.canViewDetails && !isFeaturedExhibition) { + _subscriptionBloc.add(GetSubscriptionEvent()); + final isSubscribed = await _iapService.isSubscribed(); + if (!isSubscribed) { + return; + } + } + + if (!context.mounted) { + return; + } + if (exhibition.canViewDetails && index >= 0) { + await Navigator.of(context).pushNamed( + AppRouter.exhibitionDetailPage, + arguments: ExhibitionDetailPayload( + exhibitions: viewableExhibitions, + index: index, + ), + ); + } + }, + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: exhibition.id == SOURCE_EXHIBITION_ID + ? SvgPicture.network( + exhibition.coverUrl, + height: estimatedHeight, + placeholderBuilder: (context) => SizedBox( + height: estimatedHeight, + child: const Center( + child: CircularProgressIndicator( + color: Colors.white, + backgroundColor: AppColor.auQuickSilver, + strokeWidth: 2, + ), + ), ), + ) + : CachedNetworkImage( + imageUrl: exhibition.coverUrl, + height: estimatedHeight, + maxWidthDiskCache: estimatedWidth.toInt(), + memCacheWidth: estimatedWidth.toInt(), + memCacheHeight: estimatedHeight.toInt(), + maxHeightDiskCache: estimatedHeight.toInt(), + cacheManager: injector(), + placeholder: (context, url) => SizedBox( + height: estimatedHeight, + child: const Center( + child: CircularProgressIndicator( + color: Colors.white, + backgroundColor: AppColor.auQuickSilver, + strokeWidth: 2, + ), + ), + ), + fit: BoxFit.fitWidth, + ), + ), + ), + const SizedBox(height: 20), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + if (!exhibition.canViewDetails) ...[ + _lockIcon(), + const SizedBox(width: 5), + ], + SizedBox( + width: (estimatedWidth - _exhibitionInfoDivideWidth) / 2 - + (exhibition.canViewDetails ? 0 : 13 + 5), + child: AutoSizeText( + exhibition.title, + style: titleStyle, + maxLines: 2, ), ), - fit: BoxFit.fitWidth, + ], + ), + const SizedBox(width: _exhibitionInfoDivideWidth), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (exhibition.isSoloExhibition && + exhibition.artists != null) ...[ + RichText( + text: TextSpan( + style: subTitleStyle.copyWith( + decorationColor: AppColor.disabledColor), + children: [ + TextSpan(text: 'works_by'.tr()), + TextSpan( + recognizer: TapGestureRecognizer() + ..onTap = () async { + await _navigationService + .openFeralFileArtistPage( + exhibition.artists![0].alias, + ); + }, + text: exhibition.artists![0].alias, + style: const TextStyle( + decoration: TextDecoration.underline, + )), + ], + ), + ), + ], + if (exhibition.curator != null) + RichText( + text: TextSpan( + style: subTitleStyle.copyWith( + decorationColor: AppColor.disabledColor), + children: [ + TextSpan(text: 'curated_by'.tr()), + TextSpan( + recognizer: TapGestureRecognizer() + ..onTap = () async { + await _navigationService + .openFeralFileCuratorPage( + exhibition.curator!.alias); + }, + text: exhibition.curator!.alias, + style: const TextStyle( + decoration: TextDecoration.underline, + ), + ), + ], + ), + ), + Text( + exhibition.isGroupExhibition + ? 'group_exhibition'.tr() + : 'solo_exhibition'.tr(), + style: subTitleStyle, + ), + ], + ), + ) + ], + ) + ], + ), + ], + ); + } + + Widget _listExhibitions(BuildContext context) => + BlocConsumer( + listener: (context, exhibitionsState) { + if (exhibitionsState.allExhibitions.isNotEmpty && + _autoOpenExhibitionId != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _openExhibition(context, _autoOpenExhibitionId!); + }); + } + }, + builder: (context, exhibitionsState) => + BlocBuilder( + builder: (context, subscriptionState) { + final theme = Theme.of(context); + if (exhibitionsState.currentPage == 0) { + return const SliverToBoxAdapter( + child: Center( + child: CircularProgressIndicator( + color: Colors.white, + backgroundColor: AppColor.auQuickSilver, + strokeWidth: 2, + ), + ), + ); + } else { + final featureExhibition = exhibitionsState.featuredExhibition; + final upcomingExhibition = exhibitionsState.upcomingExhibition; + final pastExhibitions = exhibitionsState.pastExhibitions; + final isSubscribed = subscriptionState.isSubscribed; + + final allExhibition = exhibitionsState.allExhibitions; + final viewableExhibitions = isSubscribed + ? allExhibition + : featureExhibition != null + ? [featureExhibition] + : []; + + final divider = addDivider( + height: 40, color: AppColor.auQuickSilver, thickness: 0.5); + return SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: _padding), + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + final exhibition = allExhibition[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (featureExhibition != null && index == 0) ...[ + Text('current_exhibition'.tr(), + style: theme.textTheme.ppMori400White14), + Text('for_essential_members'.tr(), + style: theme.textTheme.ppMori400Grey14), + const SizedBox(height: 18), + ], + if (upcomingExhibition != null && index == 1) ...[ + _exhibitionGroupHeader( + context, + isSubscribed, + 'upcoming_exhibition'.tr(), + ), + ], + if (exhibition.id == pastExhibitions?.first.id) + _exhibitionGroupHeader( + context, + isSubscribed, + 'past_exhibition'.tr(), + ), + _exhibitionItem( + context: context, + viewableExhibitions: viewableExhibitions, + exhibition: exhibition, + isFeaturedExhibition: + exhibition.id == featureExhibition?.id, + ), + divider, + if (index == allExhibition.length - 1) + const SizedBox(height: 40), + ], + ); + }, + childCount: allExhibition.length, ), ), - onTap: () async { - await Navigator.of(context) - .pushNamed(AppRouter.exhibitionDetailPage, - arguments: ExhibitionDetailPayload( - exhibitions: _exhibitionBloc.state.exhibitions! - .map((e) => e.exhibition) - .toList(), - index: index, - )); - }, + ); + } + }, + ), + ); + + Widget _exhibitionGroupHeader( + BuildContext context, bool isSubscribed, String title) { + final theme = Theme.of(context); + return Padding( + padding: const EdgeInsets.only(bottom: 18), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: theme.textTheme.ppMori400White14, ), - const SizedBox(height: 20), Row( - crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - child: Text(exhibition.title, - style: theme.textTheme.ppMori400White16), - ), - const SizedBox(width: 20), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (exhibition.curator != null) - RichText( - text: TextSpan( - style: theme.textTheme.ppMori400Grey14 - .copyWith( - decorationColor: - AppColor.disabledColor), - children: [ - TextSpan(text: 'curated_by'.tr()), - TextSpan( - recognizer: TapGestureRecognizer() - ..onTap = () async { - await _navigationService - .openFeralFileCuratorPage( - exhibition.curator!.alias); - }, - text: exhibition.curator!.alias, - style: const TextStyle( - decoration: TextDecoration.underline, - )), - ])), - Text( - exhibition.isGroupExhibition - ? 'group_exhibition'.tr() - : 'solo_exhibition'.tr(), - style: theme.textTheme.ppMori400Grey14), - if (exhibition.getSeriesArtworkModelText != null) - Text(exhibition.getSeriesArtworkModelText!, - style: theme.textTheme.ppMori400Grey14), - ], - ), - ) + if (!isSubscribed) ...[ + _lockIcon(), + const SizedBox(width: 5), + ], + Text('premium_membership'.tr(), + style: theme.textTheme.ppMori400Grey14), ], - ) + ), ], ), + if (!isSubscribed) + PrimaryButton( + color: AppColor.feralFileLightBlue, + padding: EdgeInsets.zero, + elevatedPadding: const EdgeInsets.symmetric(horizontal: 15), + borderRadius: 20, + text: 'get_premium'.tr(), + onTap: () async { + await Navigator.of(context) + .pushNamed(AppRouter.subscriptionPage); + }, + ), ], ), ); } - Widget _listExhibitions(BuildContext context) => - BlocConsumer( - builder: (context, state) { - final exhibitions = state.exhibitions; - if (exhibitions == null) { - return const Center( - child: CircularProgressIndicator( - color: Colors.white, - backgroundColor: AppColor.auQuickSilver, - strokeWidth: 2, - ), - ); - } else { - return Column( - children: [ - ...exhibitions - .map((e) => [ - _exhibitionItem(context, e, exhibitions.indexOf(e)), - const SizedBox(height: 40) - ]) - .flattened, - const SizedBox(height: 100), - ], - ); - } - }, - listener: (context, state) {}, + Widget _lockIcon() => SizedBox( + width: 13, + height: 13, + child: SvgPicture.asset( + 'assets/images/exhibition_lock_icon.svg', + ), ); } diff --git a/lib/screen/exhibitions/exhibitions_state.dart b/lib/screen/exhibitions/exhibitions_state.dart index a53f2f6144..d045db7517 100644 --- a/lib/screen/exhibitions/exhibitions_state.dart +++ b/lib/screen/exhibitions/exhibitions_state.dart @@ -4,19 +4,50 @@ class ExhibitionsEvent {} class GetAllExhibitionsEvent extends ExhibitionsEvent {} +class GetNextPageEvent extends ExhibitionsEvent { + final bool isLoop; + + GetNextPageEvent({this.isLoop = false}); +} + class GetOpeningExhibitionsEvent extends ExhibitionsEvent {} class ExhibitionsState { ExhibitionsState({ - this.exhibitions, + this.currentPage = 0, + this.upcomingExhibition, + this.featuredExhibition, + this.pastExhibitions, }); - final List? exhibitions; + final int currentPage; + final Exhibition? upcomingExhibition; + final Exhibition? featuredExhibition; + final List? pastExhibitions; ExhibitionsState copyWith({ - List? exhibitions, + final Exhibition? upcomingExhibition, + final Exhibition? featuredExhibition, + final List? pastExhibitions, + bool? isSubscribed, + int? currentPage, }) => ExhibitionsState( - exhibitions: exhibitions ?? this.exhibitions, + currentPage: currentPage ?? this.currentPage, + upcomingExhibition: upcomingExhibition ?? this.upcomingExhibition, + featuredExhibition: featuredExhibition ?? this.featuredExhibition, + pastExhibitions: pastExhibitions ?? this.pastExhibitions, ); + + List get allExhibitionIds => [ + if (featuredExhibition != null) featuredExhibition!.id, + if (upcomingExhibition != null) upcomingExhibition!.id, + ...pastExhibitions?.map((e) => e.id) ?? [], + ]; + + List get allExhibitions => [ + if (featuredExhibition != null) featuredExhibition!, + if (upcomingExhibition != null) upcomingExhibition!, + ...pastExhibitions ?? [], + ]; } diff --git a/lib/screen/featured_works_page/featured_works_page.dart b/lib/screen/featured_works_page/featured_works_page.dart new file mode 100644 index 0000000000..f59cde5c58 --- /dev/null +++ b/lib/screen/featured_works_page/featured_works_page.dart @@ -0,0 +1,103 @@ +import 'package:autonomy_flutter/common/injector.dart'; +import 'package:autonomy_flutter/model/ff_artwork.dart'; +import 'package:autonomy_flutter/screen/app_router.dart'; +import 'package:autonomy_flutter/screen/feralfile_artwork_preview/feralfile_artwork_preview_page.dart'; +import 'package:autonomy_flutter/service/feralfile_service.dart'; +import 'package:autonomy_flutter/util/style.dart'; +import 'package:autonomy_flutter/view/back_appbar.dart'; +import 'package:autonomy_flutter/view/ff_artwork_thumbnail_view.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:feralfile_app_theme/feral_file_app_theme.dart'; +import 'package:flutter/material.dart'; + +class FeaturedWorksPage extends StatefulWidget { + const FeaturedWorksPage({super.key}); + + @override + State createState() => _FeaturedWorksPageState(); +} + +class _FeaturedWorksPageState extends State { + static const _padding = 14.0; + static const _axisSpacing = 10.0; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: getFFAppBar( + context, + onBack: () => Navigator.pop(context), + title: Text( + 'featured_works'.tr(), + ), + ), + backgroundColor: AppColor.primaryBlack, + body: _artworkSliverGrid(context), + ); + + Widget _artworkSliverGrid(BuildContext context) => Padding( + padding: + const EdgeInsets.only(left: _padding, right: _padding, bottom: 20), + child: CustomScrollView( + slivers: [ + FutureBuilder>( + // ignore: discarded_futures + future: injector().getFeaturedArtworks(), + builder: (context, snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.waiting: + return SliverFillRemaining( + hasScrollBody: false, + child: Center( + child: loadingIndicator(valueColor: AppColor.auGrey)), + ); + case ConnectionState.done: + if (snapshot.data == null || snapshot.data!.isEmpty) { + final theme = Theme.of(context); + return SliverFillRemaining( + hasScrollBody: false, + child: Center( + child: Text( + 'featured_works_empty'.tr(), + style: theme.textTheme.ppMori400White14, + ), + ), + ); + } else { + return SliverGrid.builder( + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisSpacing: _axisSpacing, + mainAxisSpacing: _axisSpacing, + crossAxisCount: 3, + ), + itemCount: snapshot.data!.length, + itemBuilder: (context, index) => FFArtworkThumbnailView( + artwork: snapshot.data![index], + cacheWidth: (MediaQuery.sizeOf(context).width - + _padding * 2 - + _axisSpacing * 2) ~/ + 3, + cacheHeight: (MediaQuery.sizeOf(context).width - + _padding * 2 - + _axisSpacing * 2) ~/ + 3, + onTap: () async { + await Navigator.of(context).pushNamed( + AppRouter.ffArtworkPreviewPage, + arguments: FeralFileArtworkPreviewPagePayload( + artwork: snapshot.data![index], + ), + ); + }, + ), + ); + } + default: + return const SizedBox(); + } + }, + ), + ], + ), + ); +} diff --git a/lib/screen/feralfile_artwork_preview/feralfile_artwork_preview_page.dart b/lib/screen/feralfile_artwork_preview/feralfile_artwork_preview_page.dart index 2c8c0c4034..91d5ec1bd9 100644 --- a/lib/screen/feralfile_artwork_preview/feralfile_artwork_preview_page.dart +++ b/lib/screen/feralfile_artwork_preview/feralfile_artwork_preview_page.dart @@ -1,13 +1,28 @@ +import 'dart:async'; + import 'package:after_layout/after_layout.dart'; import 'package:autonomy_flutter/common/injector.dart'; -import 'package:autonomy_flutter/model/ff_account.dart'; +import 'package:autonomy_flutter/model/ff_artwork.dart'; +import 'package:autonomy_flutter/screen/detail/preview/canvas_device_bloc.dart'; import 'package:autonomy_flutter/service/metric_client_service.dart'; import 'package:autonomy_flutter/util/constants.dart'; import 'package:autonomy_flutter/util/exhibition_ext.dart'; +import 'package:autonomy_flutter/util/john_gerrard_helper.dart'; +import 'package:autonomy_flutter/util/series_ext.dart'; +import 'package:autonomy_flutter/util/style.dart'; +import 'package:autonomy_flutter/view/artwork_common_widget.dart'; +import 'package:autonomy_flutter/view/artwork_title_view.dart'; import 'package:autonomy_flutter/view/back_appbar.dart'; +import 'package:autonomy_flutter/view/cast_button.dart'; import 'package:autonomy_flutter/view/feralfile_artwork_preview_widget.dart'; +import 'package:autonomy_flutter/view/responsive.dart'; +import 'package:backdrop/backdrop.dart'; import 'package:feralfile_app_theme/feral_file_app_theme.dart'; +import 'package:feralfile_app_tv_proto/feralfile_app_tv_proto.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; +import 'package:sentry/sentry.dart'; class FeralFileArtworkPreviewPage extends StatefulWidget { const FeralFileArtworkPreviewPage({required this.payload, super.key}); @@ -20,8 +35,22 @@ class FeralFileArtworkPreviewPage extends StatefulWidget { } class _FeralFileArtworkPreviewPageState - extends State with AfterLayoutMixin { + extends State + with + AfterLayoutMixin, + SingleTickerProviderStateMixin { final _metricClient = injector.get(); + final _canvasDeviceBloc = injector.get(); + late bool isCrystallineWork; + + double? _appBarBottomDy; + static const _infoShrinkPosition = 0.001; + static const _infoExpandPosition = 0.99; + static const toolbarHeight = 66.0; + bool _isInfoExpand = false; + + ScrollController? _scrollController; + late AnimationController _animationController; void _sendViewArtworkEvent(Artwork artwork) { final data = { @@ -30,32 +59,244 @@ class _FeralFileArtworkPreviewPageState _metricClient.addEvent(MixpanelEvent.viewArtwork, data: data); } + @override + void initState() { + isCrystallineWork = widget.payload.artwork.series?.exhibitionID == + JohnGerrardHelper.exhibitionID; + _animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 300), + reverseDuration: const Duration(milliseconds: 300), + value: _infoShrinkPosition, + upperBound: _infoExpandPosition, + ); + _infoShrink(); + super.initState(); + } + @override void afterFirstLayout(BuildContext context) { + _appBarBottomDy ??= MediaQuery.of(context).padding.top + kToolbarHeight; _sendViewArtworkEvent(widget.payload.artwork); } @override - Widget build(BuildContext context) => Scaffold( - appBar: getFFAppBar( - context, - onBack: () => Navigator.pop(context), - ), + void dispose() { + _scrollController?.dispose(); + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => BackdropScaffold( + appBar: _isInfoExpand + ? const PreferredSize( + preferredSize: Size.fromHeight(toolbarHeight), + child: SizedBox( + height: toolbarHeight, + ), + ) + : getFFAppBar( + context, + onBack: () => Navigator.pop(context), + action: FFCastButton( + displayKey: widget.payload.artwork.series?.exhibitionID ?? '', + onDeviceSelected: _onDeviceSelected, + ), + ), backgroundColor: AppColor.primaryBlack, - body: Column( + frontLayerBackgroundColor: AppColor.primaryBlack, + backLayerBackgroundColor: AppColor.primaryBlack, + frontLayerScrim: Colors.transparent, + backLayerScrim: Colors.transparent, + reverseAnimationCurve: Curves.ease, + animationController: _animationController, + revealBackLayerAtStart: true, + subHeaderAlwaysActive: false, + frontLayerShape: const BeveledRectangleBorder(), + backLayer: Column( children: [ Expanded( - child: FeralfileArtworkPreviewWidget( - payload: FeralFileArtworkPreviewWidgetPayload( - artwork: widget.payload.artwork, - isMute: false, - isScrollable: widget.payload.artwork.isScrollablePreviewURL, - ), + child: _buildArtworkPreview(), + ), + const Padding( + padding: EdgeInsets.symmetric(vertical: 18), + child: ArtworkDetailsHeader( + title: 'I', + subTitle: 'I', + color: Colors.transparent, + ), + ), + ], + ), + frontLayer: _infoContent(context, widget.payload.artwork), + subHeader: DecoratedBox( + decoration: const BoxDecoration(color: AppColor.primaryBlack), + child: GestureDetector( + onVerticalDragEnd: (details) { + final dy = details.primaryVelocity ?? 0; + if (dy <= 0) { + _infoExpand(); + } else { + _infoShrink(); + } + }, + child: _infoHeader(context, widget.payload.artwork), + ), + ), + ); + + Widget _buildArtworkPreview() { + final artworkPreviewWidget = FeralfileArtworkPreviewWidget( + payload: FeralFileArtworkPreviewWidgetPayload( + artwork: widget.payload.artwork, + isMute: false, + isScrollable: widget.payload.artwork.isScrollablePreviewURL, + ), + ); + if (isCrystallineWork) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 14), + child: Center( + child: AspectRatio( + aspectRatio: 1, + child: artworkPreviewWidget, + ), + ), + ); + } + return artworkPreviewWidget; + } + + Future _onDeviceSelected(CanvasDevice device) async { + final exhibitionId = widget.payload.artwork.series?.exhibitionID; + if (exhibitionId == null) { + await Sentry.captureMessage('Exhibition ID is null for artwork ' + '${widget.payload.artwork.id}'); + } else { + final artworkId = widget.payload.artwork.id; + final request = CastExhibitionRequest( + exhibitionId: exhibitionId, + catalog: ExhibitionCatalog.artwork, + catalogId: artworkId, + ); + _canvasDeviceBloc.add(CanvasDeviceCastExhibitionEvent(device, request)); + } + } + + void _infoShrink() { + setState(() { + _isInfoExpand = false; + }); + _animationController.animateTo(_infoShrinkPosition); + } + + void _infoExpand() { + _scrollController?.jumpTo(0); + _scrollController ??= ScrollController(); + setState(() { + _isInfoExpand = true; + }); + _animationController.animateTo(_infoExpandPosition); + } + + Widget _artworkInfoIcon() => Semantics( + label: 'artworkInfoIcon', + child: GestureDetector( + onTap: () { + _isInfoExpand ? _infoShrink() : _infoExpand(); + }, + child: SvgPicture.asset( + !_isInfoExpand + ? 'assets/images/info_white.svg' + : 'assets/images/info_white_active.svg', + width: 22, + height: 22, + ), + ), + ); + + Widget _infoHeader(BuildContext context, Artwork artwork) => Padding( + padding: const EdgeInsets.only(left: 14, right: 14, bottom: 20), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + flex: 5, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ArtworkTitleView( + artwork: artwork, + ), + if (isCrystallineWork) ...[ + const SizedBox(height: 20), + ArtworkAttributesText( + artwork: artwork, + ), + ] + ], ), ), + Padding( + padding: const EdgeInsets.only(left: 60), + child: _artworkInfoIcon(), + ), ], ), ); + + Widget _infoContent(BuildContext context, Artwork artwork) { + final theme = Theme.of(context); + return SingleChildScrollView( + controller: _scrollController, + child: SizedBox( + width: double.infinity, + child: Column( + children: [ + Visibility( + visible: artwork.series!.mediumDescription != null, + child: Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: ResponsiveLayout.getPadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + artwork.series!.mediumDescription ?? '', + style: theme.textTheme.ppMori400White14, + ), + const SizedBox(height: 20), + ], + ), + ), + ), + ), + Padding( + padding: ResponsiveLayout.getPadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Semantics( + label: 'Desc', + child: HtmlWidget( + customStylesBuilder: auHtmlStyle, + artwork.series!.description ?? '', + textStyle: theme.textTheme.ppMori400White14, + ), + ), + const SizedBox(height: 40), + FFArtworkDetailsMetadataSection(artwork: artwork), + ], + ), + ), + ], + ), + ), + ); + } } class FeralFileArtworkPreviewPagePayload { diff --git a/lib/screen/feralfile_series/feralfile_series_bloc.dart b/lib/screen/feralfile_series/feralfile_series_bloc.dart index 2ee2d4ff63..2b0ff2801b 100644 --- a/lib/screen/feralfile_series/feralfile_series_bloc.dart +++ b/lib/screen/feralfile_series/feralfile_series_bloc.dart @@ -1,10 +1,12 @@ +import 'dart:async'; +import 'dart:typed_data'; +import 'dart:ui' as ui; + import 'package:autonomy_flutter/au_bloc.dart'; -import 'package:autonomy_flutter/model/ff_account.dart'; -import 'package:autonomy_flutter/model/ff_exhibition.dart'; -import 'package:autonomy_flutter/model/ff_series.dart'; import 'package:autonomy_flutter/screen/feralfile_series/feralfile_series_state.dart'; import 'package:autonomy_flutter/service/feralfile_service.dart'; import 'package:autonomy_flutter/util/exhibition_ext.dart'; +import 'package:http/http.dart' as http; class FeralFileSeriesBloc extends AuBloc { @@ -12,27 +14,34 @@ class FeralFileSeriesBloc FeralFileSeriesBloc(this._feralFileService) : super(FeralFileSeriesState()) { on((event, emit) async { - final result = await Future.wait([ - _feralFileService.getExhibition(event.exhibitionId), - _feralFileService.getSeriesArtworks(event.seriesId, withSeries: true), - _feralFileService.getSeries(event.seriesId), - ]); - final exhibition = result[0] as Exhibition; - final artworks = result[1] as List; - final series = result[2] as FFSeries; - final exhibitionDetail = ExhibitionDetail( - exhibition: exhibition, - artworks: artworks, - ); - final tokenIds = artworks - .map((e) => exhibitionDetail.getArtworkTokenId(e) ?? '') - .toList(); + final series = await _feralFileService.getSeries(event.seriesId, + exhibitionID: event.exhibitionId, includeFirstArtwork: true); + final thumbnailUrl = series.artwork?.thumbnailURL; + double thumbnailRatio = 1; + if (thumbnailUrl != null) { + thumbnailRatio = await _getImageRatio(thumbnailUrl); + } emit(state.copyWith( - exhibitionDetail: exhibitionDetail, series: series, - artworks: artworks, - tokenIds: tokenIds, + thumbnailRatio: thumbnailRatio, )); }); } + + Future _getImageRatio(String url) async { + try { + final response = await http.get(Uri.parse(url)); + final bytes = response.bodyBytes; + + final completer = Completer(); + ui.decodeImageFromList(Uint8List.fromList(bytes), (image) { + completer.complete(image); + }); + + final image = await completer.future; + return image.width / image.height; + } catch (e) { + return 1; + } + } } diff --git a/lib/screen/feralfile_series/feralfile_series_page.dart b/lib/screen/feralfile_series/feralfile_series_page.dart index bf7b0dea7c..cb6e790a4e 100644 --- a/lib/screen/feralfile_series/feralfile_series_page.dart +++ b/lib/screen/feralfile_series/feralfile_series_page.dart @@ -1,16 +1,23 @@ -import 'package:autonomy_flutter/model/ff_account.dart'; -import 'package:autonomy_flutter/model/ff_exhibition.dart'; +import 'package:autonomy_flutter/common/injector.dart'; +import 'package:autonomy_flutter/model/ff_artwork.dart'; +import 'package:autonomy_flutter/model/ff_list_response.dart'; import 'package:autonomy_flutter/model/ff_series.dart'; import 'package:autonomy_flutter/screen/app_router.dart'; +import 'package:autonomy_flutter/screen/detail/preview/canvas_device_bloc.dart'; import 'package:autonomy_flutter/screen/feralfile_artwork_preview/feralfile_artwork_preview_page.dart'; import 'package:autonomy_flutter/screen/feralfile_series/feralfile_series_bloc.dart'; import 'package:autonomy_flutter/screen/feralfile_series/feralfile_series_state.dart'; +import 'package:autonomy_flutter/service/feralfile_service.dart'; +import 'package:autonomy_flutter/util/series_ext.dart'; +import 'package:autonomy_flutter/util/style.dart'; import 'package:autonomy_flutter/view/back_appbar.dart'; import 'package:autonomy_flutter/view/ff_artwork_thumbnail_view.dart'; import 'package:autonomy_flutter/view/series_title_view.dart'; import 'package:feralfile_app_theme/feral_file_app_theme.dart'; +import 'package:feralfile_app_tv_proto/feralfile_app_tv_proto.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; class FeralFileSeriesPage extends StatefulWidget { const FeralFileSeriesPage({required this.payload, super.key}); @@ -23,6 +30,12 @@ class FeralFileSeriesPage extends StatefulWidget { class _FeralFileSeriesPageState extends State { late final FeralFileSeriesBloc _feralFileSeriesBloc; + final _canvasDeviceBloc = injector.get(); + static const _padding = 14.0; + static const _axisSpacing = 10.0; + final PagingController _pagingController = + PagingController(firstPageKey: 0); + static const _pageSize = 300; @override void initState() { @@ -30,16 +43,45 @@ class _FeralFileSeriesPageState extends State { _feralFileSeriesBloc = context.read(); _feralFileSeriesBloc.add(FeralFileSeriesGetSeriesEvent( widget.payload.seriesId, widget.payload.exhibitionId)); + _pagingController.addPageRequestListener((pageKey) async { + await _fetchPage(pageKey); + }); + } + + Future _fetchPage(int pageKey) async { + try { + final newItems = await injector().getSeriesArtworks( + widget.payload.seriesId, widget.payload.exhibitionId, + offset: pageKey, + // ignore: avoid_redundant_argument_values + limit: _pageSize); + final isLastPage = !newItems.paging.shouldLoadMore; + if (isLastPage) { + _pagingController.appendLastPage(newItems.result); + } else { + final nextPageKey = pageKey + _pageSize; + _pagingController.appendPage(newItems.result, nextPageKey); + } + } catch (error) { + _pagingController.error = error; + } + } + + @override + void dispose() { + _pagingController.dispose(); + super.dispose(); } @override Widget build(BuildContext context) => BlocConsumer( - builder: (context, state) => Scaffold( - appBar: _getAppBar(context, state.series), - backgroundColor: AppColor.primaryBlack, - body: _body(context, state)), - listener: (context, state) {}); + builder: (context, state) => Scaffold( + appBar: _getAppBar(context, state.series), + backgroundColor: AppColor.primaryBlack, + body: _body(context, state.series, state.thumbnailRatio)), + listener: (context, state) {}, + ); AppBar _getAppBar(BuildContext buildContext, FFSeries? series) => getFFAppBar( buildContext, @@ -52,42 +94,78 @@ class _FeralFileSeriesPageState extends State { crossAxisAlignment: CrossAxisAlignment.center), ); - Widget _body(BuildContext context, FeralFileSeriesState state) { - final series = state.series; + Widget _body(BuildContext context, FFSeries? series, double? thumbnailRatio) { if (series == null) { - return const Center( - child: CircularProgressIndicator(), - ); + return _loadingIndicator(); } - return _artworkGridView(context, state.exhibitionDetail, state.artworks); + return _artworkSliverGrid(context, series, thumbnailRatio ?? 1); } - Widget _artworkGridView(BuildContext context, - ExhibitionDetail? exhibitionDetail, List artworks) => - Padding( - padding: const EdgeInsets.only(left: 14, right: 14, bottom: 20), - child: GridView.builder( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + Widget _loadingIndicator() => Center( + child: Padding( + padding: const EdgeInsets.all(10), + child: loadingIndicator(valueColor: AppColor.auGrey), + ), + ); + + Widget _artworkSliverGrid( + BuildContext context, FFSeries series, double ratio) { + final cacheWidth = (MediaQuery.sizeOf(context).width - _padding * 2) ~/ 3; + final cacheHeight = (cacheWidth / ratio).toInt(); + return Padding( + padding: + const EdgeInsets.only(left: _padding, right: _padding, bottom: 20), + child: CustomScrollView( + slivers: [ + PagedSliverGrid( + pagingController: _pagingController, + showNewPageErrorIndicatorAsGridChild: false, + showNewPageProgressIndicatorAsGridChild: false, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisSpacing: _axisSpacing, + mainAxisSpacing: _axisSpacing, crossAxisCount: 3, - crossAxisSpacing: 10, - mainAxisSpacing: 10, + childAspectRatio: ratio, ), - itemBuilder: (context, index) { - final artwork = artworks[index]; - return FFArtworkThumbnailView( + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, artwork, index) => FFArtworkThumbnailView( artwork: artwork, + cacheWidth: cacheWidth, + cacheHeight: cacheHeight, onTap: () async { + final displayKey = series.displayKey; + final lastSelectedCanvasDevice = _canvasDeviceBloc.state + .lastSelectedActiveDeviceForKey(displayKey); + + if (lastSelectedCanvasDevice != null) { + final castRequest = CastExhibitionRequest( + exhibitionId: series.exhibitionID, + catalog: ExhibitionCatalog.artwork, + catalogId: artwork.id); + _canvasDeviceBloc.add( + CanvasDeviceCastExhibitionEvent( + lastSelectedCanvasDevice, + castRequest, + ), + ); + } await Navigator.of(context).pushNamed( AppRouter.ffArtworkPreviewPage, arguments: FeralFileArtworkPreviewPagePayload( - artwork: artwork, + artwork: artwork.copyWith(series: series), ), ); }, - ); - }, - itemCount: artworks.length), - ); + ), + newPageProgressIndicatorBuilder: (context) => _loadingIndicator(), + firstPageErrorIndicatorBuilder: (context) => const SizedBox(), + newPageErrorIndicatorBuilder: (context) => const SizedBox(), + ), + ) + ], + ), + ); + } } class FeralFileSeriesPagePayload { diff --git a/lib/screen/feralfile_series/feralfile_series_state.dart b/lib/screen/feralfile_series/feralfile_series_state.dart index 7e4eb45b30..fe1cce69e3 100644 --- a/lib/screen/feralfile_series/feralfile_series_state.dart +++ b/lib/screen/feralfile_series/feralfile_series_state.dart @@ -1,5 +1,3 @@ -import 'package:autonomy_flutter/model/ff_account.dart'; -import 'package:autonomy_flutter/model/ff_exhibition.dart'; import 'package:autonomy_flutter/model/ff_series.dart'; class FeralFileSeriesEvent {} @@ -12,28 +10,20 @@ class FeralFileSeriesGetSeriesEvent extends FeralFileSeriesEvent { } class FeralFileSeriesState { - final ExhibitionDetail? exhibitionDetail; final FFSeries? series; - final List artworks; - final List tokenIds; + final double thumbnailRatio; FeralFileSeriesState({ - this.exhibitionDetail, this.series, - this.artworks = const [], - this.tokenIds = const [], + this.thumbnailRatio = 1.0, }); FeralFileSeriesState copyWith({ - ExhibitionDetail? exhibitionDetail, FFSeries? series, - List? artworks, - List? tokenIds, + double? thumbnailRatio, }) => FeralFileSeriesState( - exhibitionDetail: exhibitionDetail ?? this.exhibitionDetail, series: series ?? this.series, - artworks: artworks ?? this.artworks, - tokenIds: tokenIds ?? this.tokenIds, + thumbnailRatio: thumbnailRatio ?? this.thumbnailRatio, ); } diff --git a/lib/screen/home/collection_home_page.dart b/lib/screen/home/collection_home_page.dart index 9a6ce3430e..346ee40280 100644 --- a/lib/screen/home/collection_home_page.dart +++ b/lib/screen/home/collection_home_page.dart @@ -9,7 +9,6 @@ import 'dart:async'; import 'package:after_layout/after_layout.dart'; import 'package:autonomy_flutter/common/injector.dart'; -import 'package:autonomy_flutter/database/cloud_database.dart'; import 'package:autonomy_flutter/main.dart'; import 'package:autonomy_flutter/model/blockchain.dart'; import 'package:autonomy_flutter/screen/app_router.dart'; @@ -30,7 +29,6 @@ import 'package:autonomy_flutter/service/metric_client_service.dart'; import 'package:autonomy_flutter/service/settings_data_service.dart'; import 'package:autonomy_flutter/service/versions_service.dart'; import 'package:autonomy_flutter/util/asset_token_ext.dart'; -import 'package:autonomy_flutter/util/au_icons.dart'; import 'package:autonomy_flutter/util/constants.dart'; import 'package:autonomy_flutter/util/log.dart'; import 'package:autonomy_flutter/util/string_ext.dart'; @@ -38,8 +36,8 @@ import 'package:autonomy_flutter/util/style.dart'; import 'package:autonomy_flutter/util/token_ext.dart'; import 'package:autonomy_flutter/view/artwork_common_widget.dart'; import 'package:autonomy_flutter/view/back_appbar.dart'; +import 'package:autonomy_flutter/view/get_started_banner.dart'; import 'package:autonomy_flutter/view/header.dart'; -import 'package:autonomy_flutter/view/primary_button.dart'; import 'package:autonomy_flutter/view/responsive.dart'; import 'package:dio/dio.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -115,7 +113,6 @@ class CollectionHomePageState extends State void afterFirstLayout(BuildContext context) { unawaited(_handleForeground()); unawaited(injector().postLinkedAddresses()); - unawaited(_checkForKeySync(context)); } @override @@ -224,7 +221,9 @@ class CollectionHomePageState extends State final paddingTop = MediaQuery.of(context).viewPadding.top; return Padding( padding: EdgeInsets.only(top: paddingTop), - child: HeaderView(title: 'collection'.tr()), + child: HeaderView( + title: 'collection'.tr(), + ), ); } @@ -251,11 +250,12 @@ class CollectionHomePageState extends State if (_showPostcardBanner) Container( padding: const EdgeInsets.symmetric(horizontal: 15), - child: MakingPostcardBanner( + child: GetStartedBanner( onClose: () async { await _hidePostcardBanner(); }, - onMakingPostcard: _onMakePostcard, + title: 'try_making_your_own_postcard'.tr(), + onGetStarted: _onMakePostcard, ), ) else @@ -325,11 +325,12 @@ class CollectionHomePageState extends State SliverToBoxAdapter( child: Container( padding: const EdgeInsets.symmetric(horizontal: 15), - child: MakingPostcardBanner( + child: GetStartedBanner( onClose: () async { await _hidePostcardBanner(); }, - onMakingPostcard: _onMakePostcard, + title: 'try_making_your_own_postcard'.tr(), + onGetStarted: _onMakePostcard, ), ), ), @@ -366,7 +367,7 @@ class CollectionHomePageState extends State }) { final theme = Theme.of(context); final asset = tokens[index]; - final title = asset.title; + final title = asset.displayTitle; final artistTitle = asset.artistTitle?.toIdentityOrMask(artistIdentities); return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -423,6 +424,7 @@ class CollectionHomePageState extends State ? PendingTokenWidget( thumbnail: asset.galleryThumbnailURL, tokenId: asset.tokenId, + shouldRefreshCache: asset.shouldRefreshThumbnailCache, ) : tokenGalleryThumbnailWidget( context, @@ -455,18 +457,6 @@ class CollectionHomePageState extends State ); } - Future _checkForKeySync(BuildContext context) async { - final cloudDatabase = injector(); - final defaultAccounts = await cloudDatabase.personaDao.getDefaultPersonas(); - - if (defaultAccounts.length >= 2) { - if (!context.mounted) { - return; - } - unawaited(Navigator.of(context).pushNamed(AppRouter.keySyncPage)); - } - } - void scrollToTop() { unawaited(_controller.animateTo(0, duration: const Duration(milliseconds: 500), @@ -477,10 +467,8 @@ class CollectionHomePageState extends State switch (event) { case FGBGType.foreground: unawaited(_handleForeground()); - break; case FGBGType.background: _handleBackground(); - break; } } @@ -529,61 +517,3 @@ class CollectionHomePageState extends State @override bool get wantKeepAlive => true; } - -class MakingPostcardBanner extends StatelessWidget { - final Function? onClose; - final Function? onMakingPostcard; - - const MakingPostcardBanner({super.key, this.onMakingPostcard, this.onClose}); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final textTheme = theme.textTheme; - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: AppColor.auGreyBackground), - padding: const EdgeInsets.all(15), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Text( - 'try_making_your_own_postcard'.tr(), - style: textTheme.ppMori400White14, - overflow: TextOverflow.ellipsis, - ), - ), - IconButton( - onPressed: () { - onClose?.call(); - }, - iconSize: 18, - constraints: const BoxConstraints(maxHeight: 18, maxWidth: 18), - icon: const Icon( - AuIcon.close, - color: AppColor.white, - ), - padding: EdgeInsets.zero, - ) - ], - ), - const SizedBox( - height: 20, - ), - PrimaryAsyncButton( - onTap: () { - onMakingPostcard?.call(); - }, - text: 'get_started'.tr(), - ) - ], - ), - ); - } -} diff --git a/lib/screen/home/home_navigation_page.dart b/lib/screen/home/home_navigation_page.dart index 661b36f9b1..1f12cc554d 100644 --- a/lib/screen/home/home_navigation_page.dart +++ b/lib/screen/home/home_navigation_page.dart @@ -9,40 +9,36 @@ import 'dart:async'; import 'package:after_layout/after_layout.dart'; import 'package:autonomy_flutter/common/injector.dart'; -import 'package:autonomy_flutter/database/entity/announcement_local.dart'; +import 'package:autonomy_flutter/database/cloud_database.dart'; import 'package:autonomy_flutter/main.dart'; import 'package:autonomy_flutter/screen/app_router.dart'; -import 'package:autonomy_flutter/screen/customer_support/support_thread_page.dart'; -import 'package:autonomy_flutter/screen/detail/artwork_detail_page.dart'; +import 'package:autonomy_flutter/screen/bloc/subscription/subscription_bloc.dart'; +import 'package:autonomy_flutter/screen/bloc/subscription/subscription_state.dart'; +import 'package:autonomy_flutter/screen/detail/preview/canvas_device_bloc.dart'; import 'package:autonomy_flutter/screen/exhibitions/exhibitions_bloc.dart'; import 'package:autonomy_flutter/screen/exhibitions/exhibitions_page.dart'; import 'package:autonomy_flutter/screen/exhibitions/exhibitions_state.dart'; import 'package:autonomy_flutter/screen/home/collection_home_page.dart'; import 'package:autonomy_flutter/screen/home/organize_home_page.dart'; -import 'package:autonomy_flutter/screen/interactive_postcard/postcard_detail_bloc.dart'; -import 'package:autonomy_flutter/screen/interactive_postcard/postcard_detail_page.dart'; import 'package:autonomy_flutter/screen/scan_qr/scan_qr_page.dart'; import 'package:autonomy_flutter/service/account_service.dart'; -import 'package:autonomy_flutter/service/airdrop_service.dart'; import 'package:autonomy_flutter/service/audit_service.dart'; import 'package:autonomy_flutter/service/backup_service.dart'; -import 'package:autonomy_flutter/service/canvas_client_service.dart'; import 'package:autonomy_flutter/service/chat_service.dart'; import 'package:autonomy_flutter/service/client_token_service.dart'; import 'package:autonomy_flutter/service/configuration_service.dart'; import 'package:autonomy_flutter/service/customer_support_service.dart'; import 'package:autonomy_flutter/service/metric_client_service.dart'; -import 'package:autonomy_flutter/service/notification_service.dart'; +import 'package:autonomy_flutter/service/notification_service.dart' as nc; import 'package:autonomy_flutter/service/playlist_service.dart'; import 'package:autonomy_flutter/service/remote_config_service.dart'; import 'package:autonomy_flutter/service/tezos_beacon_service.dart'; import 'package:autonomy_flutter/service/wc2_service.dart'; -import 'package:autonomy_flutter/util/announcement_ext.dart'; import 'package:autonomy_flutter/util/au_icons.dart'; import 'package:autonomy_flutter/util/constants.dart'; import 'package:autonomy_flutter/util/dio_util.dart'; import 'package:autonomy_flutter/util/inapp_notifications.dart'; -import 'package:autonomy_flutter/util/log.dart'; +import 'package:autonomy_flutter/util/notification_type.dart'; import 'package:autonomy_flutter/util/style.dart'; import 'package:autonomy_flutter/util/ui_helper.dart'; import 'package:autonomy_flutter/view/homepage_navigation_bar.dart'; @@ -58,7 +54,6 @@ import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:nft_collection/database/dao/asset_token_dao.dart'; -import 'package:nft_collection/database/nft_collection_database.dart'; import 'package:nft_collection/nft_collection.dart'; import 'package:onesignal_flutter/onesignal_flutter.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -100,7 +95,7 @@ class HomeNavigationPageState extends State final _configurationService = injector(); late Timer? _timer; final _clientTokenService = injector(); - final _notificationService = injector(); + final _notificationService = injector(); final _playListService = injector(); final _remoteConfig = injector(); final _metricClientService = injector(); @@ -136,6 +131,14 @@ class HomeNavigationPageState extends State } } + Future openExhibition(String exhibitionId) async { + await _onItemTapped(HomeNavigatorTab.exhibition.index); + // delay to ensure the page is loaded + Future.delayed(const Duration(milliseconds: 1000), () { + _exhibitionsPageKey.currentState?.setAutoOpenExhibition(exhibitionId); + }); + } + Future _onItemTapped(int index) async { if (index < _pages.length) { if (_selectedIndex == index) { @@ -184,20 +187,21 @@ class HomeNavigationPageState extends State context, options: [ OptionItem( - title: 'moma_postcard'.tr(), + title: 'featured_works'.tr(), icon: SvgPicture.asset( - 'assets/images/icon_3d.svg', + 'assets/images/icon_set.svg', colorFilter: const ColorFilter.mode(AppColor.white, BlendMode.srcIn), ), onTap: () { - Navigator.of(context).popAndPushNamed(AppRouter.momaPostcardPage); + Navigator.of(context) + .popAndPushNamed(AppRouter.featuredWorksPage); }, ), OptionItem( - title: 'projects'.tr(), + title: 'rnd'.tr(), icon: SvgPicture.asset( - 'assets/images/project_icon.svg', + 'assets/images/icon_3d.svg', colorFilter: const ColorFilter.mode(AppColor.white, BlendMode.srcIn), ), @@ -258,18 +262,20 @@ class HomeNavigationPageState extends State @override void initState() { - unawaited(injector().getIssuesAndAnnouncement()); super.initState(); + // since we moved to use bonsoir service, + // we don't need to wait for canvas service to init + injector().add(CanvasDeviceGetDevicesEvent(retry: true)); + unawaited(injector().getIssuesAndAnnouncement()); _initialTab = widget.payload.startedTab; _selectedIndex = _initialTab.index; NftCollectionBloc.eventController.stream.listen((event) async { switch (event.runtimeType) { - case ReloadEvent: - case GetTokensByOwnerEvent: - case UpdateTokensEvent: - case GetTokensBeforeByOwnerEvent: + case const (ReloadEvent): + case const (GetTokensByOwnerEvent): + case const (UpdateTokensEvent): + case const (GetTokensBeforeByOwnerEvent): nftBloc.add(event); - break; default: } }); @@ -286,10 +292,18 @@ class HomeNavigationPageState extends State _pages = [ CollectionHomePage(key: _collectionHomePageKey), OrganizeHomePage(key: _organizeHomePageKey), - MultiBlocProvider(providers: [ - BlocProvider.value( - value: ExhibitionBloc(injector())..add(GetAllExhibitionsEvent())), - ], child: const ExhibitionsPage()), + MultiBlocProvider( + providers: [ + BlocProvider.value( + value: injector()..add(GetAllExhibitionsEvent()), + ), + BlocProvider.value( + value: injector()..add(GetSubscriptionEvent()), + ), + ], + child: ExhibitionsPage( + key: _exhibitionsPageKey, + )), ScanQRPage( key: _scanQRPageKey, onHandleFinished: () async { @@ -300,12 +314,18 @@ class HomeNavigationPageState extends State if (!_configurationService.isReadRemoveSupport()) { unawaited(_showRemoveCustomerSupport()); } - OneSignal.shared - .setNotificationWillShowInForegroundHandler(_shouldShowNotifications); + OneSignal.shared.setNotificationWillShowInForegroundHandler((event) async { + await NotificationHandler.instance.shouldShowNotifications( + context, + event, + _pageController, + ); + }); injector().auditFirstLog(); OneSignal.shared.setNotificationOpenedHandler((openedResult) { Future.delayed(const Duration(milliseconds: 500), () { - unawaited(_handleNotificationClicked(openedResult.notification)); + unawaited(NotificationHandler.instance.handleNotificationClicked( + context, openedResult.notification, _pageController)); }); }); @@ -315,8 +335,6 @@ class HomeNavigationPageState extends State } WidgetsBinding.instance.addObserver(this); _fgbgSubscription = FGBGEvents.stream.listen(_handleForeBackground); - - unawaited(injector().init()); unawaited(_syncArtist()); } @@ -575,220 +593,21 @@ class HomeNavigationPageState extends State ); } - Future _shouldShowNotifications( - OSNotificationReceivedEvent event) async { - log.info('Receive notification: ${event.notification}'); - final data = event.notification.additionalData; - if (data == null) { - return; - } - if (_configurationService.isNotificationEnabled() != true) { - _configurationService.showNotifTip.value = true; - } - - switch (data['notification_type']) { - case 'customer_support_new_message': - case 'customer_support_close_issue': - final notificationIssueID = - '${event.notification.additionalData?['issue_id']}'; - injector().triggerReloadMessages.value += 1; - unawaited( - injector().getIssuesAndAnnouncement()); - if (notificationIssueID == memoryValues.viewingSupportThreadIssueID) { - event.complete(null); - return; - } - break; - - case 'gallery_new_nft': - case 'new_postcard_trip': - unawaited(_clientTokenService.refreshTokens()); - break; - case 'artwork_created': - case 'artwork_received': - break; - } - switch (data['notification_type']) { - case 'customer_support_new_announcement': - showInfoNotification( - const Key('Announcement'), 'au_has_announcement'.tr(), - addOnTextSpan: [ - TextSpan( - text: 'tap_to_view'.tr(), - style: Theme.of(context).textTheme.ppMori400FFYellow14), - ], openHandler: () async { - final announcementID = '${data["id"]}'; - unawaited(_openAnnouncement(announcementID)); - }); - break; - case 'new_message': - final groupId = data['group_id']; - - if (!_remoteConfig.getBool(ConfigGroup.viewDetail, ConfigKey.chat)) { - return; - } + Future _checkForKeySync(BuildContext context) async { + final cloudDatabase = injector(); + final defaultAccounts = await cloudDatabase.personaDao.getDefaultPersonas(); - final currentGroupId = memoryValues.currentGroupChatId; - if (groupId != currentGroupId) { - showNotifications(context, event.notification, - notificationOpenedHandler: _handleNotificationClicked); - } - break; - default: - showNotifications(context, event.notification, - notificationOpenedHandler: _handleNotificationClicked); + if (defaultAccounts.length >= 2) { + if (!context.mounted) { + return; + } + unawaited(Navigator.of(context).pushNamed(AppRouter.keySyncPage)); } - event.complete(null); } PageController _getPageController(int initialIndex) => PageController(initialPage: initialIndex); - Future _handleNotificationClicked(OSNotification notification) async { - if (notification.additionalData == null) { - // Skip handling the notification without data - return; - } - - log.info("Tap to notification: ${notification.body ?? "empty"} " - '\nAdditional data: ${notification.additionalData!}'); - final notificationType = notification.additionalData!['notification_type']; - switch (notificationType) { - case 'gallery_new_nft': - Navigator.of(context).popUntil((route) => - route.settings.name == AppRouter.homePage || - route.settings.name == AppRouter.homePageNoTransition); - _pageController?.jumpToPage(HomeNavigatorTab.collection.index); - break; - - case 'customer_support_new_message': - case 'customer_support_close_issue': - final issueID = '${notification.additionalData!["issue_id"]}'; - final announcement = await injector() - .findAnnouncementFromIssueId(issueID); - if (!mounted) { - return; - } - unawaited(Navigator.of(context).pushNamedAndRemoveUntil( - AppRouter.supportThreadPage, - (route) => - route.settings.name == AppRouter.homePage || - route.settings.name == AppRouter.homePageNoTransition, - arguments: DetailIssuePayload( - reportIssueType: '', - issueID: issueID, - announcement: announcement), - )); - break; - case 'customer_support_new_announcement': - final announcementID = '${notification.additionalData!["id"]}'; - unawaited(_openAnnouncement(announcementID)); - break; - - case 'artwork_created': - case 'artwork_received': - Navigator.of(context).popUntil((route) => - route.settings.name == AppRouter.homePage || - route.settings.name == AppRouter.homePageNoTransition); - _pageController?.jumpToPage(HomeNavigatorTab.collection.index); - break; - case 'new_message': - if (!_remoteConfig.getBool(ConfigGroup.viewDetail, ConfigKey.chat)) { - return; - } - final data = notification.additionalData; - if (data == null) { - return; - } - final tokenId = data['group_id']; - final tokens = await injector() - .assetTokenDao - .findAllAssetTokensByTokenIDs([tokenId]); - final owner = tokens.first.owner; - final isSkip = - injector().isConnecting(address: owner, id: tokenId); - if (isSkip) { - return; - } - final GlobalKey key = GlobalKey(); - final postcardDetailPayload = PostcardDetailPagePayload( - [ArtworkIdentity(tokenId, owner)], 0, - key: key); - if (!mounted) { - return; - } - unawaited(Navigator.of(context).pushNamed( - AppRouter.claimedPostcardDetailsPage, - arguments: postcardDetailPayload)); - Timer.periodic(const Duration(milliseconds: 100), (timer) async { - final state = key.currentState; - final assetToken = - key.currentContext?.read().state.assetToken; - if (state != null && assetToken != null) { - unawaited(state.gotoChatThread(key.currentContext!)); - timer.cancel(); - } - }); - - break; - case 'new_postcard_trip': - case 'postcard_share_expired': - final data = notification.additionalData; - if (data == null) { - return; - } - final indexID = data['indexID']; - final tokens = await injector() - .assetTokenDao - .findAllAssetTokensByTokenIDs([indexID]); - if (tokens.isEmpty) { - return; - } - final owner = tokens.first.owner; - final postcardDetailPayload = PostcardDetailPagePayload( - [ArtworkIdentity(indexID, owner)], - 0, - useIndexer: true, - ); - if (!mounted) { - return; - } - Navigator.of(context).popUntil((route) => - route.settings.name == AppRouter.homePage || - route.settings.name == AppRouter.homePageNoTransition); - unawaited(Navigator.of(context).pushNamed( - AppRouter.claimedPostcardDetailsPage, - arguments: postcardDetailPayload)); - break; - - default: - log.warning('unhandled notification type: $notificationType'); - break; - } - } - - Future _openAnnouncement(String announcementID) async { - log.info('Open announcement: id = $announcementID'); - await injector().fetchAnnouncement(); - final announcement = await injector() - .findAnnouncement(announcementID); - if (announcement != null) { - if (!mounted) { - return; - } - unawaited(Navigator.of(context).pushNamedAndRemoveUntil( - AppRouter.supportThreadPage, - (route) => - route.settings.name == AppRouter.homePage || - route.settings.name == AppRouter.homePageNoTransition, - arguments: NewIssuePayload( - reportIssueType: ReportIssueType.Announcement, - announcement: announcement, - ), - )); - } - } - void _handleBackground() { unawaited(_cloudBackup()); _metricClientService.onBackground(); @@ -800,69 +619,33 @@ class HomeNavigationPageState extends State unawaited(_handleForeground()); memoryValues.isForeground = true; unawaited(injector().reconnect()); - break; case FGBGType.background: _handleBackground(); memoryValues.isForeground = false; - break; - } - } - - Future showAnnouncementNotification( - AnnouncementLocal announcement) async { - showInfoNotification( - const Key('Announcement'), announcement.notificationTitle, - addOnTextSpan: [ - TextSpan( - text: 'tap_to_view'.tr(), - style: Theme.of(context).textTheme.ppMori400FFYellow14), - ], openHandler: () async { - final announcementID = announcement.announcementContextId; - unawaited(_openAnnouncement(announcementID)); - }); - } - - Future announcementNotificationIfNeed() async { - final announcements = - (await injector().getIssuesAndAnnouncement()) - .whereType() - .toList(); - - final showAnnouncementInfo = - _configurationService.getShowAnnouncementNotificationInfo(); - final shouldShowAnnouncements = announcements.where((element) => - (element.isMemento6 && - !_configurationService - .getAlreadyClaimedAirdrop(AirdropType.memento6.seriesId)) && - showAnnouncementInfo.shouldShowAnnouncementNotification(element)); - if (shouldShowAnnouncements.isEmpty) { - return; } - unawaited(Future.forEach(shouldShowAnnouncements, - (announcement) async { - await showAnnouncementNotification(announcement); - await _configurationService - .updateShowAnnouncementNotificationInfo(announcement); - })); } Future _handleForeground() async { _metricClientService.onForeground(); + injector().add(CanvasDeviceGetDevicesEvent(retry: true)); await injector().fetchAnnouncement(); - unawaited(announcementNotificationIfNeed()); await _remoteConfig.loadConfigs(); } @override - FutureOr afterFirstLayout(BuildContext context) { + FutureOr afterFirstLayout(BuildContext context) async { if (widget.payload.startedTab != _initialTab) { - _onItemTapped(widget.payload.startedTab.index); + await _onItemTapped(widget.payload.startedTab.index); } - _cloudBackup(); + await _cloudBackup(); final initialAction = _notificationService.initialAction; if (initialAction != null) { - NotificationService.onActionReceivedMethod(initialAction); + await nc.NotificationService.onActionReceivedMethod(initialAction); + } + if (!context.mounted) { + return; } + unawaited(_checkForKeySync(context)); } Future _cloudBackup() async { diff --git a/lib/screen/interactive_postcard/postcard_detail_page.dart b/lib/screen/interactive_postcard/postcard_detail_page.dart index e4018dfb41..4740edd5b5 100644 --- a/lib/screen/interactive_postcard/postcard_detail_page.dart +++ b/lib/screen/interactive_postcard/postcard_detail_page.dart @@ -14,6 +14,7 @@ import 'package:autonomy_flutter/common/environment.dart'; import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/model/pair.dart'; import 'package:autonomy_flutter/model/prompt.dart'; +import 'package:autonomy_flutter/model/sent_artwork.dart'; import 'package:autonomy_flutter/screen/app_router.dart'; import 'package:autonomy_flutter/screen/bloc/accounts/accounts_bloc.dart'; import 'package:autonomy_flutter/screen/bloc/identity/identity_bloc.dart'; @@ -30,6 +31,8 @@ import 'package:autonomy_flutter/screen/interactive_postcard/travel_info/postcar import 'package:autonomy_flutter/screen/interactive_postcard/travel_info/travel_info_bloc.dart'; import 'package:autonomy_flutter/screen/interactive_postcard/travel_info/travel_info_state.dart'; import 'package:autonomy_flutter/screen/irl_screen/webview_irl_screen.dart'; +import 'package:autonomy_flutter/screen/settings/crypto/send_artwork/send_artwork_page.dart'; +import 'package:autonomy_flutter/screen/settings/help_us/inapp_webview.dart'; import 'package:autonomy_flutter/service/auth_service.dart'; import 'package:autonomy_flutter/service/chat_service.dart'; import 'package:autonomy_flutter/service/configuration_service.dart'; @@ -48,6 +51,7 @@ import 'package:autonomy_flutter/util/moma_style_color.dart'; import 'package:autonomy_flutter/util/share_helper.dart'; import 'package:autonomy_flutter/util/string_ext.dart'; import 'package:autonomy_flutter/util/ui_helper.dart'; +import 'package:autonomy_flutter/util/wallet_storage_ext.dart'; import 'package:autonomy_flutter/view/artwork_common_widget.dart'; import 'package:autonomy_flutter/view/postcard_button.dart'; import 'package:autonomy_flutter/view/postcard_chat.dart'; @@ -76,7 +80,7 @@ class PostcardDetailPagePayload extends ArtworkDetailPayload { super.identities, super.currentIndex, { super.key, - super.playControl, + super.playlist, super.twitterCaption, this.isFromLeaderboard = false, super.useIndexer, @@ -204,7 +208,7 @@ class ClaimedPostcardDetailPageState extends State ), ], ), - ) + ), ]); } @@ -493,7 +497,7 @@ class ClaimedPostcardDetailPageState extends State toolbarHeight: 70, centerTitle: false, title: Text( - asset.title!, + asset.displayTitle!, style: theme.textTheme.moMASans400Black12, overflow: TextOverflow.ellipsis, ), @@ -988,11 +992,15 @@ class ClaimedPostcardDetailPageState extends State Future _showArtworkOptionsDialog( BuildContext context, AssetToken asset) async { final theme = Theme.of(context); - if (!mounted) { - return; - } + + final owner = await asset.getOwnerWallet(); + final ownerWallet = owner?.first; + final addressIndex = owner?.second; const isHidden = false; final isStamped = asset.isStamped; + if (!context.mounted) { + return; + } await UIHelper.showPostcardDrawerAction( context, options: [ @@ -1068,10 +1076,9 @@ class ClaimedPostcardDetailPageState extends State } Navigator.of(context).pop(); switch (e.runtimeType) { - case MediaPermissionException: + case MediaPermissionException _: await UIHelper.showPostcardStampPhotoAccessFailed( context); - break; default: if (!mounted) { return; @@ -1120,9 +1127,8 @@ class ClaimedPostcardDetailPageState extends State } Navigator.of(context).pop(); switch (e.runtimeType) { - case MediaPermissionException: + case MediaPermissionException _: await UIHelper.showPostcardPhotoAccessFailed(context); - break; default: if (!mounted) { return; @@ -1133,6 +1139,56 @@ class ClaimedPostcardDetailPageState extends State }, ), ], + if (ownerWallet != null && + asset.isTransferable && + asset.isCompleted) ...[ + OptionItem( + title: 'send_artwork'.tr(), + icon: SvgPicture.asset('assets/images/send_postcard.svg'), + onTap: () async { + final payload = await Navigator.of(context).popAndPushNamed( + AppRouter.sendArtworkPage, + arguments: SendArtworkPayload( + asset, + ownerWallet, + addressIndex!, + ownerWallet.getOwnedQuantity(asset))) as Map?; + if (payload == null) { + return; + } + + final sentQuantity = payload['sentQuantity'] as int; + final isSentAll = payload['isSentAll'] as bool; + unawaited(injector() + .updateRecentlySentToken([ + SentArtwork(asset.id, asset.owner, DateTime.now(), sentQuantity, + isSentAll) + ])); + if (!context.mounted) { + return; + } + setState(() {}); + if (!payload['isTezos']) { + if (isSentAll) { + unawaited(Navigator.of(context) + .popAndPushNamed(AppRouter.homePage)); + } + return; + } + unawaited(UIHelper.showMessageAction( + context, + 'success'.tr(), + 'send_success_des'.tr(), + closeButton: 'close'.tr(), + onClose: () => isSentAll + ? Navigator.of(context).popAndPushNamed( + AppRouter.homePage, + ) + : null, + )); + }, + ), + ], OptionItem( title: 'hide'.tr(), titleStyle: theme.textTheme.moMASans700Black16 diff --git a/lib/screen/interactive_postcard/stamp_preview.dart b/lib/screen/interactive_postcard/stamp_preview.dart index 02bc4df129..477a7ceb0a 100644 --- a/lib/screen/interactive_postcard/stamp_preview.dart +++ b/lib/screen/interactive_postcard/stamp_preview.dart @@ -162,7 +162,7 @@ class _StampPreviewState extends State with AfterLayoutMixin { backgroundColor: backgroundColor, appBar: getCloseAppBar( context, - title: widget.payload.asset.title ?? '', + title: widget.payload.asset.displayTitle ?? '', titleStyle: theme.textTheme.moMASans700Black16.copyWith( fontSize: 18, ), diff --git a/lib/screen/migration/key_sync_bloc.dart b/lib/screen/migration/key_sync_bloc.dart index b5bc8ba950..ac5fa4e1d0 100644 --- a/lib/screen/migration/key_sync_bloc.dart +++ b/lib/screen/migration/key_sync_bloc.dart @@ -5,10 +5,14 @@ // that can be found in the LICENSE file. // +import 'dart:async'; + import 'package:autonomy_flutter/au_bloc.dart'; import 'package:autonomy_flutter/database/cloud_database.dart'; import 'package:autonomy_flutter/screen/migration/key_sync_state.dart'; import 'package:autonomy_flutter/service/backup_service.dart'; +import 'package:autonomy_flutter/util/log.dart'; +import 'package:sentry/sentry.dart'; class KeySyncBloc extends AuBloc { final BackupService _backupService; @@ -26,41 +30,53 @@ class KeySyncBloc extends AuBloc { on((event, emit) async { emit(state.copyWith( - isProcessing: true, isLocalSelectedTmp: state.isLocalSelected)); + isProcessing: true, + isLocalSelectedTmp: state.isLocalSelected, + isError: false)); final accounts = await _cloudDatabase.personaDao.getDefaultPersonas(); - if (accounts.length < 2) return; + if (accounts.length < 2) { + return; + } final cloudWallet = accounts[1].wallet(); + try { + final cloudBackupVersion = + await _backupService.fetchBackupVersion(cloudWallet); - final cloudBackupVersion = - await _backupService.fetchBackupVersion(cloudWallet); + if (cloudBackupVersion.isNotEmpty) { + const tmpCloudDbName = 'tmp_cloud_database.db'; + await _backupService.restoreCloudDatabase( + cloudWallet, cloudBackupVersion, + dbName: tmpCloudDbName); - if (cloudBackupVersion.isNotEmpty) { - const tmpCloudDbName = 'tmp_cloud_database.db'; - await _backupService.restoreCloudDatabase( - cloudWallet, cloudBackupVersion, - dbName: tmpCloudDbName); + final tmpCloudDb = await $FloorCloudDatabase + .databaseBuilder(tmpCloudDbName) + .addMigrations(cloudDatabaseMigrations) + .build(); - final tmpCloudDb = await $FloorCloudDatabase - .databaseBuilder(tmpCloudDbName) - .addMigrations(cloudDatabaseMigrations) - .build(); - - final connections = await tmpCloudDb.connectionDao.getConnections(); - await _cloudDatabase.connectionDao.insertConnections(connections); - } + final connections = await tmpCloudDb.connectionDao.getConnections(); + await _cloudDatabase.connectionDao.insertConnections(connections); + } + log.info('ProceedKeySyncEvent done restore connection'); - if (state.isLocalSelected) { - final cloudDefaultPersona = accounts[1]; - await _backupService.deleteAllProfiles(cloudWallet); - cloudDefaultPersona.defaultAccount = null; - await _cloudDatabase.personaDao.updatePersona(cloudDefaultPersona); - } else { - final localDefaultPersona = accounts[0]; - await _backupService.deleteAllProfiles(localDefaultPersona.wallet()); - localDefaultPersona.defaultAccount = null; - await _cloudDatabase.personaDao.updatePersona(localDefaultPersona); + if (state.isLocalSelected) { + final cloudDefaultPersona = accounts[1]; + await _backupService.deleteAllProfiles(cloudWallet); + cloudDefaultPersona.defaultAccount = null; + await _cloudDatabase.personaDao.updatePersona(cloudDefaultPersona); + } else { + final localDefaultPersona = accounts[0]; + await _backupService.deleteAllProfiles(localDefaultPersona.wallet()); + localDefaultPersona.defaultAccount = null; + await _cloudDatabase.personaDao.updatePersona(localDefaultPersona); + } + } catch (e) { + log.info('ProceedKeySyncEvent select local' + ' ${state.isLocalSelected} error: $e'); + unawaited(Sentry.captureException('ProceedKeySyncEvent select local' + ' ${state.isLocalSelected} error: $e')); + emit(state.copyWith(isError: true, isProcessing: false)); } emit(state.copyWith(isProcessing: false)); diff --git a/lib/screen/migration/key_sync_page.dart b/lib/screen/migration/key_sync_page.dart index 0e7e71bb19..a6d74aa558 100644 --- a/lib/screen/migration/key_sync_page.dart +++ b/lib/screen/migration/key_sync_page.dart @@ -32,7 +32,7 @@ class KeySyncPage extends StatelessWidget { return BlocConsumer( listener: (context, state) async { - if (state.isProcessing == false) { + if (state.isProcessing == false && !state.isError) { Navigator.of(context).pop(); } }, diff --git a/lib/screen/migration/key_sync_state.dart b/lib/screen/migration/key_sync_state.dart index 854c321e5c..fe4b7f8bc1 100644 --- a/lib/screen/migration/key_sync_state.dart +++ b/lib/screen/migration/key_sync_state.dart @@ -22,16 +22,20 @@ class ProceedKeySyncEvent extends KeySyncEvent {} class KeySyncState { final bool isLocalSelected; final bool? isProcessing; + final bool isError; bool isLocalSelectedTmp; - KeySyncState( - this.isLocalSelected, this.isProcessing, this.isLocalSelectedTmp); + KeySyncState(this.isLocalSelected, this.isProcessing, this.isLocalSelectedTmp, + {this.isError = false}); KeySyncState copyWith( - {bool? isLocalSelected, bool? isProcessing, bool? isLocalSelectedTmp}) { - return KeySyncState( - isLocalSelected ?? this.isLocalSelected, - isProcessing ?? this.isProcessing, - isLocalSelectedTmp ?? this.isLocalSelectedTmp); - } + {bool? isLocalSelected, + bool? isProcessing, + bool? isLocalSelectedTmp, + bool? isError}) => + KeySyncState( + isLocalSelected ?? this.isLocalSelected, + isProcessing ?? this.isProcessing, + isLocalSelectedTmp ?? this.isLocalSelectedTmp, + isError: isError ?? this.isError); } diff --git a/lib/screen/moma_postcard_page/moma_postcard_page.dart b/lib/screen/moma_postcard_page/moma_postcard_page.dart index 9da641df3a..63528581d3 100644 --- a/lib/screen/moma_postcard_page/moma_postcard_page.dart +++ b/lib/screen/moma_postcard_page/moma_postcard_page.dart @@ -7,11 +7,11 @@ import 'dart:async'; +import 'package:autonomy_flutter/common/environment.dart'; import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/screen/app_router.dart'; import 'package:autonomy_flutter/screen/detail/artwork_detail_page.dart'; import 'package:autonomy_flutter/screen/interactive_postcard/postcard_detail_page.dart'; -import 'package:autonomy_flutter/service/client_token_service.dart'; import 'package:autonomy_flutter/util/asset_token_ext.dart'; import 'package:autonomy_flutter/util/constants.dart'; import 'package:autonomy_flutter/util/style.dart'; @@ -35,7 +35,7 @@ class MoMAPostcardPage extends StatefulWidget { class _MoMAPostcardPageState extends State { int _cachedImageSize = 0; - final nftBloc = injector().nftBloc; + final nftBloc = injector.get(param1: false); @override void initState() { @@ -44,16 +44,18 @@ class _MoMAPostcardPageState extends State { List _updateTokens(List tokens) { List filteredTokens = tokens.filterAssetToken(); - final filteredPostcards = - filteredTokens.where((element) => element.isPostcard).toList(); - final nextKey = nftBloc.state.nextKey; if (nextKey != null && !nextKey.isLoaded && - filteredPostcards.length < COLLECTION_INITIAL_MIN_SIZE) { - nftBloc.add(GetTokensByOwnerEvent(pageKey: nextKey)); + filteredTokens.length < COLLECTION_INITIAL_MIN_SIZE) { + nftBloc.add( + GetTokensByOwnerEvent( + pageKey: nextKey, + contractAddress: Environment.postcardContractAddress, + ), + ); } - return filteredPostcards; + return filteredTokens; } @override @@ -74,12 +76,11 @@ class _MoMAPostcardPageState extends State { top: false, bottom: false, child: Scaffold( - appBar: getBackAppBar( + backgroundColor: AppColor.primaryBlack, + appBar: getFFAppBar( context, - title: 'moma_postcard'.tr(), - onBack: () { - Navigator.of(context).pop(); - }, + onBack: () => Navigator.pop(context), + title: Text('moma_postcard'.tr()), ), body: Column( children: [ @@ -154,6 +155,7 @@ class _MoMAPostcardPageState extends State { ? PendingTokenWidget( thumbnail: asset.galleryThumbnailURL, tokenId: asset.tokenId, + shouldRefreshCache: asset.shouldRefreshThumbnailCache, ) : tokenGalleryThumbnailWidget( context, diff --git a/lib/screen/playlists/add_new_playlist/add_new_playlist.dart b/lib/screen/playlists/add_new_playlist/add_new_playlist.dart index 5b61421da4..19357eae9a 100644 --- a/lib/screen/playlists/add_new_playlist/add_new_playlist.dart +++ b/lib/screen/playlists/add_new_playlist/add_new_playlist.dart @@ -166,7 +166,7 @@ class _AddNewPlaylistScreenState extends State final isDone = playlistName.isNotEmpty && selectedIDs?.isNotEmpty == true; return Scaffold( - backgroundColor: theme.colorScheme.background, + backgroundColor: AppColor.white, appBar: getCustomDoneAppBar( context, title: TextFieldWidget( diff --git a/lib/screen/playlists/add_to_playlist/add_to_playlist.dart b/lib/screen/playlists/add_to_playlist/add_to_playlist.dart index 7270170d7c..8347530ed4 100644 --- a/lib/screen/playlists/add_to_playlist/add_to_playlist.dart +++ b/lib/screen/playlists/add_to_playlist/add_to_playlist.dart @@ -10,6 +10,7 @@ import 'package:autonomy_flutter/screen/playlists/add_new_playlist/add_new_playl import 'package:autonomy_flutter/service/account_service.dart'; import 'package:autonomy_flutter/service/configuration_service.dart'; import 'package:autonomy_flutter/util/constants.dart'; +import 'package:autonomy_flutter/util/string_ext.dart'; import 'package:autonomy_flutter/util/style.dart'; import 'package:autonomy_flutter/util/token_ext.dart'; import 'package:autonomy_flutter/view/artwork_common_widget.dart'; @@ -174,28 +175,43 @@ class _AddToCollectionScreenState extends State .length; return Scaffold( backgroundColor: theme.colorScheme.background, - appBar: getDoneAppBar( + appBar: getPlaylistAppBar( context, - title: 'adding_to'.tr(namedArgs: { - 'title': widget.playList.getName(), - }), - onDone: (selectedCount > 0) - ? () { - bloc.add( - CreatePlaylist( - name: widget.playList.name ?? '', - ), - ); - } - : null, - onCancel: () { - Navigator.pop(context); - }, - bottom: PreferredSize( - preferredSize: const Size.fromHeight(0.25), - child: - addOnlyDivider(color: AppColor.auQuickSilver, border: 0.25), + title: Column( + children: [ + Text( + tr('adding_to').capitalize(), + style: theme.textTheme.ppMori400White14, + ), + const SizedBox( + height: 4, + ), + Text( + widget.playList.getName(), + style: theme.textTheme.ppMori700White14, + ), + ], ), + actions: [ + GestureDetector( + onTap: () { + selectedCount > 0 + ? bloc.add( + CreatePlaylist( + name: widget.playList.name ?? '', + ), + ) + : null; + }, + child: Text( + tr('done').capitalize(), + style: theme.textTheme.ppMori400White14, + ), + ), + const SizedBox( + width: 15, + ), + ], ), body: AnnotatedRegion( value: SystemUiOverlayStyle.light, diff --git a/lib/screen/playlists/edit_playlist/edit_playlist.dart b/lib/screen/playlists/edit_playlist/edit_playlist.dart index 8bc7c3f14c..9c5831ea79 100644 --- a/lib/screen/playlists/edit_playlist/edit_playlist.dart +++ b/lib/screen/playlists/edit_playlist/edit_playlist.dart @@ -9,6 +9,7 @@ import 'package:autonomy_flutter/screen/playlists/edit_playlist/edit_playlist_st import 'package:autonomy_flutter/screen/playlists/edit_playlist/widgets/edit_playlist_gridview.dart'; import 'package:autonomy_flutter/screen/playlists/edit_playlist/widgets/text_name_playlist.dart'; import 'package:autonomy_flutter/screen/playlists/view_playlist/view_playlist.dart'; +import 'package:autonomy_flutter/service/configuration_service.dart'; import 'package:autonomy_flutter/service/navigation_service.dart'; import 'package:autonomy_flutter/service/playlist_service.dart'; import 'package:autonomy_flutter/service/settings_data_service.dart'; @@ -20,7 +21,6 @@ import 'package:autonomy_flutter/util/style.dart'; import 'package:autonomy_flutter/util/token_ext.dart'; import 'package:autonomy_flutter/util/ui_helper.dart'; import 'package:autonomy_flutter/view/back_appbar.dart'; -import 'package:autonomy_flutter/view/primary_button.dart'; import 'package:autonomy_flutter/view/responsive.dart'; import 'package:autonomy_flutter/view/search_bar.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -28,6 +28,7 @@ import 'package:feralfile_app_theme/feral_file_app_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; import 'package:nft_collection/models/asset_token.dart'; import 'package:nft_collection/nft_collection.dart'; @@ -48,6 +49,7 @@ class _EditPlaylistScreenState extends State { late bool _showSearchBar; late String _searchText; late ScrollController _controller; + bool isDemo = injector.get().isDemoArtworksMode(); @override void initState() { @@ -58,9 +60,7 @@ class _EditPlaylistScreenState extends State { _controller.addListener(_scrollListenerToShowSearchBar); nftBloc.add(RefreshNftCollectionByIDs(ids: widget.playListModel?.tokenIDs)); bloc.add(InitPlayList( - playListModel: widget.playListModel?.copyWith( - tokenIDs: List.from(widget.playListModel?.tokenIDs ?? []), - ), + playListModel: widget.playListModel, )); } @@ -110,11 +110,11 @@ class _EditPlaylistScreenState extends State { } void onSave(PlayListModel? playList) { - final thubnailUrl = tokensPlaylist + final thumbnailUrl = tokensPlaylist .where((element) => element.id == playList?.tokenIDs.firstOrDefault()) .firstOrDefault() ?.getGalleryThumbnailUrl(); - playList?.thumbnailURL = thubnailUrl; + playList?.thumbnailURL = thumbnailUrl; bloc.add(SavePlaylist()); } @@ -158,13 +158,7 @@ class _EditPlaylistScreenState extends State { bloc: bloc, listener: (context, state) async { if (state.isAddSuccess ?? false) { - injector().popUntilHomeOrSettings(); - await Navigator.pushNamed( - context, - AppRouter.viewPlayListPage, - arguments: - ViewPlaylistScreenPayload(playListModel: state.playListModel), - ); + Navigator.pop(context, state.playListModel); } }, builder: (context, state) { @@ -172,30 +166,31 @@ class _EditPlaylistScreenState extends State { final selectedItem = state.selectedItem ?? []; return Scaffold( - appBar: getCustomDoneAppBar( - context, - title: TextNamePlaylist( - focusNode: _focusNode, - playList: playList, - onEditPlaylistName: (value) { - bloc.add(UpdateNamePlaylist(name: value)); - }, - ), - onDone: () { - onSave(playList); - }, - onCancel: () { - Navigator.of(context).pop(); - }, - bottom: PreferredSize( - preferredSize: const Size.fromHeight(0.25), - child: - addOnlyDivider(color: AppColor.auQuickSilver, border: 0.25), - ), - ), + backgroundColor: AppColor.primaryBlack, + appBar: getPlaylistAppBar(context, + title: TextNamePlaylist( + focusNode: _focusNode, + playList: playList, + onEditPlaylistName: (value) { + bloc.add(UpdateNamePlaylist(name: value)); + }, + ), + actions: [ + GestureDetector( + onTap: () { + onSave(playList); + }, + child: Text( + tr('done').capitalize(), + style: theme.textTheme.ppMori400White14, + ), + ), + const SizedBox(width: 15), + ]), body: SafeArea( bottom: false, child: Stack( + alignment: Alignment.center, children: [ BlocConsumer( bloc: nftBloc, @@ -303,75 +298,90 @@ class _EditPlaylistScreenState extends State { bloc: nftBloc, builder: (context, nftState) => Positioned( bottom: 30, - child: SizedBox( - width: MediaQuery.of(context).size.width, - child: Center( + child: Align( + alignment: Alignment.bottomCenter, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 25, vertical: 13), + decoration: BoxDecoration( + color: AppColor.auGreyBackground, + borderRadius: BorderRadius.circular(50), + ), + alignment: Alignment.center, child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisAlignment: MainAxisAlignment.center, children: [ - PrimaryButton( + AddButton( + icon: SvgPicture.asset( + 'assets/images/joinFile.svg', + width: 24, + height: 24, + colorFilter: const ColorFilter.mode( + AppColor.white, BlendMode.srcIn), + ), + onTap: () async { + await moveToAddNftToCollection(context); + }, + ), + const SizedBox(width: 25), + AddButton( + icon: SvgPicture.asset( + 'assets/images/rename_icon.svg', + width: 24, + height: 24, + colorFilter: const ColorFilter.mode( + AppColor.white, BlendMode.srcIn), + ), onTap: () async { - selectedItem.isEmpty - ? null - : await UIHelper.showMessageActionNew( - context, - tr('remove_from_list'), - '', - descriptionWidget: RichText( - text: TextSpan( - children: [ - TextSpan( - style: theme - .textTheme.ppMori400White16, - text: 'you_are_about_to_remove' - .tr(), - ), - TextSpan( - style: theme - .textTheme.ppMori700White16, - text: tr( - selectedItem.length != 1 - ? 'artworks' - : 'artwork', - args: [ - selectedItem.length - .toString() - ], - ), - ), - TextSpan( - style: theme - .textTheme.ppMori400White16, - text: 'from_the_playlist'.tr(), - ), - TextSpan( - style: theme - .textTheme.ppMori700White16, - text: playList?.name ?? - tr('untitled'), - ), - TextSpan( - style: theme - .textTheme.ppMori400White16, - text: 'they_will_remain'.tr(), - ), - ], - ), - ), - actionButton: 'remove'.tr(), - onAction: () { - Navigator.pop(context); - bloc.add( - RemoveTokens( - tokenIDs: selectedItem, - ), - ); - }, - ); + _editPlaylistName(); }, - width: 170, - text: tr('remove').capitalize(), - color: AppColor.feralFileHighlight, + ), + const SizedBox(width: 25), + Stack( + alignment: Alignment.center, + children: [ + AddButton( + icon: SvgPicture.asset( + 'assets/images/trash_white.svg', + width: 24, + height: 24, + colorFilter: const ColorFilter.mode( + AppColor.white, BlendMode.srcIn), + ), + iconOnDisabled: SvgPicture.asset( + 'assets/images/trash_disable.svg', + width: 24, + height: 24, + ), + onTap: () async { + await _removeSelectedToken( + context, + selectedItem: selectedItem, + playlist: widget.playListModel!, + ); + }, + isEnable: selectedItem.isNotEmpty, + ), + if (selectedItem.isNotEmpty) ...[ + Container( + height: 14, + width: + 14 + (selectedItem.length > 9 ? 3 : 0), + decoration: BoxDecoration( + color: Colors.red, + borderRadius: BorderRadius.circular(50), + ), + padding: EdgeInsets.fromLTRB(3, 2, 3, 4), + child: Center( + child: Text( + getTextNumber(selectedItem.length), + style: theme.textTheme.ppMori400White12 + .copyWith(fontSize: 8), + ), + ), + ) + ], + ], ), ], ), @@ -387,6 +397,87 @@ class _EditPlaylistScreenState extends State { ); } + String getTextNumber(int num) { + return num > 9 ? "+9" : "$num"; + } + + Future moveToAddNftToCollection(BuildContext context) async { + await Navigator.pushNamed( + context, + AppRouter.addToCollectionPage, + arguments: bloc.state.playListModel?.copyWith( + tokenIDs: bloc.state.playListModel?.tokenIDs, + ), + ).then((value) { + if (value != null && value is PlayListModel) { + bloc.state.playListModel = bloc.state.playListModel?.copyWith( + tokenIDs: value.tokenIDs?.toList(), + name: value.name, + ); + bloc.add(UpdateNamePlaylist(name: value.name ?? '')); + nftBloc.add(RefreshNftCollectionByIDs( + ids: isDemo ? [] : value.tokenIDs, + debugTokenIds: isDemo ? value.tokenIDs : [], + )); + } + }); + } + + Future _removeSelectedToken(BuildContext context, + {required List selectedItem, + required PlayListModel playlist}) async { + final theme = Theme.of(context); + return selectedItem.isEmpty + ? null + : await UIHelper.showMessageActionNew( + context, + tr('remove_from_list'), + '', + descriptionWidget: RichText( + text: TextSpan( + children: [ + TextSpan( + style: theme.textTheme.ppMori400White16, + text: 'you_are_about_to_remove'.tr(), + ), + TextSpan( + style: theme.textTheme.ppMori700White16, + text: tr( + selectedItem.length != 1 ? 'artworks' : 'artwork', + args: [selectedItem.length.toString()], + ), + ), + TextSpan( + style: theme.textTheme.ppMori400White16, + text: 'from_the_playlist'.tr(), + ), + TextSpan( + style: theme.textTheme.ppMori700White16, + text: playlist.name ?? tr('untitled'), + ), + TextSpan( + style: theme.textTheme.ppMori400White16, + text: 'they_will_remain'.tr(), + ), + ], + ), + ), + actionButton: 'remove'.tr(), + onAction: () { + Navigator.pop(context); + bloc.add( + RemoveTokens( + tokenIDs: selectedItem, + ), + ); + }, + ); + } + + void _editPlaylistName() { + _focusNode.requestFocus(); + } + Widget tokenEmptyAction(ThemeData theme, PlayListModel? playList) => Row( children: [ Text( diff --git a/lib/screen/playlists/edit_playlist/widgets/text_name_playlist.dart b/lib/screen/playlists/edit_playlist/widgets/text_name_playlist.dart index bc5e3a3274..4a5d1fc557 100644 --- a/lib/screen/playlists/edit_playlist/widgets/text_name_playlist.dart +++ b/lib/screen/playlists/edit_playlist/widgets/text_name_playlist.dart @@ -49,10 +49,11 @@ class _TextNamePlaylistState extends State { focusNode: widget.focusNode, hintText: tr('untitled'), controller: _playlistNameC, - cursorColor: theme.colorScheme.primary, - style: theme.textTheme.ppMori400Black14, - hintStyle: theme.textTheme.ppMori400Grey14, - textAlign: TextAlign.center, + cursorColor: AppColor.white, + style: theme.textTheme.ppMori700Black36.copyWith(color: AppColor.white), + hintStyle: theme.textTheme.ppMori700Black36 + .copyWith(color: AppColor.disabledColor), + textAlign: TextAlign.left, border: InputBorder.none, onChanged: (value) { widget.onEditPlaylistName?.call(value); diff --git a/lib/screen/playlists/list_playlists/list_playlists.dart b/lib/screen/playlists/list_playlists/list_playlists.dart index f481112989..3a71e392ae 100644 --- a/lib/screen/playlists/list_playlists/list_playlists.dart +++ b/lib/screen/playlists/list_playlists/list_playlists.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/main.dart'; import 'package:autonomy_flutter/model/play_list_model.dart'; @@ -6,10 +8,13 @@ import 'package:autonomy_flutter/screen/playlists/view_playlist/view_playlist.da import 'package:autonomy_flutter/service/configuration_service.dart'; import 'package:autonomy_flutter/util/au_icons.dart'; import 'package:autonomy_flutter/util/collection_ext.dart'; +import 'package:autonomy_flutter/view/artwork_common_widget.dart'; +import 'package:autonomy_flutter/view/title_text.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:feralfile_app_theme/feral_file_app_theme.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; class ListPlaylistsScreen extends StatefulWidget { final ValueNotifier?> playlists; @@ -31,6 +36,7 @@ class ListPlaylistsScreen extends StatefulWidget { class _ListPlaylistsScreenState extends State with RouteAware, WidgetsBindingObserver { final isDemo = injector.get().isDemoArtworksMode(); + static const int _playlistNumberBreakpoint = 6; @override void initState() { @@ -58,52 +64,77 @@ class _ListPlaylistsScreenState extends State if (value == null) { return const SizedBox.shrink(); } - List playlists = value.filter(widget.filter); + List playlists = + value.filter(widget.filter).reversed.toList(); if (playlists.isEmpty && widget.filter.isNotEmpty) { return const SizedBox(); } - const height = 165.0; - return SizedBox( - height: height, - width: 400, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 15), - child: ListView.separated( - scrollDirection: Axis.horizontal, - itemCount: playlists.length + 1, - itemBuilder: (context, index) { - if (index == playlists.length) { - if (widget.filter.isNotEmpty) { - return const SizedBox(); - } - return AddPlayListItem( - onTap: () { - widget.onAdd(); - }, - ); - } - final item = playlists[index]; - return PlaylistItem( - playlist: item, - onSelected: () async => Navigator.pushNamed( - context, - AppRouter.viewPlayListPage, - arguments: ViewPlaylistScreenPayload(playListModel: item), - ), - ); - }, - separatorBuilder: (context, index) => const SizedBox(width: 10), - ), + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.filter.isNotEmpty) + TitleText(title: 'playlists'.tr()), + const SizedBox(height: 30), + _playlistHorizontalGridView(context, playlists) + ], ), ); }, ); + + Widget _playlistHorizontalGridView( + BuildContext context, List playlists) { + final rowNumber = playlists.length > _playlistNumberBreakpoint ? 2 : 1; + final height = PlaylistItem.height * rowNumber + 15 * (rowNumber - 1); + final length = playlists.length + 1; + return SizedBox( + height: height, + child: GridView.builder( + scrollDirection: Axis.horizontal, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: rowNumber, + crossAxisSpacing: 15, + mainAxisSpacing: 15, + childAspectRatio: PlaylistItem.height / PlaylistItem.width, + ), + itemBuilder: (context, index) { + if (index == 0) { + return AddPlayListItem( + onTap: () { + widget.onAdd(); + }, + ); + } + final item = playlists[index - 1]; + return PlaylistItem( + key: ValueKey(item.id), + playlist: item, + onSelected: () { + onPlaylistTap(item); + }); + }, + itemCount: length, + ), + ); + } + + void onPlaylistTap(PlayListModel playlist) { + unawaited(Navigator.pushNamed( + context, + AppRouter.viewPlayListPage, + arguments: ViewPlaylistScreenPayload(playListModel: playlist), + )); + } } class PlaylistItem extends StatefulWidget { final Function()? onSelected; final PlayListModel playlist; final bool onHold; + static const double width = 140; + static const double height = 165; const PlaylistItem({ required this.playlist, @@ -123,15 +154,13 @@ class _PlaylistItemState extends State { final numberFormatter = NumberFormat('#,###'); final thumbnailURL = widget.playlist.thumbnailURL; final name = widget.playlist.getName(); - const width = 140.0; - const height = 165.0; return GestureDetector( onTap: widget.onSelected, child: Padding( padding: EdgeInsets.zero, child: Container( - width: width, - height: height, + width: PlaylistItem.width, + height: PlaylistItem.height, decoration: BoxDecoration( color: AppColor.white, border: Border.all( @@ -177,12 +206,14 @@ class _PlaylistItemState extends State { : CachedNetworkImage( imageUrl: thumbnailURL, fit: BoxFit.cover, + cacheManager: injector(), + placeholder: (context, url) => + const GalleryThumbnailPlaceholder(), errorWidget: (context, url, error) => Container( width: double.infinity, height: double.infinity, color: theme.disableColor, ), - fadeInDuration: Duration.zero, ), ), ), diff --git a/lib/screen/playlists/view_playlist/view_playlist.dart b/lib/screen/playlists/view_playlist/view_playlist.dart index 5d0d98098e..be3a95bda5 100644 --- a/lib/screen/playlists/view_playlist/view_playlist.dart +++ b/lib/screen/playlists/view_playlist/view_playlist.dart @@ -9,23 +9,26 @@ import 'package:autonomy_flutter/screen/detail/preview/canvas_device_bloc.dart'; import 'package:autonomy_flutter/screen/interactive_postcard/postcard_detail_page.dart'; import 'package:autonomy_flutter/screen/playlists/view_playlist/view_playlist_bloc.dart'; import 'package:autonomy_flutter/screen/playlists/view_playlist/view_playlist_state.dart'; +import 'package:autonomy_flutter/service/canvas_client_service_v2.dart'; import 'package:autonomy_flutter/service/configuration_service.dart'; import 'package:autonomy_flutter/service/navigation_service.dart'; import 'package:autonomy_flutter/service/playlist_service.dart'; import 'package:autonomy_flutter/service/settings_data_service.dart'; import 'package:autonomy_flutter/util/asset_token_ext.dart'; -import 'package:autonomy_flutter/util/au_icons.dart'; import 'package:autonomy_flutter/util/constants.dart'; import 'package:autonomy_flutter/util/iterable_ext.dart'; -import 'package:autonomy_flutter/util/style.dart'; +import 'package:autonomy_flutter/util/log.dart'; +import 'package:autonomy_flutter/util/playlist_ext.dart'; import 'package:autonomy_flutter/util/token_ext.dart'; import 'package:autonomy_flutter/util/ui_helper.dart'; import 'package:autonomy_flutter/view/artwork_common_widget.dart'; -import 'package:autonomy_flutter/view/au_radio_button.dart'; import 'package:autonomy_flutter/view/back_appbar.dart'; +import 'package:autonomy_flutter/view/cast_button.dart'; import 'package:autonomy_flutter/view/responsive.dart'; +import 'package:autonomy_flutter/view/stream_common_widget.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:feralfile_app_theme/feral_file_app_theme.dart'; +import 'package:feralfile_app_tv_proto/feralfile_app_tv_proto.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; @@ -63,34 +66,11 @@ class _ViewPlaylistScreenState extends State { bool isDemo = injector.get().isDemoArtworksMode(); final _focusNode = FocusNode(); late CanvasDeviceBloc _canvasDeviceBloc; - late SortOrder _sortOrder; late bool editable; - - List _getAvailableOrders() { - switch (widget.payload.collectionType) { - case CollectionType.artist: - return [ - SortOrder.title, - SortOrder.newest, - ]; - case CollectionType.medium: - return [ - SortOrder.title, - SortOrder.artist, - SortOrder.newest, - ]; - default: - return [ - SortOrder.manual, - SortOrder.title, - SortOrder.artist, - ]; - } - } + final _canvasClientServiceV2 = injector(); @override void initState() { - _sortOrder = _getAvailableOrders().first; editable = widget.payload.collectionType == CollectionType.manual && !(widget.payload.playListModel?.isDefault ?? true); super.initState(); @@ -100,8 +80,7 @@ class _ViewPlaylistScreenState extends State { debugTokenIds: isDemo ? widget.payload.playListModel?.tokenIDs : [], )); - _canvasDeviceBloc = context.read(); - unawaited(_fetchDevice()); + _canvasDeviceBloc = injector.get(); bloc.add(GetPlayList(playListModel: widget.payload.playListModel)); } @@ -114,7 +93,7 @@ class _ViewPlaylistScreenState extends State { injector().popUntilHomeOrSettings(); } - List setupPlayList({ + List _setupPlayList({ required List tokens, List? selectedTokens, }) { @@ -127,11 +106,7 @@ class _ViewPlaylistScreenState extends State { [] ..removeWhere((element) => element == null); - tokensPlaylist = List.from(temp) - ..sort((a, b) { - final x = _sortOrder.compare(a, b); - return x; - }); + tokensPlaylist = List.from(temp); accountIdentities = tokensPlaylist .where((e) => e.pending != true || e.hasMetadata) @@ -146,116 +121,6 @@ class _ViewPlaylistScreenState extends State { super.dispose(); } - void _onSelectOrder(SortOrder order) { - setState(() { - _sortOrder = order; - }); - } - - Future _onOrderTap(BuildContext context, List orders) async { - final theme = Theme.of(context); - await showModalBottomSheet( - context: context, - backgroundColor: Colors.transparent, - enableDrag: false, - constraints: BoxConstraints( - maxWidth: ResponsiveLayout.isMobile - ? double.infinity - : Constants.maxWidthModalTablet), - barrierColor: Colors.black.withOpacity(0.5), - isScrollControlled: true, - builder: (context) => StatefulBuilder( - builder: (context, setState) => Container( - color: AppColor.feralFileHighlight, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(15, 17, 15, 20), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.only(top: 3, left: 37), - child: Text( - 'sort_by'.tr(), - style: theme.textTheme.ppMori400Black14, - overflow: TextOverflow.ellipsis, - ), - ), - ), - Align( - alignment: Alignment.centerRight, - child: SizedBox( - height: 28, - width: 28, - child: IconButton( - onPressed: () => Navigator.pop(context), - padding: const EdgeInsets.all(0), - icon: const Icon( - AuIcon.close, - size: 18, - color: AppColor.primaryBlack, - weight: 2, - ), - ), - ), - ), - ], - ), - ), - addOnlyDivider(color: AppColor.white), - const SizedBox(height: 20), - ListView.separated( - itemBuilder: (context, index) { - final order = orders[index]; - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 15), - child: GestureDetector( - onTap: () { - Navigator.pop(context); - _onSelectOrder(order); - }, - child: Container( - color: Colors.transparent, - child: Row( - children: [ - AuRadio( - onTap: (order) { - Navigator.pop(context); - _onSelectOrder(order); - }, - value: order, - groupValue: _sortOrder, - ), - const SizedBox(width: 15), - Expanded( - child: Text( - order.text, - style: theme.textTheme.ppMori400Black14, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ), - ), - ); - }, - itemCount: orders.length, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - separatorBuilder: (BuildContext context, int index) => - const SizedBox(height: 15), - ), - const SizedBox(height: 65), - ], - ), - )), - ); - } - Future _onMoreTap(BuildContext context, PlayListModel? playList) async { final theme = Theme.of(context); await UIHelper.showDrawerAction( @@ -267,16 +132,28 @@ class _ViewPlaylistScreenState extends State { 'assets/images/rename_icon.svg', width: 24, ), - onTap: () { + onTap: () async { Navigator.pop(context); if (isDemo) { return; } - Navigator.pushNamed( + await Navigator.pushNamed( context, AppRouter.editPlayListPage, - arguments: playList, - ); + arguments: playList?.copyWith( + tokenIDs: playList.tokenIDs?.toList(), + ), + ).then((value) { + if (value != null) { + final playListModel = value as PlayListModel; + bloc.state.playListModel?.tokenIDs = playListModel.tokenIDs; + bloc.add(SavePlaylist(name: playListModel.name)); + nftBloc.add(RefreshNftCollectionByIDs( + ids: isDemo ? [] : value.tokenIDs, + debugTokenIds: isDemo ? value.tokenIDs : [], + )); + } + }); }, ), OptionItem( @@ -317,114 +194,128 @@ class _ViewPlaylistScreenState extends State { UpdatePlayControl(playControlModel: playControlModel.onChangeTime())); } + Widget _appBarTitle(BuildContext context, PlayListModel playList) { + final theme = Theme.of(context); + return Row( + children: [ + if (widget.payload.titleIcon != null) ...[ + SizedBox(width: 22, height: 22, child: widget.payload.titleIcon), + const SizedBox(width: 10), + Text( + playList.getName(), + style: theme.textTheme.ppMori700Black36 + .copyWith(color: AppColor.white), + ), + ] else ...[ + Expanded( + child: Text( + playList.getName(), + style: theme.textTheme.ppMori700Black36 + .copyWith(color: AppColor.white), + textAlign: TextAlign.left, + ), + ), + ] + ], + ); + } + + List _appBarAction(BuildContext context, PlayListModel playList) => [ + if (editable) ...[ + const SizedBox(width: 15), + GestureDetector( + onTap: () async => _onMoreTap(context, playList), + child: SvgPicture.asset( + 'assets/images/more_circle.svg', + colorFilter: + const ColorFilter.mode(AppColor.white, BlendMode.srcIn), + width: 24, + )), + ], + if (_getDisplayKey(playList) != null) ...[ + const SizedBox(width: 15), + FFCastButton( + displayKey: _getDisplayKey(playList)!, + onDeviceSelected: (device) async { + final listTokenIds = playList.tokenIDs; + if (listTokenIds == null) { + log.info('Playlist tokenIds is null'); + return; + } + final duration = speedValues.values.first.inMilliseconds; + final listPlayArtwork = listTokenIds + .map((e) => PlayArtworkV2( + token: CastAssetToken(id: e), duration: duration)) + .toList(); + _canvasDeviceBloc.add(CanvasDeviceChangeControlDeviceEvent( + device, listPlayArtwork)); + }, + ), + ], + const SizedBox(width: 15), + ]; + @override Widget build(BuildContext context) { - final theme = Theme.of(context); + Theme.of(context); return BlocConsumer( bloc: bloc, listener: (context, state) {}, builder: (context, state) { - final playList = state.playListModel; - if (playList == null) { + if (state.playListModel == null) { return const SizedBox(); } + + final PlayListModel playList = state.playListModel!; return Scaffold( - appBar: AppBar( - systemOverlayStyle: systemUiOverlayLightStyle(AppColor.white), - elevation: 0, - shadowColor: Colors.transparent, - leading: GestureDetector( - onTap: () => Navigator.of(context).pop(), - child: const Row( - children: [ - SizedBox( - width: 15, - ), - Icon( - AuIcon.chevron, - color: AppColor.secondaryDimGrey, - size: 18, - ), - ], - ), - ), - leadingWidth: editable ? 90 : 55, - titleSpacing: 0, - backgroundColor: theme.colorScheme.background, - automaticallyImplyLeading: false, - centerTitle: true, - title: Row( - mainAxisAlignment: MainAxisAlignment.center, + backgroundColor: AppColor.primaryBlack, + appBar: getPlaylistAppBar( + context, + title: _appBarTitle(context, playList), + actions: _appBarAction(context, playList), + ), + body: BlocBuilder( + bloc: nftBloc, + builder: (context, nftState) => Column( children: [ - if (widget.payload.titleIcon != null) ...[ - SizedBox( - width: 22, height: 22, child: widget.payload.titleIcon), - const SizedBox(width: 10), - Text( - playList.getName(), - style: theme.textTheme.ppMori400Black16, - ), - ] else ...[ - Expanded( - child: Text( - playList.getName(), - style: theme.textTheme.ppMori400Black16, - textAlign: TextAlign.center, + BlocBuilder( + bloc: _canvasDeviceBloc, + builder: (context, canvasDeviceState) { + final displayKey = _getDisplayKey(playList); + final isPlaylistCasting = canvasDeviceState + .lastSelectedActiveDeviceForKey(displayKey ?? '') != + null; + if (isPlaylistCasting) { + return Padding( + padding: const EdgeInsets.all(15), + child: PlaylistControl( + displayKey: displayKey!, + ), + ); + } else { + return const SizedBox(); + } + }, + ), + Expanded( + child: NftCollectionGrid( + state: nftState.state, + tokens: _setupPlayList( + tokens: nftState.tokens.items, + selectedTokens: playList.tokenIDs, + ), + customGalleryViewBuilder: (context, tokens) => + _assetsWidget( + context, + tokens, + accountIdentities: accountIdentities, + playlist: playList, + onShuffleTap: () => _onShufferTap(playList), + onTimerTap: () => _onTimerTap(playList), ), ), - ] - ], - ), - actions: [ - const SizedBox(width: 15), - GestureDetector( - onTap: () async { - await _onOrderTap(context, _getAvailableOrders()); - }, - child: SvgPicture.asset( - 'assets/images/sort.svg', - colorFilter: - ColorFilter.mode(theme.primaryColor, BlendMode.srcIn), - width: 22, - height: 22, ), - ), - if (editable) ...[ - const SizedBox(width: 15), - GestureDetector( - onTap: () async => _onMoreTap(context, playList), - child: SvgPicture.asset( - 'assets/images/more_circle.svg', - colorFilter: - ColorFilter.mode(theme.primaryColor, BlendMode.srcIn), - width: 24, - )), ], - const SizedBox(width: 15), - ], - bottom: PreferredSize( - preferredSize: const Size.fromHeight(0.25), - child: - addOnlyDivider(color: AppColor.auQuickSilver, border: 0.25), - ), - ), - body: BlocBuilder( - bloc: nftBloc, - builder: (context, nftState) => NftCollectionGrid( - state: nftState.state, - tokens: setupPlayList( - tokens: nftState.tokens.items, - selectedTokens: playList.tokenIDs, - ), - customGalleryViewBuilder: (context, tokens) => _assetsWidget( - context, - tokens, - accountIdentities: accountIdentities, - playControlModel: - playList.playControlModel ?? PlayControlModel(), - onShuffleTap: () => _onShufferTap(playList), - onTimerTap: () => _onTimerTap(playList), - ), ), ), ); @@ -432,27 +323,29 @@ class _ViewPlaylistScreenState extends State { ); } - Future moveToAddNftToCollection(BuildContext context) async { - await Navigator.pushNamed( - context, - AppRouter.addToCollectionPage, - arguments: widget.payload.playListModel, - ).then((value) { - if (value != null && value is PlayListModel) { - bloc.add(SavePlaylist(name: value.name)); - nftBloc.add(RefreshNftCollectionByIDs( - ids: isDemo ? [] : value.tokenIDs, - debugTokenIds: isDemo ? value.tokenIDs : [], - )); - } - }); + String? _getDisplayKey(PlayListModel playList) => playList.displayKey; + + Future _moveToArtwork(CompactedAssetToken compactedAssetToken) { + final playlist = widget.payload.playListModel; + final displayKey = playlist?.displayKey; + if (displayKey == null) { + return Future.value(false); + } + + final lastSelectedCanvasDevice = + _canvasDeviceBloc.state.lastSelectedActiveDeviceForKey(displayKey); + if (lastSelectedCanvasDevice != null) { + return _canvasClientServiceV2.moveToArtwork(lastSelectedCanvasDevice, + artworkId: compactedAssetToken.id); + } + return Future.value(false); } Widget _assetsWidget( BuildContext context, List tokens, { required List accountIdentities, - required PlayControlModel playControlModel, + required PlayListModel playlist, Function()? onShuffleTap, Function()? onTimerTap, }) { @@ -482,6 +375,8 @@ class _ViewPlaylistScreenState extends State { ? PendingTokenWidget( thumbnail: asset.galleryThumbnailURL, tokenId: asset.tokenId, + shouldRefreshCache: + asset.shouldRefreshThumbnailCache, ) : tokenGalleryThumbnailWidget( context, @@ -499,16 +394,19 @@ class _ViewPlaylistScreenState extends State { .where((e) => e.pending != true || e.hasMetadata) .toList() .indexOf(asset); + + unawaited(_moveToArtwork(asset)); + final payload = asset.isPostcard ? PostcardDetailPagePayload( accountIdentities, index, - playControl: playControlModel, + playlist: playlist, ) : ArtworkDetailPayload( accountIdentities, index, - playControl: playControlModel, + playlist: playlist, ); final pageName = asset.isPostcard ? AppRouter.claimedPostcardDetailsPage @@ -523,89 +421,27 @@ class _ViewPlaylistScreenState extends State { ), ], ), - Positioned( - bottom: 0, - left: 0, - right: 0, - child: Column( - children: [ - if (editable) - Padding( - padding: const EdgeInsets.only(bottom: 20), - child: Center( - child: AddButton( - icon: SvgPicture.asset( - 'assets/images/Add.svg', - width: 30, - height: 30, - ), - onTap: () async { - await moveToAddNftToCollection(context); - }, - ), - ), - ), - const SizedBox(height: 22), - ], - ), - ) ], ); } - - Future _fetchDevice() async { - _canvasDeviceBloc.add(CanvasDeviceGetDevicesEvent( - widget.payload.playListModel?.id ?? '', - syncAll: false)); - } -} - -enum SortOrder { - title, - artist, - newest, - manual; - - String get text { - switch (this) { - case SortOrder.title: - return tr('sort_by_title'); - case SortOrder.artist: - return tr('sort_by_artist'); - case SortOrder.newest: - return tr('sort_by_newest'); - case SortOrder.manual: - return tr('sort_by_manual'); - } - } - - int compare(CompactedAssetToken a, CompactedAssetToken b) { - switch (this) { - case SortOrder.title: - return a.title?.compareTo(b.title ?? '') ?? 1; - case SortOrder.artist: - return a.artistID?.compareTo(b.artistID ?? '') ?? 1; - case SortOrder.newest: - return b.lastActivityTime.compareTo(a.lastActivityTime); - case SortOrder.manual: - return -1; - } - } } class AddButton extends StatelessWidget { final Widget icon; + final Widget? iconOnDisabled; final void Function() onTap; + final bool isEnable; const AddButton({ required this.icon, required this.onTap, super.key, + this.iconOnDisabled, + this.isEnable = true, }); @override Widget build(BuildContext context) => GestureDetector( - onTap: onTap, - child: icon, - ); + onTap: isEnable ? onTap : null, + child: isEnable ? icon : iconOnDisabled ?? icon); } diff --git a/lib/screen/projects/projects_bloc.dart b/lib/screen/projects/projects_bloc.dart index e105c80049..5307547e29 100644 --- a/lib/screen/projects/projects_bloc.dart +++ b/lib/screen/projects/projects_bloc.dart @@ -1,44 +1,89 @@ import 'package:autonomy_flutter/au_bloc.dart'; -import 'package:autonomy_flutter/model/ff_account.dart'; -import 'package:autonomy_flutter/model/tap_navigate.dart'; +import 'package:autonomy_flutter/common/environment.dart'; +import 'package:autonomy_flutter/common/injector.dart'; +import 'package:autonomy_flutter/model/ff_artwork.dart'; +import 'package:autonomy_flutter/model/project.dart'; import 'package:autonomy_flutter/screen/app_router.dart'; import 'package:autonomy_flutter/screen/feralfile_artwork_preview/feralfile_artwork_preview_page.dart'; import 'package:autonomy_flutter/screen/projects/projects_state.dart'; import 'package:autonomy_flutter/service/account_service.dart'; import 'package:autonomy_flutter/service/configuration_service.dart'; import 'package:autonomy_flutter/service/ethereum_service.dart'; +import 'package:autonomy_flutter/service/feralfile_service.dart'; import 'package:autonomy_flutter/service/remote_config_service.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:nft_collection/database/nft_collection_database.dart'; +import 'package:nft_collection/models/asset_token.dart'; +import 'package:nft_collection/widgets/nft_collection_bloc_event.dart'; class ProjectsBloc extends AuBloc { final EthereumService _ethereumService; final ConfigurationService _configurationService; final AccountService _accountService; final RemoteConfigService _remoteConfigService; + final FeralFileService _feralfileService; ProjectsBloc(this._ethereumService, this._configurationService, - this._accountService, this._remoteConfigService) + this._accountService, this._remoteConfigService, this._feralfileService) : super(ProjectsState()) { on((event, emit) async { - final List newProjects = []; - final showYokoOno = await _doUserHaveYokoOnoRecord(); - if (showYokoOno) { - final config = _remoteConfigService.getConfig>( - ConfigGroup.exhibition, ConfigKey.yokoOnoPublic, {}); - final artwork = Artwork.createFake(config['public_version_thumbnail'], - config['public_version_preview'], 'software'); - newProjects.add(TapNavigate( - title: 'yoko_ono_public_version'.tr(), + final List newProjects = []; + CompactedAssetToken? firstUserMoMAPostCard; + Artwork? yokoOnoRecordArtwork; + try { + firstUserMoMAPostCard = await _getFirstUserMoMAPostCard(); + final showYokoOno = await _doUserHaveYokoOnoRecord(); + if (showYokoOno) { + final config = _remoteConfigService.getConfig>( + ConfigGroup.exhibition, ConfigKey.yokoOnoPublic, {}); + yokoOnoRecordArtwork = await _feralfileService.getArtwork( + config['public_token_id'], + ); + } + } catch (_) {} + + if (yokoOnoRecordArtwork != null) { + newProjects.add( + ProjectInfo( + title: 'yoko_ono_project_title'.tr(), route: AppRouter.ffArtworkPreviewPage, arguments: FeralFileArtworkPreviewPagePayload( - artwork: artwork, - ))); + artwork: yokoOnoRecordArtwork, + ), + delegate: yokoOnoRecordArtwork, + ), + ); + } + + if (firstUserMoMAPostCard != null) { + newProjects.add( + ProjectInfo( + title: 'moma_postcard_title'.tr(), + route: AppRouter.momaPostcardPage, + delegate: firstUserMoMAPostCard, + ), + ); } emit(state.copyWith(loading: false, projects: newProjects)); }); } + Future _getFirstUserMoMAPostCard() async { + final addresses = await _accountService.getAllAddresses(); + final postCardTokens = await injector() + .assetTokenDao + .findAllAssetTokensByOwnersAndContractAddress( + addresses, + Environment.postcardContractAddress, + 1, + DateTime.now().millisecondsSinceEpoch, + PageKey.init().id); + return postCardTokens.isNotEmpty + ? CompactedAssetToken.fromAssetToken(postCardTokens[0]) + : null; + } + Future _doUserHaveYokoOnoRecord() async { final addresses = await _accountService.getAllAddresses(); final yokoOnoRecordOwners = _configurationService.getRecordOwners(); @@ -58,13 +103,13 @@ class ProjectsBloc extends AuBloc { const count = 20; int currentIndex = startIndex; final List recordOwners = []; - + List owners = []; do { - final owners = await _ethereumService.getPublicRecordOwners( + owners = await _ethereumService.getPublicRecordOwners( BigInt.from(currentIndex), BigInt.from(count)); recordOwners.addAll(owners); - currentIndex += owners.length; - } while (recordOwners.length >= count); + currentIndex += count; + } while (owners.length >= count); return recordOwners; } } diff --git a/lib/screen/projects/projects_page.dart b/lib/screen/projects/projects_page.dart index 3d4568e048..58397d25c9 100644 --- a/lib/screen/projects/projects_page.dart +++ b/lib/screen/projects/projects_page.dart @@ -1,14 +1,20 @@ import 'package:after_layout/after_layout.dart'; +import 'package:autonomy_flutter/model/ff_artwork.dart'; +import 'package:autonomy_flutter/model/project.dart'; import 'package:autonomy_flutter/screen/projects/projects_bloc.dart'; import 'package:autonomy_flutter/screen/projects/projects_state.dart'; +import 'package:autonomy_flutter/util/asset_token_ext.dart'; import 'package:autonomy_flutter/util/style.dart'; +import 'package:autonomy_flutter/view/artwork_common_widget.dart'; import 'package:autonomy_flutter/view/back_appbar.dart'; +import 'package:autonomy_flutter/view/ff_artwork_thumbnail_view.dart'; import 'package:autonomy_flutter/view/responsive.dart'; -import 'package:autonomy_flutter/view/tappable_forward_row.dart'; +import 'package:autonomy_flutter/view/title_text.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:feralfile_app_theme/feral_file_app_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:nft_collection/models/asset_token.dart'; class ProjectsPage extends StatefulWidget { const ProjectsPage({super.key}); @@ -26,44 +32,110 @@ class _ProjectsPageState extends State @override Widget build(BuildContext context) => Scaffold( - appBar: getBackAppBar(context, - title: 'projects'.tr(), onBack: () => Navigator.pop(context)), - body: BlocBuilder( - builder: (context, state) { - if (state.loading) { - return Center(child: loadingIndicator()); - } - final padding = EdgeInsets.fromLTRB( - ResponsiveLayout.padding, 40, ResponsiveLayout.padding, 32); - - if (state.projects.isEmpty) { - return Padding( - padding: padding, - child: Text('no_project_found'.tr(), - style: Theme.of(context).textTheme.ppMori400Black14)); - } - return _projectsList(context, state); - }, - )); + appBar: getFFAppBar( + context, + title: TitleText( + title: 'rnd'.tr(), + ), + centerTitle: false, + onBack: () => Navigator.pop(context), + ), + extendBody: true, + // extendBodyBehindAppBar: true, + backgroundColor: AppColor.primaryBlack, + body: CustomScrollView( + slivers: [ + const SliverToBoxAdapter( + child: SizedBox( + height: 42, + ), + ), + SliverToBoxAdapter( + child: BlocBuilder( + builder: (context, state) { + if (state.loading) { + return Center(child: loadingIndicator()); + } + return Padding( + padding: ResponsiveLayout.pageHorizontalEdgeInsets, + child: _projectsList(context, state), + ); + }, + ), + ), + ], + ), + ); Widget _projectsList(BuildContext context, ProjectsState state) { final theme = Theme.of(context); return Column( children: [ - addTitleSpace(), - ...state.projects.map((e) => TappableForwardRow( - padding: EdgeInsets.symmetric(horizontal: ResponsiveLayout.padding), - leftWidget: Text( - e.title, - style: theme.textTheme.ppMori400Black14, - ), - onTap: () async { - await Navigator.of(context).pushNamed( - e.route, - arguments: e.arguments, - ); - })) + ...state.projects.map( + (e) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + child: Container( + padding: EdgeInsets.all(ResponsiveLayout.padding * 3.5), + decoration: BoxDecoration( + color: AppColor.auGreyBackground, + borderRadius: BorderRadius.circular(20), + ), + child: _buildProjectDelegate(context, e), + ), + onTap: () async => Navigator.of(context).pushNamed( + e.route, + arguments: e.arguments, + ), + ), + SizedBox(height: ResponsiveLayout.padding), + Text( + e.title, + style: theme.textTheme.ppMori400White14, + ), + addTitleSpace(), + ], + ), + ) ], ); } + + Widget _buildProjectDelegate(BuildContext context, ProjectInfo project) { + final cachedImageSize = (MediaQuery.sizeOf(context).width - + ResponsiveLayout.padding * 2 - + ResponsiveLayout.padding * 3.5) ~/ + 1; + switch (project.delegate.runtimeType) { + case const (CompactedAssetToken): + final asset = project.delegate as CompactedAssetToken; + return asset.pending == true && !asset.hasMetadata + ? PendingTokenWidget( + thumbnail: asset.galleryThumbnailURL, + tokenId: asset.tokenId, + shouldRefreshCache: asset.shouldRefreshThumbnailCache, + ) + : tokenGalleryThumbnailWidget( + context, + asset, + cachedImageSize, + useHero: false, + usingThumbnailID: false, + ); + case const (Artwork): + return IgnorePointer( + child: AspectRatio( + aspectRatio: 1, + child: FFArtworkThumbnailView( + artwork: project.delegate as Artwork, + cacheWidth: cachedImageSize, + cacheHeight: cachedImageSize, + ), + ), + ); + default: + return const SizedBox(); + } + } } diff --git a/lib/screen/projects/projects_state.dart b/lib/screen/projects/projects_state.dart index 3d111aab36..92064eed1a 100644 --- a/lib/screen/projects/projects_state.dart +++ b/lib/screen/projects/projects_state.dart @@ -1,4 +1,4 @@ -import 'package:autonomy_flutter/model/tap_navigate.dart'; +import 'package:autonomy_flutter/model/project.dart'; abstract class ProjectsEvent {} @@ -6,8 +6,7 @@ class GetProjectsEvent extends ProjectsEvent {} class ProjectsState { final bool loading; - - final List projects; + final List projects; ProjectsState({ this.loading = true, @@ -16,7 +15,7 @@ class ProjectsState { ProjectsState copyWith({ bool? loading, - List? projects, + List? projects, }) => ProjectsState( loading: loading ?? this.loading, diff --git a/lib/screen/scan_qr/scan_qr_page.dart b/lib/screen/scan_qr/scan_qr_page.dart index c9dd46ad39..293a29f6cc 100644 --- a/lib/screen/scan_qr/scan_qr_page.dart +++ b/lib/screen/scan_qr/scan_qr_page.dart @@ -6,7 +6,6 @@ // import 'dart:async'; -import 'dart:convert'; import 'dart:io'; import 'package:autonomy_flutter/common/injector.dart'; @@ -20,7 +19,6 @@ import 'package:autonomy_flutter/screen/bloc/persona/persona_bloc.dart'; import 'package:autonomy_flutter/screen/bloc/tezos/tezos_bloc.dart'; import 'package:autonomy_flutter/screen/global_receive/receive_page.dart'; import 'package:autonomy_flutter/service/audit_service.dart'; -import 'package:autonomy_flutter/service/canvas_client_service.dart'; import 'package:autonomy_flutter/service/deeplink_service.dart'; import 'package:autonomy_flutter/service/metric_client_service.dart'; import 'package:autonomy_flutter/service/navigation_service.dart'; @@ -32,18 +30,18 @@ import 'package:autonomy_flutter/util/route_ext.dart'; import 'package:autonomy_flutter/util/style.dart'; import 'package:autonomy_flutter/util/ui_helper.dart'; import 'package:autonomy_flutter/view/back_appbar.dart'; +import 'package:autonomy_flutter/view/display_instruction_view.dart'; import 'package:autonomy_flutter/view/header.dart'; import 'package:autonomy_flutter/view/primary_button.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:feralfile_app_theme/feral_file_app_theme.dart'; -import 'package:feralfile_app_tv_proto/feralfile_app_tv_proto.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:qr_code_scanner/qr_code_scanner.dart'; -import 'package:synchronized/synchronized.dart'; // ignore_for_file: constant_identifier_names @@ -185,26 +183,17 @@ class ScanQRPageState extends State ), ); - Widget _content(BuildContext context) { - final size1 = MediaQuery.of(context).size.height / 2; - final qrSize = size1 < 240.0 ? size1 : 240.0; - - double cutPaddingTop = qrSize + 500 - MediaQuery.of(context).size.height; - if (cutPaddingTop < 0) { - cutPaddingTop = 0; - } - return Column( - children: [ - Expanded( - child: TabBarView( - controller: _tabController, - physics: const NeverScrollableScrollPhysics(), - children: _pages.sublist(0, _tabController.length), + Widget _content(BuildContext context) => Column( + children: [ + Expanded( + child: TabBarView( + controller: _tabController, + physics: const NeverScrollableScrollPhysics(), + children: _pages.sublist(0, _tabController.length), + ), ), - ), - ], - ); - } + ], + ); Widget _header(BuildContext context) { final theme = Theme.of(context); @@ -278,8 +267,65 @@ enum ScannerItem { BEACON_CONNECT, ETH_ADDRESS, XTZ_ADDRESS, - CANVAS_DEVICE, - GLOBAL + GLOBAL, + CANVAS; + + List get instructions { + switch (this) { + case WALLET_CONNECT: + case BEACON_CONNECT: + return [ScannerInstruction.web3Connect]; + case ETH_ADDRESS: + case XTZ_ADDRESS: + return []; + case GLOBAL: + return [ + ScannerInstruction.web3Connect, + ScannerInstruction.signTransaction, + ScannerInstruction.displayFF, + ]; + case CANVAS: + return [ + ScannerInstruction.displayFF, + ]; + } + } +} + +class ScannerInstruction { + final String name; + final String detail; + final Widget? icon; + + const ScannerInstruction({ + required this.name, + required this.detail, + this.icon, + }); + + static ScannerInstruction web3Connect = ScannerInstruction( + name: 'apps'.tr(), + detail: 'such_as_openSea'.tr(), + ); + + static ScannerInstruction signTransaction = ScannerInstruction( + name: 'sign_transaction'.tr(), + detail: 'after_connecting'.tr(), + ); + + static ScannerInstruction displayFF = ScannerInstruction( + name: 'display_with_ff'.tr(), + detail: 'on_tv_or_desktop'.tr(), + icon: GestureDetector( + onTap: () { + final context = + injector().navigatorKey.currentContext!; + UIHelper.showDialog( + context, 'display'.tr(), const DisplayInstructionView(), + isDismissible: true, withCloseIcon: true); + }, + child: SvgPicture.asset('assets/images/info_white.svg')), + ); } class QRScanView extends StatefulWidget { @@ -302,18 +348,22 @@ class QRScanViewState extends State bool? _cameraPermission; String? currentCode; final metricClient = injector(); - final _navigationService = injector(); - late Lock _lock; Timer? _timer; + static const _qrSize = 260.0; + static const double _topPadding = 144; + late bool _shouldPop; @override void initState() { super.initState(); - _shouldPop = !(widget.scannerItem == ScannerItem.GLOBAL); + _shouldPop = !(widget.scannerItem == ScannerItem.GLOBAL || + + /// handle canvas deeplink will pop the screen, + /// therefore no need to pop here + widget.scannerItem == ScannerItem.CANVAS); unawaited(_checkPermission()); - _lock = Lock(); } @override @@ -382,13 +432,6 @@ class QRScanViewState extends State @override Widget build(BuildContext context) { super.build(context); - final size1 = MediaQuery.of(context).size.height / 2; - final qrSize = size1 < 240.0 ? size1 : 240.0; - - var cutPaddingTop = qrSize + 500 - MediaQuery.of(context).size.height; - if (cutPaddingTop < 0) { - cutPaddingTop = 0; - } final theme = Theme.of(context); return Stack( children: [ @@ -397,22 +440,9 @@ class QRScanViewState extends State else ...[ _qrView(context), Padding( - padding: EdgeInsets.fromLTRB( - 0, - MediaQuery.of(context).size.height / 2 + - qrSize / 2 - - cutPaddingTop, - 0, - 15, - ), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _instructionView(context), - ], - ), - ), + padding: + const EdgeInsets.fromLTRB(0, _qrSize + _topPadding + 30, 0, 15), + child: _instructionView(context), ) ], if (_isLoading) ...[ @@ -429,28 +459,26 @@ class QRScanViewState extends State Widget _qrView(BuildContext context) { final theme = Theme.of(context); - final size1 = MediaQuery.of(context).size.height / 2; - final qrSize = size1 < 240.0 ? size1 : 240.0; - - var cutPaddingTop = qrSize + 500 - MediaQuery.of(context).size.height; - if (cutPaddingTop < 0) { - cutPaddingTop = 0; + double cutOutBottomOffset = + MediaQuery.of(context).size.height / 2 - (_qrSize / 2 + _topPadding); + if (cutOutBottomOffset < 0) { + cutOutBottomOffset = 0; } - final cutOutBottomOffset = 80 + cutPaddingTop; return Stack( children: [ QRView( key: qrKey, overlay: QrScannerOverlayShape( + borderLength: _qrSize / 2, borderColor: isScanDataError ? AppColor.red : theme.colorScheme.secondary, overlayColor: _cameraPermission == true ? const Color.fromRGBO(0, 0, 0, 0.6) : const Color.fromRGBO(0, 0, 0, 1), - cutOutSize: qrSize, - borderWidth: 8, - borderRadius: 40, + cutOutSize: _qrSize, + borderWidth: 1, cutOutBottomOffset: cutOutBottomOffset, + borderRadius: 40, ), onQRViewCreated: _onQRViewCreated, onPermissionSet: (ctrl, p) { @@ -461,12 +489,11 @@ class QRScanViewState extends State ), if (isScanDataError) Positioned( - left: (MediaQuery.of(context).size.width - qrSize) / 2, - top: (MediaQuery.of(context).size.height - qrSize) / 2 - - cutOutBottomOffset, + left: (MediaQuery.of(context).size.width - _qrSize) / 2, + top: _topPadding, child: SizedBox( - height: qrSize, - width: qrSize, + height: _qrSize, + width: _qrSize, child: Center( child: Text( 'invalid_qr_code'.tr(), @@ -480,61 +507,46 @@ class QRScanViewState extends State ); } - Widget _noPermissionView(BuildContext context) { - final size1 = MediaQuery.of(context).size.height / 2; - final qrSize = size1 < 240.0 ? size1 : 240.0; - - var cutPaddingTop = qrSize + 500 - MediaQuery.of(context).size.height; - if (cutPaddingTop < 0) { - cutPaddingTop = 0; - } - final cutOutBottomOffset = 80 + cutPaddingTop; - return Stack( - children: [ - _qrView(context), - Padding( - padding: EdgeInsets.fromLTRB( - 0, - MediaQuery.of(context).size.height / 2 + - qrSize / 2 - - cutOutBottomOffset + - 32, - 0, - 120, - ), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _instructionViewNoPermission(context), - Padding( - padding: const EdgeInsets.all(8), - child: PrimaryButton( - text: 'open_setting'.tr( - namedArgs: { - 'device': Platform.isAndroid ? 'Device' : 'iOS', + Widget _noPermissionView(BuildContext context) => Stack( + children: [ + _qrView(context), + Padding( + padding: const EdgeInsets.fromLTRB( + 0, + _qrSize + _topPadding + 20, + 0, + 120, + ), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _instructionViewNoPermission(context), + Padding( + padding: const EdgeInsets.all(8), + child: PrimaryButton( + text: 'open_setting'.tr( + namedArgs: { + 'device': Platform.isAndroid ? 'Device' : 'iOS', + }, + ), + onTap: () async { + await openAppSettings(); }, ), - onTap: () async { - await openAppSettings(); - }, - ), - ) - ], + ) + ], + ), ), ), - ), - ], - ); - } + ], + ); Widget _instructionViewNoPermission(BuildContext context) { final theme = Theme.of(context); - final size1 = MediaQuery.of(context).size.height / 2; - final qrSize = size1 < 240.0 ? size1 : 240.0; return Container( padding: const EdgeInsets.symmetric(horizontal: 20), - width: qrSize, + width: _qrSize, child: Text( 'please_ensure'.tr(), style: theme.textTheme.ppMori400White14, @@ -544,97 +556,102 @@ class QRScanViewState extends State } Widget _instructionView(BuildContext context) { - final theme = Theme.of(context); + if (widget.scannerItem.instructions.isEmpty) { + return const SizedBox(); + } + return Padding( + padding: const EdgeInsets.all(44), + child: Column( + children: [ + _instructionHeader(context), + _instructionBody(context, widget.scannerItem.instructions) + ], + )); + } - switch (widget.scannerItem) { - case ScannerItem.WALLET_CONNECT: - case ScannerItem.BEACON_CONNECT: - case ScannerItem.GLOBAL: - return Padding( - padding: const EdgeInsets.all(15), - child: Container( - width: MediaQuery.of(context).size.width, - decoration: BoxDecoration( - color: theme.colorScheme.primary, - borderRadius: BorderRadius.circular(5), + Widget _instructionHeader(BuildContext context) { + final theme = Theme.of(context); + return DecoratedBox( + decoration: const BoxDecoration( + color: AppColor.auGreyBackground, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(15), + topRight: Radius.circular(15), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 25), + child: Row( + children: [ + SvgPicture.asset( + 'assets/images/icon_scan.svg', ), - padding: const EdgeInsets.all(15), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'scan_qr_to'.tr(), - style: theme.textTheme.ppMori700White14, - ), - const Divider( - color: AppColor.auGreyBackground, - height: 30, - ), - RichText( - text: TextSpan( - text: 'apps'.tr(), - children: [ - TextSpan( - text: ' ', - style: theme.textTheme.ppMori400Grey14, - ), - TextSpan( - text: 'such_as_openSea'.tr(), - style: theme.textTheme.ppMori400Grey14, - ), - ], - style: theme.textTheme.ppMori400White14, + const SizedBox(width: 20), + RichText( + text: TextSpan( + text: 'scan_qr_code'.tr(), + children: [ + TextSpan( + text: ' ', + style: theme.textTheme.ppMori400Grey14, ), - ), - const SizedBox(height: 15), - RichText( - text: TextSpan( - text: 'sign_transaction'.tr(), - children: [ - TextSpan( - text: ' ', - style: theme.textTheme.ppMori400Grey14, - ), - TextSpan( - text: 'after_connecting'.tr(), - style: theme.textTheme.ppMori400Grey14, - ), - ], - style: theme.textTheme.ppMori400White14, + TextSpan( + text: 'in_order_to'.tr(), + style: theme.textTheme.ppMori400Grey14, ), - ), - const SizedBox(height: 15), - RichText( - text: TextSpan( - text: 'display_with_ff'.tr(), + ], + style: theme.textTheme.ppMori400White14, + ), + ), + ], + ), + ), + ); + } + + Widget _instructionBody( + BuildContext context, List instructions) { + final theme = Theme.of(context); + return DecoratedBox( + decoration: const BoxDecoration( + color: AppColor.primaryBlack, + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(15), + bottomRight: Radius.circular(15), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25), + child: Column( + children: instructions + .map( + (instruction) => Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - TextSpan( - text: ' ', - style: theme.textTheme.ppMori400Grey14, - ), - TextSpan( - text: 'on_tv_or_desktop'.tr(), - style: theme.textTheme.ppMori400Grey14, + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + instruction.name, + style: theme.textTheme.ppMori700White14, + ), + Text( + instruction.detail, + style: theme.textTheme.ppMori400Grey14, + ) + ], ), + instruction.icon ?? const SizedBox(), ], - style: theme.textTheme.ppMori400White14, ), ), - ], - ), - ), - ); - - case ScannerItem.ETH_ADDRESS: - case ScannerItem.XTZ_ADDRESS: - return Column( - children: [ - Text('scan_qr'.tr(), style: theme.primaryTextTheme.labelLarge), - ], - ); - case ScannerItem.CANVAS_DEVICE: - return const SizedBox(); - } + ) + .toList(), + ), + ), + ); } void _onQRViewCreated(QRViewController controller) { @@ -680,13 +697,15 @@ class QRScanViewState extends State return; } else { switch (widget.scannerItem) { + case ScannerItem.CANVAS: + + /// handled with deeplink case ScannerItem.WALLET_CONNECT: if (code.startsWith('wc:')) { await _handleAutonomyConnect(code); } else { _handleError(code); } - break; case ScannerItem.BEACON_CONNECT: if (code.startsWith('tezos://')) { @@ -694,7 +713,6 @@ class QRScanViewState extends State } else { _handleError(code); } - break; case ScannerItem.ETH_ADDRESS: case ScannerItem.XTZ_ADDRESS: @@ -709,40 +727,14 @@ class QRScanViewState extends State Navigator.pop(context, code); } await Future.delayed(const Duration(milliseconds: 300)); - break; case ScannerItem.GLOBAL: if (code.startsWith('wc:')) { await _handleAutonomyConnect(code); } else if (code.startsWith('tezos:')) { await _handleBeaconConnect(code); - /* TODO: Remove or support for multiple wallets - } else if (code.startsWith("tz1")) { - Navigator.of(context).popAndPushNamed(SendCryptoPage.tag, - arguments: SendData(CryptoType.XTZ, code)); - } else { - try { - final _ = EthereumAddress.fromHex(code); - Navigator.of(context).popAndPushNamed(SendCryptoPage.tag, - arguments: SendData(CryptoType.ETH, code)); - } catch (err) { - log(err.toString()); - } - */ - } else if (_isCanvasQrCode(code)) { - unawaited(_lock.synchronized(() async { - await _handleCanvasQrCode(code); - })); } else { _handleError(code); } - break; - case ScannerItem.CANVAS_DEVICE: - if (_isCanvasQrCode(code)) { - unawaited(_lock.synchronized(() => _handleCanvasQrCode(code))); - } else { - _handleError(code); - } - break; } if (mounted) { await resumeCamera(); @@ -757,55 +749,6 @@ class QRScanViewState extends State }); } - bool _isCanvasQrCode(String code) { - try { - CanvasDevice.fromJson(jsonDecode(code)); - return true; - } catch (err) { - return false; - } - } - - Future _handleCanvasQrCode(String code) async { - log.info('Canvas device scanned: $code'); - setState(() { - _isLoading = true; - }); - await pauseCamera(); - if (!mounted) { - return false; - } - try { - final device = CanvasDevice.fromJson(jsonDecode(code)); - final canvasClient = injector(); - final result = await canvasClient.connectToDevice(device); - if (result) { - device.isConnecting = true; - } - if (!mounted) { - return false; - } - if (_shouldPop) { - Navigator.pop(context, device); - } - return result; - } catch (e) { - if (mounted) { - if (_shouldPop) { - Navigator.pop(context); - } - if (e.toString().contains('DEADLINE_EXCEEDED') || true) { - await UIHelper.showInfoDialog( - _navigationService.navigatorKey.currentContext!, - 'failed_to_connect'.tr(), - 'canvas_ip_fail'.tr(), - closeButton: 'close'.tr()); - } - } - } - return false; - } - void _handleError(String data) { setState(() { isScanDataError = true; diff --git a/lib/screen/settings/crypto/send/send_crypto_bloc.dart b/lib/screen/settings/crypto/send/send_crypto_bloc.dart index 5a62752c44..01dfce7b89 100644 --- a/lib/screen/settings/crypto/send/send_crypto_bloc.dart +++ b/lib/screen/settings/crypto/send/send_crypto_bloc.dart @@ -20,6 +20,7 @@ import 'package:autonomy_flutter/service/tezos_service.dart'; import 'package:autonomy_flutter/util/constants.dart'; import 'package:autonomy_flutter/util/error_handler.dart'; import 'package:autonomy_flutter/util/fee_util.dart'; +import 'package:autonomy_flutter/util/log.dart'; import 'package:autonomy_flutter/util/rpc_error_extension.dart'; import 'package:autonomy_flutter/util/ui_helper.dart'; import 'package:autonomy_flutter/util/wallet_storage_ext.dart'; @@ -38,7 +39,7 @@ class SendCryptoBloc extends AuBloc { final DomainAddressService _domainAddressService; String? cachedAddress; BigInt? cachedAmount; - bool isEstimating = false; + final _estimateLock = Lock(); final _xtzSafeBuffer = BigInt.from(10); final _ethSafeBuffer = BigInt.from(100); @@ -54,68 +55,73 @@ class SendCryptoBloc extends AuBloc { this._domainAddressService, ) : super(SendCryptoState()) { on((event, emit) async { - final newState = state.clone() - ..wallet = event.wallet - ..index = event.index; - - final exchangeRate = await _currencyService.getExchangeRates(); - newState.exchangeRate = exchangeRate; - - switch (_type) { - case CryptoType.ETH: - final ownerAddress = - await event.wallet.getETHEip55Address(index: event.index); - final balance = await _ethereumService.getBalance(ownerAddress); - - newState.balance = balance.getInWei; - - if (state.feeOptionValue != null) { - final maxAllow = - balance.getInWei - state.feeOptionValue!.high - _ethSafeBuffer; - newState - ..maxAllow = maxAllow - ..isValid = _isValid(newState); - } - break; - case CryptoType.XTZ: - final address = - await event.wallet.getTezosAddress(index: event.index); - final balance = await _tezosService.getBalance(address); - - newState.balance = BigInt.from(balance); - if (state.feeOptionValue != null) { - final maxAllow = - newState.balance! - state.feeOptionValue!.high - _xtzSafeBuffer; - newState - ..maxAllow = maxAllow - ..isValid = _isValid(newState); - } - break; - case CryptoType.USDC: - final address = - await event.wallet.getETHEip55Address(index: event.index); - final ownerAddress = EthereumAddress.fromHex(address); - final contractAddress = EthereumAddress.fromHex(usdcContractAddress); - - final balance = await _ethereumService.getERC20TokenBalance( - contractAddress, ownerAddress); - final ethBalance = await _ethereumService.getBalance(address); - - newState.balance = balance; - newState.ethBalance = ethBalance.getInWei; - - if (state.feeOptionValue != null) { - final maxAllow = balance; - newState - ..maxAllow = maxAllow - ..isValid = _isValid(newState); - } - break; - default: - break; - } + await _estimateLock.synchronized(() async { + final newState = state.clone() + ..wallet = event.wallet + ..index = event.index; + + final exchangeRate = await _currencyService.getExchangeRates(); + newState.exchangeRate = exchangeRate; + + switch (_type) { + case CryptoType.ETH: + final ownerAddress = + await event.wallet.getETHEip55Address(index: event.index); + final balance = + await _ethereumService.getBalance(ownerAddress, doRetry: true); + + newState.balance = balance.getInWei; + + if (state.feeOptionValue != null) { + final maxAllow = balance.getInWei - + state.feeOptionValue!.high - + _ethSafeBuffer; + newState + ..maxAllow = maxAllow + ..isValid = _isValid(newState); + } + case CryptoType.XTZ: + final address = + await event.wallet.getTezosAddress(index: event.index); + final balance = + await _tezosService.getBalance(address, doRetry: true); + + newState.balance = BigInt.from(balance); + if (state.feeOptionValue != null) { + final maxAllow = newState.balance! - + state.feeOptionValue!.high - + _xtzSafeBuffer; + newState + ..maxAllow = maxAllow + ..isValid = _isValid(newState); + } + case CryptoType.USDC: + final address = + await event.wallet.getETHEip55Address(index: event.index); + final ownerAddress = EthereumAddress.fromHex(address); + final contractAddress = + EthereumAddress.fromHex(usdcContractAddress); + + final balance = await _ethereumService.getERC20TokenBalance( + contractAddress, ownerAddress); + final ethBalance = + await _ethereumService.getBalance(address, doRetry: true); + + newState.balance = balance; + newState.ethBalance = ethBalance.getInWei; + + if (state.feeOptionValue != null) { + final maxAllow = balance; + newState + ..maxAllow = maxAllow + ..isValid = _isValid(newState); + } + default: + break; + } - emit(newState); + emit(newState); + }); }); on((event, emit) async { @@ -142,7 +148,7 @@ class SendCryptoBloc extends AuBloc { emit(newState); }); - on((event, emit) async { + on((event, emit) { final newState = state.clone(); if (event.amount.isNotEmpty) { @@ -151,10 +157,8 @@ class SendCryptoBloc extends AuBloc { switch (_type) { case CryptoType.ETH: value *= double.parse(state.exchangeRate.eth); - break; case CryptoType.XTZ: value *= double.parse(state.exchangeRate.xtz); - break; default: break; } @@ -180,146 +184,140 @@ class SendCryptoBloc extends AuBloc { emit(newState); }); - on((event, emit) async { + on((event, emit) { final newState = state.clone()..isCrypto = event.isCrypto; emit(newState); }); on((event, emit) async { - if (isEstimating) { - return; - } - - isEstimating = true; - - final newState = - event.newState == null ? state.clone() : event.newState!.clone(); - - BigInt fee = BigInt.zero; - FeeOptionValue feeOptionValue = FeeOptionValue(fee, fee, fee); - - switch (_type) { - case CryptoType.ETH: - final address = EthereumAddress.fromHex(event.address); - final wallet = state.wallet; - final index = state.index; - if (wallet == null || index == null) { - return; - } - feeOptionValue = await _ethereumService.estimateFee( - wallet, index, address, EtherAmount.inWei(event.amount), null); - fee = feeOptionValue.getFee(state.feeOption); - break; - case CryptoType.XTZ: - final wallet = state.wallet; - final index = state.index; - if (wallet == null || index == null) { - return; - } - try { - final tezosFee = await _tezosService.estimateFee( - await wallet.getTezosPublicKey(index: index), - event.address, - event.amount.toInt(), - baseOperationCustomFee: - state.feeOption.tezosBaseOperationCustomFee); - fee = BigInt.from(tezosFee); - feeOptionValue = FeeOptionValue( - BigInt.from(tezosFee - - state.feeOption.tezosBaseOperationCustomFee + - baseOperationCustomFeeLow), - BigInt.from(tezosFee - - state.feeOption.tezosBaseOperationCustomFee + - baseOperationCustomFeeMedium), - BigInt.from(tezosFee - - state.feeOption.tezosBaseOperationCustomFee + - baseOperationCustomFeeHigh)); - } on TezartNodeError catch (err) { - unawaited(UIHelper.showInfoDialog( - injector().navigatorKey.currentContext!, - 'estimation_failed'.tr(), - getTezosErrorMessage(err), - isDismissible: true, - )); - fee = BigInt.zero; - feeOptionValue = - FeeOptionValue(BigInt.zero, BigInt.zero, BigInt.zero); - } catch (err) { - unawaited(showErrorDialogFromException(err)); - fee = BigInt.zero; - feeOptionValue = - FeeOptionValue(BigInt.zero, BigInt.zero, BigInt.zero); - } - break; - case CryptoType.USDC: - final wallet = state.wallet; - final index = state.index; - if (wallet == null || index == null) { - return; - } - - final address = await wallet.getETHEip55Address(index: index); - final ownerAddress = EthereumAddress.fromHex(address); - final toAddress = EthereumAddress.fromHex(event.address); - final contractAddress = EthereumAddress.fromHex(usdcContractAddress); - - final data = await _ethereumService.getERC20TransferTransactionData( - contractAddress, ownerAddress, toAddress, event.amount); - try { + await _estimateLock.synchronized(() async { + log.info('EstimateFeeEvent: ${event.address}, ${event.amount}'); + final newState = state.clone(); + + BigInt fee = BigInt.zero; + FeeOptionValue feeOptionValue = FeeOptionValue(fee, fee, fee); + + switch (_type) { + case CryptoType.ETH: + final address = EthereumAddress.fromHex(event.address); + final wallet = state.wallet; + final index = state.index; + if (wallet == null || index == null) { + return; + } feeOptionValue = await _ethereumService.estimateFee( - wallet, index, contractAddress, EtherAmount.zero(), data); + wallet, index, address, EtherAmount.inWei(event.amount), null); fee = feeOptionValue.getFee(state.feeOption); - } on RPCError catch (e) { - _navigationService.showErrorDialog( - ErrorEvent(e, 'estimation_failed'.tr(), e.errorMessage, - ErrorItemState.tryAgain), cancelAction: () { - _navigationService.hideInfoDialog(); + case CryptoType.XTZ: + final wallet = state.wallet; + final index = state.index; + if (wallet == null || index == null) { return; - }, defaultAction: () { - add(event); - }); - } catch (e) { - _navigationService.showErrorDialog( - ErrorEvent(e, 'estimation_failed'.tr(), e.toString(), - ErrorItemState.tryAgain), cancelAction: () { - _navigationService.hideInfoDialog(); + } + try { + final tezosFee = await _tezosService.estimateFee( + await wallet.getTezosPublicKey(index: index), + event.address, + event.amount.toInt(), + baseOperationCustomFee: + state.feeOption.tezosBaseOperationCustomFee); + fee = BigInt.from(tezosFee); + feeOptionValue = FeeOptionValue( + BigInt.from(tezosFee - + state.feeOption.tezosBaseOperationCustomFee + + baseOperationCustomFeeLow), + BigInt.from(tezosFee - + state.feeOption.tezosBaseOperationCustomFee + + baseOperationCustomFeeMedium), + BigInt.from(tezosFee - + state.feeOption.tezosBaseOperationCustomFee + + baseOperationCustomFeeHigh)); + } on TezartNodeError catch (err) { + unawaited(UIHelper.showInfoDialog( + injector().navigatorKey.currentContext!, + 'estimation_failed'.tr(), + getTezosErrorMessage(err), + isDismissible: true, + )); + fee = BigInt.zero; + feeOptionValue = + FeeOptionValue(BigInt.zero, BigInt.zero, BigInt.zero); + } catch (err) { + unawaited(showErrorDialogFromException(err)); + fee = BigInt.zero; + feeOptionValue = + FeeOptionValue(BigInt.zero, BigInt.zero, BigInt.zero); + } + case CryptoType.USDC: + final wallet = state.wallet; + final index = state.index; + if (wallet == null || index == null) { return; - }, defaultAction: () { - add(event); - }); - } - break; - default: - fee = BigInt.zero; - feeOptionValue = - FeeOptionValue(BigInt.zero, BigInt.zero, BigInt.zero); - } - - newState - ..fee = fee - ..feeOptionValue = feeOptionValue; - - if (state.balance != null) { - var maxAllow = _type != CryptoType.USDC - ? state.balance! - - fee - - (_type == CryptoType.ETH ? _ethSafeBuffer : _xtzSafeBuffer) - : state.balance!; - if (maxAllow < BigInt.zero) { - maxAllow = BigInt.zero; + } + + final address = await wallet.getETHEip55Address(index: index); + final ownerAddress = EthereumAddress.fromHex(address); + final toAddress = EthereumAddress.fromHex(event.address); + final contractAddress = + EthereumAddress.fromHex(usdcContractAddress); + + final data = await _ethereumService.getERC20TransferTransactionData( + contractAddress, ownerAddress, toAddress, event.amount); + try { + feeOptionValue = await _ethereumService.estimateFee( + wallet, index, contractAddress, EtherAmount.zero(), data); + fee = feeOptionValue.getFee(state.feeOption); + } on RPCError catch (e) { + _navigationService.showErrorDialog( + ErrorEvent(e, 'estimation_failed'.tr(), e.errorMessage, + ErrorItemState.tryAgain), cancelAction: () { + _navigationService.hideInfoDialog(); + return; + }, defaultAction: () { + add(event); + }); + } catch (e) { + _navigationService.showErrorDialog( + ErrorEvent(e, 'estimation_failed'.tr(), e.toString(), + ErrorItemState.tryAgain), cancelAction: () { + _navigationService.hideInfoDialog(); + return; + }, defaultAction: () { + add(event); + }); + } + default: + fee = BigInt.zero; + feeOptionValue = + FeeOptionValue(BigInt.zero, BigInt.zero, BigInt.zero); } + newState - ..maxAllow = maxAllow - ..address = cachedAddress - ..amount = cachedAmount - ..isValid = _isValid(newState); - } + ..fee = fee + ..feeOptionValue = feeOptionValue; + + if (state.balance != null) { + var maxAllow = _type != CryptoType.USDC + ? state.balance! - + fee - + (_type == CryptoType.ETH ? _ethSafeBuffer : _xtzSafeBuffer) + : state.balance!; + if (maxAllow < BigInt.zero) { + maxAllow = BigInt.zero; + } + newState + ..maxAllow = maxAllow + ..address = cachedAddress + ..amount = cachedAmount + ..isValid = _isValid(newState); + } - isEstimating = false; - emit(newState); + log.info('EstimateFeeEvent: done'); + emit(newState); + }); }); - on((event, emit) async { + on((event, emit) { final newState = state.clone()..feeOption = event.feeOption; if (state.balance != null && state.fee != null && diff --git a/lib/screen/settings/crypto/send/send_crypto_state.dart b/lib/screen/settings/crypto/send/send_crypto_state.dart index 3de32d0696..1437a381b4 100644 --- a/lib/screen/settings/crypto/send/send_crypto_state.dart +++ b/lib/screen/settings/crypto/send/send_crypto_state.dart @@ -47,9 +47,8 @@ class CurrencyTypeChangedEvent extends SendCryptoEvent { class EstimateFeeEvent extends SendCryptoEvent { final String address; final BigInt amount; - final SendCryptoState? newState; - EstimateFeeEvent(this.address, this.amount, {this.newState}); + EstimateFeeEvent(this.address, this.amount); } class SendCryptoState { diff --git a/lib/screen/settings/crypto/send_artwork/send_artwork_bloc.dart b/lib/screen/settings/crypto/send_artwork/send_artwork_bloc.dart index 3f91d40b59..93f1bc44bd 100644 --- a/lib/screen/settings/crypto/send_artwork/send_artwork_bloc.dart +++ b/lib/screen/settings/crypto/send_artwork/send_artwork_bloc.dart @@ -40,6 +40,7 @@ class SendArtworkBloc extends AuBloc { final DomainAddressService _domainAddressService; String? cachedAddress; BigInt? cachedBalance; + final _estimateLock = Lock(); final _safeBuffer = BigInt.from(10); @@ -56,37 +57,39 @@ class SendArtworkBloc extends AuBloc { final type = _asset.blockchain == 'ethereum' ? CryptoType.ETH : CryptoType.XTZ; on((event, emit) async { - final newState = state.clone()..wallet = event.wallet; + await _estimateLock.synchronized(() async { + final newState = state.clone()..wallet = event.wallet; - final exchangeRate = await _currencyService.getExchangeRates(); - newState.exchangeRate = exchangeRate; + final exchangeRate = await _currencyService.getExchangeRates(); + newState.exchangeRate = exchangeRate; - switch (type) { - case CryptoType.ETH: - final ownerAddress = - await event.wallet.getETHEip55Address(index: event.index); - final balance = await _ethereumService.getBalance(ownerAddress); + switch (type) { + case CryptoType.ETH: + final ownerAddress = + await event.wallet.getETHEip55Address(index: event.index); + final balance = + await _ethereumService.getBalance(ownerAddress, doRetry: true); - newState.balance = balance.getInWei; - newState.isValid = _isValid(newState); - break; - case CryptoType.XTZ: - final address = - await event.wallet.getTezosAddress(index: event.index); - final balance = await _tezosService.getBalance(address); + newState.balance = balance.getInWei; + newState.isValid = _isValid(newState); + case CryptoType.XTZ: + final address = + await event.wallet.getTezosAddress(index: event.index); + final balance = + await _tezosService.getBalance(address, doRetry: true); - newState.balance = BigInt.from(balance); - newState.isValid = _isValid(newState); - break; - default: - break; - } + newState.balance = BigInt.from(balance); + newState.isValid = _isValid(newState); + default: + break; + } - cachedBalance = newState.balance; - emit(newState); + cachedBalance = newState.balance; + emit(newState); + }); }); - on((event, emit) async { + on((event, emit) { log.info('[SendArtworkBloc] QuantityUpdateEvent: ${event.quantity}'); final newState = state.clone() ..quantity = event.quantity @@ -137,150 +140,149 @@ class SendArtworkBloc extends AuBloc { }); on((event, emit) async { - log.info('[SendArtworkBloc] Estimate fee: ${event.quantity}'); - emit(state.copyWith(isEstimating: true)); + await _estimateLock.synchronized(() async { + log.info('[SendArtworkBloc] Estimate fee: ${event.quantity}'); + emit(state.copyWith(isEstimating: true)); - BigInt? fee; - FeeOptionValue? feeOptionValue; - switch (type) { - case CryptoType.ETH: - final wallet = state.wallet; - final index = event.index; - if (wallet == null) { - return; - } - - final contractAddress = - EthereumAddress.fromHex(event.contractAddress); - final to = EthereumAddress.fromHex(event.address); - final from = EthereumAddress.fromHex( - await state.wallet!.getETHEip55Address(index: index)); + BigInt? fee; + FeeOptionValue? feeOptionValue; + switch (type) { + case CryptoType.ETH: + final wallet = state.wallet; + final index = event.index; + if (wallet == null) { + return; + } - final data = _asset.contractType == 'erc1155' - ? await _ethereumService.getERC1155TransferTransactionData( - contractAddress, from, to, event.tokenId, event.quantity, - feeOption: state.feeOption) - : await _ethereumService.getERC721TransferTransactionData( - contractAddress, from, to, event.tokenId, - feeOption: state.feeOption); + final contractAddress = + EthereumAddress.fromHex(event.contractAddress); + final to = EthereumAddress.fromHex(event.address); + final from = EthereumAddress.fromHex( + await state.wallet!.getETHEip55Address(index: index)); - try { - feeOptionValue = await _ethereumService.estimateFee( - wallet, index, contractAddress, EtherAmount.zero(), data); - fee = feeOptionValue.getFee(state.feeOption); - } on RPCError catch (e) { - log.info('[SendArtworkBloc] RPCError: ' - 'errorCode: ${e.errorCode} ' - 'message: ${e.message}' - 'data: ${e.data}'); - _navigationService.showErrorDialog( - ErrorEvent(e, 'estimation_failed'.tr(), e.errorMessage, - ErrorItemState.tryAgain), cancelAction: () { - _navigationService.hideInfoDialog(); - return; - }, defaultAction: () { - add(event); - }); - } catch (e) { - log.info('[SendArtworkBloc] Error: $e'); - _navigationService.showErrorDialog( - ErrorEvent(e, 'estimation_failed'.tr(), e.toString(), - ErrorItemState.tryAgain), cancelAction: () { - _navigationService.hideInfoDialog(); + try { + final data = _asset.contractType == 'erc1155' + ? await _ethereumService.getERC1155TransferTransactionData( + contractAddress, from, to, event.tokenId, event.quantity, + feeOption: state.feeOption) + : await _ethereumService.getERC721TransferTransactionData( + contractAddress, from, to, event.tokenId, + feeOption: state.feeOption); + feeOptionValue = await _ethereumService.estimateFee( + wallet, index, contractAddress, EtherAmount.zero(), data); + fee = feeOptionValue.getFee(state.feeOption); + } on RPCError catch (e) { + log.info('[SendArtworkBloc] RPCError: ' + 'errorCode: ${e.errorCode} ' + 'message: ${e.message}' + 'data: ${e.data}'); + _navigationService.showErrorDialog( + ErrorEvent(e, 'estimation_failed'.tr(), e.errorMessage, + ErrorItemState.tryAgain), cancelAction: () { + _navigationService.hideInfoDialog(); + return; + }, defaultAction: () { + add(event); + }); + } catch (e) { + log.info('[SendArtworkBloc] Error: $e'); + _navigationService.showErrorDialog( + ErrorEvent(e, 'estimation_failed'.tr(), e.toString(), + ErrorItemState.tryAgain), cancelAction: () { + _navigationService.hideInfoDialog(); + return; + }, defaultAction: () { + add(event); + }); + } + case CryptoType.XTZ: + final wallet = state.wallet; + final index = event.index; + if (wallet == null) { return; - }, defaultAction: () { - add(event); - }); - } - break; - case CryptoType.XTZ: - final wallet = state.wallet; - final index = event.index; - if (wallet == null) { - return; - } - try { - final operation = await _tezosService.getFa2TransferOperation( - event.contractAddress, - await wallet.getTezosAddress(index: index), - event.address, - event.tokenId, - event.quantity); - final tezosFee = await _tezosService.estimateOperationFee( - await wallet.getTezosPublicKey(index: index), [operation], - baseOperationCustomFee: - state.feeOption.tezosBaseOperationCustomFee); - fee = BigInt.from(tezosFee); - feeOptionValue = FeeOptionValue( - BigInt.from(tezosFee - - state.feeOption.tezosBaseOperationCustomFee + - baseOperationCustomFeeLow), - BigInt.from(tezosFee - - state.feeOption.tezosBaseOperationCustomFee + - baseOperationCustomFeeMedium), - BigInt.from(tezosFee - - state.feeOption.tezosBaseOperationCustomFee + - baseOperationCustomFeeHigh)); - } on TezartNodeError catch (err) { - if (!emit.isDone) { - if (_navigationService.mounted) { + } + try { + final operation = await _tezosService.getFa2TransferOperation( + event.contractAddress, + await wallet.getTezosAddress(index: index), + event.address, + event.tokenId, + event.quantity); + final tezosFee = await _tezosService.estimateOperationFee( + await wallet.getTezosPublicKey(index: index), [operation], + baseOperationCustomFee: + state.feeOption.tezosBaseOperationCustomFee); + fee = BigInt.from(tezosFee); + feeOptionValue = FeeOptionValue( + BigInt.from(tezosFee - + state.feeOption.tezosBaseOperationCustomFee + + baseOperationCustomFeeLow), + BigInt.from(tezosFee - + state.feeOption.tezosBaseOperationCustomFee + + baseOperationCustomFeeMedium), + BigInt.from(tezosFee - + state.feeOption.tezosBaseOperationCustomFee + + baseOperationCustomFeeHigh)); + } on TezartNodeError catch (err) { + if (!emit.isDone) { + if (_navigationService.context.mounted) { + unawaited(UIHelper.showInfoDialog( + _navigationService.context, + 'estimation_failed'.tr(), + getTezosErrorMessage(err), + isDismissible: true, + )); + } + fee = BigInt.zero; + feeOptionValue = + FeeOptionValue(BigInt.zero, BigInt.zero, BigInt.zero); + } + } on TezartHttpError catch (err) { + log.info(err); + if (_navigationService.context.mounted) { unawaited(UIHelper.showInfoDialog( _navigationService.context, 'estimation_failed'.tr(), - getTezosErrorMessage(err), + 'cannot_connect_to_rpc'.tr(), isDismissible: true, + closeButton: 'try_again'.tr(), + onClose: () { + add(event); + Navigator.of(_navigationService.context).pop(); + }, )); } + } catch (err) { + if (!emit.isDone) { + unawaited(showErrorDialogFromException(err)); + } fee = BigInt.zero; feeOptionValue = FeeOptionValue(BigInt.zero, BigInt.zero, BigInt.zero); } - } on TezartHttpError catch (err) { - log.info(err); - if (_navigationService.mounted) { - unawaited(UIHelper.showInfoDialog( - _navigationService.context, - 'estimation_failed'.tr(), - 'cannot_connect_to_rpc'.tr(), - isDismissible: true, - closeButton: 'try_again'.tr(), - onClose: () { - add(event); - Navigator.of(_navigationService.context).pop(); - }, - )); - } - } catch (err) { - if (!emit.isDone) { - unawaited(showErrorDialogFromException(err)); - } + default: fee = BigInt.zero; feeOptionValue = FeeOptionValue(BigInt.zero, BigInt.zero, BigInt.zero); - } - break; - default: - fee = BigInt.zero; - feeOptionValue = - FeeOptionValue(BigInt.zero, BigInt.zero, BigInt.zero); - break; - } + break; + } - if (!emit.isDone) { - final newState = - event.newState == null ? state.clone() : event.newState!.clone() - ..fee = fee - ..feeOptionValue = feeOptionValue - ..isEstimating = false; - newState.isValid = _isValid(newState); - emit(newState); - } + if (!emit.isDone) { + final newState = + event.newState == null ? state.clone() : event.newState!.clone() + ..fee = fee + ..feeOptionValue = feeOptionValue + ..isEstimating = false; + newState.isValid = _isValid(newState); + emit(newState); + } + }); }, transformer: (events, mapper) => events .debounceTime(const Duration(milliseconds: 300)) .switchMap(mapper)); - on((event, emit) async { + on((event, emit) { final newState = state.clone()..feeOption = event.feeOption; newState.fee = newState.feeOptionValue?.getFee(event.feeOption); emit(newState); diff --git a/lib/screen/settings/crypto/send_artwork/send_artwork_page.dart b/lib/screen/settings/crypto/send_artwork/send_artwork_page.dart index 90d05f3937..fd1b088ee0 100644 --- a/lib/screen/settings/crypto/send_artwork/send_artwork_page.dart +++ b/lib/screen/settings/crypto/send_artwork/send_artwork_page.dart @@ -586,7 +586,7 @@ class _SendArtworkPageState extends State { } Widget _artworkView(BuildContext context) { - final title = widget.payload.asset.title; + final title = widget.payload.asset.displayTitle; final theme = Theme.of(context); final asset = widget.payload.asset; diff --git a/lib/screen/settings/crypto/send_artwork/send_artwork_review_page.dart b/lib/screen/settings/crypto/send_artwork/send_artwork_review_page.dart index 393879e971..b38618887f 100644 --- a/lib/screen/settings/crypto/send_artwork/send_artwork_review_page.dart +++ b/lib/screen/settings/crypto/send_artwork/send_artwork_review_page.dart @@ -162,7 +162,7 @@ class _SendArtworkReviewPageState extends State { }; Navigator.of(context).pop(payload); } - } catch (e) { + } on Exception catch (_) { if (!mounted) { return; } @@ -228,7 +228,7 @@ class _SendArtworkReviewPageState extends State { _item( context: context, title: 'title'.tr(), - content: assetToken.title ?? '', + content: assetToken.displayTitle ?? '', ), divider, _item( diff --git a/lib/screen/settings/crypto/send_review_page.dart b/lib/screen/settings/crypto/send_review_page.dart index c66e01c43c..aec73813ff 100644 --- a/lib/screen/settings/crypto/send_review_page.dart +++ b/lib/screen/settings/crypto/send_review_page.dart @@ -69,7 +69,6 @@ class _SendReviewPageState extends State { 'hash': txHash, }; Navigator.of(context).pop(payload); - break; case CryptoType.XTZ: final opHash = await injector().sendTransaction( widget.payload.wallet, @@ -86,7 +85,6 @@ class _SendReviewPageState extends State { 'hash': opHash, }; Navigator.of(context).pop(payload); - break; case CryptoType.USDC: final address = await widget.payload.wallet .getETHEip55Address(index: widget.payload.index); @@ -115,11 +113,10 @@ class _SendReviewPageState extends State { 'hash': txHash, }; Navigator.of(context).pop(payload); - break; default: break; } - } catch (e) { + } on Exception catch (_) { if (!mounted) { return; } @@ -129,7 +126,6 @@ class _SendReviewPageState extends State { 'try_later'.tr(), )); } - setState(() { _isSending = false; }); diff --git a/lib/screen/settings/crypto/wallet_detail/linked_wallet_detail_page.dart b/lib/screen/settings/crypto/wallet_detail/linked_wallet_detail_page.dart index 21d0b1d04c..8a38697164 100644 --- a/lib/screen/settings/crypto/wallet_detail/linked_wallet_detail_page.dart +++ b/lib/screen/settings/crypto/wallet_detail/linked_wallet_detail_page.dart @@ -497,8 +497,6 @@ class _LinkedWalletDetailPageState extends State title: 'unhide_from_collection_view'.tr(), icon: SvgPicture.asset( 'assets/images/unhide.svg', - colorFilter: - const ColorFilter.mode(AppColor.primaryBlack, BlendMode.srcIn), ), onTap: () { unawaited(injector().setHideLinkedAccountInGallery( @@ -514,7 +512,7 @@ class _LinkedWalletDetailPageState extends State title: 'hide_from_collection_view'.tr(), icon: const Icon( AuIcon.hidden_artwork, - color: AppColor.primaryBlack, + color: AppColor.white, ), onTap: () { unawaited(injector().setHideLinkedAccountInGallery( @@ -529,8 +527,6 @@ class _LinkedWalletDetailPageState extends State title: 'rename'.tr(), icon: SvgPicture.asset( 'assets/images/rename_icon.svg', - colorFilter: - const ColorFilter.mode(AppColor.primaryBlack, BlendMode.srcIn), ), onTap: _onRenameTap, ), diff --git a/lib/screen/settings/crypto/wallet_detail/wallet_detail_page.dart b/lib/screen/settings/crypto/wallet_detail/wallet_detail_page.dart index 727e63a946..4f5ef71772 100644 --- a/lib/screen/settings/crypto/wallet_detail/wallet_detail_page.dart +++ b/lib/screen/settings/crypto/wallet_detail/wallet_detail_page.dart @@ -710,8 +710,6 @@ class _WalletDetailPageState extends State with RouteAware { title: 'unhide_from_collection_view'.tr(), icon: SvgPicture.asset( 'assets/images/unhide.svg', - colorFilter: - const ColorFilter.mode(AppColor.primaryBlack, BlendMode.srcIn), ), onTap: () { unawaited(injector() @@ -727,7 +725,7 @@ class _WalletDetailPageState extends State with RouteAware { title: 'hide_from_collection_view'.tr(), icon: const Icon( AuIcon.hidden_artwork, - color: AppColor.primaryBlack, + color: AppColor.white, ), onTap: () { unawaited(injector() @@ -742,7 +740,7 @@ class _WalletDetailPageState extends State with RouteAware { title: 'scan'.tr(), icon: const Icon( AuIcon.scan, - color: AppColor.primaryBlack, + color: AppColor.white, ), onTap: _connectionIconTap, ), @@ -750,8 +748,6 @@ class _WalletDetailPageState extends State with RouteAware { title: 'rename'.tr(), icon: SvgPicture.asset( 'assets/images/rename_icon.svg', - colorFilter: - const ColorFilter.mode(AppColor.primaryBlack, BlendMode.srcIn), ), onTap: _onRenameTap, ), diff --git a/lib/screen/settings/data_management/data_management_page.dart b/lib/screen/settings/data_management/data_management_page.dart index 47cad00c4f..f1cda6666f 100644 --- a/lib/screen/settings/data_management/data_management_page.dart +++ b/lib/screen/settings/data_management/data_management_page.dart @@ -130,6 +130,7 @@ class _DataManagementPageState extends State { () async { await injector().purgeCachedGallery(); await injector().emptyCache(); + await DefaultCacheManager().emptyCache(); await injector().refreshTokens(syncAddresses: true); NftCollectionBloc.eventController .add(GetTokensByOwnerEvent(pageKey: PageKey.init())); diff --git a/lib/screen/settings/forget_exist/forget_exist_bloc.dart b/lib/screen/settings/forget_exist/forget_exist_bloc.dart index e5cca4f145..93dfc011e7 100644 --- a/lib/screen/settings/forget_exist/forget_exist_bloc.dart +++ b/lib/screen/settings/forget_exist/forget_exist_bloc.dart @@ -82,6 +82,8 @@ class ForgetExistBloc extends AuBloc { await SentryBreadcrumbLogger.clear(); _authService.reset(); + unawaited(injector().emptyCache()); + unawaited(DefaultCacheManager().emptyCache()); unawaited(injector().mixPanelClient.reset()); memoryValues = MemoryValues( branchDeeplinkData: ValueNotifier(null), diff --git a/lib/screen/settings/help_us/inapp_webview.dart b/lib/screen/settings/help_us/inapp_webview.dart index 56f02343c2..b2cd5ca215 100644 --- a/lib/screen/settings/help_us/inapp_webview.dart +++ b/lib/screen/settings/help_us/inapp_webview.dart @@ -29,17 +29,21 @@ class InAppWebViewPage extends StatefulWidget { } class _InAppWebViewPageState extends State { - late InAppWebViewController webViewController; + InAppWebViewController? webViewController; late String title; late bool isLoading; final _configurationService = injector(); bool _isTrusted = false; + late bool _canGoBack; + late bool _canGoForward; @override void initState() { super.initState(); title = Uri.parse(widget.payload.url).host; isLoading = false; + _canGoBack = false; + _canGoForward = false; unawaited(_checkCertificate(widget.payload.url)); } @@ -135,6 +139,12 @@ class _InAppWebViewPageState extends State { isLoading = false; }); }, + onUpdateVisitedHistory: (controller, uri, _) { + setState(() { + title = uri!.host; + }); + unawaited(refreshAppBarStatus()); + }, shouldOverrideUrlLoading: (controller, navigationAction) async { log.info('shouldOverrideUrlLoading'); @@ -243,41 +253,62 @@ class _InAppWebViewPageState extends State { ); } + Future refreshAppBarStatus() async { + final canGoBack = await webViewController?.canGoBack() ?? false; + final canGoForward = await webViewController?.canGoForward() ?? false; + setState(() { + _canGoBack = canGoBack; + _canGoForward = canGoForward; + }); + } + Widget _bottomBar(BuildContext context) => Container( color: AppColor.white, padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 50), child: Row( children: [ IconButton( - icon: const Icon(AuIcon.chevron), + icon: Icon(AuIcon.chevron, + color: _canGoBack + ? AppColor.primaryBlack + : AppColor.disabledColor), onPressed: () async { - if (await webViewController.canGoBack()) { - await webViewController.goBack(); + if (await webViewController?.canGoBack() ?? false) { + await webViewController?.goBack(); + await refreshAppBarStatus(); } }, ), const Spacer(), IconButton( - icon: const RotatedBox( - quarterTurns: 2, child: Icon(AuIcon.chevron)), + icon: RotatedBox( + quarterTurns: 2, + child: Icon( + AuIcon.chevron, + color: _canGoForward + ? AppColor.primaryBlack + : AppColor.disabledColor, + )), onPressed: () async { - if (await webViewController.canGoForward()) { - await webViewController.goForward(); + if (await webViewController?.canGoForward() ?? false) { + await webViewController?.goForward(); + await refreshAppBarStatus(); } }, ), const Spacer(), IconButton( icon: SvgPicture.asset('assets/images/Reload.svg'), - onPressed: () { - unawaited(webViewController.reload()); + onPressed: () async { + await webViewController?.reload(); + await refreshAppBarStatus(); }, ), const Spacer(), IconButton( icon: SvgPicture.asset('assets/images/Share.svg'), onPressed: () async { - final currentUrl = await webViewController.getUrl(); + final currentUrl = await webViewController?.getUrl(); if (currentUrl != null) { unawaited(launchUrl( currentUrl, diff --git a/lib/screen/settings/hidden_artworks/hidden_artworks_page.dart b/lib/screen/settings/hidden_artworks/hidden_artworks_page.dart index 7ab4768474..f1320cc120 100644 --- a/lib/screen/settings/hidden_artworks/hidden_artworks_page.dart +++ b/lib/screen/settings/hidden_artworks/hidden_artworks_page.dart @@ -168,16 +168,14 @@ class _HiddenArtworksPageState extends State { width: double.infinity, height: double.infinity, fit: BoxFit.cover, + cacheManager: injector(), + maxHeightDiskCache: _cachedImageSize, + maxWidthDiskCache: _cachedImageSize, memCacheHeight: _cachedImageSize, memCacheWidth: _cachedImageSize, - cacheManager: injector(), - placeholder: (context, index) => - const GalleryThumbnailPlaceholder(), + placeholder: _loadingBuilder, errorWidget: (context, url, error) => const GalleryThumbnailErrorWidget(), - placeholderFadeInDuration: const Duration( - milliseconds: 300, - ), ), ), ClipRRect( @@ -200,7 +198,7 @@ class _HiddenArtworksPageState extends State { unawaited(injector().backup()); NftCollectionBloc.eventController.add(ReloadEvent()); - if (!mounted) { + if (!context.mounted) { return; } unawaited(UIHelper.showHideArtworkResultDialog( @@ -223,4 +221,7 @@ class _HiddenArtworksPageState extends State { controller: ScrollController(), ); } + + Widget _loadingBuilder(BuildContext context, url) => + const GalleryThumbnailPlaceholder(); } diff --git a/lib/screen/settings/subscription/subscription_page.dart b/lib/screen/settings/subscription/subscription_page.dart index 8da38b147b..d7f615d3ad 100644 --- a/lib/screen/settings/subscription/subscription_page.dart +++ b/lib/screen/settings/subscription/subscription_page.dart @@ -6,7 +6,6 @@ // import 'dart:async'; -import 'dart:io'; import 'package:after_layout/after_layout.dart'; import 'package:autonomy_flutter/common/injector.dart'; @@ -85,34 +84,26 @@ class _SubscriptionPageState extends State ); } - static String get _subscriptionsManagementLocation { - if (Platform.isIOS) { - return 'set_apl_sub'.tr(); //"Settings > Apple ID > Subscriptions."; - } else if (Platform.isAndroid) { - return 'pla_pay_sub' - .tr(); //"Play Store -> Payments & subscriptions -> Subscriptions."; - } else { - return ''; - } - } - Widget _statusSection( BuildContext context, UpgradeState state, ) { final theme = Theme.of(context); + final titleStyle = theme.textTheme.ppMori400Black16; + final contentStyle = theme.textTheme.ppMori400Black14; IAPProductStatus status = state.status; switch (status) { case IAPProductStatus.completed: return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('subscribed'.tr(), style: theme.textTheme.ppMori400Black16), - const SizedBox(height: 16), - Text('thank_support'.tr(args: [_subscriptionsManagementLocation]), - style: theme.textTheme.ppMori400Black14), + Text('subscribed'.tr(), style: titleStyle), + Text( + 'thank_support'.tr(), + style: contentStyle, + ), const SizedBox(height: 10), - _benefitImage(context), + _benefitImage(context, status), const SizedBox(height: 30), ], ); @@ -120,16 +111,15 @@ class _SubscriptionPageState extends State return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('sub_30_days'.tr(), //"Subscribed (30-day free trial)", - style: theme.textTheme.ppMori400Black16), - const SizedBox(height: 16), Text( - 'to_cancel_your_subscription'.tr( - namedArgs: {'location': _subscriptionsManagementLocation}), - //"You will be charged ${state.productDetails?.price ?? "US\$4.99"}/month starting $trialExpireDate. To cancel your subscription, go to $_subscriptionsManagementLocation", - style: theme.textTheme.ppMori400Black14), - const SizedBox(height: 10), - _benefitImage(context), + 'sub_30_days'.tr(), + style: titleStyle, + ), + Text( + 'you_are_enjoying_a_free_trial'.tr(), + style: contentStyle, + ), + _benefitImage(context, status), const SizedBox(height: 30), ], ); @@ -144,29 +134,40 @@ class _SubscriptionPageState extends State return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text( + 'free_user'.tr(), + style: titleStyle, + ), Text( 'your_subscription_has_expired'.tr(), - style: theme.textTheme.ppMori400Black14, + style: contentStyle, ), - _benefitImage(context), + _benefitImage(context, status), const SizedBox(height: 30), ], ); case IAPProductStatus.notPurchased: return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text( + 'free_user'.tr(), + style: titleStyle, + ), Text( 'upgrade_to_use'.tr(), - style: theme.textTheme.ppMori400Black14, + style: contentStyle, ), - _benefitImage(context), + _benefitImage(context, status), const SizedBox(height: 30), ], ); case IAPProductStatus.error: - return Text('error_loading_sub'.tr(), - //"Error when loading your subscription.", - style: theme.textTheme.ppMori400Black12); + return Text( + 'error_loading_sub'.tr(), + //"Error when loading your subscription.", + style: theme.textTheme.ppMori400Black12, + ); } } @@ -187,17 +188,18 @@ class _SubscriptionPageState extends State children: [ PrimaryButton( text: 'subscribed'.tr(), color: theme.disableColor), - const SizedBox(height: 6), + const SizedBox( + height: 10, + ), Text( - 'you_will_be_charged'.tr( + '${'you_are_subscribed_at'.tr( namedArgs: { 'price': state.productDetails?.price ?? '4.99usd'.tr(), - 'date': '', - 'location': _subscriptionsManagementLocation }, - ), + )}\n${'auto_renews_unless_cancelled'.tr()}', style: theme.textTheme.ppMori400Black12, + textAlign: TextAlign.center, ), ], )) @@ -215,21 +217,22 @@ class _SubscriptionPageState extends State child: Column( children: [ PrimaryButton( - onTap: () { - onPressSubscribe(context); - }, - text: 'sub_then_price'.tr()), - const SizedBox(height: 6), + text: 'subscribed_for_a_30_day'.tr(), + color: theme.disableColor, + ), + const SizedBox( + height: 10, + ), Text( - 'you_will_be_charged_starting'.tr( + '${'after_trial'.tr( namedArgs: { 'price': state.productDetails?.price ?? '4.99usd'.tr(), 'date': trialExpireDate, }, - ), + )}\n${'auto_renews_unless_cancelled'.tr()}', style: theme.textTheme.ppMori400Black12, textAlign: TextAlign.center, - ) + ), ], ), ), @@ -250,13 +253,20 @@ class _SubscriptionPageState extends State onTap: () { onPressSubscribe(context); }, - text: 'renew_for'.tr( + text: 'renew_feralfile_pro'.tr(), + ), + const SizedBox( + height: 10, + ), + Text( + '${'renew_for'.tr( namedArgs: { 'price': state.productDetails?.price ?? '4.99usd'.tr(), }, - ), + )}\n${'auto_renews_unless_cancelled'.tr()}', + style: theme.textTheme.ppMori400Black12, + textAlign: TextAlign.center, ), - const SizedBox(height: 6), ], ), ) @@ -273,15 +283,16 @@ class _SubscriptionPageState extends State onTap: () { onPressSubscribe(context); }, - text: 'sub_then_price'.tr()), + text: 'subscribe_for_a_30_day'.tr()), const SizedBox( - height: 6, + height: 10, ), Text( - 'then_price'.tr( + '${'then_price'.tr( args: [state.productDetails?.price ?? '4.99usd'.tr()], - ), + )}\n${'auto_renews_unless_cancelled'.tr()}', style: theme.textTheme.ppMori400Black12, + textAlign: TextAlign.center, ), ], ), @@ -289,17 +300,22 @@ class _SubscriptionPageState extends State ], ); case IAPProductStatus.error: - return Text('error_loading_sub'.tr(), - //"Error when loading your subscription.", - style: theme.textTheme.headlineMedium); + return Text( + 'error_loading_sub'.tr(), + //"Error when loading your subscription.", + style: theme.textTheme.headlineMedium, + ); } } - Widget _benefitImage(BuildContext context) => Column( + Widget _benefitImage(BuildContext context, IAPProductStatus status) => Column( children: [ Center( child: SvgPicture.asset( - 'assets/images/premium_comparation_light.svg', + [IAPProductStatus.trial, IAPProductStatus.completed] + .contains(status) + ? 'assets/images/premium_comparation_subscribed.svg' + : 'assets/images/premium_comparation_free_user.svg', height: 320, ), ), diff --git a/lib/screen/settings/subscription/upgrade_bloc.dart b/lib/screen/settings/subscription/upgrade_bloc.dart index e6fe60add5..d8a59905c5 100644 --- a/lib/screen/settings/subscription/upgrade_bloc.dart +++ b/lib/screen/settings/subscription/upgrade_bloc.dart @@ -22,8 +22,19 @@ class UpgradesBloc extends AuBloc { on((event, emit) async { final jwt = _configurationService.getIAPJWT(); if (jwt != null) { - if (jwt.isValid(withSubscription: true)) { - emit(UpgradeState(IAPProductStatus.completed, null)); + final subscriptionStatus = jwt.getSubscriptionStatus(); + if (subscriptionStatus.isPremium) { + if (subscriptionStatus.isTrial) { + emit( + UpgradeState( + IAPProductStatus.trial, + null, + trialExpiredDate: subscriptionStatus.expireDate, + ), + ); + } else { + emit(UpgradeState(IAPProductStatus.completed, null)); + } } else { final result = await _iapService.renewJWT(); emit(UpgradeState( diff --git a/lib/screen/tezos_beacon/tb_send_transaction_page.dart b/lib/screen/tezos_beacon/tb_send_transaction_page.dart index 3faeaab996..a2b227009d 100644 --- a/lib/screen/tezos_beacon/tb_send_transaction_page.dart +++ b/lib/screen/tezos_beacon/tb_send_transaction_page.dart @@ -206,8 +206,9 @@ class _TBSendTransactionPageState extends State { BigInt.from(fee - feeOption.tezosBaseOperationCustomFee + baseOperationCustomFeeHigh)); - balance = await injector() - .getBalance(await wallet.getTezosAddress(index: index)); + balance = await injector().getBalance( + await wallet.getTezosAddress(index: index), + doRetry: true); setState(() { _fee = fee; }); diff --git a/lib/screen/wallet/wallet_page.dart b/lib/screen/wallet/wallet_page.dart index 54213eec67..26a2295bce 100644 --- a/lib/screen/wallet/wallet_page.dart +++ b/lib/screen/wallet/wallet_page.dart @@ -28,7 +28,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; class WalletPage extends StatefulWidget { - const WalletPage({super.key}); + final WalletPagePayload? payload; + + const WalletPage({super.key, this.payload}); @override State createState() => _WalletPageState(); @@ -42,6 +44,11 @@ class _WalletPageState extends State WidgetsBinding.instance.addObserver(this); context.read().add(GetAccountsEvent()); unawaited(injector().backup()); + WidgetsBinding.instance.addPostFrameCallback((context) { + if (widget.payload?.openAddAddress == true) { + _showAddWalletOption(); + } + }); } @override @@ -74,8 +81,6 @@ class _WalletPageState extends State title: 'create_a_new_wallet'.tr(), icon: SvgPicture.asset( 'assets/images/joinFile.svg', - colorFilter: - const ColorFilter.mode(AppColor.primaryBlack, BlendMode.srcIn), height: 24, ), onTap: () { @@ -85,8 +90,8 @@ class _WalletPageState extends State ), OptionItem( title: 'add_an_existing_wallet'.tr(), - icon: Image.asset( - 'assets/images/icon_save.png', + icon: SvgPicture.asset( + 'assets/images/icon_save.svg', height: 24, ), onTap: () { @@ -99,8 +104,6 @@ class _WalletPageState extends State title: 'view_existing_address'.tr().toLowerCase().capitalize(), icon: SvgPicture.asset( 'assets/images/unhide.svg', - colorFilter: - const ColorFilter.mode(AppColor.primaryBlack, BlendMode.srcIn), height: 24, ), onTap: () { @@ -150,3 +153,9 @@ class _WalletPageState extends State ), ); } + +class WalletPagePayload { + final bool openAddAddress; + + const WalletPagePayload({required this.openAddAddress}); +} diff --git a/lib/screen/wallet_connect/send/wc_send_transaction_bloc.dart b/lib/screen/wallet_connect/send/wc_send_transaction_bloc.dart index f785084d09..658d0dc371 100644 --- a/lib/screen/wallet_connect/send/wc_send_transaction_bloc.dart +++ b/lib/screen/wallet_connect/send/wc_send_transaction_bloc.dart @@ -44,8 +44,9 @@ class WCSendTransactionBloc try { final estimatedFee = await _ethereumService.estimateFee( persona, event.index, event.address, event.amount, event.data); - final balance = await _ethereumService - .getBalance(await persona.getETHEip55Address(index: event.index)); + final balance = await _ethereumService.getBalance( + await persona.getETHEip55Address(index: event.index), + doRetry: true); newState ..feeOptionValue = estimatedFee ..fee = newState.feeOptionValue!.getFee(state.feeOption) @@ -84,8 +85,9 @@ class WCSendTransactionBloc final WalletStorage persona = LibAukDart.getWallet(event.uuid); final index = event.index; - final balance = await _ethereumService - .getBalance(await persona.getETHEip55Address(index: index)); + final balance = await _ethereumService.getBalance( + await persona.getETHEip55Address(index: index), + doRetry: true); try { final txHash = await _ethereumService.sendTransaction( persona, index, event.to, event.value, event.data, diff --git a/lib/screen/wallet_connect/v2/add_ethereum_chain_page.dart b/lib/screen/wallet_connect/v2/add_ethereum_chain_page.dart deleted file mode 100644 index f7770056da..0000000000 --- a/lib/screen/wallet_connect/v2/add_ethereum_chain_page.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'package:autonomy_flutter/model/add_ethereum_chain.dart'; -import 'package:autonomy_flutter/util/style.dart'; -import 'package:autonomy_flutter/view/back_appbar.dart'; -import 'package:autonomy_flutter/view/primary_button.dart'; -import 'package:autonomy_flutter/view/responsive.dart'; -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:feralfile_app_theme/feral_file_app_theme.dart'; -import 'package:flutter/material.dart'; - -class AddEthereumChainPage extends StatefulWidget { - const AddEthereumChainPage({required this.payload, super.key}); - - final AddEthereumChainPagePayload payload; - - @override - State createState() => _AddEthereumChainPageState(); -} - -class _AddEthereumChainPageState extends State { - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final params = widget.payload.parameter; - final textStyles = theme.textTheme.ppMori400Black14; - return Scaffold( - appBar: getBackAppBar( - context, - onBack: () { - Navigator.of(context).pop(); - }, - title: 'add_ethereum_chain_request'.tr(), - ), - body: Container( - margin: const EdgeInsets.only(bottom: 32), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - addTitleSpace(), - Text( - 'Confirm to allow connect to this chain.' - ' This action solely will not sign any ' - 'message or transaction', - style: textStyles), - const SizedBox(height: 10), - if (params.iconUrls != null && params.iconUrls!.isNotEmpty) - CachedNetworkImage( - imageUrl: params.iconUrls!.first, - width: 64, - height: 64, - ), - const SizedBox(height: 30), - Text('Blockchain: ${params.chainName ?? 'Unknown'}', - style: textStyles), - const SizedBox(height: 10), - Text('Chain: ${params.chainNet}', style: textStyles), - ], - ), - ), - ), - Padding( - padding: ResponsiveLayout.pageHorizontalEdgeInsets, - child: PrimaryButton( - onTap: () { - Navigator.of(context).pop(true); - }, - text: 'confirm'.tr(), - ), - ), - ], - ), - ), - ); - } -} - -class AddEthereumChainPagePayload { - final AddEthereumChainParameter parameter; - - const AddEthereumChainPagePayload({required this.parameter}); -} diff --git a/lib/screen/wallet_connect/wc_connect_page.dart b/lib/screen/wallet_connect/wc_connect_page.dart index 78fade8687..373051b30b 100644 --- a/lib/screen/wallet_connect/wc_connect_page.dart +++ b/lib/screen/wallet_connect/wc_connect_page.dart @@ -41,6 +41,7 @@ import 'package:feralfile_app_theme/feral_file_app_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:walletconnect_flutter_v2/apis/core/verify/models/verify_context.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:walletconnect_flutter_v2/apis/sign_api/models/sign_client_models.dart'; /* @@ -166,57 +167,73 @@ class _WCConnectPageState extends State UIHelper.showLoadingScreen(context, text: 'connecting_wallet'.tr())); late String payloadAddress; late CryptoType payloadType; - switch (connectionRequest.runtimeType) { - case Wc2Proposal: - if (connectionRequest.isAutonomyConnect) { - final account = await injector().getDefaultAccount(); - final accountDid = await account.getAccountDID(); - final walletAddresses = await injector() - .addressDao - .findByWalletID(account.uuid); - final accountNumber = - walletAddresses.map((e) => e.address).join('||'); - approveResponse = await injector().approveSession( - connectionRequest as Wc2Proposal, - accounts: [accountDid.substring('did:key:'.length)], - connectionKey: account.uuid, - accountNumber: accountNumber, - isAuConnect: true, + try { + switch (connectionRequest.runtimeType) { + case const (Wc2Proposal): + if (connectionRequest.isAutonomyConnect) { + final account = + await injector().getDefaultAccount(); + final accountDid = await account.getAccountDID(); + final walletAddresses = await injector() + .addressDao + .findByWalletID(account.uuid); + final accountNumber = + walletAddresses.map((e) => e.address).join('||'); + approveResponse = await injector().approveSession( + connectionRequest as Wc2Proposal, + accounts: [accountDid.substring('did:key:'.length)], + connectionKey: account.uuid, + accountNumber: accountNumber, + isAuConnect: true, + ); + payloadType = CryptoType.ETH; + payloadAddress = + await account.getETHEip55Address(index: selectedPersona!.index); + } else { + final address = await injector() + .getETHAddress(selectedPersona!.wallet, selectedPersona!.index); + approveResponse = await injector().approveSession( + connectionRequest as Wc2Proposal, + accounts: [address], + connectionKey: address, + accountNumber: address, + ); + payloadType = CryptoType.ETH; + payloadAddress = address; + } + + case const (BeaconRequest): + final wallet = selectedPersona!.wallet; + final index = selectedPersona!.index; + final publicKey = await wallet.getTezosPublicKey(index: index); + final address = wallet.getTezosAddressFromPubKey(publicKey); + approveResponse = + await injector().permissionResponse( + wallet.uuid, + index, + (connectionRequest as BeaconRequest).id, + publicKey, + address, ); - payloadType = CryptoType.ETH; - payloadAddress = - await account.getETHEip55Address(index: selectedPersona!.index); - } else { - final address = await injector() - .getETHAddress(selectedPersona!.wallet, selectedPersona!.index); - approveResponse = await injector().approveSession( - connectionRequest as Wc2Proposal, - accounts: [address], - connectionKey: address, - accountNumber: address, - ); - payloadType = CryptoType.ETH; payloadAddress = address; - } - - break; - case BeaconRequest: - final wallet = selectedPersona!.wallet; - final index = selectedPersona!.index; - final publicKey = await wallet.getTezosPublicKey(index: index); - final address = wallet.getTezosAddressFromPubKey(publicKey); - approveResponse = - await injector().permissionResponse( - wallet.uuid, - index, - (connectionRequest as BeaconRequest).id, - publicKey, - address, - ); - payloadAddress = address; - payloadType = CryptoType.XTZ; - break; - default: + payloadType = CryptoType.XTZ; + default: + } + } catch (e, s) { + log.info('[WCConnectPage] Approve error $e $s'); + unawaited(Sentry.captureException(e, stackTrace: s)); + if (!mounted) { + return; + } + // Pop Loading screen + Navigator.of(context).pop(); + // Pop connect screen + Navigator.of(context).pop(); + final message = 'connect_to_failed'.tr(namedArgs: { + 'name': connectionRequest.name ?? 'Secondary Wallet', + }); + await UIHelper.showConnectFailed(context, message: message); + return; } if (!mounted) { diff --git a/lib/service/account_service.dart b/lib/service/account_service.dart index fedf3645d3..1444a361f5 100644 --- a/lib/service/account_service.dart +++ b/lib/service/account_service.dart @@ -450,6 +450,11 @@ class AccountServiceImpl extends AccountService { for (var persona in currentPersonas) { if (!(await persona.wallet().isWalletCreated())) { await _cloudDB.personaDao.deletePersona(persona); + final addresses = + await _cloudDB.addressDao.getAddressesByPersona(persona.uuid); + await _addressService + .deleteAddresses(addresses.map((e) => e.address).toList()); + await _cloudDB.addressDao.deleteAddressesByPersona(persona.uuid); shouldBackup = true; } } diff --git a/lib/service/activation_service.dart b/lib/service/activation_service.dart deleted file mode 100644 index 184698ebe0..0000000000 --- a/lib/service/activation_service.dart +++ /dev/null @@ -1,78 +0,0 @@ -// -// SPDX-License-Identifier: BSD-2-Clause-Patent -// Copyright © 2022 Bitmark. All rights reserved. -// Use of this source code is governed by the BSD-2-Clause Plus Patent License -// that can be found in the LICENSE file. -// - -import 'dart:async'; - -import 'package:autonomy_flutter/gateway/activation_api.dart'; -import 'package:autonomy_flutter/service/navigation_service.dart'; -import 'package:autonomy_flutter/util/asset_token_ext.dart'; -import 'package:autonomy_flutter/util/log.dart'; -import 'package:dio/dio.dart'; -import 'package:nft_collection/models/asset_token.dart'; -import 'package:nft_collection/services/tokens_service.dart'; - -class ActivationService { - final ActivationApi _airdropApi; - final TokensService _tokensService; - final NavigationService _navigationService; - - const ActivationService( - this._airdropApi, this._tokensService, this._navigationService); - - Future getActivation({required String activationID}) async => - await _airdropApi.getActivation(activationID); - - Future claimActivation( - {required ActivationClaimRequest request, - required AssetToken assetToken}) async { - try { - final response = await _airdropApi.claim(request); - await _tokensService.setCustomTokens([ - assetToken.copyWith( - owner: request.address, - pending: true, - balance: 1, - lastActivityTime: DateTime.now(), - lastRefreshedTime: DateTime(1), - asset: assetToken.asset?.copyWith(initialSaleModel: 'airdrop')) - ]); - return response; - } catch (e) { - log.info('[Activation service] claimActivation: $e'); - if (e is DioException) { - switch (e.response?.data['message']) { - case 'cannot self claim': - await _navigationService.showAirdropJustOnce(); - break; - case 'invalid claim': - unawaited(_navigationService.showAirdropAlreadyClaimed()); - break; - case 'the token is not available for share': - unawaited(_navigationService.showAirdropAlreadyClaimed()); - break; - default: - unawaited(_navigationService.showActivationError( - e, - assetToken.id, - )); - } - } - rethrow; - } - } - - String getIndexerID(String chain, String contract, String tokenID) { - switch (chain) { - case 'ethereum': - return 'eth-$contract-$tokenID'; - case 'tezos': - return 'tez-$contract-$tokenID'; - default: - return ''; - } - } -} diff --git a/lib/service/airdrop_service.dart b/lib/service/airdrop_service.dart deleted file mode 100644 index de15cd673e..0000000000 --- a/lib/service/airdrop_service.dart +++ /dev/null @@ -1,484 +0,0 @@ -// -// SPDX-License-Identifier: BSD-2-Clause-Patent -// Copyright © 2022 Bitmark. All rights reserved. -// Use of this source code is governed by the BSD-2-Clause Plus Patent License -// that can be found in the LICENSE file. -// - -import 'dart:async'; -import 'dart:convert'; - -import 'package:autonomy_flutter/gateway/airdrop_api.dart'; -import 'package:autonomy_flutter/model/ff_account.dart'; -import 'package:autonomy_flutter/screen/claim/claim_token_page.dart'; -import 'package:autonomy_flutter/service/account_service.dart'; -import 'package:autonomy_flutter/service/feralfile_service.dart'; -import 'package:autonomy_flutter/service/navigation_service.dart'; -import 'package:autonomy_flutter/service/tezos_service.dart'; -import 'package:autonomy_flutter/util/asset_token_ext.dart'; -import 'package:autonomy_flutter/util/constants.dart'; -import 'package:autonomy_flutter/util/feralfile_extension.dart'; -import 'package:autonomy_flutter/util/ff_series_ext.dart'; -import 'package:autonomy_flutter/util/log.dart'; -import 'package:autonomy_flutter/util/ui_helper.dart'; -import 'package:collection/collection.dart'; -import 'package:dio/dio.dart'; -import 'package:flutter/foundation.dart'; -import 'package:nft_collection/database/dao/asset_token_dao.dart'; -import 'package:nft_collection/graphql/model/get_list_tokens.dart'; -import 'package:nft_collection/models/asset_token.dart'; -import 'package:nft_collection/services/indexer_service.dart'; -import 'package:nft_collection/services/tokens_service.dart'; - -class AirdropService { - final AirdropApi _airdropApi; - final AssetTokenDao _assetTokenDao; - final AccountService _accountService; - final TezosService _tezosService; - final TokensService _tokensService; - final FeralFileService _feralFileService; - final IndexerService _indexerService; - final NavigationService _navigationService; - - const AirdropService( - this._airdropApi, - this._assetTokenDao, - this._accountService, - this._tezosService, - this._tokensService, - this._feralFileService, - this._indexerService, - this._navigationService); - - Future share(AirdropShareRequest request) async => - await _airdropApi.share(request.tokenId, request); - - Future claimShare( - AirdropClaimShareRequest request) async => - await _airdropApi.claimShare(request.shareCode); - - Future requestClaim( - AirdropRequestClaimRequest request) async => - await _airdropApi.requestClaim(request); - - Future claim(AirdropClaimRequest request) async => - await _airdropApi.claim(request); - - Future getTokenByContract(List contractAddress) async { - final allTokens = await _assetTokenDao.findAllAssetTokens(); - final assetToken = allTokens.firstWhereOrNull( - (element) => contractAddress.contains(element.contractAddress)); - return assetToken; - } - - Future claimRequestGift( - AssetToken assetToken) async { - try { - final request = AirdropRequestClaimRequest( - ownerAddress: assetToken.owner, - id: MOMA_MEMENTO_6_CLAIM_ID, - indexID: assetToken.id); - final requestClaimResponse = await requestClaim(request); - return requestClaimResponse; - } catch (e) { - log.info('[Airdrop service] claimGift: $e'); - unawaited(_navigationService.showAirdropJustOnce()); - rethrow; - } - } - - Future claimGift( - {required String claimID, - required String shareCode, - required String seriesId, - required String receivingAddress}) async { - final defaultAccount = await _accountService.getDefaultAccount(); - final didKey = await defaultAccount.getAccountDID(); - final timestamp = - (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString(); - final didKeySignature = - await defaultAccount.getAccountDIDSignature(timestamp); - final series = await _feralFileService.getSeries(seriesId); - try { - final claimRequest = AirdropClaimRequest( - claimId: claimID, - shareCode: shareCode, - receivingAddress: receivingAddress, - did: didKey, - didSignature: didKeySignature, - timestamp: timestamp); - final claimResponse = await claim(claimRequest); - await _tokensService.reindexAddresses([receivingAddress]); - - final indexerId = - series.airdropInfo!.getTokenIndexerId(claimResponse.result.artworkID); - List assetTokens = await _fetchTokens( - indexerId: indexerId, - receiver: receivingAddress, - ); - if (assetTokens.isNotEmpty) { - await _tokensService.setCustomTokens(assetTokens); - } else { - assetTokens = [ - createPendingAssetToken( - series: series, - owner: receivingAddress, - tokenId: claimResponse.result.artworkID, - ) - ]; - await _tokensService.setCustomTokens(assetTokens); - } - return ClaimResponse( - token: assetTokens.first, airdropInfo: series.airdropInfo!); - } catch (e) { - log.info('[Airdrop service] claimGift: $e'); - if (e is DioException) { - switch (e.response?.data['message']) { - case 'cannot self claim': - unawaited(_navigationService.showAirdropJustOnce()); - break; - case 'invalid claim': - unawaited(_navigationService.showAirdropAlreadyClaimed()); - break; - case 'the token is not available for share': - unawaited(_navigationService.showAirdropAlreadyClaimed()); - break; - default: - unawaited(UIHelper.showClaimTokenError( - _navigationService.navigatorKey.currentContext!, - e, - series: series, - )); - } - } - rethrow; - } - } - - Future> _fetchTokens({ - required String indexerId, - required String receiver, - }) async { - try { - final List assets = await _indexerService - .getNftTokens(QueryListTokensRequest(ids: [indexerId])); - final tokens = assets - .map((e) => e - ..pending = true - ..owner = receiver - ..balance = 1 - ..owners.putIfAbsent(receiver, () => 1) - ..lastActivityTime = DateTime.now()) - .toList(); - return tokens; - } catch (e) { - return []; - } - } - - Future shareAirdrop(AssetToken assetToken) async { - try { - final ownerAddress = assetToken.owner; - final timestamp = - (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString(); - final isViewOnly = await assetToken.isViewOnly(); - String signature = ''; - String ownerPublicKey = ''; - if (!isViewOnly) { - final ownerWallet = await _accountService.getAccountByAddress( - chain: assetToken.blockchain, address: ownerAddress); - ownerPublicKey = await ownerWallet.wallet - .getTezosPublicKey(index: ownerWallet.index); - signature = await _tezosService.signMessage( - ownerWallet.wallet, - ownerWallet.index, - Uint8List.fromList(utf8.encode('${assetToken.id}|$timestamp'))); - } - final shareRequest = AirdropShareRequest( - tokenId: assetToken.id, - ownerAddress: assetToken.owner, - ownerPublicKey: ownerPublicKey, - signature: signature, - timestamp: timestamp); - final shareResponse = await share(shareRequest); - return shareResponse.deepLink; - } catch (e) { - log.info('[Airdrop service] shareGift: error $e'); - return null; - } - } - - Future claimFeralFileToken({ - required String receiveAddress, - required String id, - required String seriesID, - }) async { - final series = await _feralFileService.getSeries(seriesID); - - try { - series.checkAirdropStatusAndThrowIfError(); - - final defaultAccount = await _accountService.getDefaultAccount(); - final didKey = await defaultAccount.getAccountDID(); - final timestamp = - (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString(); - final didKeySignature = - await defaultAccount.getAccountDIDSignature(timestamp); - final tokenClaimResponse = await _airdropApi.feralfileClaim( - FeralFileTokenClaimRequest( - id: id, - receivingAddress: receiveAddress, - did: didKey, - didSignature: didKeySignature, - timestamp: timestamp)); - final claimResponse = await _feralFileService.setPendingToken( - receiver: receiveAddress, - response: tokenClaimResponse, - series: series, - ); - return claimResponse; - } catch (e) { - log.info('[Airdrop service] claimFeralFileToken: error $e'); - rethrow; - } - } -} - -class AirdropRequestClaimRequest { - String ownerAddress; - String id; - String indexID; - - AirdropRequestClaimRequest( - {required this.ownerAddress, required this.id, required this.indexID}); - - //toJson - Map toJson() => { - 'ownerAddress': ownerAddress, - 'id': id, - 'indexID': indexID, - }; - - // fromJson - factory AirdropRequestClaimRequest.fromJson(Map json) => - AirdropRequestClaimRequest( - ownerAddress: json['ownerAddress'], - id: json['id'], - indexID: json['indexID'], - ); -} - -class AirdropRequestClaimResponse { - String claimID; - String seriesID; - - AirdropRequestClaimResponse({required this.claimID, required this.seriesID}); - - //toJson - Map toJson() => { - 'claimID': claimID, - 'seriesID': seriesID, - }; - - // fromJson - factory AirdropRequestClaimResponse.fromJson(Map json) => - AirdropRequestClaimResponse( - claimID: json['claimID'], - seriesID: json['seriesID'], - ); -} - -class AirdropClaimRequest { - String claimId; - String shareCode; - String receivingAddress; - String did; - String didSignature; - String timestamp; - - AirdropClaimRequest( - {required this.claimId, - required this.shareCode, - required this.receivingAddress, - required this.did, - required this.didSignature, - required this.timestamp}); - - //toJson - Map toJson() => { - 'claimID': claimId, - 'shareCode': shareCode, - 'receivingAddress': receivingAddress, - 'did': did, - 'didSignature': didSignature, - 'timestamp': timestamp, - }; - - // fromJson - factory AirdropClaimRequest.fromJson(Map json) => - AirdropClaimRequest( - claimId: json['claimID'], - shareCode: json['shareCode'], - receivingAddress: json['receivingAddress'], - did: json['did'], - didSignature: json['didSignature'], - timestamp: json['timesStamp'], - ); -} - -class AirdropClaimShareRequest { - String shareCode; - - AirdropClaimShareRequest({required this.shareCode}); - - //toJson - Map toJson() => { - 'shareCode': shareCode, - }; - - // fromJson - factory AirdropClaimShareRequest.fromJson(Map json) => - AirdropClaimShareRequest( - shareCode: json['shareCode'], - ); -} - -class AirdropClaimShareResponse { - String shareCode; - String seriesID; - - AirdropClaimShareResponse({required this.shareCode, required this.seriesID}); - - //toJson - Map toJson() => { - 'shareCode': shareCode, - 'seriesID': seriesID, - }; - - // fromJson - factory AirdropClaimShareResponse.fromJson(Map json) => - AirdropClaimShareResponse( - shareCode: json['shareCode'], - seriesID: json['seriesID'], - ); -} - -class AirdropShareRequest { - String tokenId; - String ownerAddress; - String? ownerPublicKey; - String timestamp; - String? signature; - - AirdropShareRequest( - {required this.tokenId, - required this.ownerAddress, - required this.ownerPublicKey, - required this.signature, - required this.timestamp}); - - //toJson - Map toJson() => { - 'tokenId': tokenId, - 'ownerAddress': ownerAddress, - 'ownerPublicKey': ownerPublicKey, - 'timestamp': timestamp, - 'signature': signature, - }; - - // fromJson - factory AirdropShareRequest.fromJson(Map json) => - AirdropShareRequest( - tokenId: json['tokenId'], - ownerAddress: json['ownerAddress'], - ownerPublicKey: json['ownerPublicKey'], - timestamp: json['timestamp'], - signature: json['signature'], - ); -} - -class AirdropShareResponse { - String deepLink; - - AirdropShareResponse({required this.deepLink}); - - //toJson - Map toJson() => { - 'deeplink': deepLink, - }; - - // fromJson - factory AirdropShareResponse.fromJson(Map json) => - AirdropShareResponse( - deepLink: json['deeplink'], - ); -} - -class AirdropTokenIdentity { - String id; - String owner; - - AirdropTokenIdentity({required this.id, required this.owner}); - - //toJson - Map toJson() => { - 'id': id, - 'owner': owner, - }; - - // fromJson - factory AirdropTokenIdentity.fromJson(Map json) => - AirdropTokenIdentity( - id: json['id'], - owner: json['owner'], - ); -} - -enum AirdropType { - memento6, - unknown; - - String get seriesId { - switch (this) { - case AirdropType.memento6: - return memento6SeriesId; - default: - return 'unknown'; - } - } -} - -class FeralFileTokenClaimRequest { - final String id; - final String receivingAddress; - final String did; - final String didSignature; - final String timestamp; - - FeralFileTokenClaimRequest({ - required this.id, - required this.receivingAddress, - required this.did, - required this.didSignature, - required this.timestamp, - }); - - //toJson - Map toJson() => { - 'id': id, - 'receivingAddress': receivingAddress, - 'did': did, - 'didSignature': didSignature, - 'timestamp': timestamp, - }; - - // fromJson - factory FeralFileTokenClaimRequest.fromJson(Map json) => - FeralFileTokenClaimRequest( - id: json['id'], - receivingAddress: json['receivingAddress'], - did: json['did'], - didSignature: json['didSignature'], - timestamp: json['timestamp'], - ); -} diff --git a/lib/service/canvas_client_service.dart b/lib/service/canvas_client_service.dart deleted file mode 100644 index 0b1129c036..0000000000 --- a/lib/service/canvas_client_service.dart +++ /dev/null @@ -1,377 +0,0 @@ -import 'dart:async'; - -import 'package:autonomy_flutter/common/injector.dart'; -import 'package:autonomy_flutter/database/app_database.dart'; -import 'package:autonomy_flutter/model/pair.dart'; -import 'package:autonomy_flutter/model/play_list_model.dart'; -import 'package:autonomy_flutter/model/shared_postcard.dart'; -import 'package:autonomy_flutter/screen/detail/preview/canvas_device_bloc.dart'; -import 'package:autonomy_flutter/service/account_service.dart'; -import 'package:autonomy_flutter/service/navigation_service.dart'; -import 'package:autonomy_flutter/util/log.dart'; -import 'package:autonomy_flutter/view/user_agent_utils.dart' as my_device; -import 'package:collection/collection.dart'; -import 'package:feralfile_app_tv_proto/feralfile_app_tv_proto.dart'; -import 'package:flutter/material.dart'; -import 'package:synchronized/synchronized.dart'; -import 'package:uuid/uuid.dart'; - -class CanvasClientService { - final AppDatabase _db; - - CanvasClientService(this._db); - - final List _viewingDevices = []; - late final String _deviceId; - late final String _deviceName; - bool _didInitialized = false; - - final _connectDevice = Lock(); - final AccountService _accountService = injector(); - final NavigationService _navigationService = injector(); - - Offset currentCursorOffset = Offset.zero; - - CallOptions get _callOptions => CallOptions( - compression: const GzipCodec(), timeout: const Duration(seconds: 3)); - - Future init() async { - if (_didInitialized) { - return; - } - final device = my_device.DeviceInfo.instance; - _deviceName = await device.getMachineName() ?? 'Autonomy App'; - final account = await _accountService.getDefaultAccount(); - _deviceId = await account.getAccountDID(); - _didInitialized = true; - } - - Future shutdownAll() async { - await Future.wait(_viewingDevices.map((e) => shutDown(e))); - } - - Future shutDown(CanvasDevice device) async { - final channel = _getChannel(device); - await channel.shutdown(); - } - - ClientChannel _getChannel(CanvasDevice device) => ClientChannel( - device.ip, - port: device.port, - options: const ChannelOptions( - credentials: ChannelCredentials.insecure(), - ), - ); - - CanvasControlClient _getStub(CanvasDevice device) { - final channel = _getChannel(device); - return CanvasControlClient(channel); - } - - Future connectToDevice(CanvasDevice device) async => - _connectDevice.synchronized(() async => await _connectToDevice(device)); - - Future _connectToDevice(CanvasDevice device) async { - final stub = _getStub(device); - try { - final request = ConnectRequest() - ..device = (DeviceInfo() - ..deviceId = _deviceId - ..deviceName = _deviceName); - - final response = await stub.connect( - request, - options: _callOptions, - ); - log.info('CanvasClientService received: ${response.ok}'); - final index = - _viewingDevices.indexWhere((element) => element.ip == device.ip); - if (response.ok) { - log.info('CanvasClientService: Connected to device'); - device.isConnecting = true; - if (index == -1) { - _viewingDevices.add(device); - } else { - _viewingDevices[index].isConnecting = true; - } - await _db.canvasDeviceDao.insertCanvasDevice(device); - return true; - } else { - log.info('CanvasClientService: Failed to connect to device'); - if (index != -1) { - _viewingDevices[index].isConnecting = false; - } - return false; - } - } catch (e) { - log.info('CanvasClientService: Caught error: $e'); - rethrow; - } - } - - Future> checkDeviceStatus( - CanvasDevice device) async { - final stub = _getStub(device); - String? sceneId; - late CanvasServerStatus status; - try { - final request = CheckingStatus()..deviceId = _deviceId; - final response = await stub.status( - request, - options: _callOptions, - ); - log.info('CanvasClientService received: ${response.status}'); - switch (response.status) { - case ResponseStatus_ServingStatus.NOT_SERVING: - case ResponseStatus_ServingStatus.SERVICE_UNKNOWN: - status = CanvasServerStatus.notServing; - break; - case ResponseStatus_ServingStatus.SERVING: - if (response.sceneId.isNotEmpty) { - status = CanvasServerStatus.playing; - sceneId = response.sceneId; - } else { - status = CanvasServerStatus.connected; - } - break; - case ResponseStatus_ServingStatus.UNKNOWN: - status = CanvasServerStatus.open; - break; - } - } catch (e) { - log.info('CanvasClientService: Caught error: $e'); - status = CanvasServerStatus.error; - } - return Pair(status, sceneId); - } - - Future syncDevices() async { - final devices = await getAllDevices(); - final List devicesToAdd = []; - await Future.forEach(devices, (device) async { - final status = await checkDeviceStatus(device); - switch (status.first) { - case CanvasServerStatus.playing: - case CanvasServerStatus.connected: - device.playingSceneId = status.second; - device.isConnecting = true; - unawaited(_db.canvasDeviceDao.updateCanvasDevice(device)); - devicesToAdd.add(device); - break; - case CanvasServerStatus.open: - device.playingSceneId = status.second; - device.isConnecting = false; - unawaited(_db.canvasDeviceDao.updateCanvasDevice(device)); - devicesToAdd.add(device); - break; - case CanvasServerStatus.notServing: - unawaited(_updateLocalDisconnectedDevice(device)); - break; - case CanvasServerStatus.error: - break; - } - }); - _viewingDevices - ..clear() - ..addAll(devicesToAdd) - ..unique((element) => element.ip); - log.info( - 'CanvasClientService sync device available ${_viewingDevices.length}'); - } - - Future> getAllDevices() async { - final devices = await _db.canvasDeviceDao.getCanvasDevices(); - log.info('CanvasClientService get devices local ${devices.length}'); - return devices; - } - - Future> getConnectingDevices({bool doSync = false}) async { - if (doSync) { - await syncDevices(); - } else { - for (var device in _viewingDevices) { - final status = await checkDeviceStatus(device); - device.playingSceneId = status.second; - } - } - return _viewingDevices; - } - - Future _updateLocalDisconnectedDevice(CanvasDevice device) async { - final updatedDevice = device.copyWith(isConnecting: false) - ..playingSceneId = null; - await _db.canvasDeviceDao.updateCanvasDevice(updatedDevice); - } - - Future castSingleArtwork(CanvasDevice device, String tokenId) async { - final stub = _getStub(device); - final size = - MediaQuery.of(_navigationService.navigatorKey.currentContext!).size; - final playingDevice = _viewingDevices.firstWhereOrNull( - (element) => element.playingSceneId != null, - ); - if (playingDevice != null) { - currentCursorOffset = await getCursorOffset(playingDevice); - } - final castRequest = CastSingleRequest() - ..id = tokenId - ..cursorDrag = (DragGestureRequest() - ..dx = currentCursorOffset.dx - ..dy = currentCursorOffset.dy - ..coefficientX = 1 / size.width - ..coefficientY = 1 / size.height); - final response = await stub.castSingleArtwork(castRequest); - if (response.ok) { - final lst = _viewingDevices.firstWhereOrNull( - (element) { - final isEqual = element == device; - return isEqual; - }, - ); - lst?.playingSceneId = tokenId; - } else { - log.info('CanvasClientService: Failed to cast single artwork'); - } - return response.ok; - } - - Future uncastSingleArtwork(CanvasDevice device) async { - final stub = _getStub(device); - final uncastRequest = UncastSingleRequest()..id = ''; - final response = await stub.uncastSingleArtwork(uncastRequest); - if (response.ok) { - _viewingDevices - .firstWhereOrNull((element) => element == device) - ?.playingSceneId = null; - } - } - - Future castCollection( - CanvasDevice device, PlayListModel playlist) async { - if (playlist.tokenIDs == null || playlist.tokenIDs!.isEmpty) { - return false; - } - final stub = _getStub(device); - - final castRequest = CastCollectionRequest() - ..id = playlist.id ?? const Uuid().v4() - ..artworks.addAll(playlist.tokenIDs!.map((e) => PlayArtwork() - ..id = e - ..duration = playlist.playControlModel?.timer ?? 10)); - final response = await stub.castCollection(castRequest); - if (response.ok) { - _viewingDevices - .firstWhereOrNull((element) => element == device) - ?.playingSceneId = playlist.id; - } else { - log.info('CanvasClientService: Failed to cast collection'); - } - return response.ok; - } - - Future unCast(CanvasDevice device) async { - final stub = _getStub(device); - final unCastRequest = UnCastRequest()..id = ''; - final response = await stub.unCastArtwork(unCastRequest); - if (response.ok) { - _viewingDevices - .firstWhereOrNull((element) => element == device) - ?.playingSceneId = null; - } - } - - Future sendKeyBoard(List devices, int code) async { - for (var device in devices) { - final stub = _getStub(device); - final sendKeyboardRequest = KeyboardEventRequest()..code = code; - final response = await stub.keyboardEvent(sendKeyboardRequest); - if (response.ok) { - log.info('CanvasClientService: Keyboard Event Success $code'); - } else { - log.info('CanvasClientService: Keyboard Event Failed $code'); - } - } - } - - // function to rotate canvas - Future rotateCanvas(CanvasDevice device, - {bool clockwise = true}) async { - final stub = _getStub(device); - final rotateCanvasRequest = RotateRequest()..clockwise = clockwise; - try { - final response = await stub.rotate(rotateCanvasRequest); - log.info('CanvasClientService: Rotate Canvas Success ${response.degree}'); - } catch (e) { - log.info('CanvasClientService: Rotate Canvas Failed'); - } - } - - Future tap(List devices) async { - for (var device in devices) { - final stub = _getStub(device); - final tapRequest = TapGestureRequest(); - await stub.tapGesture(tapRequest); - } - } - - Future drag( - List devices, Offset offset, Size touchpadSize) async { - final dragRequest = DragGestureRequest() - ..dx = offset.dx - ..dy = offset.dy - ..coefficientX = 1 / touchpadSize.width - ..coefficientY = 1 / touchpadSize.height; - currentCursorOffset += offset; - for (var device in devices) { - final stub = _getStub(device); - await stub.dragGesture(dragRequest); - } - } - - Future getCursorOffset(CanvasDevice device) async { - final stub = _getStub(device); - final response = await stub.getCursorOffset(Empty()); - final size = - MediaQuery.of(_navigationService.navigatorKey.currentContext!).size; - final dx = size.width * response.coefficientX * response.dx; - final dy = size.height * response.coefficientY * response.dy; - return Offset(dx, dy); - } - - Future setCursorOffset(CanvasDevice device) async { - final stub = _getStub(device); - final size = - MediaQuery.of(_navigationService.navigatorKey.currentContext!).size; - final dx = currentCursorOffset.dx / size.width; - final dy = currentCursorOffset.dy / size.height; - final request = CursorOffset() - ..dx = dx - ..dy = dy - ..coefficientX = 1 / size.width - ..coefficientY = 1 / size.height; - - await stub.setCursorOffset(request); - } -} - -enum CanvasServerStatus { - open, - connected, - playing, - notServing, - error; - - DeviceStatus get toDeviceStatus { - switch (this) { - case CanvasServerStatus.error: - case CanvasServerStatus.notServing: - case CanvasServerStatus.open: - case CanvasServerStatus.connected: - return DeviceStatus.connected; - case CanvasServerStatus.playing: - return DeviceStatus.playing; - } - } -} - -extension CanvasServerStatusExt on CanvasServerStatus {} diff --git a/lib/service/canvas_client_service_v2.dart b/lib/service/canvas_client_service_v2.dart new file mode 100644 index 0000000000..11c9c6b40f --- /dev/null +++ b/lib/service/canvas_client_service_v2.dart @@ -0,0 +1,311 @@ +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// Copyright © 2022 Bitmark. All rights reserved. +// Use of this source code is governed by the BSD-2-Clause Plus Patent License +// that can be found in the LICENSE file. +// + +import 'dart:async'; + +import 'package:autonomy_flutter/gateway/tv_cast_api.dart'; +import 'package:autonomy_flutter/model/pair.dart'; +import 'package:autonomy_flutter/service/device_info_service.dart'; +import 'package:autonomy_flutter/service/hive_store_service.dart'; +import 'package:autonomy_flutter/service/navigation_service.dart'; +import 'package:autonomy_flutter/service/tv_cast_service.dart'; +import 'package:autonomy_flutter/util/log.dart'; +import 'package:autonomy_flutter/view/user_agent_utils.dart' as my_device; +import 'package:feralfile_app_tv_proto/feralfile_app_tv_proto.dart'; +import 'package:flutter/material.dart'; + +class CanvasClientServiceV2 { + final HiveStoreObjectService _db; + final DeviceInfoService _deviceInfoService; + final TvCastApi _tvCastApi; + final NavigationService _navigationService; + Timer? _timer; + final dragOffsets = []; + + CanvasClientServiceV2(this._db, this._deviceInfoService, this._tvCastApi, + this._navigationService); + + Offset currentCursorOffset = Offset.zero; + + DeviceInfoV2 get clientDeviceInfo => DeviceInfoV2( + deviceId: _deviceInfoService.deviceId, + deviceName: _deviceInfoService.deviceName, + platform: _platform, + ); + + TvCastService _getStub(CanvasDevice device) => + TvCastServiceImpl(_tvCastApi, device); + + Future getDeviceCastingStatus( + CanvasDevice device) async => + _getDeviceCastingStatus(device); + + Future _getDeviceCastingStatus( + CanvasDevice device) async { + final stub = _getStub(device); + final request = CheckDeviceStatusRequest(); + final response = await stub.status(request); + log.info( + 'CanvasClientService2 status: ${response.connectedDevice?.deviceId}'); + return response; + } + + DevicePlatform get _platform { + final device = my_device.DeviceInfo.instance; + if (device.isAndroid) { + return DevicePlatform.android; + } else if (device.isIOS) { + return DevicePlatform.iOS; + } else { + return DevicePlatform.other; + } + } + + Future?> addQrDevice( + CanvasDevice device) async { + final deviceStatus = await _getDeviceStatus(device); + if (deviceStatus != null) { + await _db.save(device, device.deviceId); + log.info('CanvasClientService: Added device to db ${device.name}'); + return deviceStatus; + } + return null; + } + + Future connect(CanvasDevice device) async { + final stub = _getStub(device); + final deviceInfo = clientDeviceInfo; + final request = ConnectRequestV2(clientDevice: deviceInfo); + final response = await stub.connect(request); + return response; + } + + Future connectToDevice(CanvasDevice device) async { + try { + final response = await connect(device); + return response.ok; + } catch (e) { + log.info('CanvasClientService: connectToDevice error: $e'); + return false; + } + } + + Future disconnectDevice(CanvasDevice device) async { + final stub = _getStub(device); + await stub.disconnect(DisconnectRequestV2()); + } + + Future castListArtwork( + CanvasDevice device, List artworks) async { + try { + final canConnect = await connectToDevice(device); + if (!canConnect) { + return false; + } + final stub = _getStub(device); + final castRequest = CastListArtworkRequest(artworks: artworks); + + final response = await stub.castListArtwork(castRequest); + return response.ok; + } catch (e) { + log.info('CanvasClientService: castListArtwork error: $e'); + return false; + } + } + + Future pauseCasting(CanvasDevice device) async { + final stub = _getStub(device); + final response = await stub.pauseCasting(PauseCastingRequest()); + return response.ok; + } + + Future resumeCasting(CanvasDevice device) async { + final stub = _getStub(device); + final response = await stub.resumeCasting(ResumeCastingRequest()); + return response.ok; + } + + Future nextArtwork(CanvasDevice device, {String? startTime}) async { + final stub = _getStub(device); + final request = NextArtworkRequest( + startTime: startTime == null ? null : int.tryParse(startTime)); + + final response = await stub.nextArtwork(request); + return response.ok; + } + + Future moveToArtwork(CanvasDevice device, + {required String artworkId, String? startTime}) async { + final stub = _getStub(device); + final artwork = + PlayArtworkV2(token: CastAssetToken(id: artworkId), duration: 0); + final request = MoveToArtworkRequest(artwork: artwork); + final reply = await stub.moveToArtwork(request); + return reply.ok; + } + + Future previousArtwork(CanvasDevice device, {String? startTime}) async { + final stub = _getStub(device); + final request = PreviousArtworkRequest( + startTime: startTime == null ? null : int.tryParse(startTime)); + final response = await stub.previousArtwork(request); + return response.ok; + } + + Future appendListArtwork( + CanvasDevice device, List artworks) async { + final stub = _getStub(device); + final response = await stub.appendListArtwork( + AppendArtworkToCastingListRequest(artworks: artworks)); + return response.ok; + } + + Future castExhibition( + CanvasDevice device, CastExhibitionRequest castRequest) async { + final canConnect = await connectToDevice(device); + if (!canConnect) { + return false; + } + final stub = _getStub(device); + final response = await stub.castExhibition(castRequest); + return response.ok; + } + + Future updateDuration( + CanvasDevice device, List artworks) async { + final stub = _getStub(device); + final response = + await stub.updateDuration(UpdateDurationRequest(artworks: artworks)); + return response; + } + + List _findRawDevices() { + final devices = _db.getAll(); + return devices; + } + + /// This method will get devices via mDNS and local db, for local db devices + /// it will check the status of the device by calling grpc + Future>> scanDevices() async { + final rawDevices = _findRawDevices(); + final List> devices = + await _getDeviceStatuses(rawDevices); + devices.sort((a, b) => a.first.name.compareTo(b.first.name)); + return devices; + } + + Future>> _getDeviceStatuses( + List devices) async { + final List> statuses = []; + await Future.wait(devices.map((device) async { + try { + final status = await _getDeviceStatus(device); + if (status != null) { + statuses.add(status); + } + } catch (e) { + log.info('CanvasClientService: _getDeviceStatus error: $e'); + } + })); + return statuses; + } + + Future?> _getDeviceStatus( + CanvasDevice device) async { + try { + final status = await _getDeviceCastingStatus(device); + return Pair(device, status); + } catch (e) { + log.info('CanvasClientService: _getDeviceStatus error: $e'); + return null; + } + } + + Future sendKeyBoard(List devices, int code) async { + for (var device in devices) { + final stub = _getStub(device); + final sendKeyboardRequest = KeyboardEventRequest(code: code); + final response = await stub.keyboardEvent(sendKeyboardRequest); + if (response.ok) { + log.info('CanvasClientService: Keyboard Event Success $code'); + } else { + log.info('CanvasClientService: Keyboard Event Failed $code'); + } + } + } + + // function to rotate canvas + Future rotateCanvas(CanvasDevice device, + {bool clockwise = true}) async { + final stub = _getStub(device); + final rotateCanvasRequest = RotateRequest(clockwise: clockwise); + try { + final response = await stub.rotate(rotateCanvasRequest); + log.info('CanvasClientService: Rotate Canvas Success ${response.degree}'); + } catch (e) { + log.info('CanvasClientService: Rotate Canvas Failed'); + } + } + + Future tap(List devices) async { + for (var device in devices) { + final stub = _getStub(device); + final tapRequest = TapGestureRequest(); + await stub.tap(tapRequest); + } + } + + Future drag( + List devices, Offset offset, Size touchpadSize) async { + final dragOffset = CursorOffset( + dx: offset.dx, + dy: offset.dy, + coefficientX: 1 / touchpadSize.width, + coefficientY: 1 / touchpadSize.height); + + currentCursorOffset += offset; + dragOffsets.add(dragOffset); + if (_timer == null || !_timer!.isActive) { + _timer = Timer(const Duration(milliseconds: 300), () { + for (var device in devices) { + final stub = _getStub(device); + final dragRequest = DragGestureRequest(cursorOffsets: dragOffsets); + stub.drag(dragRequest); + } + dragOffsets.clear(); + }); + } + } + + Future getCursorOffset(CanvasDevice device) async { + final stub = _getStub(device); + final response = await stub.getCursorOffset(GetCursorOffsetRequest()); + final size = + MediaQuery.of(_navigationService.navigatorKey.currentContext!).size; + final cursorOffset = response.cursorOffset; + final dx = size.width * cursorOffset.coefficientX * cursorOffset.dx; + final dy = size.height * cursorOffset.coefficientY * cursorOffset.dy; + return Offset(dx, dy); + } + + Future setCursorOffset(CanvasDevice device) async { + final stub = _getStub(device); + final size = + MediaQuery.of(_navigationService.navigatorKey.currentContext!).size; + final dx = currentCursorOffset.dx / size.width; + final dy = currentCursorOffset.dy / size.height; + final cursorOffset = CursorOffset( + dx: dx, + dy: dy, + coefficientX: 1 / size.width, + coefficientY: 1 / size.height); + + final request = SetCursorOffsetRequest(cursorOffset: cursorOffset); + + await stub.setCursorOffset(request); + } +} diff --git a/lib/service/configuration_service.dart b/lib/service/configuration_service.dart index 2c8d2e264a..1f3b6bf9bd 100644 --- a/lib/service/configuration_service.dart +++ b/lib/service/configuration_service.dart @@ -255,10 +255,6 @@ abstract class ConfigurationService { ShowAnouncementNotificationInfo getShowAnnouncementNotificationInfo(); - bool getAlreadyClaimedAirdrop(String seriesId); - - Future setAlreadyClaimedAirdrop(String seriesId, bool value); - // set and get for did_sync_artists Future setDidSyncArtists(bool value); @@ -270,6 +266,10 @@ abstract class ConfigurationService { bool getShowPostcardBanner(); + Future setShowAddAddressBanner(bool bool); + + bool getShowAddAddressBanner(); + Future setMerchandiseOrderIds(List ids, {bool override = false}); @@ -360,13 +360,13 @@ class ConfigurationServiceImpl implements ConfigurationService { static const String KEY_SHOW_ANOUNCEMENT_NOTIFICATION_INFO = 'show_anouncement_notification_info'; - static const String KEY_ALREADY_CLAIMED_AIRDROP = 'already_claimed_airdrop'; - static const String KEY_PROCESSING_STAMP_POSTCARD = 'processing_stamp_postcard'; static const String KEY_SHOW_POSTCARD_BANNER = 'show_postcard_banner'; + static const String KEY_SHOW_ADD_ADDRESS_BANNER = 'show_add_address_banner'; + static const String KEY_MERCHANDISE_ORDER_IDS = 'merchandise_order_ids'; @override @@ -1053,26 +1053,6 @@ class ConfigurationServiceImpl implements ConfigurationService { return ShowAnouncementNotificationInfo.fromJson(jsonDecode(data)); } - @override - bool getAlreadyClaimedAirdrop(String seriesId) { - final data = _preferences.getStringList(KEY_ALREADY_CLAIMED_AIRDROP); - if (data == null) { - return false; - } - return data.contains(seriesId); - } - - @override - Future setAlreadyClaimedAirdrop(String seriesId, bool value) async { - final data = _preferences.getStringList(KEY_ALREADY_CLAIMED_AIRDROP) ?? []; - if (value) { - data.add(seriesId); - } else { - data.remove(seriesId); - } - await _preferences.setStringList(KEY_ALREADY_CLAIMED_AIRDROP, data); - } - @override bool getDidSyncArtists() => _preferences.getBool(KEY_DID_SYNC_ARTISTS) ?? false; @@ -1200,6 +1180,15 @@ class ConfigurationServiceImpl implements ConfigurationService { bool getShowPostcardBanner() => _preferences.getBool(KEY_SHOW_POSTCARD_BANNER) ?? true; + @override + Future setShowAddAddressBanner(bool bool) async { + await _preferences.setBool(KEY_SHOW_ADD_ADDRESS_BANNER, bool); + } + + @override + bool getShowAddAddressBanner() => + _preferences.getBool(KEY_SHOW_ADD_ADDRESS_BANNER) ?? true; + @override List getMerchandiseOrderIds() => _preferences.getStringList(KEY_MERCHANDISE_ORDER_IDS) ?? []; diff --git a/lib/service/currency_service.dart b/lib/service/currency_service.dart index d0afd2c604..3493be0129 100644 --- a/lib/service/currency_service.dart +++ b/lib/service/currency_service.dart @@ -19,9 +19,13 @@ class CurrencyServiceImpl extends CurrencyService { @override Future getExchangeRates() async { - final response = await _currencyExchangeApi.getExchangeRates(); + try { + final response = await _currencyExchangeApi.getExchangeRates(); - return response["data"]?.rates ?? - const CurrencyExchangeRate(eth: "1.0", xtz: "1.0"); + return response['data']?.rates ?? + const CurrencyExchangeRate(eth: '1.0', xtz: '1.0'); + } catch (_) { + return const CurrencyExchangeRate(eth: '1.0', xtz: '1.0'); + } } } diff --git a/lib/service/customer_support_service.dart b/lib/service/customer_support_service.dart index 39fabf8d03..54b1e04f8e 100644 --- a/lib/service/customer_support_service.dart +++ b/lib/service/customer_support_service.dart @@ -140,7 +140,10 @@ class CustomerSupportServiceImpl extends CustomerSupportService { var issueTitle = draftData.title ?? draftData.text; if (issueTitle == null || issueTitle.isEmpty) { - issueTitle = draftData.attachments?.first.fileName ?? 'Unnamed'; + issueTitle = + draftData.attachments != null && draftData.attachments!.isNotEmpty + ? draftData.attachments!.first.fileName + : 'Unnamed'; } final draftIssue = Issue( @@ -152,6 +155,7 @@ class CustomerSupportServiceImpl extends CustomerSupportService { total: 1, unread: 0, lastMessage: null, + firstMessage: null, draft: draft, rating: draft.rating, announcementID: draftData.announcementId, diff --git a/lib/service/deeplink_service.dart b/lib/service/deeplink_service.dart index 5ac7015b25..815b933be5 100644 --- a/lib/service/deeplink_service.dart +++ b/lib/service/deeplink_service.dart @@ -16,15 +16,11 @@ import 'package:autonomy_flutter/main.dart'; import 'package:autonomy_flutter/model/otp.dart'; import 'package:autonomy_flutter/model/postcard_claim.dart'; import 'package:autonomy_flutter/screen/app_router.dart'; -import 'package:autonomy_flutter/screen/claim/activation/claim_activation_page.dart'; -import 'package:autonomy_flutter/screen/claim/airdrop/claim_airdrop_page.dart'; -import 'package:autonomy_flutter/screen/claim/claim_token_page.dart'; +import 'package:autonomy_flutter/screen/detail/preview/canvas_device_bloc.dart'; import 'package:autonomy_flutter/screen/irl_screen/webview_irl_screen.dart'; import 'package:autonomy_flutter/service/account_service.dart'; -import 'package:autonomy_flutter/service/activation_service.dart'; -import 'package:autonomy_flutter/service/airdrop_service.dart'; +import 'package:autonomy_flutter/service/canvas_client_service_v2.dart'; import 'package:autonomy_flutter/service/configuration_service.dart'; -import 'package:autonomy_flutter/service/feralfile_service.dart'; import 'package:autonomy_flutter/service/metric_client_service.dart'; import 'package:autonomy_flutter/service/navigation_service.dart'; import 'package:autonomy_flutter/service/postcard_service.dart'; @@ -32,15 +28,18 @@ import 'package:autonomy_flutter/service/remote_config_service.dart'; import 'package:autonomy_flutter/service/tezos_beacon_service.dart'; import 'package:autonomy_flutter/service/wc2_service.dart'; import 'package:autonomy_flutter/util/constants.dart'; +import 'package:autonomy_flutter/util/custom_route_observer.dart'; import 'package:autonomy_flutter/util/dio_exception_ext.dart'; import 'package:autonomy_flutter/util/log.dart'; import 'package:autonomy_flutter/util/string_ext.dart'; +import 'package:autonomy_flutter/util/ui_helper.dart'; +import 'package:autonomy_flutter/view/stream_device_view.dart'; import 'package:collection/collection.dart'; import 'package:dio/dio.dart'; +import 'package:feralfile_app_tv_proto/models/canvas_device.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_branch_sdk/flutter_branch_sdk.dart'; -import 'package:nft_collection/graphql/model/get_list_tokens.dart'; -import 'package:nft_collection/services/indexer_service.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:uni_links/uni_links.dart'; @@ -58,13 +57,9 @@ class DeeplinkServiceImpl extends DeeplinkService { final ConfigurationService _configurationService; final Wc2Service _walletConnect2Service; final TezosBeaconService _tezosBeaconService; - final FeralFileService _feralFileService; final NavigationService _navigationService; final BranchApi _branchApi; final PostcardService _postcardService; - final AirdropService _airdropService; - final ActivationService _activationService; - final IndexerService _indexerService; final RemoteConfigService _remoteConfigService; String? currentExhibitionId; @@ -76,13 +71,9 @@ class DeeplinkServiceImpl extends DeeplinkService { this._configurationService, this._walletConnect2Service, this._tezosBeaconService, - this._feralFileService, this._navigationService, this._branchApi, this._postcardService, - this._airdropService, - this._activationService, - this._indexerService, this._remoteConfigService, ); @@ -166,11 +157,9 @@ class DeeplinkServiceImpl extends DeeplinkService { switch (data) { case 'home': _navigationService.restorablePushHomePage(); - break; case 'support': unawaited( _navigationService.navigateTo(AppRouter.supportCustomerPage)); - break; default: return false; } @@ -358,7 +347,11 @@ class DeeplinkServiceImpl extends DeeplinkService { .firstWhereOrNull((prefix) => link.startsWith(prefix)); if (callingBranchDeepLinkPrefix != null) { final response = await _branchApi.getParams(Environment.branchKey, link); - await handleBranchDeeplinkData(response['data']); + try { + await handleBranchDeeplinkData(response['data']); + } catch (e) { + log.info('[DeeplinkService] _handleBranchDeeplink error $e'); + } return true; } return false; @@ -373,27 +366,6 @@ class DeeplinkServiceImpl extends DeeplinkService { } final source = data['source']; switch (source) { - case 'FeralFile_AirDrop': - final String? exhibitionId = data['exhibition_id']; - final String? seriesId = data['series_id']; - final String? expiredAt = data['expired_at']; - - if (expiredAt != null && - DateTime.now().isAfter(DateTime.fromMillisecondsSinceEpoch( - int.tryParse(expiredAt) ?? 0))) { - log.info('[DeeplinkService] FeralFile Airdrop expired'); - unawaited(_navigationService.showAirdropExpired(seriesId)); - break; - } - - if (exhibitionId?.isNotEmpty == true || seriesId?.isNotEmpty == true) { - unawaited(_claimFFAirdropToken( - exhibitionId: exhibitionId, - seriesId: seriesId, - otp: _getOtpFromBranchData(data), - )); - } - break; case 'Postcard': final String? type = data['type']; final String? id = data['id']; @@ -430,7 +402,7 @@ class DeeplinkServiceImpl extends DeeplinkService { log.info('[DeeplinkService] _handlePostcardDeeplink $sharedCode'); await _handlePostcardDeeplink(sharedCode); } - break; + case 'autonomy_irl': final url = data['irl_url']; if (url != null) { @@ -438,142 +410,48 @@ class DeeplinkServiceImpl extends DeeplinkService { await _handleIRL(url); memoryValues.irlLink.value = null; } - break; case 'moma_postcard_purchase': await _handlePayToMint(); - break; - case 'autonomy_airdrop': - final type = data['type']; - switch (type) { - case 'claim_pass': - final id = data['claim_pass_id']; - final seriesId = data['series_id']; - if (id != null) { - await _claimFFAirdropToken( - seriesId: seriesId, - otp: _getOtpFromBranchData(data), - claimFunction: ({required String receiveAddress}) async { - final response = await _airdropService.claimFeralFileToken( - receiveAddress: receiveAddress, - id: id, - seriesID: seriesId); - return response; - }, - ); - } - break; - default: - final String? sharedCode = data['share_code']; - if (sharedCode != null) { - log.info('[DeeplinkService] _handlePostcardDeeplink $sharedCode'); - final sharedInfor = await _airdropService.claimShare( - AirdropClaimShareRequest(shareCode: sharedCode), - ); - final series = - await _feralFileService.getSeries(sharedInfor.seriesID); - unawaited(_navigationService.navigateTo( - AppRouter.claimAirdropPage, - arguments: ClaimAirdropPagePayload( - claimID: '', - shareCode: sharedInfor.shareCode, - series: series, - allowViewOnlyClaim: true), - )); - } - } - - break; - - case 'Autonomy_Activation': - final String? activationID = data['activationID']; - final String? expiredAt = data['expired_at']; - - if (expiredAt != null && - DateTime.now().isAfter(DateTime.fromMillisecondsSinceEpoch( - int.tryParse(expiredAt) ?? 0))) { - log.info('[DeeplinkService] FeralFile Airdrop expired'); - // _navigationService.showAirdropExpired(seriesId); - break; - } - if (activationID?.isNotEmpty == true) { - unawaited(_handleActivationDeeplink( - activationID, _getOtpFromBranchData(data))); - } - break; case 'autonomy_connect': final wcUri = data['uri']; final decodedWcUri = Uri.decodeFull(wcUri); await _walletConnect2Service.connect(decodedWcUri); - break; + + case 'feralfile_display': + final payload = data['device'] as Map; + final device = CanvasDevice.fromJson(payload); + final canvasClient = injector(); + final result = await canvasClient.addQrDevice(device); + final isSuccessful = result != null; + if (!_navigationService.context.mounted) { + return; + } + if (isSuccessful) { + if (CustomRouteObserver.currentRoute?.settings.name == + AppRouter.scanQRPage) { + /// in case scan when open scanQRPage, + /// scan with navigation home page does not go to this flow + _navigationService.goBack(result: device); + } else { + await UIHelper.showFlexibleDialog( + _navigationService.context, + BlocProvider.value( + value: injector(), + child: const StreamDeviceView(), + ), + isDismissible: true, + autoDismissAfter: 3); + } + } + default: memoryValues.branchDeeplinkData.value = null; } _deepLinkHandlingMap.remove(data['~referring_link']); } - Future _claimFFAirdropToken({ - String? exhibitionId, - String? seriesId, - Otp? otp, - Future Function({required String receiveAddress})? - claimFunction, - }) async { - log.info('[DeeplinkService] Claim FF Airdrop token. ' - 'Exhibition $exhibitionId, otp: ${otp?.toJson()}'); - final id = '${exhibitionId}_${seriesId}_${otp?.code}'; - if (currentExhibitionId == id) { - return; - } - try { - currentExhibitionId = id; - final doneOnboarding = _configurationService.isDoneOnboarding(); - if (doneOnboarding) { - final seriesFuture = (seriesId?.isNotEmpty == true) - ? _feralFileService.getSeries(seriesId!) - : _feralFileService.getAirdropSeriesFromExhibitionId(exhibitionId!); - - await Future.delayed(const Duration(seconds: 1), () { - _navigationService.popUntilHomeOrSettings(); - }); - - final series = await seriesFuture; - final endTime = series.airdropInfo?.endedAt; - if (series.airdropInfo == null || - (endTime != null && endTime.isBefore(DateTime.now()))) { - await _navigationService.showAirdropExpired(seriesId); - } else if (series.airdropInfo?.isAirdropStarted != true) { - await _navigationService.showAirdropNotStarted(seriesId); - } else if (series.airdropInfo?.remainAmount == 0) { - await _navigationService.showNoRemainingToken( - series: series, - ); - } else if (otp?.isExpired == true) { - await _navigationService.showOtpExpired(seriesId); - } else { - Future.delayed(const Duration(seconds: 5), () { - currentExhibitionId = null; - }); - unawaited(_navigationService.openClaimTokenPage( - series, - otp: otp, - claimFunction: claimFunction, - )); - } - currentExhibitionId = null; - } else { - handlingDeepLink = null; - await Future.delayed(const Duration(seconds: 5), () { - currentExhibitionId = null; - }); - } - } catch (e) { - log.info('[DeeplinkService] _claimFFAirdropToken error $e'); - currentExhibitionId = null; - } - } - Future _deepLinkHandleClock(String message, String param, {Duration duration = const Duration(seconds: 2)}) async { handlingDeepLink = message; @@ -644,27 +522,6 @@ class DeeplinkServiceImpl extends DeeplinkService { } } } - - Future _handleActivationDeeplink(String? activationID, Otp? otp) async { - if (activationID == null) { - return; - } - final activationInfo = - await _activationService.getActivation(activationID: activationID); - final indexerId = _activationService.getIndexerID(activationInfo.blockchain, - activationInfo.contractAddress, activationInfo.tokenID); - final request = QueryListTokensRequest( - ids: [indexerId], - ); - final assetToken = await _indexerService.getNftTokens(request); - await _navigationService.openActivationPage( - payload: ClaimActivationPagePayload( - activationID: activationID, - assetToken: assetToken.first, - otp: otp!, - ), - ); - } } Otp? _getOtpFromBranchData(Map json) { diff --git a/lib/service/device_info_service.dart b/lib/service/device_info_service.dart new file mode 100644 index 0000000000..a9ff8473c1 --- /dev/null +++ b/lib/service/device_info_service.dart @@ -0,0 +1,29 @@ +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// Copyright © 2022 Bitmark. All rights reserved. +// Use of this source code is governed by the BSD-2-Clause Plus Patent License +// that can be found in the LICENSE file. +// + +import 'package:autonomy_flutter/util/device.dart'; +import 'package:autonomy_flutter/view/user_agent_utils.dart'; + +class DeviceInfoService { + late final String _deviceId; + late final String _deviceName; + bool _didInitialized = false; + + Future init() async { + if (_didInitialized) { + return; + } + final device = DeviceInfo.instance; + _deviceName = await device.getMachineName() ?? 'Feral File App'; + _deviceId = await getDeviceID(); + _didInitialized = true; + } + + String get deviceId => _deviceId; + + String get deviceName => _deviceName; +} diff --git a/lib/service/domain_service.dart b/lib/service/domain_service.dart index 11661da5c6..71c32d48dc 100644 --- a/lib/service/domain_service.dart +++ b/lib/service/domain_service.dart @@ -11,7 +11,9 @@ abstract class DomainService { class DomainServiceImpl implements DomainService { static const String _tnsDomain = 'https://api.tezos.domains/graphql'; static const String _ensDomain = - 'https://api.thegraph.com/subgraphs/name/ensdomains/ens'; + 'https://gateway-arbitrum.network.thegraph.com/api/' + '780901e32f46e70908727c94d3119788/subgraphs/id/' + '5XqPmWe6gjyrJtFn9cLy237i4cWw2j9HcUJEXsP5qGtH'; static const String _tnsQuery = ''' { domains(where: { name: { in: [""] } }) { items { address name} } } '''; diff --git a/lib/service/ethereum_service.dart b/lib/service/ethereum_service.dart index 5a09dd214a..e52e50cf1f 100644 --- a/lib/service/ethereum_service.dart +++ b/lib/service/ethereum_service.dart @@ -11,6 +11,7 @@ import 'package:autonomy_flutter/common/environment.dart'; import 'package:autonomy_flutter/gateway/etherchain_api.dart'; import 'package:autonomy_flutter/model/eth_pending_tx_amount.dart'; import 'package:autonomy_flutter/service/hive_service.dart'; +import 'package:autonomy_flutter/service/network_issue_manager.dart'; import 'package:autonomy_flutter/service/remote_config_service.dart'; import 'package:autonomy_flutter/util/constants.dart'; import 'package:autonomy_flutter/util/ether_amount_ext.dart'; @@ -27,7 +28,7 @@ const double gWeiFactor = 1000000000; abstract class EthereumService { Future getETHAddress(WalletStorage wallet, int index); - Future getBalance(String address); + Future getBalance(String address, {bool doRetry = false}); Future signPersonalMessage( WalletStorage wallet, int index, Uint8List message); @@ -71,8 +72,6 @@ abstract class EthereumService { Future getFeralFileTokenMetadata( EthereumAddress contract, Uint8List data); - - Future getFeeOptionValue(); } class EthereumServiceImpl extends EthereumService { @@ -80,9 +79,10 @@ class EthereumServiceImpl extends EthereumService { final EtherchainApi _etherchainApi; final HiveService _hiveService; final RemoteConfigService _remoteConfigService; + final NetworkIssueManager _networkIssueManager; EthereumServiceImpl(this._web3Client, this._etherchainApi, this._hiveService, - this._remoteConfigService); + this._remoteConfigService, this._networkIssueManager); @override Future estimateFee(WalletStorage wallet, int index, @@ -90,20 +90,22 @@ class EthereumServiceImpl extends EthereumService { {FeeOption feeOption = DEFAULT_FEE_OPTION}) async { log.info('[EthereumService] estimateFee - to: $to - amount $amount'); - final gasPrice = await getFeeOptionValue(); + final gasPrice = await _getFeeOptionValue(); final sender = EthereumAddress.fromHex(await wallet.getETHEip55Address(index: index)); - final fee = await getEthereumFee(feeOption); + final fee = await _getEthereumFee(feeOption); try { - BigInt gas = await _web3Client.estimateGas( - sender: sender, - to: to, - value: amount, - maxFeePerGas: fee.maxFeePerGas, - maxPriorityFeePerGas: fee.maxPriorityFeePerGas, - data: (data != null && data.isNotEmpty) ? hexToBytes(data) : null, - ); + BigInt gas = await _networkIssueManager + .retryOnConnectIssueTx(() => _web3Client.estimateGas( + sender: sender, + to: to, + value: amount, + maxFeePerGas: fee.maxFeePerGas, + maxPriorityFeePerGas: fee.maxPriorityFeePerGas, + data: + (data != null && data.isNotEmpty) ? hexToBytes(data) : null, + )); return gasPrice.multipleBy(gas); } catch (err) { if (data != null && data.isNotEmpty) { @@ -127,13 +129,15 @@ class EthereumServiceImpl extends EthereumService { } @override - Future getBalance(String address) async { + Future getBalance(String address, {bool doRetry = false}) async { if (address == '') { return EtherAmount.zero(); } final ethAddress = EthereumAddress.fromHex(address); - final amount = await _web3Client.getBalance(ethAddress); + final amount = await _networkIssueManager.retryOnConnectIssueTx( + () => _web3Client.getBalance(ethAddress), + maxRetries: doRetry ? NetworkIssueManager.maxRetries : 0); final tx = await _hiveService.getEthPendingTxAmounts(address); final List pendingTx = []; for (final element in tx) { @@ -168,7 +172,13 @@ class EthereumServiceImpl extends EthereumService { EthereumAddress to, BigInt value, String? data, {required FeeOption feeOption}) async { log.info('[EthereumService] sendTransaction - to: $to - amount $value'); + return await _networkIssueManager.retryOnConnectIssueTx(() => + _sendTransaction(wallet, index, to, value, data, feeOption: feeOption)); + } + Future _sendTransaction(WalletStorage wallet, int index, + EthereumAddress to, BigInt value, String? data, + {required FeeOption feeOption}) async { final sender = EthereumAddress.fromHex(await wallet.getETHEip55Address(index: index)); final nonce = await _web3Client.getTransactionCount(sender, @@ -177,7 +187,7 @@ class EthereumServiceImpl extends EthereumService { await _estimateGasLimit(sender, to, EtherAmount.inWei(value), data); final chainId = Environment.web3ChainId; Uint8List signedTransaction; - final fee = await getEthereumFee(feeOption); + final fee = await _getEthereumFee(feeOption); signedTransaction = await wallet.ethSignTransaction1559( nonce: nonce, @@ -189,7 +199,6 @@ class EthereumServiceImpl extends EthereumService { data: data ?? '', chainId: chainId, index: index); - final tx = await _web3Client.sendRawTransaction(signedTransaction); final deductValue = sender == to ? BigInt.zero : value; @@ -216,6 +225,16 @@ class EthereumServiceImpl extends EthereumService { @override Future getERC721TransferTransactionData( + EthereumAddress contractAddress, + EthereumAddress from, + EthereumAddress to, + String tokenId, + {FeeOption? feeOption}) => + _networkIssueManager.retryOnConnectIssueTx(() => + _getERC721TransferTransactionData(contractAddress, from, to, tokenId, + feeOption: feeOption)); + + Future _getERC721TransferTransactionData( EthereumAddress contractAddress, EthereumAddress from, EthereumAddress to, @@ -230,7 +249,7 @@ class EthereumServiceImpl extends EthereumService { atBlock: const BlockNum.pending()); Transaction transaction; if (feeOption != null) { - final fee = await getEthereumFee(feeOption); + final fee = await _getEthereumFee(feeOption); transaction = Transaction.callContract( contract: contract, function: transferFrom(), @@ -257,6 +276,18 @@ class EthereumServiceImpl extends EthereumService { @override Future getERC1155TransferTransactionData( + EthereumAddress contractAddress, + EthereumAddress from, + EthereumAddress to, + String tokenId, + int quantity, + {FeeOption? feeOption}) => + _networkIssueManager.retryOnConnectIssueTx(() => + _getERC1155TransferTransactionData( + contractAddress, from, to, tokenId, quantity, + feeOption: feeOption)); + + Future _getERC1155TransferTransactionData( EthereumAddress contractAddress, EthereumAddress from, EthereumAddress to, @@ -274,7 +305,7 @@ class EthereumServiceImpl extends EthereumService { Transaction transaction; if (feeOption != null) { - final fee = await getEthereumFee(feeOption); + final fee = await _getEthereumFee(feeOption); transaction = Transaction.callContract( contract: contract, function: transferFrom(), @@ -313,23 +344,38 @@ class EthereumServiceImpl extends EthereumService { @override Future getERC20TokenBalance( - EthereumAddress contractAddress, EthereumAddress owner) async { + EthereumAddress contractAddress, + EthereumAddress owner, { + bool doRetry = false, + }) async { final contractJson = await rootBundle.loadString('assets/erc20-abi.json'); final contract = DeployedContract( ContractAbi.fromJson(contractJson, 'ERC20'), contractAddress); ContractFunction balanceFunction() => contract.function('balanceOf'); - var response = await _web3Client.call( - contract: contract, - function: balanceFunction(), - params: [owner], - ); + var response = await _networkIssueManager.retryOnConnectIssueTx( + () => _web3Client.call( + contract: contract, + function: balanceFunction(), + params: [owner], + ), + maxRetries: doRetry ? NetworkIssueManager.maxRetries : 0); return response.first as BigInt; } @override Future getERC20TransferTransactionData( + EthereumAddress contractAddress, + EthereumAddress from, + EthereumAddress to, + BigInt quantity, + {FeeOption? feeOption}) => + _networkIssueManager.retryOnConnectIssueTx(() => + _getERC20TransferTransactionData(contractAddress, from, to, quantity, + feeOption: feeOption)); + + Future _getERC20TransferTransactionData( EthereumAddress contractAddress, EthereumAddress from, EthereumAddress to, @@ -345,7 +391,7 @@ class EthereumServiceImpl extends EthereumService { Transaction transaction; if (feeOption != null) { - final fee = await getEthereumFee(feeOption); + final fee = await _getEthereumFee(feeOption); transaction = Transaction.callContract( contract: contract, function: transferFrom(), @@ -474,7 +520,7 @@ class EthereumServiceImpl extends EthereumService { } } - Future getEthereumFee(FeeOption feeOption) async { + Future _getEthereumFee(FeeOption feeOption) async { final baseFee = await _getBaseFee(); final priorityFee = feeOption.getEthereumPriorityFee; final buffer = BigInt.from(baseFee / BigInt.from(8)); @@ -495,8 +541,7 @@ class EthereumServiceImpl extends EthereumService { } } - @override - Future getFeeOptionValue() async { + Future _getFeeOptionValue() async { final baseFee = await _getBaseFee(); final buffer = BigInt.from(baseFee / BigInt.from(8)); return FeeOptionValue( diff --git a/lib/service/feralfile_service.dart b/lib/service/feralfile_service.dart index 8e27bec45c..7c90e5b82a 100644 --- a/lib/service/feralfile_service.dart +++ b/lib/service/feralfile_service.dart @@ -7,29 +7,28 @@ import 'dart:convert'; import 'dart:io'; +import 'dart:math'; +import 'package:autonomy_flutter/common/environment.dart'; import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/gateway/feralfile_api.dart'; +import 'package:autonomy_flutter/gateway/source_exhibition_api.dart'; import 'package:autonomy_flutter/model/ff_account.dart'; +import 'package:autonomy_flutter/model/ff_artwork.dart'; import 'package:autonomy_flutter/model/ff_exhibition.dart'; +import 'package:autonomy_flutter/model/ff_list_response.dart'; import 'package:autonomy_flutter/model/ff_series.dart'; -import 'package:autonomy_flutter/model/otp.dart'; -import 'package:autonomy_flutter/screen/claim/claim_token_page.dart'; import 'package:autonomy_flutter/service/account_service.dart'; -import 'package:autonomy_flutter/util/asset_token_ext.dart'; -import 'package:autonomy_flutter/util/custom_exception.dart'; +import 'package:autonomy_flutter/util/constants.dart'; import 'package:autonomy_flutter/util/download_helper.dart'; import 'package:autonomy_flutter/util/exhibition_ext.dart'; -import 'package:autonomy_flutter/util/feralfile_extension.dart'; import 'package:autonomy_flutter/util/log.dart'; +import 'package:autonomy_flutter/util/series_ext.dart'; import 'package:autonomy_flutter/util/wallet_storage_ext.dart'; -import 'package:collection/collection.dart'; import 'package:crypto/crypto.dart'; import 'package:flutter/foundation.dart'; -import 'package:nft_collection/graphql/model/get_list_tokens.dart'; +import 'package:http/http.dart' as http; import 'package:nft_collection/models/asset_token.dart'; -import 'package:nft_collection/services/indexer_service.dart'; -import 'package:nft_collection/services/tokens_service.dart'; enum ArtworkModel { multi, @@ -128,23 +127,14 @@ enum GenerativeMediumTypes { } abstract class FeralFileService { - Future getAirdropSeriesFromExhibitionId(String id); + static const int offset = 0; + static const int limit = 300; - Future getSeries(String id); + Future getSeries(String id, + {String? exhibitionID, bool includeFirstArtwork = false}); - Future> getListSeries(String exhibitionId); - - Future setPendingToken( - {required String receiver, - required TokenClaimResponse response, - required FFSeries series}); - - Future claimToken({ - required String seriesId, - String? address, - Otp? otp, - Future Function(FFSeries)? onConfirm, - }); + Future> getListSeries(String exhibitionId, + {bool includeFirstArtwork = false}); Future getExhibitionFromTokenID(String artworkID); @@ -152,24 +142,29 @@ abstract class FeralFileService { Future getPartnerFullName(String exhibitionId); - Future getExhibition(String id); + Future getExhibition(String id, + {bool includeFirstArtwork = false}); - Future> getAllExhibitions({ + Future> getAllExhibitions({ String sortBy = 'openAt', String sortOrder = 'DESC', int limit = 8, int offset = 0, - bool withArtworks = false, - bool withSeries = false, }); + Future getSourceExhibition(); + + Future getUpcomingExhibition(); + Future getFeaturedExhibition(); - Future> getExhibitionArtworks(String exhibitionId, - {bool withSeries = false}); + Future> getFeaturedArtworks(); + + Future> getSeriesArtworks( + String seriesId, String exhibitionID, + {bool withSeries = false, int offset = offset, int limit = limit}); - Future> getSeriesArtworks(String seriesId, - {bool withSeries = false}); + Future getFirstViewableArtwork(String seriesId); Future getFeralfileActionMessage( {required String address, required FeralfileAction action}); @@ -188,133 +183,75 @@ abstract class FeralFileService { class FeralFileServiceImpl extends FeralFileService { final FeralFileApi _feralFileApi; - final AccountService _accountService; + final SourceExhibitionAPI _sourceExhibitionAPI; + Exhibition? sourceExhibition; + List beforeMintingArtworkInfos = []; FeralFileServiceImpl( this._feralFileApi, - this._accountService, + this._sourceExhibitionAPI, ); @override - Future getAirdropSeriesFromExhibitionId(String id) async { - final resp = await _feralFileApi.getExhibition(id); - final exhibition = resp.result; - final airdropSeriesId = exhibition.series - ?.firstWhereOrNull((e) => e.settings?.isAirdrop == true) - ?.id; - if (airdropSeriesId != null) { - final airdropSeries = await _feralFileApi.getSeries( - seriesId: airdropSeriesId, + Future getSeries(String id, + {String? exhibitionID, bool includeFirstArtwork = false}) async { + if (exhibitionID == SOURCE_EXHIBITION_ID) { + return await _getSourceSeries( + id, + includeFirstArtwork: includeFirstArtwork, ); - return airdropSeries.result; - } else { - throw Exception('Not airdrop exhibition'); } - } - - @override - Future getSeries(String id) async => - (await _feralFileApi.getSeries(seriesId: id)).result; - - @override - Future setPendingToken( - {required String receiver, - required TokenClaimResponse response, - required FFSeries series}) async { - final indexer = injector(); - await indexer.reindexAddresses([receiver]); - - final indexerId = - series.airdropInfo!.getTokenIndexerId(response.result.artworkID); - List assetTokens = await _fetchTokens( - indexerId: indexerId, - receiver: receiver, - ); - if (assetTokens.isNotEmpty) { - await indexer.setCustomTokens(assetTokens); - } else { - assetTokens = [ - createPendingAssetToken( - series: series, - owner: receiver, - tokenId: response.result.artworkID, - ) - ]; - await indexer.setCustomTokens(assetTokens); + final series = (await _feralFileApi.getSeries( + seriesId: id, includeFirstArtwork: includeFirstArtwork)) + .result; + + if (includeFirstArtwork && series.artwork == null) { + final exhibition = await getExhibition(series.exhibitionID); + List artworks = []; + if (!exhibition.isMinted) { + final fakeartworks = + await _getFakeSeriesArtworks(exhibition, series, 0, 1); + artworks = fakeartworks; + } + if (artworks.isNotEmpty) { + return series.copyWith(artwork: artworks.first); + } } - return ClaimResponse( - token: assetTokens.first, airdropInfo: series.airdropInfo!); + + return series; } @override - Future claimToken( - {required String seriesId, - String? address, - Otp? otp, - Future Function(FFSeries)? onConfirm}) async { - log.info('[FeralFileService] Claim token - series: $seriesId'); - final series = await getSeries(seriesId); - - if (series.airdropInfo == null || - series.airdropInfo?.endedAt?.isBefore(DateTime.now()) == true) { - throw AirdropExpired(); + Future getExhibition(String id, + {bool includeFirstArtwork = false}) async { + if (id == SOURCE_EXHIBITION_ID) { + return getSourceExhibition(); } - - if ((series.airdropInfo?.remainAmount ?? 0) > 0) { - final accepted = await onConfirm?.call(series) ?? true; - if (!accepted) { - log.info('[FeralFileService] User refused claim token'); - return null; + final resp = await _feralFileApi.getExhibition(id, + includeFirstArtwork: includeFirstArtwork); + final exhibition = resp.result!; + + if (includeFirstArtwork && + exhibition.series != null && + exhibition.series!.any((series) => series.artwork == null)) { + final List newSeries = []; + for (final FFSeries series in exhibition.series ?? []) { + if (!exhibition.isMinted) { + final seriesDetail = await getSeries(series.id); + final fakeArtwork = + await _getFakeSeriesArtworks(exhibition, seriesDetail, 0, 1); + if (fakeArtwork.isNotEmpty) { + newSeries.add(series.copyWith(artwork: fakeArtwork.first)); + } + } else { + newSeries.add(series); + } } - final wallet = await _accountService.getDefaultAccount(); - final message = - (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString(); - final accountDID = await wallet.getAccountDID(); - final signature = await wallet.getAccountDIDSignature(message); - final receiver = address ?? await wallet.getTezosAddress(); - Map body = { - 'claimer': accountDID, - 'timestamp': message, - 'signature': signature, - 'address': receiver, - if (otp != null) ...{'airdropTOTPPasscode': otp.code} - }; - final response = await _feralFileApi.claimSeries(series.id, body); - final claimResponse = setPendingToken( - receiver: receiver, response: response, series: series); - return claimResponse; - } else { - throw NoRemainingToken(); - } - } - - @override - Future getExhibition(String id) async { - final resp = await _feralFileApi.getExhibition(id); - return resp.result; - } - Future> _fetchTokens({ - required String indexerId, - required String receiver, - }) async { - try { - final indexerService = injector(); - final List assets = await indexerService - .getNftTokens(QueryListTokensRequest(ids: [indexerId])); - final tokens = assets - .map((e) => e - ..pending = true - ..owner = receiver - ..balance = 1 - ..owners.putIfAbsent(receiver, () => 1) - ..lastActivityTime = DateTime.now()) - .toList(); - return tokens; - } catch (e) { - log.info('[FeralFileService] Fetch token failed ($indexerId) $e'); - return []; + return exhibition.copyWith(series: newSeries); } + + return exhibition; } @override @@ -326,7 +263,7 @@ class FeralFileServiceImpl extends FeralFileService { @override Future getPartnerFullName(String exhibitionId) async { final exhibition = await _feralFileApi.getExhibition(exhibitionId); - return exhibition.result.partner?.fullName; + return exhibition.result!.partner?.fullName; } @override @@ -336,13 +273,11 @@ class FeralFileServiceImpl extends FeralFileService { } @override - Future> getAllExhibitions({ + Future> getAllExhibitions({ String sortBy = 'openAt', String sortOrder = 'DESC', int limit = 8, int offset = 0, - bool withArtworks = false, - bool withSeries = false, }) async { final exhibitions = await _feralFileApi.getAllExhibitions( sortBy: sortBy, sortOrder: sortOrder, limit: limit, offset: offset); @@ -351,74 +286,82 @@ class FeralFileServiceImpl extends FeralFileService { ..info('[FeralFileService] Get all exhibitions: ${listExhibition.length}') ..info('[FeralFileService] Get all exhibitions: ' '${listExhibition.map((e) => e.id).toList()}'); - final listExhibitionDetail = - listExhibition.map((e) => ExhibitionDetail(exhibition: e)).toList(); - if (withArtworks) { - try { - await Future.wait(listExhibitionDetail.map((e) async { - final artworks = - await getExhibitionArtworks(e.exhibition.id, withSeries: true); - e.artworks = artworks; - })); - } catch (e) { - log.info('[FeralFileService] Get artworks failed $e'); - } - } - if (withSeries) { - try { - await Future.wait(listExhibitionDetail.mapIndexed((index, e) async { - final series = await getListSeries(e.exhibition.id); - listExhibitionDetail[index] = - e.copyWith(exhibition: e.exhibition.copyWith(series: series)); - })); - } catch (e) { - log.info('[FeralFileService] Get series failed $e'); - } - } - return listExhibitionDetail; + return listExhibition; + } + + @override + Future getUpcomingExhibition() async { + final exhibitionResponse = await _feralFileApi.getUpcomingExhibition(); + return exhibitionResponse.result; } @override Future getFeaturedExhibition() async { - final featuredExhibition = await _feralFileApi.getFeaturedExhibition(); - return featuredExhibition.result; + final exhibitionResponse = await _feralFileApi.getFeaturedExhibition(); + return exhibitionResponse.result!; } - Future> _getExhibitionArtworkByDirectApi(String exhibitionId, - {bool withSeries = false}) async { - final artworks = - await _feralFileApi.getListArtworks(exhibitionId: exhibitionId); - List listArtwork = artworks.result; - log - ..info( - '[FeralFileService] [_getExhibitionByDirectApi] ' - 'Get exhibition artworks: ${listArtwork.length}', - ) - ..info( - '[FeralFileService] [_getExhibitionByDirectApi] ' - 'Get exhibition artworks: ' - '${listArtwork.map((e) => e.id).toList()}', + @override + Future> getFeaturedArtworks() async { + final response = await _feralFileApi.getFeaturedArtworks(); + return response.result; + } + + Future> _getFakeSeriesArtworks( + Exhibition exhibition, FFSeries series, int offset, int limit) async { + if (!series.shouldFakeArtwork) { + return []; + } + if (exhibition.isJohnGerrardShow) { + return await _getJohnGerrardFakeArtworks( + series: series, + offset: offset, + limit: limit, + onlySignedArtwork: true, ); - if (withSeries) { - final listSeries = await getListSeries(exhibitionId); - final seriesMap = - Map.fromEntries(listSeries.map((e) => MapEntry(e.id, e))); - listArtwork = listArtwork - .map((e) => e.copyWith(series: seriesMap[e.seriesID])) - .toList(); } - return listArtwork; + final fakeArtworks = + _createFakeSeriesArtworks(series, exhibition, offset, limit); + return fakeArtworks; } - Future> _getExhibitionFakeArtworks(String exhibitionId) async { - List listArtworks = []; - final exhibition = await getExhibition(exhibitionId); - final series = await getListSeries(exhibitionId); - await Future.wait(series.map((e) => _fakeSeriesArtworks(e, exhibition))) - .then((value) { - listArtworks.addAll(value.expand((element) => element)); - }); - return listArtworks; + Future> _createFakeSeriesArtworks( + FFSeries series, Exhibition exhibition, int offset, int limit) async { + final List artworks = []; + final maxArtworks = limit; + for (var i = offset; i < maxArtworks; i++) { + final previewURI = await _getPreviewURI(series, i, exhibition); + final artworkId = getFeralfileTokenId( + seriesOnchainID: series.onchainID ?? '', + exhibitionID: series.exhibitionID, + artworkIndex: i, + ); + final thumbnailURI = _getThumbnailURI(series, i); + final fakeArtwork = Artwork( + artworkId, + series.id, + i, + '#${i + 1}', + 'Artwork category $i', + 'ownerAccountID', + null, + null, + 'blockchainStatus', + false, + thumbnailURI, + previewURI ?? '', + {}, + DateTime.now(), + DateTime.now(), + DateTime.now(), + null, + series, + null, + null, + ); + artworks.add(fakeArtwork); + } + return artworks; } String getFeralfileTokenId( @@ -428,7 +371,8 @@ class FeralFileServiceImpl extends FeralFileService { final BigInt si = BigInt.parse(seriesOnchainID); final BigInt msi = si * BigInt.from(1000000) + BigInt.from(artworkIndex); final String part1 = exhibitionID.replaceAll('-', ''); - final String part2 = msi.toRadixString(16); + // padding with 0 to 32 characters + final String part2 = msi.toRadixString(16).padLeft(32, '0'); final String p = part1 + part2; final BigInt tokenIDBigInt = BigInt.parse('0x$p'); final String tokenID = tokenIDBigInt.toString(); @@ -451,30 +395,15 @@ class FeralFileServiceImpl extends FeralFileService { Future _getPreviewURI( FFSeries series, int artworkIndex, Exhibition exhibition) async { String? previewURI; - if (series.settings?.artworkModel == ArtworkModel.multiUnique && - series.previewFile == null) { - previewURI = '${series.uniquePreviewPath}/$artworkIndex'; - } - if (previewURI == null) { - if (!GenerativeMediumTypes.values - .any((element) => element.value == series.medium) && - series.uniquePreviewPath != null) { - previewURI = '${series.uniquePreviewPath}/$artworkIndex'; - } - } - - if (previewURI != null) { - return previewURI; + if (!series.isMultiUnique) { + previewURI = getFFUrl(series.previewFile?.uri ?? ''); } else { - previewURI ??= getFFUrl(series.previewFile?.uri ?? ''); - final artworkNumber = artworkIndex + 1; - previewURI = '$previewURI?edition_number=$artworkIndex' - '&artwork_number=$artworkNumber' - '&blockchain=${exhibition.mintBlockchain}'; - //TODO: check if (contract) {...} - - if (GenerativeMediumTypes.values - .any((element) => element.value == series.medium)) { + if (series.isGenerative) { + previewURI ??= getFFUrl(series.previewFile?.uri ?? ''); + final artworkNumber = artworkIndex + 1; + previewURI = '$previewURI?' + '&artwork_number=$artworkNumber' + '&blockchain=${exhibition.mintBlockchain}'; try { final tokenParameters = await previewArtCustomTokenID( seriesOnchainID: series.onchainID ?? '', @@ -486,6 +415,11 @@ class FeralFileServiceImpl extends FeralFileService { log.info( '[FeralFileService] Get preview URI failed: $error, $stackTrace'); } + } else { + previewURI = '${series.uniquePreviewPath}/$artworkIndex'; + if (exhibition.isCrawlShow) { + previewURI += '/'; + } } } return previewURI; @@ -496,79 +430,71 @@ class FeralFileServiceImpl extends FeralFileService { ? '${series.uniqueThumbnailPath}/$artworkIndex-large.jpg' : series.thumbnailURI ?? ''; - Future> _fakeSeriesArtworks( - FFSeries series, Exhibition exhibition) async { - final List artworks = []; - final maxArtworks = series.maxEdition; - for (var i = 0; i < maxArtworks; i++) { - final previewURI = await _getPreviewURI(series, i, exhibition); - final artworkId = getFeralfileTokenId( - seriesOnchainID: series.onchainID ?? '', - exhibitionID: series.exhibitionID, - artworkIndex: i, - ); - final thumbnailURI = _getThumbnailURI(series, i); - final fakeArtwork = Artwork( - artworkId, - series.id, - i, - '#${i + 1}', - 'Artwork category $i', - 'ownerAccountID', - null, - null, - 'blockchainStatus', - false, - thumbnailURI, - previewURI ?? '', - {}, - DateTime.now(), - DateTime.now(), - DateTime.now(), - null, - series, - null, - ); - artworks.add(fakeArtwork); - } - return artworks; + Future> _fakeSeriesArtworks( + String seriesId, String exhibitionId, + {required int offset, required int limit}) async { + final exhibition = await getExhibition(exhibitionId); + final series = await getSeries(seriesId); + final List seriesArtworks = + await _getFakeSeriesArtworks(exhibition, series, offset, limit); + final total = series.latestRevealedArtworkIndex == null + ? series.maxEdition + : series.latestRevealedArtworkIndex! + 1; + return FeralFileListResponse( + result: seriesArtworks, + paging: Paging(offset: offset, limit: limit, total: total)); } @override - Future> getExhibitionArtworks(String exhibitionId, - {bool withSeries = false}) async { - List listArtworks = []; - listArtworks = await _getExhibitionArtworkByDirectApi(exhibitionId, - withSeries: withSeries); - if (listArtworks.isNotEmpty) { - return listArtworks; - } else { - listArtworks = await _getExhibitionFakeArtworks(exhibitionId); + Future> getSeriesArtworks( + String seriesId, String exhibitionID, + {bool withSeries = false, + int offset = FeralFileService.offset, + int limit = FeralFileService.limit}) async { + if (exhibitionID == SOURCE_EXHIBITION_ID) { + final artworks = await _getSourceSeriesArtworks(seriesId); + return FeralFileListResponse( + result: + artworks.sublist(offset, min(artworks.length, offset + limit)), + paging: Paging(offset: 0, limit: limit, total: artworks.length)); } - return listArtworks; - } + final exhibition = await getExhibition(exhibitionID); - @override - Future> getSeriesArtworks(String seriesId, - {bool withSeries = false}) async { - final artworks = await _feralFileApi.getListArtworks(seriesId: seriesId); - List listArtwork = artworks.result; - if (listArtwork.isEmpty) { - final series = await getSeries(seriesId); - listArtwork = await _fakeSeriesArtworks(series, series.exhibition!); - } else if (withSeries) { + if (!exhibition.isMinted) { + return await _fakeSeriesArtworks(seriesId, exhibitionID, + offset: offset, limit: limit); + } + FeralFileListResponse artworksResponse = await _feralFileApi + .getListArtworks(seriesId: seriesId, offset: offset, limit: limit); + + if (withSeries) { final series = await getSeries(seriesId); - listArtwork = listArtwork.map((e) => e.copyWith(series: series)).toList(); + artworksResponse.copyWith( + result: artworksResponse.result + .map((e) => e.copyWith(series: series)) + .toList()); } log ..info( - '[FeralFileService] Get series artworks: ${listArtwork.length}', + '[FeralFileService] Get series artworks:' + ' ${artworksResponse.result.length}, offset $offset, limit $limit', ) ..info( '[FeralFileService] Get series artworks: ' - '${listArtwork.map((e) => e.id).toList()}', + '${artworksResponse.result.map((e) => e.id).toList()}', ); - return listArtwork; + return artworksResponse; + } + + @override + Future getFirstViewableArtwork(String seriesId) async { + final response = await _feralFileApi.getListArtworks( + seriesId: seriesId, + includeActiveSwap: false, + sortOrder: 'DESC', + isViewable: true, + ); + return response.result.firstOrNull; } @override @@ -636,11 +562,145 @@ class FeralFileServiceImpl extends FeralFileService { } @override - Future> getListSeries(String exhibitionId) async { + Future> getListSeries(String exhibitionId, + {bool includeFirstArtwork = false}) async { + if (exhibitionId == SOURCE_EXHIBITION_ID) { + final exhibition = await getSourceExhibition(); + return exhibition.series ?? []; + } final response = await _feralFileApi.getListSeries( exhibitionID: exhibitionId, sortBy: 'displayIndex', sortOrder: 'ASC'); return response.result; } + + // Source Exhibition + @override + Future getSourceExhibition() async { + if (sourceExhibition != null) { + return sourceExhibition!; + } + + final exhibition = await _sourceExhibitionAPI.getSourceExhibitionInfo(); + final series = await _sourceExhibitionAPI.getSourceExhibitionSeries(); + sourceExhibition = exhibition.copyWith(series: series); + return sourceExhibition!; + } + + Future _getSourceSeries(String seriesID, + {bool includeFirstArtwork = false}) async { + late List listSeries; + if (sourceExhibition != null && sourceExhibition!.series != null) { + listSeries = sourceExhibition!.series!; + } else { + listSeries = await _sourceExhibitionAPI.getSourceExhibitionSeries(); + } + + final series = listSeries.firstWhere((series) => series.id == seriesID); + if (includeFirstArtwork) { + final firstArtwork = series.artworks!.first; + return series.copyWith(artwork: firstArtwork); + } + + return series; + } + + Future> _getSourceSeriesArtworks(String seriesID) async { + final series = await _getSourceSeries(seriesID); + return series.artworks!; + } + + // John Gerrard exhibition + Future> _getBeforeMintingArtworkInfos( + FFSeries series) async { + if (beforeMintingArtworkInfos.isNotEmpty) { + return beforeMintingArtworkInfos; + } + + try { + final artworkInfoLink = + '${Environment.feralFileAssetURL}/previews/${series.id}/${series.previewFile?.version ?? ''}/info.json'; + final response = await http.get(Uri.parse(artworkInfoLink)); + if (response.statusCode == 200) { + final body = json.decode(response.body) as Map; + beforeMintingArtworkInfos = body.entries.map((entry) { + final map = entry.value as Map; + return BeforeMintingArtworkInfo.fromJson(map); + }).toList(); + return beforeMintingArtworkInfos; + } else { + throw Exception('Failed to load SOURCE series'); + } + } catch (e) { + throw Exception('Failed to load SOURCE series'); + } + } + + Future> _getJohnGerrardFakeArtworks({ + required FFSeries series, + required int offset, + bool onlySignedArtwork = false, + int limit = 50, + }) async { + int maxArtwork; + if (onlySignedArtwork) { + maxArtwork = series.latestRevealedArtworkIndex == null + ? 0 + : (series.latestRevealedArtworkIndex! + 1); + } else { + maxArtwork = series.settings?.maxArtwork ?? 0; + } + + if (maxArtwork == 0) { + return []; + } + + final endIndex = min(offset + limit, maxArtwork); + if (offset >= endIndex) { + return []; + } + + List fakeArtworks = []; + for (var index = offset; index < endIndex; index++) { + final fakeArtwork = await _getJohnGerrardArtworkByIndex(index, series); + fakeArtworks.add(fakeArtwork); + } + + return fakeArtworks; + } + + Future _getJohnGerrardArtworkByIndex( + int index, FFSeries series) async { + final beforeMintingArtworkInfos = + await _getBeforeMintingArtworkInfos(series); + final artworkId = getFeralfileTokenId( + seriesOnchainID: series.onchainID ?? '', + exhibitionID: series.exhibitionID, + artworkIndex: index, + ); + return Artwork( + artworkId, + series.id, + index, + beforeMintingArtworkInfos[index].artworkTitle, + '', + null, + null, + null, + '', + false, + 'previews/${series.id}/${series.previewFile?.version}/generated_images/crystal_${index + MAGIC_NUMBER}_img.jpg', + 'previews/${series.id}/${series.previewFile?.version}/nft.html?hourIdx=${index + MAGIC_NUMBER}', + { + 'viewableAt': beforeMintingArtworkInfos[index].viewableAt, + }, + DateTime.now(), + DateTime.now(), + DateTime.now(), + null, + series, + null, + null); + } } enum FeralfileAction { diff --git a/lib/service/hive_store_service.dart b/lib/service/hive_store_service.dart new file mode 100644 index 0000000000..1612bae712 --- /dev/null +++ b/lib/service/hive_store_service.dart @@ -0,0 +1,50 @@ +import 'dart:async'; + +import 'package:autonomy_flutter/util/log.dart'; +import 'package:hive_flutter/hive_flutter.dart'; + +abstract class HiveStoreObjectService { + Future init(String key); + + Future save(T obj, String objId); + + Future delete(String objId); + + T? get(String objId); + + List getAll(); +} + +class HiveStoreObjectServiceImpl implements HiveStoreObjectService { + late Box _box; + + @override + Future init(String key) async { + _box = await Hive.openBox(key); + } + + @override + Future delete(String objId) => _box.delete(objId); + + @override + T? get(String objId) { + try { + return _box.get(objId); + } catch (e) { + log.info('Hive error getting object from Hive: $e'); + return null; + } + } + + @override + List getAll() => _box.values.toList(); + + @override + Future save(T obj, String objId) async { + try { + await _box.put(objId, obj); + } catch (e) { + log.info('Hive error saving object to Hive: $e'); + } + } +} diff --git a/lib/service/navigation_service.dart b/lib/service/navigation_service.dart index c99005c189..b0bc4926cb 100644 --- a/lib/service/navigation_service.dart +++ b/lib/service/navigation_service.dart @@ -7,11 +7,10 @@ import 'dart:async'; -import 'package:autonomy_flutter/model/ff_series.dart'; -import 'package:autonomy_flutter/model/otp.dart'; +import 'package:autonomy_flutter/common/injector.dart'; +import 'package:autonomy_flutter/model/ff_exhibition.dart'; import 'package:autonomy_flutter/screen/app_router.dart'; -import 'package:autonomy_flutter/screen/claim/activation/claim_activation_page.dart'; -import 'package:autonomy_flutter/screen/claim/claim_token_page.dart'; +import 'package:autonomy_flutter/screen/detail/artwork_detail_page.dart'; import 'package:autonomy_flutter/screen/interactive_postcard/design_stamp.dart'; import 'package:autonomy_flutter/screen/irl_screen/webview_irl_screen.dart'; import 'package:autonomy_flutter/screen/send_receive_postcard/receive_postcard_page.dart'; @@ -27,6 +26,7 @@ import 'package:autonomy_flutter/util/ui_helper.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:feralfile_app_theme/feral_file_app_theme.dart'; import 'package:flutter/material.dart'; +import 'package:nft_collection/database/nft_collection_database.dart'; import 'package:nft_collection/models/asset_token.dart'; // ignore_for_file: implementation_imports import 'package:overlay_support/src/overlay_state_finder.dart'; @@ -119,90 +119,6 @@ class NavigationService { NavigatorState navigatorState() => Navigator.of(navigatorKey.currentContext!); - Future showAirdropNotStarted(String? artworkId) async { - log.info('NavigationService.showAirdropNotStarted'); - if (navigatorKey.currentState?.mounted == true && - navigatorKey.currentContext != null) { - await UIHelper.showAirdropNotStarted( - navigatorKey.currentContext!, artworkId); - } else { - await Future.value(0); - } - } - - Future showAirdropExpired(String? artworkId) async { - log.info('NavigationService.showAirdropExpired'); - if (navigatorKey.currentState?.mounted == true && - navigatorKey.currentContext != null) { - await UIHelper.showAirdropExpired( - navigatorKey.currentContext!, artworkId); - } else { - await Future.value(0); - } - } - - Future showNoRemainingToken({ - required FFSeries series, - }) async { - log.info('NavigationService.showNoRemainingToken'); - if (navigatorKey.currentState?.mounted == true && - navigatorKey.currentContext != null) { - await UIHelper.showNoRemainingAirdropToken( - navigatorKey.currentContext!, - series: series, - ); - } else { - await Future.value(0); - } - } - - Future showOtpExpired(String? artworkId) async { - log.info('NavigationService.showOtpExpired'); - if (navigatorKey.currentState?.mounted == true && - navigatorKey.currentContext != null) { - await UIHelper.showOtpExpired(navigatorKey.currentContext!, artworkId); - } else { - await Future.value(0); - } - } - - Future openClaimTokenPage( - FFSeries series, { - Otp? otp, - Future Function({required String receiveAddress})? - claimFunction, - }) async { - log.info('NavigationService.openClaimTokenPage'); - if (navigatorKey.currentState?.mounted == true && - navigatorKey.currentContext != null) { - await navigatorKey.currentState?.pushNamed( - AppRouter.claimFeralfileTokenPage, - arguments: ClaimTokenPagePayload( - series: series, - otp: otp, - allowViewOnlyClaim: true, - claimFunction: claimFunction, - ), - ); - } else { - await Future.value(0); - } - } - - Future openActivationPage( - {required ClaimActivationPagePayload payload}) async { - log.info('NavigationService.openActivationPage'); - if (navigatorKey.currentState?.mounted == true && - navigatorKey.currentContext != null) { - await navigatorKey.currentState?.pushNamed( - AppRouter.claimActivationPage, - arguments: payload, - ); - } else { - await Future.value(0); - } - } - void showErrorDialog( ErrorEvent event, { Function()? defaultAction, @@ -334,6 +250,28 @@ class NavigationService { } } + Future gotoExhibitionDetailsPage(String exhibitionID) async { + popUntilHome(); + await Future.delayed(const Duration(seconds: 1), () async { + await (homePageKey.currentState ?? homePageNoTransactionKey.currentState) + ?.openExhibition(exhibitionID); + }); + } + + Future gotoArtworkDetailsPage(String indexID) async { + popUntilHome(); + final tokens = await injector() + .assetTokenDao + .findAllAssetTokensByTokenIDs([indexID]); + final owner = tokens.first.owner; + final artworkDetailPayload = + ArtworkDetailPayload([ArtworkIdentity(indexID, owner)], 0); + if (context.mounted) { + unawaited(Navigator.of(context).pushNamed(AppRouter.artworkDetailsPage, + arguments: artworkDetailPayload)); + } + } + Future goToIRLWebview(IRLWebScreenPayload payload) async { if (navigatorKey.currentState?.mounted == true && navigatorKey.currentContext != null) { @@ -350,34 +288,6 @@ class NavigationService { } } - Future showAirdropJustOnce() async { - if (navigatorKey.currentContext != null && - navigatorKey.currentState?.mounted == true) { - await UIHelper.showAirdropJustOnce(navigatorKey.currentContext!); - } - } - - Future showAirdropAlreadyClaimed() async { - if (navigatorKey.currentContext != null && - navigatorKey.currentState?.mounted == true) { - await UIHelper.showAirdropAlreadyClaim(navigatorKey.currentContext!); - } - } - - Future showActivationError(Object e, String id) async { - if (navigatorKey.currentContext != null && - navigatorKey.currentState?.mounted == true) { - await UIHelper.showActivationError(navigatorKey.currentContext!, e, id); - } - } - - Future showAirdropClaimFailed() async { - if (navigatorKey.currentContext != null && - navigatorKey.currentState?.mounted == true) { - await UIHelper.showAirdropClaimFailed(navigatorKey.currentContext!); - } - } - Future showPostcardShareLinkExpired() async { if (navigatorKey.currentContext != null && navigatorKey.currentState?.mounted == true) { @@ -459,24 +369,21 @@ class NavigationService { await _browser.openUrl(url); } - Future showFeralFileClaimTokenPassLimit( - {required FFSeries series}) async { - if (navigatorKey.currentContext != null && - navigatorKey.currentState?.mounted == true) { - await UIHelper.showFeralFileClaimTokenPassLimit( - context, - series: series, - ); + Future openFeralFileExhibitionNotePage(String exhibitionSlug) async { + if (exhibitionSlug.isEmpty) { + return; } + final url = FeralFileHelper.getExhibitionNoteUrl(exhibitionSlug); + final browser = FeralFileBrowser(); + await browser.openUrl(url); } - Future showClaimTokenError( - Object e, { - required FFSeries series, - }) async { - if (navigatorKey.currentContext != null && - navigatorKey.currentState?.mounted == true) { - await UIHelper.showClaimTokenError(context, e, series: series); + Future openFeralFilePostPage(Post post, String exhibitionID) async { + if (post.slug.isEmpty || exhibitionID.isEmpty) { + return; } + final url = FeralFileHelper.getPostUrl(post, exhibitionID); + final browser = FeralFileBrowser(); + await browser.openUrl(url); } } diff --git a/lib/service/network_issue_manager.dart b/lib/service/network_issue_manager.dart new file mode 100644 index 0000000000..4ccdf64d1d --- /dev/null +++ b/lib/service/network_issue_manager.dart @@ -0,0 +1,83 @@ +import 'dart:async'; + +import 'package:autonomy_flutter/common/injector.dart'; +import 'package:autonomy_flutter/service/navigation_service.dart'; +import 'package:autonomy_flutter/util/exception_ext.dart'; +import 'package:autonomy_flutter/util/ui_helper.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:synchronized/synchronized.dart'; + +class NetworkIssueManager { + static const Duration _throttleDuration = Duration(seconds: 30); + DateTime _lastErrorTime = DateTime.fromMillisecondsSinceEpoch(0); + bool _isShowingDialog = false; + final _txDialogLock = Lock(); + static const maxRetries = 10; + + Future showNetworkIssueWarning() async { + if (_isShowingDialog) { + return; + } + final context = injector().navigatorKey.currentContext; + if (context != null && + DateTime.now().difference(_lastErrorTime) > _throttleDuration) { + _lastErrorTime = DateTime.now(); + await UIHelper.showRetryDialog(context, + description: 'network_error_desc'.tr()); + } + } + + Future showRetryDialog(BuildContext context, + {required String description, + FutureOr Function()? onRetry, + ValueNotifier? dynamicRetryNotifier}) async { + _isShowingDialog = true; + final result = await UIHelper.showRetryDialog(context, + description: description, + onRetry: onRetry, + dynamicRetryNotifier: dynamicRetryNotifier); + _isShowingDialog = false; + return result; + } + + Future retryOnConnectIssueTx(FutureOr Function() fn, + {int maxRetries = maxRetries}) async { + if (maxRetries > 0) { + return await _txDialogLock + .synchronized(() => _retryOnConnectIssue(fn, maxRetries: maxRetries)); + } else { + return await fn(); + } + } + + Future _retryOnConnectIssue(FutureOr Function() fn, + {int maxRetries = maxRetries, String? description}) async { + try { + return await fn(); + } on Exception catch (e) { + if (e.isNetworkIssue && maxRetries > 0) { + final context = + injector().navigatorKey.currentContext; + if (context != null) { + final desc = description ?? 'network_error_desc'.tr(); + final dialogResult = await showRetryDialog( + context, + description: desc, + onRetry: () => true, + ); + if (dialogResult == true) { + return await _retryOnConnectIssue(fn, + maxRetries: maxRetries - 1, description: desc); + } else { + rethrow; + } + } else { + rethrow; + } + } else { + rethrow; + } + } + } +} diff --git a/lib/service/network_service.dart b/lib/service/network_service.dart new file mode 100644 index 0000000000..cf259ef107 --- /dev/null +++ b/lib/service/network_service.dart @@ -0,0 +1,58 @@ +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// Copyright © 2022 Bitmark. All rights reserved. +// Use of this source code is governed by the BSD-2-Clause Plus Patent License +// that can be found in the LICENSE file. +// + +import 'dart:async'; + +import 'package:autonomy_flutter/util/log.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:uuid/uuid.dart'; + +class NetworkService { + static const String _defaultListenerId = 'defaultListenerId'; + final Connectivity _connectivity = Connectivity(); + final Map> _listener = {}; + ConnectivityResult _connectivityResult = ConnectivityResult.none; + final ValueNotifier isWifiNotifier = ValueNotifier(false); + Timer? _timer; + + static const String canvasBlocListenerId = 'canvasBlocListenerId'; + static const String beaconListenerId = 'beaconListenerId'; + + NetworkService() { + addListener((result) { + log.info('[NetworkService] Network changed: $result'); + _connectivityResult = result; + + _timer?.cancel(); + _timer = Timer(const Duration(seconds: 1), () { + isWifiNotifier.value = result == ConnectivityResult.wifi; + }); + }, id: _defaultListenerId); + } + + String addListener(Function(ConnectivityResult result) fn, {String? id}) { + final listenerId = id ?? const Uuid().v4(); + log.info('[NetworkService] add listener $listenerId'); + final connectivitySubscription = + _connectivity.onConnectivityChanged.listen(fn); + _listener[listenerId] = connectivitySubscription; + return listenerId; + } + + Future removeListener(String id) async { + final connectivitySubscription = _listener[id]; + if (connectivitySubscription != null) { + log.info('[NetworkService] remove listener $id'); + await connectivitySubscription.cancel(); + _listener.remove(id); + } + } + + bool get isWifi => _connectivityResult == ConnectivityResult.wifi; +} diff --git a/lib/service/playlist_service.dart b/lib/service/playlist_service.dart index 7b92cbce9d..808d2d8869 100644 --- a/lib/service/playlist_service.dart +++ b/lib/service/playlist_service.dart @@ -1,6 +1,7 @@ import 'package:autonomy_flutter/model/play_list_model.dart'; import 'package:autonomy_flutter/service/account_service.dart'; import 'package:autonomy_flutter/service/configuration_service.dart'; +import 'package:collection/collection.dart'; import 'package:nft_collection/database/dao/dao.dart'; abstract class PlaylistService { @@ -24,6 +25,21 @@ class PlayListServiceImp implements PlaylistService { PlayListServiceImp(this._configurationService, this._tokenDao, this._accountService, this._assetTokenDao); + Future getPlaylistById(String id) async { + final playlists = await getPlayList(); + return playlists.firstWhereOrNull((element) => element.id == id); + } + + Future> _getHiddenTokenIds() async { + final hiddenTokens = _configurationService.getHiddenOrSentTokenIDs(); + final hiddenAddresses = await _accountService.getHiddenAddressIndexes(); + final tokens = await _tokenDao + .findTokenIDsByOwners(hiddenAddresses.map((e) => e.address).toList()); + + hiddenTokens.addAll(tokens); + return hiddenTokens; + } + @override Future> getPlayList() async { final playlists = _getRawPlayList(); @@ -32,12 +48,7 @@ class PlayListServiceImp implements PlaylistService { return []; } - final hiddenTokens = _configurationService.getHiddenOrSentTokenIDs(); - final hiddenAddresses = await _accountService.getHiddenAddressIndexes(); - final tokens = await _tokenDao - .findTokenIDsByOwners(hiddenAddresses.map((e) => e.address).toList()); - - hiddenTokens.addAll(tokens); + final hiddenTokens = await _getHiddenTokenIds(); for (var playlist in playlists) { playlist.tokenIDs diff --git a/lib/service/remote_config_service.dart b/lib/service/remote_config_service.dart index c7193fd641..0ed57962b5 100644 --- a/lib/service/remote_config_service.dart +++ b/lib/service/remote_config_service.dart @@ -57,7 +57,15 @@ class RemoteConfigServiceImpl implements RemoteConfigService { 'previews/d15cc1f3-c2f1-4b9c-837d-7c131583bf40/1710123470/index.html', 'public_version_thumbnail': 'thumbnails/d15cc1f3-c2f1-4b9c-837d-7c131583bf40/1710123327' - } + }, + 'john_gerrard': { + 'contract_address': '0x9D57f2e1A8c864009ed0C980E2d31aa5EB42f820', + 'exhibition_id': '50fb6756-80a9-46e4-b70c-380c32dfcc77', + }, + 'crawl': { + 'exhibition_id': '3c4b0a8b-6d3e-4c32-aaae-c701bb9deca9', + }, + 'dont_fake_artwork_series_ids': ['0a954c31-d336-4e37-af0f-ec336c064879'], }, 'in_app_webview': { 'uri_scheme_white_list': ['https'], @@ -65,6 +73,11 @@ class RemoteConfigServiceImpl implements RemoteConfigService { }, 'dApp_urls': { 'deny_dApp_list': [], + 'tezos_nodes': [ + 'https://mainnet.api.tez.ie', + 'https://rpc.tzbeta.net', + 'https://mainnet.tezos.marigold.dev' + ] } }; @@ -122,6 +135,7 @@ enum ConfigGroup { inAppWebView, dAppUrls, exhibition, + johnGerrard, } // ConfigGroup getString extension @@ -146,6 +160,8 @@ extension ConfigGroupExtension on ConfigGroup { return 'dApp_urls'; case ConfigGroup.exhibition: return 'exhibition'; + case ConfigGroup.johnGerrard: + return 'john_gerrard'; } } } @@ -172,7 +188,14 @@ enum ConfigKey { scrollablePreviewUrl, specifiedSeriesArtworkModelTitle, yokoOnoPublic, + johnGerrard, + crawl, + dontFakeArtworkSeriesIds, yokoOnoPrivateTokenIds, + tezosNodes, + seriesIds, + assetIds, + customNote, uriSchemeWhiteList, denyDAppList, allowedFingerprints, @@ -224,8 +247,22 @@ extension ConfigKeyExtension on ConfigKey { return 'specified_series_artwork_model_title'; case ConfigKey.yokoOnoPublic: return 'yoko_ono_public'; + case ConfigKey.johnGerrard: + return 'john_gerrard'; + case ConfigKey.crawl: + return 'crawl'; + case ConfigKey.dontFakeArtworkSeriesIds: + return 'dont_fake_artwork_series_ids'; case ConfigKey.yokoOnoPrivateTokenIds: return 'yoko_ono_private_token_ids'; + case ConfigKey.tezosNodes: + return 'tezos_nodes'; + case ConfigKey.seriesIds: + return 'series_ids'; + case ConfigKey.assetIds: + return 'asset_ids'; + case ConfigKey.customNote: + return 'custom_notes'; case ConfigKey.uriSchemeWhiteList: return 'uri_scheme_white_list'; case ConfigKey.denyDAppList: diff --git a/lib/service/tezos_beacon_service.dart b/lib/service/tezos_beacon_service.dart index 0745cd2c73..7b0dc9ee5e 100644 --- a/lib/service/tezos_beacon_service.dart +++ b/lib/service/tezos_beacon_service.dart @@ -7,7 +7,9 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io'; +import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/database/cloud_database.dart'; import 'package:autonomy_flutter/database/entity/connection.dart'; import 'package:autonomy_flutter/main.dart'; @@ -17,12 +19,14 @@ import 'package:autonomy_flutter/model/p2p_peer.dart'; import 'package:autonomy_flutter/model/tezos_connection.dart'; import 'package:autonomy_flutter/screen/app_router.dart'; import 'package:autonomy_flutter/service/navigation_service.dart'; +import 'package:autonomy_flutter/service/network_service.dart'; import 'package:autonomy_flutter/util/constants.dart'; import 'package:autonomy_flutter/util/inapp_notifications.dart'; import 'package:autonomy_flutter/util/log.dart'; import 'package:autonomy_flutter/util/tezos_beacon_channel.dart'; import 'package:autonomy_flutter/util/ui_helper.dart'; import 'package:collection/collection.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/cupertino.dart'; @@ -41,6 +45,20 @@ class TezosBeaconService implements BeaconHandler { TezosBeaconService(this._navigationService, this._cloudDB) { _beaconChannel = TezosBeaconChannel(handler: this); unawaited(_beaconChannel.connect()); + if (Platform.isIOS) { + injector().addListener( + (result) { + if (result == ConnectivityResult.none) { + unawaited(_beaconChannel.pause()); + log.info('TezosBeaconService: pause'); + } else { + unawaited(_beaconChannel.resume()); + log.info('TezosBeaconService: resume'); + } + }, + id: NetworkService.beaconListenerId, + ); + } } void _addedConnection() { @@ -165,6 +183,14 @@ class TezosBeaconService implements BeaconHandler { } } + Future pause() async { + await _beaconChannel.pause(); + } + + Future resume() async { + await _beaconChannel.resume(); + } + @override void onAbort() { log.info('TezosBeaconService: onAbort'); diff --git a/lib/service/tezos_service.dart b/lib/service/tezos_service.dart index e0c196feb7..df46cb2571 100644 --- a/lib/service/tezos_service.dart +++ b/lib/service/tezos_service.dart @@ -11,7 +11,9 @@ import 'dart:math'; import 'dart:typed_data'; import 'package:autonomy_flutter/common/environment.dart'; -import 'package:autonomy_flutter/util/constants.dart'; +import 'package:autonomy_flutter/common/injector.dart'; +import 'package:autonomy_flutter/service/network_issue_manager.dart'; +import 'package:autonomy_flutter/service/remote_config_service.dart'; import 'package:autonomy_flutter/util/log.dart'; import 'package:autonomy_flutter/util/wallet_storage_ext.dart'; import 'package:libauk_dart/libauk_dart.dart'; @@ -24,7 +26,7 @@ const baseOperationCustomFeeMedium = 150; const baseOperationCustomFeeHigh = 200; abstract class TezosService { - Future getBalance(String address); + Future getBalance(String address, {bool doRetry = false}); Future estimateOperationFee(String publicKey, List operations, {int? baseOperationCustomFee}); @@ -48,24 +50,47 @@ abstract class TezosService { } class TezosServiceImpl extends TezosService { - final TezartClient _tezartClient; + TezartClient get _tezartClient => _getClient(); + final NetworkIssueManager _networkIssueManager; - TezosServiceImpl(this._tezartClient); + TezosServiceImpl(this._networkIssueManager); + + String _nodeUrl = ''; + + TezartClient _getClient() { + if (Environment.appTestnetConfig) { + return TezartClient(Environment.tezosNodeClientTestnetURL); + } + if (_nodeUrl.isEmpty) { + _changeNode(); + } + return TezartClient(_nodeUrl); + } + + void _changeNode() { + final publicTezosNodes = injector() + .getConfig(ConfigGroup.dAppUrls, ConfigKey.tezosNodes, []) + ..remove(_nodeUrl); + if (publicTezosNodes.isEmpty) { + return; + } + _nodeUrl = publicTezosNodes[Random().nextInt(publicTezosNodes.length)]; + } @override - Future getBalance(String address) { - log.info("TezosService.getBalance: $address"); - return _retryOnNodeError((client) async { - return client.getBalance(address: address); - }); + Future getBalance(String address, {bool doRetry = false}) { + log.info('TezosService.getBalance: $address'); + return _retryOnError( + (client) async => client.getBalance(address: address), + doRetry: doRetry); } @override Future estimateOperationFee(String publicKey, List operations, {int? baseOperationCustomFee}) async { - log.info("TezosService.estimateOperationFee"); + log.info('TezosService.estimateOperationFee'); - return _retryOnNodeError((client) async { + return _retryOnError((client) async { var operationList = OperationsList( publicKey: publicKey, rpcInterface: client.rpcInterface); @@ -79,8 +104,8 @@ class TezosServiceImpl extends TezosService { operationList.prependOperation(RevealOperation()); } - log.info( - "TezosService.estimateOperationFee: ${operationList.operations.map((e) => e.toJson()).toList()}"); + log.info('TezosService.estimateOperationFee: ' + '${operationList.operations.map((e) => e.toJson()).toList()}'); await operationList.estimate( baseOperationCustomFee: baseOperationCustomFee); @@ -95,8 +120,8 @@ class TezosServiceImpl extends TezosService { Future sendOperationTransaction( WalletStorage wallet, int index, List operations, {int? baseOperationCustomFee}) async { - log.info("TezosService.sendOperationTransaction"); - return _retryOnNodeError((client) async { + log.info('TezosService.sendOperationTransaction'); + return _retryOnError((client) async { var operationList = OperationsList( publicKey: await wallet.getTezosPublicKey(index: index), rpcInterface: client.rpcInterface); @@ -115,8 +140,8 @@ class TezosServiceImpl extends TezosService { (forgedHex) => wallet.tezosSignTransaction(forgedHex, index: index), baseOperationCustomFee: baseOperationCustomFee); - log.info( - "TezosService.sendOperationTransaction: ${operationList.result.id}"); + log.info('TezosService.sendOperationTransaction:' + ' ${operationList.result.id}'); return operationList.result.id; }); } @@ -124,9 +149,8 @@ class TezosServiceImpl extends TezosService { @override Future estimateFee(String publicKey, String to, int amount, {int? baseOperationCustomFee}) async { - log.info("TezosService.estimateFee: $to, $amount"); - - return _retryOnNodeError((client) async { + log.info('TezosService.estimateFee: $to, $amount'); + return _retryOnError((client) async { final operation = await client.transferOperation( publicKey: publicKey, destination: to, @@ -144,8 +168,8 @@ class TezosServiceImpl extends TezosService { Future sendTransaction( WalletStorage wallet, int index, String to, int amount, {int? baseOperationCustomFee}) async { - log.info("TezosService.sendTransaction: $to, $amount"); - return _retryOnNodeError((client) async { + log.info('TezosService.sendTransaction: $to, $amount'); + return _retryOnError((client) async { final operation = await client.transferOperation( publicKey: await wallet.getTezosPublicKey(index: index), destination: to, @@ -171,22 +195,22 @@ class TezosServiceImpl extends TezosService { String to, String tokenId, int quantity) async { final params = [ { - "prim": "Pair", - "args": [ - {"string": from}, + 'prim': 'Pair', + 'args': [ + {'string': from}, [ { - "args": [ - {"string": to}, + 'args': [ + {'string': to}, { - "prim": "Pair", - "args": [ - {"int": tokenId}, - {"int": "$quantity"} + 'prim': 'Pair', + 'args': [ + {'int': tokenId}, + {'int': '$quantity'} ] } ], - "prim": "Pair" + 'prim': 'Pair' } ] ] @@ -196,10 +220,15 @@ class TezosServiceImpl extends TezosService { return TransactionOperation( amount: 0, destination: contract, - entrypoint: "transfer", + entrypoint: 'transfer', params: params); } + Future _retryOnError(Future Function(TezartClient) func, + {bool doRetry = true}) => + _networkIssueManager.retryOnConnectIssueTx(() => _retryOnNodeError(func), + maxRetries: doRetry ? 3 : 0); + Future _retryOnNodeError(Future Function(TezartClient) func) async { try { return await func(_tezartClient); @@ -207,11 +236,8 @@ class TezosServiceImpl extends TezosService { if (Environment.appTestnetConfig) { rethrow; } - - final retryTezosNodeClientURL = - publicTezosNodes[Random().nextInt(publicTezosNodes.length)]; - final clientToRetry = TezartClient(retryTezosNodeClientURL); - return await func(clientToRetry); + _changeNode(); + return await func(_tezartClient); } } } diff --git a/lib/service/tv_cast_service.dart b/lib/service/tv_cast_service.dart new file mode 100644 index 0000000000..b03b650d19 --- /dev/null +++ b/lib/service/tv_cast_service.dart @@ -0,0 +1,176 @@ +import 'package:autonomy_flutter/gateway/tv_cast_api.dart'; +import 'package:feralfile_app_tv_proto/feralfile_app_tv_proto.dart'; + +abstract class TvCastService { + Future status(CheckDeviceStatusRequest request); + + Future connect(ConnectRequestV2 request); + + Future disconnect(DisconnectRequestV2 request); + + Future castListArtwork(CastListArtworkRequest request); + + Future pauseCasting(PauseCastingRequest request); + + Future resumeCasting(ResumeCastingRequest request); + + Future nextArtwork(NextArtworkRequest request); + + Future moveToArtwork(MoveToArtworkRequest request); + + Future previousArtwork(PreviousArtworkRequest request); + + Future appendListArtwork( + AppendArtworkToCastingListRequest request); + + Future updateDuration(UpdateDurationRequest request); + + Future keyboardEvent(KeyboardEventRequest request); + + Future rotate(RotateRequest request); + + Future castExhibition(CastExhibitionRequest request); + + Future tap(TapGestureRequest request); + + Future drag(DragGestureRequest request); + + Future getCursorOffset(GetCursorOffsetRequest request); + + Future setCursorOffset(SetCursorOffsetRequest request); +} + +class TvCastServiceImpl implements TvCastService { + final TvCastApi _api; + final CanvasDevice _device; + + TvCastServiceImpl(this._api, this._device); + + Future _cast(Map body) async { + final result = await _api.request( + locationId: _device.locationId, + topicId: _device.topicId, + body: body, + ); + return result['message']; + } + + Map _getBody(Request request) => + RequestBody(request).toJson(); + + @override + Future status( + CheckDeviceStatusRequest request) async { + final result = await _cast(_getBody(request)); + return CheckDeviceStatusReply.fromJson(result); + } + + @override + Future connect(ConnectRequestV2 request) async { + final result = await _cast(_getBody(request)); + return ConnectReplyV2.fromJson(result); + } + + @override + Future disconnect(DisconnectRequestV2 request) async { + final result = await _cast(_getBody(request)); + return DisconnectReplyV2.fromJson(result); + } + + @override + Future castListArtwork( + CastListArtworkRequest request) async { + final result = await _cast(_getBody(request)); + return CastListArtworkReply.fromJson(result); + } + + @override + Future pauseCasting(PauseCastingRequest request) async { + final result = await _cast(_getBody(request)); + return PauseCastingReply.fromJson(result); + } + + @override + Future resumeCasting(ResumeCastingRequest request) async { + final result = await _cast(_getBody(request)); + return ResumeCastingReply.fromJson(result); + } + + @override + Future nextArtwork(NextArtworkRequest request) async { + final result = await _cast(_getBody(request)); + return NextArtworkReply.fromJson(result); + } + + @override + Future moveToArtwork(MoveToArtworkRequest request) async { + final result = await _cast(_getBody(request)); + return MoveToArtworkReply.fromJson(result); + } + + @override + Future previousArtwork( + PreviousArtworkRequest request) async { + final result = await _cast(_getBody(request)); + return PreviousArtworkReply.fromJson(result); + } + + @override + Future appendListArtwork( + AppendArtworkToCastingListRequest request) async { + final result = await _cast(_getBody(request)); + return AppendArtworkToCastingListReply.fromJson(result); + } + + @override + Future updateDuration( + UpdateDurationRequest request) async { + final result = await _cast(_getBody(request)); + return UpdateDurationReply.fromJson(result); + } + + @override + Future keyboardEvent(KeyboardEventRequest request) async { + final result = await _cast(_getBody(request)); + return KeyboardEventReply.fromJson(result); + } + + @override + Future rotate(RotateRequest request) async { + final result = await _cast(_getBody(request)); + return RotateReply.fromJson(result); + } + + @override + Future castExhibition( + CastExhibitionRequest request) async { + final result = await _cast(_getBody(request)); + return CastExhibitionReply.fromJson(result); + } + + @override + Future tap(TapGestureRequest request) async { + final result = await _cast(_getBody(request)); + return GestureReply.fromJson(result); + } + + @override + Future drag(DragGestureRequest request) async { + final result = await _cast(_getBody(request)); + return GestureReply.fromJson(result); + } + + @override + Future getCursorOffset( + GetCursorOffsetRequest request) async { + final result = await _cast(_getBody(request)); + return GetCursorOffsetReply.fromJson(result); + } + + @override + Future setCursorOffset( + SetCursorOffsetRequest request) async { + final result = await _cast(_getBody(request)); + return SetCursorOffsetReply.fromJson(result); + } +} diff --git a/lib/service/wc2_service.dart b/lib/service/wc2_service.dart index d82824361d..dd1996decc 100644 --- a/lib/service/wc2_service.dart +++ b/lib/service/wc2_service.dart @@ -13,7 +13,6 @@ import 'dart:convert'; import 'package:autonomy_flutter/common/environment.dart'; import 'package:autonomy_flutter/database/cloud_database.dart'; import 'package:autonomy_flutter/database/entity/connection.dart'; -import 'package:autonomy_flutter/model/add_ethereum_chain.dart'; import 'package:autonomy_flutter/model/connection_request_args.dart'; import 'package:autonomy_flutter/model/wc2_request.dart'; import 'package:autonomy_flutter/model/wc_ethereum_transaction.dart'; @@ -130,7 +129,6 @@ class Wc2Service { 'eth_sign': _handleEthSign, 'eth_signTypedData': _handleEthSignType, 'eth_signTypedData_v4': _handleEthSignType, - 'wallet_addEthereumChain': _handleAddEthereumChain, }; log.info('[Wc2Service] Registering handlers for chainId: $chainId'); ethRequestHandlerMap.forEach((method, handler) { @@ -187,21 +185,6 @@ class Wc2Service { return result; } - Future _handleAddEthereumChain(String topic, params) async { - log.info('[Wc2Service] received wallet_addEthereumChain request $params'); - late final AddEthereumChainParameter addEthereumChainParam; - try { - addEthereumChainParam = AddEthereumChainParameter.fromJson(params.first); - if (!addEthereumChainParam.isValid) { - throw JsonRpcError.invalidParams('Invalid addEthereumChain params'); - } - - return true; - } catch (e) { - throw JsonRpcError.invalidParams(e.toString()); - } - } - Future _handleAuPermissions(String topic, params) async { log.info('[Wc2Service] received autonomy-au_permissions request $params'); final proposer = await _getWc2Request(topic, params); diff --git a/lib/util/announcement_ext.dart b/lib/util/announcement_ext.dart index cfec604808..a956930da3 100644 --- a/lib/util/announcement_ext.dart +++ b/lib/util/announcement_ext.dart @@ -12,10 +12,6 @@ extension AnnouncementLocalExt on AnnouncementLocal { } } - bool get isMemento6 { - return announcementType == AnnouncementType.Memento6; - } - String get notificationTitle { switch (announcementType) { case AnnouncementType.Memento6: @@ -57,36 +53,19 @@ class ShowAnouncementNotificationInfo { ShowAnouncementNotificationInfo.withMap({required this.showAnnouncementMap}); - bool shouldShowAnnouncementNotification(AnnouncementLocal announcementLocal) { - final announcementContextId = announcementLocal.announcementContextId; - const maxShowCount = MAX_ANNOUNCEMENT_SHOW_COUNT; - if (showAnnouncementMap[announcementContextId] == null) { - return true; - } - final isExpired = DateTime.now() - .subtract(MAX_ANNOUNCEMENT_SHOW_EXPIRED_DURATION) - .isAfter( - DateTime.fromMillisecondsSinceEpoch(announcementLocal.announceAt)); - if (showAnnouncementMap[announcementContextId]! < maxShowCount && - !isExpired) { - return true; - } - return false; - } - ShowAnouncementNotificationInfo merge(ShowAnouncementNotificationInfo other) { showAnnouncementMap.addAll(other.showAnnouncementMap); return this; } // toJson - Map toJson() { - return showAnnouncementMap; - } + Map toJson() => showAnnouncementMap; // fromJson factory ShowAnouncementNotificationInfo.fromJson(Map json) { - if (json.isEmpty) return ShowAnouncementNotificationInfo(); + if (json.isEmpty) { + return ShowAnouncementNotificationInfo(); + } return ShowAnouncementNotificationInfo.withMap( showAnnouncementMap: json.map((key, value) => MapEntry(key, int.tryParse(value.toString()) ?? 0))); diff --git a/lib/util/asset_token_ext.dart b/lib/util/asset_token_ext.dart index e15a32f234..305b2e4774 100644 --- a/lib/util/asset_token_ext.dart +++ b/lib/util/asset_token_ext.dart @@ -5,7 +5,6 @@ import 'dart:ui'; import 'package:autonomy_flutter/common/environment.dart'; import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/database/cloud_database.dart'; -import 'package:autonomy_flutter/model/ff_series.dart'; import 'package:autonomy_flutter/model/pair.dart'; import 'package:autonomy_flutter/model/play_list_model.dart'; import 'package:autonomy_flutter/model/postcard_metadata.dart'; @@ -21,7 +20,7 @@ import 'package:autonomy_flutter/service/postcard_service.dart'; import 'package:autonomy_flutter/service/remote_config_service.dart'; import 'package:autonomy_flutter/util/constants.dart'; import 'package:autonomy_flutter/util/exhibition_ext.dart'; -import 'package:autonomy_flutter/util/feralfile_extension.dart'; +import 'package:autonomy_flutter/util/john_gerrard_helper.dart'; import 'package:autonomy_flutter/util/log.dart'; import 'package:autonomy_flutter/util/postcard_extension.dart'; import 'package:autonomy_flutter/util/string_ext.dart'; @@ -52,6 +51,20 @@ extension AssetTokenExtension on AssetToken { } }; + String? get displayTitle { + if (title == null) { + return null; + } + + final isJohnGerrardSeries = asset?.assetID != null && + JohnGerrardHelper.assetIDs + .any((id) => asset?.assetID!.startsWith(id) ?? false); + + return mintedAt != null && !isJohnGerrardSeries + ? '$title (${mintedAt!.year})' + : title; + } + bool get hasMetadata => galleryThumbnailURL != null; String get secondaryMarketURL { @@ -71,6 +84,20 @@ extension AssetTokenExtension on AssetToken { } } + String get secondaryMarketName { + final url = secondaryMarketURL; + if (url.contains(OPENSEA_ASSET_PREFIX)) { + return 'OpenSea'; + } else if (url.contains(FXHASH_IDENTIFIER)) { + return 'FXHash'; + } else if (url.contains(TEIA_ART_ASSET_PREFIX)) { + return 'Teia Art'; + } else if (url.contains(objktAssetPrefix)) { + return 'Objkt'; + } + return ''; + } + bool get isAirdrop { final saleModel = initialSaleModel?.toLowerCase(); return ['airdrop', 'shopping_airdrop'].contains(saleModel); @@ -293,6 +320,11 @@ extension AssetTokenExtension on AssetToken { bool get isPostcard => contractAddress == Environment.postcardContractAddress; + String? get contractAddress { + final splitted = id.split('-'); + return splitted.length > 1 ? splitted[1] : null; + } + String? get feralfileArtworkId { if (!isFeralfile) { return null; @@ -370,9 +402,6 @@ extension AssetTokenExtension on AssetToken { return lst.map((e) => Artist.fromJson(e)).toList().sublist(1); } - bool get isAirdropToken => - Environment.autonomyAirDropContractAddress == contractAddress; - bool get isMoMAMemento => [ ...momaMementoContractAddresses, Environment.autonomyAirDropContractAddress @@ -448,12 +477,39 @@ extension CompactedAssetTokenExtension on CompactedAssetToken { ArtworkIdentity get identity => ArtworkIdentity(id, owner); - bool get isPostcard { + String? get displayTitle { + if (title == null) { + return null; + } + + final isJohnGerrardSeries = assetID != null && + JohnGerrardHelper.assetIDs + .any((id) => assetID?.startsWith(id) ?? false); + + return mintedAt != null && !isJohnGerrardSeries + ? '$title (${mintedAt!.year})' + : title; + } + + bool get isPostcard => contractAddress == Environment.postcardContractAddress; + + String? get contractAddress { final splitted = id.split('-'); - return splitted.length > 1 && - splitted[1] == Environment.postcardContractAddress; + return splitted.length > 1 ? splitted[1] : null; + } + + bool get isFeralfile => source == 'feralfile'; + + bool get isJohnGerrardArtwork { + final contractAddress = this.contractAddress; + final johnGerrardContractAddress = JohnGerrardHelper.contractAddress; + return isFeralfile && contractAddress == johnGerrardContractAddress; } + bool get shouldRefreshThumbnailCache => + isJohnGerrardArtwork && + edition > JohnGerrardHelper.johnGerrardLatestRevealIndex - 2; + String get getMimeType { switch (mimeType) { case 'image/avif': @@ -558,64 +614,6 @@ String _refineToCloudflareURL(String url, String thumbnailID, String variant) { : '$cloudFlareImageUrlPrefix$thumbnailID/$variant'; } -AssetToken createPendingAssetToken({ - required FFSeries series, - required String owner, - required String tokenId, -}) { - final indexerId = series.airdropInfo?.getTokenIndexerId(tokenId); - final artist = series.artist; - final exhibition = series.exhibition; - final contract = series.contract; - return AssetToken( - asset: Asset( - indexerId, - '', - DateTime.now(), - artist?.id, - artist?.fullName, - null, - null, - series.title, - series.description, - null, - null, - null, - series.maxEdition, - 'airdrop', - null, - series.thumbnailURI, - series.thumbnailURI, - series.thumbnailURI, - null, - null, - 'airdrop', - null, - null, - null, - ), - blockchain: exhibition?.mintBlockchain.toLowerCase() ?? 'tezos', - fungible: false, - contractType: '', - tokenId: tokenId, - contractAddress: contract?.address, - edition: 0, - editionName: '', - id: indexerId ?? '', - mintedAt: series.createdAt ?? DateTime.now(), - balance: 1, - owner: owner, - owners: { - owner: 1, - }, - lastActivityTime: DateTime.now(), - lastRefreshedTime: DateTime(1), - pending: true, - originTokenInfo: [], - provenance: [], - ); -} - extension AssetExt on Asset { // copyWith method Asset copyWith({ diff --git a/lib/util/canvas_device_adapter.dart b/lib/util/canvas_device_adapter.dart new file mode 100644 index 0000000000..43e5f75f3f --- /dev/null +++ b/lib/util/canvas_device_adapter.dart @@ -0,0 +1,24 @@ +import 'package:feralfile_app_tv_proto/feralfile_app_tv_proto.dart'; +import 'package:hive/hive.dart'; + +class CanvasDeviceAdapter extends TypeAdapter { + @override + final int typeId = 0; + + @override + CanvasDevice read(BinaryReader reader) => CanvasDevice( + deviceId: reader.readString(), + locationId: reader.readString(), + topicId: reader.readString(), + name: reader.readString(), + ); + + @override + void write(BinaryWriter writer, CanvasDevice obj) { + writer + ..writeString(obj.deviceId) + ..writeString(obj.locationId) + ..writeString(obj.topicId) + ..writeString(obj.name); + } +} diff --git a/lib/util/cast_request_ext.dart b/lib/util/cast_request_ext.dart new file mode 100644 index 0000000000..a74cca5992 --- /dev/null +++ b/lib/util/cast_request_ext.dart @@ -0,0 +1,5 @@ +import 'package:feralfile_app_tv_proto/models/model.dart'; + +extension CastExhibitionRequestExt on CastExhibitionRequest { + String get displayKey => exhibitionId ?? ''; +} diff --git a/lib/util/constants.dart b/lib/util/constants.dart index 57b85df0bc..35ac287c28 100644 --- a/lib/util/constants.dart +++ b/lib/util/constants.dart @@ -45,13 +45,8 @@ const DEEP_LINKS = [ 'autonomy://', 'https://autonomy.io', 'https://au.bitmark.com', - 'https://autonomy-app.app.link', - 'https://autonomy-app-alternate.app.link', - 'https://link.autonomy.io', + ...Constants.branchDeepLinks, 'feralfile://', - 'https://feralfile-app.app.link', - 'https://feralfile-app-alternate.app.link', - 'https://app.feralfile.com', ]; const FF_ARTIST_COLLECTOR = 'https://feralfile.com/docs/artist-collector-rights'; @@ -70,6 +65,8 @@ const MOMA_MEMENTO_EXHIBITION_IDS = [ '3ee3e8a4-90dd-4843-8ec3-858e6bea1965' ]; +const cloudFlarePrefix = 'https://imagedelivery.net/'; + const POSTCARD_IPFS_PREFIX_TEST = 'https://ipfs.test.bitmark.com/ipfs'; const POSTCARD_IPFS_PREFIX_PROD = 'https://ipfs.bitmark.com/ipfs'; @@ -147,15 +144,6 @@ const artworkSectionDivider = Divider( thickness: 1, ); -const MOMA_MEMENTO_6_CLAIM_ID = 'memento6'; - -const MEMENTO_6_SERIES_ID_MAINNET = '2b75da9b-c605-4842-bf59-8e2e1fe04be6'; -const MEMENTO_6_SERIES_ID_TESTNET = '420f4f8e-f45f-4627-b36c-e9fa5bf6af43'; - -String get memento6SeriesId => Environment.appTestnetConfig - ? MEMENTO_6_SERIES_ID_TESTNET - : MEMENTO_6_SERIES_ID_MAINNET; - const REMOVE_CUSTOMER_SUPPORT = '/bitmark-inc/autonomy-apps/main/customer_support/annoucement_os.md'; const int cellPerRowPhone = 3; @@ -182,9 +170,6 @@ const int MAX_STAMP_IN_POSTCARD = 15; const int STAMP_SIZE = 2160; -const int MAX_ANNOUNCEMENT_SHOW_COUNT = 3; -const Duration MAX_ANNOUNCEMENT_SHOW_EXPIRED_DURATION = Duration(days: 30); - const String POSTCARD_LOCATION_HIVE_BOX = 'postcard_location_hive_box'; const String MIXPANEL_HIVE_BOX = 'mixpanel_hive_box'; @@ -213,12 +198,6 @@ String get usdcContractAddress => Environment.appTestnetConfig ? USDC_CONTRACT_ADDRESS_GOERLI : USDC_CONTRACT_ADDRESS; -const publicTezosNodes = [ - 'https://mainnet.api.tez.ie', - 'https://rpc.tzbeta.net', - 'https://mainnet.tezos.marigold.dev', -]; - const TV_APP_STORE_URL = 'https://play.google.com/store/apps/details?id=com.bitmark.autonomy_tv'; @@ -242,12 +221,23 @@ const int LEADERBOARD_PAGE_SIZE = 50; const int maxCollectionListSize = 3; +const maxRetryCount = 3; + const double collectionListArtworkAspectRatio = 375 / 210.94; const String collectionListArtworkThumbnailVariant = 'thumbnailList'; const String POSTCARD_ONSITE_REQUEST_ID = 'moma-postcard-onsite'; const String POSTCARD_ONLINE_REQUEST_ID = 'moma-postcard-online'; +const String SOURCE_EXHIBITION_ID = 'source'; +const List YOUTUBE_DOMAINS = ['youtube.com', 'youtu.be']; +const List YOUTUBE_VARIANTS = [ + 'maxresdefault', // Higher quality - May or may not exist + 'mqdefault', // Lower quality - Guaranteed to exist +]; + +const MAGIC_NUMBER = 168; + Future isAppCenterBuild() async { final PackageInfo info = await PackageInfo.fromPlatform(); return info.packageName.contains('inhouse'); @@ -510,6 +500,9 @@ class Constants { 'https://autonomy-app.app.link', 'https://autonomy-app-alternate.app.link', 'https://link.autonomy.io', + 'https://feralfile-app.app.link', + 'https://feralfile-app-alternate.app.link', + 'https://app.feralfile.com', ]; } @@ -542,7 +535,6 @@ class MixpanelProp { static const recipientAddress = 'recipientAddress'; static const seriesId = 'seriesId'; static const method = 'method'; - static const activationId = 'activationId'; static const isOnboarding = 'isOnboarding'; static const id = 'id'; } diff --git a/lib/util/crawl_helper.dart b/lib/util/crawl_helper.dart new file mode 100644 index 0000000000..78ccf6d938 --- /dev/null +++ b/lib/util/crawl_helper.dart @@ -0,0 +1,10 @@ +import 'package:autonomy_flutter/common/injector.dart'; +import 'package:autonomy_flutter/service/remote_config_service.dart'; + +class CrawlHelper { + static String? get exhibitionID { + final config = injector() + .getConfig(ConfigGroup.exhibition, ConfigKey.crawl, {}); + return config['exhibition_id']; + } +} diff --git a/lib/util/custom_exception.dart b/lib/util/custom_exception.dart index a328040fa4..b993ea5049 100644 --- a/lib/util/custom_exception.dart +++ b/lib/util/custom_exception.dart @@ -18,9 +18,3 @@ class LinkingFailedException implements Exception {} class InvalidDeeplink implements Exception {} class FailedFetchBackupVersion implements Exception {} - -class NoRemainingToken implements Exception {} - -class AirdropExpired implements Exception {} - -class AlreadyDelivered implements Exception {} diff --git a/lib/util/custom_route_observer.dart b/lib/util/custom_route_observer.dart index 9cd2bad173..b9d9a0bca0 100644 --- a/lib/util/custom_route_observer.dart +++ b/lib/util/custom_route_observer.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/service/metric_client_service.dart'; +import 'package:autonomy_flutter/util/ui_helper.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -9,8 +10,17 @@ class CustomRouteObserver> extends RouteObserver { final _metricClient = injector(); static Route? currentRoute; + static bool _onIgnoreBackLayerPopUp = false; + + static bool get onIgnoreBackLayerPopUp => _onIgnoreBackLayerPopUp; + @override void didPush(Route route, Route? previousRoute) { + /// this must be put before super.didPush + if (route.settings.name == UIHelper.ignoreBackLayerPopUpRouteName) { + _onIgnoreBackLayerPopUp = true; + } + if (previousRoute != null) { unawaited( _metricClient.trackEndScreen(previousRoute).then( @@ -37,6 +47,11 @@ class CustomRouteObserver> extends RouteObserver { ); currentRoute = previousRoute; super.didPop(route, previousRoute); + + /// this must be put after super.didPop + if (route.settings.name == UIHelper.ignoreBackLayerPopUpRouteName) { + _onIgnoreBackLayerPopUp = false; + } } @override diff --git a/lib/util/device.dart b/lib/util/device.dart index 94b719395f..f59258974d 100644 --- a/lib/util/device.dart +++ b/lib/util/device.dart @@ -8,15 +8,19 @@ import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; +import 'package:uuid/uuid.dart'; -Future getDeviceID() async { +Future getDeviceID() async { + String deviceId = ''; var deviceInfo = DeviceInfoPlugin(); if (Platform.isIOS) { // import 'dart:io' var iosDeviceInfo = await deviceInfo.iosInfo; - return iosDeviceInfo.identifierForVendor; // unique ID on iOS + deviceId = iosDeviceInfo.identifierForVendor ?? + const Uuid().v4(); // unique ID on iOS } else { var androidDeviceInfo = await deviceInfo.androidInfo; - return androidDeviceInfo.id; // unique ID on Android + deviceId = androidDeviceInfo.id; // unique ID on Android } + return deviceId; } diff --git a/lib/util/device_status_ext.dart b/lib/util/device_status_ext.dart new file mode 100644 index 0000000000..cc7fc3b744 --- /dev/null +++ b/lib/util/device_status_ext.dart @@ -0,0 +1,49 @@ +import 'package:autonomy_flutter/common/injector.dart'; +import 'package:autonomy_flutter/model/pair.dart'; +import 'package:autonomy_flutter/service/canvas_client_service_v2.dart'; +import 'package:feralfile_app_tv_proto/feralfile_app_tv_proto.dart'; + +extension ListDeviceStatusExtension + on List> { + Map get controllingDevices { + final canvasClientServiceV2 = injector(); + final Map controllingDeviceStatus = {}; + final thisDevice = canvasClientServiceV2.clientDeviceInfo; + for (final devicePair in this) { + final status = devicePair.second; + if (status.connectedDevice?.deviceId == thisDevice.deviceId) { + controllingDeviceStatus[devicePair.first.deviceId] = status; + } + } + return controllingDeviceStatus; + } +} + +extension DeviceStatusExtension on CheckDeviceStatusReply { + String? get playingArtworkKey { + if (artworks.isEmpty && exhibitionId == null) { + return null; + } + if (exhibitionId != null) { + return exhibitionId.toString(); + } + + final hashCode = artworks.playArtworksHashCode; + return hashCode.toString(); + } +} + +extension PlayArtworksExtension on List { + int get playArtworksHashCode { + final hashCodes = map((e) => e.playArtworkHashCode); + final hashCode = hashCodes.reduce((value, element) => value ^ element); + return hashCode; + } +} + +extension PlayArtworkExtension on PlayArtworkV2 { + int get playArtworkHashCode { + final id = token?.id ?? artwork?.url ?? ''; + return id.hashCode; + } +} diff --git a/lib/util/dio_interceptors.dart b/lib/util/dio_interceptors.dart index 2219c1db7d..0bc7b660e9 100644 --- a/lib/util/dio_interceptors.dart +++ b/lib/util/dio_interceptors.dart @@ -13,6 +13,8 @@ import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/gateway/iap_api.dart'; import 'package:autonomy_flutter/model/ff_account.dart'; import 'package:autonomy_flutter/service/auth_service.dart'; +import 'package:autonomy_flutter/service/network_issue_manager.dart'; +import 'package:autonomy_flutter/util/exception_ext.dart'; import 'package:autonomy_flutter/util/isolated_util.dart'; import 'package:autonomy_flutter/util/log.dart'; import 'package:crypto/crypto.dart'; @@ -43,7 +45,7 @@ class LoggingInterceptor extends Interceptor { Future onResponse( Response response, ResponseInterceptorHandler handler) async { handler.next(response); - await writeAPILog(response); + unawaited(writeAPILog(response)); } Future writeAPILog(Response response) async { @@ -251,5 +253,29 @@ class AirdropInterceptor extends Interceptor { } finally { handler.next(exp); } + handler.next(err); + } +} + +class ConnectingExceptionInterceptor extends Interceptor { + @override + void onError(DioException err, ErrorInterceptorHandler handler) { + if (err.isNetworkIssue) { + log.warning('ConnectingExceptionInterceptor timeout'); + unawaited(injector().showNetworkIssueWarning()); + } + handler.next(err); + } +} + +class TVKeyInterceptor extends Interceptor { + final String tvKey; + + TVKeyInterceptor(this.tvKey); + + @override + void onRequest(RequestOptions options, RequestInterceptorHandler handler) { + options.headers['API-KEY'] = tvKey; + handler.next(options); } } diff --git a/lib/util/dio_util.dart b/lib/util/dio_util.dart index 76c47c3a26..c3e383840e 100644 --- a/lib/util/dio_util.dart +++ b/lib/util/dio_util.dart @@ -18,27 +18,36 @@ import 'package:sentry_flutter/sentry_flutter.dart'; Dio feralFileDio(BaseOptions options) { final dio = baseDio(options); dio.interceptors.add(FeralfileAuthInterceptor()); + dio.interceptors.add(ConnectingExceptionInterceptor()); return dio; } -Dio postcardDio(BaseOptions options) { +Dio customerSupportDio(BaseOptions options) { final dio = baseDio(options); - dio.interceptors.add(HmacAuthInterceptor(Environment.auClaimSecretKey)); dio.interceptors.add(AutonomyAuthInterceptor()); return dio; } -Dio airdropDio(BaseOptions options) { +Dio postcardDio(BaseOptions options) { final dio = baseDio(options); - dio.interceptors.add(AutonomyAuthInterceptor()); dio.interceptors.add(HmacAuthInterceptor(Environment.auClaimSecretKey)); - dio.interceptors.add(AirdropInterceptor()); + dio.interceptors.add(AutonomyAuthInterceptor()); + dio.interceptors.add(ConnectingExceptionInterceptor()); + return dio; +} + +Dio tvCastDio(BaseOptions options) { + final dio = Dio(options); + dio.interceptors.add(TVKeyInterceptor(Environment.tvKey)); + dio.interceptors.add(LoggingInterceptor()); + dio.interceptors.add(ConnectingExceptionInterceptor()); return dio; } Dio chatDio(BaseOptions options) { final dio = baseDio(options); dio.interceptors.add(HmacAuthInterceptor(Environment.chatServerHmacKey)); + dio.interceptors.add(ConnectingExceptionInterceptor()); return dio; } @@ -73,6 +82,7 @@ Dio baseDio(BaseOptions options) { )); dio.interceptors.add(LoggingInterceptor()); + dio.interceptors.add(ConnectingExceptionInterceptor()); (dio.transformer as SyncTransformer).jsonDecodeCallback = parseJson; dio ..options = dioOptions diff --git a/lib/util/exception_ext.dart b/lib/util/exception_ext.dart new file mode 100644 index 0000000000..b64a841349 --- /dev/null +++ b/lib/util/exception_ext.dart @@ -0,0 +1,31 @@ +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:http/http.dart'; +import 'package:tezart/tezart.dart'; + +extension ExceptionExt on Exception { + bool get isNetworkIssue { + if (this is DioException) { + final e = this as DioException; + return e.type == DioExceptionType.connectionError || + e.type == DioExceptionType.sendTimeout || + e.error is SocketException; + } + if (this is TezartNodeError) { + final e = this as TezartNodeError; + return e.cause?.clientError.isNetworkIssue ?? false; + } + + if (this is TezartHttpError) { + final e = this as TezartHttpError; + return e.clientError.isNetworkIssue; + } + + if (this is ClientException) { + return true; + } + + return false; + } +} diff --git a/lib/util/exhibition_ext.dart b/lib/util/exhibition_ext.dart index 4c324887a7..f2911af8f2 100644 --- a/lib/util/exhibition_ext.dart +++ b/lib/util/exhibition_ext.dart @@ -1,11 +1,17 @@ import 'package:autonomy_flutter/common/environment.dart'; import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/model/ff_account.dart'; +import 'package:autonomy_flutter/model/ff_artwork.dart'; import 'package:autonomy_flutter/model/ff_exhibition.dart'; import 'package:autonomy_flutter/model/ff_series.dart'; +import 'package:autonomy_flutter/screen/bloc/subscription/subscription_bloc.dart'; +import 'package:autonomy_flutter/screen/exhibitions/exhibitions_bloc.dart'; import 'package:autonomy_flutter/service/feralfile_service.dart'; import 'package:autonomy_flutter/service/remote_config_service.dart'; import 'package:autonomy_flutter/util/constants.dart'; +import 'package:autonomy_flutter/util/crawl_helper.dart'; +import 'package:autonomy_flutter/util/http_helper.dart'; +import 'package:autonomy_flutter/util/john_gerrard_helper.dart'; import 'package:autonomy_flutter/util/string_ext.dart'; import 'package:collection/collection.dart'; @@ -14,13 +20,45 @@ extension ExhibitionExt on Exhibition { bool get isGroupExhibition => type == 'group'; - //TODO: implement this - bool get isFreeToStream => true; + bool get isSoloExhibition => type == 'solo'; + + bool get isJohnGerrardShow => id == JohnGerrardHelper.exhibitionID; + + bool get isCrawlShow => id == CrawlHelper.exhibitionID; + + DateTime get exhibitionViewAt => + exhibitionStartAt.subtract(Duration(seconds: previewDuration ?? 0)); + + bool get canViewDetails { + final exhibitionBloc = injector(); + final subscriptionBloc = injector(); + return subscriptionBloc.state.isSubscribed || + id == exhibitionBloc.state.featuredExhibition?.id; + } + + String get displayKey => id; //TODO: implement this bool get isOnGoing => true; + bool get isMinted => status == ExhibitionStatus.issued.index; + + List get sortedSeries { + final series = this.series ?? []; + // sort by displayIndex, if displayIndex is equal, sort by createdAt + series.sort((a, b) { + if (a.displayIndex == b.displayIndex) { + return b.createdAt!.compareTo(a.createdAt!); + } + return (a.displayIndex ?? 0) - (b.displayIndex ?? 0); + }); + return series; + } + String? get getSeriesArtworkModelText { + if (this.series == null || id == SOURCE_EXHIBITION_ID) { + return null; + } const sep = ', '; final specifiedSeriesArtworkModelTitle = injector().getConfig>( @@ -29,9 +67,6 @@ extension ExhibitionExt on Exhibition { specifiedSeriesTitle, ); final specifiedSeriesIds = specifiedSeriesArtworkModelTitle.keys; - if (this.series == null) { - return null; - } final currentSpecifiedSeries = this .series! .where((element) => specifiedSeriesIds.contains(element.id)) @@ -96,15 +131,6 @@ extension ListExhibitionDetailExt on List { } extension ExhibitionDetailExt on ExhibitionDetail { - List get seriesIds => - artworks?.map((e) => e.seriesID).toSet().toList() ?? []; - - Artwork? representArtwork(String seriesId) => - artworks!.firstWhereOrNull((e) => e.seriesID == seriesId); - - List get representArtworks => - seriesIds.map((e) => representArtwork(e)).whereNotNull().toList(); - String? getArtworkTokenId(Artwork artwork) { if (artwork.swap != null) { if (artwork.swap!.token == null) { @@ -147,11 +173,91 @@ extension ArtworkExt on Artwork { } String get metricTokenId => '${seriesID}_$id'; + + Future renderingType() async { + final medium = series?.medium ?? 'unknown'; + final mediumType = FeralfileMediumTypes.fromString(medium); + if (mediumType == FeralfileMediumTypes.image) { + final contentType = await HttpHelper.contentType(previewURL); + return contentType; + } else { + return mediumType.toRenderingType; + } + } + + String? get attributesString { + if (artworkAttributes == null) { + return null; + } + + return artworkAttributes! + .map((e) => '${e.traitType}: ${e.value}') + .join('. '); + } + + FFContract? getContract(Exhibition? exhibition) { + if (swap != null) { + if (swap!.token == null) { + return null; + } + + return FFContract( + swap!.contractName, + swap!.blockchainType, + swap!.contractAddress, + ); + } + + return exhibition?.contracts?.firstWhereOrNull( + (e) => e.blockchainType == exhibition.mintBlockchain, + ); + } + + bool get isYokoOnoPublicVersion { + final config = injector() + .getConfig>( + ConfigGroup.exhibition, ConfigKey.yokoOnoPublic, {}); + return id == config['public_token_id']; + } } String getFFUrl(String uri) { + // case 1: cloudflare + if (uri.startsWith(cloudFlarePrefix)) { + return '$uri/thumbnailLarge'; + } + + // case 2 => full cdn if (uri.startsWith('http')) { return uri; } + + //case 3 => cdn return '${Environment.feralFileAssetURL}/$uri'; } + +extension FFContractExt on FFContract { + String? getBlockchainUrl() { + final network = Environment.appTestnetConfig ? 'TESTNET' : 'MAINNET'; + switch ('${network}_$blockchainType') { + case 'MAINNET_ethereum': + return 'https://etherscan.io/address/$address'; + + case 'TESTNET_ethereum': + return 'https://goerli.etherscan.io/address/$address'; + + case 'MAINNET_tezos': + case 'TESTNET_tezos': + return 'https://tzkt.io/$address'; + } + return null; + } +} + +enum ExhibitionStatus { + created, + editorReview, + operatorReview, + issuing, + issued, +} diff --git a/lib/util/feral_file_helper.dart b/lib/util/feral_file_helper.dart index 6b282eb1fb..c814d26c6c 100644 --- a/lib/util/feral_file_helper.dart +++ b/lib/util/feral_file_helper.dart @@ -1,4 +1,5 @@ import 'package:autonomy_flutter/common/environment.dart'; +import 'package:autonomy_flutter/model/ff_exhibition.dart'; class FeralFileHelper { static final String _baseUrl = Environment.feralFileAPIURL; @@ -6,4 +7,10 @@ class FeralFileHelper { static String getArtistUrl(String alias) => '$_baseUrl/artists/$alias'; static String getCuratorUrl(String alias) => '$_baseUrl/curators/$alias'; + + static String getExhibitionNoteUrl(String exhibitionSlug) => + '$_baseUrl/exhibitions/$exhibitionSlug/overview#note'; + + static String getPostUrl(Post post, String exhibitionID) => + '$_baseUrl/journal/${post.type}/${post.slug}/?exhibitionID=$exhibitionID'; } diff --git a/lib/util/feralfile_extension.dart b/lib/util/feralfile_extension.dart deleted file mode 100644 index 3310045680..0000000000 --- a/lib/util/feralfile_extension.dart +++ /dev/null @@ -1,87 +0,0 @@ -import 'package:autonomy_flutter/common/environment.dart'; -import 'package:autonomy_flutter/model/ff_account.dart'; -import 'package:autonomy_flutter/model/ff_series.dart'; -import 'package:autonomy_flutter/model/ff_user.dart'; -import 'package:easy_localization/easy_localization.dart'; - -extension FeralfileErrorExt on FeralfileError { - String get dialogTitle { - switch (code) { - case 5006: - return 'Too soon'; - case 3007: - return 'Too late'; - case 3009: - return 'Out of token'; - case 3010: - return 'Just once'; - case 3013: - case 3014: - return 'One more time'; - default: - return 'error'.tr(); - } - } - - String get dialogMessage { - switch (code) { - case 5006: - return 'It is not yet possible to redeem this gift edition.'; - case 3007: - return 'It is no longer possible to redeem this gift edition.'; - case 3009: - return 'Sorry, the tokens have been delivered to all fastest users.'; - case 3010: - return 'just_once_desc'.tr(); - case 3013: - case 3014: - return 'The validity of the QR code has expired. ' - 'Please scan the QR code again.'; - default: - return message; - } - } - - String getDialogTitle() => dialogTitle; - - String getDialogMessage({FFSeries? series}) { - if (code == 3009 && (series?.maxEdition ?? 0) < 0) { - return 'We are running out of tokens. Come back later.'; - } else { - return dialogMessage; - } - } -} - -extension FFContractExt on FFContract { - String? getBlockChainUrl() { - final network = Environment.appTestnetConfig ? 'TESTNET' : 'MAINNET'; - String? url; - switch ('${network}_$blockchainType') { - case 'MAINNET_ethereum': - url = 'https://etherscan.io/address/$address'; - break; - - case 'TESTNET_ethereum': - url = 'https://goerli.etherscan.io/address/$address}'; - break; - - case 'MAINNET_tezos': - case 'TESTNET_tezos': - url = 'https://tzkt.io/$address'; - break; - } - return url; - } -} - -extension FFArtistExt on FFArtist { - String getDisplayName() => (fullName?.isNotEmpty == true) ? fullName! : alias; -} - -extension AirdropInfoExt on AirdropInfo { - String getTokenIndexerId(String tokenId) { - final prefix = blockchain.toLowerCase() == 'tezos' ? 'tez' : 'eth'; - return '$prefix-$contractAddress-$tokenId'; - } -} diff --git a/lib/util/ff_series_ext.dart b/lib/util/ff_series_ext.dart deleted file mode 100644 index 73d3faa3e6..0000000000 --- a/lib/util/ff_series_ext.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:autonomy_flutter/model/ff_series.dart'; -import 'package:autonomy_flutter/util/custom_exception.dart'; - -extension FFSeriesExt on FFSeries { - void checkAirdropStatusAndThrowIfError() { - if (airdropInfo == null || - airdropInfo?.endedAt?.isBefore(DateTime.now()) == true) { - throw AirdropExpired(); - } - if ((airdropInfo?.remainAmount ?? 0) <= 0) { - throw NoRemainingToken(); - } - } -} diff --git a/lib/util/http_helper.dart b/lib/util/http_helper.dart index 85a62c8b19..04678d5e35 100644 --- a/lib/util/http_helper.dart +++ b/lib/util/http_helper.dart @@ -1,9 +1,11 @@ import 'dart:convert'; +import 'package:autonomy_flutter/util/string_ext.dart'; import 'package:crypto/crypto.dart'; import 'package:dio/dio.dart'; import 'package:eth_sig_util/util/utils.dart'; import 'package:http/http.dart' as http; +import 'package:nft_rendering/nft_rendering.dart'; class HttpHelper { static Map _getHmac( @@ -76,6 +78,24 @@ class HttpHelper { ); return response; } + + static Future contentType(String link) async { + String renderingType = RenderingType.webview; + final uri = Uri.tryParse(link); + if (uri != null) { + try { + final res = + await http.head(uri).timeout(const Duration(milliseconds: 10000)); + renderingType = + res.headers['content-type']?.toMimeType ?? RenderingType.webview; + } catch (e) { + renderingType = RenderingType.webview; + } + } else { + renderingType = RenderingType.webview; + } + return renderingType; + } } enum HttpMethod { diff --git a/lib/util/image_ext.dart b/lib/util/image_ext.dart new file mode 100644 index 0000000000..d39bdcec5e --- /dev/null +++ b/lib/util/image_ext.dart @@ -0,0 +1,53 @@ +import 'package:autonomy_flutter/view/image_background.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; + +extension ImageExt on CachedNetworkImage { + static Widget customNetwork( + String src, { + Duration fadeInDuration = const Duration(milliseconds: 300), + BoxFit? fit, + int? memCacheHeight, + int? memCacheWidth, + int? maxWidthDiskCache, + int? maxHeightDiskCache, + BaseCacheManager? cacheManager, + PlaceholderWidgetBuilder? placeholder, + LoadingErrorWidgetBuilder? errorWidget, + bool shouldRefreshCache = false, + }) { + if (shouldRefreshCache) { + return Image.network( + src, + fit: fit, + errorBuilder: (context, error, stackTrace) => + errorWidget!(context, src, error), + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) { + return ImageBackground(child: child); + } + if (placeholder != null) { + return placeholder(context, src); + } + return const ImageBackground(child: SizedBox()); + }, + cacheHeight: memCacheHeight, + cacheWidth: memCacheWidth, + ); + } + + return CachedNetworkImage( + imageUrl: src, + fadeInDuration: Duration.zero, + fit: BoxFit.cover, + memCacheHeight: memCacheHeight, + memCacheWidth: memCacheWidth, + maxWidthDiskCache: maxWidthDiskCache, + maxHeightDiskCache: maxHeightDiskCache, + cacheManager: cacheManager, + placeholder: placeholder, + errorWidget: errorWidget, + ); + } +} diff --git a/lib/util/john_gerrard_helper.dart b/lib/util/john_gerrard_helper.dart new file mode 100644 index 0000000000..7ad641eddc --- /dev/null +++ b/lib/util/john_gerrard_helper.dart @@ -0,0 +1,70 @@ +import 'package:autonomy_flutter/common/injector.dart'; +import 'package:autonomy_flutter/model/ff_exhibition.dart'; +import 'package:autonomy_flutter/service/feralfile_service.dart'; +import 'package:autonomy_flutter/service/remote_config_service.dart'; +import 'package:autonomy_flutter/util/log.dart'; +import 'package:autonomy_flutter/util/series_ext.dart'; + +class JohnGerrardHelper { + static int _johnGerrardLatestRevealIndex = 0; + + static int get johnGerrardLatestRevealIndex => _johnGerrardLatestRevealIndex; + + static String? get contractAddress { + final config = injector() + .getConfig(ConfigGroup.exhibition, ConfigKey.johnGerrard, {}); + return config['contract_address']; + } + + static String? get exhibitionID { + final config = injector() + .getConfig(ConfigGroup.exhibition, ConfigKey.johnGerrard, {}); + return config['exhibition_id']; + } + + static List get seriesIDs { + final listSeriesIds = injector() + .getConfig?>( + ConfigGroup.johnGerrard, ConfigKey.seriesIds, []); + return listSeriesIds ?? []; + } + + static List get assetIDs { + final listAssetIds = injector() + .getConfig?>( + ConfigGroup.johnGerrard, ConfigKey.assetIds, []); + return listAssetIds ?? []; + } + + static String getIndexID(String tokenId) { + final contractAddress = JohnGerrardHelper.contractAddress; + return 'eth-$contractAddress-$tokenId'; + } + + static List get customNote { + final listCustomNote = injector() + .getConfig?>( + ConfigGroup.johnGerrard, ConfigKey.customNote, []); + return listCustomNote + ?.map((e) => CustomExhibitionNote.fromJson(e)) + .toList() ?? + []; + } + + static Future updateJohnGerrardLatestRevealIndex() async { + try { + final exhibitionId = JohnGerrardHelper.exhibitionID!; + final exhibition = + await injector().getExhibition(exhibitionId); + final series = exhibition.series!.first; + final latestRevealedArtworkIndex = series.latestRevealedArtworkIndex; + + if (latestRevealedArtworkIndex != null) { + log.info('update latestRevealedIndex: $latestRevealedArtworkIndex'); + _johnGerrardLatestRevealIndex = latestRevealedArtworkIndex; + } + } catch (e) { + log.info('updateJohnGerrardLatestRevealIndex error: $e'); + } + } +} diff --git a/lib/util/log.dart b/lib/util/log.dart index 7513574f83..4a1f7b6006 100644 --- a/lib/util/log.dart +++ b/lib/util/log.dart @@ -63,7 +63,7 @@ class FileLogger { static final _lock = synchronization.Lock(); // uses the “synchronized” package static late File _logFile; - static const shrinkSize = 1024 * 1024; // 1MB + static const shrinkSize = 1024 * 896; // 1MB characters static Future initializeLogging() async { await shrinkLogFileIfNeeded(); @@ -72,9 +72,10 @@ class FileLogger { static Future shrinkLogFileIfNeeded() async { _logFile = await getLogFile(); - final current = await _logFile.readAsBytes(); + final current = await _logFile.readAsString(); if (current.length > shrinkSize) { - await _logFile.writeAsBytes(current.sublist(current.length - shrinkSize), + await _logFile.writeAsString( + current.substring(current.length - shrinkSize), flush: true); } @@ -94,11 +95,9 @@ class FileLogger { static File get logFile => _logFile; static Future log(LogRecord record) async { - var text = '$record\n'; - - text = _filterLog(text); - + String text = '$record\n'; debugPrint(text); + text = _filterLog(text); return _lock.synchronized(() async { await _logFile.writeAsString('${record.time}: $text', mode: FileMode.append, flush: true); diff --git a/lib/util/migration/migration_util.dart b/lib/util/migration/migration_util.dart index 8986887e0b..a10cf3e4c8 100644 --- a/lib/util/migration/migration_util.dart +++ b/lib/util/migration/migration_util.dart @@ -35,6 +35,7 @@ class MigrationUtil { final IAPService _iapService; final AuditService _auditService; final BackupService _backupService; + final AddressService _addressService = injector(); final int requiredAndroidMigrationVersion = 95; MigrationUtil(this._configurationService, this._cloudDB, this._accountService, @@ -142,6 +143,11 @@ class MigrationUtil { final currentPersonas = await _cloudDB.personaDao.getPersonas(); for (var persona in currentPersonas) { if (!(await persona.wallet().isWalletCreated())) { + final addresses = + await _cloudDB.addressDao.getAddressesByPersona(persona.uuid); + await _addressService + .deleteAddresses(addresses.map((e) => e.address).toList()); + await _cloudDB.addressDao.deleteAddressesByPersona(persona.uuid); await _cloudDB.personaDao.deletePersona(persona); } } @@ -192,6 +198,11 @@ class MigrationUtil { for (final persona in currentPersonas) { if (!(await persona.wallet().isWalletCreated())) { await _cloudDB.personaDao.deletePersona(persona); + final addresses = + await _cloudDB.addressDao.getAddressesByPersona(persona.uuid); + await _addressService + .deleteAddresses(addresses.map((e) => e.address).toList()); + await _cloudDB.addressDao.deleteAddressesByPersona(persona.uuid); } } final List ffAccounts = []; diff --git a/lib/util/notification_type.dart b/lib/util/notification_type.dart new file mode 100644 index 0000000000..28b01d6a59 --- /dev/null +++ b/lib/util/notification_type.dart @@ -0,0 +1,379 @@ +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// Copyright © 2022 Bitmark. All rights reserved. +// Use of this source code is governed by the BSD-2-Clause Plus Patent License +// that can be found in the LICENSE file. +// + +import 'dart:async'; + +import 'package:autonomy_flutter/common/injector.dart'; +import 'package:autonomy_flutter/main.dart'; +import 'package:autonomy_flutter/screen/app_router.dart'; +import 'package:autonomy_flutter/screen/customer_support/support_thread_page.dart'; +import 'package:autonomy_flutter/screen/detail/artwork_detail_page.dart'; +import 'package:autonomy_flutter/screen/interactive_postcard/postcard_detail_bloc.dart'; +import 'package:autonomy_flutter/screen/interactive_postcard/postcard_detail_page.dart'; +import 'package:autonomy_flutter/service/chat_service.dart'; +import 'package:autonomy_flutter/service/client_token_service.dart'; +import 'package:autonomy_flutter/service/configuration_service.dart'; +import 'package:autonomy_flutter/service/customer_support_service.dart'; +import 'package:autonomy_flutter/service/navigation_service.dart'; +import 'package:autonomy_flutter/service/remote_config_service.dart'; +import 'package:autonomy_flutter/util/constants.dart'; +import 'package:autonomy_flutter/util/inapp_notifications.dart'; +import 'package:autonomy_flutter/util/john_gerrard_helper.dart'; +import 'package:autonomy_flutter/util/log.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:feralfile_app_theme/extensions/extensions.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:nft_collection/database/nft_collection_database.dart'; +import 'package:onesignal_flutter/onesignal_flutter.dart'; + +enum NotificationType { + galleryNewNft, + newPostcardTrip, + postcardShareExpired, + customerSupportNewMessage, + customerSupportCloseIssue, + customerSupportNewAnnouncement, + artworkCreated, + artworkReceived, + newMessage, + jgCrystallineWorkHasArrived, + jgCrystallineWorkGenerated, + exhibitionViewingOpening, + exhibitionSalesOpening, + exhibitionSaleClosing, + unknown; + + // toString method + @override + String toString() { + switch (this) { + case NotificationType.galleryNewNft: + return 'gallery_new_nft'; + case NotificationType.newPostcardTrip: + return 'new_postcard_trip'; + case NotificationType.postcardShareExpired: + return 'postcard_share_expired'; + case NotificationType.customerSupportNewMessage: + return 'customer_support_new_message'; + case NotificationType.customerSupportCloseIssue: + return 'customer_support_close_issue'; + case NotificationType.customerSupportNewAnnouncement: + return 'customer_support_new_announcement'; + case NotificationType.artworkCreated: + return 'artwork_created'; + case NotificationType.artworkReceived: + return 'artwork_received'; + case NotificationType.newMessage: + return 'new_message'; + case NotificationType.jgCrystallineWorkHasArrived: + return 'jg_artwork_solar_day_arrived'; + case NotificationType.jgCrystallineWorkGenerated: + return 'jg_artwork_generated'; + case NotificationType.exhibitionViewingOpening: + return 'exhibition_view_opening'; + case NotificationType.exhibitionSalesOpening: + return 'exhibition_sale_opening'; + case NotificationType.exhibitionSaleClosing: + return 'exhibition_sale_closing'; + case NotificationType.unknown: + return 'unknown'; + } + } + + // fromString method + static NotificationType fromString(String value) { + switch (value) { + case 'gallery_new_nft': + return NotificationType.galleryNewNft; + case 'new_postcard_trip': + return NotificationType.newPostcardTrip; + case 'postcard_share_expired': + return NotificationType.postcardShareExpired; + case 'customer_support_new_message': + return NotificationType.customerSupportNewMessage; + case 'customer_support_close_issue': + return NotificationType.customerSupportCloseIssue; + case 'customer_support_new_announcement': + return NotificationType.customerSupportNewAnnouncement; + case 'artwork_created': + return NotificationType.artworkCreated; + case 'artwork_received': + return NotificationType.artworkReceived; + case 'new_message': + return NotificationType.newMessage; + case 'jg_artwork_solar_day_arrived': + return NotificationType.jgCrystallineWorkHasArrived; + case 'jg_artwork_generated': + return NotificationType.jgCrystallineWorkGenerated; + case 'exhibition_view_opening': + return NotificationType.exhibitionViewingOpening; + case 'exhibition_sale_opening': + return NotificationType.exhibitionSalesOpening; + case 'exhibition_sale_closing': + return NotificationType.exhibitionSaleClosing; + default: + return NotificationType.unknown; + } + } +} + +class NotificationHandler { + // singleton + static final NotificationHandler instance = NotificationHandler._(); + + NotificationHandler._(); + + final ConfigurationService _configurationService = + injector(); + final RemoteConfigService _remoteConfig = injector(); + final ClientTokenService _clientTokenService = injector(); + final NavigationService _navigationService = injector(); + + Future handleNotificationClicked(BuildContext context, + OSNotification notification, PageController? pageController) async { + if (notification.additionalData == null) { + // Skip handling the notification without data + return; + } + + log.info("Tap to notification: ${notification.body ?? "empty"} " + '\nAdditional data: ${notification.additionalData!}'); + final notificationType = NotificationType.fromString( + notification.additionalData!['notification_type']); + switch (notificationType) { + case NotificationType.galleryNewNft: + Navigator.of(context).popUntil((route) => + route.settings.name == AppRouter.homePage || + route.settings.name == AppRouter.homePageNoTransition); + pageController?.jumpToPage(HomeNavigatorTab.collection.index); + + case NotificationType.customerSupportNewMessage: + case NotificationType.customerSupportCloseIssue: + final issueID = '${notification.additionalData!["issue_id"]}'; + final announcement = await injector() + .findAnnouncementFromIssueId(issueID); + if (!context.mounted) { + return; + } + unawaited(Navigator.of(context).pushNamedAndRemoveUntil( + AppRouter.supportThreadPage, + (route) => + route.settings.name == AppRouter.homePage || + route.settings.name == AppRouter.homePageNoTransition, + arguments: DetailIssuePayload( + reportIssueType: '', + issueID: issueID, + announcement: announcement), + )); + case NotificationType.customerSupportNewAnnouncement: + final announcementID = '${notification.additionalData!["id"]}'; + unawaited(_openAnnouncement(context, announcementID)); + + case NotificationType.artworkCreated: + case NotificationType.artworkReceived: + Navigator.of(context).popUntil((route) => + route.settings.name == AppRouter.homePage || + route.settings.name == AppRouter.homePageNoTransition); + pageController?.jumpToPage(HomeNavigatorTab.collection.index); + case NotificationType.newMessage: + if (!_remoteConfig.getBool(ConfigGroup.viewDetail, ConfigKey.chat)) { + return; + } + final data = notification.additionalData; + if (data == null) { + return; + } + final tokenId = data['group_id']; + final tokens = await injector() + .assetTokenDao + .findAllAssetTokensByTokenIDs([tokenId]); + final owner = tokens.first.owner; + final isSkip = + injector().isConnecting(address: owner, id: tokenId); + if (isSkip) { + return; + } + final GlobalKey key = GlobalKey(); + final postcardDetailPayload = PostcardDetailPagePayload( + [ArtworkIdentity(tokenId, owner)], 0, + key: key); + if (!context.mounted) { + return; + } + unawaited(Navigator.of(context).pushNamed( + AppRouter.claimedPostcardDetailsPage, + arguments: postcardDetailPayload)); + Timer.periodic(const Duration(milliseconds: 100), (timer) async { + final state = key.currentState; + final assetToken = + key.currentContext?.read().state.assetToken; + if (state != null && assetToken != null) { + unawaited(state.gotoChatThread(key.currentContext!)); + timer.cancel(); + } + }); + + case NotificationType.newPostcardTrip: + case NotificationType.postcardShareExpired: + final data = notification.additionalData; + if (data == null) { + return; + } + final indexID = data['indexID']; + final tokens = await injector() + .assetTokenDao + .findAllAssetTokensByTokenIDs([indexID]); + if (tokens.isEmpty) { + return; + } + final owner = tokens.first.owner; + final postcardDetailPayload = PostcardDetailPagePayload( + [ArtworkIdentity(indexID, owner)], + 0, + useIndexer: true, + ); + if (!context.mounted) { + return; + } + Navigator.of(context).popUntil((route) => + route.settings.name == AppRouter.homePage || + route.settings.name == AppRouter.homePageNoTransition); + unawaited(Navigator.of(context).pushNamed( + AppRouter.claimedPostcardDetailsPage, + arguments: postcardDetailPayload)); + + case NotificationType.jgCrystallineWorkHasArrived: + final jgExhibitionId = JohnGerrardHelper.exhibitionID; + await _navigationService + .gotoExhibitionDetailsPage(jgExhibitionId ?? ''); + + case NotificationType.jgCrystallineWorkGenerated: + _navigationService.popUntilHome(); + final data = notification.additionalData; + if (data == null) { + return; + } + final tokenId = data['token_id']; + final indexId = JohnGerrardHelper.getIndexID(tokenId); + await _navigationService.gotoArtworkDetailsPage(indexId); + + case NotificationType.exhibitionViewingOpening: + case NotificationType.exhibitionSalesOpening: + case NotificationType.exhibitionSaleClosing: + final data = notification.additionalData; + if (data == null) { + return; + } + final exhibitionId = data['exhibition_id']; + await _navigationService.gotoExhibitionDetailsPage(exhibitionId); + default: + log.warning('unhandled notification type: $notificationType'); + break; + } + } + + Future shouldShowNotifications(BuildContext context, + OSNotificationReceivedEvent event, PageController? pageController) async { + log.info('Receive notification: ${event.notification}'); + final data = event.notification.additionalData; + if (data == null) { + return; + } + if (_configurationService.isNotificationEnabled() != true) { + _configurationService.showNotifTip.value = true; + } + + final notificationType = + NotificationType.fromString(data['notification_type']); + + // prepare for handling notification + switch (notificationType) { + case NotificationType.customerSupportNewMessage: + case NotificationType.customerSupportCloseIssue: + final notificationIssueID = + '${event.notification.additionalData?['issue_id']}'; + injector().triggerReloadMessages.value += 1; + unawaited( + injector().getIssuesAndAnnouncement()); + if (notificationIssueID == memoryValues.viewingSupportThreadIssueID) { + event.complete(null); + return; + } + + case NotificationType.galleryNewNft: + case NotificationType.newPostcardTrip: + case NotificationType.jgCrystallineWorkGenerated: + case NotificationType.jgCrystallineWorkHasArrived: + unawaited(_clientTokenService.refreshTokens()); + case NotificationType.artworkCreated: + case NotificationType.artworkReceived: + default: + break; + } + + // show notification + switch (notificationType) { + case NotificationType.customerSupportNewAnnouncement: + showInfoNotification( + const Key('Announcement'), 'au_has_announcement'.tr(), + addOnTextSpan: [ + TextSpan( + text: 'tap_to_view'.tr(), + style: Theme.of(context).textTheme.ppMori400FFYellow14), + ], openHandler: () async { + final announcementID = '${data["id"]}'; + unawaited(_openAnnouncement(context, announcementID)); + }); + case NotificationType.newMessage: + final groupId = data['group_id']; + + if (!_remoteConfig.getBool(ConfigGroup.viewDetail, ConfigKey.chat)) { + return; + } + + final currentGroupId = memoryValues.currentGroupChatId; + if (groupId != currentGroupId) { + showNotifications(context, event.notification, + notificationOpenedHandler: (notification) async { + await handleNotificationClicked( + context, notification, pageController); + }); + } + default: + showNotifications(context, event.notification, + notificationOpenedHandler: (notification) async { + await handleNotificationClicked( + context, notification, pageController); + }); + } + event.complete(null); + } + + Future _openAnnouncement( + BuildContext context, String announcementID) async { + log.info('Open announcement: id = $announcementID'); + await injector().fetchAnnouncement(); + final announcement = await injector() + .findAnnouncement(announcementID); + if (announcement != null) { + if (!context.mounted) { + return; + } + unawaited(Navigator.of(context).pushNamedAndRemoveUntil( + AppRouter.supportThreadPage, + (route) => + route.settings.name == AppRouter.homePage || + route.settings.name == AppRouter.homePageNoTransition, + arguments: NewIssuePayload( + reportIssueType: ReportIssueType.Announcement, + announcement: announcement, + ), + )); + } + } +} diff --git a/lib/util/play_control.dart b/lib/util/play_control.dart deleted file mode 100644 index 36df78f58f..0000000000 --- a/lib/util/play_control.dart +++ /dev/null @@ -1,183 +0,0 @@ -import 'package:autonomy_flutter/model/play_control_model.dart'; -import 'package:autonomy_flutter/view/cast_button.dart'; -import 'package:feralfile_app_theme/feral_file_app_theme.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.dart'; - -class PlaylistControl extends StatelessWidget { - final PlayControlModel playControl; - final Function()? onPlayTap; - final Function()? onTimerTap; - final Function()? onShuffleTap; - final Function()? onCastTap; - final bool showPlay; - final bool isCasting; - - const PlaylistControl({ - required this.playControl, - required this.isCasting, - super.key, - this.onPlayTap, - this.onTimerTap, - this.onShuffleTap, - this.showPlay = true, - this.onCastTap, - }); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - return Container( - color: theme.colorScheme.primary, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 15), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ControlItem( - icon: SvgPicture.asset( - 'assets/images/time_off_icon.svg', - width: 24, - colorFilter: - ColorFilter.mode(theme.disableColor, BlendMode.srcIn), - ), - iconFocus: Stack( - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(0, 0, 4, 2), - child: SvgPicture.asset( - 'assets/images/time_off_icon.svg', - width: 24, - colorFilter: ColorFilter.mode( - theme.colorScheme.secondary, BlendMode.srcIn), - ), - ), - Positioned( - bottom: 0, - right: 0, - child: Visibility( - visible: playControl.timer != 0, - child: Container( - padding: const EdgeInsets.fromLTRB(3, 2, 3, 0), - decoration: BoxDecoration( - color: theme.colorScheme.secondary, - borderRadius: BorderRadius.circular(6), - ), - child: Text( - playControl.timer.toString(), - style: TextStyle( - fontFamily: 'PPMori', - color: theme.colorScheme.primary, - fontSize: 8, - height: 1, - fontWeight: FontWeight.w700, - ), - ), - ), - ), - ) - ], - ), - isActive: playControl.timer != 0, - onTap: () { - onTimerTap?.call(); - }, - ), - ControlItem( - icon: SvgPicture.asset( - 'assets/images/shuffle_icon.svg', - width: 24, - colorFilter: - ColorFilter.mode(theme.disableColor, BlendMode.srcIn), - ), - iconFocus: SvgPicture.asset( - 'assets/images/shuffle_icon.svg', - width: 24, - colorFilter: ColorFilter.mode( - theme.colorScheme.secondary, BlendMode.srcIn), - ), - isActive: playControl.isShuffle, - onTap: () { - onShuffleTap?.call(); - }, - ), - if (showPlay) ...[ - ControlItem( - icon: SvgPicture.asset( - 'assets/images/play_icon.svg', - width: 24, - colorFilter: ColorFilter.mode( - theme.colorScheme.secondary, BlendMode.srcIn), - ), - iconFocus: SvgPicture.asset( - 'assets/images/play_icon.svg', - width: 24, - colorFilter: ColorFilter.mode( - theme.colorScheme.secondary, BlendMode.srcIn), - ), - onTap: () { - onPlayTap?.call(); - }, - ), - Padding( - padding: const EdgeInsets.all(8), - child: FFCastButton( - onCastTap: () => {onCastTap?.call()}, - isCasting: isCasting, - ), - ) - ] - ], - ), - ), - ); - } -} - -class ControlItem extends StatefulWidget { - final Widget icon; - final Widget iconFocus; - final bool isActive; - final Function()? onTap; - - const ControlItem({ - required this.icon, - required this.iconFocus, - super.key, - this.isActive = false, - this.onTap, - }); - - @override - State createState() => _ControlItemState(); -} - -class _ControlItemState extends State { - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - return GestureDetector( - onTap: widget.onTap, - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(8), - child: widget.isActive ? widget.iconFocus : widget.icon, - ), - Container( - width: 4, - height: 4, - decoration: widget.isActive - ? BoxDecoration( - shape: BoxShape.circle, - color: theme.colorScheme.secondary, - ) - : null, - ) - ], - ), - ); - } -} diff --git a/lib/util/playlist_ext.dart b/lib/util/playlist_ext.dart new file mode 100644 index 0000000000..8879d318c0 --- /dev/null +++ b/lib/util/playlist_ext.dart @@ -0,0 +1,13 @@ +import 'package:autonomy_flutter/model/play_list_model.dart'; + +extension PlaylistExt on PlayListModel { + String? get displayKey { + final listTokenIds = tokenIDs ?? []; + if (listTokenIds.isEmpty) { + return null; + } + final hashCodes = listTokenIds.map((e) => e.hashCode).toList(); + final hashCode = hashCodes.reduce((value, element) => value ^ element); + return hashCode.toString(); + } +} diff --git a/lib/util/predefined_collection_ext.dart b/lib/util/predefined_collection_ext.dart index 17be7f8463..fb0ad52a91 100644 --- a/lib/util/predefined_collection_ext.dart +++ b/lib/util/predefined_collection_ext.dart @@ -16,5 +16,6 @@ extension PredefinedCollectionModelExt on PredefinedCollectionModel { lastActivityTime: DateTime.now(), lastRefreshedTime: DateTime.now(), galleryThumbnailURL: thumbnailURL, + edition: 0, ); } diff --git a/lib/util/range_input_formatter.dart b/lib/util/range_input_formatter.dart new file mode 100644 index 0000000000..3137c0f5b5 --- /dev/null +++ b/lib/util/range_input_formatter.dart @@ -0,0 +1,24 @@ +import 'package:flutter/services.dart'; + +class RangeTextInputFormatter extends TextInputFormatter { + final int? min; + final int? max; + + RangeTextInputFormatter({ + required this.min, + required this.max, + }); + + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, TextEditingValue newValue) { + final int intValue = int.tryParse(newValue.text) ?? 0; + if (min != null && intValue < min!) { + return TextEditingValue(text: min.toString()); + } + if (max != null && intValue > max!) { + return TextEditingValue(text: max.toString()); + } + return newValue; + } +} diff --git a/lib/util/route_ext.dart b/lib/util/route_ext.dart index e7ee1a48f5..2c5f7224d8 100644 --- a/lib/util/route_ext.dart +++ b/lib/util/route_ext.dart @@ -1,15 +1,10 @@ import 'package:autonomy_flutter/model/connection_request_args.dart'; import 'package:autonomy_flutter/model/ff_exhibition.dart'; -import 'package:autonomy_flutter/model/ff_series.dart'; import 'package:autonomy_flutter/model/play_list_model.dart'; import 'package:autonomy_flutter/model/postcard_claim.dart'; import 'package:autonomy_flutter/screen/app_router.dart'; import 'package:autonomy_flutter/screen/bloc/connections/connections_bloc.dart'; import 'package:autonomy_flutter/screen/chat/chat_thread_page.dart'; -import 'package:autonomy_flutter/screen/claim/activation/claim_activation_page.dart'; -import 'package:autonomy_flutter/screen/claim/airdrop/claim_airdrop_page.dart'; -import 'package:autonomy_flutter/screen/claim/claim_token_page.dart'; -import 'package:autonomy_flutter/screen/claim/select_account_page.dart'; import 'package:autonomy_flutter/screen/cloud/cloud_android_page.dart'; import 'package:autonomy_flutter/screen/cloud/cloud_page.dart'; import 'package:autonomy_flutter/screen/connection/persona_connections_page.dart'; @@ -43,7 +38,6 @@ import 'package:autonomy_flutter/screen/wallet_connect/wc_sign_message_page.dart import 'package:autonomy_flutter/service/wc2_service.dart'; import 'package:autonomy_flutter/util/constants.dart'; import 'package:flutter/material.dart'; -import 'package:nft_collection/models/models.dart'; const unknownMetricTitle = 'Unknown'; @@ -196,24 +190,6 @@ extension RouteExt on Route { MixpanelProp.recipientAddress: payload.address, }; break; - case AppRouter.claimFeralfileTokenPage: - final payload = settings.arguments! as ClaimTokenPagePayload; - data = { - MixpanelProp.seriesId: payload.series.id, - }; - break; - case AppRouter.claimSelectAccountPage: - final payload = settings.arguments! as SelectAddressPagePayload; - data = { - MixpanelProp.type: payload.blockchain, - }; - break; - case AppRouter.airdropTokenDetailPage: - final payload = settings.arguments! as FFSeries; - data = { - MixpanelProp.seriesId: payload.id, - }; - break; case AppRouter.wc2ConnectPage: final payload = settings.arguments! as Wc2Proposal; data = { @@ -296,27 +272,6 @@ extension RouteExt on Route { MixpanelProp.address: payload.sourceAddress, }; break; - case AppRouter.claimAirdropPage: - final payload = settings.arguments! as ClaimAirdropPagePayload; - data = { - MixpanelProp.seriesId: payload.series.id, - }; - break; - case AppRouter.activationTokenDetailPage: - final payload = settings.arguments! as AssetToken; - data = { - MixpanelProp.tokenId: payload.id, - MixpanelProp.ownerAddress: payload.owner, - }; - break; - case AppRouter.claimActivationPage: - final payload = settings.arguments! as ClaimActivationPagePayload; - data = { - MixpanelProp.tokenId: payload.assetToken.id, - MixpanelProp.ownerAddress: payload.assetToken.owner, - MixpanelProp.activationId: payload.activationID, - }; - break; case AppRouter.postcardLocationExplain: final payload = settings.arguments! as PostcardExplainPayload; data = { @@ -441,31 +396,12 @@ extension RouteExt on Route { MixpanelProp.recipientAddress: payload.transaction.to, }; break; - case AppRouter.autonomyAirdropTokenPreviewPage: - final payload = settings.arguments! as FFSeries; - data = { - MixpanelProp.seriesId: payload.id, - }; - break; - case AppRouter.exhibitionNotePage: + case AppRouter.exhibitionCustomNote: final payload = settings.arguments! as Exhibition; data = { MixpanelProp.exhibitionId: payload.id, }; break; - case AppRouter.activationTokenPreviewPage: - final payload = settings.arguments! as AssetToken; - data = { - MixpanelProp.tokenId: payload.id, - MixpanelProp.ownerAddress: payload.owner, - }; - break; - case AppRouter.feralfileAirdropTokenPreviewPage: - final payload = settings.arguments! as FFSeries; - data = { - MixpanelProp.seriesId: payload.id, - }; - break; default: break; } @@ -507,9 +443,6 @@ final screenNameMap = { AppRouter.githubDocPage: 'Github Doc', AppRouter.sendArtworkPage: 'Send Artwork', AppRouter.sendArtworkReviewPage: 'Send Artwork Review', - AppRouter.claimFeralfileTokenPage: 'Claim Feral File Token', - AppRouter.claimSelectAccountPage: 'Claim Select Account', - AppRouter.airdropTokenDetailPage: 'Airdrop Token Detail', AppRouter.wc2ConnectPage: 'WC2 Connect', AppRouter.wc2PermissionPage: 'WC2 Permission', AppRouter.preferencesPage: 'Preferences', @@ -531,9 +464,6 @@ final screenNameMap = { AppRouter.canvasHelpPage: 'Canvas Help', AppRouter.keyboardControlPage: 'Keyboard Control', AppRouter.touchPadPage: 'Touch Pad', - AppRouter.claimAirdropPage: 'Claim Airdrop', - AppRouter.activationTokenDetailPage: 'Activation Token Detail', - AppRouter.claimActivationPage: 'Claim Activation', AppRouter.postcardLeaderboardPage: 'Postcard Leaderboard', AppRouter.postcardLocationExplain: 'Postcard Location Explain', AppRouter.predefinedCollectionPage: 'Predefined Collection', @@ -554,17 +484,15 @@ final screenNameMap = { AppRouter.wcSignMessagePage: 'WC Sign Message', AppRouter.wcSendTransactionPage: 'WC Send Transaction', AppRouter.momaPostcardPage: 'MoMA Postcards', + AppRouter.featuredWorksPage: 'Featured Works', AppRouter.tbSendTransactionPage: 'TB Send Transaction', AppRouter.feralFileSeriesPage: 'Series Detail', AppRouter.ffArtworkPreviewPage: 'Feral File Artwork Preview', AppRouter.exhibitionDetailPage: 'Exhibition Detail', - AppRouter.autonomyAirdropTokenPreviewPage: 'Autonomy Airdrop Token Preview', - AppRouter.exhibitionNotePage: 'Exhibition Note', - AppRouter.activationTokenPreviewPage: 'Activation Token Preview', - AppRouter.feralfileAirdropTokenPreviewPage: - 'Feral File Airdrop Token Preview', AppRouter.previewPrimerPage: 'Preview Primer', AppRouter.projectsList: 'Projects', + AppRouter.artistsListPage: 'Artists list', + AppRouter.exhibitionCustomNote: 'Exhibition Custom Note', }; String getPageName(String routeName) { diff --git a/lib/util/series_ext.dart b/lib/util/series_ext.dart new file mode 100644 index 0000000000..20d8adadcd --- /dev/null +++ b/lib/util/series_ext.dart @@ -0,0 +1,49 @@ +import 'package:autonomy_flutter/common/injector.dart'; +import 'package:autonomy_flutter/model/ff_series.dart'; +import 'package:autonomy_flutter/service/feralfile_service.dart'; +import 'package:autonomy_flutter/service/remote_config_service.dart'; +import 'package:autonomy_flutter/util/exhibition_ext.dart'; +import 'package:autonomy_flutter/util/john_gerrard_helper.dart'; + +extension FFSeriesExt on FFSeries { + String get displayTitle { + final year = mintedAt?.year ?? createdAt?.year; + final isJohnGerrardSeries = JohnGerrardHelper.seriesIDs.contains(id); + return (year != null && !isJohnGerrardSeries) ? '$title ($year)' : title; + } + + bool get isGenerative => + GenerativeMediumTypes.values.any((element) => element.value == medium); + + bool get isMultiUnique => settings?.artworkModel == ArtworkModel.multiUnique; + + bool get isSingle => + settings?.artworkModel == ArtworkModel.single && + settings?.artistReservation == 0 && + settings?.publisherProof == 0 && + settings?.promotionalReservation == 0 && + (settings?.tradeSeries == null || !settings!.tradeSeries!) && + (settings?.transferToCurator == null || !settings!.transferToCurator!); + + bool get shouldFakeArtwork { + final dontFakeArtworkSeriesIds = + injector().getConfig>( + ConfigGroup.exhibition, + ConfigKey.dontFakeArtworkSeriesIds, + [], + ); + return !dontFakeArtworkSeriesIds.contains(id); + } + + String get galleryURL => (metadata?['galleryURL'] ?? '') as String; + + int? get latestRevealedArtworkIndex => + metadata?['latestRevealedArtworkIndex']; + + String get displayKey => exhibition?.displayKey ?? exhibitionID; + + String? get mediumDescription => + metadata != null && metadata!['mediumDescription'] != null + ? (metadata!['mediumDescription'] as List).join('\n') + : null; +} diff --git a/lib/util/string_ext.dart b/lib/util/string_ext.dart index c3cd8ea0a0..fb554595fa 100644 --- a/lib/util/string_ext.dart +++ b/lib/util/string_ext.dart @@ -159,3 +159,70 @@ extension StringExtension on String { return RegExp(r'^[0-9a-fA-F]+$').hasMatch(hexString); } } + +extension SearchKeyExtension on String? { + String get firstSearchCharacter { + if (this == null || this!.isEmpty) { + return '#'; + } + if (listCharacters.contains(this![0].toUpperCase())) { + return this![0].toUpperCase(); + } + return '#'; + } + + int compareSearchKey(String? other) { + final a = this ?? ''; + final b = other ?? ''; + final aFirstCharacter = firstSearchCharacter; + final bFirstCharacter = other.firstSearchCharacter; + if (aFirstCharacter == '#' && bFirstCharacter != '#') { + return 1; + } + if (aFirstCharacter != '#' && bFirstCharacter == '#') { + return -1; + } + return a.toUpperCase().compareTo(b.toUpperCase()); + } +} + +extension ListStringExtension on List { + List rotateListByItem(String item) { + final index = indexOf(item); + if (index == -1) { + return this; + } + final newList = sublist(index)..addAll(sublist(0, index)); + return newList; + } +} + +final List listCharacters = [ + 'A', + 'B', + 'C', + 'D', + 'E', + 'F', + 'G', + 'H', + 'I', + 'J', + 'K', + 'L', + 'M', + 'N', + 'O', + 'P', + 'Q', + 'R', + 'S', + 'T', + 'U', + 'V', + 'W', + 'X', + 'Y', + 'Z', + '#' +]; diff --git a/lib/util/style.dart b/lib/util/style.dart index 9244506ec2..5d2cdcdffa 100644 --- a/lib/util/style.dart +++ b/lib/util/style.dart @@ -542,9 +542,10 @@ class CustomBoxDecoration extends ShapeDecoration { SizedBox addTitleSpace() => const SizedBox(height: 60); -Divider addDivider({double height = 32, Color? color}) => Divider( +Divider addDivider({double height = 32, Color? color, double thickness = 1}) => + Divider( height: height, - thickness: 1, + thickness: thickness, color: color ?? AppColor.secondaryDimGreyBackground, ); diff --git a/lib/util/tezos_beacon_channel.dart b/lib/util/tezos_beacon_channel.dart index e8a9224da3..c31b47528d 100644 --- a/lib/util/tezos_beacon_channel.dart +++ b/lib/util/tezos_beacon_channel.dart @@ -5,6 +5,7 @@ // that can be found in the LICENSE file. // +import 'dart:async'; import 'dart:convert'; import 'package:autonomy_flutter/model/connection_request_args.dart'; @@ -18,7 +19,7 @@ class TezosBeaconChannel { static const EventChannel _eventChannel = EventChannel('tezos_beacon/event'); TezosBeaconChannel({required this.handler}) { - listen(); + unawaited(listen()); } BeaconHandler? handler; @@ -29,7 +30,7 @@ class TezosBeaconChannel { } Future addPeer(String link) async { - final Map res = await _channel.invokeMethod('addPeer', {"link": link}); + final Map res = await _channel.invokeMethod('addPeer', {'link': link}); final peerData = json.decode(res['result']); return P2PPeer.fromJson(peerData); } @@ -46,29 +47,37 @@ class TezosBeaconChannel { Future permissionResponse( String id, String? publicKey, String? address) async { await _channel.invokeMethod( - 'response', {"id": id, "publicKey": publicKey, "address": address}); + 'response', {'id': id, 'publicKey': publicKey, 'address': address}); } Future signResponse(String id, String? signature) async { - await _channel.invokeMethod('response', {"id": id, "signature": signature}); + await _channel.invokeMethod('response', {'id': id, 'signature': signature}); } Future operationResponse(String id, String? txHash) async { - await _channel.invokeMethod('response', {"id": id, "txHash": txHash}); + await _channel.invokeMethod('response', {'id': id, 'txHash': txHash}); } - void listen() async { + Future pause() async { + await _channel.invokeMethod('pause', {}); + } + + Future resume() async { + await _channel.invokeMethod('resume', {}); + } + + Future listen() async { await for (Map event in _eventChannel.receiveBroadcastStream()) { var params = event['params']; switch (event['eventName']) { case 'observeRequest': - final String id = params["id"]; - final String senderID = params["senderID"]; - final String version = params["version"]; - final String originID = params["originID"]; - final String type = params["type"]; - final String? appName = params["appName"]; - final String? icon = params["icon"]; + final String id = params['id']; + final String senderID = params['senderID']; + final String version = params['version']; + final String originID = params['originID']; + final String type = params['type']; + final String? appName = params['appName']; + final String? icon = params['icon']; final request = BeaconRequest( id, @@ -80,27 +89,26 @@ class TezosBeaconChannel { icon: icon, ); switch (type) { - case "signPayload": - final String? payload = params["payload"]; - final String? sourceAddress = params["sourceAddress"]; + case 'signPayload': + final String? payload = params['payload']; + final String? sourceAddress = params['sourceAddress']; request.payload = payload; request.sourceAddress = sourceAddress; - break; - case "operation": - final List operationsDetails = params["operationDetails"]; - final String? sourceAddress = params["sourceAddress"]; + case 'operation': + final List operationsDetails = params['operationDetails']; + final String? sourceAddress = params['sourceAddress']; List operations = []; for (var element in operationsDetails) { - final String? kind = element["kind"]; - final String? storageLimit = element["storageLimit"]; - final String? gasLimit = element["gasLimit"]; - final String? fee = element["fee"]; + final String? kind = element['kind']; + final String? storageLimit = element['storageLimit']; + final String? gasLimit = element['gasLimit']; + final String? fee = element['fee']; - if (kind == "origination") { - final String balance = element["balance"] ?? "0"; - final List code = element["code"]; - final dynamic storage = element["storage"]; + if (kind == 'origination') { + final String balance = element['balance'] ?? '0'; + final List code = element['code']; + final dynamic storage = element['storage']; final operation = OriginationOperation( balance: int.parse(balance), @@ -115,11 +123,11 @@ class TezosBeaconChannel { ); operations.add(operation); } else { - final String destination = element["destination"] ?? ""; - final String amount = element["amount"] ?? "0"; - final String? entrypoint = element["entrypoint"]; - final dynamic parameters = element["parameters"] != null - ? json.decode(json.encode(element["parameters"])) + final String destination = element['destination'] ?? ''; + final String amount = element['amount'] ?? '0'; + final String? entrypoint = element['entrypoint']; + final dynamic parameters = element['parameters'] != null + ? json.decode(json.encode(element['parameters'])) : null; final operation = TransactionOperation( @@ -140,23 +148,19 @@ class TezosBeaconChannel { request.operations = operations; request.sourceAddress = sourceAddress; - break; } handler!.onRequest(request); - break; - case "observeEvent": - switch (params["type"]) { - case "beaconRequestedPermission": - final Uint8List data = params["peer"]; + case 'observeEvent': + switch (params['type']) { + case 'beaconRequestedPermission': + final Uint8List data = params['peer']; Peer peer = Peer.fromJson(json.decode(utf8.decode(data))); handler!.onRequestedPermission(peer); + case 'error': break; - case "error": - break; - case "userAborted": + case 'userAborted': handler!.onAbort(); - break; } } } diff --git a/lib/util/ui_helper.dart b/lib/util/ui_helper.dart index 1bd3988526..fcbbb4a6f1 100644 --- a/lib/util/ui_helper.dart +++ b/lib/util/ui_helper.dart @@ -14,8 +14,6 @@ import 'dart:convert'; import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/database/entity/connection.dart'; import 'package:autonomy_flutter/model/connection_request_args.dart'; -import 'package:autonomy_flutter/model/ff_account.dart'; -import 'package:autonomy_flutter/model/ff_series.dart'; import 'package:autonomy_flutter/screen/app_router.dart'; import 'package:autonomy_flutter/screen/customer_support/support_thread_page.dart'; import 'package:autonomy_flutter/service/configuration_service.dart'; @@ -24,10 +22,8 @@ import 'package:autonomy_flutter/service/metric_client_service.dart'; import 'package:autonomy_flutter/service/navigation_service.dart'; import 'package:autonomy_flutter/util/au_icons.dart'; import 'package:autonomy_flutter/util/constants.dart'; -import 'package:autonomy_flutter/util/custom_exception.dart'; import 'package:autonomy_flutter/util/distance_formater.dart'; import 'package:autonomy_flutter/util/error_handler.dart'; -import 'package:autonomy_flutter/util/feralfile_extension.dart'; import 'package:autonomy_flutter/util/log.dart'; import 'package:autonomy_flutter/util/moma_style_color.dart'; import 'package:autonomy_flutter/util/notification_util.dart'; @@ -76,7 +72,6 @@ Future doneOnboarding(BuildContext context) async { void nameContinue(BuildContext context) { if (injector().isDoneOnboarding()) { Navigator.of(context).popUntil((route) => - route.settings.name == AppRouter.claimSelectAccountPage || route.settings.name == AppRouter.tbConnectPage || route.settings.name == AppRouter.wc2ConnectPage || route.settings.name == AppRouter.homePage || @@ -108,6 +103,7 @@ Future askForNotification() async { class UIHelper { static String currentDialogTitle = ''; static final metricClient = injector.get(); + static const String ignoreBackLayerPopUpRouteName = 'popUp.ignoreBackLayer'; static Future showDialog( BuildContext context, @@ -120,6 +116,7 @@ class UIHelper { FeedbackType? feedback = FeedbackType.selection, EdgeInsets? padding, EdgeInsets? paddingTitle, + bool withCloseIcon = false, }) async { log.info('[UIHelper] showDialog: $title'); currentDialogTitle = title; @@ -145,6 +142,7 @@ class UIHelper { : Constants.maxWidthModalTablet), isScrollControlled: true, barrierColor: Colors.black.withOpacity(0.5), + routeSettings: const RouteSettings(name: ignoreBackLayerPopUpRouteName), builder: (context) => Container( color: Colors.transparent, child: ClipPath( @@ -167,8 +165,25 @@ class UIHelper { children: [ Padding( padding: paddingTitle ?? const EdgeInsets.all(0), - child: Text(title, - style: theme.primaryTextTheme.ppMori700White24), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(title, + style: theme.primaryTextTheme.ppMori700White24), + if (withCloseIcon) + Padding( + padding: const EdgeInsets.only(left: 10), + child: GestureDetector( + onTap: () => hideInfoDialog(context), + child: SvgPicture.asset( + 'assets/images/circle_close.svg', + width: 22, + height: 22, + ), + ), + ) + ], + ), ), const SizedBox(height: 40), content, @@ -507,6 +522,54 @@ class UIHelper { ); } + static Future showRetryDialog(BuildContext context, + {required String description, + FutureOr Function()? onRetry, + ValueNotifier? dynamicRetryNotifier}) async { + final theme = Theme.of(context); + final hasRetry = onRetry != null; + return await showDialog( + context, + 'network_issue'.tr(), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (description.isNotEmpty) ...[ + Text( + description, + style: theme.primaryTextTheme.ppMori400White14, + ), + ], + const SizedBox(height: 40), + if (hasRetry) ...[ + ValueListenableBuilder( + valueListenable: dynamicRetryNotifier ?? ValueNotifier(true), + builder: (context, value, child) => value + ? Column( + children: [ + PrimaryButton( + onTap: () { + hideDialogWithResult>( + context, onRetry()); + }, + text: 'retry_now'.tr(), + color: AppColor.feralFileLightBlue, + ), + const SizedBox(height: 15), + ], + ) + : const SizedBox.shrink(), + ), + ], + OutlineButton( + onTap: () => hideInfoDialog(context), + text: 'dismiss'.tr(), + ), + ], + ), + ); + } + static Future showFlexibleDialog( BuildContext context, Widget content, { @@ -538,23 +601,20 @@ class UIHelper { : Constants.maxWidthModalTablet), isScrollControlled: true, barrierColor: Colors.black.withOpacity(0.5), - builder: (context) => Container( - color: Colors.transparent, - padding: const EdgeInsets.only(top: 200), - child: ClipPath( - clipper: isRoundCorner ? null : AutonomyTopRightRectangleClipper(), - child: Container( - padding: const EdgeInsets.fromLTRB(0, 20, 0, 40), - decoration: BoxDecoration( - color: backgroundColor ?? theme.auGreyBackground, - borderRadius: isRoundCorner - ? const BorderRadius.only( - topRight: Radius.circular(20), - ) - : null, - ), - child: content, + routeSettings: const RouteSettings(name: ignoreBackLayerPopUpRouteName), + builder: (context) => ClipPath( + clipper: isRoundCorner ? null : AutonomyTopRightRectangleClipper(), + child: Container( + padding: const EdgeInsets.fromLTRB(0, 20, 0, 40), + decoration: BoxDecoration( + color: backgroundColor ?? theme.auGreyBackground, + borderRadius: isRoundCorner + ? const BorderRadius.only( + topRight: Radius.circular(20), + ) + : null, ), + child: content, ), ), ); @@ -779,163 +839,17 @@ class UIHelper { static void hideInfoDialog(BuildContext context) { currentDialogTitle = ''; try { - Navigator.popUntil(context, (route) => route.settings.name != null); + Navigator.popUntil( + context, + (route) => + route.settings.name != null && + !route.settings.name!.toLowerCase().contains('popup')); } catch (_) {} } - static Future showAirdropNotStarted( - BuildContext context, String? artworkId) async { - final theme = Theme.of(context); - final error = FeralfileError(5006, ''); - return UIHelper.showDialog( - context, - error.dialogTitle, - Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - error.dialogMessage, - style: theme.primaryTextTheme.ppMori400White14, - ), - const SizedBox( - height: 40, - ), - OutlineButton( - text: 'close'.tr(), - onTap: () { - Navigator.of(context).pop(); - }, - ), - ], - ), - isDismissible: true, - ); - } - - static Future showAirdropExpired( - BuildContext context, String? artworkId) async { - final theme = Theme.of(context); - final error = FeralfileError(3007, ''); - return UIHelper.showDialog( - context, - error.dialogTitle, - Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - error.dialogMessage, - style: theme.primaryTextTheme.bodyLarge, - ), - const SizedBox( - height: 40, - ), - OutlineButton( - text: 'close'.tr(), - onTap: () { - Navigator.of(context).pop(); - }, - ), - ], - ), - isDismissible: true, - ); - } - - static Future showNoRemainingAirdropToken( - BuildContext context, { - required FFSeries series, - }) async { - final error = FeralfileError(3009, ''); - return showErrorDialog( - context, - error.getDialogTitle(), - error.getDialogMessage(series: series), - 'close'.tr(), - ); - } - - static Future showNoRemainingActivationToken( - BuildContext context, { - required String id, - }) async { - final error = FeralfileError(3009, ''); - return showErrorDialog( - context, - error.getDialogTitle(), - error.getDialogMessage(), - 'close'.tr(), - ); - } - - static Future showOtpExpired(BuildContext context, String? artworkId) async { - final error = FeralfileError(3013, ''); - return showErrorDialog( - context, - error.dialogTitle, - error.dialogMessage, - 'close'.tr(), - ); - } - - static Future showClaimTokenError( - BuildContext context, - Object e, { - required FFSeries series, - }) async { - if (e is AirdropExpired) { - await showAirdropExpired(context, series.id); - } else if (e is DioException) { - final ffError = e.error as FeralfileError?; - final message = ffError != null - ? ffError.getDialogMessage(series: series) - : '${e.response?.data ?? e.message}'; - - await showErrorDialog( - context, - ffError?.getDialogTitle() ?? 'error'.tr(), - message, - 'close'.tr(), - ); - } else if (e is NoRemainingToken) { - await showNoRemainingAirdropToken( - context, - series: series, - ); - } - } - - static Future showFeralFileClaimTokenPassLimit(BuildContext context, - {required FFSeries series}) async { - final message = 'all_gifts_claimed_desc'.tr(); - final dialogTitle = 'all_gifts_claimed'.tr(); - - await showErrorDialog( - context, - dialogTitle, - message, - 'close'.tr(), - ); - } - - static Future showActivationError( - BuildContext context, Object e, String id) async { - if (e is AirdropExpired) { - await showAirdropExpired(context, id); - } else if (e is DioException) { - final ffError = e.error as FeralfileError?; - final message = ffError != null - ? ffError.dialogMessage - : '${e.response?.data ?? e.message}'; - - await showErrorDialog( - context, - ffError?.dialogMessage ?? 'error'.tr(), - message, - 'close'.tr(), - ); - } else if (e is NoRemainingToken) { - await showNoRemainingActivationToken(context, id: id); - } + static void hideDialogWithResult(BuildContext context, T result) { + currentDialogTitle = ''; + Navigator.pop(context, result); } static Future showAppReportBottomSheet( @@ -1355,8 +1269,6 @@ class UIHelper { static Future showDrawerAction(BuildContext context, {required List options}) async { - final theme = Theme.of(context); - await showModalBottomSheet( context: context, backgroundColor: Colors.transparent, @@ -1367,8 +1279,9 @@ class UIHelper { : Constants.maxWidthModalTablet), barrierColor: Colors.black.withOpacity(0.5), isScrollControlled: true, + routeSettings: const RouteSettings(name: ignoreBackLayerPopUpRouteName), builder: (context) => Container( - color: AppColor.feralFileHighlight, + color: AppColor.auGreyBackground, child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -1379,7 +1292,7 @@ class UIHelper { icon: const Icon( AuIcon.close, size: 18, - color: AppColor.secondaryDimGrey, + color: AppColor.white, ), ), ), @@ -1391,13 +1304,16 @@ class UIHelper { if (option.builder != null) { return option.builder!.call(context, option); } - return DrawerItem(item: option); + return DrawerItem( + item: option, + color: AppColor.white, + ); }, itemCount: options.length, - separatorBuilder: (context, index) => Divider( + separatorBuilder: (context, index) => const Divider( height: 1, thickness: 1, - color: theme.colorScheme.secondary, + color: AppColor.primaryBlack, ), ), ], @@ -1582,22 +1498,6 @@ class UIHelper { } } - static Future showAirdropClaimFailed(BuildContext context) async => - await showErrorDialog( - context, 'airdrop_claim_failed'.tr(), '', 'close'.tr()); - - static Future showAirdropAlreadyClaim(BuildContext context) async => - await showErrorDialog(context, 'already_claimed'.tr(), - 'already_claimed_desc'.tr(), 'close'.tr()); - - static Future showAirdropJustOnce(BuildContext context) async => - await showErrorDialog( - context, 'just_once'.tr(), 'just_once_desc'.tr(), 'close'.tr()); - - static Future showAirdropCannotShare(BuildContext context) async => - await showErrorDialog(context, 'already_claimed'.tr(), - 'cannot_share_aridrop_desc'.tr(), 'close'.tr()); - static Future showPostcardShareLinkExpired(BuildContext context) async { await UIHelper.showDialog( context, @@ -1880,6 +1780,11 @@ class UIHelper { await _showPostcardError(context, message: '_save_failed'.tr(args: [title]), icon: SvgPicture.asset('assets/images/exit.svg')); + + static Future showConnectFailed(BuildContext context, + {required String message}) async => + await showErrorDialog( + context, 'connect_failed'.tr(), message, 'close'.tr()); } Widget loadingScreen(ThemeData theme, String text) => Scaffold( @@ -1932,4 +1837,6 @@ class OptionItem { this.builder, this.separator, }); + + static OptionItem emptyOptionItem = OptionItem(title: ''); } diff --git a/lib/util/wallet_storage_ext.dart b/lib/util/wallet_storage_ext.dart index e72175aec1..b20e4132e0 100644 --- a/lib/util/wallet_storage_ext.dart +++ b/lib/util/wallet_storage_ext.dart @@ -27,34 +27,27 @@ extension StringExtension on WalletStorage { if (address.isNotEmpty) { return EthereumAddress.fromHex(address).hexEip55; } else { - return ""; + return ''; } } } extension StringHelper on String { - String getETHEip55Address() { - return EthereumAddress.fromHex(this).hexEip55; - } + String getETHEip55Address() => EthereumAddress.fromHex(this).hexEip55; - String publicKeyToTezosAddress() { - return crypto.addressFromPublicKey(this); - } + String publicKeyToTezosAddress() => crypto.addressFromPublicKey(this); } extension WalletStorageExtension on WalletStorage { - int getOwnedQuantity(AssetToken token) { - return token.getCurrentBalance ?? 0; - } + int getOwnedQuantity(AssetToken token) => token.getCurrentBalance ?? 0; Future getTezosAddress({int index = 0}) async { final publicKey = await getTezosPublicKey(index: index); return crypto.addressFromPublicKey(publicKey); } - getTezosAddressFromPubKey(String publicKey) { - return crypto.addressFromPublicKey(publicKey); - } + String getTezosAddressFromPubKey(String publicKey) => + crypto.addressFromPublicKey(publicKey); } class WalletIndex { @@ -79,7 +72,7 @@ extension WalletIndexExtension on WalletIndex { case Wc2Chain.autonomy: return await wallet.getAccountDIDSignature(message); } - throw Exception("Unsupported chain $chain"); + throw Exception('Unsupported chain $chain'); } Future signPermissionRequest({ @@ -87,14 +80,14 @@ extension WalletIndexExtension on WalletIndex { required String message, }) async { switch (chain.caip2Namespace) { - case "eip155": + case 'eip155': final ethAddress = await wallet.getETHEip55Address(index: index); return Wc2Chain( chain: chain, address: ethAddress, signature: await signMessage(chain: chain, message: message), ); - case "tezos": + case 'tezos': final tezosAddress = await wallet.getTezosAddress(index: index); return Wc2Chain( chain: chain, diff --git a/lib/view/account_view.dart b/lib/view/account_view.dart index 797793f1d1..dbd3759aea 100644 --- a/lib/view/account_view.dart +++ b/lib/view/account_view.dart @@ -17,7 +17,6 @@ import 'package:autonomy_flutter/service/tezos_service.dart'; import 'package:autonomy_flutter/util/account_ext.dart'; import 'package:autonomy_flutter/util/constants.dart'; import 'package:autonomy_flutter/util/eth_amount_formatter.dart'; -import 'package:autonomy_flutter/util/string_ext.dart'; import 'package:autonomy_flutter/util/style.dart'; import 'package:autonomy_flutter/util/xtz_utils.dart'; import 'package:autonomy_flutter/view/crypto_view.dart'; @@ -83,14 +82,17 @@ Widget accountItem(BuildContext context, Account account, child: Row(children: [ LogoCrypto(cryptoType: account.cryptoType, size: 24), const SizedBox(width: 10), - Text( - account.name.maskIfNeeded(), - style: theme.textTheme.ppMori700Black16, + Expanded( + child: Text( + account.name, + style: theme.textTheme.ppMori700Black16, + overflow: TextOverflow.ellipsis, + ), ), - const Expanded(child: SizedBox()), ]), ), if (account.isHidden) ...[ + const SizedBox(width: 10), SvgPicture.asset( 'assets/images/hide.svg', colorFilter: ColorFilter.mode( diff --git a/lib/view/artwork_common_widget.dart b/lib/view/artwork_common_widget.dart index 60cffc2461..37adbbc25a 100644 --- a/lib/view/artwork_common_widget.dart +++ b/lib/view/artwork_common_widget.dart @@ -3,29 +3,32 @@ import 'dart:collection'; import 'dart:math'; import 'package:after_layout/after_layout.dart'; -import 'package:autonomy_flutter/common/environment.dart'; import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/model/ff_account.dart'; +import 'package:autonomy_flutter/model/ff_artwork.dart'; import 'package:autonomy_flutter/model/ff_exhibition.dart'; -import 'package:autonomy_flutter/model/ff_series.dart'; import 'package:autonomy_flutter/screen/detail/royalty/royalty_bloc.dart'; import 'package:autonomy_flutter/service/configuration_service.dart'; import 'package:autonomy_flutter/service/feralfile_service.dart'; import 'package:autonomy_flutter/service/metric_client_service.dart'; import 'package:autonomy_flutter/service/navigation_service.dart'; +import 'package:autonomy_flutter/service/network_issue_manager.dart'; import 'package:autonomy_flutter/util/address_utils.dart'; import 'package:autonomy_flutter/util/asset_token_ext.dart'; import 'package:autonomy_flutter/util/au_icons.dart'; import 'package:autonomy_flutter/util/constants.dart'; import 'package:autonomy_flutter/util/datetime_ext.dart'; import 'package:autonomy_flutter/util/dio_util.dart'; -import 'package:autonomy_flutter/util/feralfile_extension.dart'; +import 'package:autonomy_flutter/util/exception_ext.dart'; +import 'package:autonomy_flutter/util/exhibition_ext.dart'; +import 'package:autonomy_flutter/util/feral_file_helper.dart'; +import 'package:autonomy_flutter/util/image_ext.dart'; import 'package:autonomy_flutter/util/moma_style_color.dart'; +import 'package:autonomy_flutter/util/series_ext.dart'; import 'package:autonomy_flutter/util/string_ext.dart'; import 'package:autonomy_flutter/util/style.dart'; import 'package:autonomy_flutter/util/ui_helper.dart'; -import 'package:autonomy_flutter/view/responsive.dart'; -import 'package:cached_network_image/cached_network_image.dart'; +import 'package:autonomy_flutter/view/loading.dart'; import 'package:collection/collection.dart'; import 'package:dio/dio.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -39,7 +42,6 @@ import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_vibrate/flutter_vibrate.dart'; -import 'package:gif_view/gif_view.dart'; import 'package:nft_collection/models/asset_token.dart'; import 'package:nft_collection/models/provenance.dart'; import 'package:nft_rendering/nft_rendering.dart'; @@ -94,8 +96,13 @@ class MintTokenWidget extends StatelessWidget { class PendingTokenWidget extends StatelessWidget { final String? thumbnail; final String? tokenId; + final bool shouldRefreshCache; - const PendingTokenWidget({super.key, this.thumbnail, this.tokenId}); + const PendingTokenWidget( + {super.key, + this.thumbnail, + this.tokenId, + this.shouldRefreshCache = false}); @override Widget build(BuildContext context) { @@ -111,9 +118,10 @@ class PendingTokenWidget extends StatelessWidget { SizedBox( width: double.infinity, height: double.infinity, - child: CachedNetworkImage( - imageUrl: thumbnail!, + child: ImageExt.customNetwork( + thumbnail!, fit: BoxFit.cover, + shouldRefreshCache: shouldRefreshCache, ), ) ] else ...[ @@ -152,8 +160,10 @@ Widget tokenGalleryThumbnailWidget( bool useHero = true, Widget? galleryThumbnailPlaceholder, }) { + ///hardcode for JG + final isJohnGerrard = token.isJohnGerrardArtwork; final thumbnailUrl = token.getGalleryThumbnailUrl( - usingThumbnailID: usingThumbnailID, variant: variant); + usingThumbnailID: usingThumbnailID && !isJohnGerrard, variant: variant); if (thumbnailUrl == null || thumbnailUrl.isEmpty) { return GalleryNoThumbnailWidget( @@ -161,22 +171,22 @@ Widget tokenGalleryThumbnailWidget( ); } - final ext = p.extension(thumbnailUrl); - final cacheManager = injector(); Future cachingState = _cachingStates[thumbnailUrl] ?? // ignore: discarded_futures cacheManager.store.retrieveCacheData(thumbnailUrl).then((cachedObject) { - final cached = cachedObject != null; - if (cached) { + final isCached = cachedObject != null; + if (isCached) { _cachingStates[thumbnailUrl] = Future.value(true); } - return cached; + return isCached; }); final memCacheWidth = cachedImageSize; final memCacheHeight = memCacheWidth ~/ ratio; + final ext = p.extension(thumbnailUrl); + final shouldRefreshCache = token.shouldRefreshThumbnailCache; return Semantics( label: 'gallery_artwork_${token.id}', child: Hero( @@ -192,8 +202,8 @@ Widget tokenGalleryThumbnailWidget( unsupportWidgetBuilder: (context) => const GalleryUnSupportThumbnailWidget(), ) - : CachedNetworkImage( - imageUrl: thumbnailUrl, + : ImageExt.customNetwork( + thumbnailUrl, fadeInDuration: Duration.zero, fit: BoxFit.cover, memCacheHeight: memCacheHeight, @@ -208,26 +218,32 @@ Widget tokenGalleryThumbnailWidget( GalleryThumbnailPlaceholder( loading: !(snapshot.data ?? true), )), - errorWidget: (context, url, error) => CachedNetworkImage( - imageUrl: - token.getGalleryThumbnailUrl(usingThumbnailID: false) ?? '', - fadeInDuration: Duration.zero, - fit: BoxFit.cover, - memCacheHeight: cachedImageSize, - memCacheWidth: cachedImageSize, - maxWidthDiskCache: cachedImageSize, - maxHeightDiskCache: cachedImageSize, - cacheManager: cacheManager, - placeholder: (context, index) => FutureBuilder( - future: cachingState, - builder: (context, snapshot) => - galleryThumbnailPlaceholder ?? - GalleryThumbnailPlaceholder( - loading: !(snapshot.data ?? true), - )), - errorWidget: (context, url, error) => - const GalleryThumbnailErrorWidget(), - ), + errorWidget: (context, url, error) { + if (error is Exception && error.isNetworkIssue) { + unawaited(injector() + .showNetworkIssueWarning()); + } + return ImageExt.customNetwork( + token.getGalleryThumbnailUrl(usingThumbnailID: false) ?? '', + fadeInDuration: Duration.zero, + fit: BoxFit.cover, + memCacheHeight: cachedImageSize, + memCacheWidth: cachedImageSize, + maxWidthDiskCache: cachedImageSize, + maxHeightDiskCache: cachedImageSize, + cacheManager: cacheManager, + placeholder: (context, index) => FutureBuilder( + future: cachingState, + builder: (context, snapshot) => + galleryThumbnailPlaceholder ?? + GalleryThumbnailPlaceholder( + loading: !(snapshot.data ?? true), + )), + errorWidget: (context, url, error) => + const GalleryThumbnailErrorWidget(), + ); + }, + shouldRefreshCache: shouldRefreshCache, ), ), ); @@ -395,36 +411,7 @@ class GalleryThumbnailPlaceholder extends StatelessWidget { } } -Widget placeholder(BuildContext context) { - final theme = Theme.of(context); - return AspectRatio( - aspectRatio: 1, - child: Container( - color: AppColor.primaryBlack, - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - GifView.asset( - 'assets/images/loading_white.gif', - height: 52, - frameRate: 12, - ), - const SizedBox( - height: 12, - ), - Text( - 'loading...'.tr(), - style: ResponsiveLayout.isMobile - ? theme.textTheme.ppMori400White12 - : theme.textTheme.ppMori400White14, - ), - ], - ), - ), - ), - ); -} +Widget placeholder(BuildContext context) => const LoadingWidget(); INFTRenderingWidget buildRenderingWidget( BuildContext context, @@ -444,9 +431,8 @@ INFTRenderingWidget buildRenderingWidget( ? assetToken.getPreviewUrl() : '${assetToken.getPreviewUrl()}?t=$attempt', thumbnailURL: assetToken.getGalleryThumbnailUrl(usingThumbnailID: false), - loadingWidget: loadingWidget ?? previewPlaceholder(context), + loadingWidget: loadingWidget ?? previewPlaceholder(), errorWidget: BrokenTokenWidget(token: assetToken), - cacheManager: injector(), onLoaded: onLoaded, onDispose: onDispose, overriddenHtml: overriddenHtml, @@ -476,8 +462,7 @@ INFTRenderingWidget buildFeralfileRenderingWidget( ..setRenderWidgetBuilder(RenderingWidgetBuilder( previewURL: attempt == null ? previewURL : '$previewURL?t=$attempt', thumbnailURL: thumbnailURL, - loadingWidget: loadingWidget ?? previewPlaceholder(context), - cacheManager: injector(), + loadingWidget: loadingWidget ?? previewPlaceholder(), onLoaded: onLoaded, onDispose: onDispose, overriddenHtml: overriddenHtml, @@ -652,7 +637,7 @@ class _CurrentlyCastingArtworkState extends State { } } -Widget previewPlaceholder(BuildContext context) => const PreviewPlaceholder(); +Widget previewPlaceholder() => const PreviewPlaceholder(); class PreviewPlaceholder extends StatefulWidget { const PreviewPlaceholder({ @@ -663,47 +648,9 @@ class PreviewPlaceholder extends StatefulWidget { State createState() => _PreviewPlaceholderState(); } -class _PreviewPlaceholderState extends State - with AfterLayoutMixin { - final metricClient = injector.get(); - - @override - void dispose() { - super.dispose(); - } - +class _PreviewPlaceholderState extends State { @override - void afterFirstLayout(BuildContext context) {} - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - return Center( - child: AspectRatio( - aspectRatio: 1, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - GifView.asset( - 'assets/images/loading_white.gif', - height: 52, - frameRate: 12, - ), - const SizedBox( - height: 13, - ), - Text( - 'loading...'.tr(), - style: ResponsiveLayout.isMobile - ? theme.textTheme.ppMori400White12 - : theme.textTheme.ppMori400White14, - ), - ], - ), - ), - ); - } + Widget build(BuildContext context) => const LoadingWidget(); } Widget debugInfoWidget(BuildContext context, AssetToken? token) { @@ -984,7 +931,7 @@ Widget postcardDetailsMetadataSection( child: MetaDataItem( title: 'title'.tr(), titleStyle: titleStyle, - value: assetToken.title ?? '', + value: assetToken.displayTitle ?? '', valueStyle: theme.textTheme.moMASans400Black12, ), ), @@ -1103,9 +1050,106 @@ Widget postcardDetailsMetadataSection( ); } +class ArtworkAttributesText extends StatelessWidget { + final Artwork artwork; + + const ArtworkAttributesText({required this.artwork, super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Text( + artwork.attributesString ?? '', + style: theme.textTheme.ppMori400FFQuickSilver12.copyWith( + color: AppColor.feralFileMediumGrey, + ), + ); + } +} + +class FFArtworkDetailsMetadataSection extends StatelessWidget { + final Artwork artwork; + + const FFArtworkDetailsMetadataSection({required this.artwork, super.key}); + + @override + Widget build(BuildContext context) { + const divider = artworkDataDivider; + final contract = artwork.getContract(artwork.series!.exhibition); + return SectionExpandedWidget( + header: 'metadata'.tr(), + padding: const EdgeInsets.only(bottom: 23), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MetaDataItem( + title: 'title'.tr(), + value: artwork.series!.displayTitle, + ), + if (artwork.series!.artist?.alias != null) ...[ + divider, + MetaDataItem( + title: 'artist'.tr(), + value: artwork.series!.artist!.alias, + tapLink: + FeralFileHelper.getArtistUrl(artwork.series!.artist!.alias), + forceSafariVC: true, + ), + ], + if (!artwork.isYokoOnoPublicVersion) ...[ + divider, + MetaDataItem( + title: 'edition'.tr(), + value: artwork.name, + ), + ], + divider, + MetaDataItem( + title: 'token'.tr(), + value: polishSource('feralfile'), + tapLink: feralFileArtworkUrl(artwork.id), + forceSafariVC: true, + ), + if (artwork.series!.exhibition != null) ...[ + divider, + MetaDataItem( + title: 'exhibition'.tr(), + value: artwork.series!.exhibition!.title, + tapLink: feralFileExhibitionUrl(artwork.series!.exhibition!.slug), + forceSafariVC: true, + ), + ], + divider, + MetaDataItem( + title: 'medium'.tr(), + value: artwork.series!.medium.capitalize(), + ), + if (contract != null) ...[ + divider, + MetaDataItem( + title: 'contract'.tr(), + value: contract.blockchainType.capitalize(), + tapLink: contract.getBlockchainUrl(), + forceSafariVC: true, + ) + ], + if (artwork.mintedAt != null) ...[ + divider, + MetaDataItem( + title: 'date_minted'.tr(), + value: localTimeString(artwork.mintedAt!)), + ], + const SizedBox( + height: 32, + ), + ], + ), + ); + } +} + Widget artworkDetailsMetadataSection( BuildContext context, AssetToken assetToken, String? artistName) { - final theme = Theme.of(context); final artworkID = ((assetToken.swapped ?? false) && assetToken.originTokenInfoId != null) ? assetToken.originTokenInfoId ?? '' @@ -1119,7 +1163,7 @@ Widget artworkDetailsMetadataSection( children: [ MetaDataItem( title: 'title'.tr(), - value: assetToken.title ?? '', + value: assetToken.displayTitle ?? '', ), if (artistName != null) ...[ divider, @@ -1812,198 +1856,6 @@ class _ArtworkRightsViewState extends State { }); } -Widget _rowItem( - BuildContext context, - String name, - String? value, { - String? subTitle, - Function()? onNameTap, - String? tapLink, - bool? forceSafariVC, - Function()? onValueTap, - Widget? title, - int maxLines = 2, -}) { - if (onValueTap == null && tapLink != null) { - final uri = Uri.parse(tapLink); - onValueTap = () => unawaited(launchUrl(uri, - mode: forceSafariVC == true - ? LaunchMode.externalApplication - : LaunchMode.platformDefault)); - } - final theme = Theme.of(context); - - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - flex: 3, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - onTap: onNameTap, - child: - title ?? Text(name, style: theme.textTheme.ppMori400White12), - ), - if (subTitle != null) ...[ - const SizedBox(height: 2), - Text( - subTitle, - style: ResponsiveLayout.isMobile - ? theme.textTheme.ppMori400White12 - : theme.textTheme.ppMori400White14, - ), - ] - ], - ), - ), - Flexible( - flex: 4, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Expanded( - child: GestureDetector( - onTap: onValueTap, - child: Semantics( - label: name, - child: Text( - value ?? '', - textAlign: TextAlign.end, - maxLines: maxLines, - softWrap: true, - overflow: TextOverflow.ellipsis, - style: onValueTap != null - ? theme.textTheme.ppMori400White12 - : ResponsiveLayout.isMobile - ? theme.textTheme.ppMori400White12 - : theme.textTheme.ppMori400White12, - ), - ), - ), - ), - if (onValueTap != null) ...[ - const SizedBox(width: 8), - SvgPicture.asset( - 'assets/images/iconForward.svg', - colorFilter: ColorFilter.mode( - theme.textTheme.ppMori400White12.color ?? - AppColor.primaryBlack, - BlendMode.srcIn), - ), - ] - ], - ), - ) - ], - ); -} - -class ArtworkRightWidget extends StatelessWidget { - final FFContract? contract; - final String? exhibitionID; - - const ArtworkRightWidget( - {required this.contract, super.key, this.exhibitionID}); - - @override - Widget build(BuildContext context) { - final linkStyle = Theme.of(context).primaryTextTheme.linkStyle.copyWith( - color: Colors.white, - decorationColor: Colors.white, - ); - return ArtworkRightsView( - linkStyle: linkStyle, - contract: FFContract('', '', ''), - exhibitionID: exhibitionID, - ); - } -} - -class FeralfileArtworkDetailsMetadataSection extends StatelessWidget { - final FFSeries series; - - const FeralfileArtworkDetailsMetadataSection({ - required this.series, - super.key, - }); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final artist = series.artist; - final contract = series.contract; - final df = DateFormat('yyyy-MMM-dd hh:mm'); - final mintDate = series.createdAt; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'metadata'.tr(), - style: theme.textTheme.displayMedium, - ), - const SizedBox(height: 23), - _rowItem(context, 'title'.tr(), series.title), - const Divider( - height: 32, - color: AppColor.secondarySpanishGrey, - ), - if (artist != null) ...[ - _rowItem( - context, - 'artist'.tr(), - artist.getDisplayName(), - tapLink: '${Environment.feralFileAPIURL}/profiles/${artist.id}', - ), - const Divider( - height: 32, - color: AppColor.secondarySpanishGrey, - ) - ], - _rowItem( - context, - 'token'.tr(), - 'Feral File', - // tapLink: "${Environment.feralFileAPIURL}/artworks/${artwork?.id}" - ), - const Divider( - height: 32, - color: AppColor.secondarySpanishGrey, - ), - _rowItem( - context, - 'contract'.tr(), - contract?.blockchainType.capitalize() ?? '', - tapLink: contract?.getBlockChainUrl(), - ), - const Divider( - height: 32, - color: AppColor.secondarySpanishGrey, - ), - _rowItem( - context, - 'medium'.tr(), - series.medium.capitalize(), - ), - if (mintDate != null) ...[ - const Divider( - height: 32, - color: AppColor.secondarySpanishGrey, - ), - _rowItem( - context, - 'date_minted'.tr(), - df.format(mintDate).toUpperCase(), - maxLines: 1, - ), - ], - ], - ); - } -} - class ArtworkDetailsHeader extends StatelessWidget { final String title; final String subTitle; @@ -2011,6 +1863,7 @@ class ArtworkDetailsHeader extends StatelessWidget { final Function? onTitleTap; final Function? onSubTitleTap; final bool isReverse; + final Color? color; const ArtworkDetailsHeader({ required this.title, @@ -2020,6 +1873,7 @@ class ArtworkDetailsHeader extends StatelessWidget { this.onTitleTap, this.onSubTitleTap, this.isReverse = false, + this.color, }); @override @@ -2036,9 +1890,10 @@ class ArtworkDetailsHeader extends StatelessWidget { child: Text( subTitle, style: theme.textTheme.ppMori700White14.copyWith( - color: AppColor.feralFileHighlight, - fontWeight: isReverse ? FontWeight.w400 : null), - maxLines: 1, + color: color ?? AppColor.white, + fontWeight: FontWeight.w400, + ), + maxLines: 3, overflow: TextOverflow.ellipsis, ), ), @@ -2049,9 +1904,10 @@ class ArtworkDetailsHeader extends StatelessWidget { child: Text( title, style: theme.textTheme.ppMori400White14.copyWith( - color: AppColor.feralFileHighlight, - fontWeight: isReverse ? FontWeight.w700 : null), - maxLines: 1, + color: color ?? AppColor.white, + fontWeight: FontWeight.w700, + fontStyle: FontStyle.italic), + maxLines: 2, overflow: TextOverflow.ellipsis, ), ), @@ -2062,9 +1918,11 @@ class ArtworkDetailsHeader extends StatelessWidget { class DrawerItem extends StatefulWidget { final OptionItem item; + final Color? color; const DrawerItem({ required this.item, + this.color, super.key, }); @@ -2085,7 +1943,9 @@ class _DrawerItemState extends State { Widget build(BuildContext context) { final theme = Theme.of(context); final item = widget.item; + final color = widget.color; final defaultTextStyle = theme.textTheme.ppMori400Black14; + final customTextStyle = defaultTextStyle.copyWith(color: color); final defaultProcessingTextStyle = defaultTextStyle.copyWith(color: AppColor.disabledColor); final defaultDisabledTextStyle = @@ -2099,7 +1959,7 @@ class _DrawerItemState extends State { ? (item.titleStyleOnDisable ?? defaultDisabledTextStyle) : isProcessing ? (item.titleStyleOnPrecessing ?? defaultProcessingTextStyle) - : (item.titleStyle ?? defaultTextStyle); + : (item.titleStyle ?? customTextStyle); final child = Container( color: Colors.transparent, diff --git a/lib/view/artwork_title_view.dart b/lib/view/artwork_title_view.dart new file mode 100644 index 0000000000..2e0ab9b181 --- /dev/null +++ b/lib/view/artwork_title_view.dart @@ -0,0 +1,41 @@ +import 'package:autonomy_flutter/model/ff_artwork.dart'; +import 'package:autonomy_flutter/util/exhibition_ext.dart'; +import 'package:autonomy_flutter/util/series_ext.dart'; +import 'package:feralfile_app_theme/feral_file_app_theme.dart'; +import 'package:flutter/material.dart'; + +class ArtworkTitleView extends StatelessWidget { + const ArtworkTitleView({ + required this.artwork, + super.key, + this.crossAxisAlignment, + }); + + final Artwork artwork; + final CrossAxisAlignment? crossAxisAlignment; + final showArtworkName = false; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final title = artwork.series!.displayTitle + + (artwork.isYokoOnoPublicVersion ? '' : ' ${artwork.name}'); + return Column( + crossAxisAlignment: crossAxisAlignment ?? CrossAxisAlignment.start, + children: [ + Text( + artwork.series!.artist?.alias ?? '', + style: theme.textTheme.ppMori400White14, + ), + const SizedBox(height: 3), + Text( + title, + style: theme.textTheme.ppMori700White14.copyWith( + fontStyle: FontStyle.italic, + ), + maxLines: 2, + ), + ], + ); + } +} diff --git a/lib/view/back_appbar.dart b/lib/view/back_appbar.dart index 71d9f687df..6d8626640b 100644 --- a/lib/view/back_appbar.dart +++ b/lib/view/back_appbar.dart @@ -41,17 +41,7 @@ AppBar getBackAppBar( leadingWidth: 44, scrolledUnderElevation: 0, leading: onBack != null - ? Semantics( - label: 'BACK', - child: IconButton( - onPressed: onBack, - constraints: const BoxConstraints(maxWidth: 36), - icon: SvgPicture.asset( - 'assets/images/icon_back.svg', - colorFilter: ColorFilter.mode(primaryColor, BlendMode.srcIn), - ), - ), - ) + ? backButton(context, onBack: onBack, color: primaryColor) : const SizedBox(width: 36), automaticallyImplyLeading: false, title: Row( @@ -123,17 +113,7 @@ AppBar getTitleEditAppBar(BuildContext context, leadingWidth: 44, scrolledUnderElevation: 0, leading: hasBack - ? Semantics( - label: 'BACK', - child: IconButton( - onPressed: () {}, - constraints: const BoxConstraints(maxWidth: 36), - icon: SvgPicture.asset( - 'assets/images/icon_back.svg', - colorFilter: ColorFilter.mode(primaryColor, BlendMode.srcIn), - ), - ), - ) + ? backButton(context, onBack: () {}, color: primaryColor) : const SizedBox(), automaticallyImplyLeading: false, title: Row( @@ -316,7 +296,7 @@ AppBar getDoneAppBar( ), ), ], - backgroundColor: theme.colorScheme.background, + backgroundColor: theme.colorScheme.surface, automaticallyImplyLeading: false, centerTitle: true, title: Text( @@ -376,7 +356,7 @@ AppBar getCustomDoneAppBar( ), ), ], - backgroundColor: theme.colorScheme.background, + backgroundColor: AppColor.white, automaticallyImplyLeading: false, centerTitle: true, title: title, @@ -384,11 +364,53 @@ AppBar getCustomDoneAppBar( ); } +AppBar getPlaylistAppBar( + BuildContext context, { + required Widget title, + required List actions, +}) => + AppBar( + systemOverlayStyle: systemUiOverlayDarkStyle, + elevation: 0, + shadowColor: Colors.transparent, + leading: Row( + children: [ + const SizedBox(width: 15), + Container( + width: 34, + height: 34, + decoration: BoxDecoration( + color: AppColor.auGreyBackground, + borderRadius: BorderRadius.circular(50), + ), + padding: const EdgeInsets.all(10), + child: backButton( + context, + onBack: () => Navigator.pop(context), + color: AppColor.white, + ), + ), + ], + ), + leadingWidth: 70, + titleSpacing: 0, + backgroundColor: AppColor.primaryBlack, + automaticallyImplyLeading: false, + centerTitle: true, + title: title, + actions: actions, + bottom: PreferredSize( + preferredSize: const Size.fromHeight(0.25), + child: addOnlyDivider(color: AppColor.auQuickSilver, border: 0.25), + ), + ); + AppBar getFFAppBar( BuildContext context, { required Function()? onBack, Widget? title, Widget? action, + bool? centerTitle = true, }) { const secondaryColor = AppColor.primaryBlack; return AppBar( @@ -396,7 +418,7 @@ AppBar getFFAppBar( statusBarColor: secondaryColor, statusBarIconBrightness: Brightness.light, statusBarBrightness: Brightness.dark), - centerTitle: true, + centerTitle: centerTitle, toolbarHeight: 66, leadingWidth: 44, scrolledUnderElevation: 0, @@ -409,7 +431,7 @@ AppBar getFFAppBar( icon: SvgPicture.asset( 'assets/images/ff_back_dark.svg', ), - padding: const EdgeInsets.all(0), + padding: const EdgeInsets.only(left: 15), )) : const SizedBox(width: 36), automaticallyImplyLeading: false, @@ -426,5 +448,22 @@ AppBar getFFAppBar( elevation: 0); } +Widget backButton(BuildContext context, + {required Function() onBack, Color? color}) => + Semantics( + label: 'BACK', + child: IconButton( + onPressed: onBack, + constraints: const BoxConstraints(), + padding: const EdgeInsets.all(0), + iconSize: 11, + icon: SvgPicture.asset( + 'assets/images/icon_back.svg', + colorFilter: + color != null ? ColorFilter.mode(color, BlendMode.srcIn) : null, + ), + ), + ); + // class MomaPallet to save colors // Path: lib/util/style.dart diff --git a/lib/view/canvas_device_view.dart b/lib/view/canvas_device_view.dart deleted file mode 100644 index b817cefb02..0000000000 --- a/lib/view/canvas_device_view.dart +++ /dev/null @@ -1,312 +0,0 @@ -import 'dart:async'; - -import 'package:autonomy_flutter/model/play_list_model.dart'; -import 'package:autonomy_flutter/screen/app_router.dart'; -import 'package:autonomy_flutter/screen/detail/preview/canvas_device_bloc.dart'; -import 'package:autonomy_flutter/screen/scan_qr/scan_qr_page.dart'; -import 'package:autonomy_flutter/util/au_icons.dart'; -import 'package:autonomy_flutter/util/style.dart'; -import 'package:autonomy_flutter/util/ui_helper.dart'; -import 'package:autonomy_flutter/view/primary_button.dart'; -import 'package:autonomy_flutter/view/responsive.dart'; -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:feralfile_app_theme/feral_file_app_theme.dart'; -import 'package:feralfile_app_tv_proto/models/canvas_device.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_svg/flutter_svg.dart'; - -class CanvasDeviceView extends StatefulWidget { - final String sceneId; - final bool isCollection; - final Function? onClose; - final PlayListModel? playlist; - - const CanvasDeviceView( - {required this.sceneId, - super.key, - this.onClose, - this.isCollection = false, - this.playlist}); - - @override - State createState() => _CanvasDeviceViewState(); -} - -class _CanvasDeviceViewState extends State { - late final CanvasDeviceBloc _bloc; - - @override - void initState() { - super.initState(); - _bloc = context.read(); - unawaited(_fetchDevice()); - } - - Future _fetchDevice() async { - _bloc.add(CanvasDeviceGetDevicesEvent(widget.sceneId)); - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - return BlocConsumer( - listener: (context, state) { - setState(() {}); - if (state.isConnectError) { - unawaited(UIHelper.showInfoDialog( - context, 'fail_to_connect'.tr(), 'canvas_fail_des'.tr(), - closeButton: 'close'.tr())); - } - }, - builder: (context, state) { - final devices = state.devices; - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: ResponsiveLayout.pageHorizontalEdgeInsets, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('connect_to_frame'.tr(), - style: theme.textTheme.ppMori700White24), - const SizedBox(height: 40), - RichText( - text: TextSpan(children: [ - TextSpan( - text: widget.isCollection - ? 'display_your_collection_on'.tr() - : 'display_your_artwork_on'.tr(), - style: theme.textTheme.ppMori400White14, - ), - TextSpan( - text: 'compatible_platforms'.tr(), - style: theme.textTheme.ppMori400FFYellow14, - recognizer: TapGestureRecognizer() - ..onTap = () async => Navigator.of(context) - .pushNamed(AppRouter.canvasHelpPage), - ), - TextSpan( - text: 'for_a_better_viewing'.tr(), - style: theme.textTheme.ppMori400White14, - ), - ])), - ], - ), - ), - const SizedBox(height: 20), - Flexible( - child: SingleChildScrollView( - child: Column( - children: devices - .map((device) => [ - _deviceRow(device), - addDivider(height: 1, color: AppColor.white), - ]) - .flattened - .toList(), - ), - ), - ), - const SizedBox(height: 40), - Padding( - padding: ResponsiveLayout.pageHorizontalEdgeInsets, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _newCanvas(state), - OutlineButton( - text: 'close'.tr(), - onTap: () { - widget.onClose?.call(); - }, - ), - ], - ), - ) - ], - ); - }, - ); - } - - Widget _newCanvas(CanvasDeviceState state) { - final theme = Theme.of(context); - if (!state.isLoaded) { - return const SizedBox(); - } else if (state.devices.isEmpty) { - return Column( - children: [ - PrimaryButton( - text: 'add_new_frame'.tr(), - onTap: () async => _addNewCanvas(), - ), - const SizedBox(height: 10), - ], - ); - } else { - return Column( - children: [ - GestureDetector( - onTap: () async => _addNewCanvas(), - child: DecoratedBox( - decoration: const BoxDecoration(color: Colors.transparent), - child: Row( - children: [ - Text( - 'add_new_frame'.tr(), - style: theme.textTheme.ppMori400FFYellow14, - ), - const Spacer(), - SvgPicture.asset( - 'assets/images/joinFile.svg', - colorFilter: const ColorFilter.mode( - AppColor.feralFileHighlight, BlendMode.srcIn), - ), - ], - ), - ), - ), - const SizedBox(height: 40), - ], - ); - } - } - - Future _addNewCanvas() async { - dynamic device = await Navigator.of(context) - .pushNamed(AppRouter.scanQRPage, arguments: ScannerItem.CANVAS_DEVICE); - if (!mounted) { - return; - } - if (device != null && device is CanvasDevice) { - _bloc.add(CanvasDeviceAddEvent(DeviceState(device: device))); - } - } - - // row view show DeviceState display name and status - Widget _deviceRow(DeviceState deviceState) { - final theme = Theme.of(context); - return Column( - children: [ - Padding( - padding: ResponsiveLayout.pageHorizontalEdgeInsets - .copyWith(top: 20, bottom: 20), - child: DecoratedBox( - decoration: const BoxDecoration(color: Colors.transparent), - child: Row( - children: [ - Expanded( - child: Text( - deviceState.device.name, - style: (deviceState.status == DeviceStatus.error) - ? theme.textTheme.ppMori700Black14 - .copyWith(color: AppColor.disabledColor) - : theme.textTheme.ppMori700White14, - overflow: TextOverflow.ellipsis, - ), - ), - const SizedBox( - width: 20, - ), - _deviceStatus(deviceState), - ], - ), - ), - ), - addOnlyDivider(), - ], - ); - } - - Widget _deviceStatus(DeviceState deviceState) { - final theme = Theme.of(context); - switch (deviceState.status) { - case DeviceStatus.loading: - return GestureDetector( - onTap: () { - _bloc.add(CanvasDeviceUnCastingEvent( - deviceState.device, widget.isCollection)); - }, - child: loadingIndicator( - size: 22, - valueColor: AppColor.white, - backgroundColor: AppColor.greyMedium), - ); - case DeviceStatus.playing: - return Row( - children: [ - IconButton( - onPressed: () { - _bloc.add(CanvasDeviceRotateEvent(deviceState.device)); - }, - padding: const EdgeInsets.all(0), - constraints: const BoxConstraints(), - icon: const Icon(AuIcon.rotateRounded, color: AppColor.white), - ), - const SizedBox(width: 20), - Container( - decoration: BoxDecoration( - color: Colors.transparent, - border: Border.all(color: AppColor.feralFileHighlight), - borderRadius: BorderRadius.circular(15), - ), - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 5, - ), - child: Text('playing'.tr(), - style: theme.textTheme.ppMori400FFYellow14), - ), - const SizedBox(width: 20), - GestureDetector( - child: SvgPicture.asset( - 'assets/images/stop_icon.svg', - width: 30, - height: 30, - ), - onTap: () { - _bloc.add(CanvasDeviceUnCastingEvent( - deviceState.device, widget.isCollection)); - }, - ), - ], - ); - case DeviceStatus.connected: - return GestureDetector( - child: SvgPicture.asset( - 'assets/images/play_canvas_icon.svg', - colorFilter: - const ColorFilter.mode(AppColor.white, BlendMode.srcIn), - ), - onTap: () { - if (widget.isCollection) { - if (widget.playlist == null) { - return; - } - _bloc.add(CanvasDeviceCastCollectionEvent( - deviceState.device, widget.playlist!)); - } else { - _bloc.add(CanvasDeviceCastSingleEvent( - deviceState.device, widget.sceneId)); - } - }, - ); - case DeviceStatus.error: - return GestureDetector( - onTap: () async { - await Navigator.of(context).pushNamed(AppRouter.canvasHelpPage); - }, - child: SvgPicture.asset( - 'assets/images/help_icon.svg', - colorFilter: const ColorFilter.mode( - AppColor.feralFileHighlight, BlendMode.srcIn), - ), - ); - } - } -} diff --git a/lib/view/cast_button.dart b/lib/view/cast_button.dart index 5a6c2933c8..bf7dc22b8e 100644 --- a/lib/view/cast_button.dart +++ b/lib/view/cast_button.dart @@ -1,73 +1,126 @@ +import 'package:autonomy_flutter/common/injector.dart'; +import 'package:autonomy_flutter/screen/detail/preview/artwork_preview_page.dart'; +import 'package:autonomy_flutter/screen/detail/preview/canvas_device_bloc.dart'; +import 'package:autonomy_flutter/util/ui_helper.dart'; +import 'package:autonomy_flutter/view/stream_device_view.dart'; import 'package:feralfile_app_theme/feral_file_app_theme.dart'; +import 'package:feralfile_app_tv_proto/models/canvas_device.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; -class FFCastButton extends StatelessWidget { - final VoidCallback? onCastTap; - final bool isCasting; +class FFCastButton extends StatefulWidget { + final Function(CanvasDevice device)? onDeviceSelected; + final String displayKey; final String? text; + final String? type; const FFCastButton( - {super.key, this.onCastTap, this.isCasting = false, this.text}); + {required this.displayKey, + this.type = '', + super.key, + this.onDeviceSelected, + this.text}); + + @override + State createState() => _FFCastButtonState(); +} + +class _FFCastButtonState extends State { + late CanvasDeviceBloc _canvasDeviceBloc; + final keyboardManagerKey = GlobalKey(); + + @override + void initState() { + super.initState(); + _canvasDeviceBloc = injector.get(); + } @override Widget build(BuildContext context) { final theme = Theme.of(context); - return GestureDetector( - onTap: onCastTap, - child: Semantics( - label: 'cast_icon', - child: DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - color: AppColor.feralFileLightBlue, - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 5), - child: Row( - children: [ - if (text != null) - Padding( - padding: const EdgeInsets.only(right: 10), - child: Text( - text!, - style: theme.textTheme.ppMori400Black14.copyWith( - color: theme.colorScheme.primary, + return BlocBuilder( + bloc: _canvasDeviceBloc, + builder: (context, state) { + final castingDevice = + state.lastSelectedActiveDeviceForKey(widget.displayKey); + final isCasting = castingDevice != null; + return GestureDetector( + onTap: () async { + await _showStreamAction(context, widget.displayKey); + }, + child: Semantics( + label: 'cast_icon', + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(60), + color: AppColor.feralFileLightBlue, + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 9) + .copyWith(left: 16, right: isCasting ? 9 : 16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.text != null) + Padding( + padding: const EdgeInsets.only(right: 10), + child: Text( + widget.text!, + style: theme.textTheme.ppMori400Black14.copyWith( + color: theme.colorScheme.primary, + ), + ), + ), + SvgPicture.asset( + 'assets/images/cast_icon.svg', + height: 20, + colorFilter: ColorFilter.mode( + theme.colorScheme.primary, + BlendMode.srcIn, ), ), - ), - SvgPicture.asset( - 'assets/images/cast_icon.svg', - height: 20, - colorFilter: ColorFilter.mode( - theme.colorScheme.primary, BlendMode.srcIn), + if (isCasting) ...[ + const SizedBox( + width: 3, + height: 20, + ), + Container( + width: 4, + height: 4, + margin: const EdgeInsets.only(top: 1), + decoration: const BoxDecoration( + color: AppColor.primaryBlack, + shape: BoxShape.circle, + ), + ), + ], + ], ), - ], + ), ), ), - ), - ), + ); + }, ); } -} - -class CastButton extends StatelessWidget { - final VoidCallback? onCastTap; - final bool isCasting; - - const CastButton({super.key, this.onCastTap, this.isCasting = false}); - @override - Widget build(BuildContext context) => GestureDetector( - onTap: onCastTap, - child: Semantics( - label: 'cast_icon', - child: SvgPicture.asset( - 'assets/images/cast_icon.svg', - colorFilter: ColorFilter.mode( - isCasting ? AppColor.feralFileHighlight : AppColor.white, - BlendMode.srcIn), - ), + Future _showStreamAction( + BuildContext context, String displayKey) async { + keyboardManagerKey.currentState?.hideKeyboard(); + await UIHelper.showFlexibleDialog( + context, + BlocProvider.value( + value: _canvasDeviceBloc, + child: StreamDeviceView( + displayKey: displayKey, + onDeviceSelected: (canvasDevice) { + widget.onDeviceSelected?.call(canvasDevice); + Navigator.pop(context); + }, ), - ); + ), + isDismissible: true, + ); + } } diff --git a/lib/view/custom_note.dart b/lib/view/custom_note.dart new file mode 100644 index 0000000000..0b2208ce80 --- /dev/null +++ b/lib/view/custom_note.dart @@ -0,0 +1,75 @@ +import 'package:autonomy_flutter/model/ff_exhibition.dart'; +import 'package:autonomy_flutter/screen/app_router.dart'; +import 'package:autonomy_flutter/util/style.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:feralfile_app_theme/feral_file_app_theme.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; + +class ExhibitionCustomNote extends StatelessWidget { + const ExhibitionCustomNote({ + required this.info, + super.key, + this.isFull = false, + }); + + final CustomExhibitionNote info; + final bool isFull; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Center( + child: Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: AppColor.auGreyBackground, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + info.title, + style: theme.textTheme.ppMori400White12, + ), + const SizedBox(height: 20), + ConstrainedBox( + constraints: + BoxConstraints(maxHeight: isFull ? double.infinity : 400), + child: ClipRect( + child: HtmlWidget( + isFull + ? info.content + : '
${info.content.split('
').first}
', + textStyle: theme.textTheme.ppMori400White14, + customStylesBuilder: auHtmlStyle, + ), + ), + ), + if (info.canReadMore == true && !isFull) ...[ + const SizedBox(height: 20), + GestureDetector( + onTap: () async { + await Navigator.pushNamed( + context, + AppRouter.exhibitionCustomNote, + arguments: info, + ); + }, + child: Text( + 'read_more'.tr(), + style: theme.textTheme.ppMori400White14.copyWith( + decoration: TextDecoration.underline, + decorationColor: AppColor.white, + ), + ), + ), + ] + ], + ), + ), + ); + } +} diff --git a/lib/view/display_instruction_view.dart b/lib/view/display_instruction_view.dart new file mode 100644 index 0000000000..30df52628a --- /dev/null +++ b/lib/view/display_instruction_view.dart @@ -0,0 +1,73 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:feralfile_app_theme/feral_file_app_theme.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class DisplayInstructionView extends StatelessWidget { + const DisplayInstructionView({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final st = theme.textTheme.ppMori400White14; + final indexes = [0, 1, 2, 3, 4]; + return Column( + children: [ + Text( + 'available_on_tv'.tr(), + style: theme.textTheme.ppMori400Grey14, + ), + const SizedBox(height: 10), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: indexes.map((index) { + final instruction = 'display_instruction_${index + 1}'.tr(); + return Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${index + 1}. ', + style: st, + ), + Expanded( + flex: index == 2 ? 0 : 1, + child: Text( + instruction, + style: st, + ), + ), + const SizedBox(width: 5), + if (index == 2) + DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(60), + color: AppColor.feralFileLightBlue, + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 5), + child: SvgPicture.asset( + 'assets/images/cast_icon.svg', + height: 12, + width: 12, + colorFilter: const ColorFilter.mode( + AppColor.primaryBlack, + BlendMode.srcIn, + ), + ), + ), + ), + ], + ), + ); + }).toList(), + ), + ), + ], + ); + } +} diff --git a/lib/view/event_view.dart b/lib/view/event_view.dart index 118948a547..e69de29bb2 100644 --- a/lib/view/event_view.dart +++ b/lib/view/event_view.dart @@ -1,108 +0,0 @@ -import 'package:autonomy_flutter/model/ff_exhibition.dart'; -import 'package:autonomy_flutter/util/feral_file_custom_tab.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:feralfile_app_theme/feral_file_app_theme.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; - -class ExhibitionEventView extends StatelessWidget { - final ExhibitionEvent exhibitionEvent; - final double width; - - const ExhibitionEventView({ - required this.exhibitionEvent, - required this.width, - super.key, - }); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final dateFormat = DateFormat('MMM d, y'); - final timeFormat = DateFormat('h:mm a'); - final mediaImageUrl = exhibitionEvent.mediaUri?.type == 'image' - ? exhibitionEvent.mediaUri?.url - : null; - final mediaVideoUrl = exhibitionEvent.mediaUri?.type == 'video' - ? exhibitionEvent.mediaUri?.url - : null; - final eventLink = exhibitionEvent.links?.isNotEmpty == true - ? exhibitionEvent.links!.values.first - : null; - final watchMoreUrl = mediaVideoUrl ?? eventLink; - return Padding( - padding: const EdgeInsets.only(right: 14), - child: Container( - width: width, - padding: const EdgeInsets.all(14), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: AppColor.auGreyBackground, - ), - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'event'.tr(), - style: theme.textTheme.ppMori400White12, - ), - const SizedBox(height: 30), - if (mediaImageUrl != null) ...[ - Image.network( - mediaImageUrl, - fit: BoxFit.fitWidth, - ), - const SizedBox(height: 20), - ], - Text(exhibitionEvent.title, - style: theme.textTheme.ppMori400White14), - const SizedBox(height: 20), - if (exhibitionEvent.dateTime != null) ...[ - Text( - 'Date: ${dateFormat.format(exhibitionEvent.dateTime!)}', - style: theme.textTheme.ppMori400White14, - ), - Text( - 'Time: ${timeFormat.format(exhibitionEvent.dateTime!)}', - style: theme.textTheme.ppMori400White14, - ), - ], - if (exhibitionEvent.description != null) ...[ - HtmlWidget( - exhibitionEvent.description!, - textStyle: theme.textTheme.ppMori400White14, - customStylesBuilder: (element) { - if (element.localName == 'a') { - return { - 'color': 'white', - 'text-decoration-color': 'white' - }; - } - return null; - }, - ), - const SizedBox(height: 20), - ], - if (watchMoreUrl != null && watchMoreUrl.isNotEmpty) - GestureDetector( - onTap: () async { - final browser = FeralFileBrowser(); - await browser.openUrl(watchMoreUrl); - }, - child: Text( - 'watch'.tr(), - style: theme.textTheme.ppMori400White14.copyWith( - decoration: TextDecoration.underline, - decorationColor: AppColor.white, - ), - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/view/exhibition_detail_last_page.dart b/lib/view/exhibition_detail_last_page.dart index 169101abf8..3f8527fc80 100644 --- a/lib/view/exhibition_detail_last_page.dart +++ b/lib/view/exhibition_detail_last_page.dart @@ -1,9 +1,12 @@ +import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/screen/app_router.dart'; import 'package:autonomy_flutter/screen/exhibition_details/exhibition_detail_page.dart'; import 'package:autonomy_flutter/util/exhibition_ext.dart'; +import 'package:cached_network_image/cached_network_image.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:feralfile_app_theme/feral_file_app_theme.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_svg/flutter_svg.dart'; class ExhibitionDetailLastPage extends StatelessWidget { @@ -69,11 +72,13 @@ class ExhibitionDetailLastPage extends StatelessWidget { }, child: SizedBox( width: double.infinity, - child: Image.network( - nextPayload!.exhibitions[nextPayload!.index].coverUrl, + child: CachedNetworkImage( + imageUrl: + nextPayload!.exhibitions[nextPayload!.index].coverUrl, height: 140, alignment: Alignment.topCenter, fit: BoxFit.fitWidth, + cacheManager: injector(), ), ), ) diff --git a/lib/view/exhibition_detail_preview.dart b/lib/view/exhibition_detail_preview.dart index 721440c28a..11d4424507 100644 --- a/lib/view/exhibition_detail_preview.dart +++ b/lib/view/exhibition_detail_preview.dart @@ -1,14 +1,18 @@ import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/model/ff_exhibition.dart'; import 'package:autonomy_flutter/service/navigation_service.dart'; +import 'package:autonomy_flutter/util/constants.dart'; import 'package:autonomy_flutter/util/exhibition_ext.dart'; import 'package:autonomy_flutter/view/header.dart'; +import 'package:autonomy_flutter/view/john_gerrard_live_performance.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:feralfile_app_theme/feral_file_app_theme.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; +import 'package:flutter_svg/svg.dart'; class ExhibitionPreview extends StatelessWidget { ExhibitionPreview({required this.exhibition, super.key}); @@ -34,10 +38,7 @@ class ExhibitionPreview extends StatelessWidget { children: [ ClipRRect( borderRadius: BorderRadius.circular(10), - child: CachedNetworkImage( - imageUrl: exhibition.coverUrl, - fit: BoxFit.fitWidth, - ), + child: _buildExhibitionMedia(context, exhibition), ), HeaderView( title: exhibition.title, @@ -47,8 +48,10 @@ class ExhibitionPreview extends StatelessWidget { Text('curator'.tr(), style: subTextStyle), const SizedBox(height: 3), GestureDetector( - child: Text(exhibition.curator!.alias, - style: artistTextStyle.copyWith()), + child: Text( + exhibition.curator!.alias, + style: artistTextStyle.copyWith(), + ), onTap: () async { await _navigationService .openFeralFileCuratorPage(exhibition.curator!.alias); @@ -56,7 +59,11 @@ class ExhibitionPreview extends StatelessWidget { ), ], const SizedBox(height: 10), - Text('group_exhibition'.tr(), style: subTextStyle), + Text( + exhibition.isGroupExhibition + ? 'group_exhibition'.tr() + : 'solo_exhibition'.tr(), + style: subTextStyle), const SizedBox(height: 3), RichText( text: TextSpan( @@ -86,4 +93,49 @@ class ExhibitionPreview extends StatelessWidget { ), ); } + + Widget _buildExhibitionMedia(BuildContext context, Exhibition exhibition) { + if (exhibition.id == SOURCE_EXHIBITION_ID) { + return _buildSourceExhibitionCover(context); + } else if (exhibition.isJohnGerrardShow) { + return _buildJohnGerrardExhibitionLivePerformance(context); + } else { + return CachedNetworkImage( + imageUrl: exhibition.coverUrl, + cacheManager: injector(), + fit: BoxFit.fitWidth, + ); + } + } + + Widget _buildSourceExhibitionCover(BuildContext context) { + const padding = 14.0; + final screenWidth = MediaQuery.sizeOf(context).width; + final estimatedHeight = (screenWidth - padding * 2) / 16 * 9; + return SvgPicture.network( + exhibition.coverUrl, + height: estimatedHeight, + fit: BoxFit.fitWidth, + placeholderBuilder: (context) => SizedBox( + height: estimatedHeight, + child: const Center( + child: CircularProgressIndicator( + color: Colors.white, + backgroundColor: AppColor.auQuickSilver, + strokeWidth: 2, + ), + ), + ), + ); + } + + Widget _buildJohnGerrardExhibitionLivePerformance(BuildContext context) { + const padding = 14.0; + return SizedBox( + height: MediaQuery.sizeOf(context).width - padding * 2, + child: JohnGerrardLivePerformanceWidget( + exhibition: exhibition, + ), + ); + } } diff --git a/lib/view/external_link.dart b/lib/view/external_link.dart deleted file mode 100644 index 8ba61c72c3..0000000000 --- a/lib/view/external_link.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'dart:ui' as ui; - -import 'package:autonomy_flutter/util/feral_file_custom_tab.dart'; -import 'package:autonomy_flutter/util/string_ext.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; - -class ExternalLink extends StatelessWidget { - final String? link; - final Color? color; - final Color? disableColor; - - const ExternalLink({super.key, this.color, this.link, this.disableColor}); - - @override - Widget build(BuildContext context) { - final isValid = link?.isValidUrl() ?? false; - final colorFilterWhenValid = - color == null ? null : ui.ColorFilter.mode(color!, BlendMode.srcIn); - final colorFilterWhenInvalid = disableColor == null - ? null - : ui.ColorFilter.mode(disableColor!, BlendMode.srcIn); - return GestureDetector( - onTap: () async { - if (isValid) { - final browser = FeralFileBrowser(); - await browser.openUrl(link!); - } - }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: SvgPicture.asset( - 'assets/images/external_link.svg', - width: 20, - height: 20, - colorFilter: - isValid ? colorFilterWhenValid : colorFilterWhenInvalid, - ), - )); - } -} diff --git a/lib/view/feralfile_artwork_preview_widget.dart b/lib/view/feralfile_artwork_preview_widget.dart index f6809d8d2a..596588cb94 100644 --- a/lib/view/feralfile_artwork_preview_widget.dart +++ b/lib/view/feralfile_artwork_preview_widget.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:autonomy_flutter/main.dart'; -import 'package:autonomy_flutter/model/ff_account.dart'; +import 'package:autonomy_flutter/model/ff_artwork.dart'; import 'package:autonomy_flutter/util/exhibition_ext.dart'; import 'package:autonomy_flutter/view/artwork_common_widget.dart'; import 'package:flutter/material.dart'; @@ -83,9 +83,7 @@ class _FeralfileArtworkPreviewWidgetState Widget build(BuildContext context) { final previewUrl = widget.payload.artwork.previewURL; final thumbnailUrl = widget.payload.artwork.thumbnailURL; - final feralfileMedium = FeralfileMediumTypes.fromString( - widget.payload.artwork.series?.medium ?? ''); - final medium = feralfileMedium.toRenderingType; + final artwork = widget.payload.artwork; return BlocProvider( create: (_) => RetryCubit(), child: BlocBuilder( @@ -94,37 +92,45 @@ class _FeralfileArtworkPreviewWidgetState _renderingWidget?.dispose(); _renderingWidget = null; } - _renderingWidget ??= buildFeralfileRenderingWidget( - context, - attempt: attempt > 0 ? attempt : null, - isMute: widget.payload.isMute, - mimeType: medium, - previewURL: previewUrl, - thumbnailURL: thumbnailUrl, - isScrollable: widget.payload.isScrollable, - ); - - switch (medium) { - case RenderingType.image: - case RenderingType.video: - case RenderingType.gif: - case RenderingType.svg: - return InteractiveViewer( - minScale: 1, - maxScale: 4, - child: Center( - child: _artworkView(context), - ), - ); - case RenderingType.pdf: - return Center( - child: _artworkView(context), + return FutureBuilder( + future: artwork.renderingType(), + builder: (context, snapshot) { + final medium = snapshot.data; + if (medium == null) { + return const SizedBox(); + } + _renderingWidget ??= buildFeralfileRenderingWidget( + context, + attempt: attempt > 0 ? attempt : null, + isMute: widget.payload.isMute, + mimeType: medium, + previewURL: previewUrl, + thumbnailURL: thumbnailUrl, + isScrollable: widget.payload.isScrollable, ); - default: - return Center( - child: _artworkView(context), - ); - } + switch (medium) { + case RenderingType.image: + case RenderingType.video: + case RenderingType.gif: + case RenderingType.svg: + return InteractiveViewer( + minScale: 1, + maxScale: 4, + child: Center( + child: _artworkView(context), + ), + ); + case RenderingType.pdf: + return Center( + child: _artworkView(context), + ); + default: + return Center( + child: _artworkView(context), + ); + } + }, + ); }, ), ); diff --git a/lib/view/ff_artwork_preview.dart b/lib/view/ff_artwork_preview.dart index f53e5a9954..206b1458bb 100644 --- a/lib/view/ff_artwork_preview.dart +++ b/lib/view/ff_artwork_preview.dart @@ -1,11 +1,13 @@ -import 'package:autonomy_flutter/model/ff_account.dart'; -import 'package:autonomy_flutter/model/ff_series.dart'; +import 'package:autonomy_flutter/model/ff_artwork.dart'; import 'package:autonomy_flutter/screen/app_router.dart'; +import 'package:autonomy_flutter/screen/feralfile_artwork_preview/feralfile_artwork_preview_page.dart'; import 'package:autonomy_flutter/screen/feralfile_series/feralfile_series_page.dart'; +import 'package:autonomy_flutter/util/john_gerrard_helper.dart'; +import 'package:autonomy_flutter/util/series_ext.dart'; import 'package:autonomy_flutter/view/feralfile_artwork_preview_widget.dart'; import 'package:autonomy_flutter/view/series_title_view.dart'; -import 'package:feralfile_app_theme/feral_file_app_theme.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; class FeralFileArtworkPreview extends StatelessWidget { const FeralFileArtworkPreview({required this.payload, super.key}); @@ -14,28 +16,39 @@ class FeralFileArtworkPreview extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = Theme.of(context); + final isCrystallineWork = + payload.artwork.series?.exhibitionID == JohnGerrardHelper.exhibitionID; return Column( children: [ Expanded( - child: FeralfileArtworkPreviewWidget( - payload: FeralFileArtworkPreviewWidgetPayload( - artwork: payload.artwork, - isMute: true, - ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 14), + child: _buildArtworkPreview(isCrystallineWork), ), ), const SizedBox(height: 20), Padding( padding: const EdgeInsets.only(left: 14, right: 14, bottom: 20), child: GestureDetector( - onTap: () async => Navigator.of(context).pushNamed( - AppRouter.feralFileSeriesPage, - arguments: FeralFileSeriesPagePayload( - seriesId: payload.series.id, - exhibitionId: payload.series.exhibitionID, - ), - ), + onTap: () async { + final artwork = payload.artwork; + if (artwork.series?.isSingle ?? false) { + await Navigator.of(context).pushNamed( + AppRouter.ffArtworkPreviewPage, + arguments: FeralFileArtworkPreviewPagePayload( + artwork: artwork, + ), + ); + } else { + await Navigator.of(context).pushNamed( + AppRouter.feralFileSeriesPage, + arguments: FeralFileSeriesPagePayload( + seriesId: artwork.series!.id, + exhibitionId: artwork.series!.exhibitionID, + ), + ); + } + }, child: Row( crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -43,14 +56,16 @@ class FeralFileArtworkPreview extends StatelessWidget { Flexible( flex: 5, child: SeriesTitleView( - series: payload.series, artist: payload.series.artist), + series: payload.artwork.series!, + artist: payload.artwork.series!.artist, + ), ), Padding( - padding: const EdgeInsets.only(bottom: 3), - child: Text( - '${payload.artwork.index + 1}/${payload.series.settings?.maxArtwork ?? '--'}', - style: - theme.textTheme.ppMori400White12.copyWith(fontSize: 10), + padding: const EdgeInsets.only(left: 60), + child: SvgPicture.asset( + 'assets/images/icon_series.svg', + width: 22, + height: 22, ), ) ], @@ -60,14 +75,30 @@ class FeralFileArtworkPreview extends StatelessWidget { ], ); } + + Widget _buildArtworkPreview(bool isCrystallineWork) { + final artworkPreviewWidget = FeralfileArtworkPreviewWidget( + payload: FeralFileArtworkPreviewWidgetPayload( + artwork: payload.artwork, + isMute: true, + ), + ); + if (isCrystallineWork) { + return Center( + child: AspectRatio( + aspectRatio: 1, + child: artworkPreviewWidget, + ), + ); + } + return artworkPreviewWidget; + } } class FeralFileArtworkPreviewPayload { - final FFSeries series; final Artwork artwork; const FeralFileArtworkPreviewPayload({ - required this.series, required this.artwork, }); } diff --git a/lib/view/ff_artwork_thumbnail_view.dart b/lib/view/ff_artwork_thumbnail_view.dart index 504cca9b22..d9865b5356 100644 --- a/lib/view/ff_artwork_thumbnail_view.dart +++ b/lib/view/ff_artwork_thumbnail_view.dart @@ -1,30 +1,37 @@ -import 'package:autonomy_flutter/model/ff_account.dart'; +import 'package:autonomy_flutter/model/ff_artwork.dart'; import 'package:autonomy_flutter/util/exhibition_ext.dart'; import 'package:autonomy_flutter/view/artwork_common_widget.dart'; -import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; class FFArtworkThumbnailView extends StatelessWidget { - const FFArtworkThumbnailView({required this.artwork, super.key, this.onTap}); + const FFArtworkThumbnailView( + {required this.artwork, + this.cacheWidth = 0, + this.cacheHeight = 0, + super.key, + this.onTap}); final Artwork artwork; final Function? onTap; + final int cacheWidth; + final int cacheHeight; @override Widget build(BuildContext context) => GestureDetector( onTap: () => onTap?.call(), - child: CachedNetworkImage( - imageUrl: artwork.thumbnailURL, - imageBuilder: (context, imageProvider) => Container( - decoration: BoxDecoration( - image: DecorationImage( - image: imageProvider, - fit: BoxFit.cover, - ), - ), - ), - placeholder: (context, url) => const GalleryThumbnailPlaceholder(), - errorWidget: (context, url, error) => + child: Image.network( + artwork.thumbnailURL, + cacheWidth: cacheWidth, + cacheHeight: cacheHeight, + fit: BoxFit.cover, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) { + return child; + } + return const GalleryThumbnailPlaceholder(); + }, + errorBuilder: (context, url, error) => const GalleryThumbnailErrorWidget(), ), ); diff --git a/lib/view/galery_thumbnail_item.dart b/lib/view/galery_thumbnail_item.dart index 4bb5ca1d20..6a73e146bd 100644 --- a/lib/view/galery_thumbnail_item.dart +++ b/lib/view/galery_thumbnail_item.dart @@ -37,6 +37,7 @@ class _GaleryThumbnailItemState extends State { ? PendingTokenWidget( thumbnail: asset.galleryThumbnailURL, tokenId: asset.tokenId, + shouldRefreshCache: asset.shouldRefreshThumbnailCache, ) : tokenGalleryThumbnailWidget( context, diff --git a/lib/view/get_started_banner.dart b/lib/view/get_started_banner.dart new file mode 100644 index 0000000000..1e67a2a19f --- /dev/null +++ b/lib/view/get_started_banner.dart @@ -0,0 +1,64 @@ +import 'package:autonomy_flutter/util/au_icons.dart'; +import 'package:autonomy_flutter/view/primary_button.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:feralfile_app_theme/feral_file_app_theme.dart'; +import 'package:flutter/material.dart'; + +class GetStartedBanner extends StatelessWidget { + final Function? onClose; + final Function? onGetStarted; + final String title; + + const GetStartedBanner( + {required this.title, super.key, this.onGetStarted, this.onClose}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final textTheme = theme.textTheme; + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: AppColor.auGreyBackground), + padding: const EdgeInsets.all(15), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + title, + style: textTheme.ppMori400White14, + maxLines: 2, + ), + ), + GestureDetector( + onTap: () { + onClose?.call(); + }, + child: const Icon( + size: 18, + AuIcon.close, + color: AppColor.white, + ), + //padding: EdgeInsets.zero, + ) + ], + ), + const SizedBox( + height: 20, + ), + PrimaryAsyncButton( + onTap: () { + onGetStarted?.call(); + }, + text: 'get_started'.tr(), + ) + ], + ), + ); + } +} diff --git a/lib/view/header.dart b/lib/view/header.dart index 52e15ec2b7..cefbdd8054 100644 --- a/lib/view/header.dart +++ b/lib/view/header.dart @@ -5,7 +5,7 @@ // that can be found in the LICENSE file. // -import 'package:feralfile_app_theme/feral_file_app_theme.dart'; +import 'package:autonomy_flutter/view/title_text.dart'; import 'package:flutter/material.dart'; class HeaderView extends StatelessWidget { @@ -23,30 +23,25 @@ class HeaderView extends StatelessWidget { }); @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final defaultStyle = - theme.textTheme.ppMori700White24.copyWith(fontSize: 36); - return Align( - alignment: Alignment.centerLeft, - child: Container( - padding: padding ?? const EdgeInsets.fromLTRB(12, 33, 12, 42), - child: Column( - children: [ - Row( - children: [ - Expanded( - child: Text( - title, - style: titleStyle ?? defaultStyle, + Widget build(BuildContext context) => Align( + alignment: Alignment.centerLeft, + child: Container( + padding: padding ?? const EdgeInsets.fromLTRB(12, 33, 12, 42), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: TitleText( + title: title, + style: titleStyle, + ), ), - ), - action ?? const SizedBox() - ], - ), - ], + action ?? const SizedBox() + ], + ), + ], + ), ), - ), - ); - } + ); } diff --git a/lib/view/image_background.dart b/lib/view/image_background.dart new file mode 100644 index 0000000000..74efc8551b --- /dev/null +++ b/lib/view/image_background.dart @@ -0,0 +1,14 @@ +import 'package:feralfile_app_theme/feral_file_app_theme.dart'; +import 'package:flutter/material.dart'; + +class ImageBackground extends StatelessWidget { + final Widget child; + final Color color; + + const ImageBackground( + {required this.child, this.color = AppColor.auLightGrey, super.key}); + + @override + Widget build(BuildContext context) => + DecoratedBox(decoration: BoxDecoration(color: color), child: child); +} diff --git a/lib/view/john_gerrard_live_performance.dart b/lib/view/john_gerrard_live_performance.dart new file mode 100644 index 0000000000..fe151a3394 --- /dev/null +++ b/lib/view/john_gerrard_live_performance.dart @@ -0,0 +1,99 @@ +import 'dart:async'; + +import 'package:autonomy_flutter/main.dart'; +import 'package:autonomy_flutter/model/ff_exhibition.dart'; +import 'package:autonomy_flutter/util/series_ext.dart'; +import 'package:autonomy_flutter/view/artwork_common_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:nft_rendering/nft_rendering.dart'; + +class JohnGerrardLivePerformanceWidget extends StatefulWidget { + final Exhibition exhibition; + + const JohnGerrardLivePerformanceWidget({required this.exhibition, super.key}); + + @override + State createState() => + _JohnGerrardLivePerformanceWidgetState(); +} + +class _JohnGerrardLivePerformanceWidgetState + extends State + with WidgetsBindingObserver, RouteAware { + INFTRenderingWidget? _renderingWidget; + + @override + void initState() { + WidgetsBinding.instance.addObserver(this); + super.initState(); + } + + @override + void dispose() { + _renderingWidget?.dispose(); + routeObserver.unsubscribe(this); + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangeDependencies() { + routeObserver.subscribe(this, ModalRoute.of(context)!); + super.didChangeDependencies(); + } + + @override + void didPopNext() { + _renderingWidget?.didPopNext(); + super.didPopNext(); + } + + @override + void didPushNext() { + unawaited(_renderingWidget?.clearPrevious()); + super.didPushNext(); + } + + @override + void didChangeMetrics() { + super.didChangeMetrics(); + _updateWebviewSize(); + } + + void _updateWebviewSize() { + if (_renderingWidget != null && + _renderingWidget is WebviewNFTRenderingWidget) { + // ignore: cast_nullable_to_non_nullable + (_renderingWidget as WebviewNFTRenderingWidget).updateWebviewSize(); + } + } + + @override + Widget build(BuildContext context) { + final previewUrl = widget.exhibition.series!.first.galleryURL; + final thumbnailUrl = widget.exhibition.series!.first.thumbnailURI; + return BlocProvider( + create: (_) => RetryCubit(), + child: BlocBuilder( + builder: (context, attempt) { + if (attempt > 0) { + _renderingWidget?.dispose(); + _renderingWidget = null; + } + _renderingWidget ??= buildFeralfileRenderingWidget( + context, + attempt: attempt > 0 ? attempt : null, + mimeType: widget.exhibition.series!.first.medium, + previewURL: previewUrl, + thumbnailURL: thumbnailUrl ?? '', + isScrollable: false, + ); + return Center( + child: _renderingWidget?.build(context) ?? const SizedBox(), + ); + }, + ), + ); + } +} diff --git a/lib/view/loading.dart b/lib/view/loading.dart new file mode 100644 index 0000000000..99da59c3ee --- /dev/null +++ b/lib/view/loading.dart @@ -0,0 +1,42 @@ +import 'package:autonomy_flutter/view/responsive.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:feralfile_app_theme/feral_file_app_theme.dart'; +import 'package:flutter/material.dart'; +import 'package:gif_view/gif_view.dart'; + +class LoadingWidget extends StatelessWidget { + final bool invertColors; + const LoadingWidget({super.key, this.invertColors = false}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Container( + width: double.infinity, + height: double.infinity, + color: AppColor.primaryBlack, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GifView.asset( + 'assets/images/loading_white.gif', + height: 52, + frameRate: 12, + invertColors: invertColors, + ), + const SizedBox(height: 12), + Text( + 'loading'.tr(), + style: ResponsiveLayout.isMobile + ? theme.textTheme.ppMori400White12 + : theme.textTheme.ppMori400White14, + ) + ], + ), + ), + ); + } +} diff --git a/lib/view/note_view.dart b/lib/view/note_view.dart index 35c84264f8..424d1a9aad 100644 --- a/lib/view/note_view.dart +++ b/lib/view/note_view.dart @@ -1,70 +1,71 @@ +import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/model/ff_exhibition.dart'; +import 'package:autonomy_flutter/service/navigation_service.dart'; +import 'package:autonomy_flutter/util/exhibition_ext.dart'; +import 'package:autonomy_flutter/util/style.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:feralfile_app_theme/feral_file_app_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; class ExhibitionNoteView extends StatelessWidget { - const ExhibitionNoteView( - {required this.exhibition, - this.width, - super.key, - this.onReadMore, - this.isFull = false}); + const ExhibitionNoteView({required this.exhibition, this.width, super.key}); final Exhibition exhibition; final double? width; - final Function? onReadMore; - final bool isFull; @override Widget build(BuildContext context) { final theme = Theme.of(context); - final text = isFull ? exhibition.note : exhibition.noteBrief; - return Container( - width: width, - padding: const EdgeInsets.all(14), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: AppColor.auGreyBackground, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'curators_note'.tr(), - style: theme.textTheme.ppMori400White12, - ), - const SizedBox(height: 30), - Text( - exhibition.noteTitle, - style: theme.textTheme.ppMori700White14, - ), - const SizedBox(height: 20), - ConstrainedBox( - constraints: - BoxConstraints(maxHeight: isFull ? double.infinity : 400), - child: HtmlWidget( - text, - textStyle: theme.textTheme.ppMori400White14, + return Center( + child: Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: AppColor.auGreyBackground, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + exhibition.isJohnGerrardShow + ? 'artist_note'.tr() + : 'curators_note'.tr(), + style: theme.textTheme.ppMori400White12, ), - ), - const SizedBox(height: 20), - if (onReadMore != null) - GestureDetector( - onTap: () async { - await onReadMore!(); - }, - child: Text( - 'read_more'.tr(), - style: theme.textTheme.ppMori400White14.copyWith( - decoration: TextDecoration.underline, - decorationColor: AppColor.white, - ), + const SizedBox(height: 30), + Text( + exhibition.noteTitle, + style: theme.textTheme.ppMori700White14, + ), + const SizedBox(height: 20), + ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 400), + child: HtmlWidget( + customStylesBuilder: auHtmlStyle, + exhibition.noteBrief, + textStyle: theme.textTheme.ppMori400White14, ), ), - ], + if (exhibition.noteBrief != exhibition.note) ...[ + const SizedBox(height: 20), + GestureDetector( + onTap: () async { + await injector() + .openFeralFileExhibitionNotePage(exhibition.slug); + }, + child: Text( + 'read_more'.tr(), + style: theme.textTheme.ppMori400White14.copyWith( + decoration: TextDecoration.underline, + decorationColor: AppColor.white, + ), + ), + ), + ] + ], + ), ), ); } diff --git a/lib/view/paging_bar.dart b/lib/view/paging_bar.dart new file mode 100644 index 0000000000..2e4f89170d --- /dev/null +++ b/lib/view/paging_bar.dart @@ -0,0 +1,110 @@ +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// Copyright © 2022 Bitmark. All rights reserved. +// Use of this source code is governed by the BSD-2-Clause Plus Patent License +// that can be found in the LICENSE file. +// + +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:autonomy_flutter/util/string_ext.dart'; +import 'package:feralfile_app_theme/feral_file_app_theme.dart'; +import 'package:flutter/material.dart'; + +class PagingBar extends StatelessWidget { + final Function(String value) onTap; + final Function() onDragging; + final Function() onDragEnd; + final String? selectedCharacter; + + const PagingBar( + {required this.onTap, + required this.onDragging, + required this.onDragEnd, + this.selectedCharacter, + super.key}); + + static const _height = 30.0; + static const _sensitivity = 5; + static const _dragAreaRatio = 4.0; + + @override + Widget build(BuildContext context) { + final width = MediaQuery.of(context).size.width - 30; + final int characterWidth = (width / listCharacters.length).floor() - 2; + final theme = Theme.of(context); + final index = selectedCharacter == null + ? 0 + : listCharacters.indexOf(selectedCharacter ?? listCharacters.first); + final delta = width / listCharacters.length; + final dragWidth = characterWidth * _dragAreaRatio; + final double dx = delta * index - characterWidth * (_dragAreaRatio - 1) / 2; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: SizedBox( + width: width, + height: _height, + child: Stack( + children: [ + Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: listCharacters + .map((e) => GestureDetector( + onTap: () => onTap(e), + child: SizedBox( + width: characterWidth.toDouble(), + child: AutoSizeText( + e, + style: theme.textTheme.ppMori400Grey14.copyWith( + color: e == selectedCharacter + ? AppColor.white + : AppColor.auQuickSilver), + textAlign: TextAlign.center, + maxLines: 1, + ), + ), + )) + .toList(), + ), + ), + Positioned( + left: dx, + child: Draggable( + axis: Axis.horizontal, + feedback: Material( + color: Colors.transparent, + child: _dragWidget(dragWidth), + ), + childWhenDragging: _dragWidget(dragWidth), + onDragEnd: (details) { + onDragEnd(); + }, + onDragUpdate: (details) { + onDragging(); + + final index = (details.localPosition.dx / delta).floor(); + if (index >= 0 && index < listCharacters.length) { + if (details.localPosition.dx - delta * index > + _sensitivity) { + return; + } + if (selectedCharacter != listCharacters[index]) { + onTap(listCharacters[index]); + } + } + }, + child: _dragWidget(dragWidth), + ), + ), + ], + ), + ), + ); + } + + Widget _dragWidget(double width) => Container( + width: width, + height: _height, + color: Colors.transparent, + ); +} diff --git a/lib/view/penrose_top_bar_view.dart b/lib/view/penrose_top_bar_view.dart deleted file mode 100644 index dcf4443be8..0000000000 --- a/lib/view/penrose_top_bar_view.dart +++ /dev/null @@ -1,236 +0,0 @@ -// -// SPDX-License-Identifier: BSD-2-Clause-Patent -// Copyright © 2022 Bitmark. All rights reserved. -// Use of this source code is governed by the BSD-2-Clause Plus Patent License -// that can be found in the LICENSE file. -// - -import 'dart:async'; -import 'dart:io'; - -import 'package:autonomy_flutter/common/injector.dart'; -import 'package:autonomy_flutter/main.dart'; -import 'package:autonomy_flutter/screen/app_router.dart'; -import 'package:autonomy_flutter/service/configuration_service.dart'; -import 'package:autonomy_flutter/util/style.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:feralfile_app_theme/feral_file_app_theme.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_svg/flutter_svg.dart'; - -enum PenroseTopBarViewStyle { - main, - back, - settings, -} - -class PenroseTopBarView extends StatefulWidget { - final ScrollController scrollController; - final PenroseTopBarViewStyle style; - - const PenroseTopBarView(this.scrollController, this.style, {super.key}); - - @override - State createState() => _PenroseTopBarViewState(); -} - -class _PenroseTopBarViewState extends State with RouteAware { - double _opacity = 1; - - @override - void initState() { - super.initState(); - - widget.scrollController.addListener(_scrollListener); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - routeObserver.subscribe(this, ModalRoute.of(context)!); - } - - @override - void dispose() { - super.dispose(); - routeObserver.unsubscribe(this); - } - - @override - void didPopNext() { - super.didPopNext(); - // Restore SystemUIMode - _scrollListener(); - } - - @override - void didPop() { - widget.scrollController.removeListener(_scrollListener); - } - - @override - void didPushNext() { - // Reset to normal SystemUIMode - if (Platform.isIOS) { - unawaited(SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, - overlays: SystemUiOverlay.values)); - } - } - - void _scrollListener() { - if (widget.scrollController.positions.isEmpty) { - // ScrollController not attached to any scroll views. - return; - } - - final breakpoint = - widget.style == PenroseTopBarViewStyle.settings ? 25 : 80; - - if (widget.scrollController.offset > breakpoint) { - if (Platform.isIOS) { - unawaited( - SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky)); - } - setState(() { - _opacity = 0; - }); - } else { - if (Platform.isIOS) { - unawaited(SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, - overlays: SystemUiOverlay.values)); - } - setState(() { - _opacity = 1; - }); - } - } - - @override - Widget build(BuildContext context) => SafeArea( - child: AnimatedBuilder( - builder: (context, value) => Stack(children: [ - Opacity(opacity: _opacity, child: _headerWidget(context)), - ]), - animation: widget.scrollController, - ), - ); - - Widget _headerWidget(BuildContext context) { - switch (widget.style) { - case PenroseTopBarViewStyle.main: - return Container( - alignment: Alignment.topCenter, - padding: const EdgeInsets.fromLTRB(7, 0, 2, 90), - child: _mainHeaderWidget(context, isInSettingsPage: false), - ); - case PenroseTopBarViewStyle.settings: - return Container( - alignment: Alignment.topCenter, - padding: const EdgeInsets.fromLTRB(7, 0, 2, 90), - child: _mainHeaderWidget(context, isInSettingsPage: true), - ); - case PenroseTopBarViewStyle.back: - return Container( - alignment: Alignment.topCenter, - padding: const EdgeInsets.fromLTRB(16, 12, 12, 90), - child: _backHeaderWidget(context), - ); - } - } - - Widget _backHeaderWidget(BuildContext context) { - final theme = Theme.of(context); - - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => Navigator.of(context).pop(), - child: Padding( - padding: const EdgeInsets.fromLTRB(0, 7, 18, 8), - child: Row( - children: [ - Row( - children: [ - SvgPicture.asset('assets/images/nav-arrow-left.svg'), - const SizedBox(width: 7), - Text( - 'back'.tr(), - style: theme.textTheme.labelLarge, - ), - ], - ), - ], - ), - ), - ); - } - - Widget _mainHeaderWidget(BuildContext context, - {required bool isInSettingsPage}) { - final theme = Theme.of(context); - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const SizedBox(width: 47), - Visibility( - visible: isInSettingsPage, - child: Padding( - padding: const EdgeInsets.fromLTRB(0, 0, 0, 10), - child: Text( - 'settings'.tr().toUpperCase(), - style: theme.textTheme.labelLarge, - ), - ), - ), - Container( - padding: const EdgeInsets.fromLTRB(0, 0, 0, 20), - child: Semantics( - label: isInSettingsPage ? 'close_icon' : 'Settings', - child: IconButton( - onPressed: () { - if (_opacity == 0) { - return; - } - if (isInSettingsPage) { - Navigator.of(context).pop(); - } else { - unawaited( - Navigator.of(context).pushNamed(AppRouter.settingsPage)); - } - }, - icon: isInSettingsPage ? closeIcon() : _settingIcon(), - ), - ), - ), - ], - ); - } - - Widget _settingIcon() { - final configService = injector(); - final hasPendingSettings = configService.hasPendingSettings() || - configService.shouldShowSubscriptionHint(); - return Stack( - clipBehavior: Clip.none, - children: [ - SvgPicture.asset('assets/images/userOutlinedIcon.svg'), - if (hasPendingSettings) ...[ - Positioned( - top: -1, - left: 14, - child: Align( - alignment: Alignment.topRight, - child: Container( - width: 10, - height: 10, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: AppColor.red, - ), - ), - )), - ] - ], - ); - } -} diff --git a/lib/view/post_view.dart b/lib/view/post_view.dart new file mode 100644 index 0000000000..32a2b905f3 --- /dev/null +++ b/lib/view/post_view.dart @@ -0,0 +1,118 @@ +import 'package:autonomy_flutter/common/injector.dart'; +import 'package:autonomy_flutter/model/ff_exhibition.dart'; +import 'package:autonomy_flutter/service/navigation_service.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:feralfile_app_theme/feral_file_app_theme.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; + +class ExhibitionPostView extends StatefulWidget { + final Post post; + final String exhibitionID; + + const ExhibitionPostView({ + required this.post, + required this.exhibitionID, + super.key, + }); + + @override + State createState() => _ExhibitionPostViewState(); +} + +class _ExhibitionPostViewState extends State { + late String? thumbnailUrl; + late int loadThumbnailFailedCount; + + @override + void initState() { + thumbnailUrl = widget.post.thumbnailUrls[0]; + loadThumbnailFailedCount = 0; + super.initState(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final dateFormat = DateFormat('EEEE, MMM d, y'); + final timeFormat = DateFormat('HH:mm'); + final dateTime = widget.post.dateTime ?? widget.post.createdAt; + return Center( + child: Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: AppColor.auGreyBackground, + ), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.post.displayType, + style: theme.textTheme.ppMori400White12, + ), + const SizedBox(height: 30), + _buildThumbnailWidget(), + const SizedBox(height: 20), + Text( + widget.post.title, + style: theme.textTheme.ppMori700White14, + ), + if (widget.post.type != 'close-up') ...[ + const SizedBox(height: 20), + Text( + 'Date: ${dateFormat.format(dateTime)}', + style: theme.textTheme.ppMori400White14, + ), + Text( + 'Time: ${timeFormat.format(dateTime)}', + style: theme.textTheme.ppMori400White14, + ), + ], + if (widget.post.author?.isNotEmpty ?? false) ...[ + const SizedBox(height: 10), + Text( + 'by ${widget.post.author}', + style: theme.textTheme.ppMori400White12, + ), + ], + const SizedBox(height: 20), + GestureDetector( + onTap: () async { + await injector() + .openFeralFilePostPage(widget.post, widget.exhibitionID); + }, + child: Text( + widget.post.type == 'close-up' + ? 'read_more'.tr() + : 'watch'.tr(), + style: theme.textTheme.ppMori400White14.copyWith( + decoration: TextDecoration.underline, + decorationColor: AppColor.white, + ), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildThumbnailWidget() => CachedNetworkImage( + imageUrl: thumbnailUrl!, + fit: BoxFit.fitWidth, + cacheManager: injector(), + errorWidget: (context, url, error) { + loadThumbnailFailedCount++; + if (loadThumbnailFailedCount >= widget.post.thumbnailUrls.length) { + return const SizedBox(); + } + thumbnailUrl = widget.post.thumbnailUrls[loadThumbnailFailedCount]; + return _buildThumbnailWidget(); + }, + ); +} diff --git a/lib/view/predefined_collection/predefined_collection_icon.dart b/lib/view/predefined_collection/predefined_collection_icon.dart new file mode 100644 index 0000000000..cdf641a73f --- /dev/null +++ b/lib/view/predefined_collection/predefined_collection_icon.dart @@ -0,0 +1,61 @@ +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// Copyright © 2022 Bitmark. All rights reserved. +// Use of this source code is governed by the BSD-2-Clause Plus Patent License +// that can be found in the LICENSE file. +// +import 'package:autonomy_flutter/screen/predefined_collection/predefined_collection_screen.dart'; +import 'package:autonomy_flutter/util/medium_category_ext.dart'; +import 'package:autonomy_flutter/util/predefined_collection_ext.dart'; +import 'package:autonomy_flutter/view/artwork_common_widget.dart'; +import 'package:feralfile_app_theme/feral_file_app_theme.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:nft_collection/models/predefined_collection_model.dart'; + +class PredefinedCollectionIcon extends StatelessWidget { + final PredefinedCollectionModel predefinedCollection; + final PredefinedCollectionType type; + + const PredefinedCollectionIcon( + {required this.predefinedCollection, required this.type, super.key}); + + static const iconArtistHeight = 42.0; + + static double get height => iconArtistHeight; + + @override + Widget build(BuildContext context) { + switch (type) { + case PredefinedCollectionType.medium: + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10), + width: 44, + height: 44, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: AppColor.auGreyBackground, + ), + child: SvgPicture.asset( + MediumCategoryExt.icon(predefinedCollection.id), + width: 22, + colorFilter: + const ColorFilter.mode(AppColor.white, BlendMode.srcIn), + ), + ); + case PredefinedCollectionType.artist: + final compactedAssetTokens = predefinedCollection.compactedAssetToken; + return SizedBox( + width: 42, + height: iconArtistHeight, + child: tokenGalleryThumbnailWidget(context, compactedAssetTokens, 100, + usingThumbnailID: false, + galleryThumbnailPlaceholder: Container( + width: 42, + height: 42, + color: AppColor.auLightGrey, + )), + ); + } + } +} diff --git a/lib/view/predefined_collection/predefined_collection_item.dart b/lib/view/predefined_collection/predefined_collection_item.dart new file mode 100644 index 0000000000..801920eb3f --- /dev/null +++ b/lib/view/predefined_collection/predefined_collection_item.dart @@ -0,0 +1,77 @@ +// +// SPDX-License-Identifier: BSD-2-Clause-Patent +// Copyright © 2022 Bitmark. All rights reserved. +// Use of this source code is governed by the BSD-2-Clause Plus Patent License +// that can be found in the LICENSE file. +// +import 'package:autonomy_flutter/screen/app_router.dart'; +import 'package:autonomy_flutter/screen/predefined_collection/predefined_collection_screen.dart'; +import 'package:autonomy_flutter/util/string_ext.dart'; +import 'package:autonomy_flutter/view/predefined_collection/predefined_collection_icon.dart'; +import 'package:feralfile_app_theme/extensions/theme_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:nft_collection/models/predefined_collection_model.dart'; + +class PredefinedCollectionItem extends StatelessWidget { + final PredefinedCollectionModel predefinedCollection; + final PredefinedCollectionType type; + final String searchStr; + + const PredefinedCollectionItem( + {required this.predefinedCollection, + required this.type, + required this.searchStr, + super.key}); + + static const verticalPadding = 15.0; + + static double get height => + verticalPadding * 2 + PredefinedCollectionIcon.height; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + var title = predefinedCollection.name ?? predefinedCollection.id; + if (predefinedCollection.name == predefinedCollection.id) { + title = title.maskOnly(5); + } + final titleStyle = theme.textTheme.ppMori400White14; + return GestureDetector( + onTap: () async { + await Navigator.pushNamed( + context, + AppRouter.predefinedCollectionPage, + arguments: PredefinedCollectionScreenPayload( + type: type, + predefinedCollection: predefinedCollection, + filterStr: searchStr, + ), + ); + }, + child: Container( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: verticalPadding), + child: Row( + children: [ + PredefinedCollectionIcon( + predefinedCollection: predefinedCollection, + type: type, + ), + const SizedBox(width: 33), + Expanded( + child: Text( + title, + style: titleStyle, + overflow: TextOverflow.ellipsis, + ), + ), + Text('${predefinedCollection.total}', + style: theme.textTheme.ppMori400Grey14), + ], + ), + ), + ), + ); + } +} diff --git a/lib/view/primary_button.dart b/lib/view/primary_button.dart index d8f48f00a8..3a75c21a61 100644 --- a/lib/view/primary_button.dart +++ b/lib/view/primary_button.dart @@ -12,6 +12,9 @@ class PrimaryButton extends StatelessWidget { final bool isProcessing; final bool enabled; final Color? indicatorColor; + final EdgeInsetsGeometry padding; + final EdgeInsetsGeometry? elevatedPadding; + final double borderRadius; const PrimaryButton({ super.key, @@ -24,6 +27,9 @@ class PrimaryButton extends StatelessWidget { this.enabled = true, this.isProcessing = false, this.indicatorColor, + this.padding = const EdgeInsets.symmetric(vertical: 13), + this.elevatedPadding, + this.borderRadius = 32, }); @override @@ -36,16 +42,17 @@ class PrimaryButton extends StatelessWidget { style: ElevatedButton.styleFrom( backgroundColor: enabled ? color ?? AppColor.feralFileHighlight : disabledColor, + padding: elevatedPadding, shadowColor: Colors.transparent, disabledForegroundColor: disabledColor, disabledBackgroundColor: disabledColor, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(32), + borderRadius: BorderRadius.circular(borderRadius), ), ), onPressed: enabled ? onTap : null, child: Padding( - padding: const EdgeInsets.symmetric(vertical: 13), + padding: padding, child: Center( child: Row( mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/view/search_bar.dart b/lib/view/search_bar.dart index 6adb84ee7b..b64b6f5069 100644 --- a/lib/view/search_bar.dart +++ b/lib/view/search_bar.dart @@ -4,7 +4,6 @@ import 'package:autonomy_flutter/util/au_icons.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:feralfile_app_theme/feral_file_app_theme.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.dart'; class AuSearchBar extends StatefulWidget { final Function(String)? onChanged; @@ -20,7 +19,6 @@ class AuSearchBar extends StatefulWidget { class _SearchBarState extends State { final _controller = TextEditingController(); final _focusNode = FocusNode(); - bool _isSearching = false; Timer? _timer; @override @@ -43,47 +41,30 @@ class _SearchBarState extends State { return Container( decoration: BoxDecoration( - color: AppColor.auLightGrey, + color: AppColor.auGreyBackground, borderRadius: BorderRadius.circular(5), ), - padding: const EdgeInsets.symmetric(horizontal: 10), + padding: const EdgeInsets.symmetric(horizontal: 14), child: Row( children: [ - SvgPicture.asset( - 'assets/images/search.svg', - width: 14, - height: 14, - colorFilter: const ColorFilter.mode( - AppColor.secondarySpanishGrey, BlendMode.srcIn), - ), - const SizedBox(width: 10), Expanded( child: Center( child: TextField( controller: _controller, focusNode: _focusNode, - style: theme.textTheme.ppMori400Black14, - cursorColor: AppColor.primaryBlack, + style: theme.textTheme.ppMori400White12, + cursorColor: AppColor.white, cursorWidth: 0.5, cursorHeight: 17, decoration: InputDecoration( // contentPadding: const EdgeInsets.only(bottom: 10), - hintText: 'search'.tr(), - hintStyle: theme.textTheme.ppMori400Grey14 - .copyWith(color: AppColor.secondarySpanishGrey), + hintText: 'search_by_'.tr(), + hintStyle: theme.textTheme.ppMori400Grey12 + .copyWith(color: AppColor.auQuickSilver), border: InputBorder.none, ), onChanged: (value) { widget.onChanged?.call(value); - if (value.isNotEmpty) { - setState(() { - _isSearching = true; - }); - } else { - setState(() { - _isSearching = false; - }); - } _timer?.cancel(); _timer = Timer(const Duration(milliseconds: 300), () { widget.onSearch?.call(value); @@ -95,20 +76,6 @@ class _SearchBarState extends State { ), ), ), - const SizedBox(width: 10), - if (_isSearching) - GestureDetector( - onTap: () { - _controller.clear(); - widget.onClear?.call(''); - widget.onChanged?.call(''); - }, - child: const Icon( - AuIcon.close, - size: 14, - color: AppColor.primaryBlack, - ), - ), ], ), ); @@ -132,25 +99,23 @@ class _ActionBarState extends State { } @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: widget.searchBar, - ), - const SizedBox(width: 14), - GestureDetector( - onTap: () { - widget.onCancel?.call(); - }, - child: Text( - 'Cancel', - style: theme.textTheme.ppMori400Grey14, + Widget build(BuildContext context) => Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: widget.searchBar, ), - ) - ], - ); - } + const SizedBox(width: 14), + GestureDetector( + onTap: () { + widget.onCancel?.call(); + }, + child: const Icon( + AuIcon.close, + size: 18, + color: AppColor.white, + ), + ) + ], + ); } diff --git a/lib/view/series_title_view.dart b/lib/view/series_title_view.dart index f88a7c3320..58e6994c91 100644 --- a/lib/view/series_title_view.dart +++ b/lib/view/series_title_view.dart @@ -1,5 +1,6 @@ import 'package:autonomy_flutter/model/ff_series.dart'; import 'package:autonomy_flutter/model/ff_user.dart'; +import 'package:autonomy_flutter/util/series_ext.dart'; import 'package:feralfile_app_theme/feral_file_app_theme.dart'; import 'package:flutter/material.dart'; @@ -23,7 +24,7 @@ class SeriesTitleView extends StatelessWidget { ), const SizedBox(height: 3), Text( - series.title, + series.displayTitle, style: theme.textTheme.ppMori700White14.copyWith( fontStyle: FontStyle.italic, ), diff --git a/lib/view/stream_common_widget.dart b/lib/view/stream_common_widget.dart new file mode 100644 index 0000000000..9f92af8a57 --- /dev/null +++ b/lib/view/stream_common_widget.dart @@ -0,0 +1,483 @@ +import 'dart:async'; + +import 'package:autonomy_flutter/common/injector.dart'; +import 'package:autonomy_flutter/screen/detail/preview/canvas_device_bloc.dart'; +import 'package:autonomy_flutter/util/range_input_formatter.dart'; +import 'package:autonomy_flutter/util/ui_helper.dart'; +import 'package:autonomy_flutter/view/responsive.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:feralfile_app_theme/feral_file_app_theme.dart'; +import 'package:feralfile_app_tv_proto/feralfile_app_tv_proto.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:rxdart/rxdart.dart'; + +final speedValues = { + // '5sec': const Duration(seconds: 5), + // '10sec': const Duration(seconds: 10), + // '15sec': const Duration(seconds: 15), + // '30sec': const Duration(seconds: 30), + '1min': const Duration(minutes: 1), + '2min': const Duration(minutes: 2), + '5min': const Duration(minutes: 5), + '10min': const Duration(minutes: 10), + '15min': const Duration(minutes: 15), + '30min': const Duration(minutes: 30), + '1hr': const Duration(hours: 1), + '4hr': const Duration(hours: 4), + '12hr': const Duration(hours: 12), + '24hr': const Duration(hours: 24), +}; + +class StreamDrawerItem extends StatelessWidget { + final OptionItem item; + final Color backgroundColor; + final Function()? onRotateClicked; + final bool isControlling; + + static const double rotateIconSize = 22; + + const StreamDrawerItem({ + required this.item, + required this.backgroundColor, + required this.isControlling, + super.key, + this.onRotateClicked, + }); + + @override + Widget build(BuildContext context) => Container( + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(50), + ), + width: MediaQuery.of(context).size.width, + child: Material( + type: MaterialType.transparency, + child: Stack( + children: [ + InkWell( + splashFactory: InkSparkle.splashFactory, + borderRadius: BorderRadius.circular(50), + child: Padding( + padding: EdgeInsets.symmetric( + vertical: 12, + horizontal: ResponsiveLayout.padding + rotateIconSize + 10, + ), + child: Center( + child: Text( + item.title ?? '', + style: Theme.of(context).textTheme.ppMori400Black14, + ), + ), + ), + onTap: () => item.onTap?.call(), + ), + if (isControlling) + Positioned( + top: 0, + bottom: 0, + right: ResponsiveLayout.padding, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GestureDetector( + onTap: onRotateClicked, + child: SvgPicture.asset( + 'assets/images/icon_rotate.svg', + width: rotateIconSize, + height: rotateIconSize, + ), + ), + ], + ), + ) + ], + ), + ), + ); +} + +class PlaylistControl extends StatefulWidget { + final String displayKey; + + const PlaylistControl({required this.displayKey, super.key}); + + @override + State createState() => _PlaylistControlState(); +} + +class _PlaylistControlState extends State { + Timer? _timer; + late CanvasDeviceBloc _canvasDeviceBloc; + CanvasDevice? _controllingDevice; + + @override + void initState() { + super.initState(); + _canvasDeviceBloc = injector.get(); + } + + @override + void dispose() { + super.dispose(); + _timer?.cancel(); + } + + @override + Widget build(BuildContext context) => + BlocBuilder( + bloc: _canvasDeviceBloc, + builder: (context, state) { + _controllingDevice = + state.lastSelectedActiveDeviceForKey(widget.displayKey); + return Container( + padding: const EdgeInsets.all(15), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: AppColor.auGreyBackground, + ), + child: Column( + children: [ + _buildPlayControls(context, state), + const SizedBox(height: 15), + _buildSpeedControl(context, state), + ], + )); + }, + ); + + Widget _buildPlayButton({required String icon, required Function() onTap}) => + Expanded( + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: AppColor.primaryBlack, + ), + child: Material( + type: MaterialType.transparency, + child: InkWell( + splashFactory: InkSparkle.splashFactory, + borderRadius: BorderRadius.circular(5), + onTap: onTap, + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 30, vertical: 10), + child: SvgPicture.asset( + icon, + ), + ), + ), + ), + ), + ); + + Widget _buildPlayControls(BuildContext context, CanvasDeviceState state) { + final isCasting = _controllingDevice != null; + return Row( + children: [ + _buildPlayButton( + icon: 'assets/images/chevron_left_icon.svg', + onTap: () => { + onPrevious(context), + }), + const SizedBox(width: 15), + _buildPlayButton( + icon: isCasting + ? 'assets/images/stream_pause_icon.svg' + : 'assets/images/stream_play_icon.svg', + onTap: () => { + onPauseOrResume(context), + }), + const SizedBox(width: 15), + _buildPlayButton( + icon: 'assets/images/chevron_right_icon.svg', + onTap: () => { + onNext(context), + }), + ], + ); + } + + Widget _buildSpeedControl(BuildContext context, CanvasDeviceState state) => + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'autoplay_duration'.tr(), + style: Theme.of(context).textTheme.ppMori400White12, + ), + const SizedBox(height: 4), + Container( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: AppColor.primaryBlack, + ), + child: ArtworkDurationControl( + duration: state.castingSpeed(widget.displayKey), + displayKey: widget.displayKey, + ), + ) + ], + ); + + void onPrevious(BuildContext context) { + if (_controllingDevice == null) { + return; + } + _canvasDeviceBloc + .add(CanvasDevicePreviousArtworkEvent(_controllingDevice!)); + } + + void onNext(BuildContext context) { + if (_controllingDevice == null) { + return; + } + _canvasDeviceBloc.add(CanvasDeviceNextArtworkEvent(_controllingDevice!)); + } + + void onPause(BuildContext context) { + if (_controllingDevice == null) { + return; + } + _canvasDeviceBloc.add(CanvasDevicePauseCastingEvent(_controllingDevice!)); + } + + void onResume(BuildContext context) { + if (_controllingDevice == null) { + return; + } + _canvasDeviceBloc.add(CanvasDeviceResumeCastingEvent(_controllingDevice!)); + } + + void onPauseOrResume(BuildContext context) { + // final _canvasDeviceBloc = context.read(); + final isCasting = _controllingDevice != null; + if (isCasting) { + onPause(context); + } else { + onResume(context); + } + } +} + +class ArtworkDurationControl extends StatefulWidget { + final Duration? duration; + final String displayKey; + + const ArtworkDurationControl( + {required this.displayKey, super.key, this.duration}); + + @override + State createState() => _ArtworkDurationControlState(); +} + +class _ArtworkDurationControlState extends State { + late FocusNode dayFocusNode; + late FocusNode hourFocusNode; + late FocusNode minFocusNode; + late TextEditingController dayTextController; + late TextEditingController hourTextController; + late TextEditingController minTextController; + late bool isAnyFieldFocused = false; + final _durationSubject = PublishSubject(); + final _canvasDeviceBloc = injector.get(); + Timer? _timer; + + @override + void initState() { + super.initState(); + + dayFocusNode = FocusNode(); + hourFocusNode = FocusNode(); + minFocusNode = FocusNode(); + + int? day; + int? hour; + int? min; + + if (widget.duration != null) { + day = widget.duration!.inDays; + hour = widget.duration!.inHours % 24; + min = widget.duration!.inMinutes % 60; + } + + dayTextController = + TextEditingController(text: day?.toString().padLeft(2, '0')); + hourTextController = + TextEditingController(text: hour?.toString().padLeft(2, '0')); + minTextController = + TextEditingController(text: min?.toString().padLeft(2, '0')); + + dayFocusNode.addListener(_focusChanged); + hourFocusNode.addListener(_focusChanged); + minFocusNode.addListener(_focusChanged); + + _durationSubject.stream + .debounceTime(const Duration(milliseconds: 1000)) + .listen((duration) { + _durationChanged(duration); + }); + } + + void _focusChanged() { + setState(() { + isAnyFieldFocused = dayFocusNode.hasFocus || + hourFocusNode.hasFocus || + minFocusNode.hasFocus; + }); + } + + @override + Future dispose() async { + dayFocusNode.dispose(); + hourFocusNode.dispose(); + minFocusNode.dispose(); + dayTextController.dispose(); + hourTextController.dispose(); + minTextController.dispose(); + _timer?.cancel(); + await _durationSubject.close(); + super.dispose(); + } + + void _durationChanged(Duration duration) { + _timer?.cancel(); + _timer = Timer( + const Duration(milliseconds: 300), + () { + changeSpeed(duration); + }, + ); + } + + void changeSpeed(Duration duration) { + final lastSelectedCanvasDevice = _canvasDeviceBloc.state + .lastSelectedActiveDeviceForKey(widget.displayKey); + if (lastSelectedCanvasDevice == null) { + return; + } + final canvasStatus = + _canvasDeviceBloc.state.statusOf(lastSelectedCanvasDevice); + if (canvasStatus == null) { + return; + } + final playArtworks = canvasStatus.artworks; + final playArtworkWithNewDuration = playArtworks + .map((e) => + e.copy(duration: Duration(milliseconds: duration.inMilliseconds))) + .toList(); + _canvasDeviceBloc.add(CanvasDeviceUpdateDurationEvent( + lastSelectedCanvasDevice, playArtworkWithNewDuration)); + } + + @override + Widget build(BuildContext context) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _durationWidget( + suffixText: 'Days', + focusNode: dayFocusNode, + textController: dayTextController, + onValueChanged: (value) { + _durationSubject.add( + Duration( + days: value, + hours: int.tryParse(hourTextController.text) ?? 0, + minutes: int.tryParse(minTextController.text) ?? 0, + ), + ); + }), + _durationWidget( + suffixText: 'Hours', + maxValue: 23, + focusNode: hourFocusNode, + textController: hourTextController, + onValueChanged: (value) { + _durationSubject.add( + Duration( + days: int.tryParse(dayTextController.text) ?? 0, + hours: value, + minutes: int.tryParse(minTextController.text) ?? 0, + ), + ); + }), + _durationWidget( + suffixText: 'Mins', + maxValue: 59, + focusNode: minFocusNode, + textController: minTextController, + onValueChanged: (value) { + _durationSubject.add( + Duration( + days: int.tryParse(dayTextController.text) ?? 0, + hours: int.tryParse(hourTextController.text) ?? 0, + minutes: value, + ), + ); + }), + ], + ); + + Widget _durationWidget({ + required String suffixText, + required Function(int value) onValueChanged, + required FocusNode focusNode, + required TextEditingController textController, + int? maxValue, + }) { + final textStyle = isAnyFieldFocused && !focusNode.hasFocus + ? Theme.of(context).textTheme.ppMori400Grey12 + : Theme.of(context).textTheme.ppMori400White12; + + return IntrinsicWidth( + child: TextField( + enableInteractiveSelection: false, + controller: textController, + focusNode: focusNode, + keyboardType: TextInputType.number, + style: textStyle, + cursorWidth: 1, + decoration: InputDecoration( + hintText: '00', + hintStyle: textStyle, + isCollapsed: true, + isDense: true, + border: InputBorder.none, + suffixIcon: Container( + margin: const EdgeInsets.only(left: 8, top: 1), + child: Text( + suffixText, + style: textStyle, + ), + ), + suffixIconConstraints: const BoxConstraints(), + ), + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + RangeTextInputFormatter(min: 0, max: maxValue), + ], + onTapOutside: (event) => focusNode.unfocus(), + onChanged: (value) { + onValueChanged(int.tryParse(value) ?? 0); + }, + ), + ); + } +} + +extension PlayArtworkExt on PlayArtworkV2 { + PlayArtworkV2 copy({ + CastAssetToken? token, + CastArtwork? artwork, + Duration? duration, + }) => + PlayArtworkV2( + token: token ?? this.token, + artwork: artwork ?? this.artwork, + duration: duration?.inMilliseconds ?? this.duration, + ); +} diff --git a/lib/view/stream_device_view.dart b/lib/view/stream_device_view.dart new file mode 100644 index 0000000000..1f9627eee1 --- /dev/null +++ b/lib/view/stream_device_view.dart @@ -0,0 +1,217 @@ +import 'dart:async'; + +import 'package:autonomy_flutter/common/injector.dart'; +import 'package:autonomy_flutter/screen/app_router.dart'; +import 'package:autonomy_flutter/screen/detail/preview/canvas_device_bloc.dart'; +import 'package:autonomy_flutter/screen/scan_qr/scan_qr_page.dart'; +import 'package:autonomy_flutter/util/log.dart'; +import 'package:autonomy_flutter/util/ui_helper.dart'; +import 'package:autonomy_flutter/view/responsive.dart'; +import 'package:autonomy_flutter/view/stream_common_widget.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:feralfile_app_theme/feral_file_app_theme.dart'; +import 'package:feralfile_app_tv_proto/models/canvas_device.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class StreamDeviceView extends StatefulWidget { + final Function(CanvasDevice device)? onDeviceSelected; + final String? displayKey; + + const StreamDeviceView({ + super.key, + this.onDeviceSelected, + this.displayKey, + }); + + @override + State createState() => _StreamDeviceViewState(); +} + +class _StreamDeviceViewState extends State { + late final CanvasDeviceBloc _canvasDeviceBloc; + + @override + void initState() { + super.initState(); + _canvasDeviceBloc = injector.get(); + unawaited(_fetchDevice()); + } + + Future _fetchDevice() async { + _canvasDeviceBloc.add(CanvasDeviceGetDevicesEvent()); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return BlocBuilder( + bloc: _canvasDeviceBloc, + builder: (context, state) { + final devices = state.devices; + final connectedDevice = widget.displayKey == null + ? null + : state.lastSelectedActiveDeviceForKey(widget.displayKey!); + return Padding( + padding: ResponsiveLayout.pageHorizontalEdgeInsets, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: 'display'.tr(), + style: theme.textTheme.ppMori700White24, + ), + if (connectedDevice != null) + TextSpan( + text: ' ${connectedDevice.name}', + style: theme.textTheme.ppMori400White24, + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.only(left: 10), + child: GestureDetector( + onTap: () => Navigator.pop(context), + child: SvgPicture.asset( + 'assets/images/circle_close.svg', + width: 22, + height: 22, + ), + ), + ) + ], + ), + const SizedBox(height: 40), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + itemCount: devices.length, + itemBuilder: (BuildContext context, int index) { + final device = devices[index].device; + final isControlling = + device.deviceId == connectedDevice?.deviceId; + return Column( + children: [ + Builder( + builder: (context) => StreamDrawerItem( + item: OptionItem( + title: device.name, + onTap: () { + log.info('device selected: ${device.deviceId}'); + widget.onDeviceSelected?.call(device); + }), + backgroundColor: connectedDevice == null + ? AppColor.white + : isControlling + ? AppColor.feralFileLightBlue + : AppColor.disabledColor, + isControlling: isControlling, + onRotateClicked: () => onRotate(context), + ), + ), + if (index < devices.length - 1) + const SizedBox( + height: 15, + ) + ], + ); + }, + ), + const SizedBox(height: 40), + RichText( + text: TextSpan( + children: [ + TextSpan( + text: 'not_find_canvas'.tr(), + style: theme.textTheme.ppMori400White14, + ), + // text clickable + TextSpan( + text: 'scan_the_qrcode'.tr(), + style: theme.textTheme.ppMori400White14.copyWith( + decoration: TextDecoration.underline, + ), + recognizer: TapGestureRecognizer() + ..onTap = () async { + await _scanToAddMore(context); + }, + ), + TextSpan( + text: 'that_appear_on_canvas'.tr(), + style: theme.textTheme.ppMori400White14, + ) + ], + )), + if (connectedDevice != null) ...[ + const SizedBox( + height: 40, + ), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(50), + border: Border.all( + color: AppColor.white, + ), + ), + width: MediaQuery.of(context).size.width, + child: Material( + type: MaterialType.transparency, + child: InkWell( + splashFactory: InkSparkle.splashFactory, + borderRadius: BorderRadius.circular(50), + child: Padding( + padding: const EdgeInsets.all(12), + child: Center( + child: Text( + 'disconnect'.tr(), + style: theme.textTheme.ppMori400White14, + ), + ), + ), + onTap: () async { + await onDisconnect(); + }, + ), + ), + ), + ] + ], + ), + ); + }, + ); + } + + void onRotate(BuildContext context) { + final lastSelectedCanvasDevice = _canvasDeviceBloc.state + .lastSelectedActiveDeviceForKey(widget.displayKey!); + if (lastSelectedCanvasDevice != null) { + _canvasDeviceBloc.add(CanvasDeviceRotateEvent(lastSelectedCanvasDevice)); + } + } + + Future _scanToAddMore(BuildContext context) async { + final device = await Navigator.of(context) + .pushNamed(AppRouter.scanQRPage, arguments: ScannerItem.CANVAS); + log.info('device selected: $device'); + _canvasDeviceBloc.add(CanvasDeviceGetDevicesEvent()); + } + + Future onDisconnect() async { + final allDevices = + _canvasDeviceBloc.state.devices.map((e) => e.device).toList(); + _canvasDeviceBloc.add(CanvasDeviceDisconnectEvent(allDevices)); + } +} diff --git a/lib/view/title_text.dart b/lib/view/title_text.dart new file mode 100644 index 0000000000..12bfaf18c8 --- /dev/null +++ b/lib/view/title_text.dart @@ -0,0 +1,14 @@ +import 'package:feralfile_app_theme/feral_file_app_theme.dart'; +import 'package:flutter/material.dart'; + +class TitleText extends StatelessWidget { + const TitleText({required this.title, super.key, this.style}); + + final String title; + final TextStyle? style; + + @override + Widget build(BuildContext context) => Text(title, + style: style ?? + Theme.of(context).textTheme.ppMori700White24.copyWith(fontSize: 36)); +} diff --git a/lib/view/touchpad.dart b/lib/view/touchpad.dart index fbb7fe7f72..a73cf958a2 100644 --- a/lib/view/touchpad.dart +++ b/lib/view/touchpad.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:after_layout/after_layout.dart'; import 'package:autonomy_flutter/common/injector.dart'; -import 'package:autonomy_flutter/service/canvas_client_service.dart'; +import 'package:autonomy_flutter/service/canvas_client_service_v2.dart'; import 'package:autonomy_flutter/util/log.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:feralfile_app_theme/feral_file_app_theme.dart'; @@ -21,7 +21,7 @@ class TouchPad extends StatefulWidget { } class _TouchPadState extends State with AfterLayoutMixin { - final _canvasClient = injector(); + final _canvasClient = injector(); final _touchPadKey = GlobalKey(); Size? _touchpadSize; diff --git a/pubspec.lock b/pubspec.lock index e98edc015b..5a064f070a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -105,6 +105,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.8.3" + backdrop: + dependency: "direct main" + description: + name: backdrop + sha256: cb7450b465b638835cf5908ee96785dd7d324029beb96fa7a7b6d81216610cda + url: "https://pub.dev" + source: hosted + version: "0.9.1" background_fetch: dependency: "direct main" description: @@ -253,26 +261,26 @@ packages: dependency: "direct main" description: name: cached_network_image - sha256: f98972704692ba679db144261172a8e20feb145636c617af0eb4022132a6797f + sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f" url: "https://pub.dev" source: hosted - version: "3.3.0" + version: "3.3.1" cached_network_image_platform_interface: dependency: transitive description: name: cached_network_image_platform_interface - sha256: "56aa42a7a01e3c9db8456d9f3f999931f1e05535b5a424271e9a38cabf066613" + sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "4.0.0" cached_network_image_web: dependency: transitive description: name: cached_network_image_web - sha256: "759b9a9f8f6ccbb66c185df805fac107f05730b1dab9c64626d1008cca532257" + sha256: "205d6a9f1862de34b93184f22b9d2d94586b2f05c581d546695e3d8f6a805cd7" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.0" card_swiper: dependency: "direct main" description: @@ -606,8 +614,8 @@ packages: dependency: "direct main" description: path: "." - ref: "8cb04ea43f0b78fd27c6fd96d9e273b541d10903" - resolved-ref: "8cb04ea43f0b78fd27c6fd96d9e273b541d10903" + ref: "8d791a051a9b38791e82ac1582176d1b4e8e74fa" + resolved-ref: "8d791a051a9b38791e82ac1582176d1b4e8e74fa" url: "https://github.com/bitmark-inc/feralfile-app-client-shared-theme" source: git version: "0.0.1+20" @@ -615,8 +623,8 @@ packages: dependency: "direct main" description: path: "." - ref: e050458005e8eea9ef8988bdd1c3a1469a9572ff - resolved-ref: e050458005e8eea9ef8988bdd1c3a1469a9572ff + ref: "18fcb34f2a6966312b2ca0aae858621ab89295f4" + resolved-ref: "18fcb34f2a6966312b2ca0aae858621ab89295f4" url: "https://github.com/autonomy-system/feralfile-app-tv-proto-communication" source: git version: "0.0.1" @@ -1013,6 +1021,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + flutter_staggered_grid_view: + dependency: transitive + description: + name: flutter_staggered_grid_view + sha256: "19e7abb550c96fbfeb546b23f3ff356ee7c59a019a651f8f102a4ba9b7349395" + url: "https://pub.dev" + source: hosted + version: "0.7.0" flutter_svg: dependency: "direct main" description: @@ -1552,10 +1568,10 @@ packages: dependency: "direct main" description: name: infinite_scroll_pagination - sha256: "9517328f4e373f08f57dbb11c5aac5b05554142024d6b60c903f3b73476d52db" + sha256: b68bce20752fcf36c7739e60de4175494f74e99e9a69b4dd2fe3a1dd07a7f16a url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" injector: dependency: transitive description: @@ -1661,26 +1677,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.4" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.3" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" libauk_dart: dependency: "direct main" description: @@ -1718,10 +1734,10 @@ packages: dependency: "direct main" description: name: local_auth - sha256: "0cf238be2bfa51a6c9e7e9cfc11c05ea39f2a3a4d3e5bb255d0ebc917da24401" + sha256: "280421b416b32de31405b0a25c3bd42dfcef2538dfbb20c03019e02a5ed55ed0" url: "https://pub.dev" source: hosted - version: "2.1.6" + version: "2.2.0" local_auth_android: dependency: transitive description: @@ -1730,14 +1746,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.32" - local_auth_ios: + local_auth_darwin: dependency: transitive description: - name: local_auth_ios - sha256: edc2977c5145492f3451db9507a2f2f284ee4f408950b3e16670838726761940 + name: local_auth_darwin + sha256: e424ebf90d5233452be146d4a7da4bcd7a70278b67791592f3fde1bda8eef9e2 url: "https://pub.dev" source: hosted - version: "1.1.3" + version: "1.3.1" local_auth_platform_interface: dependency: transitive description: @@ -1778,14 +1794,6 @@ packages: url: "https://pub.dev" source: hosted version: "7.1.1" - marqueer: - dependency: "direct main" - description: - name: marqueer - sha256: "15dcb57f7b8cd621eba9c45761d6a7997135e6ac219dd420fede254f828b84dd" - url: "https://pub.dev" - source: hosted - version: "1.4.0" matcher: dependency: transitive description: @@ -1822,10 +1830,10 @@ packages: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.12.0" mime: dependency: "direct main" description: @@ -1846,10 +1854,10 @@ packages: dependency: "direct dev" description: name: mockito - sha256: "7d5b53bcd556c1bc7ffbe4e4d5a19c3e112b7e925e9e172dd7c6ad0630812616" + sha256: "6841eed20a7befac0ce07df8116c8b8233ed1f4486a7647c7fc5a02ae6163917" url: "https://pub.dev" source: hosted - version: "5.4.2" + version: "5.4.4" model_viewer_plus: dependency: transitive description: @@ -1874,12 +1882,28 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + network_info_plus: + dependency: "direct main" + description: + name: network_info_plus + sha256: "5bd4b86e28fed5ed4e6ac7764133c031dfb7d3f46aa2a81b46f55038aa78ecc0" + url: "https://pub.dev" + source: hosted + version: "5.0.3" + network_info_plus_platform_interface: + dependency: transitive + description: + name: network_info_plus_platform_interface + sha256: "2e193d61d3072ac17824638793d3b89c6d581ce90c11604f4ca87311b42f2706" + url: "https://pub.dev" + source: hosted + version: "2.0.0" nft_collection: dependency: "direct main" description: path: "." - ref: "47525c5b29164c8421493a4d3d337bc9f8d5fd89" - resolved-ref: "47525c5b29164c8421493a4d3d337bc9f8d5fd89" + ref: a52716f756e11367bef636455b5488937761405e + resolved-ref: a52716f756e11367bef636455b5488937761405e url: "https://github.com/autonomy-system/nft-collection" source: git version: "0.0.1" @@ -1887,8 +1911,8 @@ packages: dependency: "direct main" description: path: "." - ref: "0d4cce1bd966cf95edfdf36d5f54505b8534b995" - resolved-ref: "0d4cce1bd966cf95edfdf36d5f54505b8534b995" + ref: "039391471be2144e8539c2b71847c3e81b3983e0" + resolved-ref: "039391471be2144e8539c2b71847c3e81b3983e0" url: "https://github.com/autonomy-system/nft-rendering.git" source: git version: "1.0.9" @@ -2269,7 +2293,7 @@ packages: source: hosted version: "7.0.8" retry: - dependency: transitive + dependency: "direct main" description: name: retry sha256: "822e118d5b3aafed083109c72d5f484c6dc66707885e07c0fbcb8b986bba7efc" @@ -2669,26 +2693,26 @@ packages: dependency: "direct dev" description: name: test - sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f + sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073" url: "https://pub.dev" source: hosted - version: "1.24.9" + version: "1.25.2" test_api: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.0" test_core: dependency: transitive description: name: test_core - sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a + sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4" url: "https://pub.dev" source: hosted - version: "0.5.9" + version: "0.6.0" tezart: dependency: "direct main" description: @@ -2942,10 +2966,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.2.1" wakelock_plus: dependency: "direct main" description: @@ -3115,5 +3139,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.3.0-279.1.beta <4.0.0" - flutter: ">=3.16.6" + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/pubspec.yaml b/pubspec.yaml index 1aa5618094..3d22f44a34 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,13 +18,12 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev version: 0.36.0+121 environment: - sdk: ">=2.17.0 <4.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: after_layout: ^1.2.0 bs58check: ^1.0.2 bubble: ^1.2.1 - cached_network_image: ^3.2.1 carousel_slider: ^4.2.1 collection: ^1.15.0 confetti: ^0.7.0 @@ -39,7 +38,6 @@ dependencies: flutter: sdk: flutter flutter_bloc: ^8.0.0 - flutter_cache_manager: ^3.3.0 flutter_chat_types: ^3.3.1 flutter_chat_ui: ^1.6.4 flutter_dotenv: ^5.0.2 @@ -62,7 +60,7 @@ dependencies: http: ^1.1.0 image_picker: ^1.0.1 in_app_purchase: ^3.1.13 - infinite_scroll_pagination: ^3.2.0 + infinite_scroll_pagination: ^4.0.0 intl: ^0.18.0 jiffy: ^6.2.1 flutter_keyboard_visibility: ^6.0.0 @@ -78,7 +76,7 @@ dependencies: nft_rendering: git: url: https://github.com/autonomy-system/nft-rendering.git - ref: 0d4cce1bd966cf95edfdf36d5f54505b8534b995 + ref: 039391471be2144e8539c2b71847c3e81b3983e0 onesignal_flutter: ^3.3.0 open_settings: ^2.0.2 overlay_support: ^2.0.0 @@ -112,8 +110,7 @@ dependencies: feralfile_app_theme: git: url: https://github.com/bitmark-inc/feralfile-app-client-shared-theme - ref: 8cb04ea43f0b78fd27c6fd96d9e273b541d10903 - marqueer: ^1.2.0 + ref: 8d791a051a9b38791e82ac1582176d1b4e8e74fa markdown: ^7.1.1 easy_localization: ^3.0.1 rxdart: ^0.27.1 @@ -121,11 +118,11 @@ dependencies: nft_collection: git: url: https://github.com/autonomy-system/nft-collection - ref: 47525c5b29164c8421493a4d3d337bc9f8d5fd89 + ref: a52716f756e11367bef636455b5488937761405e feralfile_app_tv_proto: git: url: https://github.com/autonomy-system/feralfile-app-tv-proto-communication - ref: e050458005e8eea9ef8988bdd1c3a1469a9572ff + ref: 18fcb34f2a6966312b2ca0aae858621ab89295f4 flutter_branch_sdk: ^7.0.2 flutter_rating_bar: ^4.0.1 multi_value_listenable_builder: ^0.0.2 @@ -162,6 +159,12 @@ dependencies: image_gallery_saver: ^2.0.3 hand_signature: ^3.0.1 flutter_pdfview: ^1.3.2 + retry: ^3.1.2 + + network_info_plus: ^5.0.3 + backdrop: ^0.9.1 + cached_network_image: ^3.3.1 + flutter_cache_manager: ^3.3.1 cryptography: ^2.7.0 cryptography_flutter: ^2.3.2 dependency_overrides: diff --git a/test/generate_mock/dao/mock_asset_token_dao.mocks.dart b/test/generate_mock/dao/mock_asset_token_dao.mocks.dart index a98e9ccc84..05e1e33ef5 100644 --- a/test/generate_mock/dao/mock_asset_token_dao.mocks.dart +++ b/test/generate_mock/dao/mock_asset_token_dao.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.2 from annotations +// Mocks generated by Mockito 5.4.4 from annotations // in autonomy_flutter/test/generate_mock/dao/mock_asset_token_dao.dart. // Do not manually edit this file. @@ -14,6 +14,8 @@ import 'package:sqflite/sqflite.dart' as _i2; // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: prefer_const_constructors diff --git a/test/generate_mock/gateway/mock_activation_api.dart b/test/generate_mock/gateway/mock_activation_api.dart deleted file mode 100644 index 9283eddfd3..0000000000 --- a/test/generate_mock/gateway/mock_activation_api.dart +++ /dev/null @@ -1,5 +0,0 @@ -@GenerateMocks([ - ActivationApi, -]) -import 'package:autonomy_flutter/gateway/activation_api.dart'; -import 'package:mockito/annotations.dart'; diff --git a/test/generate_mock/gateway/mock_activation_api.mocks.dart b/test/generate_mock/gateway/mock_activation_api.mocks.dart deleted file mode 100644 index 6424bcc2de..0000000000 --- a/test/generate_mock/gateway/mock_activation_api.mocks.dart +++ /dev/null @@ -1,84 +0,0 @@ -// Mocks generated by Mockito 5.4.2 from annotations -// in autonomy_flutter/test/generate_mock/gateway/mock_activation_api.dart. -// Do not manually edit this file. - -// ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i3; - -import 'package:autonomy_flutter/gateway/activation_api.dart' as _i2; -import 'package:mockito/mockito.dart' as _i1; - -// ignore_for_file: type=lint -// ignore_for_file: avoid_redundant_argument_values -// ignore_for_file: avoid_setters_without_getters -// ignore_for_file: comment_references -// ignore_for_file: implementation_imports -// ignore_for_file: invalid_use_of_visible_for_testing_member -// ignore_for_file: prefer_const_constructors -// ignore_for_file: unnecessary_parenthesis -// ignore_for_file: camel_case_types -// ignore_for_file: subtype_of_sealed_class - -class _FakeActivationInfo_0 extends _i1.SmartFake - implements _i2.ActivationInfo { - _FakeActivationInfo_0( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeActivationClaimResponse_1 extends _i1.SmartFake - implements _i2.ActivationClaimResponse { - _FakeActivationClaimResponse_1( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -/// A class which mocks [ActivationApi]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockActivationApi extends _i1.Mock implements _i2.ActivationApi { - MockActivationApi() { - _i1.throwOnMissingStub(this); - } - - @override - _i3.Future<_i2.ActivationInfo> getActivation(String? activationId) => - (super.noSuchMethod( - Invocation.method( - #getActivation, - [activationId], - ), - returnValue: _i3.Future<_i2.ActivationInfo>.value(_FakeActivationInfo_0( - this, - Invocation.method( - #getActivation, - [activationId], - ), - )), - ) as _i3.Future<_i2.ActivationInfo>); - @override - _i3.Future<_i2.ActivationClaimResponse> claim( - _i2.ActivationClaimRequest? body) => - (super.noSuchMethod( - Invocation.method( - #claim, - [body], - ), - returnValue: _i3.Future<_i2.ActivationClaimResponse>.value( - _FakeActivationClaimResponse_1( - this, - Invocation.method( - #claim, - [body], - ), - )), - ) as _i3.Future<_i2.ActivationClaimResponse>); -} diff --git a/test/generate_mock/gateway/mock_airdrop_api.dart b/test/generate_mock/gateway/mock_airdrop_api.dart deleted file mode 100644 index 3697ed32ab..0000000000 --- a/test/generate_mock/gateway/mock_airdrop_api.dart +++ /dev/null @@ -1,5 +0,0 @@ -@GenerateMocks([ - AirdropApi, -]) -import 'package:autonomy_flutter/gateway/airdrop_api.dart'; -import 'package:mockito/annotations.dart'; diff --git a/test/generate_mock/gateway/mock_airdrop_api.mocks.dart b/test/generate_mock/gateway/mock_airdrop_api.mocks.dart deleted file mode 100644 index f30d6e1f0b..0000000000 --- a/test/generate_mock/gateway/mock_airdrop_api.mocks.dart +++ /dev/null @@ -1,167 +0,0 @@ -// Mocks generated by Mockito 5.4.2 from annotations -// in autonomy_flutter/test/generate_mock/gateway/mock_airdrop_api.dart. -// Do not manually edit this file. - -// ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i5; - -import 'package:autonomy_flutter/gateway/airdrop_api.dart' as _i4; -import 'package:autonomy_flutter/model/ff_account.dart' as _i3; -import 'package:autonomy_flutter/service/airdrop_service.dart' as _i2; -import 'package:mockito/mockito.dart' as _i1; - -// ignore_for_file: type=lint -// ignore_for_file: avoid_redundant_argument_values -// ignore_for_file: avoid_setters_without_getters -// ignore_for_file: comment_references -// ignore_for_file: implementation_imports -// ignore_for_file: invalid_use_of_visible_for_testing_member -// ignore_for_file: prefer_const_constructors -// ignore_for_file: unnecessary_parenthesis -// ignore_for_file: camel_case_types -// ignore_for_file: subtype_of_sealed_class - -class _FakeAirdropRequestClaimResponse_0 extends _i1.SmartFake - implements _i2.AirdropRequestClaimResponse { - _FakeAirdropRequestClaimResponse_0( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeTokenClaimResponse_1 extends _i1.SmartFake - implements _i3.TokenClaimResponse { - _FakeTokenClaimResponse_1( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeAirdropClaimShareResponse_2 extends _i1.SmartFake - implements _i2.AirdropClaimShareResponse { - _FakeAirdropClaimShareResponse_2( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeAirdropShareResponse_3 extends _i1.SmartFake - implements _i2.AirdropShareResponse { - _FakeAirdropShareResponse_3( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -/// A class which mocks [AirdropApi]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockAirdropApi extends _i1.Mock implements _i4.AirdropApi { - MockAirdropApi() { - _i1.throwOnMissingStub(this); - } - - @override - _i5.Future<_i2.AirdropRequestClaimResponse> requestClaim( - _i2.AirdropRequestClaimRequest? body) => - (super.noSuchMethod( - Invocation.method( - #requestClaim, - [body], - ), - returnValue: _i5.Future<_i2.AirdropRequestClaimResponse>.value( - _FakeAirdropRequestClaimResponse_0( - this, - Invocation.method( - #requestClaim, - [body], - ), - )), - ) as _i5.Future<_i2.AirdropRequestClaimResponse>); - @override - _i5.Future<_i3.TokenClaimResponse> claim(_i2.AirdropClaimRequest? body) => - (super.noSuchMethod( - Invocation.method( - #claim, - [body], - ), - returnValue: - _i5.Future<_i3.TokenClaimResponse>.value(_FakeTokenClaimResponse_1( - this, - Invocation.method( - #claim, - [body], - ), - )), - ) as _i5.Future<_i3.TokenClaimResponse>); - @override - _i5.Future<_i2.AirdropClaimShareResponse> claimShare(String? shareCode) => - (super.noSuchMethod( - Invocation.method( - #claimShare, - [shareCode], - ), - returnValue: _i5.Future<_i2.AirdropClaimShareResponse>.value( - _FakeAirdropClaimShareResponse_2( - this, - Invocation.method( - #claimShare, - [shareCode], - ), - )), - ) as _i5.Future<_i2.AirdropClaimShareResponse>); - @override - _i5.Future<_i2.AirdropShareResponse> share( - String? tokenId, - _i2.AirdropShareRequest? body, - ) => - (super.noSuchMethod( - Invocation.method( - #share, - [ - tokenId, - body, - ], - ), - returnValue: _i5.Future<_i2.AirdropShareResponse>.value( - _FakeAirdropShareResponse_3( - this, - Invocation.method( - #share, - [ - tokenId, - body, - ], - ), - )), - ) as _i5.Future<_i2.AirdropShareResponse>); - @override - _i5.Future<_i3.TokenClaimResponse> feralfileClaim( - _i2.FeralFileTokenClaimRequest? body) => - (super.noSuchMethod( - Invocation.method( - #feralfileClaim, - [body], - ), - returnValue: - _i5.Future<_i3.TokenClaimResponse>.value(_FakeTokenClaimResponse_1( - this, - Invocation.method( - #feralfileClaim, - [body], - ), - )), - ) as _i5.Future<_i3.TokenClaimResponse>); -} diff --git a/test/generate_mock/gateway/mock_iap_api.mocks.dart b/test/generate_mock/gateway/mock_iap_api.mocks.dart index 6b7d8d1789..0c4805ad5f 100644 --- a/test/generate_mock/gateway/mock_iap_api.mocks.dart +++ b/test/generate_mock/gateway/mock_iap_api.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.2 from annotations +// Mocks generated by Mockito 5.4.4 from annotations // in autonomy_flutter/test/generate_mock/gateway/mock_iap_api.dart. // Do not manually edit this file. @@ -15,6 +15,8 @@ import 'package:mockito/mockito.dart' as _i1; // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: prefer_const_constructors diff --git a/test/generate_mock/gateway/mock_postcard_api.mocks.dart b/test/generate_mock/gateway/mock_postcard_api.mocks.dart index 5c3b546ee4..b6eabd2413 100644 --- a/test/generate_mock/gateway/mock_postcard_api.mocks.dart +++ b/test/generate_mock/gateway/mock_postcard_api.mocks.dart @@ -1,22 +1,25 @@ -// Mocks generated by Mockito 5.4.2 from annotations +// Mocks generated by Mockito 5.4.4 from annotations // in autonomy_flutter/test/generate_mock/gateway/mock_postcard_api.dart. // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i5; -import 'dart:io' as _i6; +import 'dart:io' as _i7; import 'package:autonomy_flutter/gateway/postcard_api.dart' as _i4; import 'package:autonomy_flutter/model/postcard_claim.dart' as _i2; -import 'package:autonomy_flutter/model/prompt.dart' as _i7; +import 'package:autonomy_flutter/model/prompt.dart' as _i8; import 'package:autonomy_flutter/screen/send_receive_postcard/receive_postcard_page.dart' as _i3; import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i6; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: prefer_const_constructors @@ -155,13 +158,19 @@ class MockPostcardApi extends _i1.Mock implements _i4.PostcardApi { #getMerchandiseEnable, [tokenId], ), - returnValue: _i5.Future.value(''), + returnValue: _i5.Future.value(_i6.dummyValue( + this, + Invocation.method( + #getMerchandiseEnable, + [tokenId], + ), + )), ) as _i5.Future); @override _i5.Future updatePostcard({ required String? tokenId, - required _i6.File? data, - required _i6.File? metadata, + required _i7.File? data, + required _i7.File? metadata, required String? signature, required String? address, required String? publicKey, @@ -220,12 +229,12 @@ class MockPostcardApi extends _i1.Mock implements _i4.PostcardApi { )), ) as _i5.Future<_i4.GetLeaderboardResponse>); @override - _i5.Future> getPrompts(String? tokenId) => + _i5.Future> getPrompts(String? tokenId) => (super.noSuchMethod( Invocation.method( #getPrompts, [tokenId], ), - returnValue: _i5.Future>.value(<_i7.Prompt>[]), - ) as _i5.Future>); + returnValue: _i5.Future>.value(<_i8.Prompt>[]), + ) as _i5.Future>); } diff --git a/test/generate_mock/service/mock_account_service.mocks.dart b/test/generate_mock/service/mock_account_service.mocks.dart index fbb6d1d1f7..ab0882fed3 100644 --- a/test/generate_mock/service/mock_account_service.mocks.dart +++ b/test/generate_mock/service/mock_account_service.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.2 from annotations +// Mocks generated by Mockito 5.4.4 from annotations // in autonomy_flutter/test/generate_mock/service/mock_account_service.dart. // Do not manually edit this file. @@ -22,6 +22,8 @@ import 'package:nft_collection/models/models.dart' as _i10; // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: prefer_const_constructors diff --git a/test/generate_mock/service/mock_chat_service.mocks.dart b/test/generate_mock/service/mock_chat_service.mocks.dart index 36d331f53d..5782991b18 100644 --- a/test/generate_mock/service/mock_chat_service.mocks.dart +++ b/test/generate_mock/service/mock_chat_service.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.2 from annotations +// Mocks generated by Mockito 5.4.4 from annotations // in autonomy_flutter/test/generate_mock/service/mock_chat_service.dart. // Do not manually edit this file. @@ -15,6 +15,8 @@ import 'package:mockito/mockito.dart' as _i1; // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: prefer_const_constructors diff --git a/test/generate_mock/service/mock_configuration_service.mocks.dart b/test/generate_mock/service/mock_configuration_service.mocks.dart index 5781b65c2d..d2f2618989 100644 --- a/test/generate_mock/service/mock_configuration_service.mocks.dart +++ b/test/generate_mock/service/mock_configuration_service.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.2 from annotations +// Mocks generated by Mockito 5.4.4 from annotations // in autonomy_flutter/test/generate_mock/service/mock_configuration_service.dart. // Do not manually edit this file. @@ -6,26 +6,29 @@ import 'dart:async' as _i6; import 'package:autonomy_flutter/database/entity/announcement_local.dart' - as _i14; + as _i15; import 'package:autonomy_flutter/model/jwt.dart' as _i7; import 'package:autonomy_flutter/model/network.dart' as _i8; import 'package:autonomy_flutter/model/play_list_model.dart' as _i10; import 'package:autonomy_flutter/model/sent_artwork.dart' as _i9; -import 'package:autonomy_flutter/model/shared_postcard.dart' as _i11; +import 'package:autonomy_flutter/model/shared_postcard.dart' as _i12; import 'package:autonomy_flutter/screen/chat/chat_thread_page.dart' as _i3; import 'package:autonomy_flutter/screen/interactive_postcard/postcard_detail_page.dart' - as _i13; + as _i14; import 'package:autonomy_flutter/screen/interactive_postcard/stamp_preview.dart' - as _i12; + as _i13; import 'package:autonomy_flutter/service/configuration_service.dart' as _i5; import 'package:autonomy_flutter/util/announcement_ext.dart' as _i4; import 'package:flutter/material.dart' as _i2; import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i11; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: prefer_const_constructors @@ -569,7 +572,13 @@ class MockConfigurationService extends _i1.Mock #getAccountHMACSecret, [], ), - returnValue: _i6.Future.value(''), + returnValue: _i6.Future.value(_i11.dummyValue( + this, + Invocation.method( + #getAccountHMACSecret, + [], + ), + )), ) as _i6.Future); @override _i6.Future setLastRemindReviewDate(String? value) => @@ -743,16 +752,16 @@ class MockConfigurationService extends _i1.Mock returnValueForMissingStub: _i6.Future.value(), ) as _i6.Future); @override - List<_i11.SharedPostcard> getSharedPostcard() => (super.noSuchMethod( + List<_i12.SharedPostcard> getSharedPostcard() => (super.noSuchMethod( Invocation.method( #getSharedPostcard, [], ), - returnValue: <_i11.SharedPostcard>[], - ) as List<_i11.SharedPostcard>); + returnValue: <_i12.SharedPostcard>[], + ) as List<_i12.SharedPostcard>); @override _i6.Future updateSharedPostcard( - List<_i11.SharedPostcard>? sharedPostcards, { + List<_i12.SharedPostcard>? sharedPostcards, { bool? override = false, bool? isRemoved = false, }) => @@ -770,7 +779,7 @@ class MockConfigurationService extends _i1.Mock ) as _i6.Future); @override _i6.Future removeSharedPostcardWhere( - bool Function(_i11.SharedPostcard)? test) => + bool Function(_i12.SharedPostcard)? test) => (super.noSuchMethod( Invocation.method( #removeSharedPostcardWhere, @@ -806,16 +815,16 @@ class MockConfigurationService extends _i1.Mock returnValueForMissingStub: _i6.Future.value(), ) as _i6.Future); @override - List<_i12.StampingPostcard> getStampingPostcard() => (super.noSuchMethod( + List<_i13.StampingPostcard> getStampingPostcard() => (super.noSuchMethod( Invocation.method( #getStampingPostcard, [], ), - returnValue: <_i12.StampingPostcard>[], - ) as List<_i12.StampingPostcard>); + returnValue: <_i13.StampingPostcard>[], + ) as List<_i13.StampingPostcard>); @override _i6.Future updateStampingPostcard( - List<_i12.StampingPostcard>? values, { + List<_i13.StampingPostcard>? values, { bool? override = false, bool? isRemove = false, }) => @@ -833,7 +842,7 @@ class MockConfigurationService extends _i1.Mock ) as _i6.Future); @override _i6.Future setProcessingStampPostcard( - List<_i12.ProcessingStampPostcard>? values, { + List<_i13.ProcessingStampPostcard>? values, { bool? override = false, bool? isRemove = false, }) => @@ -850,14 +859,14 @@ class MockConfigurationService extends _i1.Mock returnValueForMissingStub: _i6.Future.value(), ) as _i6.Future); @override - List<_i12.ProcessingStampPostcard> getProcessingStampPostcard() => + List<_i13.ProcessingStampPostcard> getProcessingStampPostcard() => (super.noSuchMethod( Invocation.method( #getProcessingStampPostcard, [], ), - returnValue: <_i12.ProcessingStampPostcard>[], - ) as List<_i12.ProcessingStampPostcard>); + returnValue: <_i13.ProcessingStampPostcard>[], + ) as List<_i13.ProcessingStampPostcard>); @override _i6.Future setAutoShowPostcard(bool? value) => (super.noSuchMethod( Invocation.method( @@ -876,17 +885,17 @@ class MockConfigurationService extends _i1.Mock returnValue: false, ) as bool); @override - List<_i13.PostcardIdentity> getListPostcardAlreadyShowYouDidIt() => + List<_i14.PostcardIdentity> getListPostcardAlreadyShowYouDidIt() => (super.noSuchMethod( Invocation.method( #getListPostcardAlreadyShowYouDidIt, [], ), - returnValue: <_i13.PostcardIdentity>[], - ) as List<_i13.PostcardIdentity>); + returnValue: <_i14.PostcardIdentity>[], + ) as List<_i14.PostcardIdentity>); @override _i6.Future setListPostcardAlreadyShowYouDidIt( - List<_i13.PostcardIdentity>? value, { + List<_i14.PostcardIdentity>? value, { bool? override = false, }) => (super.noSuchMethod( @@ -900,7 +909,7 @@ class MockConfigurationService extends _i1.Mock ) as _i6.Future); @override _i6.Future setAlreadyShowPostcardUpdates( - List<_i13.PostcardIdentity>? value, { + List<_i14.PostcardIdentity>? value, { bool? override = false, }) => (super.noSuchMethod( @@ -913,21 +922,27 @@ class MockConfigurationService extends _i1.Mock returnValueForMissingStub: _i6.Future.value(), ) as _i6.Future); @override - List<_i13.PostcardIdentity> getAlreadyShowPostcardUpdates() => + List<_i14.PostcardIdentity> getAlreadyShowPostcardUpdates() => (super.noSuchMethod( Invocation.method( #getAlreadyShowPostcardUpdates, [], ), - returnValue: <_i13.PostcardIdentity>[], - ) as List<_i13.PostcardIdentity>); + returnValue: <_i14.PostcardIdentity>[], + ) as List<_i14.PostcardIdentity>); @override String getVersionInfo() => (super.noSuchMethod( Invocation.method( #getVersionInfo, [], ), - returnValue: '', + returnValue: _i11.dummyValue( + this, + Invocation.method( + #getVersionInfo, + [], + ), + ), ) as String); @override _i6.Future setVersionInfo(String? version) => (super.noSuchMethod( @@ -940,7 +955,7 @@ class MockConfigurationService extends _i1.Mock ) as _i6.Future); @override _i6.Future updateShowAnnouncementNotificationInfo( - _i14.AnnouncementLocal? announcement) => + _i15.AnnouncementLocal? announcement) => (super.noSuchMethod( Invocation.method( #updateShowAnnouncementNotificationInfo, @@ -965,30 +980,6 @@ class MockConfigurationService extends _i1.Mock ), ) as _i4.ShowAnouncementNotificationInfo); @override - bool getAlreadyClaimedAirdrop(String? seriesId) => (super.noSuchMethod( - Invocation.method( - #getAlreadyClaimedAirdrop, - [seriesId], - ), - returnValue: false, - ) as bool); - @override - _i6.Future setAlreadyClaimedAirdrop( - String? seriesId, - bool? value, - ) => - (super.noSuchMethod( - Invocation.method( - #setAlreadyClaimedAirdrop, - [ - seriesId, - value, - ], - ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); - @override _i6.Future setDidSyncArtists(bool? value) => (super.noSuchMethod( Invocation.method( #setDidSyncArtists, @@ -1031,6 +1022,23 @@ class MockConfigurationService extends _i1.Mock returnValue: false, ) as bool); @override + _i6.Future setShowAddAddressBanner(bool? bool) => (super.noSuchMethod( + Invocation.method( + #setShowAddAddressBanner, + [bool], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + @override + bool getShowAddAddressBanner() => (super.noSuchMethod( + Invocation.method( + #getShowAddAddressBanner, + [], + ), + returnValue: false, + ) as bool); + @override _i6.Future setMerchandiseOrderIds( List? ids, { bool? override = false, diff --git a/test/generate_mock/service/mock_feral_file_service.mocks.dart b/test/generate_mock/service/mock_feral_file_service.mocks.dart index 2d5eca8391..ced1c90860 100644 --- a/test/generate_mock/service/mock_feral_file_service.mocks.dart +++ b/test/generate_mock/service/mock_feral_file_service.mocks.dart @@ -1,24 +1,27 @@ -// Mocks generated by Mockito 5.4.2 from annotations +// Mocks generated by Mockito 5.4.4 from annotations // in autonomy_flutter/test/generate_mock/service/mock_feral_file_service.dart. // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i7; -import 'dart:io' as _i9; +import 'dart:async' as _i8; +import 'dart:io' as _i10; -import 'package:autonomy_flutter/model/ff_account.dart' as _i4; -import 'package:autonomy_flutter/model/ff_exhibition.dart' as _i5; +import 'package:autonomy_flutter/model/ff_account.dart' as _i3; +import 'package:autonomy_flutter/model/ff_artwork.dart' as _i6; +import 'package:autonomy_flutter/model/ff_exhibition.dart' as _i4; +import 'package:autonomy_flutter/model/ff_list_response.dart' as _i5; import 'package:autonomy_flutter/model/ff_series.dart' as _i2; -import 'package:autonomy_flutter/model/otp.dart' as _i8; -import 'package:autonomy_flutter/screen/claim/claim_token_page.dart' as _i3; -import 'package:autonomy_flutter/service/feralfile_service.dart' as _i6; +import 'package:autonomy_flutter/service/feralfile_service.dart' as _i7; import 'package:mockito/mockito.dart' as _i1; -import 'package:nft_collection/models/asset_token.dart' as _i10; +import 'package:mockito/src/dummies.dart' as _i9; +import 'package:nft_collection/models/asset_token.dart' as _i11; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: prefer_const_constructors @@ -36,8 +39,9 @@ class _FakeFFSeries_0 extends _i1.SmartFake implements _i2.FFSeries { ); } -class _FakeClaimResponse_1 extends _i1.SmartFake implements _i3.ClaimResponse { - _FakeClaimResponse_1( +class _FakeFeralFileResaleInfo_1 extends _i1.SmartFake + implements _i3.FeralFileResaleInfo { + _FakeFeralFileResaleInfo_1( Object parent, Invocation parentInvocation, ) : super( @@ -46,9 +50,8 @@ class _FakeClaimResponse_1 extends _i1.SmartFake implements _i3.ClaimResponse { ); } -class _FakeFeralFileResaleInfo_2 extends _i1.SmartFake - implements _i4.FeralFileResaleInfo { - _FakeFeralFileResaleInfo_2( +class _FakeExhibition_2 extends _i1.SmartFake implements _i4.Exhibition { + _FakeExhibition_2( Object parent, Invocation parentInvocation, ) : super( @@ -57,8 +60,9 @@ class _FakeFeralFileResaleInfo_2 extends _i1.SmartFake ); } -class _FakeExhibition_3 extends _i1.SmartFake implements _i5.Exhibition { - _FakeExhibition_3( +class _FakeFeralFileListResponse_3 extends _i1.SmartFake + implements _i5.FeralFileListResponse { + _FakeFeralFileListResponse_3( Object parent, Invocation parentInvocation, ) : super( @@ -67,7 +71,7 @@ class _FakeExhibition_3 extends _i1.SmartFake implements _i5.Exhibition { ); } -class _FakeArtwork_4 extends _i1.SmartFake implements _i4.Artwork { +class _FakeArtwork_4 extends _i1.SmartFake implements _i6.Artwork { _FakeArtwork_4( Object parent, Invocation parentInvocation, @@ -80,154 +84,111 @@ class _FakeArtwork_4 extends _i1.SmartFake implements _i4.Artwork { /// A class which mocks [FeralFileService]. /// /// See the documentation for Mockito's code generation for more information. -class MockFeralFileService extends _i1.Mock implements _i6.FeralFileService { +class MockFeralFileService extends _i1.Mock implements _i7.FeralFileService { MockFeralFileService() { _i1.throwOnMissingStub(this); } @override - _i7.Future<_i2.FFSeries> getAirdropSeriesFromExhibitionId(String? id) => + _i8.Future<_i2.FFSeries> getSeries( + String? id, { + String? exhibitionID, + bool? includeFirstArtwork = false, + }) => (super.noSuchMethod( - Invocation.method( - #getAirdropSeriesFromExhibitionId, - [id], - ), - returnValue: _i7.Future<_i2.FFSeries>.value(_FakeFFSeries_0( - this, - Invocation.method( - #getAirdropSeriesFromExhibitionId, - [id], - ), - )), - ) as _i7.Future<_i2.FFSeries>); - @override - _i7.Future<_i2.FFSeries> getSeries(String? id) => (super.noSuchMethod( Invocation.method( #getSeries, [id], - ), - returnValue: _i7.Future<_i2.FFSeries>.value(_FakeFFSeries_0( - this, - Invocation.method( - #getSeries, - [id], - ), - )), - ) as _i7.Future<_i2.FFSeries>); - @override - _i7.Future> getListSeries(String? exhibitionId) => - (super.noSuchMethod( - Invocation.method( - #getListSeries, - [exhibitionId], - ), - returnValue: _i7.Future>.value(<_i2.FFSeries>[]), - ) as _i7.Future>); - @override - _i7.Future<_i3.ClaimResponse> setPendingToken({ - required String? receiver, - required _i4.TokenClaimResponse? response, - required _i2.FFSeries? series, - }) => - (super.noSuchMethod( - Invocation.method( - #setPendingToken, - [], { - #receiver: receiver, - #response: response, - #series: series, + #exhibitionID: exhibitionID, + #includeFirstArtwork: includeFirstArtwork, }, ), - returnValue: _i7.Future<_i3.ClaimResponse>.value(_FakeClaimResponse_1( + returnValue: _i8.Future<_i2.FFSeries>.value(_FakeFFSeries_0( this, Invocation.method( - #setPendingToken, - [], + #getSeries, + [id], { - #receiver: receiver, - #response: response, - #series: series, + #exhibitionID: exhibitionID, + #includeFirstArtwork: includeFirstArtwork, }, ), )), - ) as _i7.Future<_i3.ClaimResponse>); + ) as _i8.Future<_i2.FFSeries>); @override - _i7.Future<_i3.ClaimResponse?> claimToken({ - required String? seriesId, - String? address, - _i8.Otp? otp, - _i7.Future Function(_i2.FFSeries)? onConfirm, + _i8.Future> getListSeries( + String? exhibitionId, { + bool? includeFirstArtwork = false, }) => (super.noSuchMethod( Invocation.method( - #claimToken, - [], - { - #seriesId: seriesId, - #address: address, - #otp: otp, - #onConfirm: onConfirm, - }, + #getListSeries, + [exhibitionId], + {#includeFirstArtwork: includeFirstArtwork}, ), - returnValue: _i7.Future<_i3.ClaimResponse?>.value(), - ) as _i7.Future<_i3.ClaimResponse?>); + returnValue: _i8.Future>.value(<_i2.FFSeries>[]), + ) as _i8.Future>); @override - _i7.Future<_i5.Exhibition?> getExhibitionFromTokenID(String? artworkID) => + _i8.Future<_i4.Exhibition?> getExhibitionFromTokenID(String? artworkID) => (super.noSuchMethod( Invocation.method( #getExhibitionFromTokenID, [artworkID], ), - returnValue: _i7.Future<_i5.Exhibition?>.value(), - ) as _i7.Future<_i5.Exhibition?>); + returnValue: _i8.Future<_i4.Exhibition?>.value(), + ) as _i8.Future<_i4.Exhibition?>); @override - _i7.Future<_i4.FeralFileResaleInfo> getResaleInfo(String? exhibitionID) => + _i8.Future<_i3.FeralFileResaleInfo> getResaleInfo(String? exhibitionID) => (super.noSuchMethod( Invocation.method( #getResaleInfo, [exhibitionID], ), - returnValue: _i7.Future<_i4.FeralFileResaleInfo>.value( - _FakeFeralFileResaleInfo_2( + returnValue: _i8.Future<_i3.FeralFileResaleInfo>.value( + _FakeFeralFileResaleInfo_1( this, Invocation.method( #getResaleInfo, [exhibitionID], ), )), - ) as _i7.Future<_i4.FeralFileResaleInfo>); + ) as _i8.Future<_i3.FeralFileResaleInfo>); @override - _i7.Future getPartnerFullName(String? exhibitionId) => + _i8.Future getPartnerFullName(String? exhibitionId) => (super.noSuchMethod( Invocation.method( #getPartnerFullName, [exhibitionId], ), - returnValue: _i7.Future.value(), - ) as _i7.Future); + returnValue: _i8.Future.value(), + ) as _i8.Future); @override - _i7.Future<_i5.Exhibition> getExhibition(String? id) => (super.noSuchMethod( + _i8.Future<_i4.Exhibition> getExhibition( + String? id, { + bool? includeFirstArtwork = false, + }) => + (super.noSuchMethod( Invocation.method( #getExhibition, [id], + {#includeFirstArtwork: includeFirstArtwork}, ), - returnValue: _i7.Future<_i5.Exhibition>.value(_FakeExhibition_3( + returnValue: _i8.Future<_i4.Exhibition>.value(_FakeExhibition_2( this, Invocation.method( #getExhibition, [id], + {#includeFirstArtwork: includeFirstArtwork}, ), )), - ) as _i7.Future<_i5.Exhibition>); + ) as _i8.Future<_i4.Exhibition>); @override - _i7.Future> getAllExhibitions({ + _i8.Future> getAllExhibitions({ String? sortBy = r'openAt', String? sortOrder = r'DESC', int? limit = 8, int? offset = 0, - bool? withArtworks = false, - bool? withSeries = false, }) => (super.noSuchMethod( Invocation.method( @@ -238,57 +199,105 @@ class MockFeralFileService extends _i1.Mock implements _i6.FeralFileService { #sortOrder: sortOrder, #limit: limit, #offset: offset, - #withArtworks: withArtworks, - #withSeries: withSeries, }, ), - returnValue: _i7.Future>.value( - <_i5.ExhibitionDetail>[]), - ) as _i7.Future>); + returnValue: _i8.Future>.value(<_i4.Exhibition>[]), + ) as _i8.Future>); @override - _i7.Future<_i5.Exhibition> getFeaturedExhibition() => (super.noSuchMethod( + _i8.Future<_i4.Exhibition> getSourceExhibition() => (super.noSuchMethod( + Invocation.method( + #getSourceExhibition, + [], + ), + returnValue: _i8.Future<_i4.Exhibition>.value(_FakeExhibition_2( + this, + Invocation.method( + #getSourceExhibition, + [], + ), + )), + ) as _i8.Future<_i4.Exhibition>); + @override + _i8.Future<_i4.Exhibition?> getUpcomingExhibition() => (super.noSuchMethod( + Invocation.method( + #getUpcomingExhibition, + [], + ), + returnValue: _i8.Future<_i4.Exhibition?>.value(), + ) as _i8.Future<_i4.Exhibition?>); + @override + _i8.Future<_i4.Exhibition> getFeaturedExhibition() => (super.noSuchMethod( Invocation.method( #getFeaturedExhibition, [], ), - returnValue: _i7.Future<_i5.Exhibition>.value(_FakeExhibition_3( + returnValue: _i8.Future<_i4.Exhibition>.value(_FakeExhibition_2( this, Invocation.method( #getFeaturedExhibition, [], ), )), - ) as _i7.Future<_i5.Exhibition>); + ) as _i8.Future<_i4.Exhibition>); @override - _i7.Future> getExhibitionArtworks( - String? exhibitionId, { - bool? withSeries = false, - }) => - (super.noSuchMethod( + _i8.Future> getFeaturedArtworks() => (super.noSuchMethod( Invocation.method( - #getExhibitionArtworks, - [exhibitionId], - {#withSeries: withSeries}, + #getFeaturedArtworks, + [], ), - returnValue: _i7.Future>.value(<_i4.Artwork>[]), - ) as _i7.Future>); + returnValue: _i8.Future>.value(<_i6.Artwork>[]), + ) as _i8.Future>); @override - _i7.Future> getSeriesArtworks( - String? seriesId, { + _i8.Future<_i5.FeralFileListResponse<_i6.Artwork>> getSeriesArtworks( + String? seriesId, + String? exhibitionID, { bool? withSeries = false, + int? offset = 0, + int? limit = 300, }) => (super.noSuchMethod( Invocation.method( #getSeriesArtworks, + [ + seriesId, + exhibitionID, + ], + { + #withSeries: withSeries, + #offset: offset, + #limit: limit, + }, + ), + returnValue: _i8.Future<_i5.FeralFileListResponse<_i6.Artwork>>.value( + _FakeFeralFileListResponse_3<_i6.Artwork>( + this, + Invocation.method( + #getSeriesArtworks, + [ + seriesId, + exhibitionID, + ], + { + #withSeries: withSeries, + #offset: offset, + #limit: limit, + }, + ), + )), + ) as _i8.Future<_i5.FeralFileListResponse<_i6.Artwork>>); + @override + _i8.Future<_i6.Artwork?> getFirstViewableArtwork(String? seriesId) => + (super.noSuchMethod( + Invocation.method( + #getFirstViewableArtwork, [seriesId], - {#withSeries: withSeries}, ), - returnValue: _i7.Future>.value(<_i4.Artwork>[]), - ) as _i7.Future>); + returnValue: _i8.Future<_i6.Artwork?>.value(), + ) as _i8.Future<_i6.Artwork?>); @override - _i7.Future getFeralfileActionMessage({ + _i8.Future getFeralfileActionMessage({ required String? address, - required _i6.FeralfileAction? action, + required _i7.FeralfileAction? action, }) => (super.noSuchMethod( Invocation.method( @@ -299,10 +308,20 @@ class MockFeralFileService extends _i1.Mock implements _i6.FeralFileService { #action: action, }, ), - returnValue: _i7.Future.value(''), - ) as _i7.Future); + returnValue: _i8.Future.value(_i9.dummyValue( + this, + Invocation.method( + #getFeralfileActionMessage, + [], + { + #address: address, + #action: action, + }, + ), + )), + ) as _i8.Future); @override - _i7.Future getFeralfileArtworkDownloadUrl({ + _i8.Future getFeralfileArtworkDownloadUrl({ required String? artworkId, required String? owner, required String? signature, @@ -317,25 +336,36 @@ class MockFeralFileService extends _i1.Mock implements _i6.FeralFileService { #signature: signature, }, ), - returnValue: _i7.Future.value(''), - ) as _i7.Future); + returnValue: _i8.Future.value(_i9.dummyValue( + this, + Invocation.method( + #getFeralfileArtworkDownloadUrl, + [], + { + #artworkId: artworkId, + #owner: owner, + #signature: signature, + }, + ), + )), + ) as _i8.Future); @override - _i7.Future<_i4.Artwork> getArtwork(String? artworkId) => (super.noSuchMethod( + _i8.Future<_i6.Artwork> getArtwork(String? artworkId) => (super.noSuchMethod( Invocation.method( #getArtwork, [artworkId], ), - returnValue: _i7.Future<_i4.Artwork>.value(_FakeArtwork_4( + returnValue: _i8.Future<_i6.Artwork>.value(_FakeArtwork_4( this, Invocation.method( #getArtwork, [artworkId], ), )), - ) as _i7.Future<_i4.Artwork>); + ) as _i8.Future<_i6.Artwork>); @override - _i7.Future<_i9.File?> downloadFeralfileArtwork( - _i10.AssetToken? assetToken, { + _i8.Future<_i10.File?> downloadFeralfileArtwork( + _i11.AssetToken? assetToken, { dynamic Function( int, int, @@ -347,6 +377,6 @@ class MockFeralFileService extends _i1.Mock implements _i6.FeralFileService { [assetToken], {#onReceiveProgress: onReceiveProgress}, ), - returnValue: _i7.Future<_i9.File?>.value(), - ) as _i7.Future<_i9.File?>); + returnValue: _i8.Future<_i10.File?>.value(), + ) as _i8.Future<_i10.File?>); } diff --git a/test/generate_mock/service/mock_indexer_service.mocks.dart b/test/generate_mock/service/mock_indexer_service.mocks.dart index 9e41995ffa..c42833b5f6 100644 --- a/test/generate_mock/service/mock_indexer_service.mocks.dart +++ b/test/generate_mock/service/mock_indexer_service.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.2 from annotations +// Mocks generated by Mockito 5.4.4 from annotations // in autonomy_flutter/test/generate_mock/service/mock_indexer_service.dart. // Do not manually edit this file. @@ -16,6 +16,8 @@ import 'package:nft_collection/services/indexer_service.dart' as _i3; // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: prefer_const_constructors diff --git a/test/generate_mock/service/mock_metric_client_service.mocks.dart b/test/generate_mock/service/mock_metric_client_service.mocks.dart index 7ca9394c7d..c615a8c386 100644 --- a/test/generate_mock/service/mock_metric_client_service.mocks.dart +++ b/test/generate_mock/service/mock_metric_client_service.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.2 from annotations +// Mocks generated by Mockito 5.4.4 from annotations // in autonomy_flutter/test/generate_mock/service/mock_metric_client_service.dart. // Do not manually edit this file. @@ -14,6 +14,8 @@ import 'package:mockito/mockito.dart' as _i1; // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: prefer_const_constructors diff --git a/test/generate_mock/service/mock_navigation_service.mocks.dart b/test/generate_mock/service/mock_navigation_service.mocks.dart index 7ed193edbb..d835f7f758 100644 --- a/test/generate_mock/service/mock_navigation_service.mocks.dart +++ b/test/generate_mock/service/mock_navigation_service.mocks.dart @@ -1,19 +1,15 @@ -// Mocks generated by Mockito 5.4.2 from annotations +// Mocks generated by Mockito 5.4.4 from annotations // in autonomy_flutter/test/generate_mock/service/mock_navigation_service.dart. // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i4; -import 'package:autonomy_flutter/model/ff_series.dart' as _i6; -import 'package:autonomy_flutter/model/otp.dart' as _i7; -import 'package:autonomy_flutter/screen/claim/activation/claim_activation_page.dart' - as _i9; -import 'package:autonomy_flutter/screen/claim/claim_token_page.dart' as _i8; +import 'package:autonomy_flutter/model/ff_exhibition.dart' as _i8; import 'package:autonomy_flutter/screen/irl_screen/webview_irl_screen.dart' - as _i11; + as _i7; import 'package:autonomy_flutter/service/navigation_service.dart' as _i3; -import 'package:autonomy_flutter/util/error_handler.dart' as _i10; +import 'package:autonomy_flutter/util/error_handler.dart' as _i6; import 'package:flutter/material.dart' as _i1; import 'package:mockito/mockito.dart' as _i2; import 'package:nft_collection/models/asset_token.dart' as _i5; @@ -22,6 +18,8 @@ import 'package:nft_collection/models/asset_token.dart' as _i5; // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: prefer_const_constructors @@ -161,74 +159,8 @@ class MockNavigationService extends _i2.Mock implements _i3.NavigationService { ), ) as _i1.NavigatorState); @override - _i4.Future showAirdropNotStarted(String? artworkId) => - (super.noSuchMethod( - Invocation.method( - #showAirdropNotStarted, - [artworkId], - ), - returnValue: _i4.Future.value(), - ) as _i4.Future); - @override - _i4.Future showAirdropExpired(String? artworkId) => - (super.noSuchMethod( - Invocation.method( - #showAirdropExpired, - [artworkId], - ), - returnValue: _i4.Future.value(), - ) as _i4.Future); - @override - _i4.Future showNoRemainingToken({required _i6.FFSeries? series}) => - (super.noSuchMethod( - Invocation.method( - #showNoRemainingToken, - [], - {#series: series}, - ), - returnValue: _i4.Future.value(), - ) as _i4.Future); - @override - _i4.Future showOtpExpired(String? artworkId) => (super.noSuchMethod( - Invocation.method( - #showOtpExpired, - [artworkId], - ), - returnValue: _i4.Future.value(), - ) as _i4.Future); - @override - _i4.Future openClaimTokenPage( - _i6.FFSeries? series, { - _i7.Otp? otp, - _i4.Future<_i8.ClaimResponse?> Function({required String receiveAddress})? - claimFunction, - }) => - (super.noSuchMethod( - Invocation.method( - #openClaimTokenPage, - [series], - { - #otp: otp, - #claimFunction: claimFunction, - }, - ), - returnValue: _i4.Future.value(), - ) as _i4.Future); - @override - _i4.Future openActivationPage( - {required _i9.ClaimActivationPagePayload? payload}) => - (super.noSuchMethod( - Invocation.method( - #openActivationPage, - [], - {#payload: payload}, - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - @override void showErrorDialog( - _i10.ErrorEvent? event, { + _i6.ErrorEvent? event, { dynamic Function()? defaultAction, dynamic Function()? cancelAction, }) => @@ -337,7 +269,7 @@ class MockNavigationService extends _i2.Mock implements _i3.NavigationService { returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @override - _i4.Future goToIRLWebview(_i11.IRLWebScreenPayload? payload) => + _i4.Future goToIRLWebview(_i7.IRLWebScreenPayload? payload) => (super.noSuchMethod( Invocation.method( #goToIRLWebview, @@ -355,49 +287,6 @@ class MockNavigationService extends _i2.Mock implements _i3.NavigationService { returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @override - _i4.Future showAirdropJustOnce() => (super.noSuchMethod( - Invocation.method( - #showAirdropJustOnce, - [], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - @override - _i4.Future showAirdropAlreadyClaimed() => (super.noSuchMethod( - Invocation.method( - #showAirdropAlreadyClaimed, - [], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - @override - _i4.Future showActivationError( - Object? e, - String? id, - ) => - (super.noSuchMethod( - Invocation.method( - #showActivationError, - [ - e, - id, - ], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - @override - _i4.Future showAirdropClaimFailed() => (super.noSuchMethod( - Invocation.method( - #showAirdropClaimFailed, - [], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - @override _i4.Future showPostcardShareLinkExpired() => (super.noSuchMethod( Invocation.method( #showPostcardShareLinkExpired, @@ -497,28 +386,29 @@ class MockNavigationService extends _i2.Mock implements _i3.NavigationService { returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @override - _i4.Future showFeralFileClaimTokenPassLimit( - {required _i6.FFSeries? series}) => + _i4.Future openFeralFileExhibitionNotePage(String? exhibitionSlug) => (super.noSuchMethod( Invocation.method( - #showFeralFileClaimTokenPassLimit, - [], - {#series: series}, + #openFeralFileExhibitionNotePage, + [exhibitionSlug], ), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @override - _i4.Future showClaimTokenError( - Object? e, { - required _i6.FFSeries? series, - }) => + _i4.Future openFeralFilePostPage( + _i8.Post? post, + String? exhibitionID, + ) => (super.noSuchMethod( Invocation.method( - #showClaimTokenError, - [e], - {#series: series}, + #openFeralFilePostPage, + [ + post, + exhibitionID, + ], ), - returnValue: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); } diff --git a/test/generate_mock/service/mock_tezos_service.mocks.dart b/test/generate_mock/service/mock_tezos_service.mocks.dart index 4e9123de39..736b3f3196 100644 --- a/test/generate_mock/service/mock_tezos_service.mocks.dart +++ b/test/generate_mock/service/mock_tezos_service.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.2 from annotations +// Mocks generated by Mockito 5.4.4 from annotations // in autonomy_flutter/test/generate_mock/service/mock_tezos_service.dart. // Do not manually edit this file. @@ -9,12 +9,15 @@ import 'dart:typed_data' as _i6; import 'package:autonomy_flutter/service/tezos_service.dart' as _i3; import 'package:libauk_dart/libauk_dart.dart' as _i5; import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i7; import 'package:tezart/tezart.dart' as _i2; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: prefer_const_constructors @@ -41,10 +44,15 @@ class MockTezosService extends _i1.Mock implements _i3.TezosService { } @override - _i4.Future getBalance(String? address) => (super.noSuchMethod( + _i4.Future getBalance( + String? address, { + bool? doRetry = false, + }) => + (super.noSuchMethod( Invocation.method( #getBalance, [address], + {#doRetry: doRetry}, ), returnValue: _i4.Future.value(0), ) as _i4.Future); @@ -139,7 +147,17 @@ class MockTezosService extends _i1.Mock implements _i3.TezosService { message, ], ), - returnValue: _i4.Future.value(''), + returnValue: _i4.Future.value(_i7.dummyValue( + this, + Invocation.method( + #signMessage, + [ + wallet, + index, + message, + ], + ), + )), ) as _i4.Future); @override _i4.Future<_i2.Operation> getFa2TransferOperation( diff --git a/test/generate_mock/service/mock_tokens_service.mocks.dart b/test/generate_mock/service/mock_tokens_service.mocks.dart index 8b2e45bffb..5dad78e820 100644 --- a/test/generate_mock/service/mock_tokens_service.mocks.dart +++ b/test/generate_mock/service/mock_tokens_service.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.2 from annotations +// Mocks generated by Mockito 5.4.4 from annotations // in autonomy_flutter/test/generate_mock/service/mock_tokens_service.dart. // Do not manually edit this file. @@ -14,6 +14,8 @@ import 'package:nft_collection/services/tokens_service.dart' as _i2; // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: prefer_const_constructors diff --git a/test/mock_data/activation_mock.dart b/test/mock_data/activation_mock.dart deleted file mode 100644 index 829e6f172e..0000000000 --- a/test/mock_data/activation_mock.dart +++ /dev/null @@ -1,215 +0,0 @@ -// ignore_for_file: discarded_futures - -import 'package:autonomy_flutter/gateway/activation_api.dart'; -import 'package:dio/dio.dart'; -import 'package:mockito/mockito.dart'; - -import 'api_mock_data.dart'; -import 'constants.dart'; - -class ActivationApiMock { - ///// getActivation - static final MockData getActivationValid = MockData( - req: activationId, - res: ActivationInfo( - name, - description, - blockchain, - contractAddress, - tokenID, - )); - static final MockData getActivationDioException4xx = MockData( - req: activationIdDioException4xx, - res: DioException( - requestOptions: RequestOptions(path: 'path'), - response: Response( - requestOptions: RequestOptions(path: 'path'), - statusCode: 400, - data: {'message': 'invalid id'}))); - static final MockData getActivationDioException5xx = MockData( - req: activationIdDioException5xx, - res: DioException( - requestOptions: RequestOptions(path: 'path'), - response: Response( - requestOptions: RequestOptions(path: 'path'), - statusCode: 500, - data: {'message': 'internal server error'}))); - - static final MockData getActivationConnectionTimeout = MockData( - req: activationIdConnectionTimeout, - res: DioException( - requestOptions: RequestOptions(path: 'path'), - type: DioExceptionType.connectionTimeout, - error: 'activationIdConnectionTimeout')); - - static final MockData getActivationReceiveTimeout = MockData( - req: activationIdReceiveTimeout, - res: DioException( - requestOptions: RequestOptions(path: 'path'), - type: DioExceptionType.receiveTimeout, - error: 'activationIdReceiveTimeout')); - - static final MockData getActivationExceptionOther = MockData( - req: activationIdExceptionOther, - res: Exception('activationIdExceptionOther')); - - ///// claim - static final MockData claimValid = MockData( - req: ActivationClaimRequest( - activationID: activationId, - address: address, - airdropTOTPPasscode: airdropTOTPPasscode, - ), - res: ActivationClaimResponse()); - - static final MockData claimDioException4xx = MockData( - req: ActivationClaimRequest( - activationID: activationIdDioException4xx, - address: address, - airdropTOTPPasscode: airdropTOTPPasscode), - res: DioException( - requestOptions: RequestOptions(path: 'path'), - response: Response( - requestOptions: RequestOptions(path: 'path'), - statusCode: 400, - data: {'message': 'invalid claim'}))); - - static final MockData claimDioException5xx = MockData( - req: ActivationClaimRequest( - activationID: activationIdDioException5xx, - address: address, - airdropTOTPPasscode: airdropTOTPPasscode), - res: DioException( - requestOptions: RequestOptions(path: 'path'), - response: Response( - requestOptions: RequestOptions(path: 'path'), - statusCode: 500, - data: {'message': 'internal server error'}))); - - static final MockData claimConnectionTimeout = MockData( - req: ActivationClaimRequest( - activationID: activationIdConnectionTimeout, - address: address, - airdropTOTPPasscode: airdropTOTPPasscode), - res: DioException( - requestOptions: RequestOptions(path: 'path'), - type: DioExceptionType.connectionTimeout, - error: 'claimConnectionTimeout')); - - static final MockData claimReceiveTimeout = MockData( - req: ActivationClaimRequest( - activationID: activationIdReceiveTimeout, - address: address, - airdropTOTPPasscode: airdropTOTPPasscode), - res: DioException( - requestOptions: RequestOptions(path: 'path'), - type: DioExceptionType.receiveTimeout, - error: 'claimReceiveTimeout')); - - static final MockData claimDioExceptionOther = MockData( - req: ActivationClaimRequest( - activationID: activationIdExceptionOther, - address: address, - airdropTOTPPasscode: airdropTOTPPasscode), - res: Exception('claimExceptionOther')); - - static final MockData claimSelfClaim = MockData( - req: ActivationClaimRequest( - activationID: cannotSelfClaim, - address: address, - airdropTOTPPasscode: airdropTOTPPasscode), - res: DioException( - requestOptions: RequestOptions(path: 'path'), - response: Response( - requestOptions: RequestOptions(path: 'path'), - statusCode: 403, - data: {'message': cannotSelfClaim}))); - - static final MockData claimInvalidClaim = MockData( - req: ActivationClaimRequest( - activationID: invalidClaim, - address: address, - airdropTOTPPasscode: airdropTOTPPasscode), - res: DioException( - requestOptions: RequestOptions(path: 'path'), - response: Response( - requestOptions: RequestOptions(path: 'path'), - statusCode: 403, - data: {'message': invalidClaim}))); - - static final MockData claimAlreadyShare = MockData( - req: ActivationClaimRequest( - activationID: alreadyShare, - address: address, - airdropTOTPPasscode: airdropTOTPPasscode), - res: DioException( - requestOptions: RequestOptions(path: 'path'), - response: Response( - requestOptions: RequestOptions(path: 'path'), - statusCode: 403, - data: {'message': alreadyShare}))); - - static void setup(ActivationApi mockActivationApi) { - when(mockActivationApi - .getActivation(ActivationApiMock.getActivationValid.req)) - .thenAnswer((_) async => - ActivationApiMock.getActivationValid.res as ActivationInfo); - - when(mockActivationApi - .getActivation(ActivationApiMock.getActivationDioException4xx.req)) - .thenThrow( - ActivationApiMock.getActivationDioException4xx.res as DioException); - - when(mockActivationApi - .getActivation(ActivationApiMock.getActivationDioException5xx.req)) - .thenThrow( - ActivationApiMock.getActivationDioException5xx.res as DioException); - - when(mockActivationApi.getActivation( - ActivationApiMock.getActivationConnectionTimeout.req)) - .thenThrow(ActivationApiMock.getActivationConnectionTimeout.res - as DioException); - - when(mockActivationApi - .getActivation(ActivationApiMock.getActivationReceiveTimeout.req)) - .thenThrow( - ActivationApiMock.getActivationReceiveTimeout.res as DioException); - - when(mockActivationApi - .getActivation(ActivationApiMock.getActivationExceptionOther.req)) - .thenThrow( - ActivationApiMock.getActivationExceptionOther.res as Exception); - - when(mockActivationApi.claim(ActivationApiMock.claimValid.req)).thenAnswer( - (_) async => - ActivationApiMock.claimValid.res as ActivationClaimResponse); - - when(mockActivationApi.claim(ActivationApiMock.claimDioException4xx.req)) - .thenThrow(ActivationApiMock.claimDioException4xx.res as DioException); - - when(mockActivationApi.claim(ActivationApiMock.claimDioException5xx.req)) - .thenThrow(ActivationApiMock.claimDioException5xx.res as DioException); - - when(mockActivationApi.claim(ActivationApiMock.claimConnectionTimeout.req)) - .thenThrow( - ActivationApiMock.claimConnectionTimeout.res as DioException); - - when(mockActivationApi.claim(ActivationApiMock.claimReceiveTimeout.req)) - .thenThrow(ActivationApiMock.claimReceiveTimeout.res as DioException); - - when(mockActivationApi.claim(ActivationApiMock.claimDioExceptionOther.req)) - .thenThrow(ActivationApiMock.claimDioExceptionOther.res as Exception); - - when(mockActivationApi.claim(ActivationApiMock.claimSelfClaim.req)) - .thenAnswer((_) async => - throw ActivationApiMock.claimSelfClaim.res as DioException); - - when(mockActivationApi.claim(ActivationApiMock.claimInvalidClaim.req)) - .thenAnswer((_) async => - throw ActivationApiMock.claimInvalidClaim.res as DioException); - - when(mockActivationApi.claim(ActivationApiMock.claimAlreadyShare.req)) - .thenAnswer((_) async => - throw ActivationApiMock.claimAlreadyShare.res as DioException); - } -} diff --git a/test/mock_data/airdrop_mock.dart b/test/mock_data/airdrop_mock.dart deleted file mode 100644 index ed7b2394e7..0000000000 --- a/test/mock_data/airdrop_mock.dart +++ /dev/null @@ -1,422 +0,0 @@ -import 'package:autonomy_flutter/gateway/airdrop_api.dart'; -import 'package:autonomy_flutter/model/ff_account.dart'; -import 'package:autonomy_flutter/service/airdrop_service.dart'; -import 'package:dio/dio.dart'; -import 'package:mockito/mockito.dart'; - -import 'api_mock_data.dart'; -import 'constants.dart'; - -class AirdropApiMock { - //// requestClaim - static final MockData requestClaimValid = MockData( - req: AirdropRequestClaimRequest( - ownerAddress: ownerAddress, - id: id, - indexID: indexID, - ), - res: AirdropRequestClaimResponse(claimID: claimID, seriesID: seriesID), - ); - - static final MockData requestClaimDioException4xx = MockData( - req: AirdropRequestClaimRequest( - ownerAddress: ownerAddress, - id: idDioException4xx, - indexID: indexID, - ), - res: DioException( - requestOptions: RequestOptions(path: 'path'), - response: Response( - requestOptions: RequestOptions(path: 'path'), - statusCode: 400, - data: {'message': 'invalid id'}, - ), - ), - ); - - static final MockData requestClaimDioException5xx = MockData( - req: AirdropRequestClaimRequest( - ownerAddress: ownerAddress, - id: idDioException5xx, - indexID: indexID, - ), - res: DioException( - requestOptions: RequestOptions(path: 'path'), - response: Response( - requestOptions: RequestOptions(path: 'path'), - statusCode: 500, - data: {'message': 'internal server error'}, - ), - ), - ); - - static final MockData requestClaimConnectionTimeout = MockData( - req: AirdropRequestClaimRequest( - ownerAddress: ownerAddress, - id: idConnectionTimeout, - indexID: indexID, - ), - res: DioException( - requestOptions: RequestOptions(path: 'path'), - type: DioExceptionType.connectionTimeout, - error: 'requestClaimConnectionTimeout', - ), - ); - - static final MockData requestClaimReceiveTimeout = MockData( - req: AirdropRequestClaimRequest( - ownerAddress: ownerAddress, - id: idReceiveTimeout, - indexID: indexID, - ), - res: DioException( - requestOptions: RequestOptions(path: 'path'), - type: DioExceptionType.receiveTimeout, - error: 'requestClaimReceiveTimeout', - ), - ); - - static final MockData requestClaimExceptionOther = MockData( - req: AirdropRequestClaimRequest( - ownerAddress: ownerAddress, - id: idExceptionOther, - indexID: indexID, - ), - res: Exception('requestClaimExceptionOther'), - ); - - //// claim - static final MockData claimValid = MockData( - req: AirdropClaimRequest( - claimId: claimID, - shareCode: shareCode, - receivingAddress: receivingAddress, - did: did, - didSignature: didSignature, - timestamp: timestamp, - ), - res: TokenClaimResponse(TokenClaimResult( - id, - claimerID, - exhibitionID, - artworkID, - txID, - seriesID, - metadata, - )), - ); - - static final MockData claimDioException4xx = MockData( - req: AirdropClaimRequest( - claimId: claimIDDioException4xx, - shareCode: shareCode, - receivingAddress: receivingAddress, - did: did, - didSignature: didSignature, - timestamp: timestamp, - ), - res: DioException( - requestOptions: RequestOptions(path: 'path'), - response: Response( - requestOptions: RequestOptions(path: 'path'), - statusCode: 400, - data: {'message': 'invalid id'}, - ), - ), - ); - - static final MockData claimDioException5xx = MockData( - req: AirdropClaimRequest( - claimId: claimIDDioException5xx, - shareCode: shareCode, - receivingAddress: receivingAddress, - did: did, - didSignature: didSignature, - timestamp: timestamp, - ), - res: DioException( - requestOptions: RequestOptions(path: 'path'), - response: Response( - requestOptions: RequestOptions(path: 'path'), - statusCode: 500, - data: {'message': 'internal server error'}, - ), - ), - ); - - static final MockData claimConnectionTimeout = MockData( - req: AirdropClaimRequest( - claimId: claimIDConnectionTimeout, - shareCode: shareCode, - receivingAddress: receivingAddress, - did: did, - didSignature: didSignature, - timestamp: timestamp, - ), - res: DioException( - requestOptions: RequestOptions(path: 'path'), - type: DioExceptionType.connectionTimeout, - error: 'claimConnectionTimeout', - ), - ); - - static final MockData claimReceiveTimeout = MockData( - req: AirdropClaimRequest( - claimId: claimIDReceiveTimeout, - shareCode: shareCode, - receivingAddress: receivingAddress, - did: did, - didSignature: didSignature, - timestamp: timestamp, - ), - res: DioException( - requestOptions: RequestOptions(path: 'path'), - type: DioExceptionType.receiveTimeout, - error: 'claimReceiveTimeout', - ), - ); - - static final MockData claimExceptionOther = MockData( - req: AirdropClaimRequest( - claimId: claimIDExceptionOther, - shareCode: shareCode, - receivingAddress: receivingAddress, - did: did, - didSignature: didSignature, - timestamp: timestamp, - ), - res: Exception('claimExceptionOther'), - ); - - //// claimShare - static final MockData claimShareValid = MockData( - req: shareCode, - res: AirdropClaimShareResponse(shareCode: shareCode, seriesID: seriesID), - ); - - static final MockData claimShareDioException4xx = MockData( - req: shareCodeDioException4xx, - res: DioException( - requestOptions: RequestOptions(path: 'path'), - response: Response( - requestOptions: RequestOptions(path: 'path'), - statusCode: 400, - data: {'message': 'invalid id'}, - ), - ), - ); - - static final MockData claimShareDioException5xx = MockData( - req: shareCodeDioException5xx, - res: DioException( - requestOptions: RequestOptions(path: 'path'), - response: Response( - requestOptions: RequestOptions(path: 'path'), - statusCode: 500, - data: {'message': 'internal server error'}, - ), - ), - ); - - static final MockData claimShareConnectionTimeout = MockData( - req: shareCodeConnectionTimeout, - res: DioException( - requestOptions: RequestOptions(path: 'path'), - type: DioExceptionType.connectionTimeout, - error: 'claimShareConnectionTimeout', - ), - ); - - static final MockData claimShareReceiveTimeout = MockData( - req: shareCodeReceiveTimeout, - res: DioException( - requestOptions: RequestOptions(path: 'path'), - type: DioExceptionType.receiveTimeout, - error: 'claimShareReceiveTimeout', - ), - ); - - static final MockData claimShareExceptionOther = MockData( - req: shareCodeExceptionOther, - res: Exception('claimShareExceptionOther'), - ); - - //// share - static final MockData shareValid = - MockData, AirdropShareResponse>( - req: [ - tokenID, - AirdropShareRequest( - tokenId: tokenID, - ownerAddress: ownerAddress, - ownerPublicKey: ownerPublicKey, - timestamp: timestamp, - signature: signature, - ) - ], - res: AirdropShareResponse(deepLink: deepLink), - ); - - static final MockData shareDioException4xx = MockData( - req: [ - tokenIDDioException4xx, - AirdropShareRequest( - tokenId: tokenIDDioException4xx, - ownerAddress: ownerAddress, - ownerPublicKey: ownerPublicKey, - timestamp: timestamp, - signature: signature, - ) - ], - res: DioException( - requestOptions: RequestOptions(path: 'path'), - response: Response( - requestOptions: RequestOptions(path: 'path'), - statusCode: 400, - data: {'message': 'invalid id'}, - ), - ), - ); - - static final MockData shareDioException5xx = MockData( - req: [ - tokenIDDioException5xx, - AirdropShareRequest( - tokenId: tokenIDDioException5xx, - ownerAddress: ownerAddress, - ownerPublicKey: ownerPublicKey, - timestamp: timestamp, - signature: signature, - ) - ], - res: DioException( - requestOptions: RequestOptions(path: 'path'), - response: Response( - requestOptions: RequestOptions(path: 'path'), - statusCode: 500, - data: {'message': 'internal server error'}, - ), - ), - ); - - static final MockData shareConnectionTimeout = MockData( - req: [ - tokenIDConnectionTimeout, - AirdropShareRequest( - tokenId: tokenIDConnectionTimeout, - ownerAddress: ownerAddress, - ownerPublicKey: ownerPublicKey, - timestamp: timestamp, - signature: signature, - ) - ], - res: DioException( - requestOptions: RequestOptions(path: 'path'), - type: DioExceptionType.connectionTimeout, - error: 'shareConnectionTimeout', - ), - ); - - static final MockData shareReceiveTimeout = MockData( - req: [ - tokenIDReceiveTimeout, - AirdropShareRequest( - tokenId: tokenIDReceiveTimeout, - ownerAddress: ownerAddress, - ownerPublicKey: ownerPublicKey, - timestamp: timestamp, - signature: signature, - ) - ], - res: DioException( - requestOptions: RequestOptions(path: 'path'), - type: DioExceptionType.receiveTimeout, - error: 'shareReceiveTimeout', - ), - ); - - static final MockData shareExceptionOther = MockData( - req: [ - tokenIDExceptionOther, - AirdropShareRequest( - tokenId: tokenIDExceptionOther, - ownerAddress: ownerAddress, - ownerPublicKey: ownerPublicKey, - timestamp: timestamp, - signature: signature, - ) - ], - res: Exception('shareExceptionOther'), - ); - - static Future setup(AirdropApi mockAirdropApi) async { - when(mockAirdropApi.share(AirdropApiMock.shareValid.req.first, - AirdropApiMock.shareValid.req.last)) - .thenAnswer((_) async => AirdropApiMock.shareValid.res); - when(mockAirdropApi.share(AirdropApiMock.shareDioException4xx.req.first, - AirdropApiMock.shareDioException4xx.req.last)) - .thenThrow(AirdropApiMock.shareDioException4xx.res); - when(mockAirdropApi.share(AirdropApiMock.shareDioException5xx.req.first, - AirdropApiMock.shareDioException5xx.req.last)) - .thenThrow(AirdropApiMock.shareDioException5xx.res); - when(mockAirdropApi.share(AirdropApiMock.shareConnectionTimeout.req.first, - AirdropApiMock.shareConnectionTimeout.req.last)) - .thenThrow(AirdropApiMock.shareConnectionTimeout.res); - when(mockAirdropApi.share(AirdropApiMock.shareReceiveTimeout.req.first, - AirdropApiMock.shareReceiveTimeout.req.last)) - .thenThrow(AirdropApiMock.shareReceiveTimeout.res); - when(mockAirdropApi.share(AirdropApiMock.shareExceptionOther.req.first, - AirdropApiMock.shareExceptionOther.req.last)) - .thenThrow(AirdropApiMock.shareExceptionOther.res); - - // claimShare - when(mockAirdropApi.claimShare(AirdropApiMock.claimShareValid.req)) - .thenAnswer((_) async => AirdropApiMock.claimShareValid.res); - when(mockAirdropApi - .claimShare(AirdropApiMock.claimShareDioException4xx.req)) - .thenThrow(AirdropApiMock.claimShareDioException4xx.res); - when(mockAirdropApi - .claimShare(AirdropApiMock.claimShareDioException5xx.req)) - .thenThrow(AirdropApiMock.claimShareDioException5xx.res); - when(mockAirdropApi - .claimShare(AirdropApiMock.claimShareConnectionTimeout.req)) - .thenThrow(AirdropApiMock.claimShareConnectionTimeout.res); - when(mockAirdropApi.claimShare(AirdropApiMock.claimShareReceiveTimeout.req)) - .thenThrow(AirdropApiMock.claimShareReceiveTimeout.res); - when(mockAirdropApi.claimShare(AirdropApiMock.claimShareExceptionOther.req)) - .thenThrow(AirdropApiMock.claimShareExceptionOther.res); - - //requestClaim - when(mockAirdropApi.requestClaim(AirdropApiMock.requestClaimValid.req)) - .thenAnswer((_) async => AirdropApiMock.requestClaimValid.res); - when(mockAirdropApi - .requestClaim(AirdropApiMock.requestClaimDioException4xx.req)) - .thenThrow(AirdropApiMock.requestClaimDioException4xx.res); - when(mockAirdropApi - .requestClaim(AirdropApiMock.requestClaimDioException5xx.req)) - .thenThrow(AirdropApiMock.requestClaimDioException5xx.res); - when(mockAirdropApi - .requestClaim(AirdropApiMock.requestClaimConnectionTimeout.req)) - .thenThrow(AirdropApiMock.requestClaimConnectionTimeout.res); - when(mockAirdropApi - .requestClaim(AirdropApiMock.requestClaimReceiveTimeout.req)) - .thenThrow(AirdropApiMock.requestClaimReceiveTimeout.res); - when(mockAirdropApi - .requestClaim(AirdropApiMock.requestClaimExceptionOther.req)) - .thenThrow(AirdropApiMock.requestClaimExceptionOther.res); - - //claim - when(mockAirdropApi.claim(AirdropApiMock.claimValid.req)) - .thenAnswer((_) async => AirdropApiMock.claimValid.res); - when(mockAirdropApi.claim(AirdropApiMock.claimDioException4xx.req)) - .thenThrow(AirdropApiMock.claimDioException4xx.res); - when(mockAirdropApi.claim(AirdropApiMock.claimDioException5xx.req)) - .thenThrow(AirdropApiMock.claimDioException5xx.res); - when(mockAirdropApi.claim(AirdropApiMock.claimConnectionTimeout.req)) - .thenThrow(AirdropApiMock.claimConnectionTimeout.res); - when(mockAirdropApi.claim(AirdropApiMock.claimReceiveTimeout.req)) - .thenThrow(AirdropApiMock.claimReceiveTimeout.res); - when(mockAirdropApi.claim(AirdropApiMock.claimExceptionOther.req)) - .thenThrow(AirdropApiMock.claimExceptionOther.res); - } -} diff --git a/test/mock_data/constants.dart b/test/mock_data/constants.dart index e864590f43..e7a7ad5587 100644 --- a/test/mock_data/constants.dart +++ b/test/mock_data/constants.dart @@ -1,16 +1,5 @@ import 'package:autonomy_flutter/model/postcard_metadata.dart'; -const String activationId = 'activationId'; -const String activationIdDioException4xx = 'activationIdDioException4xx'; -const String activationIdDioException5xx = 'activationIdDioException5xx'; -const String activationIdConnectionTimeout = 'activationIdConnectionTimeout'; -const String activationIdReceiveTimeout = 'activationIdReceiveTimeout'; -const String activationIdExceptionOther = 'activationIdExceptionOther'; - -const String cannotSelfClaim = 'cannot self claim'; -const String invalidClaim = 'invalid claim'; -const String alreadyShare = 'the token is not available for share'; - const String id = 'id'; const String idDioException4xx = 'idDioException4xx'; const String idDioException5xx = 'idDioException5xx'; diff --git a/test/services/activation_service_test.dart b/test/services/activation_service_test.dart deleted file mode 100644 index 1c10a18789..0000000000 --- a/test/services/activation_service_test.dart +++ /dev/null @@ -1,243 +0,0 @@ -import 'package:autonomy_flutter/gateway/activation_api.dart'; -import 'package:autonomy_flutter/service/activation_service.dart'; -import 'package:autonomy_flutter/service/navigation_service.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/mockito.dart'; -import 'package:nft_collection/services/tokens_service.dart'; - -import '../generate_mock/gateway/mock_activation_api.mocks.dart'; -import '../generate_mock/service/mock_navigation_service.mocks.dart'; -import '../generate_mock/service/mock_tokens_service.mocks.dart'; -import '../mock_data/activation_mock.dart'; -import '../mock_data/constants.dart'; -import '../mock_data/token_service_mock_data.dart'; - -void main() async { - late ActivationApi mockActivationApi; - late TokensService mockTokensService; - late NavigationService mockNavigationService; - late ActivationService activationService; - - setUp(() { - mockActivationApi = MockActivationApi(); - mockTokensService = MockTokensService(); - mockNavigationService = MockNavigationService(); - activationService = ActivationService( - mockActivationApi, mockTokensService, mockNavigationService); - - ActivationApiMock.setup(mockActivationApi); - TokenServiceMockData.setUp(mockTokensService as MockTokensService); - }); - group('getActivation tests', () { - test('getActivation case valid', () async { - expect( - await activationService.getActivation( - activationID: ActivationApiMock.getActivationValid.req), - ActivationApiMock.getActivationValid.res); - - verify(mockActivationApi - .getActivation(ActivationApiMock.getActivationValid.req)) - .called(1); - }); - - test('getActivation case 400', () async { - final error = activationService.getActivation( - activationID: ActivationApiMock.getActivationDioException4xx.req); - expect( - error, throwsA(ActivationApiMock.getActivationDioException4xx.res)); - verify(mockActivationApi.getActivation( - ActivationApiMock.getActivationDioException4xx.req)) - .called(1); - }); - - test('getActivation case 500', () async { - final error = activationService.getActivation( - activationID: ActivationApiMock.getActivationDioException5xx.req); - expect( - error, throwsA(ActivationApiMock.getActivationDioException5xx.res)); - verify(mockActivationApi.getActivation( - ActivationApiMock.getActivationDioException5xx.req)) - .called(1); - }); - - test('getActivation case connectionTimeout', () async { - final error = activationService.getActivation( - activationID: ActivationApiMock.getActivationConnectionTimeout.req); - expect( - error, throwsA(ActivationApiMock.getActivationConnectionTimeout.res)); - verify(mockActivationApi.getActivation( - ActivationApiMock.getActivationConnectionTimeout.req)) - .called(1); - }); - - test('getActivation case receiveTimeout', () async { - final error = activationService.getActivation( - activationID: ActivationApiMock.getActivationReceiveTimeout.req); - expect(error, throwsA(ActivationApiMock.getActivationReceiveTimeout.res)); - verify(mockActivationApi - .getActivation(ActivationApiMock.getActivationReceiveTimeout.req)) - .called(1); - }); - - test('getActivation case exceptionOther', () async { - final error = activationService.getActivation( - activationID: ActivationApiMock.getActivationExceptionOther.req); - expect(error, throwsA(ActivationApiMock.getActivationExceptionOther.res)); - verify(mockActivationApi - .getActivation(ActivationApiMock.getActivationExceptionOther.req)) - .called(1); - }); - - // Add more test cases for other methods if needed - - tearDown(() { - // Verify that methods on dependencies were called as expected - //verifyNoMoreInteractions(mockActivationApi); - //verifyNoMoreInteractions(mockTokensService); - //verifyNoMoreInteractions(mockNavigationService); - }); - }); - - group('claimActivation test', () { - test('claimActivation: case valid', () async { - expect( - await activationService.claimActivation( - request: ActivationApiMock.claimValid.req, - assetToken: TokenServiceMockData.anyAssetToken), - ActivationApiMock.claimValid.res); - - verify(mockActivationApi.claim(ActivationApiMock.claimValid.req)) - .called(1); - }); - - test('claimActivation: case 400', () async { - final error = activationService.claimActivation( - request: ActivationApiMock.claimDioException4xx.req, - assetToken: TokenServiceMockData.anyAssetToken); - expect(error, throwsA(ActivationApiMock.claimDioException4xx.res)); - verify(mockActivationApi - .claim(ActivationApiMock.claimDioException4xx.req)) - .called(1); - }); - - test('claimActivation: case 500', () async { - final error = activationService - .claimActivation( - request: ActivationApiMock.claimDioException5xx.req, - assetToken: TokenServiceMockData.anyAssetToken) - .then((value) { - verify(mockNavigationService.showActivationError( - value, TokenServiceMockData.anyAssetToken.id)) - .called(1); - }); - expect(error, throwsA(ActivationApiMock.claimDioException5xx.res)); - verify(mockActivationApi - .claim(ActivationApiMock.claimDioException5xx.req)) - .called(1); - }); - - test('claimActivation: case connectionTimeout', () async { - final error = activationService - .claimActivation( - request: ActivationApiMock.claimConnectionTimeout.req, - assetToken: TokenServiceMockData.anyAssetToken) - .then((value) { - verify(mockNavigationService.showActivationError( - value, TokenServiceMockData.anyAssetToken.id)) - .called(1); - }); - expect(error, throwsA(ActivationApiMock.claimConnectionTimeout.res)); - verify(mockActivationApi - .claim(ActivationApiMock.claimConnectionTimeout.req)) - .called(1); - }); - - test('claimActivation: case receiveTimeout', () async { - final error = activationService - .claimActivation( - request: ActivationApiMock.claimReceiveTimeout.req, - assetToken: TokenServiceMockData.anyAssetToken) - .then((value) { - verify(mockNavigationService.showActivationError( - value, TokenServiceMockData.anyAssetToken.id)) - .called(1); - }); - expect(error, throwsA(ActivationApiMock.claimReceiveTimeout.res)); - verify(mockActivationApi.claim(ActivationApiMock.claimReceiveTimeout.req)) - .called(1); - }); - - test('claimActivation: case exceptionOther', () async { - final error = activationService - .claimActivation( - request: ActivationApiMock.claimDioExceptionOther.req, - assetToken: TokenServiceMockData.anyAssetToken) - .then((value) { - verify(mockNavigationService.showActivationError( - value, TokenServiceMockData.anyAssetToken.id)) - .called(0); - }); - expect(error, throwsA(ActivationApiMock.claimDioExceptionOther.res)); - verify(mockActivationApi - .claim(ActivationApiMock.claimDioExceptionOther.req)) - .called(1); - }); - - test('claimActivation: error self claim', () async { - final error = activationService - .claimActivation( - request: ActivationApiMock.claimSelfClaim.req, - assetToken: TokenServiceMockData.anyAssetToken) - .then((value) { - verify(mockNavigationService.showAirdropJustOnce()).called(1); - }); - - expect(error, throwsA(ActivationApiMock.claimSelfClaim.res)); - }); - - test('claimActivation: error invalid claim', () async { - final error = activationService - .claimActivation( - request: ActivationApiMock.claimInvalidClaim.req, - assetToken: TokenServiceMockData.anyAssetToken) - .then((value) { - verify(mockNavigationService.showAirdropAlreadyClaimed()).called(1); - }); - - expect(error, throwsA(ActivationApiMock.claimInvalidClaim.res)); - }); - - test('claimActivation: error already share', () async { - final error = activationService - .claimActivation( - request: ActivationApiMock.claimAlreadyShare.req, - assetToken: TokenServiceMockData.anyAssetToken) - .then((value) { - verify(mockNavigationService.showAirdropAlreadyClaimed()).called(1); - }); - - expect(error, throwsA(ActivationApiMock.claimAlreadyShare.res)); - }); - - tearDown(() { - // Verify that methods on dependencies were called as expected - //verifyNoMoreInteractions(mockActivationApi); - //verifyNoMoreInteractions(mockTokensService); - //verifyNoMoreInteractions(mockNavigationService); - }); - }); - - group('getIndexerId', () { - test('case ethereum', () { - final indexerID = - activationService.getIndexerID(ethChain, contractAddress, tokenID); - expect(indexerID, ethIndexerID); - }); - - test('case tezos', () { - final indexerID = - activationService.getIndexerID(tezChain, contractAddress, tokenID); - expect(indexerID, tezosIndexerID); - }); - }); -} diff --git a/test/services/airdrop_service_test.dart b/test/services/airdrop_service_test.dart deleted file mode 100644 index 81fe4966df..0000000000 --- a/test/services/airdrop_service_test.dart +++ /dev/null @@ -1,204 +0,0 @@ -import 'package:autonomy_flutter/gateway/airdrop_api.dart'; -import 'package:autonomy_flutter/service/account_service.dart'; -import 'package:autonomy_flutter/service/airdrop_service.dart'; -import 'package:autonomy_flutter/service/feralfile_service.dart'; -import 'package:autonomy_flutter/service/navigation_service.dart'; -import 'package:autonomy_flutter/service/tezos_service.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/mockito.dart'; -import 'package:nft_collection/database/dao/asset_token_dao.dart'; -import 'package:nft_collection/services/indexer_service.dart'; -import 'package:nft_collection/services/tokens_service.dart'; - -import '../generate_mock/dao/mock_asset_token_dao.mocks.dart'; -import '../generate_mock/gateway/mock_airdrop_api.mocks.dart'; -import '../generate_mock/service/mock_account_service.mocks.dart'; -import '../generate_mock/service/mock_feral_file_service.mocks.dart'; -import '../generate_mock/service/mock_indexer_service.mocks.dart'; -import '../generate_mock/service/mock_navigation_service.mocks.dart'; -import '../generate_mock/service/mock_tezos_service.mocks.dart'; -import '../generate_mock/service/mock_tokens_service.mocks.dart'; -import '../mock_data/airdrop_mock.dart'; - -void main() async { - late AirdropApi airdropApi; - late AssetTokenDao assetTokenDao; - late AccountService accountService; - late TezosService tezosService; - late TokensService tokensService; - late FeralFileService feralFileService; - late IndexerService indexerService; - late NavigationService navigationService; - - late AirdropService airdropService; - - setUp(() { - airdropApi = MockAirdropApi(); - assetTokenDao = MockAssetTokenDao(); - accountService = MockAccountService(); - tezosService = MockTezosService(); - tokensService = MockTokensService(); - feralFileService = MockFeralFileService(); - indexerService = MockIndexerService(); - navigationService = MockNavigationService(); - - airdropService = AirdropService( - airdropApi, - assetTokenDao, - accountService, - tezosService, - tokensService, - feralFileService, - indexerService, - navigationService, - ); - AirdropApiMock.setup(airdropApi); - }); - - group('share api', () { - test('share valid', () async { - final req = AirdropApiMock.shareValid.req; - expect( - await airdropService.share(req.last), AirdropApiMock.shareValid.res); - - verify(airdropApi.share(req.first, req.last)).called(1); - }); - - test('share 400', () async { - final req = AirdropApiMock.shareDioException4xx.req; - final error = airdropService.share(req.last); - expect(error, throwsA(AirdropApiMock.shareDioException4xx.res)); - verify(airdropApi.share(req.first, req.last)).called(1); - }); - - test('share 500', () async { - final req = AirdropApiMock.shareDioException5xx.req; - final error = airdropService.share(req.last); - expect(error, throwsA(AirdropApiMock.shareDioException5xx.res)); - verify(airdropApi.share(req.first, req.last)).called(1); - }); - - test('share connection timeout', () async { - final req = AirdropApiMock.shareConnectionTimeout.req; - final error = airdropService.share(req.last); - expect(error, throwsA(AirdropApiMock.shareConnectionTimeout.res)); - verify(airdropApi.share(req.first, req.last)).called(1); - }); - - test('share receive timeout', () async { - final req = AirdropApiMock.shareReceiveTimeout.req; - final error = airdropService.share(req.last); - expect(error, throwsA(AirdropApiMock.shareReceiveTimeout.res)); - verify(airdropApi.share(req.first, req.last)).called(1); - }); - - test('share exception other', () async { - final req = AirdropApiMock.shareExceptionOther.req; - final error = airdropService.share(req.last); - expect(error, throwsA(AirdropApiMock.shareExceptionOther.res)); - verify(airdropApi.share(req.first, req.last)).called(1); - }); - }); - - group('claimShare api', () { - test('claimShare valid', () async { - final req = AirdropClaimShareRequest( - shareCode: AirdropApiMock.claimShareValid.req, - ); - expect(await airdropService.claimShare(req), - AirdropApiMock.claimShareValid.res); - - verify(airdropApi.claimShare(req.shareCode)).called(1); - }); - - test('claimShare 400', () async { - final req = AirdropClaimShareRequest( - shareCode: AirdropApiMock.claimShareDioException4xx.req, - ); - final error = airdropService.claimShare(req); - expect(error, throwsA(AirdropApiMock.claimShareDioException4xx.res)); - verify(airdropApi.claimShare(req.shareCode)).called(1); - }); - - test('claimShare 500', () async { - final req = AirdropClaimShareRequest( - shareCode: AirdropApiMock.claimShareDioException5xx.req, - ); - final error = airdropService.claimShare(req); - expect(error, throwsA(AirdropApiMock.claimShareDioException5xx.res)); - verify(airdropApi.claimShare(req.shareCode)).called(1); - }); - - test('claimShare connection timeout', () async { - final req = AirdropClaimShareRequest( - shareCode: AirdropApiMock.claimShareConnectionTimeout.req, - ); - final error = airdropService.claimShare(req); - expect(error, throwsA(AirdropApiMock.claimShareConnectionTimeout.res)); - verify(airdropApi.claimShare(req.shareCode)).called(1); - }); - - test('claimShare receive timeout', () async { - final req = AirdropClaimShareRequest( - shareCode: AirdropApiMock.claimShareReceiveTimeout.req, - ); - final error = airdropService.claimShare(req); - expect(error, throwsA(AirdropApiMock.claimShareReceiveTimeout.res)); - verify(airdropApi.claimShare(req.shareCode)).called(1); - }); - - test('claimShare exception other', () async { - final req = AirdropClaimShareRequest( - shareCode: AirdropApiMock.claimShareExceptionOther.req, - ); - final error = airdropService.claimShare(req); - expect(error, throwsA(AirdropApiMock.claimShareExceptionOther.res)); - verify(airdropApi.claimShare(req.shareCode)).called(1); - }); - }); - - group('requestClaim api', () { - test('requestClaim valid', () async { - final req = AirdropApiMock.requestClaimValid.req; - expect(await airdropService.requestClaim(req), - AirdropApiMock.requestClaimValid.res); - - verify(airdropApi.requestClaim(req)).called(1); - }); - - test('requestClaim 400', () async { - final req = AirdropApiMock.requestClaimDioException4xx.req; - final error = airdropService.requestClaim(req); - expect(error, throwsA(AirdropApiMock.requestClaimDioException4xx.res)); - verify(airdropApi.requestClaim(req)).called(1); - }); - - test('requestClaim 500', () async { - final req = AirdropApiMock.requestClaimDioException5xx.req; - final error = airdropService.requestClaim(req); - expect(error, throwsA(AirdropApiMock.requestClaimDioException5xx.res)); - verify(airdropApi.requestClaim(req)).called(1); - }); - - test('requestClaim connection timeout', () async { - final req = AirdropApiMock.requestClaimConnectionTimeout.req; - final error = airdropService.requestClaim(req); - expect(error, throwsA(AirdropApiMock.requestClaimConnectionTimeout.res)); - verify(airdropApi.requestClaim(req)).called(1); - }); - - test('requestClaim receive timeout', () async { - final req = AirdropApiMock.requestClaimReceiveTimeout.req; - final error = airdropService.requestClaim(req); - expect(error, throwsA(AirdropApiMock.requestClaimReceiveTimeout.res)); - verify(airdropApi.requestClaim(req)).called(1); - }); - - test('requestClaim exception other', () async { - final req = AirdropApiMock.requestClaimExceptionOther.req; - final error = airdropService.requestClaim(req); - expect(error, throwsA(AirdropApiMock.requestClaimExceptionOther.res)); - verify(airdropApi.requestClaim(req)).called(1); - }); - }); -} diff --git a/test/services/tezos_service_test.dart b/test/services/tezos_service_test.dart index 2620456312..70ce91d6d1 100644 --- a/test/services/tezos_service_test.dart +++ b/test/services/tezos_service_test.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:typed_data'; +import 'package:autonomy_flutter/service/network_issue_manager.dart'; import 'package:autonomy_flutter/service/tezos_service.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:libauk_dart/libauk_dart.dart'; @@ -8,8 +9,7 @@ import 'package:tezart/tezart.dart'; import 'package:uuid/uuid.dart'; void main() { - final client = TezartClient('https://ghostnet.tezos.marigold.dev'); - final tezosService = TezosServiceImpl(client); + final tezosService = TezosServiceImpl(NetworkIssueManager()); final walletStorage = MockWalletStorage(const Uuid().v4()); const publicKey = 'edpkvB8a5H6uwbzKysXRzZ96EqT5pVouZFvz6Qye67sgcZFkSZS92x';