From 65ba31b0367a133bda536e89f96c2d92cfea1e9d Mon Sep 17 00:00:00 2001 From: enriqueloz88 Date: Fri, 2 Feb 2024 13:04:56 +0100 Subject: [PATCH 1/5] feat: Add account display order to the DB --- assets/sql/migrations/v6.sql | 4 +- lib/app/accounts/account_form.dart | 3 +- lib/app/settings/import_csv.dart | 1 + lib/core/database/app_db.dart | 3 +- lib/core/database/app_db.g.dart | 47 ++++++++++++++++++- .../services/account/account_service.dart | 3 +- lib/core/database/sql/initial/tables.drift | 3 ++ lib/core/models/account/account.dart | 28 ++++++----- lib/i18n/translations.g.dart | 2 +- 9 files changed, 74 insertions(+), 20 deletions(-) diff --git a/assets/sql/migrations/v6.sql b/assets/sql/migrations/v6.sql index d62dcdb8..3beb1c46 100644 --- a/assets/sql/migrations/v6.sql +++ b/assets/sql/migrations/v6.sql @@ -1,3 +1,5 @@ DELETE FROM userSettings WHERE settingKey = 'transactionMobileMode'; -ALTER TABLE accounts ADD COLUMN color TEXT; \ No newline at end of file +ALTER TABLE accounts ADD COLUMN color TEXT; + +ALTER TABLE accounts ADD COLUMN displayOrder INTEGER NOT NULL DEFAULT 0; \ No newline at end of file diff --git a/lib/app/accounts/account_form.dart b/lib/app/accounts/account_form.dart index 37a90c84..f3520520 100644 --- a/lib/app/accounts/account_form.dart +++ b/lib/app/accounts/account_form.dart @@ -27,7 +27,7 @@ import 'package:uuid/uuid.dart'; @RoutePage() class AccountFormPage extends StatefulWidget { - const AccountFormPage({Key? key, this.account}) : super(key: key); + const AccountFormPage({super.key, this.account}); /// Account UUID to edit (if any) final Account? account; @@ -86,6 +86,7 @@ class _AccountFormPageState extends State { Account accountToSubmit = Account( id: _accountToEdit?.id ?? const Uuid().v4(), name: _nameController.text, + displayOrder: 10, iniValue: newBalance, date: _openingDate, closingDate: _closeDate, diff --git a/lib/app/settings/import_csv.dart b/lib/app/settings/import_csv.dart index 058cbb74..bf851b8f 100644 --- a/lib/app/settings/import_csv.dart +++ b/lib/app/settings/import_csv.dart @@ -197,6 +197,7 @@ class _ImportCSVPageState extends State { id: accountID, name: row[accountColumn!].toString(), iniValue: 0, + displayOrder: 10, date: DateTime.now(), type: AccountType.normal, iconId: SupportedIconService.instance.defaultSupportedIcon.id, diff --git a/lib/core/database/app_db.dart b/lib/core/database/app_db.dart index ebb61634..a204e3c3 100644 --- a/lib/core/database/app_db.dart +++ b/lib/core/database/app_db.dart @@ -42,7 +42,7 @@ class AppDB extends _$AppDB { Future get databasePath async => join((await getApplicationDocumentsDirectory()).path, dbName); - migrateDB(int from, int to) async { + Future migrateDB(int from, int to) async { print('Executing migrations from previous version...'); for (var i = from + 1; i <= to; i++) { @@ -58,6 +58,7 @@ class AppDB extends _$AppDB { .toList(); for (final sqlStatement in statements) { + print("Running custom statement: $sqlStatement"); await customStatement(sqlStatement); } diff --git a/lib/core/database/app_db.g.dart b/lib/core/database/app_db.g.dart index 1f9fc0e8..aeb05860 100644 --- a/lib/core/database/app_db.g.dart +++ b/lib/core/database/app_db.g.dart @@ -244,6 +244,13 @@ class Accounts extends Table with TableInfo { type: DriftSqlType.string, requiredDuringInsert: true, $customConstraints: 'NOT NULL'); + static const VerificationMeta _displayOrderMeta = + const VerificationMeta('displayOrder'); + late final GeneratedColumn displayOrder = GeneratedColumn( + 'displayOrder', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL'); static const VerificationMeta _colorMeta = const VerificationMeta('color'); late final GeneratedColumn color = GeneratedColumn( 'color', aliasedName, true, @@ -286,6 +293,7 @@ class Accounts extends Table with TableInfo { description, type, iconId, + displayOrder, color, closingDate, currencyId, @@ -338,6 +346,14 @@ class Accounts extends Table with TableInfo { } else if (isInserting) { context.missing(_iconIdMeta); } + if (data.containsKey('displayOrder')) { + context.handle( + _displayOrderMeta, + displayOrder.isAcceptableOrUnknown( + data['displayOrder']!, _displayOrderMeta)); + } else if (isInserting) { + context.missing(_displayOrderMeta); + } if (data.containsKey('color')) { context.handle( _colorMeta, color.isAcceptableOrUnknown(data['color']!, _colorMeta)); @@ -387,6 +403,8 @@ class Accounts extends Table with TableInfo { .read(DriftSqlType.string, data['${effectivePrefix}type'])!), iconId: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}iconId'])!, + displayOrder: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}displayOrder'])!, color: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}color']), closingDate: attachedDatabase.typeMapping @@ -424,6 +442,9 @@ class AccountInDB extends DataClass implements Insertable { final AccountType type; final String iconId; + /// The display order when listing accounts + final int displayOrder; + /// If null, an automatic color will be applied final String? color; @@ -442,6 +463,7 @@ class AccountInDB extends DataClass implements Insertable { this.description, required this.type, required this.iconId, + required this.displayOrder, this.color, this.closingDate, required this.currencyId, @@ -462,6 +484,7 @@ class AccountInDB extends DataClass implements Insertable { map['type'] = Variable(converter.toSql(type)); } map['iconId'] = Variable(iconId); + map['displayOrder'] = Variable(displayOrder); if (!nullToAbsent || color != null) { map['color'] = Variable(color); } @@ -489,6 +512,7 @@ class AccountInDB extends DataClass implements Insertable { : Value(description), type: Value(type), iconId: Value(iconId), + displayOrder: Value(displayOrder), color: color == null && nullToAbsent ? const Value.absent() : Value(color), closingDate: closingDate == null && nullToAbsent @@ -513,6 +537,7 @@ class AccountInDB extends DataClass implements Insertable { type: Accounts.$convertertype .fromJson(serializer.fromJson(json['type'])), iconId: serializer.fromJson(json['iconId']), + displayOrder: serializer.fromJson(json['displayOrder']), color: serializer.fromJson(json['color']), closingDate: serializer.fromJson(json['closingDate']), currencyId: serializer.fromJson(json['currencyId']), @@ -531,6 +556,7 @@ class AccountInDB extends DataClass implements Insertable { 'description': serializer.toJson(description), 'type': serializer.toJson(Accounts.$convertertype.toJson(type)), 'iconId': serializer.toJson(iconId), + 'displayOrder': serializer.toJson(displayOrder), 'color': serializer.toJson(color), 'closingDate': serializer.toJson(closingDate), 'currencyId': serializer.toJson(currencyId), @@ -547,6 +573,7 @@ class AccountInDB extends DataClass implements Insertable { Value description = const Value.absent(), AccountType? type, String? iconId, + int? displayOrder, Value color = const Value.absent(), Value closingDate = const Value.absent(), String? currencyId, @@ -560,6 +587,7 @@ class AccountInDB extends DataClass implements Insertable { description: description.present ? description.value : this.description, type: type ?? this.type, iconId: iconId ?? this.iconId, + displayOrder: displayOrder ?? this.displayOrder, color: color.present ? color.value : this.color, closingDate: closingDate.present ? closingDate.value : this.closingDate, currencyId: currencyId ?? this.currencyId, @@ -576,6 +604,7 @@ class AccountInDB extends DataClass implements Insertable { ..write('description: $description, ') ..write('type: $type, ') ..write('iconId: $iconId, ') + ..write('displayOrder: $displayOrder, ') ..write('color: $color, ') ..write('closingDate: $closingDate, ') ..write('currencyId: $currencyId, ') @@ -587,7 +616,7 @@ class AccountInDB extends DataClass implements Insertable { @override int get hashCode => Object.hash(id, name, iniValue, date, description, type, - iconId, color, closingDate, currencyId, iban, swift); + iconId, displayOrder, color, closingDate, currencyId, iban, swift); @override bool operator ==(Object other) => identical(this, other) || @@ -599,6 +628,7 @@ class AccountInDB extends DataClass implements Insertable { other.description == this.description && other.type == this.type && other.iconId == this.iconId && + other.displayOrder == this.displayOrder && other.color == this.color && other.closingDate == this.closingDate && other.currencyId == this.currencyId && @@ -614,6 +644,7 @@ class AccountsCompanion extends UpdateCompanion { final Value description; final Value type; final Value iconId; + final Value displayOrder; final Value color; final Value closingDate; final Value currencyId; @@ -628,6 +659,7 @@ class AccountsCompanion extends UpdateCompanion { this.description = const Value.absent(), this.type = const Value.absent(), this.iconId = const Value.absent(), + this.displayOrder = const Value.absent(), this.color = const Value.absent(), this.closingDate = const Value.absent(), this.currencyId = const Value.absent(), @@ -643,6 +675,7 @@ class AccountsCompanion extends UpdateCompanion { this.description = const Value.absent(), required AccountType type, required String iconId, + required int displayOrder, this.color = const Value.absent(), this.closingDate = const Value.absent(), required String currencyId, @@ -655,6 +688,7 @@ class AccountsCompanion extends UpdateCompanion { date = Value(date), type = Value(type), iconId = Value(iconId), + displayOrder = Value(displayOrder), currencyId = Value(currencyId); static Insertable custom({ Expression? id, @@ -664,6 +698,7 @@ class AccountsCompanion extends UpdateCompanion { Expression? description, Expression? type, Expression? iconId, + Expression? displayOrder, Expression? color, Expression? closingDate, Expression? currencyId, @@ -679,6 +714,7 @@ class AccountsCompanion extends UpdateCompanion { if (description != null) 'description': description, if (type != null) 'type': type, if (iconId != null) 'iconId': iconId, + if (displayOrder != null) 'displayOrder': displayOrder, if (color != null) 'color': color, if (closingDate != null) 'closingDate': closingDate, if (currencyId != null) 'currencyId': currencyId, @@ -696,6 +732,7 @@ class AccountsCompanion extends UpdateCompanion { Value? description, Value? type, Value? iconId, + Value? displayOrder, Value? color, Value? closingDate, Value? currencyId, @@ -710,6 +747,7 @@ class AccountsCompanion extends UpdateCompanion { description: description ?? this.description, type: type ?? this.type, iconId: iconId ?? this.iconId, + displayOrder: displayOrder ?? this.displayOrder, color: color ?? this.color, closingDate: closingDate ?? this.closingDate, currencyId: currencyId ?? this.currencyId, @@ -745,6 +783,9 @@ class AccountsCompanion extends UpdateCompanion { if (iconId.present) { map['iconId'] = Variable(iconId.value); } + if (displayOrder.present) { + map['displayOrder'] = Variable(displayOrder.value); + } if (color.present) { map['color'] = Variable(color.value); } @@ -776,6 +817,7 @@ class AccountsCompanion extends UpdateCompanion { ..write('description: $description, ') ..write('type: $type, ') ..write('iconId: $iconId, ') + ..write('displayOrder: $displayOrder, ') ..write('color: $color, ') ..write('closingDate: $closingDate, ') ..write('currencyId: $currencyId, ') @@ -4138,6 +4180,7 @@ abstract class _$AppDB extends GeneratedDatabase { iniValue: row.read('iniValue'), date: row.read('date'), type: Accounts.$convertertype.fromSql(row.read('type')), + displayOrder: row.read('displayOrder'), iconId: row.read('iconId'), currency: await currencies.mapFromRow(row, tablePrefix: 'nested_0'), closingDate: row.readNullable('closingDate'), @@ -4191,7 +4234,7 @@ abstract class _$AppDB extends GeneratedDatabase { startIndex: $arrayStartIndex); $arrayStartIndex += generatedlimit.amountOfVariables; return customSelect( - 'SELECT t.*,"a"."id" AS "nested_0.id", "a"."name" AS "nested_0.name", "a"."iniValue" AS "nested_0.iniValue", "a"."date" AS "nested_0.date", "a"."description" AS "nested_0.description", "a"."type" AS "nested_0.type", "a"."iconId" AS "nested_0.iconId", "a"."color" AS "nested_0.color", "a"."closingDate" AS "nested_0.closingDate", "a"."currencyId" AS "nested_0.currencyId", "a"."iban" AS "nested_0.iban", "a"."swift" AS "nested_0.swift","accountCurrency"."code" AS "nested_1.code", "accountCurrency"."symbol" AS "nested_1.symbol","receivingAccountCurrency"."code" AS "nested_2.code", "receivingAccountCurrency"."symbol" AS "nested_2.symbol","ra"."id" AS "nested_3.id", "ra"."name" AS "nested_3.name", "ra"."iniValue" AS "nested_3.iniValue", "ra"."date" AS "nested_3.date", "ra"."description" AS "nested_3.description", "ra"."type" AS "nested_3.type", "ra"."iconId" AS "nested_3.iconId", "ra"."color" AS "nested_3.color", "ra"."closingDate" AS "nested_3.closingDate", "ra"."currencyId" AS "nested_3.currencyId", "ra"."iban" AS "nested_3.iban", "ra"."swift" AS "nested_3.swift","c"."id" AS "nested_4.id", "c"."name" AS "nested_4.name", "c"."iconId" AS "nested_4.iconId", "c"."color" AS "nested_4.color", "c"."type" AS "nested_4.type", "c"."parentCategoryID" AS "nested_4.parentCategoryID","pc"."id" AS "nested_5.id", "pc"."name" AS "nested_5.name", "pc"."iconId" AS "nested_5.iconId", "pc"."color" AS "nested_5.color", "pc"."type" AS "nested_5.type", "pc"."parentCategoryID" AS "nested_5.parentCategoryID", t.value * COALESCE(excRate.exchangeRate, 1) AS currentValueInPreferredCurrency, t.valueInDestiny * COALESCE(excRateOfDestiny.exchangeRate, 1) AS currentValueInDestinyInPreferredCurrency, t.id AS "\$n_0" FROM transactions AS t INNER JOIN accounts AS a ON t.accountID = a.id INNER JOIN currencies AS accountCurrency ON a.currencyId = accountCurrency.code LEFT JOIN accounts AS ra ON t.receivingAccountID = ra.id INNER JOIN currencies AS receivingAccountCurrency ON a.currencyId = receivingAccountCurrency.code LEFT JOIN categories AS c ON t.categoryID = c.id LEFT JOIN categories AS pc ON c.parentCategoryID = pc.id LEFT JOIN (SELECT currencyCode, exchangeRate FROM exchangeRates AS er WHERE date = (SELECT MAX(date) FROM exchangeRates WHERE currencyCode = er.currencyCode AND DATE <= DATE(\'now\')) ORDER BY currencyCode) AS excRate ON a.currencyId = excRate.currencyCode LEFT JOIN (SELECT currencyCode, exchangeRate FROM exchangeRates AS er WHERE date = (SELECT MAX(date) FROM exchangeRates WHERE currencyCode = er.currencyCode AND DATE <= DATE(\'now\')) ORDER BY currencyCode) AS excRateOfDestiny ON ra.currencyId = excRateOfDestiny.currencyCode WHERE ${generatedpredicate.sql} ${generatedorderBy.sql} ${generatedlimit.sql}', + 'SELECT t.*,"a"."id" AS "nested_0.id", "a"."name" AS "nested_0.name", "a"."iniValue" AS "nested_0.iniValue", "a"."date" AS "nested_0.date", "a"."description" AS "nested_0.description", "a"."type" AS "nested_0.type", "a"."iconId" AS "nested_0.iconId", "a"."displayOrder" AS "nested_0.displayOrder", "a"."color" AS "nested_0.color", "a"."closingDate" AS "nested_0.closingDate", "a"."currencyId" AS "nested_0.currencyId", "a"."iban" AS "nested_0.iban", "a"."swift" AS "nested_0.swift","accountCurrency"."code" AS "nested_1.code", "accountCurrency"."symbol" AS "nested_1.symbol","receivingAccountCurrency"."code" AS "nested_2.code", "receivingAccountCurrency"."symbol" AS "nested_2.symbol","ra"."id" AS "nested_3.id", "ra"."name" AS "nested_3.name", "ra"."iniValue" AS "nested_3.iniValue", "ra"."date" AS "nested_3.date", "ra"."description" AS "nested_3.description", "ra"."type" AS "nested_3.type", "ra"."iconId" AS "nested_3.iconId", "ra"."displayOrder" AS "nested_3.displayOrder", "ra"."color" AS "nested_3.color", "ra"."closingDate" AS "nested_3.closingDate", "ra"."currencyId" AS "nested_3.currencyId", "ra"."iban" AS "nested_3.iban", "ra"."swift" AS "nested_3.swift","c"."id" AS "nested_4.id", "c"."name" AS "nested_4.name", "c"."iconId" AS "nested_4.iconId", "c"."color" AS "nested_4.color", "c"."type" AS "nested_4.type", "c"."parentCategoryID" AS "nested_4.parentCategoryID","pc"."id" AS "nested_5.id", "pc"."name" AS "nested_5.name", "pc"."iconId" AS "nested_5.iconId", "pc"."color" AS "nested_5.color", "pc"."type" AS "nested_5.type", "pc"."parentCategoryID" AS "nested_5.parentCategoryID", t.value * COALESCE(excRate.exchangeRate, 1) AS currentValueInPreferredCurrency, t.valueInDestiny * COALESCE(excRateOfDestiny.exchangeRate, 1) AS currentValueInDestinyInPreferredCurrency, t.id AS "\$n_0" FROM transactions AS t INNER JOIN accounts AS a ON t.accountID = a.id INNER JOIN currencies AS accountCurrency ON a.currencyId = accountCurrency.code LEFT JOIN accounts AS ra ON t.receivingAccountID = ra.id INNER JOIN currencies AS receivingAccountCurrency ON a.currencyId = receivingAccountCurrency.code LEFT JOIN categories AS c ON t.categoryID = c.id LEFT JOIN categories AS pc ON c.parentCategoryID = pc.id LEFT JOIN (SELECT currencyCode, exchangeRate FROM exchangeRates AS er WHERE date = (SELECT MAX(date) FROM exchangeRates WHERE currencyCode = er.currencyCode AND DATE <= DATE(\'now\')) ORDER BY currencyCode) AS excRate ON a.currencyId = excRate.currencyCode LEFT JOIN (SELECT currencyCode, exchangeRate FROM exchangeRates AS er WHERE date = (SELECT MAX(date) FROM exchangeRates WHERE currencyCode = er.currencyCode AND DATE <= DATE(\'now\')) ORDER BY currencyCode) AS excRateOfDestiny ON ra.currencyId = excRateOfDestiny.currencyCode WHERE ${generatedpredicate.sql} ${generatedorderBy.sql} ${generatedlimit.sql}', variables: [ ...generatedpredicate.introducedVariables, ...generatedorderBy.introducedVariables, diff --git a/lib/core/database/services/account/account_service.dart b/lib/core/database/services/account/account_service.dart index 584a33b3..8da70da0 100644 --- a/lib/core/database/services/account/account_service.dart +++ b/lib/core/database/services/account/account_service.dart @@ -37,7 +37,8 @@ class AccountService { return db .getAccountsWithFullData( predicate: predicate, - orderBy: orderBy, + orderBy: orderBy ?? + (acc, curr) => OrderBy([OrderingTerm.asc(acc.displayOrder)]), limit: (a, currency) => Limit(limit ?? -1, offset), ) .watch(); diff --git a/lib/core/database/sql/initial/tables.drift b/lib/core/database/sql/initial/tables.drift index 8a9759cc..3ab851f2 100644 --- a/lib/core/database/sql/initial/tables.drift +++ b/lib/core/database/sql/initial/tables.drift @@ -49,6 +49,9 @@ CREATE TABLE IF NOT EXISTS accounts ( iconId TEXT NOT NULL, + -- The display order when listing accounts + displayOrder INTEGER NOT NULL, + -- If null, an automatic color will be applied color TEXT, diff --git a/lib/core/models/account/account.dart b/lib/core/models/account/account.dart index e21b0dc1..0038138b 100644 --- a/lib/core/models/account/account.dart +++ b/lib/core/models/account/account.dart @@ -50,19 +50,20 @@ enum AccountType { } class Account extends AccountInDB { - Account( - {required super.id, - required super.name, - required super.iniValue, - required super.date, - required super.type, - required super.iconId, - required this.currency, - super.closingDate, - super.description, - super.iban, - super.swift}) - : super(currencyId: currency.code); + Account({ + required super.id, + required super.name, + required super.iniValue, + required super.date, + required super.type, + required super.displayOrder, + required super.iconId, + required this.currency, + super.closingDate, + super.description, + super.iban, + super.swift, + }) : super(currencyId: currency.code); /// Currency of all the transactions of this account. When you change this currency all transactions in this account /// will have the new currency but their amount/value will remain the same. @@ -102,6 +103,7 @@ class Account extends AccountInDB { currency: currency, iniValue: account.iniValue, date: account.date, + displayOrder: account.displayOrder, description: account.description, iban: account.iban, swift: account.swift, diff --git a/lib/i18n/translations.g.dart b/lib/i18n/translations.g.dart index a5f46db9..c88a90d1 100644 --- a/lib/i18n/translations.g.dart +++ b/lib/i18n/translations.g.dart @@ -6,7 +6,7 @@ /// Locales: 2 /// Strings: 1018 (509 per locale) /// -/// Built on 2024-01-24 at 16:04 UTC +/// Built on 2024-02-02 at 11:31 UTC // coverage:ignore-file // ignore_for_file: type=lint From ca9100155d936ee85b53b70ace0ba2b0009130c0 Mon Sep 17 00:00:00 2001 From: enriqueloz88 Date: Fri, 2 Feb 2024 14:02:47 +0100 Subject: [PATCH 2/5] feat: Reorder accounts --- lib/app/accounts/all_accounts_page.dart | 107 ++++++++++++------ .../accounts/monekin_reorderable_list.dart | 54 +++++++++ lib/core/models/account/account.dart | 2 +- lib/core/presentation/widgets/tappable.dart | 49 ++++++++ 4 files changed, 178 insertions(+), 34 deletions(-) create mode 100644 lib/app/accounts/monekin_reorderable_list.dart create mode 100644 lib/core/presentation/widgets/tappable.dart diff --git a/lib/app/accounts/all_accounts_page.dart b/lib/app/accounts/all_accounts_page.dart index 53c710de..7c01c4ee 100644 --- a/lib/app/accounts/all_accounts_page.dart +++ b/lib/app/accounts/all_accounts_page.dart @@ -1,6 +1,10 @@ import 'package:auto_route/auto_route.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; +import 'package:monekin/app/accounts/monekin_reorderable_list.dart'; import 'package:monekin/core/database/services/account/account_service.dart'; +import 'package:monekin/core/presentation/app_colors.dart'; +import 'package:monekin/core/presentation/widgets/tappable.dart'; import 'package:monekin/core/routes/app_router.dart'; import 'package:monekin/i18n/translations.g.dart'; @@ -22,37 +26,39 @@ class AllAccountsPage extends StatelessWidget { onPressed: () => context.pushRoute(AccountFormRoute()), ), body: StreamBuilder( - stream: AccountService.instance.getAccounts(), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const LinearProgressIndicator(); - } + stream: AccountService.instance.getAccounts(), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const LinearProgressIndicator(); + } - final accounts = snapshot.data!; + final accounts = snapshot.data!; - if (accounts.isEmpty) { - return Column( - children: [ - Expanded( - child: EmptyIndicator( - title: t.general.empty_warn, - description: t.account.no_accounts)), - ], - ); - } + if (accounts.isEmpty) { + return Column( + children: [ + Expanded( + child: EmptyIndicator( + title: t.general.empty_warn, + description: t.account.no_accounts)), + ], + ); + } - return ListView.separated( - padding: EdgeInsets.zero, - itemCount: accounts.length, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - separatorBuilder: (context, index) { - return const Divider(indent: 56); - }, - itemBuilder: (context, index) { - final account = accounts[index]; + return MonekinReorderableList( + itemBuilder: (context, index) { + final account = accounts.elementAt(index); - return ListTile( + return Tappable( + onTap: () => context.pushRoute( + AccountDetailsRoute(account: account), + ), + bgColor: AppColors.of(context).light, + margin: + const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + borderRadius: 12, + child: ListTile( + trailing: ReorderableDragIcon(index: index), title: Row( children: [ Flexible( @@ -72,12 +78,47 @@ class AllAccountsPage extends StatelessWidget { tag: 'account-icon-${account.id}', child: account.displayIcon(context), ), - subtitle: Text(account.type.title(context)), - onTap: () => context - .pushRoute(AccountDetailsRoute(account: account)), - ); - }); - }), + subtitle: Text( + account.type.title(context), + ), + ), + ); + }, + onReorder: (from, to) async { + if (to > from) to--; + + final item = accounts.removeAt(from); + accounts.insert(to, item); + + Future.wait( + accounts.mapIndexed( + (index, element) => AccountService.instance.updateAccount( + element.copyWith(displayOrder: index), + ), + ), + ); + }, + totalItemCount: accounts.length); + }, + ), + ); + } +} + +class ReorderableDragIcon extends StatelessWidget { + const ReorderableDragIcon({super.key, required this.index}); + + final int index; + + @override + Widget build(BuildContext context) { + return ReorderableDragStartListener( + index: index, + child: Container( + padding: const EdgeInsets.fromLTRB(14, 4, 2, 4), + // Padding to increase the dragabble area + //color: Colors.red, + child: const Icon(Icons.drag_handle_rounded)), ); } } diff --git a/lib/app/accounts/monekin_reorderable_list.dart b/lib/app/accounts/monekin_reorderable_list.dart new file mode 100644 index 00000000..e1c259f4 --- /dev/null +++ b/lib/app/accounts/monekin_reorderable_list.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class MonekinReorderableList extends StatefulWidget { + const MonekinReorderableList({ + super.key, + required this.itemBuilder, + required this.onReorder, + required this.totalItemCount, + }); + + final Widget Function(BuildContext context, int index) itemBuilder; + final void Function(int from, int to) onReorder; + + final int totalItemCount; + + @override + State createState() => _MonekinReorderableListState(); +} + +class _MonekinReorderableListState extends State { + int? isOrderingItem; + + @override + Widget build(BuildContext context) { + return ReorderableListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) => Opacity( + key: Key(index.toString()), + opacity: isOrderingItem == null || isOrderingItem == index ? 1 : 0.3, + child: ReorderableDelayedDragStartListener( + index: index, + child: widget.itemBuilder(context, index), + ), + ), + buildDefaultDragHandles: false, + itemCount: widget.totalItemCount, + onReorder: (from, to) => widget.onReorder(from, to), + onReorderStart: (index) { + HapticFeedback.lightImpact(); + + setState(() { + isOrderingItem = index; + }); + }, + onReorderEnd: (index) { + setState(() { + isOrderingItem = null; + }); + }, + ); + } +} diff --git a/lib/core/models/account/account.dart b/lib/core/models/account/account.dart index 0038138b..9418690b 100644 --- a/lib/core/models/account/account.dart +++ b/lib/core/models/account/account.dart @@ -83,7 +83,7 @@ class Account extends AccountInDB { Widget displayIcon( BuildContext context, { - double size = 22, + double size = 24, double? padding, bool isOutline = false, }) { diff --git a/lib/core/presentation/widgets/tappable.dart b/lib/core/presentation/widgets/tappable.dart new file mode 100644 index 00000000..4fbc7829 --- /dev/null +++ b/lib/core/presentation/widgets/tappable.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; + +class Tappable extends StatelessWidget { + const Tappable({ + super.key, + this.bgColor, + this.borderRadius, + this.onTap, + this.shape, + required this.child, + this.onLongPress, + this.onDoubleTap, + this.margin, + }); + + final Color? bgColor; + final double? borderRadius; + final ShapeBorder? shape; + + final EdgeInsets? margin; + + final void Function()? onTap; + final void Function()? onLongPress; + final void Function()? onDoubleTap; + + final Widget child; + + @override + Widget build(BuildContext context) { + return Container( + margin: margin, + child: Material( + color: bgColor, + borderRadius: + borderRadius == null ? null : BorderRadius.circular(borderRadius!), + shape: shape, + child: InkWell( + onTap: onTap, + onLongPress: onLongPress, + onDoubleTap: onDoubleTap, + customBorder: shape, + borderRadius: borderRadius == null + ? null + : BorderRadius.circular(borderRadius!), + child: child), + ), + ); + } +} From 624ac12d8e5c72987d4b9151be7cc4f74d9c2f06 Mon Sep 17 00:00:00 2001 From: enriqueloz88 Date: Fri, 2 Feb 2024 16:11:18 +0100 Subject: [PATCH 3/5] feat: Add display order to tags and categories --- assets/sql/migrations/v6.sql | 5 +- lib/app/accounts/all_accounts_page.dart | 94 ++++----- .../accounts/monekin_reorderable_list.dart | 2 +- lib/app/categories/form/category_form.dart | 4 + .../chart_by_categories.dart | 1 + lib/app/tags/tag_list.dart | 152 -------------- lib/app/tags/tag_list.page.dart | 188 ++++++++++++++++++ .../form/transaction_form.page.dart | 21 +- lib/core/database/app_db.g.dart | 102 +++++++++- .../services/category/category_service.dart | 2 + .../database/services/tags/tags_service.dart | 1 + lib/core/database/sql/initial/tables.drift | 6 + lib/core/models/category/category.dart | 2 + lib/core/models/tags/tag.dart | 1 + lib/core/routes/app_router.dart | 2 +- lib/core/routes/app_router.gr.dart | 8 +- lib/i18n/translations.g.dart | 2 +- 17 files changed, 370 insertions(+), 223 deletions(-) delete mode 100644 lib/app/tags/tag_list.dart create mode 100644 lib/app/tags/tag_list.page.dart diff --git a/assets/sql/migrations/v6.sql b/assets/sql/migrations/v6.sql index 3beb1c46..2f1f3464 100644 --- a/assets/sql/migrations/v6.sql +++ b/assets/sql/migrations/v6.sql @@ -1,5 +1,8 @@ DELETE FROM userSettings WHERE settingKey = 'transactionMobileMode'; ALTER TABLE accounts ADD COLUMN color TEXT; +ALTER TABLE accounts ADD COLUMN displayOrder INTEGER NOT NULL DEFAULT 0; -ALTER TABLE accounts ADD COLUMN displayOrder INTEGER NOT NULL DEFAULT 0; \ No newline at end of file +ALTER TABLE tags ADD COLUMN displayOrder INTEGER NOT NULL DEFAULT 0; + +ALTER TABLE categories ADD COLUMN displayOrder INTEGER NOT NULL DEFAULT 0; \ No newline at end of file diff --git a/lib/app/accounts/all_accounts_page.dart b/lib/app/accounts/all_accounts_page.dart index 7c01c4ee..c314eafd 100644 --- a/lib/app/accounts/all_accounts_page.dart +++ b/lib/app/accounts/all_accounts_page.dart @@ -46,59 +46,59 @@ class AllAccountsPage extends StatelessWidget { } return MonekinReorderableList( - itemBuilder: (context, index) { - final account = accounts.elementAt(index); + totalItemCount: accounts.length, + itemBuilder: (context, index) { + final account = accounts.elementAt(index); - return Tappable( - onTap: () => context.pushRoute( - AccountDetailsRoute(account: account), - ), - bgColor: AppColors.of(context).light, - margin: - const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - borderRadius: 12, - child: ListTile( - trailing: ReorderableDragIcon(index: index), - title: Row( - children: [ - Flexible( - child: Text( - account.name, - softWrap: false, - overflow: TextOverflow.fade, - ), + return Tappable( + onTap: () => context.pushRoute( + AccountDetailsRoute(account: account), + ), + bgColor: AppColors.of(context).light, + margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + borderRadius: 12, + child: ListTile( + trailing: ReorderableDragIcon(index: index), + title: Row( + children: [ + Flexible( + child: Text( + account.name, + softWrap: false, + overflow: TextOverflow.fade, ), - const SizedBox(width: 4), - if (account.isClosed) - const Icon(Icons.archive_outlined, - color: Colors.amber, size: 16) - ], - ), - leading: Hero( - tag: 'account-icon-${account.id}', - child: account.displayIcon(context), - ), - subtitle: Text( - account.type.title(context), - ), + ), + const SizedBox(width: 4), + if (account.isClosed) + const Icon(Icons.archive_outlined, + color: Colors.amber, size: 16) + ], + ), + leading: Hero( + tag: 'account-icon-${account.id}', + child: account.displayIcon(context), + ), + subtitle: Text( + account.type.title(context), ), - ); - }, - onReorder: (from, to) async { - if (to > from) to--; + ), + ); + }, + onReorder: (from, to) async { + if (to > from) to--; - final item = accounts.removeAt(from); - accounts.insert(to, item); + final item = accounts.removeAt(from); + accounts.insert(to, item); - Future.wait( - accounts.mapIndexed( - (index, element) => AccountService.instance.updateAccount( - element.copyWith(displayOrder: index), - ), + Future.wait( + accounts.mapIndexed( + (index, element) => AccountService.instance.updateAccount( + element.copyWith(displayOrder: index), ), - ); - }, - totalItemCount: accounts.length); + ), + ); + }, + ); }, ), ); diff --git a/lib/app/accounts/monekin_reorderable_list.dart b/lib/app/accounts/monekin_reorderable_list.dart index e1c259f4..8c549ef1 100644 --- a/lib/app/accounts/monekin_reorderable_list.dart +++ b/lib/app/accounts/monekin_reorderable_list.dart @@ -28,7 +28,7 @@ class _MonekinReorderableListState extends State { physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) => Opacity( key: Key(index.toString()), - opacity: isOrderingItem == null || isOrderingItem == index ? 1 : 0.3, + opacity: isOrderingItem == null || isOrderingItem == index ? 1 : 0.4, child: ReorderableDelayedDragStartListener( index: index, child: widget.itemBuilder(context, index), diff --git a/lib/app/categories/form/category_form.dart b/lib/app/categories/form/category_form.dart index dbfd93e4..8f9ef1fa 100644 --- a/lib/app/categories/form/category_form.dart +++ b/lib/app/categories/form/category_form.dart @@ -79,6 +79,7 @@ class _CategoryFormPageState extends State { categoryToEdit = Category( id: categoryToEdit!.id, name: _nameController.text, + displayOrder: categoryToEdit!.displayOrder, iconId: _icon.id, color: _color, parentCategory: categoryToEdit!.parentCategory, @@ -112,6 +113,7 @@ class _CategoryFormPageState extends State { id: const Uuid().v4(), name: _nameController.text, iconId: _icon.id, + displayOrder: 10, type: _type, color: _color)) .then((value) { @@ -390,6 +392,7 @@ class _CategoryFormPageState extends State { CategoryService.instance.updateCategory( CategoryInDB( id: subcategory.id, + displayOrder: 10, name: name, iconId: icon.id, parentCategoryID: @@ -413,6 +416,7 @@ class _CategoryFormPageState extends State { CategoryService.instance.insertCategory( CategoryInDB( id: const Uuid().v4(), + displayOrder: 10, name: name, iconId: icon.id, parentCategoryID: categoryToEdit!.id)); diff --git a/lib/app/stats/widgets/movements_distribution/chart_by_categories.dart b/lib/app/stats/widgets/movements_distribution/chart_by_categories.dart index 5157aea1..9770ea8f 100644 --- a/lib/app/stats/widgets/movements_distribution/chart_by_categories.dart +++ b/lib/app/stats/widgets/movements_distribution/chart_by_categories.dart @@ -131,6 +131,7 @@ class _ChartByCategoriesState extends State { category: Category( id: 'Other', name: 'Other', + displayOrder: 1000, iconId: 'iconId', type: CategoryType.B, color: 'DEDEDE')); diff --git a/lib/app/tags/tag_list.dart b/lib/app/tags/tag_list.dart deleted file mode 100644 index e9a6042a..00000000 --- a/lib/app/tags/tag_list.dart +++ /dev/null @@ -1,152 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:monekin/core/database/services/tags/tags_service.dart'; -import 'package:monekin/core/models/tags/tag.dart'; -import 'package:monekin/core/presentation/widgets/bottomSheetFooter.dart'; -import 'package:monekin/core/presentation/widgets/empty_indicator.dart'; -import 'package:monekin/core/presentation/widgets/scrollable_with_bottom_gradient.dart'; -import 'package:monekin/core/routes/app_router.dart'; -import 'package:monekin/i18n/translations.g.dart'; - -Future?> showTagListModal(BuildContext context, TagListPage page) { - return showModalBottomSheet>( - context: context, - isScrollControlled: true, - showDragHandle: true, - builder: (context) { - return DraggableScrollableSheet( - expand: false, - maxChildSize: 0.85, - minChildSize: 0.85, - initialChildSize: 0.85, - builder: (context, scrollController) { - return page; - }); - }, - ); -} - -@RoutePage() -class TagListPage extends StatefulWidget { - const TagListPage({ - super.key, - this.isModal = false, - this.selected = const [], - }); - - final bool isModal; - final List selected; - - @override - State createState() => _TagListPageState(); -} - -class _TagListPageState extends State { - late List selectedTags; - - @override - void initState() { - super.initState(); - - selectedTags = [...widget.selected]; - } - - Widget buildList() { - return StreamBuilder( - stream: TagService.instance.getTags(), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const LinearProgressIndicator(); - } - - if (snapshot.data!.isEmpty) { - return EmptyIndicator( - title: t.general.empty_warn, - description: t.tags.empty_list, - ); - } - - return ListView.separated( - itemBuilder: (context, index) { - final tag = snapshot.data!.elementAt(index); - - if (widget.isModal) { - return CheckboxListTile.adaptive( - value: selectedTags.any((element) => element.id == tag.id), - secondary: tag.displayIcon(), - title: Text(tag.name), - subtitle: - tag.description != null && tag.description!.isNotEmpty - ? Text(tag.description!) - : null, - onChanged: (newValue) { - if (newValue == null) return; - - if (!newValue) { - selectedTags - .removeWhere((element) => element.id == tag.id); - } else { - selectedTags.add(tag); - } - - setState(() {}); - }, - ); - } - - return ListTile( - leading: tag.displayIcon(), - title: Text(tag.name), - subtitle: tag.description != null && tag.description!.isNotEmpty - ? Text(tag.description!) - : null, - onTap: () => context.pushRoute(TagFormRoute(tag: tag)), - ); - }, - separatorBuilder: (context, index) => const Divider(), - itemCount: snapshot.data!.length, - ); - }); - } - - @override - Widget build(BuildContext context) { - final t = Translations.of(context); - - if (widget.isModal) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 6), - child: Text( - t.tags.select, - style: Theme.of(context).textTheme.titleLarge, - ), - ), - Expanded( - child: Stack( - children: [ - buildList(), - ScrollableWithBottomGradient.buildPositionedGradient( - Theme.of(context).dialogBackgroundColor) - ], - ), - ), - BottomSheetFooter( - onSaved: () => Navigator.of(context).pop(selectedTags), - ), - ], - ); - } - - return Scaffold( - appBar: AppBar(title: Text(t.tags.display(n: 10))), - floatingActionButton: FloatingActionButton( - child: const Icon(Icons.add_rounded), - onPressed: () => context.pushRoute(TagFormRoute()), - ), - body: buildList(), - ); - } -} diff --git a/lib/app/tags/tag_list.page.dart b/lib/app/tags/tag_list.page.dart new file mode 100644 index 00000000..d94c413d --- /dev/null +++ b/lib/app/tags/tag_list.page.dart @@ -0,0 +1,188 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:monekin/app/accounts/all_accounts_page.dart'; +import 'package:monekin/app/accounts/monekin_reorderable_list.dart'; +import 'package:monekin/core/database/services/tags/tags_service.dart'; +import 'package:monekin/core/models/tags/tag.dart'; +import 'package:monekin/core/presentation/app_colors.dart'; +import 'package:monekin/core/presentation/widgets/bottomSheetFooter.dart'; +import 'package:monekin/core/presentation/widgets/empty_indicator.dart'; +import 'package:monekin/core/presentation/widgets/modal_container.dart'; +import 'package:monekin/core/presentation/widgets/scrollable_with_bottom_gradient.dart'; +import 'package:monekin/core/presentation/widgets/tappable.dart'; +import 'package:monekin/core/routes/app_router.dart'; +import 'package:monekin/i18n/translations.g.dart'; + +Future?> showTagListModal( + BuildContext context, { + List selectedTags = const [], +}) { + return showModalBottomSheet>( + context: context, + isScrollControlled: true, + showDragHandle: true, + builder: (context) { + return DraggableScrollableSheet( + expand: false, + maxChildSize: 0.85, + minChildSize: 0.65, + initialChildSize: 0.65, + builder: (context, scrollController) { + return TagListPage( + isModal: true, + scrollController: scrollController, + selected: selectedTags, + ); + }); + }, + ); +} + +@RoutePage() +class TagListPage extends StatefulWidget { + const TagListPage({ + super.key, + this.isModal = false, + this.selected = const [], + this.scrollController, + }); + + final bool isModal; + final List selected; + + final ScrollController? scrollController; + + @override + State createState() => _TagListPageState(); +} + +class _TagListPageState extends State { + late List selectedTags; + + @override + void initState() { + super.initState(); + + selectedTags = [...widget.selected]; + } + + Widget buildList() { + return StreamBuilder( + stream: TagService.instance.getTags(), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const LinearProgressIndicator(); + } + + if (snapshot.data!.isEmpty) { + return EmptyIndicator( + title: t.general.empty_warn, + description: t.tags.empty_list, + ); + } + + final tags = snapshot.data!; + + if (!widget.isModal) { + return MonekinReorderableList( + totalItemCount: tags.length, + itemBuilder: (context, index) { + final tag = tags.elementAt(index); + + return Tappable( + onTap: () => context.pushRoute(TagFormRoute(tag: tag)), + bgColor: AppColors.of(context).light, + margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + borderRadius: 12, + child: ListTile( + trailing: ReorderableDragIcon(index: index), + leading: tag.displayIcon(), + title: Text(tag.name), + subtitle: + tag.description != null && tag.description!.isNotEmpty + ? Text(tag.description!) + : null, + ), + ); + }, + onReorder: (from, to) async { + if (to > from) to--; + + final item = tags.removeAt(from); + tags.insert(to, item); + + Future.wait( + tags.mapIndexed( + (index, element) => TagService.instance.updateTag( + element.copyWith(displayOrder: index), + ), + ), + ); + }, + ); + } + + return ListView.separated( + controller: widget.scrollController, + itemBuilder: (context, index) { + final tag = snapshot.data!.elementAt(index); + + return CheckboxListTile.adaptive( + value: selectedTags.any((element) => element.id == tag.id), + secondary: tag.displayIcon(), + title: Text(tag.name), + subtitle: tag.description != null && tag.description!.isNotEmpty + ? Text(tag.description!) + : null, + onChanged: (newValue) { + if (newValue == null) return; + + if (!newValue) { + selectedTags.removeWhere((element) => element.id == tag.id); + } else { + selectedTags.add(tag); + } + + setState(() {}); + }, + ); + }, + separatorBuilder: (context, index) => const Divider(), + itemCount: snapshot.data!.length, + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + final t = Translations.of(context); + + if (widget.isModal) { + return ModalContainer( + title: t.tags.select, + footer: BottomSheetFooter( + onSaved: () => Navigator.of(context).pop(selectedTags), + ), + body: Stack( + children: [ + buildList(), + ScrollableWithBottomGradient.buildPositionedGradient( + Theme.of(context).dialogBackgroundColor) + ], + ), + ); + } + + return Scaffold( + appBar: AppBar(title: Text(t.tags.display(n: 10))), + floatingActionButton: FloatingActionButton.extended( + icon: const Icon(Icons.add_rounded), + label: Text(t.tags.add), + onPressed: () => context.pushRoute(TagFormRoute()), + ), + body: buildList(), + ); + } +} diff --git a/lib/app/transactions/form/transaction_form.page.dart b/lib/app/transactions/form/transaction_form.page.dart index 942cd67d..9c281a14 100644 --- a/lib/app/transactions/form/transaction_form.page.dart +++ b/lib/app/transactions/form/transaction_form.page.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:monekin/app/accounts/account_selector.dart'; import 'package:monekin/app/categories/categories_list.dart'; -import 'package:monekin/app/tags/tag_list.dart'; +import 'package:monekin/app/tags/tag_list.page.dart'; import 'package:monekin/app/transactions/form/calculator_modal.dart'; import 'package:monekin/core/database/app_db.dart'; import 'package:monekin/core/database/services/account/account_service.dart'; @@ -466,15 +466,16 @@ class _TransactionFormPageState extends State { ActionChip( label: Text(t.tags.add), avatar: const Icon(Icons.add), - onPressed: () => showTagListModal( - context, TagListPage(isModal: true, selected: tags)) - .then((value) { - if (value != null) { - setState(() { - tags = value; - }); - } - }), + onPressed: () => + showTagListModal(context, selectedTags: tags).then( + (value) { + if (value != null) { + setState(() { + tags = value; + }); + } + }, + ), ), ], ), diff --git a/lib/core/database/app_db.g.dart b/lib/core/database/app_db.g.dart index aeb05860..0bd7a514 100644 --- a/lib/core/database/app_db.g.dart +++ b/lib/core/database/app_db.g.dart @@ -858,6 +858,13 @@ class Categories extends Table with TableInfo { type: DriftSqlType.string, requiredDuringInsert: false, $customConstraints: ''); + static const VerificationMeta _displayOrderMeta = + const VerificationMeta('displayOrder'); + late final GeneratedColumn displayOrder = GeneratedColumn( + 'displayOrder', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL'); static const VerificationMeta _typeMeta = const VerificationMeta('type'); late final GeneratedColumnWithTypeConverter type = GeneratedColumn('type', aliasedName, true, @@ -875,7 +882,7 @@ class Categories extends Table with TableInfo { 'REFERENCES categories(id)ON UPDATE CASCADE ON DELETE CASCADE'); @override List get $columns => - [id, name, iconId, color, type, parentCategoryID]; + [id, name, iconId, color, displayOrder, type, parentCategoryID]; @override String get aliasedName => _alias ?? actualTableName; @override @@ -907,6 +914,14 @@ class Categories extends Table with TableInfo { context.handle( _colorMeta, color.isAcceptableOrUnknown(data['color']!, _colorMeta)); } + if (data.containsKey('displayOrder')) { + context.handle( + _displayOrderMeta, + displayOrder.isAcceptableOrUnknown( + data['displayOrder']!, _displayOrderMeta)); + } else if (isInserting) { + context.missing(_displayOrderMeta); + } context.handle(_typeMeta, const VerificationResult.success()); if (data.containsKey('parentCategoryID')) { context.handle( @@ -931,6 +946,8 @@ class Categories extends Table with TableInfo { .read(DriftSqlType.string, data['${effectivePrefix}iconId'])!, color: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}color']), + displayOrder: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}displayOrder'])!, type: Categories.$convertertypen.fromSql(attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}type'])), parentCategoryID: attachedDatabase.typeMapping.read( @@ -968,6 +985,9 @@ class CategoryInDB extends DataClass implements Insertable { /// Color that will be used to represent this category in some screens. If null, the color of the parent's category will be used final String? color; + /// The display order when listing categories + final int displayOrder; + /// Type of the category. If null, the type of the parent's category will be used final CategoryType? type; @@ -978,6 +998,7 @@ class CategoryInDB extends DataClass implements Insertable { required this.name, required this.iconId, this.color, + required this.displayOrder, this.type, this.parentCategoryID}); @override @@ -989,6 +1010,7 @@ class CategoryInDB extends DataClass implements Insertable { if (!nullToAbsent || color != null) { map['color'] = Variable(color); } + map['displayOrder'] = Variable(displayOrder); if (!nullToAbsent || type != null) { final converter = Categories.$convertertypen; map['type'] = Variable(converter.toSql(type)); @@ -1006,6 +1028,7 @@ class CategoryInDB extends DataClass implements Insertable { iconId: Value(iconId), color: color == null && nullToAbsent ? const Value.absent() : Value(color), + displayOrder: Value(displayOrder), type: type == null && nullToAbsent ? const Value.absent() : Value(type), parentCategoryID: parentCategoryID == null && nullToAbsent ? const Value.absent() @@ -1021,6 +1044,7 @@ class CategoryInDB extends DataClass implements Insertable { name: serializer.fromJson(json['name']), iconId: serializer.fromJson(json['iconId']), color: serializer.fromJson(json['color']), + displayOrder: serializer.fromJson(json['displayOrder']), type: Categories.$convertertypen .fromJson(serializer.fromJson(json['type'])), parentCategoryID: serializer.fromJson(json['parentCategoryID']), @@ -1034,6 +1058,7 @@ class CategoryInDB extends DataClass implements Insertable { 'name': serializer.toJson(name), 'iconId': serializer.toJson(iconId), 'color': serializer.toJson(color), + 'displayOrder': serializer.toJson(displayOrder), 'type': serializer.toJson(Categories.$convertertypen.toJson(type)), 'parentCategoryID': serializer.toJson(parentCategoryID), @@ -1045,6 +1070,7 @@ class CategoryInDB extends DataClass implements Insertable { String? name, String? iconId, Value color = const Value.absent(), + int? displayOrder, Value type = const Value.absent(), Value parentCategoryID = const Value.absent()}) => CategoryInDB( @@ -1052,6 +1078,7 @@ class CategoryInDB extends DataClass implements Insertable { name: name ?? this.name, iconId: iconId ?? this.iconId, color: color.present ? color.value : this.color, + displayOrder: displayOrder ?? this.displayOrder, type: type.present ? type.value : this.type, parentCategoryID: parentCategoryID.present ? parentCategoryID.value @@ -1064,6 +1091,7 @@ class CategoryInDB extends DataClass implements Insertable { ..write('name: $name, ') ..write('iconId: $iconId, ') ..write('color: $color, ') + ..write('displayOrder: $displayOrder, ') ..write('type: $type, ') ..write('parentCategoryID: $parentCategoryID') ..write(')')) @@ -1071,8 +1099,8 @@ class CategoryInDB extends DataClass implements Insertable { } @override - int get hashCode => - Object.hash(id, name, iconId, color, type, parentCategoryID); + int get hashCode => Object.hash( + id, name, iconId, color, displayOrder, type, parentCategoryID); @override bool operator ==(Object other) => identical(this, other) || @@ -1081,6 +1109,7 @@ class CategoryInDB extends DataClass implements Insertable { other.name == this.name && other.iconId == this.iconId && other.color == this.color && + other.displayOrder == this.displayOrder && other.type == this.type && other.parentCategoryID == this.parentCategoryID); } @@ -1090,6 +1119,7 @@ class CategoriesCompanion extends UpdateCompanion { final Value name; final Value iconId; final Value color; + final Value displayOrder; final Value type; final Value parentCategoryID; final Value rowid; @@ -1098,6 +1128,7 @@ class CategoriesCompanion extends UpdateCompanion { this.name = const Value.absent(), this.iconId = const Value.absent(), this.color = const Value.absent(), + this.displayOrder = const Value.absent(), this.type = const Value.absent(), this.parentCategoryID = const Value.absent(), this.rowid = const Value.absent(), @@ -1107,17 +1138,20 @@ class CategoriesCompanion extends UpdateCompanion { required String name, required String iconId, this.color = const Value.absent(), + required int displayOrder, this.type = const Value.absent(), this.parentCategoryID = const Value.absent(), this.rowid = const Value.absent(), }) : id = Value(id), name = Value(name), - iconId = Value(iconId); + iconId = Value(iconId), + displayOrder = Value(displayOrder); static Insertable custom({ Expression? id, Expression? name, Expression? iconId, Expression? color, + Expression? displayOrder, Expression? type, Expression? parentCategoryID, Expression? rowid, @@ -1127,6 +1161,7 @@ class CategoriesCompanion extends UpdateCompanion { if (name != null) 'name': name, if (iconId != null) 'iconId': iconId, if (color != null) 'color': color, + if (displayOrder != null) 'displayOrder': displayOrder, if (type != null) 'type': type, if (parentCategoryID != null) 'parentCategoryID': parentCategoryID, if (rowid != null) 'rowid': rowid, @@ -1138,6 +1173,7 @@ class CategoriesCompanion extends UpdateCompanion { Value? name, Value? iconId, Value? color, + Value? displayOrder, Value? type, Value? parentCategoryID, Value? rowid}) { @@ -1146,6 +1182,7 @@ class CategoriesCompanion extends UpdateCompanion { name: name ?? this.name, iconId: iconId ?? this.iconId, color: color ?? this.color, + displayOrder: displayOrder ?? this.displayOrder, type: type ?? this.type, parentCategoryID: parentCategoryID ?? this.parentCategoryID, rowid: rowid ?? this.rowid, @@ -1167,6 +1204,9 @@ class CategoriesCompanion extends UpdateCompanion { if (color.present) { map['color'] = Variable(color.value); } + if (displayOrder.present) { + map['displayOrder'] = Variable(displayOrder.value); + } if (type.present) { final converter = Categories.$convertertypen; @@ -1188,6 +1228,7 @@ class CategoriesCompanion extends UpdateCompanion { ..write('name: $name, ') ..write('iconId: $iconId, ') ..write('color: $color, ') + ..write('displayOrder: $displayOrder, ') ..write('type: $type, ') ..write('parentCategoryID: $parentCategoryID, ') ..write('rowid: $rowid') @@ -2273,6 +2314,13 @@ class Tags extends Table with TableInfo { type: DriftSqlType.string, requiredDuringInsert: true, $customConstraints: 'NOT NULL'); + static const VerificationMeta _displayOrderMeta = + const VerificationMeta('displayOrder'); + late final GeneratedColumn displayOrder = GeneratedColumn( + 'displayOrder', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL'); static const VerificationMeta _descriptionMeta = const VerificationMeta('description'); late final GeneratedColumn description = GeneratedColumn( @@ -2281,7 +2329,8 @@ class Tags extends Table with TableInfo { requiredDuringInsert: false, $customConstraints: ''); @override - List get $columns => [id, name, color, description]; + List get $columns => + [id, name, color, displayOrder, description]; @override String get aliasedName => _alias ?? actualTableName; @override @@ -2309,6 +2358,14 @@ class Tags extends Table with TableInfo { } else if (isInserting) { context.missing(_colorMeta); } + if (data.containsKey('displayOrder')) { + context.handle( + _displayOrderMeta, + displayOrder.isAcceptableOrUnknown( + data['displayOrder']!, _displayOrderMeta)); + } else if (isInserting) { + context.missing(_displayOrderMeta); + } if (data.containsKey('description')) { context.handle( _descriptionMeta, @@ -2330,6 +2387,8 @@ class Tags extends Table with TableInfo { .read(DriftSqlType.string, data['${effectivePrefix}name'])!, color: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}color'])!, + displayOrder: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}displayOrder'])!, description: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}description']), ); @@ -2353,12 +2412,16 @@ class TagInDB extends DataClass implements Insertable { /// The display color of the tag final String color; + /// The display order when listing tag + final int displayOrder; + /// The description of the tag final String? description; const TagInDB( {required this.id, required this.name, required this.color, + required this.displayOrder, this.description}); @override Map toColumns(bool nullToAbsent) { @@ -2366,6 +2429,7 @@ class TagInDB extends DataClass implements Insertable { map['id'] = Variable(id); map['name'] = Variable(name); map['color'] = Variable(color); + map['displayOrder'] = Variable(displayOrder); if (!nullToAbsent || description != null) { map['description'] = Variable(description); } @@ -2377,6 +2441,7 @@ class TagInDB extends DataClass implements Insertable { id: Value(id), name: Value(name), color: Value(color), + displayOrder: Value(displayOrder), description: description == null && nullToAbsent ? const Value.absent() : Value(description), @@ -2390,6 +2455,7 @@ class TagInDB extends DataClass implements Insertable { id: serializer.fromJson(json['id']), name: serializer.fromJson(json['name']), color: serializer.fromJson(json['color']), + displayOrder: serializer.fromJson(json['displayOrder']), description: serializer.fromJson(json['description']), ); } @@ -2400,6 +2466,7 @@ class TagInDB extends DataClass implements Insertable { 'id': serializer.toJson(id), 'name': serializer.toJson(name), 'color': serializer.toJson(color), + 'displayOrder': serializer.toJson(displayOrder), 'description': serializer.toJson(description), }; } @@ -2408,11 +2475,13 @@ class TagInDB extends DataClass implements Insertable { {String? id, String? name, String? color, + int? displayOrder, Value description = const Value.absent()}) => TagInDB( id: id ?? this.id, name: name ?? this.name, color: color ?? this.color, + displayOrder: displayOrder ?? this.displayOrder, description: description.present ? description.value : this.description, ); @override @@ -2421,13 +2490,14 @@ class TagInDB extends DataClass implements Insertable { ..write('id: $id, ') ..write('name: $name, ') ..write('color: $color, ') + ..write('displayOrder: $displayOrder, ') ..write('description: $description') ..write(')')) .toString(); } @override - int get hashCode => Object.hash(id, name, color, description); + int get hashCode => Object.hash(id, name, color, displayOrder, description); @override bool operator ==(Object other) => identical(this, other) || @@ -2435,6 +2505,7 @@ class TagInDB extends DataClass implements Insertable { other.id == this.id && other.name == this.name && other.color == this.color && + other.displayOrder == this.displayOrder && other.description == this.description); } @@ -2442,12 +2513,14 @@ class TagsCompanion extends UpdateCompanion { final Value id; final Value name; final Value color; + final Value displayOrder; final Value description; final Value rowid; const TagsCompanion({ this.id = const Value.absent(), this.name = const Value.absent(), this.color = const Value.absent(), + this.displayOrder = const Value.absent(), this.description = const Value.absent(), this.rowid = const Value.absent(), }); @@ -2455,15 +2528,18 @@ class TagsCompanion extends UpdateCompanion { required String id, required String name, required String color, + required int displayOrder, this.description = const Value.absent(), this.rowid = const Value.absent(), }) : id = Value(id), name = Value(name), - color = Value(color); + color = Value(color), + displayOrder = Value(displayOrder); static Insertable custom({ Expression? id, Expression? name, Expression? color, + Expression? displayOrder, Expression? description, Expression? rowid, }) { @@ -2471,6 +2547,7 @@ class TagsCompanion extends UpdateCompanion { if (id != null) 'id': id, if (name != null) 'name': name, if (color != null) 'color': color, + if (displayOrder != null) 'displayOrder': displayOrder, if (description != null) 'description': description, if (rowid != null) 'rowid': rowid, }); @@ -2480,12 +2557,14 @@ class TagsCompanion extends UpdateCompanion { {Value? id, Value? name, Value? color, + Value? displayOrder, Value? description, Value? rowid}) { return TagsCompanion( id: id ?? this.id, name: name ?? this.name, color: color ?? this.color, + displayOrder: displayOrder ?? this.displayOrder, description: description ?? this.description, rowid: rowid ?? this.rowid, ); @@ -2503,6 +2582,9 @@ class TagsCompanion extends UpdateCompanion { if (color.present) { map['color'] = Variable(color.value); } + if (displayOrder.present) { + map['displayOrder'] = Variable(displayOrder.value); + } if (description.present) { map['description'] = Variable(description.value); } @@ -2518,6 +2600,7 @@ class TagsCompanion extends UpdateCompanion { ..write('id: $id, ') ..write('name: $name, ') ..write('color: $color, ') + ..write('displayOrder: $displayOrder, ') ..write('description: $description, ') ..write('rowid: $rowid') ..write(')')) @@ -4234,7 +4317,7 @@ abstract class _$AppDB extends GeneratedDatabase { startIndex: $arrayStartIndex); $arrayStartIndex += generatedlimit.amountOfVariables; return customSelect( - 'SELECT t.*,"a"."id" AS "nested_0.id", "a"."name" AS "nested_0.name", "a"."iniValue" AS "nested_0.iniValue", "a"."date" AS "nested_0.date", "a"."description" AS "nested_0.description", "a"."type" AS "nested_0.type", "a"."iconId" AS "nested_0.iconId", "a"."displayOrder" AS "nested_0.displayOrder", "a"."color" AS "nested_0.color", "a"."closingDate" AS "nested_0.closingDate", "a"."currencyId" AS "nested_0.currencyId", "a"."iban" AS "nested_0.iban", "a"."swift" AS "nested_0.swift","accountCurrency"."code" AS "nested_1.code", "accountCurrency"."symbol" AS "nested_1.symbol","receivingAccountCurrency"."code" AS "nested_2.code", "receivingAccountCurrency"."symbol" AS "nested_2.symbol","ra"."id" AS "nested_3.id", "ra"."name" AS "nested_3.name", "ra"."iniValue" AS "nested_3.iniValue", "ra"."date" AS "nested_3.date", "ra"."description" AS "nested_3.description", "ra"."type" AS "nested_3.type", "ra"."iconId" AS "nested_3.iconId", "ra"."displayOrder" AS "nested_3.displayOrder", "ra"."color" AS "nested_3.color", "ra"."closingDate" AS "nested_3.closingDate", "ra"."currencyId" AS "nested_3.currencyId", "ra"."iban" AS "nested_3.iban", "ra"."swift" AS "nested_3.swift","c"."id" AS "nested_4.id", "c"."name" AS "nested_4.name", "c"."iconId" AS "nested_4.iconId", "c"."color" AS "nested_4.color", "c"."type" AS "nested_4.type", "c"."parentCategoryID" AS "nested_4.parentCategoryID","pc"."id" AS "nested_5.id", "pc"."name" AS "nested_5.name", "pc"."iconId" AS "nested_5.iconId", "pc"."color" AS "nested_5.color", "pc"."type" AS "nested_5.type", "pc"."parentCategoryID" AS "nested_5.parentCategoryID", t.value * COALESCE(excRate.exchangeRate, 1) AS currentValueInPreferredCurrency, t.valueInDestiny * COALESCE(excRateOfDestiny.exchangeRate, 1) AS currentValueInDestinyInPreferredCurrency, t.id AS "\$n_0" FROM transactions AS t INNER JOIN accounts AS a ON t.accountID = a.id INNER JOIN currencies AS accountCurrency ON a.currencyId = accountCurrency.code LEFT JOIN accounts AS ra ON t.receivingAccountID = ra.id INNER JOIN currencies AS receivingAccountCurrency ON a.currencyId = receivingAccountCurrency.code LEFT JOIN categories AS c ON t.categoryID = c.id LEFT JOIN categories AS pc ON c.parentCategoryID = pc.id LEFT JOIN (SELECT currencyCode, exchangeRate FROM exchangeRates AS er WHERE date = (SELECT MAX(date) FROM exchangeRates WHERE currencyCode = er.currencyCode AND DATE <= DATE(\'now\')) ORDER BY currencyCode) AS excRate ON a.currencyId = excRate.currencyCode LEFT JOIN (SELECT currencyCode, exchangeRate FROM exchangeRates AS er WHERE date = (SELECT MAX(date) FROM exchangeRates WHERE currencyCode = er.currencyCode AND DATE <= DATE(\'now\')) ORDER BY currencyCode) AS excRateOfDestiny ON ra.currencyId = excRateOfDestiny.currencyCode WHERE ${generatedpredicate.sql} ${generatedorderBy.sql} ${generatedlimit.sql}', + 'SELECT t.*,"a"."id" AS "nested_0.id", "a"."name" AS "nested_0.name", "a"."iniValue" AS "nested_0.iniValue", "a"."date" AS "nested_0.date", "a"."description" AS "nested_0.description", "a"."type" AS "nested_0.type", "a"."iconId" AS "nested_0.iconId", "a"."displayOrder" AS "nested_0.displayOrder", "a"."color" AS "nested_0.color", "a"."closingDate" AS "nested_0.closingDate", "a"."currencyId" AS "nested_0.currencyId", "a"."iban" AS "nested_0.iban", "a"."swift" AS "nested_0.swift","accountCurrency"."code" AS "nested_1.code", "accountCurrency"."symbol" AS "nested_1.symbol","receivingAccountCurrency"."code" AS "nested_2.code", "receivingAccountCurrency"."symbol" AS "nested_2.symbol","ra"."id" AS "nested_3.id", "ra"."name" AS "nested_3.name", "ra"."iniValue" AS "nested_3.iniValue", "ra"."date" AS "nested_3.date", "ra"."description" AS "nested_3.description", "ra"."type" AS "nested_3.type", "ra"."iconId" AS "nested_3.iconId", "ra"."displayOrder" AS "nested_3.displayOrder", "ra"."color" AS "nested_3.color", "ra"."closingDate" AS "nested_3.closingDate", "ra"."currencyId" AS "nested_3.currencyId", "ra"."iban" AS "nested_3.iban", "ra"."swift" AS "nested_3.swift","c"."id" AS "nested_4.id", "c"."name" AS "nested_4.name", "c"."iconId" AS "nested_4.iconId", "c"."color" AS "nested_4.color", "c"."displayOrder" AS "nested_4.displayOrder", "c"."type" AS "nested_4.type", "c"."parentCategoryID" AS "nested_4.parentCategoryID","pc"."id" AS "nested_5.id", "pc"."name" AS "nested_5.name", "pc"."iconId" AS "nested_5.iconId", "pc"."color" AS "nested_5.color", "pc"."displayOrder" AS "nested_5.displayOrder", "pc"."type" AS "nested_5.type", "pc"."parentCategoryID" AS "nested_5.parentCategoryID", t.value * COALESCE(excRate.exchangeRate, 1) AS currentValueInPreferredCurrency, t.valueInDestiny * COALESCE(excRateOfDestiny.exchangeRate, 1) AS currentValueInDestinyInPreferredCurrency, t.id AS "\$n_0" FROM transactions AS t INNER JOIN accounts AS a ON t.accountID = a.id INNER JOIN currencies AS accountCurrency ON a.currencyId = accountCurrency.code LEFT JOIN accounts AS ra ON t.receivingAccountID = ra.id INNER JOIN currencies AS receivingAccountCurrency ON a.currencyId = receivingAccountCurrency.code LEFT JOIN categories AS c ON t.categoryID = c.id LEFT JOIN categories AS pc ON c.parentCategoryID = pc.id LEFT JOIN (SELECT currencyCode, exchangeRate FROM exchangeRates AS er WHERE date = (SELECT MAX(date) FROM exchangeRates WHERE currencyCode = er.currencyCode AND DATE <= DATE(\'now\')) ORDER BY currencyCode) AS excRate ON a.currencyId = excRate.currencyCode LEFT JOIN (SELECT currencyCode, exchangeRate FROM exchangeRates AS er WHERE date = (SELECT MAX(date) FROM exchangeRates WHERE currencyCode = er.currencyCode AND DATE <= DATE(\'now\')) ORDER BY currencyCode) AS excRateOfDestiny ON ra.currencyId = excRateOfDestiny.currencyCode WHERE ${generatedpredicate.sql} ${generatedorderBy.sql} ${generatedlimit.sql}', variables: [ ...generatedpredicate.introducedVariables, ...generatedorderBy.introducedVariables, @@ -4346,7 +4429,7 @@ abstract class _$AppDB extends GeneratedDatabase { startIndex: $arrayStartIndex); $arrayStartIndex += generatedpredicate.amountOfVariables; return customSelect( - 'SELECT a.*,"parentCategory"."id" AS "nested_0.id", "parentCategory"."name" AS "nested_0.name", "parentCategory"."iconId" AS "nested_0.iconId", "parentCategory"."color" AS "nested_0.color", "parentCategory"."type" AS "nested_0.type", "parentCategory"."parentCategoryID" AS "nested_0.parentCategoryID" FROM categories AS a LEFT JOIN categories AS parentCategory ON a.parentCategoryID = parentCategory.id WHERE ${generatedpredicate.sql} LIMIT ?1', + 'SELECT a.*,"parentCategory"."id" AS "nested_0.id", "parentCategory"."name" AS "nested_0.name", "parentCategory"."iconId" AS "nested_0.iconId", "parentCategory"."color" AS "nested_0.color", "parentCategory"."displayOrder" AS "nested_0.displayOrder", "parentCategory"."type" AS "nested_0.type", "parentCategory"."parentCategoryID" AS "nested_0.parentCategoryID" FROM categories AS a LEFT JOIN categories AS parentCategory ON a.parentCategoryID = parentCategory.id WHERE ${generatedpredicate.sql} LIMIT ?1', variables: [ Variable(limit), ...generatedpredicate.introducedVariables @@ -4358,6 +4441,7 @@ abstract class _$AppDB extends GeneratedDatabase { id: row.read('id'), name: row.read('name'), iconId: row.read('iconId'), + displayOrder: row.read('displayOrder'), color: row.readNullable('color'), type: NullAwareTypeConverter.wrapFromSql( Categories.$convertertype, row.readNullable('type')), diff --git a/lib/core/database/services/category/category_service.dart b/lib/core/database/services/category/category_service.dart index b7f461c8..1b07633d 100644 --- a/lib/core/database/services/category/category_service.dart +++ b/lib/core/database/services/category/category_service.dart @@ -70,6 +70,7 @@ class CategoryService { for (final category in json) { final categoryToPush = CategoryInDB( id: uuid.v4(), + displayOrder: 10, name: category['names'][systemLang] ?? category['names']['en'], iconId: category['icon'], color: category['color'], @@ -82,6 +83,7 @@ class CategoryService { for (final subcategory in category['subcategories']) { final subcategoryToPush = CategoryInDB( id: uuid.v4(), + displayOrder: 10, name: subcategory['names']['es'], iconId: subcategory['icon'], parentCategoryID: categoryToPush.id); diff --git a/lib/core/database/services/tags/tags_service.dart b/lib/core/database/services/tags/tags_service.dart index 0d91d394..3f6ffce1 100644 --- a/lib/core/database/services/tags/tags_service.dart +++ b/lib/core/database/services/tags/tags_service.dart @@ -29,6 +29,7 @@ class TagService { return (db.select(db.tags) ..where(filter ?? (tbl) => const CustomExpression('(TRUE)')) + ..orderBy([(acc) => OrderingTerm.asc(acc.displayOrder)]) ..limit(limit, offset: offset)) .watch() .map( diff --git a/lib/core/database/sql/initial/tables.drift b/lib/core/database/sql/initial/tables.drift index 3ab851f2..033ee28d 100644 --- a/lib/core/database/sql/initial/tables.drift +++ b/lib/core/database/sql/initial/tables.drift @@ -77,6 +77,9 @@ CREATE TABLE IF NOT EXISTS categories ( -- Color that will be used to represent this category in some screens. If null, the color of the parent's category will be used color TEXT, + -- The display order when listing categories + displayOrder INTEGER NOT NULL, + -- Type of the category. If null, the type of the parent's category will be used type ENUMNAME(CategoryType) CHECK(type IN ('E', 'I', 'B')), @@ -147,6 +150,9 @@ CREATE TABLE IF NOT EXISTS tags ( -- The display color of the tag color TEXT NOT NULL, + -- The display order when listing tag + displayOrder INTEGER NOT NULL, + -- The description of the tag description TEXT ) AS TagInDB; diff --git a/lib/core/models/category/category.dart b/lib/core/models/category/category.dart index bbf4a57e..3c13221e 100644 --- a/lib/core/models/category/category.dart +++ b/lib/core/models/category/category.dart @@ -50,6 +50,7 @@ class Category extends CategoryInDB { {required super.id, required super.name, required super.iconId, + required super.displayOrder, String? color, CategoryType? type, CategoryInDB? parentCategory}) @@ -72,6 +73,7 @@ class Category extends CategoryInDB { static Category fromDB(CategoryInDB cat, CategoryInDB? parentCategory) => Category( id: cat.id, + displayOrder: cat.displayOrder, name: cat.name, iconId: cat.iconId, parentCategory: parentCategory, diff --git a/lib/core/models/tags/tag.dart b/lib/core/models/tags/tag.dart index da86970d..a9daa27c 100644 --- a/lib/core/models/tags/tag.dart +++ b/lib/core/models/tags/tag.dart @@ -7,6 +7,7 @@ class Tag extends TagInDB { required super.id, required super.name, required super.color, + super.displayOrder = 10, super.description, }); diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart index 57158e39..2d124731 100644 --- a/lib/core/routes/app_router.dart +++ b/lib/core/routes/app_router.dart @@ -23,7 +23,7 @@ import 'package:monekin/app/settings/import_csv.dart'; import 'package:monekin/app/settings/settings.page.dart'; import 'package:monekin/app/stats/stats_page.dart'; import 'package:monekin/app/tags/tag_form_page.dart'; -import 'package:monekin/app/tags/tag_list.dart'; +import 'package:monekin/app/tags/tag_list.page.dart'; import 'package:monekin/app/transactions/form/transaction_form.page.dart'; import 'package:monekin/app/transactions/form/widgets/interval_selector.dart'; import 'package:monekin/app/transactions/recurrent_transactions_page.dart'; diff --git a/lib/core/routes/app_router.gr.dart b/lib/core/routes/app_router.gr.dart index bd247866..32959ef6 100644 --- a/lib/core/routes/app_router.gr.dart +++ b/lib/core/routes/app_router.gr.dart @@ -216,6 +216,7 @@ abstract class _$AppRouter extends RootStackRouter { key: args.key, isModal: args.isModal, selected: args.selected, + scrollController: args.scrollController, ), ); }, @@ -836,6 +837,7 @@ class TagListRoute extends PageRouteInfo { Key? key, bool isModal = false, List selected = const [], + ScrollController? scrollController, List? children, }) : super( TagListRoute.name, @@ -843,6 +845,7 @@ class TagListRoute extends PageRouteInfo { key: key, isModal: isModal, selected: selected, + scrollController: scrollController, ), initialChildren: children, ); @@ -858,6 +861,7 @@ class TagListRouteArgs { this.key, this.isModal = false, this.selected = const [], + this.scrollController, }); final Key? key; @@ -866,9 +870,11 @@ class TagListRouteArgs { final List selected; + final ScrollController? scrollController; + @override String toString() { - return 'TagListRouteArgs{key: $key, isModal: $isModal, selected: $selected}'; + return 'TagListRouteArgs{key: $key, isModal: $isModal, selected: $selected, scrollController: $scrollController}'; } } diff --git a/lib/i18n/translations.g.dart b/lib/i18n/translations.g.dart index c88a90d1..d72ec391 100644 --- a/lib/i18n/translations.g.dart +++ b/lib/i18n/translations.g.dart @@ -6,7 +6,7 @@ /// Locales: 2 /// Strings: 1018 (509 per locale) /// -/// Built on 2024-02-02 at 11:31 UTC +/// Built on 2024-02-02 at 14:44 UTC // coverage:ignore-file // ignore_for_file: type=lint From c0f43bf1b674056eb5a12731da513c0f478c8695 Mon Sep 17 00:00:00 2001 From: enriqueloz88 Date: Fri, 2 Feb 2024 16:29:13 +0100 Subject: [PATCH 4/5] fix: Disable reorder actions when only one item --- lib/app/accounts/all_accounts_page.dart | 4 +++- lib/app/accounts/monekin_reorderable_list.dart | 1 + lib/app/tags/tag_list.page.dart | 4 +++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/app/accounts/all_accounts_page.dart b/lib/app/accounts/all_accounts_page.dart index c314eafd..cb380c87 100644 --- a/lib/app/accounts/all_accounts_page.dart +++ b/lib/app/accounts/all_accounts_page.dart @@ -58,7 +58,9 @@ class AllAccountsPage extends StatelessWidget { margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), borderRadius: 12, child: ListTile( - trailing: ReorderableDragIcon(index: index), + trailing: accounts.length > 1 + ? ReorderableDragIcon(index: index) + : null, title: Row( children: [ Flexible( diff --git a/lib/app/accounts/monekin_reorderable_list.dart b/lib/app/accounts/monekin_reorderable_list.dart index 8c549ef1..f1a572ce 100644 --- a/lib/app/accounts/monekin_reorderable_list.dart +++ b/lib/app/accounts/monekin_reorderable_list.dart @@ -31,6 +31,7 @@ class _MonekinReorderableListState extends State { opacity: isOrderingItem == null || isOrderingItem == index ? 1 : 0.4, child: ReorderableDelayedDragStartListener( index: index, + enabled: widget.totalItemCount > 1, child: widget.itemBuilder(context, index), ), ), diff --git a/lib/app/tags/tag_list.page.dart b/lib/app/tags/tag_list.page.dart index d94c413d..40adf8c6 100644 --- a/lib/app/tags/tag_list.page.dart +++ b/lib/app/tags/tag_list.page.dart @@ -96,7 +96,9 @@ class _TagListPageState extends State { margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), borderRadius: 12, child: ListTile( - trailing: ReorderableDragIcon(index: index), + trailing: tags.length > 1 + ? ReorderableDragIcon(index: index) + : null, leading: tag.displayIcon(), title: Text(tag.name), subtitle: From 283d4f065dac67af056a03ce724366e911e16310 Mon Sep 17 00:00:00 2001 From: enriqueloz88 Date: Mon, 5 Feb 2024 14:00:18 +0100 Subject: [PATCH 5/5] chore: Build runner --- lib/i18n/translations.g.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/i18n/translations.g.dart b/lib/i18n/translations.g.dart index 29134794..b7dc52e5 100644 --- a/lib/i18n/translations.g.dart +++ b/lib/i18n/translations.g.dart @@ -6,7 +6,7 @@ /// Locales: 2 /// Strings: 1028 (514 per locale) /// -/// Built on 2024-02-05 at 12:58 UTC +/// Built on 2024-02-05 at 12:59 UTC // coverage:ignore-file // ignore_for_file: type=lint