sojorn/sojorn_app/lib/routes/app_routes.dart

258 lines
7.7 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:latlong2/latlong.dart';
import '../services/auth_service.dart';
import '../services/api_service.dart';
import '../screens/admin/admin_dashboard_screen.dart';
import '../screens/admin/admin_scaffold.dart';
import '../screens/admin/admin_user_base_screen.dart';
import '../screens/admin/moderation_queue_screen.dart';
import '../screens/beacon/beacon_screen.dart';
import '../screens/home/feed_personal_screen.dart';
import '../screens/home/home_shell.dart';
import '../screens/quips/create/quip_creation_flow.dart';
import '../screens/quips/feed/quips_feed_screen.dart';
import '../screens/profile/profile_screen.dart';
import '../screens/profile/viewable_profile_screen.dart';
import '../screens/profile/blocked_users_screen.dart';
import '../screens/auth/auth_gate.dart';
import '../screens/discover/discover_screen.dart';
import '../screens/secure_chat/secure_chat_full_screen.dart';
import '../screens/secure_chat/secure_chat_loader_screen.dart';
import '../screens/post/threaded_conversation_screen.dart';
import '../screens/notifications/notifications_screen.dart';
/// App routing config (GoRouter).
class AppRoutes {
static final GlobalKey<NavigatorState> rootNavigatorKey =
GlobalKey<NavigatorState>();
static const String home = '/';
static const String homeAlias = '/home';
static const String userPrefix = '/u';
static const String postPrefix = '/p';
static const String beaconPrefix = '/beacon';
static const String quips = '/quips';
static const String profile = '/profile';
static const String secureChat = '/secure-chat';
static const String quipCreate = '/quips/create';
static final AuthRefreshNotifier _authRefreshNotifier =
AuthRefreshNotifier(AuthService.instance.authStateChanges);
static final GoRouter router = GoRouter(
navigatorKey: rootNavigatorKey,
initialLocation: homeAlias,
refreshListenable: _authRefreshNotifier,
redirect: _adminRedirect,
routes: [
GoRoute(
path: home,
redirect: (_, __) => homeAlias,
),
GoRoute(
path: '$userPrefix/:username',
parentNavigatorKey: rootNavigatorKey,
builder: (_, state) => ViewableProfileScreen(
handle: state.pathParameters['username'] ?? '',
),
),
GoRoute(
path: quipCreate,
parentNavigatorKey: rootNavigatorKey,
builder: (_, __) => const QuipCreationFlow(),
),
GoRoute(
path: secureChat,
parentNavigatorKey: rootNavigatorKey,
builder: (_, __) => const SecureChatFullScreen(),
routes: [
GoRoute(
path: ':id',
parentNavigatorKey: rootNavigatorKey,
builder: (_, state) => SecureChatLoaderScreen(
conversationId: state.pathParameters['id'] ?? '',
),
),
],
),
GoRoute(
path: '$postPrefix/:id',
parentNavigatorKey: rootNavigatorKey,
builder: (_, state) => ThreadedConversationScreen(
rootPostId: state.pathParameters['id'] ?? '',
),
),
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) => AuthGate(
authenticatedChild: HomeShell(navigationShell: navigationShell),
),
branches: [
// Tab 0: Home
StatefulShellBranch(
routes: [
GoRoute(
path: homeAlias,
builder: (_, __) => const FeedPersonalScreen(),
),
],
),
// Tab 1: Search / Discover
StatefulShellBranch(
routes: [
GoRoute(
path: '/discover',
builder: (_, __) => const DiscoverScreen(),
),
],
),
// Tab 2: Activity / Notifications
StatefulShellBranch(
routes: [
GoRoute(
path: '/activity',
builder: (_, __) => const NotificationsScreen(),
),
],
),
// Tab 3: Profile
StatefulShellBranch(
routes: [
GoRoute(
path: profile,
builder: (_, __) => const ProfileScreen(),
routes: [
GoRoute(
path: 'blocked',
builder: (_, __) => const BlockedUsersScreen(),
),
],
),
],
),
],
),
ShellRoute(
builder: (context, state, child) {
final index = _adminIndexForPath(state.uri.path);
return AdminScaffold(selectedIndex: index, child: child);
},
routes: [
GoRoute(
path: '/admin',
builder: (_, __) => const AdminDashboardScreen(),
),
GoRoute(
path: '/admin/moderation',
builder: (_, __) => const ModerationQueueScreen(),
),
GoRoute(
path: '/admin/users',
builder: (_, __) => const AdminUserBaseScreen(),
),
],
),
],
);
static int _adminIndexForPath(String path) {
if (path.startsWith('/admin/moderation')) return 1;
if (path.startsWith('/admin/users')) return 2;
return 0;
}
static FutureOr<String?> _adminRedirect(
BuildContext context,
GoRouterState state,
) async {
final path = state.uri.path;
if (!path.startsWith('/admin')) return null;
final user = AuthService.instance.currentUser;
if (user == null) return homeAlias;
try {
final data = await ApiService.instance.callGoApi('/profile', method: 'GET');
final profile = data['profile'];
if (profile is Map<String, dynamic>) {
final role = profile['role'] as String?;
if (role == 'admin' || role == 'moderator') {
return null;
}
}
} catch (_) {}
return homeAlias;
}
/// Navigate to a user profile by username
static void navigateToProfile(BuildContext context, String username) {
context.push('/u/$username');
}
/// Get shareable URL for a user profile
/// Returns: https://sojorn.net/u/username
static String getProfileUrl(
String username, {
String baseUrl = 'https://sojorn.net',
}) {
return '$baseUrl/u/$username';
}
/// Get shareable URL for a quip
/// Returns: https://sojorn.net/quips?postId=postid
static String getQuipUrl(
String postId, {
String baseUrl = 'https://sojorn.net',
}) {
return '$baseUrl/quips?postId=$postId';
}
/// Get shareable URL for a post
/// Returns: https://sojorn.net/p/postid
static String getPostUrl(
String postId, {
String baseUrl = 'https://sojorn.net',
}) {
return '$baseUrl/p/$postId';
}
/// Get shareable URL for a beacon location
/// Returns: https://sojorn.net/beacon?lat=...&long=...
static String getBeaconUrl(
double lat,
double long, {
String baseUrl = 'https://sojorn.net',
}) {
return '$baseUrl/beacon?lat=${lat.toStringAsFixed(6)}&long=${long.toStringAsFixed(6)}';
}
/// Navigate to a beacon location
static void navigateToBeacon(BuildContext context, LatLng location) {
final url =
'/beacon?lat=${location.latitude.toStringAsFixed(6)}&long=${location.longitude.toStringAsFixed(6)}';
context.push(url);
}
/// Navigate to secure chat
static void navigateToSecureChat(BuildContext context) {
context.push(secureChat);
}
}
class AuthRefreshNotifier extends ChangeNotifier {
late final StreamSubscription<AuthState> _subscription;
AuthRefreshNotifier(Stream<AuthState> stream) {
_subscription = stream.listen((_) => notifyListeners());
}
@override
void dispose() {
_subscription.cancel();
super.dispose();
}
}