diff --git a/lib/blocs/create_manifest/create_manifest_cubit.dart b/lib/blocs/create_manifest/create_manifest_cubit.dart index f4e2775162..02532bb9bd 100644 --- a/lib/blocs/create_manifest/create_manifest_cubit.dart +++ b/lib/blocs/create_manifest/create_manifest_cubit.dart @@ -347,7 +347,7 @@ class CreateManifestCubit extends Cubit { return ARNSUndername( name: '@', domain: _selectedAntRecord!.domain, - record: ARNSRecord( + record: const ARNSRecord( transactionId: 'to_assign', ttlSeconds: 3600, ), diff --git a/lib/blocs/upload/upload_cubit.dart b/lib/blocs/upload/upload_cubit.dart index f995653243..1ee7243204 100644 --- a/lib/blocs/upload/upload_cubit.dart +++ b/lib/blocs/upload/upload_cubit.dart @@ -10,6 +10,7 @@ import 'package:ardrive/blocs/upload/upload_file_checker.dart'; import 'package:ardrive/core/activity_tracker.dart'; import 'package:ardrive/core/upload/domain/repository/upload_repository.dart'; import 'package:ardrive/core/upload/uploader.dart'; +import 'package:ardrive/core/upload/view/blocs/upload_manifest_options_bloc.dart'; import 'package:ardrive/main.dart'; import 'package:ardrive/manifest/domain/manifest_repository.dart'; import 'package:ardrive/models/forms/cc.dart'; @@ -102,29 +103,22 @@ class UploadCubit extends Cubit { late final bool _isUploadFolders; /// Manifest - List _manifestFiles = []; - final List _selectedManifestFiles = []; + + Map _manifestFiles = {}; + final List _selectedManifestModels = []; UploadMethod? _manifestUploadMethod; bool _isManifestsUploadCancelled = false; - void selectManifestFile(FileEntry file) { - final readyState = state as UploadReady; - - final newReadyState = readyState.copyWith( - selectedManifests: List.of(_selectedManifestFiles)..add(file)); - - _selectedManifestFiles.add(file); + void updateManifestSelection(List selections) { + _selectedManifestModels.clear(); - emit(newReadyState); - } - - void unselectManifestFile(FileEntry file) { - _selectedManifestFiles.remove(file); + _selectedManifestModels.addAll(selections); - emit((state as UploadReady) - .copyWith(selectedManifests: _selectedManifestFiles)); + emit((state as UploadReady).copyWith( + selectedManifestSelections: selections, + )); } void setManifestUploadMethod( @@ -133,18 +127,16 @@ class UploadCubit extends Cubit { } Future prepareManifestUpload() async { - final manifestModels = _selectedManifestFiles - .map( - (f) => UploadManifestModel( - name: f.name, - isCompleted: false, - freeThanksToTurbo: - f.size <= configService.config.allowedDataItemSizeForTurbo, - isUploading: false, - existingManifestFileId: f.id, - ), - ) + final manifestModels = _selectedManifestModels + .map((e) => UploadManifestModel( + entry: e.manifest, + freeThanksToTurbo: false, + existingManifestFileId: e.manifest.id, + antRecord: e.antRecord, + undername: e.undername, + )) .toList(); + for (int i = 0; i < manifestModels.length; i++) { if (_isManifestsUploadCancelled) { break; @@ -153,7 +145,7 @@ class UploadCubit extends Cubit { manifestModels[i] = manifestModels[i].copyWith(isUploading: true); await _createManifestCubit.prepareManifestTx( - manifestName: manifestModels[i].name, + manifestName: manifestModels[i].entry.name, folderId: _targetFolder.id, existingManifestFileId: manifestModels[i].existingManifestFileId, ); @@ -208,7 +200,7 @@ class UploadCubit extends Cubit { )); await _createManifestCubit.prepareManifestTx( - manifestName: manifestModels[i].name, + manifestName: manifestModels[i].entry.name, folderId: _targetFolder.id, existingManifestFileId: manifestModels[i].existingManifestFileId, ); @@ -222,25 +214,70 @@ class UploadCubit extends Cubit { method: _manifestUploadMethod, ); - manifestModels[i] = - manifestModels[i].copyWith(isCompleted: true, isUploading: false); + final manifestFile = await _driveDao + .fileById( + driveId: _driveId, + fileId: manifestModels[i].existingManifestFileId, + ) + .getSingleOrNull(); - emit(UploadingManifests( - manifestFiles: manifestModels, - completedCount: ++completedCount, - )); + if (manifestFile == null) { + throw StateError('Manifest file not found'); + } + + if (manifestModels[i].antRecord != null) { + ARNSUndername undername; + + if (manifestModels[i].undername == null) { + undername = ARNSUndername( + name: '@', + domain: manifestModels[i].antRecord!.domain, + record: ARNSRecord( + transactionId: manifestFile.dataTxId, + ttlSeconds: 3600, + ), + ); + } else { + undername = ARNSUndername( + name: manifestModels[i].undername!.name, + domain: manifestModels[i].antRecord!.domain, + record: ARNSRecord( + transactionId: manifestFile.dataTxId, + ttlSeconds: 3600, + ), + ); + } + + manifestModels[i] = manifestModels[i].copyWith( + isCompleted: false, isUploading: false, isAssigningUndername: true); + emit(UploadingManifests( + manifestFiles: manifestModels, + completedCount: ++completedCount, + )); + + await _arnsRepository.setUndernamesToFile( + undername: undername, + driveId: _driveId, + fileId: manifestModels[i].existingManifestFileId, + processId: manifestModels[i].antRecord!.processId, + ); + + manifestModels[i] = manifestModels[i].copyWith( + isCompleted: true, isUploading: false, isAssigningUndername: false); + + emit(UploadingManifests( + manifestFiles: manifestModels, + completedCount: completedCount, + )); + } } - emit(UploadComplete( - manifestFiles: _selectedManifestFiles, - )); + emit(UploadComplete()); } void cancelManifestsUpload() { _isManifestsUploadCancelled = true; - emit(UploadComplete( - manifestFiles: _selectedManifestFiles, - )); + emit(UploadComplete()); } /// License forms @@ -272,6 +309,7 @@ class UploadCubit extends Cubit { /// ArNS ANTRecord? _selectedAntRecord; + List _ants = []; ARNSUndername? _selectedUndername; /// Thumbnail upload @@ -403,10 +441,12 @@ class UploadCubit extends Cubit { loadingArNSNames: true, arnsCheckboxChecked: _showArnsNameSelectionCheckBoxValue, totalSize: await _getTotalSize(), - selectedManifests: _selectedManifestFiles, showSettings: showSettings, canShowSettings: showSettings, - manifestFiles: _manifestFiles, + manifestFiles: _manifestFiles.values.toList(), + arnsRecords: _ants, + showReviewButtonText: false, + selectedManifestSelections: _selectedManifestModels, ), ); @@ -445,10 +485,12 @@ class UploadCubit extends Cubit { showArnsNameSelection: false, arnsCheckboxChecked: _showArnsNameSelectionCheckBoxValue, totalSize: await _getTotalSize(), - selectedManifests: _selectedManifestFiles, showSettings: showSettings, - manifestFiles: _manifestFiles, + manifestFiles: _manifestFiles.values.toList(), + arnsRecords: _ants, canShowSettings: showSettings, + showReviewButtonText: false, + selectedManifestSelections: _selectedManifestModels, ), ); } @@ -459,7 +501,7 @@ class UploadCubit extends Cubit { if (state is UploadReady) { if (_showArnsNameSelectionCheckBoxValue) { showArnsNameSelection(state as UploadReady); - } else if (_selectedManifestFiles.isNotEmpty) { + } else if (_selectedManifestModels.isNotEmpty) { emit(UploadReview(readyState: state as UploadReady)); } else { final readyState = state as UploadReady; @@ -815,11 +857,22 @@ class UploadCubit extends Cubit { emit(UploadLoadingFilesSuccess()); } + Future> getARNSUndernames( + ANTRecord antRecord, + ) async { + return _arnsRepository.getARNSUndernames(antRecord); + } + Future startUploadPreparation({ bool isRetryingToPayWithTurbo = false, }) async { final walletAddress = await _auth.getWalletAddress(); - _arnsRepository.getAntRecordsForWallet(walletAddress!); + _arnsRepository.getAntRecordsForWallet(walletAddress!).then((value) { + _ants = value; + if (state is UploadReady) { + emit((state as UploadReady).copyWith(arnsRecords: value)); + } + }); _files .removeWhere((file) => filesNamesToExclude.contains(file.ioFile.name)); @@ -942,16 +995,34 @@ class UploadCubit extends Cubit { ), ); - _manifestFiles = await _manifestRepository.getManifestFilesInFolder( + final manifestFileEntries = + await _manifestRepository.getManifestFilesInFolder( folderId: _targetFolder.id, driveId: _targetDrive.id, ); + _manifestFiles = {}; + + for (var entry in manifestFileEntries) { + _manifestFiles[entry.id] = UploadManifestModel( + entry: entry, + existingManifestFileId: entry.id, + freeThanksToTurbo: + entry.size <= configService.config.allowedDataItemSizeForTurbo, + ); + } + // if there are no files that can be used to generate a thumbnail, we disable the option if (!containsSupportedImageTypeForThumbnailGeneration) { _uploadThumbnail = false; } + if (manifestFileEntries.isNotEmpty) { + // load arns names + await _arnsRepository + .getAntRecordsForWallet(_auth.currentUser.walletAddress); + } + emit( UploadReadyToPrepare( params: UploadParams( @@ -1171,13 +1242,11 @@ class UploadCubit extends Cubit { ); } - if (_selectedManifestFiles.isNotEmpty) { + if (_selectedManifestModels.isNotEmpty) { await prepareManifestUpload(); } - emit(UploadComplete( - manifestFiles: _selectedManifestFiles, - )); + emit(UploadComplete()); unawaited(_profileCubit.refreshBalance()); }, @@ -1251,11 +1320,11 @@ class UploadCubit extends Cubit { 'Upload finished with success. Number of tasks: ${tasks.length}', ); - if (_selectedManifestFiles.isNotEmpty) { + if (_selectedManifestModels.isNotEmpty) { await prepareManifestUpload(); } - emit(UploadComplete(manifestFiles: _selectedManifestFiles)); + emit(UploadComplete()); PlausibleEventTracker.trackUploadSuccess(); }, @@ -1334,7 +1403,7 @@ class UploadCubit extends Cubit { return ARNSUndername( name: '@', domain: _selectedAntRecord!.domain, - record: ARNSRecord( + record: const ARNSRecord( transactionId: 'to_assign', ttlSeconds: 3600, ), diff --git a/lib/blocs/upload/upload_state.dart b/lib/blocs/upload/upload_state.dart index 340c9c7a94..c71c3f7db9 100644 --- a/lib/blocs/upload/upload_state.dart +++ b/lib/blocs/upload/upload_state.dart @@ -107,12 +107,14 @@ class UploadReady extends UploadState { final bool loadingArNSNamesError; final bool arnsCheckboxChecked; final int totalSize; - final List selectedManifests; - final List manifestFiles; + final List manifestFiles; + final List selectedManifestSelections; final bool showSettings; final bool canShowSettings; + final List arnsRecords; final bool isArConnect; + final bool showReviewButtonText; UploadReady({ required this.paymentInfo, @@ -128,10 +130,12 @@ class UploadReady extends UploadState { this.loadingArNSNamesError = false, required this.arnsCheckboxChecked, required this.totalSize, - required this.selectedManifests, required this.showSettings, required this.canShowSettings, required this.manifestFiles, + required this.arnsRecords, + required this.showReviewButtonText, + required this.selectedManifestSelections, }); // copyWith @@ -151,9 +155,11 @@ class UploadReady extends UploadState { bool? loadingArNSNamesError, bool? arnsCheckboxChecked, int? totalSize, - List? selectedManifests, - List? manifestFiles, + List? manifestFiles, bool? canShowSettings, + List? arnsRecords, + bool? showReviewButtonText, + List? selectedManifestSelections, }) { return UploadReady( loadingArNSNames: loadingArNSNames ?? this.loadingArNSNames, @@ -171,10 +177,13 @@ class UploadReady extends UploadState { loadingArNSNamesError ?? this.loadingArNSNamesError, arnsCheckboxChecked: arnsCheckboxChecked ?? this.arnsCheckboxChecked, totalSize: totalSize ?? this.totalSize, - selectedManifests: selectedManifests ?? this.selectedManifests, showSettings: showSettings ?? this.showSettings, manifestFiles: manifestFiles ?? this.manifestFiles, canShowSettings: canShowSettings ?? this.canShowSettings, + arnsRecords: arnsRecords ?? this.arnsRecords, + showReviewButtonText: showReviewButtonText ?? this.showReviewButtonText, + selectedManifestSelections: + selectedManifestSelections ?? this.selectedManifestSelections, ); } @@ -288,10 +297,9 @@ class UploadFailure extends UploadState { } class UploadComplete extends UploadState { - final List manifestFiles; final ARNSRecord? arnsRecord; - UploadComplete({required this.manifestFiles, this.arnsRecord}); + UploadComplete({this.arnsRecord}); } class UploadingManifests extends UploadState { @@ -304,7 +312,7 @@ class UploadingManifests extends UploadState { }); @override - List get props => [manifestFiles, completedCount]; + List get props => [UniqueKey()]; } class UploadWalletMismatch extends UploadState {} @@ -345,20 +353,28 @@ class UploadManifestSelectPaymentMethod extends UploadState { } class UploadManifestModel extends Equatable { - final String name; + final FileEntry entry; final bool isCompleted; + final bool isAssigningUndername; final bool freeThanksToTurbo; final bool isUploading; - final String? existingManifestFileId; + final String existingManifestFileId; final IOFile? file; + final ARNSUndername? undername; + final ANTRecord? antRecord; + final bool selectionExpanded; const UploadManifestModel({ - required this.name, + required this.entry, this.isCompleted = false, required this.freeThanksToTurbo, this.isUploading = false, - this.existingManifestFileId, + required this.existingManifestFileId, this.file, + this.undername, + this.antRecord, + this.isAssigningUndername = false, + this.selectionExpanded = false, }); UploadManifestModel copyWith({ @@ -367,25 +383,38 @@ class UploadManifestModel extends Equatable { String? existingManifestFileId, bool? freeThanksToTurbo, IOFile? file, + ARNSUndername? undername, + ANTRecord? antRecord, + bool? isAssigningUndername, + FileEntry? entry, + bool? selectionExpanded, }) { return UploadManifestModel( - name: name, + entry: entry ?? this.entry, isCompleted: isCompleted ?? this.isCompleted, isUploading: isUploading ?? this.isUploading, existingManifestFileId: existingManifestFileId ?? this.existingManifestFileId, freeThanksToTurbo: freeThanksToTurbo ?? this.freeThanksToTurbo, file: file ?? this.file, + undername: undername ?? this.undername, + antRecord: antRecord ?? this.antRecord, + isAssigningUndername: isAssigningUndername ?? this.isAssigningUndername, + selectionExpanded: selectionExpanded ?? this.selectionExpanded, ); } @override List get props => [ - name, + entry, isCompleted, isUploading, existingManifestFileId, - freeThanksToTurbo + freeThanksToTurbo, + isAssigningUndername, + antRecord, + undername, + file, ]; } diff --git a/lib/components/upload_form.dart b/lib/components/upload_form.dart index 1f1016db4c..da7a9de389 100644 --- a/lib/components/upload_form.dart +++ b/lib/components/upload_form.dart @@ -27,6 +27,8 @@ import 'package:ardrive/core/arfs/repository/folder_repository.dart'; import 'package:ardrive/core/arfs/utils/arfs_revision_status_utils.dart'; import 'package:ardrive/core/upload/domain/repository/upload_repository.dart'; import 'package:ardrive/core/upload/uploader.dart'; +import 'package:ardrive/core/upload/view/blocs/upload_manifest_options_bloc.dart'; +import 'package:ardrive/core/upload/view/manifest_options/manifest_options.dart'; import 'package:ardrive/entities/manifest_data.dart'; import 'package:ardrive/l11n/validation_messages.dart'; import 'package:ardrive/main.dart'; @@ -50,6 +52,7 @@ import 'package:ardrive_ui/ardrive_ui.dart'; import 'package:ardrive_uploader/ardrive_uploader.dart'; import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:ario_sdk/ario_sdk.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -251,8 +254,57 @@ class _UploadFormState extends State { state is UploadPreparationInitialized) { return _PreparingUploadWidget(state: state); } else if (state is UploadReady) { - return _UploadReadyWidget( - state: state, driveDetailCubit: widget.driveDetailCubit); + return BlocProvider( + create: (context) { + List manifestSelections = []; + List selectedManifestIds = []; + + manifestSelections = state.manifestFiles.map((e) { + final selectedManifest = state.selectedManifestSelections + .firstWhereOrNull((selectedManifest) => + selectedManifest.manifest.id == e.entry.id); + + ANTRecord? antRecord; + ARNSUndername? undername; + + if (selectedManifest != null) { + antRecord = selectedManifest.antRecord; + undername = selectedManifest.undername; + selectedManifestIds.add(e.entry.id); + } + + return ManifestSelection( + manifest: e.entry, + antRecord: antRecord, + undername: undername, + ); + }).toList(); + + return UploadManifestOptionsBloc( + manifestFiles: manifestSelections, + arnsRepository: context.read(), + arDriveAuth: context.read(), + selectedManifestIds: selectedManifestIds, + )..add(LoadAnts()); + }, + child: BlocListener( + listener: (context, state) { + if (state is UploadManifestOptionsReady) { + context.read().updateManifestSelection( + state.manifestFiles + .where((e) => state.selectedManifestIds + .contains(e.manifest.id)) + .toList(), + ); + } + }, + child: _UploadReadyWidget( + state: state, + driveDetailCubit: widget.driveDetailCubit, + ), + ), + ); } else if (state is UploadConfiguringLicense) { return _UploadConfiguringLicenseWidget(state: state); } else if (state is UploadReview) { @@ -328,31 +380,43 @@ class _UploadingManifestsWidget extends StatelessWidget { itemCount: state.manifestFiles.length, shrinkWrap: true, itemBuilder: (context, index) { - return Row( + return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - ArDriveIcons.manifest(size: 16), - const SizedBox(width: 8), - Text( - '${state.manifestFiles[index].name}...', - style: typography.paragraphNormal( - fontWeight: ArFontWeight.semiBold, - ), - ), - const Spacer(), - if (state.manifestFiles[index].isUploading) ...[ - const SizedBox(width: 8), - const SizedBox( - height: 16, - width: 16, - child: CircularProgressIndicator( - strokeWidth: 2, + Row( + children: [ + ArDriveIcons.manifest(size: 16), + const SizedBox(width: 8), + Text( + '${state.manifestFiles[index].entry.name}...', + style: typography.paragraphNormal( + fontWeight: ArFontWeight.semiBold, + ), ), + const Spacer(), + if (state.manifestFiles[index].isUploading) ...[ + const SizedBox(width: 8), + const SizedBox( + height: 16, + width: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ), + ], + if (state.manifestFiles[index].isCompleted) ...[ + const SizedBox(width: 8), + ArDriveIcons.checkCirle(size: 16), + ], + ], + ), + if (state.manifestFiles[index].isAssigningUndername) ...[ + const SizedBox(height: 2), + Text( + 'Assigning ArNS Name...', + style: typography.paragraphNormal(), ), ], - if (state.manifestFiles[index].isCompleted) ...[ - const SizedBox(width: 8), - ArDriveIcons.checkCirle(size: 16), - ], ], ); }, @@ -1008,72 +1072,8 @@ class _UploadReadyModalBaseState extends State { Widget manifestOptionsView( UploadReady state, BuildContext context, ArdriveTypographyNew typography, {bool scrollable = true}) { - return Padding( - padding: const EdgeInsets.only(bottom: 42.0), - child: ListView.builder( - physics: scrollable - ? const ScrollPhysics() - : const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: state.manifestFiles.length, - itemBuilder: (context, index) { - final file = state.manifestFiles[index]; - final hiddenColor = - ArDriveTheme.of(context).themeData.colors.themeFgDisabled; - - return Row( - mainAxisSize: MainAxisSize.max, - children: [ - Flexible( - flex: 2, - child: Row( - children: [ - ArDriveIcons.manifest( - size: 16, color: file.isHidden ? hiddenColor : null), - const SizedBox(width: 8), - Text( - file.name, - style: typography.paragraphNormal( - color: file.isHidden ? hiddenColor : null, - ), - ), - if (file.isHidden) ...[ - const SizedBox(width: 8), - Text('(hidden)', - style: typography.paragraphNormal( - color: hiddenColor, - )) - ] - ], - ), - ), - Flexible( - flex: 1, - child: ArDriveCheckBox( - checked: state.selectedManifests - .contains(state.manifestFiles[index]), - onChange: (value) { - if (value) { - context - .read() - .selectManifestFile(state.manifestFiles[index]); - } else { - context - .read() - .unselectManifestFile(state.manifestFiles[index]); - } - }, - ), - ), - // TODO: Add back when we have the right UI for it - // const Expanded( - // flex: 1, - // child: AntSelector(), - // ), - ], - ); - }, - ), + return ManifestOptions( + scrollable: scrollable, ); } } @@ -2031,6 +2031,17 @@ class _UploadReviewWithLicenseWidget extends StatelessWidget { final typography = ArDriveTypographyNew.of(context); final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; + double heightForManifestSelections = + (readyState.selectedManifestSelections.length * 30) + 16; + + if (heightForManifestSelections > 200) { + heightForManifestSelections = 175; + } + + if (readyState.params.arnsUnderName == null) { + heightForManifestSelections += 50; + } + return StatsScreen( readyState: readyState, modalActions: [ @@ -2081,12 +2092,13 @@ class _UploadReviewWithLicenseWidget extends StatelessWidget { ), ), ], - if (state.readyState.selectedManifests.isNotEmpty) ...[ + LicenseReviewInfo(licenseState: state.licenseState), + if (state.readyState.selectedManifestSelections.isNotEmpty) ...[ Padding( - padding: const EdgeInsets.only(bottom: 16.0), + padding: const EdgeInsets.only(bottom: 8.0), child: ConstrainedBox( - constraints: const BoxConstraints( - maxHeight: 125, + constraints: BoxConstraints( + maxHeight: heightForManifestSelections, minWidth: kLargeDialogWidth, ), child: Column( @@ -2094,7 +2106,6 @@ class _UploadReviewWithLicenseWidget extends StatelessWidget { mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, children: [ - const SizedBox(height: 8), Text( 'Updated manifest(s):', style: typography.paragraphNormal( @@ -2103,36 +2114,61 @@ class _UploadReviewWithLicenseWidget extends StatelessWidget { ), ), const SizedBox(height: 4), - Flexible( - child: Expanded( - child: ListView( - shrinkWrap: true, - children: [ - ...state.readyState.selectedManifests.map( - (e) => Row( - children: [ - ArDriveIcons.manifest(size: 16), - const SizedBox(width: 8), - Text( - e.name, - style: typography.paragraphNormal( - fontWeight: ArFontWeight.semiBold, + Expanded( + child: ListView( + shrinkWrap: true, + children: [ + ...state.readyState.selectedManifestSelections.map( + (e) => Column( + children: [ + Row( + children: [ + ArDriveIcons.manifest(size: 16), + const SizedBox(width: 8), + Flexible( + child: Text( + e.manifest.name, + style: typography.paragraphNormal( + fontWeight: ArFontWeight.semiBold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), ), + ], + ), + if (e.antRecord != null || + e.undername != null) ...[ + const SizedBox(height: 2), + Row( + children: [ + ArDriveIcons.arnsName(size: 16), + const SizedBox(width: 8), + Flexible( + child: Text( + getLiteralArNSName( + e.antRecord!, e.undername), + style: typography.paragraphNormal( + fontWeight: ArFontWeight.semiBold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], ), ], - ), + ], ), - ], - ), + ), + ], ), ), - const SizedBox(height: 8), ], ), ), ), ], - LicenseReviewInfo(licenseState: state.licenseState), ], ); } @@ -2148,6 +2184,17 @@ class _UploadReviewWithArnsNameWidget extends StatelessWidget { final typography = ArDriveTypographyNew.of(context); final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; + double heightForManifestSelections = + (state.readyState.selectedManifestSelections.length * 30) + 16; + + if (heightForManifestSelections > 200) { + heightForManifestSelections = 200; + } + + if (state.readyState.params.arnsUnderName == null) { + heightForManifestSelections += 50; + } + return StatsScreen( readyState: state.readyState, modalActions: [ @@ -2193,12 +2240,12 @@ class _UploadReviewWithArnsNameWidget extends StatelessWidget { ), ), ], - if (state.readyState.selectedManifests.isNotEmpty) ...[ + if (state.readyState.selectedManifestSelections.isNotEmpty) ...[ Padding( padding: const EdgeInsets.only(bottom: 16.0), child: ConstrainedBox( - constraints: const BoxConstraints( - // maxHeight: 125, + constraints: BoxConstraints( + maxHeight: heightForManifestSelections, minWidth: kLargeDialogWidth, ), child: Column( @@ -2206,7 +2253,6 @@ class _UploadReviewWithArnsNameWidget extends StatelessWidget { mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, children: [ - const SizedBox(height: 8), Text( 'Updated manifest(s):', style: typography.paragraphNormal( @@ -2216,27 +2262,57 @@ class _UploadReviewWithArnsNameWidget extends StatelessWidget { ), const SizedBox(height: 4), Flexible( - child: ListView( - shrinkWrap: true, - children: [ - ...state.readyState.selectedManifests.map( - (e) => Row( - mainAxisSize: MainAxisSize.min, - children: [ - ArDriveIcons.manifest(size: 16), - const SizedBox(width: 8), - Text( - e.name, - style: typography.paragraphNormal( - fontWeight: ArFontWeight.semiBold, + child: Expanded( + child: ListView( + shrinkWrap: true, + children: [ + ...state.readyState.selectedManifestSelections.map( + (e) => Column( + children: [ + Row( + children: [ + ArDriveIcons.manifest(size: 16), + const SizedBox(width: 8), + Flexible( + child: Text( + e.manifest.name, + style: typography.paragraphNormal( + fontWeight: ArFontWeight.semiBold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], ), - ), - ], + if (e.antRecord != null || + e.undername != null) ...[ + const SizedBox(height: 2), + Row( + children: [ + ArDriveIcons.arnsName(size: 16), + const SizedBox(width: 8), + Flexible( + child: Text( + getLiteralArNSName( + e.antRecord!, e.undername), + style: typography.paragraphNormal( + fontWeight: ArFontWeight.semiBold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ], + ), ), - ), - ], + ], + ), ), - ) + ), ], ), ), @@ -2879,7 +2955,7 @@ List getModalActions( variant: ButtonVariant.primary, ), ]; - } else if (state.selectedManifests.isNotEmpty) { + } else if (state.selectedManifestSelections.isNotEmpty) { return [ ArDriveButtonNew( isDisabled: !state.isNextButtonEnabled, @@ -2910,83 +2986,11 @@ List getModalActions( ]; } -class AntSelector extends StatefulWidget { - const AntSelector({super.key}); - - @override - State createState() => _AntSelectorState(); -} - -class _AntSelectorState extends State { - final List ants = ['Ant 1', 'Ant 2', 'Ant 3']; - String _selectedAnt = 'Ant 1'; - - @override - Widget build(BuildContext context) { - return ArDriveDropdown( - height: 45, - items: ants.map((ant) => _buildDropdownItem(context, ant)).toList(), - child: ArDriveClickArea( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Row( - children: [ - _buildSelectedItem(context), - ], - ), - ArDriveIcons.chevronDown(), - ], - ), - ), - ); +String getLiteralArNSName(ANTRecord record, ARNSUndername? undername) { + if (undername != null) { + return getLiteralARNSRecordName(undername); } - Widget _buildSelectedItem(BuildContext context) { - final typography = ArDriveTypographyNew.of(context); - - return Text( - _selectedAnt, - style: typography.paragraphLarge( - fontWeight: ArFontWeight.semiBold, - ), - ); - } - - ArDriveDropdownItem _buildDropdownItem(BuildContext context, String ant) { - final typography = ArDriveTypographyNew.of(context); - - return ArDriveDropdownItem( - content: SizedBox( - width: 164, - height: 45, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - ant, - style: typography.paragraphLarge( - fontWeight: ArFontWeight.semiBold, - ), - ), - if (ant == 'Ant 1') - ArDriveIcons.checkmark( - size: 16, - ) - ], - ), - ), - ), - onClick: () { - setState(() { - _selectedAnt = ant; - }); - }, - ); - } + return record.domain; } - // diff --git a/lib/core/upload/view/blocs/upload_manifest_options_bloc.dart b/lib/core/upload/view/blocs/upload_manifest_options_bloc.dart new file mode 100644 index 0000000000..784a3eca9b --- /dev/null +++ b/lib/core/upload/view/blocs/upload_manifest_options_bloc.dart @@ -0,0 +1,164 @@ +import 'package:ardrive/arns/domain/arns_repository.dart'; +import 'package:ardrive/authentication/ardrive_auth.dart'; +import 'package:ardrive/models/models.dart'; +import 'package:ario_sdk/ario_sdk.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +part 'upload_manifest_options_event.dart'; +part 'upload_manifest_options_state.dart'; + +/// Outputs the selected manifest +/// with the corresponding names if any +class UploadManifestOptionsBloc + extends Bloc { + final ARNSRepository arnsRepository; + final ArDriveAuth arDriveAuth; + + final List manifestFiles; + final Set _selectedManifestIds = {}; + final Set _showingArNSSelection = {}; + Map> reservedNames = {}; + + List? _ants; + + UploadManifestOptionsBloc({ + required this.manifestFiles, + required this.arnsRepository, + required this.arDriveAuth, + List? selectedManifestIds, + }) : super(UploadManifestOptionsReady( + manifestFiles: Set.from(manifestFiles), + selectedManifestIds: Set.from(selectedManifestIds ?? []), + showingArNSSelection: const {}, + ants: null, + reservedNames: const {}, + arnsNamesLoaded: false, + )) { + if (selectedManifestIds != null) { + _selectedManifestIds.addAll(selectedManifestIds); + + final selectedManifests = selectedManifestIds + .map((id) => manifestFiles.firstWhere((e) => e.manifest.id == id)) + .toList(); + + for (var manifest in selectedManifests) { + if (manifest.antRecord != null) { + if (reservedNames[manifest.antRecord!.domain] == null) { + reservedNames[manifest.antRecord!.domain] = []; + } + + reservedNames[manifest.antRecord!.domain]! + .add(manifest.undername?.name ?? '@'); + } + } + } + + on((event, emit) async { + final walletAddress = await arDriveAuth.getWalletAddress(); + _ants = await arnsRepository.getAntRecordsForWallet(walletAddress!); + emit(_createReadyState()); + }); + + on((event, emit) async { + _selectedManifestIds.add(event.manifest.id); + emit(_createReadyState()); + }); + + on((event, emit) { + _selectedManifestIds.remove(event.manifest.id); + _showingArNSSelection.remove(event.manifest.id); + + final indexOf = + manifestFiles.indexWhere((e) => e.manifest.id == event.manifest.id); + + if (manifestFiles[indexOf].antRecord != null) { + reservedNames[manifestFiles[indexOf].antRecord!.domain]! + .remove(manifestFiles[indexOf].undername?.name ?? '@'); + + manifestFiles[indexOf] = ManifestSelection( + manifest: manifestFiles[indexOf].manifest, + ); + } + + emit(_createReadyState()); + }); + + on((event, emit) async { + _showingArNSSelection.add(event.manifest.id); + + emit(_createReadyState()); + }); + + on((event, emit) { + _showingArNSSelection.remove(event.manifest.id); + emit(_createReadyState()); + }); + + on((event, emit) { + if (reservedNames[event.antRecord.domain] == null) { + reservedNames[event.antRecord.domain] = []; + } + + final indexOf = + manifestFiles.indexWhere((e) => e.manifest.id == event.manifest.id); + + final manifest = manifestFiles[indexOf]; + + if (manifest.antRecord != null) { + reservedNames[manifest.antRecord!.domain]! + .remove(manifest.undername?.name ?? '@'); + } + + manifestFiles[indexOf] = manifestFiles[indexOf].copyWith( + antRecord: event.antRecord, + undername: event.undername, + ); + + reservedNames[event.antRecord.domain]!.add(event.undername?.name ?? '@'); + _showingArNSSelection.remove(event.manifest.id); + emit(_createReadyState()); + }); + } + + Future> getARNSUndernames( + ANTRecord antRecord, + ) async { + return arnsRepository.getARNSUndernames(antRecord); + } + + UploadManifestOptionsReady _createReadyState() { + return UploadManifestOptionsReady( + manifestFiles: Set.from(manifestFiles), + selectedManifestIds: Set.from(_selectedManifestIds), + showingArNSSelection: Set.from(_showingArNSSelection), + ants: _ants, + reservedNames: reservedNames, + arnsNamesLoaded: _ants != null, + ); + } +} + +class ManifestSelection extends Equatable { + final FileEntry manifest; + final ANTRecord? antRecord; + final ARNSUndername? undername; + + const ManifestSelection({ + required this.manifest, + this.antRecord, + this.undername, + }); + + @override + List get props => [manifest, antRecord?.domain, undername?.name]; + + ManifestSelection copyWith({ + ANTRecord? antRecord, + ARNSUndername? undername, + }) { + return ManifestSelection( + manifest: manifest, antRecord: antRecord, undername: undername); + } +} +// diff --git a/lib/core/upload/view/blocs/upload_manifest_options_event.dart b/lib/core/upload/view/blocs/upload_manifest_options_event.dart new file mode 100644 index 0000000000..d28392cd8e --- /dev/null +++ b/lib/core/upload/view/blocs/upload_manifest_options_event.dart @@ -0,0 +1,49 @@ +part of 'upload_manifest_options_bloc.dart'; + +sealed class UploadManifestOptionsEvent extends Equatable { + const UploadManifestOptionsEvent(); + + @override + List get props => []; +} + +final class SelectManifest extends UploadManifestOptionsEvent { + final FileEntry manifest; + + const SelectManifest({required this.manifest}); + + @override + List get props => [manifest]; +} + +final class DeselectManifest extends UploadManifestOptionsEvent { + final FileEntry manifest; + + const DeselectManifest({required this.manifest}); +} + +final class ShowArNSSelection extends UploadManifestOptionsEvent { + final FileEntry manifest; + + const ShowArNSSelection({required this.manifest}); +} + +final class HideArNSSelection extends UploadManifestOptionsEvent { + final FileEntry manifest; + + const HideArNSSelection({required this.manifest}); +} + +final class LinkManifestToUndername extends UploadManifestOptionsEvent { + final FileEntry manifest; + final ANTRecord antRecord; + final ARNSUndername? undername; + + const LinkManifestToUndername({ + required this.manifest, + required this.antRecord, + required this.undername, + }); +} + +final class LoadAnts extends UploadManifestOptionsEvent {} diff --git a/lib/core/upload/view/blocs/upload_manifest_options_state.dart b/lib/core/upload/view/blocs/upload_manifest_options_state.dart new file mode 100644 index 0000000000..9b8a72164c --- /dev/null +++ b/lib/core/upload/view/blocs/upload_manifest_options_state.dart @@ -0,0 +1,37 @@ +part of 'upload_manifest_options_bloc.dart'; + +sealed class UploadManifestOptionsState extends Equatable { + const UploadManifestOptionsState(); + + @override + List get props => []; +} + +final class UploadManifestOptionsInitial extends UploadManifestOptionsState {} + +final class UploadManifestOptionsReady extends UploadManifestOptionsState { + final Set manifestFiles; + final Set selectedManifestIds; + final Set showingArNSSelection; + final List? ants; + final Map> reservedNames; + final bool arnsNamesLoaded; + + const UploadManifestOptionsReady({ + required this.manifestFiles, + required this.selectedManifestIds, + required this.showingArNSSelection, + required this.ants, + required this.reservedNames, + required this.arnsNamesLoaded, + }); + + @override + List get props => [ + manifestFiles, + selectedManifestIds, + showingArNSSelection, + ants, + reservedNames, + ]; +} diff --git a/lib/core/upload/view/manifest_options/manifest_options.dart b/lib/core/upload/view/manifest_options/manifest_options.dart new file mode 100644 index 0000000000..755957b05f --- /dev/null +++ b/lib/core/upload/view/manifest_options/manifest_options.dart @@ -0,0 +1,518 @@ +import 'package:ardrive/components/components.dart'; +import 'package:ardrive/core/upload/view/blocs/upload_manifest_options_bloc.dart'; +import 'package:ardrive_ui/ardrive_ui.dart'; +import 'package:ario_sdk/ario_sdk.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class ManifestOptions extends StatelessWidget { + const ManifestOptions({super.key, this.scrollable = true}); + + final bool scrollable; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is UploadManifestOptionsReady) { + final selectedManifestIds = state.selectedManifestIds; + final manifestFiles = state.manifestFiles; + + return Padding( + padding: const EdgeInsets.only(bottom: 42.0), + child: ListView.separated( + separatorBuilder: (context, index) => const SizedBox(height: 4), + physics: scrollable + ? const ScrollPhysics() + : const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: manifestFiles.length, + itemBuilder: (context, index) { + final file = manifestFiles.elementAt(index).manifest; + final isSelected = selectedManifestIds.contains(file.id); + + return _ManifestOptionTile( + manifestSelection: manifestFiles.elementAt(index), + isSelected: isSelected, + onSelect: () { + if (isSelected) { + context + .read() + .add(DeselectManifest(manifest: file)); + } else { + context + .read() + .add(SelectManifest(manifest: file)); + } + }, + ); + }, + ), + ); + } + + return const SizedBox(); + }, + ); + } +} + +class _ManifestOptionTile extends StatefulWidget { + final ManifestSelection manifestSelection; + final bool isSelected; + final VoidCallback onSelect; + + const _ManifestOptionTile({ + required this.manifestSelection, + required this.isSelected, + required this.onSelect, + }); + + @override + State<_ManifestOptionTile> createState() => __ManifestOptionTileState(); +} + +class __ManifestOptionTileState extends State<_ManifestOptionTile> { + @override + Widget build(BuildContext context) { + final state = context.read().state; + + if (state is UploadManifestOptionsReady) { + final isExpanded = state.showingArNSSelection + .contains(widget.manifestSelection.manifest.id); + final file = widget.manifestSelection.manifest; + + final hiddenColor = + ArDriveTheme.of(context).themeData.colors.themeFgDisabled; + final typography = ArDriveTypographyNew.of(context); + final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; + final showingName = !isExpanded && + (widget.manifestSelection.antRecord != null || + widget.manifestSelection.undername != null); + final hasSelectedAnt = widget.manifestSelection.antRecord != null; + + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + decoration: BoxDecoration( + color: colorTokens.containerL2, + borderRadius: BorderRadius.circular(5), + ), + curve: Curves.easeInOut, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + height: isExpanded + ? 168 + : showingName + ? 70 + : 50, + child: GestureDetector( + onTap: () {}, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Row( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: Row( + children: [ + Flexible( + flex: 2, + child: Row( + children: [ + ArDriveIcons.manifest( + size: 16, + color: file.isHidden ? hiddenColor : null), + const SizedBox(width: 8), + Text( + file.name, + style: typography.paragraphNormal( + color: file.isHidden ? hiddenColor : null, + ), + ), + if (file.isHidden) ...[ + const SizedBox(width: 8), + Text('(hidden)', + style: typography.paragraphNormal( + color: hiddenColor, + )) + ] + ], + ), + ), + Flexible( + flex: 1, + child: ArDriveCheckBox( + checked: widget.isSelected, + onChange: (value) { + if (value) { + context + .read() + .add(SelectManifest(manifest: file)); + } else { + context + .read() + .add(DeselectManifest(manifest: file)); + } + }, + ), + ), + ], + ), + ), + ArDriveTooltip( + message: (state.arnsNamesLoaded && state.ants!.isEmpty) + ? 'No ArNS names found for your wallet' + : '', + child: ArDriveButtonNew( + text: !state.arnsNamesLoaded + ? 'Loading Names...' + : hasSelectedAnt + ? 'Change ArNS' + : 'Add ArNS', + typography: typography, + isDisabled: isExpanded || + !widget.isSelected || + !state.arnsNamesLoaded || + (state.arnsNamesLoaded && state.ants!.isEmpty), + fontStyle: typography.paragraphSmall(), + variant: ButtonVariant.primary, + maxWidth: state.arnsNamesLoaded ? 100 : 120, + maxHeight: 30, + onPressed: () { + context + .read() + .add(ShowArNSSelection(manifest: file)); + }, + ), + ) + // TODO: Add back when we have the right UI for it + ], + ), + if (showingName) ...[ + const SizedBox(height: 8), + Row( + children: [ + ArDriveIcons.arnsName( + size: 16, + color: colorTokens.textHigh, + ), + const SizedBox(width: 8), + Flexible( + child: Text( + getLiteralArNSName(widget.manifestSelection.antRecord!, + widget.manifestSelection.undername), + style: typography.paragraphNormal( + fontWeight: ArFontWeight.semiBold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + if (isExpanded) ...[ + const SizedBox(height: 8), + Expanded( + flex: 1, + child: AntSelector( + manifestSelection: widget.manifestSelection, + ), + ), + ], + ], + ), + ), + ); + } + return const SizedBox(); + } +} + +class AntSelector extends StatefulWidget { + final ManifestSelection manifestSelection; + + const AntSelector({super.key, required this.manifestSelection}); + + @override + State createState() => _AntSelectorState(); +} + +class _AntSelectorState extends State { + ANTRecord? _selectedAnt; + ARNSUndername? _selectedUndername; + + List _arnsUndernames = []; + bool _loadingUndernames = false; + + Future loadARNSUndernames( + ANTRecord ant, + ) async { + setState(() { + _loadingUndernames = true; + }); + + _arnsUndernames = + await context.read().getARNSUndernames(ant); + _arnsUndernames.removeWhere((e) => e.name == '@'); + + setState(() { + _loadingUndernames = false; + }); + } + + @override + void initState() { + super.initState(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + _selectedAnt = widget.manifestSelection.antRecord; + _selectedUndername = widget.manifestSelection.undername; + } + + @override + Widget build(BuildContext context) { + final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; + final typography = ArDriveTypographyNew.of(context); + + return BlocBuilder( + builder: (context, state) { + if (state is UploadManifestOptionsReady) { + final reservedNames = + context.read().reservedNames; + + bool isNameAlreadyInUse = reservedNames[_selectedAnt?.domain] + ?.contains(_selectedUndername?.name ?? '@') ?? + false; + + return Column( + children: [ + Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: colorTokens.inputDisabled, + border: Border.all( + color: colorTokens.textXLow, + width: 1, + ), + ), + child: ArDriveDropdown( + height: 45, + maxHeight: (state.ants!.length > 6) ? 45 * 6 : null, + items: state.ants! + .map((ant) => _buildDropdownItem(context, ant)) + .toList(), + child: ArDriveClickArea( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Flexible(child: _buildSelectedItem(context)), + ArDriveIcons.chevronDown(), + ], + ), + ), + ), + ), + if (_loadingUndernames) const CircularProgressIndicator(), + if (_selectedUndername != null || + (!_loadingUndernames && _arnsUndernames.isNotEmpty)) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Container( + alignment: Alignment.centerLeft, + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: colorTokens.inputDisabled, + border: Border.all( + color: colorTokens.textXLow, + width: 1, + ), + ), + child: ArDriveDropdown( + maxHeight: (_arnsUndernames.length > 6) ? 45 * 6 : null, + height: 45, + items: _arnsUndernames + .map((undername) => + _buildDropdownItemUndername(context, undername)) + .toList(), + child: ArDriveClickArea( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + children: [ + _buildSelectedItemUndername(context), + ], + ), + ArDriveIcons.chevronDown(), + ], + ), + ), + ), + ), + ), + const Spacer(), + Padding( + padding: const EdgeInsets.only( + top: 8, + left: 8, + bottom: 4, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (isNameAlreadyInUse && + widget.manifestSelection.antRecord?.domain != + _selectedAnt?.domain) + Expanded( + child: Text( + 'Name already in use, please choose another name or select a undername', + style: typography.paragraphSmall( + fontWeight: ArFontWeight.semiBold, + ), + ), + ), + Align( + alignment: Alignment.centerRight, + child: ArDriveButtonNew( + text: 'Add', + typography: typography, + fontStyle: typography.paragraphSmall(), + variant: ButtonVariant.primary, + maxWidth: 80, + maxHeight: 30, + isDisabled: isNameAlreadyInUse || _selectedAnt == null, + onPressed: () { + context + .read() + .add(LinkManifestToUndername( + manifest: widget.manifestSelection.manifest, + antRecord: _selectedAnt!, + undername: _selectedUndername, + )); + }, + ), + ), + ], + ), + ), + ], + ); + } + return const SizedBox(); + }, + ); + } + + Widget _buildSelectedItem(BuildContext context) { + final typography = ArDriveTypographyNew.of(context); + + return Text( + _selectedAnt?.domain ?? 'Choose ArNS name', + style: typography.paragraphSmall( + fontWeight: ArFontWeight.semiBold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ); + } + + Widget _buildSelectedItemUndername(BuildContext context) { + final typography = ArDriveTypographyNew.of(context); + + return Text( + _selectedUndername?.name ?? 'under_name (optional)', + style: typography.paragraphSmall( + fontWeight: ArFontWeight.semiBold, + ), + ); + } + + ArDriveDropdownItem _buildDropdownItem(BuildContext context, ANTRecord ant) { + final typography = ArDriveTypographyNew.of(context); + + return ArDriveDropdownItem( + content: SizedBox( + width: 235, + height: 45, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + ant.domain, + style: typography.paragraphSmall( + fontWeight: ArFontWeight.semiBold, + ), + ), + ), + if (ant.domain == _selectedAnt?.domain) + ArDriveIcons.checkmark( + size: 16, + ) + ], + ), + ), + ), + onClick: () { + setState(() { + _selectedAnt = ant; + + _arnsUndernames = []; + _selectedUndername = null; + loadARNSUndernames(ant); + }); + }, + ); + } + + ArDriveDropdownItem _buildDropdownItemUndername( + BuildContext context, ARNSUndername undername) { + final typography = ArDriveTypographyNew.of(context); + + return ArDriveDropdownItem( + content: SizedBox( + width: 235, + height: 45, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + undername.name, + style: typography.paragraphSmall( + fontWeight: ArFontWeight.semiBold, + ), + ), + ), + if (undername.name == _selectedUndername?.name) + ArDriveIcons.checkmark( + size: 16, + ) + ], + ), + ), + ), + onClick: () { + setState(() { + _selectedUndername = undername; + }); + }, + ); + } +} diff --git a/lib/pages/drive_detail/drive_detail_page.dart b/lib/pages/drive_detail/drive_detail_page.dart index 543da301e2..691b06a7af 100644 --- a/lib/pages/drive_detail/drive_detail_page.dart +++ b/lib/pages/drive_detail/drive_detail_page.dart @@ -842,7 +842,7 @@ class _DriveDetailPageState extends State { List items, GlobalHideState globalHideState, ) { - final isShowingHiddenFiles = globalHideState is HiddingItems; + final isShowingHiddenFiles = globalHideState is ShowingHiddenItems; final List filteredItems; diff --git a/packages/ardrive_ui/lib/src/components/button.dart b/packages/ardrive_ui/lib/src/components/button.dart index 089fb77cb5..4cd2e75dc1 100644 --- a/packages/ardrive_ui/lib/src/components/button.dart +++ b/packages/ardrive_ui/lib/src/components/button.dart @@ -366,8 +366,9 @@ class _ArDriveButtonNewState extends State { final text = Text(widget.text, textAlign: TextAlign.center, - style: typography.paragraphLarge( - color: foregroundColor, fontWeight: ArFontWeight.semiBold)); + style: widget.fontStyle ?? + typography.paragraphLarge( + color: foregroundColor, fontWeight: ArFontWeight.semiBold)); final buttonH = widget.maxHeight ?? buttonDefaultHeight; diff --git a/packages/ario_sdk/lib/src/models/ant_record.dart b/packages/ario_sdk/lib/src/models/ant_record.dart index 6165e85f08..f154fb3bcd 100644 --- a/packages/ario_sdk/lib/src/models/ant_record.dart +++ b/packages/ario_sdk/lib/src/models/ant_record.dart @@ -1,9 +1,14 @@ -class ANTRecord { +import 'package:equatable/equatable.dart'; + +class ANTRecord extends Equatable { final String domain; final String processId; - ANTRecord({ + const ANTRecord({ required this.domain, required this.processId, }); + + @override + List get props => [domain, processId]; } diff --git a/packages/ario_sdk/lib/src/models/arns_record.dart b/packages/ario_sdk/lib/src/models/arns_record.dart index dc86687c5f..fd1394756a 100644 --- a/packages/ario_sdk/lib/src/models/arns_record.dart +++ b/packages/ario_sdk/lib/src/models/arns_record.dart @@ -1,16 +1,20 @@ +import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; part 'arns_record.g.dart'; @JsonSerializable() -class ARNSRecord { +class ARNSRecord extends Equatable { final String transactionId; final int ttlSeconds; - ARNSRecord({required this.transactionId, required this.ttlSeconds}); + const ARNSRecord({required this.transactionId, required this.ttlSeconds}); factory ARNSRecord.fromJson(Map json) => _$ARNSRecordFromJson(json); Map toJson() => _$ARNSRecordToJson(this); + + @override + List get props => [transactionId, ttlSeconds]; } diff --git a/packages/ario_sdk/lib/src/utils/get_literal_arns_record_name.dart b/packages/ario_sdk/lib/src/utils/get_literal_arns_record_name.dart index dcd7018f26..bcd4c8dd66 100644 --- a/packages/ario_sdk/lib/src/utils/get_literal_arns_record_name.dart +++ b/packages/ario_sdk/lib/src/utils/get_literal_arns_record_name.dart @@ -6,3 +6,4 @@ String getLiteralARNSRecordName(ARNSUndername undername) { } return '${undername.name}_${undername.domain}'; } + diff --git a/packages/ario_sdk/test/src/utils/get_literal_arns_record_name_test.dart b/packages/ario_sdk/test/src/utils/get_literal_arns_record_name_test.dart index c69a721ea8..2bd02abc4b 100644 --- a/packages/ario_sdk/test/src/utils/get_literal_arns_record_name_test.dart +++ b/packages/ario_sdk/test/src/utils/get_literal_arns_record_name_test.dart @@ -5,7 +5,7 @@ void main() { group('getLiteralARNSRecordName', () { test('returns the correct record name for a given ARNSUndername', () { // Arrange - final undername = ARNSUndername( + const undername = ARNSUndername( name: 'test', domain: 'example.com', record: ARNSRecord( @@ -24,7 +24,7 @@ void main() { test('returns the correct record name for a given ARNSUndername with @', () { // Arrange - final undername = ARNSUndername( + const undername = ARNSUndername( name: '@', // @ is the default name for the root domain domain: 'example.com', record: ARNSRecord( diff --git a/test/arns/presentation/assign_name_bloc/assign_name_bloc_test.dart b/test/arns/presentation/assign_name_bloc/assign_name_bloc_test.dart index 874ec963e5..e3375298a2 100644 --- a/test/arns/presentation/assign_name_bloc/assign_name_bloc_test.dart +++ b/test/arns/presentation/assign_name_bloc/assign_name_bloc_test.dart @@ -14,13 +14,13 @@ class MockFileDataTableItem extends Mock implements FileDataTableItem {} void main() { setUpAll(() { - registerFallbackValue(ARNSUndername( + registerFallbackValue(const ARNSUndername( name: 'test_undername', domain: 'test.ar', record: ARNSRecord(transactionId: 'test_tx_id', ttlSeconds: 3600), )); registerFallbackValue( - ANTRecord(domain: 'test.ar', processId: 'test_process_id')); + const ANTRecord(domain: 'test.ar', processId: 'test_process_id')); }); group('AssignNameBloc', () { @@ -51,8 +51,8 @@ void main() { // Arrange const walletAddress = 'test_wallet_address'; final antRecords = [ - ANTRecord(domain: 'test1.ar', processId: 'process1'), - ANTRecord(domain: 'test2.ar', processId: 'process2'), + const ANTRecord(domain: 'test1.ar', processId: 'process1'), + const ANTRecord(domain: 'test2.ar', processId: 'process2'), ]; when(() => mockAuth.getWalletAddress()) @@ -138,8 +138,8 @@ void main() { () async { // Arrange final antRecords = [ - ANTRecord(domain: 'domain1.ar', processId: 'process1'), - ANTRecord(domain: 'domain2.ar', processId: 'process2'), + const ANTRecord(domain: 'domain1.ar', processId: 'process1'), + const ANTRecord(domain: 'domain2.ar', processId: 'process2'), ]; final selectedName = antRecords[0]; @@ -165,12 +165,12 @@ void main() { () async { // Arrange final antRecords = [ - ANTRecord(domain: 'domain1.ar', processId: 'process1'), - ANTRecord(domain: 'domain2.ar', processId: 'process2'), + const ANTRecord(domain: 'domain1.ar', processId: 'process1'), + const ANTRecord(domain: 'domain2.ar', processId: 'process2'), ]; final selectedName = antRecords[1]; final undernames = [ - ARNSUndername( + const ARNSUndername( name: 'undername1', domain: 'domain1.ar', record: ARNSRecord(transactionId: 'tx1', ttlSeconds: 3600)), @@ -205,17 +205,17 @@ void main() { () async { // Arrange final antRecords = [ - ANTRecord(domain: 'domain1.ar', processId: 'process1'), - ANTRecord(domain: 'domain2.ar', processId: 'process2'), + const ANTRecord(domain: 'domain1.ar', processId: 'process1'), + const ANTRecord(domain: 'domain2.ar', processId: 'process2'), ]; final selectedName = antRecords[0]; final undernames = [ - ARNSUndername( + const ARNSUndername( name: 'undername1', domain: 'domain1.ar', record: ARNSRecord(transactionId: 'tx1', ttlSeconds: 3600), ), - ARNSUndername( + const ARNSUndername( name: 'undername2', domain: 'domain1.ar', record: ARNSRecord(transactionId: 'tx2', ttlSeconds: 3600), @@ -258,17 +258,17 @@ void main() { () async { // Arrange final antRecords = [ - ANTRecord(domain: 'domain1.ar', processId: 'process1'), - ANTRecord(domain: 'domain2.ar', processId: 'process2'), + const ANTRecord(domain: 'domain1.ar', processId: 'process1'), + const ANTRecord(domain: 'domain2.ar', processId: 'process2'), ]; final selectedName = antRecords[0]; final undernames = [ - ARNSUndername( + const ARNSUndername( name: 'undername1', domain: 'domain1.ar', record: ARNSRecord(transactionId: 'tx1', ttlSeconds: 3600), ), - ARNSUndername( + const ARNSUndername( name: 'undername2', domain: 'domain1.ar', record: ARNSRecord(transactionId: 'tx2', ttlSeconds: 3600), @@ -311,8 +311,8 @@ void main() { when(() => mockFileDataTableItem.fileId).thenReturn('test_file_id'); when(() => mockFileDataTableItem.driveId).thenReturn('test_drive_id'); final antRecords = [ - ANTRecord(domain: 'test1.ar', processId: 'process1'), - ANTRecord(domain: 'test2.ar', processId: 'process2'), + const ANTRecord(domain: 'test1.ar', processId: 'process1'), + const ANTRecord(domain: 'test2.ar', processId: 'process2'), ]; const walletAddress = 'test_wallet_address'; @@ -329,7 +329,7 @@ void main() { )).thenAnswer((_) async {}); when(() => mockArnsRepository.getARNSUndernames(any())).thenAnswer( (_) async => [ - ARNSUndername( + const ARNSUndername( name: 'undername', domain: 'domain', record: @@ -342,9 +342,9 @@ void main() { act: (bloc) { bloc.add(const LoadNames()); bloc.add( - SelectName(ANTRecord(domain: 'domain', processId: 'process_id'))); + const SelectName(ANTRecord(domain: 'domain', processId: 'process_id'))); bloc.add(const LoadUndernames()); - bloc.add(SelectUndername( + bloc.add(const SelectUndername( undername: ARNSUndername( name: 'undername', domain: 'domain', @@ -385,8 +385,8 @@ void main() { when(() => mockFileDataTableItem.fileId).thenReturn('test_file_id'); when(() => mockFileDataTableItem.driveId).thenReturn('test_drive_id'); final antRecords = [ - ANTRecord(domain: 'test1.ar', processId: 'process1'), - ANTRecord(domain: 'test2.ar', processId: 'process2'), + const ANTRecord(domain: 'test1.ar', processId: 'process1'), + const ANTRecord(domain: 'test2.ar', processId: 'process2'), ]; const walletAddress = 'test_wallet_address'; @@ -406,7 +406,7 @@ void main() { )).thenThrow(StateError('Test error')); when(() => mockArnsRepository.getARNSUndernames(any())).thenAnswer( (_) async => [ - ARNSUndername( + const ARNSUndername( name: 'undername', domain: 'domain', record: ARNSRecord(transactionId: 'test_tx_id', ttlSeconds: 3600), @@ -418,9 +418,9 @@ void main() { act: (bloc) { bloc.add(const LoadNames()); bloc.add( - SelectName(ANTRecord(domain: 'domain', processId: 'process_id'))); + const SelectName(ANTRecord(domain: 'domain', processId: 'process_id'))); bloc.add(const LoadUndernames()); - bloc.add(SelectUndername( + bloc.add(const SelectUndername( undername: ARNSUndername( name: 'undername', domain: 'domain', diff --git a/test/manifest/domain/manifest_repository_test.dart b/test/manifest/domain/manifest_repository_test.dart index 83d6fb82af..ee6306307a 100644 --- a/test/manifest/domain/manifest_repository_test.dart +++ b/test/manifest/domain/manifest_repository_test.dart @@ -65,7 +65,7 @@ void main() async { registerFallbackValue(FileEntity()); registerFallbackValue(const FileRevisionsCompanion()); registerFallbackValue( - ARNSUndername( + const ARNSUndername( name: 'undername', domain: 'domain', record: ARNSRecord( @@ -284,7 +284,7 @@ void main() async { await repository.uploadManifest( params: mockUploadParams, processId: 'process_id', - undername: ARNSUndername( + undername: const ARNSUndername( name: 'undername', domain: 'domain', record: ARNSRecord( @@ -347,7 +347,7 @@ void main() async { await repository.uploadManifest( params: mockUploadParams, processId: 'process_id', - undername: ARNSUndername( + undername: const ARNSUndername( name: 'undername', domain: 'domain', record: ARNSRecord(