This commit is contained in:
2026-05-27 10:36:03 +03:00
parent 456986e79e
commit fe67189ac7
20 changed files with 696 additions and 67 deletions
+3
View File
@@ -0,0 +1,3 @@
arb-dir: lib/l10n
template-arb-file: app_ru.arb
output-localization-file: app_localizations.dart
+67
View File
@@ -0,0 +1,67 @@
{
"@@locale": "en",
"appTitle": "NewBudget",
"navHome": "Home",
"navAnalytics": "Analytics",
"navAccounts": "Accounts",
"navProfile": "Profile",
"allAccounts": "All",
"comingSoon": "Coming soon",
"budget": "BUDGET",
"balance": "BALANCE",
"income": "INCOME",
"expenses": "EXPENSES",
"transactions": "Transactions",
"transactionCount": "{count, plural, one{{count} transaction} other{{count} transactions}}",
"@transactionCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"allCategoriesFilter": "All categories · all types",
"moreCategories": "{count, plural, one{+ {count} more →} other{+ {count} more →}}",
"@moreCategories": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"todayWithDate": "Today · {date}",
"@todayWithDate": {
"placeholders": {
"date": {
"type": "String"
}
}
},
"yesterdayWithDate": "Yesterday · {date}",
"@yesterdayWithDate": {
"placeholders": {
"date": {
"type": "String"
}
}
},
"analyticsTitle": "Reports",
"analyticsSubtitle": "Analytics",
"accountsSubtitle": "Management",
"profileSubtitle": "Settings",
"darkTheme": "Dark theme",
"profileHint": "User profile, currency, locale and other settings will appear here."
}
+272
View File
@@ -0,0 +1,272 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:intl/intl.dart' as intl;
import 'app_localizations_en.dart';
import 'app_localizations_ru.dart';
// ignore_for_file: type=lint
/// Callers can lookup localized strings with an instance of AppLocalizations
/// returned by `AppLocalizations.of(context)`.
///
/// Applications need to include `AppLocalizations.delegate()` in their app's
/// `localizationDelegates` list, and the locales they support in the app's
/// `supportedLocales` list. For example:
///
/// ```dart
/// import 'l10n/app_localizations.dart';
///
/// return MaterialApp(
/// localizationsDelegates: AppLocalizations.localizationsDelegates,
/// supportedLocales: AppLocalizations.supportedLocales,
/// home: MyApplicationHome(),
/// );
/// ```
///
/// ## Update pubspec.yaml
///
/// Please make sure to update your pubspec.yaml to include the following
/// packages:
///
/// ```yaml
/// dependencies:
/// # Internationalization support.
/// flutter_localizations:
/// sdk: flutter
/// intl: any # Use the pinned version from flutter_localizations
///
/// # Rest of dependencies
/// ```
///
/// ## iOS Applications
///
/// iOS applications define key application metadata, including supported
/// locales, in an Info.plist file that is built into the application bundle.
/// To configure the locales supported by your app, youll need to edit this
/// file.
///
/// First, open your projects ios/Runner.xcworkspace Xcode workspace file.
/// Then, in the Project Navigator, open the Info.plist file under the Runner
/// projects Runner folder.
///
/// Next, select the Information Property List item, select Add Item from the
/// Editor menu, then select Localizations from the pop-up menu.
///
/// Select and expand the newly-created Localizations item then, for each
/// locale your application supports, add a new item and select the locale
/// you wish to add from the pop-up menu in the Value field. This list should
/// be consistent with the languages listed in the AppLocalizations.supportedLocales
/// property.
abstract class AppLocalizations {
AppLocalizations(String locale)
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
final String localeName;
static AppLocalizations? of(BuildContext context) {
return Localizations.of<AppLocalizations>(context, AppLocalizations);
}
static const LocalizationsDelegate<AppLocalizations> delegate =
_AppLocalizationsDelegate();
/// A list of this localizations delegate along with the default localizations
/// delegates.
///
/// Returns a list of localizations delegates containing this delegate along with
/// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
/// and GlobalWidgetsLocalizations.delegate.
///
/// Additional delegates can be added by appending to this list in
/// MaterialApp. This list does not have to be used at all if a custom list
/// of delegates is preferred or required.
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
<LocalizationsDelegate<dynamic>>[
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
/// A list of this localizations delegate's supported locales.
static const List<Locale> supportedLocales = <Locale>[
Locale('en'),
Locale('ru'),
];
/// No description provided for @appTitle.
///
/// In ru, this message translates to:
/// **'NewBudget'**
String get appTitle;
/// No description provided for @navHome.
///
/// In ru, this message translates to:
/// **'Главная'**
String get navHome;
/// No description provided for @navAnalytics.
///
/// In ru, this message translates to:
/// **'Аналитика'**
String get navAnalytics;
/// No description provided for @navAccounts.
///
/// In ru, this message translates to:
/// **'Счета'**
String get navAccounts;
/// No description provided for @navProfile.
///
/// In ru, this message translates to:
/// **'Профиль'**
String get navProfile;
/// No description provided for @allAccounts.
///
/// In ru, this message translates to:
/// **'Все'**
String get allAccounts;
/// No description provided for @comingSoon.
///
/// In ru, this message translates to:
/// **'Скоро'**
String get comingSoon;
/// No description provided for @budget.
///
/// In ru, this message translates to:
/// **'БЮДЖЕТ'**
String get budget;
/// No description provided for @balance.
///
/// In ru, this message translates to:
/// **'БАЛАНС'**
String get balance;
/// No description provided for @income.
///
/// In ru, this message translates to:
/// **'ДОХОДЫ'**
String get income;
/// No description provided for @expenses.
///
/// In ru, this message translates to:
/// **'РАСХОДЫ'**
String get expenses;
/// No description provided for @transactions.
///
/// In ru, this message translates to:
/// **'Транзакции'**
String get transactions;
/// No description provided for @transactionCount.
///
/// In ru, this message translates to:
/// **'{count, plural, one{{count} операция} few{{count} операции} many{{count} операций} other{{count} операций}}'**
String transactionCount(int count);
/// No description provided for @allCategoriesFilter.
///
/// In ru, this message translates to:
/// **'Все категории · все типы'**
String get allCategoriesFilter;
/// No description provided for @moreCategories.
///
/// In ru, this message translates to:
/// **'{count, plural, one{+ ещё {count} категория →} few{+ ещё {count} категории →} many{+ ещё {count} категорий →} other{+ ещё {count} категорий →}}'**
String moreCategories(int count);
/// No description provided for @todayWithDate.
///
/// In ru, this message translates to:
/// **'Сегодня · {date}'**
String todayWithDate(String date);
/// No description provided for @yesterdayWithDate.
///
/// In ru, this message translates to:
/// **'Вчера · {date}'**
String yesterdayWithDate(String date);
/// No description provided for @analyticsTitle.
///
/// In ru, this message translates to:
/// **'Отчёты'**
String get analyticsTitle;
/// No description provided for @analyticsSubtitle.
///
/// In ru, this message translates to:
/// **'Аналитика'**
String get analyticsSubtitle;
/// No description provided for @accountsSubtitle.
///
/// In ru, this message translates to:
/// **'Управление'**
String get accountsSubtitle;
/// No description provided for @profileSubtitle.
///
/// In ru, this message translates to:
/// **'Настройки'**
String get profileSubtitle;
/// No description provided for @darkTheme.
///
/// In ru, this message translates to:
/// **'Тёмная тема'**
String get darkTheme;
/// No description provided for @profileHint.
///
/// In ru, this message translates to:
/// **'Здесь появится профиль пользователя, валюта, локаль и другие настройки.'**
String get profileHint;
}
class _AppLocalizationsDelegate
extends LocalizationsDelegate<AppLocalizations> {
const _AppLocalizationsDelegate();
@override
Future<AppLocalizations> load(Locale locale) {
return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));
}
@override
bool isSupported(Locale locale) =>
<String>['en', 'ru'].contains(locale.languageCode);
@override
bool shouldReload(_AppLocalizationsDelegate old) => false;
}
AppLocalizations lookupAppLocalizations(Locale locale) {
// Lookup logic when only language code is specified.
switch (locale.languageCode) {
case 'en':
return AppLocalizationsEn();
case 'ru':
return AppLocalizationsRu();
}
throw FlutterError(
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
'an issue with the localizations generation tool. Please file an issue '
'on GitHub with a reproducible sample app and the gen-l10n configuration '
'that was used.',
);
}
+100
View File
@@ -0,0 +1,100 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for English (`en`).
class AppLocalizationsEn extends AppLocalizations {
AppLocalizationsEn([String locale = 'en']) : super(locale);
@override
String get appTitle => 'NewBudget';
@override
String get navHome => 'Home';
@override
String get navAnalytics => 'Analytics';
@override
String get navAccounts => 'Accounts';
@override
String get navProfile => 'Profile';
@override
String get allAccounts => 'All';
@override
String get comingSoon => 'Coming soon';
@override
String get budget => 'BUDGET';
@override
String get balance => 'BALANCE';
@override
String get income => 'INCOME';
@override
String get expenses => 'EXPENSES';
@override
String get transactions => 'Transactions';
@override
String transactionCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count transactions',
one: '$count transaction',
);
return '$_temp0';
}
@override
String get allCategoriesFilter => 'All categories · all types';
@override
String moreCategories(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '+ $count more →',
one: '+ $count more →',
);
return '$_temp0';
}
@override
String todayWithDate(String date) {
return 'Today · $date';
}
@override
String yesterdayWithDate(String date) {
return 'Yesterday · $date';
}
@override
String get analyticsTitle => 'Reports';
@override
String get analyticsSubtitle => 'Analytics';
@override
String get accountsSubtitle => 'Management';
@override
String get profileSubtitle => 'Settings';
@override
String get darkTheme => 'Dark theme';
@override
String get profileHint =>
'User profile, currency, locale and other settings will appear here.';
}
+104
View File
@@ -0,0 +1,104 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for Russian (`ru`).
class AppLocalizationsRu extends AppLocalizations {
AppLocalizationsRu([String locale = 'ru']) : super(locale);
@override
String get appTitle => 'NewBudget';
@override
String get navHome => 'Главная';
@override
String get navAnalytics => 'Аналитика';
@override
String get navAccounts => 'Счета';
@override
String get navProfile => 'Профиль';
@override
String get allAccounts => 'Все';
@override
String get comingSoon => 'Скоро';
@override
String get budget => 'БЮДЖЕТ';
@override
String get balance => 'БАЛАНС';
@override
String get income => 'ДОХОДЫ';
@override
String get expenses => 'РАСХОДЫ';
@override
String get transactions => 'Транзакции';
@override
String transactionCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count операций',
many: '$count операций',
few: '$count операции',
one: '$count операция',
);
return '$_temp0';
}
@override
String get allCategoriesFilter => 'Все категории · все типы';
@override
String moreCategories(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '+ ещё $count категорий →',
many: '+ ещё $count категорий →',
few: '+ ещё $count категории →',
one: '+ ещё $count категория →',
);
return '$_temp0';
}
@override
String todayWithDate(String date) {
return 'Сегодня · $date';
}
@override
String yesterdayWithDate(String date) {
return 'Вчера · $date';
}
@override
String get analyticsTitle => 'Отчёты';
@override
String get analyticsSubtitle => 'Аналитика';
@override
String get accountsSubtitle => 'Управление';
@override
String get profileSubtitle => 'Настройки';
@override
String get darkTheme => 'Тёмная тема';
@override
String get profileHint =>
'Здесь появится профиль пользователя, валюта, локаль и другие настройки.';
}
+67
View File
@@ -0,0 +1,67 @@
{
"@@locale": "ru",
"appTitle": "NewBudget",
"navHome": "Главная",
"navAnalytics": "Аналитика",
"navAccounts": "Счета",
"navProfile": "Профиль",
"allAccounts": "Все",
"comingSoon": "Скоро",
"budget": "БЮДЖЕТ",
"balance": "БАЛАНС",
"income": "ДОХОДЫ",
"expenses": "РАСХОДЫ",
"transactions": "Транзакции",
"transactionCount": "{count, plural, one{{count} операция} few{{count} операции} many{{count} операций} other{{count} операций}}",
"@transactionCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"allCategoriesFilter": "Все категории · все типы",
"moreCategories": "{count, plural, one{+ ещё {count} категория →} few{+ ещё {count} категории →} many{+ ещё {count} категорий →} other{+ ещё {count} категорий →}}",
"@moreCategories": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"todayWithDate": "Сегодня · {date}",
"@todayWithDate": {
"placeholders": {
"date": {
"type": "String"
}
}
},
"yesterdayWithDate": "Вчера · {date}",
"@yesterdayWithDate": {
"placeholders": {
"date": {
"type": "String"
}
}
},
"analyticsTitle": "Отчёты",
"analyticsSubtitle": "Аналитика",
"accountsSubtitle": "Управление",
"profileSubtitle": "Настройки",
"darkTheme": "Тёмная тема",
"profileHint": "Здесь появится профиль пользователя, валюта, локаль и другие настройки."
}
+3 -7
View File
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:new_budget/l10n/app_localizations.dart';
import 'router/app_router.dart';
import 'theme/app_theme.dart';
@@ -21,12 +21,8 @@ class NewBudgetApp extends ConsumerWidget {
darkTheme: AppTheme.dark(),
themeMode: themeMode,
routerConfig: router,
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('ru'), Locale('en')],
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
);
}
}
+8
View File
@@ -0,0 +1,8 @@
import 'package:flutter/widgets.dart';
import 'package:new_budget/l10n/app_localizations.dart';
export 'package:new_budget/l10n/app_localizations.dart';
extension AppLocalizationsX on BuildContext {
AppLocalizations get l10n => AppLocalizations.of(this)!;
}
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import '../../../../app/l10n/l10n.dart';
import '../../../../shared/widgets/placeholder_screen.dart';
class AccountsScreen extends StatelessWidget {
@@ -7,9 +8,10 @@ class AccountsScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return const PlaceholderScreen(
subtitle: 'Управление',
title: 'Счета',
final l10n = context.l10n;
return PlaceholderScreen(
subtitle: l10n.accountsSubtitle,
title: l10n.navAccounts,
);
}
}
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import '../../../../app/l10n/l10n.dart';
import '../../../../shared/widgets/placeholder_screen.dart';
class AnalyticsScreen extends StatelessWidget {
@@ -7,9 +8,10 @@ class AnalyticsScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return const PlaceholderScreen(
subtitle: 'Аналитика',
title: 'Отчёты',
final l10n = context.l10n;
return PlaceholderScreen(
subtitle: l10n.analyticsSubtitle,
title: l10n.analyticsTitle,
);
}
}
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import '../../../../app/l10n/l10n.dart';
import '../../../../app/theme/app_colors.dart';
import '../../../categories/domain/entities/category.dart';
import '../../../transactions/domain/entities/transaction.dart';
@@ -22,10 +23,12 @@ class HomeScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final p = context.palette;
final l10n = context.l10n;
final locale = Localizations.localeOf(context).toString();
final categories = ref.watch(mockCategoriesProvider);
final categoryById = {for (final c in categories) c.id: c};
final txs = ref.watch(filteredTransactionsProvider);
final groups = _groupByDay(txs);
final groups = _groupByDay(txs, locale, l10n);
return Scaffold(
backgroundColor: p.paper,
@@ -91,7 +94,11 @@ class _DayGroup {
final int totalSpentMinor;
}
List<_DayGroup> _groupByDay(List<Transaction> txs) {
List<_DayGroup> _groupByDay(
List<Transaction> txs,
String locale,
AppLocalizations l10n,
) {
final sorted = [...txs]..sort((a, b) => b.date.compareTo(a.date));
final map = <DateTime, List<Transaction>>{};
for (final t in sorted) {
@@ -101,12 +108,12 @@ List<_DayGroup> _groupByDay(List<Transaction> txs) {
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');
final fmt = DateFormat('d MMMM', locale);
return [
for (final k in keys)
_DayGroup(
label: _dayLabel(k, today, fmt),
label: _dayLabel(k, today, fmt, l10n),
items: map[k]!,
totalSpentMinor: map[k]!
.where((t) => t.amount > 0 && t.note != null)
@@ -117,19 +124,22 @@ List<_DayGroup> _groupByDay(List<Transaction> txs) {
}
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) {
String _dayLabel(
DateTime day,
DateTime today,
DateFormat fmt,
AppLocalizations l10n,
) {
final diff = today.difference(day).inDays;
final base = fmt.format(day);
if (diff == 0) return 'Сегодня · $base';
if (diff == 1) return 'Вчера · $base';
if (diff == 0) return l10n.todayWithDate(base);
if (diff == 1) return l10n.yesterdayWithDate(base);
return base;
}
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../app/l10n/l10n.dart';
import '../../../../app/theme/app_colors.dart';
import '../../../accounts/domain/entities/account.dart';
import '../_mock_data.dart';
@@ -22,7 +23,7 @@ class AccountTabs extends ConsumerWidget {
children: [
_Pill(
icon: Icons.account_balance_wallet_outlined,
label: 'Все',
label: context.l10n.allAccounts,
active: selected == kAllAccountsId,
onTap: () => ref.read(selectedAccountProvider.notifier).state =
kAllAccountsId,
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../app/l10n/l10n.dart';
import '../../../../app/theme/app_colors.dart';
import '../../../categories/domain/entities/category.dart';
import '../_mock_data.dart';
@@ -15,6 +16,7 @@ class CategoryDonutCard extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final p = context.palette;
final l10n = context.l10n;
final summary = ref.watch(monthSummaryProvider);
final categories = ref.watch(mockCategoriesProvider);
final selectedCat = ref.watch(selectedCategoryFilterProvider);
@@ -53,7 +55,7 @@ class CategoryDonutCard extends ConsumerWidget {
mainAxisSize: MainAxisSize.min,
children: [
Text(
'РАСХОДЫ',
l10n.expenses,
style: TextStyle(
fontSize: 9,
color: p.ink2,
@@ -88,6 +90,7 @@ class _Legend extends StatelessWidget {
@override
Widget build(BuildContext context) {
final p = context.palette;
final l10n = context.l10n;
final top = entries.take(4).toList();
final restCount = entries.length - top.length;
@@ -135,21 +138,11 @@ class _Legend extends StatelessWidget {
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
'+ ещё $restCount ${_word(restCount)}',
l10n.moreCategories(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 'категорий';
}
}
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../../../../app/l10n/l10n.dart';
import '../../../../app/theme/app_colors.dart';
class MonthHeader extends StatelessWidget {
@@ -9,8 +10,10 @@ class MonthHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
final p = context.palette;
final l10n = context.l10n;
final locale = Localizations.localeOf(context).toString();
final now = DateTime.now();
final monthName = DateFormat.MMMM('ru').format(now);
final monthName = DateFormat.MMMM(locale).format(now);
final monthCap = '${monthName[0].toUpperCase()}${monthName.substring(1)}';
final title = '$monthCap ${now.year}';
@@ -24,7 +27,7 @@ class MonthHeader extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'БЮДЖЕТ',
l10n.budget,
style: TextStyle(
fontSize: 11,
color: p.ink2,
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../app/l10n/l10n.dart';
import '../../../../app/theme/app_colors.dart';
import '../month_summary.dart';
import 'money_text.dart';
@@ -11,6 +12,7 @@ class MonthKpiCard extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final p = context.palette;
final l10n = context.l10n;
final s = ref.watch(monthSummaryProvider);
return Container(
@@ -30,7 +32,7 @@ class MonthKpiCard extends ConsumerWidget {
children: [
Expanded(
child: Text(
'БАЛАНС',
l10n.balance,
style: TextStyle(
fontSize: 11,
color: p.ink2,
@@ -53,7 +55,7 @@ class MonthKpiCard extends ConsumerWidget {
children: [
Expanded(
child: _SubKpi(
label: 'ДОХОДЫ',
label: l10n.income,
value: s.incomeMinor,
color: p.positive,
withSign: true,
@@ -63,7 +65,7 @@ class MonthKpiCard extends ConsumerWidget {
const SizedBox(width: 12),
Expanded(
child: _SubKpi(
label: 'РАСХОДЫ',
label: l10n.expenses,
value: -s.expensesMinor,
color: p.negative,
withSign: true,
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../app/l10n/l10n.dart';
import '../../../../app/theme/app_colors.dart';
import '../_mock_data.dart';
import '../month_summary.dart';
@@ -12,13 +13,14 @@ class TransactionsSectionHeader extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final p = context.palette;
final l10n = context.l10n;
final count = ref.watch(filteredTransactionsProvider).length;
return Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
child: Row(
children: [
Text(
'Транзакции',
l10n.transactions,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
@@ -27,23 +29,13 @@ class TransactionsSectionHeader extends ConsumerWidget {
),
const Spacer(),
Text(
'$count ${_opsWord(count)}',
l10n.transactionCount(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 {
@@ -52,6 +44,7 @@ class CategoryFilterPill extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final p = context.palette;
final l10n = context.l10n;
final categories = ref.watch(mockCategoriesProvider);
final selectedId = ref.watch(selectedCategoryFilterProvider);
final activeCat = selectedId == null
@@ -79,7 +72,7 @@ class CategoryFilterPill extends ConsumerWidget {
Expanded(
child: activeCat == null
? Text(
'Все категории · все типы',
l10n.allCategoriesFilter,
style: TextStyle(fontSize: 13, color: p.ink),
)
: Row(
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../app/l10n/l10n.dart';
import '../../../../app/theme/app_colors.dart';
import '../../../../app/theme/theme_mode_controller.dart';
import '../../../../shared/widgets/placeholder_screen.dart';
@@ -11,12 +12,13 @@ class ProfileScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final p = context.palette;
final l10n = context.l10n;
final mode = ref.watch(themeModeControllerProvider);
final controller = ref.read(themeModeControllerProvider.notifier);
return PlaceholderScreen(
subtitle: 'Настройки',
title: 'Профиль',
subtitle: l10n.profileSubtitle,
title: l10n.navProfile,
body: ListView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
children: [
@@ -29,7 +31,7 @@ class ProfileScreen extends ConsumerWidget {
children: [
_Row(
icon: Icons.dark_mode_outlined,
title: 'Тёмная тема',
title: l10n.darkTheme,
trailing: Switch(
value: mode == ThemeMode.dark,
activeThumbColor: p.accent,
@@ -42,7 +44,7 @@ class ProfileScreen extends ConsumerWidget {
),
const SizedBox(height: 12),
Text(
'Здесь появится профиль пользователя, валюта, локаль и другие настройки.',
l10n.profileHint,
style: TextStyle(fontSize: 12, color: p.ink2),
),
],
+11 -9
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import '../../app/l10n/l10n.dart';
import '../../app/theme/app_colors.dart';
class AppBottomNav extends StatelessWidget {
@@ -12,16 +13,17 @@ class AppBottomNav extends StatelessWidget {
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;
final l10n = context.l10n;
final items = <_NavItem>[
_NavItem(icon: Icons.home_outlined, label: l10n.navHome),
_NavItem(icon: Icons.bar_chart_outlined, label: l10n.navAnalytics),
_NavItem(icon: Icons.account_balance_wallet_outlined, label: l10n.navAccounts),
_NavItem(icon: Icons.person_outline, label: l10n.navProfile),
];
return Container(
decoration: BoxDecoration(
color: p.paper,
@@ -33,10 +35,10 @@ class AppBottomNav extends StatelessWidget {
height: 60,
child: Row(
children: [
for (var i = 0; i < _items.length; i++)
for (var i = 0; i < items.length; i++)
Expanded(
child: _NavTab(
item: _items[i],
item: items[i],
active: i == activeIndex,
onTap: () => onTap(i),
),
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import '../../app/l10n/l10n.dart';
import '../../app/theme/app_colors.dart';
class PlaceholderScreen extends StatelessWidget {
@@ -53,7 +54,7 @@ class PlaceholderScreen extends StatelessWidget {
child: body ??
Center(
child: Text(
'Скоро',
context.l10n.comingSoon,
style: TextStyle(
fontSize: 14,
color: p.ink2,
+1
View File
@@ -57,3 +57,4 @@ dev_dependencies:
flutter:
uses-material-design: true
generate: true