Merge pull request 'dev' (#1) from dev into master

Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
2025-05-04 14:49:30 +03:00
9 changed files with 219 additions and 77 deletions
-8
View File
@@ -478,13 +478,6 @@
</list>
</value>
</entry>
<entry key="rxdart">
<value>
<list>
<option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/rxdart-0.28.0/lib" />
</list>
</value>
</entry>
<entry key="shelf">
<value>
<list>
@@ -725,7 +718,6 @@
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/pub_semver-2.2.0/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/pubspec_parse-1.5.0/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/recase-4.1.0/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/rxdart-0.28.0/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/shelf-1.4.2/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/shelf_web_socket-3.0.0/lib" />
<root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/source_gen-2.0.0/lib" />
+10 -23
View File
@@ -1,15 +1,14 @@
import 'dart:io';
// ignore_for_file: unused_import // Часто генерируется при условном импорте
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
// import 'package:flutter/material.dart'; // Не требуется напрямую, т.к. Category импортируется
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
// import 'package:rxdart/rxdart.dart'; // Не используется
// Условный импорт бэкенда базы данных
// Выбирает реализацию connect() в зависимости от платформы
import 'database_connection/connection.dart' // Базовый импорт
if (dart.library.html) 'database_connection/connection_web.dart' // Для Веб
if (dart.library.io) 'database_connection/connection_native.dart'; // Для Нативных платформ (Android, iOS, Desktop)
// Импортируем модель категории для возвращаемого типа и утилиты
import '../models/category.dart';
// Удален ненужный импорт: import '../models/transaction_record.dart';
import '../utils/category_utils.dart'; // Helper for category details
// Эта строка указывает Drift сгенерировать файл database.g.dart
@@ -30,7 +29,10 @@ class Transactions extends Table {
// Аннотация @DriftDatabase указывает Drift сгенерировать код для этой базы данных
@DriftDatabase(tables: [Transactions])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
// Используем функцию connect() из условного импорта для создания соединения
// Во время компиляции будет выбрана правильная реализация connect()
// из connection_web.dart или connection_native.dart.
AppDatabase() : super(connect());
// Версия схемы. Увеличивайте при изменении структуры таблиц.
@override
@@ -128,18 +130,3 @@ class AppDatabase extends _$AppDatabase {
}
}
}
// Функция для открытия соединения с базой данных
LazyDatabase _openConnection() {
// Вычисление пути к файлу базы данных в папке документов приложения
return LazyDatabase(() async {
final dbFolder = await getApplicationDocumentsDirectory();
// Создаем файл 'db.sqlite' в этой папке
final file = File(p.join(dbFolder.path, 'db.sqlite'));
print("Database file path: ${file.path}"); // Логируем путь для отладки
// Используем NativeDatabase для открытия соединения
// logStatements: true полезен для отладки SQL-запросов
return NativeDatabase(file, logStatements: false);
});
}
@@ -0,0 +1,12 @@
// lib/database/database_connection/connection.dart
import 'package:drift/drift.dart';
// Этот файл служит базой для условного импорта.
// Реализации находятся в connection_web.dart и connection_native.dart.
// Определим функцию-заглушку, чтобы основной файл database.dart
// мог ее импортировать без ошибок анализатора.
// Во время выполнения будет вызвана реализация из соответствующего
// файла (web или native) благодаря условному импорту.
QueryExecutor connect() => throw UnsupportedError(
'Stub connect function should not be called. Ensure conditional imports are set up correctly.');
@@ -0,0 +1,33 @@
// lib/database/database_connection/connection_native.dart
import 'dart:io';
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
// sqlite3_flutter_libs и sqlite3 импортируются для возможной тонкой настройки,
// но часто NativeDatabase справляется автоматически.
// import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart';
// import 'package:sqlite3/sqlite3.dart';
QueryExecutor connect() {
print("Connecting to database using NativeDatabase (Lazy)");
// Используем LazyDatabase для отложенной инициализации на нативных платформах
return LazyDatabase(() async {
// Получаем папку для хранения документов приложения
final dbFolder = await getApplicationDocumentsDirectory();
// Создаем путь к файлу 'db.sqlite' в этой папке
final file = File(p.join(dbFolder.path, 'db.sqlite'));
print("Database file path (Native): ${file.path}"); // Логируем путь для отладки
// Настройка библиотеки sqlite3 (обычно не требуется для Android/iOS/macOS с sqlite3_flutter_libs)
// if (Platform.isWindows || Platform.isLinux) {
// // Может потребоваться дополнительная настройка для Desktop
// // await applyWorkaroundToOpenSqlite3Library();
// }
// Используем NativeDatabase для открытия соединения
// logStatements: true полезен для отладки SQL-запросов
return NativeDatabase(file, logStatements: false);
});
}
@@ -0,0 +1,15 @@
// lib/database/database_connection/connection_web.dart
import 'package:drift/drift.dart';
import 'package:drift/web.dart';
// import 'package:drift/wasm.dart'; // <-- Удалить этот импорт, он больше не нужен здесь
QueryExecutor connect() {
print("Connecting to database using WebDatabase (default configuration)");
// Используем стандартный конструктор WebDatabase.
// Drift попытается автоматически найти и загрузить sqlite3.wasm по пути /sqlite3.wasm
return WebDatabase(
'db', // Имя базы данных в IndexedDB
logStatements: false,
);
}
+28
View File
@@ -1,4 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart'; // Для kIsWeb
// import 'package:drift/wasm.dart'; // <-- Удалить этот импорт
import 'app.dart'; // Import the new app root widget
import 'database/database.dart'; // Import the database
@@ -6,11 +8,37 @@ Future<void> main() async {
// Необходимо для асинхронных операций перед runApp, например, инициализации БД
WidgetsFlutterBinding.ensureInitialized();
// --- Удалить весь этот блок ---
// // Инициализация WASM-модуля SQLite для веб-платформы
// if (kIsWeb) {
// print("Running on Web, attempting to initialize sqlite3.wasm...");
// // Указываем Drift, где найти файл sqlite3.wasm.
// // WasmDatabase.resolveFile попытается найти его автоматически (обычно в корне /sqlite3.wasm).
// final result = await WasmDatabase.resolveFile('sqlite3.wasm'); // <-- Ошибка здесь
//
// if (result.isSuccessful) {
// print("sqlite3.wasm loaded successfully.");
// } else {
// // Если файл не найден или произошла ошибка загрузки
// print("Error loading sqlite3.wasm: ${result.errorMessage}");
// // Здесь можно предпринять действия, если загрузка не удалась,
// // например, показать сообщение об ошибке пользователю или использовать
// // альтернативное хранилище. Пока просто выводим ошибку в консоль.
// // Приложение может не работать корректно без базы данных.
// }
// } else {
// print("Running on Native platform, skipping WASM initialization.");
// }
// --- Конец удаляемого блока ---
// Создаем единственный экземпляр базы данных для всего приложения
// WebDatabase (вызываемый через connect() на вебе) должен сам справиться с WASM
final database = AppDatabase();
// Опционально: Вставляем начальные данные, если база данных пуста
// Это полезно для первого запуска или демонстрации
// Делаем это после создания экземпляра БД
await database.insertInitialDataIfNeeded();
// Запускаем приложение, передавая экземпляр базы данных
+113 -30
View File
@@ -24,6 +24,7 @@ class SpendingPieChart extends StatelessWidget {
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final theme = Theme.of(context);
final currencyFormatter = NumberFormat.currency(locale: 'en_US', symbol: '\$'); // Or your preferred locale/symbol
// Handle the case where there are no categories to display
if (categories.isEmpty) {
@@ -61,38 +62,48 @@ class SpendingPieChart extends StatelessWidget {
// --- Pie Chart ---
Expanded(
flex: 5, // Give more space to the chart itself
child: PieChart(
PieChartData(
// Handle touch events on the pie chart
pieTouchData: PieTouchData(
touchCallback: (FlTouchEvent event, pieTouchResponse) {
// Ignore events not related to interaction
if (!event.isInterestedForInteractions ||
pieTouchResponse == null ||
pieTouchResponse.touchedSection == null) {
// If touch ends outside a section, deselect
if (event is FlPanEndEvent || event is FlTapUpEvent) {
if (selectedPieIndex != -1) {
onSelectPieCategory(-1); // Deselect
child: Stack( // Use Stack to overlay text on the chart center
alignment: Alignment.center,
children: [
PieChart(
PieChartData(
// Handle touch events on the pie chart
pieTouchData: PieTouchData(
touchCallback: (FlTouchEvent event, PieTouchResponse? pieTouchResponse) {
// We are only interested in TapUp events to trigger selection changes
if (event is FlTapUpEvent) {
final section = pieTouchResponse?.touchedSection;
if (section != null) {
// Tap occurred ON a section
final touchedIndex = section.touchedSectionIndex;
// Toggle selection: if tapped section is already selected, deselect (-1), otherwise select it.
onSelectPieCategory(touchedIndex == selectedPieIndex ? -1 : touchedIndex);
} else {
// Tap occurred OUTSIDE any section
// Deselect if something was selected
if (selectedPieIndex != -1) {
onSelectPieCategory(-1);
}
}
}
}
return;
}
// Get the index of the touched section
final touchedIndex = pieTouchResponse.touchedSection!.touchedSectionIndex;
// Call the callback, toggling selection if the same slice is touched again
onSelectPieCategory(touchedIndex == selectedPieIndex ? -1 : touchedIndex);
},
},
),
borderData: FlBorderData(show: false), // No border around the chart
sectionsSpace: 2, // Space between slices
centerSpaceRadius: 50, // Radius of the center hole
sections: _generatePieSections(context), // Generate slices data
startDegreeOffset: -90, // Start chart from the top (12 o'clock)
),
// Optional animation when data changes
swapAnimationDuration: const Duration(milliseconds: 250),
swapAnimationCurve: Curves.easeInOut,
),
borderData: FlBorderData(show: false), // No border around the chart
sectionsSpace: 2, // Space between slices
centerSpaceRadius: 50, // Radius of the center hole
sections: _generatePieSections(context), // Generate slices data
startDegreeOffset: -90, // Start chart from the top (12 o'clock)
),
// Optional animation when data changes
swapAnimationDuration: const Duration(milliseconds: 250),
swapAnimationCurve: Curves.easeInOut,
// --- Center Text (Displayed when a slice is selected) ---
if (selectedPieIndex != -1)
_buildCenterText(context, currencyFormatter)
else // Optional: Display total or default text when nothing is selected
_buildDefaultCenterText(context, currencyFormatter),
],
),
),
const SizedBox(width: 8), // Spacing between chart and legend
@@ -116,11 +127,77 @@ class SpendingPieChart extends StatelessWidget {
);
}
// Builds the text displayed in the center when a slice is selected
Widget _buildCenterText(BuildContext context, NumberFormat formatter) {
final theme = Theme.of(context);
// Check if selectedPieIndex is valid before accessing categories
if (selectedPieIndex < 0 || selectedPieIndex >= categories.length) {
// Return an empty container or default text if index is invalid
return _buildDefaultCenterText(context, formatter);
}
final selectedCategory = categories[selectedPieIndex];
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
selectedCategory.name,
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.textTheme.bodyLarge?.color?.withOpacity(0.8),
),
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
formatter.format(selectedCategory.amount), // Format the amount as currency
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: selectedCategory.colorCode, // Use category color for amount
),
textAlign: TextAlign.center,
),
],
);
}
// Builds the default text displayed in the center when no slice is selected
Widget _buildDefaultCenterText(BuildContext context, NumberFormat formatter) {
final theme = Theme.of(context);
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Total', // Label for the total amount
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.textTheme.bodyLarge?.color?.withOpacity(0.7),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 4),
Text(
formatter.format(totalExpenses), // Display total expenses
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.textTheme.bodyLarge?.color // Use default text color
),
textAlign: TextAlign.center,
),
],
);
}
// Builds a single item for the legend
Widget _buildPieLegendItem(BuildContext context, int index) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final theme = Theme.of(context);
final isSelected = index == selectedPieIndex; // Check if this item is selected
// Check if index is valid before accessing categories
if (index < 0 || index >= categories.length) {
return const SizedBox.shrink(); // Return empty if index is invalid
}
final category = categories[index];
// Calculate percentage, handle totalExpenses being zero
final percentage = totalExpenses > 0 ? (category.amount / totalExpenses * 100) : 0.0;
@@ -188,6 +265,12 @@ class SpendingPieChart extends StatelessWidget {
final isDark = Theme.of(context).brightness == Brightness.dark;
return List.generate(categories.length, (i) {
// Check if index is valid before accessing categories
if (i < 0 || i >= categories.length) {
// This should ideally not happen if List.generate is used correctly,
// but adding a safeguard.
return PieChartSectionData(); // Return an empty section
}
final isTouched = i == selectedPieIndex; // Check if this slice is selected
// Make selected slice slightly larger
final double radius = isTouched ? 65 : 55;
-8
View File
@@ -539,14 +539,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.0"
rxdart:
dependency: "direct main"
description:
name: rxdart
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
url: "https://pub.dev"
source: hosted
version: "0.28.0"
shelf:
dependency: transitive
description:
+8 -8
View File
@@ -19,7 +19,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1
environment:
sdk: ^3.7.0
sdk: ^3.7.0 # Drift 2.18 requires Dart 3.4+, Drift 2.16 requires Dart 3.3+
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
@@ -32,11 +32,11 @@ dependencies:
sdk: flutter
fl_chart: ^0.71.0
intl: ^0.19.0 # For date formatting
drift: ^2.26.0 # Added Drift
sqlite3_flutter_libs: ^0.5.22 # Recommended for Drift on Flutter
path_provider: ^2.1.3 # To find database file location
path: ^1.9.0 # To construct database file path
rxdart: ^0.28.0 # Added rxdart for combining streams in database
drift: ^2.18.0 # Updated Drift - основной пакет
sqlite3_flutter_libs: ^0.5.24 # Needed for native platforms
path_provider: ^2.1.3 # To find database file location on native
path: ^1.9.0 # To construct database file path on native
# rxdart: ^0.28.0 # Removed as it wasn't used in database.dart
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
@@ -44,8 +44,8 @@ dependencies:
dev_dependencies:
flutter_test:
sdk: flutter
drift_dev: ^2.18.0 # Added Drift code generator
build_runner: ^2.4.11 # Added build_runner
drift_dev: ^2.18.0 # Updated Drift code generator
build_runner: ^2.4.11 # Updated build_runner
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is