diff --git a/CHANGELOG.md b/CHANGELOG.md index 23745ef..0931268 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## Beta 0.8.0 + +* Fixed account card "this month" summary +* Added new theme selector +* Enhanced searching +* Added icons for each color (iOS exclusive) +* Added total balance in the accounts tab +* Added income/expense report in the home tab +* Swapped icon for income/expense buttons (oof) + ## Beta 0.7.2 * Added themes, closes [#105](https://github.com/flow-mn/flow/issues/105) diff --git a/android/app/build.gradle b/android/app/build.gradle index 98d4902..8a560fd 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -84,5 +84,5 @@ configurations { } dependencies { - debugImplementation("io.objectbox:objectbox-android-objectbrowser:4.0.1") + debugImplementation("io.objectbox:objectbox-android-objectbrowser:4.0.3") } diff --git a/assets/l10n/en_IN.json b/assets/l10n/en_IN.json index a8c262a..3dec72e 100644 --- a/assets/l10n/en_IN.json +++ b/assets/l10n/en_IN.json @@ -82,6 +82,7 @@ "account.delete": "Delete account", "account.delete.warning": "Deleting this account will also delete {transactionCount} transactions associated. This action is irreversible!", "account.noAccounts": "You don't have any accounts!", + "account.thisMonth": "This month", "transaction": "Transaction", "transaction.new": "New transaction", @@ -150,6 +151,9 @@ "preferences.theme.choose": "Select a theme", "preferences.theme.light": "Light", "preferences.theme.dark": "Dark", + "preferences.theme.other": "Other themes", + "preferences.theme.themeChangesAppIcon": "App icon follows theme", + "preferences.theme.enableDynamicTheme": "Dynamic theme", "preferences.numpad": "Numpad", "preferences.numpad.layout": "Numpad layout", "preferences.numpad.layout.classic": "Classic", @@ -182,15 +186,15 @@ "tabs.home": "Home", "tabs.home.greetings": "Hi, {name}!", - "tabs.home.noTransactions.allTime": "You don't have any transactions", - "tabs.home.noTransactions.last7Days": "No transactions for the last 7 days", + "tabs.home.noTransactions": "No transactions matching the criteria", "tabs.home.noTransactions.addSome": "Click on (+) button below to add a new transaction", + "tabs.home.noTransactions.tryChangingFilters": "Try changing the filters", "tabs.home.upcomingTransactions": "Upcoming ({count})", "tabs.home.upcomingTransactions.seeAll": "See all", "tabs.home.transactionsCount": "{count} transactions", "tabs.home.last7days": "Last 7 days", "tabs.home.totalBalance": "Total balance", - "tabs.home.flowToday": "Flow today", + "tabs.home.flow": "Flow", "tabs.stats": "Stats", "tabs.stats.timeRange.select": "Select range", @@ -349,5 +353,7 @@ "error.sync.exportFailed": "Unable to export, please contact developer.", "error.sync.fileDeleteFailed": "An error occured during backup deletion", "error.transaction.missingAccount": "Please select an account", - "error.url.cannotOpen": "Can't open the link" + "error.url.cannotOpen": "Can't open the link", + "error.exchangeRates.inaccurateDataDueToMissingRates": "Failed to fetch exchange rates, transaction data might not be fully accurate", + "error.exchangeRates.cannotFetch": "Failed to fetch, please check your internet connection." } diff --git a/assets/l10n/en_US.json b/assets/l10n/en_US.json index a8c262a..3dec72e 100644 --- a/assets/l10n/en_US.json +++ b/assets/l10n/en_US.json @@ -82,6 +82,7 @@ "account.delete": "Delete account", "account.delete.warning": "Deleting this account will also delete {transactionCount} transactions associated. This action is irreversible!", "account.noAccounts": "You don't have any accounts!", + "account.thisMonth": "This month", "transaction": "Transaction", "transaction.new": "New transaction", @@ -150,6 +151,9 @@ "preferences.theme.choose": "Select a theme", "preferences.theme.light": "Light", "preferences.theme.dark": "Dark", + "preferences.theme.other": "Other themes", + "preferences.theme.themeChangesAppIcon": "App icon follows theme", + "preferences.theme.enableDynamicTheme": "Dynamic theme", "preferences.numpad": "Numpad", "preferences.numpad.layout": "Numpad layout", "preferences.numpad.layout.classic": "Classic", @@ -182,15 +186,15 @@ "tabs.home": "Home", "tabs.home.greetings": "Hi, {name}!", - "tabs.home.noTransactions.allTime": "You don't have any transactions", - "tabs.home.noTransactions.last7Days": "No transactions for the last 7 days", + "tabs.home.noTransactions": "No transactions matching the criteria", "tabs.home.noTransactions.addSome": "Click on (+) button below to add a new transaction", + "tabs.home.noTransactions.tryChangingFilters": "Try changing the filters", "tabs.home.upcomingTransactions": "Upcoming ({count})", "tabs.home.upcomingTransactions.seeAll": "See all", "tabs.home.transactionsCount": "{count} transactions", "tabs.home.last7days": "Last 7 days", "tabs.home.totalBalance": "Total balance", - "tabs.home.flowToday": "Flow today", + "tabs.home.flow": "Flow", "tabs.stats": "Stats", "tabs.stats.timeRange.select": "Select range", @@ -349,5 +353,7 @@ "error.sync.exportFailed": "Unable to export, please contact developer.", "error.sync.fileDeleteFailed": "An error occured during backup deletion", "error.transaction.missingAccount": "Please select an account", - "error.url.cannotOpen": "Can't open the link" + "error.url.cannotOpen": "Can't open the link", + "error.exchangeRates.inaccurateDataDueToMissingRates": "Failed to fetch exchange rates, transaction data might not be fully accurate", + "error.exchangeRates.cannotFetch": "Failed to fetch, please check your internet connection." } diff --git a/assets/l10n/it_IT.json b/assets/l10n/it_IT.json index e84b6af..9c41a5c 100644 --- a/assets/l10n/it_IT.json +++ b/assets/l10n/it_IT.json @@ -82,6 +82,7 @@ "account.delete": "Elimina conto", "account.delete.warning": "Eliminando questo conto verranno eliminati anche {transactionCount} transazioni associate. Questa azione è irreversibile!", "account.noAccounts": "Non hai nessun conto!", + "account.thisMonth": "Questo mese", "transaction": "Transazione", "transaction.new": "Nuova transazione", @@ -150,6 +151,9 @@ "preferences.theme.choose": "Seleziona un tema", "preferences.theme.light": "Chiaro", "preferences.theme.dark": "Scuro", + "preferences.theme.other": "Altri temi", + "preferences.theme.themeChangesAppIcon": "Icona dell'app segue il tema", + "preferences.theme.enableDynamicTheme": "Tema dinamico", "preferences.numpad": "Tastierino numerico", "preferences.numpad.layout": "Layout del tastierino numerico", "preferences.numpad.layout.classic": "Classico", @@ -182,15 +186,15 @@ "tabs.home": "Home", "tabs.home.greetings": "Ciao, {name}!", - "tabs.home.noTransactions.allTime": "Non hai nessuna transazione", - "tabs.home.noTransactions.last7Days": "Nessuna transazione negli ultimi 7 giorni", + "tabs.home.noTransactions": "Nessuna transazione corrisponde ai criteri di ricerca", "tabs.home.noTransactions.addSome": "Clicca sul pulsante (+) qui sotto per aggiungere una nuova transazione", + "tabs.home.noTransactions.tryChangingFilters": "Prova a modificare i filtri", "tabs.home.upcomingTransactions": "Prossimi ({count})", "tabs.home.upcomingTransactions.seeAll": "Vedi tutto", "tabs.home.transactionsCount": "{count} transazioni", "tabs.home.last7days": "Ultimi 7 giorni", "tabs.home.totalBalance": "Saldo totale", - "tabs.home.flowToday": "Movimenti di oggi", + "tabs.home.flow": "Flusso", "tabs.stats": "Statistiche", "tabs.stats.timeRange.select": "Seleziona intervallo", @@ -349,5 +353,7 @@ "error.sync.exportFailed": "Impossibile esportare, si prega di contattare lo sviluppatore.", "error.sync.fileDeleteFailed": "Impossibile eliminare il file", "error.transaction.missingAccount": "Selezionare un account", - "error.url.cannotOpen": "Impossibile aprire il collegamento" + "error.url.cannotOpen": "Impossibile aprire il collegamento", + "error.exchangeRates.inaccurateDataDueToMissingRates": "Impossibile recuperare i tassi di cambio, i dati delle transazioni potrebbero non essere completamente accurati.", + "error.exchangeRates.cannotFetch": "Impossibile recuperare i dati, si prega di verificare la connessione internet." } diff --git a/assets/l10n/mn_MN.json b/assets/l10n/mn_MN.json index be7312e..fe20b29 100644 --- a/assets/l10n/mn_MN.json +++ b/assets/l10n/mn_MN.json @@ -82,6 +82,7 @@ "account.delete": "Дансыг устгах", "account.delete.warning": "Энэ дансыг устгавал холбоотой {transactionCount} гүйлгээг хамт устгах болно. Энэ үйлдлийг буцаах боломжгүй юм!", "account.noAccounts": "Танд үүсгэсэн данс алга байна!", + "account.thisMonth": "Энэ сар", "transaction": "Гүйлгээ", "transaction.new": "Гүйлгээ нэмэх", @@ -150,6 +151,9 @@ "preferences.theme.choose": "Үзэмж сонгох", "preferences.theme.light": "Гэгээлэг", "preferences.theme.dark": "Харанхуй", + "preferences.theme.other": "Бусад үзэмжүүд", + "preferences.theme.themeChangesAppIcon": "Аппын дүрсийг үзэмж дагах", + "preferences.theme.enableDynamicTheme": "Динамик үзэмж", "preferences.numpad": "Тоон товчлуур", "preferences.numpad.layout": "Тооны байрлал", "preferences.numpad.layout.classic": "Хуучны", @@ -182,15 +186,15 @@ "tabs.home": "Нүүр", "tabs.home.greetings": "Сайн уу, {name}?", - "tabs.home.noTransactions.allTime": "Танд одоогоор гүйлгээ алга байна", - "tabs.home.noTransactions.last7Days": "Сүүлийн долоо хоногт хийгдсэн гүйлгээ алга байна", + "tabs.home.noTransactions": "Шүүлтүүрт тохирох гүйлгээ олдсонгүй", "tabs.home.noTransactions.addSome": "Доор байрлах (+) товч дээр дарж гүйлгээ нэмээрэй", + "tabs.home.noTransactions.tryChangingFilters": "Шүүлтүүрийг өөрчлөөд үзээрэй", "tabs.home.upcomingTransactions": "Төлөвлөсөн ({count})", "tabs.home.upcomingTransactions.seeAll": "Бүгд", "tabs.home.transactionsCount": "{count} гүйлгээ", "tabs.home.last7days": "Сүүлийн 7 хоног", "tabs.home.totalBalance": "Нийт үлдэгдэл", - "tabs.home.flowToday": "Өнөөдөр", + "tabs.home.flow": "Урсгал", "tabs.stats": "Тоо", "tabs.stats.timeRange.select": "Хугацаа сонгох", @@ -349,5 +353,7 @@ "error.sync.exportFailed": "Нөөцлөх явцад алдаа гарлаа, хөгжүүлэгчид хандана уу.", "error.sync.fileDeleteFailed": "Нөөц устгах үед алдаа гарлаа", "error.transaction.missingAccount": "Гүйлгээ хийх данс сонгоно уу", - "error.url.cannotOpen": "Холбоосыг нээхэд алдаа гарлаа" + "error.url.cannotOpen": "Холбоосыг нээхэд алдаа гарлаа", + "error.exchangeRates.inaccurateDataDueToMissingRates": "Ханшийн мэдээлэл татахад асуудал гарсан тул гүйлгээний мэдээлэл зөрөх магадлалтай", + "error.exchangeRates.cannotFetch": "Мэдээлэл татахад алдаа гарлаа. Холболтоо шалгаад дахин оролдоно уу." } diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 2cf6198..ffac54e 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 70; objects = { /* Begin PBXBuildFile section */ @@ -67,6 +67,10 @@ FE95B23EAA62B849DC8F3352 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 2F7D63EA2CD8F63D00B8BE47 /* LauncherIcons */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = LauncherIcons; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -130,6 +134,7 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + 2F7D63EA2CD8F63D00B8BE47 /* LauncherIcons */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, @@ -203,6 +208,9 @@ ); dependencies = ( ); + fileSystemSynchronizedGroups = ( + 2F7D63EA2CD8F63D00B8BE47 /* LauncherIcons */, + ); name = Runner; productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; @@ -225,9 +233,7 @@ }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; - DevelopmentTeam = NJH37247C9; LastSwiftMigration = 1100; - ProvisioningStyle = Manual; }; }; }; @@ -472,8 +478,8 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = "Apple Distribution: Batmend Ganbaatar (NJH37247C9)"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = NJH37247C9; ENABLE_BITCODE = NO; @@ -484,7 +490,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = mn.flow.flow; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = "Flow - Personal finance management app ios_app_store 1707567126"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -664,8 +670,8 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = "Apple Distribution: Batmend Ganbaatar (NJH37247C9)"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = NJH37247C9; ENABLE_BITCODE = NO; @@ -676,7 +682,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = mn.flow.flow; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = "Flow - Personal finance management app ios_app_store 1707567126"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -690,8 +696,8 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = "Apple Distribution: Batmend Ganbaatar (NJH37247C9)"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = NJH37247C9; ENABLE_BITCODE = NO; @@ -702,7 +708,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = mn.flow.flow; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = "Flow - Personal finance management app ios_app_store 1707567126"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index e491c03..806c881 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -1,65 +1,399 @@ - - CFBundleLocalizations - - en - mn - it - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Flow - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - flow - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - CADisableMinimumFrameDurationOnPhone - - NSLocationWhenInUseUsageDescription - Location is used if you choose to auto-attach your current location to your transactions. - UIApplicationSupportsIndirectInputEvents - - LSSupportsOpeningDocumentsInPlace - - ITSAppUsesNonExemptEncryption - - UIFileSharingEnabled - - NSPhotoLibraryUsageDescription - Flow uses the photo library when user updates their picture, or chooses to use image as category/account icon - - + + CFBundleLocalizations + + en + mn + it + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Flow + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + flow + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + NSLocationWhenInUseUsageDescription + Location is used if you choose to auto-attach your current location to your transactions. + UIApplicationSupportsIndirectInputEvents + + LSSupportsOpeningDocumentsInPlace + + ITSAppUsesNonExemptEncryption + + UIFileSharingEnabled + + NSPhotoLibraryUsageDescription + Flow uses the photo library when user updates their picture, or chooses to use image as category/account icon + CFBundleIcons + + 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 + + CFBundleIconFiles + + shadeOfViolet + + UIPrerenderedIcon + + + + CFBundleIcons~ipad + + 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 + + CFBundleIconFiles + + shadeOfViolet + + UIPrerenderedIcon + + + + + \ No newline at end of file diff --git a/ios/Runner/LauncherIcons/blissfulBerry-ipad-pro@2x.png b/ios/Runner/LauncherIcons/blissfulBerry-ipad-pro@2x.png new file mode 100644 index 0000000..1f5e9ab Binary files /dev/null and b/ios/Runner/LauncherIcons/blissfulBerry-ipad-pro@2x.png differ diff --git a/ios/Runner/LauncherIcons/blissfulBerry-ipad@2x.png b/ios/Runner/LauncherIcons/blissfulBerry-ipad@2x.png new file mode 100644 index 0000000..28bc20c Binary files /dev/null and b/ios/Runner/LauncherIcons/blissfulBerry-ipad@2x.png differ diff --git a/ios/Runner/LauncherIcons/blissfulBerry@2x.png b/ios/Runner/LauncherIcons/blissfulBerry@2x.png new file mode 100644 index 0000000..ce4b35f Binary files /dev/null and b/ios/Runner/LauncherIcons/blissfulBerry@2x.png differ diff --git a/ios/Runner/LauncherIcons/blissfulBerry@3x.png b/ios/Runner/LauncherIcons/blissfulBerry@3x.png new file mode 100644 index 0000000..26cb4c9 Binary files /dev/null and b/ios/Runner/LauncherIcons/blissfulBerry@3x.png differ diff --git a/ios/Runner/LauncherIcons/bohemianBlue-ipad-pro@2x.png b/ios/Runner/LauncherIcons/bohemianBlue-ipad-pro@2x.png new file mode 100644 index 0000000..8431f54 Binary files /dev/null and b/ios/Runner/LauncherIcons/bohemianBlue-ipad-pro@2x.png differ diff --git a/ios/Runner/LauncherIcons/bohemianBlue-ipad@2x.png b/ios/Runner/LauncherIcons/bohemianBlue-ipad@2x.png new file mode 100644 index 0000000..124224f Binary files /dev/null and b/ios/Runner/LauncherIcons/bohemianBlue-ipad@2x.png differ diff --git a/ios/Runner/LauncherIcons/bohemianBlue@2x.png b/ios/Runner/LauncherIcons/bohemianBlue@2x.png new file mode 100644 index 0000000..f25b29a Binary files /dev/null and b/ios/Runner/LauncherIcons/bohemianBlue@2x.png differ diff --git a/ios/Runner/LauncherIcons/bohemianBlue@3x.png b/ios/Runner/LauncherIcons/bohemianBlue@3x.png new file mode 100644 index 0000000..079477e Binary files /dev/null and b/ios/Runner/LauncherIcons/bohemianBlue@3x.png differ diff --git a/ios/Runner/LauncherIcons/burntSienna-ipad-pro@2x.png b/ios/Runner/LauncherIcons/burntSienna-ipad-pro@2x.png new file mode 100644 index 0000000..0bd8ed0 Binary files /dev/null and b/ios/Runner/LauncherIcons/burntSienna-ipad-pro@2x.png differ diff --git a/ios/Runner/LauncherIcons/burntSienna-ipad@2x.png b/ios/Runner/LauncherIcons/burntSienna-ipad@2x.png new file mode 100644 index 0000000..c180286 Binary files /dev/null and b/ios/Runner/LauncherIcons/burntSienna-ipad@2x.png differ diff --git a/ios/Runner/LauncherIcons/burntSienna@2x.png b/ios/Runner/LauncherIcons/burntSienna@2x.png new file mode 100644 index 0000000..0859328 Binary files /dev/null and b/ios/Runner/LauncherIcons/burntSienna@2x.png differ diff --git a/ios/Runner/LauncherIcons/burntSienna@3x.png b/ios/Runner/LauncherIcons/burntSienna@3x.png new file mode 100644 index 0000000..11f01fd Binary files /dev/null and b/ios/Runner/LauncherIcons/burntSienna@3x.png differ diff --git a/ios/Runner/LauncherIcons/cherryPlum-ipad-pro@2x.png b/ios/Runner/LauncherIcons/cherryPlum-ipad-pro@2x.png new file mode 100644 index 0000000..13fdaba Binary files /dev/null and b/ios/Runner/LauncherIcons/cherryPlum-ipad-pro@2x.png differ diff --git a/ios/Runner/LauncherIcons/cherryPlum-ipad@2x.png b/ios/Runner/LauncherIcons/cherryPlum-ipad@2x.png new file mode 100644 index 0000000..7f0736a Binary files /dev/null and b/ios/Runner/LauncherIcons/cherryPlum-ipad@2x.png differ diff --git a/ios/Runner/LauncherIcons/cherryPlum@2x.png b/ios/Runner/LauncherIcons/cherryPlum@2x.png new file mode 100644 index 0000000..e235ae2 Binary files /dev/null and b/ios/Runner/LauncherIcons/cherryPlum@2x.png differ diff --git a/ios/Runner/LauncherIcons/cherryPlum@3x.png b/ios/Runner/LauncherIcons/cherryPlum@3x.png new file mode 100644 index 0000000..56e6757 Binary files /dev/null and b/ios/Runner/LauncherIcons/cherryPlum@3x.png differ diff --git a/ios/Runner/LauncherIcons/crispChristmasCranberries-ipad-pro@2x.png b/ios/Runner/LauncherIcons/crispChristmasCranberries-ipad-pro@2x.png new file mode 100644 index 0000000..0662350 Binary files /dev/null and b/ios/Runner/LauncherIcons/crispChristmasCranberries-ipad-pro@2x.png differ diff --git a/ios/Runner/LauncherIcons/crispChristmasCranberries-ipad@2x.png b/ios/Runner/LauncherIcons/crispChristmasCranberries-ipad@2x.png new file mode 100644 index 0000000..22a96e7 Binary files /dev/null and b/ios/Runner/LauncherIcons/crispChristmasCranberries-ipad@2x.png differ diff --git a/ios/Runner/LauncherIcons/crispChristmasCranberries@2x.png b/ios/Runner/LauncherIcons/crispChristmasCranberries@2x.png new file mode 100644 index 0000000..bc7a213 Binary files /dev/null and b/ios/Runner/LauncherIcons/crispChristmasCranberries@2x.png differ diff --git a/ios/Runner/LauncherIcons/crispChristmasCranberries@3x.png b/ios/Runner/LauncherIcons/crispChristmasCranberries@3x.png new file mode 100644 index 0000000..e42ed58 Binary files /dev/null and b/ios/Runner/LauncherIcons/crispChristmasCranberries@3x.png differ diff --git a/ios/Runner/LauncherIcons/egyptianBlue-ipad-pro@2x.png b/ios/Runner/LauncherIcons/egyptianBlue-ipad-pro@2x.png new file mode 100644 index 0000000..99058f6 Binary files /dev/null and b/ios/Runner/LauncherIcons/egyptianBlue-ipad-pro@2x.png differ diff --git a/ios/Runner/LauncherIcons/egyptianBlue-ipad@2x.png b/ios/Runner/LauncherIcons/egyptianBlue-ipad@2x.png new file mode 100644 index 0000000..d68f43c Binary files /dev/null and b/ios/Runner/LauncherIcons/egyptianBlue-ipad@2x.png differ diff --git a/ios/Runner/LauncherIcons/egyptianBlue@2x.png b/ios/Runner/LauncherIcons/egyptianBlue@2x.png new file mode 100644 index 0000000..55f959c Binary files /dev/null and b/ios/Runner/LauncherIcons/egyptianBlue@2x.png differ diff --git a/ios/Runner/LauncherIcons/egyptianBlue@3x.png b/ios/Runner/LauncherIcons/egyptianBlue@3x.png new file mode 100644 index 0000000..931c634 Binary files /dev/null and b/ios/Runner/LauncherIcons/egyptianBlue@3x.png differ diff --git a/ios/Runner/LauncherIcons/flagGreen-ipad-pro@2x.png b/ios/Runner/LauncherIcons/flagGreen-ipad-pro@2x.png new file mode 100644 index 0000000..e35a87f Binary files /dev/null and b/ios/Runner/LauncherIcons/flagGreen-ipad-pro@2x.png differ diff --git a/ios/Runner/LauncherIcons/flagGreen-ipad@2x.png b/ios/Runner/LauncherIcons/flagGreen-ipad@2x.png new file mode 100644 index 0000000..8e70f5e Binary files /dev/null and b/ios/Runner/LauncherIcons/flagGreen-ipad@2x.png differ diff --git a/ios/Runner/LauncherIcons/flagGreen@2x.png b/ios/Runner/LauncherIcons/flagGreen@2x.png new file mode 100644 index 0000000..1c9ea29 Binary files /dev/null and b/ios/Runner/LauncherIcons/flagGreen@2x.png differ diff --git a/ios/Runner/LauncherIcons/flagGreen@3x.png b/ios/Runner/LauncherIcons/flagGreen@3x.png new file mode 100644 index 0000000..2518146 Binary files /dev/null and b/ios/Runner/LauncherIcons/flagGreen@3x.png differ diff --git a/ios/Runner/LauncherIcons/hydraTurquoise-ipad-pro@2x.png b/ios/Runner/LauncherIcons/hydraTurquoise-ipad-pro@2x.png new file mode 100644 index 0000000..75317b7 Binary files /dev/null and b/ios/Runner/LauncherIcons/hydraTurquoise-ipad-pro@2x.png differ diff --git a/ios/Runner/LauncherIcons/hydraTurquoise-ipad@2x.png b/ios/Runner/LauncherIcons/hydraTurquoise-ipad@2x.png new file mode 100644 index 0000000..0e92387 Binary files /dev/null and b/ios/Runner/LauncherIcons/hydraTurquoise-ipad@2x.png differ diff --git a/ios/Runner/LauncherIcons/hydraTurquoise@2x.png b/ios/Runner/LauncherIcons/hydraTurquoise@2x.png new file mode 100644 index 0000000..53e4c43 Binary files /dev/null and b/ios/Runner/LauncherIcons/hydraTurquoise@2x.png differ diff --git a/ios/Runner/LauncherIcons/hydraTurquoise@3x.png b/ios/Runner/LauncherIcons/hydraTurquoise@3x.png new file mode 100644 index 0000000..b000c56 Binary files /dev/null and b/ios/Runner/LauncherIcons/hydraTurquoise@3x.png differ diff --git a/ios/Runner/LauncherIcons/peacockBlue-ipad-pro@2x.png b/ios/Runner/LauncherIcons/peacockBlue-ipad-pro@2x.png new file mode 100644 index 0000000..3bcc7e0 Binary files /dev/null and b/ios/Runner/LauncherIcons/peacockBlue-ipad-pro@2x.png differ diff --git a/ios/Runner/LauncherIcons/peacockBlue-ipad@2x.png b/ios/Runner/LauncherIcons/peacockBlue-ipad@2x.png new file mode 100644 index 0000000..7392c82 Binary files /dev/null and b/ios/Runner/LauncherIcons/peacockBlue-ipad@2x.png differ diff --git a/ios/Runner/LauncherIcons/peacockBlue@2x.png b/ios/Runner/LauncherIcons/peacockBlue@2x.png new file mode 100644 index 0000000..a888293 Binary files /dev/null and b/ios/Runner/LauncherIcons/peacockBlue@2x.png differ diff --git a/ios/Runner/LauncherIcons/peacockBlue@3x.png b/ios/Runner/LauncherIcons/peacockBlue@3x.png new file mode 100644 index 0000000..f3da800 Binary files /dev/null and b/ios/Runner/LauncherIcons/peacockBlue@3x.png differ diff --git a/ios/Runner/LauncherIcons/shadeOfViolet-ipad-pro@2x.png b/ios/Runner/LauncherIcons/shadeOfViolet-ipad-pro@2x.png new file mode 100644 index 0000000..335ffd7 Binary files /dev/null and b/ios/Runner/LauncherIcons/shadeOfViolet-ipad-pro@2x.png differ diff --git a/ios/Runner/LauncherIcons/shadeOfViolet-ipad@2x.png b/ios/Runner/LauncherIcons/shadeOfViolet-ipad@2x.png new file mode 100644 index 0000000..5ca8c40 Binary files /dev/null and b/ios/Runner/LauncherIcons/shadeOfViolet-ipad@2x.png differ diff --git a/ios/Runner/LauncherIcons/shadeOfViolet@2x.png b/ios/Runner/LauncherIcons/shadeOfViolet@2x.png new file mode 100644 index 0000000..6235cd5 Binary files /dev/null and b/ios/Runner/LauncherIcons/shadeOfViolet@2x.png differ diff --git a/ios/Runner/LauncherIcons/shadeOfViolet@3x.png b/ios/Runner/LauncherIcons/shadeOfViolet@3x.png new file mode 100644 index 0000000..10fc52b Binary files /dev/null and b/ios/Runner/LauncherIcons/shadeOfViolet@3x.png differ diff --git a/ios/Runner/LauncherIcons/soilOfAvagddu-ipad-pro@2x.png b/ios/Runner/LauncherIcons/soilOfAvagddu-ipad-pro@2x.png new file mode 100644 index 0000000..eb97453 Binary files /dev/null and b/ios/Runner/LauncherIcons/soilOfAvagddu-ipad-pro@2x.png differ diff --git a/ios/Runner/LauncherIcons/soilOfAvagddu-ipad@2x.png b/ios/Runner/LauncherIcons/soilOfAvagddu-ipad@2x.png new file mode 100644 index 0000000..cb0ba3f Binary files /dev/null and b/ios/Runner/LauncherIcons/soilOfAvagddu-ipad@2x.png differ diff --git a/ios/Runner/LauncherIcons/soilOfAvagddu@2x.png b/ios/Runner/LauncherIcons/soilOfAvagddu@2x.png new file mode 100644 index 0000000..0f54ebe Binary files /dev/null and b/ios/Runner/LauncherIcons/soilOfAvagddu@2x.png differ diff --git a/ios/Runner/LauncherIcons/soilOfAvagddu@3x.png b/ios/Runner/LauncherIcons/soilOfAvagddu@3x.png new file mode 100644 index 0000000..c072fe7 Binary files /dev/null and b/ios/Runner/LauncherIcons/soilOfAvagddu@3x.png differ diff --git a/ios/Runner/LauncherIcons/spaceBattleBlue-ipad-pro@2x.png b/ios/Runner/LauncherIcons/spaceBattleBlue-ipad-pro@2x.png new file mode 100644 index 0000000..dd7a210 Binary files /dev/null and b/ios/Runner/LauncherIcons/spaceBattleBlue-ipad-pro@2x.png differ diff --git a/ios/Runner/LauncherIcons/spaceBattleBlue-ipad@2x.png b/ios/Runner/LauncherIcons/spaceBattleBlue-ipad@2x.png new file mode 100644 index 0000000..f9b9ecd Binary files /dev/null and b/ios/Runner/LauncherIcons/spaceBattleBlue-ipad@2x.png differ diff --git a/ios/Runner/LauncherIcons/spaceBattleBlue@2x.png b/ios/Runner/LauncherIcons/spaceBattleBlue@2x.png new file mode 100644 index 0000000..b051e3d Binary files /dev/null and b/ios/Runner/LauncherIcons/spaceBattleBlue@2x.png differ diff --git a/ios/Runner/LauncherIcons/spaceBattleBlue@3x.png b/ios/Runner/LauncherIcons/spaceBattleBlue@3x.png new file mode 100644 index 0000000..b1e122d Binary files /dev/null and b/ios/Runner/LauncherIcons/spaceBattleBlue@3x.png differ diff --git a/ios/Runner/LauncherIcons/spreadsheetGreen-ipad-pro@2x.png b/ios/Runner/LauncherIcons/spreadsheetGreen-ipad-pro@2x.png new file mode 100644 index 0000000..07c51af Binary files /dev/null and b/ios/Runner/LauncherIcons/spreadsheetGreen-ipad-pro@2x.png differ diff --git a/ios/Runner/LauncherIcons/spreadsheetGreen-ipad@2x.png b/ios/Runner/LauncherIcons/spreadsheetGreen-ipad@2x.png new file mode 100644 index 0000000..35872c1 Binary files /dev/null and b/ios/Runner/LauncherIcons/spreadsheetGreen-ipad@2x.png differ diff --git a/ios/Runner/LauncherIcons/spreadsheetGreen@2x.png b/ios/Runner/LauncherIcons/spreadsheetGreen@2x.png new file mode 100644 index 0000000..2041af3 Binary files /dev/null and b/ios/Runner/LauncherIcons/spreadsheetGreen@2x.png differ diff --git a/ios/Runner/LauncherIcons/spreadsheetGreen@3x.png b/ios/Runner/LauncherIcons/spreadsheetGreen@3x.png new file mode 100644 index 0000000..349fe5d Binary files /dev/null and b/ios/Runner/LauncherIcons/spreadsheetGreen@3x.png differ diff --git a/ios/Runner/LauncherIcons/tokiwaGreen-ipad-pro@2x.png b/ios/Runner/LauncherIcons/tokiwaGreen-ipad-pro@2x.png new file mode 100644 index 0000000..2b7c232 Binary files /dev/null and b/ios/Runner/LauncherIcons/tokiwaGreen-ipad-pro@2x.png differ diff --git a/ios/Runner/LauncherIcons/tokiwaGreen-ipad@2x.png b/ios/Runner/LauncherIcons/tokiwaGreen-ipad@2x.png new file mode 100644 index 0000000..6929099 Binary files /dev/null and b/ios/Runner/LauncherIcons/tokiwaGreen-ipad@2x.png differ diff --git a/ios/Runner/LauncherIcons/tokiwaGreen@2x.png b/ios/Runner/LauncherIcons/tokiwaGreen@2x.png new file mode 100644 index 0000000..3b981b3 Binary files /dev/null and b/ios/Runner/LauncherIcons/tokiwaGreen@2x.png differ diff --git a/ios/Runner/LauncherIcons/tokiwaGreen@3x.png b/ios/Runner/LauncherIcons/tokiwaGreen@3x.png new file mode 100644 index 0000000..6587a2a Binary files /dev/null and b/ios/Runner/LauncherIcons/tokiwaGreen@3x.png differ diff --git a/ios/Runner/LauncherIcons/toyCamouflage-ipad-pro@2x.png b/ios/Runner/LauncherIcons/toyCamouflage-ipad-pro@2x.png new file mode 100644 index 0000000..c03ff38 Binary files /dev/null and b/ios/Runner/LauncherIcons/toyCamouflage-ipad-pro@2x.png differ diff --git a/ios/Runner/LauncherIcons/toyCamouflage-ipad@2x.png b/ios/Runner/LauncherIcons/toyCamouflage-ipad@2x.png new file mode 100644 index 0000000..f5c54a8 Binary files /dev/null and b/ios/Runner/LauncherIcons/toyCamouflage-ipad@2x.png differ diff --git a/ios/Runner/LauncherIcons/toyCamouflage@2x.png b/ios/Runner/LauncherIcons/toyCamouflage@2x.png new file mode 100644 index 0000000..8edb4eb Binary files /dev/null and b/ios/Runner/LauncherIcons/toyCamouflage@2x.png differ diff --git a/ios/Runner/LauncherIcons/toyCamouflage@3x.png b/ios/Runner/LauncherIcons/toyCamouflage@3x.png new file mode 100644 index 0000000..c39ba05 Binary files /dev/null and b/ios/Runner/LauncherIcons/toyCamouflage@3x.png differ diff --git a/ios/Runner/LauncherIcons/tropicana-ipad-pro@2x.png b/ios/Runner/LauncherIcons/tropicana-ipad-pro@2x.png new file mode 100644 index 0000000..cfa888b Binary files /dev/null and b/ios/Runner/LauncherIcons/tropicana-ipad-pro@2x.png differ diff --git a/ios/Runner/LauncherIcons/tropicana-ipad@2x.png b/ios/Runner/LauncherIcons/tropicana-ipad@2x.png new file mode 100644 index 0000000..650a268 Binary files /dev/null and b/ios/Runner/LauncherIcons/tropicana-ipad@2x.png differ diff --git a/ios/Runner/LauncherIcons/tropicana@2x.png b/ios/Runner/LauncherIcons/tropicana@2x.png new file mode 100644 index 0000000..1f45f9a Binary files /dev/null and b/ios/Runner/LauncherIcons/tropicana@2x.png differ diff --git a/ios/Runner/LauncherIcons/tropicana@3x.png b/ios/Runner/LauncherIcons/tropicana@3x.png new file mode 100644 index 0000000..a17683f Binary files /dev/null and b/ios/Runner/LauncherIcons/tropicana@3x.png differ diff --git a/launcher_icons/defaults-light/blissfulBerry-ipad-pro@2x.png b/launcher_icons/defaults-light/blissfulBerry-ipad-pro@2x.png new file mode 100644 index 0000000..1f5e9ab Binary files /dev/null and b/launcher_icons/defaults-light/blissfulBerry-ipad-pro@2x.png differ diff --git a/launcher_icons/defaults-light/blissfulBerry-ipad@2x.png b/launcher_icons/defaults-light/blissfulBerry-ipad@2x.png new file mode 100644 index 0000000..28bc20c Binary files /dev/null and b/launcher_icons/defaults-light/blissfulBerry-ipad@2x.png differ diff --git a/launcher_icons/defaults-light/blissfulBerry@2x.png b/launcher_icons/defaults-light/blissfulBerry@2x.png new file mode 100644 index 0000000..ce4b35f Binary files /dev/null and b/launcher_icons/defaults-light/blissfulBerry@2x.png differ diff --git a/launcher_icons/defaults-light/blissfulBerry@3x.png b/launcher_icons/defaults-light/blissfulBerry@3x.png new file mode 100644 index 0000000..26cb4c9 Binary files /dev/null and b/launcher_icons/defaults-light/blissfulBerry@3x.png differ diff --git a/launcher_icons/defaults-light/bohemianBlue-ipad-pro@2x.png b/launcher_icons/defaults-light/bohemianBlue-ipad-pro@2x.png new file mode 100644 index 0000000..8431f54 Binary files /dev/null and b/launcher_icons/defaults-light/bohemianBlue-ipad-pro@2x.png differ diff --git a/launcher_icons/defaults-light/bohemianBlue-ipad@2x.png b/launcher_icons/defaults-light/bohemianBlue-ipad@2x.png new file mode 100644 index 0000000..124224f Binary files /dev/null and b/launcher_icons/defaults-light/bohemianBlue-ipad@2x.png differ diff --git a/launcher_icons/defaults-light/bohemianBlue@2x.png b/launcher_icons/defaults-light/bohemianBlue@2x.png new file mode 100644 index 0000000..f25b29a Binary files /dev/null and b/launcher_icons/defaults-light/bohemianBlue@2x.png differ diff --git a/launcher_icons/defaults-light/bohemianBlue@3x.png b/launcher_icons/defaults-light/bohemianBlue@3x.png new file mode 100644 index 0000000..079477e Binary files /dev/null and b/launcher_icons/defaults-light/bohemianBlue@3x.png differ diff --git a/launcher_icons/defaults-light/burntSienna-ipad-pro@2x.png b/launcher_icons/defaults-light/burntSienna-ipad-pro@2x.png new file mode 100644 index 0000000..0bd8ed0 Binary files /dev/null and b/launcher_icons/defaults-light/burntSienna-ipad-pro@2x.png differ diff --git a/launcher_icons/defaults-light/burntSienna-ipad@2x.png b/launcher_icons/defaults-light/burntSienna-ipad@2x.png new file mode 100644 index 0000000..c180286 Binary files /dev/null and b/launcher_icons/defaults-light/burntSienna-ipad@2x.png differ diff --git a/launcher_icons/defaults-light/burntSienna@2x.png b/launcher_icons/defaults-light/burntSienna@2x.png new file mode 100644 index 0000000..0859328 Binary files /dev/null and b/launcher_icons/defaults-light/burntSienna@2x.png differ diff --git a/launcher_icons/defaults-light/burntSienna@3x.png b/launcher_icons/defaults-light/burntSienna@3x.png new file mode 100644 index 0000000..11f01fd Binary files /dev/null and b/launcher_icons/defaults-light/burntSienna@3x.png differ diff --git a/launcher_icons/defaults-light/cherryPlum-ipad-pro@2x.png b/launcher_icons/defaults-light/cherryPlum-ipad-pro@2x.png new file mode 100644 index 0000000..13fdaba Binary files /dev/null and b/launcher_icons/defaults-light/cherryPlum-ipad-pro@2x.png differ diff --git a/launcher_icons/defaults-light/cherryPlum-ipad@2x.png b/launcher_icons/defaults-light/cherryPlum-ipad@2x.png new file mode 100644 index 0000000..7f0736a Binary files /dev/null and b/launcher_icons/defaults-light/cherryPlum-ipad@2x.png differ diff --git a/launcher_icons/defaults-light/cherryPlum@2x.png b/launcher_icons/defaults-light/cherryPlum@2x.png new file mode 100644 index 0000000..e235ae2 Binary files /dev/null and b/launcher_icons/defaults-light/cherryPlum@2x.png differ diff --git a/launcher_icons/defaults-light/cherryPlum@3x.png b/launcher_icons/defaults-light/cherryPlum@3x.png new file mode 100644 index 0000000..56e6757 Binary files /dev/null and b/launcher_icons/defaults-light/cherryPlum@3x.png differ diff --git a/launcher_icons/defaults-light/crispChristmasCranberries-ipad-pro@2x.png b/launcher_icons/defaults-light/crispChristmasCranberries-ipad-pro@2x.png new file mode 100644 index 0000000..0662350 Binary files /dev/null and b/launcher_icons/defaults-light/crispChristmasCranberries-ipad-pro@2x.png differ diff --git a/launcher_icons/defaults-light/crispChristmasCranberries-ipad@2x.png b/launcher_icons/defaults-light/crispChristmasCranberries-ipad@2x.png new file mode 100644 index 0000000..22a96e7 Binary files /dev/null and b/launcher_icons/defaults-light/crispChristmasCranberries-ipad@2x.png differ diff --git a/launcher_icons/defaults-light/crispChristmasCranberries@2x.png b/launcher_icons/defaults-light/crispChristmasCranberries@2x.png new file mode 100644 index 0000000..bc7a213 Binary files /dev/null and b/launcher_icons/defaults-light/crispChristmasCranberries@2x.png differ diff --git a/launcher_icons/defaults-light/crispChristmasCranberries@3x.png b/launcher_icons/defaults-light/crispChristmasCranberries@3x.png new file mode 100644 index 0000000..e42ed58 Binary files /dev/null and b/launcher_icons/defaults-light/crispChristmasCranberries@3x.png differ diff --git a/launcher_icons/defaults-light/egyptianBlue-ipad-pro@2x.png b/launcher_icons/defaults-light/egyptianBlue-ipad-pro@2x.png new file mode 100644 index 0000000..99058f6 Binary files /dev/null and b/launcher_icons/defaults-light/egyptianBlue-ipad-pro@2x.png differ diff --git a/launcher_icons/defaults-light/egyptianBlue-ipad@2x.png b/launcher_icons/defaults-light/egyptianBlue-ipad@2x.png new file mode 100644 index 0000000..d68f43c Binary files /dev/null and b/launcher_icons/defaults-light/egyptianBlue-ipad@2x.png differ diff --git a/launcher_icons/defaults-light/egyptianBlue@2x.png b/launcher_icons/defaults-light/egyptianBlue@2x.png new file mode 100644 index 0000000..55f959c Binary files /dev/null and b/launcher_icons/defaults-light/egyptianBlue@2x.png differ diff --git a/launcher_icons/defaults-light/egyptianBlue@3x.png b/launcher_icons/defaults-light/egyptianBlue@3x.png new file mode 100644 index 0000000..931c634 Binary files /dev/null and b/launcher_icons/defaults-light/egyptianBlue@3x.png differ diff --git a/launcher_icons/defaults-light/flagGreen-ipad-pro@2x.png b/launcher_icons/defaults-light/flagGreen-ipad-pro@2x.png new file mode 100644 index 0000000..e35a87f Binary files /dev/null and b/launcher_icons/defaults-light/flagGreen-ipad-pro@2x.png differ diff --git a/launcher_icons/defaults-light/flagGreen-ipad@2x.png b/launcher_icons/defaults-light/flagGreen-ipad@2x.png new file mode 100644 index 0000000..8e70f5e Binary files /dev/null and b/launcher_icons/defaults-light/flagGreen-ipad@2x.png differ diff --git a/launcher_icons/defaults-light/flagGreen@2x.png b/launcher_icons/defaults-light/flagGreen@2x.png new file mode 100644 index 0000000..1c9ea29 Binary files /dev/null and b/launcher_icons/defaults-light/flagGreen@2x.png differ diff --git a/launcher_icons/defaults-light/flagGreen@3x.png b/launcher_icons/defaults-light/flagGreen@3x.png new file mode 100644 index 0000000..2518146 Binary files /dev/null and b/launcher_icons/defaults-light/flagGreen@3x.png differ diff --git a/launcher_icons/defaults-light/hydraTurquoise-ipad-pro@2x.png b/launcher_icons/defaults-light/hydraTurquoise-ipad-pro@2x.png new file mode 100644 index 0000000..75317b7 Binary files /dev/null and b/launcher_icons/defaults-light/hydraTurquoise-ipad-pro@2x.png differ diff --git a/launcher_icons/defaults-light/hydraTurquoise-ipad@2x.png b/launcher_icons/defaults-light/hydraTurquoise-ipad@2x.png new file mode 100644 index 0000000..0e92387 Binary files /dev/null and b/launcher_icons/defaults-light/hydraTurquoise-ipad@2x.png differ diff --git a/launcher_icons/defaults-light/hydraTurquoise@2x.png b/launcher_icons/defaults-light/hydraTurquoise@2x.png new file mode 100644 index 0000000..53e4c43 Binary files /dev/null and b/launcher_icons/defaults-light/hydraTurquoise@2x.png differ diff --git a/launcher_icons/defaults-light/hydraTurquoise@3x.png b/launcher_icons/defaults-light/hydraTurquoise@3x.png new file mode 100644 index 0000000..b000c56 Binary files /dev/null and b/launcher_icons/defaults-light/hydraTurquoise@3x.png differ diff --git a/launcher_icons/defaults-light/peacockBlue-ipad-pro@2x.png b/launcher_icons/defaults-light/peacockBlue-ipad-pro@2x.png new file mode 100644 index 0000000..3bcc7e0 Binary files /dev/null and b/launcher_icons/defaults-light/peacockBlue-ipad-pro@2x.png differ diff --git a/launcher_icons/defaults-light/peacockBlue-ipad@2x.png b/launcher_icons/defaults-light/peacockBlue-ipad@2x.png new file mode 100644 index 0000000..7392c82 Binary files /dev/null and b/launcher_icons/defaults-light/peacockBlue-ipad@2x.png differ diff --git a/launcher_icons/defaults-light/peacockBlue@2x.png b/launcher_icons/defaults-light/peacockBlue@2x.png new file mode 100644 index 0000000..a888293 Binary files /dev/null and b/launcher_icons/defaults-light/peacockBlue@2x.png differ diff --git a/launcher_icons/defaults-light/peacockBlue@3x.png b/launcher_icons/defaults-light/peacockBlue@3x.png new file mode 100644 index 0000000..f3da800 Binary files /dev/null and b/launcher_icons/defaults-light/peacockBlue@3x.png differ diff --git a/launcher_icons/defaults-light/shadeOfViolet-ipad-pro@2x.png b/launcher_icons/defaults-light/shadeOfViolet-ipad-pro@2x.png new file mode 100644 index 0000000..335ffd7 Binary files /dev/null and b/launcher_icons/defaults-light/shadeOfViolet-ipad-pro@2x.png differ diff --git a/launcher_icons/defaults-light/shadeOfViolet-ipad@2x.png b/launcher_icons/defaults-light/shadeOfViolet-ipad@2x.png new file mode 100644 index 0000000..5ca8c40 Binary files /dev/null and b/launcher_icons/defaults-light/shadeOfViolet-ipad@2x.png differ diff --git a/launcher_icons/defaults-light/shadeOfViolet@2x.png b/launcher_icons/defaults-light/shadeOfViolet@2x.png new file mode 100644 index 0000000..6235cd5 Binary files /dev/null and b/launcher_icons/defaults-light/shadeOfViolet@2x.png differ diff --git a/launcher_icons/defaults-light/shadeOfViolet@3x.png b/launcher_icons/defaults-light/shadeOfViolet@3x.png new file mode 100644 index 0000000..10fc52b Binary files /dev/null and b/launcher_icons/defaults-light/shadeOfViolet@3x.png differ diff --git a/launcher_icons/defaults-light/soilOfAvagddu-ipad-pro@2x.png b/launcher_icons/defaults-light/soilOfAvagddu-ipad-pro@2x.png new file mode 100644 index 0000000..eb97453 Binary files /dev/null and b/launcher_icons/defaults-light/soilOfAvagddu-ipad-pro@2x.png differ diff --git a/launcher_icons/defaults-light/soilOfAvagddu-ipad@2x.png b/launcher_icons/defaults-light/soilOfAvagddu-ipad@2x.png new file mode 100644 index 0000000..cb0ba3f Binary files /dev/null and b/launcher_icons/defaults-light/soilOfAvagddu-ipad@2x.png differ diff --git a/launcher_icons/defaults-light/soilOfAvagddu@2x.png b/launcher_icons/defaults-light/soilOfAvagddu@2x.png new file mode 100644 index 0000000..0f54ebe Binary files /dev/null and b/launcher_icons/defaults-light/soilOfAvagddu@2x.png differ diff --git a/launcher_icons/defaults-light/soilOfAvagddu@3x.png b/launcher_icons/defaults-light/soilOfAvagddu@3x.png new file mode 100644 index 0000000..c072fe7 Binary files /dev/null and b/launcher_icons/defaults-light/soilOfAvagddu@3x.png differ diff --git a/launcher_icons/defaults-light/spaceBattleBlue-ipad-pro@2x.png b/launcher_icons/defaults-light/spaceBattleBlue-ipad-pro@2x.png new file mode 100644 index 0000000..dd7a210 Binary files /dev/null and b/launcher_icons/defaults-light/spaceBattleBlue-ipad-pro@2x.png differ diff --git a/launcher_icons/defaults-light/spaceBattleBlue-ipad@2x.png b/launcher_icons/defaults-light/spaceBattleBlue-ipad@2x.png new file mode 100644 index 0000000..f9b9ecd Binary files /dev/null and b/launcher_icons/defaults-light/spaceBattleBlue-ipad@2x.png differ diff --git a/launcher_icons/defaults-light/spaceBattleBlue@2x.png b/launcher_icons/defaults-light/spaceBattleBlue@2x.png new file mode 100644 index 0000000..b051e3d Binary files /dev/null and b/launcher_icons/defaults-light/spaceBattleBlue@2x.png differ diff --git a/launcher_icons/defaults-light/spaceBattleBlue@3x.png b/launcher_icons/defaults-light/spaceBattleBlue@3x.png new file mode 100644 index 0000000..b1e122d Binary files /dev/null and b/launcher_icons/defaults-light/spaceBattleBlue@3x.png differ diff --git a/launcher_icons/defaults-light/spreadsheetGreen-ipad-pro@2x.png b/launcher_icons/defaults-light/spreadsheetGreen-ipad-pro@2x.png new file mode 100644 index 0000000..07c51af Binary files /dev/null and b/launcher_icons/defaults-light/spreadsheetGreen-ipad-pro@2x.png differ diff --git a/launcher_icons/defaults-light/spreadsheetGreen-ipad@2x.png b/launcher_icons/defaults-light/spreadsheetGreen-ipad@2x.png new file mode 100644 index 0000000..35872c1 Binary files /dev/null and b/launcher_icons/defaults-light/spreadsheetGreen-ipad@2x.png differ diff --git a/launcher_icons/defaults-light/spreadsheetGreen@2x.png b/launcher_icons/defaults-light/spreadsheetGreen@2x.png new file mode 100644 index 0000000..2041af3 Binary files /dev/null and b/launcher_icons/defaults-light/spreadsheetGreen@2x.png differ diff --git a/launcher_icons/defaults-light/spreadsheetGreen@3x.png b/launcher_icons/defaults-light/spreadsheetGreen@3x.png new file mode 100644 index 0000000..349fe5d Binary files /dev/null and b/launcher_icons/defaults-light/spreadsheetGreen@3x.png differ diff --git a/launcher_icons/defaults-light/tokiwaGreen-ipad-pro@2x.png b/launcher_icons/defaults-light/tokiwaGreen-ipad-pro@2x.png new file mode 100644 index 0000000..2b7c232 Binary files /dev/null and b/launcher_icons/defaults-light/tokiwaGreen-ipad-pro@2x.png differ diff --git a/launcher_icons/defaults-light/tokiwaGreen-ipad@2x.png b/launcher_icons/defaults-light/tokiwaGreen-ipad@2x.png new file mode 100644 index 0000000..6929099 Binary files /dev/null and b/launcher_icons/defaults-light/tokiwaGreen-ipad@2x.png differ diff --git a/launcher_icons/defaults-light/tokiwaGreen@2x.png b/launcher_icons/defaults-light/tokiwaGreen@2x.png new file mode 100644 index 0000000..3b981b3 Binary files /dev/null and b/launcher_icons/defaults-light/tokiwaGreen@2x.png differ diff --git a/launcher_icons/defaults-light/tokiwaGreen@3x.png b/launcher_icons/defaults-light/tokiwaGreen@3x.png new file mode 100644 index 0000000..6587a2a Binary files /dev/null and b/launcher_icons/defaults-light/tokiwaGreen@3x.png differ diff --git a/launcher_icons/defaults-light/toyCamouflage-ipad-pro@2x.png b/launcher_icons/defaults-light/toyCamouflage-ipad-pro@2x.png new file mode 100644 index 0000000..c03ff38 Binary files /dev/null and b/launcher_icons/defaults-light/toyCamouflage-ipad-pro@2x.png differ diff --git a/launcher_icons/defaults-light/toyCamouflage-ipad@2x.png b/launcher_icons/defaults-light/toyCamouflage-ipad@2x.png new file mode 100644 index 0000000..f5c54a8 Binary files /dev/null and b/launcher_icons/defaults-light/toyCamouflage-ipad@2x.png differ diff --git a/launcher_icons/defaults-light/toyCamouflage@2x.png b/launcher_icons/defaults-light/toyCamouflage@2x.png new file mode 100644 index 0000000..8edb4eb Binary files /dev/null and b/launcher_icons/defaults-light/toyCamouflage@2x.png differ diff --git a/launcher_icons/defaults-light/toyCamouflage@3x.png b/launcher_icons/defaults-light/toyCamouflage@3x.png new file mode 100644 index 0000000..c39ba05 Binary files /dev/null and b/launcher_icons/defaults-light/toyCamouflage@3x.png differ diff --git a/launcher_icons/defaults-light/tropicana-ipad-pro@2x.png b/launcher_icons/defaults-light/tropicana-ipad-pro@2x.png new file mode 100644 index 0000000..cfa888b Binary files /dev/null and b/launcher_icons/defaults-light/tropicana-ipad-pro@2x.png differ diff --git a/launcher_icons/defaults-light/tropicana-ipad@2x.png b/launcher_icons/defaults-light/tropicana-ipad@2x.png new file mode 100644 index 0000000..650a268 Binary files /dev/null and b/launcher_icons/defaults-light/tropicana-ipad@2x.png differ diff --git a/launcher_icons/defaults-light/tropicana@2x.png b/launcher_icons/defaults-light/tropicana@2x.png new file mode 100644 index 0000000..1f45f9a Binary files /dev/null and b/launcher_icons/defaults-light/tropicana@2x.png differ diff --git a/launcher_icons/defaults-light/tropicana@3x.png b/launcher_icons/defaults-light/tropicana@3x.png new file mode 100644 index 0000000..a17683f Binary files /dev/null and b/launcher_icons/defaults-light/tropicana@3x.png differ diff --git a/lib/data/chart_data.dart b/lib/data/chart_data.dart index 4a22773..d77aeb7 100644 --- a/lib/data/chart_data.dart +++ b/lib/data/chart_data.dart @@ -4,6 +4,7 @@ import "package:flow/services/exchange_rates.dart"; class ChartData implements Comparable> { final String key; final Money money; + final String currency; final T? associatedData; double get displayTotal => money.amount.abs(); @@ -11,6 +12,7 @@ class ChartData implements Comparable> { ChartData({ required this.key, required this.money, + required this.currency, required this.associatedData, }); diff --git a/lib/data/money.dart b/lib/data/money.dart index 8dfba58..4455287 100644 --- a/lib/data/money.dart +++ b/lib/data/money.dart @@ -133,6 +133,11 @@ class Money { @override int get hashCode => Object.hashAll([amount, currency]); + + @override + toString() { + return "Money($currency $amount)"; + } } class MoneyException implements Exception { diff --git a/lib/entity/account.dart b/lib/entity/account.dart index 5592582..e32d16c 100644 --- a/lib/entity/account.dart +++ b/lib/entity/account.dart @@ -1,4 +1,5 @@ import "package:flow/data/flow_icon.dart"; +import "package:flow/data/money.dart"; import "package:flow/entity/_base.dart"; import "package:flow/entity/transaction.dart"; import "package:json_annotation/json_annotation.dart"; @@ -55,14 +56,15 @@ class Account implements EntityBase { /// TODO should this be cached? @Transient() @JsonKey(includeFromJson: false, includeToJson: false) - double get balance { - return transactions - .where((element) => element.transactionDate.isPast) - .fold( - 0, - (previousValue, element) => previousValue + element.amount, - ); - } + Money get balance => Money( + transactions + .where((element) => element.transactionDate.isPast) + .fold( + 0, + (previousValue, element) => previousValue + element.amount, + ), + currency, + ); Account({ this.id = 0, diff --git a/lib/l10n/extensions.dart b/lib/l10n/extensions.dart index 028a624..e39d9fb 100644 --- a/lib/l10n/extensions.dart +++ b/lib/l10n/extensions.dart @@ -1,7 +1,6 @@ +import "package:flow/data/money.dart"; import "package:flow/l10n/flow_localizations.dart"; -import "package:flow/prefs.dart"; import "package:flutter/widgets.dart"; - import "package:intl/intl.dart"; extension L10nHelper on BuildContext { @@ -46,46 +45,40 @@ extension L10nStringHelper on String { FlowLocalizations.getTransalation(this, replace: replace); } -extension MoneyFormatters on num { +extension MoneyFormatters on Money { String formatMoney({ - String? currency, bool includeCurrency = true, bool useCurrencySymbol = true, bool compact = false, bool takeAbsoluteValue = false, int? decimalDigits, }) { - final num amount = takeAbsoluteValue ? abs() : this; - - if (!includeCurrency) { - currency = ""; - useCurrencySymbol = false; - } else { - currency ??= LocalPreferences().getPrimaryCurrency(); - } + final num amountToFormat = takeAbsoluteValue ? amount.abs() : amount; + final String currencyToFormat = !includeCurrency ? "" : currency; + useCurrencySymbol = useCurrencySymbol && includeCurrency; final String? symbol = useCurrencySymbol ? NumberFormat.simpleCurrency( locale: Intl.defaultLocale, - name: currency, + name: currencyToFormat, ).currencySymbol : null; if (compact) { return NumberFormat.compactCurrency( locale: Intl.defaultLocale, - name: currency, + name: currencyToFormat, symbol: symbol, decimalDigits: decimalDigits, - ).format(amount); + ).format(amountToFormat); } return NumberFormat.currency( locale: Intl.defaultLocale, - name: currency, + name: currencyToFormat, symbol: symbol, decimalDigits: decimalDigits, - ).format(amount); + ).format(amountToFormat); } /// Returns money-formatted string in the primary currency diff --git a/lib/main.dart b/lib/main.dart index 1b18dba..a35bab3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -100,6 +100,7 @@ class FlowState extends State { LocalPreferences().localeOverride.addListener(_reloadLocale); LocalPreferences().themeName.addListener(_reloadTheme); + LocalPreferences().primaryCurrency.addListener(_refreshExchangeRates); ObjectBox().box().query().watch().listen((event) { ObjectBox().invalidateAccountsTab(); @@ -114,6 +115,7 @@ class FlowState extends State { void dispose() { LocalPreferences().localeOverride.removeListener(_reloadLocale); LocalPreferences().themeName.removeListener(_reloadTheme); + LocalPreferences().primaryCurrency.removeListener(_refreshExchangeRates); super.dispose(); } @@ -204,4 +206,10 @@ class FlowState extends State { Intl.defaultLocale = overriddenLocale.code; setState(() {}); } + + void _refreshExchangeRates() { + ExchangeRatesService().tryFetchRates( + LocalPreferences().getPrimaryCurrency(), + ); + } } diff --git a/lib/objectbox/actions.dart b/lib/objectbox/actions.dart index b9750ad..0d31cc2 100644 --- a/lib/objectbox/actions.dart +++ b/lib/objectbox/actions.dart @@ -2,8 +2,10 @@ import "dart:developer"; import "dart:io"; import "dart:math" as math; +import "package:flow/data/exchange_rates.dart"; import "package:flow/data/flow_analytics.dart"; import "package:flow/data/memo.dart"; +import "package:flow/data/money.dart"; import "package:flow/data/money_flow.dart"; import "package:flow/data/prefs/frecency_group.dart"; import "package:flow/data/transactions_filter.dart"; @@ -17,6 +19,7 @@ import "package:flow/l10n/extensions.dart"; import "package:flow/objectbox.dart"; import "package:flow/objectbox/objectbox.g.dart"; import "package:flow/prefs.dart"; +import "package:flow/services/exchange_rates.dart"; import "package:flow/utils/utils.dart"; import "package:fuzzywuzzy/fuzzywuzzy.dart"; import "package:moment_dart/moment_dart.dart"; @@ -25,16 +28,59 @@ import "package:uuid/uuid.dart"; typedef RelevanceScoredTitle = ({String title, double relevancy}); extension MainActions on ObjectBox { - double getTotalBalance() { + /// Returns the grand total of all accounts in primary currency in the primary currency + Money getPrimaryCurrencyGrandTotal() { + final String primaryCurrency = LocalPreferences().getPrimaryCurrency(); + final Query accountsQuery = box() - .query(Account_.excludeFromTotalBalance.equals(false)) + .query(Account_.excludeFromTotalBalance + .notEquals(true) + .and(Account_.currency.equals(primaryCurrency))) .build(); final List accounts = accountsQuery.find(); - return accounts - .map((e) => e.balance) - .fold(0, (previousValue, element) => previousValue + element); + accountsQuery.close(); + + return accounts.map((e) => e.balance).fold(Money(0, primaryCurrency), + (previousValue, element) => previousValue + element); + } + + /// Returns the grand total of all accounts (including non-primary currency accounts) in the primary currency + Future getGrandTotal() async { + final String primaryCurrency = LocalPreferences().getPrimaryCurrency(); + + final Query accountsQuery = box() + .query(Account_.excludeFromTotalBalance.notEquals(true)) + .build(); + + final List accounts = accountsQuery.find(); + + accountsQuery.close(); + + Money total = accounts + .where((account) => account.currency == primaryCurrency) + .fold( + Money(0, primaryCurrency), + (previousValue, element) => previousValue + element.balance, + ); + + final List nonPrimaryCurrencyAccounts = accounts + .where((account) => account.currency != primaryCurrency) + .toList(); + + final ExchangeRates? rates = + await ExchangeRatesService().tryFetchRates(primaryCurrency); + + if (rates == null) return null; + + for (final Account account in nonPrimaryCurrencyAccounts) { + final Money converted = account.balance.convert(primaryCurrency, rates); + + total += converted; + } + + return total; } List getAccounts([bool sortByFrecency = true]) { @@ -431,7 +477,13 @@ extension TransactionListActions on Iterable { List search(TransactionSearchData? data) { if (data == null || data.normalizedKeyword == null) return toList(); - return where(data.predicate).toList(); + final matches = where(data.predicate).toList(); + + if (data.smartMatch && matches.isEmpty) { + return search(data.copyWithOptional(smartMatch: false)); + } + + return matches; } } @@ -467,7 +519,7 @@ extension AccountActions on Account { String? title, DateTime? transactionDate, }) { - final double delta = targetBalance - balance; + final double delta = targetBalance - balance.amount; return createAndSaveTransaction( amount: delta, diff --git a/lib/prefs.dart b/lib/prefs.dart index c0205c2..4805dab 100644 --- a/lib/prefs.dart +++ b/lib/prefs.dart @@ -78,6 +78,8 @@ class LocalPreferences { late final ThemeModeSettingsEntry themeMode; late final PrimitiveSettingsEntry themeName; + late final BoolSettingsEntry themeChangesAppIcon; + late final BoolSettingsEntry enableDynamicTheme; LocalPreferences._internal(this._prefs) { primaryCurrency = PrimitiveSettingsEntry( @@ -183,6 +185,16 @@ class LocalPreferences { preferences: _prefs, initialValue: lightThemes.keys.first, ); + themeChangesAppIcon = BoolSettingsEntry( + key: "flow.themeChangesAppIcon", + preferences: _prefs, + initialValue: true, + ); + enableDynamicTheme = BoolSettingsEntry( + key: "flow.enableDynamicTheme", + preferences: _prefs, + initialValue: true, + ); updateTransitiveProperties(); } @@ -369,6 +381,13 @@ class LocalPreferences { return primaryCurrencyName; } + String getCurrentTheme() { + final String? preferencesTheme = LocalPreferences().themeName.get(); + return validateThemeName(preferencesTheme) + ? preferencesTheme! + : lightThemes.keys.first; + } + factory LocalPreferences() { if (_instance == null) { throw Exception( diff --git a/lib/routes/account/account_edit_page.dart b/lib/routes/account/account_edit_page.dart index cd861d7..415d3f9 100644 --- a/lib/routes/account/account_edit_page.dart +++ b/lib/routes/account/account_edit_page.dart @@ -2,6 +2,7 @@ import "dart:async"; import "dart:developer"; import "package:flow/data/flow_icon.dart"; +import "package:flow/data/money.dart"; import "package:flow/entity/account.dart"; import "package:flow/entity/backup_entry.dart"; import "package:flow/entity/transaction.dart"; @@ -75,7 +76,7 @@ class _AccountEditPageState extends State { } else { _nameTextController = TextEditingController(text: _currentlyEditing?.name); - _balance = _currentlyEditing?.balance ?? 0.0; + _balance = _currentlyEditing?.balance.amount ?? 0.0; _currency = _currentlyEditing?.currency ?? LocalPreferences().getPrimaryCurrency(); _iconData = _currentlyEditing?.icon; @@ -210,7 +211,7 @@ class _AccountEditPageState extends State { Padding( padding: contentPadding, child: Text( - _balance.formatMoney(currency: _currency), + Money(_balance, _currency).formatMoney(), style: context.textTheme.displayMedium, ), ), @@ -301,7 +302,7 @@ class _AccountEditPageState extends State { _currentlyEditing.iconCode = iconCodeOrError; _currentlyEditing.excludeFromTotalBalance = _excludeFromTotalBalance; - if (_balance != _currentlyEditing.balance) { + if (_balance != _currentlyEditing.balance.amount) { _currentlyEditing.updateBalanceAndSave( _balance, title: "account.updateBalance.transactionTitle".t(context), @@ -374,7 +375,7 @@ class _AccountEditPageState extends State { _currentlyEditing.currency != _currency || _currentlyEditing.excludeFromTotalBalance != _excludeFromTotalBalance || - _balance != _currentlyEditing.balance; + _balance != _currentlyEditing.balance.amount; } return _nameTextController.text.trim().isNotEmpty || diff --git a/lib/routes/account_page.dart b/lib/routes/account_page.dart index df4f9e2..db53e49 100644 --- a/lib/routes/account_page.dart +++ b/lib/routes/account_page.dart @@ -1,3 +1,4 @@ +import "package:flow/data/money.dart"; import "package:flow/data/money_flow.dart"; import "package:flow/entity/account.dart"; import "package:flow/entity/transaction.dart"; @@ -87,10 +88,8 @@ class _AccountPageState extends State { final bool noTransactions = (transactions?.length ?? 0) == 0; final MoneyFlow flow = transactions?.flow ?? MoneyFlow(); - final double totalIncome = - flow.getIncomeByCurrency(account.currency).amount; - final double totalExpense = - flow.getExpenseByCurrency(account.currency).amount; + final Money totalIncome = flow.getIncomeByCurrency(account.currency); + final Money totalExpense = flow.getExpenseByCurrency(account.currency); const double firstHeaderTopPadding = 0.0; diff --git a/lib/routes/category_page.dart b/lib/routes/category_page.dart index 71a7852..38e276a 100644 --- a/lib/routes/category_page.dart +++ b/lib/routes/category_page.dart @@ -15,6 +15,7 @@ import "package:flow/widgets/general/spinner.dart"; import "package:flow/widgets/grouped_transaction_list.dart"; import "package:flow/widgets/home/transactions_date_header.dart"; import "package:flow/widgets/no_result.dart"; +import "package:flow/widgets/rates_missing_warning.dart"; import "package:flow/widgets/time_range_selector.dart"; import "package:flutter/material.dart"; import "package:go_router/go_router.dart"; @@ -96,6 +97,8 @@ class _CategoryPageState extends State { const double firstHeaderTopPadding = 0.0; + final bool missingRates = rates == null; + final Widget header = Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ @@ -107,8 +110,8 @@ class _CategoryPageState extends State { TransactionsInfo( count: transactions?.length, flow: rates == null - ? flow.getFlowByCurrency(primaryCurrency).amount - : flow.getTotalFlow(rates, primaryCurrency).amount, + ? flow.getFlowByCurrency(primaryCurrency) + : flow.getTotalFlow(rates, primaryCurrency), icon: category.icon, ), const SizedBox(height: 12.0), @@ -116,23 +119,27 @@ class _CategoryPageState extends State { children: [ Expanded( child: FlowCard( - flow: rates == null - ? flow.getIncomeByCurrency(primaryCurrency).amount - : flow.getTotalIncome(rates, primaryCurrency).amount, + flow: missingRates + ? flow.getIncomeByCurrency(primaryCurrency) + : flow.getTotalIncome(rates, primaryCurrency), type: TransactionType.income, ), ), const SizedBox(width: 12.0), Expanded( child: FlowCard( - flow: rates == null - ? flow.getExpenseByCurrency(primaryCurrency).amount - : flow.getTotalExpense(rates, primaryCurrency).amount, + flow: missingRates + ? flow.getExpenseByCurrency(primaryCurrency) + : flow.getTotalExpense(rates, primaryCurrency), type: TransactionType.expense, ), ), ], ), + if (missingRates) ...[ + const SizedBox(height: 12.0), + RatesMissingWarning(), + ], ], ); diff --git a/lib/routes/home/accounts_tab.dart b/lib/routes/home/accounts_tab.dart index ffc1550..09b7e4d 100644 --- a/lib/routes/home/accounts_tab.dart +++ b/lib/routes/home/accounts_tab.dart @@ -12,6 +12,7 @@ import "package:flow/widgets/account_card.dart"; import "package:flow/widgets/account_card_skeleton.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:flutter/material.dart"; import "package:go_router/go_router.dart"; import "package:material_symbols_icons/symbols.dart"; @@ -88,6 +89,10 @@ class _AccountsTabState extends State : ListView( padding: const EdgeInsets.all(16.0), children: [ + TotalBalance(), + const SizedBox(height: 16.0), + Divider(), + const SizedBox(height: 16.0), ...accounts.map( (account) => Padding( padding: const EdgeInsets.only( diff --git a/lib/routes/home/home_tab.dart b/lib/routes/home/home_tab.dart index 503e2bf..3f7a8b0 100644 --- a/lib/routes/home/home_tab.dart +++ b/lib/routes/home/home_tab.dart @@ -1,16 +1,19 @@ +import "package:flow/data/exchange_rates.dart"; import "package:flow/data/transactions_filter.dart"; import "package:flow/data/upcoming_transactions.dart"; import "package:flow/entity/transaction.dart"; -import "package:flow/objectbox.dart"; import "package:flow/objectbox/actions.dart"; import "package:flow/prefs.dart"; +import "package:flow/services/exchange_rates.dart"; import "package:flow/utils/optional.dart"; import "package:flow/widgets/default_transaction_filter_head.dart"; import "package:flow/widgets/general/wavy_divider.dart"; import "package:flow/widgets/grouped_transaction_list.dart"; import "package:flow/widgets/home/greetings_bar.dart"; +import "package:flow/widgets/home/home/flow_cards.dart"; import "package:flow/widgets/home/home/no_transactions.dart"; import "package:flow/widgets/home/transactions_date_header.dart"; +import "package:flow/widgets/rates_missing_warning.dart"; import "package:flow/widgets/utils/time_and_range.dart"; import "package:flutter/material.dart"; import "package:moment_dart/moment_dart.dart"; @@ -63,7 +66,6 @@ class _HomeTabState extends State with AutomaticKeepAliveClientMixin { @override void initState() { super.initState(); - noTransactionsAtAll = ObjectBox().box().count(limit: 1) == 0; _updatePlannedTransactionDays(); LocalPreferences() .homeTabPlannedTransactionsDuration @@ -85,6 +87,8 @@ class _HomeTabState extends State with AutomaticKeepAliveClientMixin { Widget build(BuildContext context) { super.build(context); + final bool isFilterModified = currentFilter != defaultFilter; + return StreamBuilder>( stream: currentFilterWithPlanned .queryBuilder() @@ -94,6 +98,8 @@ class _HomeTabState extends State with AutomaticKeepAliveClientMixin { ), builder: (context, snapshot) { final DateTime now = DateTime.now().startOfNextMinute(); + final ExchangeRates? rates = + ExchangeRatesService().getPrimaryCurrencyRates(); final List? transactions = snapshot.data; final Widget header = DefaultTransactionsFilterHead( @@ -118,12 +124,11 @@ class _HomeTabState extends State with AutomaticKeepAliveClientMixin { ), switch ((transactions?.length ?? 0, snapshot.hasData)) { (0, true) => Expanded( - child: NoTransactions( - allTime: noTransactionsAtAll, - ), + child: NoTransactions(isFilterModified: isFilterModified), ), (_, true) => Expanded( - child: buildGroupedList(context, now, transactions ?? []), + child: + buildGroupedList(context, now, transactions ?? [], rates), ), (_, false) => const Expanded( child: Center( @@ -141,6 +146,7 @@ class _HomeTabState extends State with AutomaticKeepAliveClientMixin { BuildContext context, DateTime now, List transactions, + ExchangeRates? rates, ) { final Map> grouped = transactions .where((transaction) => !transaction.transactionDate.isAfter(now)) @@ -153,6 +159,20 @@ class _HomeTabState extends State with AutomaticKeepAliveClientMixin { currentFilter.accounts?.isNotEmpty != true; return GroupedTransactionList( + header: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 12.0), + FlowCards( + transactions: transactions, + rates: rates, + ), + if (rates == null) ...[ + const SizedBox(height: 12.0), + RatesMissingWarning(), + ], + ], + ), controller: widget.scrollController, transactions: grouped, futureTransactions: groupedFuture, diff --git a/lib/routes/home/stats_tab.dart b/lib/routes/home/stats_tab.dart index d6dee48..13edb2a 100644 --- a/lib/routes/home/stats_tab.dart +++ b/lib/routes/home/stats_tab.dart @@ -207,6 +207,7 @@ class _StatsTabState extends State ChartData( key: entry.key, money: cache[entry.key]!, + currency: primaryCurrency, associatedData: entry.value.associatedData, ), ), diff --git a/lib/routes/new_transaction/amount_text.dart b/lib/routes/new_transaction/amount_text.dart index 5bd746f..52a8e7e 100644 --- a/lib/routes/new_transaction/amount_text.dart +++ b/lib/routes/new_transaction/amount_text.dart @@ -1,7 +1,9 @@ import "dart:math" as math; import "package:auto_size_text/auto_size_text.dart"; +import "package:flow/data/money.dart"; import "package:flow/l10n/extensions.dart"; +import "package:flow/prefs.dart"; import "package:flow/routes/new_transaction/input_amount_sheet/input_value.dart"; import "package:flow/theme/theme.dart"; import "package:flow/utils/utils.dart"; @@ -109,11 +111,14 @@ class _AmountTextState extends State } String amountText() { - final String formatted = currentValue.currentAmount.formatMoney( + final String currency = + widget.currency ?? LocalPreferences().getPrimaryCurrency(); + + final String formatted = + Money(currentValue.currentAmount, currency).formatMoney( decimalDigits: math.max(currentValue.decimalLength, _inputtingDecimal ? 1 : 0), includeCurrency: !widget.hideCurrencySymbol, - currency: widget.currency, ); if (currentValue.decimalLength == 0) { diff --git a/lib/routes/preferences/theme_preferences_page.dart b/lib/routes/preferences/theme_preferences_page.dart index d7ce187..5e61d0a 100644 --- a/lib/routes/preferences/theme_preferences_page.dart +++ b/lib/routes/preferences/theme_preferences_page.dart @@ -1,8 +1,12 @@ import "package:flow/l10n/extensions.dart"; import "package:flow/prefs.dart"; -import "package:flow/routes/preferences/theme_preferences/theme_entry.dart"; import "package:flow/theme/color_themes/registry.dart"; +import "package:flow/theme/helpers.dart"; +import "package:flow/widgets/general/list_header.dart"; +import "package:flow/widgets/theme_petal_selector.dart"; import "package:flutter/material.dart"; +import "package:material_symbols_icons/symbols.dart"; +// import "package:material_symbols_icons/symbols.dart"; class ThemePreferencesPage extends StatefulWidget { const ThemePreferencesPage({super.key}); @@ -11,81 +15,110 @@ class ThemePreferencesPage extends StatefulWidget { State createState() => _ThemePreferencesPageState(); } -class _ThemePreferencesPageState extends State - with SingleTickerProviderStateMixin { - late final TabController _tabController; - +class _ThemePreferencesPageState extends State { bool busy = false; + bool appIconBusy = false; + bool dynamicThemeBusy = false; @override void initState() { super.initState(); - _tabController = TabController( - length: 2, - vsync: this, - ); } @override Widget build(BuildContext context) { - final String? preferencesTheme = LocalPreferences().themeName.get(); - final String currentTheme = validateThemeName(preferencesTheme) - ? preferencesTheme! - : lightThemes.keys.first; + final String currentTheme = LocalPreferences().getCurrentTheme(); + final bool themeChangesAppIcon = + LocalPreferences().themeChangesAppIcon.get(); + // final bool enableDynamicTheme = LocalPreferences().enableDynamicTheme.get(); return Scaffold( appBar: AppBar( title: Text("preferences.theme.choose".t(context)), - bottom: TabBar( - tabs: [ - Tab( - text: "preferences.theme.light".t(context), - ), - Tab( - text: "preferences.theme.dark".t(context), - ), - ], - controller: _tabController, - ), ), body: SafeArea( - child: TabBarView( - controller: _tabController, - children: [ - ListView( - children: lightThemes.entries - .map( - (entry) => ThemeEntry( - entry: entry, - currentTheme: currentTheme, - handleChange: handleChange, - ), - ) - .toList(), - ), - ListView( - children: darkThemes.entries - .map( - (entry) => ThemeEntry( - entry: entry, - currentTheme: currentTheme, - handleChange: handleChange, - ), - ) - .toList(), - ), - ], + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: ThemePetalSelector( + updateOnHover: true, + ), + ), + const SizedBox(height: 16.0), + CheckboxListTile.adaptive( + title: Text("preferences.theme.themeChangesAppIcon".t(context)), + value: themeChangesAppIcon, + onChanged: changeThemeChangesAppIcon, + secondary: Icon(Symbols.photo_prints_rounded), + activeColor: context.colorScheme.primary, + ), + // CheckboxListTile.adaptive( + // title: Text("preferences.theme.enableDynamicTheme".t(context)), + // value: enableDynamicTheme, + // onChanged: changeEnableDynamicTheme, + // secondary: Icon(Symbols.palette), + // activeColor: context.colorScheme.primary, + // ), + const SizedBox(height: 16.0), + ListHeader( + "preferences.theme.other".t(context), + ), + RadioListTile.adaptive( + title: Text(palenight.name), + value: "palenight", + groupValue: currentTheme, + onChanged: (value) => handleChange(value), + activeColor: context.colorScheme.primary, + ), + ], + ), ), ), ); } + void changeThemeChangesAppIcon(bool? newValue) async { + if (newValue == null) return; + if (appIconBusy) return; + + try { + appIconBusy = true; + await LocalPreferences().themeChangesAppIcon.set(newValue); + trySetThemeIcon(newValue ? LocalPreferences().getCurrentTheme() : null); + } finally { + appIconBusy = false; + if (mounted) { + setState(() {}); + } + } + } + + void changeEnableDynamicTheme(bool? newValue) { + if (newValue == null) return; + if (dynamicThemeBusy) return; + + try { + dynamicThemeBusy = true; + LocalPreferences().enableDynamicTheme.set(newValue); + } finally { + dynamicThemeBusy = false; + if (mounted) { + setState(() {}); + } + } + } + void handleChange(String? name) async { if (name == null) return; if (busy) return; try { await LocalPreferences().themeName.set(name); + if (LocalPreferences().themeChangesAppIcon.get()) { + trySetThemeIcon(name); + } } finally { busy = false; diff --git a/lib/routes/preferences_page.dart b/lib/routes/preferences_page.dart index 89032d2..ce1841f 100644 --- a/lib/routes/preferences_page.dart +++ b/lib/routes/preferences_page.dart @@ -228,6 +228,13 @@ class _PreferencesPageState extends State { void openTheme() async { await context.push("/preferences/theme"); + final bool themeChangesAppIcon = + LocalPreferences().themeChangesAppIcon.get(); + + trySetThemeIcon( + themeChangesAppIcon ? LocalPreferences().themeName.get() : null, + ); + // Rebuild to update description text if (mounted) setState(() {}); } diff --git a/lib/routes/transaction_page.dart b/lib/routes/transaction_page.dart index c6da0a6..0adf56d 100644 --- a/lib/routes/transaction_page.dart +++ b/lib/routes/transaction_page.dart @@ -2,6 +2,7 @@ import "dart:developer"; import "dart:io"; import "package:flow/constants.dart"; +import "package:flow/data/money.dart"; import "package:flow/entity/account.dart"; import "package:flow/entity/category.dart"; import "package:flow/entity/transaction.dart"; @@ -169,6 +170,8 @@ class _TransactionPageState extends State { @override Widget build(BuildContext context) { + final String primaryCurrency = LocalPreferences().getPrimaryCurrency(); + return CallbackShortcuts( bindings: { const SingleActivator(LogicalKeyboardKey.escape): () => pop(), @@ -222,9 +225,11 @@ class _TransactionPageState extends State { onTap: inputAmount, child: Center( child: Text( - _amount.formatMoney( - currency: _selectedAccount?.currency, - ), + Money( + _amount, + _selectedAccount?.currency ?? + primaryCurrency) + .formatMoney(), style: context.textTheme.displayMedium, ), ), @@ -247,9 +252,7 @@ class _TransactionPageState extends State { "transaction.edit.selectAccount".t(context)), subtitle: _selectedAccount == null ? null - : Text(_selectedAccount!.balance.formatMoney( - currency: _selectedAccount!.currency, - )), + : Text(_selectedAccount!.balance.formatMoney()), onTap: () => selectAccount(), trailing: _selectedAccount == null ? const Icon(Symbols.chevron_right) @@ -274,10 +277,7 @@ class _TransactionPageState extends State { subtitle: _selectedAccountTransferTo == null ? null : Text(_selectedAccountTransferTo!.balance - .formatMoney( - currency: - _selectedAccountTransferTo!.currency, - )), + .formatMoney()), onTap: () => selectAccountTransferTo(), trailing: _selectedAccountTransferTo == null ? const Icon(Symbols.chevron_right) diff --git a/lib/theme/color_themes/registry.dart b/lib/theme/color_themes/registry.dart index 5ac5201..78f4de7 100644 --- a/lib/theme/color_themes/registry.dart +++ b/lib/theme/color_themes/registry.dart @@ -1,10 +1,12 @@ import "dart:developer"; +import "dart:io"; import "package:flow/theme/color_themes/default_darks.dart"; import "package:flow/theme/color_themes/default_lights.dart"; import "package:flow/theme/color_themes/palenight.dart"; import "package:flow/theme/flow_color_scheme.dart"; import "package:flutter/material.dart"; +import "package:flutter_dynamic_icon_plus/flutter_dynamic_icon_plus.dart"; export "default_darks.dart"; export "default_lights.dart"; @@ -46,12 +48,48 @@ final Map darkThemes = { "arcLight": arcLight, "driedLilac": driedLilac, "neonBoneyard": neonBoneyard, +}; + +final Map otherThemes = { "palenight": palenight, }; +final Map lightDarkThemeMapping = { + "shadeOfViolet": "electricLavender", + "blissfulBerry": "pinkQuartz", + "cherryPlum": "cottonCandy", + "crispChristmasCranberries": "piglet", + "burntSienna": "simplyDelicious", + "soilOfAvagddu": "creamyApricot", + "flagGreen": "yellYellow", + "tropicana": "fallGreen", + "toyCamouflage": "frostedMintHills", + "spreadsheetGreen": "coastalTrim", + "tokiwaGreen": "seafairGreen", + "hydraTurquoise": "crushedIce", + "peacockBlue": "iceEffect", + "egyptianBlue": "arcLight", + "bohemianBlue": "driedLilac", + "spaceBattleBlue": "neonBoneyard", +}; + +String? reverseThemeMode(String themeName) { + if (lightThemes.containsKey(themeName)) { + return lightDarkThemeMapping[themeName]; + } else if (darkThemes.containsKey(themeName)) { + return lightDarkThemeMapping.entries + .where((entry) => entry.value == themeName) + .firstOrNull + ?.key; + } + + return null; +} + final Map allThemes = { ...lightThemes, ...darkThemes, + ...otherThemes, }; bool validateThemeName(String? themeName) { @@ -60,15 +98,48 @@ bool validateThemeName(String? themeName) { return allThemes.containsKey(themeName); } +bool isThemeDark(String? themeName) { + if (themeName == null) return false; + + return darkThemes.containsKey(themeName); +} + ({FlowColorScheme scheme, ThemeMode mode})? getTheme(String? themeName) { if (themeName == null) return null; - final light = lightThemes[themeName]; - if (light != null) return (scheme: light, mode: ThemeMode.light); + final FlowColorScheme? scheme = allThemes[themeName]; - final dark = darkThemes[themeName]; - if (dark != null) return (scheme: dark, mode: ThemeMode.dark); + if (scheme == null) { + log("Unknown theme: $themeName"); + return null; + } - log("Unknown theme: $themeName"); - return null; + final mode = scheme.isDark ? ThemeMode.dark : ThemeMode.light; + + return (scheme: scheme, mode: mode); +} + +void trySetThemeIcon(String? name) async { + name ??= "shadeOfViolet"; + + if (!Platform.isIOS) return; + + late final String icon; + + if (lightThemes.containsKey(name)) { + icon = name; + } else if (darkThemes.containsKey(name)) { + icon = lightDarkThemeMapping.keys.firstWhere( + (key) => lightDarkThemeMapping[key] == name, + orElse: () => "shadeOfViolet", + ); + } else { + icon = "shadeOfViolet"; + } + + try { + await FlutterDynamicIconPlus.setAlternateIconName(iconName: icon); + } catch (e) { + log("Failed to set app icon: $e"); + } } diff --git a/lib/theme/helpers.dart b/lib/theme/helpers.dart index 9bd8bd4..22d3b78 100644 --- a/lib/theme/helpers.dart +++ b/lib/theme/helpers.dart @@ -25,9 +25,9 @@ extension TransactionTypeWidgetData on TransactionType { IconData get icon { switch (this) { case TransactionType.income: - return Symbols.stat_2_rounded; - case TransactionType.expense: return Symbols.stat_minus_2_rounded; + case TransactionType.expense: + return Symbols.stat_2_rounded; case TransactionType.transfer: return Symbols.compare_arrows_rounded; } diff --git a/lib/widgets/account_card.dart b/lib/widgets/account_card.dart index bf24fba..3e65030 100644 --- a/lib/widgets/account_card.dart +++ b/lib/widgets/account_card.dart @@ -1,5 +1,8 @@ +import "package:flow/data/money.dart"; import "package:flow/entity/account.dart"; +import "package:flow/entity/transaction.dart"; import "package:flow/l10n/extensions.dart"; +import "package:flow/l10n/named_enum.dart"; import "package:flow/objectbox/actions.dart"; import "package:flow/theme/theme.dart"; import "package:flow/utils/optional.dart"; @@ -8,6 +11,7 @@ import "package:flow/widgets/general/surface.dart"; import "package:flutter/cupertino.dart"; import "package:flutter/material.dart"; import "package:go_router/go_router.dart"; +import "package:moment_dart/moment_dart.dart"; class AccountCard extends StatelessWidget { final Account account; @@ -31,12 +35,24 @@ class AccountCard extends StatelessWidget { @override Widget build(BuildContext context) { + final DateTime now = DateTime.now(); + final double incomeSum = excludeTransfersInTotal - ? account.transactions.nonTransfers.incomeSum - : account.transactions.incomeSum; + ? account.transactions + .where((x) => x.transactionDate.isAtSameMonthAs(now)) + .nonTransfers + .incomeSum + : account.transactions + .where((x) => x.transactionDate.isAtSameMonthAs(now)) + .incomeSum; final double expenseSum = excludeTransfersInTotal - ? account.transactions.nonTransfers.expenseSum - : account.transactions.expenseSum; + ? account.transactions + .where((x) => x.transactionDate.isAtSameMonthAs(now)) + .nonTransfers + .expenseSum + : account.transactions + .where((x) => x.transactionDate.isAtSameMonthAs(now)) + .expenseSum; final child = Surface( shape: RoundedRectangleBorder(borderRadius: borderRadius), @@ -67,7 +83,7 @@ class AccountCard extends StatelessWidget { style: context.textTheme.titleSmall, ), Text( - account.balance.formatMoney(currency: account.currency), + account.balance.formatMoney(), style: context.textTheme.displaySmall, ), ], @@ -75,18 +91,19 @@ class AccountCard extends StatelessWidget { ], ), const SizedBox(height: 24.0), - Text("This month", style: context.textTheme.bodyLarge), + Text("account.thisMonth".t(context), + style: context.textTheme.bodyLarge), Row( children: [ Expanded( child: Text( - "Income", + TransactionType.income.localizedNameContext(context), style: context.textTheme.labelSmall?.semi(context), ), ), Expanded( child: Text( - "Expense", + TransactionType.expense.localizedNameContext(context), style: context.textTheme.labelSmall?.semi(context), ), ), @@ -96,17 +113,13 @@ class AccountCard extends StatelessWidget { children: [ Expanded( child: Text( - incomeSum.formatMoney( - currency: account.currency, - ), + Money(incomeSum, account.currency).formatMoney(), style: context.textTheme.bodyLarge, ), ), Expanded( child: Text( - expenseSum.formatMoney( - currency: account.currency, - ), + Money(expenseSum, account.currency).formatMoney(), style: context.textTheme.bodyLarge, ), ), diff --git a/lib/widgets/category/transactions_info.dart b/lib/widgets/category/transactions_info.dart index 384c25a..7e302c7 100644 --- a/lib/widgets/category/transactions_info.dart +++ b/lib/widgets/category/transactions_info.dart @@ -1,4 +1,5 @@ import "package:flow/data/flow_icon.dart"; +import "package:flow/data/money.dart"; import "package:flow/l10n/extensions.dart"; import "package:flow/theme/theme.dart"; import "package:flow/widgets/general/flow_icon.dart"; @@ -7,7 +8,7 @@ import "package:flutter/material.dart"; class TransactionsInfo extends StatelessWidget { final int? count; - final double flow; + final Money flow; final FlowIconData icon; diff --git a/lib/widgets/category_card.dart b/lib/widgets/category_card.dart index 8dfed5f..f13845f 100644 --- a/lib/widgets/category_card.dart +++ b/lib/widgets/category_card.dart @@ -1,6 +1,8 @@ +import "package:flow/data/money.dart"; import "package:flow/entity/category.dart"; import "package:flow/l10n/extensions.dart"; import "package:flow/objectbox/actions.dart"; +import "package:flow/prefs.dart"; import "package:flow/theme/theme.dart"; import "package:flow/utils/optional.dart"; import "package:flow/widgets/general/flow_icon.dart"; @@ -30,6 +32,8 @@ class CategoryCard extends StatelessWidget { @override Widget build(BuildContext context) { + final String primaryCurrency = LocalPreferences().getPrimaryCurrency(); + return Surface( shape: RoundedRectangleBorder(borderRadius: borderRadius), builder: (context) => InkWell( @@ -55,7 +59,7 @@ class CategoryCard extends StatelessWidget { ), if (showAmount) Text( - category.transactions.sum.money, + Money(category.transactions.sum, primaryCurrency).money, style: context.textTheme.bodyMedium?.semi(context), ), ], diff --git a/lib/widgets/flow_card.dart b/lib/widgets/flow_card.dart index 4a8b6bd..dd26616 100644 --- a/lib/widgets/flow_card.dart +++ b/lib/widgets/flow_card.dart @@ -1,4 +1,5 @@ import "package:auto_size_text/auto_size_text.dart"; +import "package:flow/data/money.dart"; import "package:flow/entity/transaction.dart"; import "package:flow/l10n/extensions.dart"; import "package:flow/theme/theme.dart"; @@ -7,7 +8,7 @@ import "package:flutter/material.dart"; class FlowCard extends StatelessWidget { final TransactionType type; - final double flow; + final Money flow; const FlowCard({super.key, required this.flow, required this.type}); diff --git a/lib/widgets/home/home/account/total_balance.dart b/lib/widgets/home/home/account/total_balance.dart new file mode 100644 index 0000000..128c29c --- /dev/null +++ b/lib/widgets/home/home/account/total_balance.dart @@ -0,0 +1,74 @@ +import "package:flow/data/exchange_rates.dart"; +import "package:flow/data/money.dart"; +import "package:flow/l10n/extensions.dart"; +import "package:flow/objectbox.dart"; +import "package:flow/objectbox/actions.dart"; +import "package:flow/prefs.dart"; +import "package:flow/services/exchange_rates.dart"; +import "package:flow/theme/theme.dart"; +import "package:flutter/material.dart"; + +class TotalBalance extends StatefulWidget { + const TotalBalance({super.key}); + + @override + State createState() => _TotalBalanceState(); +} + +class _TotalBalanceState extends State { + @override + void initState() { + super.initState(); + LocalPreferences().primaryCurrency.addListener(_refresh); + ExchangeRatesService().exchangeRatesCache.addListener(_refresh); + } + + @override + void dispose() { + LocalPreferences().primaryCurrency.removeListener(_refresh); + ExchangeRatesService().exchangeRatesCache.removeListener(_refresh); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final Money totalBalance = ObjectBox().getPrimaryCurrencyGrandTotal(); + final ExchangeRates? rates = + ExchangeRatesService().getPrimaryCurrencyRates(); + + return FutureBuilder( + future: rates == null ? null : ObjectBox().getGrandTotal(), + builder: (context, snapshot) { + final String value = snapshot.hasData + ? snapshot.data!.moneyCompact + : totalBalance.moneyCompact; + + return Padding( + padding: EdgeInsets.only(left: 12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "tabs.home.totalBalance".t(context), + style: context.textTheme.bodyLarge, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Flexible( + child: Text( + value, + style: context.textTheme.displayMedium, + ), + ), + ], + ), + ); + }, + ); + } + + void _refresh() { + setState(() {}); + } +} diff --git a/lib/widgets/home/home/analytics_card.dart b/lib/widgets/home/home/analytics_card.dart index cd183d3..ee21475 100644 --- a/lib/widgets/home/home/analytics_card.dart +++ b/lib/widgets/home/home/analytics_card.dart @@ -4,11 +4,15 @@ import "package:flutter/material.dart"; class AnalyticsCard extends StatelessWidget { final Widget child; - static const borderRadius = BorderRadius.all( - Radius.circular(24.0), - ); + final BorderRadius borderRadius; - const AnalyticsCard({super.key, required this.child}); + const AnalyticsCard({ + super.key, + required this.child, + this.borderRadius = const BorderRadius.all( + Radius.circular(16.0), + ), + }); @override Widget build(BuildContext context) { @@ -18,7 +22,7 @@ class AnalyticsCard extends StatelessWidget { borderRadius: borderRadius, child: child, ), - shape: const RoundedRectangleBorder(borderRadius: borderRadius), + shape: RoundedRectangleBorder(borderRadius: borderRadius), ); } } diff --git a/lib/widgets/home/home/flow_cards.dart b/lib/widgets/home/home/flow_cards.dart new file mode 100644 index 0000000..25d5e68 --- /dev/null +++ b/lib/widgets/home/home/flow_cards.dart @@ -0,0 +1,70 @@ +import "package:flow/data/exchange_rates.dart"; +import "package:flow/data/money_flow.dart"; +import "package:flow/entity/transaction.dart"; +import "package:flow/l10n/extensions.dart"; +import "package:flow/l10n/named_enum.dart"; +import "package:flow/objectbox/actions.dart"; +import "package:flow/prefs.dart"; +import "package:flow/theme/theme.dart"; +import "package:flow/widgets/home/home/info_card.dart"; +import "package:flutter/cupertino.dart"; + +class FlowCards extends StatefulWidget { + final List? transactions; + final ExchangeRates? rates; + + const FlowCards({super.key, required this.transactions, required this.rates}); + + @override + State createState() => _FlowCardsState(); +} + +class _FlowCardsState extends State { + @override + Widget build(BuildContext context) { + final MoneyFlow? flow = widget.transactions?.flow; + final String primaryCurrency = LocalPreferences().getPrimaryCurrency(); + + final String expensesText = switch ((flow, widget.rates)) { + (null, _) => "-", + (MoneyFlow moneyFlow, null) => + moneyFlow.getExpenseByCurrency(primaryCurrency).moneyCompact, + (MoneyFlow moneyFlow, ExchangeRates exchangeRates) => + moneyFlow.getTotalExpense(exchangeRates, primaryCurrency).moneyCompact, + }; + + final String incomesText = switch ((flow, widget.rates)) { + (null, _) => "-", + (MoneyFlow moneyFlow, null) => + moneyFlow.getIncomeByCurrency(primaryCurrency).moneyCompact, + (MoneyFlow moneyFlow, ExchangeRates exchangeRates) => + moneyFlow.getTotalIncome(exchangeRates, primaryCurrency).moneyCompact, + }; + + return Row( + children: [ + Expanded( + child: InfoCard( + title: TransactionType.income.localizedNameContext(context), + value: incomesText, + trailing: Icon( + TransactionType.income.icon, + color: TransactionType.income.color(context), + ), + ), + ), + const SizedBox(width: 16.0), + Expanded( + child: InfoCard( + title: TransactionType.expense.localizedNameContext(context), + value: expensesText, + trailing: Icon( + TransactionType.expense.icon, + color: TransactionType.expense.color(context), + ), + ), + ), + ], + ); + } +} diff --git a/lib/widgets/home/home/flow_today_card.dart b/lib/widgets/home/home/flow_today_card.dart index acc26e4..722d082 100644 --- a/lib/widgets/home/home/flow_today_card.dart +++ b/lib/widgets/home/home/flow_today_card.dart @@ -1,6 +1,8 @@ +import "package:flow/data/money.dart"; import "package:flow/entity/transaction.dart"; import "package:flow/l10n/flow_localizations.dart"; import "package:flow/objectbox/actions.dart"; +import "package:flow/prefs.dart"; import "package:flow/theme/theme.dart"; import "package:flow/widgets/home/home/analytics_card.dart"; import "package:flutter/material.dart"; @@ -13,6 +15,8 @@ class FlowTodayCard extends StatelessWidget { @override Widget build(BuildContext context) { + final String primaryCurrency = LocalPreferences().getPrimaryCurrency(); + final double flow = transactions == null ? 0 : transactions! @@ -40,7 +44,7 @@ class FlowTodayCard extends StatelessWidget { ), Flexible( child: Text( - flow.moneyCompact, + Money(flow, primaryCurrency).moneyCompact, style: context.textTheme.displaySmall, ), ), diff --git a/lib/widgets/home/home/info_card.dart b/lib/widgets/home/home/info_card.dart new file mode 100644 index 0000000..b9ed278 --- /dev/null +++ b/lib/widgets/home/home/info_card.dart @@ -0,0 +1,57 @@ +import "package:flow/theme/theme.dart"; +import "package:flow/widgets/general/surface.dart"; +import "package:flutter/cupertino.dart"; + +class InfoCard extends StatelessWidget { + final String title; + final String value; + + final Widget? trailing; + + const InfoCard({ + super.key, + required this.title, + required this.value, + this.trailing, + }); + + @override + Widget build(BuildContext context) { + return Surface( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16.0)), + builder: (BuildContext context) => Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 12.0, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + title, + style: context.textTheme.bodyMedium, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Row( + children: [ + Flexible( + child: Text( + value, + style: context.textTheme.displaySmall, + ), + ), + if (trailing != null) ...[ + const SizedBox(width: 8.0), + trailing!, + ], + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/home/home/no_transactions.dart b/lib/widgets/home/home/no_transactions.dart index d0534e1..f26c83e 100644 --- a/lib/widgets/home/home/no_transactions.dart +++ b/lib/widgets/home/home/no_transactions.dart @@ -6,9 +6,9 @@ import "package:flutter/material.dart"; import "package:material_symbols_icons/symbols.dart"; class NoTransactions extends StatelessWidget { - final bool allTime; + final bool isFilterModified; - const NoTransactions({super.key, this.allTime = false}); + const NoTransactions({super.key, required this.isFilterModified}); @override Widget build(BuildContext context) { @@ -19,9 +19,7 @@ class NoTransactions extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Text( - allTime - ? "tabs.home.noTransactions.allTime".t(context) - : "tabs.home.noTransactions.last7Days".t(context), + "tabs.home.noTransactions".t(context), textAlign: TextAlign.center, style: context.textTheme.headlineSmall, ), @@ -33,7 +31,9 @@ class NoTransactions extends StatelessWidget { ), const SizedBox(height: 8.0), Text( - "tabs.home.noTransactions.addSome".t(context), + isFilterModified + ? "tabs.home.noTransactions.tryChangingFilters".t(context) + : "tabs.home.noTransactions.addSome".t(context), textAlign: TextAlign.center, ), ], diff --git a/lib/widgets/home/home/total_balance_card.dart b/lib/widgets/home/home/total_balance_card.dart deleted file mode 100644 index 2fb70a2..0000000 --- a/lib/widgets/home/home/total_balance_card.dart +++ /dev/null @@ -1,40 +0,0 @@ -import "package:flow/l10n/flow_localizations.dart"; -import "package:flow/objectbox.dart"; -import "package:flow/objectbox/actions.dart"; -import "package:flow/theme/theme.dart"; -import "package:flow/widgets/home/home/analytics_card.dart"; -import "package:flutter/material.dart"; - -class TotalBalanceCard extends StatelessWidget { - const TotalBalanceCard({super.key}); - - @override - Widget build(BuildContext context) { - return AnalyticsCard( - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 12.0, - ), - width: double.infinity, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "tabs.home.totalBalance".t(context), - style: context.textTheme.bodyLarge, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - Flexible( - child: Text( - ObjectBox().getTotalBalance().moneyCompact, - style: context.textTheme.displaySmall, - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/widgets/home/stats/group_pie_chart.dart b/lib/widgets/home/stats/group_pie_chart.dart index 6644044..c52bdb6 100644 --- a/lib/widgets/home/stats/group_pie_chart.dart +++ b/lib/widgets/home/stats/group_pie_chart.dart @@ -5,6 +5,7 @@ import "package:fl_chart/fl_chart.dart"; import "package:flow/data/chart_data.dart"; import "package:flow/data/exchange_rates.dart"; import "package:flow/data/flow_icon.dart"; +import "package:flow/data/money.dart"; import "package:flow/entity/account.dart"; import "package:flow/entity/category.dart"; import "package:flow/l10n/extensions.dart"; @@ -47,10 +48,10 @@ class GroupPieChart extends StatefulWidget { class _GroupPieChartState extends State> { late Map> data; - double get totalValue { - return data.values.fold( - 0.0, - (previousValue, element) => previousValue + element.money.amount, + Money get totalAmount { + return data.values.fold( + Money(0, data.values.first.money.currency), + (previousValue, element) => previousValue + element.money, ); } @@ -76,7 +77,7 @@ class _GroupPieChartState extends State> { selectedKey == null ? null : data[selectedKey!]; final String selectedSectionTotal = - selectedSection?.money.amount.abs().formatMoney() ?? "-"; + selectedSection?.money.abs().formatMoney() ?? "-"; return Column( mainAxisSize: MainAxisSize.min, @@ -87,7 +88,7 @@ class _GroupPieChartState extends State> { style: context.textTheme.labelMedium, ), Text( - totalValue.formatMoney(), + totalAmount.formatMoney(), style: context.textTheme.headlineMedium, ), Padding( @@ -219,7 +220,7 @@ class _GroupPieChartState extends State> { data.associatedData, color: color, backgroundColor: backgroundColor, - percent: data.displayTotal / totalValue, + percent: data.displayTotal / totalAmount.amount, ) : null, badgePositionPercentageOffset: 0.8, diff --git a/lib/widgets/home/transactions_date_header.dart b/lib/widgets/home/transactions_date_header.dart index 5d63cd2..8fe981c 100644 --- a/lib/widgets/home/transactions_date_header.dart +++ b/lib/widgets/home/transactions_date_header.dart @@ -1,6 +1,8 @@ +import "package:flow/data/money.dart"; import "package:flow/entity/transaction.dart"; import "package:flow/l10n/extensions.dart"; import "package:flow/objectbox/actions.dart"; +import "package:flow/prefs.dart"; import "package:flow/theme/theme.dart"; import "package:flutter/widgets.dart"; import "package:moment_dart/moment_dart.dart"; @@ -35,7 +37,13 @@ class TransactionListDateHeader extends StatelessWidget { return title; } - final double flow = transactions.sum; + final String primaryCurrency = LocalPreferences().getPrimaryCurrency(); + + final double flow = transactions + .where((transaction) => transaction.currency == primaryCurrency) + .sum; + final bool containsNonPrimaryCurrency = transactions + .any((transaction) => transaction.currency != primaryCurrency); return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -43,7 +51,7 @@ class TransactionListDateHeader extends StatelessWidget { children: [ title, Text( - "${flow.moneyCompact} • ${'tabs.home.transactionsCount'.t(context, transactions.renderableCount)}", + "${Money(flow, primaryCurrency).moneyCompact}${containsNonPrimaryCurrency ? '+' : ''} • ${'tabs.home.transactionsCount'.t(context, transactions.renderableCount)}", style: context.textTheme.labelMedium, ), ], diff --git a/lib/widgets/rates_missing_warning.dart b/lib/widgets/rates_missing_warning.dart new file mode 100644 index 0000000..392401c --- /dev/null +++ b/lib/widgets/rates_missing_warning.dart @@ -0,0 +1,85 @@ +import "package:flow/l10n/extensions.dart"; +import "package:flow/prefs.dart"; +import "package:flow/services/exchange_rates.dart"; +import "package:flow/theme/theme.dart"; +import "package:flow/utils/extensions/toast.dart"; +import "package:flow/widgets/general/spinner.dart"; +import "package:flutter/material.dart"; +import "package:material_symbols_icons/symbols.dart"; + +class RatesMissingWarning extends StatefulWidget { + const RatesMissingWarning({super.key}); + + @override + State createState() => _RatesMissingWarningState(); +} + +class _RatesMissingWarningState extends State { + bool busy = false; + + @override + Widget build(BuildContext context) { + return InkWell( + borderRadius: BorderRadius.all(Radius.circular(8.0)), + onTap: fetch, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + Symbols.error_circle_rounded_error_rounded, + fill: 0, + color: context.colorScheme.error, + size: 24.0, + ), + const SizedBox(width: 12.0), + Expanded( + child: DefaultTextStyle( + style: context.textTheme.bodyMedium! + .semi(context) + .copyWith(color: context.colorScheme.error), + child: Text("error.exchangeRates.inaccurateDataDueToMissingRates" + .t(context)), + ), + ), + const SizedBox(width: 12.0), + busy + ? SizedBox( + width: 24.0, + height: 24.0, + child: Spinner(), + ) + : Icon( + Symbols.refresh_rounded, + fill: 0, + size: 24.0, + color: context.colorScheme.error, + ), + ], + ), + ); + } + + void fetch() async { + if (busy) return; + + setState(() { + busy = true; + }); + + try { + await ExchangeRatesService() + .fetchRates(LocalPreferences().getPrimaryCurrency()); + } catch (e) { + if (mounted) { + context.showErrorToast( + error: "error.exchangeRates.cannotFetch".t(context), + ); + } + } finally { + busy = false; + if (mounted) { + setState(() {}); + } + } + } +} diff --git a/lib/widgets/setup/accounts/account_preset_card.dart b/lib/widgets/setup/accounts/account_preset_card.dart index 18da393..235bb36 100644 --- a/lib/widgets/setup/accounts/account_preset_card.dart +++ b/lib/widgets/setup/accounts/account_preset_card.dart @@ -57,7 +57,7 @@ class AccountPresetCard extends StatelessWidget { ), Text( account.balance - .formatMoney(currency: account.currency), + .formatMoney(), style: context.textTheme.displaySmall, ), ], diff --git a/lib/widgets/theme_petal_selector.dart b/lib/widgets/theme_petal_selector.dart new file mode 100644 index 0000000..6c988a9 --- /dev/null +++ b/lib/widgets/theme_petal_selector.dart @@ -0,0 +1,291 @@ +import "dart:developer" show log; +import "dart:math" as math; + +import "package:flow/prefs.dart"; +import "package:flow/theme/color_themes/registry.dart"; +import "package:flow/theme/theme.dart"; +import "package:flow/widgets/theme_petal_selector/theme_petal_painter.dart"; +import "package:flutter/material.dart"; +import "package:material_symbols_icons/symbols.dart"; + +class ThemePetalSelector extends StatefulWidget { + final bool playInitialAnimation; + + final double maxSize; + + final Duration animationDuration; + final Duration animationStartDelay; + + final bool updateOnHover; + + const ThemePetalSelector({ + super.key, + this.playInitialAnimation = true, + this.updateOnHover = false, + this.maxSize = 400.0, + this.animationStartDelay = const Duration(milliseconds: 250), + this.animationDuration = const Duration(milliseconds: 1000), + }); + + @override + State createState() => _ThemePetalSelectorState(); +} + +class _ThemePetalSelectorState extends State + with SingleTickerProviderStateMixin { + late final AnimationController animationController; + late final Animation flowerAnimation; + late final Animation opacityAnimation; + + MouseCursor _cursor = MouseCursor.defer; + + int? hoveringIndex; + + bool busy = false; + + static const double petalRadiusProc = 0.05; + static const double centerSpaceRadiusProc = 0.3; + static const double angleOffset = math.pi / -2; + + @override + void initState() { + super.initState(); + + animationController = AnimationController( + vsync: this, + duration: widget.animationDuration, + ); + + flowerAnimation = CurvedAnimation( + parent: animationController, + curve: Interval( + 0, + 0.8, + curve: Curves.easeIn, + ), + ); + opacityAnimation = CurvedAnimation( + parent: animationController, + curve: Interval( + 0.8, + 1.0, + curve: Curves.decelerate, + ), + ); + + Future.delayed(widget.animationStartDelay).then( + (_) { + if (!mounted) return; + + animationController.reset(); + animationController.forward(from: 0.0); + }, + ); + } + + @override + void dispose() { + animationController.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final String currentTheme = LocalPreferences().getCurrentTheme(); + final bool isDark = isThemeDark(currentTheme); + final int selectedIndex = isDark + ? lightDarkThemeMapping.values.toList().indexOf(currentTheme) + : lightDarkThemeMapping.keys.toList().indexOf(currentTheme); + + return ConstrainedBox( + constraints: BoxConstraints.tightForFinite( + width: widget.maxSize, + height: widget.maxSize, + ), + child: AspectRatio( + aspectRatio: 1.0, + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + final double middleButtonSize = + constraints.maxWidth * petalRadiusProc * 3.0; + + return Stack( + children: [ + SizedBox( + width: constraints.maxWidth, + height: constraints.maxHeight, + child: MouseRegion( + cursor: _cursor, + onHover: (event) { + final int? itemIndexAtPointer = + getItemAtPointer(event.localPosition, constraints); + + _cursor = itemIndexAtPointer != null + ? SystemMouseCursors.click + : MouseCursor.defer; + + if (itemIndexAtPointer != null && widget.updateOnHover) { + setThemeByIndex(itemIndexAtPointer, isDark); + } + + setState(() { + hoveringIndex = itemIndexAtPointer; + }); + }, + onExit: (_) => setState(() => hoveringIndex = null), + child: GestureDetector( + onPanUpdate: widget.updateOnHover + ? ((details) { + final int? itemIndexAtPointer = getItemAtPointer( + details.localPosition, + constraints, + ); + + if (itemIndexAtPointer == null) return; + + setThemeByIndex(itemIndexAtPointer, isDark); + }) + : null, + onTapUp: (details) { + final int? itemIndexAtPointer = getItemAtPointer( + details.localPosition, + constraints, + ); + + if (itemIndexAtPointer == null) return; + + setThemeByIndex(itemIndexAtPointer, isDark); + }, + child: AnimatedBuilder( + builder: (context, child) => CustomPaint( + painter: ThemePetalPainter( + animationValue: flowerAnimation.value, + colors: (isDark ? darkThemes : lightThemes) + .values + .map((theme) => theme.primary) + .toList(), + selectedIndex: selectedIndex, + hoveringIndex: hoveringIndex, + petalRadiusProc: petalRadiusProc, + centerSpaceRadiusProc: centerSpaceRadiusProc, + selectedColor: context.colorScheme.onSurface, + angleOffset: angleOffset, + ), + ), + animation: animationController, + ), + ), + ), + ), + AnimatedBuilder( + builder: (context, child) => SizedBox( + width: constraints.maxWidth, + height: constraints.maxHeight, + child: Opacity( + opacity: opacityAnimation.value, + child: Center( + child: InkWell( + onTap: () => switchThemeMode(currentTheme), + borderRadius: BorderRadius.circular(999.0), + child: Container( + width: middleButtonSize, + height: middleButtonSize, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: context.colorScheme.primary, + ), + alignment: Alignment.center, + child: Icon( + isDark + ? Symbols.dark_mode_rounded + : Symbols.light_mode_rounded, + size: middleButtonSize * 0.67, + color: context.colorScheme.onPrimary, + ), + ), + ), + ), + ), + ), + animation: animationController, + ), + ], + ); + }, + ), + ), + ); + } + + void setThemeByIndex(int index, bool dark) async { + if (busy) return; + + try { + setState(() { + busy = true; + }); + + final String themeName = dark + ? lightDarkThemeMapping.values.elementAt(index) + : lightDarkThemeMapping.keys.elementAt(index); + + await LocalPreferences().themeName.set(themeName); + } catch (e) { + log("[Theme Petal Selector] Something went wrong with the theme petal selector.", + error: e); + } finally { + busy = false; + if (mounted) { + setState(() {}); + } + } + } + + void switchThemeMode(String currentTheme) async { + if (busy) return; + + try { + setState(() { + busy = true; + }); + + final String? themeName = reverseThemeMode(currentTheme); + + if (themeName == null) { + return; + } + + await LocalPreferences().themeName.set(themeName); + } catch (e) { + log("[Theme Petal Selector] Something went wrong with the theme petal selector.", + error: e); + } finally { + busy = false; + if (mounted) { + setState(() {}); + } + } + } + + int? getItemAtPointer(Offset localPosition, BoxConstraints constraints) { + final Offset adjustedPosition = localPosition - + Offset(constraints.maxWidth / 2, constraints.maxHeight / 2); + final double r = adjustedPosition.distance; + + final double innerRadius = constraints.maxWidth * centerSpaceRadiusProc; + final double outerRadius = + innerRadius + (constraints.maxWidth * 2 * petalRadiusProc); + + if (r < innerRadius || r > outerRadius) { + return null; + } + + final double angle = (math.atan2(adjustedPosition.dy, adjustedPosition.dx) + + (math.pi * 3) + + angleOffset) % + (math.pi * 2); + + return (angle / (math.pi / 8.0)).round(); + } +} diff --git a/lib/widgets/theme_petal_selector/theme_petal_painter.dart b/lib/widgets/theme_petal_selector/theme_petal_painter.dart new file mode 100644 index 0000000..7799aca --- /dev/null +++ b/lib/widgets/theme_petal_selector/theme_petal_painter.dart @@ -0,0 +1,91 @@ +import "dart:math" as math; + +import "package:flutter/material.dart"; + +class ThemePetalPainter extends CustomPainter { + final double animationValue; + + final double petalRadiusProc; + + final double centerSpaceRadiusProc; + + final double angleOffset; + + final List colors; + + final Color selectedColor; + + final int? selectedIndex; + final int? hoveringIndex; + + const ThemePetalPainter({ + required this.animationValue, + required this.colors, + required this.angleOffset, + required this.petalRadiusProc, + required this.centerSpaceRadiusProc, + required this.selectedColor, + required this.selectedIndex, + required this.hoveringIndex, + }); + + @override + void paint(Canvas canvas, Size size) { + canvas.translate(size.width * 0.5, size.height * 0.5); + + final Paint paint = Paint()..style = PaintingStyle.fill; + + final Paint ringPaint = Paint() + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round + ..strokeJoin = StrokeJoin.round + ..strokeWidth = 2.0 + ..color = selectedColor; + + final double petalCenterDistance = + size.width * (petalRadiusProc + centerSpaceRadiusProc); + final double petalRadius = size.width * petalRadiusProc; + final double ringRadius = petalRadius + 4.0; + + final double angleDelta = math.pi * 2 / colors.length; + + for (int i = 0; i < colors.length; i++) { + final double localProgress = + math.min(1, math.max(0, animationValue * colors.length - i)); + + final double theta = angleOffset + angleDelta * (i - 1 + localProgress); + + final Offset center = Offset(math.cos(theta), math.sin(theta)) * + petalCenterDistance * + localProgress; + + paint.color = i == hoveringIndex + ? Color.alphaBlend(selectedColor.withAlpha(0x80), colors[i]) + : colors[i]; + canvas.drawCircle( + center, + petalRadius * localProgress, + paint, + ); + + if (localProgress == 1 && selectedIndex == i) { + canvas.drawCircle( + center, + ringRadius, + ringPaint, + ); + } + } + } + + @override + bool shouldRepaint(ThemePetalPainter oldDelegate) => + animationValue != oldDelegate.animationValue || + colors != oldDelegate.colors || + angleOffset != oldDelegate.angleOffset || + petalRadiusProc != oldDelegate.petalRadiusProc || + centerSpaceRadiusProc != oldDelegate.centerSpaceRadiusProc || + selectedColor != oldDelegate.selectedColor || + selectedIndex != oldDelegate.selectedIndex || + hoveringIndex != oldDelegate.hoveringIndex; +} diff --git a/lib/widgets/transaction_list_tile.dart b/lib/widgets/transaction_list_tile.dart index c27b691..fcd1b95 100644 --- a/lib/widgets/transaction_list_tile.dart +++ b/lib/widgets/transaction_list_tile.dart @@ -110,8 +110,7 @@ class TransactionListTile extends StatelessWidget { Widget _buildAmountText(BuildContext context) { return Text( - transaction.amount.formatMoney( - currency: transaction.currency, + transaction.money.formatMoney( takeAbsoluteValue: transaction.isTransfer && combineTransfers, ), style: context.textTheme.bodyLarge?.copyWith( diff --git a/pubspec.lock b/pubspec.lock index 233bfd5..0909edc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -395,6 +395,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_dynamic_icon_plus: + dependency: "direct main" + description: + name: flutter_dynamic_icon_plus + sha256: a5bea99437c7b036e6047694a798930244eacd01bd9a348d771e8df84be14202 + url: "https://pub.dev" + source: hosted + version: "1.2.1" flutter_floating_bottom_bar: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index b9ee9b5..de29b71 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.7.2+63" +version: "0.8.0+70" environment: sdk: ">=3.5.0 <4.0.0" @@ -23,6 +23,7 @@ dependencies: fl_chart: ^0.69.0 flutter: sdk: flutter + flutter_dynamic_icon_plus: ^1.2.1 flutter_floating_bottom_bar: ^1.2.0 flutter_localizations: sdk: flutter @@ -31,7 +32,7 @@ dependencies: flutter_slidable: ^3.0.1 flutter_staggered_grid_view: ^0.7.0 flutter_typeahead: ^5.2.0 - fuzzywuzzy: ^1.1.6 + fuzzywuzzy: ^1.2.0 geolocator: ^13.0.1 go_router: ^14.2.1 http: ^1.2.2 @@ -44,8 +45,8 @@ dependencies: mask_text_input_formatter: ^2.8.0 material_symbols_icons: ^4.2719.1 moment_dart: ^2.2.1+beta.0 - objectbox: ^4.0.1 - objectbox_flutter_libs: ^4.0.1 + objectbox: ^4.0.3 + objectbox_flutter_libs: ^4.0.3 package_info_plus: ^8.0.0 path: ^1.8.3 path_provider: ^2.1.1 diff --git a/test/database_test.dart b/test/database_test.dart index 701f0c4..4f71bcc 100644 --- a/test/database_test.dart +++ b/test/database_test.dart @@ -1,5 +1,6 @@ import "dart:io"; +import "package:flow/data/money.dart"; import "package:flow/entity/account.dart"; import "package:flow/entity/transaction.dart"; import "package:flow/objectbox.dart"; @@ -103,6 +104,136 @@ void main() { expect(() => txn!.setAccount(usdAccount), throwsException); }); + test("Creating an income should increase account balance", () async { + final Query accountQuery = + ObjectBox().box().query().build(); + + late Account account; + + account = (await accountQuery.findFirstAsync())!; + + final Money initialBalance = account.balance; + + final int txnId = account.createAndSaveTransaction( + amount: 1000.0, + title: "Income", + ); + + try { + account = (await accountQuery.findFirstAsync())!; + } finally { + accountQuery.close(); + } + + expect( + account.balance, + equals( + initialBalance + Money(1000.0, initialBalance.currency), + ), + ); + + final Transaction txn = + (await ObjectBox().box().getAsync(txnId))!; + + expect(txn.amount, equals(1000.0)); + expect(txn.type, equals(TransactionType.income)); + expect(txn.uuid, isNotNull); + }); + + test("Creating an expense should decrease account balance", () async { + final Query accountQuery = + ObjectBox().box().query().build(); + + late Account account; + + account = (await accountQuery.findFirstAsync())!; + + final Money initialBalance = account.balance; + + final int txnId = account.createAndSaveTransaction( + amount: -1000.0, + title: "Expense", + ); + + try { + account = (await accountQuery.findFirstAsync())!; + } finally { + accountQuery.close(); + } + + expect( + account.balance, + equals( + initialBalance - Money(1000.0, initialBalance.currency), + ), + ); + + final Transaction txn = + (await ObjectBox().box().getAsync(txnId))!; + + expect(txn.amount, equals(-1000.0)); + expect(txn.type, equals(TransactionType.expense)); + expect(txn.uuid, isNotNull); + }); + + test( + "Creating a transfer should move money from one account to another", + () async { + final Query accountQuery = ObjectBox() + .box() + .query(Account_.currency.equals("MNT")) + .build(); + + late Account mntAccount1; + late Account mntAccount2; + + final mntAccounts = await accountQuery.findAsync(); + + mntAccount1 = mntAccounts[0]; + mntAccount2 = mntAccounts[1]; + + final Money initialBalanceAccount1 = mntAccount1.balance; + final Money initialBalanceAccount2 = mntAccount2.balance; + + final (fromTxnId, toTxnId) = + mntAccount1.transferTo(targetAccount: mntAccount2, amount: 1000.0); + + final Transaction? fromTxn = + await ObjectBox().box().getAsync(fromTxnId); + final Transaction? toTxn = + await ObjectBox().box().getAsync(toTxnId); + + expect(fromTxn!.amount, equals(-1000.0)); + expect(fromTxn.type, equals(TransactionType.transfer)); + expect(fromTxn.uuid, isNotNull); + + expect(toTxn!.amount, equals(1000.0)); + expect(toTxn.type, equals(TransactionType.transfer)); + expect(toTxn.uuid, isNotNull); + + expect(toTxn.amount, equals(-fromTxn.amount)); + + final mntAccountsUpdated = await accountQuery.findAsync(); + accountQuery.close(); + + mntAccount1 = mntAccountsUpdated[0]; + mntAccount2 = mntAccountsUpdated[1]; + + expect( + mntAccount1.balance, + equals( + initialBalanceAccount1 - Money(1000, "MNT"), + ), + ); + expect( + mntAccount2.balance, + equals( + initialBalanceAccount2 + Money(1000, "MNT"), + ), + ); + }, + ); + tearDownAll(() async { await testCleanupObject( instance: ObjectBox(),