diff --git a/lib/feature/root/domain/root_tab_service.dart b/lib/feature/root/domain/root_tab_service.dart index bd66983de..5648b79fb 100644 --- a/lib/feature/root/domain/root_tab_service.dart +++ b/lib/feature/root/domain/root_tab_service.dart @@ -50,7 +50,7 @@ class RootTabService { final currentTab = RootTab.getByRoute(_router.currentRoutes.firstOrNull); final isTabUpdated = currentTab != tab; - if (isTabUpdated) { + if (!isTabUpdated) { _scrollTabToTopSubject.add(tab); } diff --git a/lib/feature/wallet/token_wallet_details/view/token_wallet_details_page.dart b/lib/feature/wallet/token_wallet_details/view/token_wallet_details_page.dart index 6b137f292..cac70f0df 100644 --- a/lib/feature/wallet/token_wallet_details/view/token_wallet_details_page.dart +++ b/lib/feature/wallet/token_wallet_details/view/token_wallet_details_page.dart @@ -8,6 +8,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:nekoton_repository/nekoton_repository.dart'; +import 'package:ui_components_lib/components/common/default_sliver_app_bar.dart'; import 'package:ui_components_lib/ui_components_lib.dart'; import 'package:ui_components_lib/v2/ui_components_lib_v2.dart'; @@ -82,87 +83,100 @@ class _Body extends StatelessWidget { return CustomScrollView( controller: controller, slivers: [ - SliverToBoxAdapter( - child: Stack( - children: [ - const _Background(), - Column( - mainAxisSize: MainAxisSize.min, - children: [ - const DefaultAppBar(), - ValueListenableBuilder( - valueListenable: contractName, - builder: (_, contractName, __) => Text( - contractName, - style: theme.textStyles.labelSmall.copyWith( - color: theme.colors.content3, - ), - ), - ), - const SizedBox(height: DimensSizeV2.d12), - StateNotifierBuilder( - listenableState: tokenBalance, - builder: (_, tokenBalance) { - if (tokenBalance == null) return const SizedBox.shrink(); - return AmountWidget.fromMoney( - amount: tokenBalance, - includeSymbol: false, - style: theme.textStyles.headingXLarge, - ); - }, - ), - const SizedBox(height: DimensSizeV2.d4), - StateNotifierBuilder( - listenableState: fiatBalance, - builder: (_, fiatBalance) { - if (fiatBalance == null) return const SizedBox.shrink(); - return AmountWidget.dollars( - amount: fiatBalance, - style: theme.textStyles.labelXSmall, - ); - }, - ), - const SizedBox(height: DimensSizeV2.d16), - SizedBox( - height: DimensSizeV2.d74, - child: SeparatedRow( - separator: VerticalDivider( - width: DimensStroke.small, - thickness: DimensStroke.small, - color: theme.colors.borderAlpha, - ), - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - WalletActionButton( - label: LocaleKeys.receiveWord.tr(), - icon: LucideIcons.arrowDown, - onPressed: () => - showReceiveFundsSheet(context, owner), - ), - DoubleSourceBuilder( - firstSource: canSend, - secondSource: tokenBalance, - builder: (_, canSend, tokenBalance) { - if (canSend != true || tokenBalance == null) { - return const SizedBox.shrink(); - } + ValueListenableBuilder( + valueListenable: contractName, + builder: (_, contractName, __) { + return DefaultSliverAppBar( + title: contractName, + flexibleSpace: FlexibleSpaceBar( + background: Stack( + fit: StackFit.expand, + children: [ + const _Background(), + SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 72), // Space for AppBar + const SizedBox(height: DimensSizeV2.d12), + Text( + contractName, + style: theme.textStyles.labelSmall.copyWith( + color: theme.colors.content3, + ), + ), + StateNotifierBuilder( + listenableState: tokenBalance, + builder: (_, tokenBalance) { + if (tokenBalance == null) { + return const SizedBox.shrink(); + } - return WalletActionButton( - label: LocaleKeys.sendWord.tr(), - icon: LucideIcons.arrowUp, - onPressed: onSend, - ); - }, - ), - ], + return AmountWidget.fromMoney( + amount: tokenBalance, + includeSymbol: false, + style: theme.textStyles.headingXLarge, + ); + }, + ), + const SizedBox(height: DimensSizeV2.d4), + StateNotifierBuilder( + listenableState: fiatBalance, + builder: (_, fiatBalance) { + if (fiatBalance == null) { + return const SizedBox.shrink(); + } + return AmountWidget.dollars( + amount: fiatBalance, + style: theme.textStyles.labelXSmall, + ); + }, + ), + const SizedBox(height: DimensSizeV2.d16), + SizedBox( + height: DimensSizeV2.d74, + child: SeparatedRow( + separator: VerticalDivider( + width: DimensStroke.small, + thickness: DimensStroke.small, + color: theme.colors.borderAlpha, + ), + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + WalletActionButton( + label: LocaleKeys.receiveWord.tr(), + icon: LucideIcons.arrowDown, + onPressed: () => + showReceiveFundsSheet(context, owner), + ), + DoubleSourceBuilder( + firstSource: canSend, + secondSource: tokenBalance, + builder: (_, canSend, tokenBalance) { + if (canSend != true || + tokenBalance == null) { + return const SizedBox.shrink(); + } + + return WalletActionButton( + label: LocaleKeys.sendWord.tr(), + icon: LucideIcons.arrowUp, + onPressed: onSend, + ); + }, + ), + ], + ), + ), + ], + ), ), - ), - const SizedBox(height: DimensSizeV2.d48), - ], + ], + ), ), - ], - ), + ); + }, ), DecoratedSliver( decoration: BoxDecoration( diff --git a/lib/feature/wallet/ton_wallet_details/view/ton_wallet_details_page.dart b/lib/feature/wallet/ton_wallet_details/view/ton_wallet_details_page.dart index 7992aaeb3..71c32a1de 100644 --- a/lib/feature/wallet/ton_wallet_details/view/ton_wallet_details_page.dart +++ b/lib/feature/wallet/ton_wallet_details/view/ton_wallet_details_page.dart @@ -6,6 +6,7 @@ import 'package:app/utils/utils.dart'; import 'package:elementary_helper/elementary_helper.dart'; import 'package:flutter/material.dart'; import 'package:nekoton_repository/nekoton_repository.dart'; +import 'package:ui_components_lib/components/common/default_sliver_app_bar.dart'; import 'package:ui_components_lib/ui_components_lib.dart'; import 'package:ui_components_lib/v2/ui_components_lib_v2.dart'; @@ -73,55 +74,65 @@ class _Body extends StatelessWidget { return CustomScrollView( controller: controller, slivers: [ - SliverToBoxAdapter( - child: Stack( - children: [ - const _Background(), - Column( - mainAxisSize: MainAxisSize.min, - children: [ - const DefaultAppBar(), - Text( - symbol, - style: theme.textStyles.labelSmall.copyWith( - color: theme.colors.content3, - ), + DefaultSliverAppBar( + title: symbol, + flexibleSpace: FlexibleSpaceBar( + background: Stack( + fit: StackFit.expand, + children: [ + const _Background(), + SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 72), + Text( + symbol, + style: theme.textStyles.labelSmall.copyWith( + color: theme.colors.content3, + ), + ), + const SizedBox(height: DimensSizeV2.d12), + StateNotifierBuilder( + listenableState: tokenBalanceState, + builder: (_, tokenBalance) { + if (tokenBalance == null) { + return const SizedBox.shrink(); + } + return AmountWidget.fromMoney( + amount: tokenBalance, + includeSymbol: false, + style: theme.textStyles.headingXLarge, + ); + }, + ), + const SizedBox(height: DimensSizeV2.d4), + StateNotifierBuilder( + listenableState: fiatBalanceState, + builder: (_, fiatBalance) { + if (fiatBalance == null) { + return const SizedBox.shrink(); + } + return AmountWidget.dollars( + amount: fiatBalance, + style: theme.textStyles.labelXSmall, + ); + }, + ), + const SizedBox(height: DimensSizeV2.d16), + if (error == null) + WalletAccountActions( + account: account, + allowStake: false, + sendSpecified: true, + padding: EdgeInsets.zero, + ), + const SizedBox(height: DimensSizeV2.d48), + ], ), - const SizedBox(height: DimensSizeV2.d12), - StateNotifierBuilder( - listenableState: tokenBalanceState, - builder: (_, tokenBalance) { - if (tokenBalance == null) return const SizedBox.shrink(); - return AmountWidget.fromMoney( - amount: tokenBalance, - includeSymbol: false, - style: theme.textStyles.headingXLarge, - ); - }, - ), - const SizedBox(height: DimensSizeV2.d4), - StateNotifierBuilder( - listenableState: fiatBalanceState, - builder: (_, fiatBalance) { - if (fiatBalance == null) return const SizedBox.shrink(); - return AmountWidget.dollars( - amount: fiatBalance, - style: theme.textStyles.labelXSmall, - ); - }, - ), - const SizedBox(height: DimensSizeV2.d16), - if (error == null) - WalletAccountActions( - account: account, - allowStake: false, - sendSpecified: true, - padding: EdgeInsets.zero, - ), - const SizedBox(height: DimensSizeV2.d48), - ], - ), - ], + ), + ], + ), ), ), DecoratedSliver( diff --git a/packages/ui_components_lib/lib/components/common/default_sliver_app_bar.dart b/packages/ui_components_lib/lib/components/common/default_sliver_app_bar.dart new file mode 100644 index 000000000..5c5b2aa99 --- /dev/null +++ b/packages/ui_components_lib/lib/components/common/default_sliver_app_bar.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:ui_components_lib/components/button/app_bar_back_button.dart'; +import 'package:ui_components_lib/components/common/default_app_bar.dart'; +import 'package:ui_components_lib/v2/dimens_v2.dart'; +import 'package:ui_components_lib/v2/theme_style_v2.dart'; + +typedef IsColapsed = bool Function(SliverConstraints); + +class DefaultSliverAppBar extends StatelessWidget { + const DefaultSliverAppBar({ + required this.title, + this.isCollapsed = defaultIsCollapsed, + this.flexibleSpace, + super.key, + }); + + final IsColapsed isCollapsed; + final String title; + final Widget? flexibleSpace; + + @override + Widget build(BuildContext context) { + final theme = context.themeStyleV2; + + return SliverLayoutBuilder( + builder: (context, constraints) { + // Calculate if we're in collapsed state + final isCollapsed = this.isCollapsed(constraints); + + return SliverAppBar( + expandedHeight: 320, + collapsedHeight: defaultAppBarHeight, + toolbarHeight: defaultAppBarHeight, + pinned: true, + elevation: 0, + scrolledUnderElevation: 0, + backgroundColor: + isCollapsed ? theme.colors.background0 : Colors.transparent, + surfaceTintColor: Colors.transparent, + leading: Container( + margin: const EdgeInsets.only( + left: DimensSizeV2.d16, + top: DimensSizeV2.d12, + bottom: DimensSizeV2.d12, + ), + child: AppBarBackButton( + onPressed: () => Navigator.of(context).maybePop(), + ), + ), + leadingWidth: DimensSizeV2.d64, + title: AnimatedOpacity( + opacity: isCollapsed ? 1.0 : 0.0, + duration: const Duration(milliseconds: 200), + child: Text( + title, + style: theme.textStyles.headingMedium, + ), + ), + centerTitle: true, + flexibleSpace: flexibleSpace, + ); + }, + ); + } + + static bool defaultIsCollapsed(SliverConstraints constraints) { + return constraints.scrollOffset > 200; + } +} diff --git a/test/feature/root/domain/root_tab_service_test.dart b/test/feature/root/domain/root_tab_service_test.dart index 4d9936153..8a3a2f5f2 100644 --- a/test/feature/root/domain/root_tab_service_test.dart +++ b/test/feature/root/domain/root_tab_service_test.dart @@ -365,11 +365,9 @@ void main() { when(() => mockRouter.currentRoutes).thenReturn([walletRoute]); var tabEmitted = false; - RootTab? emittedTab; final subscription = rootTabService.scrollTabToTopSubject.listen((tab) { tabEmitted = true; - emittedTab = tab; }); // Act @@ -381,8 +379,7 @@ void main() { // Assert expect(result, isTrue); - expect(tabEmitted, isTrue); - expect(emittedTab, equals(RootTab.browser)); + expect(tabEmitted, isFalse); await subscription.cancel(); }); @@ -408,7 +405,7 @@ void main() { // Assert expect(result, isFalse); - expect(tabEmitted, isFalse); + expect(tabEmitted, isTrue); await subscription.cancel(); }); @@ -418,11 +415,9 @@ void main() { when(() => mockRouter.currentRoutes).thenReturn([]); var tabEmitted = false; - RootTab? emittedTab; final subscription = rootTabService.scrollTabToTopSubject.listen((tab) { tabEmitted = true; - emittedTab = tab; }); // Act @@ -434,8 +429,7 @@ void main() { // Assert expect(result, isTrue); - expect(tabEmitted, isTrue); - expect(emittedTab, equals(RootTab.browser)); + expect(tabEmitted, isFalse); await subscription.cancel(); }); @@ -446,11 +440,9 @@ void main() { when(() => mockRouter.currentRoutes).thenReturn([unknownRoute]); var tabEmitted = false; - RootTab? emittedTab; final subscription = rootTabService.scrollTabToTopSubject.listen((tab) { tabEmitted = true; - emittedTab = tab; }); // Act @@ -462,44 +454,7 @@ void main() { // Assert expect(result, isTrue); - expect(tabEmitted, isTrue); - expect(emittedTab, equals(RootTab.profile)); - - await subscription.cancel(); - }); - - test('should work correctly with multiple consecutive calls', () async { - // Arrange - final walletRoute = MockWalletRoute(); - when(() => mockRouter.currentRoutes).thenReturn([walletRoute]); - - final emittedTabs = []; - - final subscription = - rootTabService.scrollTabToTopSubject.listen(emittedTabs.add); - - // Act - final result1 = - rootTabService.tryToChangeTabAndCheckDiff(RootTab.browser); - final result2 = - rootTabService.tryToChangeTabAndCheckDiff(RootTab.browser); - final result3 = - rootTabService.tryToChangeTabAndCheckDiff(RootTab.profile); - - // Wait a brief moment for all streams to emit - await Future.delayed(const Duration(milliseconds: 10)); - - // Assert - expect(result1, isTrue); // wallet -> browser (different) - expect( - result2, - isTrue, - ); // wallet -> browser (still different, method doesn't track state) - expect(result3, isTrue); // wallet -> profile (different) - expect( - emittedTabs, - equals([RootTab.browser, RootTab.browser, RootTab.profile]), - ); + expect(tabEmitted, isFalse); await subscription.cancel(); });