diff --git a/.config/dictionaries/project.dic b/.config/dictionaries/project.dic index 4b09fc32bcd..5cc9193c233 100644 --- a/.config/dictionaries/project.dic +++ b/.config/dictionaries/project.dic @@ -252,3 +252,5 @@ interps todos vsync damian-molinski +LTRB +hotspots \ No newline at end of file diff --git a/catalyst_voices/lib/widgets/cards/funded_proposal_card.dart b/catalyst_voices/lib/widgets/cards/funded_proposal_card.dart new file mode 100644 index 00000000000..083a6443085 --- /dev/null +++ b/catalyst_voices/lib/widgets/cards/funded_proposal_card.dart @@ -0,0 +1,233 @@ +import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; +import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; +import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; +import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; +import 'package:flutter/material.dart'; + +/// Displays a proposal in funded state on a card. +class FundedProposalCard extends StatelessWidget { + final AssetGenImage image; + final FundedProposal proposal; + + const FundedProposalCard({ + super.key, + required this.image, + required this.proposal, + }); + + @override + Widget build(BuildContext context) { + return Container( + width: 326, + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _Header(image: image), + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _FundCategory( + fund: proposal.fund, + category: proposal.category, + ), + const SizedBox(height: 4), + _Title(text: proposal.title), + const SizedBox(height: 4), + _FundedDate(dateTime: proposal.fundedDate), + const SizedBox(height: 24), + _FundsAndComments( + funds: proposal.fundsRequested, + commentsCount: proposal.commentsCount, + ), + const SizedBox(height: 24), + _Description(text: proposal.description), + ], + ), + ), + ], + ), + ); + } +} + +class _Header extends StatelessWidget { + final AssetGenImage image; + + const _Header({required this.image}); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 168, + child: Stack( + children: [ + Positioned.fill( + child: CatalystImage.asset( + image.path, + fit: BoxFit.cover, + ), + ), + Positioned( + top: 2, + right: 2, + child: IconButton( + padding: EdgeInsets.zero, + onPressed: () {}, + icon: Icon( + CatalystVoicesIcons.plus_circle, + size: 20, + color: Theme.of(context).colors.iconsOnImage, + ), + ), + ), + Positioned( + left: 12, + bottom: 12, + child: VoicesChip.rectangular( + padding: const EdgeInsets.fromLTRB(10, 6, 10, 4), + leading: Icon( + CatalystVoicesIcons.briefcase, + color: Theme.of(context).colorScheme.primary, + ), + content: Text(context.l10n.fundedProposal), + backgroundColor: Theme.of(context).colors.primary98, + ), + ), + ], + ), + ); + } +} + +class _FundCategory extends StatelessWidget { + final String fund; + final String category; + + const _FundCategory({ + required this.fund, + required this.category, + }); + + @override + Widget build(BuildContext context) { + return Text.rich( + TextSpan( + text: fund + ' / ', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colors.textDisabled, + ), + children: [ + TextSpan( + text: category, + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + ); + } +} + +class _Title extends StatelessWidget { + final String text; + + const _Title({required this.text}); + + @override + Widget build(BuildContext context) { + return Text( + text, + style: Theme.of(context).textTheme.titleLarge, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ); + } +} + +class _FundedDate extends StatelessWidget { + final DateTime dateTime; + + const _FundedDate({required this.dateTime}); + + @override + Widget build(BuildContext context) { + return Text( + context.l10n.fundedProposalDate(dateTime), + style: Theme.of(context).textTheme.bodySmall, + ); + } +} + +class _FundsAndComments extends StatelessWidget { + final Coin funds; + final int commentsCount; + + const _FundsAndComments({ + required this.funds, + required this.commentsCount, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 6), + decoration: BoxDecoration( + color: Theme.of(context).colors.success?.withOpacity(0.08), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + children: [ + Text( + CryptocurrencyFormatter.formatAmount(funds), + style: Theme.of(context).textTheme.titleLarge, + ), + Text( + context.l10n.fundsRequested, + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + VoicesChip.rectangular( + padding: const EdgeInsets.fromLTRB(8, 6, 12, 6), + leading: Icon( + CatalystVoicesIcons.check_circle, + color: Theme.of(context).colors.success, + ), + content: Text(context.l10n.noOfComments(commentsCount)), + backgroundColor: Theme.of(context).colors.successContainer, + ), + ], + ), + ); + } +} + +class _Description extends StatelessWidget { + final String text; + + const _Description({required this.text}); + + @override + Widget build(BuildContext context) { + return Text( + text, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Theme.of(context).colors.textOnPrimary, + ), + maxLines: 4, + overflow: TextOverflow.ellipsis, + ); + } +} diff --git a/catalyst_voices/lib/widgets/widgets.dart b/catalyst_voices/lib/widgets/widgets.dart index 1ea955a589b..9e55ddc7b68 100644 --- a/catalyst_voices/lib/widgets/widgets.dart +++ b/catalyst_voices/lib/widgets/widgets.dart @@ -6,6 +6,7 @@ export 'buttons/voices_icon_button.dart'; export 'buttons/voices_outlined_button.dart'; export 'buttons/voices_segmented_button.dart'; export 'buttons/voices_text_button.dart'; +export 'cards/funded_proposal_card.dart'; export 'chips/voices_chip.dart'; export 'common/link_text.dart'; export 'common/navigation_location.dart'; diff --git a/catalyst_voices/macos/Flutter/GeneratedPluginRegistrant.swift b/catalyst_voices/macos/Flutter/GeneratedPluginRegistrant.swift index 91bdbe11660..200cbdf930b 100644 --- a/catalyst_voices/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/catalyst_voices/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,16 +5,30 @@ import FlutterMacOS import Foundation +import device_info_plus +import file_selector_macos +import flutter_inappwebview_macos import flutter_secure_storage_macos +import gal +import irondash_engine_context import package_info_plus import path_provider_foundation import sentry_flutter +import super_native_extensions import url_launcher_macos +import video_player_avfoundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) + GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin")) + IrondashEngineContextPlugin.register(with: registry.registrar(forPlugin: "IrondashEngineContextPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SentryFlutterPlugin.register(with: registry.registrar(forPlugin: "SentryFlutterPlugin")) + SuperNativeExtensionsPlugin.register(with: registry.registrar(forPlugin: "SuperNativeExtensionsPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) } diff --git a/catalyst_voices/packages/catalyst_voices_assets/assets/colors/colors.xml b/catalyst_voices/packages/catalyst_voices_assets/assets/colors/colors.xml index 38ff44d3c2d..f38784a7ea4 100644 --- a/catalyst_voices/packages/catalyst_voices_assets/assets/colors/colors.xml +++ b/catalyst_voices/packages/catalyst_voices_assets/assets/colors/colors.xml @@ -5,6 +5,7 @@ #FFFFFF #61212A3D #123CD3 + #E8ECFD #FFFFFF #A1B4F7 #081B5E @@ -44,6 +45,7 @@ #29CC0000 #212A3D #FFFFFF + #FFFFFF #61212A3D #123CD3 #C014EB @@ -66,6 +68,7 @@ #0C288D #61D9DEE8 #728EF3 + #364463 #0C288D #1035BC #E8ECFD @@ -105,6 +108,7 @@ #29CC0000 #F2F4F8 #212A3D + #FFFFFF #61BFC8D9 #728EF3 #DF8AF5 diff --git a/catalyst_voices/packages/catalyst_voices_assets/assets/images/proposal_background_1.webp b/catalyst_voices/packages/catalyst_voices_assets/assets/images/proposal_background_1.webp new file mode 100644 index 00000000000..7136a7e9151 Binary files /dev/null and b/catalyst_voices/packages/catalyst_voices_assets/assets/images/proposal_background_1.webp differ diff --git a/catalyst_voices/packages/catalyst_voices_assets/assets/images/proposal_background_2.webp b/catalyst_voices/packages/catalyst_voices_assets/assets/images/proposal_background_2.webp new file mode 100644 index 00000000000..f93fc1fdc43 Binary files /dev/null and b/catalyst_voices/packages/catalyst_voices_assets/assets/images/proposal_background_2.webp differ diff --git a/catalyst_voices/packages/catalyst_voices_assets/lib/generated/assets.gen.dart b/catalyst_voices/packages/catalyst_voices_assets/lib/generated/assets.gen.dart index 292105ed702..ad41ff51fec 100644 --- a/catalyst_voices/packages/catalyst_voices_assets/lib/generated/assets.gen.dart +++ b/catalyst_voices/packages/catalyst_voices_assets/lib/generated/assets.gen.dart @@ -7,10 +7,10 @@ // ignore_for_file: type=lint // ignore_for_file: directives_ordering,unnecessary_import,implicit_dynamic_list_literal,deprecated_member_use -import 'package:flutter/widgets.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:vector_graphics/vector_graphics.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_svg/flutter_svg.dart' as _svg; +import 'package:vector_graphics/vector_graphics.dart' as _vg; class $AssetsImagesGen { const $AssetsImagesGen(); @@ -71,6 +71,14 @@ class $AssetsImagesGen { /// File path: assets/images/node_open.svg SvgGenImage get nodeOpen => const SvgGenImage('assets/images/node_open.svg'); + /// File path: assets/images/proposal_background_1.webp + AssetGenImage get proposalBackground1 => + const AssetGenImage('assets/images/proposal_background_1.webp'); + + /// File path: assets/images/proposal_background_2.webp + AssetGenImage get proposalBackground2 => + const AssetGenImage('assets/images/proposal_background_2.webp'); + /// File path: assets/images/view_grid.svg SvgGenImage get viewGrid => const SvgGenImage('assets/images/view_grid.svg'); @@ -97,6 +105,8 @@ class $AssetsImagesGen { linkedinMono, nodeClosed, nodeOpen, + proposalBackground1, + proposalBackground2, viewGrid, x, xMono @@ -208,7 +218,7 @@ class SvgGenImage { final Set flavors; final bool _isVecFormat; - SvgPicture svg({ + _svg.SvgPicture svg({ Key? key, bool matchTextDirection = false, AssetBundle? bundle, @@ -221,29 +231,29 @@ class SvgGenImage { WidgetBuilder? placeholderBuilder, String? semanticsLabel, bool excludeFromSemantics = false, - SvgTheme? theme, + _svg.SvgTheme? theme, ColorFilter? colorFilter, Clip clipBehavior = Clip.hardEdge, @deprecated Color? color, @deprecated BlendMode colorBlendMode = BlendMode.srcIn, @deprecated bool cacheColorFilter = false, }) { - final BytesLoader loader; + final _svg.BytesLoader loader; if (_isVecFormat) { - loader = AssetBytesLoader( + loader = _vg.AssetBytesLoader( _assetName, assetBundle: bundle, packageName: package, ); } else { - loader = SvgAssetLoader( + loader = _svg.SvgAssetLoader( _assetName, assetBundle: bundle, packageName: package, theme: theme, ); } - return SvgPicture( + return _svg.SvgPicture( loader, key: key, matchTextDirection: matchTextDirection, diff --git a/catalyst_voices/packages/catalyst_voices_assets/lib/generated/colors.gen.dart b/catalyst_voices/packages/catalyst_voices_assets/lib/generated/colors.gen.dart index 9d9095cf49b..ed85446334e 100644 --- a/catalyst_voices/packages/catalyst_voices_assets/lib/generated/colors.gen.dart +++ b/catalyst_voices/packages/catalyst_voices_assets/lib/generated/colors.gen.dart @@ -58,6 +58,9 @@ class VoicesColors { /// Color: #F2F4F8 static const Color darkIconsForeground = Color(0xFFF2F4F8); + /// Color: #FFFFFF + static const Color darkIconsOnImage = Color(0xFFFFFFFF); + /// Color: #728EF3 static const Color darkIconsPrimary = Color(0xFF728EF3); @@ -163,6 +166,9 @@ class VoicesColors { /// Color: #728EF3 static const Color darkPrimary = Color(0xFF728EF3); + /// Color: #364463 + static const Color darkPrimary98 = Color(0xFF364463); + /// Color: #1035BC static const Color darkPrimaryContainer = Color(0xFF1035BC); @@ -242,6 +248,9 @@ class VoicesColors { /// Color: #212A3D static const Color lightIconsForeground = Color(0xFF212A3D); + /// Color: #FFFFFF + static const Color lightIconsOnImage = Color(0xFFFFFFFF); + /// Color: #123CD3 static const Color lightIconsPrimary = Color(0xFF123CD3); @@ -347,6 +356,9 @@ class VoicesColors { /// Color: #123CD3 static const Color lightPrimary = Color(0xFF123CD3); + /// Color: #E8ECFD + static const Color lightPrimary98 = Color(0xFFE8ECFD); + /// Color: #A1B4F7 static const Color lightPrimaryContainer = Color(0xFFA1B4F7); diff --git a/catalyst_voices/packages/catalyst_voices_brands/lib/src/theme_extensions/voices_color_scheme.dart b/catalyst_voices/packages/catalyst_voices_brands/lib/src/theme_extensions/voices_color_scheme.dart index 0db64721394..cb64279e33a 100644 --- a/catalyst_voices/packages/catalyst_voices_brands/lib/src/theme_extensions/voices_color_scheme.dart +++ b/catalyst_voices/packages/catalyst_voices_brands/lib/src/theme_extensions/voices_color_scheme.dart @@ -37,6 +37,7 @@ class VoicesColorScheme extends ThemeExtension { final Color? onSurfaceError016; final Color? iconsForeground; final Color? iconsBackground; + final Color? iconsOnImage; final Color? iconsDisabled; final Color? iconsPrimary; final Color? iconsSecondary; @@ -54,6 +55,7 @@ class VoicesColorScheme extends ThemeExtension { final Color? elevationsOnSurfaceNeutralLv2; final Color? outlineBorder; final Color? outlineBorderVariant; + final Color? primary98; final Color? primaryContainer; final Color? onPrimaryContainer; final Color? errorContainer; @@ -90,6 +92,7 @@ class VoicesColorScheme extends ThemeExtension { required this.onSurfaceError016, required this.iconsForeground, required this.iconsBackground, + required this.iconsOnImage, required this.iconsDisabled, required this.iconsPrimary, required this.iconsSecondary, @@ -107,6 +110,7 @@ class VoicesColorScheme extends ThemeExtension { required this.elevationsOnSurfaceNeutralLv2, required this.outlineBorder, required this.outlineBorderVariant, + required this.primary98, required this.primaryContainer, required this.onPrimaryContainer, required this.errorContainer, @@ -145,6 +149,7 @@ class VoicesColorScheme extends ThemeExtension { this.onSurfaceError016, this.iconsForeground, this.iconsBackground, + this.iconsOnImage, this.iconsDisabled, this.iconsPrimary, this.iconsSecondary, @@ -162,6 +167,7 @@ class VoicesColorScheme extends ThemeExtension { this.elevationsOnSurfaceNeutralLv2, this.outlineBorder, this.outlineBorderVariant, + this.primary98, this.primaryContainer, this.onPrimaryContainer, this.errorContainer, @@ -200,6 +206,7 @@ class VoicesColorScheme extends ThemeExtension { Color? onSurfaceError016, Color? iconsForeground, Color? iconsBackground, + Color? iconsOnImage, Color? iconsDisabled, Color? iconsPrimary, Color? iconsSecondary, @@ -217,6 +224,7 @@ class VoicesColorScheme extends ThemeExtension { Color? elevationsOnSurfaceNeutralLv2, Color? outlineBorder, Color? outlineBorderVariant, + Color? primary98, Color? primaryContainer, Color? onPrimaryContainer, Color? errorContainer, @@ -260,6 +268,7 @@ class VoicesColorScheme extends ThemeExtension { onSurfaceError016: onSurfaceError016 ?? this.onSurfaceError016, iconsForeground: iconsForeground ?? this.iconsForeground, iconsBackground: iconsBackground ?? this.iconsBackground, + iconsOnImage: iconsOnImage ?? this.iconsOnImage, iconsDisabled: iconsDisabled ?? this.iconsDisabled, iconsPrimary: iconsPrimary ?? this.iconsPrimary, iconsSecondary: iconsSecondary ?? this.iconsSecondary, @@ -281,6 +290,7 @@ class VoicesColorScheme extends ThemeExtension { elevationsOnSurfaceNeutralLv2 ?? this.elevationsOnSurfaceNeutralLv2, outlineBorder: outlineBorder ?? this.outlineBorder, outlineBorderVariant: outlineBorderVariant ?? this.outlineBorderVariant, + primary98: primary98 ?? this.primary98, primaryContainer: primaryContainer ?? this.primaryContainer, onPrimaryContainer: onPrimaryContainer ?? this.onPrimaryContainer, errorContainer: errorContainer ?? this.errorContainer, @@ -357,6 +367,7 @@ class VoicesColorScheme extends ThemeExtension { Color.lerp(onSurfaceError016, other.onSurfaceError016, t), iconsForeground: Color.lerp(iconsForeground, other.iconsForeground, t), iconsBackground: Color.lerp(iconsBackground, other.iconsBackground, t), + iconsOnImage: Color.lerp(iconsOnImage, other.iconsOnImage, t), iconsDisabled: Color.lerp(iconsDisabled, other.iconsDisabled, t), iconsPrimary: Color.lerp(iconsPrimary, other.iconsPrimary, t), iconsSecondary: Color.lerp(iconsSecondary, other.iconsSecondary, t), @@ -391,6 +402,7 @@ class VoicesColorScheme extends ThemeExtension { outlineBorder: Color.lerp(outlineBorder, other.outlineBorder, t), outlineBorderVariant: Color.lerp(outlineBorderVariant, other.outlineBorderVariant, t), + primary98: Color.lerp(primary98, other.primary98, t), primaryContainer: Color.lerp(primaryContainer, other.primaryContainer, t), onPrimaryContainer: Color.lerp(onPrimaryContainer, other.onPrimaryContainer, t), diff --git a/catalyst_voices/packages/catalyst_voices_brands/lib/src/themes/catalyst.dart b/catalyst_voices/packages/catalyst_voices_brands/lib/src/themes/catalyst.dart index ff49ba484da..03e65371ead 100644 --- a/catalyst_voices/packages/catalyst_voices_brands/lib/src/themes/catalyst.dart +++ b/catalyst_voices/packages/catalyst_voices_brands/lib/src/themes/catalyst.dart @@ -53,6 +53,7 @@ const VoicesColorScheme darkVoicesColorScheme = VoicesColorScheme( onSurfaceError016: VoicesColors.darkOnSurfaceError016, iconsForeground: VoicesColors.darkIconsForeground, iconsBackground: VoicesColors.darkIconsBackground, + iconsOnImage: VoicesColors.darkIconsOnImage, iconsDisabled: VoicesColors.darkIconsDisabled, iconsPrimary: VoicesColors.darkIconsPrimary, iconsSecondary: VoicesColors.darkIconsSecondary, @@ -72,6 +73,7 @@ const VoicesColorScheme darkVoicesColorScheme = VoicesColorScheme( elevationsOnSurfaceNeutralLv2: VoicesColors.darkElevationsOnSurfaceNeutralLv2, outlineBorder: VoicesColors.darkOutlineBorderOutline, outlineBorderVariant: VoicesColors.darkOutlineBorderOutlineVariant, + primary98: VoicesColors.darkPrimary98, primaryContainer: VoicesColors.darkPrimaryContainer, onPrimaryContainer: VoicesColors.darkOnPrimaryContainer, errorContainer: VoicesColors.darkErrorContainer, @@ -124,8 +126,9 @@ const VoicesColorScheme lightVoicesColorScheme = VoicesColorScheme( onSurfaceError08: VoicesColors.lightOnSurfaceError08, onSurfaceError012: VoicesColors.lightOnSurfaceError012, onSurfaceError016: VoicesColors.lightOnSurfaceError016, - iconsForeground: VoicesColors.lightIconsForeground, + iconsForeground: Color.fromARGB(255, 151, 164, 193), iconsBackground: VoicesColors.lightIconsBackground, + iconsOnImage: VoicesColors.lightIconsOnImage, iconsDisabled: VoicesColors.lightIconsDisabled, iconsPrimary: VoicesColors.lightIconsPrimary, iconsSecondary: VoicesColors.lightIconsSecondary, @@ -147,6 +150,7 @@ const VoicesColorScheme lightVoicesColorScheme = VoicesColorScheme( VoicesColors.lightElevationsOnSurfaceNeutralLv2, outlineBorder: VoicesColors.lightOutlineBorderOutline, outlineBorderVariant: VoicesColors.lightOutlineBorderOutlineVariant, + primary98: VoicesColors.lightPrimary98, primaryContainer: VoicesColors.lightPrimaryContainer, onPrimaryContainer: VoicesColors.lightOnPrimaryContainer, errorContainer: VoicesColors.lightErrorContainer, diff --git a/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations.dart b/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations.dart index d851bdba30d..1d2f27599a3 100644 --- a/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations.dart +++ b/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations.dart @@ -333,6 +333,30 @@ abstract class VoicesLocalizations { /// In en, this message translates to: /// **'Draft'** String get proposalStatusDraft; + + /// Label shown on a proposal card indicating that the proposal is funded. + /// + /// In en, this message translates to: + /// **'Funded Proposal'** + String get fundedProposal; + + /// Indicates when a proposal has been funded on a proposal card. + /// + /// In en, this message translates to: + /// **'Funded {date}'** + String fundedProposalDate(DateTime date); + + /// Indicates the amount of ADA requested in a fund on a proposal card. + /// + /// In en, this message translates to: + /// **'Funds requested'** + String get fundsRequested; + + /// Indicates the amount of comments on a proposal card. + /// + /// In en, this message translates to: + /// **'{count} {count, plural, =0{comments} =1{comment} other{comments}}'** + String noOfComments(num count); } class _VoicesLocalizationsDelegate extends LocalizationsDelegate { diff --git a/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_en.dart b/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_en.dart index c158b637076..2d0b2d57970 100644 --- a/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_en.dart +++ b/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_en.dart @@ -1,3 +1,5 @@ +import 'package:intl/intl.dart' as intl; + import 'catalyst_voices_localizations.dart'; // ignore_for_file: type=lint @@ -127,4 +129,36 @@ class VoicesLocalizationsEn extends VoicesLocalizations { @override String get proposalStatusDraft => 'Draft'; + + @override + String get fundedProposal => 'Funded Proposal'; + + @override + String fundedProposalDate(DateTime date) { + final intl.DateFormat dateDateFormat = intl.DateFormat.yMMMMd(localeName); + final String dateString = dateDateFormat.format(date); + + return 'Funded $dateString'; + } + + @override + String get fundsRequested => 'Funds requested'; + + @override + String noOfComments(num count) { + final intl.NumberFormat countNumberFormat = intl.NumberFormat.compact( + locale: localeName, + + ); + final String countString = countNumberFormat.format(count); + + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'comments', + one: 'comment', + zero: 'comments', + ); + return '$countString $_temp0'; + } } diff --git a/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_es.dart b/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_es.dart index d5f1de3c2de..c795f0fd880 100644 --- a/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_es.dart +++ b/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_es.dart @@ -1,3 +1,5 @@ +import 'package:intl/intl.dart' as intl; + import 'catalyst_voices_localizations.dart'; // ignore_for_file: type=lint @@ -127,4 +129,36 @@ class VoicesLocalizationsEs extends VoicesLocalizations { @override String get proposalStatusDraft => 'Draft'; + + @override + String get fundedProposal => 'Funded Proposal'; + + @override + String fundedProposalDate(DateTime date) { + final intl.DateFormat dateDateFormat = intl.DateFormat.yMMMMd(localeName); + final String dateString = dateDateFormat.format(date); + + return 'Funded $dateString'; + } + + @override + String get fundsRequested => 'Funds requested'; + + @override + String noOfComments(num count) { + final intl.NumberFormat countNumberFormat = intl.NumberFormat.compact( + locale: localeName, + + ); + final String countString = countNumberFormat.format(count); + + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'comments', + one: 'comment', + zero: 'comments', + ); + return '$countString $_temp0'; + } } diff --git a/catalyst_voices/packages/catalyst_voices_localization/lib/l10n/intl_en.arb b/catalyst_voices/packages/catalyst_voices_localization/lib/l10n/intl_en.arb index 261fc3a9ad9..3d0136b698a 100644 --- a/catalyst_voices/packages/catalyst_voices_localization/lib/l10n/intl_en.arb +++ b/catalyst_voices/packages/catalyst_voices_localization/lib/l10n/intl_en.arb @@ -164,5 +164,33 @@ "proposalStatusDraft": "Draft", "@proposalStatusDraft": { "description": "Indicates to user that status is in draft mode" + }, + "fundedProposal": "Funded Proposal", + "@fundedProposal": { + "description": "Label shown on a proposal card indicating that the proposal is funded." + }, + "fundedProposalDate": "Funded {date}", + "@fundedProposalDate": { + "description": "Indicates when a proposal has been funded on a proposal card.", + "placeholders": { + "date": { + "type": "DateTime", + "format": "yMMMMd" + } + } + }, + "fundsRequested": "Funds requested", + "@fundsRequested": { + "description": "Indicates the amount of ADA requested in a fund on a proposal card." + }, + "noOfComments": "{count} {count, plural, =0{comments} =1{comment} other{comments}}", + "@noOfComments": { + "description": "Indicates the amount of comments on a proposal card.", + "placeholders": { + "count": { + "type": "num", + "format": "compact" + } + } } -} +} \ No newline at end of file diff --git a/catalyst_voices/packages/catalyst_voices_models/lib/catalyst_voices_models.dart b/catalyst_voices/packages/catalyst_voices_models/lib/catalyst_voices_models.dart index 4d812c5ca9c..34ee251355e 100644 --- a/catalyst_voices/packages/catalyst_voices_models/lib/catalyst_voices_models.dart +++ b/catalyst_voices/packages/catalyst_voices_models/lib/catalyst_voices_models.dart @@ -1,7 +1,3 @@ library catalyst_voices_models; -export 'src/authentication_status.dart'; export 'src/catalyst_voices_models.dart'; -export 'src/proposal_status.dart'; -export 'src/session_data.dart'; -export 'src/space.dart'; diff --git a/catalyst_voices/packages/catalyst_voices_models/lib/src/catalyst_voices_models.dart b/catalyst_voices/packages/catalyst_voices_models/lib/src/catalyst_voices_models.dart index 003d2bf5018..9c8052769c7 100644 --- a/catalyst_voices/packages/catalyst_voices_models/lib/src/catalyst_voices_models.dart +++ b/catalyst_voices/packages/catalyst_voices_models/lib/src/catalyst_voices_models.dart @@ -1,2 +1,10 @@ +library catalyst_voices_models; + +export 'authentication_status.dart'; export 'errors/errors.dart'; +export 'proposal/funded_proposal.dart'; +export 'proposal/pending_proposal.dart'; +export 'proposal/proposal_status.dart'; +export 'session_data.dart'; +export 'space.dart'; export 'user/user.dart'; diff --git a/catalyst_voices/packages/catalyst_voices_models/lib/src/proposal/funded_proposal.dart b/catalyst_voices/packages/catalyst_voices_models/lib/src/proposal/funded_proposal.dart new file mode 100644 index 00000000000..bb1768a6eb6 --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_models/lib/src/proposal/funded_proposal.dart @@ -0,0 +1,34 @@ +import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; +import 'package:equatable/equatable.dart'; + +/// Defines the already funded proposal. +final class FundedProposal extends Equatable { + final String fund; + final String category; + final String title; + final DateTime fundedDate; + final Coin fundsRequested; + final int commentsCount; + final String description; + + const FundedProposal({ + required this.fund, + required this.category, + required this.title, + required this.fundedDate, + required this.fundsRequested, + required this.commentsCount, + required this.description, + }); + + @override + List get props => [ + fund, + category, + title, + fundedDate, + fundsRequested, + commentsCount, + description, + ]; +} diff --git a/catalyst_voices/packages/catalyst_voices_models/lib/src/proposal/pending_proposal.dart b/catalyst_voices/packages/catalyst_voices_models/lib/src/proposal/pending_proposal.dart new file mode 100644 index 00000000000..424c54a345d --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_models/lib/src/proposal/pending_proposal.dart @@ -0,0 +1,40 @@ +import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; +import 'package:equatable/equatable.dart'; + +/// Defines the pending proposal that is not funded yet. +final class PendingProposal extends Equatable { + final String fund; + final String category; + final String title; + final DateTime lastUpdateDate; + final Coin fundsRequested; + final int commentsCount; + final String description; + final int completedSegments; + final int totalSegments; + + const PendingProposal({ + required this.fund, + required this.category, + required this.title, + required this.lastUpdateDate, + required this.fundsRequested, + required this.commentsCount, + required this.description, + required this.completedSegments, + required this.totalSegments, + }); + + @override + List get props => [ + fund, + category, + title, + lastUpdateDate, + fundsRequested, + commentsCount, + description, + completedSegments, + totalSegments, + ]; +} diff --git a/catalyst_voices/packages/catalyst_voices_models/lib/src/proposal_status.dart b/catalyst_voices/packages/catalyst_voices_models/lib/src/proposal/proposal_status.dart similarity index 100% rename from catalyst_voices/packages/catalyst_voices_models/lib/src/proposal_status.dart rename to catalyst_voices/packages/catalyst_voices_models/lib/src/proposal/proposal_status.dart diff --git a/catalyst_voices/packages/catalyst_voices_models/pubspec.yaml b/catalyst_voices/packages/catalyst_voices_models/pubspec.yaml index 3df3022210b..bf137446228 100644 --- a/catalyst_voices/packages/catalyst_voices_models/pubspec.yaml +++ b/catalyst_voices/packages/catalyst_voices_models/pubspec.yaml @@ -7,9 +7,10 @@ environment: sdk: ">=3.5.0 <4.0.0" dependencies: + catalyst_cardano_serialization: ^0.4.0 equatable: ^2.0.5 meta: ^1.10.0 dev_dependencies: - catalyst_analysis: ^2.0.0 - test: ^1.24.9 \ No newline at end of file + catalyst_analysis: ^2.0.0 + test: ^1.24.9 diff --git a/catalyst_voices/packages/catalyst_voices_shared/lib/catalyst_voices_shared.dart b/catalyst_voices/packages/catalyst_voices_shared/lib/catalyst_voices_shared.dart index b3e795a7c25..3e3b306f20e 100644 --- a/catalyst_voices/packages/catalyst_voices_shared/lib/catalyst_voices_shared.dart +++ b/catalyst_voices/packages/catalyst_voices_shared/lib/catalyst_voices_shared.dart @@ -1,4 +1,3 @@ -/// A Very Good Project created by Very Good CLI. library catalyst_voices_shared; export 'src/catalyst_voices_shared.dart'; diff --git a/catalyst_voices/packages/catalyst_voices_shared/lib/src/catalyst_voices_shared.dart b/catalyst_voices/packages/catalyst_voices_shared/lib/src/catalyst_voices_shared.dart index 047ffdf07fe..26a462cc6a2 100644 --- a/catalyst_voices/packages/catalyst_voices_shared/lib/src/catalyst_voices_shared.dart +++ b/catalyst_voices/packages/catalyst_voices_shared/lib/src/catalyst_voices_shared.dart @@ -1,6 +1,7 @@ export 'common/build_config.dart'; export 'common/build_environment.dart'; export 'dependency/dependency_provider.dart'; +export 'formatter/cryptocurrency_formatter.dart'; export 'platform/catalyst_platform.dart'; export 'platform_aware_builder/platform_aware_builder.dart'; export 'responsive/responsive_builder.dart'; diff --git a/catalyst_voices/packages/catalyst_voices_shared/lib/src/formatter/cryptocurrency_formatter.dart b/catalyst_voices/packages/catalyst_voices_shared/lib/src/formatter/cryptocurrency_formatter.dart new file mode 100644 index 00000000000..74affa5e108 --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_shared/lib/src/formatter/cryptocurrency_formatter.dart @@ -0,0 +1,31 @@ +import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; +import 'package:intl/intl.dart'; + +/// Formats amounts of ADA cryptocurrency. +abstract class CryptocurrencyFormatter { + static const adaSymbol = '₳'; + + static const _million = 1000000; + static const _thousand = 1000; + + /// Formats the [amount] of ADA cryptocurrency. + /// + /// Uses K (thousands) or M (millions) multipliers. + /// Examples: + /// - ₳123 = ₳123 + /// - ₳1000 = ₳1K + /// - ₳1000000 = ₳1M + static String formatAmount(Coin amount) { + final numberFormat = NumberFormat('#.##'); + final ada = amount.ada; + if (ada >= _million) { + final millions = ada / _million; + return adaSymbol + numberFormat.format(millions) + 'M'; + } else if (ada >= _thousand) { + final thousands = ada / _thousand; + return adaSymbol + numberFormat.format(thousands) + 'K'; + } else { + return adaSymbol + numberFormat.format(ada); + } + } +} diff --git a/catalyst_voices/packages/catalyst_voices_shared/pubspec.yaml b/catalyst_voices/packages/catalyst_voices_shared/pubspec.yaml index 13e13430a30..fa0fa9dec3c 100644 --- a/catalyst_voices/packages/catalyst_voices_shared/pubspec.yaml +++ b/catalyst_voices/packages/catalyst_voices_shared/pubspec.yaml @@ -8,10 +8,12 @@ environment: flutter: ">=3.24.1" dependencies: + catalyst_cardano_serialization: ^0.4.0 collection: ^1.18.0 flutter: sdk: flutter get_it: ^7.6.7 + intl: ^0.19.0 web: ^0.5.0 dev_dependencies: diff --git a/catalyst_voices/packages/catalyst_voices_shared/test/src/formatter/cryptocurrency_formatter_test.dart b/catalyst_voices/packages/catalyst_voices_shared/test/src/formatter/cryptocurrency_formatter_test.dart new file mode 100644 index 00000000000..ea18173170b --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_shared/test/src/formatter/cryptocurrency_formatter_test.dart @@ -0,0 +1,49 @@ +import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group(CryptocurrencyFormatter, () { + test('should format fractional ADA', () { + expect( + CryptocurrencyFormatter.formatAmount(Coin.fromAda(0.21)), + equals('₳0.21'), + ); + }); + + test('should format less than 1000 ADA', () { + expect( + CryptocurrencyFormatter.formatAmount(Coin.fromAda(975)), + equals('₳975'), + ); + }); + + test('should format 1000 ADA', () { + expect( + CryptocurrencyFormatter.formatAmount(Coin.fromAda(1000)), + equals('₳1K'), + ); + }); + + test('should format amounts in thousands of ADA', () { + expect( + CryptocurrencyFormatter.formatAmount(Coin.fromAda(15000)), + equals('₳15K'), + ); + }); + + test('should format amounts in millions of ADA', () { + expect( + CryptocurrencyFormatter.formatAmount(Coin.fromAda(2500000)), + equals('₳2.5M'), + ); + }); + + test('should format exactly 1 million ADA', () { + expect( + CryptocurrencyFormatter.formatAmount(Coin.fromAda(1000000)), + equals('₳1M'), + ); + }); + }); +} diff --git a/catalyst_voices/uikit_example/lib/examples/voices_proposal_card_example.dart b/catalyst_voices/uikit_example/lib/examples/voices_proposal_card_example.dart new file mode 100644 index 00000000000..edee7409d66 --- /dev/null +++ b/catalyst_voices/uikit_example/lib/examples/voices_proposal_card_example.dart @@ -0,0 +1,59 @@ +import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; +import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:flutter/material.dart'; + +final _description = """ +Zanzibar is becoming one of the hotspots for DID's through +World Mobile and PRISM, but its potential is only barely exploited. +Zanzibar is becoming one of the hotspots for DID's through World Mobile +and PRISM, but its potential is only barely exploited. +""" + .replaceAll('\n', ' '); + +class VoicesProposalCardExample extends StatelessWidget { + static const String route = '/proposal-card-example'; + + const VoicesProposalCardExample({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Proposal Card')), + body: Padding( + padding: const EdgeInsets.all(32), + child: Wrap( + spacing: 16, + runSpacing: 16, + children: [ + FundedProposalCard( + image: VoicesAssets.images.proposalBackground1, + proposal: FundedProposal( + fund: 'F14', + category: 'Cardano Use Cases / MVP', + title: 'Proposal Title that rocks the world', + fundedDate: DateTime(2025, 1, 28), + fundsRequested: Coin.fromAda(100000), + commentsCount: 0, + description: _description, + ), + ), + FundedProposalCard( + image: VoicesAssets.images.proposalBackground2, + proposal: FundedProposal( + fund: 'F14', + category: 'Cardano Use Cases / MVP', + title: 'Proposal Title that rocks the world', + fundedDate: DateTime(2025, 1, 28), + fundsRequested: Coin.fromAda(100000), + commentsCount: 0, + description: _description, + ), + ), + ], + ), + ), + ); + } +} diff --git a/catalyst_voices/uikit_example/lib/examples_list.dart b/catalyst_voices/uikit_example/lib/examples_list.dart index 90983ced1cf..59b38108b4d 100644 --- a/catalyst_voices/uikit_example/lib/examples_list.dart +++ b/catalyst_voices/uikit_example/lib/examples_list.dart @@ -17,6 +17,7 @@ import 'package:uikit_example/examples/voices_list_tile_example.dart'; import 'package:uikit_example/examples/voices_menu_example.dart'; import 'package:uikit_example/examples/voices_modals_example.dart'; import 'package:uikit_example/examples/voices_navigation_example.dart'; +import 'package:uikit_example/examples/voices_proposal_card_example.dart'; import 'package:uikit_example/examples/voices_radio_example.dart'; import 'package:uikit_example/examples/voices_rich_text_example.dart'; import 'package:uikit_example/examples/voices_seed_phrase_example.dart'; @@ -170,6 +171,11 @@ class ExamplesListPage extends StatelessWidget { route: VoicesRichTextExample.route, page: VoicesRichTextExample(), ), + ExampleTile( + title: 'Voices Proposal Card', + route: VoicesProposalCardExample.route, + page: VoicesProposalCardExample(), + ), ]; } diff --git a/catalyst_voices_packages/catalyst_cardano_serialization/lib/src/types.dart b/catalyst_voices_packages/catalyst_cardano_serialization/lib/src/types.dart index 95b375a214a..970ad2154de 100644 --- a/catalyst_voices_packages/catalyst_cardano_serialization/lib/src/types.dart +++ b/catalyst_voices_packages/catalyst_cardano_serialization/lib/src/types.dart @@ -40,6 +40,14 @@ enum NetworkId { /// Specifies an amount of ADA in terms of lovelace. extension type const Coin(int value) { + /// The amount of lovelaces in one ADA. + static const int adaInLovelaces = 1000000; + + /// Creates a [Coin] from [amount] specified in ADAs. + factory Coin.fromAda(double amount) { + return Coin((amount * adaInLovelaces).toInt()); + } + /// Deserializes the type from cbor. factory Coin.fromCbor(CborValue value) { return Coin((value as CborSmallInt).value); @@ -48,6 +56,9 @@ extension type const Coin(int value) { /// Serializes the type as cbor. CborValue toCbor() => CborSmallInt(value); + /// Converts lovelaces to ADAs + double get ada => value / adaInLovelaces; + /// Adds [other] value to this value and returns a new [Coin]. Coin operator +(Coin other) => Coin(value + other.value); diff --git a/catalyst_voices_packages/catalyst_cardano_serialization/test/types_test.dart b/catalyst_voices_packages/catalyst_cardano_serialization/test/types_test.dart index 53ce4ba345b..91fb22b3530 100644 --- a/catalyst_voices_packages/catalyst_cardano_serialization/test/types_test.dart +++ b/catalyst_voices_packages/catalyst_cardano_serialization/test/types_test.dart @@ -5,6 +5,11 @@ import 'package:test/test.dart'; void main() { group(Coin, () { + test('fromAda constructor', () { + final coin = Coin.fromAda(123); + expect(coin.ada, equals(123)); + }); + test('addition', () { expect(const Coin(3) + const Coin(100), equals(const Coin(103))); expect(const Coin(-100) + const Coin(100), equals(const Coin(0))); diff --git a/cspell.json b/cspell.json index 1ba6ae15bfb..c42691faf8f 100644 --- a/cspell.json +++ b/cspell.json @@ -174,6 +174,7 @@ "styles.min.css", "web-components.min.js", "**/generated/**", + "**/GeneratedPluginRegistrant.swift", "utilities/catalyst_voices_remote_widgets/example/**/**", "utilities/poc_local_storage/**/**", "**/*.svg"