Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## 0.19.1

### Fixes

* Fixed "Add an expense" Siri Shortcut struggling with non-US formatted numbers
* Other minor fixes

## 0.19.0

### New features
Expand Down
23 changes: 20 additions & 3 deletions RELEASE_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,29 @@ data loss under any circumstances!

## What's Changed

{{changelog}}
### New features

* 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)
* Now it's possible to duplicate transfers

### 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

* Action arrows are now correctly displayed in RTL languages (Arabic, Persian)
IDK why it was flipped, probably Flutter update did something.

## Download

* [Google Play](https://play.google.com/store/apps/details?id=mn.flow.flow?utm_source=gh-release-{{version}}) for Android
* [App Store](https://apps.apple.com/mn/app/flow-expense-tracker/id6477741670?utm_source=gh-release-{{version}}) for iOS
* [Google Play](https://play.google.com/store/apps/details?id=mn.flow.flow?utm_source=gh-release-0-19-0) for Android
* [App Store](https://apps.apple.com/mn/app/flow-expense-tracker/id6477741670?utm_source=gh-release-0-19-0) for iOS

Also:

Expand Down
2 changes: 2 additions & 0 deletions assets/l10n/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"category.delete.description": "حذف هذه الفئة سيترك {transactionCount} معاملة بدون فئة. هذا الإجراء لا يمكن التراجع عنه!",
"category.name": "اسم الفئة",
"category.new": "إضافة فئة",
"category.new.success": "تم إنشاء فئة جديدة بنجاح",
"category.none": "لا فئة",
"category.skip": "بدون فئة",
"contributors": "المساهمون",
Expand Down Expand Up @@ -248,6 +249,7 @@
"general.edit": "تحرير",
"general.enabled": "مُفعّل",
"general.flow": "Flow",
"general.new": "جديد",
"general.nextNDays": "الأيام القادمة {n}",
"general.paste": "لصق",
"general.save": "حفظ",
Expand Down
2 changes: 2 additions & 0 deletions assets/l10n/cs_CZ.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"category.delete.description": "Smazáním této kategorie zůstane {transactionCount} transakcí bez kategorie. Tato akce je nevratná!",
"category.name": "Název kategorie",
"category.new": "Nová kategorie",
"category.new.success": "Nová kategorie byla úspěšně vytvořena.",
"category.none": "Žádná kategorie",
"category.skip": "Bez kategorie",
"contributors": "Přispěvatelé",
Expand Down Expand Up @@ -248,6 +249,7 @@
"general.edit": "Upravit",
"general.enabled": "Zapnuto",
"general.flow": "Tok",
"general.new": "Nový",
"general.nextNDays": "Dalších {n} dní",
"general.paste": "Vložit",
"general.save": "Uložit",
Expand Down
2 changes: 2 additions & 0 deletions assets/l10n/de_DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"category.delete.description": "Wenn du diese Kategorie löschst, haben {transactionCount} Buchungen keine Kategorie mehr. Das kann nicht rückgängig gemacht werden!",
"category.name": "Name der Kategorie",
"category.new": "Kategorie hinzufügen",
"category.new.success": "Neue Kategorie erfolgreich erstellt.",
"category.none": "Keine Kategorie",
"category.skip": "Keine Kategorie",
"contributors": "Mitwirkende",
Expand Down Expand Up @@ -248,6 +249,7 @@
"general.edit": "Bearbeiten",
"general.enabled": "Aktiviert",
"general.flow": "Flow",
"general.new": "Neu",
"general.nextNDays": "Die nächsten {n} Tag(e)",
"general.paste": "Einfügen",
"general.save": "Speichern",
Expand Down
2 changes: 2 additions & 0 deletions assets/l10n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"category.delete.description": "Deleting this category will leave {transactionCount} transactions with no category. This action is irreversible!",
"category.name": "Category name",
"category.new": "Add a category",
"category.new.success": "Successfully created a new category",
"category.none": "No category",
"category.skip": "No category",
"contributors": "Contributors",
Expand Down Expand Up @@ -248,6 +249,7 @@
"general.edit": "Edit",
"general.enabled": "Enabled",
"general.flow": "Flow",
"general.new": "New",
"general.nextNDays": "Next {n} day(s)",
"general.paste": "Paste",
"general.save": "Save",
Expand Down
2 changes: 2 additions & 0 deletions assets/l10n/es_ES.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"category.delete.description": "Eliminar esta categoría dejará {transactionCount} transacciones sin categoría. ¡Esta acción es irreversible!",
"category.name": "Nombre de la categoría",
"category.new": "Añadir una categoría",
"category.new.success": "Se ha creado correctamente una nueva categoría",
"category.none": "Sin categoría",
"category.skip": "Sin categoría",
"contributors": "Colaboradores",
Expand Down Expand Up @@ -248,6 +249,7 @@
"general.edit": "Editar",
"general.enabled": "Activado",
"general.flow": "Flow",
"general.new": "Nuevo",
"general.nextNDays": "Próximos {n} día(s)",
"general.paste": "Pegar",
"general.save": "Guardar",
Expand Down
2 changes: 2 additions & 0 deletions assets/l10n/fa_IR.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"category.delete.description": "با حذف این دسته‌بندی، {transactionCount} تراکنش بدون دسته‌بندی می‌ماند. این کار غیرقابل بازگشت است!",
"category.name": "نام دسته‌بندی",
"category.new": "افزودن دسته‌بندی",
"category.new.success": "دسته‌بندی جدید با موفقیت ایجاد شد",
"category.none": "بدون دسته‌بندی",
"category.skip": "بدون دسته‌بندی",
"contributors": "مشارکت‌کنندگان",
Expand Down Expand Up @@ -248,6 +249,7 @@
"general.edit": "ویرایش",
"general.enabled": "فعال",
"general.flow": "Flow",
"general.new": "جدید",
"general.nextNDays": "{n} روز آینده",
"general.paste": "چسباندن",
"general.save": "ذخیره",
Expand Down
2 changes: 2 additions & 0 deletions assets/l10n/fr_FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"category.delete.description": "Supprimer cette catégorie laissera {transactionCount} transactions sans catégorie. Cette action est irréversible!",
"category.name": "Nom de la catégorie",
"category.new": "Ajouter une catégorie",
"category.new.success": "Nouvelle catégorie créée avec succès",
"category.none": "Pas de catégorie",
"category.skip": "Pas de catégorie",
"contributors": "Contributeurs",
Expand Down Expand Up @@ -248,6 +249,7 @@
"general.edit": "Modifier",
"general.enabled": "Activé",
"general.flow": "Flow",
"general.new": "Nouveau",
"general.nextNDays": "Prochain(s) {n} jour(s)",
"general.paste": "Coller",
"general.save": "Enregistrer",
Expand Down
2 changes: 2 additions & 0 deletions assets/l10n/it_IT.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"category.delete.description": "Eliminando questa categoria, {transactionCount} transazioni rimarranno senza categoria. Questa azione è irreversibile!",
"category.name": "Nome della categoria",
"category.new": "Aggiungi una categoria",
"category.new.success": "Categoria creata con successo",
"category.none": "Nessuna categoria",
"category.skip": "Nessuna categoria",
"contributors": "Collaboratori",
Expand Down Expand Up @@ -248,6 +249,7 @@
"general.edit": "Modifica",
"general.enabled": "Abilitato",
"general.flow": "Flusso",
"general.new": "Nuovo",
"general.nextNDays": "Prossimi {n} giorni",
"general.paste": "Incolla",
"general.save": "Salva",
Expand Down
2 changes: 2 additions & 0 deletions assets/l10n/mn_MN.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"category.delete.description": "Энэ ангиллыг устгавал холбоотой {transactionCount} гүйлгээг ангилалгүй болохыг анхаарна уу. Энэ үйлдлийг буцаах боломжгүй юм!",
"category.name": "Нэр",
"category.new": "Ангилал үүсгэх",
"category.new.success": "Шинэ ангилал амжилттай үүслээ",
"category.none": "Ангилалгүй",
"category.skip": "Ангилалгүй",
"contributors": "Хувь нэмэр оруулсан",
Expand Down Expand Up @@ -248,6 +249,7 @@
"general.edit": "Засварлах",
"general.enabled": "Идэвхтэй",
"general.flow": "Урсгал",
"general.new": "Шинэ",
"general.nextNDays": "Ирэх {n} хоног",
"general.paste": "Буулгах",
"general.save": "Хадгалах",
Expand Down
2 changes: 2 additions & 0 deletions assets/l10n/ru_RU.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"category.delete.description": "Удаление этой категории оставит {transactionCount} транзакций без категории. Это действие необратимо!",
"category.name": "Название категории",
"category.new": "Добавить категорию",
"category.new.success": "Новая категория успешно создана",
"category.none": "Без категории",
"category.skip": "Без категории",
"contributors": "Участники",
Expand Down Expand Up @@ -248,6 +249,7 @@
"general.edit": "Редактировать",
"general.enabled": "Включено",
"general.flow": "Flow",
"general.new": "Создать",
"general.nextNDays": "Следующие {n} дн.",
"general.paste": "Вставить",
"general.save": "Сохранить",
Expand Down
2 changes: 2 additions & 0 deletions assets/l10n/tr_TR.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"category.delete.description": "Bu kategoriyi silmek, işlemleri kategorisiz bırakır {transactionCount} . Bu eylem geri alınamaz!",
"category.name": "Kategori adı",
"category.new": "Kategori ekleme",
"category.new.success": "Yeni kategori başarıyla oluşturuldu",
"category.none": "Kategori yok",
"category.skip": "Kategori yok",
"contributors": "Katkıda bulunanlar",
Expand Down Expand Up @@ -248,6 +249,7 @@
"general.edit": "Düzenlemek",
"general.enabled": "Etkin",
"general.flow": "Akış",
"general.new": "Yeni",
"general.nextNDays": "Sonraki {n} gün",
"general.paste": "Yapıştır",
"general.save": "Kaydetmek",
Expand Down
2 changes: 2 additions & 0 deletions assets/l10n/uk_UA.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"category.delete.description": "Видалення цієї категорії залишить {transactionCount} транзакцій без категорії. Ця дія незворотна!",
"category.name": "Назва категорії",
"category.new": "Додати категорію",
"category.new.success": "Успішно створено нову категорію",
"category.none": "Без категорії",
"category.skip": "Без категорії",
"contributors": "Учасники",
Expand Down Expand Up @@ -248,6 +249,7 @@
"general.edit": "Редагувати",
"general.enabled": "Увімкнено",
"general.flow": "Flow",
"general.new": "Новий",
"general.nextNDays": "Наступні {n} дн.",
"general.paste": "Вставити",
"general.save": "Зберегти",
Expand Down
25 changes: 21 additions & 4 deletions ios/Runner/RecordTransactionIntent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,38 @@ struct RecordTransactionIntent: AppIntent {
var account: String

@Parameter(title: "Amount", description: "Expense amount. Sign doesn't matter.")
var amount: Double
var amount: String

@Parameter(title: "Category", description: "Exact name, or UUID of the target account.")
var category: String

@Parameter(title: "Notes", description: "Transaction notes. Markdown supported.")
var notes: String

@Parameter(title: "Title", description: "Transaction title.")
var title: String

static var openAppWhenRun = false

func perform() async throws -> some IntentResult & ProvidesDialog {
let tx = RecordedTransaction(type: .expense, amount: amount, title: title, fromAccount: account, category: category, notes: notes)
let formatter = NumberFormatter()
formatter.locale = Locale.current
formatter.numberStyle = .decimal

let parsedAmount: Double
if let number = formatter.number(from: amount) {
parsedAmount = number.doubleValue
} else {
let digitsOnly = amount.replacingOccurrences(of: "[^0-9]", with: "", options: .regularExpression)
let hadDecimalSeparator = amount.contains(formatter.decimalSeparator)
guard let fallbackNumber = Double(digitsOnly) else {
throw NSError(domain: "InvalidAmount", code: 1)
}
parsedAmount = fallbackNumber / (hadDecimalSeparator ? 100 : 1)
}
let tx = RecordedTransaction(
type: .expense, amount: parsedAmount, title: title, fromAccount: account, category: category,
notes: notes)
try RecordedTransactionService.append(tx)
return .result(dialog: "Expense recorded ✅")
}
Expand Down
78 changes: 78 additions & 0 deletions lib/data/multi_filter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import "package:flow/entity/_base.dart";
import "package:flutter/foundation.dart";
import "package:json_annotation/json_annotation.dart";

typedef Comparer<T> = bool Function(T a, T b);

/// Used to filter items based on a whitelist or blacklist of items.
///
/// Only [MultiFilter<String>] can be serialized
class MultiFilter<T> {
final bool whitelist;

final List<T> items;

@JsonKey(includeFromJson: false, includeToJson: false)
final Comparer<T>? comparer;

const MultiFilter({
required this.whitelist,
required this.items,
this.comparer,
});
const MultiFilter.keepNothing()
: items = const [],
whitelist = true,
comparer = _alwaysFalse;
const MultiFilter.keepEverything()
: items = const [],
whitelist = false,
comparer = _alwaysFalse;
const MultiFilter.whitelist(this.items, {this.comparer}) : whitelist = true;
const MultiFilter.blacklist(this.items, {this.comparer}) : whitelist = false;

static bool _alwaysFalse(dynamic a, dynamic b) => false;
static bool flowDefaultComparer(dynamic a, dynamic b) {
if (a is EntityBase && b is EntityBase) {
return a.uuid == b.uuid;
}

if (a is List && b is List) {
return listEquals(a, b);
}

if (a is Set && b is Set) {
return setEquals(a, b);
}

if (a is Map && b is Map) {
return mapEquals(a, b);
}

return a == b;
}

/// Always outputs the items to keep.
List<T> filter(Iterable<T> input) {
final Comparer<T> comparer = this.comparer ?? flowDefaultComparer;
return input.where((element) {
final bool contains = items.any((item) => comparer(item, element));
return whitelist ? contains : !contains;
}).toList();
}

List<K> mappedFilter<K>(Iterable<K> input, T Function(K) mapper) {
final Comparer<T> comparer = this.comparer ?? flowDefaultComparer;
return input.where((element) {
final T mapped = mapper(element);
final bool contains = items.any((item) => comparer(item, mapped));
return whitelist ? contains : !contains;
}).toList();
}

bool contains(T item) {
final Comparer<T> comparer = this.comparer ?? flowDefaultComparer;
final bool contains = items.any((i) => comparer(i, item));
return whitelist ? contains : !contains;
}
}
Loading