diff --git a/.vscode/settings.json b/.vscode/settings.json index cfd503c5..1eee6b6c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,6 @@ "cmake.configureOnOpen": false, "IDX.aI.enableInlineCompletion": true, "IDX.aI.enableCodebaseIndexing": true, - "editor.tabSize": 2 + "editor.tabSize": 2, + "swift.swiftSDK": "arm64-apple-ios" } diff --git a/CHANGELOG.md b/CHANGELOG.md index f67f26f3..63cf7224 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## 0.19.0 + +### 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. + ## 0.18.2 ### Fixes diff --git a/assets/images/2.0x/siri.png b/assets/images/2.0x/siri.png new file mode 100644 index 00000000..6b9c9145 Binary files /dev/null and b/assets/images/2.0x/siri.png differ diff --git a/assets/images/3.0x/siri.png b/assets/images/3.0x/siri.png new file mode 100644 index 00000000..0d9c17bd Binary files /dev/null and b/assets/images/3.0x/siri.png differ diff --git a/assets/images/4.0x/siri.png b/assets/images/4.0x/siri.png new file mode 100644 index 00000000..ad37467b Binary files /dev/null and b/assets/images/4.0x/siri.png differ diff --git a/assets/images/siri.png b/assets/images/siri.png new file mode 100644 index 00000000..6d32726f Binary files /dev/null and b/assets/images/siri.png differ diff --git a/assets/l10n/ar.json b/assets/l10n/ar.json index d4f6bacf..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": "ابحث...", @@ -383,8 +389,9 @@ "preferences.transactions.listTile.leading.account": "الحساب", "preferences.transactions.listTile.leading.category": "الفئة", "preferences.transactions.listTile.preview": "معاينة", + "preferences.transactions.listTile.relaxedDensity": "تخطيط أقل كثافة", "preferences.transactions.listTile.showCategoryInList": "عرض الفئة بعد الحساب", - "preferences.transactions.listTile.transactionListTileRelaxedDensity": "تخطيط أقل كثافة", + "preferences.transactions.listTile.showExternalSource": "عرض المصادر الخارجية (مثل Eny)", "preferences.transactions.pending": "المعاملات المعلقة", "preferences.transactions.pending.homeTimeframe": "عرضها على الصفحة الرئيسية", "preferences.transactions.pending.notify": "إعلام", @@ -667,8 +674,8 @@ "transaction.edit.selectAccount.noPossibleChoice": "لا يوجد حساب للاختيار", "transaction.edit.selectCategory": "اختر فئة", "transaction.edit.selectCategory.multiple": "اختر الفئات", - "transaction.edit.selectTags": "اختر الوسوم", "transaction.external.added": "تمت إضافة معاملة جديدة", + "transaction.external.added.from": "تمت إضافة معاملة جديدة بواسطة {name}", "transaction.external.from": "مضافة من {name}", "transaction.fallbackTitle": "معاملة بدون عنوان", "transaction.location": "الموقع", @@ -687,15 +694,16 @@ "transaction.recurring.edit": "تعديل المعاملة المتكررة", "transaction.recurring.setup": "إعداد المعاملة المتكررة", "transaction.tags": "الوسوم", + "transaction.tags.add": "إضافة وسوم", "transaction.tags.contact.name": "اسم جهة الاتصال", "transaction.tags.contact.select": "اختر جهة اتصال من الهاتف", "transaction.tags.delete": "حذف الوسم", "transaction.tags.delete.description": "سيؤدي حذف هذا الوسم إلى فك ارتباطه عن {transactionCount} معاملة. لا يمكن التراجع عن هذا الإجراء!", - "transaction.tags.editGuide": "اضغط لتعديل الوسوم", "transaction.tags.location.name": "اسم الموقع", "transaction.tags.location.useCurrent": "استخدام الموقع الحالي", "transaction.tags.name": "اسم الوسم", "transaction.tags.new": "وسم جديد", + "transaction.tags.suggestionGuide": "اضغط على الوسم المقترح لإضافته", "transaction.transfer.conversionRate": "سعر التحويل", "transaction.transfer.from": "الحساب المرسل", "transaction.transfer.from.select": "التحويل من", diff --git a/assets/l10n/cs_CZ.json b/assets/l10n/cs_CZ.json index b3ca12ae..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...", @@ -383,8 +389,9 @@ "preferences.transactions.listTile.leading.account": "Účet", "preferences.transactions.listTile.leading.category": "Kategorie", "preferences.transactions.listTile.preview": "Náhled", + "preferences.transactions.listTile.relaxedDensity": "Volnější rozložení seznamu", "preferences.transactions.listTile.showCategoryInList": "Zobrazit kategorii vedle účtu", - "preferences.transactions.listTile.transactionListTileRelaxedDensity": "Volnější rozložení seznamu", + "preferences.transactions.listTile.showExternalSource": "Zobrazit externí zdroje (např. Eny)", "preferences.transactions.pending": "Čekající transakce", "preferences.transactions.pending.homeTimeframe": "Zobrazit na domovské obrazovce", "preferences.transactions.pending.notify": "Upozornit", @@ -667,8 +674,8 @@ "transaction.edit.selectAccount.noPossibleChoice": "Žádné účty k výběru.", "transaction.edit.selectCategory": "Vyberte kategorii", "transaction.edit.selectCategory.multiple": "Vyberte kategorie", - "transaction.edit.selectTags": "Vybrat štítky", "transaction.external.added": "Byla přidána nová transakce", + "transaction.external.added.from": "Nová transakce byla přidána uživatelem {name}.", "transaction.external.from": "Přidáno z {name}", "transaction.fallbackTitle": "Nepojmenovaná transakce", "transaction.location": "Místo", @@ -687,15 +694,16 @@ "transaction.recurring.edit": "Upravit opakující se transakci", "transaction.recurring.setup": "Nastavit opakující se transakci", "transaction.tags": "Štítky", + "transaction.tags.add": "Přidat štítky", "transaction.tags.contact.name": "Jméno kontaktu", "transaction.tags.contact.select": "Vybrat kontakt z telefonu", "transaction.tags.delete": "Smazat štítek", "transaction.tags.delete.description": "Smazáním tohoto štítku se odstraní ze všech ({transactionCount}) transakcí. Tato akce je nevratná!", - "transaction.tags.editGuide": "Klepněte pro úpravu štítků", "transaction.tags.location.name": "Název místa", "transaction.tags.location.useCurrent": "Použít aktuální polohu", "transaction.tags.name": "Název štítku", "transaction.tags.new": "Nový štítek", + "transaction.tags.suggestionGuide": "Klepněte na navržený štítek pro jeho přidání", "transaction.transfer.conversionRate": "Směnný kurz", "transaction.transfer.from": "Z účtu", "transaction.transfer.from.select": "Převést z", diff --git a/assets/l10n/de_DE.json b/assets/l10n/de_DE.json index 7d631578..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...", @@ -383,8 +389,9 @@ "preferences.transactions.listTile.leading.account": "Konto", "preferences.transactions.listTile.leading.category": "Kategorie", "preferences.transactions.listTile.preview": "Vorschau", + "preferences.transactions.listTile.relaxedDensity": "Weniger dichtes Layout", "preferences.transactions.listTile.showCategoryInList": "Kategorie nach dem Konto anzeigen", - "preferences.transactions.listTile.transactionListTileRelaxedDensity": "Weniger dichtes Layout", + "preferences.transactions.listTile.showExternalSource": "Externe Quellen anzeigen (z. B. Eny)", "preferences.transactions.pending": "Ausstehende Buchungen", "preferences.transactions.pending.homeTimeframe": "Auf der Startseite anzeigen", "preferences.transactions.pending.notify": "Benachrichtigen", @@ -667,8 +674,8 @@ "transaction.edit.selectAccount.noPossibleChoice": "Keine Konten zur Auswahl.", "transaction.edit.selectCategory": "Kategorie auswählen", "transaction.edit.selectCategory.multiple": "Kategorien auswählen", - "transaction.edit.selectTags": "Tags auswählen", "transaction.external.added": "Eine neue Transaktion wurde hinzugefügt", + "transaction.external.added.from": "Eine neue Transaktion wurde von {name} hinzugefügt", "transaction.external.from": "Hinzugefügt von {name}", "transaction.fallbackTitle": "Buchung ohne Titel", "transaction.location": "Standort", @@ -687,15 +694,16 @@ "transaction.recurring.edit": "Wiederkehrende Buchung bearbeiten", "transaction.recurring.setup": "Wiederkehrende Buchung einrichten", "transaction.tags": "Tags", + "transaction.tags.add": "Tags hinzufügen", "transaction.tags.contact.name": "Kontaktname", "transaction.tags.contact.select": "Kontakt vom Telefon auswählen", "transaction.tags.delete": "Schlagwort löschen", "transaction.tags.delete.description": "Wenn Sie dieses Schlagwort löschen, wird seine Verknüpfung mit {transactionCount} Transaktionen entfernt. Diese Aktion kann nicht rückgängig gemacht werden!", - "transaction.tags.editGuide": "Zum Bearbeiten der Tags tippen", "transaction.tags.location.name": "Standortname", "transaction.tags.location.useCurrent": "Aktuellen Standort verwenden", "transaction.tags.name": "Tag-Name", "transaction.tags.new": "Neues Tag", + "transaction.tags.suggestionGuide": "Tippe auf den vorgeschlagenen Tag, um ihn hinzuzufügen", "transaction.transfer.conversionRate": "Umrechnungskurs", "transaction.transfer.from": "Senderkonto", "transaction.transfer.from.select": "Umbuchen von", diff --git a/assets/l10n/en.json b/assets/l10n/en.json index 755abf65..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...", @@ -383,8 +389,9 @@ "preferences.transactions.listTile.leading.account": "Account", "preferences.transactions.listTile.leading.category": "Category", "preferences.transactions.listTile.preview": "Preview", + "preferences.transactions.listTile.relaxedDensity": "Less dense layout", "preferences.transactions.listTile.showCategoryInList": "Show category after the account", - "preferences.transactions.listTile.transactionListTileRelaxedDensity": "Less dense layout", + "preferences.transactions.listTile.showExternalSource": "Show external sources (e.g., Eny)", "preferences.transactions.pending": "Pending transactions", "preferences.transactions.pending.homeTimeframe": "Show on home", "preferences.transactions.pending.notify": "Notify", @@ -667,8 +674,8 @@ "transaction.edit.selectAccount.noPossibleChoice": "No accounts to select", "transaction.edit.selectCategory": "Select a category", "transaction.edit.selectCategory.multiple": "Select categories", - "transaction.edit.selectTags": "Select tags", "transaction.external.added": "A new transaction was added", + "transaction.external.added.from": "A new transaction was added by {name}", "transaction.external.from": "Added from {name}", "transaction.fallbackTitle": "Untitled transaction", "transaction.location": "Location", @@ -687,15 +694,16 @@ "transaction.recurring.edit": "Edit recurring transaction", "transaction.recurring.setup": "Setup recurring transaction", "transaction.tags": "Tags", + "transaction.tags.add": "Add tags", "transaction.tags.contact.name": "Contact name", "transaction.tags.contact.select": "Select contact from phone", "transaction.tags.delete": "Delete tag", "transaction.tags.delete.description": "Deleting this tag will detach {transactionCount} transactions' tag. This action is irreversible!", - "transaction.tags.editGuide": "Tap to edit the tags", "transaction.tags.location.name": "Location name", "transaction.tags.location.useCurrent": "Use current location", "transaction.tags.name": "Tag name", "transaction.tags.new": "New tag", + "transaction.tags.suggestionGuide": "Tap on the suggested tag to add", "transaction.transfer.conversionRate": "Conversion rate", "transaction.transfer.from": "Sending account", "transaction.transfer.from.select": "Transfer from", diff --git a/assets/l10n/es_ES.json b/assets/l10n/es_ES.json index 83f79a23..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...", @@ -383,8 +389,9 @@ "preferences.transactions.listTile.leading.account": "Cuenta", "preferences.transactions.listTile.leading.category": "Categoría", "preferences.transactions.listTile.preview": "Vista previa", + "preferences.transactions.listTile.relaxedDensity": "Diseño menos denso", "preferences.transactions.listTile.showCategoryInList": "Mostrar categoría después de la cuenta", - "preferences.transactions.listTile.transactionListTileRelaxedDensity": "Diseño menos denso", + "preferences.transactions.listTile.showExternalSource": "Mostrar fuentes externas (p. ej., Eny)", "preferences.transactions.pending": "Transacciones pendientes", "preferences.transactions.pending.homeTimeframe": "Mostrar en inicio", "preferences.transactions.pending.notify": "Notificar", @@ -667,8 +674,8 @@ "transaction.edit.selectAccount.noPossibleChoice": "No hay cuentas para seleccionar", "transaction.edit.selectCategory": "Seleccionar una categoría", "transaction.edit.selectCategory.multiple": "Seleccionar categorías", - "transaction.edit.selectTags": "Seleccionar etiquetas", "transaction.external.added": "Se ha añadido una nueva transacción", + "transaction.external.added.from": "Una nueva transacción ha sido añadida por {name}", "transaction.external.from": "Añadido desde {name}", "transaction.fallbackTitle": "Transacción sin título", "transaction.location": "Ubicación", @@ -687,15 +694,16 @@ "transaction.recurring.edit": "Editar transacción recurrente", "transaction.recurring.setup": "Configurar transacción recurrente", "transaction.tags": "Etiquetas", + "transaction.tags.add": "Añadir etiquetas", "transaction.tags.contact.name": "Nombre del contacto", "transaction.tags.contact.select": "Seleccionar contacto del teléfono", "transaction.tags.delete": "Eliminar etiqueta", "transaction.tags.delete.description": "Al eliminar esta etiqueta, se quitará la etiqueta de {transactionCount} transacciones. ¡Esta acción es irreversible!", - "transaction.tags.editGuide": "Toca para editar las etiquetas", "transaction.tags.location.name": "Nombre de la ubicación", "transaction.tags.location.useCurrent": "Usar ubicación actual", "transaction.tags.name": "Nombre de la etiqueta", "transaction.tags.new": "Nueva etiqueta", + "transaction.tags.suggestionGuide": "Pulsa la etiqueta sugerida para añadirla", "transaction.transfer.conversionRate": "Tasa de conversión", "transaction.transfer.from": "Cuenta de envío", "transaction.transfer.from.select": "Transferir desde", diff --git a/assets/l10n/fa_IR.json b/assets/l10n/fa_IR.json new file mode 100644 index 00000000..1bcc31d4 --- /dev/null +++ b/assets/l10n/fa_IR.json @@ -0,0 +1,766 @@ +{ + "account": "حساب", + "account.archive": "غیرفعال کردن", + "account.archive.description": "غیرفعال کردن حساب، آن را در همه‌جا به‌جز گزارش‌ها مخفی می‌کند. می‌توانید بعد از غیرفعال‌سازی، حساب را به‌همراه تراکنش‌های مرتبط، برای همیشه حذف کنید.", + "account.archived": "غیرفعال", + "account.balance": "موجودی", + "account.balance.upcomingDescription": "تراکنش‌های آتی روی موجودی فعلی اثر ندارند", + "account.creditLimit": "سقف اعتبار", + "account.delete": "حذف حساب", + "account.delete.description": "با حذف این حساب، {transactionCount} تراکنش مرتبط هم حذف می‌شود. این کار غیرقابل بازگشت است و در سطل زباله هم قرار نمی‌گیرد!", + "account.edit": "ویرایش حساب", + "account.edit.selectCurrency": "یک ارز انتخاب کنید", + "account.excludeFromTotalBalance": "عدم لحاظ در موجودی کل", + "account.excludeFromTotalBalance.description": "اگر این گزینه را فعال کنید، موجودی این حساب در موجودی کل لحاظ نمی‌شود. مناسب برای پس‌انداز یا حساب‌های غیرشخصی.", + "account.name": "نام حساب", + "account.new": "افزودن حساب", + "account.noAccounts": "هیچ حسابی ندارید!", + "account.postTransactionBalance": "موجودی پس از این تراکنش", + "account.primaryAccount": "حساب اصلی", + "account.primaryAccount.changeDescription": "می‌توانید با رفتن به صفحه ویرایش یک حساب دیگر، آن را به‌عنوان حساب اصلی تنظیم کنید.", + "account.primaryAccount.description": "حساب اصلی به‌عنوان حساب پیش‌فرض برای تراکنش‌های جدید و برخی قابلیت‌های دیگر استفاده می‌شود.", + "account.primaryAccount.notPrimary": "حساب اصلی نیست", + "account.primaryAccount.set": "تنظیم به‌عنوان حساب اصلی", + "account.thisMonth": "این ماه", + "account.transactions": "تراکنش‌ها", + "account.transactions.title": "تراکنش‌های «{account}»", + "account.type": "نوع حساب", + "account.updateBalance": "به‌روزرسانی موجودی", + "account.updateBalance.chooseUpdateMode": "انتخاب روش به‌روزرسانی", + "account.updateBalance.transactionTitle": "به‌روزرسانی موجودی", + "account.updateBalance.updateAtDate": "همگام‌سازی موجودی گذشته بر اساس تاریخ", + "account.updateBalance.updateAtDate.description": "مناسب وقتی که موجودی دقیق در یک تاریخ مشخص را می‌دانید", + "account.updateBalance.updateCurrent": "به‌روزرسانی موجودی فعلی", + "accounts": "حساب‌ها", + "appName": "Flow", + "appShortDesc": "مدیریت مالی شخصی شما", + "categories": "دسته‌بندی‌ها", + "categories.addFromPresets": "افزودن از پیش‌فرض‌ها", + "categories.noCategories": "هیچ دسته‌بندی‌ای ندارید", + "category": "دسته‌بندی", + "category.delete": "حذف دسته‌بندی", + "category.delete.description": "با حذف این دسته‌بندی، {transactionCount} تراکنش بدون دسته‌بندی می‌ماند. این کار غیرقابل بازگشت است!", + "category.name": "نام دسته‌بندی", + "category.new": "افزودن دسته‌بندی", + "category.none": "بدون دسته‌بندی", + "category.skip": "بدون دسته‌بندی", + "contributors": "مشارکت‌کنندگان", + "currency": "ارز", + "currency.searchHint": "جستجو... (کشور، ارز، کد)", + "enum.AccountType": "نوع حساب", + "enum.AccountType@asset": "دارایی", + "enum.AccountType@creditLine": "اعتباری (مثلاً کارت)", + "enum.AccountType@debit": "جاری", + "enum.AccountType@loan": "وام", + "enum.AccountType@other": "سایر", + "enum.AccountType@savings": "پس‌انداز", + "enum.BackupEntryType@automated": "پشتیبان‌گیری خودکار", + "enum.BackupEntryType@automated.description": "نسخه پشتیبان به‌صورت خودکار (یعنی زمان‌بندی‌شده) ساخته شده است", + "enum.BackupEntryType@manual": "دستی", + "enum.BackupEntryType@manual.description": "نسخه پشتیبان توسط کاربر ساخته شده است", + "enum.BackupEntryType@other": "پشتیبان دیگر", + "enum.BackupEntryType@other.description": "پشتیبان دیگر", + "enum.BackupEntryType@preAccountDeletion": "احتیاطی (حذف حساب)", + "enum.BackupEntryType@preAccountDeletion.description": "نسخه پشتیبان به‌عنوان احتیاط قبل از حذف یک حساب توسط کاربر ساخته شده است", + "enum.BackupEntryType@preImport": "احتیاطی (پیش از ایمپورت)", + "enum.BackupEntryType@preImport.description": "نسخه پشتیبان به‌عنوان احتیاط قبل از ایمپورت از نسخه پشتیبان قبلی ساخته شده است", + "enum.CSVHeader": "سربرگ‌های CSV", + "enum.CSVHeader@account": "حساب", + "enum.CSVHeader@accountUuid": "شناسه حساب", + "enum.CSVHeader@amount": "مبلغ", + "enum.CSVHeader@category": "دسته‌بندی", + "enum.CSVHeader@categoryUuid": "شناسه دسته‌بندی", + "enum.CSVHeader@createdDate": "تاریخ ایجاد", + "enum.CSVHeader@currency": "ارز", + "enum.CSVHeader@extra": "اضافی (JSON)", + "enum.CSVHeader@latitude": "عرض جغرافیایی", + "enum.CSVHeader@longitude": "طول جغرافیایی", + "enum.CSVHeader@notes": "یادداشت‌ها", + "enum.CSVHeader@subtype": "کلاس تراکنش", + "enum.CSVHeader@title": "عنوان", + "enum.CSVHeader@transactionDate": "تاریخ تراکنش", + "enum.CSVHeader@transactionDateIso8601": "تاریخ تراکنش (ISO 8601)", + "enum.CSVHeader@type": "نوع", + "enum.CSVHeader@uuid": "شناسه", + "enum.FlowButtonType": "نوع تراکنش", + "enum.FlowButtonType@eny": "Eny", + "enum.FlowButtonType@expense": "هزینه", + "enum.FlowButtonType@income": "درآمد", + "enum.FlowButtonType@transfer": "انتقال", + "enum.ImportCSVProgress@creatingAccounts": "در حال ساخت حساب‌ها", + "enum.ImportCSVProgress@creatingCategories": "در حال ساخت دسته‌بندی‌ها", + "enum.ImportCSVProgress@creatingTransactions": "در حال ساخت تراکنش‌ها", + "enum.ImportCSVProgress@erasing": "در حال پاک کردن داده‌های فعلی", + "enum.ImportCSVProgress@error": "مشکلی پیش آمد ({error})", + "enum.ImportCSVProgress@parsing": "در حال پردازش داده‌ها", + "enum.ImportCSVProgress@success": "موفق", + "enum.ImportCSVProgress@waitingConfirmation": "در انتظار تأیید", + "enum.ImportV1Progress@copyingFileAttachments": "در حال کپی پیوست‌های فایل", + "enum.ImportV1Progress@erasing": "در حال پاک کردن داده‌های فعلی", + "enum.ImportV1Progress@error": "مشکلی پیش آمد ({error})", + "enum.ImportV1Progress@resolvingTransactions": "در حال مرتب‌سازی تراکنش‌ها", + "enum.ImportV1Progress@success": "موفق", + "enum.ImportV1Progress@waitingConfirmation": "در انتظار تأیید", + "enum.ImportV1Progress@writingAccounts": "در حال نوشتن حساب‌ها", + "enum.ImportV1Progress@writingCategories": "در حال نوشتن دسته‌بندی‌ها", + "enum.ImportV1Progress@writingTransactions": "در حال نوشتن تراکنش‌ها", + "enum.ImportV2Progress@copyingImages": "در حال کپی تصاویر", + "enum.ImportV2Progress@erasing": "در حال پاک کردن داده‌های فعلی", + "enum.ImportV2Progress@error": "مشکلی پیش آمد ({error})", + "enum.ImportV2Progress@resolvingTransactions": "در حال مرتب‌سازی تراکنش‌ها", + "enum.ImportV2Progress@settingPrimaryCurrency": "در حال تنظیم ارز اصلی", + "enum.ImportV2Progress@success": "موفق", + "enum.ImportV2Progress@waitingConfirmation": "در انتظار تأیید", + "enum.ImportV2Progress@writingAccounts": "در حال نوشتن حساب‌ها", + "enum.ImportV2Progress@writingCategories": "در حال نوشتن دسته‌بندی‌ها", + "enum.ImportV2Progress@writingFileAttachments": "در حال نوشتن پیوست‌های فایل", + "enum.ImportV2Progress@writingProfile": "در حال نوشتن اطلاعات پروفایل", + "enum.ImportV2Progress@writingRecurringTransactions": "در حال نوشتن تراکنش‌های تکرارشونده", + "enum.ImportV2Progress@writingTransactionTags": "در حال نوشتن برچسب‌های تراکنش", + "enum.ImportV2Progress@writingTransactions": "در حال نوشتن تراکنش‌ها", + "enum.ImportV2Progress@writingTranscationFilterPresets": "در حال نوشتن پریست‌های فیلتر تراکنش", + "enum.ImportV2Progress@writingUserPreferences": "در حال نوشتن تنظیمات کاربر", + "enum.PDFHeader@account": "حساب", + "enum.PDFHeader@amount": "مبلغ", + "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": "هر روز", + "enum.RecurrenceMode@everyMonth": "هر ماه، {dayOfMonth}", + "enum.RecurrenceMode@everyWeek": "هر هفته، {weekday}", + "enum.RecurrenceMode@everyYear": "هر سال، {monthAndDay}", + "enum.RecurringUpdateMode@all": "همه تراکنش‌ها", + "enum.RecurringUpdateMode@current": "این تراکنش", + "enum.RecurringUpdateMode@thisAndFuture": "این و تراکنش‌های آینده", + "enum.TransactionEditMode@normal": "عادی", + "enum.TransactionEditMode@pending": "در انتظار", + "enum.TransactionEditMode@recurring": "تکرارشونده", + "enum.TransactionEntryAction": "اقدام", + "enum.TransactionEntryAction@attachFiles": "پیوست فایل", + "enum.TransactionEntryAction@inputAmount": "وارد کردن مبلغ", + "enum.TransactionEntryAction@inputNote": "وارد کردن توضیحات", + "enum.TransactionEntryAction@inputTitle": "وارد کردن عنوان", + "enum.TransactionEntryAction@selectAccount": "انتخاب حساب", + "enum.TransactionEntryAction@selectCategoryOrTransferAccount": "انتخاب دسته‌بندی/حساب انتقال", + "enum.TransactionEntryAction@selectPrimaryAccount": "انتخاب حساب اصلی", + "enum.TransactionEntryAction@selectTags": "انتخاب برچسب‌ها", + "enum.TransactionFilterRangePreset@allTime": "تمام مدت", + "enum.TransactionFilterRangePreset@last30Days": "۳۰ روز اخیر", + "enum.TransactionFilterRangePreset@thisMonth": "این ماه", + "enum.TransactionFilterRangePreset@thisWeek": "این هفته", + "enum.TransactionFilterRangePreset@thisYear": "امسال", + "enum.TransactionGroupRange": "واحد گروه‌بندی", + "enum.TransactionGroupRange@allTime": "تمام مدت", + "enum.TransactionGroupRange@day": "روز", + "enum.TransactionGroupRange@hour": "ساعت", + "enum.TransactionGroupRange@month": "ماه", + "enum.TransactionGroupRange@week": "هفته", + "enum.TransactionGroupRange@year": "سال", + "enum.TransactionSearchMode": "حالت جستجو", + "enum.TransactionSearchMode@exact": "تطابق دقیق", + "enum.TransactionSearchMode@none": "بدون عنوان", + "enum.TransactionSearchMode@smart": "هوشمند", + "enum.TransactionSearchMode@substring": "تطابق جزئی", + "enum.TransactionSubtype": "زیرنوع", + "enum.TransactionSubtype#null": "پیش‌فرض", + "enum.TransactionSubtype@givenLoan": "وام (داده‌شده)", + "enum.TransactionSubtype@receivedLoan": "وام (گرفته‌شده)", + "enum.TransactionSubtype@transactionFee": "کارمزد تراکنش", + "enum.TransactionSubtype@updateBalance": "به‌روزرسانی موجودی", + "enum.TransactionTagType": "نوع برچسب", + "enum.TransactionTagType@contact": "شخص", + "enum.TransactionTagType@generic": "عمومی", + "enum.TransactionTagType@location": "موقعیت", + "enum.TransactionType": "نوع تراکنش", + "enum.TransactionType@expense": "هزینه", + "enum.TransactionType@income": "درآمد", + "enum.TransactionType@transfer": "انتقال", + "error.exchangeRates.cannotFetch": "دریافت ناموفق بود، لطفاً اتصال اینترنت را بررسی کنید.", + "error.exchangeRates.inaccurateDataDueToMissingRates": "دریافت نرخ ارز ناموفق بود؛ ممکن است داده‌های تراکنش کاملاً دقیق نباشد", + "error.failedLocalAuth": "احراز هویت ناموفق بود، لطفاً دوباره تلاش کنید.", + "error.input.cropFailed": "حین برش تصویر خطایی رخ داد", + "error.input.duplicate.accountName": "نام «{}» قبلاً استفاده شده است. نام دیگری انتخاب کنید.", + "error.input.invalidZip": "این فایل zip معتبرِ Flow نیست", + "error.input.mustBeNotEmpty": "لطفاً این فیلد را پر کنید", + "error.input.noFilePicked": "هیچ فایلی انتخاب نشد", + "error.input.noImagePicked": "هیچ تصویری انتخاب نشد", + "error.input.pasteFormatMismatch": "امکان پردازش وجود ندارد", + "error.input.wrongFileType": "لطفاً یک فایل {type} انتخاب کنید", + "error.noConnection": "اتصال اینترنت وجود ندارد", + "error.route.400": "بارگذاری صفحه ناموفق بود", + "error.route.404": "صفحه پیدا نشد", + "error.sync.exportFailed": "امکان خروجی گرفتن نیست، لطفاً با توسعه‌دهنده تماس بگیرید.", + "error.sync.fileDeleteFailed": "حین حذف نسخه پشتیبان خطایی رخ داد", + "error.sync.fileNotFound": "فایل پیدا نشد", + "error.sync.invalidBackupFile": "فایل پشتیبان نامعتبر است", + "error.sync.safetyBackupFailed": "امکان شروع ایمپورت نیست", + "error.transaction.missingAccount": "لطفاً یک حساب انتخاب کنید", + "error.url.cannotOpen": "باز کردن لینک ممکن نیست", + "fileAttachment": "فایل", + "fileAttachment.add": "افزودن فایل‌ها", + "fileAttachment.cleanupHangingFiles": "حذف فایل‌های استفاده‌نشده", + "fileAttachment.cleanupHangingFiles.description": "این کار تمام پیوست‌هایی را که به هیچ تراکنشی وصل نیستند حذف می‌کند. اگر تراکنش مرتبط در سطل زباله باشد، فایل حذف نمی‌شود. این کار غیرقابل بازگشت است.", + "fileAttachment.delete": "حذف فایل", + "fileAttachment.delete.description": "این کار فقط کپی ذخیره‌شده در Flow را حذف می‌کند و فایل اصلی دست‌نخورده می‌ماند. می‌توانید بعداً فایل اصلی را دوباره اضافه کنید، اما این عمل قابل بازگشت نیست.", + "fileAttachment.delete.success": "فایل با موفقیت حذف شد", + "fileAttachment.file": "انتخاب از فایل‌ها", + "fileAttachment.open": "{name} باز شود؟", + "fileAttachment.open.description": "مطمئن هستید این فایل را باز کنید؟", + "fileAttachment.photo": "انتخاب یک عکس", + "fileAttachment.photos": "انتخاب چند رسانه", + "fileAttachment.pick": "انتخاب فایل(ها)", + "fileAttachment.share": "اشتراک‌گذاری فایل", + "fileAttachment.takePhoto": "گرفتن عکس", + "flowIcon.change": "تغییر آیکن", + "flowIcon.type.character": "ایموجی/حرف", + "flowIcon.type.character.description": "برای استفاده به‌عنوان آیکن، یک ایموجی یا حرف وارد کنید", + "flowIcon.type.icon": "آیکن", + "flowIcon.type.icon.brands": "برندها و لوگوها", + "flowIcon.type.icon.search": "جستجوی آیکن‌ها...", + "flowIcon.type.icon.symbols": "نمادها", + "flowIcon.type.image": "تصویر", + "flowIcon.type.image.description": "برای استفاده به‌عنوان آیکن، یک تصویر انتخاب کنید", + "flowIcon.type.image.paste": "چسباندن تصویر", + "flowIcon.type.image.pick": "انتخاب تصویر", + "general.areYouSure": "مطمئن هستید؟", + "general.back": "بازگشت", + "general.cancel": "انصراف", + "general.confirm": "تأیید", + "general.copy": "کپی", + "general.copy.clickToCopy": "برای کپی کلیک کنید", + "general.copy.success": "در کلیپ‌بورد کپی شد", + "general.delete": "حذف", + "general.delete.all": "حذف همه", + "general.delete.confirmName": "حذف {name} را تأیید می‌کنید؟", + "general.delete.permanentWarning": "این کار غیرقابل بازگشت است", + "general.delete.unsavedProgress": "بدون ذخیره ببندم؟", + "general.delete.unsavedProgress.description": "تمام تغییرات از بین می‌رود.", + "general.disabled": "غیرفعال", + "general.done": "انجام شد", + "general.edit": "ویرایش", + "general.enabled": "فعال", + "general.flow": "Flow", + "general.nextNDays": "{n} روز آینده", + "general.paste": "چسباندن", + "general.save": "ذخیره", + "general.search": "جستجو...", + "general.select": "انتخاب", + "general.select.all": "انتخاب همه", + "general.selectLocation": "انتخاب موقعیت", + "general.unlockToOpen": "برای باز کردن Flow قفل را باز کنید", + "integrations.eny": "Eny", + "integrations.eny.connect": "اتصال Eny", + "integrations.eny.connect.conflict": "یک حساب قبلاً متصل شده است؛ می‌خواهید جایگزین شود؟", + "integrations.eny.connect.success": "Eny با موفقیت متصل شد", + "integrations.eny.connected#false": "قطع اتصال", + "integrations.eny.connected#true": "متصل", + "integrations.eny.creditsRemaining": "اعتبار باقی‌مانده", + "integrations.eny.dashboard": "داشبورد Eny", + "integrations.eny.dashboard.description": "می‌توانید حساب Eny خود را از داشبورد متصل کنید", + "integrations.eny.disconnect": "قطع اتصال Eny", + "integrations.eny.invalidCredentials": "اطلاعات ورود نامعتبر است", + "integrations.eny.invalidCredentials.configure": "پیکربندی", + "integrations.eny.invalidCredentials.description": "از داشبورد دوباره Eny را متصل کنید", + "integrations.eny.multipleImagesNotice": "{n} تصویر برای پردازش ارسال شود؟", + "integrations.eny.multipleImagesNotice.checkNotice": "حتی اگر برخی تصاویر ناموفق باشند هم اعتبار مصرف می‌شود. پس قبل از تأیید، تصاویر را دوباره چک کنید. حداکثر ۵ تصویر را هم‌زمان می‌توانید ارسال کنید.", + "integrations.eny.multipleImagesNotice.description": "مصرف {n} اعتبار Eny", + "integrations.eny.privacyNotice": "اعلان حریم خصوصی", + "integrations.eny.privacyNotice.dataSharing": "داده‌های شما به اینجا ارسال می‌شود:", + "integrations.eny.privacyNotice.dataSharing#eny": "Eny", + "integrations.eny.privacyNotice.dataSharing#google": "Google", + "integrations.eny.privacyNotice.description": "این یک سرویس خارجی است که داده‌های شما را پردازش می‌کند؛ پس درباره چیزی که می‌فرستید دقت کنید. Flow به نمایندگی از شما چیزی ارسال نمی‌کند؛ فقط همان تصاویر/داده‌ای که انتخاب می‌کنید ارسال می‌شود.", + "integrations.eny.privacyNotice.legal": "لطفاً شرایط استفاده و سیاست حریم خصوصی Eny را بررسی کنید.", + "integrations.eny.send": "ارسال", + "integrations.eny.sent": "در حال پردازش رسید، لطفاً صبر کنید...", + "logs.delete": "حذف فایل لاگ", + "logs.delete.confirmation": "مطمئنید می‌خواهید این فایل لاگ را حذف کنید؟", + "logs.deleted": "فایل لاگ با موفقیت حذف شد", + "notifications.alarm.androidDescription": "برای دریافت یادآوری‌های دقیق، مجوز «Alarms and Reminder» (آخرین گزینه در لیست مجوزها) را بدهید.", + "notifications.alarm.permissionNotGranted": "مجوز آلارم/یادآوری داده نشده", + "notifications.openSettingsToGrantPermission": "مجوز اعلان را از تنظیمات سیستم بدهید", + "notifications.permissionNotGranted": "مجوز اعلان داده نشده", + "notifications.reminderText#1": "امروز هزینه‌هایت را ثبت کردی؟", + "notifications.reminderText#2": "ثبت تراکنش‌ها را فراموش نکن!", + "notifications.reminderText#3": "وقت ثبت هزینه‌های امروز است.", + "notifications.reminderText#4": "وضعیت مالی‌ات را کنترل کن، تراکنش‌ها را اضافه کن!", + "notifications.reminderText#5": "Flow یادآوری می‌کند هزینه‌هایت را ثبت کنی!", + "notifications.reminderText#6": "تراکنش‌ها را به‌روزرسانی کن و مدیریت مالی‌ات را حفظ کن.", + "notifications.reminderText#7": "فراموش نکن امروز تراکنش‌هایت را اضافه کنی!", + "preferences": "تنظیمات", + "preferences.appearance": "ظاهر", + "preferences.changeVisuals": "تغییر", + "preferences.changeVisuals.arrow": "فلش", + "preferences.changeVisuals.clickToChange": "برای تغییر، روی فلش/رنگ کلیک کنید", + "preferences.changeVisuals.color": "رنگ", + "preferences.changeVisuals.expenseIncrease": "افزایش هزینه", + "preferences.changeVisuals.incomeIncrease": "افزایش درآمد", + "preferences.dateFormat": "فرمت تاریخ", + "preferences.feedback": "مشکلات و بازخورد", + "preferences.feedback.debugLogs": "مشاهده لاگ‌های دیباگ", + "preferences.hapticFeedback": "بازخورد دکمه", + "preferences.hapticFeedback.description": "بازخورد صوتی/هپتیک هنگام کلیک", + "preferences.integrations": "یکپارچه‌سازی‌ها", + "preferences.language": "زبان", + "preferences.language.choose": "یک زبان انتخاب کنید", + "preferences.moneyFormatting": "فرمت نمایش پول", + "preferences.moneyFormatting.preferFull": "ترجیح مقدار کامل", + "preferences.moneyFormatting.preferFull.description": "تا جای ممکن اعداد را خلاصه نکن", + "preferences.moneyFormatting.setICUPattern": "انتخاب فرمت سفارشی", + "preferences.moneyFormatting.setICUPattern.default": "پیش‌فرض", + "preferences.moneyFormatting.useCurrencySymbol": "استفاده از نماد ارز", + "preferences.moneyFormatting.useCurrencySymbol.description": "مثلاً «$5» به‌جای «5 USD»", + "preferences.numpad": "صفحه‌کلید عددی", + "preferences.numpad.layout": "چیدمان صفحه‌کلید عددی", + "preferences.numpad.layout.classic": "کلاسیک", + "preferences.numpad.layout.modern": "مدرن", + "preferences.primaryCurrency": "ارز اصلی", + "preferences.privacy": "حریم خصوصی", + "preferences.privacy.appLock": "قفل برنامه", + "preferences.privacy.appLock.description#Android": "برای باز کردن برنامه، احراز هویت بیومتریک لازم باشد", + "preferences.privacy.appLock.description#iOS": "برای باز کردن برنامه، Face ID یا Touch ID لازم باشد", + "preferences.privacy.appLock.lockAfterClosing": "قفل پس از بستن", + "preferences.privacy.maskAtShake": "مخفی‌سازی اعداد (*) با تکان دادن دستگاه", + "preferences.privacy.maskAtStartup": "مخفی‌سازی اعداد (*) هنگام شروع", + "preferences.reminders": "یادآوری", + "preferences.reminders.remindDaily": "یادآوری روزانه", + "preferences.reminders.remindDaily.description": "روزانه یادآوری کند هزینه‌ها را ثبت کنید", + "preferences.reminders.remindDaily.expiryWarning": "اگر ۷ روز پشت‌سرهم Flow را باز نکنید، یادآوری‌ها متوقف می‌شود", + "preferences.reminders.remindDaily.time": "یادآوری در ساعت", + "preferences.reminders.unsupportedPlatform": "زمان‌بندی اعلان‌ها در این پلتفرم پشتیبانی نمی‌شود", + "preferences.scan": "اسکن مدارک", + "preferences.scan.createTransactionsPerItemInScans": "ساخت یک تراکنش برای هر آیتم", + "preferences.scan.createTransactionsPerItemInScans.description": "برای رسیدهای طولانی ممکن است شلوغ شود", + "preferences.scan.markPendingThreshold": "علامت‌گذاری تراکنش‌ها به‌صورت در انتظار", + "preferences.scan.markPendingThreshold.description": "اگر خاموش باشد، تراکنش‌هایی که تاریخِ استخراج‌شده‌شان بیش از ۶ ساعت قبل باشد هم در حالت در انتظار می‌مانند", + "preferences.sync": "همگام‌سازی", + "preferences.sync.autoBackup": "پشتیبان‌گیری خودکار", + "preferences.sync.autoBackup.disabled": "غیرفعال", + "preferences.sync.autoBackup.interval": "فاصله پشتیبان‌گیری", + "preferences.sync.autoBackup.interval.description": "پشتیبان‌ها به‌صورت خودکار هنگام باز کردن برنامه ساخته می‌شوند، به شرط اینکه از آخرین پشتیبان، بازه زمانی تنظیم‌شده گذشته باشد.", + "preferences.sync.iCloud": "همگام‌سازی با iCloud", + "preferences.sync.iCloud.connectionFailed": "اتصال به iCloud ممکن نیست!", + "preferences.sync.iCloud.connectionFailed.tips#1": "اتصال اینترنت را بررسی کنید", + "preferences.sync.iCloud.connectionFailed.tips#2": "مطمئن شوید با Apple Account روی دستگاه وارد شده‌اید", + "preferences.sync.iCloud.connectionFailed.tips#3": "مطمئن شوید iCloud را در System Settings > Apple Account > iCloud > Drive فعال کرده‌اید", + "preferences.sync.iCloud.lastSyncFailed": "آخرین تلاش برای پشتیبان‌گیری در iCloud ناموفق بود. برای دیدن راه‌حل اینجا را کلیک کنید.", + "preferences.sync.iCloud.lastSyncedAt": "آخرین همگام‌سازی در {date}", + "preferences.sync.iCloud.noOfBackupsToKeep": "تعداد نسخه‌های پشتیبان قابل نگه‌داری", + "preferences.sync.iCloud.noOfBackupsToKeep.description": "حداکثر تعداد فایل پشتیبان که در iCloud نگه‌داری می‌شود. چون این فضا از iCloud شما مصرف می‌کند، شاید بخواهید این عدد کم باشد. بسته به این تنظیم، پشتیبان‌های قدیمی‌تر هنگام شروع برنامه حذف می‌شوند.", + "preferences.sync.iCloud.noOfBackupsToKeep.infiniteBackups": "نسخه‌های پشتیبان نامحدود", + "preferences.sync.iCloud.noOfBackupsToKeep.nBackups": "{n} نسخه پشتیبان", + "preferences.sync.iCloud.privacyNotice": "داده‌های شما در iCloud شما و در یک کانتینر خصوصی ذخیره می‌شود که فقط Flow به آن دسترسی دارد. هیچ‌کس دیگر به داده‌ها دسترسی ندارد و Flow هم به هیچ داده دیگری در iCloud شما دسترسی ندارد", + "preferences.sync.iCloud.singleDeviceSupportDisclaimer": "این قابلیت از همگام‌سازی روی بیش از یک دستگاه پشتیبانی نمی‌کند. استفاده از چند دستگاه ممکن است باعث از دست رفتن داده‌ها شود", + "preferences.theme": "پوسته", + "preferences.theme.choose": "یک پوسته انتخاب کنید", + "preferences.theme.enableDynamicTheme": "پوسته پویا", + "preferences.theme.enableOledTheme": "استفاده از پوسته OLED", + "preferences.theme.other": "پوسته‌های دیگر", + "preferences.theme.themeChangesAppIcon": "آیکن برنامه با پوسته هماهنگ شود", + "preferences.transactionButtonOrder": "جای‌گذاری دکمه‌ها", + "preferences.transactionButtonOrder.description": "جای دکمه تراکنش جدید را تغییر دهید", + "preferences.transactionButtonOrder.guide": "برای مرتب‌سازی، دکمه‌ها را بکشید و جابه‌جا کنید", + "preferences.transactionButtonOrder.widgetDescription": "این ترتیب در ویجت دکمه‌های تراکنش روی صفحه اصلی هم اعمال می‌شود", + "preferences.transactionEntryFlow": "ثبت تراکنش", + "preferences.transactionEntryFlow.abandonUponCancelForm": "اگر یکی از فرم‌ها بسته شد، جریان را متوقف کن", + "preferences.transactionEntryFlow.actions": "فهرست اقدامات", + "preferences.transactionEntryFlow.actions.description": "می‌توانید با کشیدن، ترتیب را تغییر دهید", + "preferences.transactionEntryFlow.actions.lastItem": "باید آخرین مورد باشد", + "preferences.transactionEntryFlow.description": "برای صرفه‌جویی در زمان، Flow هنگام ساخت تراکنش برخی فرم‌ها را خودکار باز می‌کند. اگر ترجیح می‌دهید هر فرم را دستی باز کنید، می‌توانید همه ورودی‌ها را غیرفعال کنید.", + "preferences.transactionEntryFlow.skipSelectedFields": "رد کردن فیلدهای از قبل انتخاب‌شده", + "preferences.transactions": "تراکنش‌ها", + "preferences.transactions.geo": "موقعیت تراکنش", + "preferences.transactions.geo.auto.description": "به‌صورت خودکار موقعیت فعلی شما را به تراکنش‌های جدید اضافه می‌کند. حتی اگر این گزینه خاموش باشد، باز هم می‌توانید روی نقشه یک موقعیت انتخاب کنید و بچسبانید.", + "preferences.transactions.geo.auto.enable": "پیوست خودکار", + "preferences.transactions.geo.auto.enabled": "پیوست خودکار فعال است", + "preferences.transactions.geo.auto.permissionDenied": "مجوز موقعیت مکانی داده نشده است", + "preferences.transactions.geo.disableInstructions": "می‌توانید این بخش را در تنظیمات مخفی کنید", + "preferences.transactions.geo.enable": "فعال کردن", + "preferences.transactions.listTile": "نمایش آیتم‌های لیست", + "preferences.transactions.listTile.fallbackToCategoryName": "برای تراکنش‌های بدون عنوان، دسته‌بندی را نشان بده", + "preferences.transactions.listTile.leading": "آیکن ابتدای آیتم", + "preferences.transactions.listTile.leading.account": "حساب", + "preferences.transactions.listTile.leading.category": "دسته‌بندی", + "preferences.transactions.listTile.preview": "پیش‌نمایش", + "preferences.transactions.listTile.relaxedDensity": "چیدمان خلوت‌تر", + "preferences.transactions.listTile.showCategoryInList": "نمایش دسته‌بندی بعد از حساب", + "preferences.transactions.listTile.showExternalSource": "نمایش منابع خارجی (مثلاً Eny)", + "preferences.transactions.pending": "تراکنش‌های در انتظار", + "preferences.transactions.pending.homeTimeframe": "نمایش در صفحه خانه", + "preferences.transactions.pending.notify": "اعلان", + "preferences.transactions.pending.notify.earlyReminder": "یادآوری زودتر", + "preferences.transactions.pending.notify.earlyReminder.none": "هیچ‌کدام", + "preferences.transactions.pending.notify.schedulingUnsupported": "وقتی Flow باز نباشد، اعلان‌ها کار نمی‌کنند", + "preferences.transactions.pending.notify.schedulingUnsupported.description": "اعلان‌های زمان‌بندی‌شده فقط روی Android، iOS و macOS در دسترس است", + "preferences.transactions.pending.requireConfirmation": "نیاز به تأیید", + "preferences.transactions.pending.requireConfirmation.description": "تراکنش‌های در انتظار در درآمد، هزینه و موجودی حساب لحاظ نمی‌شوند", + "preferences.transactions.pending.updateDateUponConfirmation": "به‌روزرسانی تاریخ هنگام تأیید", + "preferences.transactions.pending.updateDateUponConfirmation.description": "خاموش کنید تا تاریخ اصلی تراکنش حفظ شود", + "preferences.transactions.tags": "برچسب‌های تراکنش", + "preferences.transactions.tags.contactUsageDescription": "برای راحتی می‌توانید از مخاطبین خود استفاده کنید. این کار نیاز به مجوز مخاطبین دارد. اطلاعات مخاطبین از دستگاه شما خارج نمی‌شود، اما ممکن است بخشی از آن در پشتیبان‌ها ذخیره شود. مخاطبین ممکن است پس از تعویض/ریست گوشی به‌هم بریزند، اما نام‌ها باقی می‌مانند.", + "preferences.transfer": "انتقال‌ها", + "preferences.transfer.combineTransferTransaction": "چیدمان", + "preferences.transfer.combineTransferTransaction.combine": "ترکیبی", + "preferences.transfer.combineTransferTransaction.combineSupportDisclaimer": "در بعضی بخش‌ها انتقال‌ها همیشه جدا نمایش داده می‌شوند", + "preferences.transfer.combineTransferTransaction.separate": "جدا", + "preferences.transfer.description": "ترکیب در یک مورد، عدم لحاظ در هزینه/درآمد", + "preferences.transfer.excludeTransferFromFlow": "عدم لحاظ در مجموع‌ها", + "preferences.transfer.excludeTransferFromFlow.description": "در مجموع هزینه/درآمد حساب نشود", + "preferences.trashBin": "سطل زباله", + "preferences.trashBin.emptyBin": "خالی کردن سطل زباله", + "preferences.trashBin.emptyBin.description": "حذف دائمی همه موارد داخل سطل زباله. این کار غیرقابل بازگشت است", + "preferences.trashBin.retention": "مدت نگه‌داری", + "preferences.trashBin.retention.description": "تراکنش‌ها بعد از پایان مدت نگه‌داری به‌صورت خودکار حذف می‌شوند", + "preferences.trashBin.retention.forever": "برای همیشه", + "preferences.trashBin.seeItems": "مشاهده موارد", + "profile.name": "نام", + "select.color": "تغییر رنگ", + "select.color.clear": "پاک کردن رنگ", + "select.color.none": "رنگ پیش‌فرض", + "select.contact": "انتخاب مخاطب", + "select.contact.editPermissions": "باز کردن تنظیمات", + "select.contact.empty": "هیچ مخاطبی پیدا نشد", + "select.contact.emptyPermissionSuggestion": "مجوزها ندارید؟ مخاطبین را دوباره بارگذاری کنید یا مجوز بدهید", + "select.contact.none": "پاک کردن انتخاب", + "select.dropFile": "انتخاب یا رها کردن فایل", + "select.dropFile.acceptedTypes": "فرمت‌های مجاز: {types}", + "select.dropFile.dropHere": "فایل(ها) را اینجا رها کنید", + "select.recurrence": "تکرار", + "select.recurrence.addMore": "افزودن بیشتر", + "select.recurrence.from": "شروع از", + "select.recurrence.occurrences": "تعداد دفعات را وارد کنید", + "select.recurrence.occurrences.n": "{n} بار", + "select.recurrence.occurrences.times.prefix": "", + "select.recurrence.occurrences.times.suffix": "بار", + "select.recurrence.until": "پایان در", + "select.recurrence.until.date": "در یک تاریخ", + "select.recurrence.until.never": "هرگز", + "select.recurrence.until.noOfOccurrences": "تعداد دفعات", + "select.time.now": "اکنون", + "select.time.select.month": "انتخاب ماه", + "select.time.select.year": "انتخاب سال", + "select.timeRange": "انتخاب بازه", + "select.timeRange.allTime": "تمام مدت", + "select.timeRange.changeMode": "گزینه‌های بیشتر", + "select.timeRange.last30Days": "۳۰ روز اخیر", + "select.timeRange.mode.byMonth": "بر اساس ماه", + "select.timeRange.mode.byWeek": "بر اساس هفته", + "select.timeRange.mode.byYear": "بر اساس سال", + "select.timeRange.mode.custom": "بازه سفارشی", + "select.timeRange.presets": "گزینه‌های رایج", + "select.timeRange.thisMonth": "این ماه", + "select.timeRange.thisWeek": "این هفته", + "select.timeRange.thisYear": "امسال", + "setup.accounts.addAccount": "افزودن حساب جدید", + "setup.accounts.description": "حساب‌های جدید بسازید و/یا از پیش‌فرض‌ها اضافه کنید. بعداً می‌توانید این را در تب «حساب‌ها» تغییر دهید.", + "setup.accounts.preset.cash": "نقدی", + "setup.accounts.preset.main": "اصلی", + "setup.accounts.preset.savings": "پس‌انداز", + "setup.accounts.setup": "راه‌اندازی حساب‌ها", + "setup.categories.description": "دسته‌بندی‌ها را بسازید و/یا از پیش‌فرض‌ها اضافه کنید. بعداً می‌توانید این را در منوی «پروفایل > دسته‌بندی‌ها» تغییر دهید.", + "setup.categories.existing": "دسته‌بندی‌های موجود", + "setup.categories.preset.beauty": "زیبایی", + "setup.categories.preset.childCare": "مراقبت کودک", + "setup.categories.preset.donations": "کمک‌های مالی", + "setup.categories.preset.drinks": "نوشیدنی‌ها", + "setup.categories.preset.eatingOut": "غذا بیرون", + "setup.categories.preset.education": "آموزش", + "setup.categories.preset.entertainment": "سرگرمی", + "setup.categories.preset.fitness": "تناسب اندام", + "setup.categories.preset.gadgets": "گجت‌ها", + "setup.categories.preset.gifts": "هدایا", + "setup.categories.preset.groceries": "خواربار", + "setup.categories.preset.health": "سلامت", + "setup.categories.preset.hobby": "سرگرمی/علاقه‌مندی", + "setup.categories.preset.hygiene": "بهداشت", + "setup.categories.preset.insurance": "بیمه", + "setup.categories.preset.onlineServices": "اشتراک‌های آنلاین", + "setup.categories.preset.paychecks": "حقوق", + "setup.categories.preset.petCare": "رسیدگی به حیوان خانگی", + "setup.categories.preset.petrol": "بنزین", + "setup.categories.preset.rent": "اجاره", + "setup.categories.preset.services": "خدمات", + "setup.categories.preset.shopping": "خرید", + "setup.categories.preset.snacks": "تنقلات", + "setup.categories.preset.stationery": "لوازم‌التحریر", + "setup.categories.preset.taxes": "مالیات", + "setup.categories.preset.transport": "حمل‌ونقل", + "setup.categories.preset.travel": "سفر", + "setup.categories.preset.utils": "قبوض/هزینه‌های عمومی", + "setup.categories.setup": "راه‌اندازی دسته‌بندی‌ها", + "setup.getStarted": "شروع", + "setup.next": "بعدی", + "setup.onboarding": "شروع کنیم", + "setup.onboarding.freshStart": "شروع از صفر", + "setup.onboarding.freshStart.description": "اولین بار است از Flow استفاده می‌کنم", + "setup.onboarding.importExisting": "ایمپورت از نسخه پشتیبان", + "setup.onboarding.importExisting.description": "بازیابی داده از نسخه پشتیبان قبلی Flow", + "setup.onboarding.recoverICloudBackup": "بازیابی از iCloud", + "setup.onboarding.recoverICloudBackup.description": "بازیابی از iCloud (آخرین همگام‌سازی در {lastSynced})", + "setup.primaryCurrency.choose": "یک ارز انتخاب کنید", + "setup.primaryCurrency.description": "این ارزِ اصلی شما خواهد بود. بعداً می‌توانید آن را در منوی «تنظیمات» تغییر دهید.", + "setup.primaryCurrency.setup": "انتخاب یک ارز", + "setup.profile.addPhoto": "افزودن عکس", + "setup.profile.addPhoto.skip": "رد کردن", + "setup.profile.setup": "نام شما چیست؟", + "setup.slides.foss.description": "کاملاً رایگان و کد منبع به‌صورت عمومی در دسترس است.", + "setup.slides.foss.seeRepo": "مشاهده پروژه در GitHub", + "setup.slides.foss.title": "رایگان و متن‌باز", + "setup.slides.privacy": "کنترل داده‌ها دست شماست", + "setup.slides.privacy.description": "تمام داده‌های شما فقط روی دستگاه (یا ارائه‌دهنده خصوصی فضای ابری شما) ذخیره می‌شود و امکان خروجی گرفتن کامل وجود دارد", + "setup.transactionTags": "انتخاب برچسب‌ها", + "support": "پشتیبانی", + "support.contribute": "مشارکت در کدنویسی", + "support.contribute.description": "اگر توسعه‌دهنده هستید می‌توانید در توسعه Flow مشارکت کنید. فهرست مشارکت‌کنندگان منتظر اضافه شدن نام شماست.", + "support.description": "Flow با عشق ساخته شده؛ رایگان و برای همه باز است. اگر Flow برایتان ارزشمند است، در رشد پروژه کمک کنید. چند راه برای این کار:", + "support.donateDeveloper": "انعام به سازنده", + "support.donateDeveloper.action": "یک قهوه مهمان سازنده کنید", + "support.donateDeveloper.description": "تمام قابلیت‌های Flow رایگان است و انعام دادن هیچ ویژگی اضافی را باز نمی‌کند", + "support.leaveAReview": "ثبت نظر", + "support.leaveAReview.action": "امتیاز دادن به Flow", + "support.leaveAReview.description": "می‌توانید در {appStore} به Flow امتیاز بدهید و نظر ثبت کنید", + "support.requestFeatures": "ایده بدهید", + "support.requestFeatures.action": "رفتن به Issue Tracker", + "support.requestFeatures.description": "می‌توانید با بازخورد و پیشنهاد ایده‌ها هم از Flow حمایت کنید.", + "support.starOnGitHub": "ستاره دادن در GitHub", + "support.starOnGitHub.description": "ستاره دادن به Flow در GitHub باعث می‌شود افراد بیشتری آن را پیدا کنند", + "sync.export": "خروجی گرفتن", + "sync.export.asCSV": "به‌صورت CSV", + "sync.export.asCSV.description": "برای بازیابی/ایمپورت قابل استفاده نیست! مناسب برای باز کردن در ابزارهایی مثل Google Sheets", + "sync.export.asJSON": "به‌صورت پشتیبان (json)", + "sync.export.asJSON.description": "فقط داده‌های ضروری؛ تصاویر و فایل‌های پیوست شامل نمی‌شود.", + "sync.export.asPDF": "صورت‌حساب‌ها (PDF)", + "sync.export.asPDF.description": "صورت‌حساب حساب(ها)؛ مناسب برای چاپ. این سند رسمی نیست و فقط برای استفاده شخصی شماست.", + "sync.export.asZIP": "به‌صورت پشتیبان (zip)", + "sync.export.asZIP.description": "بعداً به‌طور کامل قابل بازیابی است. شامل فایل‌های پیوست", + "sync.export.autoBackup": "پشتیبان‌گیری خودکار", + "sync.export.deleteCloudBackupConfirmation": "با حذف این پشتیبان، کپی iCloud هم حذف می‌شود. این عمل غیرقابل بازگشت است!", + "sync.export.fileDeleted": "فایل پیدا نشد", + "sync.export.history": "تاریخچه پشتیبان‌ها", + "sync.export.history.description": "پشتیبان‌هایی که شما ساخته‌اید و همچنین پشتیبان‌های خودکار را ببینید", + "sync.export.history.empty": "هیچ پشتیبان ندارید", + "sync.export.history.empty.description": "پشتیبان‌های دستی و خودکار اینجا نمایش داده می‌شوند", + "sync.export.onDeviceWarning": "همه پشتیبان‌ها روی دستگاه ذخیره می‌شوند؛ یعنی اگر Flow را حذف کنید یا دستگاه را ریست کنید، همه پشتیبان‌ها از بین می‌روند!", + "sync.export.pdf.accounts": "حساب‌ها", + "sync.export.pdf.accounts.selected": "{n} انتخاب شد (از {total})", + "sync.export.pdf.categories": "دسته‌بندی‌ها", + "sync.export.pdf.categories.selected": "{n} انتخاب شد (از {total})", + "sync.export.pdf.header": "Flow - سوابق مالی شخصی (غیررسمی، {range})", + "sync.export.pdf.notice[0]": "تولید شده توسط ", + "sync.export.pdf.notice[1]": ". این سند قانونی نیست. این یک صورت‌حساب مالی نیست. این رسید نیست. این هیچ ادعایی درباره واقعیت به هیچ شکل و صورتی ندارد. صرفاً برای استفاده شخصی است.", + "sync.export.pdf.size": "اندازه کاغذ", + "sync.export.pdf.summary": "خلاصه بر اساس حساب ({range})", + "sync.export.pdf.summary.allAcounts": "همه حساب‌ها", + "sync.export.pdf.summary.expense": "هزینه", + "sync.export.pdf.summary.flow": "Flow", + "sync.export.pdf.summary.income": "درآمد", + "sync.export.pdf.timeRange": "بازه زمانی", + "sync.export.save": "ذخیره پشتیبان", + "sync.export.save.shareTitle": "پشتیبان Flow ({type}، {date})", + "sync.export.savedTo": "ذخیره شد در {path}", + "sync.export.success": "خروجی با موفقیت گرفته شد!", + "sync.export.success.filePath[0]": "ذخیره شد در ", + "sync.export.success.filePath[1]": "", + "sync.export.type": "خروجی ({type})", + "sync.import": "ایمپورت", + "sync.import.emergencyBackup": "برای احتیاط، Flow قبل از ادامه تلاش می‌کند از داده‌های فعلی روی دستگاه شما پشتیبان بگیرد", + "sync.import.emergencyBackup.successful": "از داده‌های قبلی پشتیبان گرفته شد. می‌توانید فایل پشتیبان را از Backup > Backup history ذخیره کنید", + "sync.import.eraseWarning": "ادامه دادن باعث پاک شدن همه داده‌های فعلی می‌شود", + "sync.import.getCSVTemplate": "دریافت قالب CSV", + "sync.import.other": "گزینه‌های دیگر", + "sync.import.pickCurrencies": "اختصاص ارز به حساب‌ها", + "sync.import.pickCurrencies.incomplete": "لطفاً برای هر حساب یک ارز تعیین کنید", + "sync.import.pickFile": "انتخاب فایل", + "sync.import.pickFile.description": "یک فایل پشتیبان Flow برای بازیابی انتخاب کنید. فرمت‌های مجاز: {exts}", + "sync.import.pickFile.dropzone.active": "اینجا رها کنید", + "sync.import.pickFile.pickOrDrop": "انتخاب یا رها کردن فایل", + "sync.import.start": "شروع ایمپورت", + "sync.import.success": "ایمپورت با موفقیت انجام شد!", + "sync.import.syncData.createdDate": "تاریخ پشتیبان", + "sync.import.syncData.olderBackupWarning": "چون این پشتیبان با نسخه قدیمی‌تری از برنامه ساخته شده، ممکن است مشکلی پیش بیاید!", + "sync.import.syncData.parsedEstimate": "برآورد داده‌های قابل بازیابی", + "sync.import.syncData.parsedEstimate.accountCount": "{count} حساب", + "sync.import.syncData.parsedEstimate.budgetCount": "{count} بودجه", + "sync.import.syncData.parsedEstimate.categoryCount": "{count} دسته‌بندی", + "sync.import.syncData.parsedEstimate.fileAttachmentsCount": "{count} پیوست فایل", + "sync.import.syncData.parsedEstimate.goalCount": "{count} هدف", + "sync.import.syncData.parsedEstimate.transactionCount": "{count} تراکنش", + "sync.import.syncData.parsedEstimate.transactionFilterPresets": "{count} پریست فیلتر تراکنش", + "sync.import.syncData.parsedEstimate.transactionTagCount": "{count} برچسب تراکنش", + "sync.import.zipWarning": "حتماً فایل ZIPی را ایمپورت کنید که توسط خودِ برنامه Flow ساخته شده باشد!", + "tabs.accounts": "حساب‌ها", + "tabs.accounts.reorder": "مرتب‌سازی حساب‌ها", + "tabs.accounts.reorder.guide": "لمس طولانی و کشیدن", + "tabs.home": "خانه", + "tabs.home.flow": "Flow", + "tabs.home.greetings": "سلام، {name}!", + "tabs.home.last7days": "۷ روز اخیر", + "tabs.home.noTransactions": "هیچ تراکنشی مطابق معیارها نیست", + "tabs.home.noTransactions.addSome": "برای افزودن تراکنش جدید، روی دکمه (+) پایین کلیک کنید", + "tabs.home.noTransactions.tryChangingFilters": "فیلترها را تغییر دهید", + "tabs.home.pendingTransactions": "در انتظار ({count})", + "tabs.home.pendingTransactions.needAttention": "{} تراکنش نیاز به تأیید دارد", + "tabs.home.pendingTransactions.seeAll": "مشاهده همه", + "tabs.home.reminders.autoBackup": "یک نسخه پشتیبان ساخته شد", + "tabs.home.reminders.autoBackup.subtitle": "به‌صورت خودکار ساخته شده", + "tabs.home.reminders.rateApp": "به Flow در {store} امتیاز بدهید!", + "tabs.home.reminders.rateApp.action": "امتیاز دادن", + "tabs.home.reminders.starOnGitHub": "به Flow در GitHub ستاره بدهید", + "tabs.home.reminders.turnOnICloudSync": "می‌توانید پشتیبان iCloud را فعال کنید", + "tabs.home.reminders.turnOnICloudSync.action": "فعال کردن", + "tabs.home.reminders.turnOnICloudSync.subtitle": "پشتیبان‌گیری قابل‌اعتماد و رایگان از داده‌ها", + "tabs.home.totalBalance": "موجودی کل", + "tabs.home.transactionsCount": "{count} تراکنش", + "tabs.profile": "پروفایل", + "tabs.profile.backup": "پشتیبان", + "tabs.profile.community": "جامعه", + "tabs.profile.guide": "راهنمای استفاده", + "tabs.profile.import": "ایمپورت", + "tabs.profile.joinDiscord": "عضویت در Discord Flow", + "tabs.profile.other": "سایر", + "tabs.profile.preferences": "تنظیمات", + "tabs.profile.recommend": "پیشنهاد Flow", + "tabs.profile.support": "حمایت از Flow", + "tabs.profile.withLoveFromTheCreator": "با 🤍 از sadespresso", + "tabs.stats": "آمار", + "tabs.stats.categories": "دسته‌بندی‌ها", + "tabs.stats.categories.seeAll": "مشاهده همه دسته‌بندی‌ها", + "tabs.stats.categories.top": "بیشترین هزینه", + "tabs.stats.chart.noData": "داده‌ای برای نمایش نیست", + "tabs.stats.chart.select.clickToSelect": "برای انتخاب کلیک کنید", + "tabs.stats.chart.total": "جمع", + "tabs.stats.intervalReport.averages.expense": "هزینه", + "tabs.stats.intervalReport.averages.flow": "Flow", + "tabs.stats.intervalReport.averages.income": "درآمد", + "tabs.stats.intervalReport.averages@day": "میانگین‌ها بر اساس روز", + "tabs.stats.intervalReport.averages@hour": "میانگین‌ها بر اساس ساعت", + "tabs.stats.intervalReport.averages@month": "میانگین‌ها بر اساس ماه", + "tabs.stats.intervalReport.averages@week": "میانگین‌ها بر اساس هفته", + "tabs.stats.intervalReport.averages@year": "میانگین‌ها بر اساس سال", + "tabs.stats.intervalReport.forecast": "پیش‌بینی هزینه برای {}", + "tabs.stats.intervalReport.totalExpense": "مجموع هزینه {}", + "tabs.stats.trends": "روندها", + "tabs.stats.trends.average": "میانگین هزینه", + "tabs.stats.trends.average.description": "میانگین مبلغ هزینه در بازه زمانی انتخاب‌شده", + "tabs.stats.trends.median": "میانه هزینه", + "tabs.stats.trends.median.description": "میانه مبلغ هزینه در بازه زمانی انتخاب‌شده", + "tabs.stats.trends.topSpendingTitles": "تراکنش‌های پرتکرار", + "tabs.stats.trends.topSpendingTitles.description": "عنوان‌های پرتکرارِ تراکنش‌ها", + "transaction": "تراکنش", + "transaction.actions": "اقدامات", + "transaction.attachments": "پیوست‌های فایل", + "transaction.attachments.warning": "پیوست(ها) در پشتیبان‌های شما {size} فضا می‌گیرند. اگر از پشتیبان ابری (مثل iCloud) استفاده کنید، فضای مصرفی افزایش می‌یابد.", + "transaction.createdDate": "ایجاد شده در", + "transaction.date": "تاریخ تراکنش", + "transaction.delete": "حذف تراکنش", + "transaction.deleted": "اخیراً حذف شده", + "transaction.description": "یادداشت‌ها", + "transaction.description.add": "افزودن یادداشت", + "transaction.description.markdownSupported": "پشتیبانی از Markdown", + "transaction.description.placeholder": "جزئیات تراکنش...", + "transaction.description.preview": "پیش‌نمایش", + "transaction.duplicate": "کپی تراکنش", + "transaction.duplicate.success": "تراکنش کپی شد", + "transaction.edit": "ویرایش تراکنش", + "transaction.edit.selectAccount": "یک حساب انتخاب کنید", + "transaction.edit.selectAccount.multiple": "انتخاب حساب‌ها", + "transaction.edit.selectAccount.noPossibleChoice": "هیچ حسابی برای انتخاب وجود ندارد", + "transaction.edit.selectCategory": "یک دسته‌بندی انتخاب کنید", + "transaction.edit.selectCategory.multiple": "انتخاب دسته‌بندی‌ها", + "transaction.external.added": "یک تراکنش جدید اضافه شد", + "transaction.external.added.from": "تراکنش جدیدی توسط {name} اضافه شد.", + "transaction.external.from": "اضافه شده از {name}", + "transaction.fallbackTitle": "تراکنش بدون عنوان", + "transaction.location": "موقعیت", + "transaction.location.add": "افزودن موقعیت", + "transaction.location.edit": "برای ویرایش روی نقشه بزنید", + "transaction.moveToTrashBin": "انتقال به سطل زباله", + "transaction.moveToTrashBin.restore": "بازیابی تراکنش", + "transaction.moveToTrashBin.restore.success": "تراکنش بازیابی شد", + "transaction.moveToTrashBin.success": "به سطل زباله منتقل شد", + "transaction.new": "تراکنش جدید", + "transaction.pending": "در انتظار", + "transaction.pending.preapproved": "پیش‌تأیید شده", + "transaction.recurring": "تراکنش تکرارشونده", + "transaction.recurring.delete": "حذف تراکنش تکرارشونده", + "transaction.recurring.delete.deleteAllDisclaimer": "این کار همه تراکنش‌های مرتبط را حذف می‌کند و ساخت تراکنش‌های جدید را متوقف می‌کند. بازیابی تراکنش‌ها از سطل زباله باعث ساخت دوباره تراکنش‌های جدید نمی‌شود.", + "transaction.recurring.edit": "ویرایش تراکنش تکرارشونده", + "transaction.recurring.setup": "تنظیم تراکنش تکرارشونده", + "transaction.tags": "برچسب‌ها", + "transaction.tags.add": "افزودن برچسب‌ها", + "transaction.tags.contact.name": "نام مخاطب", + "transaction.tags.contact.select": "انتخاب مخاطب از گوشی", + "transaction.tags.delete": "حذف برچسب", + "transaction.tags.delete.description": "با حذف این برچسب، برچسبِ {transactionCount} تراکنش جدا می‌شود. این کار غیرقابل بازگشت است!", + "transaction.tags.location.name": "نام موقعیت", + "transaction.tags.location.useCurrent": "استفاده از موقعیت فعلی", + "transaction.tags.name": "نام برچسب", + "transaction.tags.new": "برچسب جدید", + "transaction.tags.suggestionGuide": "برای افزودن، روی برچسب پیشنهادی ضربه بزنید", + "transaction.transfer.conversionRate": "نرخ تبدیل", + "transaction.transfer.from": "حساب فرستنده", + "transaction.transfer.from.select": "انتقال از", + "transaction.transfer.from.title": "از {account}", + "transaction.transfer.fromToTitle": "از {from} به {to}", + "transaction.transfer.to": "حساب گیرنده", + "transaction.transfer.to.select": "انتقال به", + "transaction.transfer.to.title": "به {account}", + "transactionFilterPreset": "پریست فیلتر", + "transactionFilterPreset.default": "پریست پیش‌فرض", + "transactionFilterPreset.delete": "حذف پریست", + "transactionFilterPreset.invalid": "نامعتبر", + "transactionFilterPreset.invalid.description": "بعضی حساب/دسته‌بندی‌های این پریست حذف شده‌اند. لطفاً پریست را دوباره بسازید یا حذف کنید", + "transactionFilterPreset.makeDefault": "تنظیم به‌عنوان پیش‌فرض", + "transactionFilterPreset.saveAsNew": "ذخیره به‌عنوان جدید", + "transactionFilterPreset.saveAsNew.guide": "برای ذخیره یک پریست جدید، فیلترها را تغییر دهید و دوباره به اینجا برگردید", + "transactionFilterPreset.saveAsNew.name": "نام پریست", + "transactions.all": "همه تراکنش‌ها", + "transactions.batch.assignAccountForAll": "اختصاص حساب برای همه", + "transactions.batch.assignAccountIndividually": "اختصاص حساب به‌صورت جداگانه", + "transactions.batch.import": "ایمپورت گروهی", + "transactions.batch.import.success": "{n} تراکنش با موفقیت ایمپورت شد", + "transactions.batch.importN": "ایمپورت {n} تراکنش", + "transactions.batch.review": "لطفاً تراکنش‌ها را بررسی کنید", + "transactions.count": "{} تراکنش", + "transactions.pending": "تراکنش‌های در انتظار", + "transactions.query.clearAll": "پاک کردن فیلترها", + "transactions.query.clearSelection": "پاک کردن انتخاب‌ها", + "transactions.query.filter.accounts": "حساب‌ها", + "transactions.query.filter.accounts.all": "همه حساب‌ها", + "transactions.query.filter.accounts.n": "{} حساب", + "transactions.query.filter.categories": "دسته‌بندی‌ها", + "transactions.query.filter.categories.all": "همه دسته‌بندی‌ها", + "transactions.query.filter.categories.n": "{} دسته‌بندی", + "transactions.query.filter.currency": "ارز", + "transactions.query.filter.groupBy": "گروه‌بندی بر اساس", + "transactions.query.filter.hasAttachments": "پیوست‌ها", + "transactions.query.filter.hasAttachments#false": "بدون پیوست", + "transactions.query.filter.hasAttachments#true": "دارای پیوست", + "transactions.query.filter.hasAttachments.all": "پیوست‌ها", + "transactions.query.filter.isPending": "وضعیت در انتظار", + "transactions.query.filter.isPending#false": "در انتظار نیست", + "transactions.query.filter.isPending#true": "در انتظار", + "transactions.query.filter.isPending.all": "همه", + "transactions.query.filter.keyword": "جستجو", + "transactions.query.filter.keyword.all": "جستجو", + "transactions.query.filter.keyword.clear": "پاک کردن", + "transactions.query.filter.keyword.hint": "جستجو بر اساس عنوان...", + "transactions.query.filter.keyword.includeDescription": "شامل توضیحات هم باشد", + "transactions.query.filter.sort": "مرتب‌سازی", + "transactions.query.filter.tags": "برچسب‌ها", + "transactions.query.filter.tags.all": "همه برچسب‌ها", + "transactions.query.filter.tags.n": "{} برچسب", + "transactions.query.filter.timeRange": "بازه زمانی", + "transactions.query.filter.timeRange.all": "تمام مدت", + "transactions.query.filter.transactionType": "نوع", + "transactions.query.noResult": "هیچ تراکنشی برای نمایش نیست", + "transactions.query.noResult.description": "فیلترها را به‌روزرسانی کنید", + "visitGitHubRepo": "مشاهده ریپو در GitHub" +} \ No newline at end of file diff --git a/assets/l10n/fr_FR.json b/assets/l10n/fr_FR.json index 78be3204..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...", @@ -383,8 +389,9 @@ "preferences.transactions.listTile.leading.account": "Compte", "preferences.transactions.listTile.leading.category": "Catégorie", "preferences.transactions.listTile.preview": "Aperçu", + "preferences.transactions.listTile.relaxedDensity": "Affichage moins dense", "preferences.transactions.listTile.showCategoryInList": "Afficher la catégorie après le compte", - "preferences.transactions.listTile.transactionListTileRelaxedDensity": "Affichage moins dense", + "preferences.transactions.listTile.showExternalSource": "Afficher les sources externes (p. ex. : Eny)", "preferences.transactions.pending": "Transactions en attente", "preferences.transactions.pending.homeTimeframe": "Afficher sur l'accueil", "preferences.transactions.pending.notify": "Notifier", @@ -667,8 +674,8 @@ "transaction.edit.selectAccount.noPossibleChoice": "Aucun compte à sélectionner", "transaction.edit.selectCategory": "Sélectionner une catégorie", "transaction.edit.selectCategory.multiple": "Sélectionner des catégories", - "transaction.edit.selectTags": "Sélectionner des étiquettes", "transaction.external.added": "Une nouvelle transaction a été ajoutée", + "transaction.external.added.from": "Une nouvelle transaction a été ajoutée par {name}", "transaction.external.from": "Ajouté depuis {name}", "transaction.fallbackTitle": "Transaction sans titre", "transaction.location": "Lieu", @@ -687,15 +694,16 @@ "transaction.recurring.edit": "Modifier la transaction récurrente", "transaction.recurring.setup": "Configurer la transaction récurrente", "transaction.tags": "Étiquettes", + "transaction.tags.add": "Ajouter des étiquettes", "transaction.tags.contact.name": "Nom du contact", "transaction.tags.contact.select": "Sélectionner un contact du téléphone", "transaction.tags.delete": "Supprimer l’étiquette", "transaction.tags.delete.description": "La suppression de cette étiquette la dissociera de {transactionCount} transactions. Cette action est irréversible !", - "transaction.tags.editGuide": "Appuyez pour modifier les étiquettes", "transaction.tags.location.name": "Nom du lieu", "transaction.tags.location.useCurrent": "Utiliser la position actuelle", "transaction.tags.name": "Nom de l’étiquette", "transaction.tags.new": "Nouvelle étiquette", + "transaction.tags.suggestionGuide": "Appuyez sur l'étiquette suggérée pour l'ajouter", "transaction.transfer.conversionRate": "Taux de conversion", "transaction.transfer.from": "Compte d'envoi", "transaction.transfer.from.select": "Transférer depuis", diff --git a/assets/l10n/it_IT.json b/assets/l10n/it_IT.json index 5b2a90c5..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...", @@ -383,8 +389,9 @@ "preferences.transactions.listTile.leading.account": "Conto", "preferences.transactions.listTile.leading.category": "Categoria", "preferences.transactions.listTile.preview": "Anteprima", + "preferences.transactions.listTile.relaxedDensity": "Layout meno denso", "preferences.transactions.listTile.showCategoryInList": "Mostra categoria dopo il conto", - "preferences.transactions.listTile.transactionListTileRelaxedDensity": "Layout meno denso", + "preferences.transactions.listTile.showExternalSource": "Mostra fonti esterne (ad es. Eny)", "preferences.transactions.pending": "Transazioni in sospeso", "preferences.transactions.pending.homeTimeframe": "Mostra in Home", "preferences.transactions.pending.notify": "Notifica", @@ -667,8 +674,8 @@ "transaction.edit.selectAccount.noPossibleChoice": "Nessun conto da selezionare", "transaction.edit.selectCategory": "Seleziona una categoria", "transaction.edit.selectCategory.multiple": "Seleziona categorie", - "transaction.edit.selectTags": "Seleziona tag", "transaction.external.added": "È stata aggiunta una nuova transazione", + "transaction.external.added.from": "Una nuova transazione è stata aggiunta da {name}", "transaction.external.from": "Aggiunta da {name}", "transaction.fallbackTitle": "Transazione senza titolo", "transaction.location": "Posizione", @@ -687,15 +694,16 @@ "transaction.recurring.edit": "Modifica transazione ricorrente", "transaction.recurring.setup": "Configurare la transazione ricorrente", "transaction.tags": "Tag", + "transaction.tags.add": "Aggiungi tag", "transaction.tags.contact.name": "Nome del contatto", "transaction.tags.contact.select": "Seleziona contatto dalla rubrica", "transaction.tags.delete": "Elimina tag", "transaction.tags.delete.description": "L'eliminazione di questo tag lo rimuoverà da {transactionCount} transazioni. Questa azione è irreversibile!", - "transaction.tags.editGuide": "Tocca per modificare i tag", "transaction.tags.location.name": "Nome posizione", "transaction.tags.location.useCurrent": "Usa posizione attuale", "transaction.tags.name": "Nome tag", "transaction.tags.new": "Nuovo tag", + "transaction.tags.suggestionGuide": "Tocca il tag suggerito per aggiungerlo", "transaction.transfer.conversionRate": "Tasso di conversione", "transaction.transfer.from": "Conto di origine", "transaction.transfer.from.select": "Trasferisci da", diff --git a/assets/l10n/mn_MN.json b/assets/l10n/mn_MN.json index b436b3dd..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": "Хайх...", @@ -383,8 +389,9 @@ "preferences.transactions.listTile.leading.account": "Данс", "preferences.transactions.listTile.leading.category": "Ангилал", "preferences.transactions.listTile.preview": "Урьдчилан харах", + "preferences.transactions.listTile.relaxedDensity": "Илүү сийрэг байршил", "preferences.transactions.listTile.showCategoryInList": "Дансны ард ангилал харуулах", - "preferences.transactions.listTile.transactionListTileRelaxedDensity": "Илүү сийрэг байршил", + "preferences.transactions.listTile.showExternalSource": "Гадны эх сурвалжийг харуулах", "preferences.transactions.pending": "Хүлээгдэж буй гүйлгээнүүд", "preferences.transactions.pending.homeTimeframe": "Нүүр хуудсанд харуулах", "preferences.transactions.pending.notify": "Мэдэгдэл илгээх", @@ -667,8 +674,8 @@ "transaction.edit.selectAccount.noPossibleChoice": "Сонгох боломжтой данс алга байна", "transaction.edit.selectCategory": "Ангилал сонгох", "transaction.edit.selectCategory.multiple": "Ангилаллууд сонгох", - "transaction.edit.selectTags": "Шошгууд сонгох", "transaction.external.added": "Шинэ гүйлгээ нэмэгдлээ", + "transaction.external.added.from": "{name}-с шинэ гүйлгээ нэмэгдлээ", "transaction.external.from": "{name}-с нэмэгдсэн гүйлгээ", "transaction.fallbackTitle": "Гарчиггүй гүйлгээ", "transaction.location": "Байршил", @@ -687,15 +694,16 @@ "transaction.recurring.edit": "Давтамжит гүйлгээг засварлах", "transaction.recurring.setup": "Давтамжит гүйлгээ тохируулах", "transaction.tags": "Шошго", + "transaction.tags.add": "Шошго нэмэх", "transaction.tags.contact.name": "Хүний нэр", "transaction.tags.contact.select": "Утасны дэвтрээс сонгох", "transaction.tags.delete": "Шошго устгах", "transaction.tags.delete.description": "Энэ шошгыг устгавал холбоотой {transactionCount} гүйлгээний шошго цуг арилахыг анхаарна уу. Энэ үйлдлийг буцаах боломжгүй юм!", - "transaction.tags.editGuide": "Товшиж шошгуудыг өөрчлөх боломжтой", "transaction.tags.location.name": "Байршлын нэр", "transaction.tags.location.useCurrent": "Одоогийн байршлыг ашиглах", "transaction.tags.name": "Шошгоны нэр", "transaction.tags.new": "Шинэ шошго", + "transaction.tags.suggestionGuide": "Санал болгож буй шошгоны дээр товшиж нэмэх боломжтой", "transaction.transfer.conversionRate": "Хөрвүүлэх ханш", "transaction.transfer.from": "Илгээх данс", "transaction.transfer.from.select": "Илгээх данс", diff --git a/assets/l10n/ru_RU.json b/assets/l10n/ru_RU.json index 8e66c10e..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": "Поиск...", @@ -383,8 +389,9 @@ "preferences.transactions.listTile.leading.account": "Счет", "preferences.transactions.listTile.leading.category": "Категория", "preferences.transactions.listTile.preview": "Предпросмотр", + "preferences.transactions.listTile.relaxedDensity": "Менее плотная компоновка", "preferences.transactions.listTile.showCategoryInList": "Показывать категорию после счета", - "preferences.transactions.listTile.transactionListTileRelaxedDensity": "Менее плотная компоновка", + "preferences.transactions.listTile.showExternalSource": "Показывать внешние источники (например, Eny)", "preferences.transactions.pending": "Ожидающие транзакции", "preferences.transactions.pending.homeTimeframe": "Показывать на главном экране", "preferences.transactions.pending.notify": "Уведомлять", @@ -667,8 +674,8 @@ "transaction.edit.selectAccount.noPossibleChoice": "Нет счетов для выбора", "transaction.edit.selectCategory": "Выберите категорию", "transaction.edit.selectCategory.multiple": "Выберите категории", - "transaction.edit.selectTags": "Выберите теги", "transaction.external.added": "Добавлена новая транзакция", + "transaction.external.added.from": "Новая транзакция была добавлена пользователем {name}", "transaction.external.from": "Добавлено от {name}", "transaction.fallbackTitle": "Транзакция без названия", "transaction.location": "Местоположение", @@ -687,15 +694,16 @@ "transaction.recurring.edit": "Редактировать повторяющуюся транзакцию", "transaction.recurring.setup": "Настроить повторяющуюся транзакцию", "transaction.tags": "Теги", + "transaction.tags.add": "Добавить теги", "transaction.tags.contact.name": "Имя контакта", "transaction.tags.contact.select": "Выбрать контакт из телефона", "transaction.tags.delete": "Удалить тег", "transaction.tags.delete.description": "Удаление этого тега снимет его с {transactionCount} транзакций. Это действие необратимо!", - "transaction.tags.editGuide": "Нажмите, чтобы редактировать теги", "transaction.tags.location.name": "Название места", "transaction.tags.location.useCurrent": "Использовать текущее местоположение", "transaction.tags.name": "Название тега", "transaction.tags.new": "Новый тег", + "transaction.tags.suggestionGuide": "Нажмите на предложенный тег, чтобы добавить", "transaction.transfer.conversionRate": "Курс конвертации", "transaction.transfer.from": "Счет отправителя", "transaction.transfer.from.select": "Перевести из", diff --git a/assets/l10n/tr_TR.json b/assets/l10n/tr_TR.json index 424c175d..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...", @@ -383,8 +389,9 @@ "preferences.transactions.listTile.leading.account": "Hesap", "preferences.transactions.listTile.leading.category": "Kategori", "preferences.transactions.listTile.preview": "Önizleme", + "preferences.transactions.listTile.relaxedDensity": "Daha seyrek yerleşim", "preferences.transactions.listTile.showCategoryInList": "Hesaptan sonra kategoriyi göster", - "preferences.transactions.listTile.transactionListTileRelaxedDensity": "Daha seyrek yerleşim", + "preferences.transactions.listTile.showExternalSource": "Harici kaynakları göster (örn. Eny)", "preferences.transactions.pending": "Bekleyen işlemler", "preferences.transactions.pending.homeTimeframe": "Ana sayfada göster", "preferences.transactions.pending.notify": "Bildirim", @@ -667,8 +674,8 @@ "transaction.edit.selectAccount.noPossibleChoice": "Seçilecek hesap yok", "transaction.edit.selectCategory": "Bir kategori seçin", "transaction.edit.selectCategory.multiple": "Kategorileri seçin", - "transaction.edit.selectTags": "Etiketleri seç", "transaction.external.added": "Yeni bir işlem eklendi", + "transaction.external.added.from": "{name} tarafından eklendi", "transaction.external.from": "{name} tarafından eklendi", "transaction.fallbackTitle": "İsimsiz işlem", "transaction.location": "Konum", @@ -687,15 +694,16 @@ "transaction.recurring.edit": "Yinelenen işlemi düzenle", "transaction.recurring.setup": "Yinelenen işlemi ayarlayın", "transaction.tags": "Etiketler", + "transaction.tags.add": "Etiket ekle", "transaction.tags.contact.name": "Kişi adı", "transaction.tags.contact.select": "Telefondan kişi seç", "transaction.tags.delete": "Etiketi sil", "transaction.tags.delete.description": "Bu etiketi silmek, {transactionCount} işlemden etiketi kaldırır. Bu işlem geri alınamaz!", - "transaction.tags.editGuide": "Etiketleri düzenlemek için dokunun", "transaction.tags.location.name": "Konum adı", "transaction.tags.location.useCurrent": "Mevcut konumu kullan", "transaction.tags.name": "Etiket adı", "transaction.tags.new": "Yeni etiket", + "transaction.tags.suggestionGuide": "Eklemek için önerilen etikete dokunun", "transaction.transfer.conversionRate": "Dönüşüm oranı", "transaction.transfer.from": "Hesap gönderiliyor", "transaction.transfer.from.select": "Transfer etmek", diff --git a/assets/l10n/uk_UA.json b/assets/l10n/uk_UA.json index e5f0bcb0..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": "Пошук...", @@ -383,8 +389,9 @@ "preferences.transactions.listTile.leading.account": "Рахунок", "preferences.transactions.listTile.leading.category": "Категорія", "preferences.transactions.listTile.preview": "Попередній перегляд", + "preferences.transactions.listTile.relaxedDensity": "Менш щільне компонування", "preferences.transactions.listTile.showCategoryInList": "Показувати категорію після рахунку", - "preferences.transactions.listTile.transactionListTileRelaxedDensity": "Менш щільне компонування", + "preferences.transactions.listTile.showExternalSource": "Показувати зовнішні джерела (наприклад, Eny)", "preferences.transactions.pending": "Очікувані транзакції", "preferences.transactions.pending.homeTimeframe": "Показувати на головному екрані", "preferences.transactions.pending.notify": "Сповіщати", @@ -667,8 +674,8 @@ "transaction.edit.selectAccount.noPossibleChoice": "Немає рахунків для вибору", "transaction.edit.selectCategory": "Виберіть категорію", "transaction.edit.selectCategory.multiple": "Виберіть категорії", - "transaction.edit.selectTags": "Виберіть теги", "transaction.external.added": "Додано нову транзакцію", + "transaction.external.added.from": "Додано з {name}", "transaction.external.from": "Додано з {name}", "transaction.fallbackTitle": "Транзакція без назви", "transaction.location": "Місцезнаходження", @@ -687,15 +694,16 @@ "transaction.recurring.edit": "Редагувати повторювану транзакцію", "transaction.recurring.setup": "Налаштувати повторювану транзакцію", "transaction.tags": "Теги", + "transaction.tags.add": "Додати теги", "transaction.tags.contact.name": "Ім’я контакту", "transaction.tags.contact.select": "Вибрати контакт із телефону", "transaction.tags.delete": "Видалити мітку", "transaction.tags.delete.description": "Видалення цієї мітки від’єднає її від {transactionCount} транзакцій. Цю дію неможливо скасувати!", - "transaction.tags.editGuide": "Торкніться, щоб редагувати теги", "transaction.tags.location.name": "Назва локації", "transaction.tags.location.useCurrent": "Використати поточну локацію", "transaction.tags.name": "Назва тега", "transaction.tags.new": "Новий тег", + "transaction.tags.suggestionGuide": "Торкніться запропонованого тегу, щоб додати", "transaction.transfer.conversionRate": "Курс конвертації", "transaction.transfer.from": "Рахунок відправника", "transaction.transfer.from.select": "Переказати з", diff --git a/ios/Podfile.lock b/ios/Podfile.lock index cd2f6636..775202ac 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -42,6 +42,8 @@ PODS: - file_saver (0.0.1): - Flutter - Flutter (1.0.0) + - flutter_app_group_directory (0.0.1): + - Flutter - flutter_contacts (0.0.1): - Flutter - flutter_dynamic_icon_plus (1.0.0): @@ -106,6 +108,7 @@ DEPENDENCIES: - file_picker (from `.symlinks/plugins/file_picker/ios`) - file_saver (from `.symlinks/plugins/file_saver/ios`) - Flutter (from `Flutter`) + - flutter_app_group_directory (from `.symlinks/plugins/flutter_app_group_directory/ios`) - flutter_contacts (from `.symlinks/plugins/flutter_contacts/ios`) - flutter_dynamic_icon_plus (from `.symlinks/plugins/flutter_dynamic_icon_plus/ios`) - flutter_keyboard_visibility_temp_fork (from `.symlinks/plugins/flutter_keyboard_visibility_temp_fork/ios`) @@ -151,6 +154,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/file_saver/ios" Flutter: :path: Flutter + flutter_app_group_directory: + :path: ".symlinks/plugins/flutter_app_group_directory/ios" flutter_contacts: :path: ".symlinks/plugins/flutter_contacts/ios" flutter_dynamic_icon_plus: @@ -205,6 +210,7 @@ SPEC CHECKSUMS: file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + flutter_app_group_directory: 55b5362007d1c0cb45dc1dd1e94f67d615f45a6b flutter_contacts: 5383945387e7ca37cf963d4be57c21f2fc15ca9f flutter_dynamic_icon_plus: 3ddd12bc2316b98ffdd4048e9cdd3cf51e02095d flutter_keyboard_visibility_temp_fork: 95b2d534bacf6ac62e7fcbe5c2a9e2c2a17ce06f diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 48fda4f2..4ca4ffa6 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -8,6 +8,10 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 2F5FC4CA2F38B4010045CB46 /* RecordTransactionIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F5FC4C92F38B4010045CB46 /* RecordTransactionIntent.swift */; }; + 2F5FC4CD2F38B5F80045CB46 /* RecordedTransactionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F5FC4CC2F38B5F80045CB46 /* RecordedTransactionService.swift */; }; + 2F5FC4CE2F38B5F80045CB46 /* RecordedTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F5FC4CB2F38B5F80045CB46 /* RecordedTransaction.swift */; }; + 2F9A54282F3F806F001A859E /* TransactionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F9A54272F3F806F001A859E /* TransactionType.swift */; }; 2FA9672D2EAA8E7900D758DC /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2FA9672C2EAA8E7900D758DC /* WidgetKit.framework */; }; 2FA9672F2EAA8E7900D758DC /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2FA9672E2EAA8E7900D758DC /* SwiftUI.framework */; }; 2FA9673E2EAA8E7A00D758DC /* Flow WidgetsExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 2FA9672B2EAA8E7900D758DC /* Flow WidgetsExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -68,7 +72,11 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 2C95C2C1064154F5713B5FDA /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 2F5FC4C92F38B4010045CB46 /* RecordTransactionIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordTransactionIntent.swift; sourceTree = ""; }; + 2F5FC4CB2F38B5F80045CB46 /* RecordedTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordedTransaction.swift; sourceTree = ""; }; + 2F5FC4CC2F38B5F80045CB46 /* RecordedTransactionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordedTransactionService.swift; sourceTree = ""; }; 2F88D2CA2D82C1E200BE0559 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; + 2F9A54272F3F806F001A859E /* TransactionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionType.swift; sourceTree = ""; }; 2FA9672B2EAA8E7900D758DC /* Flow WidgetsExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Flow WidgetsExtension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 2FA9672C2EAA8E7900D758DC /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; 2FA9672E2EAA8E7900D758DC /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; @@ -206,6 +214,9 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + 2F9A54272F3F806F001A859E /* TransactionType.swift */, + 2F5FC4CB2F38B5F80045CB46 /* RecordedTransaction.swift */, + 2F5FC4CC2F38B5F80045CB46 /* RecordedTransactionService.swift */, 2F88D2CA2D82C1E200BE0559 /* Runner.entitlements */, 2F7D63EA2CD8F63D00B8BE47 /* LauncherIcons */, 97C146FA1CF9000F007C117D /* Main.storyboard */, @@ -216,6 +227,7 @@ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + 2F5FC4C92F38B4010045CB46 /* RecordTransactionIntent.swift */, ); path = Runner; sourceTree = ""; @@ -323,7 +335,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - BuildIndependentTargetsInParallel = NO; + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 2600; LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; @@ -527,6 +539,10 @@ files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + 2F5FC4CD2F38B5F80045CB46 /* RecordedTransactionService.swift in Sources */, + 2F5FC4CE2F38B5F80045CB46 /* RecordedTransaction.swift in Sources */, + 2F9A54282F3F806F001A859E /* TransactionType.swift in Sources */, + 2F5FC4CA2F38B4010045CB46 /* RecordTransactionIntent.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -597,6 +613,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = NJH37247C9; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -611,6 +628,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SUPPORTED_PLATFORMS = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; @@ -627,10 +645,9 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = NJH37247C9; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 15.6; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -659,7 +676,6 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = NJH37247C9; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; @@ -705,7 +721,6 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = NJH37247C9; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; @@ -748,7 +763,6 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = NJH37247C9; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; @@ -784,7 +798,6 @@ CODE_SIGN_IDENTITY = "Apple Distribution: Batmend Ganbaatar (NJH37247C9)"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = NJH37247C9; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = mn.flow.flow.RunnerTests; @@ -804,7 +817,6 @@ CODE_SIGN_IDENTITY = "Apple Distribution: Batmend Ganbaatar (NJH37247C9)"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = NJH37247C9; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = mn.flow.flow.RunnerTests; @@ -822,7 +834,6 @@ CODE_SIGN_IDENTITY = "Apple Distribution: Batmend Ganbaatar (NJH37247C9)"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = NJH37247C9; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = mn.flow.flow.RunnerTests; @@ -864,6 +875,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = NJH37247C9; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -885,6 +897,7 @@ MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -921,6 +934,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = NJH37247C9; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -935,6 +949,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SUPPORTED_PLATFORMS = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; @@ -953,10 +968,9 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = NJH37247C9; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 15.6; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -981,10 +995,9 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = NJH37247C9; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 15.6; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index c3fedb29..95d6e55f 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,7 +1,7 @@ + version = "1.7"> diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 26589e95..32742108 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -1,434 +1,436 @@ + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Flow + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIcons - CADisableMinimumFrameDurationOnPhone - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Flow - CFBundleExecutable - $(EXECUTABLE_NAME) - FlutterDeepLinkingEnabled - - CFBundleIcons + CFBundleAlternateIcons - CFBundleAlternateIcons - - blissfulBerry - - CFBundleIconFiles - - blissfulBerry - - UIPrerenderedIcon - - - bohemianBlue - - CFBundleIconFiles - - bohemianBlue - - UIPrerenderedIcon - - - burntSienna - - CFBundleIconFiles - - burntSienna - - UIPrerenderedIcon - - - cherryPlum - - CFBundleIconFiles - - cherryPlum - - UIPrerenderedIcon - - - crispChristmasCranberries - - CFBundleIconFiles - - crispChristmasCranberries - - UIPrerenderedIcon - - - egyptianBlue - - CFBundleIconFiles - - egyptianBlue - - UIPrerenderedIcon - - - flagGreen - - CFBundleIconFiles - - flagGreen - - UIPrerenderedIcon - - - hydraTurquoise - - CFBundleIconFiles - - hydraTurquoise - - UIPrerenderedIcon - - - peacockBlue - - CFBundleIconFiles - - peacockBlue - - UIPrerenderedIcon - - - shadeOfViolet - - CFBundleIconFiles - - shadeOfViolet - - UIPrerenderedIcon - - - soilOfAvagddu - - CFBundleIconFiles - - soilOfAvagddu - - UIPrerenderedIcon - - - spaceBattleBlue - - CFBundleIconFiles - - spaceBattleBlue - - UIPrerenderedIcon - - - spreadsheetGreen - - CFBundleIconFiles - - spreadsheetGreen - - UIPrerenderedIcon - - - tokiwaGreen - - CFBundleIconFiles - - tokiwaGreen - - UIPrerenderedIcon - - - toyCamouflage - - CFBundleIconFiles - - toyCamouflage - - UIPrerenderedIcon - - - tropicana - - CFBundleIconFiles - - tropicana - - UIPrerenderedIcon - - - - CFBundlePrimaryIcon + blissfulBerry + + CFBundleIconFiles + + blissfulBerry + + UIPrerenderedIcon + + + bohemianBlue + + CFBundleIconFiles + + bohemianBlue + + UIPrerenderedIcon + + + burntSienna + + CFBundleIconFiles + + burntSienna + + UIPrerenderedIcon + + + cherryPlum + + CFBundleIconFiles + + cherryPlum + + UIPrerenderedIcon + + + crispChristmasCranberries + + CFBundleIconFiles + + crispChristmasCranberries + + UIPrerenderedIcon + + + egyptianBlue + + CFBundleIconFiles + + egyptianBlue + + UIPrerenderedIcon + + + flagGreen + + CFBundleIconFiles + + flagGreen + + UIPrerenderedIcon + + + hydraTurquoise + + CFBundleIconFiles + + hydraTurquoise + + UIPrerenderedIcon + + + peacockBlue + + CFBundleIconFiles + + peacockBlue + + UIPrerenderedIcon + + + shadeOfViolet CFBundleIconFiles shadeOfViolet UIPrerenderedIcon - + + + soilOfAvagddu + + CFBundleIconFiles + + soilOfAvagddu + + UIPrerenderedIcon + + + spaceBattleBlue + + CFBundleIconFiles + + spaceBattleBlue + + UIPrerenderedIcon + + + spreadsheetGreen + + CFBundleIconFiles + + spreadsheetGreen + + UIPrerenderedIcon + + + tokiwaGreen + + CFBundleIconFiles + + tokiwaGreen + + UIPrerenderedIcon + + + toyCamouflage + + CFBundleIconFiles + + toyCamouflage + + UIPrerenderedIcon + + + tropicana + + CFBundleIconFiles + + tropicana + + UIPrerenderedIcon + - CFBundleIcons~ipad + CFBundlePrimaryIcon + + CFBundleIconFiles + + shadeOfViolet + + UIPrerenderedIcon + + + + CFBundleIcons~ipad + + CFBundleAlternateIcons - CFBundleAlternateIcons - - blissfulBerry - - CFBundleIconFiles - - blissfulBerry-ipad - blissfulBerry-ipad-pro - - UIPrerenderedIcon - - - bohemianBlue - - CFBundleIconFiles - - bohemianBlue-ipad - bohemianBlue-ipad-pro - - UIPrerenderedIcon - - - burntSienna - - CFBundleIconFiles - - burntSienna-ipad - burntSienna-ipad-pro - - UIPrerenderedIcon - - - cherryPlum - - CFBundleIconFiles - - cherryPlum-ipad - cherryPlum-ipad-pro - - UIPrerenderedIcon - - - crispChristmasCranberries - - CFBundleIconFiles - - crispChristmasCranberries-ipad - crispChristmasCranberries-ipad-pro - - UIPrerenderedIcon - - - egyptianBlue - - CFBundleIconFiles - - egyptianBlue-ipad - egyptianBlue-ipad-pro - - UIPrerenderedIcon - - - flagGreen - - CFBundleIconFiles - - flagGreen-ipad - flagGreen-ipad-pro - - UIPrerenderedIcon - - - hydraTurquoise - - CFBundleIconFiles - - hydraTurquoise-ipad - hydraTurquoise-ipad-pro - - UIPrerenderedIcon - - - peacockBlue - - CFBundleIconFiles - - peacockBlue-ipad - peacockBlue-ipad-pro - - UIPrerenderedIcon - - - shadeOfViolet - - CFBundleIconFiles - - shadeOfViolet-ipad - shadeOfViolet-ipad-pro - - UIPrerenderedIcon - - - soilOfAvagddu - - CFBundleIconFiles - - soilOfAvagddu-ipad - soilOfAvagddu-ipad-pro - - UIPrerenderedIcon - - - spaceBattleBlue - - CFBundleIconFiles - - spaceBattleBlue-ipad - spaceBattleBlue-ipad-pro - - UIPrerenderedIcon - - - spreadsheetGreen - - CFBundleIconFiles - - spreadsheetGreen-ipad - spreadsheetGreen-ipad-pro - - UIPrerenderedIcon - - - tokiwaGreen - - CFBundleIconFiles - - tokiwaGreen-ipad - tokiwaGreen-ipad-pro - - UIPrerenderedIcon - - - toyCamouflage - - CFBundleIconFiles - - toyCamouflage-ipad - toyCamouflage-ipad-pro - - UIPrerenderedIcon - - - tropicana - - CFBundleIconFiles - - tropicana-ipad - tropicana-ipad-pro - - UIPrerenderedIcon - - - - CFBundlePrimaryIcon + blissfulBerry CFBundleIconFiles - shadeOfViolet + blissfulBerry-ipad + blissfulBerry-ipad-pro UIPrerenderedIcon - + + bohemianBlue + + CFBundleIconFiles + + bohemianBlue-ipad + bohemianBlue-ipad-pro + + UIPrerenderedIcon + + + burntSienna + + CFBundleIconFiles + + burntSienna-ipad + burntSienna-ipad-pro + + UIPrerenderedIcon + + + cherryPlum + + CFBundleIconFiles + + cherryPlum-ipad + cherryPlum-ipad-pro + + UIPrerenderedIcon + + + crispChristmasCranberries + + CFBundleIconFiles + + crispChristmasCranberries-ipad + crispChristmasCranberries-ipad-pro + + UIPrerenderedIcon + + + egyptianBlue + + CFBundleIconFiles + + egyptianBlue-ipad + egyptianBlue-ipad-pro + + UIPrerenderedIcon + + + flagGreen + + CFBundleIconFiles + + flagGreen-ipad + flagGreen-ipad-pro + + UIPrerenderedIcon + + + hydraTurquoise + + CFBundleIconFiles + + hydraTurquoise-ipad + hydraTurquoise-ipad-pro + + UIPrerenderedIcon + + + peacockBlue + + CFBundleIconFiles + + peacockBlue-ipad + peacockBlue-ipad-pro + + UIPrerenderedIcon + + + shadeOfViolet + + CFBundleIconFiles + + shadeOfViolet-ipad + shadeOfViolet-ipad-pro + + UIPrerenderedIcon + + + soilOfAvagddu + + CFBundleIconFiles + + soilOfAvagddu-ipad + soilOfAvagddu-ipad-pro + + UIPrerenderedIcon + + + spaceBattleBlue + + CFBundleIconFiles + + spaceBattleBlue-ipad + spaceBattleBlue-ipad-pro + + UIPrerenderedIcon + + + spreadsheetGreen + + CFBundleIconFiles + + spreadsheetGreen-ipad + spreadsheetGreen-ipad-pro + + UIPrerenderedIcon + + + tokiwaGreen + + CFBundleIconFiles + + tokiwaGreen-ipad + tokiwaGreen-ipad-pro + + UIPrerenderedIcon + + + toyCamouflage + + CFBundleIconFiles + + toyCamouflage-ipad + toyCamouflage-ipad-pro + + UIPrerenderedIcon + + + tropicana + + CFBundleIconFiles + + tropicana-ipad + tropicana-ipad-pro + + UIPrerenderedIcon + + + + CFBundlePrimaryIcon + + CFBundleIconFiles + + shadeOfViolet + + UIPrerenderedIcon + - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleLocalizations - - en - mn - it - tr - fr - ar - de - ru - es - uk - - CFBundleName - flow - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleURLTypes - - - CFBundleTypeRole - Viewer - CFBundleURLIconFile - shadeOfViolet@3x - CFBundleURLName - mn.flow.flow - CFBundleURLSchemes - - flow-mn - - - - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - ITSAppUsesNonExemptEncryption - - LSRequiresIPhoneOS - - LSSupportsOpeningDocumentsInPlace - - NSFaceIDUsageDescription - Flow uses Face ID if you choose to require Face ID to open the app for authentication - NSCameraUsageDescription - Flow uses camera if you choose to take photos or videos and attach it to a transaction - NSMicrophoneUsageDescription - Flow uses microphone if you choose to record audio/video and attach it to a transaction - NSLocationWhenInUseUsageDescription - Location is used if you choose to auto-attach your current location to your + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleLocalizations + + en + mn + it + tr + fr + ar + de + ru + es + uk + + CFBundleName + flow + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleTypeRole + Viewer + CFBundleURLIconFile + shadeOfViolet@3x + CFBundleURLName + mn.flow.flow + CFBundleURLSchemes + + flow-mn + + + + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + FlutterDeepLinkingEnabled + + ITSAppUsesNonExemptEncryption + + LSRequiresIPhoneOS + + LSSupportsOpeningDocumentsInPlace + + NSCameraUsageDescription + Flow uses camera if you choose to take photos or videos and attach it to a transaction + NSContactsUsageDescription + Flow will need access to your contacts if you want to attach contact tags to your transactions. - NSPhotoLibraryUsageDescription - Flow uses the photo library when user updates their picture, or chooses to use image as - category/account icon - NSContactsUsageDescription - Flow will need access to your contacts if you want to attach contact tags to your + NSFaceIDUsageDescription + Flow uses Face ID if you choose to require Face ID to open the app for authentication + NSLocationWhenInUseUsageDescription + Location is used if you choose to auto-attach your current location to your transactions. - UIApplicationSupportsIndirectInputEvents - - UIFileSharingEnabled - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - - \ No newline at end of file + NSMicrophoneUsageDescription + Flow uses microphone if you choose to record audio/video and attach it to a transaction + NSPhotoLibraryUsageDescription + Flow uses the photo library when user updates their picture, or chooses to use image as + category/account icon + NSSiriUsageDescription + Flow provides shortcuts to record transactions if you choose to use them + UIApplicationSupportsIndirectInputEvents + + UIFileSharingEnabled + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/ios/Runner/RecordTransactionIntent.swift b/ios/Runner/RecordTransactionIntent.swift new file mode 100644 index 00000000..97dc990d --- /dev/null +++ b/ios/Runner/RecordTransactionIntent.swift @@ -0,0 +1,29 @@ +import AppIntents + +struct RecordTransactionIntent: AppIntent { + static var title: LocalizedStringResource = "Record an Expense" + static var description: IntentDescription = "Log expenses" + + @Parameter(title: "Account", description: "Exact name, or UUID of the target account.") + var account: String + + @Parameter(title: "Amount", description: "Expense amount. Sign doesn't matter.") + var amount: Double + + @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) + try RecordedTransactionService.append(tx) + return .result(dialog: "Expense recorded ✅") + } +} diff --git a/ios/Runner/RecordedTransaction.swift b/ios/Runner/RecordedTransaction.swift new file mode 100644 index 00000000..c8cc4a5b --- /dev/null +++ b/ios/Runner/RecordedTransaction.swift @@ -0,0 +1,35 @@ +import Foundation + +struct RecordedTransaction: Codable { + let id: UUID + let transactionDate: Date + let type: TransactionType + let amount: Double + let title: String? + let notes: String? + let fromAccount: String? + let toAccount: String? + let category: String? + + init( + id: UUID = UUID(), + transactionDate: Date = Date(), + type: TransactionType = .expense, + amount: Double, + title: String? = nil, + fromAccount: String? = nil, + toAccount: String? = nil, + category: String? = nil, + notes: String? = nil + ) { + self.id = id + self.transactionDate = transactionDate + self.type = type + self.amount = amount + self.title = title + self.notes = notes + self.fromAccount = fromAccount + self.toAccount = toAccount + self.category = category + } +} diff --git a/ios/Runner/RecordedTransactionService.swift b/ios/Runner/RecordedTransactionService.swift new file mode 100644 index 00000000..140c3159 --- /dev/null +++ b/ios/Runner/RecordedTransactionService.swift @@ -0,0 +1,26 @@ +struct RecordedTransactionService { + static let groupId = "group.mn.flow.flow" + static let fileName = "recorded_transactions.jsonl" + + static func append(_ tx: RecordedTransaction) throws { + let fm = FileManager.default + let url = fm + .containerURL(forSecurityApplicationGroupIdentifier: groupId)! + .appendingPathComponent(fileName) + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .iso8601 + encoder.outputFormatting = .withoutEscapingSlashes + + let data = try encoder.encode(tx) + let line = data + Data([0x0A]) // newline + + if fm.fileExists(atPath: url.path) { + let handle = try FileHandle(forWritingTo: url) + try handle.seekToEnd() + try handle.write(contentsOf: line) + try handle.close() + } else { + try line.write(to: url) + } + } +} diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements index db75016e..af3dddd6 100644 --- a/ios/Runner/Runner.entitlements +++ b/ios/Runner/Runner.entitlements @@ -13,6 +13,8 @@ CloudKit CloudDocuments + com.apple.developer.siri + com.apple.developer.ubiquity-container-identifiers iCloud.mn.flow.flow diff --git a/ios/Runner/TransactionType.swift b/ios/Runner/TransactionType.swift new file mode 100644 index 00000000..27adf399 --- /dev/null +++ b/ios/Runner/TransactionType.swift @@ -0,0 +1,5 @@ +enum TransactionType: String, Codable { + case expense + case income + case transfer +} \ No newline at end of file diff --git a/lib/constants.dart b/lib/constants.dart index 07c4d90c..7ac98ea3 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -37,3 +37,5 @@ const LatLng sukhbaatarSquareCenter = LatLng( sukhbaatarSquareCenterLat, sukhbaatarSquareCenterLong, ); + +const String iOSAppGroupId = "group.mn.flow.flow"; diff --git a/lib/data/transaction_programmable_object.dart b/lib/data/transaction_programmable_object.dart index 0dcbc1af..e5ac355f 100644 --- a/lib/data/transaction_programmable_object.dart +++ b/lib/data/transaction_programmable_object.dart @@ -3,6 +3,7 @@ import "package:flow/entity/transaction/type.dart"; import "package:flow/services/user_preferences.dart"; import "package:flow/utils/loose_parsers.dart"; import "package:flow/utils/utils.dart"; +import "package:uuid/uuid.dart"; class TransactionProgrammableObject { final String? title; @@ -134,6 +135,32 @@ class TransactionProgrammableObject { } } + static TransactionProgrammableObject? fromSiriJson(Map json) { + try { + if (json["fromAccount"] case String account) { + if (Uuid.isValidUUID(fromString: account)) { + json["fromAccountUuid"] = account; + json["fromAccount"] = null; + } + } + + if (json["category"] case String category) { + if (Uuid.isValidUUID(fromString: category)) { + json["categoryUuid"] = category; + json["category"] = null; + } + } + + if (json["amount"] case num amount) { + json["amount"] = -(amount.toDouble().abs()); + } + + return parse(json.cast()); + } catch (e) { + return null; + } + } + static TransactionProgrammableObject? fromEnyJson(Map json) { String? itemsToNote(List items) { return items 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/transaction.dart b/lib/entity/transaction.dart index f816c563..2897c0f0 100644 --- a/lib/entity/transaction.dart +++ b/lib/entity/transaction.dart @@ -91,6 +91,20 @@ class Transaction implements EntityBase { /// in [extra]. List extraTags; + static const String importedFromSiriTag = "ios:importedFromSiri"; + + String? get externalProviderName { + if (extraTags.contains(importedFromSiriTag)) { + return "Siri"; + } + + if (extensions.eny != null) { + return "Eny"; + } + + return null; + } + @Transient() @JsonKey(includeFromJson: false, includeToJson: false) ExtensionsWrapper get extensions => ExtensionsWrapper.parse(extra); diff --git a/lib/entity/transaction/extensions/default/geo.dart b/lib/entity/transaction/extensions/default/geo.dart index 0b0b8099..7b5819bf 100644 --- a/lib/entity/transaction/extensions/default/geo.dart +++ b/lib/entity/transaction/extensions/default/geo.dart @@ -88,4 +88,8 @@ class Geo extends TransactionExtension implements Jasonable { List? toLatLng() => (latitude != null && longitude != null) ? [latitude!, longitude!] : null; + + LatLng? toLatLngPosition() => (latitude != null && longitude != null) + ? LatLng(latitude!, longitude!) + : null; } diff --git a/lib/entity/user_preferences.dart b/lib/entity/user_preferences.dart index 12a5c476..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 @@ -63,6 +73,7 @@ class UserPreferences implements EntityBase { bool transactionListTileShowCategoryName; bool transactionListTileShowAccountForLeading; + bool transactionListTileShowExternalSource; bool transactionListTileRelaxedDensity; bool createTransactionsPerItemInScans; @@ -161,6 +172,7 @@ class UserPreferences implements EntityBase { this.useCategoryNameForUntitledTransactions = false, this.transactionListTileShowCategoryName = false, this.transactionListTileShowAccountForLeading = false, + this.transactionListTileShowExternalSource = true, this.transactionListTileRelaxedDensity = false, this.createTransactionsPerItemInScans = true, this.scansPendingThresholdInHours = 6, @@ -168,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 f2ac9907..1d5675d5 100644 --- a/lib/entity/user_preferences.g.dart +++ b/lib/entity/user_preferences.g.dart @@ -17,6 +17,8 @@ UserPreferences _$UserPreferencesFromJson(Map json) => json['transactionListTileShowCategoryName'] as bool? ?? false, transactionListTileShowAccountForLeading: json['transactionListTileShowAccountForLeading'] as bool? ?? false, + transactionListTileShowExternalSource: + json['transactionListTileShowExternalSource'] as bool? ?? true, transactionListTileRelaxedDensity: json['transactionListTileRelaxedDensity'] as bool? ?? false, createTransactionsPerItemInScans: @@ -29,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, @@ -57,6 +61,8 @@ Map _$UserPreferencesToJson( 'excludeTransfersFromFlow': instance.excludeTransfersFromFlow, 'trashBinRetentionDays': instance.trashBinRetentionDays, 'defaultFilterPreset': instance.defaultFilterPreset, + 'homePendingTransactionsTimeRangeSerialized': + instance.homePendingTransactionsTimeRangeSerialized, 'remindDailyAtRelativeSeconds': instance.remindDailyAtRelativeSeconds, 'useCategoryNameForUntitledTransactions': instance.useCategoryNameForUntitledTransactions, @@ -64,6 +70,8 @@ Map _$UserPreferencesToJson( instance.transactionListTileShowCategoryName, 'transactionListTileShowAccountForLeading': instance.transactionListTileShowAccountForLeading, + 'transactionListTileShowExternalSource': + instance.transactionListTileShowExternalSource, 'transactionListTileRelaxedDensity': instance.transactionListTileRelaxedDensity, 'createTransactionsPerItemInScans': instance.createTransactionsPerItemInScans, 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/l10n/supported_languages.dart b/lib/l10n/supported_languages.dart index 4eca4b01..fc75bbb6 100644 --- a/lib/l10n/supported_languages.dart +++ b/lib/l10n/supported_languages.dart @@ -16,4 +16,5 @@ final Map supportedLanguages = { const Locale("es", "ES"): ("Spanish (Spain)", "Español (España)"), const Locale("uk", "UA"): ("Ukrainian (Ukraine)", "Українська (Україна)"), const Locale("ar"): ("Arabic", "العربية"), + const Locale("fa", "IR"): ("Persian (Iran)", "فارسی (ایران)"), }; diff --git a/lib/main.dart b/lib/main.dart index fdcc257b..1bd654e8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -34,6 +34,7 @@ import "package:flow/providers/transaction_tags_provider.dart"; import "package:flow/routes.dart"; import "package:flow/services/currency_registry.dart"; import "package:flow/services/exchange_rates.dart"; +import "package:flow/services/integrations/siri_pending.dart"; import "package:flow/services/local_auth.dart"; import "package:flow/services/navigation.dart"; import "package:flow/services/notifications.dart"; @@ -216,6 +217,8 @@ class FlowState extends State { migratePrimaryCurrencyToDb(); migrateThemePrefsToDb(); migratePrivacyPreferencesToUserPreferences(); + + unawaited(SiriPendingService().resolveSiriTransactions()); }); _tryUnlockTempLock(); @@ -232,6 +235,7 @@ class FlowState extends State { onShow: () { if (!mounted) return; _tryUnlockTempLock(); + unawaited(SiriPendingService().resolveSiriTransactions()); if (LocalAuthService.platformSupported && LocalPreferences().requireLocalAuthOnBlur.get()) { _tempLock = true; diff --git a/lib/objectbox/actions.dart b/lib/objectbox/actions.dart index b04b3be1..73ece6ab 100644 --- a/lib/objectbox/actions.dart +++ b/lib/objectbox/actions.dart @@ -614,8 +614,40 @@ extension TransactionActions on Transaction { /// Returns the ObjectBox ID for the newly created transaction int duplicate() { - if (isTransfer) { - throw Exception("Cannot duplicate transfer transactions"); + if (extensions.transfer case Transfer transferDetails) { + final Account? fromAccount = AccountsService().findOneSync( + transferDetails.fromAccountUuid, + ); + final Account? toAccount = AccountsService().findOneSync( + transferDetails.toAccountUuid, + ); + + if (fromAccount == null || toAccount == null) { + _log.severe( + "Failed to duplicate transfer transaction due to missing account data", + ); + throw Exception( + "Failed to duplicate transfer transaction due to missing account data", + ); + } + + final (int a, _) = fromAccount.transferTo( + targetAccount: toAccount, + amount: amount, + description: description, + createdDate: Moment.now(), + isPending: isPending, + transactionDate: transactionDate, + title: title, + tags: tags.toList(), + attachments: attachments.toList(), + extraTags: extraTags, + extensions: extensions.data, + latitude: extensions.geo?.latitude, + longitude: extensions.geo?.longitude, + ); + + return a; } final Transaction duplicate = @@ -630,6 +662,7 @@ extension TransactionActions on Transaction { uuid: Uuid().v4(), extraTags: extraTags, subtype: subtype, + location: extensions.geo?.toLatLng(), ) ..setTags(tags.toList()) ..setAttachments(attachments.toList()) diff --git a/lib/objectbox/objectbox-model.json b/lib/objectbox/objectbox-model.json index 2684db13..e17bce61 100644 --- a/lib/objectbox/objectbox-model.json +++ b/lib/objectbox/objectbox-model.json @@ -362,7 +362,7 @@ }, { "id": "10:7829328581176695647", - "lastPropertyId": "28:7803371152939021971", + "lastPropertyId": "30:5353888497210708730", "name": "UserPreferences", "properties": [ { @@ -497,6 +497,16 @@ "id": "28:7803371152939021971", "name": "privacyModeUponShaking", "type": 1 + }, + { + "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 67a4a523..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(28, 7803371152939021971), + lastPropertyId: const obx_int.IdUid(30, 5353888497210708730), flags: 0, properties: [ obx_int.ModelProperty( @@ -615,6 +615,18 @@ final _entities = [ type: 1, flags: 0, ), + obx_int.ModelProperty( + id: const obx_int.IdUid(29, 4843097333162455732), + name: 'transactionListTileShowExternalSource', + type: 1, + flags: 0, + ), + obx_int.ModelProperty( + id: const obx_int.IdUid(30, 5353888497210708730), + name: 'homePendingTransactionsTimeRangeSerialized', + type: 9, + flags: 0, + ), ], relations: [], backlinks: [], @@ -1634,7 +1646,13 @@ obx_int.ModelDefinition getObjectBoxModel() { final primaryAccountUuidOffset = object.primaryAccountUuid == null ? null : fbb.writeString(object.primaryAccountUuid!); - fbb.startTable(29); + 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); @@ -1661,6 +1679,8 @@ obx_int.ModelDefinition getObjectBoxModel() { fbb.addInt64(25, object.scansPendingThresholdInHours); 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; }, @@ -1691,6 +1711,8 @@ obx_int.ModelDefinition getObjectBoxModel() { .vTableGet(buffer, rootOffset, 24, false); final transactionListTileShowAccountForLeadingParam = const fb.BoolReader().vTableGet(buffer, rootOffset, 26, false); + final transactionListTileShowExternalSourceParam = const fb.BoolReader() + .vTableGet(buffer, rootOffset, 60, false); final transactionListTileRelaxedDensityParam = const fb.BoolReader() .vTableGet(buffer, rootOffset, 46, false); final createTransactionsPerItemInScansParam = const fb.BoolReader() @@ -1714,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, @@ -1761,6 +1787,8 @@ obx_int.ModelDefinition getObjectBoxModel() { transactionListTileShowCategoryNameParam, transactionListTileShowAccountForLeading: transactionListTileShowAccountForLeadingParam, + transactionListTileShowExternalSource: + transactionListTileShowExternalSourceParam, transactionListTileRelaxedDensity: transactionListTileRelaxedDensityParam, createTransactionsPerItemInScans: @@ -1770,6 +1798,8 @@ obx_int.ModelDefinition getObjectBoxModel() { privacyModeUponShaking: privacyModeUponShakingParam, trashBinRetentionDays: trashBinRetentionDaysParam, defaultFilterPreset: defaultFilterPresetParam, + homePendingTransactionsTimeRangeSerialized: + homePendingTransactionsTimeRangeSerializedParam, enableICloudSync: enableICloudSyncParam, iCloudBackupsToKeep: iCloudBackupsToKeepParam, autoBackupIntervalInHours: autoBackupIntervalInHoursParam, @@ -2639,6 +2669,14 @@ class UserPreferences_ { /// See [UserPreferences.privacyModeUponShaking]. static final privacyModeUponShaking = obx.QueryBooleanProperty(_entities[6].properties[25]); + + /// 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/providers/transaction_tags_provider.dart b/lib/providers/transaction_tags_provider.dart index 503c641d..92c93ab5 100644 --- a/lib/providers/transaction_tags_provider.dart +++ b/lib/providers/transaction_tags_provider.dart @@ -1,11 +1,13 @@ import "package:flow/data/prefs/frecency_group.dart"; import "package:flow/entity/transaction_tag.dart"; +import "package:flow/entity/transaction_type/payload.dart"; import "package:flow/objectbox.dart"; import "package:flow/objectbox/objectbox.g.dart"; import "package:flow/prefs/transitive.dart"; import "package:flow/utils/extensions/iterables.dart"; import "package:flow/widgets/transaction_watcher.dart"; import "package:flutter/material.dart"; +import "package:latlong2/latlong.dart"; class TransactionTagsProviderScope extends StatefulWidget { final Widget child; @@ -66,6 +68,47 @@ class TransactionTagsProvider extends InheritedWidget { String? getName(dynamic id) => get(id)?.title; + List getCloseGeoTags( + LatLng latLng, { + double thresholdInMeters = 50, + List? exclusionList, + }) { + final Distance distance = const Distance(); + + final List<(TransactionTag tag, double distanceInMeters)> tagsByDistance = + tags + .map((tag) { + if (exclusionList?.any( + (excludedTag) => excludedTag.uuid == tag.uuid, + ) == + true) { + return (null, double.infinity); + } + if (tag.parsedPayload?.location + case TransactionTagLocationPayload locationPayload) { + final double distanceInMeters = distance.as( + LengthUnit.Meter, + latLng, + locationPayload.latLng, + ); + + return (tag, distanceInMeters); + } + return (null, double.infinity); + }) + .where( + (tagAndDistance) => + tagAndDistance.$1 != null && + tagAndDistance.$2 <= thresholdInMeters, + ) + .cast<(TransactionTag tag, double distanceInMeters)>() + .toList(); + + tagsByDistance.sort((a, b) => a.$2.compareTo(b.$2)); + + return tagsByDistance.map((e) => e.$1).toList(); + } + TransactionTag? get(dynamic id) => switch (id) { String uuid => _tags?.firstWhereOrNull((tag) => tag.uuid == uuid), int id => _tags?.firstWhereOrNull((tag) => tag.id == id), diff --git a/lib/routes/account/account_edit_page.dart b/lib/routes/account/account_edit_page.dart index b069f1a0..183d0660 100644 --- a/lib/routes/account/account_edit_page.dart +++ b/lib/routes/account/account_edit_page.dart @@ -225,7 +225,7 @@ class _AccountEditPageState extends State { Text(_currency, style: context.textTheme.labelLarge), if (widget.isNewAccount) ...[ const SizedBox(width: 8.0), - const DirectionalChevron(), + const LeChevron(), ], ], ), @@ -242,7 +242,7 @@ class _AccountEditPageState extends State { style: context.textTheme.labelLarge, ), const SizedBox(width: 8.0), - const DirectionalChevron(), + const LeChevron(), ], ), onTap: selectAccountType, diff --git a/lib/routes/debug/debug_theme_page.dart b/lib/routes/debug/debug_theme_page.dart index 9f2d23f3..211e2250 100644 --- a/lib/routes/debug/debug_theme_page.dart +++ b/lib/routes/debug/debug_theme_page.dart @@ -11,6 +11,7 @@ import "package:flow/widgets/general/frame.dart"; import "package:flow/widgets/general/list_header.dart"; import "package:flow/widgets/general/wavy_divider.dart"; import "package:flow/widgets/transaction_list_tile.dart"; +import "package:flow/widgets/transaction_tag_chip.dart"; import "package:flutter/material.dart"; import "package:go_router/go_router.dart"; import "package:material_symbols_icons/symbols.dart"; @@ -56,6 +57,15 @@ class DebugThemePage extends StatelessWidget { selected: true, ), ActionChip(label: Text("ActionChip"), onPressed: () {}), + TransactionTagChip(tag: .new(title: "Example Tag")), + TransactionTagChip( + tag: .new(title: "Example Tag"), + selected: true, + ), + TransactionTagChip( + tag: .new(title: "Example Tag"), + isSuggestion: true, + ), ], ), ), diff --git a/lib/routes/export/export_pdf_page.dart b/lib/routes/export/export_pdf_page.dart index 4b53397e..b5c02fda 100644 --- a/lib/routes/export/export_pdf_page.dart +++ b/lib/routes/export/export_pdf_page.dart @@ -88,7 +88,7 @@ class _ExportPdfPageState extends State { const Spacer(), Button( onTap: canGenerate ? _generatePDF : null, - trailing: DirectionalChevron(), + trailing: const LeChevron(), child: Text("general.confirm".t(context)), ), ], @@ -128,10 +128,10 @@ class _ExportPdfPageState extends State { }), ), onTap: _selectAccounts, - trailing: DirectionalChevron(), + trailing: const LeChevron(), ), ListTile( - leading: Icon(Symbols.category_rounded), + leading: const Icon(Symbols.category_rounded), title: Text("sync.export.pdf.categories".t(context)), subtitle: Text( "sync.export.pdf.categories.selected".tr({ @@ -140,21 +140,21 @@ class _ExportPdfPageState extends State { }), ), onTap: _selectCategories, - trailing: DirectionalChevron(), + trailing: const LeChevron(), ), ListTile( leading: Icon(Symbols.schedule_rounded), title: Text("sync.export.pdf.timeRange".t(context)), subtitle: Text(rangeText), onTap: _selectRange, - trailing: DirectionalChevron(), + trailing: const LeChevron(), ), ListTile( leading: Icon(Symbols.expand_content_rounded), title: Text("sync.export.pdf.size".t(context)), subtitle: Text(_useA4 ? "A4" : "Letter"), onTap: _selectPaperSize, - trailing: DirectionalChevron(), + trailing: const LeChevron(), ), ], ), diff --git a/lib/routes/home/accounts_tab.dart b/lib/routes/home/accounts_tab.dart index 11333d1a..39eb986d 100644 --- a/lib/routes/home/accounts_tab.dart +++ b/lib/routes/home/accounts_tab.dart @@ -14,6 +14,7 @@ import "package:flow/widgets/general/frame.dart"; import "package:flow/widgets/general/spinner.dart"; import "package:flow/widgets/home/home/account/no_accounts.dart"; import "package:flow/widgets/home/home/account/total_balance.dart"; +import "package:flow/widgets/home/privacy_toggler.dart"; import "package:flutter/material.dart"; import "package:go_router/go_router.dart"; import "package:material_symbols_icons/symbols.dart"; @@ -165,6 +166,7 @@ class _AccountsTabState extends State ? const Icon(Symbols.check_rounded) : const Icon(Symbols.reorder_rounded), ), + const PrivacyToggler(), ], ), TotalBalance(), 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/home/stats_tab.dart b/lib/routes/home/stats_tab.dart index b3083932..c2bddc8a 100644 --- a/lib/routes/home/stats_tab.dart +++ b/lib/routes/home/stats_tab.dart @@ -291,7 +291,7 @@ class _StatsTabState extends State label: Text( "tabs.stats.categories.seeAll".t(context), ), - icon: DirectionalChevron(), + icon: const LeChevron(), iconAlignment: IconAlignment.end, ), ), diff --git a/lib/routes/preferences/button_order_preferences_page.dart b/lib/routes/preferences/button_order_preferences_page.dart index 036ad140..db71ab92 100644 --- a/lib/routes/preferences/button_order_preferences_page.dart +++ b/lib/routes/preferences/button_order_preferences_page.dart @@ -1,6 +1,6 @@ import "dart:developer"; -import "package:dotted_border/dotted_border.dart"; +import "package:dashed_border/dashed_border.dart"; import "package:flow/data/flow_button_type.dart"; import "package:flow/l10n/extensions.dart"; import "package:flow/services/integrations/eny.dart"; @@ -140,37 +140,38 @@ class ButtonOrderPreferencesPageState left: position.dx, top: position.dy, child: Container( - decoration: BoxDecoration(borderRadius: .all(widget.radius)), - clipBehavior: .none, - child: DottedBorder( - options: RoundedRectDottedBorderOptions( + decoration: BoxDecoration( + borderRadius: .all(widget.radius), + border: DashedBorder( color: Theme.of(context).dividerColor.withAlpha(0x80), - strokeWidth: 4.0, - radius: widget.radius, - strokeCap: .round, - dashPattern: const [6.0, 10.0], + width: 4.0, + borderRadius: BorderRadius.all(widget.radius), + dashLength: 6.0, + dashGap: 10.0, + style: .dashed, ), - child: SizedBox.square( - dimension: size, - child: DragTarget( - onWillAcceptWithDetails: (details) => - details.data != transactionType, - onAcceptWithDetails: (details) => - swap(transactionButtonOrder, details.data, transactionType), - onMove: (details) => updateAnimationData( - transactionButtonOrder, - details.data, - transactionType, - ), - builder: - ( - BuildContext context, - List candidateData, - List rejectedData, - ) { - return SizedBox.shrink(); - }, + ), + clipBehavior: .none, + child: SizedBox.square( + dimension: size, + child: DragTarget( + onWillAcceptWithDetails: (details) => + details.data != transactionType, + onAcceptWithDetails: (details) => + swap(transactionButtonOrder, details.data, transactionType), + onMove: (details) => updateAnimationData( + transactionButtonOrder, + details.data, + transactionType, ), + builder: + ( + BuildContext context, + List candidateData, + List rejectedData, + ) { + return SizedBox.shrink(); + }, ), ), ), diff --git a/lib/routes/preferences/integrations/eny_preferences_page.dart b/lib/routes/preferences/integrations/eny_preferences_page.dart index 0cfe551b..340e2669 100644 --- a/lib/routes/preferences/integrations/eny_preferences_page.dart +++ b/lib/routes/preferences/integrations/eny_preferences_page.dart @@ -71,7 +71,7 @@ class _EnyPreferencesPageState extends State { child: AnimatedEnyLogo(noAnimation: true), ), title: Text("integrations.eny.dashboard".t(context)), - trailing: const DirectionalChevron(), + trailing: const LeChevron(), onTap: () { openUrl(enyDashboardLink, .externalApplication); }, diff --git a/lib/routes/preferences/money_formatting_preferences_page.dart b/lib/routes/preferences/money_formatting_preferences_page.dart index d13741a7..8c758ba5 100644 --- a/lib/routes/preferences/money_formatting_preferences_page.dart +++ b/lib/routes/preferences/money_formatting_preferences_page.dart @@ -70,7 +70,7 @@ class _MoneyFormattingPreferencesPageState "preferences.moneyFormatting.setICUPattern".t(context), ), onTap: updateCustomICUCurrencyFormatter, - trailing: const DirectionalChevron(), + trailing: const LeChevron(), ), ], ), 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/preferences/transaction_list_item_appearance_preferences_page.dart b/lib/routes/preferences/transaction_list_item_appearance_preferences_page.dart index c6c04589..e36d68ae 100644 --- a/lib/routes/preferences/transaction_list_item_appearance_preferences_page.dart +++ b/lib/routes/preferences/transaction_list_item_appearance_preferences_page.dart @@ -30,6 +30,8 @@ class _TransactionListItemAppearancePreferencesPageState UserPreferencesService().useCategoryNameForUntitledTransactions; final bool transactionListTileShowCategoryName = UserPreferencesService().transactionListTileShowCategoryName; + final bool transactionListTileShowExternalSource = + UserPreferencesService().transactionListTileShowExternalSource; final bool transactionListTileShowAccountForLeading = UserPreferencesService().transactionListTileShowAccountForLeading; final bool transactionListTileRelaxedDensity = @@ -90,9 +92,21 @@ class _TransactionListItemAppearancePreferencesPageState ), SwitchListTile( title: Text( - "preferences.transactions.listTile.transactionListTileRelaxedDensity" + "preferences.transactions.listTile.transactionListTileShowExternalSource" .t(context), ), + value: transactionListTileShowExternalSource, + onChanged: (bool newValue) { + UserPreferencesService() + .transactionListTileShowExternalSource = + newValue; + setState(() {}); + }, + ), + SwitchListTile( + title: Text( + "preferences.transactions.listTile.relaxedDensity".t(context), + ), value: transactionListTileRelaxedDensity, onChanged: (bool newValue) { UserPreferencesService().transactionListTileRelaxedDensity = diff --git a/lib/routes/preferences_page.dart b/lib/routes/preferences_page.dart index e1d4d4ec..cb39626a 100644 --- a/lib/routes/preferences_page.dart +++ b/lib/routes/preferences_page.dart @@ -18,7 +18,6 @@ import "package:flow/utils/extensions.dart"; import "package:flow/widgets/animated_eny_logo.dart"; import "package:flow/widgets/general/directional_chevron.dart"; import "package:flow/widgets/general/list_header.dart"; -import "package:flow/widgets/general/rtl_flipper.dart"; import "package:flow/widgets/sheets/select_currency_sheet.dart"; import "package:flutter/material.dart" hide Flow; import "package:go_router/go_router.dart"; @@ -89,23 +88,21 @@ class PreferencesPageState extends State { title: Text("preferences.sync".t(context)), leading: const Icon(Symbols.sync_rounded), onTap: () => _pushAndRefreshAfter("/preferences/sync"), - trailing: DirectionalChevron(), + trailing: const LeChevron(), ), if (flowDebugMode || NotificationsService.schedulingSupported) ListTile( title: Text("preferences.reminders".t(context)), leading: const Icon(Symbols.notifications_rounded), onTap: () => _pushAndRefreshAfter("/preferences/reminders"), - trailing: RTLFlipper( - child: const Icon(Symbols.chevron_right_rounded), - ), + trailing: const LeChevron(), ), ListTile( title: Text("preferences.language".t(context)), leading: const Icon(Symbols.language_rounded), onTap: () => _updateLanguage(), subtitle: Text(FlowLocalizations.of(context).locale.endonym), - trailing: DirectionalChevron(), + trailing: const LeChevron(), ), ListTile( title: Text("preferences.primaryCurrency".t(context)), @@ -113,7 +110,7 @@ class PreferencesPageState extends State { leading: const Icon(Symbols.universal_currency_alt_rounded), onTap: () => _updatePrimaryCurrency(), subtitle: Text(currentPrimaryCurrency), - trailing: DirectionalChevron(), + trailing: const LeChevron(), ), ListTile( title: Text("preferences.transfer".t(context)), @@ -124,19 +121,19 @@ class PreferencesPageState extends State { maxLines: 1, overflow: TextOverflow.ellipsis, ), - trailing: DirectionalChevron(), + trailing: const LeChevron(), ), ListTile( title: Text("preferences.trashBin".t(context)), leading: const Icon(Symbols.delete_rounded), onTap: () => _pushAndRefreshAfter("/preferences/trashBin"), - trailing: DirectionalChevron(), + trailing: const LeChevron(), ), ListTile( title: Text("preferences.moneyFormatting".t(context)), leading: const Icon(Symbols.numbers_rounded), onTap: () => _pushAndRefreshAfter("/preferences/moneyFormatting"), - trailing: DirectionalChevron(), + trailing: const LeChevron(), ), const SizedBox(height: 24.0), ListHeader("preferences.integrations".t(context)), @@ -150,7 +147,7 @@ class PreferencesPageState extends State { ), onTap: () => _pushAndRefreshAfter("/preferences/integrations/eny"), - trailing: DirectionalChevron(), + trailing: const LeChevron(), ), const SizedBox(height: 24.0), ListHeader("preferences.transactions".t(context)), @@ -165,7 +162,7 @@ class PreferencesPageState extends State { leading: const Icon(Symbols.search_activity_rounded), onTap: () => _pushAndRefreshAfter("/preferences/pendingTransactions"), - trailing: DirectionalChevron(), + trailing: const LeChevron(), ), ListTile( title: Text("preferences.transactions.geo".t(context)), @@ -182,7 +179,7 @@ class PreferencesPageState extends State { maxLines: 1, overflow: TextOverflow.ellipsis, ), - trailing: DirectionalChevron(), + trailing: const LeChevron(), ), ListTile( leading: const Icon(Symbols.list_rounded), @@ -190,14 +187,14 @@ class PreferencesPageState extends State { onTap: () => _pushAndRefreshAfter( "/preferences/transactionListItemAppearance", ), - trailing: DirectionalChevron(), + trailing: const LeChevron(), ), ListTile( leading: const Icon(Symbols.automation_rounded), title: Text("preferences.transactionEntryFlow".t(context)), onTap: () => _pushAndRefreshAfter("/preferences/transactionEntryFlow"), - trailing: DirectionalChevron(), + trailing: const LeChevron(), ), const SizedBox(height: 24.0), ListHeader("preferences.appearance".t(context)), @@ -211,7 +208,7 @@ class PreferencesPageState extends State { themeNames[currentTheme.name] ?? currentTheme.name, ), onTap: _openTheme, - trailing: DirectionalChevron(), + trailing: const LeChevron(), ), ListTile( title: Text("preferences.numpad".t(context)), @@ -222,7 +219,7 @@ class PreferencesPageState extends State { ? "preferences.numpad.layout.modern".t(context) : "preferences.numpad.layout.classic".t(context), ), - trailing: DirectionalChevron(), + trailing: const LeChevron(), ), ListTile( title: Text("preferences.transactionButtonOrder".t(context)), @@ -234,13 +231,13 @@ class PreferencesPageState extends State { maxLines: 1, overflow: TextOverflow.ellipsis, ), - trailing: DirectionalChevron(), + trailing: const LeChevron(), ), ListTile( title: Text("preferences.changeVisuals".t(context)), leading: const Icon(Symbols.moving_rounded), onTap: () => _pushAndRefreshAfter("/preferences/changeVisuals"), - trailing: DirectionalChevron(), + trailing: const LeChevron(), ), const SizedBox(height: 24.0), ListHeader("preferences.privacy".t(context)), @@ -258,13 +255,13 @@ class PreferencesPageState extends State { title: Text("fileAttachment.cleanupHangingFiles".t(context)), leading: const Icon(Symbols.bug_report_rounded), onTap: () => _deleteHangingFiles(), - trailing: DirectionalChevron(), + trailing: const LeChevron(), ), ListTile( title: Text("preferences.feedback.debugLogs".t(context)), leading: const Icon(Symbols.bug_report_rounded), onTap: () => context.push("/_debug/logs"), - trailing: DirectionalChevron(), + trailing: const LeChevron(), ), const SizedBox(height: 16.0), ], diff --git a/lib/routes/transaction_page.dart b/lib/routes/transaction_page.dart index 0e4759b5..e292fc38 100644 --- a/lib/routes/transaction_page.dart +++ b/lib/routes/transaction_page.dart @@ -42,7 +42,6 @@ import "package:flow/services/transactions.dart"; import "package:flow/services/user_preferences.dart"; import "package:flow/theme/theme.dart"; import "package:flow/utils/utils.dart"; -import "package:flow/widgets/animated_eny_logo.dart"; import "package:flow/widgets/general/button.dart"; import "package:flow/widgets/general/directional_chevron.dart"; import "package:flow/widgets/general/flow_icon.dart"; @@ -52,6 +51,8 @@ import "package:flow/widgets/general/money_text.dart"; import "package:flow/widgets/location_picker_sheet.dart"; import "package:flow/widgets/open_street_map.dart"; import "package:flow/widgets/sheets/select_transaction_tags_sheet.dart"; +import "package:flow/widgets/transaction/imported_from_eny.dart"; +import "package:flow/widgets/transaction/imported_from_siri.dart"; import "package:flow/widgets/transaction/type_selector.dart"; import "package:flutter/foundation.dart" hide Category; import "package:flutter/material.dart"; @@ -149,8 +150,6 @@ class _TransactionPageState extends State { bool _isPending = false; - bool _importedFromEny = false; - @override void initState() { super.initState(); @@ -223,7 +222,6 @@ class _TransactionPageState extends State { _currentlyEditing.extensions.transfer?.toAccountUuid, ); _geo = _currentlyEditing.extensions.geo; - _importedFromEny = _currentlyEditing.extensions.eny != null; _isPending = _currentlyEditing.isPending ?? _isPending; if (_currentlyEditing.isTransfer == true) { _conversionRate = @@ -413,7 +411,7 @@ class _TransactionPageState extends State { ), onTap: () => inputPostConversionAmount(), trailing: _selectedAccountTransferTo == null - ? DirectionalChevron() + ? LeChevron() : null, focusNode: _selectAccountTransferToFocusNode, ), @@ -444,6 +442,8 @@ class _TransactionPageState extends State { TagsSection( selectTags: selectTags, selectedTags: _selectedTags, + onTagsChanged: onTagsChanged, + location: _geo, ), DescriptionSection( value: _descriptionMarkdown, @@ -470,7 +470,7 @@ class _TransactionPageState extends State { title: Text(transactionDate.toMoment().LLL), onTap: () => selectTransactionDate(), leading: Icon(Symbols.calendar_month_rounded), - trailing: const DirectionalChevron(), + trailing: const LeChevron(), ), SwitchListTile( title: Text("transaction.pending".t(context)), @@ -502,7 +502,7 @@ class _TransactionPageState extends State { "transaction.recurring.setup".t(context), ), onTap: _setupRecurring, - trailing: const DirectionalChevron(), + trailing: const LeChevron(), ), ), ), @@ -579,14 +579,11 @@ class _TransactionPageState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - if (!isTransfer) - ListTile( - leading: Icon(Symbols.content_copy_rounded), - title: Text( - "transaction.duplicate".t(context), - ), - onTap: () => _duplicate(), - ), + ListTile( + leading: Icon(Symbols.content_copy_rounded), + title: Text("transaction.duplicate".t(context)), + onTap: () => _duplicate(), + ), if (_currentlyEditing.isDeleted == true) ListTile( leading: Icon(Symbols.restore_page_rounded), @@ -620,38 +617,25 @@ class _TransactionPageState extends State { context, ), ), - if (_importedFromEny) ...[ - const SizedBox(height: 8.0), - GestureDetector( - onTap: () { - openUrl(enyHomeLink); - }, - child: RichText( - text: TextSpan( + if (UserPreferencesService() + .transactionListTileShowExternalSource) + if (_currentlyEditing.externalProviderName + case String providerName) ...[ + const SizedBox(height: 8.0), + switch (providerName.toLowerCase()) { + "eny" => const ImportedFromEny(), + "siri" => const ImportedFromSiri(), + _ => Text( + "transaction.external.from".t( + context, + providerName, + ), style: context.textTheme.bodyMedium?.semi( context, ), - children: [ - WidgetSpan( - child: SizedBox.square( - dimension: 16.0, - child: AnimatedEnyLogo( - noAnimation: true, - ), - ), - ), - TextSpan(text: " "), - TextSpan( - text: "transaction.external.from".t( - context, - "Eny", - ), - ), - ], ), - ), - ), - ], + }, + ], ], ), ), @@ -871,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; @@ -1026,6 +1010,12 @@ class _TransactionPageState extends State { setState(() {}); } + void onTagsChanged(List newTags) { + _selectedTags = newTags; + + setState(() {}); + } + Future selectTags([bool fromAutomatedFlow = false]) async { final List allTags = TransactionTagsProvider.of( context, diff --git a/lib/routes/transaction_page/sections/description_section.dart b/lib/routes/transaction_page/sections/description_section.dart index 5ea20204..65a61767 100644 --- a/lib/routes/transaction_page/sections/description_section.dart +++ b/lib/routes/transaction_page/sections/description_section.dart @@ -55,7 +55,7 @@ class _DescriptionSectionState extends State { onTap: () => showEditModal(context), title: Text("transaction.description.add".t(context)), leading: Icon(Symbols.add_notes_rounded), - trailing: const DirectionalChevron(), + trailing: const LeChevron(), ) : InkWell( onTap: () => showEditModal(context), diff --git a/lib/routes/transaction_page/sections/tags_section.dart b/lib/routes/transaction_page/sections/tags_section.dart index a9f21a84..5c4e6d59 100644 --- a/lib/routes/transaction_page/sections/tags_section.dart +++ b/lib/routes/transaction_page/sections/tags_section.dart @@ -1,65 +1,94 @@ +import "package:flow/entity/transaction/extensions/default/geo.dart"; import "package:flow/entity/transaction_tag.dart"; import "package:flow/l10n/extensions.dart"; import "package:flow/prefs/local_preferences.dart"; +import "package:flow/providers/transaction_tags_provider.dart"; import "package:flow/routes/transaction_page/section.dart"; -import "package:flow/widgets/general/directional_chevron.dart"; import "package:flow/widgets/general/frame.dart"; import "package:flow/widgets/general/info_text.dart"; +import "package:flow/widgets/transaction_tag_add_chip.dart"; import "package:flow/widgets/transaction_tag_chip.dart"; import "package:flutter/material.dart"; import "package:flutter/services.dart"; -import "package:material_symbols_icons/symbols.dart"; +import "package:latlong2/latlong.dart"; class TagsSection extends StatelessWidget { final List? selectedTags; final VoidCallback selectTags; + final ValueChanged> onTagsChanged; - const TagsSection({super.key, this.selectedTags, required this.selectTags}); + /// Used for suggesting nearby tags based on the transaction's location. + final Geo? location; + + const TagsSection({ + super.key, + this.selectedTags, + required this.selectTags, + required this.onTagsChanged, + this.location, + }); @override Widget build(BuildContext context) { + final List? suggestedGeoTags = switch (location + ?.toLatLngPosition()) { + LatLng latLng => TransactionTagsProvider.of( + context, + ).getCloseGeoTags(latLng, exclusionList: selectedTags), + _ => null, + }; + + final bool hasSuggestedGeoTags = suggestedGeoTags?.isNotEmpty == true; + return Section( title: "transaction.tags".t(context), child: GestureDetector( behavior: HitTestBehavior.opaque, - child: selectedTags?.isNotEmpty == true - ? Frame( - child: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Align( - alignment: AlignmentDirectional.topStart, - child: Column( - crossAxisAlignment: .start, - spacing: 8.0, - children: [ - IgnorePointer( - child: Wrap( - spacing: 12.0, - runSpacing: 8.0, - children: selectedTags! - .map( - (tag) => TransactionTagChip( - tag: tag, - selected: true, - ), - ) - .toList(), - ), + child: Frame( + child: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Align( + alignment: AlignmentDirectional.topStart, + child: Column( + crossAxisAlignment: .start, + spacing: 8.0, + children: [ + Wrap( + spacing: 12.0, + runSpacing: 8.0, + children: [ + TransactionTagAddChip( + onPressed: selectTags, + title: "transaction.tags.add".t(context), + ), + ...?suggestedGeoTags?.map( + (tag) => TransactionTagChip( + tag: tag, + selected: false, + isSuggestion: true, + onPressed: () { + _addTag(context, tag); + }, ), - InfoText( - child: Text("transaction.tags.editGuide".t(context)), + ), + ...?selectedTags?.map( + (tag) => IgnorePointer( + child: TransactionTagChip(tag: tag, selected: true), ), - ], - ), + ), + ], ), - ), - ) - : ListTile( - leading: Icon(Symbols.style_rounded), - title: Text("transaction.edit.selectTags".t(context)), - trailing: DirectionalChevron(), - onTap: selectTags, + if (hasSuggestedGeoTags) + InfoText( + child: Text( + "transaction.tags.suggestionGuide".t(context), + ), + ), + ], ), + ), + ), + ), onTap: () { if (LocalPreferences().enableHapticFeedback.get()) { HapticFeedback.lightImpact(); @@ -70,4 +99,14 @@ class TagsSection extends StatelessWidget { ), ); } + + void _addTag(BuildContext context, TransactionTag tag) { + if (selectedTags?.contains(tag) == true) return; + + if (LocalPreferences().enableHapticFeedback.get()) { + HapticFeedback.lightImpact(); + } + + onTagsChanged([...?selectedTags, tag]); + } } diff --git a/lib/routes/transaction_page/select_account_sheet.dart b/lib/routes/transaction_page/select_account_sheet.dart index 9d78cdf9..fbeb90b6 100644 --- a/lib/routes/transaction_page/select_account_sheet.dart +++ b/lib/routes/transaction_page/select_account_sheet.dart @@ -103,7 +103,7 @@ class _SelectAccountSheetState extends State { ) : null, leading: FlowIcon(account.icon), - trailing: widget.showTrailing ? DirectionalChevron() : null, + trailing: widget.showTrailing ? LeChevron() : null, onTap: () => context.pop(account), selected: widget.currentlySelectedAccountId == account.id, ), diff --git a/lib/routes/transaction_page/select_category_sheet.dart b/lib/routes/transaction_page/select_category_sheet.dart index a49266d5..d22c9df4 100644 --- a/lib/routes/transaction_page/select_category_sheet.dart +++ b/lib/routes/transaction_page/select_category_sheet.dart @@ -79,7 +79,7 @@ class _SelectCategorySheetState extends State { category.icon, colorScheme: category.colorScheme, ), - trailing: widget.showTrailing ? DirectionalChevron() : null, + trailing: widget.showTrailing ? LeChevron() : null, onTap: () => context.pop(Optional(category)), selected: widget.currentlySelectedCategoryId == category.id, ), diff --git a/lib/routes/transaction_page/select_recurrence/select_until_mode_sheet.dart b/lib/routes/transaction_page/select_recurrence/select_until_mode_sheet.dart index e63c6f36..c6f3e2df 100644 --- a/lib/routes/transaction_page/select_recurrence/select_until_mode_sheet.dart +++ b/lib/routes/transaction_page/select_recurrence/select_until_mode_sheet.dart @@ -23,7 +23,7 @@ class SelectUntilModeSheet extends StatelessWidget { onTap: () { context.pop(value); }, - trailing: const DirectionalChevron(), + trailing: const LeChevron(), ), ) .toList(), diff --git a/lib/routes/transaction_page/select_recurring_update_mode_sheet.dart b/lib/routes/transaction_page/select_recurring_update_mode_sheet.dart index 5f2d0484..aac8b20c 100644 --- a/lib/routes/transaction_page/select_recurring_update_mode_sheet.dart +++ b/lib/routes/transaction_page/select_recurring_update_mode_sheet.dart @@ -56,7 +56,7 @@ class SelectRecurringUpdateModeSheet extends StatelessWidget { key: ValueKey(mode), title: Text(mode.localizedNameContext(context)), onTap: () => context.pop(mode), - trailing: showTrailing ? DirectionalChevron() : null, + trailing: showTrailing ? LeChevron() : null, selected: current == mode, ), ), diff --git a/lib/routes/transaction_tag_page.dart b/lib/routes/transaction_tag_page.dart index d0cc9492..b8f30093 100644 --- a/lib/routes/transaction_tag_page.dart +++ b/lib/routes/transaction_tag_page.dart @@ -216,20 +216,20 @@ class _TransactionTagPageState extends State { _type == TransactionTagType.location) ListTile( enabled: !_locationBusy, - leading: Icon(Symbols.my_location_rounded), + leading: const Icon(Symbols.my_location_rounded), onTap: _useMyLocation, title: Text( "transaction.tags.location.useCurrent".t(context), ), - trailing: DirectionalChevron(), + trailing: const LeChevron(), ), if ((Platform.isAndroid || Platform.isIOS) && _type == TransactionTagType.contact) ...[ ListTile( - leading: Icon(Symbols.contact_page_rounded), + leading: const Icon(Symbols.contact_page_rounded), onTap: _selectContact, title: Text("transaction.tags.contact.select".t(context)), - trailing: DirectionalChevron(), + trailing: const LeChevron(), ), Frame( child: InfoText( diff --git a/lib/services/integrations/eny.dart b/lib/services/integrations/eny.dart index 7335eb6c..1adffc90 100644 --- a/lib/services/integrations/eny.dart +++ b/lib/services/integrations/eny.dart @@ -355,7 +355,7 @@ class EnyService { .catchError((error) => false); if (succeeded) { ExternalToastsService().addToast( - "transaction.external.added".tr(), + "transaction.external.added.from".tr("Eny"), .success, ); } diff --git a/lib/services/integrations/siri_pending.dart b/lib/services/integrations/siri_pending.dart new file mode 100644 index 00000000..1f091943 --- /dev/null +++ b/lib/services/integrations/siri_pending.dart @@ -0,0 +1,66 @@ +import "dart:io"; + +import "package:flow/data/transaction_programmable_object.dart"; +import "package:flow/entity/transaction.dart"; +import "package:flow/l10n/extensions.dart"; +import "package:flow/objectbox/actions.dart"; +import "package:flow/services/external_toasts.dart"; +import "package:flow/services/user_preferences.dart"; +import "package:flow/utils/ios/get_siri_transactions.dart"; +import "package:logging/logging.dart"; + +final Logger _log = Logger("SiriPendingService"); + +class SiriPendingService { + static SiriPendingService? _instance; + + factory SiriPendingService() => _instance ??= SiriPendingService._internal(); + + SiriPendingService._internal() { + // Constructor + } + + Future resolveSiriTransactions() async { + try { + if (!Platform.isIOS) { + _log.fine("Not on iOS, skipping Siri transactions resolution"); + return; + } + + final bool markPendingAllTransactions = + UserPreferencesService().scansPendingThresholdInHours == 0; + + final List transactions = + await getSiriTransactions(); + + int saved = 0; + + for (final TransactionProgrammableObject transaction in transactions) { + try { + transaction.save( + extraTags: [Transaction.importedFromSiriTag], + isPendingOverride: markPendingAllTransactions, + ); + saved++; + } catch (e, stackTrace) { + _log.severe( + "Failed to save transaction from Siri: $transaction", + e, + stackTrace, + ); + } + } + if (saved > 0) { + ExternalToastsService().addToast( + "transaction.external.added.from".tr("Siri"), + .success, + ); + } + _log.info( + "Successfully imported $saved out of ${transactions.length} transactions from Siri", + ); + } catch (e, stackTrace) { + _log.severe("Failed to resolve Siri transactions", e, stackTrace); + } + } +} 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 301d68f7..5dcd8e81 100644 --- a/lib/services/user_preferences.dart +++ b/lib/services/user_preferences.dart @@ -1,9 +1,11 @@ import "dart:async"; import "dart:math"; +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"; @@ -67,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); @@ -166,6 +179,16 @@ class UserPreferencesService { ObjectBox().box().put(value); } + bool get transactionListTileShowExternalSource => + value.transactionListTileShowExternalSource; + set transactionListTileShowExternalSource( + bool newTransactionListTileShowExternalSource, + ) { + value.transactionListTileShowExternalSource = + newTransactionListTileShowExternalSource; + ObjectBox().box().put(value); + } + bool get transactionListTileRelaxedDensity => value.transactionListTileRelaxedDensity; set transactionListTileRelaxedDensity( @@ -351,7 +374,7 @@ class UserPreferencesService { .where((e) => e != FlowButtonType.eny) .map((e) => e.value) .join(","); - await HomeWidget.setAppGroupId("group.mn.flow.flow"); + await HomeWidget.setAppGroupId(iOSAppGroupId); await HomeWidget.saveWidgetData("buttonOrder", value); await Future.wait([ HomeWidget.updateWidget( diff --git a/lib/utils/ios/get_siri_transactions.dart b/lib/utils/ios/get_siri_transactions.dart new file mode 100644 index 00000000..d1e253b5 --- /dev/null +++ b/lib/utils/ios/get_siri_transactions.dart @@ -0,0 +1,102 @@ +import "dart:async"; +import "dart:convert"; +import "dart:io"; + +import "package:flow/constants.dart"; +import "package:flow/data/transaction_programmable_object.dart"; +import "package:flutter_app_group_directory/flutter_app_group_directory.dart"; + +final String _siriFileName = "recorded_transactions.jsonl"; + +Future _getAppGroupFile(String filename) async { + try { + final Directory? directory = + await FlutterAppGroupDirectory.getAppGroupDirectory(iOSAppGroupId); + + if (directory == null) { + throw Exception("App group directory not found"); + } + + final File file = File("${directory.path}/$filename"); + + if (!(await file.exists())) { + throw Exception("File not found"); + } else { + return file; + } + } catch (e) { + return null; + } +} + +Future _readAppGroupFile(String filename) async { + try { + final File? file = await _getAppGroupFile(filename); + + if (file == null) { + throw Exception("File not found"); + } + + final String contents = await file.readAsString(); + return contents; + } catch (e) { + return null; + } +} + +/// Returns whether the file was successfully deleted or not. +/// If the file doesn't exist, it returns false. +Future _deleteAppGroupFile(String filename) async { + try { + final File? file = await _getAppGroupFile(filename); + + if (file == null) { + throw Exception("File not found"); + } + + await file.delete(); + return true; + } catch (e) { + return false; + } +} + +Future> getSiriTransactions() async { + final String? fileContent = await _readAppGroupFile(_siriFileName); + + if (fileContent == null) { + return []; + } + + try { + final List lines = fileContent + .split("\n") + .where((line) => line.trim().isNotEmpty) + .toList(); + + final List transactions = []; + + for (final String line in lines) { + try { + final TransactionProgrammableObject? transaction = + TransactionProgrammableObject.fromSiriJson(jsonDecode(line)); + if (transaction != null) { + transactions.add(transaction); + } + } catch (e) { + // If a line is malformed, skip it and continue processing other lines + } + } + + unawaited( + _deleteAppGroupFile("recorded_transactions.jsonl").catchError((_) { + // If deletion fails, we can ignore it since it's not critical for the app's functionality + return false; + }), + ); + + return transactions; + } catch (e) { + return []; + } +} 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/account_card.dart b/lib/widgets/account_card.dart index 14b44f5a..cddf52b0 100644 --- a/lib/widgets/account_card.dart +++ b/lib/widgets/account_card.dart @@ -86,10 +86,14 @@ class AccountCard extends StatelessWidget { if (primary) WidgetSpan( alignment: .middle, - child: Icon( - Symbols.star_rounded, - size: context.textTheme.titleSmall?.fontSize, - color: context.colorScheme.primary, + child: Padding( + padding: const EdgeInsets.only(right: 4.0), + child: Icon( + Symbols.star_rounded, + size: + context.textTheme.titleSmall?.fontSize, + color: context.colorScheme.primary, + ), ), ), TextSpan( diff --git a/lib/widgets/flow_themes.dart b/lib/widgets/flow_themes.dart index 7088861b..4d6e7634 100644 --- a/lib/widgets/flow_themes.dart +++ b/lib/widgets/flow_themes.dart @@ -36,6 +36,8 @@ class _FlowThemesState extends State { UserPreferencesService().useCategoryNameForUntitledTransactions, useAccountIconForLeading: UserPreferencesService().transactionListTileShowAccountForLeading, + showExternalSource: + UserPreferencesService().transactionListTileShowExternalSource, showCategory: UserPreferencesService().transactionListTileShowCategoryName, padding: relaxed diff --git a/lib/widgets/general/directional_chevron.dart b/lib/widgets/general/directional_chevron.dart index 6ddd6c15..b408bc8c 100644 --- a/lib/widgets/general/directional_chevron.dart +++ b/lib/widgets/general/directional_chevron.dart @@ -1,12 +1,11 @@ -import "package:flow/widgets/general/rtl_flipper.dart"; import "package:flutter/material.dart"; import "package:material_symbols_icons/symbols.dart"; -class DirectionalChevron extends StatelessWidget { - const DirectionalChevron({super.key}); +class LeChevron extends StatelessWidget { + const LeChevron({super.key}); @override Widget build(BuildContext context) { - return const RTLFlipper(child: Icon(Symbols.chevron_right_rounded)); + return const Icon(Symbols.chevron_right_rounded); } } diff --git a/lib/widgets/home/greetings_bar.dart b/lib/widgets/home/greetings_bar.dart index 823f4c85..01d2cf05 100644 --- a/lib/widgets/home/greetings_bar.dart +++ b/lib/widgets/home/greetings_bar.dart @@ -41,7 +41,7 @@ class GreetingsBar extends StatelessWidget { ), ), const SizedBox(width: 12.0), - PrivacyToggler(), + const PrivacyToggler(), ], ); }, diff --git a/lib/widgets/home/stats/most_spending_category.dart b/lib/widgets/home/stats/most_spending_category.dart index c019c89e..fe9c4635 100644 --- a/lib/widgets/home/stats/most_spending_category.dart +++ b/lib/widgets/home/stats/most_spending_category.dart @@ -103,7 +103,7 @@ class _MostSpendingCategoryState extends State { ], ), ), - DirectionalChevron(), + const LeChevron(), ], ), ), diff --git a/lib/widgets/integrations/eny_page/eny_error_sheet.dart b/lib/widgets/integrations/eny_page/eny_error_sheet.dart index a2acf140..d29dd0e5 100644 --- a/lib/widgets/integrations/eny_page/eny_error_sheet.dart +++ b/lib/widgets/integrations/eny_page/eny_error_sheet.dart @@ -26,7 +26,7 @@ class EnyErrorSheet extends StatelessWidget { } context.push("/preferences/integrations/eny"); }, - trailing: DirectionalChevron(), + trailing: LeChevron(), child: Text( "integrations.eny.invalidCredentials.configure".t(context), ), 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/select_color_scheme_list_tile.dart b/lib/widgets/select_color_scheme_list_tile.dart index b11d9e2f..79b0e066 100644 --- a/lib/widgets/select_color_scheme_list_tile.dart +++ b/lib/widgets/select_color_scheme_list_tile.dart @@ -63,7 +63,7 @@ class _SelectColorSchemeListTileState extends State { ), duration: const Duration(milliseconds: 200), ), - DirectionalChevron(), + const LeChevron(), ], ), ); diff --git a/lib/widgets/setup/icloud_backup_picker_sheet.dart b/lib/widgets/setup/icloud_backup_picker_sheet.dart index f4b1553f..be6880b4 100644 --- a/lib/widgets/setup/icloud_backup_picker_sheet.dart +++ b/lib/widgets/setup/icloud_backup_picker_sheet.dart @@ -41,7 +41,7 @@ class _ICloudBackupPickerSheetState extends State { title: Text(backup.inferredBackupDate!.toMoment().lll), subtitle: Text(path.extension(backup.path).substring(1)), onTap: () => context.pop(backup), - trailing: DirectionalChevron(), + trailing: LeChevron(), ), ) .toList(), diff --git a/lib/widgets/sheets/select_account_type_sheet.dart b/lib/widgets/sheets/select_account_type_sheet.dart index 57fafe0f..4bb55eb2 100644 --- a/lib/widgets/sheets/select_account_type_sheet.dart +++ b/lib/widgets/sheets/select_account_type_sheet.dart @@ -23,7 +23,7 @@ class SelectAccountTypeSheet extends StatelessWidget { (value) => ListTile( title: Text(value.localizedNameContext(context)), selected: currentlySelected == value, - trailing: const DirectionalChevron(), + trailing: const LeChevron(), onTap: () => context.pop(value), ), ) diff --git a/lib/widgets/sheets/select_flow_icon_sheet.dart b/lib/widgets/sheets/select_flow_icon_sheet.dart index 068ce9c9..b1612900 100644 --- a/lib/widgets/sheets/select_flow_icon_sheet.dart +++ b/lib/widgets/sheets/select_flow_icon_sheet.dart @@ -32,19 +32,19 @@ class _SelectFlowIconSheetState extends State children: [ ListTile( leading: const Icon(Symbols.category_rounded), - trailing: DirectionalChevron(), + trailing: const LeChevron(), title: Text("flowIcon.type.icon".t(context)), onTap: () => _selectIcon(), ), ListTile( leading: const Icon(Symbols.glyphs_rounded), - trailing: DirectionalChevron(), + trailing: const LeChevron(), title: Text("flowIcon.type.character".t(context)), onTap: () => _selectEmoji(), ), ListTile( leading: const Icon(Symbols.image_rounded), - trailing: DirectionalChevron(), + trailing: const LeChevron(), title: Text("flowIcon.type.image".t(context)), onTap: () => _selectImage(), ), diff --git a/lib/widgets/sheets/select_time_range_mode_sheet.dart b/lib/widgets/sheets/select_time_range_mode_sheet.dart index 056b8759..01f72ee3 100644 --- a/lib/widgets/sheets/select_time_range_mode_sheet.dart +++ b/lib/widgets/sheets/select_time_range_mode_sheet.dart @@ -109,17 +109,17 @@ class SelectTimeRangeModeSheet extends StatelessWidget { ListTile( title: Text("select.timeRange.mode.byMonth".t(context)), onTap: () => context.pop(TimeRangeMode.byMonth), - trailing: DirectionalChevron(), + trailing: const LeChevron(), ), ListTile( title: Text("select.timeRange.mode.byYear".t(context)), onTap: () => context.pop(TimeRangeMode.byYear), - trailing: DirectionalChevron(), + trailing: const LeChevron(), ), ListTile( title: Text("select.timeRange.mode.custom".t(context)), onTap: () => context.pop(TimeRangeMode.custom), - trailing: DirectionalChevron(), + trailing: const LeChevron(), ), ], ), diff --git a/lib/widgets/sheets/select_transaction_type_sheet.dart b/lib/widgets/sheets/select_transaction_type_sheet.dart index c71c8a1e..0ae5f0c2 100644 --- a/lib/widgets/sheets/select_transaction_type_sheet.dart +++ b/lib/widgets/sheets/select_transaction_type_sheet.dart @@ -34,7 +34,7 @@ class SelectTransactionTypeSheet extends StatelessWidget { (value) => ListTile( title: Text(value.localizedNameContext(context)), selected: currentlySelected == value, - trailing: const DirectionalChevron(), + trailing: const LeChevron(), onTap: () => context.pop(value), ), ) diff --git a/lib/widgets/transaction/imported_from_eny.dart b/lib/widgets/transaction/imported_from_eny.dart new file mode 100644 index 00000000..d396402c --- /dev/null +++ b/lib/widgets/transaction/imported_from_eny.dart @@ -0,0 +1,35 @@ +import "package:flow/constants.dart"; +import "package:flow/l10n/extensions.dart"; +import "package:flow/theme/helpers.dart"; +import "package:flow/utils/utils.dart"; +import "package:flow/widgets/animated_eny_logo.dart"; +import "package:flutter/material.dart"; + +class ImportedFromEny extends StatelessWidget { + const ImportedFromEny({super.key}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + openUrl(enyHomeLink); + }, + child: RichText( + text: TextSpan( + style: context.textTheme.bodyMedium?.semi(context), + children: [ + WidgetSpan( + child: SizedBox.square( + dimension: 16.0, + child: AnimatedEnyLogo(noAnimation: true), + ), + alignment: .middle, + ), + TextSpan(text: " "), + TextSpan(text: "transaction.external.added.from".t(context, "Eny")), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/transaction/imported_from_siri.dart b/lib/widgets/transaction/imported_from_siri.dart new file mode 100644 index 00000000..11f663ab --- /dev/null +++ b/lib/widgets/transaction/imported_from_siri.dart @@ -0,0 +1,27 @@ +import "package:flow/l10n/extensions.dart"; +import "package:flow/theme/helpers.dart"; +import "package:flutter/material.dart"; + +class ImportedFromSiri extends StatelessWidget { + const ImportedFromSiri({super.key}); + + @override + Widget build(BuildContext context) { + return RichText( + text: TextSpan( + style: context.textTheme.bodyMedium?.semi(context), + children: [ + WidgetSpan( + child: SizedBox.square( + dimension: 16.0, + child: Image.asset("assets/images/siri.png"), + ), + alignment: .middle, + ), + TextSpan(text: " "), + TextSpan(text: "transaction.external.from".t(context, "Siri")), + ], + ), + ); + } +} diff --git a/lib/widgets/transaction_list_tile.dart b/lib/widgets/transaction_list_tile.dart index 41f5ce61..0104e489 100644 --- a/lib/widgets/transaction_list_tile.dart +++ b/lib/widgets/transaction_list_tile.dart @@ -1,3 +1,4 @@ +import "package:flow/constants.dart"; import "package:flow/data/flow_icon.dart"; import "package:flow/data/money.dart"; import "package:flow/data/transaction_filter.dart"; @@ -11,6 +12,7 @@ import "package:flow/utils/extensions/transaction.dart"; import "package:flow/widgets/general/directional_slidable.dart"; import "package:flow/widgets/general/flow_icon.dart"; import "package:flow/widgets/general/money_text.dart"; +import "package:flow/widgets/transaction_list_tile/transaction_subtitle.dart"; import "package:flow/widgets/transaction_list_tile_theme.dart"; import "package:flutter/material.dart"; import "package:flutter_slidable/flutter_slidable.dart"; @@ -70,9 +72,7 @@ class TransactionListTile extends StatelessWidget { confirmFn != null && transaction.confirmable(); final bool showDuplicateButton = - transaction.isDeleted != true && - !transaction.isTransfer && - duplicateFn != null; + transaction.isDeleted != true && duplicateFn != null; final bool showHoldButton = confirmFn != null && transaction.holdable(); final bool showConfirmButton = confirmFn != null && transaction.confirmable(); @@ -94,30 +94,50 @@ class TransactionListTile extends StatelessWidget { ? transaction.extensions.transfer : null; - final TextDirection textDirection = Directionality.of(context); - - final List subtitleParts = [ - (transaction.isTransfer && combineTransfers) - ? "${AccountsProvider.of(context).getName(transfer!.fromAccountUuid)} → ${AccountsProvider.of(context).getName(transfer.toAccountUuid)}" - : (AccountsProvider.of(context).getName(transaction.accountUuid) ?? - transaction.account.target?.name), + final List subtitleComponents = [ + TextSpan( + text: (transaction.isTransfer && combineTransfers) + ? "${AccountsProvider.of(context).getName(transfer!.fromAccountUuid)} → ${AccountsProvider.of(context).getName(transfer.toAccountUuid)}" + : (AccountsProvider.of(context).getName(transaction.accountUuid) ?? + transaction.account.target?.name), + ), if (effectiveTheme.showCategoryOrDefault && transaction.category.target != null) - transaction.category.target!.name, - dateString, + TextSpan(text: transaction.category.target!.name), + if (effectiveTheme.showExternalSourceOrDefault) + if (transaction.externalProviderName + case String externalProviderName) ...[ + TextSpan( + children: [ + if (externalProviderName == "Siri") + WidgetSpan( + child: Padding( + padding: .only(right: 4.0), + child: Image.asset("assets/images/siri.png", height: 12.0), + ), + alignment: .middle, + ), + if (externalProviderName == "Eny") + WidgetSpan( + child: Padding( + padding: .only(right: 4.0), + child: Image.network(enyLogoUrl, height: 12.0), + ), + alignment: .middle, + ), + TextSpan(text: externalProviderName), + ], + ), + ], + TextSpan(text: dateString), if (transaction.transactionDate.isFuture) - transaction.isPending == true - ? "transaction.pending".t(context) - : "transaction.pending.preapproved".t(context), + TextSpan( + text: transaction.isPending == true + ? "transaction.pending".t(context) + : "transaction.pending.preapproved".t(context), + ), ]; - final String subtitle = - (textDirection == TextDirection.ltr - ? subtitleParts - : subtitleParts.reversed) - .nonNulls - .join(" • "); - final WidgetSpan? titleLeadingIconSpan = transaction.isRecurring ? titleIconSpan(context, Symbols.repeat_rounded) : (transaction.transactionDate.isFutureAnchored( @@ -166,12 +186,7 @@ class TransactionListTile extends StatelessWidget { maxLines: 3, overflow: TextOverflow.ellipsis, ), - Text( - subtitle, - style: context.textTheme.labelSmall, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), + TransactionSubtitle(components: subtitleComponents), ], ), ), diff --git a/lib/widgets/transaction_list_tile/transaction_subtitle.dart b/lib/widgets/transaction_list_tile/transaction_subtitle.dart new file mode 100644 index 00000000..3599049c --- /dev/null +++ b/lib/widgets/transaction_list_tile/transaction_subtitle.dart @@ -0,0 +1,29 @@ +import "package:flutter/material.dart"; + +class TransactionSubtitle extends StatelessWidget { + final List components; + + const TransactionSubtitle({super.key, required this.components}); + + @override + Widget build(BuildContext context) { + final TextDirection textDirection = Directionality.of(context); + const TextSpan divider = TextSpan(text: " • "); + + final List orderedComponents = [ + for (int i = 0; i < components.length; i++) ...[ + if (i != 0) divider, + components[i], + ], + ]; + + return RichText( + text: TextSpan( + children: textDirection == TextDirection.ltr + ? orderedComponents + : orderedComponents.reversed.toList(), + style: Theme.of(context).textTheme.labelSmall, + ), + ); + } +} diff --git a/lib/widgets/transaction_list_tile_theme.dart b/lib/widgets/transaction_list_tile_theme.dart index 231fe4be..87d62267 100644 --- a/lib/widgets/transaction_list_tile_theme.dart +++ b/lib/widgets/transaction_list_tile_theme.dart @@ -32,6 +32,7 @@ class TransactionListTileThemeData { final bool? useCategoryNameForUntitledTransactions; final bool? useAccountIconForLeading; + final bool? showExternalSource; final bool? showCategory; final double? spacing; @@ -48,6 +49,9 @@ class TransactionListTileThemeData { bool get showCategoryOrDefault => showCategory ?? fallback.showCategory!; + bool get showExternalSourceOrDefault => + showExternalSource ?? fallback.showExternalSource!; + double get spacingOrDefault => spacing ?? fallback.spacing!; double get titleSpacingOrDefault => titleSpacing ?? fallback.titleSpacing!; @@ -56,6 +60,7 @@ class TransactionListTileThemeData { this.padding, this.useCategoryNameForUntitledTransactions, this.useAccountIconForLeading, + this.showExternalSource, this.showCategory, this.spacing, this.titleSpacing, @@ -66,6 +71,7 @@ class TransactionListTileThemeData { padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0), useCategoryNameForUntitledTransactions: true, useAccountIconForLeading: false, + showExternalSource: true, showCategory: false, spacing: 8.0, titleSpacing: 0.0, @@ -79,6 +85,7 @@ class TransactionListTileThemeData { useCategoryNameForUntitledTransactions, useAccountIconForLeading: other?.useAccountIconForLeading ?? useAccountIconForLeading, + showExternalSource: other?.showExternalSource ?? showExternalSource, showCategory: other?.showCategory ?? showCategory, spacing: other?.spacing ?? spacing, titleSpacing: other?.titleSpacing ?? titleSpacing, @@ -88,6 +95,7 @@ class TransactionListTileThemeData { EdgeInsetsGeometry? padding, bool? useCategoryNameForUntitledTransactions, bool? useAccountIconForLeading, + bool? showExternalSource, bool? showCategory, double? spacing, double? titleSpacing, @@ -99,6 +107,7 @@ class TransactionListTileThemeData { this.useCategoryNameForUntitledTransactions, useAccountIconForLeading: useAccountIconForLeading ?? this.useAccountIconForLeading, + showExternalSource: showExternalSource ?? this.showExternalSource, showCategory: showCategory ?? this.showCategory, spacing: spacing ?? this.spacing, titleSpacing: titleSpacing ?? this.titleSpacing, @@ -113,6 +122,7 @@ class TransactionListTileThemeData { useCategoryNameForUntitledTransactions == other.useCategoryNameForUntitledTransactions && useAccountIconForLeading == other.useAccountIconForLeading && + showExternalSource == other.showExternalSource && showCategory == other.showCategory && spacing == other.spacing && titleSpacing == other.titleSpacing; @@ -124,6 +134,7 @@ class TransactionListTileThemeData { padding, useCategoryNameForUntitledTransactions, useAccountIconForLeading, + showExternalSource, showCategory, spacing, titleSpacing, diff --git a/lib/widgets/transaction_tag_add_chip.dart b/lib/widgets/transaction_tag_add_chip.dart index d302eb87..f409028b 100644 --- a/lib/widgets/transaction_tag_add_chip.dart +++ b/lib/widgets/transaction_tag_add_chip.dart @@ -1,22 +1,27 @@ import "package:flow/data/flow_icon.dart"; +import "package:flow/entity/transaction_tag.dart"; import "package:flow/l10n/flow_localizations.dart"; -import "package:flow/widgets/general/flow_icon.dart"; +import "package:flow/widgets/transaction_tag_chip.dart"; import "package:flutter/material.dart"; import "package:material_symbols_icons/symbols.dart"; class TransactionTagAddChip extends StatelessWidget { + final String? title; + final VoidCallback? onPressed; - const TransactionTagAddChip({super.key, this.onPressed}); + const TransactionTagAddChip({super.key, this.onPressed, this.title}); @override Widget build(BuildContext context) { - return InputChip( - label: Text("transaction.tags.new".t(context)), + return TransactionTagChip( + tag: TransactionTag( + title: title ?? "transaction.tags.new".t(context), + iconCode: FlowIconData.icon(Symbols.add_rounded).toString(), + ), selected: false, + isSuggestion: false, onPressed: onPressed ?? () {}, - showCheckmark: false, - avatar: FlowIcon(FlowIconData.icon(Symbols.add_rounded), size: 16.0), ); } } diff --git a/lib/widgets/transaction_tag_chip.dart b/lib/widgets/transaction_tag_chip.dart index 5ad6dbdf..cd3de39b 100644 --- a/lib/widgets/transaction_tag_chip.dart +++ b/lib/widgets/transaction_tag_chip.dart @@ -1,18 +1,26 @@ +import "dart:math"; + +import "package:dashed_border/dashed_border.dart"; import "package:flow/entity/transaction_tag.dart"; import "package:flow/theme/flow_color_scheme.dart"; +import "package:flow/theme/theme.dart"; import "package:flow/widgets/general/flow_icon.dart"; import "package:flutter/material.dart"; +import "package:material_symbols_icons/material_symbols_icons.dart"; class TransactionTagChip extends StatelessWidget { final TransactionTag tag; final bool selected; + final bool isSuggestion; + final VoidCallback? onPressed; const TransactionTagChip({ super.key, required this.tag, this.selected = false, + this.isSuggestion = false, this.onPressed, }); @@ -20,12 +28,52 @@ class TransactionTagChip extends StatelessWidget { Widget build(BuildContext context) { final FlowColorScheme? colorScheme = tag.colorScheme; - return InputChip( - label: Text(tag.title), - selected: selected, - onPressed: onPressed ?? () {}, - showCheckmark: false, - avatar: FlowIcon(tag.icon, colorScheme: colorScheme, size: 16.0), + final Color borderColor = selected + ? kTransparent + : context.colorScheme.outline.withAlpha(0x80); + + final BorderRadius borderRadius = .circular(8.0); + + final Widget child = GestureDetector( + onTap: onPressed, + behavior: onPressed != null ? .opaque : .translucent, + child: Container( + margin: .symmetric(vertical: 4.0), + decoration: BoxDecoration( + color: selected + ? context.colorScheme.secondary + : context.colorScheme.surface, + borderRadius: borderRadius, + border: isSuggestion + ? DashedBorder( + color: borderColor, + width: 1.0, + borderRadius: borderRadius, + dashLength: 4.0, + dashGap: 4.0, + ) + : Border.all(color: borderColor, width: 1.0), + ), + padding: EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + child: Row( + spacing: 8.0, + mainAxisSize: .min, + children: [ + FlowIcon(tag.icon, colorScheme: colorScheme, size: 16.0), + Text(tag.title, style: context.textTheme.labelLarge), + ], + ), + ), ); + + if (isSuggestion) { + return Badge( + label: Icon(Symbols.star_rounded, color: context.colorScheme.onPrimary), + backgroundColor: context.colorScheme.primary, + child: Opacity(opacity: .7, child: child), + ); + } + + return child; } } 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/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index c6217fb9..b7f06a0d 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -11,6 +11,7 @@ import desktop_drop import file_picker import file_saver import file_selector_macos +import flutter_app_group_directory import flutter_local_notifications import flutter_timezone import geolocator_apple @@ -35,6 +36,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + FlutterAppGroupDirectoryPlugin.register(with: registry.registrar(forPlugin: "FlutterAppGroupDirectoryPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) FlutterTimezonePlugin.register(with: registry.registrar(forPlugin: "FlutterTimezonePlugin")) GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) 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.lock b/pubspec.lock index 713ed4b6..ff0af732 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -369,6 +369,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.3" + dashed_border: + dependency: "direct main" + description: + name: dashed_border + sha256: "5138b2a05da270238aedc6c6decddf9c1d151850d1857023ab8cb51c4a2b434f" + url: "https://pub.dev" + source: hosted + version: "1.0.2" dbus: dependency: transitive description: @@ -409,14 +417,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" - dotted_border: - dependency: "direct main" - description: - name: dotted_border - sha256: "99b091ec6891ba0c5331fdc2b502993c7c108f898995739a73c6845d71dad70c" - url: "https://pub.dev" - source: hosted - version: "3.1.0" equatable: dependency: transitive description: @@ -526,6 +526,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_app_group_directory: + dependency: "direct main" + description: + name: flutter_app_group_directory + sha256: "680ef9b2dee84c237cd7bb7fc78bc45867b32556a8a5f0de61278078b9fefd05" + url: "https://pub.dev" + source: hosted + version: "1.1.0" flutter_colorpicker: dependency: transitive description: @@ -610,10 +618,10 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: "76cd20bcfa72fabe50ea27eeaf165527f446f55d3033021462084b87805b4cac" + sha256: "2b50e938a275e1ad77352d6a25e25770f4130baa61eaf02de7a9a884680954ad" url: "https://pub.dev" source: hosted - version: "20.0.0" + version: "20.1.0" flutter_local_notifications_linux: dependency: transitive description: @@ -634,10 +642,10 @@ packages: dependency: transitive description: name: flutter_local_notifications_windows - sha256: "7ddd964fa85b6a23e96956c5b63ef55cdb9e5947b71b95712204db42ad46da61" + sha256: e97a1a3016512437d9c0b12fae7d1491c3c7b9aa7f03a69b974308840656b02a url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.1" flutter_localizations: dependency: "direct main" description: flutter @@ -1225,10 +1233,10 @@ packages: dependency: "direct main" description: name: moment_dart - sha256: "04599f4f21a577debfd666249b73f91c89b2e11c864b857c2ef63cada0a84ddb" + sha256: c868448a4813e57d07a2a236ea010a4960311895b1166aeaf7a8e0750c2cb0dd url: "https://pub.dev" source: hosted - version: "5.3.0+1" + version: "5.3.1" nm: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 99f2e122..b2c194bf 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.18.2+317" +version: "0.19.0+326" environment: sdk: ">=3.10.0 <4.0.0" @@ -19,15 +19,16 @@ dependencies: cross_file: ^0.3.5+2 csv: ^6.0.0 desktop_drop: ^0.7.0 - dotted_border: ^3.1.0 + dashed_border: ^1.0.2 file_picker: ^10.3.10 file_saver: ^0.3.1 fl_chart: ^1.1.1 flutter: sdk: flutter + flutter_app_group_directory: ^1.1.0 flutter_contacts: ^1.1.9+2 flutter_dynamic_icon_plus: ^1.4.0 - flutter_local_notifications: ^20.0.0 + flutter_local_notifications: ^20.1.0 flutter_localizations: sdk: flutter flutter_map: ^8.2.2 @@ -55,7 +56,7 @@ dependencies: markdown: ^7.3.0 markdown_quill: ^4.3.0 material_symbols_icons: ^4.2906.0 - moment_dart: ^5.3.0+1 + moment_dart: ^5.3.1 objectbox: ^5.1.0 objectbox_flutter_libs: ^5.1.0 open_app_file: ^4.0.4 diff --git a/scripts/translate_missing.dart b/scripts/translate_missing.dart index bbfa7b98..9a434012 100644 --- a/scripts/translate_missing.dart +++ b/scripts/translate_missing.dart @@ -25,7 +25,7 @@ Future?> translate( messages: [ ChatCompletionMessage.developer( content: ChatCompletionDeveloperMessageContent.text( - "You are a professional translator with expertise in user interface design. You will be provided a JSON localization file in english, and you should translate the file into a localization json in $targetLanguage language.", + "You are a professional translator with expertise in user interface design. You will be provided a JSON localization file in english, and you should translate the file into a localization json in $targetLanguage language. If there's no input, just output nothing. The output should be a valid JSON object with the same keys as the input, but with the values translated to $targetLanguage. Do not include any explanations or additional text, just the JSON object.", ), ), ChatCompletionMessage.user( @@ -53,6 +53,7 @@ final Map filenameToTargetLanguageMapping = { "en.json": "English (generic)", "es_ES.json": "Spanish (Spain)", "fr_FR.json": "French (France)", + "fa_IR.json": "Persian (Iran)", "it_IT.json": "Italian (Italy)", "mn_MN.json": "Mongolian (Mongolia)", "ru_RU.json": "Russian (Russia)",