Notification parsing: - Per-app parse rules (parse_rules.packageName; getEnabledForApp, NULL = legacy global) - Transfer pairing: transfer_pair_matcher + transfer_pairing_blocklist table/dao/repo - source_apps.selfMerchant flag (Ozon inbox rework: default account picker, suppress AI category prefill + rule suggestion) - raw_messages.diagnostics dump captured under diagnostic-mode toggle - Inbox card / settings / log UI reworks Onboarding: - Two-step flow (name -> first account); UserSeeder seeds categories only, no accounts Schema bumped to v11; drop obsolete migration + mixed-merchant tests, add new coverage. Add ios/ platform folder. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
225 lines
8.0 KiB
Dart
225 lines
8.0 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
|
|
import '../../core/database/converters/enum_converters.dart';
|
|
import '../../features/accounts/presentation/screens/account_form_screen.dart';
|
|
import '../../features/accounts/presentation/screens/accounts_screen.dart';
|
|
import '../../features/analytics/presentation/screens/analytics_screen.dart';
|
|
import '../../features/analytics/presentation/screens/habit_analysis_screen.dart';
|
|
import '../../features/categories/presentation/screens/categories_list_screen.dart';
|
|
import '../../features/categories/presentation/screens/category_form_screen.dart';
|
|
import '../../features/home/presentation/screens/home_screen.dart';
|
|
import '../../features/notification_parsing/presentation/screens/ai_consent_screen.dart';
|
|
import '../../features/notification_parsing/presentation/screens/app_bindings_screen.dart';
|
|
import '../../features/notification_parsing/presentation/screens/debug_inject_screen.dart';
|
|
import '../../features/notification_parsing/presentation/screens/inbox_screen.dart';
|
|
import '../../features/notification_parsing/presentation/screens/parsing_log_screen.dart';
|
|
import '../../features/notification_parsing/presentation/screens/parsing_settings_screen.dart';
|
|
import '../../features/notification_parsing/presentation/screens/rule_editor_screen.dart';
|
|
import '../../features/notification_parsing/presentation/screens/rules_list_screen.dart';
|
|
import '../../features/notification_parsing/presentation/screens/source_apps_screen.dart';
|
|
import '../../features/profile/presentation/screens/profile_screen.dart';
|
|
import '../../features/transactions/presentation/screens/transaction_form_screen.dart';
|
|
import '../../features/user/application/active_user_controller.dart';
|
|
import '../../features/user/presentation/screens/onboarding_screen.dart';
|
|
import '../../shared/widgets/app_scaffold.dart';
|
|
import 'app_routes.dart';
|
|
|
|
part 'app_router.g.dart';
|
|
|
|
@Riverpod(keepAlive: true)
|
|
GoRouter appRouter(Ref ref) {
|
|
// ValueNotifier, который дёргается каждый раз, когда меняется состояние
|
|
// активного пользователя — это сигнал для GoRouter пересчитать redirect.
|
|
final refresh = ValueNotifier<int>(0);
|
|
ref.listen<AsyncValue<Object?>>(
|
|
activeUserControllerProvider,
|
|
(_, _) => refresh.value++,
|
|
);
|
|
ref.onDispose(refresh.dispose);
|
|
|
|
return GoRouter(
|
|
initialLocation: AppRoutes.home,
|
|
refreshListenable: refresh,
|
|
redirect: (context, state) {
|
|
final active = ref.read(activeUserControllerProvider);
|
|
// Пока активный пользователь загружается из БД — никуда не уводим.
|
|
if (active.isLoading) return null;
|
|
final hasUser = active.value != null;
|
|
final atOnboarding = state.matchedLocation == AppRoutes.onboarding;
|
|
if (!hasUser) return atOnboarding ? null : AppRoutes.onboarding;
|
|
if (atOnboarding) return AppRoutes.home;
|
|
return null;
|
|
},
|
|
routes: [
|
|
GoRoute(
|
|
path: AppRoutes.onboarding,
|
|
builder: (context, state) => const OnboardingScreen(),
|
|
),
|
|
GoRoute(
|
|
path: AppRoutes.transactionNew,
|
|
pageBuilder: (context, state) => _slideUpPage<String?>(
|
|
state,
|
|
TransactionFormScreen(
|
|
prefill: state.extra as TransactionFormPrefill?,
|
|
),
|
|
),
|
|
),
|
|
GoRoute(
|
|
path: AppRoutes.transactionEditPattern,
|
|
pageBuilder: (context, state) => _slideUpPage<void>(
|
|
state,
|
|
TransactionFormScreen(txId: state.pathParameters['id']),
|
|
),
|
|
),
|
|
GoRoute(
|
|
path: AppRoutes.categoriesList,
|
|
pageBuilder: (context, state) => _slideUpPage<void>(
|
|
state,
|
|
const CategoriesListScreen(),
|
|
),
|
|
),
|
|
GoRoute(
|
|
path: AppRoutes.categoryNew,
|
|
pageBuilder: (context, state) => _slideUpPage<String?>(
|
|
state,
|
|
CategoryFormScreen(presetType: state.extra as CategoryType?),
|
|
),
|
|
),
|
|
GoRoute(
|
|
path: AppRoutes.categoryEditPattern,
|
|
pageBuilder: (context, state) => _slideUpPage<String?>(
|
|
state,
|
|
CategoryFormScreen(categoryId: state.pathParameters['id']),
|
|
),
|
|
),
|
|
GoRoute(
|
|
path: AppRoutes.accountNew,
|
|
pageBuilder: (context, state) => _slideUpPage<String?>(
|
|
state,
|
|
const AccountFormScreen(),
|
|
),
|
|
),
|
|
GoRoute(
|
|
path: AppRoutes.accountEditPattern,
|
|
pageBuilder: (context, state) => _slideUpPage<String?>(
|
|
state,
|
|
AccountFormScreen(accountId: state.pathParameters['id']),
|
|
),
|
|
),
|
|
GoRoute(
|
|
path: AppRoutes.inbox,
|
|
pageBuilder: (context, state) =>
|
|
_slideUpPage<void>(state, const InboxScreen()),
|
|
),
|
|
GoRoute(
|
|
path: AppRoutes.parsingSettings,
|
|
builder: (context, state) => const ParsingSettingsScreen(),
|
|
),
|
|
GoRoute(
|
|
path: AppRoutes.parsingAi,
|
|
builder: (context, state) => const AiConsentScreen(),
|
|
),
|
|
GoRoute(
|
|
path: AppRoutes.parsingRules,
|
|
builder: (context, state) => const RulesListScreen(),
|
|
),
|
|
// 'new' должен идти раньше ':id', иначе перехватится как id.
|
|
GoRoute(
|
|
path: AppRoutes.parsingRuleNew,
|
|
builder: (context, state) =>
|
|
RuleEditorScreen(prefill: state.extra as RuleEditorPrefill?),
|
|
),
|
|
GoRoute(
|
|
path: AppRoutes.parsingRuleEditPattern,
|
|
builder: (context, state) =>
|
|
RuleEditorScreen(ruleId: state.pathParameters['id']),
|
|
),
|
|
GoRoute(
|
|
path: AppRoutes.parsingDebugInject,
|
|
builder: (context, state) => const DebugInjectScreen(),
|
|
),
|
|
GoRoute(
|
|
path: AppRoutes.parsingLog,
|
|
builder: (context, state) => const ParsingLogScreen(),
|
|
),
|
|
GoRoute(
|
|
path: AppRoutes.parsingApps,
|
|
builder: (context, state) => const SourceAppsScreen(),
|
|
),
|
|
GoRoute(
|
|
path: AppRoutes.parsingAppBindingsPattern,
|
|
builder: (context, state) => AppBindingsScreen(
|
|
packageName: Uri.decodeComponent(state.pathParameters['pkg']!),
|
|
),
|
|
),
|
|
GoRoute(
|
|
path: AppRoutes.habitAnalysis,
|
|
pageBuilder: (context, state) =>
|
|
_slideUpPage<void>(state, const HabitAnalysisScreen()),
|
|
),
|
|
StatefulShellRoute.indexedStack(
|
|
builder: (context, state, navigationShell) => AppScaffold(
|
|
navigationShell: navigationShell,
|
|
),
|
|
branches: [
|
|
StatefulShellBranch(
|
|
routes: [
|
|
GoRoute(
|
|
path: AppRoutes.home,
|
|
builder: (context, state) => const HomeScreen(),
|
|
),
|
|
],
|
|
),
|
|
StatefulShellBranch(
|
|
routes: [
|
|
GoRoute(
|
|
path: AppRoutes.analytics,
|
|
builder: (context, state) => const AnalyticsScreen(),
|
|
),
|
|
],
|
|
),
|
|
StatefulShellBranch(
|
|
routes: [
|
|
GoRoute(
|
|
path: AppRoutes.accounts,
|
|
builder: (context, state) => const AccountsScreen(),
|
|
),
|
|
],
|
|
),
|
|
StatefulShellBranch(
|
|
routes: [
|
|
GoRoute(
|
|
path: AppRoutes.profile,
|
|
builder: (context, state) => const ProfileScreen(),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Page<T> _slideUpPage<T>(GoRouterState state, Widget child) {
|
|
return CustomTransitionPage<T>(
|
|
key: state.pageKey,
|
|
child: child,
|
|
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
|
final curved = CurvedAnimation(
|
|
parent: animation,
|
|
curve: Curves.easeOutCubic,
|
|
reverseCurve: Curves.easeInCubic,
|
|
);
|
|
return SlideTransition(
|
|
position: Tween<Offset>(
|
|
begin: const Offset(0, 1),
|
|
end: Offset.zero,
|
|
).animate(curved),
|
|
child: child,
|
|
);
|
|
},
|
|
);
|
|
}
|