feat: Phase 1.3 - Nav helper badges (Videos/Alerts) and long-press tooltips
This commit is contained in:
parent
0c183c3491
commit
bf4ac02d4b
|
|
@ -13,6 +13,7 @@ import '../discover/discover_screen.dart';
|
||||||
import '../beacon/beacon_screen.dart';
|
import '../beacon/beacon_screen.dart';
|
||||||
import '../quips/create/quip_creation_flow.dart';
|
import '../quips/create/quip_creation_flow.dart';
|
||||||
import '../secure_chat/secure_chat_full_screen.dart';
|
import '../secure_chat/secure_chat_full_screen.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import '../../widgets/radial_menu_overlay.dart';
|
import '../../widgets/radial_menu_overlay.dart';
|
||||||
import '../../widgets/onboarding_modal.dart';
|
import '../../widgets/onboarding_modal.dart';
|
||||||
import '../../providers/quip_upload_provider.dart';
|
import '../../providers/quip_upload_provider.dart';
|
||||||
|
|
@ -35,17 +36,56 @@ class _HomeShellState extends ConsumerState<HomeShell> with WidgetsBindingObserv
|
||||||
final SecureChatService _chatService = SecureChatService();
|
final SecureChatService _chatService = SecureChatService();
|
||||||
StreamSubscription<RemoteMessage>? _notifSub;
|
StreamSubscription<RemoteMessage>? _notifSub;
|
||||||
|
|
||||||
|
// Nav helper badges — show descriptive subtitle for first N taps
|
||||||
|
static const _maxHelperShows = 3;
|
||||||
|
Map<int, int> _navTapCounts = {};
|
||||||
|
|
||||||
|
static const _helperBadges = {
|
||||||
|
1: 'Videos', // Quips tab
|
||||||
|
2: 'Alerts', // Beacons tab
|
||||||
|
};
|
||||||
|
|
||||||
|
static const _longPressTooltips = {
|
||||||
|
0: 'Your main feed with posts from people you follow',
|
||||||
|
1: 'Quips are short-form videos — your stories',
|
||||||
|
2: 'Beacons are local alerts and real-time updates',
|
||||||
|
3: 'Your profile, settings, and saved posts',
|
||||||
|
};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
_chatService.startBackgroundSync();
|
_chatService.startBackgroundSync();
|
||||||
_initNotificationListener();
|
_initNotificationListener();
|
||||||
|
_loadNavTapCounts();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (mounted) OnboardingModal.showIfNeeded(context);
|
if (mounted) OnboardingModal.showIfNeeded(context);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _loadNavTapCounts() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_navTapCounts = {
|
||||||
|
for (final i in [1, 2])
|
||||||
|
i: prefs.getInt('nav_tap_$i') ?? 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _incrementNavTap(int index) async {
|
||||||
|
if (!_helperBadges.containsKey(index)) return;
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final current = prefs.getInt('nav_tap_$index') ?? 0;
|
||||||
|
await prefs.setInt('nav_tap_$index', current + 1);
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _navTapCounts[index] = current + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _initNotificationListener() {
|
void _initNotificationListener() {
|
||||||
_notifSub = NotificationService.instance.foregroundMessages.listen((message) {
|
_notifSub = NotificationService.instance.foregroundMessages.listen((message) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
|
@ -369,45 +409,126 @@ class _HomeShellState extends ConsumerState<HomeShell> with WidgetsBindingObserv
|
||||||
String? assetPath,
|
String? assetPath,
|
||||||
}) {
|
}) {
|
||||||
final isActive = widget.navigationShell.currentIndex == index;
|
final isActive = widget.navigationShell.currentIndex == index;
|
||||||
|
final helperBadge = _helperBadges[index];
|
||||||
|
final tapCount = _navTapCounts[index] ?? 0;
|
||||||
|
final showHelper = helperBadge != null && tapCount < _maxHelperShows;
|
||||||
|
final tooltip = _longPressTooltips[index];
|
||||||
|
|
||||||
return Expanded(
|
return Expanded(
|
||||||
child: InkWell(
|
child: GestureDetector(
|
||||||
onTap: () {
|
onLongPress: tooltip != null ? () {
|
||||||
widget.navigationShell.goBranch(
|
final overlay = Overlay.of(context);
|
||||||
index,
|
late OverlayEntry entry;
|
||||||
initialLocation: index == widget.navigationShell.currentIndex,
|
entry = OverlayEntry(
|
||||||
|
builder: (ctx) => _NavTooltipOverlay(
|
||||||
|
message: tooltip,
|
||||||
|
onDismiss: () => entry.remove(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
overlay.insert(entry);
|
||||||
child: Container(
|
Future.delayed(const Duration(seconds: 2), () {
|
||||||
height: double.infinity,
|
if (entry.mounted) entry.remove();
|
||||||
alignment: Alignment.center,
|
});
|
||||||
child: Column(
|
} : null,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
child: InkWell(
|
||||||
mainAxisSize: MainAxisSize.min,
|
onTap: () {
|
||||||
children: [
|
_incrementNavTap(index);
|
||||||
assetPath != null
|
widget.navigationShell.goBranch(
|
||||||
? Image.asset(
|
index,
|
||||||
assetPath,
|
initialLocation: index == widget.navigationShell.currentIndex,
|
||||||
width: SojornNav.bottomBarIconSize,
|
);
|
||||||
height: SojornNav.bottomBarIconSize,
|
},
|
||||||
color: isActive ? AppTheme.navyBlue : SojornColors.bottomNavUnselected,
|
child: Container(
|
||||||
)
|
height: double.infinity,
|
||||||
: Icon(
|
alignment: Alignment.center,
|
||||||
isActive ? activeIcon : icon,
|
child: Column(
|
||||||
color: isActive ? AppTheme.navyBlue : SojornColors.bottomNavUnselected,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
size: SojornNav.bottomBarIconSize,
|
mainAxisSize: MainAxisSize.min,
|
||||||
),
|
children: [
|
||||||
SizedBox(height: SojornNav.bottomBarLabelTopGap),
|
Stack(
|
||||||
Text(
|
clipBehavior: Clip.none,
|
||||||
label,
|
children: [
|
||||||
maxLines: 1,
|
assetPath != null
|
||||||
overflow: TextOverflow.ellipsis,
|
? Image.asset(
|
||||||
style: TextStyle(
|
assetPath,
|
||||||
fontSize: SojornNav.bottomBarLabelSize,
|
width: SojornNav.bottomBarIconSize,
|
||||||
fontWeight: isActive ? FontWeight.w700 : FontWeight.w600,
|
height: SojornNav.bottomBarIconSize,
|
||||||
color: isActive ? AppTheme.navyBlue : SojornColors.bottomNavUnselected,
|
color: isActive ? AppTheme.navyBlue : SojornColors.bottomNavUnselected,
|
||||||
|
)
|
||||||
|
: Icon(
|
||||||
|
isActive ? activeIcon : icon,
|
||||||
|
color: isActive ? AppTheme.navyBlue : SojornColors.bottomNavUnselected,
|
||||||
|
size: SojornNav.bottomBarIconSize,
|
||||||
|
),
|
||||||
|
if (showHelper)
|
||||||
|
Positioned(
|
||||||
|
right: -18,
|
||||||
|
top: -4,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.brightNavy,
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
child: Text(helperBadge, style: const TextStyle(
|
||||||
|
fontSize: 8, fontWeight: FontWeight.w700, color: Colors.white,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
SizedBox(height: SojornNav.bottomBarLabelTopGap),
|
||||||
],
|
Text(
|
||||||
|
label,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: SojornNav.bottomBarLabelSize,
|
||||||
|
fontWeight: isActive ? FontWeight.w700 : FontWeight.w600,
|
||||||
|
color: isActive ? AppTheme.navyBlue : SojornColors.bottomNavUnselected,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Nav Tooltip Overlay (long-press on nav items) ─────────────────────────
|
||||||
|
class _NavTooltipOverlay extends StatelessWidget {
|
||||||
|
final String message;
|
||||||
|
final VoidCallback onDismiss;
|
||||||
|
|
||||||
|
const _NavTooltipOverlay({required this.message, required this.onDismiss});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Positioned.fill(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: onDismiss,
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
child: Align(
|
||||||
|
alignment: const Alignment(0, 0.85),
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 32),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.navyBlue,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.2),
|
||||||
|
blurRadius: 12,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Text(message, style: const TextStyle(
|
||||||
|
color: Colors.white, fontSize: 13, fontWeight: FontWeight.w500,
|
||||||
|
), textAlign: TextAlign.center),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue