260 lines
8.1 KiB
Dart
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),
|
|
);
|
|
}
|
|
}
|
|
}
|