Add main screen

This commit is contained in:
2026-05-27 10:10:38 +03:00
parent 04d1ad6629
commit 456986e79e
34 changed files with 3174 additions and 151 deletions
+342
View File
@@ -0,0 +1,342 @@
# Реализация UI: главный экран (V1) и каркас навигации
## Context
`PLAN.md` описывает каркас приложения учёта личных финансов на Flutter; data/domain/application
слои уже разложены по фичам, но кодогенерация (`*.g.dart`, `*.freezed.dart`) ещё не запускалась,
а `lib/main.dart` — это дефолтный counter-шаблон Flutter. UI отсутствует.
Пользователь просит начать реализацию интерфейса по дизайну `design/index.html`. В HTML рендерится
**только вариант V1** (`design/variants.jsx:82``function V1()`) — главный экран:
- хедер «Бюджет / Май 2026» + иконки поиска и колокольчика
- горизонтальные таб-пиллы счетов (`design/common.jsx:157``ACCOUNTS`)
- карточка KPI: баланс + доходы/расходы
- donut-диаграмма расходов по категориям + легенда (`design/common.jsx:76``Donut`)
- пилл-триггер фильтра по категории
- список транзакций, сгруппированный по дням (`design/common.jsx:179``TxRow`)
- FAB (`design/common.jsx:262`) и bottom-nav из 4 вкладок (`design/common.jsx:230``BottomNav`)
- две темы; в `design/index.html:74` дефолт `theme: dark`
Уточнено пользователем: **только V1** (с навигационным каркасом для остальных вкладок),
**mock-данные в presentation**, **обе темы** с переключением в Профиле.
Цель — собрать запускаемое приложение, где главный экран визуально соответствует V1, а добавление
реальных данных потом сведётся к замене mock-источника на Riverpod-контроллеры из `application/`.
## Подход
Mock-данные складываются в `lib/src/features/home/presentation/_mock_data.dart` и типизируются
**существующими доменными сущностями** (`Account`, `Transaction`, `Category`) — так UI с самого
начала работает с теми же типами, на которые потом будут переключены реальные репозитории. Это
требует, чтобы скелет компилировался, поэтому перед запуском нужно прогнать `build_runner`.
UI делится на маленькие виджеты-композиты, каждый получает данные через конструктор (никакого
обращения к `ref` внутри презентационных виджетов, кроме экранов-контейнеров и темы) — это держит
их близкими к тому, чем будут render-функции вроде `KPI`/`TxRow` из `common.jsx`.
## План работ
### 1. Зависимости и кодогенерация
`pubspec.yaml`: добавить
- `google_fonts: ^6.2.1` — для DM Sans / JetBrains Mono (минимум кода, без bundle .ttf)
- `fl_chart: ^0.69.0` — donut-диаграмма категорий через `PieChart`, без рисования вручную
Запустить:
- `flutter pub get`
- `dart run build_runner build --delete-conflicting-outputs` — генерирует `*.g.dart` и `*.freezed.dart`
для всех файлов с `part`-директивами. Без этого скелет (`Account`, `Transaction`, `@riverpod`) не
компилируется и UI, типизированный доменными сущностями, не соберётся.
### 2. Дизайн-токены
Создать `lib/src/app/theme/app_colors.dart` — мапнуть CSS-переменные из `design/index.html:13-41`
в `Color`:
| CSS-токен | Light | Dark | Назначение |
|----------------|-------------|-------------|---------------------------|
| `--paper` | `#f6f4ef` | `#19191a` | основной фон |
| `--paper-2` | `#efece5` | `#232325` | поднятые поверхности |
| `--card-soft` | `#edeae3` | `#232325` | мягкие чипы |
| `--ink` | `#1c1c1a` | `#ece9e2` | основной текст |
| `--ink-2` | `#6b6b66` | `#8d8a83` | вторичный текст |
| `--line` | `#d8d5cc` | `#2e2d2a` | границы |
| `--line-2` | `#b8b5ac` | `#4a4845` | рамка устройства |
| `--accent` | `#4a8a82` | `#76b3a9` | FAB, активная вкладка |
| `--accent-soft`| `#dde9e6` | `#23332f` | пастель акцента |
| `--pos` | `#6f8c69` | `#92b58a` | доходы |
| `--neg` | `#b3675a` | `#d18d7e` | расходы |
Класс `AppPalette` — immutable, два экземпляра `light`/`dark`. Доступ через
`ThemeExtension<AppPalette>``Theme.of(context).extension<AppPalette>()!`. Это идиоматичнее, чем
глобальные синглтоны, и автоматически переключается с темой.
`lib/src/app/theme/app_theme.dart``ThemeData light()` / `dark()`:
- `useMaterial3: true`
- `colorScheme` через `ColorScheme.fromSeed(seedColor: accent, brightness: ...)`, затем `copyWith`
для `surface`, `onSurface`, `outline` — чтобы материаловские виджеты (Material, Card, Divider)
на дефолте уже использовали наши токены
- `textTheme: GoogleFonts.dmSansTextTheme(...)`
- `extensions: [AppPalette.light / .dark]`
- семейство для числовых стилей — отдельный публичный helper `Text monoText(...)`, который
применяет `GoogleFonts.jetBrainsMono(fontFeatures: [tabular-nums])`. Использовать там, где в
дизайне `fontFamily: 'JetBrains Mono'` (балансы, суммы транзакций).
### 3. Контроллер темы
`lib/src/app/theme/theme_mode_controller.dart`:
```dart
@riverpod
class ThemeModeController extends _$ThemeModeController {
@override
ThemeMode build() => ThemeMode.dark; // дефолт совпадает с design/index.html
void set(ThemeMode mode) => state = mode;
void toggle() => state = state == ThemeMode.dark ? ThemeMode.light : ThemeMode.dark;
}
```
Простой `@riverpod` Notifier; персистентность не требуется в этой итерации (отмечено в
«Будущие шаги»). Использовать через `ref.watch(themeModeControllerProvider)` в `App`.
### 4. Навигация
`lib/src/app/router/app_routes.dart` — константы:
```dart
class AppRoutes {
static const home = '/home';
static const analytics = '/analytics';
static const accounts = '/accounts';
static const profile = '/profile';
}
```
`lib/src/app/router/app_router.dart``@riverpod` провайдер `GoRouter`:
- корневая `StatefulShellRoute.indexedStack` с 4 ветками (home, analytics, accounts, profile);
каждая ветка — обычный `GoRoute` без вложенности
- `initialLocation: AppRoutes.home`
- shell-builder возвращает `AppScaffold` (см. ниже) — он рисует bottom nav и индекс активной вкладки
### 5. Каркасный Scaffold с bottom nav
`lib/src/shared/widgets/app_scaffold.dart`:
- получает `StatefulNavigationShell`, рендерит `Scaffold(body: shell, bottomNavigationBar: AppBottomNav(...))`
- `AppBottomNav` — собственная реализация, не `NavigationBar`, чтобы повторить визуал из
`design/common.jsx:230` (иконка в пилюле с `--accent-soft` фоном для активной)
- 4 пункта: Главная (Icons.home_outlined), Аналитика (Icons.bar_chart_outlined),
Счета (Icons.account_balance_wallet_outlined), Профиль (Icons.person_outline) —
иконки берём из Material, иконки дизайна (`wallet`, `stats`...) близки к материаловским аналогам
### 6. Главный экран (V1)
`lib/src/features/home/presentation/screens/home_screen.dart``ConsumerWidget`, который
читает mock-провайдеры и собирает композицию. Структура `build`:
```
Scaffold(
body: Stack(
children: [
CustomScrollView(slivers: [
SliverToBoxAdapter(MonthHeader)
SliverToBoxAdapter(AccountTabs)
SliverToBoxAdapter(MonthKpiCard)
SliverToBoxAdapter(CategoryDonutCard)
SliverToBoxAdapter(TransactionsSectionHeader)
SliverToBoxAdapter(CategoryFilterPill)
SliverList(... DayHeader + TxRow ...)
SliverPadding(80px)
])
Positioned(FabAddTransaction, bottom: 16, right: 16)
]
)
)
```
Виджеты, по одному на файл, под `lib/src/features/home/presentation/widgets/`:
| Виджет | Соответствие в variants.jsx |
|-----------------------------|---------------------------------------|
| `MonthHeader` | строки 108-116 |
| `AccountTabs` | строки 119-133 |
| `MonthKpiCard` | строки 136-165 |
| `CategoryDonutCard` | строки 167-205 |
| `CategoryDonut` (CustomPainter) | `design/common.jsx:76``Donut` |
| `TransactionsSectionHeader` | строки 207-211 |
| `CategoryFilterPill` | строки 213-239 |
| `DayHeader` | `design/common.jsx:215``DayHeader` |
| `TxRow` | `design/common.jsx:179``TxRow` |
| `FabAddTransaction` | `design/common.jsx:262``FAB` |
Поведение фильтра (выбор сегмента donut фильтрует список транзакций) — состояние храним в
локальном `StateProvider`/`Notifier` внутри `home` фичи: `selected_category_filter.dart`
`@riverpod` `int? selectedCategoryFilter(ref)` либо просто `StateProvider<int?>`.
Группировка по дням — в `home_screen.dart` через `Map<DateTime, List<Transaction>>` (ключ — дата без
времени). Заголовок дня — «Сегодня», «Вчера» или `dd MMM` через `intl`.
### 7. Donut-диаграмма (через `fl_chart`)
`lib/src/features/home/presentation/widgets/category_donut.dart`:
- обёртка над `PieChart` из `fl_chart`:
```dart
PieChart(PieChartData(
sections: [
for (final s in spend) PieChartSectionData(
value: s.amount.toDouble(),
color: s.color,
radius: isActive(s) ? 26 : 22, // визуальный аналог rOff=4
showTitle: false,
),
],
centerSpaceRadius: 44, // совпадает с `thickness: 22` при size: 130
sectionsSpace: 0,
pieTouchData: PieTouchData(
touchCallback: (event, response) {
if (event is FlTapUpEvent) {
final idx = response?.touchedSection?.touchedSectionIndex;
onSegment?.call(idx);
}
},
),
))
```
- центральный текст («Расходы / 67 200 ₽») рисуем поверх через `Stack` — `PieChart` сам центр не
занимает (это и есть «дырка» donut'а)
- активный сегмент получает увеличенный `radius`; для не-активных, когда выбран другой, можно
понизить `opacity` через `color.withOpacity(0.4)` — равнозначно `opacity: 0.35` из референса
- получаем встроенный hit-test по сегментам — никакого ручного расчёта угла
### 8. Mock-данные
`lib/src/features/home/presentation/_mock_data.dart`:
- `mockUserId = 1`
- `mockAccounts: List<Account>` — 4 счёта из `design/common.jsx:157` (all / card / cash / pig),
с балансами в **минорных единицах** (умножить на 100): `184_320` → `18_432_000`. Поле `type`
мапим в `AccountType`; `iconCode`/`colorValue` — `null`, иконку выбираем в `AccountTabs` по
`type`. Виртуальный «Все счета» — отдельная константа `kAllAccountsId = 0` (т.к. это не реальный
счёт, а агрегат); в UI он рендерится первой пиллой и фильтрует список «все».
- `mockCategories: List<Category>` — 6 категорий (food/rent/transp/cafe/enter/other),
`type: CategoryType.expense`, `colorValue`/`iconCode` хранят hex и иконку из дизайна
- `mockTransactions: List<Transaction>` — 10 строк из `design/common.jsx:165-176`, амаунты в
минорных единицах со знаком, `date` — `DateTime.now()` сдвинутый на нужное кол-во дней назад
- `mockCategorySpend: Map<int, int>` — суммы по категории для donut'а (`SPEND` из `common.jsx:142`)
Простые провайдеры:
```dart
@riverpod
List<Account> mockAccounts(...) => _mockAccountsList;
@riverpod
List<Transaction> mockTransactions(...) => _mockTransactionsList;
// и т.д.
```
Так замена на реальные данные потом — это **переименование провайдеров** в виджетах с
`mockAccountsProvider` на `accountsStreamProvider(userId)` (уже существует в скелете) — UI не
меняется.
### 9. Палитра категорий и иконок
В дизайне категории имеют hex-цвета и кастомные SVG. В Flutter:
- цвет хранится прямо в mock-данных в `colorValue` (`int` уже есть в `Category`)
- иконку выбираем через статическую мапу `categoryIconFor(Category c)` в
`lib/src/features/categories/presentation/category_icon.dart` — ключ это `iconCode` или
имя категории; значение — `IconData` из `Icons` (cart → `shopping_cart_outlined`,
house → `home_outlined`, car → `directions_car_outlined`, food → `restaurant_outlined`,
film → `movie_outlined`, more → `more_horiz`). Дешевле SVG, ближе к материаловскому стилю.
### 10. Экраны-заглушки остальных вкладок
`lib/src/features/{analytics,accounts_screen,profile}/presentation/screens/*.dart` —
`AnalyticsScreen`, `AccountsScreen`, `ProfileScreen`. Каждый — `Scaffold` с заголовком в
стиле V1 хедера и `Center(child: Text('Скоро'))` либо `EmptyState` виджетом. `ProfileScreen`
содержит **рабочий** `SwitchListTile` темы (читает/пишет `themeModeControllerProvider`) —
это и есть единственный нон-плейсхолдер вне главного экрана.
`AnalyticsScreen` и `AccountsScreen` создаём в существующих фичах
(`features/categories` не подходит — добавим `features/analytics/presentation/` —
presentation-only). `AccountsScreen` логично положить в `features/accounts/presentation/screens/`.
### 11. Точка входа
`lib/main.dart` — переписать:
```dart
void main() {
runApp(const ProviderScope(child: NewBudgetApp()));
}
```
`lib/src/app/app.dart` — `NewBudgetApp` (`ConsumerWidget`):
- читает `themeModeControllerProvider` и `appRouterProvider`
- возвращает `MaterialApp.router(theme: AppTheme.light(), darkTheme: AppTheme.dark(),
themeMode: ..., routerConfig: ...)`
### 12. Удалить мусор
- `test/widget_test.dart` — ссылается на `MyApp` из counter-шаблона, удалить или заменить
smoke-тестом, который только инициализирует `NewBudgetApp` под `ProviderScope`.
## Файлы
**Создать (presentation/app):**
- `lib/src/app/app.dart`
- `lib/src/app/theme/app_colors.dart`
- `lib/src/app/theme/app_theme.dart`
- `lib/src/app/theme/theme_mode_controller.dart`
- `lib/src/app/router/app_routes.dart`
- `lib/src/app/router/app_router.dart`
- `lib/src/shared/widgets/app_scaffold.dart`
- `lib/src/shared/widgets/app_bottom_nav.dart`
- `lib/src/shared/text/mono_text.dart` (helper для JetBrains Mono)
**Создать (home feature):**
- `lib/src/features/home/presentation/screens/home_screen.dart`
- `lib/src/features/home/presentation/widgets/month_header.dart`
- `lib/src/features/home/presentation/widgets/account_tabs.dart`
- `lib/src/features/home/presentation/widgets/month_kpi_card.dart`
- `lib/src/features/home/presentation/widgets/category_donut_card.dart`
- `lib/src/features/home/presentation/widgets/category_donut.dart`
- `lib/src/features/home/presentation/widgets/transactions_section.dart` (header + filter pill)
- `lib/src/features/home/presentation/widgets/day_header.dart`
- `lib/src/features/home/presentation/widgets/tx_row.dart`
- `lib/src/features/home/presentation/widgets/fab_add_transaction.dart`
- `lib/src/features/home/presentation/state/selected_category_filter.dart`
- `lib/src/features/home/presentation/_mock_data.dart`
**Создать (другие вкладки):**
- `lib/src/features/analytics/presentation/screens/analytics_screen.dart`
- `lib/src/features/accounts/presentation/screens/accounts_screen.dart`
- `lib/src/features/profile/presentation/screens/profile_screen.dart`
- `lib/src/features/categories/presentation/category_icon.dart`
**Изменить:**
- `lib/main.dart` — runApp + ProviderScope
- `pubspec.yaml` — добавить `google_fonts`
**Удалить/заменить:**
- `test/widget_test.dart` — заменить на smoke-тест приложения
## Сборка и проверка
1. `flutter pub get`
2. `dart run build_runner build --delete-conflicting-outputs` — без ошибок
3. `flutter analyze` — без ошибок (исключения для `*.g.dart`/`*.freezed.dart` уже настроены в
`analysis_options.yaml`)
4. `flutter run` на Android-эмуляторе:
- открывается главный экран в тёмной теме
- визуально совпадает с V1 в `design/index.html` (структура, отступы, цвета, шрифты)
- таб-пиллы счетов переключаются (хайлайт активного)
- тап по сегменту donut'а / по pill-фильтру меняет список транзакций
- bottom-nav переключает между 4 вкладками; состояние главного экрана сохраняется
(благодаря `StatefulShellRoute`)
- в Профиле есть переключатель темы; нажатие действительно меняет палитру всего приложения
- FAB виден над списком, тап — пока no-op (TODO: открыть экран добавления транзакции)
## Будущие шаги (вне этой итерации)
- Заменить mock-провайдеры на стримы из `application/` (`accountsStreamProvider`,
`transactionsStreamProvider` и т.д.) — UI остаётся прежним.
- Персистентность `ThemeMode` через `SettingsRepository` (`SettingsEntity.themeMode` уже
существует).
- Точный hit-test сегментов donut'а (по углу относительно центра) для тапа.
- Экран добавления/редактирования транзакции (FAB-таргет).
- Реализация Аналитики и Счетов поверх существующих DAO.
+7 -118
View File
@@ -1,122 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/date_symbol_data_local.dart';
void main() {
runApp(const MyApp());
}
import 'src/app/app.dart';
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// TRY THIS: Try running your application with "flutter run". You'll see
// the application has a purple toolbar. Then, without quitting the app,
// try changing the seedColor in the colorScheme below to Colors.green
// and then invoke "hot reload" (save your changes or press the "hot
// reload" button in a Flutter-supported IDE, or press "r" if you used
// the command line to start the app).
//
// Notice that the counter didn't reset back to zero; the application
// state is not lost during the reload. To reset the state, use hot
// restart instead.
//
// This works for code too, not just values: Most code changes can be
// tested with just a hot reload.
colorScheme: .fromSeed(seedColor: Colors.deepPurple),
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// TRY THIS: Try changing the color here to a specific color (to
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
// change color while the other colors stay the same.
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
//
// TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
// action in the IDE, or press "p" in the console), to see the
// wireframe for each widget.
mainAxisAlignment: .center,
children: [
const Text('You have pushed the button this many times:'),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await initializeDateFormatting('ru');
runApp(const ProviderScope(child: NewBudgetApp()));
}
+32
View File
@@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'router/app_router.dart';
import 'theme/app_theme.dart';
import 'theme/theme_mode_controller.dart';
class NewBudgetApp extends ConsumerWidget {
const NewBudgetApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final router = ref.watch(appRouterProvider);
final themeMode = ref.watch(themeModeControllerProvider);
return MaterialApp.router(
title: 'NewBudget',
debugShowCheckedModeBanner: false,
theme: AppTheme.light(),
darkTheme: AppTheme.dark(),
themeMode: themeMode,
routerConfig: router,
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('ru'), Locale('en')],
);
}
}
+60
View File
@@ -0,0 +1,60 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../features/accounts/presentation/screens/accounts_screen.dart';
import '../../features/analytics/presentation/screens/analytics_screen.dart';
import '../../features/home/presentation/screens/home_screen.dart';
import '../../features/profile/presentation/screens/profile_screen.dart';
import '../../shared/widgets/app_scaffold.dart';
import 'app_routes.dart';
part 'app_router.g.dart';
@Riverpod(keepAlive: true)
GoRouter appRouter(Ref ref) {
return GoRouter(
initialLocation: AppRoutes.home,
routes: [
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(),
),
],
),
],
),
],
);
}
+8
View File
@@ -0,0 +1,8 @@
class AppRoutes {
AppRoutes._();
static const home = '/home';
static const analytics = '/analytics';
static const accounts = '/accounts';
static const profile = '/profile';
}
+109
View File
@@ -0,0 +1,109 @@
import 'package:flutter/material.dart';
@immutable
class AppPalette extends ThemeExtension<AppPalette> {
const AppPalette({
required this.paper,
required this.paper2,
required this.cardSoft,
required this.ink,
required this.ink2,
required this.line,
required this.line2,
required this.accent,
required this.accentSoft,
required this.positive,
required this.negative,
});
final Color paper;
final Color paper2;
final Color cardSoft;
final Color ink;
final Color ink2;
final Color line;
final Color line2;
final Color accent;
final Color accentSoft;
final Color positive;
final Color negative;
static const light = AppPalette(
paper: Color(0xFFF6F4EF),
paper2: Color(0xFFEFECE5),
cardSoft: Color(0xFFEDEAE3),
ink: Color(0xFF1C1C1A),
ink2: Color(0xFF6B6B66),
line: Color(0xFFD8D5CC),
line2: Color(0xFFB8B5AC),
accent: Color(0xFF4A8A82),
accentSoft: Color(0xFFDDE9E6),
positive: Color(0xFF6F8C69),
negative: Color(0xFFB3675A),
);
static const dark = AppPalette(
paper: Color(0xFF19191A),
paper2: Color(0xFF232325),
cardSoft: Color(0xFF232325),
ink: Color(0xFFECE9E2),
ink2: Color(0xFF8D8A83),
line: Color(0xFF2E2D2A),
line2: Color(0xFF4A4845),
accent: Color(0xFF76B3A9),
accentSoft: Color(0xFF23332F),
positive: Color(0xFF92B58A),
negative: Color(0xFFD18D7E),
);
@override
AppPalette copyWith({
Color? paper,
Color? paper2,
Color? cardSoft,
Color? ink,
Color? ink2,
Color? line,
Color? line2,
Color? accent,
Color? accentSoft,
Color? positive,
Color? negative,
}) {
return AppPalette(
paper: paper ?? this.paper,
paper2: paper2 ?? this.paper2,
cardSoft: cardSoft ?? this.cardSoft,
ink: ink ?? this.ink,
ink2: ink2 ?? this.ink2,
line: line ?? this.line,
line2: line2 ?? this.line2,
accent: accent ?? this.accent,
accentSoft: accentSoft ?? this.accentSoft,
positive: positive ?? this.positive,
negative: negative ?? this.negative,
);
}
@override
AppPalette lerp(ThemeExtension<AppPalette>? other, double t) {
if (other is! AppPalette) return this;
return AppPalette(
paper: Color.lerp(paper, other.paper, t)!,
paper2: Color.lerp(paper2, other.paper2, t)!,
cardSoft: Color.lerp(cardSoft, other.cardSoft, t)!,
ink: Color.lerp(ink, other.ink, t)!,
ink2: Color.lerp(ink2, other.ink2, t)!,
line: Color.lerp(line, other.line, t)!,
line2: Color.lerp(line2, other.line2, t)!,
accent: Color.lerp(accent, other.accent, t)!,
accentSoft: Color.lerp(accentSoft, other.accentSoft, t)!,
positive: Color.lerp(positive, other.positive, t)!,
negative: Color.lerp(negative, other.negative, t)!,
);
}
}
extension AppPaletteX on BuildContext {
AppPalette get palette => Theme.of(this).extension<AppPalette>()!;
}
+54
View File
@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'app_colors.dart';
class AppTheme {
const AppTheme._();
static ThemeData light() => _build(AppPalette.light, Brightness.light);
static ThemeData dark() => _build(AppPalette.dark, Brightness.dark);
static ThemeData _build(AppPalette p, Brightness brightness) {
final base = ThemeData(
useMaterial3: true,
brightness: brightness,
colorScheme: ColorScheme.fromSeed(
seedColor: p.accent,
brightness: brightness,
).copyWith(
surface: p.paper,
onSurface: p.ink,
surfaceContainerHighest: p.paper2,
outline: p.line,
outlineVariant: p.line,
),
scaffoldBackgroundColor: p.paper,
dividerColor: p.line,
extensions: [p],
);
return base.copyWith(
textTheme: GoogleFonts.dmSansTextTheme(base.textTheme).apply(
bodyColor: p.ink,
displayColor: p.ink,
),
);
}
}
/// Helper for tabular-numbers monospace text (balances, amounts).
TextStyle monoStyle({
required Color color,
double fontSize = 14,
FontWeight fontWeight = FontWeight.w500,
double letterSpacing = 0,
}) {
return GoogleFonts.jetBrainsMono(
color: color,
fontSize: fontSize,
fontWeight: fontWeight,
letterSpacing: letterSpacing,
fontFeatures: const [FontFeature.tabularFigures()],
);
}
@@ -0,0 +1,18 @@
import 'package:flutter/material.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'theme_mode_controller.g.dart';
/// Активный [ThemeMode] приложения. Дефолт — `dark`, чтобы совпадать с
/// `design/index.html` (TWEAK_DEFAULTS.theme = 'dark').
@Riverpod(keepAlive: true)
class ThemeModeController extends _$ThemeModeController {
@override
ThemeMode build() => ThemeMode.dark;
void set(ThemeMode mode) => state = mode;
void toggle() {
state = state == ThemeMode.dark ? ThemeMode.light : ThemeMode.dark;
}
}
@@ -1,5 +1,5 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../../../core/database/converters/enum_converters.dart';
import '../../../core/database/converters/enum_converters.dart';
import '../domain/entities/account.dart';
import 'account_providers.dart';
@@ -0,0 +1,15 @@
import 'package:flutter/material.dart';
import '../../../../shared/widgets/placeholder_screen.dart';
class AccountsScreen extends StatelessWidget {
const AccountsScreen({super.key});
@override
Widget build(BuildContext context) {
return const PlaceholderScreen(
subtitle: 'Управление',
title: 'Счета',
);
}
}
@@ -0,0 +1,15 @@
import 'package:flutter/material.dart';
import '../../../../shared/widgets/placeholder_screen.dart';
class AnalyticsScreen extends StatelessWidget {
const AnalyticsScreen({super.key});
@override
Widget build(BuildContext context) {
return const PlaceholderScreen(
subtitle: 'Аналитика',
title: 'Отчёты',
);
}
}
@@ -1,5 +1,5 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../../../core/database/converters/enum_converters.dart';
import '../../../core/database/converters/enum_converters.dart';
import '../domain/entities/category.dart';
import 'category_providers.dart';
@@ -0,0 +1,311 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/database/converters/enum_converters.dart';
import '../../accounts/domain/entities/account.dart';
import '../../categories/domain/entities/category.dart';
import '../../transactions/domain/entities/transaction.dart';
const int mockUserId = 1;
/// Sentinel id для виртуального счёта «Все счета» — это агрегат, а не запись.
const int kAllAccountsId = 0;
/// Конвертирует hex `0xRRGGBB` в `argb` (без прозрачности) для хранения в
/// `colorValue` доменных сущностей (Account/Category).
int _hex(int rgb) => 0xFF000000 | rgb;
// ─── Категории (соответствуют design/common.jsx:132 — CATS) ──────────
const int _icoFood = 1; // shopping_cart_outlined
const int _icoHouse = 2; // home_outlined
const int _icoTransp = 3; // directions_car_outlined
const int _icoCafe = 4; // restaurant_outlined
const int _icoEnter = 5; // movie_outlined
const int _icoOther = 6; // more_horiz
const int catFoodId = 11;
const int catRentId = 12;
const int catTranspId = 13;
const int catCafeId = 14;
const int catEnterId = 15;
const int catOtherId = 16;
final List<Category> _mockCategories = [
Category(
id: catFoodId,
userId: mockUserId,
name: 'Продукты',
type: CategoryType.expense,
iconCode: _icoFood,
colorValue: _hex(0x8AA6A0),
archived: false,
),
Category(
id: catRentId,
userId: mockUserId,
name: 'Жильё',
type: CategoryType.expense,
iconCode: _icoHouse,
colorValue: _hex(0xC89A86),
archived: false,
),
Category(
id: catTranspId,
userId: mockUserId,
name: 'Транспорт',
type: CategoryType.expense,
iconCode: _icoTransp,
colorValue: _hex(0xB3A589),
archived: false,
),
Category(
id: catCafeId,
userId: mockUserId,
name: 'Кафе',
type: CategoryType.expense,
iconCode: _icoCafe,
colorValue: _hex(0x9FB38A),
archived: false,
),
Category(
id: catEnterId,
userId: mockUserId,
name: 'Досуг',
type: CategoryType.expense,
iconCode: _icoEnter,
colorValue: _hex(0xA99CB9),
archived: false,
),
Category(
id: catOtherId,
userId: mockUserId,
name: 'Другое',
type: CategoryType.expense,
iconCode: _icoOther,
colorValue: _hex(0xB8B5AC),
archived: false,
),
];
IconData iconForCategory(Category c) {
switch (c.iconCode) {
case _icoFood:
return Icons.shopping_cart_outlined;
case _icoHouse:
return Icons.home_outlined;
case _icoTransp:
return Icons.directions_car_outlined;
case _icoCafe:
return Icons.restaurant_outlined;
case _icoEnter:
return Icons.movie_outlined;
default:
return Icons.more_horiz;
}
}
// ─── Счета (3 реальных; «Все» — виртуальная вкладка) ──────────────────
const int accCardId = 21;
const int accCashId = 22;
const int accSaveId = 23;
final DateTime _now = DateTime.now();
final DateTime _createdAt = _now.subtract(const Duration(days: 60));
final List<Account> _mockAccounts = [
Account(
id: accCardId,
userId: mockUserId,
name: 'Карта',
type: AccountType.card,
currency: 'RUB',
initialBalance: 14250000, // 142 500 ₽
iconCode: null,
colorValue: null,
archived: false,
createdAt: _createdAt,
),
Account(
id: accCashId,
userId: mockUserId,
name: 'Наличные',
type: AccountType.cash,
currency: 'RUB',
initialBalance: 1282000, // 12 820 ₽
iconCode: null,
colorValue: null,
archived: false,
createdAt: _createdAt,
),
Account(
id: accSaveId,
userId: mockUserId,
name: 'Копилка',
type: AccountType.savings,
currency: 'RUB',
initialBalance: 2900000, // 29 000 ₽
iconCode: null,
colorValue: null,
archived: false,
createdAt: _createdAt,
),
];
IconData iconForAccount(Account a) {
switch (a.type) {
case AccountType.cash:
return Icons.payments_outlined;
case AccountType.card:
return Icons.credit_card_outlined;
case AccountType.bank:
return Icons.account_balance_outlined;
case AccountType.savings:
return Icons.savings_outlined;
}
}
String shortAccountLabel(Account a) {
switch (a.type) {
case AccountType.cash:
return 'Кэш';
case AccountType.card:
return 'Карта';
case AccountType.bank:
return 'Банк';
case AccountType.savings:
return 'Копилка';
}
}
// ─── Транзакции (из design/common.jsx:165 — TX) ──────────────────────
Transaction _tx({
required int id,
required int categoryId,
required int accountId,
required TransactionType type,
required int amount,
required DateTime date,
required String merchant,
}) {
return Transaction(
id: id,
userId: mockUserId,
accountId: accountId,
categoryId: categoryId,
type: type,
amount: amount,
date: date,
note: merchant,
transferToAccountId: null,
createdAt: date,
);
}
DateTime _atToday(int h, int m) =>
DateTime(_now.year, _now.month, _now.day, h, m);
DateTime _atYesterday(int h, int m) =>
_atToday(h, m).subtract(const Duration(days: 1));
DateTime _daysAgo(int d, [int h = 12, int m = 0]) =>
DateTime(_now.year, _now.month, _now.day, h, m)
.subtract(Duration(days: d));
final List<Transaction> _mockTransactions = [
_tx(
id: 1,
categoryId: catFoodId,
accountId: accCardId,
type: TransactionType.expense,
amount: 234000,
date: _atToday(19, 42),
merchant: 'Лента',
),
_tx(
id: 2,
categoryId: catCafeId,
accountId: accCardId,
type: TransactionType.expense,
amount: 48000,
date: _atToday(9, 15),
merchant: 'Кофе Хауз',
),
_tx(
id: 3,
categoryId: catTranspId,
accountId: accCardId,
type: TransactionType.expense,
amount: 6200,
date: _atToday(8, 50),
merchant: 'Метро',
),
_tx(
id: 4,
categoryId: catFoodId,
accountId: accCashId,
type: TransactionType.expense,
amount: 112000,
date: _atYesterday(21, 8),
merchant: 'Перекрёсток',
),
_tx(
id: 5,
categoryId: catEnterId,
accountId: accCardId,
type: TransactionType.expense,
amount: 65000,
date: _atYesterday(19, 30),
merchant: 'Кинотеатр',
),
_tx(
id: 6,
categoryId: catRentId,
accountId: accCardId,
type: TransactionType.expense,
amount: 3200000,
date: _daysAgo(3, 12, 0),
merchant: 'Аренда квартиры',
),
_tx(
id: 7,
categoryId: catOtherId,
accountId: accCardId,
type: TransactionType.income,
amount: 9500000,
date: _daysAgo(4, 11, 0),
merchant: 'Зарплата',
),
_tx(
id: 8,
categoryId: catTranspId,
accountId: accCardId,
type: TransactionType.expense,
amount: 34000,
date: _daysAgo(4, 18, 30),
merchant: 'Яндекс Такси',
),
_tx(
id: 9,
categoryId: catCafeId,
accountId: accCashId,
type: TransactionType.expense,
amount: 72000,
date: _daysAgo(5, 14, 0),
merchant: 'Шоколадница',
),
_tx(
id: 10,
categoryId: catFoodId,
accountId: accCardId,
type: TransactionType.expense,
amount: 89000,
date: _daysAgo(5, 9, 20),
merchant: 'Магнит',
),
];
// ─── Provider-обёртки. Когда придёт время — заменяются на стримы из
// application/ слоя без изменений на стороне виджетов. ────────────
final mockAccountsProvider = Provider<List<Account>>((ref) => _mockAccounts);
final mockCategoriesProvider =
Provider<List<Category>>((ref) => _mockCategories);
final mockTransactionsProvider =
Provider<List<Transaction>>((ref) => _mockTransactions);
@@ -0,0 +1,105 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../accounts/domain/entities/account.dart';
import '../../categories/domain/entities/category.dart';
import '../../../core/database/converters/enum_converters.dart';
import '../../transactions/domain/entities/transaction.dart';
import '_mock_data.dart';
import 'state/selected_category_filter.dart';
/// Сводка по месяцу — баланс, доходы, расходы для текущего выбранного счёта.
class MonthSummary {
const MonthSummary({
required this.balanceMinor,
required this.incomeMinor,
required this.expensesMinor,
required this.spendByCategory,
required this.transactionsCount,
});
final int balanceMinor;
final int incomeMinor;
final int expensesMinor;
final Map<int, int> spendByCategory; // categoryId -> sum in minor units
final int transactionsCount;
int get spendTotalMinor =>
spendByCategory.values.fold(0, (sum, v) => sum + v);
}
/// Возвращает транзакции отфильтрованные по выбранному счёту и опционально
/// по выбранной категории.
final filteredTransactionsProvider = Provider<List<Transaction>>((ref) {
final all = ref.watch(mockTransactionsProvider);
final accountId = ref.watch(selectedAccountProvider);
final categoryId = ref.watch(selectedCategoryFilterProvider);
return all.where((t) {
if (accountId != 0 && t.accountId != accountId) return false;
if (categoryId != null && t.categoryId != categoryId) return false;
return true;
}).toList();
});
final monthSummaryProvider = Provider<MonthSummary>((ref) {
final accounts = ref.watch(mockAccountsProvider);
final txs = ref.watch(mockTransactionsProvider);
final selectedAccount = ref.watch(selectedAccountProvider);
// Балансы считаем не учитывая фильтр категории (баланс счёта от неё не зависит).
final scopedTxs = selectedAccount == 0
? txs
: txs.where((t) => t.accountId == selectedAccount).toList();
var income = 0;
var expense = 0;
final spendByCat = <int, int>{};
for (final t in scopedTxs) {
switch (t.type) {
case TransactionType.income:
income += t.amount;
case TransactionType.expense:
expense += t.amount;
final cid = t.categoryId;
if (cid != null) {
spendByCat[cid] = (spendByCat[cid] ?? 0) + t.amount;
}
case TransactionType.transfer:
break;
}
}
// Баланс = сумма initialBalance по выбранным счетам + доход - расход.
final selectedAccounts = selectedAccount == 0
? accounts
: accounts.where((a) => a.id == selectedAccount);
final baseBalance = selectedAccounts.fold<int>(
0,
(s, Account a) => s + a.initialBalance,
);
return MonthSummary(
balanceMinor: baseBalance + income - expense,
incomeMinor: income,
expensesMinor: expense,
spendByCategory: spendByCat,
transactionsCount: scopedTxs.length,
);
});
/// Возвращает категории, отсортированные по сумме трат (по убыванию).
List<MapEntry<Category, int>> categoriesBySpend(
Map<int, int> spend,
List<Category> categories,
) {
final result = <MapEntry<Category, int>>[];
for (final c in categories) {
final v = spend[c.id];
if (v != null && v > 0) result.add(MapEntry(c, v));
}
result.sort((a, b) => b.value.compareTo(a.value));
return result;
}
Color colorFor(Category c) => Color(c.colorValue ?? 0xFFB8B5AC);
@@ -0,0 +1,135 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import '../../../../app/theme/app_colors.dart';
import '../../../categories/domain/entities/category.dart';
import '../../../transactions/domain/entities/transaction.dart';
import '../_mock_data.dart';
import '../month_summary.dart';
import '../widgets/account_tabs.dart';
import '../widgets/category_donut_card.dart';
import '../widgets/day_header.dart';
import '../widgets/fab_add_transaction.dart';
import '../widgets/month_header.dart';
import '../widgets/month_kpi_card.dart';
import '../widgets/transactions_section.dart';
import '../widgets/tx_row.dart';
class HomeScreen extends ConsumerWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final p = context.palette;
final categories = ref.watch(mockCategoriesProvider);
final categoryById = {for (final c in categories) c.id: c};
final txs = ref.watch(filteredTransactionsProvider);
final groups = _groupByDay(txs);
return Scaffold(
backgroundColor: p.paper,
body: Stack(
children: [
SafeArea(
child: CustomScrollView(
slivers: [
const SliverToBoxAdapter(child: MonthHeader()),
const SliverToBoxAdapter(child: AccountTabs()),
const SliverToBoxAdapter(child: SizedBox(height: 0)),
const SliverToBoxAdapter(child: MonthKpiCard()),
const SliverToBoxAdapter(child: CategoryDonutCard()),
const SliverToBoxAdapter(child: TransactionsSectionHeader()),
const SliverToBoxAdapter(child: CategoryFilterPill()),
const SliverToBoxAdapter(child: SizedBox(height: 4)),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, i) => _DayGroupBlock(
group: groups[i],
categoryById: categoryById,
),
childCount: groups.length,
),
),
const SliverToBoxAdapter(child: SizedBox(height: 96)),
],
),
),
Positioned(
right: 16,
bottom: 16,
child: FabAddTransaction(onPressed: () {}),
),
],
),
);
}
}
class _DayGroupBlock extends StatelessWidget {
const _DayGroupBlock({required this.group, required this.categoryById});
final _DayGroup group;
final Map<int, Category> categoryById;
@override
Widget build(BuildContext context) {
return Column(
children: [
DayHeader(label: group.label, totalMinor: group.totalSpentMinor),
for (final t in group.items)
TxRow(tx: t, category: categoryById[t.categoryId]),
],
);
}
}
class _DayGroup {
_DayGroup({required this.label, required this.items, required this.totalSpentMinor});
final String label;
final List<Transaction> items;
final int totalSpentMinor;
}
List<_DayGroup> _groupByDay(List<Transaction> txs) {
final sorted = [...txs]..sort((a, b) => b.date.compareTo(a.date));
final map = <DateTime, List<Transaction>>{};
for (final t in sorted) {
final key = DateTime(t.date.year, t.date.month, t.date.day);
map.putIfAbsent(key, () => []).add(t);
}
final keys = map.keys.toList()..sort((a, b) => b.compareTo(a));
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final fmt = DateFormat('d MMMM', 'ru');
return [
for (final k in keys)
_DayGroup(
label: _dayLabel(k, today, fmt),
items: map[k]!,
totalSpentMinor: map[k]!
.where((t) => t.amount > 0 && t.note != null)
.where((t) => _isExpense(t))
.fold<int>(0, (s, t) => s + t.amount),
),
];
}
bool _isExpense(Transaction t) {
// Помечаем расходы — для day-итога. (Type-based filter.)
return t.amount > 0 && t.categoryId != null && _isExpenseType(t);
}
bool _isExpenseType(Transaction t) {
// Доступ к энам — без импорта в этой утилите.
return t.type.name == 'expense';
}
String _dayLabel(DateTime day, DateTime today, DateFormat fmt) {
final diff = today.difference(day).inDays;
final base = fmt.format(day);
if (diff == 0) return 'Сегодня · $base';
if (diff == 1) return 'Вчера · $base';
return base;
}
@@ -0,0 +1,8 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
/// id выбранной категории для фильтра списка транзакций.
/// `null` — фильтр снят, отображаются все категории.
final selectedCategoryFilterProvider = StateProvider<int?>((ref) => null);
/// id выбранного счёта в табах. `kAllAccountsId` (0) — «Все счета».
final selectedAccountProvider = StateProvider<int>((ref) => 0);
@@ -0,0 +1,111 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../app/theme/app_colors.dart';
import '../../../accounts/domain/entities/account.dart';
import '../_mock_data.dart';
import '../state/selected_category_filter.dart';
class AccountTabs extends ConsumerWidget {
const AccountTabs({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final accounts = ref.watch(mockAccountsProvider);
final selected = ref.watch(selectedAccountProvider);
return SizedBox(
height: 40,
child: ListView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
children: [
_Pill(
icon: Icons.account_balance_wallet_outlined,
label: 'Все',
active: selected == kAllAccountsId,
onTap: () => ref.read(selectedAccountProvider.notifier).state =
kAllAccountsId,
),
for (final a in accounts) ...[
const SizedBox(width: 8),
_AccountPill(
account: a,
active: selected == a.id,
onTap: () =>
ref.read(selectedAccountProvider.notifier).state = a.id,
),
],
],
),
);
}
}
class _AccountPill extends StatelessWidget {
const _AccountPill({
required this.account,
required this.active,
required this.onTap,
});
final Account account;
final bool active;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return _Pill(
icon: iconForAccount(account),
label: shortAccountLabel(account),
active: active,
onTap: onTap,
);
}
}
class _Pill extends StatelessWidget {
const _Pill({
required this.icon,
required this.label,
required this.active,
required this.onTap,
});
final IconData icon;
final String label;
final bool active;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final p = context.palette;
final fg = active ? p.paper : p.ink;
return GestureDetector(
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
decoration: BoxDecoration(
color: active ? p.ink : p.paper,
border: Border.all(color: active ? p.ink : p.line),
borderRadius: BorderRadius.circular(999),
),
child: Row(
children: [
Icon(icon, size: 16, color: fg.withValues(alpha: active ? 1 : 0.7)),
const SizedBox(width: 6),
Text(
label,
style: TextStyle(
fontSize: 13,
fontWeight: active ? FontWeight.w600 : FontWeight.w400,
color: fg,
),
),
],
),
),
);
}
}
@@ -0,0 +1,82 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
class DonutSlice {
const DonutSlice({
required this.id,
required this.value,
required this.color,
});
final int id;
final double value;
final Color color;
}
class CategoryDonut extends StatelessWidget {
const CategoryDonut({
super.key,
required this.slices,
required this.size,
required this.thickness,
this.activeId,
this.onSegmentTap,
this.centerChild,
});
final List<DonutSlice> slices;
final double size;
final double thickness;
final int? activeId;
final ValueChanged<int?>? onSegmentTap;
final Widget? centerChild;
@override
Widget build(BuildContext context) {
final radius = thickness;
final activeRadius = thickness + 4;
return SizedBox(
width: size,
height: size,
child: Stack(
alignment: Alignment.center,
children: [
PieChart(
PieChartData(
sectionsSpace: 0,
centerSpaceRadius: (size / 2) - thickness,
startDegreeOffset: -90,
sections: [
for (final s in slices)
PieChartSectionData(
value: s.value,
color: activeId == null || activeId == s.id
? s.color
: s.color.withValues(alpha: 0.35),
radius: activeId == s.id ? activeRadius : radius,
showTitle: false,
),
],
pieTouchData: PieTouchData(
enabled: onSegmentTap != null,
touchCallback: (event, response) {
if (event is FlTapUpEvent) {
final i = response?.touchedSection?.touchedSectionIndex;
if (i == null || i < 0 || i >= slices.length) {
onSegmentTap?.call(null);
} else {
onSegmentTap?.call(slices[i].id);
}
}
},
),
),
),
if (centerChild != null)
IgnorePointer(child: centerChild!),
],
),
);
}
}
@@ -0,0 +1,155 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../app/theme/app_colors.dart';
import '../../../categories/domain/entities/category.dart';
import '../_mock_data.dart';
import '../month_summary.dart';
import '../state/selected_category_filter.dart';
import 'category_donut.dart';
import 'money_text.dart';
class CategoryDonutCard extends ConsumerWidget {
const CategoryDonutCard({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final p = context.palette;
final summary = ref.watch(monthSummaryProvider);
final categories = ref.watch(mockCategoriesProvider);
final selectedCat = ref.watch(selectedCategoryFilterProvider);
final sorted = categoriesBySpend(summary.spendByCategory, categories);
final slices = [
for (final entry in sorted)
DonutSlice(
id: entry.key.id,
value: entry.value.toDouble(),
color: colorFor(entry.key),
),
];
return Container(
margin: const EdgeInsets.fromLTRB(16, 0, 16, 14),
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
border: Border.all(color: p.line),
borderRadius: BorderRadius.circular(14),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
CategoryDonut(
slices: slices,
size: 130,
thickness: 22,
activeId: selectedCat,
onSegmentTap: (id) {
final notifier =
ref.read(selectedCategoryFilterProvider.notifier);
notifier.state = id == notifier.state ? null : id;
},
centerChild: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'РАСХОДЫ',
style: TextStyle(
fontSize: 9,
color: p.ink2,
letterSpacing: 0.6,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
MoneyText(
-summary.spendTotalMinor,
color: p.ink,
fontSize: 14,
fontWeight: FontWeight.w600,
),
],
),
),
const SizedBox(width: 14),
Expanded(child: _Legend(entries: sorted, totalMinor: summary.spendTotalMinor)),
],
),
);
}
}
class _Legend extends StatelessWidget {
const _Legend({required this.entries, required this.totalMinor});
final List<MapEntry<Category, int>> entries;
final int totalMinor;
@override
Widget build(BuildContext context) {
final p = context.palette;
final top = entries.take(4).toList();
final restCount = entries.length - top.length;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
for (final e in top)
Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: Row(
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: colorFor(e.key),
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 6),
Expanded(
child: Text(
e.key.name,
style: TextStyle(fontSize: 12, color: p.ink),
overflow: TextOverflow.ellipsis,
),
),
MoneyText(
totalMinor == 0
? 0
: (e.value * 100 ~/ totalMinor),
color: p.ink2,
fontSize: 12,
withCurrency: false,
),
Text(
'%',
style: TextStyle(fontSize: 12, color: p.ink2),
),
],
),
),
if (restCount > 0)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
'+ ещё $restCount ${_word(restCount)}',
style: TextStyle(fontSize: 10, color: p.ink2),
),
),
],
);
}
String _word(int n) {
final mod10 = n % 10;
final mod100 = n % 100;
if (mod10 == 1 && mod100 != 11) return 'категория';
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) {
return 'категории';
}
return 'категорий';
}
}
@@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import '../../../../app/theme/app_colors.dart';
import 'money_text.dart';
class DayHeader extends StatelessWidget {
const DayHeader({super.key, required this.label, required this.totalMinor});
final String label;
final int totalMinor;
@override
Widget build(BuildContext context) {
final p = context.palette;
return Padding(
padding: const EdgeInsets.fromLTRB(16, 10, 16, 4),
child: Row(
children: [
Expanded(
child: Text(
label.toUpperCase(),
style: TextStyle(
fontSize: 11,
color: p.ink2,
letterSpacing: 0.6,
fontWeight: FontWeight.w500,
),
),
),
MoneyText(
-totalMinor,
color: p.ink2,
fontSize: 11,
withSign: true,
),
],
),
);
}
}
@@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import '../../../../app/theme/app_colors.dart';
class FabAddTransaction extends StatelessWidget {
const FabAddTransaction({super.key, this.onPressed});
final VoidCallback? onPressed;
@override
Widget build(BuildContext context) {
final p = context.palette;
return Material(
color: p.accent,
borderRadius: BorderRadius.circular(16),
elevation: 6,
shadowColor: Colors.black.withValues(alpha: 0.25),
child: InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(16),
child: SizedBox(
width: 52,
height: 52,
child: Icon(Icons.add, color: p.paper, size: 24),
),
),
);
}
}
@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../../../../app/theme/app_theme.dart';
String formatMinor(int minor, {bool withSign = false, bool withCurrency = true}) {
final value = minor.abs() ~/ 100;
final formatter = NumberFormat.decimalPattern('ru');
final body = formatter.format(value).replaceAll(',', ' ');
final sign = !withSign
? ''
: minor > 0
? '+'
: minor < 0
? '' // U+2212 minus
: '';
final suffix = withCurrency ? '' : '';
return '$sign$body$suffix';
}
/// Денежный текст моно-шрифтом с табулярными цифрами.
class MoneyText extends StatelessWidget {
const MoneyText(
this.minor, {
super.key,
required this.color,
this.fontSize = 14,
this.fontWeight = FontWeight.w500,
this.withSign = false,
this.withCurrency = true,
this.letterSpacing = 0,
});
final int minor;
final Color color;
final double fontSize;
final FontWeight fontWeight;
final bool withSign;
final bool withCurrency;
final double letterSpacing;
@override
Widget build(BuildContext context) {
return Text(
formatMinor(minor, withSign: withSign, withCurrency: withCurrency),
style: monoStyle(
color: color,
fontSize: fontSize,
fontWeight: fontWeight,
letterSpacing: letterSpacing,
),
);
}
}
@@ -0,0 +1,64 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../../../../app/theme/app_colors.dart';
class MonthHeader extends StatelessWidget {
const MonthHeader({super.key});
@override
Widget build(BuildContext context) {
final p = context.palette;
final now = DateTime.now();
final monthName = DateFormat.MMMM('ru').format(now);
final monthCap = '${monthName[0].toUpperCase()}${monthName.substring(1)}';
final title = '$monthCap ${now.year}';
return Padding(
padding: const EdgeInsets.fromLTRB(16, 14, 16, 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'БЮДЖЕТ',
style: TextStyle(
fontSize: 11,
color: p.ink2,
letterSpacing: 0.6,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
title,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: p.ink,
),
),
],
),
),
IconButton(
onPressed: () {},
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
icon: Icon(Icons.search, size: 20, color: p.ink2),
),
const SizedBox(width: 4),
IconButton(
onPressed: () {},
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
icon: Icon(Icons.notifications_outlined, size: 20, color: p.ink2),
),
],
),
);
}
}
@@ -0,0 +1,120 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../app/theme/app_colors.dart';
import '../month_summary.dart';
import 'money_text.dart';
class MonthKpiCard extends ConsumerWidget {
const MonthKpiCard({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final p = context.palette;
final s = ref.watch(monthSummaryProvider);
return Container(
margin: const EdgeInsets.fromLTRB(16, 12, 16, 14),
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: p.paper,
border: Border.all(color: p.line),
borderRadius: BorderRadius.circular(14),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Expanded(
child: Text(
'БАЛАНС',
style: TextStyle(
fontSize: 11,
color: p.ink2,
letterSpacing: 0.6,
fontWeight: FontWeight.w500,
),
),
),
MoneyText(
s.balanceMinor,
color: p.ink,
fontSize: 22,
fontWeight: FontWeight.w600,
letterSpacing: -0.4,
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _SubKpi(
label: 'ДОХОДЫ',
value: s.incomeMinor,
color: p.positive,
withSign: true,
),
),
Container(width: 1, height: 32, color: p.line),
const SizedBox(width: 12),
Expanded(
child: _SubKpi(
label: 'РАСХОДЫ',
value: -s.expensesMinor,
color: p.negative,
withSign: true,
),
),
],
),
],
),
);
}
}
class _SubKpi extends StatelessWidget {
const _SubKpi({
required this.label,
required this.value,
required this.color,
this.withSign = false,
});
final String label;
final int value;
final Color color;
final bool withSign;
@override
Widget build(BuildContext context) {
final p = context.palette;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 10,
color: p.ink2,
letterSpacing: 0.6,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
MoneyText(
value,
color: color,
fontSize: 18,
fontWeight: FontWeight.w600,
letterSpacing: -0.3,
withSign: withSign,
),
],
);
}
}
@@ -0,0 +1,130 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../app/theme/app_colors.dart';
import '../_mock_data.dart';
import '../month_summary.dart';
import '../state/selected_category_filter.dart';
class TransactionsSectionHeader extends ConsumerWidget {
const TransactionsSectionHeader({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final p = context.palette;
final count = ref.watch(filteredTransactionsProvider).length;
return Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
child: Row(
children: [
Text(
'Транзакции',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: p.ink,
),
),
const Spacer(),
Text(
'$count ${_opsWord(count)}',
style: TextStyle(fontSize: 11, color: p.ink2),
),
],
),
);
}
String _opsWord(int n) {
final mod10 = n % 10;
final mod100 = n % 100;
if (mod10 == 1 && mod100 != 11) return 'операция';
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) {
return 'операции';
}
return 'операций';
}
}
class CategoryFilterPill extends ConsumerWidget {
const CategoryFilterPill({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final p = context.palette;
final categories = ref.watch(mockCategoriesProvider);
final selectedId = ref.watch(selectedCategoryFilterProvider);
final activeCat = selectedId == null
? null
: categories.firstWhere((c) => c.id == selectedId,
orElse: () => categories.first);
final count = ref.watch(filteredTransactionsProvider).length;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: GestureDetector(
onTap: () =>
ref.read(selectedCategoryFilterProvider.notifier).state = null,
behavior: HitTestBehavior.opaque,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
border: Border.all(color: p.line),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Icon(Icons.tune, size: 18, color: p.ink2),
const SizedBox(width: 8),
Expanded(
child: activeCat == null
? Text(
'Все категории · все типы',
style: TextStyle(fontSize: 13, color: p.ink),
)
: Row(
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: colorFor(activeCat),
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 6),
Flexible(
child: Text(
activeCat.name,
style: TextStyle(fontSize: 13, color: p.ink),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: p.accentSoft,
borderRadius: BorderRadius.circular(99),
),
child: Text(
'$count',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: p.accent,
),
),
),
const SizedBox(width: 6),
Icon(Icons.keyboard_arrow_down, size: 18, color: p.ink2),
],
),
),
),
);
}
}
@@ -0,0 +1,100 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../../../../app/theme/app_colors.dart';
import '../../../../core/database/converters/enum_converters.dart';
import '../../../categories/domain/entities/category.dart';
import '../../../transactions/domain/entities/transaction.dart';
import '../_mock_data.dart';
import '../month_summary.dart';
import 'money_text.dart';
class TxRow extends StatelessWidget {
const TxRow({
super.key,
required this.tx,
required this.category,
});
final Transaction tx;
final Category? category;
@override
Widget build(BuildContext context) {
final p = context.palette;
final cat = category;
final color = cat != null ? colorFor(cat) : p.ink2;
final icon = cat != null ? iconForCategory(cat) : Icons.more_horiz;
final signedAmount = tx.type == TransactionType.income
? tx.amount
: tx.type == TransactionType.expense
? -tx.amount
: 0;
final amountColor =
tx.type == TransactionType.income ? p.positive : p.ink;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
border: Border(bottom: BorderSide(color: p.line)),
),
child: Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: color.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, size: 18, color: color),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
tx.note ?? '',
style: TextStyle(
fontSize: 14,
color: p.ink,
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
'${cat?.name ?? ''} · ${_subtitleTime(tx.date)}',
style: TextStyle(fontSize: 11, color: p.ink2),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
const SizedBox(width: 8),
MoneyText(
signedAmount,
color: amountColor,
fontSize: 14,
withSign: true,
),
],
),
);
}
}
String _subtitleTime(DateTime date) {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final dt = DateTime(date.year, date.month, date.day);
final diff = today.difference(dt).inDays;
final hm = DateFormat('HH:mm').format(date);
if (diff == 0) return 'Сегодня, $hm';
if (diff == 1) return 'Вчера, $hm';
return DateFormat('d MMM', 'ru').format(date);
}
@@ -0,0 +1,81 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../app/theme/app_colors.dart';
import '../../../../app/theme/theme_mode_controller.dart';
import '../../../../shared/widgets/placeholder_screen.dart';
class ProfileScreen extends ConsumerWidget {
const ProfileScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final p = context.palette;
final mode = ref.watch(themeModeControllerProvider);
final controller = ref.read(themeModeControllerProvider.notifier);
return PlaceholderScreen(
subtitle: 'Настройки',
title: 'Профиль',
body: ListView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
children: [
Container(
decoration: BoxDecoration(
border: Border.all(color: p.line),
borderRadius: BorderRadius.circular(14),
),
child: Column(
children: [
_Row(
icon: Icons.dark_mode_outlined,
title: 'Тёмная тема',
trailing: Switch(
value: mode == ThemeMode.dark,
activeThumbColor: p.accent,
onChanged: (v) =>
controller.set(v ? ThemeMode.dark : ThemeMode.light),
),
),
],
),
),
const SizedBox(height: 12),
Text(
'Здесь появится профиль пользователя, валюта, локаль и другие настройки.',
style: TextStyle(fontSize: 12, color: p.ink2),
),
],
),
);
}
}
class _Row extends StatelessWidget {
const _Row({required this.icon, required this.title, required this.trailing});
final IconData icon;
final String title;
final Widget trailing;
@override
Widget build(BuildContext context) {
final p = context.palette;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
child: Row(
children: [
Icon(icon, size: 20, color: p.ink2),
const SizedBox(width: 12),
Expanded(
child: Text(
title,
style: TextStyle(fontSize: 14, color: p.ink),
),
),
trailing,
],
),
);
}
}
@@ -1,5 +1,5 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../../../core/database/converters/enum_converters.dart';
import '../../../core/database/converters/enum_converters.dart';
import '../domain/entities/transaction.dart';
import 'transaction_providers.dart';
+106
View File
@@ -0,0 +1,106 @@
import 'package:flutter/material.dart';
import '../../app/theme/app_colors.dart';
class AppBottomNav extends StatelessWidget {
const AppBottomNav({
super.key,
required this.activeIndex,
required this.onTap,
});
final int activeIndex;
final ValueChanged<int> onTap;
static const _items = <_NavItem>[
_NavItem(icon: Icons.home_outlined, label: 'Главная'),
_NavItem(icon: Icons.bar_chart_outlined, label: 'Аналитика'),
_NavItem(icon: Icons.account_balance_wallet_outlined, label: 'Счета'),
_NavItem(icon: Icons.person_outline, label: 'Профиль'),
];
@override
Widget build(BuildContext context) {
final p = context.palette;
return Container(
decoration: BoxDecoration(
color: p.paper,
border: Border(top: BorderSide(color: p.line)),
),
child: SafeArea(
top: false,
child: SizedBox(
height: 60,
child: Row(
children: [
for (var i = 0; i < _items.length; i++)
Expanded(
child: _NavTab(
item: _items[i],
active: i == activeIndex,
onTap: () => onTap(i),
),
),
],
),
),
),
);
}
}
class _NavItem {
const _NavItem({required this.icon, required this.label});
final IconData icon;
final String label;
}
class _NavTab extends StatelessWidget {
const _NavTab({
required this.item,
required this.active,
required this.onTap,
});
final _NavItem item;
final bool active;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final p = context.palette;
final color = active ? p.accent : p.ink2;
return InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 180),
padding: EdgeInsets.symmetric(
vertical: 2,
horizontal: active ? 14 : 0,
),
decoration: BoxDecoration(
color: active ? p.accentSoft : Colors.transparent,
borderRadius: BorderRadius.circular(12),
),
child: Icon(item.icon, size: 22, color: color),
),
const SizedBox(height: 3),
Text(
item.label,
style: TextStyle(
fontSize: 10,
fontWeight: active ? FontWeight.w600 : FontWeight.w400,
color: color,
),
),
],
),
),
);
}
}
+24
View File
@@ -0,0 +1,24 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'app_bottom_nav.dart';
class AppScaffold extends StatelessWidget {
const AppScaffold({super.key, required this.navigationShell});
final StatefulNavigationShell navigationShell;
@override
Widget build(BuildContext context) {
return Scaffold(
body: navigationShell,
bottomNavigationBar: AppBottomNav(
activeIndex: navigationShell.currentIndex,
onTap: (i) => navigationShell.goBranch(
i,
initialLocation: i == navigationShell.currentIndex,
),
),
);
}
}
@@ -0,0 +1,70 @@
import 'package:flutter/material.dart';
import '../../app/theme/app_colors.dart';
class PlaceholderScreen extends StatelessWidget {
const PlaceholderScreen({
super.key,
required this.subtitle,
required this.title,
this.body,
});
final String subtitle;
final String title;
final Widget? body;
@override
Widget build(BuildContext context) {
final p = context.palette;
return Scaffold(
backgroundColor: p.paper,
body: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 14, 16, 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
subtitle.toUpperCase(),
style: TextStyle(
fontSize: 11,
color: p.ink2,
letterSpacing: 0.6,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
title,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: p.ink,
),
),
],
),
),
Expanded(
child: body ??
Center(
child: Text(
'Скоро',
style: TextStyle(
fontSize: 14,
color: p.ink2,
letterSpacing: 0.4,
),
),
),
),
],
),
),
);
}
}
+767 -5
View File
@@ -1,6 +1,38 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f
url: "https://pub.dev"
source: hosted
version: "85.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: f4ad0fea5f102201015c9aae9d93bc02f75dd9491529a8c21f88d17a8523d44c
url: "https://pub.dev"
source: hosted
version: "7.6.0"
analyzer_plugin:
dependency: transitive
description:
name: analyzer_plugin
sha256: a5ab7590c27b779f3d4de67f31c4109dbe13dd7339f86461a6f2a8ab2594d8ce
url: "https://pub.dev"
source: hosted
version: "0.13.4"
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
@@ -17,6 +49,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.2"
build:
dependency: transitive
description:
name: build
sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
build_config:
dependency: transitive
description:
name: build_config
sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33"
url: "https://pub.dev"
source: hosted
version: "1.1.2"
build_daemon:
dependency: transitive
description:
name: build_daemon
sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957
url: "https://pub.dev"
source: hosted
version: "4.1.1"
build_resolvers:
dependency: transitive
description:
name: build_resolvers
sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62
url: "https://pub.dev"
source: hosted
version: "2.5.4"
build_runner:
dependency: "direct dev"
description:
name: build_runner
sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
build_runner_core:
dependency: transitive
description:
name: build_runner_core
sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792"
url: "https://pub.dev"
source: hosted
version: "9.1.2"
built_collection:
dependency: transitive
description:
name: built_collection
sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
url: "https://pub.dev"
source: hosted
version: "5.1.1"
built_value:
dependency: transitive
description:
name: built_value
sha256: "34e4067d30ce212937df995f03b69992eea683539ceeac7f679a1f1eba055b56"
url: "https://pub.dev"
source: hosted
version: "8.12.6"
characters:
dependency: transitive
description:
@@ -25,6 +121,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.1"
charcode:
dependency: transitive
description:
name: charcode
sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a
url: "https://pub.dev"
source: hosted
version: "1.4.0"
checked_yaml:
dependency: transitive
description:
name: checked_yaml
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
url: "https://pub.dev"
source: hosted
version: "2.0.4"
ci:
dependency: transitive
description:
name: ci
sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13"
url: "https://pub.dev"
source: hosted
version: "0.1.0"
cli_util:
dependency: transitive
description:
name: cli_util
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
url: "https://pub.dev"
source: hosted
version: "0.4.2"
clock:
dependency: transitive
description:
@@ -33,6 +161,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.2"
code_assets:
dependency: transitive
description:
name: code_assets
sha256: dad6bf6b9f4f378b0a69edbf42584d336efd1a9ce15deb1ba591cbb1b5ff440f
url: "https://pub.dev"
source: hosted
version: "1.1.0"
code_builder:
dependency: transitive
description:
name: code_builder
sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d"
url: "https://pub.dev"
source: hosted
version: "4.11.1"
collection:
dependency: transitive
description:
@@ -41,6 +185,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.19.1"
convert:
dependency: transitive
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev"
source: hosted
version: "3.1.2"
crypto:
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.7"
cupertino_icons:
dependency: "direct main"
description:
@@ -49,6 +209,78 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.9"
custom_lint:
dependency: "direct dev"
description:
name: custom_lint
sha256: "9656925637516c5cf0f5da018b33df94025af2088fe09c8ae2ca54c53f2d9a84"
url: "https://pub.dev"
source: hosted
version: "0.7.6"
custom_lint_builder:
dependency: transitive
description:
name: custom_lint_builder
sha256: "6cdc8e87e51baaaba9c43e283ed8d28e59a0c4732279df62f66f7b5984655414"
url: "https://pub.dev"
source: hosted
version: "0.7.6"
custom_lint_core:
dependency: transitive
description:
name: custom_lint_core
sha256: "31110af3dde9d29fb10828ca33f1dce24d2798477b167675543ce3d208dee8be"
url: "https://pub.dev"
source: hosted
version: "0.7.5"
custom_lint_visitor:
dependency: transitive
description:
name: custom_lint_visitor
sha256: "4a86a0d8415a91fbb8298d6ef03e9034dc8e323a599ddc4120a0e36c433983a2"
url: "https://pub.dev"
source: hosted
version: "1.0.0+7.7.0"
dart_style:
dependency: transitive
description:
name: dart_style
sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
drift:
dependency: "direct main"
description:
name: drift
sha256: "540cf382a3bfa99b76e51514db5b0ebcd81ce3679b7c1c9cb9478ff3735e47a1"
url: "https://pub.dev"
source: hosted
version: "2.28.2"
drift_dev:
dependency: "direct dev"
description:
name: drift_dev
sha256: "68c138e884527d2bd61df2ade276c3a144df84d1adeb0ab8f3196b5afe021bd4"
url: "https://pub.dev"
source: hosted
version: "2.28.0"
drift_flutter:
dependency: "direct main"
description:
name: drift_flutter
sha256: b7534bf320aac5213259aac120670ba67b63a1fd010505babc436ff86083818f
url: "https://pub.dev"
source: hosted
version: "0.2.7"
equatable:
dependency: transitive
description:
name: equatable
sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b"
url: "https://pub.dev"
source: hosted
version: "2.0.8"
fake_async:
dependency: transitive
description:
@@ -57,6 +289,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.3"
ffi:
dependency: transitive
description:
name: ffi
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
fl_chart:
dependency: "direct main"
description:
name: fl_chart
sha256: "74959b99b92b9eebeed1a4049426fd67c4abc3c5a0f4d12e2877097d6a11ae08"
url: "https://pub.dev"
source: hosted
version: "0.69.2"
flutter:
dependency: "direct main"
description: flutter
@@ -66,15 +330,185 @@ packages:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
url: "https://pub.dev"
source: hosted
version: "6.0.0"
version: "5.0.0"
flutter_localizations:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_riverpod:
dependency: "direct main"
description:
name: flutter_riverpod
sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1"
url: "https://pub.dev"
source: hosted
version: "2.6.1"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
freezed:
dependency: "direct dev"
description:
name: freezed
sha256: "2d399f823b8849663744d2a9ddcce01c49268fb4170d0442a655bf6a2f47be22"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
freezed_annotation:
dependency: "direct main"
description:
name: freezed_annotation
sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.dev"
source: hosted
version: "4.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.3"
go_router:
dependency: "direct main"
description:
name: go_router
sha256: f02fd7d2a4dc512fec615529824fdd217fecb3a3d3de68360293a551f21634b3
url: "https://pub.dev"
source: hosted
version: "14.8.1"
google_fonts:
dependency: "direct main"
description:
name: google_fonts
sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055
url: "https://pub.dev"
source: hosted
version: "6.3.3"
graphs:
dependency: transitive
description:
name: graphs
sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
hooks:
dependency: transitive
description:
name: hooks
sha256: a41af4e8fc687cd6d33de9751eb936c8c0204ebe2bcb6c15ecf707504bf47f31
url: "https://pub.dev"
source: hosted
version: "2.0.0"
hotreloader:
dependency: transitive
description:
name: hotreloader
sha256: "66871df468fc24eee81f1a0a7cb98acc104716f9b7376d355437b48d633c4ebf"
url: "https://pub.dev"
source: hosted
version: "4.4.0"
http:
dependency: transitive
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.dev"
source: hosted
version: "1.6.0"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
url: "https://pub.dev"
source: hosted
version: "3.2.2"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
intl:
dependency: "direct main"
description:
name: intl
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev"
source: hosted
version: "0.20.2"
io:
dependency: transitive
description:
name: io
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
url: "https://pub.dev"
source: hosted
version: "1.0.5"
jni:
dependency: transitive
description:
name: jni
sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f
url: "https://pub.dev"
source: hosted
version: "1.0.0"
jni_flutter:
dependency: transitive
description:
name: jni_flutter
sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
js:
dependency: transitive
description:
name: js
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
url: "https://pub.dev"
source: hosted
version: "0.7.2"
json_annotation:
dependency: "direct main"
description:
name: json_annotation
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
url: "https://pub.dev"
source: hosted
version: "4.9.0"
json_serializable:
dependency: "direct dev"
description:
name: json_serializable
sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c
url: "https://pub.dev"
source: hosted
version: "6.9.5"
leak_tracker:
dependency: transitive
description:
@@ -103,10 +537,18 @@ packages:
dependency: transitive
description:
name: lints
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
url: "https://pub.dev"
source: hosted
version: "6.1.0"
version: "5.1.1"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher:
dependency: transitive
description:
@@ -131,6 +573,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.18.0"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
objective_c:
dependency: transitive
description:
name: objective_c
sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed"
url: "https://pub.dev"
source: hosted
version: "9.4.1"
package_config:
dependency: transitive
description:
name: package_config
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
url: "https://pub.dev"
source: hosted
version: "2.2.0"
path:
dependency: transitive
description:
@@ -139,11 +605,195 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_provider:
dependency: transitive
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
url: "https://pub.dev"
source: hosted
version: "2.6.0"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
pool:
dependency: transitive
description:
name: pool
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
url: "https://pub.dev"
source: hosted
version: "1.5.2"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
pubspec_parse:
dependency: transitive
description:
name: pubspec_parse
sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
recase:
dependency: transitive
description:
name: recase
sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213
url: "https://pub.dev"
source: hosted
version: "4.1.0"
record_use:
dependency: transitive
description:
name: record_use
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
riverpod:
dependency: transitive
description:
name: riverpod
sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959"
url: "https://pub.dev"
source: hosted
version: "2.6.1"
riverpod_analyzer_utils:
dependency: transitive
description:
name: riverpod_analyzer_utils
sha256: "03a17170088c63aab6c54c44456f5ab78876a1ddb6032ffde1662ddab4959611"
url: "https://pub.dev"
source: hosted
version: "0.5.10"
riverpod_annotation:
dependency: "direct main"
description:
name: riverpod_annotation
sha256: e14b0bf45b71326654e2705d462f21b958f987087be850afd60578fcd502d1b8
url: "https://pub.dev"
source: hosted
version: "2.6.1"
riverpod_generator:
dependency: "direct dev"
description:
name: riverpod_generator
sha256: "44a0992d54473eb199ede00e2260bd3c262a86560e3c6f6374503d86d0580e36"
url: "https://pub.dev"
source: hosted
version: "2.6.5"
riverpod_lint:
dependency: "direct dev"
description:
name: riverpod_lint
sha256: "89a52b7334210dbff8605c3edf26cfe69b15062beed5cbfeff2c3812c33c9e35"
url: "https://pub.dev"
source: hosted
version: "2.6.5"
rxdart:
dependency: transitive
description:
name: rxdart
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
url: "https://pub.dev"
source: hosted
version: "0.28.0"
shelf:
dependency: transitive
description:
name: shelf
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
url: "https://pub.dev"
source: hosted
version: "1.4.2"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_gen:
dependency: transitive
description:
name: source_gen
sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
source_helper:
dependency: transitive
description:
name: source_helper
sha256: a447acb083d3a5ef17f983dd36201aeea33fedadb3228fa831f2f0c92f0f3aca
url: "https://pub.dev"
source: hosted
version: "1.3.7"
source_span:
dependency: transitive
description:
@@ -152,6 +802,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.10.2"
sqlite3:
dependency: transitive
description:
name: sqlite3
sha256: "3145bd74dcdb4fd6f5c6dda4d4e4490a8087d7f286a14dee5d37087290f0f8a2"
url: "https://pub.dev"
source: hosted
version: "2.9.4"
sqlite3_flutter_libs:
dependency: transitive
description:
name: sqlite3_flutter_libs
sha256: eeb9e3a45207649076b808f8a5a74d68770d0b7f26ccef6d5f43106eee5375ad
url: "https://pub.dev"
source: hosted
version: "0.5.42"
sqlparser:
dependency: transitive
description:
name: sqlparser
sha256: "57090342af1ce32bb499aa641f4ecdd2d6231b9403cea537ac059e803cc20d67"
url: "https://pub.dev"
source: hosted
version: "0.41.2"
stack_trace:
dependency: transitive
description:
@@ -160,6 +834,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.12.1"
state_notifier:
dependency: transitive
description:
name: state_notifier
sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb
url: "https://pub.dev"
source: hosted
version: "1.0.0"
stream_channel:
dependency: transitive
description:
@@ -168,6 +850,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
stream_transform:
dependency: transitive
description:
name: stream_transform
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
url: "https://pub.dev"
source: hosted
version: "2.1.1"
string_scanner:
dependency: transitive
description:
@@ -192,6 +882,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.11"
timing:
dependency: transitive
description:
name: timing
sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
uuid:
dependency: transitive
description:
name: uuid
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
url: "https://pub.dev"
source: hosted
version: "4.5.3"
vector_math:
dependency: transitive
description:
@@ -208,6 +922,54 @@ packages:
url: "https://pub.dev"
source: hosted
version: "15.2.0"
watcher:
dependency: transitive
description:
name: watcher
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
url: "https://pub.dev"
source: hosted
version: "3.0.3"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.12.0 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"
flutter: ">=3.38.4"
+10 -2
View File
@@ -9,6 +9,8 @@ environment:
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
# State management
flutter_riverpod: ^2.6.1
@@ -22,7 +24,7 @@ dependencies:
go_router: ^14.8.1
# Immutable entities
freezed_annotation: ^2.4.4
freezed_annotation: ^3.0.0
json_annotation: ^4.9.0
# Formatting
@@ -31,6 +33,12 @@ dependencies:
# Icons
cupertino_icons: ^1.0.8
# Fonts
google_fonts: ^6.2.1
# Charts
fl_chart: ^0.69.0
dev_dependencies:
flutter_test:
sdk: flutter
@@ -44,7 +52,7 @@ dev_dependencies:
riverpod_lint: ^2.6.5
custom_lint: ^0.7.5
drift_dev: ^2.26.1
freezed: ^2.5.7
freezed: ^3.0.0
json_serializable: ^6.9.5
flutter:
+9 -23
View File
@@ -1,30 +1,16 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:new_budget/main.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:new_budget/src/app/app.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
testWidgets('App boots without crashes', (tester) async {
await initializeDateFormatting('ru');
await tester.pumpWidget(
const ProviderScope(child: NewBudgetApp()),
);
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
expect(find.byType(MaterialApp), findsOneWidget);
});
}