diff --git a/android/fastlane/metadata/android/en-US/changelogs/111.txt b/android/fastlane/metadata/android/en-US/changelogs/111.txt new file mode 100644 index 0000000000..09abeaa676 --- /dev/null +++ b/android/fastlane/metadata/android/en-US/changelogs/111.txt @@ -0,0 +1,3 @@ +- Updates user interface to include new Creative Commons license options: CC0, CC-BY-NC, CC-BY-NC-ND, CC-BY-NC-SA, CC-BY-ND, and CC-BY-SA. +- Fixes issue with moving files in private drives +- Fixes issue displaying long content in the activity section of the details panel \ No newline at end of file diff --git a/lib/blocs/fs_entry_info/fs_entry_info_cubit.dart b/lib/blocs/fs_entry_info/fs_entry_info_cubit.dart index 117c27c12f..ffd2130eb0 100644 --- a/lib/blocs/fs_entry_info/fs_entry_info_cubit.dart +++ b/lib/blocs/fs_entry_info/fs_entry_info_cubit.dart @@ -89,7 +89,6 @@ class FsEntryInfoCubit extends Cubit { licenseDefinitionTxId: '', name: 'Pending', shortName: 'Pending', - version: '', ), ); } diff --git a/lib/blocs/fs_entry_license/fs_entry_license_bloc.dart b/lib/blocs/fs_entry_license/fs_entry_license_bloc.dart index 6a74e310ac..ee59fe792b 100644 --- a/lib/blocs/fs_entry_license/fs_entry_license_bloc.dart +++ b/lib/blocs/fs_entry_license/fs_entry_license_bloc.dart @@ -21,19 +21,63 @@ part 'fs_entry_license_state.dart'; class FsEntryLicenseBloc extends Bloc { + FsEntryLicenseBloc({ + required this.driveId, + required this.selectedItems, + required ArweaveService arweave, + required TurboUploadService turboUploadService, + required DriveDao driveDao, + required ProfileCubit profileCubit, + required ArDriveCrypto crypto, + required LicenseService licenseService, + Platform platform = const LocalPlatform(), + }) : _arweave = arweave, + _turboUploadService = turboUploadService, + _driveDao = driveDao, + _profileCubit = profileCubit, + _crypto = crypto, + _licenseService = licenseService, + super(const FsEntryLicenseLoadInProgress()) { + if (selectedItems.isEmpty) { + addError(Exception('selectedItems cannot be empty')); + } + + on(_onEvent, transformer: restartable()); + } + final String driveId; final List selectedItems; - final selectForm = FormGroup({ - 'licenseType': FormControl( + // We initialize with UDL license by default + LicenseMeta _selectedLicenseMeta = udlLicenseMeta; + LicenseMeta get selectedLicenseMeta => _selectedLicenseMeta; + + List? filesToLicense; + LicenseParams? licenseParams; + + final ArweaveService _arweave; + final TurboUploadService _turboUploadService; + final DriveDao _driveDao; + final ProfileCubit _profileCubit; + final ArDriveCrypto _crypto; + final LicenseService _licenseService; + + final List errorLog = []; + + /// Form getters + FormGroup get selectForm => _selectForm; + FormGroup get udlForm => _udlForm; + FormGroup get ccForm => _ccForm; + + // Forms + final _selectForm = FormGroup({ + 'licenseType': FormControl( validators: [Validators.required], - value: udlLicenseMeta, + value: LicenseCategory.udl, ), }); - LicenseMeta get selectFormLicenseMeta => - selectForm.control('licenseType').value; - final udlForm = FormGroup({ + final _udlForm = FormGroup({ 'licenseFeeAmount': FormControl( validators: [ Validators.composeOR([ @@ -59,139 +103,141 @@ class FsEntryLicenseBloc ), }); - List? filesToLicense; - LicenseParams? licenseParams; - - final ArweaveService _arweave; - final TurboUploadService _turboUploadService; - final DriveDao _driveDao; - final ProfileCubit _profileCubit; - final ArDriveCrypto _crypto; - final LicenseService _licenseService; - - final List errorLog = []; + final _ccForm = FormGroup({ + 'ccAttributionField': FormControl( + validators: [Validators.required], + value: cc0LicenseMeta, + ), + }); - FsEntryLicenseBloc({ - required this.driveId, - required this.selectedItems, - required ArweaveService arweave, - required TurboUploadService turboUploadService, - required DriveDao driveDao, - required ProfileCubit profileCubit, - required ArDriveCrypto crypto, - required LicenseService licenseService, - Platform platform = const LocalPlatform(), - }) : _arweave = arweave, - _turboUploadService = turboUploadService, - _driveDao = driveDao, - _profileCubit = profileCubit, - _crypto = crypto, - _licenseService = licenseService, - super(const FsEntryLicenseLoadInProgress()) { - if (selectedItems.isEmpty) { - addError(Exception('selectedItems cannot be empty')); + Future _onEvent( + FsEntryLicenseEvent event, + Emitter emit, + ) async { + if (event is FsEntryLicenseInitial) { + await _handleInitial(event, emit); + } else if (event is FsEntryLicenseSelect) { + await _handleSelect(event, emit); + } else if (event is FsEntryLicenseConfigurationSubmit) { + await _handleConfigurationSubmit(event, emit); + } else if (event is FsEntryLicenseReviewConfirm) { + await _handleReviewConfirm(event, emit); + } else if (event is FsEntryLicenseReviewBack) { + _handleReviewBack(event, emit); + } else if (event is FsEntryLicenseConfigurationBack) { + _handleConfigurationBack(event, emit); + } else if (event is FsEntryLicenseSuccessClose) { + _handleSuccessClose(event, emit); + } else if (event is FsEntryLicenseFailureTryAgain) { + _handleFailureTryAgain(event, emit); + } else { + addError('Unsupported event: ${event.runtimeType}'); } + } - final profile = _profileCubit.state as ProfileLoggedIn; - - on( - (event, emit) async { - if (await _profileCubit.logoutIfWalletMismatch()) { - emit(const FsEntryLicenseWalletMismatch()); - return; - } + Future _handleInitial( + FsEntryLicenseInitial event, + Emitter emit, + ) async { + filesToLicense = await _enumerateFiles(items: selectedItems, emit: emit); + if (filesToLicense!.isEmpty) { + emit(const FsEntryLicenseNoFiles()); + } else { + emit(const FsEntryLicenseSelecting()); + } + } - if (event is FsEntryLicenseInitial) { - filesToLicense = - await enumerateFiles(items: selectedItems, emit: emit); - if (filesToLicense!.isEmpty) { - emit(const FsEntryLicenseNoFiles()); - } else { - emit(const FsEntryLicenseSelecting()); - } - } + Future _handleSelect( + FsEntryLicenseSelect event, + Emitter emit, + ) async { + final licenseType = selectForm.control('licenseType').value; + + switch (licenseType) { + case LicenseCategory.udl: + _selectedLicenseMeta = udlLicenseMetaV2; + break; + case LicenseCategory.cc: + _selectedLicenseMeta = ccByLicenseMetaV2; + break; + } - if (event is FsEntryLicenseSelect) { - if (selectFormLicenseMeta.hasParams) { - emit(const FsEntryLicenseConfiguring()); - } else { - licenseParams = null; - emit(const FsEntryLicenseReviewing()); - } - } + emit(const FsEntryLicenseConfiguring()); + } - if (event is FsEntryLicenseConfigurationBack) { - emit(const FsEntryLicenseSelecting()); - } + Future _handleConfigurationSubmit( + FsEntryLicenseConfigurationSubmit event, + Emitter emit, + ) async { + final licenseCategory = selectForm.control('licenseType').value; - if (event is FsEntryLicenseConfigurationSubmit) { - if (selectFormLicenseMeta.licenseType == LicenseType.udl) { - licenseParams = await udlFormToLicenseParams(udlForm); - } else { - addError( - 'Unsupported license configuration: ${selectFormLicenseMeta.licenseType}'); - } - emit(const FsEntryLicenseReviewing()); - } + if (licenseCategory == LicenseCategory.cc) { + _selectedLicenseMeta = ccForm.control('ccAttributionField').value; + } - if (event is FsEntryLicenseReviewBack) { - if (selectFormLicenseMeta.hasParams) { - licenseParams = null; - emit(const FsEntryLicenseConfiguring()); - } else { - emit(const FsEntryLicenseSelecting()); - } - } + if (_selectedLicenseMeta.licenseType == LicenseType.udlV2) { + licenseParams = await _udlFormToLicenseParams(udlForm); + } else { + addError( + 'Unsupported license configuration: ${_selectedLicenseMeta.licenseType}'); + } - if (event is FsEntryLicenseReviewConfirm) { - emit(const FsEntryLicenseLoadInProgress()); - try { - await licenseEntities( - profile: profile, - licenseMeta: selectFormLicenseMeta, - licenseParams: licenseParams, - ); - emit(const FsEntryLicenseSuccess()); - } catch (_, trace) { - addError('Error licensing entities', trace); - emit(const FsEntryLicenseFailure()); - } - } + emit(const FsEntryLicenseReviewing()); + } - if (event is FsEntryLicenseSuccessClose) { - emit(const FsEntryLicenseComplete()); - } + Future _handleReviewConfirm( + FsEntryLicenseReviewConfirm event, + Emitter emit, + ) async { + emit(const FsEntryLicenseLoadInProgress()); + try { + final profile = _profileCubit.state as ProfileLoggedIn; + await _licenseEntities( + profile: profile, + licenseMeta: _selectedLicenseMeta, + licenseParams: licenseParams, + ); + emit(const FsEntryLicenseSuccess()); + } catch (_, trace) { + addError('Error licensing entities', trace); + emit(const FsEntryLicenseFailure()); + } + } - if (event is FsEntryLicenseFailureTryAgain) { - emit(const FsEntryLicenseReviewing()); - } - }, - transformer: restartable(), - ); + void _handleReviewBack( + FsEntryLicenseReviewBack event, + Emitter emit, + ) { + if (_selectedLicenseMeta.hasParams) { + licenseParams = null; + emit(const FsEntryLicenseConfiguring()); + } else { + emit(const FsEntryLicenseSelecting()); + } } - Future udlFormToLicenseParams(FormGroup udlForm) async { - final String? licenseFeeAmountString = - udlForm.control('licenseFeeAmount').value; - final double? licenseFeeAmount = licenseFeeAmountString == null - ? null - : double.tryParse(licenseFeeAmountString); + void _handleConfigurationBack( + FsEntryLicenseConfigurationBack event, + Emitter emit, + ) { + emit(const FsEntryLicenseSelecting()); + } - final UdlCurrency licenseFeeCurrency = - udlForm.control('licenseFeeCurrency').value; - final UdlCommercialUse commercialUse = - udlForm.control('commercialUse').value; - final UdlDerivation derivations = udlForm.control('derivations').value; + void _handleSuccessClose( + FsEntryLicenseSuccessClose event, + Emitter emit, + ) { + emit(const FsEntryLicenseComplete()); + } - return UdlLicenseParams( - licenseFeeAmount: licenseFeeAmount, - licenseFeeCurrency: licenseFeeCurrency, - commercialUse: commercialUse, - derivations: derivations, - ); + void _handleFailureTryAgain( + FsEntryLicenseFailureTryAgain event, + Emitter emit, + ) { + emit(const FsEntryLicenseReviewing()); } - Future> enumerateFiles({ + Future> _enumerateFiles({ required List items, required Emitter emit, }) async { @@ -231,7 +277,7 @@ class FsEntryLicenseBloc return files.where((file) => file.pinnedDataOwnerAddress == null).toList(); } - Future licenseEntities({ + Future _licenseEntities({ required ProfileLoggedIn profile, required LicenseMeta licenseMeta, LicenseParams? licenseParams, @@ -323,6 +369,27 @@ class FsEntryLicenseBloc } } + Future _udlFormToLicenseParams(FormGroup udlForm) async { + final String? licenseFeeAmountString = + udlForm.control('licenseFeeAmount').value; + final double? licenseFeeAmount = licenseFeeAmountString == null + ? null + : double.tryParse(licenseFeeAmountString); + + final UdlCurrency licenseFeeCurrency = + udlForm.control('licenseFeeCurrency').value; + final UdlCommercialUse commercialUse = + udlForm.control('commercialUse').value; + final UdlDerivation derivations = udlForm.control('derivations').value; + + return UdlLicenseParams( + licenseFeeAmount: licenseFeeAmount, + licenseFeeCurrency: licenseFeeCurrency, + commercialUse: commercialUse, + derivations: derivations, + ); + } + @override void onError(Object error, StackTrace stackTrace) { errorLog.add(error.toString()); diff --git a/lib/blocs/fs_entry_move/fs_entry_move_bloc.dart b/lib/blocs/fs_entry_move/fs_entry_move_bloc.dart index 340055cacf..b8b29d1354 100644 --- a/lib/blocs/fs_entry_move/fs_entry_move_bloc.dart +++ b/lib/blocs/fs_entry_move/fs_entry_move_bloc.dart @@ -20,7 +20,7 @@ part 'fs_entry_move_state.dart'; class FsEntryMoveBloc extends Bloc { final String driveId; - final List selectedItems; + final List _selectedItems; final ArweaveService _arweave; final TurboUploadService _turboUploadService; @@ -32,7 +32,7 @@ class FsEntryMoveBloc extends Bloc { FsEntryMoveBloc({ required this.driveId, - required this.selectedItems, + required List selectedItems, required ArweaveService arweave, required TurboUploadService turboUploadService, required DriveDao driveDao, @@ -41,7 +41,8 @@ class FsEntryMoveBloc extends Bloc { required ArDriveCrypto crypto, required DriveDetailCubit driveDetailCubit, Platform platform = const LocalPlatform(), - }) : _arweave = arweave, + }) : _selectedItems = List.from(selectedItems, growable: false), + _arweave = arweave, _turboUploadService = turboUploadService, _driveDao = driveDao, _profileCubit = profileCubit, @@ -49,7 +50,7 @@ class FsEntryMoveBloc extends Bloc { _syncCubit = syncCubit, _crypto = crypto, super(const FsEntryMoveLoadInProgress()) { - if (selectedItems.isEmpty) { + if (_selectedItems.isEmpty) { addError(Exception('selectedItems cannot be empty')); } @@ -93,7 +94,7 @@ class FsEntryMoveBloc extends Bloc { FsEntryMoveNameConflict( conflictingItems: conflictingItems, folderInView: folderInView, - allItems: selectedItems, + allItems: _selectedItems, ), ); } @@ -145,7 +146,7 @@ class FsEntryMoveBloc extends Bloc { onData: (FolderWithContents folderWithContents) => FsEntryMoveLoadSuccess( viewingRootFolder: folderWithContents.folder.parentFolderId == null, viewingFolder: folderWithContents, - itemsToMove: selectedItems, + itemsToMove: _selectedItems, ), ); } @@ -156,7 +157,7 @@ class FsEntryMoveBloc extends Bloc { }) async { final conflictingItems = []; try { - for (var itemToMove in selectedItems) { + for (var itemToMove in _selectedItems) { final entityWithSameNameExists = await _driveDao.doesEntityWithNameExist( name: itemToMove.name, @@ -184,7 +185,7 @@ class FsEntryMoveBloc extends Bloc { final isShowingHiddenItems = (_driveDetailCubit.state as DriveDetailLoadSuccess) .isShowingHiddenFiles; - final files = selectedItems.whereType().toList(); + final files = _selectedItems.whereType().toList(); if (!isShowingHiddenItems) { files.removeWhere((element) => element.isHidden); @@ -198,7 +199,7 @@ class FsEntryMoveBloc extends Bloc { files.clear(); - final folders = selectedItems.whereType().toList(); + final folders = _selectedItems.whereType().toList(); if (!isShowingHiddenItems) { folders.removeWhere((element) => element.isHidden); diff --git a/lib/components/details_panel.dart b/lib/components/details_panel.dart index a484d517b7..5bed5b0c89 100644 --- a/lib/components/details_panel.dart +++ b/lib/components/details_panel.dart @@ -1039,10 +1039,10 @@ class DetailsPanelItem extends StatelessWidget { children: [ Flexible( child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.max, children: [ - Flexible( + Expanded( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -1051,7 +1051,7 @@ class DetailsPanelItem extends StatelessWidget { child: Text( itemTitle, style: ArDriveTypography.body.buttonNormalRegular(), - maxLines: 2, + maxLines: 4, ), ), if (itemSubtitle != null) @@ -1062,7 +1062,11 @@ class DetailsPanelItem extends StatelessWidget { ], ), ), - if (leading != null) Flexible(child: leading!), + if (leading != null) + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: leading!, + ), ], ), ), diff --git a/lib/components/fs_entry_license_form.dart b/lib/components/fs_entry_license_form.dart index a94d1414bf..76d823f52c 100644 --- a/lib/components/fs_entry_license_form.dart +++ b/lib/components/fs_entry_license_form.dart @@ -2,11 +2,13 @@ import 'package:ardrive/blocs/blocs.dart'; import 'package:ardrive/components/license_summary.dart'; import 'package:ardrive/core/crypto/crypto.dart'; import 'package:ardrive/l11n/validation_messages.dart'; +import 'package:ardrive/misc/resources.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/services/services.dart'; import 'package:ardrive/theme/theme.dart'; import 'package:ardrive/turbo/services/upload_service.dart'; import 'package:ardrive/utils/app_localizations_wrapper.dart'; +import 'package:ardrive/utils/open_url.dart'; import 'package:ardrive/utils/show_general_dialog.dart'; import 'package:ardrive_ui/ardrive_ui.dart'; import 'package:flutter/material.dart'; @@ -89,7 +91,7 @@ class _FsEntryLicenseFormState extends State { builder: (context, state) { return Builder(builder: (context) { final licenseMeta = - context.read().selectFormLicenseMeta; + context.read().selectedLicenseMeta; if (state is FsEntryLicenseNoFiles) { return ArDriveCard( height: 350, @@ -196,20 +198,23 @@ class _FsEntryLicenseFormState extends State { width: kMediumDialogWidth, // TODO: Localize // title: appLocalizationsOf(context).renameFolderEmphasized, - content: SizedBox( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - LicenseFileList(fileList: filesToLicense), - const SizedBox(height: 16), - const Divider(height: 24), - ReactiveForm( + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + LicenseFileList(fileList: filesToLicense), + const SizedBox(height: 16), + const Divider(height: 24), + SizedBox( + child: ReactiveForm( formGroup: context.watch().selectForm, child: ReactiveDropdownField( + alignment: AlignmentDirectional.centerStart, + isExpanded: true, formControlName: 'licenseType', decoration: InputDecoration( + border: InputBorder.none, label: Text( 'License', // TODO: Localize @@ -232,40 +237,56 @@ class _FsEntryLicenseFormState extends State { control.dirty && control.invalid, validationMessages: kValidationMessages(appLocalizationsOf(context)), - items: licenseMetaMap.values - .map( - (value) => DropdownMenuItem( - value: value, - child: - Text('${value.name} (${value.shortName})'), + items: LicenseCategory.values.map( + (value) { + return DropdownMenuItem( + value: value, + child: Text( + '${licenseCategoryNames[value]}', ), - ) - .toList(), + ); + }, + ).toList(), ), ), - const Divider(height: 32), - Text( - // TODO: Localize - 'Cost: 0 AR', - style: ArDriveTypography.body.buttonLargeRegular( - color: ArDriveTheme.of(context) - .themeData - .colors - .themeFgDefault, + ), + ArDriveClickArea( + child: GestureDetector( + onTap: () { + openUrl( + url: Resources.howDoesKeyFileLoginWork, + ); + }, + child: Text( + 'Learn More about Licensing', + style: ArDriveTypography.body + .buttonNormalRegular() + .copyWith(decoration: TextDecoration.underline), ), ), - Text( - // TODO: Localize - 'Free for now, maybe paid later.', - style: ArDriveTypography.body.captionRegular( - color: ArDriveTheme.of(context) - .themeData - .colors - .themeFgSubtle, - ), + ), + const Divider(height: 32), + Text( + // TODO: Localize + 'Cost: 0 AR', + style: ArDriveTypography.body.buttonLargeRegular( + color: ArDriveTheme.of(context) + .themeData + .colors + .themeFgDefault, ), - ], - ), + ), + Text( + // TODO: Localize + 'Free for now, maybe paid later.', + style: ArDriveTypography.body.captionRegular( + color: ArDriveTheme.of(context) + .themeData + .colors + .themeFgSubtle, + ), + ), + ], ), actions: [ ModalAction( @@ -281,12 +302,20 @@ class _FsEntryLicenseFormState extends State { ], ); } else if (state is FsEntryLicenseConfiguring) { + final licenseType = context + .read() + .selectedLicenseMeta + .licenseType; + final modalTitle = licenseType == LicenseType.udlV2 + ? 'Configure Universal Data License' + : licenseType == LicenseType.ccByV2 + ? 'Configure Creative Commons License' + : 'Unsupported license type'; return ArDriveScrollBar( child: SingleChildScrollView( child: ArDriveStandardModal( - title: - 'Configure ${licenseMeta.name} (${licenseMeta.shortName})', - width: kMediumDialogWidth, + title: modalTitle, + width: kLargeDialogWidth, content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -298,11 +327,7 @@ class _FsEntryLicenseFormState extends State { .filesToLicense!), const SizedBox(height: 16), const Divider(height: 24), - context - .read() - .selectFormLicenseMeta - .licenseType == - LicenseType.udl + licenseType == LicenseType.udlV2 ? UdlParamsForm( onChangeLicenseFee: () { setState(() {}); @@ -310,7 +335,13 @@ class _FsEntryLicenseFormState extends State { formGroup: context.watch().udlForm, ) - : const Text('Unsupported license type'), + : licenseType == LicenseType.ccByV2 + ? CcParamsForm( + formGroup: context + .watch() + .ccForm, + ) + : const Text('Unsupported license type'), ], ), actions: [ @@ -828,3 +859,93 @@ class _UdlParamsFormState extends State { )); } } + +class CcParamsForm extends StatefulWidget { + const CcParamsForm({super.key, required this.formGroup}); + + final FormGroup formGroup; + + @override + State createState() => _CcParamsFormState(); +} + +class _CcParamsFormState extends State { + LicenseMeta get licenseMeta => + context.read().selectedLicenseMeta; + + @override + Widget build(BuildContext context) { + final inputBorder = OutlineInputBorder( + borderSide: BorderSide( + color: ArDriveTheme.of(context) + .themeData + .colors + .themeFgDisabled + .withOpacity(0.3), + width: 2, + ), + borderRadius: BorderRadius.circular(4), + ); + + return ReactiveForm( + formGroup: widget.formGroup, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + child: LabeledInput( + labelText: 'Type', + child: ReactiveDropdownField( + formControlName: 'ccAttributionField', + decoration: InputDecoration( + enabledBorder: inputBorder, + focusedBorder: inputBorder, + ), + onChanged: (e) { + setState(() {}); + }, + showErrors: (control) => control.dirty && control.invalid, + validationMessages: + kValidationMessages(appLocalizationsOf(context)), + items: ccLicenses + .map( + (e) => DropdownMenuItem( + value: e, + child: Text(e.name), + ), + ) + .toList(), + ), + ), + ), + ], + ), + Builder( + builder: (context) { + final selectedLicenseMeta = context + .watch() + .ccForm + .value['ccAttributionField'] as LicenseMeta; + + return Text( + selectedLicenseMeta.shortName, + style: ArDriveTypography.body.buttonNormalBold( + color: ArDriveTheme.of(context) + .themeData + .colors + .themeFgDisabled, + ), + ); + }, + ), + ]), + ); + } +} diff --git a/lib/entities/license_assertion.dart b/lib/entities/license_assertion.dart index eb2d547e86..ff4122af7e 100644 --- a/lib/entities/license_assertion.dart +++ b/lib/entities/license_assertion.dart @@ -1,3 +1,4 @@ +import 'package:ardrive/utils/logger.dart'; import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:arweave/arweave.dart'; import 'package:drift/drift.dart'; @@ -53,7 +54,11 @@ class LicenseAssertionEntity with TransactionPropertiesMixin { DateTime.fromMillisecondsSinceEpoch(transaction.block!.timestamp); } return licenseAssertionEntity; - } catch (_) { + } catch (e, stacktrace) { + logger.e( + 'Failed to parse license assertion transaction. licenseDefinitionTxId: ${transaction.tags.firstWhere((tag) => tag.name == LicenseTag.licenseDefinitionTxId).value}', + e, + stacktrace); throw LicenseAssertionTransactionParseException( transactionId: transaction.id, ); diff --git a/lib/services/license/license_service.dart b/lib/services/license/license_service.dart index c42041f370..8600c3240e 100644 --- a/lib/services/license/license_service.dart +++ b/lib/services/license/license_service.dart @@ -31,8 +31,16 @@ class LicenseService { }) { switch (licenseType) { case LicenseType.udl: + case LicenseType.udlV2: return UdlLicenseParams.fromAdditionalTags(additionalTags ?? {}); + case LicenseType.cc0: case LicenseType.ccBy: + case LicenseType.ccByV2: + case LicenseType.ccByNC: + case LicenseType.ccByNCND: + case LicenseType.ccByNCSA: + case LicenseType.ccByND: + case LicenseType.ccBySA: return EmptyParams(); default: throw ArgumentError('Unknown license type: $licenseType'); diff --git a/lib/services/license/license_state.dart b/lib/services/license/license_state.dart index b80edb1bb3..5f48dd9bfa 100644 --- a/lib/services/license/license_state.dart +++ b/lib/services/license/license_state.dart @@ -1,18 +1,47 @@ -import 'package:ardrive/services/license/licenses/licenses.dart'; +import 'package:ardrive/services/license/license.dart'; import 'package:equatable/equatable.dart'; +/// Updating a license type version will require to update the corresponding [licenseMetaMap]. +/// The [licenseMetaMap] is used to map the license type to the corresponding [LicenseMeta]. +/// +/// Please ensure to update the [licenseMetaMap] when adding a new license type. enum LicenseType { udl, + udlV2, + cc0, ccBy, + ccByV2, + ccByNC, + ccByNCND, + ccByNCSA, + ccByND, + ccBySA, unknown, } +enum LicenseCategory { + cc, + udl, +} + +final licenseMetaMap = { + LicenseType.udl: udlLicenseMeta, + LicenseType.udlV2: udlLicenseMetaV2, + LicenseType.cc0: cc0LicenseMeta, + LicenseType.ccBy: ccByLicenseMeta, + LicenseType.ccByV2: ccByLicenseMetaV2, + LicenseType.ccByNC: ccByNCLicenseMeta, + LicenseType.ccByNCND: ccByNCNDLicenseMeta, + LicenseType.ccByNCSA: ccByNCSAMeta, + LicenseType.ccByND: ccByNDLicenseMeta, + LicenseType.ccBySA: ccBySAMeta, +}; + class LicenseMeta extends Equatable { final LicenseType licenseType; final String licenseDefinitionTxId; final String name; final String shortName; - final String version; final bool hasParams; const LicenseMeta({ @@ -20,7 +49,6 @@ class LicenseMeta extends Equatable { required this.licenseDefinitionTxId, required this.name, required this.shortName, - required this.version, this.hasParams = false, }); @@ -30,7 +58,6 @@ class LicenseMeta extends Equatable { licenseDefinitionTxId, name, shortName, - version, hasParams, ]; } @@ -44,9 +71,9 @@ abstract class LicenseParams extends Equatable { class EmptyParams extends LicenseParams {} -final licenseMetaMap = { - LicenseType.udl: udlLicenseMeta, - LicenseType.ccBy: ccByLicenseMeta, +final licenseCategoryNames = { + LicenseCategory.udl: 'Universal Data License - UDL', + LicenseCategory.cc: 'Creative Commons - CC', }; class LicenseState extends Equatable { diff --git a/lib/services/license/licenses/cc_by.dart b/lib/services/license/licenses/cc_by.dart index b28dc8e801..4a34c3ffb3 100644 --- a/lib/services/license/licenses/cc_by.dart +++ b/lib/services/license/licenses/cc_by.dart @@ -1,9 +1,74 @@ import '../license_state.dart'; +List ccLicenses = [ + cc0LicenseMeta, + ccByLicenseMetaV2, + ccByNCLicenseMeta, + ccByNCNDLicenseMeta, + ccByNCSAMeta, + ccByNDLicenseMeta, + ccBySAMeta, +]; + +const cc0LicenseMeta = LicenseMeta( + licenseType: LicenseType.cc0, + licenseDefinitionTxId: 'nF6Mjy_Yy_Gv-DYLq7QPxz5PdXUQ4rtOpbJZdcaFEKw', + name: 'Public Domain', + shortName: 'CC0', +); + +// Version 4.0 const ccByLicenseMeta = LicenseMeta( licenseType: LicenseType.ccBy, licenseDefinitionTxId: 'rz2DNzn9pnYOU6049Wm6V7kr0BhyfWE6ZD_mqrXMv5A', - name: 'Creative Commons Attribution', + name: 'Attribution', + shortName: 'CC-BY', +); + +const ccByLicenseMetaV2 = LicenseMeta( + licenseType: LicenseType.ccByV2, + licenseDefinitionTxId: 'mSOFUrl5mUQvG7VBP36DD39kzJASv9FDe3GxHpcCvRA', + name: 'Attribution', shortName: 'CC-BY', - version: '4.0', + hasParams: true, +); + +// Version 4.0 +const ccByNCLicenseMeta = LicenseMeta( + licenseType: LicenseType.ccByNC, + licenseDefinitionTxId: '9jG6a1fWgQ_wE4R6OGA2Xg9vGRAwpkrQIMC83nC3kvI', + name: 'Attribution Non-Commercial', + shortName: 'CC-BY-NC', +); + +// Version 4.0 +const ccByNCNDLicenseMeta = LicenseMeta( + licenseType: LicenseType.ccByNCND, + licenseDefinitionTxId: 'OlTlW1xEw75UC0cdmNqvxc3j6iAmFXrS4usWIBfu_3E', + name: 'Attribution Non-Commercial No-Derivatives', + shortName: 'CC-BY-NC-ND', +); + +// Version 4.0 +const ccByNCSAMeta = LicenseMeta( + licenseType: LicenseType.ccByNCSA, + licenseDefinitionTxId: '2PO2MDRNZLJjgA_0hNGUAD7yXg9nneq-3fxTTLP-uo8', + name: 'Attribution Non-Commercial Share-A-Like', + shortName: 'CC-BY-NC-SA', +); + +// Version 4.0 +const ccByNDLicenseMeta = LicenseMeta( + licenseType: LicenseType.ccByND, + licenseDefinitionTxId: 'XaIMRBMNqTUlHa_hzypkopfRFyAKqit-AWo-OxwIxoo', + name: 'Attribution No-Derivatives', + shortName: 'CC-BY-ND', +); + +// Version 4.0 +const ccBySAMeta = LicenseMeta( + licenseType: LicenseType.ccBySA, + licenseDefinitionTxId: 'sKz-PZ96ApDoy5RTBspxhs1GP-cHommw4_9hEiZ6K3c', + name: 'Attribution Share-A-Like', + shortName: 'CC-BY-SA', ); diff --git a/lib/services/license/licenses/udl.dart b/lib/services/license/licenses/udl.dart index 6c2c2d4684..7f223cce3f 100644 --- a/lib/services/license/licenses/udl.dart +++ b/lib/services/license/licenses/udl.dart @@ -1,11 +1,20 @@ import '../license_state.dart'; +// Version 0.1 const udlLicenseMeta = LicenseMeta( licenseType: LicenseType.udl, licenseDefinitionTxId: 'yRj4a5KMctX_uOmKWCFJIjmY8DeJcusVk6-HzLiM_t8', name: 'Universal Data License', shortName: 'UDL', - version: '1.0', + hasParams: true, +); + +// Version 0.2 +const udlLicenseMetaV2 = LicenseMeta( + licenseType: LicenseType.udlV2, + licenseDefinitionTxId: 'IVjAM1C3x3GFdc3t9EqMnbtGnpgTuJbaiYZa1lk09_8', + name: 'Universal Data License', + shortName: 'UDL', hasParams: true, ); diff --git a/pubspec.yaml b/pubspec.yaml index f16538d0cf..84a039995d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: Secure, permanent storage publish_to: 'none' -version: 2.35.0 +version: 2.36.0 environment: sdk: '>=3.0.2 <4.0.0' diff --git a/test/services/license/license_state_test.dart b/test/services/license/license_state_test.dart new file mode 100644 index 0000000000..eacbf0d006 --- /dev/null +++ b/test/services/license/license_state_test.dart @@ -0,0 +1,78 @@ +import 'package:ardrive/services/license/license_state.dart'; +import 'package:test/test.dart'; + +void main() { + group('License Meta Map', () { + test('should contain all LicenseType enum values', () { + // Iterate over all LicenseType values + for (var type in LicenseType.values) { + if (type == LicenseType.unknown) continue; + // Check if the licenseMetaMap contains the current enum value + expect(licenseMetaMap.containsKey(type), isTrue, + reason: 'Missing meta for $type'); + } + }); + + test('should not contain unknown LicenseType in map', () { + // Verify the 'unknown' LicenseType is not in the map + expect(licenseMetaMap.containsKey(LicenseType.unknown), isFalse, + reason: 'Map should not contain meta for unknown license type'); + }); + }); + + group( + 'Ensure we wont change a licenseDefinitionTxId of existing licenses meta', + () { + test( + 'cc0', + () => expect(licenseMetaMap[LicenseType.cc0]!.licenseDefinitionTxId, + 'nF6Mjy_Yy_Gv-DYLq7QPxz5PdXUQ4rtOpbJZdcaFEKw')); + + test( + 'ccBy v1', + () => expect(licenseMetaMap[LicenseType.ccBy]!.licenseDefinitionTxId, + 'rz2DNzn9pnYOU6049Wm6V7kr0BhyfWE6ZD_mqrXMv5A')); + + test( + 'ccBy v2', + () => expect(licenseMetaMap[LicenseType.ccByV2]!.licenseDefinitionTxId, + 'mSOFUrl5mUQvG7VBP36DD39kzJASv9FDe3GxHpcCvRA')); + + test( + 'ccByNC', + () => expect(licenseMetaMap[LicenseType.ccByNC]!.licenseDefinitionTxId, + '9jG6a1fWgQ_wE4R6OGA2Xg9vGRAwpkrQIMC83nC3kvI')); + + test( + 'ccByNCND', + () => expect( + licenseMetaMap[LicenseType.ccByNCND]!.licenseDefinitionTxId, + 'OlTlW1xEw75UC0cdmNqvxc3j6iAmFXrS4usWIBfu_3E')); + + test( + 'ccByNCSA', + () => expect( + licenseMetaMap[LicenseType.ccByNCSA]!.licenseDefinitionTxId, + '2PO2MDRNZLJjgA_0hNGUAD7yXg9nneq-3fxTTLP-uo8')); + + test( + 'ccByND', + () => expect(licenseMetaMap[LicenseType.ccByND]!.licenseDefinitionTxId, + 'XaIMRBMNqTUlHa_hzypkopfRFyAKqit-AWo-OxwIxoo')); + + test( + 'ccBySA', + () => expect(licenseMetaMap[LicenseType.ccBySA]!.licenseDefinitionTxId, + 'sKz-PZ96ApDoy5RTBspxhs1GP-cHommw4_9hEiZ6K3c')); + + test( + 'udl', + () => expect(licenseMetaMap[LicenseType.udl]!.licenseDefinitionTxId, + 'yRj4a5KMctX_uOmKWCFJIjmY8DeJcusVk6-HzLiM_t8')); + + test( + 'udlV2', + () => expect(licenseMetaMap[LicenseType.udlV2]!.licenseDefinitionTxId, + 'IVjAM1C3x3GFdc3t9EqMnbtGnpgTuJbaiYZa1lk09_8')); + }); +}