From 520fccaad211792a386d3f1cffe7370da3c999b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damian=20Moli=C5=84ski?= <47773413+damian-molinski@users.noreply.github.com> Date: Thu, 30 Jan 2025 07:57:49 +0100 Subject: [PATCH] feat(cat-voices): account dropdown (#1709) * chore: wip * chore: most of UI implemented * wip: theme changing * fix: remove theme switch reference * feat: move popup config to theme * refactor: segmented button theme * fix: theme and colors adjustments * feat: make app react to session cubit * feat: loc strings * feat: account popup CatalystId * feat: MenuItemTile on tap * feat: links redirects * feat: Make avatar letter bold * chore: cleanup * chore: self review code cleanup * chore: PR review adjustments --- .../apps/voices/lib/app/view/app_content.dart | 51 ++-- .../lib/common/constants/constants.dart | 3 + .../lib/common/ext/preferences_ext.dart | 43 +++ .../lib/pages/account/account_popup.dart | 264 ---------------- .../account_popup/session_account_avatar.dart | 45 +++ .../session_account_display_name.dart | 36 +++ .../session_account_popup_catalyst_id.dart | 18 ++ .../session_account_popup_menu.dart | 288 ++++++++++++++++++ .../session_theme_menu_tile.dart | 53 ++++ .../session_timezone_menu_tile.dart | 53 ++++ .../spaces/appbar/session_state_header.dart | 22 +- .../appbar/spaces_theme_mode_switch.dart | 15 - .../lib/pages/spaces/spaces_shell_page.dart | 3 - .../lib/widgets/avatars/voices_avatar.dart | 1 + .../lib/widgets/common/affix_decorator.dart | 22 +- .../widgets/separators/voices_divider.dart | 8 + .../widgets/text/timezone_date_time_text.dart | 9 +- .../lib/widgets/tiles/menu_item_tile.dart | 71 +++++ .../tiles/menu_segments_item_tile.dart | 77 +++++ .../toggles/voices_theme_mode_switch.dart | 33 -- .../apps/voices/lib/widgets/widgets.dart | 3 +- .../lib/src/session/session_cubit.dart | 2 +- .../lib/src/themes/catalyst.dart | 8 + .../lib/src/themes/widgets/buttons_theme.dart | 23 -- .../widgets/voices_popup_menu_theme.dart | 15 + .../voices_segmented_button_theme.dart | 40 +++ .../lib/l10n/intl_en.arb | 11 +- .../lib/src/session/session_account.dart | 21 +- .../utilities/uikit_example/lib/main.dart | 13 +- 29 files changed, 847 insertions(+), 404 deletions(-) create mode 100644 catalyst_voices/apps/voices/lib/common/ext/preferences_ext.dart delete mode 100644 catalyst_voices/apps/voices/lib/pages/account/account_popup.dart create mode 100644 catalyst_voices/apps/voices/lib/pages/spaces/appbar/account_popup/session_account_avatar.dart create mode 100644 catalyst_voices/apps/voices/lib/pages/spaces/appbar/account_popup/session_account_display_name.dart create mode 100644 catalyst_voices/apps/voices/lib/pages/spaces/appbar/account_popup/session_account_popup_catalyst_id.dart create mode 100644 catalyst_voices/apps/voices/lib/pages/spaces/appbar/account_popup/session_account_popup_menu.dart create mode 100644 catalyst_voices/apps/voices/lib/pages/spaces/appbar/account_popup/session_theme_menu_tile.dart create mode 100644 catalyst_voices/apps/voices/lib/pages/spaces/appbar/account_popup/session_timezone_menu_tile.dart delete mode 100644 catalyst_voices/apps/voices/lib/pages/spaces/appbar/spaces_theme_mode_switch.dart create mode 100644 catalyst_voices/apps/voices/lib/widgets/tiles/menu_item_tile.dart create mode 100644 catalyst_voices/apps/voices/lib/widgets/tiles/menu_segments_item_tile.dart delete mode 100644 catalyst_voices/apps/voices/lib/widgets/toggles/voices_theme_mode_switch.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/widgets/voices_popup_menu_theme.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/widgets/voices_segmented_button_theme.dart diff --git a/catalyst_voices/apps/voices/lib/app/view/app_content.dart b/catalyst_voices/apps/voices/lib/app/view/app_content.dart index d27d3ca5844..e189ff5c8f3 100644 --- a/catalyst_voices/apps/voices/lib/app/view/app_content.dart +++ b/catalyst_voices/apps/voices/lib/app/view/app_content.dart @@ -1,14 +1,17 @@ import 'package:catalyst_voices/app/view/app_active_state_listener.dart'; import 'package:catalyst_voices/app/view/app_precache_image_assets.dart'; import 'package:catalyst_voices/app/view/app_session_listener.dart'; +import 'package:catalyst_voices/common/ext/preferences_ext.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localized_locales/flutter_localized_locales.dart'; const _restorationScopeId = 'rootVoices'; -final class AppContent extends StatefulWidget { +class AppContent extends StatelessWidget { final RouterConfig routerConfig; const AppContent({ @@ -17,21 +20,33 @@ final class AppContent extends StatefulWidget { }); @override - State createState() => AppContentState(); - - /// Returns the state associated with the [AppContent]. - static AppContentState of(BuildContext context) { - return context.findAncestorStateOfType()!; + Widget build(BuildContext context) { + return BlocSelector( + selector: (state) => state.settings.theme.asThemeMode(), + builder: (context, state) { + return _AppContent( + routerConfig: routerConfig, + themeMode: state, + ); + }, + ); } } -class AppContentState extends State { - ThemeMode _themeMode = ThemeMode.light; +final class _AppContent extends StatelessWidget { + final RouterConfig routerConfig; + final ThemeMode themeMode; - void updateThemeMode(ThemeMode themeMode) { - setState(() { - _themeMode = themeMode; - }); + const _AppContent({ + required this.routerConfig, + required this.themeMode, + }); + + List> get _localizationsDelegates { + return const [ + ...VoicesLocalizations.localizationsDelegates, + LocaleNamesLocalizationsDelegate(), + ]; } @override @@ -41,8 +56,8 @@ class AppContentState extends State { localizationsDelegates: _localizationsDelegates, supportedLocales: VoicesLocalizations.supportedLocales, localeListResolutionCallback: basicLocaleListResolution, - routerConfig: widget.routerConfig, - themeMode: _themeMode, + routerConfig: routerConfig, + themeMode: themeMode, theme: ThemeBuilder.buildTheme( brand: Brand.catalyst, brightness: Brightness.light, @@ -51,6 +66,7 @@ class AppContentState extends State { brand: Brand.catalyst, brightness: Brightness.dark, ), + debugShowCheckedModeBanner: false, builder: (context, child) { return Scaffold( primary: false, @@ -66,11 +82,4 @@ class AppContentState extends State { }, ); } - - List> get _localizationsDelegates { - return const [ - ...VoicesLocalizations.localizationsDelegates, - LocaleNamesLocalizationsDelegate(), - ]; - } } diff --git a/catalyst_voices/apps/voices/lib/common/constants/constants.dart b/catalyst_voices/apps/voices/lib/common/constants/constants.dart index 08f23f66d18..c7c5f08f13d 100644 --- a/catalyst_voices/apps/voices/lib/common/constants/constants.dart +++ b/catalyst_voices/apps/voices/lib/common/constants/constants.dart @@ -6,4 +6,7 @@ abstract class VoicesConstants { 'https://docs.projectcatalyst.io/current-fund/fund-basics/project-catalyst-terms-and-conditions'; static const privacyPolicyUrl = 'https://docs.projectcatalyst.io/current-fund/fund-basics/project-catalyst-terms-and-conditions/catalyst-fc-privacy-policy'; + static const supportUrl = + 'https://catalystiog.zendesk.com/hc/en-us/requests/new'; + static const docsUrl = 'https://docs.projectcatalyst.io/'; } diff --git a/catalyst_voices/apps/voices/lib/common/ext/preferences_ext.dart b/catalyst_voices/apps/voices/lib/common/ext/preferences_ext.dart new file mode 100644 index 00000000000..498c3ee4b97 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/common/ext/preferences_ext.dart @@ -0,0 +1,43 @@ +import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; +import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:flutter/material.dart'; + +extension TimezonePreferencesExt on TimezonePreferences { + String localizedName(BuildContext context) { + return switch (this) { + TimezonePreferences.utc => 'UTC', + TimezonePreferences.local => context.l10n.local, + }; + } + + SvgGenImage icon() { + return switch (this) { + TimezonePreferences.utc => VoicesAssets.icons.globeAlt, + TimezonePreferences.local => VoicesAssets.icons.locationMarker, + }; + } +} + +extension ThemePreferencesExt on ThemePreferences { + ThemeMode asThemeMode() { + return switch (this) { + ThemePreferences.dark => ThemeMode.dark, + ThemePreferences.light => ThemeMode.light, + }; + } + + String localizedName(BuildContext context) { + return switch (this) { + ThemePreferences.dark => context.l10n.themeDark, + ThemePreferences.light => context.l10n.themeLight, + }; + } + + SvgGenImage icon() { + return switch (this) { + ThemePreferences.dark => VoicesAssets.icons.moon, + ThemePreferences.light => VoicesAssets.icons.sun, + }; + } +} diff --git a/catalyst_voices/apps/voices/lib/pages/account/account_popup.dart b/catalyst_voices/apps/voices/lib/pages/account/account_popup.dart deleted file mode 100644 index 97f5e86c708..00000000000 --- a/catalyst_voices/apps/voices/lib/pages/account/account_popup.dart +++ /dev/null @@ -1,264 +0,0 @@ -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_shared/catalyst_voices_shared.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -class AccountPopup extends StatelessWidget { - final String initials; - final VoidCallback? onProfileKeychainTap; - final VoidCallback? onLockAccountTap; - - const AccountPopup({ - super.key, - required this.initials, - this.onProfileKeychainTap, - this.onLockAccountTap, - }); - - @override - Widget build(BuildContext context) { - return PopupMenuButton<_MenuItemValue>( - color: Theme.of(context).colors.elevationsOnSurfaceNeutralLv1White, - onSelected: (_MenuItemValue value) { - switch (value) { - case _MenuItemValue.profileAndKeychain: - onProfileKeychainTap?.call(); - break; - case _MenuItemValue.lock: - onLockAccountTap?.call(); - break; - } - }, - itemBuilder: (BuildContext bc) { - return [ - PopupMenuItem( - padding: EdgeInsets.zero, - enabled: false, - value: null, - key: const Key('PopUpMenuAccountHeader'), - child: _Header( - initials: initials, - walletName: 'Wallet name', - walletBalance: '₳ 1,750,000', - accountType: 'Basis', - /* cSpell:disable */ - walletAddress: ShelleyAddress.fromBech32( - 'addr_test1vzpwq95z3xyum8vqndgdd' - '9mdnmafh3djcxnc6jemlgdmswcve6tkw', - ), - /* cSpell:enable */ - ), - ), - const PopupMenuItem( - height: 48, - padding: EdgeInsets.zero, - enabled: false, - value: null, - key: Key('PopUpMenuMyAccount'), - child: _Section('My account'), - ), - PopupMenuItem( - padding: EdgeInsets.zero, - value: _MenuItemValue.profileAndKeychain, - key: const Key('PopUpMenuProfileAndKeychain'), - child: _MenuItem( - 'Profile & Keychain', - VoicesAssets.icons.userCircle, - ), - ), - PopupMenuItem( - padding: EdgeInsets.zero, - value: _MenuItemValue.lock, - key: const Key('PopUpMenuLockAccount'), - child: _MenuItem( - 'Lock account', - VoicesAssets.icons.lockClosed, - showDivider: false, - ), - ), - ]; - }, - offset: const Offset(0, kToolbarHeight), - child: IgnorePointer( - child: VoicesAvatar( - icon: Text(initials), - ), - ), - ); - } -} - -class _Header extends StatelessWidget { - final String initials; - final String walletName; - final String walletBalance; - final String accountType; - final ShelleyAddress walletAddress; - - const _Header({ - required this.initials, - required this.walletName, - required this.walletBalance, - required this.accountType, - required this.walletAddress, - }); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: _padding), - child: Row( - children: [ - VoicesAvatar( - icon: Text(initials), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.all(_padding), - child: Wrap( - children: [ - Text( - walletName, - style: Theme.of(context).textTheme.bodyLarge, - ), - Text( - walletBalance, - style: Theme.of(context).textTheme.bodyMedium, - ), - ], - ), - ), - ), - VoicesChip.rectangular( - content: Text( - accountType, - style: TextStyle( - color: Theme.of(context).colors.successContainer, - ), - ), - backgroundColor: Theme.of(context).colors.success, - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.only( - left: _padding, - right: _padding, - bottom: _padding, - top: 8, - ), - child: Row( - children: [ - Expanded( - child: Text( - WalletAddressFormatter.formatShort(walletAddress), - style: Theme.of(context).textTheme.bodyLarge, - ), - ), - InkWell( - onTap: () async { - await Clipboard.setData( - ClipboardData(text: walletAddress.toBech32()), - ); - }, - child: VoicesAssets.icons.clipboardCopy.buildIcon(), - ), - ], - ), - ), - VoicesDivider( - height: 1, - color: Theme.of(context).colors.outlineBorder, - indent: 0, - endIndent: 0, - ), - ], - ); - } -} - -class _MenuItem extends StatelessWidget { - final String text; - final SvgGenImage icon; - final bool showDivider; - - const _MenuItem( - this.text, - this.icon, { - this.showDivider = true, - }); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Container( - height: 47, - alignment: Alignment.centerLeft, - padding: const EdgeInsets.symmetric(horizontal: _padding), - child: Row( - children: [ - icon.buildIcon(), - const SizedBox(width: _padding), - Text( - text, - style: Theme.of(context).textTheme.bodyLarge, - ), - ], - ), - ), - if (showDivider) - VoicesDivider( - height: 1, - color: Theme.of(context).colors.outlineBorderVariant, - indent: 0, - endIndent: 0, - ), - ], - ); - } -} - -class _Section extends StatelessWidget { - final String text; - - const _Section( - this.text, - ); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Container( - height: 47, - alignment: Alignment.centerLeft, - padding: const EdgeInsets.symmetric(horizontal: _padding), - child: Text( - text, - style: Theme.of(context).textTheme.titleSmall, - ), - ), - VoicesDivider( - height: 1, - color: Theme.of(context).colors.outlineBorderVariant, - indent: 0, - endIndent: 0, - ), - ], - ); - } -} - -const _padding = 12.0; - -enum _MenuItemValue { - profileAndKeychain, - lock, -} diff --git a/catalyst_voices/apps/voices/lib/pages/spaces/appbar/account_popup/session_account_avatar.dart b/catalyst_voices/apps/voices/lib/pages/spaces/appbar/account_popup/session_account_avatar.dart new file mode 100644 index 00000000000..41085307886 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/pages/spaces/appbar/account_popup/session_account_avatar.dart @@ -0,0 +1,45 @@ +import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class SessionAccountAvatar extends StatelessWidget { + final VoidCallback? onTap; + + const SessionAccountAvatar({ + super.key, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + return BlocSelector( + selector: (state) => state.account?.initials ?? '', + builder: (context, state) { + return _Avatar( + letter: state, + onTap: onTap, + ); + }, + ); + } +} + +class _Avatar extends StatelessWidget { + final String letter; + final VoidCallback? onTap; + + const _Avatar({ + required this.letter, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + return VoicesAvatar( + icon: Text(letter), + radius: 20, + onTap: onTap, + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/pages/spaces/appbar/account_popup/session_account_display_name.dart b/catalyst_voices/apps/voices/lib/pages/spaces/appbar/account_popup/session_account_display_name.dart new file mode 100644 index 00000000000..ab287d70058 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/pages/spaces/appbar/account_popup/session_account_display_name.dart @@ -0,0 +1,36 @@ +import 'package:catalyst_voices/common/ext/build_context_ext.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class SessionAccountDisplayName extends StatelessWidget { + const SessionAccountDisplayName({super.key}); + + @override + Widget build(BuildContext context) { + return BlocSelector( + selector: (state) => state.account?.displayName ?? '', + builder: (context, state) => _Text(state), + ); + } +} + +class _Text extends StatelessWidget { + final String data; + + const _Text(this.data); + + @override + Widget build(BuildContext context) { + final textStyle = context.textTheme.titleMedium?.copyWith( + color: context.colors.textOnPrimaryLevel0, + ); + + return Text( + data, + style: textStyle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/pages/spaces/appbar/account_popup/session_account_popup_catalyst_id.dart b/catalyst_voices/apps/voices/lib/pages/spaces/appbar/account_popup/session_account_popup_catalyst_id.dart new file mode 100644 index 00000000000..fa6851f07dd --- /dev/null +++ b/catalyst_voices/apps/voices/lib/pages/spaces/appbar/account_popup/session_account_popup_catalyst_id.dart @@ -0,0 +1,18 @@ +import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class SessionAccountPopupCatalystId extends StatelessWidget { + const SessionAccountPopupCatalystId({super.key}); + + @override + Widget build(BuildContext context) { + return BlocSelector( + selector: (state) => state.account?.catalystId ?? '', + builder: (context, state) { + return CatalystIdText(state, isCompact: true); + }, + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/pages/spaces/appbar/account_popup/session_account_popup_menu.dart b/catalyst_voices/apps/voices/lib/pages/spaces/appbar/account_popup/session_account_popup_menu.dart new file mode 100644 index 00000000000..3cbfdb3ab37 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/pages/spaces/appbar/account_popup/session_account_popup_menu.dart @@ -0,0 +1,288 @@ +import 'dart:async'; + +import 'package:catalyst_voices/common/constants/constants.dart'; +import 'package:catalyst_voices/common/ext/build_context_ext.dart'; +import 'package:catalyst_voices/pages/spaces/appbar/account_popup/session_account_avatar.dart'; +import 'package:catalyst_voices/pages/spaces/appbar/account_popup/session_account_display_name.dart'; +import 'package:catalyst_voices/pages/spaces/appbar/account_popup/session_account_popup_catalyst_id.dart'; +import 'package:catalyst_voices/pages/spaces/appbar/account_popup/session_theme_menu_tile.dart'; +import 'package:catalyst_voices/pages/spaces/appbar/account_popup/session_timezone_menu_tile.dart'; +import 'package:catalyst_voices/routes/routes.dart'; +import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; +import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +sealed class _MenuItemEvent { + const _MenuItemEvent(); +} + +final class _OpenAccountDetails extends _MenuItemEvent { + const _OpenAccountDetails(); +} + +final class _SetupRoles extends _MenuItemEvent { + const _SetupRoles(); +} + +final class _RedirectToSupport extends _MenuItemEvent { + const _RedirectToSupport(); +} + +final class _RedirectToDocs extends _MenuItemEvent { + const _RedirectToDocs(); +} + +final class _Lock extends _MenuItemEvent { + const _Lock(); +} + +class SessionAccountPopupMenu extends StatefulWidget { + const SessionAccountPopupMenu({ + super.key, + }); + + @override + State createState() { + return _SessionAccountPopupMenuState(); + } +} + +class _SessionAccountPopupMenuState extends State + with LaunchUrlMixin { + final _popupMenuButtonKey = GlobalKey>(); + + @override + Widget build(BuildContext context) { + return PopupMenuButton<_MenuItemEvent>( + key: _popupMenuButtonKey, + initialValue: null, + onSelected: _handleEvent, + itemBuilder: (context) => const [_PopupMenuItem()], + tooltip: context.l10n.accountMenuPopupTooltip, + constraints: const BoxConstraints(maxWidth: 320), + routeSettings: const RouteSettings(name: '/account_menu'), + color: PopupMenuTheme.of(context).color, + // disable because PopupMenuButton internally always wraps child in + // InkWell which adds unwanted background over color. + enabled: false, + child: SessionAccountAvatar( + onTap: () { + _popupMenuButtonKey.currentState?.showButtonMenu(); + }, + ), + ); + } + + void _handleEvent(_MenuItemEvent event) { + switch (event) { + case _OpenAccountDetails(): + unawaited(const AccountRoute().push(context)); + case _SetupRoles(): + // TODO(damian-molinski): don't know what it should do + break; + case _RedirectToSupport(): + final uri = Uri.parse(VoicesConstants.supportUrl); + unawaited(launchUri(uri)); + case _RedirectToDocs(): + final uri = Uri.parse(VoicesConstants.docsUrl); + unawaited(launchUri(uri)); + case _Lock(): + unawaited(context.read().lock()); + } + } +} + +class _PopupMenuItem extends PopupMenuItem<_MenuItemEvent> { + const _PopupMenuItem() + : super( + // disabled because PopupMenuItem always adds InkWell + // and ripple which we don't want. + enabled: false, + padding: EdgeInsets.zero, + value: null, + child: const _PopupMenu(), + ); +} + +class _PopupMenu extends StatelessWidget { + const _PopupMenu(); + + @override + Widget build(BuildContext context) { + final theme = PopupMenuTheme.of(context); + return DecoratedBox( + decoration: ShapeDecoration( + color: theme.color, + shape: theme.shape ?? const RoundedRectangleBorder(), + ), + child: const Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _AccountHeader(), + VoicesDivider.expanded(), + _Account(), + _Settings(), + VoicesDivider.expanded(height: 17), + _Links(), + VoicesDivider.expanded(height: 17), + _Session(), + SizedBox(height: 8), + ], + ), + ); + } +} + +class _AccountHeader extends StatelessWidget { + const _AccountHeader(); + + @override + Widget build(BuildContext context) { + return Container( + constraints: const BoxConstraints.tightFor(height: 60), + padding: const EdgeInsets.symmetric(horizontal: 16), + child: const Row( + children: [ + SessionAccountAvatar(), + SizedBox(width: 16), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SessionAccountDisplayName(), + SessionAccountPopupCatalystId(), + ], + ), + ), + ], + ), + ); + } +} + +class _Account extends StatelessWidget { + const _Account(); + + @override + Widget build(BuildContext context) { + return _Section( + name: context.l10n.account, + children: [ + MenuItemTile( + leading: VoicesAssets.icons.userCircle.buildIcon(), + title: Text(context.l10n.profileAndKeychain), + trailing: VoicesAssets.icons.chevronRight.buildIcon(), + onTap: () => Navigator.pop(context, const _OpenAccountDetails()), + ), + ], + ); + } +} + +class _Settings extends StatelessWidget { + const _Settings(); + + @override + Widget build(BuildContext context) { + return _Section( + name: context.l10n.settings, + children: const [ + SessionTimezoneMenuTile(), + SessionThemeMenuTile(), + ], + ); + } +} + +class _Links extends StatelessWidget { + const _Links(); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + MenuItemTile( + leading: VoicesAssets.icons.userGroup.buildIcon(), + title: Text(context.l10n.setupCatalystRoles), + onTap: () => Navigator.pop(context, const _SetupRoles()), + ), + MenuItemTile( + leading: VoicesAssets.icons.support.buildIcon(), + title: Text(context.l10n.submitSupportRequest), + onTap: () => Navigator.pop(context, const _RedirectToSupport()), + ), + MenuItemTile( + leading: VoicesAssets.icons.academicCap.buildIcon(), + title: Text(context.l10n.catalystKnowledgeBase), + onTap: () => Navigator.pop(context, const _RedirectToDocs()), + ), + ], + ); + } +} + +class _Session extends StatelessWidget { + const _Session(); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + MenuItemTile( + leading: VoicesAssets.icons.lockClosed.buildIcon(), + title: Text(context.l10n.lockAccount), + onTap: () => Navigator.pop(context, const _Lock()), + ), + ], + ); + } +} + +class _Section extends StatelessWidget { + final String name; + final List children; + + const _Section({ + required this.name, + required this.children, + }); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + _SectionName(name), + ...children, + ], + ); + } +} + +class _SectionName extends StatelessWidget { + final String data; + + const _SectionName(this.data); + + @override + Widget build(BuildContext context) { + return Container( + constraints: const BoxConstraints.tightFor(height: 40), + padding: const EdgeInsets.symmetric(horizontal: 16), + alignment: Alignment.centerLeft, + child: Text( + data, + style: context.textTheme.bodyMedium?.copyWith( + color: context.colors.textOnPrimaryLevel1, + ), + ), + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/pages/spaces/appbar/account_popup/session_theme_menu_tile.dart b/catalyst_voices/apps/voices/lib/pages/spaces/appbar/account_popup/session_theme_menu_tile.dart new file mode 100644 index 00000000000..02d60a57901 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/pages/spaces/appbar/account_popup/session_theme_menu_tile.dart @@ -0,0 +1,53 @@ +import 'package:catalyst_voices/common/ext/preferences_ext.dart'; +import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; +import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class SessionThemeMenuTile extends StatelessWidget { + const SessionThemeMenuTile({super.key}); + + @override + Widget build(BuildContext context) { + return BlocSelector( + selector: (state) => state.settings.theme, + builder: (context, state) => _SegmentsTile(selected: state), + ); + } +} + +class _SegmentsTile extends StatelessWidget { + final ThemePreferences selected; + + const _SegmentsTile({ + required this.selected, + }); + + @override + Widget build(BuildContext context) { + return MenuSegmentsItemTile( + title: Text(context.l10n.theme), + segments: ( + first: ThemePreferences.dark.asSegmentButton(context), + second: ThemePreferences.light.asSegmentButton(context), + ), + selected: selected, + onChanged: (value) { + context.read().updateTheme(value); + }, + ); + } +} + +extension _ButtonSegmentBuilder on ThemePreferences { + ButtonSegment asSegmentButton(BuildContext context) { + return ButtonSegment( + value: this, + icon: icon().buildIcon(), + label: Text(localizedName(context)), + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/pages/spaces/appbar/account_popup/session_timezone_menu_tile.dart b/catalyst_voices/apps/voices/lib/pages/spaces/appbar/account_popup/session_timezone_menu_tile.dart new file mode 100644 index 00000000000..71b0ee7b5ac --- /dev/null +++ b/catalyst_voices/apps/voices/lib/pages/spaces/appbar/account_popup/session_timezone_menu_tile.dart @@ -0,0 +1,53 @@ +import 'package:catalyst_voices/common/ext/preferences_ext.dart'; +import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; +import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class SessionTimezoneMenuTile extends StatelessWidget { + const SessionTimezoneMenuTile({super.key}); + + @override + Widget build(BuildContext context) { + return BlocSelector( + selector: (state) => state.settings.timezone, + builder: (context, state) => _SegmentsTile(selected: state), + ); + } +} + +class _SegmentsTile extends StatelessWidget { + final TimezonePreferences selected; + + const _SegmentsTile({ + required this.selected, + }); + + @override + Widget build(BuildContext context) { + return MenuSegmentsItemTile( + title: Text(context.l10n.timezone), + segments: ( + first: TimezonePreferences.utc.asSegmentButton(context), + second: TimezonePreferences.local.asSegmentButton(context), + ), + selected: selected, + onChanged: (value) { + context.read().updateTimezone(value); + }, + ); + } +} + +extension _ButtonSegmentBuilder on TimezonePreferences { + ButtonSegment asSegmentButton(BuildContext context) { + return ButtonSegment( + value: this, + icon: icon().buildIcon(), + label: Text(localizedName(context)), + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/pages/spaces/appbar/session_state_header.dart b/catalyst_voices/apps/voices/lib/pages/spaces/appbar/session_state_header.dart index 64e8cd25c03..9c6d6f49105 100644 --- a/catalyst_voices/apps/voices/lib/pages/spaces/appbar/session_state_header.dart +++ b/catalyst_voices/apps/voices/lib/pages/spaces/appbar/session_state_header.dart @@ -1,7 +1,4 @@ -import 'dart:async'; - -import 'package:catalyst_voices/pages/account/account_popup.dart'; -import 'package:catalyst_voices/routes/routing/account_route.dart'; +import 'package:catalyst_voices/pages/spaces/appbar/account_popup/session_account_popup_menu.dart'; import 'package:catalyst_voices/widgets/widgets.dart'; import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; @@ -20,26 +17,11 @@ class SessionStateHeader extends StatelessWidget { return switch (state.status) { SessionStatus.visitor => const _VisitorButton(), SessionStatus.guest => const _GuestButton(), - SessionStatus.actor => AccountPopup( - key: const Key('AccountPopupButton'), - initials: state.account?.initials ?? '', - onLockAccountTap: () => _onLockAccount(context), - onProfileKeychainTap: () => _onSeeProfile(context), - ), + SessionStatus.actor => const SessionAccountPopupMenu() }; }, ); } - - void _onLockAccount(BuildContext context) { - unawaited(context.read().lock()); - } - - void _onSeeProfile(BuildContext context) { - unawaited( - const AccountRoute().push(context), - ); - } } class _GuestButton extends StatelessWidget { diff --git a/catalyst_voices/apps/voices/lib/pages/spaces/appbar/spaces_theme_mode_switch.dart b/catalyst_voices/apps/voices/lib/pages/spaces/appbar/spaces_theme_mode_switch.dart deleted file mode 100644 index 7f6212d74b3..00000000000 --- a/catalyst_voices/apps/voices/lib/pages/spaces/appbar/spaces_theme_mode_switch.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:catalyst_voices/app/app.dart'; -import 'package:catalyst_voices/widgets/widgets.dart'; -import 'package:flutter/material.dart'; - -/// A switch that updates the app theme mode. -class SpacesThemeModeSwitch extends StatelessWidget { - const SpacesThemeModeSwitch({super.key}); - - @override - Widget build(BuildContext context) { - return VoicesThemeModeSwitch( - onChanged: AppContent.of(context).updateThemeMode, - ); - } -} diff --git a/catalyst_voices/apps/voices/lib/pages/spaces/spaces_shell_page.dart b/catalyst_voices/apps/voices/lib/pages/spaces/spaces_shell_page.dart index e131eaa521a..9d470703b64 100644 --- a/catalyst_voices/apps/voices/lib/pages/spaces/spaces_shell_page.dart +++ b/catalyst_voices/apps/voices/lib/pages/spaces/spaces_shell_page.dart @@ -5,7 +5,6 @@ import 'package:catalyst_voices/pages/campaign/admin_tools/campaign_admin_tools_ import 'package:catalyst_voices/pages/campaign/details/widgets/campaign_management.dart'; import 'package:catalyst_voices/pages/spaces/appbar/session_action_header.dart'; import 'package:catalyst_voices/pages/spaces/appbar/session_state_header.dart'; -import 'package:catalyst_voices/pages/spaces/appbar/spaces_theme_mode_switch.dart'; import 'package:catalyst_voices/pages/spaces/drawer/spaces_drawer.dart'; import 'package:catalyst_voices/widgets/widgets.dart'; import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; @@ -118,11 +117,9 @@ class _SpacesShellPageState extends State { if (space == Space.treasury) { return [ const CampaignManagement(), - const SpacesThemeModeSwitch(), ]; } else { return [ - const SpacesThemeModeSwitch(), const SessionActionHeader(), const SessionStateHeader(), ]; diff --git a/catalyst_voices/apps/voices/lib/widgets/avatars/voices_avatar.dart b/catalyst_voices/apps/voices/lib/widgets/avatars/voices_avatar.dart index 8989efd60a4..99fe3a80d96 100644 --- a/catalyst_voices/apps/voices/lib/widgets/avatars/voices_avatar.dart +++ b/catalyst_voices/apps/voices/lib/widgets/avatars/voices_avatar.dart @@ -66,6 +66,7 @@ class VoicesAvatar extends StatelessWidget { ), child: DefaultTextStyle( style: Theme.of(context).textTheme.bodyLarge!.copyWith( + fontWeight: FontWeight.w700, fontSize: 18, height: 1, color: foregroundColor ?? diff --git a/catalyst_voices/apps/voices/lib/widgets/common/affix_decorator.dart b/catalyst_voices/apps/voices/lib/widgets/common/affix_decorator.dart index 3a28ad7bb0b..b85514d5f21 100644 --- a/catalyst_voices/apps/voices/lib/widgets/common/affix_decorator.dart +++ b/catalyst_voices/apps/voices/lib/widgets/common/affix_decorator.dart @@ -31,6 +31,9 @@ class AffixDecorator extends StatelessWidget { /// The widget to be displayed after the child widget. final Widget? suffix; + /// See [Row.mainAxisSize]. + final MainAxisSize mainAxisSize; + /// The widget to be decorated with prefix and/or suffix. final Widget child; @@ -44,6 +47,7 @@ class AffixDecorator extends StatelessWidget { this.iconTheme, this.prefix, this.suffix, + this.mainAxisSize = MainAxisSize.min, required this.child, }); @@ -52,8 +56,19 @@ class AffixDecorator extends StatelessWidget { final suffix = this.suffix; final prefix = this.prefix; + final child = switch (mainAxisSize) { + MainAxisSize.min => Flexible( + key: const Key('DecoratorData'), + child: this.child, + ), + MainAxisSize.max => Expanded( + key: const Key('DecoratorData'), + child: this.child, + ), + }; + return Row( - mainAxisSize: MainAxisSize.min, + mainAxisSize: mainAxisSize, children: [ if (prefix != null) ...[ IconTheme( @@ -63,10 +78,7 @@ class AffixDecorator extends StatelessWidget { ), SizedBox(width: gap), ], - Flexible( - key: const Key('DecoratorData'), - child: child, - ), + child, if (suffix != null) ...[ SizedBox(width: gap), IconTheme( diff --git a/catalyst_voices/apps/voices/lib/widgets/separators/voices_divider.dart b/catalyst_voices/apps/voices/lib/widgets/separators/voices_divider.dart index 389dde54de5..91acc999440 100644 --- a/catalyst_voices/apps/voices/lib/widgets/separators/voices_divider.dart +++ b/catalyst_voices/apps/voices/lib/widgets/separators/voices_divider.dart @@ -36,6 +36,14 @@ class VoicesDivider extends StatelessWidget { this.color, }); + const VoicesDivider.expanded({ + super.key, + this.height, + this.indent = 0, + this.endIndent = 0, + this.color, + }); + @override Widget build(BuildContext context) { return Divider( diff --git a/catalyst_voices/apps/voices/lib/widgets/text/timezone_date_time_text.dart b/catalyst_voices/apps/voices/lib/widgets/text/timezone_date_time_text.dart index ea2b2a114b8..a72038e8d79 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text/timezone_date_time_text.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text/timezone_date_time_text.dart @@ -1,7 +1,7 @@ +import 'package:catalyst_voices/common/ext/preferences_ext.dart'; import 'package:catalyst_voices/widgets/common/affix_decorator.dart'; import 'package:catalyst_voices_blocs/catalyst_voices_blocs.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:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -118,18 +118,13 @@ class _TimezoneCard extends StatelessWidget { color: foregroundColor, ); - final text = switch (data) { - TimezonePreferences.utc => 'UTC', - TimezonePreferences.local => context.l10n.local, - }; - return Material( color: backgroundColor, borderRadius: BorderRadius.circular(4), textStyle: style, child: Padding( padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 4), - child: Text(text), + child: Text(data.localizedName(context)), ), ); } diff --git a/catalyst_voices/apps/voices/lib/widgets/tiles/menu_item_tile.dart b/catalyst_voices/apps/voices/lib/widgets/tiles/menu_item_tile.dart new file mode 100644 index 00000000000..df56340464e --- /dev/null +++ b/catalyst_voices/apps/voices/lib/widgets/tiles/menu_item_tile.dart @@ -0,0 +1,71 @@ +import 'package:catalyst_voices/common/ext/build_context_ext.dart'; +import 'package:catalyst_voices/widgets/common/affix_decorator.dart'; +import 'package:flutter/material.dart'; + +class MenuItemTile extends StatelessWidget { + final Widget leading; + final Widget title; + final Widget? trailing; + final VoidCallback? onTap; + + const MenuItemTile({ + super.key, + required this.leading, + required this.title, + this.trailing, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + final trailing = this.trailing; + + return ConstrainedBox( + constraints: const BoxConstraints.tightFor(height: 40), + child: Material( + type: MaterialType.transparency, + child: InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: AffixDecorator( + gap: 12, + iconTheme: IconThemeData( + size: 24, + color: context.colors.iconsForeground, + ), + prefix: leading, + suffix: trailing, + mainAxisSize: MainAxisSize.max, + child: _TitleDecoration(child: title), + ), + ), + ), + ), + ); + } +} + +class _TitleDecoration extends StatelessWidget { + final Widget child; + + const _TitleDecoration({ + required this.child, + }); + + @override + Widget build(BuildContext context) { + final textTheme = context.textTheme; + + final textStyle = (textTheme.bodyLarge ?? const TextStyle()).copyWith( + color: context.colors.textOnPrimaryLevel0, + ); + + return DefaultTextStyle( + style: textStyle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + child: child, + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/widgets/tiles/menu_segments_item_tile.dart b/catalyst_voices/apps/voices/lib/widgets/tiles/menu_segments_item_tile.dart new file mode 100644 index 00000000000..422c1a874e5 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/widgets/tiles/menu_segments_item_tile.dart @@ -0,0 +1,77 @@ +import 'package:catalyst_voices/common/ext/build_context_ext.dart'; +import 'package:catalyst_voices/widgets/buttons/voices_segmented_button.dart'; +import 'package:flutter/material.dart'; + +typedef MenuSegmentsButtons = ({ + ButtonSegment first, + ButtonSegment second, +}); + +class MenuSegmentsItemTile extends StatelessWidget { + final Widget title; + final MenuSegmentsButtons segments; + final T? selected; + final ValueChanged? onChanged; + + const MenuSegmentsItemTile({ + super.key, + required this.title, + required this.segments, + this.selected, + this.onChanged, + }); + + @override + Widget build(BuildContext context) { + final onChanged = this.onChanged; + + return Container( + constraints: const BoxConstraints.tightFor(height: 48), + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + Expanded(child: _TitleDecoration(child: title)), + const SizedBox(width: 8), + VoicesSegmentedButton( + segments: [segments.first, segments.second], + selected: { + if (selected != null) selected!, + }, + onChanged: onChanged != null + ? (value) { + onChanged(value.single); + } + : null, + multiSelectionEnabled: false, + emptySelectionAllowed: false, + showSelectedIcon: false, + ), + ], + ), + ); + } +} + +class _TitleDecoration extends StatelessWidget { + final Widget child; + + const _TitleDecoration({ + required this.child, + }); + + @override + Widget build(BuildContext context) { + final textTheme = context.textTheme; + + final textStyle = (textTheme.bodyMedium ?? const TextStyle()).copyWith( + color: context.colors.textOnPrimaryLevel0, + ); + + return DefaultTextStyle( + style: textStyle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + child: child, + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/widgets/toggles/voices_theme_mode_switch.dart b/catalyst_voices/apps/voices/lib/widgets/toggles/voices_theme_mode_switch.dart deleted file mode 100644 index 4211d352938..00000000000 --- a/catalyst_voices/apps/voices/lib/widgets/toggles/voices_theme_mode_switch.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:catalyst_voices/widgets/toggles/voices_switch.dart'; -import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; -import 'package:flutter/material.dart'; - -/// A switch that toggles between light & dark theme mode. -class VoicesThemeModeSwitch extends StatelessWidget { - final ValueChanged onChanged; - - const VoicesThemeModeSwitch({ - super.key, - required this.onChanged, - }); - - @override - Widget build(BuildContext context) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text('${context.l10n.themeLight} / ${context.l10n.themeDark}'), - const SizedBox(width: 8), - VoicesSwitch( - key: const Key('ThemeSwitch'), - value: Theme.of(context).brightness == Brightness.dark, - onChanged: (value) { - onChanged( - value ? ThemeMode.dark : ThemeMode.light, - ); - }, - ), - ], - ); - } -} diff --git a/catalyst_voices/apps/voices/lib/widgets/widgets.dart b/catalyst_voices/apps/voices/lib/widgets/widgets.dart index 46b66686df2..3d7a2c62e27 100644 --- a/catalyst_voices/apps/voices/lib/widgets/widgets.dart +++ b/catalyst_voices/apps/voices/lib/widgets/widgets.dart @@ -82,6 +82,8 @@ export 'text_field/voices_int_field.dart'; export 'text_field/voices_password_text_field.dart'; export 'text_field/voices_text_field.dart'; export 'tiles/document_builder_section_tile.dart'; +export 'tiles/menu_item_tile.dart'; +export 'tiles/menu_segments_item_tile.dart'; export 'tiles/selectable_tile.dart'; export 'tiles/voices_expansion_tile.dart'; export 'tiles/voices_nav_tile.dart'; @@ -89,6 +91,5 @@ export 'toggles/voices_checkbox.dart'; export 'toggles/voices_checkbox_group.dart'; export 'toggles/voices_radio.dart'; export 'toggles/voices_switch.dart'; -export 'toggles/voices_theme_mode_switch.dart'; export 'tooltips/voices_plain_tooltip.dart'; export 'tooltips/voices_rich_tooltip.dart'; diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/session/session_cubit.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/session/session_cubit.dart index cfa9d6d7874..38095d1e997 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/session/session_cubit.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/session/session_cubit.dart @@ -98,7 +98,7 @@ final class SessionCubit extends Cubit await _userService.useAccount(dummyAccount); } - void updateTimezonePreferences(TimezonePreferences value) { + void updateTimezone(TimezonePreferences value) { final settings = _userService.user.settings; final updatedSettings = settings.copyWith(timezone: Optional.of(value)); diff --git a/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/catalyst.dart b/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/catalyst.dart index 8cc5e442bb9..9ccebb30f83 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/catalyst.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/catalyst.dart @@ -6,6 +6,8 @@ import 'package:catalyst_voices_brands/src/themes/widgets/buttons_theme.dart'; import 'package:catalyst_voices_brands/src/themes/widgets/toggles_theme.dart'; import 'package:catalyst_voices_brands/src/themes/widgets/voices_dialog_theme.dart'; import 'package:catalyst_voices_brands/src/themes/widgets/voices_input_decoration_theme.dart'; +import 'package:catalyst_voices_brands/src/themes/widgets/voices_popup_menu_theme.dart'; +import 'package:catalyst_voices_brands/src/themes/widgets/voices_segmented_button_theme.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; @@ -322,7 +324,13 @@ ThemeData _buildThemeData( drawerTheme: DrawerThemeData( backgroundColor: voicesColorScheme.onSurfaceNeutralOpaqueLv0, ), + popupMenuTheme: VoicesPopupMenuThemeData(colors: voicesColorScheme), dialogTheme: VoicesDialogTheme(colors: voicesColorScheme), + segmentedButtonTheme: VoicesSegmentedButtonTheme( + colors: colorScheme, + voicesColors: voicesColorScheme, + textTheme: textTheme, + ), listTileTheme: ListTileThemeData( shape: const StadiumBorder(), minTileHeight: 56, diff --git a/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/widgets/buttons_theme.dart b/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/widgets/buttons_theme.dart index 52a03e9693a..6662c696953 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/widgets/buttons_theme.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/widgets/buttons_theme.dart @@ -59,29 +59,6 @@ extension ButtonsThemeExt on ThemeData { shape: const CircleBorder(), ).merge(_buildBaseButtonStyle(textTheme)), ), - segmentedButtonTheme: SegmentedButtonThemeData( - style: SegmentedButton.styleFrom( - foregroundColor: colors.textOnPrimary, - backgroundColor: Colors.transparent, - selectedForegroundColor: colors.textOnPrimary, - selectedBackgroundColor: colors.onSurfacePrimary012, - disabledForegroundColor: colors.iconsDisabled, - disabledBackgroundColor: Colors.transparent, - textStyle: textTheme.labelLarge, - ).copyWith( - side: WidgetStateProperty.resolveWith( - (states) { - if (states.contains(WidgetState.disabled)) { - return BorderSide(color: colors.iconsDisabled); - } - - return BorderSide(color: colors.outlineBorder); - }, - ), - iconSize: const WidgetStatePropertyAll(18), - ), - selectedIcon: const Icon(Icons.check), - ), ); } } diff --git a/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/widgets/voices_popup_menu_theme.dart b/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/widgets/voices_popup_menu_theme.dart new file mode 100644 index 00000000000..515b827d84d --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/widgets/voices_popup_menu_theme.dart @@ -0,0 +1,15 @@ +import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; +import 'package:flutter/material.dart'; + +class VoicesPopupMenuThemeData extends PopupMenuThemeData { + VoicesPopupMenuThemeData({ + required VoicesColorScheme colors, + }) : super( + color: colors.elevationsOnSurfaceNeutralLv1White, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + menuPadding: EdgeInsets.zero, + position: PopupMenuPosition.over, + ); +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/widgets/voices_segmented_button_theme.dart b/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/widgets/voices_segmented_button_theme.dart new file mode 100644 index 00000000000..05dbbf34fb0 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/widgets/voices_segmented_button_theme.dart @@ -0,0 +1,40 @@ +import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; +import 'package:flutter/material.dart'; + +class VoicesSegmentedButtonTheme extends SegmentedButtonThemeData { + VoicesSegmentedButtonTheme({ + required ColorScheme colors, + required VoicesColorScheme voicesColors, + required TextTheme textTheme, + }) : super( + selectedIcon: const Icon(Icons.check), + style: SegmentedButton.styleFrom( + foregroundColor: voicesColors.textOnPrimary, + backgroundColor: Colors.transparent, + selectedForegroundColor: colors.onPrimary, + selectedBackgroundColor: colors.primary, + disabledForegroundColor: voicesColors.iconsDisabled, + disabledBackgroundColor: Colors.transparent, + textStyle: textTheme.labelLarge, + ).copyWith( + side: _Side(colors: voicesColors), + ), + ); +} + +class _Side extends WidgetStateBorderSide { + final VoicesColorScheme colors; + + const _Side({ + required this.colors, + }); + + @override + BorderSide? resolve(Set states) { + if (states.contains(WidgetState.disabled)) { + return BorderSide(color: colors.iconsDisabled); + } + + return BorderSide(color: colors.outlineBorder); + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_localization/lib/l10n/intl_en.arb b/catalyst_voices/packages/internal/catalyst_voices_localization/lib/l10n/intl_en.arb index 638056da2d4..8bc1682c28f 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_localization/lib/l10n/intl_en.arb +++ b/catalyst_voices/packages/internal/catalyst_voices_localization/lib/l10n/intl_en.arb @@ -849,6 +849,7 @@ "@uploadKeychainInfo": { "description": "An info on keychain upload dialog" }, + "theme": "Theme", "themeLight": "Light", "@themeLight": { "description": "Refers to a light theme mode." @@ -1411,5 +1412,13 @@ }, "finalProposal": "Final", "mostRecent": "Most Recent", - "viewAllProposals": "View All Proposals" + "viewAllProposals": "View All Proposals", + "accountMenuPopupTooltip": "Account menu", + "account": "Account", + "settings": "Settings", + "setupCatalystRoles": "Setup Catalyst roles", + "submitSupportRequest": "Submit support request", + "catalystKnowledgeBase": "Catalyst knowledge base", + "lockAccount": "Lock account", + "timezone": "Timezone" } \ No newline at end of file diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/session/session_account.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/session/session_account.dart index 3ad1b315f54..a881729b057 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/session/session_account.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/session/session_account.dart @@ -2,13 +2,15 @@ import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:equatable/equatable.dart'; final class SessionAccount extends Equatable { - final String initials; + final String catalystId; + final String displayName; final bool isAdmin; final bool isProposer; final bool isDrep; const SessionAccount({ - this.initials = '', + this.catalystId = '', + this.displayName = '', this.isAdmin = false, this.isProposer = false, this.isDrep = false, @@ -16,25 +18,30 @@ final class SessionAccount extends Equatable { const SessionAccount.mocked() : this( - initials: 'C', + catalystId: 'cardano/uuid', + displayName: 'Account Mocked', isAdmin: true, isProposer: true, ); factory SessionAccount.fromAccount(Account account) { return SessionAccount( - initials: account.displayName.isNotEmpty - ? account.displayName.substring(0, 1) - : '', + catalystId: account.catalystId, + displayName: account.displayName, isAdmin: account.isAdmin, isProposer: account.roles.contains(AccountRole.proposer), isDrep: account.roles.contains(AccountRole.drep), ); } + String get initials { + return displayName.isNotEmpty ? displayName.substring(0, 1) : ''; + } + @override List get props => [ - initials, + catalystId, + displayName, isAdmin, isProposer, isDrep, diff --git a/catalyst_voices/utilities/uikit_example/lib/main.dart b/catalyst_voices/utilities/uikit_example/lib/main.dart index a4ba2f31735..bddfa7e88d0 100644 --- a/catalyst_voices/utilities/uikit_example/lib/main.dart +++ b/catalyst_voices/utilities/uikit_example/lib/main.dart @@ -1,4 +1,4 @@ -import 'package:catalyst_voices/widgets/toggles/voices_theme_mode_switch.dart'; +import 'package:catalyst_voices/widgets/widgets.dart'; import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; import 'package:catalyst_voices_localization/generated/catalyst_voices_localizations.dart'; import 'package:flutter/material.dart'; @@ -50,6 +50,8 @@ class _UIKitExampleAppState extends State { settings: settings, builder: (_) { return _ThemeModeSwitcherWrapper( + brightness: + _themeMode == ThemeMode.dark ? Brightness.dark : Brightness.light, onChanged: _onThemeModeChanged, child: page, ); @@ -65,10 +67,12 @@ class _UIKitExampleAppState extends State { } class _ThemeModeSwitcherWrapper extends StatelessWidget { + final Brightness brightness; final ValueChanged onChanged; final Widget child; const _ThemeModeSwitcherWrapper({ + required this.brightness, required this.onChanged, required this.child, }); @@ -86,8 +90,11 @@ class _ThemeModeSwitcherWrapper extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16), child: Align( alignment: Alignment.centerRight, - child: VoicesThemeModeSwitch( - onChanged: onChanged, + child: VoicesSwitch( + value: brightness == Brightness.dark, + onChanged: (value) { + onChanged(value ? ThemeMode.dark : ThemeMode.light); + }, ), ), ),