- Replace NULLIF with CASE WHEN for proper UUID casting - Fix missing ::uuid casting in WHERE clauses - Resolve 'operator does not exist: uuid = text' errors - Focus on post_repository.go, notification_repository.go, and category_repository.go
308 lines
9.5 KiB
Dart
308 lines
9.5 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import '../../services/secure_chat_service.dart';
|
|
import '../../theme/app_theme.dart';
|
|
import '../notifications/notifications_screen.dart';
|
|
import '../compose/compose_screen.dart';
|
|
import '../search/search_screen.dart';
|
|
import '../beacon/beacon_screen.dart';
|
|
import '../quips/create/quip_creation_flow.dart';
|
|
import '../secure_chat/secure_chat_full_screen.dart';
|
|
import '../../widgets/radial_menu_overlay.dart';
|
|
|
|
/// Root shell for the main tabs. The active tab is controlled by GoRouter's
|
|
/// [StatefulNavigationShell] so navigation state and tab selection stay in sync.
|
|
class HomeShell extends StatefulWidget {
|
|
final StatefulNavigationShell navigationShell;
|
|
|
|
const HomeShell({super.key, required this.navigationShell});
|
|
|
|
@override
|
|
State<HomeShell> createState() => _HomeShellState();
|
|
}
|
|
|
|
class _HomeShellState extends State<HomeShell> with WidgetsBindingObserver {
|
|
bool _isRadialMenuVisible = false;
|
|
final SecureChatService _chatService = SecureChatService();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
WidgetsBinding.instance.addObserver(this);
|
|
_chatService.startBackgroundSync();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
WidgetsBinding.instance.removeObserver(this);
|
|
_chatService.stopBackgroundSync();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
if (state == AppLifecycleState.resumed) {
|
|
_chatService.startBackgroundSync();
|
|
} else if (state == AppLifecycleState.paused) {
|
|
_chatService.stopBackgroundSync();
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final currentIndex = widget.navigationShell.currentIndex;
|
|
|
|
return Scaffold(
|
|
appBar: _buildAppBar(),
|
|
body: Stack(
|
|
children: [
|
|
NavigationShellScope(
|
|
currentIndex: currentIndex,
|
|
child: widget.navigationShell,
|
|
),
|
|
RadialMenuOverlay(
|
|
isVisible: _isRadialMenuVisible,
|
|
onDismiss: () => setState(() => _isRadialMenuVisible = false),
|
|
onPostTap: () {
|
|
Navigator.of(context).push(
|
|
MaterialPageRoute(
|
|
builder: (_) => const ComposeScreen(),
|
|
),
|
|
);
|
|
},
|
|
onQuipTap: () {
|
|
Navigator.of(context).push(
|
|
MaterialPageRoute(
|
|
builder: (_) => const QuipCreationFlow(),
|
|
),
|
|
);
|
|
},
|
|
onBeaconTap: () {
|
|
Navigator.of(context).push(
|
|
MaterialPageRoute(
|
|
builder: (_) => const BeaconScreen(),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
floatingActionButton: Transform.translate(
|
|
offset: const Offset(0, 12),
|
|
child: GestureDetector(
|
|
onTap: () => setState(() => _isRadialMenuVisible = !_isRadialMenuVisible),
|
|
child: Container(
|
|
width: 56,
|
|
height: 56,
|
|
decoration: BoxDecoration(
|
|
color: AppTheme.navyBlue,
|
|
borderRadius: BorderRadius.circular(16),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: AppTheme.navyBlue.withOpacity(0.4),
|
|
blurRadius: 12,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: const Icon(
|
|
Icons.add,
|
|
color: Colors.white,
|
|
size: 32,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
|
|
bottomNavigationBar: Padding(
|
|
padding: const EdgeInsets.only(bottom: 2),
|
|
child: BottomAppBar(
|
|
notchMargin: 8.0,
|
|
padding: EdgeInsets.zero,
|
|
height: 58,
|
|
clipBehavior: Clip.antiAlias,
|
|
shape: const CircularNotchedRectangle(),
|
|
child: ClipRRect(
|
|
borderRadius: const BorderRadius.only(
|
|
topLeft: Radius.circular(16),
|
|
topRight: Radius.circular(16),
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
children: [
|
|
_buildNavBarItem(
|
|
icon: Icons.home_outlined,
|
|
activeIcon: Icons.home,
|
|
index: 0,
|
|
label: 'Home',
|
|
),
|
|
_buildNavBarItem(
|
|
icon: Icons.explore_outlined,
|
|
activeIcon: Icons.explore,
|
|
index: 1,
|
|
label: 'Discover',
|
|
),
|
|
const SizedBox(width: 48),
|
|
_buildNavBarItem(
|
|
icon: Icons.play_circle_outline,
|
|
activeIcon: Icons.play_circle,
|
|
index: 2,
|
|
label: 'Quips',
|
|
),
|
|
_buildNavBarItem(
|
|
icon: Icons.person_outline,
|
|
activeIcon: Icons.person,
|
|
index: 3,
|
|
label: 'Profile',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
PreferredSizeWidget _buildAppBar() {
|
|
return AppBar(
|
|
title: Text(
|
|
'sojorn',
|
|
style: GoogleFonts.literata(
|
|
fontSize: 28,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: -0.5,
|
|
color: AppTheme.navyBlue,
|
|
),
|
|
),
|
|
centerTitle: false,
|
|
elevation: 0,
|
|
backgroundColor: AppTheme.scaffoldBg,
|
|
shape: const RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.only(
|
|
bottomLeft: Radius.circular(16),
|
|
bottomRight: Radius.circular(16),
|
|
),
|
|
),
|
|
actions: [
|
|
IconButton(
|
|
icon: Icon(Icons.search, color: AppTheme.navyBlue),
|
|
tooltip: 'Search',
|
|
onPressed: () {
|
|
Navigator.of(context).push(
|
|
MaterialPageRoute(builder: (_) => const SearchScreen()),
|
|
);
|
|
},
|
|
),
|
|
IconButton(
|
|
icon: Icon(Icons.chat_bubble_outline, color: AppTheme.navyBlue),
|
|
tooltip: 'Messages',
|
|
onPressed: () {
|
|
Navigator.of(context, rootNavigator: true).push(
|
|
MaterialPageRoute(
|
|
builder: (_) => const SecureChatFullScreen(),
|
|
fullscreenDialog: true,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
IconButton(
|
|
icon: Icon(Icons.notifications_none, color: AppTheme.navyBlue),
|
|
tooltip: 'Notifications',
|
|
onPressed: () {
|
|
showGeneralDialog(
|
|
context: context,
|
|
barrierDismissible: true,
|
|
barrierLabel: 'Notifications',
|
|
barrierColor: Colors.black54,
|
|
transitionDuration: const Duration(milliseconds: 250),
|
|
pageBuilder: (context, _, __) {
|
|
final height = MediaQuery.of(context).size.height;
|
|
return Align(
|
|
alignment: Alignment.topCenter,
|
|
child: SafeArea(
|
|
bottom: false,
|
|
child: ClipRRect(
|
|
borderRadius: const BorderRadius.vertical(
|
|
bottom: Radius.circular(20),
|
|
),
|
|
child: SizedBox(
|
|
height: height * 0.9,
|
|
child: const NotificationsScreen(),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
transitionBuilder: (context, animation, _, child) {
|
|
final curve = CurvedAnimation(
|
|
parent: animation,
|
|
curve: Curves.easeOutCubic,
|
|
);
|
|
return SlideTransition(
|
|
position: Tween<Offset>(
|
|
begin: const Offset(0, -1),
|
|
end: Offset.zero,
|
|
).animate(curve),
|
|
child: child,
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
const SizedBox(width: 4),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildNavBarItem({
|
|
required IconData icon,
|
|
required IconData activeIcon,
|
|
required int index,
|
|
required String label,
|
|
}) {
|
|
final isActive = widget.navigationShell.currentIndex == index;
|
|
return Expanded(
|
|
child: InkWell(
|
|
onTap: () => widget.navigationShell.goBranch(
|
|
index,
|
|
initialLocation: index == widget.navigationShell.currentIndex,
|
|
),
|
|
child: Container(
|
|
height: double.infinity,
|
|
alignment: Alignment.center,
|
|
child: Icon(
|
|
isActive ? activeIcon : icon,
|
|
color: isActive ? AppTheme.navyBlue : Colors.grey,
|
|
size: 26,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Provides the current navigation shell index to descendants that need to
|
|
/// react (e.g. pausing quip playback when the tab is not active).
|
|
class NavigationShellScope extends InheritedWidget {
|
|
final int currentIndex;
|
|
|
|
const NavigationShellScope({
|
|
super.key,
|
|
required this.currentIndex,
|
|
required super.child,
|
|
});
|
|
|
|
static NavigationShellScope? of(BuildContext context) {
|
|
return context.dependOnInheritedWidgetOfExactType<NavigationShellScope>();
|
|
}
|
|
|
|
@override
|
|
bool updateShouldNotify(covariant NavigationShellScope oldWidget) {
|
|
return currentIndex != oldWidget.currentIndex;
|
|
}
|
|
}
|