mirror of
https://github.com/zapstore/zapstore.git
synced 2026-10-05 20:48:24 +00:00
hotfix(nav): fix lag and UI inconsistencies in navigation bar
Fixes #333 The navigation bar felt laggy on tap, especially on mid-range devices The splash/ripple touch effect on nav items and TextButtons was contributing to the perceived delay. Additionally, the pill height changed when switching tabs, and the profile avatar border made the selected pill grow. Performance: - Remove splash, ripple, and highlight from BottomNavigationBar and NavigationRail - Apply NoSplash to TextButtons globally UI: - Use fixed pill size for all nav items so height stays consistent - Unify icon size (28px) for Search, Updates, and Profile - Add avatar border when selected using strokeAlignInside so it doesn't increase layout size
This commit is contained in:
+137
-101
@@ -8,6 +8,10 @@ import 'package:zapstore/widgets/common/badges.dart';
|
|||||||
import '../widgets/common/profile_avatar.dart';
|
import '../widgets/common/profile_avatar.dart';
|
||||||
import '../theme.dart';
|
import '../theme.dart';
|
||||||
|
|
||||||
|
/// Fixed size for nav bar item pills so they don't grow when content changes.
|
||||||
|
const _kNavItemPillWidth = 60.0;
|
||||||
|
const _kNavItemPillHeight = 44.0;
|
||||||
|
|
||||||
/// Main scaffold with bottom navigation that adapts to screen size
|
/// Main scaffold with bottom navigation that adapts to screen size
|
||||||
class MainScaffold extends StatelessWidget {
|
class MainScaffold extends StatelessWidget {
|
||||||
const MainScaffold({super.key, required this.navigationShell});
|
const MainScaffold({super.key, required this.navigationShell});
|
||||||
@@ -152,7 +156,13 @@ class MobileScaffold extends ConsumerWidget {
|
|||||||
removeBottom: true,
|
removeBottom: true,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 8),
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
child: BottomNavigationBar(
|
child: Theme(
|
||||||
|
data: Theme.of(context).copyWith(
|
||||||
|
splashFactory: NoSplash.splashFactory,
|
||||||
|
highlightColor: Colors.transparent,
|
||||||
|
splashColor: Colors.transparent,
|
||||||
|
),
|
||||||
|
child: BottomNavigationBar(
|
||||||
currentIndex: navigationShell.currentIndex,
|
currentIndex: navigationShell.currentIndex,
|
||||||
type: BottomNavigationBarType.fixed,
|
type: BottomNavigationBarType.fixed,
|
||||||
onTap: (index) => onNavTap(context, navigationShell, index),
|
onTap: (index) => onNavTap(context, navigationShell, index),
|
||||||
@@ -165,117 +175,123 @@ class MobileScaffold extends ConsumerWidget {
|
|||||||
context,
|
context,
|
||||||
).colorScheme.onSurface.withValues(alpha: 0.5),
|
).colorScheme.onSurface.withValues(alpha: 0.5),
|
||||||
selectedIconTheme: IconThemeData(
|
selectedIconTheme: IconThemeData(
|
||||||
size: 26,
|
size: 28,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
unselectedIconTheme: IconThemeData(
|
unselectedIconTheme: IconThemeData(
|
||||||
size: 22,
|
size: 28,
|
||||||
color: Theme.of(
|
color: Theme.of(
|
||||||
context,
|
context,
|
||||||
).colorScheme.onSurface.withValues(alpha: 0.5),
|
).colorScheme.onSurface.withValues(alpha: 0.5),
|
||||||
),
|
),
|
||||||
items: [
|
items: [
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: Container(
|
icon: SizedBox(
|
||||||
padding: const EdgeInsets.symmetric(
|
width: _kNavItemPillWidth,
|
||||||
horizontal: 16,
|
height: _kNavItemPillHeight,
|
||||||
vertical: 8,
|
child: Container(
|
||||||
|
decoration: navigationShell.currentIndex == 0
|
||||||
|
? BoxDecoration(
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.primary.withValues(alpha: 0.15),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: const Icon(Icons.search_rounded),
|
||||||
),
|
),
|
||||||
decoration: navigationShell.currentIndex == 0
|
|
||||||
? BoxDecoration(
|
|
||||||
color: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.primary.withValues(alpha: 0.15),
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
child: const Icon(Icons.search_rounded),
|
|
||||||
),
|
),
|
||||||
label: 'Search',
|
label: 'Search',
|
||||||
),
|
),
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: Container(
|
icon: SizedBox(
|
||||||
padding: const EdgeInsets.symmetric(
|
width: _kNavItemPillWidth,
|
||||||
horizontal: 16,
|
height: _kNavItemPillHeight,
|
||||||
vertical: 8,
|
child: Container(
|
||||||
),
|
decoration: navigationShell.currentIndex == 1
|
||||||
decoration: navigationShell.currentIndex == 1
|
? BoxDecoration(
|
||||||
? BoxDecoration(
|
color: Theme.of(
|
||||||
color: Theme.of(
|
context,
|
||||||
context,
|
).colorScheme.primary.withValues(alpha: 0.15),
|
||||||
).colorScheme.primary.withValues(alpha: 0.15),
|
borderRadius: BorderRadius.circular(16),
|
||||||
borderRadius: BorderRadius.circular(16),
|
)
|
||||||
)
|
: null,
|
||||||
: null,
|
alignment: Alignment.center,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.update),
|
const Icon(Icons.update),
|
||||||
if (isLoadingUpdates)
|
if (isLoadingUpdates)
|
||||||
Positioned(
|
Positioned(
|
||||||
right: -8,
|
right: -8,
|
||||||
top: -8,
|
top: -8,
|
||||||
child: BadgePill(
|
child: BadgePill(
|
||||||
child: const SizedBox(
|
child: const SizedBox(
|
||||||
width: 8,
|
width: 8,
|
||||||
height: 8,
|
height: 8,
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
else if (updateCount > 0)
|
||||||
|
Positioned(
|
||||||
|
right: -8,
|
||||||
|
top: -8,
|
||||||
|
child: CountBadge(count: updateCount),
|
||||||
),
|
),
|
||||||
)
|
],
|
||||||
else if (updateCount > 0)
|
),
|
||||||
Positioned(
|
|
||||||
right: -8,
|
|
||||||
top: -8,
|
|
||||||
child: CountBadge(count: updateCount),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
label: 'Updates',
|
label: 'Updates',
|
||||||
),
|
),
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: Container(
|
icon: SizedBox(
|
||||||
padding: const EdgeInsets.symmetric(
|
width: _kNavItemPillWidth,
|
||||||
horizontal: 16,
|
height: _kNavItemPillHeight,
|
||||||
vertical: 8,
|
child: Container(
|
||||||
|
decoration: navigationShell.currentIndex == 2
|
||||||
|
? BoxDecoration(
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.primary.withValues(alpha: 0.15),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: pubkey != null
|
||||||
|
? Container(
|
||||||
|
decoration: navigationShell.currentIndex == 2
|
||||||
|
? BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.primary,
|
||||||
|
width: 2,
|
||||||
|
strokeAlign:
|
||||||
|
BorderSide.strokeAlignInside,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
child: ProfileAvatar(
|
||||||
|
profile: profile,
|
||||||
|
pubkey: pubkey,
|
||||||
|
radius: 14,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.person_rounded),
|
||||||
),
|
),
|
||||||
decoration: navigationShell.currentIndex == 2
|
|
||||||
? BoxDecoration(
|
|
||||||
color: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.primary.withValues(alpha: 0.15),
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
child: pubkey != null
|
|
||||||
? Container(
|
|
||||||
decoration: navigationShell.currentIndex == 2
|
|
||||||
? BoxDecoration(
|
|
||||||
border: Border.all(
|
|
||||||
color: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.primary,
|
|
||||||
width: 2,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
child: ProfileAvatar(
|
|
||||||
profile: profile,
|
|
||||||
pubkey: pubkey,
|
|
||||||
radius: 14,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const Icon(Icons.person_rounded),
|
|
||||||
),
|
),
|
||||||
label: 'Profile',
|
label: 'Profile',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -331,12 +347,28 @@ class DesktopScaffold extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: NavigationRail(
|
child: Theme(
|
||||||
backgroundColor: Colors.transparent,
|
data: Theme.of(context).copyWith(
|
||||||
selectedIndex: navigationShell.currentIndex,
|
splashFactory: NoSplash.splashFactory,
|
||||||
onDestinationSelected: (index) =>
|
highlightColor: Colors.transparent,
|
||||||
onNavTap(context, navigationShell, index),
|
splashColor: Colors.transparent,
|
||||||
labelType: NavigationRailLabelType.all,
|
),
|
||||||
|
child: NavigationRail(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
selectedIndex: navigationShell.currentIndex,
|
||||||
|
onDestinationSelected: (index) =>
|
||||||
|
onNavTap(context, navigationShell, index),
|
||||||
|
selectedIconTheme: IconThemeData(
|
||||||
|
size: 28,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
unselectedIconTheme: IconThemeData(
|
||||||
|
size: 28,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme.onSurface
|
||||||
|
.withValues(alpha: 0.5),
|
||||||
|
),
|
||||||
|
labelType: NavigationRailLabelType.all,
|
||||||
destinations: [
|
destinations: [
|
||||||
const NavigationRailDestination(
|
const NavigationRailDestination(
|
||||||
icon: Icon(Icons.search),
|
icon: Icon(Icons.search),
|
||||||
@@ -375,17 +407,20 @@ class DesktopScaffold extends ConsumerWidget {
|
|||||||
NavigationRailDestination(
|
NavigationRailDestination(
|
||||||
icon: pubkey != null
|
icon: pubkey != null
|
||||||
? Container(
|
? Container(
|
||||||
decoration: navigationShell.currentIndex == 2
|
decoration:
|
||||||
? BoxDecoration(
|
navigationShell.currentIndex == 2
|
||||||
border: Border.all(
|
? BoxDecoration(
|
||||||
color: Theme.of(
|
shape: BoxShape.circle,
|
||||||
context,
|
border: Border.all(
|
||||||
).colorScheme.primary,
|
color: Theme.of(
|
||||||
width: 2,
|
context,
|
||||||
),
|
).colorScheme.primary,
|
||||||
borderRadius: BorderRadius.circular(16),
|
width: 2,
|
||||||
)
|
strokeAlign:
|
||||||
: null,
|
BorderSide.strokeAlignInside,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
child: ProfileAvatar(
|
child: ProfileAvatar(
|
||||||
profile: profile,
|
profile: profile,
|
||||||
pubkey: pubkey,
|
pubkey: pubkey,
|
||||||
@@ -397,6 +432,7 @@ class DesktopScaffold extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Expanded(child: navigationShell),
|
Expanded(child: navigationShell),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -308,6 +308,12 @@ final darkTheme = ThemeData(
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
textButtonTheme: TextButtonThemeData(
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
splashFactory: NoSplash.splashFactory,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
// Professional dark input design
|
// Professional dark input design
|
||||||
inputDecorationTheme: InputDecorationTheme(
|
inputDecorationTheme: InputDecorationTheme(
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
|
|||||||
Reference in New Issue
Block a user