Files
ai-contract-platform/mobile/lib/app/shell/app_shell.dart
T

260 lines
8.1 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../features/auth/application/auth_controller.dart';
import '../../features/auth/domain/auth_session.dart';
import '../../features/auth/presentation/login_screen.dart';
import '../../features/contracts/presentation/generated_contract_result_screen.dart';
import '../../features/dashboard/presentation/dashboard_screen.dart';
import '../../features/profile/presentation/profile_screen.dart';
import '../../features/review/presentation/review_entry_screen.dart';
import '../../features/review_history/presentation/review_detail_screen.dart';
import '../../features/review_history/presentation/review_history_screen.dart';
import '../../features/contracts/presentation/contract_template_list_screen.dart';
import '../../features/tools/presentation/tools_hub_screen.dart';
import '../../shared/network/api_client.dart';
import '../../shared/notifications/app_notification_action.dart';
import '../../shared/notifications/notification_service.dart';
import '../../shared/storage/session_store.dart';
import '../navigation/app_router.dart';
class AiContractAppShell extends StatefulWidget {
const AiContractAppShell({super.key});
@override
State<AiContractAppShell> createState() => _AiContractAppShellState();
}
class _AiContractAppShellState extends State<AiContractAppShell> with WidgetsBindingObserver {
AppTab _currentTab = AppTab.dashboard;
late final AuthController _authController;
final ApiClient _apiClient = const ApiClient();
String? _activeSessionUserId;
bool _isHandlingNotificationAction = false;
@override
void initState() {
super.initState();
_authController = AuthController();
WidgetsBinding.instance.addObserver(this);
SessionStore.listenable.addListener(_handleSessionChanged);
NotificationService.instance.pendingAction.addListener(_handleNotificationSignal);
unawaited(_syncNotificationLifecycle());
}
@override
void dispose() {
NotificationService.instance.pendingAction.removeListener(_handleNotificationSignal);
SessionStore.listenable.removeListener(_handleSessionChanged);
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
unawaited(_syncNotificationLifecycle(runSyncNow: true));
}
}
void _selectTab(AppTab tab) {
setState(() {
_currentTab = tab;
});
}
void _openTools() {
setState(() {
_currentTab = AppTab.tools;
});
}
Future<void> _openContractWorkspace() async {
await Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const ContractTemplateListScreen(),
),
);
}
void _handleSessionChanged() {
unawaited(_syncNotificationLifecycle());
}
void _handleNotificationSignal() {
unawaited(_tryHandlePendingNotificationAction());
}
Future<void> _syncNotificationLifecycle({bool runSyncNow = false}) async {
final session = SessionStore.current;
if (session == null || !session.isAuthenticated) {
if (_activeSessionUserId != null) {
_activeSessionUserId = null;
await _safeNotificationCall(NotificationService.instance.onSignedOut);
}
return;
}
if (_activeSessionUserId != session.userId) {
_activeSessionUserId = session.userId;
await _safeNotificationCall(NotificationService.instance.onSignedIn);
runSyncNow = true;
}
if (runSyncNow) {
await _safeNotificationCall(NotificationService.instance.syncReviewNotifications);
}
await _tryHandlePendingNotificationAction();
}
Future<void> _tryHandlePendingNotificationAction() async {
if (!mounted || _isHandlingNotificationAction) {
return;
}
final session = SessionStore.current;
final action = NotificationService.instance.pendingAction.value;
if (session == null || !session.isAuthenticated || action == null) {
return;
}
_isHandlingNotificationAction = true;
try {
switch (action.type) {
case AppNotificationActionType.reviewStatus:
final reviewId = action.reviewId;
if (reviewId == null || reviewId.isEmpty) {
break;
}
_selectTab(AppTab.history);
await Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => ReviewDetailScreen(
reviewId: reviewId,
initialTitle: action.reviewTitle?.isNotEmpty == true ? action.reviewTitle! : '审查详情',
),
),
);
break;
case AppNotificationActionType.generatedContract:
final contractId = action.contractId;
if (contractId == null || contractId.isEmpty) {
break;
}
final contract = await _apiClient.getGeneratedContract(
session.accessToken,
contractId,
);
if (!mounted) {
return;
}
await Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => GeneratedContractResultScreen(contract: contract),
),
);
break;
}
NotificationService.instance.clearPendingAction();
} catch (_) {
// Keep the action and allow a later retry.
} finally {
_isHandlingNotificationAction = false;
}
}
Future<T?> _safeNotificationCall<T>(Future<T> Function() action) async {
try {
return await action();
} on MissingPluginException {
return null;
} catch (_) {
return null;
}
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<AuthSession?>(
valueListenable: SessionStore.listenable,
builder: (context, session, _) {
final isAuthenticated = session?.isAuthenticated ?? false;
if (!isAuthenticated) {
return Scaffold(
resizeToAvoidBottomInset: true,
body: SafeArea(
child: LoginScreen(controller: _authController),
),
);
}
return Scaffold(
appBar: AppBar(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(_currentTab.title),
Text(
'${session!.userDisplayName} · ${session.role}',
style: Theme.of(context).textTheme.labelSmall,
),
],
),
),
body: SafeArea(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 240),
child: _buildCurrentScreen(),
),
),
bottomNavigationBar: NavigationBar(
selectedIndex: AppTab.values.indexOf(_currentTab),
onDestinationSelected: (index) {
_selectTab(AppTab.values[index]);
},
destinations: AppTab.values
.map(
(item) => NavigationDestination(
icon: Icon(item.icon),
label: item.label,
),
)
.toList(growable: false),
),
);
},
);
}
Widget _buildCurrentScreen() {
switch (_currentTab) {
case AppTab.dashboard:
return DashboardScreen(
key: const ValueKey(AppTab.dashboard),
onStartReview: () => _selectTab(AppTab.review),
onOpenHistory: () => _selectTab(AppTab.history),
onOpenProfile: () => _selectTab(AppTab.profile),
onOpenLegalChat: _openTools,
onOpenContracts: _openContractWorkspace,
);
case AppTab.review:
return const ReviewEntryScreen(key: ValueKey(AppTab.review));
case AppTab.tools:
return const ToolsHubScreen(key: ValueKey(AppTab.tools));
case AppTab.history:
return const ReviewHistoryScreen(key: ValueKey(AppTab.history));
case AppTab.profile:
return ProfileScreen(
key: const ValueKey(AppTab.profile),
onOpenHistory: () => _selectTab(AppTab.history),
);
}
}
}