diff --git a/CHANGELOG.md b/CHANGELOG.md index d41f77ea..54d1be54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### New features -* Added a Siri integration (intent). +* Added a Siri integration (intent), fix [#657](https://github.com/flow-mn/flow/issues/657) * Persian (Iran) support thanks to @arefbhrn[https://github.com/arefbhrn] * Location tags are suggested if you're within 50m of the tag. Requires location to be enabled. Closes [#648](https://github.com/flow-mn/flow/issues/648) @@ -13,6 +13,8 @@ ### Changes * Added an option to show/hide external source (Eny, Siri) in transactions +* Enhanced home tab pending transactions timeframe options, closes [#666](https://github.com/flow-mn/flow/issues/666) +* Home tab pending transaction group now shows sum and count of transactions ### Fixes diff --git a/assets/l10n/ar.json b/assets/l10n/ar.json index 98ffc668..7e9da657 100644 --- a/assets/l10n/ar.json +++ b/assets/l10n/ar.json @@ -125,6 +125,12 @@ "enum.PDFHeader@category": "الفئة", "enum.PDFHeader@title": "العنوان", "enum.PDFHeader@transactionDate": "تاريخ المعاملة", + "enum.PendingTimeRange@allTime": "كل الأوقات", + "enum.PendingTimeRange@followHome": "كما في المنزل", + "enum.PendingTimeRange@nextNDays": "الأيام الـ{n} القادمة", + "enum.PendingTimeRange@thisMonth": "هذا الشهر", + "enum.PendingTimeRange@thisWeek": "هذا الأسبوع", + "enum.PendingTimeRange@thisYear": "هذا العام", "enum.RecurrenceMode@custom": "مخصص", "enum.RecurrenceMode@every2Week": "كل أسبوعين، {weekday}", "enum.RecurrenceMode@everyDay": "كل يوم", @@ -242,7 +248,7 @@ "general.edit": "تحرير", "general.enabled": "مُفعّل", "general.flow": "Flow", - "general.nextNDays": "الأيام القادمة {}", + "general.nextNDays": "الأيام القادمة {n}", "general.paste": "لصق", "general.save": "حفظ", "general.search": "ابحث...", diff --git a/assets/l10n/cs_CZ.json b/assets/l10n/cs_CZ.json index 4bc53c1b..e9cc0741 100644 --- a/assets/l10n/cs_CZ.json +++ b/assets/l10n/cs_CZ.json @@ -125,6 +125,12 @@ "enum.PDFHeader@category": "Kategorie", "enum.PDFHeader@title": "Název", "enum.PDFHeader@transactionDate": "Datum transakce", + "enum.PendingTimeRange@allTime": "Celkově", + "enum.PendingTimeRange@followHome": "Stejné jako doma", + "enum.PendingTimeRange@nextNDays": "Následujících {n} dní", + "enum.PendingTimeRange@thisMonth": "Tento měsíc", + "enum.PendingTimeRange@thisWeek": "Tento týden", + "enum.PendingTimeRange@thisYear": "Tento rok", "enum.RecurrenceMode@custom": "Vlastní", "enum.RecurrenceMode@every2Week": "Každé 2 týdny, v {weekday}", "enum.RecurrenceMode@everyDay": "Každý den", @@ -242,7 +248,7 @@ "general.edit": "Upravit", "general.enabled": "Zapnuto", "general.flow": "Tok", - "general.nextNDays": "Dalších {count} dní", + "general.nextNDays": "Dalších {n} dní", "general.paste": "Vložit", "general.save": "Uložit", "general.search": "Hledat...", diff --git a/assets/l10n/de_DE.json b/assets/l10n/de_DE.json index 9f62bcb1..c465451e 100644 --- a/assets/l10n/de_DE.json +++ b/assets/l10n/de_DE.json @@ -125,6 +125,12 @@ "enum.PDFHeader@category": "Kategorie", "enum.PDFHeader@title": "Titel", "enum.PDFHeader@transactionDate": "Buchungsdatum", + "enum.PendingTimeRange@allTime": "Gesamter Zeitraum", + "enum.PendingTimeRange@followHome": "Wie zu Hause", + "enum.PendingTimeRange@nextNDays": "Nächste {n} Tage", + "enum.PendingTimeRange@thisMonth": "Diesen Monat", + "enum.PendingTimeRange@thisWeek": "Diese Woche", + "enum.PendingTimeRange@thisYear": "Dieses Jahr", "enum.RecurrenceMode@custom": "Benutzerdefiniert", "enum.RecurrenceMode@every2Week": "Alle 2 Wochen, {weekday}", "enum.RecurrenceMode@everyDay": "Jeden Tag", @@ -242,7 +248,7 @@ "general.edit": "Bearbeiten", "general.enabled": "Aktiviert", "general.flow": "Flow", - "general.nextNDays": "Die nächsten {} Tag(e)", + "general.nextNDays": "Die nächsten {n} Tag(e)", "general.paste": "Einfügen", "general.save": "Speichern", "general.search": "Suchen...", diff --git a/assets/l10n/en.json b/assets/l10n/en.json index 484b216a..77653671 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -125,6 +125,12 @@ "enum.PDFHeader@category": "Category", "enum.PDFHeader@title": "Title", "enum.PDFHeader@transactionDate": "Transaction date", + "enum.PendingTimeRange@allTime": "All time", + "enum.PendingTimeRange@followHome": "Same as home", + "enum.PendingTimeRange@nextNDays": "Next {n} days", + "enum.PendingTimeRange@thisMonth": "This month", + "enum.PendingTimeRange@thisWeek": "This week", + "enum.PendingTimeRange@thisYear": "This year", "enum.RecurrenceMode@custom": "Custom", "enum.RecurrenceMode@every2Week": "Every 2 weeks, {weekday}", "enum.RecurrenceMode@everyDay": "Every day", @@ -242,7 +248,7 @@ "general.edit": "Edit", "general.enabled": "Enabled", "general.flow": "Flow", - "general.nextNDays": "Next {} day(s)", + "general.nextNDays": "Next {n} day(s)", "general.paste": "Paste", "general.save": "Save", "general.search": "Search...", diff --git a/assets/l10n/es_ES.json b/assets/l10n/es_ES.json index ac603f87..0c49cf76 100644 --- a/assets/l10n/es_ES.json +++ b/assets/l10n/es_ES.json @@ -125,6 +125,12 @@ "enum.PDFHeader@category": "Categoría", "enum.PDFHeader@title": "Título", "enum.PDFHeader@transactionDate": "Fecha de transacción", + "enum.PendingTimeRange@allTime": "Todo el tiempo", + "enum.PendingTimeRange@followHome": "Igual que en Inicio", + "enum.PendingTimeRange@nextNDays": "Próximos {n} días", + "enum.PendingTimeRange@thisMonth": "Este mes", + "enum.PendingTimeRange@thisWeek": "Esta semana", + "enum.PendingTimeRange@thisYear": "Este año", "enum.RecurrenceMode@custom": "Personalizado", "enum.RecurrenceMode@every2Week": "Cada 2 semanas, {weekday}", "enum.RecurrenceMode@everyDay": "Cada día", @@ -242,7 +248,7 @@ "general.edit": "Editar", "general.enabled": "Activado", "general.flow": "Flow", - "general.nextNDays": "Próximos {} día(s)", + "general.nextNDays": "Próximos {n} día(s)", "general.paste": "Pegar", "general.save": "Guardar", "general.search": "Buscar...", diff --git a/assets/l10n/fa_IR.json b/assets/l10n/fa_IR.json index 91b54562..1bcc31d4 100644 --- a/assets/l10n/fa_IR.json +++ b/assets/l10n/fa_IR.json @@ -125,6 +125,12 @@ "enum.PDFHeader@category": "دسته‌بندی", "enum.PDFHeader@title": "عنوان", "enum.PDFHeader@transactionDate": "تاریخ تراکنش", + "enum.PendingTimeRange@allTime": "همه زمان‌ها", + "enum.PendingTimeRange@followHome": "مشابه خانه", + "enum.PendingTimeRange@nextNDays": "{n} روز آینده", + "enum.PendingTimeRange@thisMonth": "این ماه", + "enum.PendingTimeRange@thisWeek": "این هفته", + "enum.PendingTimeRange@thisYear": "امسال", "enum.RecurrenceMode@custom": "سفارشی", "enum.RecurrenceMode@every2Week": "هر ۲ هفته، {weekday}", "enum.RecurrenceMode@everyDay": "هر روز", @@ -242,7 +248,7 @@ "general.edit": "ویرایش", "general.enabled": "فعال", "general.flow": "Flow", - "general.nextNDays": "{} روز آینده", + "general.nextNDays": "{n} روز آینده", "general.paste": "چسباندن", "general.save": "ذخیره", "general.search": "جستجو...", diff --git a/assets/l10n/fr_FR.json b/assets/l10n/fr_FR.json index b388192e..3e2b41be 100644 --- a/assets/l10n/fr_FR.json +++ b/assets/l10n/fr_FR.json @@ -125,6 +125,12 @@ "enum.PDFHeader@category": "Catégorie", "enum.PDFHeader@title": "Titre", "enum.PDFHeader@transactionDate": "Date de transaction", + "enum.PendingTimeRange@allTime": "Toutes les périodes", + "enum.PendingTimeRange@followHome": "Identique à l'accueil", + "enum.PendingTimeRange@nextNDays": "Prochains {n} jours", + "enum.PendingTimeRange@thisMonth": "Ce mois-ci", + "enum.PendingTimeRange@thisWeek": "Cette semaine", + "enum.PendingTimeRange@thisYear": "Cette année", "enum.RecurrenceMode@custom": "Personnalisé", "enum.RecurrenceMode@every2Week": "Toutes les 2 semaines, {weekday}", "enum.RecurrenceMode@everyDay": "Tous les jours", @@ -242,7 +248,7 @@ "general.edit": "Modifier", "general.enabled": "Activé", "general.flow": "Flow", - "general.nextNDays": "Prochain(s) {} jour(s)", + "general.nextNDays": "Prochain(s) {n} jour(s)", "general.paste": "Coller", "general.save": "Enregistrer", "general.search": "Rechercher...", diff --git a/assets/l10n/it_IT.json b/assets/l10n/it_IT.json index a19dd1ab..e8de6fe8 100644 --- a/assets/l10n/it_IT.json +++ b/assets/l10n/it_IT.json @@ -125,6 +125,12 @@ "enum.PDFHeader@category": "Categoria", "enum.PDFHeader@title": "Titolo", "enum.PDFHeader@transactionDate": "Data transazione", + "enum.PendingTimeRange@allTime": "Tutto il periodo", + "enum.PendingTimeRange@followHome": "Come nella home", + "enum.PendingTimeRange@nextNDays": "Prossimi {n} giorni", + "enum.PendingTimeRange@thisMonth": "Questo mese", + "enum.PendingTimeRange@thisWeek": "Questa settimana", + "enum.PendingTimeRange@thisYear": "Quest'anno", "enum.RecurrenceMode@custom": "Personalizzata", "enum.RecurrenceMode@every2Week": "Ogni 2 settimane, {weekday}", "enum.RecurrenceMode@everyDay": "Ogni giorno", @@ -242,7 +248,7 @@ "general.edit": "Modifica", "general.enabled": "Abilitato", "general.flow": "Flusso", - "general.nextNDays": "Prossimi {} giorni", + "general.nextNDays": "Prossimi {n} giorni", "general.paste": "Incolla", "general.save": "Salva", "general.search": "Cerca...", diff --git a/assets/l10n/mn_MN.json b/assets/l10n/mn_MN.json index f7cbc2e4..0e8635d9 100644 --- a/assets/l10n/mn_MN.json +++ b/assets/l10n/mn_MN.json @@ -125,6 +125,12 @@ "enum.PDFHeader@category": "Ангилал", "enum.PDFHeader@title": "Гарчиг", "enum.PDFHeader@transactionDate": "Гүйлгээний огноо", + "enum.PendingTimeRange@allTime": "Бүх цаг үе", + "enum.PendingTimeRange@followHome": "Нүүр хуудасны тохиргоог дагах", + "enum.PendingTimeRange@nextNDays": "Ирэх {n} хоног", + "enum.PendingTimeRange@thisMonth": "Энэ сар", + "enum.PendingTimeRange@thisWeek": "Энэ долоо хоног", + "enum.PendingTimeRange@thisYear": "Энэ жил", "enum.RecurrenceMode@custom": "Өөрөө тохируулах", "enum.RecurrenceMode@every2Week": "Хоёр долоо хоног бүр, {weekday}", "enum.RecurrenceMode@everyDay": "Өдөр бүр", @@ -242,7 +248,7 @@ "general.edit": "Засварлах", "general.enabled": "Идэвхтэй", "general.flow": "Урсгал", - "general.nextNDays": "Ирэх {} хоног", + "general.nextNDays": "Ирэх {n} хоног", "general.paste": "Буулгах", "general.save": "Хадгалах", "general.search": "Хайх...", diff --git a/assets/l10n/ru_RU.json b/assets/l10n/ru_RU.json index 72aa8367..1bb91adc 100644 --- a/assets/l10n/ru_RU.json +++ b/assets/l10n/ru_RU.json @@ -125,6 +125,12 @@ "enum.PDFHeader@category": "Категория", "enum.PDFHeader@title": "Название", "enum.PDFHeader@transactionDate": "Дата транзакции", + "enum.PendingTimeRange@allTime": "Всё время", + "enum.PendingTimeRange@followHome": "Как для дома", + "enum.PendingTimeRange@nextNDays": "Следующие {n} дней", + "enum.PendingTimeRange@thisMonth": "В этом месяце", + "enum.PendingTimeRange@thisWeek": "На этой неделе", + "enum.PendingTimeRange@thisYear": "В этом году", "enum.RecurrenceMode@custom": "Пользовательский", "enum.RecurrenceMode@every2Week": "Каждые 2 недели, {weekday}", "enum.RecurrenceMode@everyDay": "Каждый день", @@ -242,7 +248,7 @@ "general.edit": "Редактировать", "general.enabled": "Включено", "general.flow": "Flow", - "general.nextNDays": "Следующие {} дн.", + "general.nextNDays": "Следующие {n} дн.", "general.paste": "Вставить", "general.save": "Сохранить", "general.search": "Поиск...", diff --git a/assets/l10n/tr_TR.json b/assets/l10n/tr_TR.json index 240c9be9..de5331d3 100644 --- a/assets/l10n/tr_TR.json +++ b/assets/l10n/tr_TR.json @@ -125,6 +125,12 @@ "enum.PDFHeader@category": "Kategori", "enum.PDFHeader@title": "Başlık", "enum.PDFHeader@transactionDate": "İşlem tarihi", + "enum.PendingTimeRange@allTime": "Tüm zamanlar", + "enum.PendingTimeRange@followHome": "Ev ile aynı", + "enum.PendingTimeRange@nextNDays": "Gelecek {n} gün", + "enum.PendingTimeRange@thisMonth": "Bu ay", + "enum.PendingTimeRange@thisWeek": "Bu hafta", + "enum.PendingTimeRange@thisYear": "Bu yıl", "enum.RecurrenceMode@custom": "Özel", "enum.RecurrenceMode@every2Week": "Her 2 haftada bir, {weekday}", "enum.RecurrenceMode@everyDay": "Her gün", @@ -242,7 +248,7 @@ "general.edit": "Düzenlemek", "general.enabled": "Etkin", "general.flow": "Akış", - "general.nextNDays": "Sonraki {} gün", + "general.nextNDays": "Sonraki {n} gün", "general.paste": "Yapıştır", "general.save": "Kaydetmek", "general.search": "Ara...", diff --git a/assets/l10n/uk_UA.json b/assets/l10n/uk_UA.json index 8877a517..3935894d 100644 --- a/assets/l10n/uk_UA.json +++ b/assets/l10n/uk_UA.json @@ -125,6 +125,12 @@ "enum.PDFHeader@category": "Категорія", "enum.PDFHeader@title": "Назва", "enum.PDFHeader@transactionDate": "Дата транзакції", + "enum.PendingTimeRange@allTime": "За весь час", + "enum.PendingTimeRange@followHome": "Як удома", + "enum.PendingTimeRange@nextNDays": "Наступні {n} днів", + "enum.PendingTimeRange@thisMonth": "Цього місяця", + "enum.PendingTimeRange@thisWeek": "Цього тижня", + "enum.PendingTimeRange@thisYear": "Цього року", "enum.RecurrenceMode@custom": "Користувацький", "enum.RecurrenceMode@every2Week": "Кожні 2 тижні, {weekday}", "enum.RecurrenceMode@everyDay": "Щодня", @@ -242,7 +248,7 @@ "general.edit": "Редагувати", "general.enabled": "Увімкнено", "general.flow": "Flow", - "general.nextNDays": "Наступні {} дн.", + "general.nextNDays": "Наступні {n} дн.", "general.paste": "Вставити", "general.save": "Зберегти", "general.search": "Пошук...", diff --git a/lib/data/transactions_filter/pending_time_range.dart b/lib/data/transactions_filter/pending_time_range.dart new file mode 100644 index 00000000..b2ed5244 --- /dev/null +++ b/lib/data/transactions_filter/pending_time_range.dart @@ -0,0 +1,126 @@ +import "package:flow/l10n/named_enum.dart"; +import "package:flow/utils/time_and_range.dart"; +import "package:moment_dart/moment_dart.dart"; + +class PendingTimeRange with LocalizedEnum { + final String value; + final Duration? futureDuration; + + static const List presets = [ + PendingTimeRange.followHome(), + PendingTimeRange.duration(Duration(days: 3)), + PendingTimeRange.duration(Duration(days: 7)), + PendingTimeRange.duration(Duration(days: 14)), + PendingTimeRange.duration(Duration(days: 30)), + PendingTimeRange.duration(Duration(days: 60)), + PendingTimeRange.thisWeek(), + PendingTimeRange.thisMonth(), + PendingTimeRange.thisYear(), + PendingTimeRange.allTime(), + ]; + + factory PendingTimeRange._normalized( + String? value, + Duration? futureDuration, + ) { + if (value == "nextNDays") { + if (futureDuration == null) { + throw ArgumentError( + "futureDuration must be provided for nextNDays preset", + ); + } + return PendingTimeRange.duration(futureDuration); + } + + switch (value) { + case "followHome": + return const PendingTimeRange.followHome(); + case "thisWeek": + return const PendingTimeRange.thisWeek(); + case "thisMonth": + return const PendingTimeRange.thisMonth(); + case "thisYear": + return const PendingTimeRange.thisYear(); + case "allTime": + return const PendingTimeRange.allTime(); + default: + throw ArgumentError("Invalid value for PendingTimeRange: $value"); + } + } + + const PendingTimeRange._(this.value, {this.futureDuration}); + const PendingTimeRange.duration(this.futureDuration) : value = "nextNDays"; + const PendingTimeRange.followHome() : this._("followHome"); + const PendingTimeRange.thisWeek() : this._("thisWeek"); + const PendingTimeRange.thisMonth() : this._("thisMonth"); + const PendingTimeRange.thisYear() : this._("thisYear"); + const PendingTimeRange.allTime() : this._("allTime"); + + @override + String toString() { + if (value == "nextNDays") { + return (futureDuration ?? Duration.zero).abs().inSeconds.toRadixString( + 36, + ); + } + + return value; + } + + static PendingTimeRange? tryParse(String? value) { + if (value == null) return null; + + if (value == "followHome") return const PendingTimeRange.followHome(); + if (value == "thisWeek") return const PendingTimeRange.thisWeek(); + if (value == "thisMonth") return const PendingTimeRange.thisMonth(); + if (value == "thisYear") return const PendingTimeRange.thisYear(); + if (value == "allTime") return const PendingTimeRange.allTime(); + + try { + final int seconds = int.parse(value, radix: 36); + return PendingTimeRange.duration(Duration(seconds: seconds)); + } catch (_) { + return null; + } + } + + /// Throws [FormatException] if the value is not valid + static PendingTimeRange parse(String value) { + return tryParse(value) ?? + (throw FormatException("Invalid PendingTimeRangePreset value: $value")); + } + + PendingTimeRange copyWith({String? value, Duration? futureDuration}) { + return PendingTimeRange._normalized( + value ?? this.value, + futureDuration ?? this.futureDuration, + ); + } + + @override + String get localizationEnumName => "PendingTimeRange"; + + @override + String get localizationEnumValue => value; + + @override + bool operator ==(Object other) { + if (other is! PendingTimeRange) return false; + if (identical(this, other)) return true; + + return value == other.value && futureDuration == other.futureDuration; + } + + @override + int get hashCode => Object.hash(value, futureDuration); + + TimeRange range({TimeRange? homeTimeRange}) => switch (value) { + "nextNDays" => nextNDaysRange(futureDuration?.inDays ?? 0), + "thisWeek" => TimeRange.thisLocalWeek(), + "thisMonth" => TimeRange.thisMonth(), + "thisYear" => TimeRange.thisYear(), + "allTime" => TimeRange.allTime(), + "followHome" when homeTimeRange != null => homeTimeRange, + _ => nextNDaysRange(7), + }; +} diff --git a/lib/entity/user_preferences.dart b/lib/entity/user_preferences.dart index 3eb5b78e..5cc60a08 100644 --- a/lib/entity/user_preferences.dart +++ b/lib/entity/user_preferences.dart @@ -1,5 +1,6 @@ import "package:flow/data/flow_button_type.dart"; import "package:flow/data/prefs/change_visuals.dart"; +import "package:flow/data/transactions_filter/pending_time_range.dart"; import "package:flow/entity/_base.dart"; import "package:flow/entity/user_preferences/transaction_entry_flow.dart"; import "package:flow/utils/json/utc_datetime_converter.dart"; @@ -41,6 +42,15 @@ class UserPreferences implements EntityBase { /// Le UUID of it String? defaultFilterPreset; + String? homePendingTransactionsTimeRangeSerialized; + + /// The time range for the home pending transactions list + /// + /// If null, the default time range is used + PendingTimeRange get homePendingTransactionsTimeRange => + PendingTimeRange.tryParse(homePendingTransactionsTimeRangeSerialized) ?? + PendingTimeRange.duration(Duration(days: 7)); + /// It's a added to a start of the day /// /// e.g., to set a daily reminder at 9:00 AM, set it to 9 hours @@ -170,6 +180,7 @@ class UserPreferences implements EntityBase { this.privacyModeUponShaking = false, this.trashBinRetentionDays = 30, this.defaultFilterPreset, + this.homePendingTransactionsTimeRangeSerialized, this.enableICloudSync = false, this.iCloudBackupsToKeep = 10, this.autoBackupIntervalInHours = 72, diff --git a/lib/entity/user_preferences.g.dart b/lib/entity/user_preferences.g.dart index 2d492d71..1d5675d5 100644 --- a/lib/entity/user_preferences.g.dart +++ b/lib/entity/user_preferences.g.dart @@ -31,6 +31,8 @@ UserPreferences _$UserPreferencesFromJson(Map json) => trashBinRetentionDays: (json['trashBinRetentionDays'] as num?)?.toInt() ?? 30, defaultFilterPreset: json['defaultFilterPreset'] as String?, + homePendingTransactionsTimeRangeSerialized: + json['homePendingTransactionsTimeRangeSerialized'] as String?, enableICloudSync: json['enableICloudSync'] as bool? ?? false, iCloudBackupsToKeep: (json['iCloudBackupsToKeep'] as num?)?.toInt() ?? 10, @@ -59,6 +61,8 @@ Map _$UserPreferencesToJson( 'excludeTransfersFromFlow': instance.excludeTransfersFromFlow, 'trashBinRetentionDays': instance.trashBinRetentionDays, 'defaultFilterPreset': instance.defaultFilterPreset, + 'homePendingTransactionsTimeRangeSerialized': + instance.homePendingTransactionsTimeRangeSerialized, 'remindDailyAtRelativeSeconds': instance.remindDailyAtRelativeSeconds, 'useCategoryNameForUntitledTransactions': instance.useCategoryNameForUntitledTransactions, diff --git a/lib/graceful_migrations.dart b/lib/graceful_migrations.dart index 40a761a8..19b554a7 100644 --- a/lib/graceful_migrations.dart +++ b/lib/graceful_migrations.dart @@ -1,4 +1,5 @@ import "package:flow/data/transaction_filter.dart"; +import "package:flow/data/transactions_filter/pending_time_range.dart"; import "package:flow/entity/transaction.dart"; import "package:flow/entity/transaction/extensions/default/geo.dart"; import "package:flow/l10n/flow_localizations.dart"; @@ -263,3 +264,49 @@ void migrateGeoExtensionToLocation() async { ); } } + +void migrateHomePendingTransactionsRange() async { + const String migrationUuid = "2130fe7d-6cdc-45c2-9632-56ba9de56c08"; + + try { + final SharedPreferencesWithCache prefs = + await SharedPreferencesWithCache.create( + cacheOptions: SharedPreferencesWithCacheOptions(), + ); + + final ok = prefs.getString("flow.migration.$migrationUuid"); + + if (ok != null) return; + + try { + final int? old = LocalPreferences().pendingTransactions.homeTimeframe + .get(); + + if (old != null) { + final PendingTimeRange newRange = PendingTimeRange.duration( + Duration(days: old.abs()), + ); + + UserPreferencesService().homePendingTransactionsTimeRange = newRange; + } else { + UserPreferencesService().homePendingTransactionsTimeRange = + PendingTimeRange.followHome(); + } + + await prefs.setString("flow.migration.$migrationUuid", "ok"); + _log.info( + "Migrated home pending transactions range for migration $migrationUuid", + ); + } catch (e) { + _log.warning( + "Failed to migrate home pending transactions range for migration $migrationUuid", + e, + ); + } + } catch (e) { + _log.warning( + "Failed to read migration status for migration $migrationUuid", + e, + ); + } +} diff --git a/lib/objectbox/objectbox-model.json b/lib/objectbox/objectbox-model.json index 06509ff8..e17bce61 100644 --- a/lib/objectbox/objectbox-model.json +++ b/lib/objectbox/objectbox-model.json @@ -362,7 +362,7 @@ }, { "id": "10:7829328581176695647", - "lastPropertyId": "29:4843097333162455732", + "lastPropertyId": "30:5353888497210708730", "name": "UserPreferences", "properties": [ { @@ -502,6 +502,11 @@ "id": "29:4843097333162455732", "name": "transactionListTileShowExternalSource", "type": 1 + }, + { + "id": "30:5353888497210708730", + "name": "homePendingTransactionsTimeRangeSerialized", + "type": 9 } ], "relations": [] diff --git a/lib/objectbox/objectbox.g.dart b/lib/objectbox/objectbox.g.dart index e2d36470..c65d886f 100644 --- a/lib/objectbox/objectbox.g.dart +++ b/lib/objectbox/objectbox.g.dart @@ -455,7 +455,7 @@ final _entities = [ obx_int.ModelEntity( id: const obx_int.IdUid(10, 7829328581176695647), name: 'UserPreferences', - lastPropertyId: const obx_int.IdUid(29, 4843097333162455732), + lastPropertyId: const obx_int.IdUid(30, 5353888497210708730), flags: 0, properties: [ obx_int.ModelProperty( @@ -621,6 +621,12 @@ final _entities = [ type: 1, flags: 0, ), + obx_int.ModelProperty( + id: const obx_int.IdUid(30, 5353888497210708730), + name: 'homePendingTransactionsTimeRangeSerialized', + type: 9, + flags: 0, + ), ], relations: [], backlinks: [], @@ -1640,7 +1646,13 @@ obx_int.ModelDefinition getObjectBoxModel() { final primaryAccountUuidOffset = object.primaryAccountUuid == null ? null : fbb.writeString(object.primaryAccountUuid!); - fbb.startTable(30); + final homePendingTransactionsTimeRangeSerializedOffset = + object.homePendingTransactionsTimeRangeSerialized == null + ? null + : fbb.writeString( + object.homePendingTransactionsTimeRangeSerialized!, + ); + fbb.startTable(31); fbb.addInt64(0, object.id); fbb.addOffset(1, uuidOffset); fbb.addBool(2, object.combineTransfers); @@ -1668,6 +1680,7 @@ obx_int.ModelDefinition getObjectBoxModel() { fbb.addBool(26, object.privacyModeUponLaunch); fbb.addBool(27, object.privacyModeUponShaking); fbb.addBool(28, object.transactionListTileShowExternalSource); + fbb.addOffset(29, homePendingTransactionsTimeRangeSerializedOffset); fbb.finish(fbb.endTable()); return object.id; }, @@ -1723,6 +1736,10 @@ obx_int.ModelDefinition getObjectBoxModel() { final defaultFilterPresetParam = const fb.StringReader( asciiOptimization: true, ).vTableGetNullable(buffer, rootOffset, 12); + final homePendingTransactionsTimeRangeSerializedParam = + const fb.StringReader( + asciiOptimization: true, + ).vTableGetNullable(buffer, rootOffset, 62); final enableICloudSyncParam = const fb.BoolReader().vTableGet( buffer, rootOffset, @@ -1781,6 +1798,8 @@ obx_int.ModelDefinition getObjectBoxModel() { privacyModeUponShaking: privacyModeUponShakingParam, trashBinRetentionDays: trashBinRetentionDaysParam, defaultFilterPreset: defaultFilterPresetParam, + homePendingTransactionsTimeRangeSerialized: + homePendingTransactionsTimeRangeSerializedParam, enableICloudSync: enableICloudSyncParam, iCloudBackupsToKeep: iCloudBackupsToKeepParam, autoBackupIntervalInHours: autoBackupIntervalInHoursParam, @@ -2654,6 +2673,10 @@ class UserPreferences_ { /// See [UserPreferences.transactionListTileShowExternalSource]. static final transactionListTileShowExternalSource = obx.QueryBooleanProperty(_entities[6].properties[26]); + + /// See [UserPreferences.homePendingTransactionsTimeRangeSerialized]. + static final homePendingTransactionsTimeRangeSerialized = + obx.QueryStringProperty(_entities[6].properties[27]); } /// [Budget] entity fields to define ObjectBox queries. diff --git a/lib/routes/home/home_tab.dart b/lib/routes/home/home_tab.dart index 08ec19fe..0c116a34 100644 --- a/lib/routes/home/home_tab.dart +++ b/lib/routes/home/home_tab.dart @@ -4,6 +4,7 @@ import "package:flow/data/actionable_nofications/actionable_notification.dart"; import "package:flow/data/exchange_rates.dart"; import "package:flow/data/single_currency_flow.dart"; import "package:flow/data/transaction_filter.dart"; +import "package:flow/data/transactions_filter/pending_time_range.dart"; import "package:flow/data/transactions_filter/time_range.dart"; import "package:flow/entity/transaction.dart"; import "package:flow/entity/transaction_filter_preset.dart"; @@ -43,7 +44,7 @@ class _HomeTabState extends State with AutomaticKeepAliveClientMixin { late final AppLifecycleListener _listener; late final Timer _timer; - late int _plannedTransactionsNextNDays; + late PendingTimeRange _plannedTransactionsTimeRange; late TransactionFilter defaultFilter; DateTime dateKey = Moment.startOfToday(); @@ -53,19 +54,17 @@ class _HomeTabState extends State with AutomaticKeepAliveClientMixin { late TransactionFilter currentFilter; TransactionFilter get currentFilterWithPlanned { - final DateTime plannedTransactionTo = Moment.now() - .add(Duration(days: _plannedTransactionsNextNDays)) - .startOfNextDay(); - final TimeRange? timeRange = currentFilter.range?.range; + final TimeRange plannedTranasctionsTimeRange = _plannedTransactionsTimeRange + .range(homeTimeRange: timeRange); if (timeRange != null && timeRange.contains(Moment.now()) && - !timeRange.contains(plannedTransactionTo)) { + !timeRange.contains(plannedTranasctionsTimeRange.to)) { return currentFilter.copyWithOptional( range: Optional( TransactionFilterTimeRange.fromTimeRange( - CustomTimeRange(timeRange.from, plannedTransactionTo), + CustomTimeRange(timeRange.from, plannedTranasctionsTimeRange.to), ), ), ); @@ -80,9 +79,6 @@ class _HomeTabState extends State with AutomaticKeepAliveClientMixin { void initState() { super.initState(); _updatePlannedTransactionDays(); - LocalPreferences().pendingTransactions.homeTimeframe.addListener( - _updatePlannedTransactionDays, - ); _rawUpdateDefaultFilter(); @@ -98,6 +94,9 @@ class _HomeTabState extends State with AutomaticKeepAliveClientMixin { ); UserPreferencesService().valueNotifier.addListener(_rawUpdateDefaultFilter); + UserPreferencesService().valueNotifier.addListener( + _updatePlannedTransactionDays, + ); ActionableNotificationsService().notifications.addListener( _updateActionableNotification, ); @@ -109,13 +108,13 @@ class _HomeTabState extends State with AutomaticKeepAliveClientMixin { @override void dispose() { _listener.dispose(); - LocalPreferences().pendingTransactions.homeTimeframe.removeListener( - _updatePlannedTransactionDays, - ); _timer.cancel(); UserPreferencesService().valueNotifier.removeListener( _rawUpdateDefaultFilter, ); + UserPreferencesService().valueNotifier.removeListener( + _updatePlannedTransactionDays, + ); ActionableNotificationsService().notifications.removeListener( _updateActionableNotification, ); @@ -139,16 +138,16 @@ class _HomeTabState extends State with AutomaticKeepAliveClientMixin { ), builder: (context, snapshot) { final DateTime now = Moment.now().startOfNextMinute(); - final DateTime cutoffPlanned = now - .add(Duration(days: _plannedTransactionsNextNDays)) - .startOfNextDay(); + final TimeRange cutoffPlanned = _plannedTransactionsTimeRange.range( + homeTimeRange: currentFilter.range?.range, + ); final List? transactions = snapshot.data; if (currentFilter.range?.range?.contains(now) == true) { transactions?.removeWhere((transaction) { if (transaction.transactionDate <= now) return false; - return transaction.transactionDate > cutoffPlanned; + return transaction.transactionDate > cutoffPlanned.to; }); } @@ -336,9 +335,8 @@ class _HomeTabState extends State with AutomaticKeepAliveClientMixin { } void _updatePlannedTransactionDays() { - _plannedTransactionsNextNDays = - LocalPreferences().pendingTransactions.homeTimeframe.get() ?? - PendingTransactionsLocalPreferences.homeTimeframeDefault; + _plannedTransactionsTimeRange = + UserPreferencesService().homePendingTransactionsTimeRange; setState(() {}); } diff --git a/lib/routes/preferences/pending_transactions_preferences_page.dart b/lib/routes/preferences/pending_transactions_preferences_page.dart index 078a343a..627459ee 100644 --- a/lib/routes/preferences/pending_transactions_preferences_page.dart +++ b/lib/routes/preferences/pending_transactions_preferences_page.dart @@ -1,9 +1,12 @@ import "dart:async"; +import "package:flow/data/transactions_filter/pending_time_range.dart"; import "package:flow/l10n/extensions.dart"; +import "package:flow/l10n/named_enum.dart"; import "package:flow/prefs/local_preferences.dart"; import "package:flow/services/notifications.dart"; import "package:flow/services/transactions.dart"; +import "package:flow/services/user_preferences.dart"; import "package:flow/widgets/general/frame.dart"; import "package:flow/widgets/general/info_text.dart"; import "package:flow/widgets/general/list_header.dart"; @@ -31,9 +34,8 @@ class _PendingTransactionPreferencesPageState @override Widget build(BuildContext context) { - final int pendingTransactionsHomeTimeframe = - LocalPreferences().pendingTransactions.homeTimeframe.get() ?? - PendingTransactionsLocalPreferences.homeTimeframeDefault; + final PendingTimeRange pendingTransactionsHomeTimeframe = + UserPreferencesService().homePendingTransactionsTimeRange; final bool pendingTransactionsRequireConfrimation = LocalPreferences() .pendingTransactions .requireConfrimation @@ -76,24 +78,24 @@ class _PendingTransactionPreferencesPageState child: Wrap( spacing: 12.0, runSpacing: 8.0, - children: [1, 2, 3, 5, 7, 14, 30] - .map( - (value) => FilterChip( - showCheckmark: false, - key: ValueKey(value), - label: Text( - "general.nextNDays".t(context, value), + children: [ + ...PendingTimeRange.presets.map( + (value) => FilterChip( + showCheckmark: false, + key: ValueKey(value), + label: Text( + value.localizedNameContext( + context, + value.futureDuration?.inDays, ), - onSelected: (bool selected) => selected - ? updatePendingTransactionsHomeTimeframe( - value, - ) - : null, - selected: - value == pendingTransactionsHomeTimeframe, ), - ) - .toList(), + onSelected: (bool selected) => selected + ? updatePendingTransactionsHomeTimeframe(value) + : null, + selected: value == pendingTransactionsHomeTimeframe, + ), + ), + ], ), ), const SizedBox(height: 16.0), @@ -219,8 +221,8 @@ class _PendingTransactionPreferencesPageState ); } - void updatePendingTransactionsHomeTimeframe(int days) async { - await LocalPreferences().pendingTransactions.homeTimeframe.set(days); + void updatePendingTransactionsHomeTimeframe(PendingTimeRange newValue) async { + UserPreferencesService().homePendingTransactionsTimeRange = newValue; if (mounted) setState(() {}); } diff --git a/lib/routes/transaction_page.dart b/lib/routes/transaction_page.dart index f74cbe63..e292fc38 100644 --- a/lib/routes/transaction_page.dart +++ b/lib/routes/transaction_page.dart @@ -855,7 +855,7 @@ class _TransactionPageState extends State { } Future selectCategory([bool fromAutomatedFlow = false]) async { - final categories = CategoriesProvider.of(context).categories; + final List categories = CategoriesProvider.of(context).categories; if (fromAutomatedFlow && categories.isEmpty) { return true; diff --git a/lib/routes/transaction_page/sections/tags_section.dart b/lib/routes/transaction_page/sections/tags_section.dart index 1402cf32..5c4e6d59 100644 --- a/lib/routes/transaction_page/sections/tags_section.dart +++ b/lib/routes/transaction_page/sections/tags_section.dart @@ -38,9 +38,6 @@ class TagsSection extends StatelessWidget { _ => null, }; - print("suggestedGeoTags: $suggestedGeoTags"); - print("location: $location"); - final bool hasSuggestedGeoTags = suggestedGeoTags?.isNotEmpty == true; return Section( diff --git a/lib/services/notifications.dart b/lib/services/notifications.dart index 3438c309..2ddaec38 100644 --- a/lib/services/notifications.dart +++ b/lib/services/notifications.dart @@ -554,6 +554,7 @@ class NotificationsService { return null; } + /// Throws if notifications are not available or permissions are not granted Future _checkSupportAndPermission() async { if (!available) { _log.warning("Notifications not available"); diff --git a/lib/services/sync.dart b/lib/services/sync.dart index 0cdd0d2f..a87dd250 100644 --- a/lib/services/sync.dart +++ b/lib/services/sync.dart @@ -122,7 +122,6 @@ class SyncService { } } - // TODO @sadespresso - enable multi-syncer support Future putToAll( BackupEntry entry, { Function(double)? onProgress, diff --git a/lib/services/user_preferences.dart b/lib/services/user_preferences.dart index dca2b25d..5dcd8e81 100644 --- a/lib/services/user_preferences.dart +++ b/lib/services/user_preferences.dart @@ -5,6 +5,7 @@ import "package:flow/constants.dart"; import "package:flow/data/flow_button_type.dart"; import "package:flow/data/flow_notification_payload.dart"; import "package:flow/data/prefs/change_visuals.dart"; +import "package:flow/data/transactions_filter/pending_time_range.dart"; import "package:flow/entity/account.dart"; import "package:flow/entity/transaction_filter_preset.dart"; import "package:flow/entity/user_preferences.dart"; @@ -68,6 +69,17 @@ class UserPreferencesService { } } + PendingTimeRange get homePendingTransactionsTimeRange => + value.homePendingTransactionsTimeRange; + + set homePendingTransactionsTimeRange( + PendingTimeRange newHomePendingTransactionsTimeRange, + ) { + value.homePendingTransactionsTimeRangeSerialized = + newHomePendingTransactionsTimeRange.toString(); + ObjectBox().box().put(value); + } + ChangeVisuals get changeVisuals { final ChangeVisuals? parsed = ChangeVisuals.tryParse(value.changeVisuals); diff --git a/lib/utils/time_and_range.dart b/lib/utils/time_and_range.dart index b351953b..ce195989 100644 --- a/lib/utils/time_and_range.dart +++ b/lib/utils/time_and_range.dart @@ -72,3 +72,9 @@ CustomTimeRange last30DaysRange([DateTime? anchor]) => (anchor ?? Moment.now()) .subtract(const Duration(days: 29)) .startOfDay() .rangeTo(Moment.endOfToday()); + +CustomTimeRange nextNDaysRange(int n, [DateTime? anchor]) => + (anchor ?? Moment.now()) + .add(Duration(days: n - 1)) + .startOfDay() + .rangeTo(Moment.endOfToday()); diff --git a/lib/widgets/schdeuled_notification_permission_builder.dart b/lib/widgets/schdeuled_notification_permission_builder.dart index 4e2b5aa0..d3a7d4a2 100644 --- a/lib/widgets/schdeuled_notification_permission_builder.dart +++ b/lib/widgets/schdeuled_notification_permission_builder.dart @@ -79,7 +79,14 @@ class _SchdeuledNotificationPermissionBuilderState Future _checkNotificationPermission() async { try { - _hasNotificationPermission = await Permission.notification.isGranted; + if (Platform.isLinux) { + _hasNotificationPermission = false; + } else if (Platform.isMacOS) { + _hasNotificationPermission = + (await NotificationsService().hasPermissions()) ?? false; + } else { + _hasNotificationPermission = await Permission.notification.isGranted; + } } finally { if (mounted) { setState(() {}); diff --git a/lib/widgets/transactions_date_header.dart b/lib/widgets/transactions_date_header.dart index 5a7d5995..2a5dbf55 100644 --- a/lib/widgets/transactions_date_header.dart +++ b/lib/widgets/transactions_date_header.dart @@ -134,31 +134,29 @@ class _TransactionListDateHeaderState extends State { style: context.textTheme.headlineSmall!, child: title, ), - if (!widget.pendingGroup) - // - MoneyTextBuilder( - builder: (context, formattedSum, originalSum) => RichText( - text: TextSpan( - style: context.textTheme.labelMedium, - children: [ - TextSpan( - text: "$formattedSum$exclamation", - style: showMissingExchangeRatesWarning - ? TextStyle(color: context.colorScheme.error) - : null, + MoneyTextBuilder( + builder: (context, formattedSum, originalSum) => RichText( + text: TextSpan( + style: context.textTheme.labelMedium, + children: [ + TextSpan( + text: "$formattedSum$exclamation", + style: showMissingExchangeRatesWarning + ? TextStyle(color: context.colorScheme.error) + : null, + ), + TextSpan(text: " • "), + TextSpan( + text: "tabs.home.transactionsCount".t( + context, + widget.transactions.renderableCount, ), - TextSpan(text: " • "), - TextSpan( - text: "tabs.home.transactionsCount".t( - context, - widget.transactions.renderableCount, - ), - ), - ], - ), + ), + ], ), - money: mergedFlow.totalFlow, ), + money: mergedFlow.totalFlow, + ), ], ), ), diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 3dc65f72..dfb79dc3 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -11,6 +11,8 @@ PODS: - FlutterMacOS - file_selector_macos (0.0.1): - FlutterMacOS + - flutter_app_group_directory (0.0.1): + - FlutterMacOS - flutter_local_notifications (0.0.1): - FlutterMacOS - flutter_timezone (0.1.0): @@ -58,6 +60,7 @@ DEPENDENCIES: - file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`) - file_saver (from `Flutter/ephemeral/.symlinks/plugins/file_saver/macos`) - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) + - flutter_app_group_directory (from `Flutter/ephemeral/.symlinks/plugins/flutter_app_group_directory/macos`) - flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`) - flutter_timezone (from `Flutter/ephemeral/.symlinks/plugins/flutter_timezone/macos`) - FlutterMacOS (from `Flutter/ephemeral`) @@ -93,6 +96,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/file_saver/macos file_selector_macos: :path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos + flutter_app_group_directory: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_app_group_directory/macos flutter_local_notifications: :path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos flutter_timezone: @@ -135,6 +140,7 @@ SPEC CHECKSUMS: file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a file_saver: e35bd97de451dde55ff8c38862ed7ad0f3418d0f file_selector_macos: 9e9e068e90ebee155097d00e89ae91edb2374db7 + flutter_app_group_directory: 14eb7e7a2b0e30a6a68bb855197b4ed6f5063e55 flutter_local_notifications: 4bf37a31afde695b56091b4ae3e4d9c7a7e6cda0 flutter_timezone: d272288c69082ad571630e0d17140b3d6b93dc0c FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 diff --git a/pubspec.yaml b/pubspec.yaml index f3a55cee..34358a1e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A personal finance managing app publish_to: "none" # Remove this line if you wish to publish to pub.dev -version: "0.19.0+324" +version: "0.19.0+325" environment: sdk: ">=3.10.0 <4.0.0"