mirror of
https://github.com/koloideal/Casha.git
synced 2026-06-10 10:25:28 +03:00
update
This commit is contained in:
@@ -8,12 +8,32 @@ import 'tables.dart';
|
|||||||
|
|
||||||
part 'app_database.g.dart';
|
part 'app_database.g.dart';
|
||||||
|
|
||||||
@DriftDatabase(tables: [Transactions, Categories, Budgets, ExchangeRates])
|
@DriftDatabase(tables: [Transactions, Categories, Budgets, ExchangeRates, Accounts])
|
||||||
class AppDatabase extends _$AppDatabase {
|
class AppDatabase extends _$AppDatabase {
|
||||||
AppDatabase() : super(_openConnection());
|
AppDatabase() : super(_openConnection());
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 1;
|
int get schemaVersion => 3;
|
||||||
|
|
||||||
|
@override
|
||||||
|
MigrationStrategy get migration => MigrationStrategy(
|
||||||
|
onUpgrade: (migrator, from, to) async {
|
||||||
|
if (from == 1) {
|
||||||
|
await migrator.createTable(accounts);
|
||||||
|
await customStatement(
|
||||||
|
'INSERT INTO accounts (name, is_main, currency, sort_order, created_at) '
|
||||||
|
'VALUES (?, ?, ?, ?, ?)',
|
||||||
|
['Main', 1, 'USD', 0, DateTime.now().millisecondsSinceEpoch],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (from == 2) {
|
||||||
|
// Add currency column to existing accounts table
|
||||||
|
await customStatement(
|
||||||
|
'ALTER TABLE accounts ADD COLUMN currency TEXT NOT NULL DEFAULT "USD"',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TRANSACTIONS
|
// TRANSACTIONS
|
||||||
|
|||||||
@@ -1850,6 +1850,396 @@ class ExchangeRatesCompanion extends UpdateCompanion<ExchangeRate> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class $AccountsTable extends Accounts with TableInfo<$AccountsTable, Account> {
|
||||||
|
@override
|
||||||
|
final GeneratedDatabase attachedDatabase;
|
||||||
|
final String? _alias;
|
||||||
|
$AccountsTable(this.attachedDatabase, [this._alias]);
|
||||||
|
static const VerificationMeta _idMeta = const VerificationMeta('id');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<int> id = GeneratedColumn<int>(
|
||||||
|
'id',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
hasAutoIncrement: true,
|
||||||
|
type: DriftSqlType.int,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultConstraints: GeneratedColumn.constraintIsAlways(
|
||||||
|
'PRIMARY KEY AUTOINCREMENT',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
static const VerificationMeta _nameMeta = const VerificationMeta('name');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<String> name = GeneratedColumn<String>(
|
||||||
|
'name',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: DriftSqlType.string,
|
||||||
|
requiredDuringInsert: true,
|
||||||
|
);
|
||||||
|
static const VerificationMeta _isMainMeta = const VerificationMeta('isMain');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<bool> isMain = GeneratedColumn<bool>(
|
||||||
|
'is_main',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: DriftSqlType.bool,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultConstraints: GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("is_main" IN (0, 1))',
|
||||||
|
),
|
||||||
|
defaultValue: const Constant(false),
|
||||||
|
);
|
||||||
|
static const VerificationMeta _sortOrderMeta = const VerificationMeta(
|
||||||
|
'sortOrder',
|
||||||
|
);
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<int> sortOrder = GeneratedColumn<int>(
|
||||||
|
'sort_order',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: DriftSqlType.int,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultValue: const Constant(0),
|
||||||
|
);
|
||||||
|
static const VerificationMeta _currencyMeta = const VerificationMeta(
|
||||||
|
'currency',
|
||||||
|
);
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<String> currency = GeneratedColumn<String>(
|
||||||
|
'currency',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: DriftSqlType.string,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultValue: const Constant('USD'),
|
||||||
|
);
|
||||||
|
static const VerificationMeta _createdAtMeta = const VerificationMeta(
|
||||||
|
'createdAt',
|
||||||
|
);
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>(
|
||||||
|
'created_at',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: DriftSqlType.dateTime,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultValue: currentDateAndTime,
|
||||||
|
);
|
||||||
|
@override
|
||||||
|
List<GeneratedColumn> get $columns => [
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
isMain,
|
||||||
|
sortOrder,
|
||||||
|
currency,
|
||||||
|
createdAt,
|
||||||
|
];
|
||||||
|
@override
|
||||||
|
String get aliasedName => _alias ?? actualTableName;
|
||||||
|
@override
|
||||||
|
String get actualTableName => $name;
|
||||||
|
static const String $name = 'accounts';
|
||||||
|
@override
|
||||||
|
VerificationContext validateIntegrity(
|
||||||
|
Insertable<Account> instance, {
|
||||||
|
bool isInserting = false,
|
||||||
|
}) {
|
||||||
|
final context = VerificationContext();
|
||||||
|
final data = instance.toColumns(true);
|
||||||
|
if (data.containsKey('id')) {
|
||||||
|
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
|
||||||
|
}
|
||||||
|
if (data.containsKey('name')) {
|
||||||
|
context.handle(
|
||||||
|
_nameMeta,
|
||||||
|
name.isAcceptableOrUnknown(data['name']!, _nameMeta),
|
||||||
|
);
|
||||||
|
} else if (isInserting) {
|
||||||
|
context.missing(_nameMeta);
|
||||||
|
}
|
||||||
|
if (data.containsKey('is_main')) {
|
||||||
|
context.handle(
|
||||||
|
_isMainMeta,
|
||||||
|
isMain.isAcceptableOrUnknown(data['is_main']!, _isMainMeta),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (data.containsKey('sort_order')) {
|
||||||
|
context.handle(
|
||||||
|
_sortOrderMeta,
|
||||||
|
sortOrder.isAcceptableOrUnknown(data['sort_order']!, _sortOrderMeta),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (data.containsKey('currency')) {
|
||||||
|
context.handle(
|
||||||
|
_currencyMeta,
|
||||||
|
currency.isAcceptableOrUnknown(data['currency']!, _currencyMeta),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (data.containsKey('created_at')) {
|
||||||
|
context.handle(
|
||||||
|
_createdAtMeta,
|
||||||
|
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<GeneratedColumn> get $primaryKey => {id};
|
||||||
|
@override
|
||||||
|
Account map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||||
|
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||||
|
return Account(
|
||||||
|
id: attachedDatabase.typeMapping.read(
|
||||||
|
DriftSqlType.int,
|
||||||
|
data['${effectivePrefix}id'],
|
||||||
|
)!,
|
||||||
|
name: attachedDatabase.typeMapping.read(
|
||||||
|
DriftSqlType.string,
|
||||||
|
data['${effectivePrefix}name'],
|
||||||
|
)!,
|
||||||
|
isMain: attachedDatabase.typeMapping.read(
|
||||||
|
DriftSqlType.bool,
|
||||||
|
data['${effectivePrefix}is_main'],
|
||||||
|
)!,
|
||||||
|
sortOrder: attachedDatabase.typeMapping.read(
|
||||||
|
DriftSqlType.int,
|
||||||
|
data['${effectivePrefix}sort_order'],
|
||||||
|
)!,
|
||||||
|
currency: attachedDatabase.typeMapping.read(
|
||||||
|
DriftSqlType.string,
|
||||||
|
data['${effectivePrefix}currency'],
|
||||||
|
)!,
|
||||||
|
createdAt: attachedDatabase.typeMapping.read(
|
||||||
|
DriftSqlType.dateTime,
|
||||||
|
data['${effectivePrefix}created_at'],
|
||||||
|
)!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
$AccountsTable createAlias(String alias) {
|
||||||
|
return $AccountsTable(attachedDatabase, alias);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Account extends DataClass implements Insertable<Account> {
|
||||||
|
final int id;
|
||||||
|
final String name;
|
||||||
|
final bool isMain;
|
||||||
|
final int sortOrder;
|
||||||
|
final String currency;
|
||||||
|
final DateTime createdAt;
|
||||||
|
const Account({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.isMain,
|
||||||
|
required this.sortOrder,
|
||||||
|
required this.currency,
|
||||||
|
required this.createdAt,
|
||||||
|
});
|
||||||
|
@override
|
||||||
|
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||||
|
final map = <String, Expression>{};
|
||||||
|
map['id'] = Variable<int>(id);
|
||||||
|
map['name'] = Variable<String>(name);
|
||||||
|
map['is_main'] = Variable<bool>(isMain);
|
||||||
|
map['sort_order'] = Variable<int>(sortOrder);
|
||||||
|
map['currency'] = Variable<String>(currency);
|
||||||
|
map['created_at'] = Variable<DateTime>(createdAt);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
AccountsCompanion toCompanion(bool nullToAbsent) {
|
||||||
|
return AccountsCompanion(
|
||||||
|
id: Value(id),
|
||||||
|
name: Value(name),
|
||||||
|
isMain: Value(isMain),
|
||||||
|
sortOrder: Value(sortOrder),
|
||||||
|
currency: Value(currency),
|
||||||
|
createdAt: Value(createdAt),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory Account.fromJson(
|
||||||
|
Map<String, dynamic> json, {
|
||||||
|
ValueSerializer? serializer,
|
||||||
|
}) {
|
||||||
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
|
return Account(
|
||||||
|
id: serializer.fromJson<int>(json['id']),
|
||||||
|
name: serializer.fromJson<String>(json['name']),
|
||||||
|
isMain: serializer.fromJson<bool>(json['isMain']),
|
||||||
|
sortOrder: serializer.fromJson<int>(json['sortOrder']),
|
||||||
|
currency: serializer.fromJson<String>(json['currency']),
|
||||||
|
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||||
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
|
return <String, dynamic>{
|
||||||
|
'id': serializer.toJson<int>(id),
|
||||||
|
'name': serializer.toJson<String>(name),
|
||||||
|
'isMain': serializer.toJson<bool>(isMain),
|
||||||
|
'sortOrder': serializer.toJson<int>(sortOrder),
|
||||||
|
'currency': serializer.toJson<String>(currency),
|
||||||
|
'createdAt': serializer.toJson<DateTime>(createdAt),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Account copyWith({
|
||||||
|
int? id,
|
||||||
|
String? name,
|
||||||
|
bool? isMain,
|
||||||
|
int? sortOrder,
|
||||||
|
String? currency,
|
||||||
|
DateTime? createdAt,
|
||||||
|
}) => Account(
|
||||||
|
id: id ?? this.id,
|
||||||
|
name: name ?? this.name,
|
||||||
|
isMain: isMain ?? this.isMain,
|
||||||
|
sortOrder: sortOrder ?? this.sortOrder,
|
||||||
|
currency: currency ?? this.currency,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
);
|
||||||
|
Account copyWithCompanion(AccountsCompanion data) {
|
||||||
|
return Account(
|
||||||
|
id: data.id.present ? data.id.value : this.id,
|
||||||
|
name: data.name.present ? data.name.value : this.name,
|
||||||
|
isMain: data.isMain.present ? data.isMain.value : this.isMain,
|
||||||
|
sortOrder: data.sortOrder.present ? data.sortOrder.value : this.sortOrder,
|
||||||
|
currency: data.currency.present ? data.currency.value : this.currency,
|
||||||
|
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return (StringBuffer('Account(')
|
||||||
|
..write('id: $id, ')
|
||||||
|
..write('name: $name, ')
|
||||||
|
..write('isMain: $isMain, ')
|
||||||
|
..write('sortOrder: $sortOrder, ')
|
||||||
|
..write('currency: $currency, ')
|
||||||
|
..write('createdAt: $createdAt')
|
||||||
|
..write(')'))
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
Object.hash(id, name, isMain, sortOrder, currency, createdAt);
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
(other is Account &&
|
||||||
|
other.id == this.id &&
|
||||||
|
other.name == this.name &&
|
||||||
|
other.isMain == this.isMain &&
|
||||||
|
other.sortOrder == this.sortOrder &&
|
||||||
|
other.currency == this.currency &&
|
||||||
|
other.createdAt == this.createdAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
class AccountsCompanion extends UpdateCompanion<Account> {
|
||||||
|
final Value<int> id;
|
||||||
|
final Value<String> name;
|
||||||
|
final Value<bool> isMain;
|
||||||
|
final Value<int> sortOrder;
|
||||||
|
final Value<String> currency;
|
||||||
|
final Value<DateTime> createdAt;
|
||||||
|
const AccountsCompanion({
|
||||||
|
this.id = const Value.absent(),
|
||||||
|
this.name = const Value.absent(),
|
||||||
|
this.isMain = const Value.absent(),
|
||||||
|
this.sortOrder = const Value.absent(),
|
||||||
|
this.currency = const Value.absent(),
|
||||||
|
this.createdAt = const Value.absent(),
|
||||||
|
});
|
||||||
|
AccountsCompanion.insert({
|
||||||
|
this.id = const Value.absent(),
|
||||||
|
required String name,
|
||||||
|
this.isMain = const Value.absent(),
|
||||||
|
this.sortOrder = const Value.absent(),
|
||||||
|
this.currency = const Value.absent(),
|
||||||
|
this.createdAt = const Value.absent(),
|
||||||
|
}) : name = Value(name);
|
||||||
|
static Insertable<Account> custom({
|
||||||
|
Expression<int>? id,
|
||||||
|
Expression<String>? name,
|
||||||
|
Expression<bool>? isMain,
|
||||||
|
Expression<int>? sortOrder,
|
||||||
|
Expression<String>? currency,
|
||||||
|
Expression<DateTime>? createdAt,
|
||||||
|
}) {
|
||||||
|
return RawValuesInsertable({
|
||||||
|
if (id != null) 'id': id,
|
||||||
|
if (name != null) 'name': name,
|
||||||
|
if (isMain != null) 'is_main': isMain,
|
||||||
|
if (sortOrder != null) 'sort_order': sortOrder,
|
||||||
|
if (currency != null) 'currency': currency,
|
||||||
|
if (createdAt != null) 'created_at': createdAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
AccountsCompanion copyWith({
|
||||||
|
Value<int>? id,
|
||||||
|
Value<String>? name,
|
||||||
|
Value<bool>? isMain,
|
||||||
|
Value<int>? sortOrder,
|
||||||
|
Value<String>? currency,
|
||||||
|
Value<DateTime>? createdAt,
|
||||||
|
}) {
|
||||||
|
return AccountsCompanion(
|
||||||
|
id: id ?? this.id,
|
||||||
|
name: name ?? this.name,
|
||||||
|
isMain: isMain ?? this.isMain,
|
||||||
|
sortOrder: sortOrder ?? this.sortOrder,
|
||||||
|
currency: currency ?? this.currency,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||||
|
final map = <String, Expression>{};
|
||||||
|
if (id.present) {
|
||||||
|
map['id'] = Variable<int>(id.value);
|
||||||
|
}
|
||||||
|
if (name.present) {
|
||||||
|
map['name'] = Variable<String>(name.value);
|
||||||
|
}
|
||||||
|
if (isMain.present) {
|
||||||
|
map['is_main'] = Variable<bool>(isMain.value);
|
||||||
|
}
|
||||||
|
if (sortOrder.present) {
|
||||||
|
map['sort_order'] = Variable<int>(sortOrder.value);
|
||||||
|
}
|
||||||
|
if (currency.present) {
|
||||||
|
map['currency'] = Variable<String>(currency.value);
|
||||||
|
}
|
||||||
|
if (createdAt.present) {
|
||||||
|
map['created_at'] = Variable<DateTime>(createdAt.value);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return (StringBuffer('AccountsCompanion(')
|
||||||
|
..write('id: $id, ')
|
||||||
|
..write('name: $name, ')
|
||||||
|
..write('isMain: $isMain, ')
|
||||||
|
..write('sortOrder: $sortOrder, ')
|
||||||
|
..write('currency: $currency, ')
|
||||||
|
..write('createdAt: $createdAt')
|
||||||
|
..write(')'))
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
abstract class _$AppDatabase extends GeneratedDatabase {
|
abstract class _$AppDatabase extends GeneratedDatabase {
|
||||||
_$AppDatabase(QueryExecutor e) : super(e);
|
_$AppDatabase(QueryExecutor e) : super(e);
|
||||||
$AppDatabaseManager get managers => $AppDatabaseManager(this);
|
$AppDatabaseManager get managers => $AppDatabaseManager(this);
|
||||||
@@ -1857,6 +2247,7 @@ abstract class _$AppDatabase extends GeneratedDatabase {
|
|||||||
late final $CategoriesTable categories = $CategoriesTable(this);
|
late final $CategoriesTable categories = $CategoriesTable(this);
|
||||||
late final $BudgetsTable budgets = $BudgetsTable(this);
|
late final $BudgetsTable budgets = $BudgetsTable(this);
|
||||||
late final $ExchangeRatesTable exchangeRates = $ExchangeRatesTable(this);
|
late final $ExchangeRatesTable exchangeRates = $ExchangeRatesTable(this);
|
||||||
|
late final $AccountsTable accounts = $AccountsTable(this);
|
||||||
@override
|
@override
|
||||||
Iterable<TableInfo<Table, Object?>> get allTables =>
|
Iterable<TableInfo<Table, Object?>> get allTables =>
|
||||||
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
|
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
|
||||||
@@ -1866,6 +2257,7 @@ abstract class _$AppDatabase extends GeneratedDatabase {
|
|||||||
categories,
|
categories,
|
||||||
budgets,
|
budgets,
|
||||||
exchangeRates,
|
exchangeRates,
|
||||||
|
accounts,
|
||||||
];
|
];
|
||||||
@override
|
@override
|
||||||
DriftDatabaseOptions get options =>
|
DriftDatabaseOptions get options =>
|
||||||
@@ -2825,6 +3217,213 @@ typedef $$ExchangeRatesTableProcessedTableManager =
|
|||||||
ExchangeRate,
|
ExchangeRate,
|
||||||
PrefetchHooks Function()
|
PrefetchHooks Function()
|
||||||
>;
|
>;
|
||||||
|
typedef $$AccountsTableCreateCompanionBuilder =
|
||||||
|
AccountsCompanion Function({
|
||||||
|
Value<int> id,
|
||||||
|
required String name,
|
||||||
|
Value<bool> isMain,
|
||||||
|
Value<int> sortOrder,
|
||||||
|
Value<String> currency,
|
||||||
|
Value<DateTime> createdAt,
|
||||||
|
});
|
||||||
|
typedef $$AccountsTableUpdateCompanionBuilder =
|
||||||
|
AccountsCompanion Function({
|
||||||
|
Value<int> id,
|
||||||
|
Value<String> name,
|
||||||
|
Value<bool> isMain,
|
||||||
|
Value<int> sortOrder,
|
||||||
|
Value<String> currency,
|
||||||
|
Value<DateTime> createdAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
class $$AccountsTableFilterComposer
|
||||||
|
extends Composer<_$AppDatabase, $AccountsTable> {
|
||||||
|
$$AccountsTableFilterComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
ColumnFilters<int> get id => $composableBuilder(
|
||||||
|
column: $table.id,
|
||||||
|
builder: (column) => ColumnFilters(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
ColumnFilters<String> get name => $composableBuilder(
|
||||||
|
column: $table.name,
|
||||||
|
builder: (column) => ColumnFilters(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
ColumnFilters<bool> get isMain => $composableBuilder(
|
||||||
|
column: $table.isMain,
|
||||||
|
builder: (column) => ColumnFilters(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
ColumnFilters<int> get sortOrder => $composableBuilder(
|
||||||
|
column: $table.sortOrder,
|
||||||
|
builder: (column) => ColumnFilters(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
ColumnFilters<String> get currency => $composableBuilder(
|
||||||
|
column: $table.currency,
|
||||||
|
builder: (column) => ColumnFilters(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
ColumnFilters<DateTime> get createdAt => $composableBuilder(
|
||||||
|
column: $table.createdAt,
|
||||||
|
builder: (column) => ColumnFilters(column),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$AccountsTableOrderingComposer
|
||||||
|
extends Composer<_$AppDatabase, $AccountsTable> {
|
||||||
|
$$AccountsTableOrderingComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
ColumnOrderings<int> get id => $composableBuilder(
|
||||||
|
column: $table.id,
|
||||||
|
builder: (column) => ColumnOrderings(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
ColumnOrderings<String> get name => $composableBuilder(
|
||||||
|
column: $table.name,
|
||||||
|
builder: (column) => ColumnOrderings(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
ColumnOrderings<bool> get isMain => $composableBuilder(
|
||||||
|
column: $table.isMain,
|
||||||
|
builder: (column) => ColumnOrderings(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
ColumnOrderings<int> get sortOrder => $composableBuilder(
|
||||||
|
column: $table.sortOrder,
|
||||||
|
builder: (column) => ColumnOrderings(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
ColumnOrderings<String> get currency => $composableBuilder(
|
||||||
|
column: $table.currency,
|
||||||
|
builder: (column) => ColumnOrderings(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
ColumnOrderings<DateTime> get createdAt => $composableBuilder(
|
||||||
|
column: $table.createdAt,
|
||||||
|
builder: (column) => ColumnOrderings(column),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$AccountsTableAnnotationComposer
|
||||||
|
extends Composer<_$AppDatabase, $AccountsTable> {
|
||||||
|
$$AccountsTableAnnotationComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
GeneratedColumn<int> get id =>
|
||||||
|
$composableBuilder(column: $table.id, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumn<String> get name =>
|
||||||
|
$composableBuilder(column: $table.name, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumn<bool> get isMain =>
|
||||||
|
$composableBuilder(column: $table.isMain, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumn<int> get sortOrder =>
|
||||||
|
$composableBuilder(column: $table.sortOrder, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumn<String> get currency =>
|
||||||
|
$composableBuilder(column: $table.currency, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumn<DateTime> get createdAt =>
|
||||||
|
$composableBuilder(column: $table.createdAt, builder: (column) => column);
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$AccountsTableTableManager
|
||||||
|
extends
|
||||||
|
RootTableManager<
|
||||||
|
_$AppDatabase,
|
||||||
|
$AccountsTable,
|
||||||
|
Account,
|
||||||
|
$$AccountsTableFilterComposer,
|
||||||
|
$$AccountsTableOrderingComposer,
|
||||||
|
$$AccountsTableAnnotationComposer,
|
||||||
|
$$AccountsTableCreateCompanionBuilder,
|
||||||
|
$$AccountsTableUpdateCompanionBuilder,
|
||||||
|
(Account, BaseReferences<_$AppDatabase, $AccountsTable, Account>),
|
||||||
|
Account,
|
||||||
|
PrefetchHooks Function()
|
||||||
|
> {
|
||||||
|
$$AccountsTableTableManager(_$AppDatabase db, $AccountsTable table)
|
||||||
|
: super(
|
||||||
|
TableManagerState(
|
||||||
|
db: db,
|
||||||
|
table: table,
|
||||||
|
createFilteringComposer: () =>
|
||||||
|
$$AccountsTableFilterComposer($db: db, $table: table),
|
||||||
|
createOrderingComposer: () =>
|
||||||
|
$$AccountsTableOrderingComposer($db: db, $table: table),
|
||||||
|
createComputedFieldComposer: () =>
|
||||||
|
$$AccountsTableAnnotationComposer($db: db, $table: table),
|
||||||
|
updateCompanionCallback:
|
||||||
|
({
|
||||||
|
Value<int> id = const Value.absent(),
|
||||||
|
Value<String> name = const Value.absent(),
|
||||||
|
Value<bool> isMain = const Value.absent(),
|
||||||
|
Value<int> sortOrder = const Value.absent(),
|
||||||
|
Value<String> currency = const Value.absent(),
|
||||||
|
Value<DateTime> createdAt = const Value.absent(),
|
||||||
|
}) => AccountsCompanion(
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
isMain: isMain,
|
||||||
|
sortOrder: sortOrder,
|
||||||
|
currency: currency,
|
||||||
|
createdAt: createdAt,
|
||||||
|
),
|
||||||
|
createCompanionCallback:
|
||||||
|
({
|
||||||
|
Value<int> id = const Value.absent(),
|
||||||
|
required String name,
|
||||||
|
Value<bool> isMain = const Value.absent(),
|
||||||
|
Value<int> sortOrder = const Value.absent(),
|
||||||
|
Value<String> currency = const Value.absent(),
|
||||||
|
Value<DateTime> createdAt = const Value.absent(),
|
||||||
|
}) => AccountsCompanion.insert(
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
isMain: isMain,
|
||||||
|
sortOrder: sortOrder,
|
||||||
|
currency: currency,
|
||||||
|
createdAt: createdAt,
|
||||||
|
),
|
||||||
|
withReferenceMapper: (p0) => p0
|
||||||
|
.map((e) => (e.readTable(table), BaseReferences(db, table, e)))
|
||||||
|
.toList(),
|
||||||
|
prefetchHooksCallback: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef $$AccountsTableProcessedTableManager =
|
||||||
|
ProcessedTableManager<
|
||||||
|
_$AppDatabase,
|
||||||
|
$AccountsTable,
|
||||||
|
Account,
|
||||||
|
$$AccountsTableFilterComposer,
|
||||||
|
$$AccountsTableOrderingComposer,
|
||||||
|
$$AccountsTableAnnotationComposer,
|
||||||
|
$$AccountsTableCreateCompanionBuilder,
|
||||||
|
$$AccountsTableUpdateCompanionBuilder,
|
||||||
|
(Account, BaseReferences<_$AppDatabase, $AccountsTable, Account>),
|
||||||
|
Account,
|
||||||
|
PrefetchHooks Function()
|
||||||
|
>;
|
||||||
|
|
||||||
class $AppDatabaseManager {
|
class $AppDatabaseManager {
|
||||||
final _$AppDatabase _db;
|
final _$AppDatabase _db;
|
||||||
@@ -2837,4 +3436,6 @@ class $AppDatabaseManager {
|
|||||||
$$BudgetsTableTableManager(_db, _db.budgets);
|
$$BudgetsTableTableManager(_db, _db.budgets);
|
||||||
$$ExchangeRatesTableTableManager get exchangeRates =>
|
$$ExchangeRatesTableTableManager get exchangeRates =>
|
||||||
$$ExchangeRatesTableTableManager(_db, _db.exchangeRates);
|
$$ExchangeRatesTableTableManager(_db, _db.exchangeRates);
|
||||||
|
$$AccountsTableTableManager get accounts =>
|
||||||
|
$$AccountsTableTableManager(_db, _db.accounts);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,3 +47,13 @@ class ExchangeRates extends Table {
|
|||||||
RealColumn get rate => real()();
|
RealColumn get rate => real()();
|
||||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
|
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Accounts table for multi-account support
|
||||||
|
class Accounts extends Table {
|
||||||
|
IntColumn get id => integer().autoIncrement()();
|
||||||
|
TextColumn get name => text()();
|
||||||
|
BoolColumn get isMain => boolean().withDefault(const Constant(false))();
|
||||||
|
IntColumn get sortOrder => integer().withDefault(const Constant(0))();
|
||||||
|
TextColumn get currency => text().withDefault(const Constant('USD'))();
|
||||||
|
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import '../database/app_database.dart';
|
||||||
|
import '../../shared/models/account.dart' as model;
|
||||||
|
|
||||||
|
class AccountLimitException implements Exception {
|
||||||
|
final String message;
|
||||||
|
AccountLimitException(this.message);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'AccountLimitException: $message';
|
||||||
|
}
|
||||||
|
|
||||||
|
class AccountRepository {
|
||||||
|
final AppDatabase _db;
|
||||||
|
|
||||||
|
AccountRepository(this._db);
|
||||||
|
|
||||||
|
Stream<List<model.Account>> watchAll() {
|
||||||
|
return (_db.select(_db.accounts)
|
||||||
|
..orderBy([(a) => OrderingTerm.asc(a.sortOrder)]))
|
||||||
|
.watch()
|
||||||
|
.asyncMap((rows) async {
|
||||||
|
if (rows.isEmpty) {
|
||||||
|
// Fallback: insert default account if none exists
|
||||||
|
await _db.into(_db.accounts).insert(
|
||||||
|
AccountsCompanion.insert(
|
||||||
|
name: 'Main',
|
||||||
|
isMain: const Value(true),
|
||||||
|
currency: const Value('USD'),
|
||||||
|
sortOrder: const Value(0),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
// Re-query after insert
|
||||||
|
final newRows = await (_db.select(_db.accounts)
|
||||||
|
..orderBy([(a) => OrderingTerm.asc(a.sortOrder)]))
|
||||||
|
.get();
|
||||||
|
return newRows
|
||||||
|
.map((row) => model.Account(
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
isMain: row.isMain,
|
||||||
|
sortOrder: row.sortOrder,
|
||||||
|
currency: row.currency,
|
||||||
|
createdAt: row.createdAt,
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
return rows
|
||||||
|
.map((row) => model.Account(
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
isMain: row.isMain,
|
||||||
|
sortOrder: row.sortOrder,
|
||||||
|
currency: row.currency,
|
||||||
|
createdAt: row.createdAt,
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<model.Account>> getAll() async {
|
||||||
|
try {
|
||||||
|
var rows = await (_db.select(_db.accounts)
|
||||||
|
..orderBy([(a) => OrderingTerm.asc(a.sortOrder)]))
|
||||||
|
.get();
|
||||||
|
|
||||||
|
print('AccountRepository.getAll(): rows.length = ${rows.length}');
|
||||||
|
|
||||||
|
// Fallback: insert default account if none exists
|
||||||
|
if (rows.isEmpty) {
|
||||||
|
print('AccountRepository.getAll(): inserting default account');
|
||||||
|
try {
|
||||||
|
await _db.into(_db.accounts).insert(
|
||||||
|
AccountsCompanion.insert(
|
||||||
|
name: 'Main',
|
||||||
|
isMain: const Value(true),
|
||||||
|
currency: const Value('USD'),
|
||||||
|
sortOrder: const Value(0),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
print('AccountRepository.getAll(): default account inserted');
|
||||||
|
} catch (e) {
|
||||||
|
print('AccountRepository.getAll(): insert error: $e');
|
||||||
|
}
|
||||||
|
// Re-query after insert
|
||||||
|
rows = await (_db.select(_db.accounts)
|
||||||
|
..orderBy([(a) => OrderingTerm.asc(a.sortOrder)]))
|
||||||
|
.get();
|
||||||
|
print('AccountRepository.getAll(): after insert, rows.length = ${rows.length}');
|
||||||
|
}
|
||||||
|
|
||||||
|
final accounts = rows
|
||||||
|
.map((row) => model.Account(
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
isMain: row.isMain,
|
||||||
|
sortOrder: row.sortOrder,
|
||||||
|
currency: row.currency,
|
||||||
|
createdAt: row.createdAt,
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
print('AccountRepository.getAll(): returning ${accounts.length} accounts');
|
||||||
|
return accounts;
|
||||||
|
} catch (e, stack) {
|
||||||
|
print('AccountRepository.getAll(): error: $e');
|
||||||
|
print('Stack: $stack');
|
||||||
|
// Return empty list on error
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<model.Account> getMain() async {
|
||||||
|
final row = await (_db.select(_db.accounts)
|
||||||
|
..where((a) => a.isMain.equals(true)))
|
||||||
|
.getSingle();
|
||||||
|
|
||||||
|
return model.Account(
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
isMain: row.isMain,
|
||||||
|
sortOrder: row.sortOrder,
|
||||||
|
currency: row.currency,
|
||||||
|
createdAt: row.createdAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,13 +9,19 @@ import '../../core/l10n/app_strings.dart';
|
|||||||
import '../../core/l10n/locale_provider.dart';
|
import '../../core/l10n/locale_provider.dart';
|
||||||
import '../../core/services/haptic_service.dart';
|
import '../../core/services/haptic_service.dart';
|
||||||
import '../../shared/models/transaction.dart';
|
import '../../shared/models/transaction.dart';
|
||||||
import '../../shared/widgets/error_snackbar.dart';
|
|
||||||
import '../dashboard/provider.dart';
|
import '../dashboard/provider.dart';
|
||||||
import '../settings/provider.dart';
|
import '../settings/provider.dart';
|
||||||
import 'provider.dart';
|
import 'provider.dart';
|
||||||
|
|
||||||
const _uuid = Uuid();
|
const _uuid = Uuid();
|
||||||
|
|
||||||
|
// Provider to get main account name
|
||||||
|
final mainAccountNameProvider = FutureProvider<String>((ref) async {
|
||||||
|
final repository = ref.watch(accountRepositoryProvider);
|
||||||
|
final mainAccount = await repository.getMain();
|
||||||
|
return mainAccount.name;
|
||||||
|
});
|
||||||
|
|
||||||
class AddTransactionScreen extends ConsumerStatefulWidget {
|
class AddTransactionScreen extends ConsumerStatefulWidget {
|
||||||
final Transaction? initial;
|
final Transaction? initial;
|
||||||
|
|
||||||
@@ -220,6 +226,7 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen>
|
|||||||
final categories = ref.watch(availableCategoriesProvider(widget.initial));
|
final categories = ref.watch(availableCategoriesProvider(widget.initial));
|
||||||
final overrideCurrency = state.overrideCurrency;
|
final overrideCurrency = state.overrideCurrency;
|
||||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
final accountNameAsync = ref.watch(mainAccountNameProvider);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
@@ -271,6 +278,19 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen>
|
|||||||
child: ListView(
|
child: ListView(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
children: [
|
children: [
|
||||||
|
accountNameAsync.when(
|
||||||
|
data: (accountName) => Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 16),
|
||||||
|
child: Text(
|
||||||
|
'Account: $accountName',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
loading: () => const SizedBox.shrink(),
|
||||||
|
error: (_, __) => const SizedBox.shrink(),
|
||||||
|
),
|
||||||
_TypeToggle(
|
_TypeToggle(
|
||||||
selected: state.type,
|
selected: state.type,
|
||||||
strings: s,
|
strings: s,
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import '../../core/services/card_color_service.dart';
|
|||||||
import '../../core/utils/result.dart';
|
import '../../core/utils/result.dart';
|
||||||
import '../../data/database/app_database.dart' as db;
|
import '../../data/database/app_database.dart' as db;
|
||||||
import '../../data/repositories/transaction_repository.dart';
|
import '../../data/repositories/transaction_repository.dart';
|
||||||
|
import '../../data/repositories/account_repository.dart';
|
||||||
import '../../shared/models/transaction.dart';
|
import '../../shared/models/transaction.dart';
|
||||||
|
import '../../shared/models/account.dart';
|
||||||
import '../../shared/services/storage_service.dart';
|
import '../../shared/services/storage_service.dart';
|
||||||
import '../settings/provider.dart';
|
import '../settings/provider.dart';
|
||||||
|
|
||||||
@@ -22,6 +24,11 @@ final transactionRepositoryProvider = Provider<TransactionRepository>((ref) {
|
|||||||
return TransactionRepository(db);
|
return TransactionRepository(db);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final accountRepositoryProvider = Provider<AccountRepository>((ref) {
|
||||||
|
final db = ref.watch(appDatabaseProvider);
|
||||||
|
return AccountRepository(db);
|
||||||
|
});
|
||||||
|
|
||||||
final storageServiceProvider = Provider<StorageService>((ref) {
|
final storageServiceProvider = Provider<StorageService>((ref) {
|
||||||
return StorageService(ref.watch(sharedPreferencesProvider));
|
return StorageService(ref.watch(sharedPreferencesProvider));
|
||||||
});
|
});
|
||||||
@@ -217,6 +224,18 @@ final recentTransactionsProvider = Provider<List<Transaction>>((ref) {
|
|||||||
return ref.watch(filteredTransactionsProvider).take(20).toList();
|
return ref.watch(filteredTransactionsProvider).take(20).toList();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Watches the list of all accounts
|
||||||
|
final accountsProvider = StreamProvider<List<Account>>((ref) async* {
|
||||||
|
final repository = ref.watch(accountRepositoryProvider);
|
||||||
|
while (true) {
|
||||||
|
yield await repository.getAll();
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ephemeral UI state — active carousel index, starts at 0, not persisted
|
||||||
|
final activeAccountIndexProvider = StateProvider<int>((ref) => 0);
|
||||||
|
|
||||||
class CardColors {
|
class CardColors {
|
||||||
final Color primary;
|
final Color primary;
|
||||||
final Color secondary;
|
final Color secondary;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import '../../core/services/card_color_service.dart';
|
|||||||
import '../../core/services/haptic_service.dart';
|
import '../../core/services/haptic_service.dart';
|
||||||
import '../settings/provider.dart';
|
import '../settings/provider.dart';
|
||||||
import 'provider.dart';
|
import 'provider.dart';
|
||||||
import 'widgets/balance_card.dart';
|
import 'widgets/balance_card_carousel.dart';
|
||||||
import 'widgets/budget_progress.dart';
|
import 'widgets/budget_progress.dart';
|
||||||
import 'widgets/color_editor_overlay.dart';
|
import 'widgets/color_editor_overlay.dart';
|
||||||
import 'widgets/filter_chips.dart';
|
import 'widgets/filter_chips.dart';
|
||||||
@@ -183,7 +183,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
BalanceCard(
|
BalanceCardCarousel(
|
||||||
balance: balance,
|
balance: balance,
|
||||||
currencyInfo: currencyInfo,
|
currencyInfo: currencyInfo,
|
||||||
onLongPress: _onCardLongPress,
|
onLongPress: _onCardLongPress,
|
||||||
|
|||||||
@@ -0,0 +1,309 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import '../../../core/services/card_color_service.dart';
|
||||||
|
import '../../settings/provider.dart';
|
||||||
|
import '../provider.dart';
|
||||||
|
import 'balance_card.dart';
|
||||||
|
|
||||||
|
class BalanceCardCarousel extends ConsumerStatefulWidget {
|
||||||
|
final double balance;
|
||||||
|
final CurrencyInfo currencyInfo;
|
||||||
|
final VoidCallback? onLongPress;
|
||||||
|
final Color? previewPrimary;
|
||||||
|
final Color? previewSecondary;
|
||||||
|
final GradientType? previewGradientType;
|
||||||
|
|
||||||
|
const BalanceCardCarousel({
|
||||||
|
super.key,
|
||||||
|
required this.balance,
|
||||||
|
required this.currencyInfo,
|
||||||
|
this.onLongPress,
|
||||||
|
this.previewPrimary,
|
||||||
|
this.previewSecondary,
|
||||||
|
this.previewGradientType,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<BalanceCardCarousel> createState() =>
|
||||||
|
_BalanceCardCarouselState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BalanceCardCarouselState extends ConsumerState<BalanceCardCarousel> {
|
||||||
|
late PageController _pageController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_pageController = PageController(viewportFraction: 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_pageController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final accountsAsync = ref.watch(accountsProvider);
|
||||||
|
final activeIndex = ref.watch(activeAccountIndexProvider);
|
||||||
|
|
||||||
|
return accountsAsync.when(
|
||||||
|
data: (accounts) {
|
||||||
|
// Debug logging
|
||||||
|
debugPrint('BalanceCardCarousel: accounts.length = ${accounts.length}');
|
||||||
|
if (accounts.isNotEmpty) {
|
||||||
|
debugPrint('BalanceCardCarousel: first account = ${accounts[0].name}');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Page 0: Total balance
|
||||||
|
// Pages 1..N: Account cards
|
||||||
|
// Page N+1: AddAccountCard (if < 5 accounts)
|
||||||
|
final totalPages = 1 + accounts.length + (accounts.length < 5 ? 1 : 0);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: 220,
|
||||||
|
child: PageView.builder(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
controller: _pageController,
|
||||||
|
itemCount: totalPages,
|
||||||
|
onPageChanged: (index) {
|
||||||
|
ref.read(activeAccountIndexProvider.notifier).state = index;
|
||||||
|
},
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
Widget pageContent;
|
||||||
|
|
||||||
|
if (index == 0) {
|
||||||
|
// Page 0: Total balance card
|
||||||
|
pageContent = BalanceCard(
|
||||||
|
balance: widget.balance,
|
||||||
|
currencyInfo: widget.currencyInfo,
|
||||||
|
onLongPress: widget.onLongPress,
|
||||||
|
previewPrimary: widget.previewPrimary,
|
||||||
|
previewSecondary: widget.previewSecondary,
|
||||||
|
previewGradientType: widget.previewGradientType,
|
||||||
|
);
|
||||||
|
} else if (index <= accounts.length) {
|
||||||
|
// Pages 1..N: Account cards
|
||||||
|
final account = accounts[index - 1];
|
||||||
|
debugPrint('BalanceCardCarousel: building account card at index $index for ${account.name}');
|
||||||
|
pageContent = _AccountBalanceCard(
|
||||||
|
account: account,
|
||||||
|
balance: widget.balance, // TODO: Calculate per-account balance
|
||||||
|
currencyInfo: widget.currencyInfo,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Page N+1: AddAccountCard
|
||||||
|
pageContent = AddAccountCard(
|
||||||
|
onTap: () {},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add horizontal padding to create gap between cards
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||||
|
child: pageContent,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_DotIndicators(
|
||||||
|
count: totalPages,
|
||||||
|
activeIndex: activeIndex,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
loading: () => const SizedBox(
|
||||||
|
height: 220,
|
||||||
|
child: Center(child: CircularProgressIndicator()),
|
||||||
|
),
|
||||||
|
error: (error, stack) {
|
||||||
|
debugPrint('BalanceCardCarousel error: $error');
|
||||||
|
debugPrint('Stack: $stack');
|
||||||
|
// Show total balance card on error
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: 220,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||||
|
child: BalanceCard(
|
||||||
|
balance: widget.balance,
|
||||||
|
currencyInfo: widget.currencyInfo,
|
||||||
|
onLongPress: widget.onLongPress,
|
||||||
|
previewPrimary: widget.previewPrimary,
|
||||||
|
previewSecondary: widget.previewSecondary,
|
||||||
|
previewGradientType: widget.previewGradientType,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_DotIndicators(
|
||||||
|
count: 1,
|
||||||
|
activeIndex: 0,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AccountBalanceCard extends ConsumerWidget {
|
||||||
|
final dynamic account;
|
||||||
|
final double balance;
|
||||||
|
final CurrencyInfo currencyInfo;
|
||||||
|
|
||||||
|
const _AccountBalanceCard({
|
||||||
|
required this.account,
|
||||||
|
required this.balance,
|
||||||
|
required this.currencyInfo,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
BalanceCard(
|
||||||
|
balance: balance,
|
||||||
|
currencyInfo: currencyInfo,
|
||||||
|
onLongPress: null, // No long press for account cards
|
||||||
|
previewPrimary: null,
|
||||||
|
previewSecondary: null,
|
||||||
|
previewGradientType: null,
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: 16,
|
||||||
|
left: 24,
|
||||||
|
child: Text(
|
||||||
|
account.name,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
letterSpacing: 1.5,
|
||||||
|
color: Colors.white.withOpacity(0.6),
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AddAccountCard extends StatelessWidget {
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
|
const AddAccountCard({super.key, this.onTap});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: CustomPaint(
|
||||||
|
painter: _DashedBorderPainter(),
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 220,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surface.withOpacity(0.4),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.add_rounded,
|
||||||
|
size: 32,
|
||||||
|
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Add account',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5),
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DashedBorderPainter extends CustomPainter {
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final paint = Paint()
|
||||||
|
..color = const Color(0xFF888888)
|
||||||
|
..strokeWidth = 2
|
||||||
|
..style = PaintingStyle.stroke;
|
||||||
|
|
||||||
|
const dashWidth = 8.0;
|
||||||
|
const dashSpace = 4.0;
|
||||||
|
const radius = 20.0;
|
||||||
|
|
||||||
|
final path = Path()
|
||||||
|
..addRRect(
|
||||||
|
RRect.fromRectAndRadius(
|
||||||
|
Rect.fromLTWH(0, 0, size.width, size.height),
|
||||||
|
const Radius.circular(radius),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final pathMetrics = path.computeMetrics();
|
||||||
|
for (final metric in pathMetrics) {
|
||||||
|
double distance = 0;
|
||||||
|
while (distance < metric.length) {
|
||||||
|
final segment = metric.extractPath(
|
||||||
|
distance,
|
||||||
|
distance + dashWidth,
|
||||||
|
);
|
||||||
|
canvas.drawPath(segment, paint);
|
||||||
|
distance += dashWidth + dashSpace;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DotIndicators extends StatelessWidget {
|
||||||
|
final int count;
|
||||||
|
final int activeIndex;
|
||||||
|
|
||||||
|
const _DotIndicators({
|
||||||
|
required this.count,
|
||||||
|
required this.activeIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: List.generate(count, (index) {
|
||||||
|
final isActive = index == activeIndex;
|
||||||
|
return AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
|
width: isActive ? 8 : 6,
|
||||||
|
height: isActive ? 8 : 6,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: isActive
|
||||||
|
? const Color(0xFF7C6DED)
|
||||||
|
: Theme.of(context).colorScheme.onSurface.withOpacity(0.2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
class Account {
|
||||||
|
final int id;
|
||||||
|
final String name;
|
||||||
|
final bool isMain;
|
||||||
|
final int sortOrder;
|
||||||
|
final String currency;
|
||||||
|
final DateTime createdAt;
|
||||||
|
|
||||||
|
const Account({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.isMain,
|
||||||
|
required this.sortOrder,
|
||||||
|
required this.currency,
|
||||||
|
required this.createdAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user