From 4dd582a1dff3cd7cdcd1cfb61c1172afc2d1fef1 Mon Sep 17 00:00:00 2001 From: Krzysztof Rudnicki Date: Mon, 3 Aug 2026 11:38:01 +0200 Subject: [PATCH] Move billsplit Flutter app into the monorepo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit billsplit was a standalone Flutter app in ~/Downloads with one commit and no git remote — that directory was the only copy of its history. Three things the move needed beyond copying the tree: - The root .gitignore silently swallowed 32 of its files (*.png, *.ico, *.manifest). Ignored files do not error on `git add`, they just vanish, so this would have committed an app that cannot build. Added a trailing block of ! overrides plus .binary-allowlist (which did not exist yet) — the documented in-repo exception for binaries a build genuinely needs. The ../testsAndMisc_binaries/ symlink scheme does not apply: those directories are gone, and a symlinked PNG breaks a fresh clone. - billsplit/.github/workflows/ci.yaml was inert once nested; GitHub only reads workflows at the repository root. Relocated with a working-directory and a billsplit/** path filter. - The app's coverage gate moved to python_pkg/billsplit_coverage/ so every Python file stays under python_pkg/, and gained a test suite at 100% branch coverage as that location requires. billsplit/ now holds only Dart. Also replaces the stock Flutter icon with a family icon from the shared generator. The glyph is a filled silhouette because the family's 72px stroke cannot carry a fine torn edge, and the split is an evenodd-cut perforation so the holes stay transparent on the foreground and monochrome layers. Co-Authored-By: Claude Opus 5 (1M context) --- billsplit/.gitignore | 45 + billsplit/.metadata | 42 + billsplit/README.md | 77 ++ billsplit/analysis_options.yaml | 7 + billsplit/android/.gitignore | 14 + billsplit/android/app/build.gradle.kts | 45 + .../android/app/src/debug/AndroidManifest.xml | 7 + .../android/app/src/main/AndroidManifest.xml | 45 + .../kotlin/dev/kuhy/billsplit/MainActivity.kt | 5 + .../drawable-hdpi/ic_launcher_foreground.png | Bin 0 -> 2796 bytes .../drawable-hdpi/ic_launcher_monochrome.png | Bin 0 -> 2664 bytes .../drawable-mdpi/ic_launcher_foreground.png | Bin 0 -> 1823 bytes .../drawable-mdpi/ic_launcher_monochrome.png | Bin 0 -> 1707 bytes .../res/drawable-v21/launch_background.xml | 12 + .../drawable-xhdpi/ic_launcher_foreground.png | Bin 0 -> 3775 bytes .../drawable-xhdpi/ic_launcher_monochrome.png | Bin 0 -> 3358 bytes .../ic_launcher_foreground.png | Bin 0 -> 5627 bytes .../ic_launcher_monochrome.png | Bin 0 -> 4872 bytes .../ic_launcher_foreground.png | Bin 0 -> 8050 bytes .../ic_launcher_monochrome.png | Bin 0 -> 6824 bytes .../main/res/drawable/launch_background.xml | 12 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 14 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 959 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 519 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 984 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1656 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 2032 bytes .../app/src/main/res/values-night/styles.xml | 18 + .../app/src/main/res/values/colors.xml | 4 + .../app/src/main/res/values/styles.xml | 18 + .../app/src/profile/AndroidManifest.xml | 7 + billsplit/android/build.gradle.kts | 24 + billsplit/android/gradle.properties | 6 + .../gradle/wrapper/gradle-wrapper.properties | 5 + billsplit/android/settings.gradle.kts | 26 + billsplit/assets/icon/icon.png | Bin 0 -> 13540 bytes billsplit/assets/icon/icon.svg | 6 + billsplit/assets/icon/icon_foreground.png | Bin 0 -> 14136 bytes billsplit/assets/icon/icon_foreground.svg | 5 + billsplit/assets/icon/icon_monochrome.png | Bin 0 -> 12557 bytes billsplit/assets/icon/icon_monochrome.svg | 5 + billsplit/ios/.gitignore | 34 + billsplit/ios/Flutter/AppFrameworkInfo.plist | 24 + billsplit/ios/Flutter/Debug.xcconfig | 1 + billsplit/ios/Flutter/Release.xcconfig | 1 + .../ios/Runner.xcodeproj/project.pbxproj | 644 +++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 119 +++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + billsplit/ios/Runner/AppDelegate.swift | 16 + .../AppIcon.appiconset/Contents.json | 122 +++ .../Icon-App-1024x1024@1x.png | Bin 0 -> 10932 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 0 -> 295 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 0 -> 450 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 0 -> 282 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 0 -> 462 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 0 -> 704 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 0 -> 586 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 0 -> 1674 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 0 -> 762 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 0 -> 1226 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 1418 bytes .../LaunchImage.imageset/Contents.json | 23 + .../LaunchImage.imageset/LaunchImage.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/README.md | 5 + .../Runner/Base.lproj/LaunchScreen.storyboard | 37 + .../ios/Runner/Base.lproj/Main.storyboard | 26 + billsplit/ios/Runner/Info.plist | 70 ++ billsplit/ios/Runner/Runner-Bridging-Header.h | 1 + billsplit/ios/Runner/SceneDelegate.swift | 6 + billsplit/ios/RunnerTests/RunnerTests.swift | 12 + billsplit/lib/domain/eparagon_parser.dart | 293 +++++++ billsplit/lib/domain/models.dart | 278 +++++++ billsplit/lib/domain/split_engine.dart | 155 ++++ billsplit/lib/domain/xlsx_export.dart | 215 +++++ billsplit/lib/main.dart | 37 + billsplit/lib/state/app_state.dart | 166 ++++ billsplit/lib/ui/assignment_sheet.dart | 143 ++++ billsplit/lib/ui/home_screen.dart | 208 +++++ billsplit/lib/ui/item_edit.dart | 130 +++ billsplit/lib/ui/people_screen.dart | 273 ++++++ billsplit/lib/ui/receipt_screen.dart | 357 ++++++++ billsplit/linux/.gitignore | 1 + billsplit/linux/CMakeLists.txt | 128 +++ billsplit/linux/flutter/CMakeLists.txt | 88 ++ .../flutter/generated_plugin_registrant.cc | 11 + .../flutter/generated_plugin_registrant.h | 15 + .../linux/flutter/generated_plugins.cmake | 24 + billsplit/linux/runner/CMakeLists.txt | 26 + billsplit/linux/runner/main.cc | 6 + billsplit/linux/runner/my_application.cc | 148 ++++ billsplit/linux/runner/my_application.h | 21 + billsplit/macos/.gitignore | 7 + .../macos/Flutter/Flutter-Debug.xcconfig | 1 + .../macos/Flutter/Flutter-Release.xcconfig | 1 + .../Flutter/GeneratedPluginRegistrant.swift | 12 + .../macos/Runner.xcodeproj/project.pbxproj | 729 ++++++++++++++++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 117 +++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + billsplit/macos/Runner/AppDelegate.swift | 13 + .../AppIcon.appiconset/Contents.json | 68 ++ .../AppIcon.appiconset/app_icon_1024.png | Bin 0 -> 102994 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 0 -> 5680 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 0 -> 520 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 0 -> 14142 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 0 -> 1066 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 0 -> 36406 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 0 -> 2218 bytes .../macos/Runner/Base.lproj/MainMenu.xib | 343 ++++++++ .../macos/Runner/Configs/AppInfo.xcconfig | 14 + billsplit/macos/Runner/Configs/Debug.xcconfig | 2 + .../macos/Runner/Configs/Release.xcconfig | 2 + .../macos/Runner/Configs/Warnings.xcconfig | 13 + .../macos/Runner/DebugProfile.entitlements | 12 + billsplit/macos/Runner/Info.plist | 32 + .../macos/Runner/MainFlutterWindow.swift | 15 + billsplit/macos/Runner/Release.entitlements | 8 + billsplit/macos/RunnerTests/RunnerTests.swift | 12 + billsplit/pubspec.lock | 554 +++++++++++++ billsplit/pubspec.yaml | 37 + billsplit/test/app_state_test.dart | 184 +++++ billsplit/test/engine_extra_test.dart | 29 + billsplit/test/eparagon_edge_test.dart | 232 ++++++ billsplit/test/eparagon_parser_test.dart | 82 ++ billsplit/test/fixtures/sample_receipt.json | 779 ++++++++++++++++++ billsplit/test/helpers.dart | 78 ++ billsplit/test/models_test.dart | 78 ++ billsplit/test/split_engine_test.dart | 189 +++++ billsplit/test/ui_flows_test.dart | 536 ++++++++++++ billsplit/test/widget_test.dart | 16 + billsplit/test/xlsx_export_test.dart | 31 + billsplit/test/xlsx_extra_test.dart | 36 + billsplit/windows/.gitignore | 17 + billsplit/windows/CMakeLists.txt | 108 +++ billsplit/windows/flutter/CMakeLists.txt | 109 +++ .../flutter/generated_plugin_registrant.cc | 11 + .../flutter/generated_plugin_registrant.h | 15 + .../windows/flutter/generated_plugins.cmake | 24 + billsplit/windows/runner/CMakeLists.txt | 40 + billsplit/windows/runner/Runner.rc | 121 +++ billsplit/windows/runner/flutter_window.cpp | 71 ++ billsplit/windows/runner/flutter_window.h | 33 + billsplit/windows/runner/main.cpp | 43 + billsplit/windows/runner/resource.h | 16 + .../windows/runner/resources/app_icon.ico | Bin 0 -> 33772 bytes billsplit/windows/runner/runner.exe.manifest | 14 + billsplit/windows/runner/utils.cpp | 69 ++ billsplit/windows/runner/utils.h | 19 + billsplit/windows/runner/win32_window.cpp | 288 +++++++ billsplit/windows/runner/win32_window.h | 102 +++ python_pkg/billsplit_coverage/__init__.py | 13 + python_pkg/billsplit_coverage/__main__.py | 14 + python_pkg/billsplit_coverage/checker.py | 130 +++ .../billsplit_coverage/tests/__init__.py | 3 + .../billsplit_coverage/tests/test_checker.py | 191 +++++ 167 files changed, 10095 insertions(+) create mode 100644 billsplit/.gitignore create mode 100644 billsplit/.metadata create mode 100644 billsplit/README.md create mode 100644 billsplit/analysis_options.yaml create mode 100644 billsplit/android/.gitignore create mode 100644 billsplit/android/app/build.gradle.kts create mode 100644 billsplit/android/app/src/debug/AndroidManifest.xml create mode 100644 billsplit/android/app/src/main/AndroidManifest.xml create mode 100644 billsplit/android/app/src/main/kotlin/dev/kuhy/billsplit/MainActivity.kt create mode 100644 billsplit/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png create mode 100644 billsplit/android/app/src/main/res/drawable-hdpi/ic_launcher_monochrome.png create mode 100644 billsplit/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png create mode 100644 billsplit/android/app/src/main/res/drawable-mdpi/ic_launcher_monochrome.png create mode 100644 billsplit/android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 billsplit/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png create mode 100644 billsplit/android/app/src/main/res/drawable-xhdpi/ic_launcher_monochrome.png create mode 100644 billsplit/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png create mode 100644 billsplit/android/app/src/main/res/drawable-xxhdpi/ic_launcher_monochrome.png create mode 100644 billsplit/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png create mode 100644 billsplit/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_monochrome.png create mode 100644 billsplit/android/app/src/main/res/drawable/launch_background.xml create mode 100644 billsplit/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 billsplit/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 billsplit/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 billsplit/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 billsplit/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 billsplit/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 billsplit/android/app/src/main/res/values-night/styles.xml create mode 100644 billsplit/android/app/src/main/res/values/colors.xml create mode 100644 billsplit/android/app/src/main/res/values/styles.xml create mode 100644 billsplit/android/app/src/profile/AndroidManifest.xml create mode 100644 billsplit/android/build.gradle.kts create mode 100644 billsplit/android/gradle.properties create mode 100644 billsplit/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 billsplit/android/settings.gradle.kts create mode 100644 billsplit/assets/icon/icon.png create mode 100644 billsplit/assets/icon/icon.svg create mode 100644 billsplit/assets/icon/icon_foreground.png create mode 100644 billsplit/assets/icon/icon_foreground.svg create mode 100644 billsplit/assets/icon/icon_monochrome.png create mode 100644 billsplit/assets/icon/icon_monochrome.svg create mode 100644 billsplit/ios/.gitignore create mode 100644 billsplit/ios/Flutter/AppFrameworkInfo.plist create mode 100644 billsplit/ios/Flutter/Debug.xcconfig create mode 100644 billsplit/ios/Flutter/Release.xcconfig create mode 100644 billsplit/ios/Runner.xcodeproj/project.pbxproj create mode 100644 billsplit/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 billsplit/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 billsplit/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 billsplit/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 billsplit/ios/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 billsplit/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 billsplit/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 billsplit/ios/Runner/AppDelegate.swift create mode 100644 billsplit/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 billsplit/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png create mode 100644 billsplit/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png create mode 100644 billsplit/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png create mode 100644 billsplit/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png create mode 100644 billsplit/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png create mode 100644 billsplit/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png create mode 100644 billsplit/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png create mode 100644 billsplit/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png create mode 100644 billsplit/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png create mode 100644 billsplit/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png create mode 100644 billsplit/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png create mode 100644 billsplit/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png create mode 100644 billsplit/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png create mode 100644 billsplit/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png create mode 100644 billsplit/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 billsplit/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json create mode 100644 billsplit/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png create mode 100644 billsplit/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png create mode 100644 billsplit/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png create mode 100644 billsplit/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md create mode 100644 billsplit/ios/Runner/Base.lproj/LaunchScreen.storyboard create mode 100644 billsplit/ios/Runner/Base.lproj/Main.storyboard create mode 100644 billsplit/ios/Runner/Info.plist create mode 100644 billsplit/ios/Runner/Runner-Bridging-Header.h create mode 100644 billsplit/ios/Runner/SceneDelegate.swift create mode 100644 billsplit/ios/RunnerTests/RunnerTests.swift create mode 100644 billsplit/lib/domain/eparagon_parser.dart create mode 100644 billsplit/lib/domain/models.dart create mode 100644 billsplit/lib/domain/split_engine.dart create mode 100644 billsplit/lib/domain/xlsx_export.dart create mode 100644 billsplit/lib/main.dart create mode 100644 billsplit/lib/state/app_state.dart create mode 100644 billsplit/lib/ui/assignment_sheet.dart create mode 100644 billsplit/lib/ui/home_screen.dart create mode 100644 billsplit/lib/ui/item_edit.dart create mode 100644 billsplit/lib/ui/people_screen.dart create mode 100644 billsplit/lib/ui/receipt_screen.dart create mode 100644 billsplit/linux/.gitignore create mode 100644 billsplit/linux/CMakeLists.txt create mode 100644 billsplit/linux/flutter/CMakeLists.txt create mode 100644 billsplit/linux/flutter/generated_plugin_registrant.cc create mode 100644 billsplit/linux/flutter/generated_plugin_registrant.h create mode 100644 billsplit/linux/flutter/generated_plugins.cmake create mode 100644 billsplit/linux/runner/CMakeLists.txt create mode 100644 billsplit/linux/runner/main.cc create mode 100644 billsplit/linux/runner/my_application.cc create mode 100644 billsplit/linux/runner/my_application.h create mode 100644 billsplit/macos/.gitignore create mode 100644 billsplit/macos/Flutter/Flutter-Debug.xcconfig create mode 100644 billsplit/macos/Flutter/Flutter-Release.xcconfig create mode 100644 billsplit/macos/Flutter/GeneratedPluginRegistrant.swift create mode 100644 billsplit/macos/Runner.xcodeproj/project.pbxproj create mode 100644 billsplit/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 billsplit/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 billsplit/macos/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 billsplit/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 billsplit/macos/Runner/AppDelegate.swift create mode 100644 billsplit/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 billsplit/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png create mode 100644 billsplit/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png create mode 100644 billsplit/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png create mode 100644 billsplit/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png create mode 100644 billsplit/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png create mode 100644 billsplit/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png create mode 100644 billsplit/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png create mode 100644 billsplit/macos/Runner/Base.lproj/MainMenu.xib create mode 100644 billsplit/macos/Runner/Configs/AppInfo.xcconfig create mode 100644 billsplit/macos/Runner/Configs/Debug.xcconfig create mode 100644 billsplit/macos/Runner/Configs/Release.xcconfig create mode 100644 billsplit/macos/Runner/Configs/Warnings.xcconfig create mode 100644 billsplit/macos/Runner/DebugProfile.entitlements create mode 100644 billsplit/macos/Runner/Info.plist create mode 100644 billsplit/macos/Runner/MainFlutterWindow.swift create mode 100644 billsplit/macos/Runner/Release.entitlements create mode 100644 billsplit/macos/RunnerTests/RunnerTests.swift create mode 100644 billsplit/pubspec.lock create mode 100644 billsplit/pubspec.yaml create mode 100644 billsplit/test/app_state_test.dart create mode 100644 billsplit/test/engine_extra_test.dart create mode 100644 billsplit/test/eparagon_edge_test.dart create mode 100644 billsplit/test/eparagon_parser_test.dart create mode 100644 billsplit/test/fixtures/sample_receipt.json create mode 100644 billsplit/test/helpers.dart create mode 100644 billsplit/test/models_test.dart create mode 100644 billsplit/test/split_engine_test.dart create mode 100644 billsplit/test/ui_flows_test.dart create mode 100644 billsplit/test/widget_test.dart create mode 100644 billsplit/test/xlsx_export_test.dart create mode 100644 billsplit/test/xlsx_extra_test.dart create mode 100644 billsplit/windows/.gitignore create mode 100644 billsplit/windows/CMakeLists.txt create mode 100644 billsplit/windows/flutter/CMakeLists.txt create mode 100644 billsplit/windows/flutter/generated_plugin_registrant.cc create mode 100644 billsplit/windows/flutter/generated_plugin_registrant.h create mode 100644 billsplit/windows/flutter/generated_plugins.cmake create mode 100644 billsplit/windows/runner/CMakeLists.txt create mode 100644 billsplit/windows/runner/Runner.rc create mode 100644 billsplit/windows/runner/flutter_window.cpp create mode 100644 billsplit/windows/runner/flutter_window.h create mode 100644 billsplit/windows/runner/main.cpp create mode 100644 billsplit/windows/runner/resource.h create mode 100644 billsplit/windows/runner/resources/app_icon.ico create mode 100644 billsplit/windows/runner/runner.exe.manifest create mode 100644 billsplit/windows/runner/utils.cpp create mode 100644 billsplit/windows/runner/utils.h create mode 100644 billsplit/windows/runner/win32_window.cpp create mode 100644 billsplit/windows/runner/win32_window.h create mode 100644 python_pkg/billsplit_coverage/__init__.py create mode 100644 python_pkg/billsplit_coverage/__main__.py create mode 100644 python_pkg/billsplit_coverage/checker.py create mode 100644 python_pkg/billsplit_coverage/tests/__init__.py create mode 100644 python_pkg/billsplit_coverage/tests/test_checker.py diff --git a/billsplit/.gitignore b/billsplit/.gitignore new file mode 100644 index 0000000..3820a95 --- /dev/null +++ b/billsplit/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/billsplit/.metadata b/billsplit/.metadata new file mode 100644 index 0000000..c17b941 --- /dev/null +++ b/billsplit/.metadata @@ -0,0 +1,42 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "058e0af2c2b57e369d905a03ac9748b0ebf543c6" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6 + base_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6 + - platform: android + create_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6 + base_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6 + - platform: ios + create_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6 + base_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6 + - platform: linux + create_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6 + base_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6 + - platform: macos + create_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6 + base_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6 + - platform: windows + create_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6 + base_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/billsplit/README.md b/billsplit/README.md new file mode 100644 index 0000000..046d207 --- /dev/null +++ b/billsplit/README.md @@ -0,0 +1,77 @@ +# BillSplit + +Flutter app (mobile + desktop) for splitting shop receipts between people — +including the drinkers vs non-drinkers case — with Biedronka e-paragon import +and .xlsx export. + +## Features + +- **People roster** with a per-person _drinks alcohol_ toggle, plus arbitrary + **groups** (tags) like "meat eaters". +- **Receipts**: import a Polish fiscal e-receipt JSON (`e-paragon`, + JPK_KASA_PARAGON_v2-0 — the file the Biedronka app exports; the embedded JWS + payload is decoded, signature not verified) or build one manually. Multiple + receipts persist locally (`state.json` in app documents). +- **Per-item assignment**: each item is split among _Everyone / Drinkers / + Non-drinkers / a group / hand-picked people_. Alcohol is auto-detected on + import (wódka/piwo/wino/… keywords) and defaults to drinkers only; deposits + (kaucja) become their own line split among everyone. Category and assignment + are always editable — the keyword detection is a default, not a decision. +- **Grosz-exact math**: every item's shares sum to exactly the item total + (largest-remainder distribution), so per-person totals reconcile with the + receipt to the grosz — no floating point drift. +- **Totals view**: per person, with alcohol / rest breakdown, unassigned-item + warnings, and a reconciliation check against the paid amount. +- **Export .xlsx**: one row per item with a 1/0 person matrix plus a summary — + same layout as the hand-made spreadsheet this app grew out of. Matrix and + summary are exported values (the app is the calculator); column totals are + live SUM formulas. + +## Running + +The repo ships `lib/`, `test/`, and `pubspec.yaml` without platform runner +folders. Generate them once for the platforms you want: + +```sh +cd billsplit +flutter create . --platforms=linux,windows,macos,android,ios +flutter pub get +flutter run # picks a connected device / desktop +``` + +Tested with Flutter 3.44.8 / Dart 3.10. + +## Tests + +```sh +flutter test +``` + +Covers: parsing the real sample receipt in `test/fixtures/` (49 items + +deposit, totals 590.55 + 7.00 = 597.55 zł), split invariants incl. a 500-case +fuzz of the remainder distribution, assignment-mode resolution, and xlsx +generation (the test writes `build/test_export.xlsx` for external inspection). + +## Design notes / edge cases + +- Money is `int` grosze end to end; formatting happens only at the UI edge. +- An item resolving to zero people (e.g. "non-drinkers" on an all-drinker + receipt) is **excluded from totals and loudly flagged**, never silently + dropped into someone's bill. +- Deleting a person prunes them from groups, receipt participants and custom + assignments; deleting a group falls back affected items to _Everyone_. +- Assignment modes are stored symbolically (not as materialized ID sets), so + adding a person later automatically includes them in _Everyone_ items. + Touching a person checkbox materializes the current set and switches the + item to _custom_. +- `paidTotalGr` (what the card was charged) is kept separately from the item + sum; a mismatch shows red in Totals instead of being "fixed" silently. + +## Quality gates (enforced in CI) + +- `dart format --set-exit-if-changed .` — formatting is canonical +- `flutter analyze --fatal-infos` — `very_good_analysis` + `strict-casts`, + `strict-inference`, `strict-raw-types`; any info fails the build +- `flutter test --coverage` + `tool/coverage_check.py` — **100% line + coverage of `lib/` is mandatory**; the gate also fails if a lib file is + never executed by any test diff --git a/billsplit/analysis_options.yaml b/billsplit/analysis_options.yaml new file mode 100644 index 0000000..ae2b39e --- /dev/null +++ b/billsplit/analysis_options.yaml @@ -0,0 +1,7 @@ +include: package:very_good_analysis/analysis_options.yaml + +analyzer: + language: + strict-casts: true + strict-inference: true + strict-raw-types: true diff --git a/billsplit/android/.gitignore b/billsplit/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/billsplit/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/billsplit/android/app/build.gradle.kts b/billsplit/android/app/build.gradle.kts new file mode 100644 index 0000000..c42c15b --- /dev/null +++ b/billsplit/android/app/build.gradle.kts @@ -0,0 +1,45 @@ +plugins { + id("com.android.application") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "dev.kuhy.billsplit" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "dev.kuhy.billsplit" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } +} + +flutter { + source = "../.." +} diff --git a/billsplit/android/app/src/debug/AndroidManifest.xml b/billsplit/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/billsplit/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/billsplit/android/app/src/main/AndroidManifest.xml b/billsplit/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..551a641 --- /dev/null +++ b/billsplit/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/billsplit/android/app/src/main/kotlin/dev/kuhy/billsplit/MainActivity.kt b/billsplit/android/app/src/main/kotlin/dev/kuhy/billsplit/MainActivity.kt new file mode 100644 index 0000000..877a7f1 --- /dev/null +++ b/billsplit/android/app/src/main/kotlin/dev/kuhy/billsplit/MainActivity.kt @@ -0,0 +1,5 @@ +package dev.kuhy.billsplit + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/billsplit/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png b/billsplit/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..2e123caa799acd80a1657c19d0a0ab21feb3c79d GIT binary patch literal 2796 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 literal 0 HcmV?d00001 diff --git a/billsplit/android/app/src/main/res/drawable-hdpi/ic_launcher_monochrome.png b/billsplit/android/app/src/main/res/drawable-hdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000000000000000000000000000000000000..6b19a02b2d8c783b1370a2dd5049ff3d5409f19e GIT binary patch literal 2664 zcmd5;{X5fp9~XyMdCWsc;jBAn)WJDBrI~{oLRgWjGb?Yi$D?yEoGzV6R;eLkNb-q-tmy+5z_>;3w?@18?C zD1&rCGBPsCPL6i&!2RIsR#X5^=4b5#GBO}DC%aP~_+5(=L>I0^uO|u@cjvn8#X#qC z-~aQa0h3DmPSki3cGd2qtD4&bMg0vqNlZU_n6i4=ZFlp*!`Uy#l!Em2lCmwRHZpX- zs*M`Ftmx8^Cp|&$h@<8TGX7xm`udM~WRsZ-E?e8?^$r}z_9@kS@wI2$7xN4jJs>g) z6@0=j8CjCzPe0fN?gGp1QB?Oinfy`~@twBXp@v&(3G)AA6U;7I|B6E6I($KJmE*L~ zmd?7CtY&kV+BrGjkfi%KXOy5vnNb7Z7K8sQU&?B#Y3v--CC@kdI-Aj#WlBO9hs4Ch z5c}EeiOM1Lw#o0AtSdH1XXh<;ciMqixFB9jIW}KwIP8mql9G~TD*gD06R{ZnlTiAr zK(zOfKila%ei;Z%9_Df%4)*qrhDQ*Q$N@oBRh3pWDYaqdd6^2B?BVNsy^Kzeef@U( z{(W#|cD9D6o0~d9a~I$@F>#Y18?+sq@)V{)GRkghTFvn9+K&gU#!x5}e)Q=4`<3q6 zi!D&T*L;+wqFdRmTerFjPhk8j+^dlVe6zeh(Gcs*BO1>zMsWzVTUGxzXz*V-PrJIh zj(3lDbaVu3X=xEFRNFC?AzJ4sGfR!H+}HFx@vV)AUAU#18~ z-3M0K%5+!FZUu$Yr|EA-MxF^pA|lG)zeLBtpq*PXPdP*~awigZY(e2oQKWA7^XFre z5)-BTzP_U`N3K$!BqI)s0@-;zG_(Q$202@D<2(w5D)l(x9NuDDZJT_^uRXqv#o1qs z42jzNLJ>?}zvF#OA@v8lEKQI-9*0|NR&7;RR~I}(Wjs8Ye8|M7AsyKhEa;f|njNVv~3cB$Vo=Hu`^gsrXjSa_61qFUE{Mp|0h zfqeqHzD@jZc3UG;%~9{YBO)T278+sIh~z_Wcszsf&`?Q6i*$Wyg7CnrW)GLkRUM7= z%}!FYf1q|ew;QdPh)$V-?IjsaE+5sEJFWJ>F(D!0l(+}-J}rxN<&-%pz2hrDzK=gl zv|vTsbu=0~FswZ(spkNT#oGAg=Pba3d6wtl?QKBmn*qVGOUEZq+}srNG7N0|g-c86 z^@e1U8tt7(q*KeAug}ZP)doR+d=lz|hoxs^48FvC^4$$avAVi~bC(Tui}c!czYz_{ z{hFG2EIhO>c5}yf^+rIz6uo#^m+y5d*$Xe89V-6t;lswGM~|@GkWGw1k`|{DVI3*>e&`KNP2{cMYMFmCZKXa;-I|rY0 z^exKFZ0O6K&xVtXH0*tXLS6j){FD)zyTRmRFxcq&k?xl%z;mswt+l7#g&YccXAgv! z>C1x*6j`jE1S(!%E#+3Y7G?u0bUwyO5248d>f7}A@#p1H9jrmQdv*N$;-b}*{#Od4 z)WSS^Op5ZFsUb&2Z}AzXf&FR7N|>m4>q2#mD}x@U`z7x@>)~fbg*m) zd-G_Woz_BFU(%T$q4gD55!O<%q5(f#Ow1SPuVSiu_1mK2Vm}LjpuSWp)qq;qR9`=s znUj;Vxh&|YFDNJwUXP30e*1Qm$z-YmfTVLosX*mC;8e*jKESxGtgNh>y$T(7uJY!OP-^58O5vy!-$mB?1(2`}RP*nn4VPvbgL($IYrGzn z-_hu*QpLW+3_51Yzab}|ianQKNuw1eg z4i3%hrU8r_1aX|Fr{_Zy&2;}3<1auat&EM0$4}R*s;F4+6TZN&OzW7Mnl5z*rI5+i zrPrT4;CAT*vjV-rjSZEaU$1@hzIDkTLc+ z*trN<*=mDB@)^R{_iaOpN~Qw5>n>W@4;rlY3^>uxGa?1dh|9>zR{bxGck;^qK VY4X<_GXo|_87F(BT^%Cu_TOjs3hw{_ literal 0 HcmV?d00001 diff --git a/billsplit/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png b/billsplit/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..c070750fa74bce1cdb0aad60ba610cd1c03f2062 GIT binary patch literal 1823 zcmb_d`8OMg7G|gfiESupF|8$9Yb-%ibcE4TTP+pKR8ysG>{7J^(MXji5n9x~Co(!h zZM8mQuf4TXt0 zwHP>Pc&}Kqjf{KeRT}ZzoaW{2=7YXbY@i(+^t0r-Io?=20H_EOvyiz!g8eLmj)isb zpozR4Kt7_l!2d8HAg8xn1p&biR)7%)8ra-`_GDc%d*Jz*`8{e(A&1pG& z?6Fv@t^GQ9$w4BLc(JFM8Gh9N#}<3z+$5E1-dZtL-n5WF)f)$zfvVwpIJu`g`jnN> zrhV22s2orjipn5=lc!Q@W*Z_;nl4-WCjiHk7W)6$e`(*Qf5jwhKRKtS^bBB2YMItk zIgW9H=C-F1p=j$6Pp`8H;mj#_W?#r*z?D9>ZP)Jzv zFad$CEt zn<(fmGNG~MIXd8XL}MQX$0;m*sID!ukU`ShF>M^0E3M8Z$e?kr{Nc+0!(!dUoci_Soi}lc`QIeU+0RZ%^a;QmLQ72L zVt${ybpiiV#^}6Z=x$fT&MBx;<*0W_US3lJ0JeJG8Rw2;(bM-8m+XjPgs{)$-_M1h zt_2ULo|pd3Ms~VnAo>7g=NS?(fsrx8F-!M_T8zFp2xQ&?ujN%>X1X_#11r4Zf&9Cg z&ds6|GmUD@=Z{-A9^um`LTmdL>yDe-tRo0W)aT>ueJ*CZ_-}CIsJeJ4`;f*r$=OEe z-^cpQ|3!n@5Go&FBQ+Utcb!l-eWjq>l8FPB8+Is1{?4}FFvv1F@%>3e=h~O>q{BM_ z$zhyfJrapB4&K};&EDz!)aKp51~PghA<#Vv)2P^gB0u{|OVEavSCE)whpa{v);87q zd-XQaQB~^oOU-&+T-c8As7+Pxkam8{Q3gq>CNeLDGa{_;r8LRJy{RgfCDOT1VV?Qg ze_R}iaa5aE7*QM2_8(G<9$wW`ExDvXiDIvx4Xl}SZ+#m{8&?~{OlyMTy2t?E=4K=Jxr4))m9iKEV4i@R)cCjVJwQ>4ZE5UGY7dNXm#8|`Q ze)@{@(um62QF)#!!X#Ndx#v87o2_6`Fz_e$trzMciI zW}}z;JhLY)dsBCN=0S#gjPygr=xvQR?cQ1}091*fjEC-*EjC5rFH SIE`z3JeI%Mnlq4h34a69cU)2c literal 0 HcmV?d00001 diff --git a/billsplit/android/app/src/main/res/drawable-mdpi/ic_launcher_monochrome.png b/billsplit/android/app/src/main/res/drawable-mdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000000000000000000000000000000000000..97cc0040e4818e38ae6c7bddabd99701e6283eb5 GIT binary patch literal 1707 zcmb_d`!^E`93PHnCW^5Yjxw!L8!8vEn_Nb$A{4@oS22&fnQgh0j65oIfzSU~)ea$I!9=yRWodqBBHC0+OSvrmDV71hrAac=Mf?e4+y}kDm z22;bsuC`xlU7E2?N{JT{1{`dz4|&Wkqu2nAWur zovGp;1hq?5GVs_i2tbaM?85ebH^DyF_8be%d5VA7-Q7L<32(dG*s)Zxq6JdBd&bZM zPTeyyJUskpXjXFx9}pn+0|J34aIV0mysgUF6dS-`Fpf_$h(zL%^z`&c;4OKb4j%yZ{mX<3qlk(~??CsXp z)~cqaCeumVM2%@`mX`YIjCdaIgqV?i;f!4SyYN-6X%lW9XrNGOtCOSW9b zd6^U-YkERpE2XckvvYFljzK(WFz=*%YJU=<1bpVqnSBHTVK1C9M>MLeaxumB6%`dR zJLAqC*3~HXdU*Sj-cFBZbu1QZWHzCK?Ca~}19om(d8zGo@rG~Rk=wbsKEbW=J)4_I zthT#>WIR<#)$6Um{yD~^1UbEfIL5$Zdbzm&bIt-JQ8LvN#`WKVFMD_$C`8r`M>!_^ zb8hb4yOJ~pwh^~5KfkGHX5>Hlw#vyAo0pTFtufJLpmmGSsHlhoV8Q1K2|_dQ0rkzr zcid=HlHY((D06ghNIp~T1^uc9J$^u4tOo`k4h{?)m(~})2vg9}(GmTRdby2(rm~)% zp4njE_;{cvnY{k;|G?PjVc_QX#hkf?g$2Bv5if7=q3L$t9(!h?8dOv5^=twZ_t7w{ zM%%MULPv&W!h|VXKfdWC)`ZGgk5f@qy|toc3UyNNAbc6fLyFL86+`u!d+E=&x3}4M z3JRzcZC4nULZKM9hg*h-I%}2Xl8RZzG`$-nexI{cN0wCCNt#nkwlA33s+Kg3WDjZ_lUGB`!D| z?q){}g|czV#U%qY^5?unh4PH^ULX+Mbh$5zi;3AHC|Y;X z`}_O#3_91alKnU(g?BHRi0ivL`bsf1hANj;m2i>wB2mB9jZ_BzkjZ3l`=hdQODn6d z4y9QLCzPs~*#(MLQn&=l`Q(m9XDVhNB@0b}vK|pXAbLG`_6hoaNpq-`ySuypc4ft2=+|C0Rp+Pt ez^MFE5g1GE5}5~?4>ROX2;lYy#<{^M + + + + + + + diff --git a/billsplit/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png b/billsplit/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..c400742eebe1ad15aa1667f4b343af7ae65e5767 GIT binary patch literal 3775 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 literal 0 HcmV?d00001 diff --git a/billsplit/android/app/src/main/res/drawable-xxhdpi/ic_launcher_monochrome.png b/billsplit/android/app/src/main/res/drawable-xxhdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000000000000000000000000000000000000..f45dec47314cac76da6b5f57bc3c84f38bd9e3ff GIT binary patch literal 4872 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 T#+(HIKyeuvnCe$ub$;|;#+#5p literal 0 HcmV?d00001 diff --git a/billsplit/android/app/src/main/res/drawable/launch_background.xml b/billsplit/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/billsplit/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/billsplit/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/billsplit/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..d506366 --- /dev/null +++ b/billsplit/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/billsplit/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/billsplit/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..061a3ef0d5d8e06b7b12e96275d8b4f0cdcfadab GIT binary patch literal 959 zcmV;w13>(VP)j2r%sPw6%>06cs1OmX2(+TNh@gkwdJ&}Pwf7)|D61EvCJ3>A$y^g5lHNP0TSre3*3(x76bLB&Xeijf8tBMmA>8dQulsMzKlFLPRNWCNYUvZ7gPAr4|E%3anQ z*+BPbNYRj$7U)p4#>|r1IuO(mQ(NlvW(gP~;tJ>jsM4-HPyO8eYFa*Uk$1YT*lzA0|(EdD;WfMLQ z8Ej%UjxEj?@x(I0m+sZ*y2RUXyeOOJ#QQ&;IeTIj$K5r-JOtB1W(L^aA! z`T`XNqRXD}{DDIZhM*qH_OPa{mlFyOF~P>xSGphf1z;$iEO(|jlPKLEDRh*1jjZT) z;ILwFW<_jDCYZ!vX&#?fVJH@4@f?>#3v^3Q;O*xy6b|kbFV&=Ch=@ZhqM*L06)-#* zfekT+?II2_-o~UjS-))TJnRDF6q98r!lb%bo%-la01jq#hupB)#}E|L9NUv4TtAnN zCzc7l^RR{-)?a@LLApP@&yku;JCCLgKdfU=7@CY+J>$V}*2EzeQjlo{%q?rHlAf8G z9Csc(leg72y<}YNZu4--bLRmA?{aOLZ$ySOOI490skT&UE)GsEcw(8*biX`(Ph zr~g=y19n(U^hcxTgYp$8SCB2}I)VmO%hv~S5Sv-j+6Lw7Vh3~xmf+Tbpf{V>b>_v8 z3{~)HtoJE{%^CR<)aLzrQWS`r11dhNTStX;vR}Np1(LMc)ssZ@1e0 zQ$ONh#l%Yt9Hti6J7gNJv1aveRSF^aTdPhk#>a}GS&=-b7->*3(x76bLB&Xeijf8t hBMmA>8dQul{s4x)UZZ?aK`Q_N002ovPDHLkV1nw5#9#mb literal 0 HcmV?d00001 diff --git a/billsplit/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/billsplit/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..88fa56e133e3de0b20df2d494897cb4a0d166173 GIT binary patch literal 519 zcmV+i0{H!jP)v;P5=@8$6C%Nc zNH8H1|5>QRYaH#V{Kg*h!PMSqhQ81z+{-HFxSC2q`zQ)RSBd=OauQ>1+;8>NN%{*V zWTc{(5Pv|PI<3sSTzjKY`!otd1_R&QV%9#V00p7EikEj4gz=EiVw8CO zg(4T(?UOVFSFPC9WYS;AWfv_b9?p>4L17w&5jLwZF;Wdd619FWj#e?sPly%c$FO1~ z!HSUtD@GE!6(clF0Tp4vz`qZI@b(ObiY&VOwve(=^fDVysy3@(Wkzhh3m=FmjMB>j0Jp=3a%N(-Tv2*|c002ov JPDHLkV1lCb>e~PS literal 0 HcmV?d00001 diff --git a/billsplit/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/billsplit/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..a42a652fd7a244ba63c164edebeee67b097dbb5d GIT binary patch literal 984 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1SE5RJ;-NZV4m;k;uuoF_;%KP@8Cp<`4iVqDXs zdRf}QO2CD|k%dg6igQ=M+dIKomoq6m^hRSCM;O6rcQ!;wU5Py zv(x6MpZiiKB;k6&N1H50ZXzxna9;Fj+r#>pPSXlY&W81m8 z1!-m8g*{3cS!Szht>5;q`*QMsPaw2FCZFpla`TJdT^a*^7vlgmH6 z?n?6hDpdn1da05hjk zfDe0<5+e$Fw8|mij2({(hohk^M;8k!TFvBW_}gHD0?W}71q~row2q7A=o@E!$B9#d zJ-Akv&9l@C@*s%M`sruXxr*Z;nFUj+qdb2cqmF+7Jy|3ykK3zW+HQmze z^v}D7zAh2bC375Qor_#Imi`Z$o$m0r@g(cfAsOx3fC!;d*h-LR#pp-X#ZjR zUh(aR)%Nm_fsd}I&Mul&-Bi^(U*Z?nr}i6j`2wRKp1qUDuk-uHW%JYOCp!EkKWg6M zx_@hwu0MIp=de=leb1@8_Ivmb>d2IgkoSLPA2$$q|9x zsd=A6diPEaJ`%MoApx*jDh@5ALT7LH8>v9`24;G&Wk*F|x^%$kMpE&oM9MM*40CpTohNV4(dz7VK(tP)D=% ziK-r+=6ochzWNs{%4KUvqfOsLfXSMZ&k1?x-@n&-ej!f)~65Hy+&@ok)1Kym8vH zJ`16T?Je4vr1GA4oe(^EI{=kx&-l~oM9t$`##k~+0}13XNwZ;NejWHrR*g?ER&Pj} zsMCCD=b*+yio=|siayulPPS@vEFfux>SAk~r=#T@^!gJ<&Uv1VIXMh#{_)VGwyCai z{SgJoN-VoV6eRSp=Tz2unT+Ta_*WolY0PZfQoxCPUeh$yeQ1s0VXT&GR!l+DF7ka_ z;U6aMS)9syuHUqw#$}KnAdM-tWrjpE_h;MhL$o*qqz zEUHyK#+R1KggxxjU4+WUw8YOdO;J$;2v`gWSktG=riR)?4b}Dh;Nsy_rlHh1l7`0h zL^;!^OISV($kjN%&%^8?{Z*sg8Dpt+Y5=w2UmQVqFvg~wT``cmHIi1NtLqZ><%o91 z*ifcJN)tQasV5Xj4oUsvT$my2ksQFcWi_C|L^+h%o!Z2Hjd5321_LrGXgidoZ%h7H z>YGmXtImS;C`an6?ur5!07y@s1nsy$X}y%RSD&te^!9kmi)|-W1}+vzf;^Mq@wiVp zZKh4+>)coOpM_#jxUr++Mtlt;aH5iHeskOcj!0QPx z&E+bNHOYylM5CIA_lE<)>0;r#1;O5NM?#00;&FXY&IIl}o2=D-kyUHAy3x=@46xx1 zVqBuOPcn92uh!c_V0jx4U;JiQ+Ln}fTFidVu6CneyN`7Iw>KM^ZyFe40}^0d)fZe{ zdwRugewr>5tx)+lToomu%c!u?Qk$HnWvxAHB2)_%%>E}vxG|QjJBgLPx%?;k6lRY$ z#6CmhZOb)-tyiBHvjz$xCFcaqy+^}5w*m$JuJlh1YRU&u{=yuk$?e@Q^U35x2L3-? z0eMLeM*4S=GALkjT=QFb%$&Tj$PcKYz@gn=orH@I(z9|US=h*t&Bom0`z6Fs% z!qbKTalQ;AL-28a&^2FQL5h?TUVr-{n(o5wZ|@UX%Wh)ySUN;*q}l3rAo1`!wxa?u z?6h)Hq-R^-@>oG-z?tRZUHz5Q_s!|i?Ss4py){*ty36^72AkNxuHXAmnNu|fHpLC) zl)6k5UF9Y?iH8efM9MVSd(RnhItn*W2?ckNU_3;8v)nhAw^ng|{Jc-HH)GjaIDG}MGuAsaQs+*Jon$;Jx&retBa;zrQq4Q-C2Kp^HEz`{jR_YZ{ zG*Gb{IP{SSoz0q(5?NcnSMbxheOo>~*#;Z%#qB%({VuTkm*xw7Cg#pt{GE>?;biZM Jcx)Sx@;@j41wa4* literal 0 HcmV?d00001 diff --git a/billsplit/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/billsplit/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..35c1f1a8e2176a099a93ee279679a3d94d12ad36 GIT binary patch literal 2032 zcmd5-`8OMg7M9S;7-}uX-WVK9N?J=3gEV3qgwaq-wNz0mZ4@PzSjrRCDUDOLwx!e( z>#3!&MMg^*ty092wDKs@hR{?aEfUYn>6tgL?+P9GWVZt9z6wz2n~}o|EXwNz+3j6b|WKS5QEKKwv2; zMPNDzaQq+tt1{mgA@5EJu&8XJ6Zj6ynUWo1as!<(pFRjgR((6>tc6<)`*)11{}v`} z$}-u;T57FWy*CNjQCC+-r+P<$hFlmM@4IFSBq?*Fce}`*P&ZA@#Hi)+E7wVuQXzup zYa#5!L>?^9@6k)&V=-d_Hr7C2&n?VSDBA3p@FD0pj~115CWO38#8aLqlB<`(;iaZ= z84kBrva~D&=_1~R(V|R#nkLooKLcbOIKoW^#wrr=ah3X|&#&PzO;@DU#amuXk z09dbH(GmD7$Ev{>rv)$Ni55Ga{d42iB*k3t_L+$?U{>8*H$K$e@x+gW9inw)NhN9j zpzQY_>`4${^yG|!$F4wl*hKrAV-D?C&R){jGc{B?d+005DQDvq$)s!)6@-3@s_>V3 z7zBO=0ghYby;CJ*{jDA{fT}Q+3dzXs|ENgTH&l9l{2S@C^r6mw0S(>AM)`cj|7r-h zScJa%YnmnI$F6}l5B^OA@Pp>PlL5@013w~4End&=kN#GXmn`yp?SU=l!LJT}#Xl$f zTSF&=hN03Mu-+dFMG?aPB631_@6jO<)$Jn_>tCs3Vly?g>r%xrq9kkfq_0p z=wmfm@L7#Mq1NhVlmB^zFtE|t1AEvsgM&_3-ii$|k~Qb6(wKL)Qu$Rx9b)TLFo0)Q z{6!QVvDF_@qggw+{gm-@a^r55-+&=Kmk`1h8-C>8-HLM3{cLXC59aUSVpb$m1~2R$ zEAVzW39)*W@Is&6YC6G5O2*UJ36#g1FGeBR;B5**=Xu#LhA}f|m^WN6T5Mb|_kyqQdwAfZt|-E+>M zJA%)2T%2*(4(x=*LF?=?^rJRPFZ1^uhs#MazKipq+EZ2=2{k@ za{%o}H-{}%k2QBI!#G(88uRpHX3;K!w?(s-&ggY@)7HfP5PElz%lPG_5#_|XmIsm2 zDD636M6!8onDg*0&O~4kE9*HO;Ll5u9&zl^CRe<)cj6i!Bfy0Cn6^c%tIwSP?%3=l zR_E>oik;Ds#+oArg>_f%%hxWx^%9Sm#86~7U(&|t{|`3xmlLH*LcDL5e|*v$zvtu< N&Q9*ACgi#Fe*ws?e{TQ) literal 0 HcmV?d00001 diff --git a/billsplit/android/app/src/main/res/values-night/styles.xml b/billsplit/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/billsplit/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/billsplit/android/app/src/main/res/values/colors.xml b/billsplit/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..1e7986a --- /dev/null +++ b/billsplit/android/app/src/main/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #211D1B + diff --git a/billsplit/android/app/src/main/res/values/styles.xml b/billsplit/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/billsplit/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/billsplit/android/app/src/profile/AndroidManifest.xml b/billsplit/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/billsplit/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/billsplit/android/build.gradle.kts b/billsplit/android/build.gradle.kts new file mode 100644 index 0000000..dbee657 --- /dev/null +++ b/billsplit/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/billsplit/android/gradle.properties b/billsplit/android/gradle.properties new file mode 100644 index 0000000..e96108c --- /dev/null +++ b/billsplit/android/gradle.properties @@ -0,0 +1,6 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +# This newDsl flag was added by the Flutter template +android.newDsl=false +# This builtInKotlin flag was added by the Flutter template +android.builtInKotlin=false diff --git a/billsplit/android/gradle/wrapper/gradle-wrapper.properties b/billsplit/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..2d428bf --- /dev/null +++ b/billsplit/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip diff --git a/billsplit/android/settings.gradle.kts b/billsplit/android/settings.gradle.kts new file mode 100644 index 0000000..c21f0c5 --- /dev/null +++ b/billsplit/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "9.0.1" apply false + id("org.jetbrains.kotlin.android") version "2.3.20" apply false +} + +include(":app") diff --git a/billsplit/assets/icon/icon.png b/billsplit/assets/icon/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..7245aaa71eb0d6e16148e97cff1ac811e7b12dba GIT binary patch literal 13540 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 + + + + + diff --git a/billsplit/assets/icon/icon_foreground.png b/billsplit/assets/icon/icon_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..afcebf385b9446b426c91c3de277a57f151f8417 GIT binary patch literal 14136 zcmeHNc{r5&+kd2TP9@DzXUbYLrKC)b>=6o??9*5~oobMglBI0L5Drpy zh8A078zIY#G8kh*_HCH=`?Q?%-}}$+djI&nmvLS5oy+q)-}}Bl_vc=o`*}jo7~zFC ziEV-)NciLloG}Eg2S3+CKWzj{u%z4#uxzw9z~dl3{Fg$@xDP?Qp_91d=ZFcP8CTpG zJ}dmmd3FIe<&MXW(i1aM!5_5B6ff4wefS{LYq3QPAGb4No{T^*nb#ALd(B>(q1KC{ zWr!X+EBxzQbbi{{x$c73x-fMKIhozH{fBSdSYMR?s;7`WU3HcxTD9~x`vxdML0&58TrwD}XI`}{eYL(Y{^r&=ZBjl7+DO|on+44?5 zf-VU54w(~$C8Vrd2QTNe;qQ=5Kf%ix0Reb9xEub~-T66I05_gsNeP&4bjIwoDWA&?by%^_yLkhT8i+bafNz71CCK&jr7}j^ zIZt2BJ(OHZRS|#)O_C7Qy0|s1Z^Zp3^}TB(mT$Rp-0DO38y5XG@f0jMGhmcCb)m#Wn1@5qs? z%gR<@XOVeU97*UU0>JNmH=(^)eitzy7#wsNDob2O)hSW^AP5hlq*YC7w0B`JhK^3x z@2`#OMNiGqHK34_@O5OpqZ{=JaV~21Ivio>$-`bj6_2PVxs6k`VOX2%P=evlx?uV#%6|RR-~VAuan?T_r<`fp zQo2rC=ocU;C3^H{NNs>a7l%@ufWh03GDnx3Il}FZMqya03ZxDD zybMubQK@M!EOI83qisC?)?*h0i6NX}3nlizo4$?0NHhC*E*!~;j?Sj0Y;=tytOfy= zDAa)HmzHwNJwRhI#Kf$K9$_V!K;pM3H1zVK_}@}iYQbO!6MM=yI!*N)b3~g7X*2W% z=81Q%D7vO|SqK?eGIO_B@mR@K5B&%sw0*7C^*zu#Gx&_6c5$@{qTSWDEV>TGiTJD( z#U`qvj4-qfkg7TyNBtFM>EYSS%FMbygW)(sgZH)Qa@d7OBNR!<_ON2>K`)FD|=s zl{oz*Tr6T-&OEobG;c8OiXc1jSWo-XAC!vpL7;G=mk>y>yVa(KM04Jp7P;aQq{cq6 zPkBp!YlU|~hSDzx`BeeCjUX89hSd~}c>%<$pWc8{cCB9#} z*+Vbv{9SbO$+q(nQ5Z6r`Fic&h9ORVlOchj~VU(Gk;jjEj2?W-9a;b|e-hy8h zJF;k|A{f%$a3AL2|CdJKe`#(; z+QJph3x`lPVdN@&HH*LWG1=Yn82e^yt=6wG5}&m{)FCeanL%$x`U8S|In{^}6|3aL^2-8jPnET&%DD1>jC( zJ+6#6?JkS8&ziX+QHn(o{>|K%a1JRmw=!eU%F&=S8p35&(ao~(LRTskZ&&nX1=ki~$mth=X+y79--pX0pmND$SsCsTWGu+h+GF7C1BdWD z&&8K>lXQ(9M|eHq;S6j!qXDkk#0{$j6}rGue{meHu)YAgXrHSSA3kZtfEu{GwJ=B; zA@olf>_oc?t^Q_4!j+#&av=TvPwOyt2y53fNPIH0{#!4xZ-|&a^VjA37PAR*koRs_ zkbY(obQhiUwGn}PxZ|vx+~62cvxn0_HzNqWhkf#AS#6z-JnceRg?ff_FzA)e!2Q~$ z9%&DN4Ts+GBJbe`0VweV!1CL@$(dG=)u<|lv7!0*jSj(0&ey#~8Q2c1?b5+Sx1fJE z(O2*=gIf}NRPrDH$P<%omIdMl_0)^u;tlsf`>0RZPgGS#)yzQ~3ByrXG81z7z9*Kw z36cboL!QUD?OL!q4NCZ2k6O_4e?XW`B3jMk4I(cUU&BV6#^DGs$Y1q0$mwM36D}3! zo^aI%ok$PdR#`0#6kT^@P|XV(;Z<#5`X7Hy+-XzuH;)CLbYNiS@YTD192r@EdEg;k zv}YeE8^Bu-z(yTsQ8Zhti;S^R=os&>`0|8&-WbqrH$a$uX=oxQ>Bs|5%KLiG(!b3p zNC4dQ;T=>z!v~*U>Dq>EHvs;HWWF)Gq4*e)HE>^lLe;EyW3z@DDmV`==Rd)s^wqOF z#P}yUJL+7#jkp=$Rt64`S(~g0UMd z_-fm21CeVQZ*FWxX1pgJK{@e6cdmQswhx@)tU-&KrU-3B<{ZQhUb-dpNY0aT8$B>X zw}e7c;aHw-avchoXidLqD~BEI7baND%qBRv;vU+uf%HGMeNi*nBX%@^S&1H8Wl z8u4jB8gkkOpmIu5MAx+37D8$sD=00!ShiI?fc2WH^XOsK+lUes6jRH(ZUdBlD;U1_ zk~yEeJ__|>qa>`?iBl5HUGUN&EO$)>VzMN8G+T|#x0WgXRdr#9UDFXwrokUx&s z2|49FZfde332vX!$B~ZWU7f=gTq#5nXwg}zIkaW;RvhkKWLy`v*kkIGgmW5sgW5pC z_J|t9;-BP^!xhqDoZe@>6ZJ3u2Hg@AmqaRpv#d= z+)d>LuNWFOAhhObUlxei$VjwjbqjdRc`5cB3DO3S9Tz@Uw3S+)#!>jDB!fMChM2}` zMXsn#V!gv;-HFL61CfIxRhABQ$$kM6mrPd`5m5u(ZM7oosQwi$ zqeI+Jt3%)8)Z3Fayx#dIJ;|Zu69_*0-t6hsMcWDUWni!BYZfb*x6Bf>Q~m`C`D0?< z;WTp_qE~-nfj`FRkF;{WCiI1!=)uCB#BAaTKIoutcF8iMeWd@=-0Utk`{bTz_nvKk zqAtoJLXAInGssal1oouPJvp@=xtk>VvrFi~gKntDsLita=tgEYBmKZ}Sp-7(*H{CyMDI|=UUG2=Tc z;uQ%4Bi7UGprUS;kUASP{JvyAs`J))vwiNJ;V0umNB9x*w4COKbm?IkDst-V-AGsW zhaw>!_TVA%&K%!)Z``U`hflN&n_yI2u7bUJtt@brn&1$$bDybGIBn;0Jej<`Eoi=< zNO1b>zejq*Vu@4sBD52!426W=TiuP7kPR~Sk=(by8BHo|xib4Ur+YjzVW9iB%~`EQ zB!yZ<1S0>cbNO;%j^?#HU#~ylCMONCUT%broLwsBHQ?U%NbLpa8 z7%}zT>x0k6Z0>Mi1WNOrS=XacgxL)aJ)0K~)Z;Jo0tJL%1r$4M)E}V&ACIbUlo?vg z>>dyA4eWmRxpZ?|J(vlXA#vg-H9psT0PeuBRXw}d=Bs|?7>RP5di-(C+#U~2NG}mM z1VwvRw9r9bxX|JA*@B{2)jjW=;kz`I$ue;Eq)e8u=jHtslz}0#HKCl+aY}G(J_q(F z>mg%U&@LLZiZaF-qO@6?rHeRT)~JQ=r>>^q+WpGq)NV6c2@{J*<<|yv(RSXCC)axn z(JxGrwzYn)2v?rUZ^OxIvI8cYLu%#dxsgoOK;#Xr}u{ow-f_*l&qOGjoH? zDDqB5`vsx*;ZYzwHgnY^nPaz^WQCEhT4Y@x60|!9Yti%mGkxCEsQ+mhWlTS+iuurz z*PPP1ywErGC~Dw3xvhM6XNtc<7O+HZcHmVx8!6=bsqF07H(Rc=1?{%OyD%TjVk7k^ zvDvu}jh+=1MZS#=H*AVJ#^ndeZDsq+PSSRk0!~B^yni3y_pvxvy!0EIvcRjUwZN;B zI^)b^gKfV0)8?2`|fFb)bN7q54DE5h#z@e7ph=lR1vKa2$sANK6 ztcC0YzF+-jTPEKxie=lV*=!`7jr0NFk`o5Lc$Y65a_Ta#ebwmZ1sGjbLFl8Oaah7! z+G#vCZ!U1pkfo;59T61-;%fvQ!`^Wid)Z1lI~`AsW@TOoEct$QmEyYq=*}ka@Mhw- z*?ER-=NFfL82iq?Zrd%>DKrgvEinW&@%IWHoeAAj&M1`qBK=CSmgYOb&VsM8`nq*m z;K(Mxj|zM?!dU&X`}Gpa0#m^6CPt>HbA zk6a~u5X!au2J5*Pk&v<%0?<`Sng;XQG1Or*Ydmf>ub4n%oj1i-O@~hBrIBxEtFKOE z7a?ckLjh4|KIRWKxW6$={^s6HRcXk_3BaK41jH(ddF<6awnz51)GOVRS! ziAjuU=QgDg-BV5|&+1}JFNQuJ>p~B2a7O*EnVa?2@IC6=9(JNRtZ=y~6UY9ix($)V zl)U!?;)MnKDSDLA%Cp%gF


M8}*`i*Q0h_Jl6h>Feb|GNbo_5uW1OA>#FF^w;Kd ze9H!p`MV;tBOT+-Tew-nmXm#NSENI?^W)L(;!}@ZJHj_P+ZV>t>l!MGmwn%YkY5@U zOkpMf7zDp18l2Sdj@sa_`K7Glag~cpZh4=9!sl$4u3Y4fCR`dB&z<|E2jaACzce%p zTQB3})M9-ptv176wYan)N<6ueJ1tjOkv9iOa}KwKTiH`MR&y6PvW}D4?S6Pd0|PSV z2vB_Kxd8NRgI^IcxkYxxVG-ZJZ+)o2pBR#MX3ym+QS_=^hom!^w4F@DYHohzWa~D{ z3kNt^Yc4`h_XGY|MiYWFBoU5D_GJ}*V`HVibV-v16Q_}0E`6d1*L_k>dr1m^#h2Ve z_xFzeU12()u<>_bE(j@T7KD>sIPA5;?$45;r6f=j_K_14AY{+MTTFHaS>R7HEe zi!I>Q&pa872kAa3$(r=|PNC4LAEv`wh;Nm$3{u0B+*-Nm(b8C=TWMJ@c}&~2$p2sl z(YKXLvA{Gu3?-j0dBIq*_?a8?4{o5+*gZ;jMpj8hRVkM2XrE8-G3ejfM|$Hi>(Vbf z5FwDOevh`3XITBnr@U<0>@7YSBv-){O~CrkFCT1Ajv#mWl^cZ-9ZvrpV>qiA0X(%^{UI56?LVl{q}x@&;B%t zWZuH(V7|*4AJYq?3-tH|R(QFU)1bdX3JBYQ>KUu~+kywPfB6Bs5P)6ds80aLaM0T& zBd;Z@N?o3C(bj4riAKl59O97Ye+ zNLI{z?scVMwv;7hd8@be-uK6&?DuC3QA{zu3ZlqPFZBRW`~3pTh}QMr4Yz>vmL6k1 znO9$__rgOSwR$xib8SDN+V+%I+ses!p#7)DWy>hma{esew)QzvBQ7+7G+Asi&A4u> zRO#|Ve~Kam`Cc9l9ZF75&@7%cAG}>RCcI*rT&%SE;DQK8 zhdOw@W22pm`{X0U+`Wl4;V?xKkW36z#3v&9?6%ylt)c%I36s&Z?R7kRD;FuJ#p6E)O5pr1{d&5aa%v zXq1UWBI(I;&)vqXzu@lbv?g?`Wh3+wIwft(^BCk&=e6!teE=0O@)JXavjsU+Iu^kTkCDDrTXmCq^DsK`G9X5NA~(a>W;`?W~@B1S + + + + diff --git a/billsplit/assets/icon/icon_monochrome.png b/billsplit/assets/icon/icon_monochrome.png new file mode 100644 index 0000000000000000000000000000000000000000..19ce46846713418f185728b2b86cfbbefd1ae112 GIT binary patch literal 12557 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 literal 0 HcmV?d00001 diff --git a/billsplit/assets/icon/icon_monochrome.svg b/billsplit/assets/icon/icon_monochrome.svg new file mode 100644 index 0000000..1962d67 --- /dev/null +++ b/billsplit/assets/icon/icon_monochrome.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/billsplit/ios/.gitignore b/billsplit/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/billsplit/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/billsplit/ios/Flutter/AppFrameworkInfo.plist b/billsplit/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..391a902 --- /dev/null +++ b/billsplit/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + + diff --git a/billsplit/ios/Flutter/Debug.xcconfig b/billsplit/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/billsplit/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/billsplit/ios/Flutter/Release.xcconfig b/billsplit/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/billsplit/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/billsplit/ios/Runner.xcodeproj/project.pbxproj b/billsplit/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..363d280 --- /dev/null +++ b/billsplit/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,644 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.kuhy.billsplit; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.kuhy.billsplit.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.kuhy.billsplit.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.kuhy.billsplit.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.kuhy.billsplit; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.kuhy.billsplit; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/billsplit/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/billsplit/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/billsplit/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/billsplit/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/billsplit/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/billsplit/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/billsplit/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/billsplit/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/billsplit/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/billsplit/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/billsplit/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..c3fedb2 --- /dev/null +++ b/billsplit/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/billsplit/ios/Runner.xcworkspace/contents.xcworkspacedata b/billsplit/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/billsplit/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/billsplit/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/billsplit/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/billsplit/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/billsplit/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/billsplit/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/billsplit/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/billsplit/ios/Runner/AppDelegate.swift b/billsplit/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..c30b367 --- /dev/null +++ b/billsplit/ios/Runner/AppDelegate.swift @@ -0,0 +1,16 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } +} diff --git a/billsplit/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/billsplit/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..e882ab9 --- /dev/null +++ b/billsplit/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images": [ + { + "size": "20x20", + "idiom": "iphone", + "filename": "Icon-App-20x20@2x.png", + "scale": "2x" + }, + { + "size": "20x20", + "idiom": "iphone", + "filename": "Icon-App-20x20@3x.png", + "scale": "3x" + }, + { + "size": "29x29", + "idiom": "iphone", + "filename": "Icon-App-29x29@1x.png", + "scale": "1x" + }, + { + "size": "29x29", + "idiom": "iphone", + "filename": "Icon-App-29x29@2x.png", + "scale": "2x" + }, + { + "size": "29x29", + "idiom": "iphone", + "filename": "Icon-App-29x29@3x.png", + "scale": "3x" + }, + { + "size": "40x40", + "idiom": "iphone", + "filename": "Icon-App-40x40@2x.png", + "scale": "2x" + }, + { + "size": "40x40", + "idiom": "iphone", + "filename": "Icon-App-40x40@3x.png", + "scale": "3x" + }, + { + "size": "60x60", + "idiom": "iphone", + "filename": "Icon-App-60x60@2x.png", + "scale": "2x" + }, + { + "size": "60x60", + "idiom": "iphone", + "filename": "Icon-App-60x60@3x.png", + "scale": "3x" + }, + { + "size": "20x20", + "idiom": "ipad", + "filename": "Icon-App-20x20@1x.png", + "scale": "1x" + }, + { + "size": "20x20", + "idiom": "ipad", + "filename": "Icon-App-20x20@2x.png", + "scale": "2x" + }, + { + "size": "29x29", + "idiom": "ipad", + "filename": "Icon-App-29x29@1x.png", + "scale": "1x" + }, + { + "size": "29x29", + "idiom": "ipad", + "filename": "Icon-App-29x29@2x.png", + "scale": "2x" + }, + { + "size": "40x40", + "idiom": "ipad", + "filename": "Icon-App-40x40@1x.png", + "scale": "1x" + }, + { + "size": "40x40", + "idiom": "ipad", + "filename": "Icon-App-40x40@2x.png", + "scale": "2x" + }, + { + "size": "76x76", + "idiom": "ipad", + "filename": "Icon-App-76x76@1x.png", + "scale": "1x" + }, + { + "size": "76x76", + "idiom": "ipad", + "filename": "Icon-App-76x76@2x.png", + "scale": "2x" + }, + { + "size": "83.5x83.5", + "idiom": "ipad", + "filename": "Icon-App-83.5x83.5@2x.png", + "scale": "2x" + }, + { + "size": "1024x1024", + "idiom": "ios-marketing", + "filename": "Icon-App-1024x1024@1x.png", + "scale": "1x" + } + ], + "info": { + "version": 1, + "author": "xcode" + } +} diff --git a/billsplit/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/billsplit/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..dc9ada4725e9b0ddb1deab583e5b5102493aa332 GIT binary patch literal 10932 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 literal 0 HcmV?d00001 diff --git a/billsplit/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/billsplit/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..797d452e458972bab9d994556c8305db4c827017 GIT binary patch literal 406 zcmV;H0crk;P))>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c literal 0 HcmV?d00001 diff --git a/billsplit/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/billsplit/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..6ed2d933e1120817fe9182483a228007b18ab6ae GIT binary patch literal 450 zcmV;z0X_bSP)iGWQ_5NJQ_~rNh*z)}eT%KUb z`7gNk0#AwF^#0T0?hIa^`~Ck;!}#m+_uT050aTR(J!bU#|IzRL%^UsMS#KsYnTF*!YeDOytlP4VhV?b} z%rz_<=#CPc)tU1MZTq~*2=8~iZ!lSa<{9b@2Jl;?IEV8)=fG217*|@)CCYgFze-x? zIFODUIA>nWKpE+bn~n7;-89sa>#DR>TSlqWk*!2hSN6D~Qb#VqbP~4Fk&m`@1$JGr zXPIdeRE&b2Thd#{MtDK$px*d3-Wx``>!oimf%|A-&-q*6KAH)e$3|6JV%HX{Hig)k suLT-RhftRq8b9;(V=235Wa|I=027H2wCDra;{X5v07*qoM6N<$f;9x^2LJ#7 literal 0 HcmV?d00001 diff --git a/billsplit/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/billsplit/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..4cd7b0099ca80c806f8fe495613e8d6c69460d76 GIT binary patch literal 282 zcmV+#0p(^bcu7P-R4C8Q z&e;xxFbF_Vrezo%_kH*OKhshZ6BFpG-Y1e10`QXJKbND7AMQ&cMj60B5TNObaZxYybcN07*qoM6N<$g3m;S%K!iX literal 0 HcmV?d00001 diff --git a/billsplit/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/billsplit/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..fe730945a01f64a61e2235dbe3f45b08f7729182 GIT binary patch literal 462 zcmV;<0WtoGP)-}iV`2<;=$?g5M=KQbZ{F&YRNy7Nn@%_*5{gvDM0aKI4?ESmw z{NnZg)A0R`+4?NF_RZexyVB&^^ZvN!{I28tr{Vje;QNTz`dG&Jz0~Ek&f2;*Z7>B|cg}xYpxEFY+0YrKLF;^Q+-HreN0P{&i zK~zY`?b7ECf-n?@;d<&orQ*Q7KoR%4|C>{W^h6@&01>0SKS`dn{Q}GT%Qj_{PLZ_& zs`MFI#j-(>?bvdZ!8^xTwlY{qA)T4QLbY@j(!YJ7aXJervHy6HaG_2SB`6CC{He}f zHVw(fJWApwPq!6VY7r1w-Fs)@ox~N+q|w~e;JI~C4Vf^@d>Wvj=fl`^u9x9wd9 zR%3*Q+)t%S!MU_`id^@&Y{y7-r98lZX0?YrHlfmwb?#}^1b{8g&KzmkE(L>Z&)179 zp<)v6Y}pRl100G2FL_t(o!|l{-Q-VMg#&MKg7c{O0 z2wJImOS3Gy*Z2Qifdv~JYOp;v+U)a|nLoc7hNH;I$;lzDt$}rkaFw1mYK5_0Q(Sut zvbEloxON7$+HSOgC9Z8ltuC&0OSF!-mXv5caV>#bc3@hBPX@I$58-z}(ZZE!t-aOG zpjNkbau@>yEzH(5Yj4kZiMH32XI!4~gVXNnjAvRx;Sdg^`>2DpUEwoMhTs_st8pKG z(%SHyHdU&v%f36~uERh!bd`!T2dw;z6PrOTQ7Vt*#9F2uHlUVnb#ev_o^fh}Dzmq} zWtlk35}k=?xj28uO|5>>$yXadTUE@@IPpgH`gJ~Ro4>jd1IF|(+IX>8M4Ps{PNvmI zNj4D+XgN83gPt_Gm}`Ybv{;+&yu-C(Grdiahmo~BjG-l&mWM+{e5M1sm&=xduwgM9 z`8OEh`=F3r`^E{n_;%9weN{cf2%7=VzC@cYj+lg>+3|D|_1C@{hcU(DyQG_BvBWe? zvTv``=%b1zrol#=R`JB)>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c literal 0 HcmV?d00001 diff --git a/billsplit/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/billsplit/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..502f463a9bc882b461c96aadf492d1729e49e725 GIT binary patch literal 586 zcmV-Q0=4~#P)+}#`wDE{8-2Mebf5<{{PqV{TgVcv*r8?UZ3{-|G?_}T*&y;@cqf{ z{Q*~+qr%%p!1pS*_Uicl#q9lc(D`!D`LN62sNwq{oYw(Wmhk)k<@f$!$@ng~_5)Ru z0Z)trIA5^j{DIW^c+vT2%lW+2<(RtE2wR;4O@)Tm`Xr*?A(qYoM}7i5Yxw>D(&6ou zxz!_Xr~yNF+waPe00049Nkl*;a!v6h%{rlvIH#gW3s8p;bFr=l}mRqpW2h zw=OA%hdyL~z+UHOzl0eKhEr$YYOL-c-%Y<)=j?(bzDweB7{b+%_ypvm_cG{SvM=DK zhv{K@m>#Bw>2W$eUI#iU)Wdgs8Y3U+A$Gd&{+j)d)BmGKx+43U_!tik_YlN)>$7G! zhkE!s;%oku3;IwG3U^2kw?z+HM)jB{@zFhK8P#KMSytSthr+4!c(5c%+^UBn`0X*2 zy3(k600_CSZj?O$Qu%&$;|TGUJrptR(HzyIx>5E(2r{eA(<6t3e3I0B)7d6s7?Z5J zZ!rtKvA{MiEBm&KFtoifx>5P^Z=vl)95XJn()aS5%ad(s?4-=Tkis9IGu{`Fy8r+H07*qoM6N<$f20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU literal 0 HcmV?d00001 diff --git a/billsplit/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/billsplit/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0ec303439225b78712f49115768196d8d76f6790 GIT binary patch literal 862 zcmV-k1EKthP)20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU literal 0 HcmV?d00001 diff --git a/billsplit/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/billsplit/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..e9f5fea27c705180eb716271f41b582e76dcbd90 GIT binary patch literal 1674 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 literal 0 HcmV?d00001 diff --git a/billsplit/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/billsplit/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0467bf12aa4d28f374bb26596605a46dcbb3e7c8 GIT binary patch literal 1418 zcmV;51$Fv~P)q zKfU)WzW*n(@|xWGCA9ScMt*e9`2kdxPQ&&>|-UCa7_51w+ zLUsW@ZzZSW0y$)Hp~e9%PvP|a03ks1`~K?q{u;6NC8*{AOqIUq{CL&;p56Lf$oQGq z^={4hPQv)y=I|4n+?>7Fim=dxt1 z2H+Dm+1+fh+IF>G0SjJMkQQre1x4|G*Z==(Ot&kCnUrL4I(rf(ucITwmuHf^hXiJT zkdTm&kdTm&kdTm&kdP`esgWG0BcWCVkVZ&2dUwN`cgM8QJb`Z7Z~e<&Yj2(}>Tmf` zm1{eLgw!b{bXkjWbF%dTkTZEJWyWOb##Lfw4EK2}<0d6%>AGS{po>WCOy&f$Tay_> z?NBlkpo@s-O;0V%Y_Xa-G#_O08q5LR*~F%&)}{}r&L%Sbs8AS4t7Y0NEx*{soY=0MZExqA5XHQkqi#4gW3 zqODM^iyZl;dvf)-bOXtOru(s)Uc7~BFx{w-FK;2{`VA?(g&@3z&bfLFyctOH!cVsF z7IL=fo-qBndRUm;kAdXR4e6>k-z|21AaN%ubeVrHl*<|s&Ax@W-t?LR(P-24A5=>a z*R9#QvjzF8n%@1Nw@?CG@6(%>+-0ASK~jEmCV|&a*7-GKT72W<(TbSjf)&Eme6nGE z>Gkj4Sq&2e+-G%|+NM8OOm5zVl9{Z8Dd8A5z3y8mZ=4Bv4%>as_{9cN#bm~;h>62( zdqY93Zy}v&c4n($Vv!UybR8ocs7#zbfX1IY-*w~)p}XyZ-SFC~4w>BvMVr`dFbelV{lLL0bx7@*ZZdebr3`sP;? zVImji)kG)(6Juv0lz@q`F!k1FE;CQ(D0iG$wchPbKZQELlsZ#~rt8#90Y_Xh&3U-< z{s<&cCV_1`^TD^ia9!*mQDq& zn2{r`j};V|uV%_wsP!zB?m%;FeaRe+X47K0e+KE!8C{gAWF8)lCd1u1%~|M!XNRvw zvtqy3iz0WSpWdhn6$hP8PaRBmp)q`#PCA`Vd#Tc$@f1tAcM>f_I@bC)hkI9|o(Iqv zo}Piadq!j76}004RBio<`)70k^`K1NK)q>w?p^C6J2ZC!+UppiK6&y3Kmbv&O!oYF z34$0Z;QO!JOY#!`qyGH<3Pd}Pt@q*A0V=3SVtWKRR8d8Z&@)3qLPA19LPA19LPEUC YUoZo%k(ykuW&i*H07*qoM6N<$f+CH{y8r+H literal 0 HcmV?d00001 diff --git a/billsplit/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/billsplit/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..781d7cd --- /dev/null +++ b/billsplit/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images": [ + { + "idiom": "universal", + "filename": "LaunchImage.png", + "scale": "1x" + }, + { + "idiom": "universal", + "filename": "LaunchImage@2x.png", + "scale": "2x" + }, + { + "idiom": "universal", + "filename": "LaunchImage@3x.png", + "scale": "3x" + } + ], + "info": { + "version": 1, + "author": "xcode" + } +} diff --git a/billsplit/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/billsplit/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/billsplit/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/billsplit/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/billsplit/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/billsplit/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/billsplit/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/billsplit/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..b5b843a --- /dev/null +++ b/billsplit/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. diff --git a/billsplit/ios/Runner/Base.lproj/LaunchScreen.storyboard b/billsplit/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/billsplit/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/billsplit/ios/Runner/Base.lproj/Main.storyboard b/billsplit/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/billsplit/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/billsplit/ios/Runner/Info.plist b/billsplit/ios/Runner/Info.plist new file mode 100644 index 0000000..bf9bdb6 --- /dev/null +++ b/billsplit/ios/Runner/Info.plist @@ -0,0 +1,70 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Billsplit + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + billsplit + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/billsplit/ios/Runner/Runner-Bridging-Header.h b/billsplit/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/billsplit/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/billsplit/ios/Runner/SceneDelegate.swift b/billsplit/ios/Runner/SceneDelegate.swift new file mode 100644 index 0000000..b9ce8ea --- /dev/null +++ b/billsplit/ios/Runner/SceneDelegate.swift @@ -0,0 +1,6 @@ +import Flutter +import UIKit + +class SceneDelegate: FlutterSceneDelegate { + +} diff --git a/billsplit/ios/RunnerTests/RunnerTests.swift b/billsplit/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/billsplit/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/billsplit/lib/domain/eparagon_parser.dart b/billsplit/lib/domain/eparagon_parser.dart new file mode 100644 index 0000000..35aa51b --- /dev/null +++ b/billsplit/lib/domain/eparagon_parser.dart @@ -0,0 +1,293 @@ +/// Parser for Polish fiscal e-receipts (e-paragon / JPK_KASA_PARAGON_v2-0), +/// e.g. the JSON files produced by the Biedronka app. +/// +/// Accepted inputs: +/// * the full wrapper JSON containing a `data` field with a JWS +/// (header.payload.signature, base64url) whose payload is the JPK document; +/// * the decoded JPK payload itself (an object with `dokument.paragon`); +/// * a bare JWS token string. +/// +/// The signature is NOT verified — this app only reads the contents. +library; + +import 'dart:convert'; + +import 'package:billsplit/domain/models.dart'; + +/// A parsed receipt plus non-fatal [warnings] collected on the way. +class EParagonResult { + /// Bundles a parsed [receipt] with its [warnings]. + EParagonResult({required this.receipt, required this.warnings}); + + /// The parsed receipt. + final Receipt receipt; + + /// Human-readable warnings (e.g. sum mismatches, skipped lines). + final List warnings; +} + +/// Thrown when the input cannot be interpreted as an e-paragon. +class EParagonParseException implements Exception { + /// Creates the exception with a user-facing [message]. + EParagonParseException(this.message); + + /// User-facing description of what went wrong. + final String message; + + @override + String toString() => 'EParagonParseException: $message'; +} + +const _alcoholKeywords = [ + 'wódk', + 'wodka', + 'vodka', + 'piwo', + 'wino', + 'whisk', + 'likier', + 'nalewk', + 'cydr', + ' rum', + 'gin ', + 'tequil', + 'brandy', + 'koniak', + 'bourbon', +]; + +const _mixerKeywords = [ + 'tonic', + 'tonik', + 'kinley', + 'citrus mix', + 'limonka', + 'cytryna', +]; + +/// Parses [source] and returns just the receipt; see [parseEParagonDetailed]. +Receipt parseEParagon(String source) => parseEParagonDetailed(source).receipt; + +/// Parses [source] (wrapper JSON, bare JPK payload, or bare JWS) into a +/// [Receipt] plus warnings. Throws [EParagonParseException] on unusable input. +EParagonResult parseEParagonDetailed(String source) { + final warnings = []; + final payload = _extractPayload(source); + + final dokument = payload['dokument']; + if (dokument is! Map) { + throw EParagonParseException('No "dokument" object in payload.'); + } + final paragon = dokument['paragon']; + if (paragon is! Map) { + throw EParagonParseException('No "dokument.paragon" object in payload.'); + } + + final store = _storeName(dokument); + final date = _saleDate(paragon, dokument); + + final items = []; + final pozycje = paragon['pozycja']; + if (pozycje is List) { + for (final p in pozycje) { + final towar = (p is Map) ? p['towar'] : null; + if (towar is! Map) continue; + final it = _itemFromTowar(towar.cast(), warnings); + if (it != null) items.add(it); + } + } + if (items.isEmpty) { + throw EParagonParseException('Receipt has no line items.'); + } + + // Bottle deposits ("opakowania zwrotne" / kaucja) are outside sumaBrutto but + // part of what was paid; add each as a regular splittable line. + final opak = paragon['opak']; + if (opak is Map) { + final dane = opak['daneOpak']; + if (dane is List) { + for (final d in dane) { + if (d is! Map) continue; + final cena = _asInt(d['cena']); + final iloscMil = _asInt(d['ilosc']); // thousandths + final qty = iloscMil / 1000; + final total = (cena * iloscMil / 1000).round(); + if (total == 0) continue; + items.add( + Item( + id: Ids.next(), + name: 'Kaucja: ' + '${(d['nazwa'] as String? ?? 'opakowanie').trim()} (deposit)', + totalGr: total, + qty: qty, + unitPriceGr: cena, + category: Categories.deposit, + ), + ); + } + } + } + + // Reconciliation figures. + var paid = _asIntOrNull((paragon['total'] as Map?)?['zaplZwrot']); + final sumaBrutto = _asIntOrNull((paragon['podsum'] as Map?)?['sumaBrutto']); + final opakWart = _asIntOrNull((paragon['opak'] as Map?)?['wart']) ?? 0; + paid ??= sumaBrutto == null ? null : sumaBrutto + opakWart; + + if (sumaBrutto != null) { + final goods = items + .where((i) => i.category != Categories.deposit) + .fold(0, (s, i) => s + i.totalGr); + if (goods != sumaBrutto) { + warnings.add( + 'Sum of parsed items (${formatPln(goods)}) differs from receipt ' + 'sumaBrutto (${formatPln(sumaBrutto)}).', + ); + } + } + + final title = [ + if (store != null) store, + _iso(date), + ].join(' '); + + return EParagonResult( + receipt: Receipt( + id: Ids.next(), + title: title, + store: store, + date: date, + items: items, + paidTotalGr: paid, + ), + warnings: warnings, + ); +} + +String? _storeName(Map dokument) { + final podmiot = dokument['podmiot1']; + if (podmiot is! Map) return null; + final name = (podmiot['nazwaPod'] as String?)?.trim(); + if (name == null || name.isEmpty) return null; + // Friendlier alias for the most common chain. + if (name.toUpperCase().contains('JERONIMO MARTINS')) return 'Biedronka'; + return name; +} + +DateTime _saleDate( + Map paragon, + Map dokument, +) { + final raw = + paragon['zakSprzed'] ?? (dokument['naglowek'] as Map?)?['dataJPK']; + if (raw is String) { + final d = DateTime.tryParse(raw); + if (d != null) return d.toLocal(); + } + return DateTime.now(); +} + +String _iso(DateTime d) { + final m = d.month.toString().padLeft(2, '0'); + final day = d.day.toString().padLeft(2, '0'); + return '${d.year}-$m-$day'; +} + +Map _extractPayload(String source) { + dynamic root; + try { + root = jsonDecode(source.trim()); + } on FormatException { + // Maybe the user pasted just the JWS token. + final p = _tryJws(source.trim()); + if (p != null) return p; + throw EParagonParseException( + 'Input is neither valid JSON nor a JWS.', + ); + } + if (root is Map) { + if (root['dokument'] is Map) return root.cast(); + final data = root['data']; + if (data is String) { + final p = _tryJws(data); + if (p != null) return p; + throw EParagonParseException('"data" field is not a decodable JWS.'); + } + } + throw EParagonParseException( + 'Unrecognized structure: expected e-paragon wrapper or JPK payload.', + ); +} + +Map? _tryJws(String token) { + final parts = token.split('.'); + if (parts.length != 3) return null; + try { + final normalized = base64Url.normalize(parts[1]); + final decoded = utf8.decode(base64Url.decode(normalized)); + final obj = jsonDecode(decoded); + return obj is Map ? obj.cast() : null; + } on FormatException { + return null; + } +} + +Item? _itemFromTowar(Map t, List warnings) { + var name = (t['nazwa'] as String? ?? '').trim(); + // Names end with the VAT rate letter, e.g. "Tonic A". + if (name.length > 2 && + name[name.length - 2] == ' ' && + RegExp(r'[A-G]$').hasMatch(name)) { + name = name.substring(0, name.length - 2).trim(); + } + name = name.replaceAll(RegExp(r'\s+'), ' '); + if (name.isEmpty) return null; + + final total = _asIntOrNull(t['brutto']); + if (total == null) { + warnings.add('Skipped "$name": missing brutto amount.'); + return null; + } + final unit = _asIntOrNull(t['cena']) ?? 0; + final qty = _parseQty(t['ilosc']); + final rabat = t['rabat']; + final discount = rabat is Map ? (_asIntOrNull(rabat['wart']) ?? 0) : 0; + + final lower = name.toLowerCase(); + var category = Categories.general; + var assignment = Assignment(); + if (_alcoholKeywords.any(lower.contains)) { + category = Categories.alcohol; + assignment = Assignment(mode: AssignMode.drinkers); + } else if (_mixerKeywords.any(lower.contains)) { + category = Categories.mixer; // still split among everyone by default + } + + return Item( + id: Ids.next(), + name: name, + totalGr: total, + qty: qty, + unitPriceGr: unit, + discountGr: discount, + category: category, + assignment: assignment, + ); +} + +double _parseQty(dynamic v) { + if (v is num) return v.toDouble(); + if (v is String) { + return double.tryParse(v.replaceAll(',', '.').trim()) ?? 1; + } + return 1; +} + +int _asInt(dynamic v) => _asIntOrNull(v) ?? 0; + +int? _asIntOrNull(dynamic v) { + if (v is int) return v; + if (v is num) return v.round(); + if (v is String) return int.tryParse(v); + return null; +} diff --git a/billsplit/lib/domain/models.dart b/billsplit/lib/domain/models.dart new file mode 100644 index 0000000..771e512 --- /dev/null +++ b/billsplit/lib/domain/models.dart @@ -0,0 +1,278 @@ +/// Domain models. All money values are ints in grosze (1/100 PLN) to avoid +/// floating point drift; formatting happens only at the UI/export edge. +library; + +/// Formats [grosze] as a Polish złoty string, e.g. `1234` → `12,34 zł`. +String formatPln(int grosze) { + final neg = grosze < 0; + final a = grosze.abs(); + final zl = a ~/ 100; + final gr = (a % 100).toString().padLeft(2, '0'); + return '${neg ? '-' : ''}$zl,$gr zł'; +} + +/// A person on the shared roster. +class Person { + /// Creates a person; [drinks] controls alcohol-item defaults. + Person({required this.id, required this.name, this.drinks = true}); + + /// Restores a person from its [toJson] map. + factory Person.fromJson(Map j) => Person( + id: j['id'] as String, + name: j['name'] as String, + drinks: j['drinks'] as bool? ?? true, + ); + + /// Stable unique id. + final String id; + + /// Display name. + String name; + + /// Whether this person shares alcohol items by default. + bool drinks; + + /// JSON representation for persistence. + Map toJson() => {'id': id, 'name': name, 'drinks': drinks}; +} + +/// A user-defined tag over roster members, e.g. "meat eaters". +class Group { + /// Creates a group with optional initial [memberIds]. + Group({required this.id, required this.name, Set? memberIds}) + : memberIds = memberIds ?? {}; + + /// Restores a group from its [toJson] map. + factory Group.fromJson(Map j) => Group( + id: j['id'] as String, + name: j['name'] as String, + memberIds: (j['memberIds'] as List? ?? const []) + .cast() + .toSet(), + ); + + /// Stable unique id. + final String id; + + /// Display name. + String name; + + /// Ids of [Person]s belonging to this group. + final Set memberIds; + + /// JSON representation for persistence. + Map toJson() => + {'id': id, 'name': name, 'memberIds': memberIds.toList()}; +} + +/// How an item is assigned to people. +enum AssignMode { + /// Everyone on the receipt shares the item. + everyone, + + /// Only participants with [Person.drinks] set share the item. + drinkers, + + /// Only participants without [Person.drinks] share the item. + nonDrinkers, + + /// Members of one [Group] share the item. + group, + + /// An explicit hand-picked set of people shares the item. + custom, +} + +/// The assignment of one item: a symbolic mode plus mode-specific data. +class Assignment { + /// Creates an assignment; defaults to [AssignMode.everyone]. + Assignment({ + this.mode = AssignMode.everyone, + this.groupId, + Set? customIds, + }) : customIds = customIds ?? {}; + + /// Restores an assignment from its [toJson] map. + factory Assignment.fromJson(Map j) => Assignment( + mode: AssignMode.values.asNameMap()[j['mode']] ?? AssignMode.everyone, + groupId: j['groupId'] as String?, + customIds: (j['customIds'] as List? ?? const []) + .cast() + .toSet(), + ); + + /// Current mode. + AssignMode mode; + + /// Group id when [mode] is [AssignMode.group]. + String? groupId; + + /// Hand-picked person ids when [mode] is [AssignMode.custom]. + final Set customIds; + + /// JSON representation for persistence. + Map toJson() => { + 'mode': mode.name, + if (groupId != null) 'groupId': groupId, + 'customIds': customIds.toList(), + }; +} + +/// Standard item categories. [Item.category] is free text so users can add +/// their own; these constants are the ones the importer produces. +abstract final class Categories { + /// Alcoholic drinks — default to drinkers only. + static const alcohol = 'Alcohol'; + + /// Mixers (tonic, limes, …) — tagged for visibility, split among everyone. + static const mixer = 'Mixer'; + + /// Returnable bottle deposits (kaucja). + static const deposit = 'Deposit'; + + /// Anything else. + static const general = 'General'; +} + +/// One receipt line. +class Item { + /// Creates an item; [totalGr] is the post-discount line total. + Item({ + required this.id, + required this.name, + required this.totalGr, + this.qty = 1, + this.unitPriceGr = 0, + this.discountGr = 0, + this.category = Categories.general, + Assignment? assignment, + }) : assignment = assignment ?? Assignment(); + + /// Restores an item from its [toJson] map. + factory Item.fromJson(Map j) => Item( + id: j['id'] as String, + name: j['name'] as String, + totalGr: j['totalGr'] as int, + qty: (j['qty'] as num? ?? 1).toDouble(), + unitPriceGr: j['unitPriceGr'] as int? ?? 0, + discountGr: j['discountGr'] as int? ?? 0, + category: j['category'] as String? ?? Categories.general, + assignment: Assignment.fromJson( + (j['assignment'] as Map?)?.cast() ?? const {}, + ), + ); + + /// Stable unique id. + final String id; + + /// Display name. + String name; + + /// Final line total after discount — the ground truth used for splitting. + int totalGr; + + /// Quantity (may be fractional for weighed goods). + double qty; + + /// Unit price in grosze. + int unitPriceGr; + + /// Discount already included in [totalGr]; kept for display (<= 0). + int discountGr; + + /// Free-text category; see [Categories] for the standard values. + String category; + + /// Who shares this item. + Assignment assignment; + + /// JSON representation for persistence. + Map toJson() => { + 'id': id, + 'name': name, + 'totalGr': totalGr, + 'qty': qty, + 'unitPriceGr': unitPriceGr, + 'discountGr': discountGr, + 'category': category, + 'assignment': assignment.toJson(), + }; +} + +/// One shopping receipt with its items and participants. +class Receipt { + /// Creates a receipt; [participantIds] defaults to empty. + Receipt({ + required this.id, + required this.title, + this.store, + DateTime? date, + List? items, + Set? participantIds, + this.paidTotalGr, + }) : date = date ?? DateTime.now(), + items = items ?? [], + participantIds = participantIds ?? {}; + + /// Restores a receipt from its [toJson] map. + factory Receipt.fromJson(Map j) => Receipt( + id: j['id'] as String, + title: j['title'] as String, + store: j['store'] as String?, + date: DateTime.tryParse(j['date'] as String? ?? '') ?? DateTime.now(), + items: (j['items'] as List? ?? const []) + .map((e) => Item.fromJson((e as Map).cast())) + .toList(), + participantIds: (j['participantIds'] as List? ?? const []) + .cast() + .toSet(), + paidTotalGr: j['paidTotalGr'] as int?, + ); + + /// Stable unique id. + final String id; + + /// Display title. + String title; + + /// Store name, if known. + String? store; + + /// Sale date. + DateTime date; + + /// Line items. + final List items; + + /// Subset of the roster taking part in this receipt. + final Set participantIds; + + /// What was actually paid (incl. deposits) — used for the reconciliation + /// check. Null for manual receipts where the user never entered it. + int? paidTotalGr; + + /// Sum of all item totals in grosze. + int get itemsTotalGr => items.fold(0, (s, i) => s + i.totalGr); + + /// JSON representation for persistence. + Map toJson() => { + 'id': id, + 'title': title, + 'store': store, + 'date': date.toIso8601String(), + 'items': items.map((i) => i.toJson()).toList(), + 'participantIds': participantIds.toList(), + 'paidTotalGr': paidTotalGr, + }; +} + +/// Simple unique id source: time + counter. Good enough for a local, +/// single-user app. +class Ids { + static int _c = 0; + + /// Returns a new unique id. + static String next() => + '${DateTime.now().microsecondsSinceEpoch.toRadixString(36)}' + '-${(_c++).toRadixString(36)}'; +} diff --git a/billsplit/lib/domain/split_engine.dart b/billsplit/lib/domain/split_engine.dart new file mode 100644 index 0000000..cdddf36 --- /dev/null +++ b/billsplit/lib/domain/split_engine.dart @@ -0,0 +1,155 @@ +/// Split computation. Grosz-exact: every item's shares sum to exactly the +/// item total (largest-remainder distribution, deterministic order), so the +/// per-person totals always reconcile with the receipt to the grosz. +library; + +import 'package:billsplit/domain/models.dart'; + +/// Accumulated amounts one person owes on a receipt. +class PersonTotal { + /// Creates an empty total for [personId]. + PersonTotal(this.personId); + + /// The person this total belongs to. + final String personId; + + /// Everything owed, in grosze. + int totalGr = 0; + + /// The alcohol portion of [totalGr]. + int alcoholGr = 0; + + /// The non-alcohol portion of [totalGr]. + int get restGr => totalGr - alcoholGr; +} + +/// Result of [computeSplit]. +class SplitResult { + /// Bundles the computed maps. + SplitResult({ + required this.perPerson, + required this.itemShares, + required this.unassigned, + }); + + /// personId → totals. Contains every receipt participant, even if 0. + final Map perPerson; + + /// itemId → (personId → share in grosze). Only resolved participants. + final Map> itemShares; + + /// Items that resolved to zero participants (excluded from totals). + final List unassigned; + + /// Sum of all per-person totals, in grosze. + int get assignedTotalGr => perPerson.values.fold(0, (s, p) => s + p.totalGr); +} + +/// People from [Receipt.participantIds] that still exist in [roster]. +List receiptParticipants(Receipt receipt, List roster) => + roster.where((p) => receipt.participantIds.contains(p.id)).toList(); + +/// Resolves an item's assignment to concrete people, always intersected with +/// the receipt's participant pool. +List resolveParticipants( + Item item, + Receipt receipt, + List roster, + List groups, +) { + final pool = receiptParticipants(receipt, roster); + final a = item.assignment; + switch (a.mode) { + case AssignMode.everyone: + return pool; + case AssignMode.drinkers: + return pool.where((p) => p.drinks).toList(); + case AssignMode.nonDrinkers: + return pool.where((p) => !p.drinks).toList(); + case AssignMode.group: + final g = groups.where((g) => g.id == a.groupId).firstOrNull; + if (g == null) return const []; + return pool.where((p) => g.memberIds.contains(p.id)).toList(); + case AssignMode.custom: + return pool.where((p) => a.customIds.contains(p.id)).toList(); + } +} + +/// Splits [totalGr] between [personIds]. Base share is floor(total/n); the +/// remainder is handed out one grosz at a time starting at a position derived +/// from [rotation] (e.g. a stable hash of the item id). Rotating the start +/// per item avoids systematically overcharging the same person across a whole +/// receipt, while staying fully deterministic. Shares always sum to exactly +/// [totalGr] (also for negative totals, e.g. returns). +Map splitAmount( + int totalGr, + List personIds, { + int rotation = 0, +}) { + if (personIds.isEmpty) return const {}; + final ids = [...personIds]..sort(); + final n = ids.length; + final base = (totalGr / n).floor(); + final remainder = totalGr - base * n; // 0 <= remainder < n, also if negative + final start = ((rotation % n) + n) % n; + final out = {for (final id in ids) id: base}; + for (var k = 0; k < remainder; k++) { + out[ids[(start + k) % n]] = base + 1; + } + return out; +} + +/// Stable string hash (unlike `String.hashCode`, which is not guaranteed +/// identical across Dart VM runs) so splits don't shift between app launches. +int stableHash(String s) { + var h = 0; + for (final c in s.codeUnits) { + h = (h * 31 + c) & 0x7fffffff; + } + return h; +} + +/// Computes the full split of [receipt] between [roster] members using +/// [groups] for group-mode assignments. +SplitResult computeSplit( + Receipt receipt, + List roster, + List groups, +) { + final perPerson = { + for (final p in receiptParticipants(receipt, roster)) + p.id: PersonTotal(p.id), + }; + final itemShares = >{}; + final unassigned = []; + + for (final item in receipt.items) { + final people = resolveParticipants(item, receipt, roster, groups); + if (people.isEmpty) { + unassigned.add(item); + continue; + } + final shares = splitAmount( + item.totalGr, + people.map((p) => p.id).toList(), + rotation: stableHash(item.id), + ); + itemShares[item.id] = shares; + final isAlcohol = item.category == Categories.alcohol; + shares.forEach((pid, amount) { + perPerson[pid]! + ..totalGr += amount + ..alcoholGr += isAlcohol ? amount : 0; + }); + } + + return SplitResult( + perPerson: perPerson, + itemShares: itemShares, + unassigned: unassigned, + ); +} + +extension on Iterable { + T? get firstOrNull => isEmpty ? null : first; +} diff --git a/billsplit/lib/domain/xlsx_export.dart b/billsplit/lib/domain/xlsx_export.dart new file mode 100644 index 0000000..b2231a3 --- /dev/null +++ b/billsplit/lib/domain/xlsx_export.dart @@ -0,0 +1,215 @@ +/// Exports a receipt + computed split to an .xlsx workbook whose layout +/// mirrors the hand-made "podzial_rachunku" sheet: one row per item, a 1/0 +/// person matrix, per-item cost-per-person, and a per-person summary. +/// +/// Values are written as computed numbers (grosz-exact, matching the in-app +/// engine), plus live SUM formulas for the column totals so the sheet stays +/// consistent when opened in Excel/LibreOffice. +library; + +import 'package:billsplit/domain/models.dart'; +import 'package:billsplit/domain/split_engine.dart'; +import 'package:excel/excel.dart'; + +/// Renders [receipt] with its current split into xlsx bytes. +List exportXlsx({ + required Receipt receipt, + required List roster, + required List groups, +}) { + final excel = Excel.createExcel()..rename('Sheet1', 'Split'); + final sheet = excel['Split']; + + final people = receiptParticipants(receipt, roster) + ..sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); + final split = computeSplit(receipt, roster, groups); + + final bold = CellStyle(bold: true); + final boldGrey = CellStyle( + bold: true, + backgroundColorHex: ExcelColor.fromHexString('#D9D9D9'), + ); + final alcoholStyle = + CellStyle(backgroundColorHex: ExcelColor.fromHexString('#FCE4D6')); + final warnStyle = CellStyle( + bold: true, + backgroundColorHex: ExcelColor.fromHexString('#FFC7CE'), + ); + + void put(int col, int row, CellValue value, [CellStyle? style]) { + final index = CellIndex.indexByColumnRow(columnIndex: col, rowIndex: row); + final cell = sheet.cell(index)..value = value; + if (style != null) cell.cellStyle = style; + } + + double zl(int gr) => gr / 100.0; + + var r = 0; + put(0, r, TextCellValue(receipt.title), bold); + r++; + put( + 0, + r, + TextCellValue( + [ + if (receipt.store != null) receipt.store!, + receipt.date.toIso8601String().substring(0, 10), + if (receipt.paidTotalGr != null) + 'paid ${formatPln(receipt.paidTotalGr!)}', + ].join(' · '), + ), + ); + r += 2; + + // Header row. + const fixed = ['Lp', 'Item', 'Category', 'Qty', 'Unit', 'Discount', 'Total']; + for (var c = 0; c < fixed.length; c++) { + put(c, r, TextCellValue(fixed[c]), boldGrey); + } + const p0 = 7; + for (var i = 0; i < people.length; i++) { + put(p0 + i, r, TextCellValue(people[i].name), boldGrey); + } + put(p0 + people.length, r, TextCellValue('People on item'), boldGrey); + put(p0 + people.length + 1, r, TextCellValue('Cost / person'), boldGrey); + r++; + + final firstDataRow = r; + var lp = 1; + for (final item in receipt.items) { + final shares = split.itemShares[item.id] ?? const {}; + final isAlcohol = item.category == Categories.alcohol; + final rowStyle = shares.isEmpty + ? warnStyle + : isAlcohol + ? alcoholStyle + : null; + put(0, r, IntCellValue(lp++), rowStyle); + put(1, r, TextCellValue(item.name), rowStyle); + put(2, r, TextCellValue(item.category), rowStyle); + put(3, r, DoubleCellValue(item.qty), rowStyle); + put(4, r, DoubleCellValue(zl(item.unitPriceGr)), rowStyle); + put(5, r, DoubleCellValue(zl(item.discountGr)), rowStyle); + put(6, r, DoubleCellValue(zl(item.totalGr)), rowStyle); + for (var i = 0; i < people.length; i++) { + put( + p0 + i, + r, + IntCellValue(shares.containsKey(people[i].id) ? 1 : 0), + rowStyle, + ); + } + put(p0 + people.length, r, IntCellValue(shares.length), rowStyle); + if (shares.isNotEmpty) { + // Representative value; the exact grosz split may differ by ±0.01 + // between people — the summary uses the exact per-person numbers. + put( + p0 + people.length + 1, + r, + DoubleCellValue(zl((item.totalGr / shares.length).round())), + rowStyle, + ); + } else { + put(p0 + people.length + 1, r, TextCellValue('UNASSIGNED'), warnStyle); + } + r++; + } + final lastDataRow = r - 1; + + // Totals row with a live formula over the Total column. + put(1, r, TextCellValue('TOTAL'), bold); + final totalCol = colName(6); + put( + 6, + r, + FormulaCellValue( + 'SUM($totalCol${firstDataRow + 1}:$totalCol${lastDataRow + 1})', + ), + bold, + ); + r += 2; + + // Per-person summary (exact grosz numbers from the engine). + put(1, r, TextCellValue('SUMMARY'), bold); + r++; + put(1, r, TextCellValue('Person'), boldGrey); + put(2, r, TextCellValue('Owes'), boldGrey); + put(3, r, TextCellValue('of which alcohol'), boldGrey); + put(4, r, TextCellValue('rest'), boldGrey); + r++; + final firstSummaryRow = r; + for (final p in people) { + final t = split.perPerson[p.id]; + put(1, r, TextCellValue(p.name + (p.drinks ? '' : ' (non-drinker)'))); + put(2, r, DoubleCellValue(zl(t?.totalGr ?? 0))); + put(3, r, DoubleCellValue(zl(t?.alcoholGr ?? 0))); + put(4, r, DoubleCellValue(zl(t?.restGr ?? 0))); + r++; + } + put(1, r, TextCellValue('SUM'), bold); + for (var c = 2; c <= 4; c++) { + final col = colName(c); + put(c, r, FormulaCellValue('SUM($col$firstSummaryRow:$col$r)'), bold); + } + r += 2; + + // Reconciliation note. + final assigned = split.assignedTotalGr; + final itemsTotal = receipt.itemsTotalGr; + put( + 1, + r, + TextCellValue( + _checkLine(receipt, assigned, itemsTotal, split), + ), + split.unassigned.isEmpty && assigned == itemsTotal ? bold : warnStyle, + ); + r += 2; + put( + 0, + r, + TextCellValue( + '1 = person shares the item. Matrix and summary are exported values ' + 'from the app (grosz-exact); edit splits in the app, not here.', + ), + ); + + // Column widths. + sheet + ..setColumnWidth(0, 5) + ..setColumnWidth(1, 34) + ..setColumnWidth(2, 11); + for (var c = 3; c < 7; c++) { + sheet.setColumnWidth(c, 10); + } + for (var i = 0; i < people.length + 2; i++) { + sheet.setColumnWidth(p0 + i, 12); + } + + return excel.encode()!; +} + +String _checkLine( + Receipt receipt, + int assigned, + int itemsTotal, + SplitResult split, +) { + final paid = receipt.paidTotalGr; + final paidPart = paid == null ? '' : ' / paid ${formatPln(paid)}'; + final un = split.unassigned.length; + final unPart = un == 0 ? '' : ' — $un UNASSIGNED item(s) excluded'; + return 'Check: assigned ${formatPln(assigned)} ' + '/ items ${formatPln(itemsTotal)}$paidPart$unPart'; +} + +/// Converts a 0-based column index into its A1-style name (0 → A, 26 → AA). +String colName(int index) { + var i = index; + var s = ''; + while (i >= 0) { + s = String.fromCharCode(65 + (i % 26)) + s; + i = i ~/ 26 - 1; + } + return s; +} diff --git a/billsplit/lib/main.dart b/billsplit/lib/main.dart new file mode 100644 index 0000000..cd84c2c --- /dev/null +++ b/billsplit/lib/main.dart @@ -0,0 +1,37 @@ +import 'dart:async'; + +import 'package:billsplit/state/app_state.dart'; +import 'package:billsplit/ui/home_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + final state = AppState(); + unawaited(state.load()); + runApp(BillSplitApp(state: state)); +} + +/// Root widget wiring [AppState] into the tree. +class BillSplitApp extends StatelessWidget { + /// Creates the app around an existing [state]. + const BillSplitApp({required this.state, super.key}); + + /// The single app-wide state instance. + final AppState state; + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider.value( + value: state, + child: MaterialApp( + title: 'BillSplit', + debugShowCheckedModeBanner: false, + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal), + ), + home: const HomeScreen(), + ), + ); + } +} diff --git a/billsplit/lib/state/app_state.dart b/billsplit/lib/state/app_state.dart new file mode 100644 index 0000000..9c3ba73 --- /dev/null +++ b/billsplit/lib/state/app_state.dart @@ -0,0 +1,166 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:billsplit/domain/models.dart'; +import 'package:flutter/foundation.dart'; +import 'package:path_provider/path_provider.dart'; + +/// Single source of truth: roster, groups, receipts. Persisted as one JSON +/// file in the app documents directory; saves are debounced. +class AppState extends ChangeNotifier { + /// Creates the state; [overrideDir] replaces the platform documents + /// directory (used by tests). + AppState({Directory? overrideDir}) : _overrideDir = overrideDir; + + final Directory? _overrideDir; + + /// The shared roster of people. + final List people = []; + + /// User-defined groups over [people]. + final List groups = []; + + /// Saved receipts, newest first. + final List receipts = []; + + /// Whether [load] has completed. + bool loaded = false; + + Timer? _saveTimer; + + Future _file() async { + final dir = _overrideDir ?? await getApplicationDocumentsDirectory(); + final d = Directory('${dir.path}/billsplit'); + if (!d.existsSync()) d.createSync(recursive: true); + return File('${d.path}/state.json'); + } + + /// Loads persisted state; failures leave the state empty but usable. + Future load() async { + try { + final f = await _file(); + if (f.existsSync()) { + final j = jsonDecode(f.readAsStringSync()) as Map; + people + ..clear() + ..addAll( + (j['people'] as List? ?? const []) + .map((e) => Person.fromJson((e as Map).cast())), + ); + groups + ..clear() + ..addAll( + (j['groups'] as List? ?? const []) + .map((e) => Group.fromJson((e as Map).cast())), + ); + receipts + ..clear() + ..addAll( + (j['receipts'] as List? ?? const []) + .map((e) => Receipt.fromJson((e as Map).cast())), + ); + _prune(); + } + } on FormatException catch (e) { + debugPrint('State load failed (corrupt JSON): $e'); + } on FileSystemException catch (e) { + debugPrint('State load failed (IO): $e'); + } + loaded = true; + notifyListeners(); + } + + /// Drop references to people that no longer exist. + void _prune() { + final ids = people.map((p) => p.id).toSet(); + for (final g in groups) { + g.memberIds.removeWhere((id) => !ids.contains(id)); + } + for (final r in receipts) { + r.participantIds.removeWhere((id) => !ids.contains(id)); + for (final i in r.items) { + i.assignment.customIds.removeWhere((id) => !ids.contains(id)); + } + } + } + + void _scheduleSave() { + _saveTimer?.cancel(); + _saveTimer = Timer(const Duration(milliseconds: 400), save); + } + + /// Writes the current state to disk immediately. + Future save() async { + try { + final f = await _file(); + f.writeAsStringSync( + jsonEncode({ + 'people': people.map((p) => p.toJson()).toList(), + 'groups': groups.map((g) => g.toJson()).toList(), + 'receipts': receipts.map((r) => r.toJson()).toList(), + }), + ); + } on FileSystemException catch (e) { + debugPrint('State save failed: $e'); + } + } + + /// Runs [fn], prunes dangling references, notifies, schedules a save. + void mutate(void Function() fn) { + fn(); + _prune(); + notifyListeners(); + _scheduleSave(); + } + + // Convenience CRUD ------------------------------------------------------- + + /// Adds a person to the roster and returns it. + Person addPerson(String name, {bool drinks = true}) { + final p = Person(id: Ids.next(), name: name, drinks: drinks); + mutate(() => people.add(p)); + return p; + } + + /// Removes [p] everywhere (roster, groups, receipts, assignments). + void removePerson(Person p) => mutate(() => people.remove(p)); + + /// Adds an empty group and returns it. + Group addGroup(String name) { + final g = Group(id: Ids.next(), name: name); + mutate(() => groups.add(g)); + return g; + } + + /// Removes [g]; items assigned to it fall back to everyone. + void removeGroup(Group g) => mutate(() { + groups.remove(g); + for (final r in receipts) { + for (final i in r.items) { + if (i.assignment.groupId == g.id) { + i.assignment + ..mode = AssignMode.everyone + ..groupId = null; + } + } + } + }); + + /// Inserts [r] at the top; empty participants default to the whole roster. + void addReceipt(Receipt r) => mutate(() { + if (r.participantIds.isEmpty) { + r.participantIds.addAll(people.map((p) => p.id)); + } + receipts.insert(0, r); + }); + + /// Deletes [r]. + void removeReceipt(Receipt r) => mutate(() => receipts.remove(r)); + + @override + void dispose() { + _saveTimer?.cancel(); + super.dispose(); + } +} diff --git a/billsplit/lib/ui/assignment_sheet.dart b/billsplit/lib/ui/assignment_sheet.dart new file mode 100644 index 0000000..6ef5d8d --- /dev/null +++ b/billsplit/lib/ui/assignment_sheet.dart @@ -0,0 +1,143 @@ +import 'package:billsplit/domain/models.dart'; +import 'package:billsplit/domain/split_engine.dart'; +import 'package:billsplit/state/app_state.dart'; +import 'package:flutter/material.dart'; + +/// Bottom sheet to assign an item: quick modes (everyone / drinkers / +/// non-drinkers / group) or explicit person selection. Toggling a person +/// checkbox on a non-custom mode first materializes the current selection, +/// then switches to custom. +Future showAssignmentSheet( + BuildContext context, + AppState state, + Receipt receipt, + Item item, +) { + return showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (c) => StatefulBuilder( + builder: (c, setSheet) { + final pool = receiptParticipants(receipt, state.people) + ..sort( + (a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()), + ); + final resolved = + resolveParticipants(item, receipt, state.people, state.groups) + .map((p) => p.id) + .toSet(); + + void setMode(AssignMode mode, {String? groupId}) { + state.mutate(() { + item.assignment + ..mode = mode + ..groupId = groupId; + }); + setSheet(() {}); + } + + void togglePerson(Person p, {required bool selected}) { + state.mutate(() { + if (item.assignment.mode != AssignMode.custom) { + item.assignment + ..customIds.clear() + ..customIds.addAll(resolved) + ..mode = AssignMode.custom + ..groupId = null; + } + if (selected) { + item.assignment.customIds.add(p.id); + } else { + item.assignment.customIds.remove(p.id); + } + }); + setSheet(() {}); + } + + final selectedSeg = switch (item.assignment.mode) { + AssignMode.everyone => 'everyone', + AssignMode.drinkers => 'drinkers', + AssignMode.nonDrinkers => 'nonDrinkers', + AssignMode.group => 'group:${item.assignment.groupId}', + AssignMode.custom => 'custom', + }; + + return SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(item.name, style: Theme.of(c).textTheme.titleMedium), + Text( + '${formatPln(item.totalGr)} · ${item.category}', + style: Theme.of(c).textTheme.bodySmall, + ), + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + ChoiceChip( + label: const Text('Everyone'), + selected: selectedSeg == 'everyone', + onSelected: (_) => setMode(AssignMode.everyone), + ), + ChoiceChip( + label: const Text('Drinkers'), + selected: selectedSeg == 'drinkers', + onSelected: (_) => setMode(AssignMode.drinkers), + ), + ChoiceChip( + label: const Text('Non-drinkers'), + selected: selectedSeg == 'nonDrinkers', + onSelected: (_) => setMode(AssignMode.nonDrinkers), + ), + for (final g in state.groups) + ChoiceChip( + label: Text(g.name), + selected: selectedSeg == 'group:${g.id}', + onSelected: (_) => + setMode(AssignMode.group, groupId: g.id), + ), + ], + ), + const SizedBox(height: 8), + Flexible( + child: ListView( + shrinkWrap: true, + children: [ + for (final p in pool) + CheckboxListTile( + dense: true, + title: Text(p.name), + subtitle: p.drinks ? null : const Text('non-drinker'), + value: resolved.contains(p.id), + onChanged: (v) => + togglePerson(p, selected: v ?? false), + ), + ], + ), + ), + Text( + resolved.isEmpty + ? 'Nobody selected — this item will be excluded from ' + 'totals until someone is assigned.' + : _eachLabel(resolved.length, item.totalGr), + style: TextStyle( + color: resolved.isEmpty ? Colors.red : Colors.black54, + fontSize: 12, + ), + ), + ], + ), + ), + ); + }, + ), + ); +} + +String _eachLabel(int n, int totalGr) => + '$n people · ~${formatPln((totalGr / n).round())} each'; diff --git a/billsplit/lib/ui/home_screen.dart b/billsplit/lib/ui/home_screen.dart new file mode 100644 index 0000000..dcdec10 --- /dev/null +++ b/billsplit/lib/ui/home_screen.dart @@ -0,0 +1,208 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:billsplit/domain/eparagon_parser.dart'; +import 'package:billsplit/domain/models.dart'; +import 'package:billsplit/state/app_state.dart'; +import 'package:billsplit/ui/people_screen.dart'; +import 'package:billsplit/ui/receipt_screen.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +/// Receipt list plus entry points for import / manual creation. +class HomeScreen extends StatelessWidget { + /// Creates the home screen. + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + final state = context.watch(); + return Scaffold( + appBar: AppBar( + title: const Text('BillSplit'), + actions: [ + IconButton( + tooltip: 'People & groups', + icon: const Icon(Icons.group), + onPressed: () => unawaited( + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const PeopleScreen()), + ), + ), + ), + ], + ), + body: !state.loaded + ? const Center(child: CircularProgressIndicator()) + : state.receipts.isEmpty + ? const _EmptyHint() + : ListView.builder( + itemCount: state.receipts.length, + itemBuilder: (context, i) { + final r = state.receipts[i]; + return Dismissible( + key: ValueKey(r.id), + direction: DismissDirection.endToStart, + background: Container( + color: Colors.red.shade300, + alignment: Alignment.centerRight, + padding: const EdgeInsets.only(right: 16), + child: const Icon(Icons.delete, color: Colors.white), + ), + confirmDismiss: (_) => _confirmDelete(context, r), + onDismissed: (_) => state.removeReceipt(r), + child: ListTile( + title: Text(r.title), + subtitle: Text( + '${r.items.length} items · ' + '${r.participantIds.length} people', + ), + trailing: Text( + formatPln(r.itemsTotalGr), + style: Theme.of(context).textTheme.titleMedium, + ), + onTap: () => unawaited( + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => ReceiptScreen(receiptId: r.id), + ), + ), + ), + ), + ); + }, + ), + floatingActionButton: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + FloatingActionButton.extended( + heroTag: 'import', + icon: const Icon(Icons.file_download), + label: const Text('Import e-paragon'), + onPressed: () => _import(context), + ), + const SizedBox(height: 12), + FloatingActionButton.extended( + heroTag: 'new', + icon: const Icon(Icons.add), + label: const Text('New receipt'), + onPressed: () => _createManual(context), + ), + ], + ), + ); + } + + Future _confirmDelete(BuildContext context, Receipt r) async { + return await showDialog( + context: context, + builder: (c) => AlertDialog( + title: const Text('Delete receipt?'), + content: Text('"${r.title}" will be removed permanently.'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(c, false), + child: const Text('Cancel'), + ), + FilledButton( + onPressed: () => Navigator.pop(c, true), + child: const Text('Delete'), + ), + ], + ), + ) ?? + false; + } + + Future _import(BuildContext context) async { + final state = context.read(); + final messenger = ScaffoldMessenger.of(context); + final nav = Navigator.of(context); + final picked = await FilePicker.pickFiles( + type: FileType.custom, + allowedExtensions: ['json'], + withData: true, + ); + final data = picked?.files.firstOrNull?.bytes; + if (data == null) return; + try { + final result = parseEParagonDetailed(utf8.decode(data)); + state.addReceipt(result.receipt); + for (final w in result.warnings) { + messenger.showSnackBar(SnackBar(content: Text(w))); + } + unawaited( + nav.push( + MaterialPageRoute( + builder: (_) => ReceiptScreen(receiptId: result.receipt.id), + ), + ), + ); + } on EParagonParseException catch (e) { + messenger.showSnackBar(SnackBar(content: Text(e.message))); + } on Exception catch (e) { + messenger.showSnackBar(SnackBar(content: Text('Import failed: $e'))); + } + } + + Future _createManual(BuildContext context) async { + final state = context.read(); + final nav = Navigator.of(context); + final controller = TextEditingController(); + final title = await showDialog( + context: context, + builder: (c) => AlertDialog( + title: const Text('New receipt'), + content: TextField( + controller: controller, + autofocus: true, + decoration: const InputDecoration(labelText: 'Title'), + onSubmitted: (v) => Navigator.pop(c, v), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(c), + child: const Text('Cancel'), + ), + FilledButton( + onPressed: () => Navigator.pop(c, controller.text), + child: const Text('Create'), + ), + ], + ), + ); + if (title == null || title.trim().isEmpty) return; + final r = Receipt(id: Ids.next(), title: title.trim()); + state.addReceipt(r); + unawaited( + nav.push( + MaterialPageRoute( + builder: (_) => ReceiptScreen(receiptId: r.id), + ), + ), + ); + } +} + +class _EmptyHint extends StatelessWidget { + const _EmptyHint(); + + @override + Widget build(BuildContext context) { + return Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Text( + 'No receipts yet.\n\nAdd people first (top right), then import a ' + 'Biedronka e-paragon JSON or create a receipt manually.', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyLarge, + ), + ), + ); + } +} diff --git a/billsplit/lib/ui/item_edit.dart b/billsplit/lib/ui/item_edit.dart new file mode 100644 index 0000000..227bee1 --- /dev/null +++ b/billsplit/lib/ui/item_edit.dart @@ -0,0 +1,130 @@ +import 'package:billsplit/domain/models.dart'; +import 'package:billsplit/state/app_state.dart'; +import 'package:flutter/material.dart'; + +/// Add ([item] == null) or edit an item. The user types amounts in złoty +/// ("12,34" or "12.34"); they are stored as int grosze. +Future showItemEditDialog( + BuildContext context, + AppState state, + Receipt receipt, + Item? item, +) async { + final name = TextEditingController(text: item?.name ?? ''); + final total = TextEditingController( + text: item == null ? '' : _zl(item.totalGr), + ); + final qty = TextEditingController(text: (item?.qty ?? 1).toString()); + var category = item?.category ?? Categories.general; + final categories = { + Categories.general, + Categories.alcohol, + Categories.mixer, + Categories.deposit, + ...receipt.items.map((i) => i.category), + }.toList(); + + await showDialog( + context: context, + builder: (c) => StatefulBuilder( + builder: (c, setState) => AlertDialog( + title: Text(item == null ? 'Add item' : 'Edit item'), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: name, + autofocus: item == null, + decoration: const InputDecoration(labelText: 'Name'), + ), + TextField( + controller: total, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration( + labelText: 'Total (zł)', + hintText: 'e.g. 12,34', + ), + ), + TextField( + controller: qty, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration(labelText: 'Quantity'), + ), + const SizedBox(height: 8), + DropdownButtonFormField( + initialValue: category, + decoration: const InputDecoration(labelText: 'Category'), + items: [ + for (final cat in categories) + DropdownMenuItem(value: cat, child: Text(cat)), + ], + onChanged: (v) => + setState(() => category = v ?? Categories.general), + ), + const SizedBox(height: 4), + const Text( + 'Alcohol items default to drinkers only; everything else to ' + 'everyone. Fine-tune by tapping the item afterwards.', + style: TextStyle(fontSize: 11, color: Colors.black54), + ), + ], + ), + ), + actions: [ + if (item != null) + TextButton( + onPressed: () { + state.mutate(() => receipt.items.remove(item)); + Navigator.pop(c); + }, + child: const Text('Delete', style: TextStyle(color: Colors.red)), + ), + TextButton( + onPressed: () => Navigator.pop(c), + child: const Text('Cancel'), + ), + FilledButton( + onPressed: () { + final gr = _parseZl(total.text); + if (name.text.trim().isEmpty || gr == null) return; + state.mutate(() { + if (item == null) { + final it = Item( + id: Ids.next(), + name: name.text.trim(), + totalGr: gr, + qty: double.tryParse(qty.text.replaceAll(',', '.')) ?? 1, + category: category, + assignment: category == Categories.alcohol + ? Assignment(mode: AssignMode.drinkers) + : Assignment(), + ); + receipt.items.add(it); + } else { + item + ..name = name.text.trim() + ..totalGr = gr + ..qty = double.tryParse(qty.text.replaceAll(',', '.')) ?? 1 + ..category = category; + } + }); + Navigator.pop(c); + }, + child: const Text('Save'), + ), + ], + ), + ), + ); +} + +String _zl(int gr) => (gr / 100).toStringAsFixed(2); + +int? _parseZl(String s) { + final v = double.tryParse(s.replaceAll(',', '.').replaceAll(' ', '')); + if (v == null) return null; + return (v * 100).round(); +} diff --git a/billsplit/lib/ui/people_screen.dart b/billsplit/lib/ui/people_screen.dart new file mode 100644 index 0000000..70648b4 --- /dev/null +++ b/billsplit/lib/ui/people_screen.dart @@ -0,0 +1,273 @@ +import 'package:billsplit/domain/models.dart'; +import 'package:billsplit/state/app_state.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +/// Roster management: people with a drinks toggle, plus custom groups. +class PeopleScreen extends StatelessWidget { + /// Creates the people screen. + const PeopleScreen({super.key}); + + @override + Widget build(BuildContext context) { + final state = context.watch(); + return Scaffold( + appBar: AppBar(title: const Text('People & groups')), + body: ListView( + padding: const EdgeInsets.only(bottom: 96), + children: [ + const _SectionHeader('People'), + if (state.people.isEmpty) + const Padding( + padding: EdgeInsets.all(16), + child: Text('Nobody yet — add people below.'), + ), + for (final p in state.people) _PersonTile(person: p), + const Divider(height: 32), + const _SectionHeader('Groups'), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Text( + 'Drinkers / non-drinkers are built in (the toggle on each ' + 'person). Groups below are extra tags, e.g. "meat eaters".', + style: TextStyle(fontSize: 12, color: Colors.black54), + ), + ), + for (final g in state.groups) _GroupTile(group: g), + Padding( + padding: const EdgeInsets.all(16), + child: OutlinedButton.icon( + icon: const Icon(Icons.add), + label: const Text('Add group'), + onPressed: () => _addGroup(context), + ), + ), + ], + ), + floatingActionButton: FloatingActionButton.extended( + icon: const Icon(Icons.person_add), + label: const Text('Add person'), + onPressed: () => _addPerson(context), + ), + ); + } + + Future _addPerson(BuildContext context) async { + final state = context.read(); + final controller = TextEditingController(); + var drinks = true; + await showDialog( + context: context, + builder: (c) => StatefulBuilder( + builder: (c, setState) => AlertDialog( + title: const Text('Add person'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: controller, + autofocus: true, + decoration: const InputDecoration(labelText: 'Name'), + ), + SwitchListTile( + title: const Text('Drinks alcohol'), + value: drinks, + onChanged: (v) => setState(() => drinks = v), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(c), + child: const Text('Cancel'), + ), + FilledButton( + onPressed: () { + if (controller.text.trim().isNotEmpty) { + state.addPerson(controller.text.trim(), drinks: drinks); + } + Navigator.pop(c); + }, + child: const Text('Add'), + ), + ], + ), + ), + ); + } + + Future _addGroup(BuildContext context) async { + final state = context.read(); + final controller = TextEditingController(); + await showDialog( + context: context, + builder: (c) => AlertDialog( + title: const Text('Add group'), + content: TextField( + controller: controller, + autofocus: true, + decoration: const InputDecoration(labelText: 'Group name'), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(c), + child: const Text('Cancel'), + ), + FilledButton( + onPressed: () { + if (controller.text.trim().isNotEmpty) { + state.addGroup(controller.text.trim()); + } + Navigator.pop(c); + }, + child: const Text('Add'), + ), + ], + ), + ); + } +} + +class _SectionHeader extends StatelessWidget { + const _SectionHeader(this.text); + final String text; + + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 4), + child: Text(text, style: Theme.of(context).textTheme.titleMedium), + ); +} + +class _PersonTile extends StatelessWidget { + const _PersonTile({required this.person}); + final Person person; + + @override + Widget build(BuildContext context) { + final state = context.read(); + return ListTile( + leading: CircleAvatar( + child: Text( + person.name.isEmpty ? '?' : person.name.substring(0, 1).toUpperCase(), + ), + ), + title: Text(person.name), + subtitle: Wrap( + spacing: 6, + children: [ + for (final g in state.groups.where( + (g) => g.memberIds.contains(person.id), + )) + Chip( + label: Text(g.name, style: const TextStyle(fontSize: 11)), + visualDensity: VisualDensity.compact, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ], + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Tooltip( + message: 'Drinks alcohol', + child: Switch( + value: person.drinks, + onChanged: (v) => state.mutate(() => person.drinks = v), + ), + ), + IconButton( + icon: const Icon(Icons.delete_outline), + onPressed: () => _confirmRemove(context, state), + ), + ], + ), + onTap: () => _editMembership(context, state), + ); + } + + Future _confirmRemove(BuildContext context, AppState state) async { + final ok = await showDialog( + context: context, + builder: (c) => AlertDialog( + title: Text('Remove ${person.name}?'), + content: const Text( + 'They will be removed from all receipts and item assignments.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(c, false), + child: const Text('Cancel'), + ), + FilledButton( + onPressed: () => Navigator.pop(c, true), + child: const Text('Remove'), + ), + ], + ), + ); + if (ok == true) state.removePerson(person); + } + + Future _editMembership(BuildContext context, AppState state) async { + if (state.groups.isEmpty) return; + await showModalBottomSheet( + context: context, + builder: (c) => StatefulBuilder( + builder: (c, setState) => SafeArea( + child: ListView( + shrinkWrap: true, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Text( + '${person.name} — groups', + style: Theme.of(c).textTheme.titleMedium, + ), + ), + for (final g in state.groups) + CheckboxListTile( + title: Text(g.name), + value: g.memberIds.contains(person.id), + onChanged: (v) { + state.mutate(() { + if (v == true) { + g.memberIds.add(person.id); + } else { + g.memberIds.remove(person.id); + } + }); + setState(() {}); + }, + ), + ], + ), + ), + ), + ); + } +} + +class _GroupTile extends StatelessWidget { + const _GroupTile({required this.group}); + final Group group; + + @override + Widget build(BuildContext context) { + final state = context.read(); + final names = state.people + .where((p) => group.memberIds.contains(p.id)) + .map((p) => p.name) + .join(', '); + return ListTile( + leading: const Icon(Icons.label_outline), + title: Text(group.name), + subtitle: + Text(names.isEmpty ? 'No members — tap a person to add' : names), + trailing: IconButton( + icon: const Icon(Icons.delete_outline), + onPressed: () => state.removeGroup(group), + ), + ); + } +} diff --git a/billsplit/lib/ui/receipt_screen.dart b/billsplit/lib/ui/receipt_screen.dart new file mode 100644 index 0000000..0f881eb --- /dev/null +++ b/billsplit/lib/ui/receipt_screen.dart @@ -0,0 +1,357 @@ +import 'package:billsplit/domain/models.dart'; +import 'package:billsplit/domain/split_engine.dart'; +import 'package:billsplit/domain/xlsx_export.dart'; +import 'package:billsplit/state/app_state.dart'; +import 'package:billsplit/ui/assignment_sheet.dart'; +import 'package:billsplit/ui/item_edit.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +/// One receipt: item list with assignments, totals, participants, export. +class ReceiptScreen extends StatefulWidget { + /// Creates the screen for the receipt with [receiptId]. + const ReceiptScreen({required this.receiptId, super.key}); + + /// Id of the receipt to show; resolved against [AppState.receipts]. + final String receiptId; + + @override + State createState() => _ReceiptScreenState(); +} + +class _ReceiptScreenState extends State { + @override + Widget build(BuildContext context) { + final state = context.watch(); + final receipt = + state.receipts.where((r) => r.id == widget.receiptId).firstOrNull; + if (receipt == null) { + return const Scaffold(body: Center(child: Text('Receipt deleted.'))); + } + final split = computeSplit(receipt, state.people, state.groups); + final wide = MediaQuery.of(context).size.width > 800; + + final itemsList = _ItemsList(receipt: receipt, split: split); + final totals = _TotalsView(receipt: receipt, split: split); + + return DefaultTabController( + length: 2, + child: Scaffold( + appBar: AppBar( + title: Text(receipt.title, overflow: TextOverflow.ellipsis), + actions: [ + IconButton( + tooltip: 'Participants', + icon: const Icon(Icons.group), + onPressed: () => _editParticipants(context, state, receipt), + ), + IconButton( + tooltip: 'Export .xlsx', + icon: const Icon(Icons.table_view), + onPressed: () => _export(context, state, receipt), + ), + ], + bottom: wide + ? null + : const TabBar( + tabs: [ + Tab(text: 'Items'), + Tab(text: 'Totals'), + ], + ), + ), + floatingActionButton: FloatingActionButton( + tooltip: 'Add item', + child: const Icon(Icons.add), + onPressed: () => showItemEditDialog(context, state, receipt, null), + ), + body: wide + ? Row( + children: [ + Expanded(flex: 3, child: itemsList), + const VerticalDivider(width: 1), + Expanded(flex: 2, child: totals), + ], + ) + : TabBarView(children: [itemsList, totals]), + ), + ); + } + + Future _editParticipants( + BuildContext context, + AppState state, + Receipt receipt, + ) async { + if (state.people.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Add people first (People & groups screen).'), + ), + ); + return; + } + await showModalBottomSheet( + context: context, + builder: (c) => StatefulBuilder( + builder: (c, setSheet) => SafeArea( + child: ListView( + shrinkWrap: true, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Text( + 'Who is on this receipt?', + style: Theme.of(c).textTheme.titleMedium, + ), + ), + for (final p in state.people) + CheckboxListTile( + title: Text(p.name), + subtitle: p.drinks ? null : const Text('non-drinker'), + value: receipt.participantIds.contains(p.id), + onChanged: (v) { + state.mutate(() { + if (v == true) { + receipt.participantIds.add(p.id); + } else { + receipt.participantIds.remove(p.id); + } + }); + setSheet(() {}); + }, + ), + ], + ), + ), + ), + ); + } + + Future _export( + BuildContext context, + AppState state, + Receipt receipt, + ) async { + final messenger = ScaffoldMessenger.of(context); + if (receipt.participantIds.isEmpty) { + messenger.showSnackBar( + const SnackBar(content: Text('Add participants before exporting.')), + ); + return; + } + try { + final bytes = Uint8List.fromList( + exportXlsx( + receipt: receipt, + roster: state.people, + groups: state.groups, + ), + ); + final safe = receipt.title.replaceAll( + RegExp(r'[^\w\d ąćęłńóśżźĄĆĘŁŃÓŚŻŹ-]'), + '_', + ); + final path = await FilePicker.saveFile( + dialogTitle: 'Save split as .xlsx', + fileName: '$safe split.xlsx', + type: FileType.custom, + allowedExtensions: ['xlsx'], + bytes: bytes, + ); + if (path != null) { + messenger.showSnackBar( + SnackBar(content: Text('Saved: ${path.split('/').last}')), + ); + } + } on Exception catch (e) { + messenger.showSnackBar(SnackBar(content: Text('Export failed: $e'))); + } + } +} + +class _ItemsList extends StatelessWidget { + const _ItemsList({required this.receipt, required this.split}); + final Receipt receipt; + final SplitResult split; + + @override + Widget build(BuildContext context) { + final state = context.read(); + if (receipt.items.isEmpty) { + return const Center(child: Text('No items — add with +.')); + } + return ListView.builder( + padding: const EdgeInsets.only(bottom: 96), + itemCount: receipt.items.length, + itemBuilder: (context, i) { + final item = receipt.items[i]; + final people = + resolveParticipants(item, receipt, state.people, state.groups); + final unassigned = people.isEmpty; + return ListTile( + dense: true, + leading: _CategoryDot(category: item.category), + title: Text(item.name), + subtitle: Text( + _assignmentLabel(item, people.length, state), + style: TextStyle( + color: unassigned ? Colors.red : null, + fontWeight: unassigned ? FontWeight.bold : null, + ), + ), + trailing: Text(formatPln(item.totalGr)), + onTap: () => showAssignmentSheet(context, state, receipt, item), + onLongPress: () => showItemEditDialog(context, state, receipt, item), + ); + }, + ); + } + + String _assignmentLabel(Item item, int n, AppState state) { + switch (item.assignment.mode) { + case AssignMode.everyone: + return 'Everyone ($n)'; + case AssignMode.drinkers: + return 'Drinkers only ($n)'; + case AssignMode.nonDrinkers: + return 'Non-drinkers ($n)'; + case AssignMode.group: + final g = state.groups + .where((g) => g.id == item.assignment.groupId) + .firstOrNull; + return 'Group: ${g?.name ?? '?'} ($n)'; + case AssignMode.custom: + return n == 0 ? 'UNASSIGNED — tap to fix' : 'Selected people ($n)'; + } + } +} + +class _CategoryDot extends StatelessWidget { + const _CategoryDot({required this.category}); + final String category; + + @override + Widget build(BuildContext context) { + final color = switch (category) { + Categories.alcohol => Colors.deepOrange, + Categories.mixer => Colors.amber, + Categories.deposit => Colors.blueGrey, + _ => Colors.teal, + }; + return CircleAvatar(radius: 6, backgroundColor: color); + } +} + +class _TotalsView extends StatelessWidget { + const _TotalsView({required this.receipt, required this.split}); + final Receipt receipt; + final SplitResult split; + + @override + Widget build(BuildContext context) { + final state = context.read(); + final people = receiptParticipants(receipt, state.people) + ..sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); + final itemsTotal = receipt.itemsTotalGr; + final assigned = split.assignedTotalGr; + final paid = receipt.paidTotalGr; + + return ListView( + padding: const EdgeInsets.all(16), + children: [ + if (split.unassigned.isNotEmpty) + Card( + color: Colors.red.shade50, + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + '${split.unassigned.length} item(s) have nobody assigned and ' + 'are excluded from totals:\n' + '${split.unassigned.map((i) => '· ${i.name}').join('\n')}', + style: const TextStyle(color: Colors.red), + ), + ), + ), + for (final p in people) + _PersonRow(person: p, total: split.perPerson[p.id]), + const Divider(height: 32), + _kv(context, 'Items total', formatPln(itemsTotal), bold: true), + _kv( + context, + 'Assigned to people', + formatPln(assigned), + warn: assigned != itemsTotal, + ), + if (paid != null) + _kv( + context, + 'Paid (receipt)', + formatPln(paid), + warn: paid != itemsTotal, + ), + const SizedBox(height: 8), + Text( + 'Splits are grosz-exact: shares always add up to the item total, ' + 'so no fractional grosze are lost.', + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ); + } + + Widget _kv( + BuildContext context, + String k, + String v, { + bool bold = false, + bool warn = false, + }) { + final style = TextStyle( + fontWeight: bold || warn ? FontWeight.bold : null, + color: warn ? Colors.red : null, + ); + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [Text(k, style: style), Text(v, style: style)], + ), + ); + } +} + +class _PersonRow extends StatelessWidget { + const _PersonRow({required this.person, required this.total}); + final Person person; + final PersonTotal? total; + + @override + Widget build(BuildContext context) { + final t = total; + return ListTile( + contentPadding: EdgeInsets.zero, + leading: CircleAvatar( + child: Text( + person.name.isEmpty ? '?' : person.name.substring(0, 1).toUpperCase(), + ), + ), + title: Text(person.name), + subtitle: t == null || t.alcoholGr == 0 + ? null + : Text( + 'alcohol ${formatPln(t.alcoholGr)} · rest ${formatPln(t.restGr)}', + ), + trailing: Text( + formatPln(t?.totalGr ?? 0), + style: Theme.of(context).textTheme.titleMedium, + ), + ); + } +} + +extension on Iterable { + T? get firstOrNull => isEmpty ? null : first; +} diff --git a/billsplit/linux/.gitignore b/billsplit/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/billsplit/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/billsplit/linux/CMakeLists.txt b/billsplit/linux/CMakeLists.txt new file mode 100644 index 0000000..0f0a3a3 --- /dev/null +++ b/billsplit/linux/CMakeLists.txt @@ -0,0 +1,128 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "billsplit") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "dev.kuhy.billsplit") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/billsplit/linux/flutter/CMakeLists.txt b/billsplit/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/billsplit/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/billsplit/linux/flutter/generated_plugin_registrant.cc b/billsplit/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..e71a16d --- /dev/null +++ b/billsplit/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void fl_register_plugins(FlPluginRegistry* registry) { +} diff --git a/billsplit/linux/flutter/generated_plugin_registrant.h b/billsplit/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/billsplit/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/billsplit/linux/flutter/generated_plugins.cmake b/billsplit/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..be1ee3e --- /dev/null +++ b/billsplit/linux/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/billsplit/linux/runner/CMakeLists.txt b/billsplit/linux/runner/CMakeLists.txt new file mode 100644 index 0000000..e97dabc --- /dev/null +++ b/billsplit/linux/runner/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the application ID. +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/billsplit/linux/runner/main.cc b/billsplit/linux/runner/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/billsplit/linux/runner/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/billsplit/linux/runner/my_application.cc b/billsplit/linux/runner/my_application.cc new file mode 100644 index 0000000..7d595bb --- /dev/null +++ b/billsplit/linux/runner/my_application.cc @@ -0,0 +1,148 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Called when first Flutter frame received. +static void first_frame_cb(MyApplication* self, FlView* view) { + gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view))); +} + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "billsplit"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "billsplit"); + } + + gtk_window_set_default_size(window, 1280, 720); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments( + project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + GdkRGBA background_color; + // Background defaults to black, override it here if necessary, e.g. #00000000 + // for transparent. + gdk_rgba_parse(&background_color, "#000000"); + fl_view_set_background_color(view, &background_color); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + // Show the window when Flutter renders. + // Requires the view to be realized so we can start rendering. + g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb), + self); + gtk_widget_realize(GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, + gchar*** arguments, + int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + // MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + // MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = + my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + // Set the program name to the application ID, which helps various systems + // like GTK and desktop environments map this running application to its + // corresponding .desktop file. This ensures better integration by allowing + // the application to be recognized beyond its binary name. + g_set_prgname(APPLICATION_ID); + + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, "flags", + G_APPLICATION_NON_UNIQUE, nullptr)); +} diff --git a/billsplit/linux/runner/my_application.h b/billsplit/linux/runner/my_application.h new file mode 100644 index 0000000..db16367 --- /dev/null +++ b/billsplit/linux/runner/my_application.h @@ -0,0 +1,21 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, + my_application, + MY, + APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/billsplit/macos/.gitignore b/billsplit/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/billsplit/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/billsplit/macos/Flutter/Flutter-Debug.xcconfig b/billsplit/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/billsplit/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/billsplit/macos/Flutter/Flutter-Release.xcconfig b/billsplit/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/billsplit/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/billsplit/macos/Flutter/GeneratedPluginRegistrant.swift b/billsplit/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..774a6b8 --- /dev/null +++ b/billsplit/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,12 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import file_picker + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) +} diff --git a/billsplit/macos/Runner.xcodeproj/project.pbxproj b/billsplit/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..47cb314 --- /dev/null +++ b/billsplit/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,729 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* billsplit.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "billsplit.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* billsplit.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* billsplit.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.kuhy.billsplit.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/billsplit.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/billsplit"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.kuhy.billsplit.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/billsplit.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/billsplit"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.kuhy.billsplit.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/billsplit.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/billsplit"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/billsplit/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/billsplit/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/billsplit/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/billsplit/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/billsplit/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..64de869 --- /dev/null +++ b/billsplit/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/billsplit/macos/Runner.xcworkspace/contents.xcworkspacedata b/billsplit/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/billsplit/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/billsplit/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/billsplit/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/billsplit/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/billsplit/macos/Runner/AppDelegate.swift b/billsplit/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..b3c1761 --- /dev/null +++ b/billsplit/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/billsplit/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/billsplit/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..60e60c9 --- /dev/null +++ b/billsplit/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images": [ + { + "size": "16x16", + "idiom": "mac", + "filename": "app_icon_16.png", + "scale": "1x" + }, + { + "size": "16x16", + "idiom": "mac", + "filename": "app_icon_32.png", + "scale": "2x" + }, + { + "size": "32x32", + "idiom": "mac", + "filename": "app_icon_32.png", + "scale": "1x" + }, + { + "size": "32x32", + "idiom": "mac", + "filename": "app_icon_64.png", + "scale": "2x" + }, + { + "size": "128x128", + "idiom": "mac", + "filename": "app_icon_128.png", + "scale": "1x" + }, + { + "size": "128x128", + "idiom": "mac", + "filename": "app_icon_256.png", + "scale": "2x" + }, + { + "size": "256x256", + "idiom": "mac", + "filename": "app_icon_256.png", + "scale": "1x" + }, + { + "size": "256x256", + "idiom": "mac", + "filename": "app_icon_512.png", + "scale": "2x" + }, + { + "size": "512x512", + "idiom": "mac", + "filename": "app_icon_512.png", + "scale": "1x" + }, + { + "size": "512x512", + "idiom": "mac", + "filename": "app_icon_1024.png", + "scale": "2x" + } + ], + "info": { + "version": 1, + "author": "xcode" + } +} diff --git a/billsplit/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/billsplit/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000000000000000000000000000000000000..82b6f9d9a33e198f5747104729e1fcef999772a5 GIT binary patch literal 102994 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ⅅ8MglG&ER{i5lN315cO?EdHNLrg? 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 z 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 Pe7HEdAEg|=c=LY&YVNkY literal 0 HcmV?d00001 diff --git a/billsplit/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/billsplit/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000000000000000000000000000000000000..13b35eba55c6dabc3aac36f33d859266c18fa0d0 GIT binary patch literal 5680 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 literal 0 HcmV?d00001 diff --git a/billsplit/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/billsplit/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000000000000000000000000000000000000..0a3f5fa40fb3d1e0710331a48de5d256da3f275d GIT binary patch literal 520 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 literal 0 HcmV?d00001 diff --git a/billsplit/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/billsplit/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000000000000000000000000000000000000..2f1632cfddf3d9dade342351e627a0a75609fb46 GIT binary patch literal 2218 zcmV;b2vzrqP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91K%fHv1ONa40RR91KmY&$07g+lumAuE6iGxuRCodHTWf3-RTMruyW6Fu zQYeUM04eX6D5c0FCjKKPrco1(K`<0SL=crI{PC3-^hZU0kQie$gh-5!7z6SH6Q0J% zqot*`H1q{R5fHFYS}dje@;kG=v$L0(yY0?wY2%*c?A&{2?!D*x?m71{of2gv!$5|C z3>qG_BW}7K_yUcT3A5C6QD<+{aq?x;MAUyAiJn#Jv8_zZtQ{P zTRzbL3U9!qVuZzS$xKU10KiW~Bgdcv1-!uAhQxf3a7q+dU6lj?yoO4Lq4TUN4}h{N z*fIM=SS8|C2$(T>w$`t@3Tka!(r!7W`x z-isCVgQD^mG-MJ;XtJuK3V{Vy72GQ83KRWsHU?e*wrhKk=ApIYeDqLi;JI1e zuvv}5^Dc=k7F7?nm3nIw$NVmU-+R>> zyqOR$-2SDpJ}Pt;^RkJytDVXNTsu|mI1`~G7yw`EJR?VkGfNdqK9^^8P`JdtTV&tX4CNcV4 z&N06nZa??Fw1AgQOUSE2AmPE@WO(Fvo`%m`cDgiv(fAeRA%3AGXUbsGw{7Q`cY;1BI#ac3iN$$Hw z0LT0;xc%=q)me?Y*$xI@GRAw?+}>=9D+KTk??-HJ4=A>`V&vKFS75@MKdSF1JTq{S zc1!^8?YA|t+uKigaq!sT;Z!&0F2=k7F0PIU;F$leJLaw2UI6FL^w}OG&!;+b%ya1c z1n+6-inU<0VM-Y_s5iTElq)ThyF?StVcebpGI znw#+zLx2@ah{$_2jn+@}(zJZ{+}_N9BM;z)0yr|gF-4=Iyu@hI*Lk=-A8f#bAzc9f z`Kd6K--x@t04swJVC3JK1cHY-Hq+=|PN-VO;?^_C#;coU6TDP7Bt`;{JTG;!+jj(` zw5cLQ-(Cz-Tlb`A^w7|R56Ce;Wmr0)$KWOUZ6ai0PhzPeHwdl0H(etP zUV`va_i0s-4#DkNM8lUlqI7>YQLf)(lz9Q3Uw`)nc(z3{m5ZE77Ul$V%m)E}3&8L0 z-XaU|eB~Is08eORPk;=<>!1w)Kf}FOVS2l&9~A+@R#koFJ$Czd%Y(ENTV&A~U(IPI z;UY+gf+&6ioZ=roly<0Yst8ck>(M=S?B-ys3mLdM&)ex!hbt+ol|T6CTS+Sc0jv(& z7ijdvFwBq;0a{%3GGwkDKTeG`b+lyj0jjS1OMkYnepCdoosNY`*zmBIo*981BU%%U z@~$z0V`OVtIbEx5pa|Tct|Lg#ZQf5OYMUMRD>Wdxm5SAqV2}3!ceE-M2 z@O~lQ0OiKQp}o9I;?uxCgYVV?FH|?Riri*U$Zi_`V2eiA>l zdSm6;SEm6#T+SpcE8Ro_f2AwxzI z44hfe^WE3!h@W3RDyA_H440cpmYkv*)6m1XazTqw%=E5Xv7^@^^T7Q2wxr+Z2kVYr + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/billsplit/macos/Runner/Configs/AppInfo.xcconfig b/billsplit/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..aa7d2d4 --- /dev/null +++ b/billsplit/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = billsplit + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = dev.kuhy.billsplit + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2026 dev.kuhy. All rights reserved. diff --git a/billsplit/macos/Runner/Configs/Debug.xcconfig b/billsplit/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/billsplit/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/billsplit/macos/Runner/Configs/Release.xcconfig b/billsplit/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/billsplit/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/billsplit/macos/Runner/Configs/Warnings.xcconfig b/billsplit/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/billsplit/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/billsplit/macos/Runner/DebugProfile.entitlements b/billsplit/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..dddb8a3 --- /dev/null +++ b/billsplit/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/billsplit/macos/Runner/Info.plist b/billsplit/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/billsplit/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/billsplit/macos/Runner/MainFlutterWindow.swift b/billsplit/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/billsplit/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/billsplit/macos/Runner/Release.entitlements b/billsplit/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/billsplit/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/billsplit/macos/RunnerTests/RunnerTests.swift b/billsplit/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..61f3bd1 --- /dev/null +++ b/billsplit/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/billsplit/pubspec.lock b/billsplit/pubspec.lock new file mode 100644 index 0000000..141cfb7 --- /dev/null +++ b/billsplit/pubspec.lock @@ -0,0 +1,554 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + archive: + dependency: transitive + description: + name: archive + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d + url: "https://pub.dev" + source: hosted + version: "3.6.1" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://pub.dev" + source: hosted + version: "0.4.2" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51" + url: "https://pub.dev" + source: hosted + version: "0.3.5+4" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + dbus: + dependency: transitive + description: + name: dbus + sha256: "0ce9b0a839e6dee59a37a623d2fc26a35bbbe6404213e419b0d6411023d62645" + url: "https://pub.dev" + source: hosted + version: "0.7.14" + equatable: + dependency: transitive + description: + name: equatable + sha256: "3bce007a596ff8b3119c45d68aaef631272537c03d30e5d4534dd24bf4c5eaa2" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + excel: + dependency: "direct main" + description: + name: excel + sha256: "1a15327dcad260d5db21d1f6e04f04838109b39a2f6a84ea486ceda36e468780" + url: "https://pub.dev" + source: hosted + version: "4.0.6" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: "29cc1fdb20613876cc7afc529738c1c0f11a9ca159b010edad0c566ac330847e" + url: "https://pub.dev" + source: hosted + version: "11.0.3" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_launcher_icons: + dependency: "direct dev" + description: + name: flutter_launcher_icons + sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" + url: "https://pub.dev" + source: hosted + version: "0.14.4" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785" + url: "https://pub.dev" + source: hosted + version: "2.0.35" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + hooks: + dependency: transitive + description: + name: hooks + sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + image: + dependency: transitive + description: + name: image + sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d + url: "https://pub.dev" + source: hosted + version: "4.3.0" + jni: + dependency: transitive + description: + name: jni + sha256: f038e58b4dc2c9037f50e233175086337e0b305e356d28211bf55f21c504cbd3 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + jni_flutter: + dependency: transitive + description: + name: jni_flutter + sha256: "7b717011ea40d04fd47c2731d3d1d36eb99eba3435c2753d62489e8c3c9991d5" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + jni_util: + dependency: transitive + description: + name: jni_util + sha256: "1ba86da04a5f2bf18fde2edb235587e70c5b0fc5bd4ba955f46b00942c3fc35f" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80" + url: "https://pub.dev" + source: hosted + version: "4.12.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + url: "https://pub.dev" + source: hosted + version: "0.12.19" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + meta: + dependency: transitive + description: + name: meta + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + url: "https://pub.dev" + source: hosted + version: "1.18.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: b7fb95a6d9a4f009edd63dc5ac69f07420b23a16161c6dd8660290b59c602e8e + url: "https://pub.dev" + source: hosted + version: "9.5.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: ffcf4cf3d6c0b74ac43708d9f56625506e8a68aa935abe9d267a7330f320eb5d + url: "https://pub.dev" + source: hosted + version: "3.0.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825 + url: "https://pub.dev" + source: hosted + version: "2.1.6" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + url: "https://pub.dev" + source: hosted + version: "2.6.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + provider: + dependency: "direct main" + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.dev" + source: hosted + version: "6.1.5+1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + record_use: + dependency: transitive + description: + name: record_use + sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + url: "https://pub.dev" + source: hosted + version: "0.6.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + url: "https://pub.dev" + source: hosted + version: "0.7.11" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + very_good_analysis: + dependency: "direct dev" + description: + name: very_good_analysis + sha256: "481af67ab5877af20325251dc215a4ebac7666a1c8cf09198ffd457bc612b33d" + url: "https://pub.dev" + source: hosted + version: "10.3.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + url: "https://pub.dev" + source: hosted + version: "15.2.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.12.0 <4.0.0" + flutter: ">=3.38.4" diff --git a/billsplit/pubspec.yaml b/billsplit/pubspec.yaml new file mode 100644 index 0000000..c940289 --- /dev/null +++ b/billsplit/pubspec.yaml @@ -0,0 +1,37 @@ +name: billsplit +description: Receipt bill-splitting with people, groups, per-item assignment and xlsx export. +publish_to: "none" +version: 0.1.0 + +environment: + sdk: ^3.5.0 + +dependencies: + excel: ^4.0.6 + file_picker: ^11.0.0 + flutter: + sdk: flutter + path_provider: ^2.1.4 + provider: ^6.1.2 + +dev_dependencies: + flutter_launcher_icons: ^0.14.4 + flutter_lints: ^6.0.0 + flutter_test: + sdk: flutter + very_good_analysis: ^10.3.0 + +flutter: + uses-material-design: true + +# Launcher icon generation. Source art is produced by the shared generator: +# PYTHONPATH=~/testsAndMisc python3 -m python_pkg.app_icons generate --app billsplit +# then run: dart run flutter_launcher_icons +flutter_launcher_icons: + android: true + ios: false + image_path: "assets/icon/icon.png" + adaptive_icon_background: "#211D1B" + adaptive_icon_foreground: "assets/icon/icon_foreground.png" + adaptive_icon_monochrome: "assets/icon/icon_monochrome.png" + min_sdk_android: 21 diff --git a/billsplit/test/app_state_test.dart b/billsplit/test/app_state_test.dart new file mode 100644 index 0000000..44a4ecc --- /dev/null +++ b/billsplit/test/app_state_test.dart @@ -0,0 +1,184 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:billsplit/domain/models.dart'; +import 'package:billsplit/state/app_state.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + Directory temp() { + final d = Directory.systemTemp.createTempSync('billsplit_state'); + addTearDown(() { + if (d.existsSync()) d.deleteSync(recursive: true); + }); + return d; + } + + test('load on a fresh directory yields empty, usable state', () async { + final s = AppState(overrideDir: temp()); + await s.load(); + expect(s.loaded, isTrue); + expect(s.people, isEmpty); + s.dispose(); + }); + + test('save/load roundtrip preserves everything', () async { + final dir = temp(); + final s = AppState(overrideDir: dir); + await s.load(); + final p = s.addPerson('Ala', drinks: false); + final g = s.addGroup('Meat')..memberIds.add(p.id); + s.addReceipt( + Receipt( + id: 'r1', + title: 'T', + items: [ + Item( + id: 'i1', + name: 'x', + totalGr: 100, + assignment: Assignment(mode: AssignMode.group, groupId: g.id), + ), + ], + ), + ); + await s.save(); + s.dispose(); + + final s2 = AppState(overrideDir: dir); + await s2.load(); + expect(s2.people.single.name, 'Ala'); + expect(s2.people.single.drinks, isFalse); + expect(s2.groups.single.memberIds, {p.id}); + expect(s2.receipts.single.participantIds, {p.id}); + expect(s2.receipts.single.items.single.assignment.groupId, g.id); + s2.dispose(); + }); + + test('load prunes dangling person references', () async { + final dir = temp(); + File('${dir.path}/billsplit/state.json') + ..createSync(recursive: true) + ..writeAsStringSync( + jsonEncode({ + 'people': [ + {'id': 'p1', 'name': 'Ala'}, + ], + 'groups': [ + { + 'id': 'g1', + 'name': 'G', + 'memberIds': ['p1', 'ghost'], + }, + ], + 'receipts': [ + { + 'id': 'r1', + 'title': 'T', + 'date': '2026-01-01T00:00:00.000', + 'participantIds': ['p1', 'ghost'], + 'items': [ + { + 'id': 'i1', + 'name': 'x', + 'totalGr': 1, + 'assignment': { + 'mode': 'custom', + 'customIds': ['p1', 'ghost'], + }, + }, + ], + }, + ], + }), + ); + final s = AppState(overrideDir: dir); + await s.load(); + expect(s.groups.single.memberIds, {'p1'}); + expect(s.receipts.single.participantIds, {'p1'}); + expect(s.receipts.single.items.single.assignment.customIds, {'p1'}); + s.dispose(); + }); + + test('corrupt JSON is survived', () async { + final dir = temp(); + File('${dir.path}/billsplit/state.json') + ..createSync(recursive: true) + ..writeAsStringSync('{oops'); + final s = AppState(overrideDir: dir); + await s.load(); + expect(s.loaded, isTrue); + expect(s.people, isEmpty); + s.dispose(); + }); + + test('IO failure on load is survived (billsplit path is a file)', () async { + final dir = temp(); + File('${dir.path}/billsplit').createSync(recursive: true); + final s = AppState(overrideDir: dir); + await s.load(); + expect(s.loaded, isTrue); + s.dispose(); + }); + + test('IO failure on save is survived (state.json is a directory)', () async { + final dir = temp(); + Directory('${dir.path}/billsplit/state.json').createSync(recursive: true); + final s = AppState(overrideDir: dir); + await s.save(); // must not throw + s.dispose(); + }); + + test('mutations debounce into a save', () async { + final dir = temp(); + final s = AppState(overrideDir: dir); + await s.load(); + s.addPerson('Ala'); + expect(File('${dir.path}/billsplit/state.json').existsSync(), isFalse); + await Future.delayed(const Duration(milliseconds: 600)); + expect(File('${dir.path}/billsplit/state.json').existsSync(), isTrue); + s.dispose(); + }); + + test('removePerson / removeGroup fallback / removeReceipt', () async { + final s = AppState(overrideDir: temp()); + await s.load(); + final p = s.addPerson('Ala'); + final g = s.addGroup('G')..memberIds.add(p.id); + final item = Item( + id: 'i1', + name: 'x', + totalGr: 1, + assignment: Assignment(mode: AssignMode.group, groupId: g.id), + ); + final r = Receipt(id: 'r1', title: 'T', items: [item]); + s + ..addReceipt(r) + ..removeGroup(g); + expect(item.assignment.mode, AssignMode.everyone); + expect(item.assignment.groupId, isNull); + + s.removePerson(p); + expect(r.participantIds, isEmpty); + + s.removeReceipt(r); + expect(s.receipts, isEmpty); + s.dispose(); + }); + + test('default documents dir is used when no override is given', () async { + final dir = temp(); + mockPathProvider(dir); + addTearDown(clearChannelMocks); + final s = AppState(); + await s.load(); + s.addPerson('Ala'); + await s.save(); + expect(File('${dir.path}/billsplit/state.json').existsSync(), isTrue); + s.dispose(); + }); +} diff --git a/billsplit/test/engine_extra_test.dart b/billsplit/test/engine_extra_test.dart new file mode 100644 index 0000000..b27d9af --- /dev/null +++ b/billsplit/test/engine_extra_test.dart @@ -0,0 +1,29 @@ +import 'package:billsplit/domain/split_engine.dart'; +import 'package:billsplit/domain/xlsx_export.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('splitAmount with no people returns empty', () { + expect(splitAmount(100, const []), isEmpty); + }); + + test('splitAmount accepts negative rotation', () { + final s = splitAmount(10, ['a', 'b', 'c'], rotation: -1); + expect(s.values.fold(0, (x, y) => x + y), 10); + }); + + test('stableHash is stable and differs between strings', () { + expect(stableHash(''), 0); + expect(stableHash('abc'), stableHash('abc')); + expect(stableHash('abc'), isNot(stableHash('abd'))); + }); + + test('colName covers single and multi letter columns', () { + expect(colName(0), 'A'); + expect(colName(25), 'Z'); + expect(colName(26), 'AA'); + expect(colName(27), 'AB'); + expect(colName(701), 'ZZ'); + expect(colName(702), 'AAA'); + }); +} diff --git a/billsplit/test/eparagon_edge_test.dart b/billsplit/test/eparagon_edge_test.dart new file mode 100644 index 0000000..755c308 --- /dev/null +++ b/billsplit/test/eparagon_edge_test.dart @@ -0,0 +1,232 @@ +import 'dart:convert'; + +import 'package:billsplit/domain/eparagon_parser.dart'; +import 'package:billsplit/domain/models.dart'; +import 'package:flutter_test/flutter_test.dart'; + +Map minimalPayload({ + List? pozycja, + Map? extraParagon, + Map? dokumentExtra, +}) { + return { + 'dokument': { + ...?dokumentExtra, + 'paragon': { + if (pozycja != null) 'pozycja': pozycja, + ...?extraParagon, + }, + }, + }; +} + +Map towar(String name, int brutto) => { + 'towar': { + 'nazwa': name, + 'ilosc': '1,000', + 'cena': brutto, + 'brutto': brutto, + }, + }; + +String asJws(Map payload) { + final body = base64Url.encode(utf8.encode(jsonEncode(payload))); + return 'eyJh.$body.sig'; +} + +void main() { + test('exception toString carries the message', () { + expect( + EParagonParseException('boom').toString(), + 'EParagonParseException: boom', + ); + }); + + test('bare JWS with non-map payload is rejected', () { + final body = base64Url.encode(utf8.encode('123')); + expect( + () => parseEParagon('h.$body.s'), + throwsA(isA()), + ); + }); + + test('three-part token with invalid base64 payload is rejected', () { + expect( + () => parseEParagon('aa.§§§.bb'), + throwsA(isA()), + ); + }); + + test('JSON root that is not a map is rejected', () { + expect( + () => parseEParagon('[1, 2]'), + throwsA(isA()), + ); + }); + + test('wrapper with undecodable data field is rejected', () { + expect( + () => parseEParagon('{"data": "a.b"}'), + throwsA( + predicate( + (e) => e is EParagonParseException && e.message.contains('"data"'), + ), + ), + ); + }); + + test('missing dokument / paragon / items are rejected with clear messages', + () { + expect( + () => parseEParagon(asJws({'dokument': 1})), + throwsA( + predicate( + (e) => e is EParagonParseException && e.message.contains('dokument'), + ), + ), + ); + expect( + () => parseEParagon(jsonEncode({'dokument': {}})), + throwsA( + predicate( + (e) => e is EParagonParseException && e.message.contains('paragon'), + ), + ), + ); + expect( + () => parseEParagon(jsonEncode(minimalPayload())), + throwsA( + predicate( + (e) => + e is EParagonParseException && e.message.contains('line items'), + ), + ), + ); + }); + + test('malformed pozycja entries are skipped, missing brutto warns', () { + final res = parseEParagonDetailed( + jsonEncode( + minimalPayload( + pozycja: [ + 1, + {'towar': 2}, + { + 'towar': {'nazwa': 'NoPrice', 'ilosc': 'x'}, + }, + { + 'towar': {'nazwa': ' ', 'brutto': 5}, + }, + towar('Chleb', 500), + ], + ), + ), + ); + expect(res.receipt.items.single.name, 'Chleb'); + expect(res.warnings.single, contains('NoPrice')); + expect(res.receipt.items.single.qty, 1); + }); + + test('sumaBrutto mismatch produces a warning; paid falls back to sum', () { + final res = parseEParagonDetailed( + jsonEncode( + minimalPayload( + pozycja: [towar('Chleb', 500)], + extraParagon: { + 'podsum': {'sumaBrutto': 600}, + 'opak': { + 'wart': 100, + 'daneOpak': [ + {'nazwa': 'butelka', 'cena': 50, 'ilosc': 2000}, + {'nazwa': 'zero', 'cena': 50, 'ilosc': 0}, + 1, + ], + }, + }, + ), + ), + ); + expect(res.warnings.single, contains('differs')); + expect(res.receipt.paidTotalGr, 700); // 600 declared + 100 deposit + final deposit = + res.receipt.items.where((i) => i.category == Categories.deposit).single; + expect(deposit.totalGr, 100); + expect(deposit.qty, 2); + }); + + test('store aliasing, plain store names and date fallbacks', () { + final jm = parseEParagon( + asJws( + minimalPayload( + pozycja: [towar('Chleb', 500)], + dokumentExtra: { + 'podmiot1': {'nazwaPod': ' JERONIMO MARTINS POLSKA S.A. '}, + }, + ), + ), + ); + expect(jm.store, 'Biedronka'); + + final other = parseEParagon( + jsonEncode( + minimalPayload( + pozycja: [towar('Chleb', 500)], + dokumentExtra: { + 'podmiot1': {'nazwaPod': 'Żabka'}, + 'naglowek': {'dataJPK': '2026-05-01T10:00:00'}, + }, + ), + ), + ); + expect(other.store, 'Żabka'); + expect(other.date.year, 2026); + expect(other.title, contains('Żabka 2026-05-01')); + + final noStore = parseEParagon( + jsonEncode( + minimalPayload( + pozycja: [towar('Chleb', 500)], + dokumentExtra: { + 'podmiot1': {'nazwaPod': ' '}, + }, + ), + ), + ); + expect(noStore.store, isNull); + expect(noStore.date.year, DateTime.now().year); + }); + + test('quantity and int parsing variants', () { + final r = parseEParagon( + jsonEncode( + minimalPayload( + pozycja: [ + { + 'towar': { + 'nazwa': 'Ser', + 'ilosc': 2, + 'cena': '100', + 'brutto': 200.4, + }, + }, + { + 'towar': {'nazwa': 'Mleko', 'ilosc': 'abc', 'brutto': 300}, + }, + ], + ), + ), + ); + expect(r.items[0].qty, 2); + expect(r.items[0].unitPriceGr, 100); + expect(r.items[0].totalGr, 200); + expect(r.items[1].qty, 1); + }); + + test('mixer keyword tagging without alcohol', () { + final r = parseEParagon( + jsonEncode(minimalPayload(pozycja: [towar('Tonik cytrynowy', 400)])), + ); + expect(r.items.single.category, Categories.mixer); + expect(r.items.single.assignment.mode, AssignMode.everyone); + }); +} diff --git a/billsplit/test/eparagon_parser_test.dart b/billsplit/test/eparagon_parser_test.dart new file mode 100644 index 0000000..f43e532 --- /dev/null +++ b/billsplit/test/eparagon_parser_test.dart @@ -0,0 +1,82 @@ +import 'dart:io'; + +import 'package:billsplit/domain/eparagon_parser.dart'; +import 'package:billsplit/domain/models.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + final fixture = File('test/fixtures/sample_receipt.json').readAsStringSync(); + + test('parses the real Biedronka e-paragon wrapper', () { + final res = parseEParagonDetailed(fixture); + final r = res.receipt; + + // 49 goods lines + 1 deposit line. + expect(r.items.length, 50); + final deposit = + r.items.where((i) => i.category == Categories.deposit).toList(); + expect(deposit.length, 1); + expect(deposit.single.totalGr, 700); + expect(deposit.single.qty, 14); + + // Goods sum matches receipt sumaBrutto; paid total includes deposit. + final goods = r.items + .where((i) => i.category != Categories.deposit) + .fold(0, (s, i) => s + i.totalGr); + expect(goods, 59055); + expect(r.paidTotalGr, 59755); + expect(res.warnings, isEmpty); + + // Alcohol detection: exactly vodka + beer, drinkers-only by default. + final alcohol = + r.items.where((i) => i.category == Categories.alcohol).toList(); + expect(alcohol.length, 2); + expect( + alcohol.map((i) => i.name).join(' | ').toLowerCase(), + allOf(contains('wódka'), contains('piwo')), + ); + for (final a in alcohol) { + expect(a.assignment.mode, AssignMode.drinkers); + } + expect(alcohol.fold(0, (s, i) => s + i.totalGr), 10872); + + // Mixers tagged but split among everyone. + final mixers = + r.items.where((i) => i.category == Categories.mixer).toList(); + expect(mixers.length, greaterThanOrEqualTo(4)); + for (final m in mixers) { + expect(m.assignment.mode, AssignMode.everyone); + } + + // Name cleanup: VAT letter stripped, spaces collapsed. + expect(r.items.first.name, 'Tonic'); + // Weight-based quantity with comma decimal. + final kielbasa = + r.items.firstWhere((i) => i.name.startsWith('KiełbBrocka')); + expect(kielbasa.qty, closeTo(0.871, 1e-9)); + // Discount captured (vodka -28.00). + final vodka = r.items.firstWhere((i) => i.name.startsWith('WódkaB')); + expect(vodka.discountGr, -2800); + expect(vodka.totalGr, 7998); + }); + + test('accepts bare JPK payload and bare JWS token', () { + final res = parseEParagonDetailed(fixture); + expect(res.receipt.items, isNotEmpty); + // Raw JWS from the wrapper should parse identically. + final jws = RegExp('"data":"([^"]+)"').firstMatch(fixture)!.group(1)!; + final fromJws = parseEParagon(jws); + expect(fromJws.items.length, res.receipt.items.length); + }); + + test('rejects garbage input with a clear error', () { + expect( + () => parseEParagon('not json'), + throwsA(isA()), + ); + expect( + () => parseEParagon('{"foo": 1}'), + throwsA(isA()), + ); + }); +} diff --git a/billsplit/test/fixtures/sample_receipt.json b/billsplit/test/fixtures/sample_receipt.json new file mode 100644 index 0000000..21a9911 --- /dev/null +++ b/billsplit/test/fixtures/sample_receipt.json @@ -0,0 +1,779 @@ +{ + "protoVersion": "000", + "IDZ": "c=068b8822a7d1459697c518bacdc26abf|g={b6b16726-f935-4e67-b756-bca881c12124}|s=5834|p=1|t=4320", + "deviceType": 2, + "printed": false, + "data": "eyJhbGciOiJSUzI1NiIsIng1YyI6WyJNSUlGUVRDQ0F5bWdBd0lCQWdJRERxR0dNQTBHQ1NxR1NJYjNEUUVCQ3dVQU1JR2JNUXN3Q1FZRFZRUUdFd0pRVERFVU1CSUdBMVVFQ0F3TFRXRjZiM2RwWldOcmFXVXhFVEFQQmdOVkJBY01DRmRoY25ONllYZGhNU1F3SWdZRFZRUUxEQnREWlc1MGNuVnRJRU5sY25SNVptbHJZV05xYVNCbExVdGhjM2t4R3pBWkJnTlZCQW9NRWxCdmMyNWxkQ0JRYjJ4emEyRWdVeTVCTGpFZ01CNEdBMVVFQXd3WFVFOVRUa1ZVSUdVdFMyRnplU0JTYjI5MElFTkJJREV3SWhnUE1qQXlNekV4TWprd05qRXlNakZhR0E4eU1ETXpNVEV5T0RBMk1USXlNVm93T3pFTE1Ba0dBMVVFQmhNQ1VFd3hGakFVQmdOVkJBTVREVVZCV2pJeU1ESXhOakF4T1RZeEZEQVNCZ05WQkFvVEMxQnZjMjVsZENCVExrRXVNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXQrSUxlZlJWNjhqcFdDa2RveitZZWprRnU1dkh3N01vYjgxSFIyQnhqUVp6eENhN2drMjExTEs5T1J6Y2ZIcW1YeU5aV3c3U0tpcUp1U0ZhM1Z5R29CaDFFMmF6VWxvcU5Db3g0TFNaM0FwQ3U1d0NIOVFaaS94YW5nQ2pReUtybG44bFFZaWtkNENTYTJyYlQycWVUZytRckJEdHBreVpmc3hQTWF6aFFuek9MNTRWMlZqQ2ZWUzFTajRNQ3lHeHVwQnl5RFY3UzlaKzRPOEdqZ0JYL3RSa1pxRW1SQmpELy83N2o4cjJWTkIyTHlPUXM3alN6RE1zbFo0VUFFN1lzMm1WNHhnZmtLYlh5TUhQK013VjFoZGpMMDJzR3E4ckFUK25aWXFtdURQNGJZKzJiWG5LNzZWQk1uT0pLVEQ0M0NQamhEVXErYjJsd2k4RUZmSlNxUUlEQVFBQm80SG9NSUhsTUF3R0ExVWRFd0VCL3dRQ01BQXdIUVlEVlIwT0JCWUVGQkRiZUErelZVTCtQYnBXam5sVFNlcmlrN0FkTUI4R0ExVWRJd1FZTUJhQUZFNElhYlJWR0FHTFJkSGlwZVNaeldHQmo0MUpNQTRHQTFVZER3RUIvd1FFQXdJRjREQTFCZ05WSFI4RUxqQXNNQ3FnS0tBbWhpUm9kSFJ3T2k4dlpXc3VjRzl6Ym1WMExtTnZiUzV3YkM5amNtd3ZjbU5oTVM1amNtd3dUZ1lEVlIwZ0JFY3dSVEJEQmdzckJnRUVBWU9mYmdFQkFUQTBNRElHQ0NzR0FRVUZCd0lCRmlab2RIUndjem92TDJWckxuQnZjMjVsZEM1amIyMHVjR3d2WTNCekwzSmpZVEV1YUhSdGJEQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FnRUFrUk5nNEhuMXUrM2tzeEZ2SmFTMHplYnBqQ3pVb2hyYldTZ3dHaGZWTUk1ZnJaZG5Id0V0SytPL1FuT2FIZXdzblZ0am5Samx0NWIrWHA3bFZNVFdMYnpYNjRVTUZQZkhzVER1OER6Zml6NVZjRUJ0MkNnSTlacFVSNHgyR0VJTDlSQmJzTzRqanFZajh1YnA3OWdsckgxMjF6Y3BhNmhSYkpZOHFReHNSTnkxZkNTL3ppazlQTTFla214eWc5b3pxOUJ4WmxKMnREZW12Y0pxbUhMaWNTUCsxWkRZWGR2SnJiNVJHMmFWL2JLeFJmTm15NGZxUVRNSFlucC94K2FBRlByTFdjV3lDbUMya1FodnNFYVJ1cWZDdlJ0TEtsT0t0UC9zdzRxQzlXbVdzbG8zeXl4YW9WRkRsYk5NZ3lWWWpicmtWZ0U1SXBPRWJpUG1GZTY4cUNKMk5KL0tsNFNWUGJiN2RSWXJyeHdYYVAzN2VUSXJKQ3NwbUMrWC9oNzQ3ak5mYTQxeXd3OCsrb1BMblNpby9PcFpNT0IzZmtBaWdjODlnNEtRalJLNDFsOGRJQnYvRmpMZm9XM3JidW5YQTM4R0VKT1ZhY1ZYM3ZQcUNoWExVYTN3dHBQTWRWbm9NbkFRMUJCOEU4RUs1YUtrK0tyQ1NzVnVPdmE0dTlsbURPdXFaMGt1Snd1M2RhM0c2TUpzSm90cloyTWNpMTV0S215UkU1VVViYlhBcXN6a2hXZTZ2bUdMa0ZTemliemZ2TXZmQ0xoMkxvQTBDakNCMGJLVHFydUdsSUpGZ0NhNFpKTDRnTHBiV2d0K2lMZDg3UEZwdGtBMUVHSldHSXNSVFBzVnhLNFQvWE5MME5td3hzTHdPTHhXZ2JxK1hjNDlZNUhYb0MzNDRSND0iXSwianBrbWV0YWRhdGEiOiJleUpqYjNKeVpXeGhkR2x2Ymtsa0lqb2lWRVpFTGtWQldqSXlNREl4TmpBeE9UWXVNakF5TkMwd05pMHdOVlF3T0RveE1Eb3dNQzQyTkRGYUluMCIsImVQYXJhZ29uLm1mLmdvdi5wbCI6ImV5SjNaWEp6YW1FaU9pSktVRXRmUzBGVFFWOVFRVkpCUjA5T1gzWXlMVEFpTENKS1VFdEpSQ0k2SWpBd01UQXdNREF3TURBd01EWXhNREl4TXlJc0ltUmhkR0ZLVUVzaU9pSXlNREkyTFRBM0xUTXdWREUzT2pJeE9qRTVMakF3TUZvaUxDSktVRXRTUlVZaU9uc2lTbEJMU1VRaU9pSXdNREV3TURBd01EQXdNREEyTVRBeU1URWlMQ0pUU0VFeU5UWWlPaUl6TXpVNVFrRkVRa0kzTURSRE5UZzBNVVk1T1RBd01ERkdOREE1T1VGRVJUSkZSVVF6TUVFME4wRkZSa1JDT1RNd05rVTFRVUZEUlRoRk0wVTJOak00SW4xOSJ9.eyJkb2t1bWVudCI6eyJuYWdsb3dlayI6eyJ3ZXJzamEiOiJKUEtfS0FTQV9QQVJBR09OX3YyLTAiLCJkYXRhSlBLIjoiMjAyNi0wNy0zMFQxNzoyMToxOS4wMDBaIn0sInBvZG1pb3QxIjp7Ik5JUCI6Ijc3OTEwMTEzMjciLCJuYXp3YVBvZCI6IkpFUk9OSU1PIE1BUlRJTlMgUE9MU0tBIFMuQS4iLCJhZHJlc1BvZCI6eyJrb2RQb2N6dCI6IjM0LTMyNSIsIm1pZWpzYyI6IsWBT0RZR09XSUNFIiwidWxpY2EiOiJCRVNLSURaS0EgOCJ9LCJuclVuaWsiOiJFQVoyMjAyMTYwMTk2IiwibnJGYWJyIjoiUE8xMTQwNTQzOTEiLCJuckV3aWQiOiIyMDI0LzAwMDA5MjEyNiJ9LCJwYXJhZ29uIjp7IkpQS0lEIjo2MTAyMTMsInBhbWllY0NociI6MSwibnJEb2siOjYwODgyNywicG96eWNqYSI6W3sidG93YXIiOnsiYnJ1dHRvIjo1NDksImNlbmEiOjU0OSwiaWRTdFBUVSI6IkEiLCJpbG9zYyI6IjEiLCJuYXp3YSI6IlRvbmljICAgICAgICAgICAgICAgICAgICBBIiwib3BlciI6ZmFsc2V9fSx7InRvd2FyIjp7ImJydXR0byI6NTQ5LCJjZW5hIjo1NDksImlkU3RQVFUiOiJBIiwiaWxvc2MiOiIxIiwibmF6d2EiOiJDaXRydXMgbWl4ICAgICAgICAgICAgICAgQSIsIm9wZXIiOmZhbHNlfX0seyJ0b3dhciI6eyJicnV0dG8iOjE3OTgsImNlbmEiOjg5OSwiaWRTdFBUVSI6IkEiLCJpbG9zYyI6IjIiLCJuYXp3YSI6Ik5hcEdhekNvY2FDb01JWDIsNWwgICAgICBBIiwib3BlciI6ZmFsc2V9fSx7InRvd2FyIjp7ImJydXR0byI6MTc5OCwiY2VuYSI6ODk5LCJpZFN0UFRVIjoiQSIsImlsb3NjIjoiMiIsIm5hendhIjoiTmFwR2F6Q29jYUNvTUlYMiw1bCAgICAgIEEiLCJvcGVyIjpmYWxzZX19LHsidG93YXIiOnsiYnJ1dHRvIjo0NDksImNlbmEiOjQ0OSwiaWRTdFBUVSI6IkMiLCJpbG9zYyI6IjEiLCJuYXp3YSI6Ik9zaGVlVml0YW1NYWduZXoxLDFsICAgICBDIiwib3BlciI6ZmFsc2V9fSx7InRvd2FyIjp7ImJydXR0byI6NzE0LCJjZW5hIjoxMTksImlkU3RQVFUiOiJBIiwiaWxvc2MiOiI2IiwibmF6d2EiOiJQb2xhcmlzIE5HYXogIDEsNWwgICAgICAgQSIsIm9wZXIiOmZhbHNlfX0seyJ0b3dhciI6eyJicnV0dG8iOjE1OTYsImNlbmEiOjM5OSwiaWRTdFBUVSI6IkMiLCJpbG9zYyI6IjQiLCJuYXp3YSI6IlRvc3QgTWHFm2xhbnkgNTAwZyAgICAgICAgQyIsIm9wZXIiOmZhbHNlfX0seyJ0b3dhciI6eyJicnV0dG8iOjc5OTgsImNlbmEiOjUzOTksImlkU3RQVFUiOiJBIiwiaWxvc2MiOiIyIiwibmF6d2EiOiJXw7Nka2FCIEJvY2lhbiAwLDdsICAgICAgIEEiLCJvcGVyIjpmYWxzZSwicmFiYXQiOnsib3BpcyI6IjEiLCJ3YXJ0IjotMjgwMH19fSx7InRvd2FyIjp7ImJydXR0byI6OTk5LCJjZW5hIjo5OTksImlkU3RQVFUiOiJDIiwiaWxvc2MiOiIxIiwibmF6d2EiOiJPbGVqUnplcFpPbGl3V3liMWwgICAgICAgQyIsIm9wZXIiOmZhbHNlfX0seyJ0b3dhciI6eyJicnV0dG8iOjY0OSwiY2VuYSI6NjQ5LCJpZFN0UFRVIjoiQyIsImlsb3NjIjoiMSIsIm5hendhIjoiR3Jhbm9sYU1sZWN6Q3plazM1MGcgICAgIEMiLCJvcGVyIjpmYWxzZX19LHsidG93YXIiOnsiYnJ1dHRvIjozOTksImNlbmEiOjM5OSwiaWRTdFBUVSI6IkEiLCJpbG9zYyI6IjEiLCJuYXp3YSI6IlBvZHBhxYJOYXR1cjY0S29zdGtpICAgICAgQSIsIm9wZXIiOmZhbHNlfX0seyJ0b3dhciI6eyJicnV0dG8iOjk5OSwiY2VuYSI6OTk5LCJpZFN0UFRVIjoiQyIsImlsb3NjIjoiMSIsIm5hendhIjoiR3Jhbm9sYUZpdE5lc09hdDMwMGcgICAgIEMiLCJvcGVyIjpmYWxzZX19LHsidG93YXIiOnsiYnJ1dHRvIjo5OTksImNlbmEiOjk5OSwiaWRTdFBUVSI6IkMiLCJpbG9zYyI6IjEiLCJuYXp3YSI6IkdyYW5vbGFGaXROZXNRdWkzMDBnICAgICBDIiwib3BlciI6ZmFsc2V9fSx7InRvd2FyIjp7ImJydXR0byI6Nzk4LCJjZW5hIjozOTksImlkU3RQVFUiOiJBIiwiaWxvc2MiOiIyIiwibmF6d2EiOiJETyBNT0pFSiBJRMSYICAgICAgICAgICAgIEEiLCJvcGVyIjpmYWxzZX19LHsidG93YXIiOnsiYnJ1dHRvIjoyMTY5LCJjZW5hIjozMjkwLCJpZFN0UFRVIjoiQyIsImlsb3NjIjoiMCw4NzEiLCJuYXp3YSI6IktpZcWCYkJyb2NrYVNva2_FgmtnICAgICAgIEMiLCJvcGVyIjpmYWxzZSwicmFiYXQiOnsib3BpcyI6IjEiLCJ3YXJ0IjotNjk3fX19LHsidG93YXIiOnsiYnJ1dHRvIjoyMDcyLCJjZW5hIjozMjkwLCJpZFN0UFRVIjoiQyIsImlsb3NjIjoiMCw4MzIiLCJuYXp3YSI6IktpZcWCYkJyb2NrYVNva2_FgmtnICAgICAgIEMiLCJvcGVyIjpmYWxzZSwicmFiYXQiOnsib3BpcyI6IjEiLCJ3YXJ0IjotNjY1fX19LHsidG93YXIiOnsiYnJ1dHRvIjozNzQ1LCJjZW5hIjo3NDksImlkU3RQVFUiOiJDIiwiaWxvc2MiOiI1IiwibmF6d2EiOiJHdWFuY2lhbGVDdWJHQjE1MGcgICAgICAgQyIsIm9wZXIiOmZhbHNlfX0seyJ0b3dhciI6eyJicnV0dG8iOjExMzcsImNlbmEiOjM3OSwiaWRTdFBUVSI6IkMiLCJpbG9zYyI6IjMiLCJuYXp3YSI6IkJhZ2lldGthIDE3NWcgICAgICAgICAgICBDIiwib3BlciI6ZmFsc2V9fSx7InRvd2FyIjp7ImJydXR0byI6NTk5LCJjZW5hIjo1OTksImlkU3RQVFUiOiJDIiwiaWxvc2MiOiIxIiwibmF6d2EiOiJNYXNsb0Vrc3RyYTIwMGcgICAgICAgICAgQyIsIm9wZXIiOmZhbHNlfX0seyJ0b3dhciI6eyJicnV0dG8iOjc0OSwiY2VuYSI6NzQ5LCJpZFN0UFRVIjoiQSIsImlsb3NjIjoiMSIsIm5hendhIjoiTmFwR2F6S2lubGV5TWl4MWwgICAgICAgIEEiLCJvcGVyIjpmYWxzZX19LHsidG93YXIiOnsiYnJ1dHRvIjo0OTksImNlbmEiOjQ5OSwiaWRTdFBUVSI6IkMiLCJpbG9zYyI6IjEiLCJuYXp3YSI6IkNlYnVsYSAxa2cgb3BhayAgICAgICAgICBDIiwib3BlciI6ZmFsc2V9fSx7InRvd2FyIjp7ImJydXR0byI6NzM4LCJjZW5hIjozNjksImlkU3RQVFUiOiJCIiwiaWxvc2MiOiIyIiwibmF6d2EiOiJNdXN6U2FyS2FtMTg1ZyAgICAgICAgICAgQiIsIm9wZXIiOmZhbHNlfX0seyJ0b3dhciI6eyJicnV0dG8iOjI5OSwiY2VuYSI6Mjk5LCJpZFN0UFRVIjoiQiIsImlsb3NjIjoiMSIsIm5hendhIjoiRElQIE9SSUVOVEFMTlkgQ1pPU04gICAgIEIiLCJvcGVyIjpmYWxzZX19LHsidG93YXIiOnsiYnJ1dHRvIjoyOTksImNlbmEiOjI5OSwiaWRTdFBUVSI6IkIiLCJpbG9zYyI6IjEiLCJuYXp3YSI6IlByenlwRG9Tb0FtZXJLYW1pMjBnICAgICBCIiwib3BlciI6ZmFsc2V9fSx7InRvd2FyIjp7ImJydXR0byI6MTI5OSwiY2VuYSI6MTI5OSwiaWRTdFBUVSI6IkIiLCJpbG9zYyI6IjEiLCJuYXp3YSI6IktldFBpa2FudG55UHVkIDk5MCBnICAgICBCIiwib3BlciI6ZmFsc2V9fSx7InRvd2FyIjp7ImJydXR0byI6MTA5OSwiY2VuYSI6MTA5OSwiaWRTdFBUVSI6IkIiLCJpbG9zYyI6IjEiLCJuYXp3YSI6IlNvc0N6b3NNYWRYWEw4NjBnICAgICAgICBCIiwib3BlciI6ZmFsc2V9fSx7InRvd2FyIjp7ImJydXR0byI6MTI5LCJjZW5hIjoxMjksImlkU3RQVFUiOiJBIiwiaWxvc2MiOiIxIiwibmF6d2EiOiJaYXBhbG5pY3prU29sZWJvMXN6dCAgICAgQSIsIm9wZXIiOmZhbHNlfX0seyJ0b3dhciI6eyJicnV0dG8iOjg0OSwiY2VuYSI6ODQ5LCJpZFN0UFRVIjoiQSIsImlsb3NjIjoiMSIsIm5hendhIjoiUsSZY3puaWsgUXVlZW4gMiA0ICAgICAgICBBIiwib3BlciI6ZmFsc2V9fSx7InRvd2FyIjp7ImJydXR0byI6Mjc5OCwiY2VuYSI6MTM5OSwiaWRTdFBUVSI6IkMiLCJpbG9zYyI6IjIiLCJuYXp3YSI6IkphamEgVyBXeWIgTCAxMHN6dCAgICAgICBDIiwib3BlciI6ZmFsc2V9fSx7InRvd2FyIjp7ImJydXR0byI6MTI5NywiY2VuYSI6MjE5OSwiaWRTdFBUVSI6IkMiLCJpbG9zYyI6IjAsOTI3IiwibmF6d2EiOiJNacSZc1pub2dpS3UgYiBrIGtnICAgICAgIEMiLCJvcGVyIjpmYWxzZSwicmFiYXQiOnsib3BpcyI6IjEiLCJ3YXJ0IjotNzQxfX19LHsidG93YXIiOnsiYnJ1dHRvIjoyOTg5LCJjZW5hIjoyOTg5LCJpZFN0UFRVIjoiQyIsImlsb3NjIjoiMSIsIm5hendhIjoiU2VyIEdvdWRhIMWad2lhdDFrZyAgICAgICBDIiwib3BlciI6ZmFsc2V9fSx7InRvd2FyIjp7ImJydXR0byI6NTQ5LCJjZW5hIjo1NDksImlkU3RQVFUiOiJDIiwiaWxvc2MiOiIxIiwibmF6d2EiOiJNaXhTYcWCYXRSb3N6cG9WZjE1MGcgICAgIEMiLCJvcGVyIjpmYWxzZX19LHsidG93YXIiOnsiYnJ1dHRvIjozOTgsImNlbmEiOjE5OSwiaWRTdFBUVSI6IkMiLCJpbG9zYyI6IjIiLCJuYXp3YSI6IkxpbW9ua2Egc3p0ICAgICAgICAgICAgICBDIiwib3BlciI6ZmFsc2V9fSx7InRvd2FyIjp7ImJydXR0byI6NTk5LCJjZW5hIjo1OTksImlkU3RQVFUiOiJDIiwiaWxvc2MiOiIxIiwibmF6d2EiOiJDeXRyeW5hIDUwMCBnICAgICAgICAgICAgQyIsIm9wZXIiOmZhbHNlfX0seyJ0b3dhciI6eyJicnV0dG8iOjEyOTksImNlbmEiOjEyOTksImlkU3RQVFUiOiJDIiwiaWxvc2MiOiIxIiwibmF6d2EiOiJQb21pZG9yUGFwcnljejUwMGcgICAgICAgQyIsIm9wZXIiOmZhbHNlfX0seyJ0b3dhciI6eyJicnV0dG8iOjU5OCwiY2VuYSI6Njk5LCJpZFN0UFRVIjoiQyIsImlsb3NjIjoiMCw4NTYiLCJuYXp3YSI6IkJhbmFuIEx1eiAgICAgICAgICAgICAgICBDIiwib3BlciI6ZmFsc2V9fSx7InRvd2FyIjp7ImJydXR0byI6MzQwLCJjZW5hIjo4OTksImlkU3RQVFUiOiJDIiwiaWxvc2MiOiIwLDM3OCIsIm5hendhIjoiT2fDs3Jla1N6a0x1eiAgICAgICAgICAgICBDIiwib3BlciI6ZmFsc2V9fSx7InRvd2FyIjp7ImJydXR0byI6MTQ5OCwiY2VuYSI6MTQ5OCwiaWRTdFBUVSI6IkMiLCJpbG9zYyI6IjEiLCJuYXp3YSI6IkJvcsOzd2thQW1lcnlrIDMwMGcgICAgICAgQyIsIm9wZXIiOmZhbHNlfX0seyJ0b3dhciI6eyJicnV0dG8iOjU1NiwiY2VuYSI6Nzk5LCJpZFN0UFRVIjoiQyIsImlsb3NjIjoiMSIsIm5hendhIjoiQ2hpcHN5Q3IxMzAtMTQwZyAgICAgICAgIEMiLCJvcGVyIjpmYWxzZSwicmFiYXQiOnsib3BpcyI6IjEiLCJ3YXJ0IjotMjQzfX19LHsidG93YXIiOnsiYnJ1dHRvIjozNDUsImNlbmEiOjQ5NSwiaWRTdFBUVSI6IkMiLCJpbG9zYyI6IjEiLCJuYXp3YSI6IkNoaXBzeVRvcEZyb21hZ2UxNTBnICAgICBDIiwib3BlciI6ZmFsc2UsInJhYmF0Ijp7Im9waXMiOiIxIiwid2FydCI6LTE1MH19fSx7InRvd2FyIjp7ImJydXR0byI6NjA1LCJjZW5hIjo4NjksImlkU3RQVFUiOiJDIiwiaWxvc2MiOiIxIiwibmF6d2EiOiJDaGlwc3lMYXkgczEyMGcgICAgICAgICAgQyIsIm9wZXIiOmZhbHNlLCJyYWJhdCI6eyJvcGlzIjoiMSIsIndhcnQiOi0yNjR9fX0seyJ0b3dhciI6eyJicnV0dG8iOjU1NywiY2VuYSI6Nzk5LCJpZFN0UFRVIjoiQyIsImlsb3NjIjoiMSIsIm5hendhIjoiQ2hpcHN5Q3J1bjEzMC0xNDBnICAgICAgIEMiLCJvcGVyIjpmYWxzZSwicmFiYXQiOnsib3BpcyI6IjEiLCJ3YXJ0IjotMjQyfX19LHsidG93YXIiOnsiYnJ1dHRvIjozNDcsImNlbmEiOjQ5OSwiaWRTdFBUVSI6IkMiLCJpbG9zYyI6IjEiLCJuYXp3YSI6IlByYcW8eW5raU5pZXRLZXQxMDBnICAgICAgQyIsIm9wZXIiOmZhbHNlLCJyYWJhdCI6eyJvcGlzIjoiMSIsIndhcnQiOi0xNTJ9fX0seyJ0b3dhciI6eyJicnV0dG8iOjU1NiwiY2VuYSI6Nzk5LCJpZFN0UFRVIjoiQyIsImlsb3NjIjoiMSIsIm5hendhIjoiQ2hpcHlDcnVuMTMwLTE0MGcgICAgICAgIEMiLCJvcGVyIjpmYWxzZSwicmFiYXQiOnsib3BpcyI6IjEiLCJ3YXJ0IjotMjQzfX19LHsidG93YXIiOnsiYnJ1dHRvIjoxMzk5LCJjZW5hIjoxNjk5LCJpZFN0UFRVIjoiQyIsImlsb3NjIjoiMSIsIm5hendhIjoiUGl6emFLdXJQaWVjejYwMGcgICAgICAgIEMiLCJvcGVyIjpmYWxzZSwicmFiYXQiOnsib3BpcyI6IjEiLCJ3YXJ0IjotMzAwfX19LHsidG93YXIiOnsiYnJ1dHRvIjoxMzk5LCJjZW5hIjoxNjk5LCJpZFN0UFRVIjoiQyIsImlsb3NjIjoiMSIsIm5hendhIjoiUGl6Um9kelN6eW5TZXI2MDBnICAgICAgIEMiLCJvcGVyIjpmYWxzZSwicmFiYXQiOnsib3BpcyI6IjEiLCJ3YXJ0IjotMzAwfX19LHsidG93YXIiOnsiYnJ1dHRvIjoxMzk4LCJjZW5hIjo2OTksImlkU3RQVFUiOiJDIiwiaWxvc2MiOiIzIiwibmF6d2EiOiJTenlua2FLb25zZXJ3b0tXMjAwICAgICAgQyIsIm9wZXIiOmZhbHNlLCJyYWJhdCI6eyJvcGlzIjoiMSIsIndhcnQiOi02OTl9fX0seyJ0b3dhciI6eyJicnV0dG8iOjY3OCwiY2VuYSI6MzM5LCJpZFN0UFRVIjoiQyIsImlsb3NjIjoiMyIsIm5hendhIjoiTWFrYXJvblBlbm5lUGE1MDBnICAgICAgIEMiLCJvcGVyIjpmYWxzZSwicmFiYXQiOnsib3BpcyI6IjEiLCJ3YXJ0IjotMzM5fX19LHsidG93YXIiOnsiYnJ1dHRvIjoyODc0LCJjZW5hIjo0NzksImlkU3RQVFUiOiJBIiwiaWxvc2MiOiIxMiIsIm5hendhIjoiUElXTyBIRUlORUtFTiAwLDVCICAgICAgIEEiLCJvcGVyIjpmYWxzZSwicmFiYXQiOnsib3BpcyI6IjEiLCJ3YXJ0IjotMjg3NH19fV0sInN0UFRVIjpbeyJpZCI6IkEiLCJ3YXJ0IjoyMzAwfSx7ImlkIjoiQiIsIndhcnQiOjgwMH0seyJpZCI6IkMiLCJ3YXJ0Ijo1MDB9LHsiaWQiOiJEIiwid2FydCI6MH0seyJpZCI6IkUiLCJ3YXJ0IjoiWlcifSx7ImlkIjoiRiIsIndhcnQiOjE3MDB9LHsiaWQiOiJHIiwid2FydCI6MH1dLCJwb2RzdW0iOnsid2FsdXRhIjoiUExOIiwic3VtYUJydXR0byI6NTkwNTUsInN1bWFQb2QiOjU1ODgsInN1bWFPcHVzdCI6LTEwNzA5LCJzdW1hTmV0dG8iOlt7ImlkU3RQVFUiOiJBIiwiYnJ1dHRvIjoxOTIwNCwidmF0IjozNTkxfSx7ImlkU3RQVFUiOiJCIiwiYnJ1dHRvIjozNzM0LCJ2YXQiOjI3N30seyJpZFN0UFRVIjoiQyIsImJydXR0byI6MzYxMTcsInZhdCI6MTcyMH1dfSwidG90YWwiOnsiemFwbFp3cm90Ijo1OTc1NX0sIm9wYWsiOnsiZGFuZU9wYWsiOlt7ImNlbmEiOjUwLCJpbG9zYyI6MTQwMDAsIm5hendhIjoiQnV0IFBsYXN0aWsga2F1Y2phIn1dLCJ3YXJ0Ijo3MDB9LCJwbGF0bm9zYyI6W3sicmVzenRhIjpmYWxzZSwiZm9ybWEiOiIyIiwid2FydCI6NTk3NTUsIm5hendhIjoiVklTQSBDQVJEIDA3IDEifV0sInpha1NwcnplZCI6IjIwMjYtMDctMzBUMTc6MjE6MTkuMDAwWiIsIm5yS2FzeSI6Ikthc2EgMSIsImthc2plciI6Ikthc2plciBuciAyNyIsIm5yUGFyYWciOjQxOSwiZ3JhZmlrYSI6MX19fQ.mISQbxs3V0GxEIPLaUviW3n2bgAewkMHOV3dmMo64RFMBXPQin3nMg5BUtX_irNEAjaKqpfHJTXy9sxDncqgo1KbynblPschfOeZWuJmY4wyBHjLkdEJYcqAATGBorl4Awo2kTAoSK0_fiRD6EkOk5ooMS5CiSDueV2t-AJW4POmuhq_KNX7TXyxqGDvKcrFHe2hPoGx4cmayC59ymDJKWcSOyStFkUWm_hpui2IObLmXgPxCj8KjTVtWA6_Qsq8JDLuAcAyCzWvi23AFbCelDUo846IUVe9m-DSUCgiuDDCMLmYCBnul0Q049tY5kMc2IhOOCl5F_Qy9SHTi_k4SQ", + "header": [ + { + "image": { + "id": "EAZ2202160196-1-445785", + "hash": "DmwwvSQRHLaIxnrFwFSH2ZMvlLZHRrslUP5n/gwzXpU=", + "data": "Qk02FwAAAAAAAD4AAAAoAAAAMAEAAJMAAAABAAEAAAAAAPgWAAAjLgAAIy4AAAAAAAAAAAAAAAAAAP///wD//////////////////////////////////////////////////wAA//////////////////////////////////////////////////8AAP//////////////////////////////////////////////////AAD//////////////////////////////////////////////////wAA//////////////////////////////////////////////////8AAP//////////////////////////////////////////////////AAD//////////////////////////////////////////////////wAA//////////////////////////////////////////////////8AAP//////////////////////////////////////////////////AAD//////////////////////////////////////////////////wAA//////wAAf////////////////////////////////////////8AAP/////AAAAf////////////////////////////////////////AAD////+AAAAA////////////////////////////////////////wAA////+ABVVAD///////////////////////////////////////8AAP///+AAAAAAD///////////////////////////////////////AAD///+AVaqqqAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/////wAA////AAAAAAAFVVVVVVVaqqqqqqqqqqqqVVVVVVVVVVVVQD////8AAP///wqqVVVVQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH////AAD///8AAAAAAAAqqqqqqqVVVVVVVVVVVVWqqqqqqqqqqqqlUf///wAA////FVJUqqqoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB///8AAP///wAFqgAAABVVVVVVWqqqqqqqqqqqqlVVVVVVVVVVVVqqn///AAD//x8KqlVAFVUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf//wAA//4fFVWqAAAAA////////////////////////////////VVX//8AAP/8HwqqVAACqoFVVVVVWqqqqqqqqqqqqlVVVVVVVVVVVVqAAP//AAD//A8VVagAAAAB////////////////////////////////+qp//wAA//gPCqpQAAVVUKqqqqqlVVVVVVVVVVVVqqqqqqqqqqqqpVYAP/8AAP/wDxVVoAACAAB/////////////////////////////////1V//AAD/4IYKqlAAAVKoVVVVVVqqqqqqqqqqqqpVVVVVVVVVVVVaqqAP/wAA/8EAFVWgAAKoAD/////////////////////////////////6p/8AAP/CgAqqUAABVVUKqqqqpVVVVVVVVVVVVaqqqqqqqqqqqqVVUAP/AAD/hUAVVagAAqoAH///////////////////////////////x/1V/wAA/wqACqpUAAVVKoVVVVVaqqqqqqqqqqqqVVVVVVVVVVVVWqKqAf8AAP8FQBVVqgAKqoAH//////////H////8f///x///x/8fD//j/6r/AAD/CoAKqlVAVVUBQqqqD4FgEBjAYxjEcBWjEYOYxwGqCgY40VUA/wAA/hVAFVWqqqqqgAf//AcAwDAYxmMYzGMf4xmZmMYx/ExmePH/1X8AAP4VQBVVqqqqqoAH//wHAMAwGMZjGMxjH+MZmZjGMfxMZnjx/9V/AAD+KqAKqlVVVVVAoVVYZjxGKOiP4xjEY+pjEXkBRn9U+P44oKqAfwAA/BVABVWqqqqqgAP/+P48Rjx4gGMYzGAf4xnhh8YB/PgGeOD/6r8AAPwqoAqqVVVVVUBQqrGqPEY2KI5jGMRjlaMRgoPGOajs5jjEVUA/AAD4VVAFVaqqqqqAAf/x/jzGPzjOYxjMc5/jGY+Rxzn8TOZ4jH/1XwAA+CqgCqpVVVVVQAhVUVUAoDAIwKAoBHAqYBFBGMcDVgYOAIoqoB8AAPhVUAVVqqqqqqAA//H/gfEwH+DgeB/4P+B/45//h/8fHgP///qvAADwqqgCqlVVVVVAFKqouqVVNVVVVVVVVVWqqqqaqqqqqqqlVVVQDwAA8VVQBVWqqqqqoAD/+Gf//z///////v//////n////////////U8AAPCKqAKqVVVVVVAIVVQFWqoqqKqqqqxqqlVRVRVFVVVVVVqqqqgHAADxFVQFVaqqqqqoAH/+D///P/j////8f///+f+fx//////////+pwAA4AqoAqpVFVVVVAQqqqqlVVVVVVVVVFVVqqiqqqqqqqqqpVVVVAcAAOAFVAFVqAKqqqoIf/////////////////////////////////9TAADiCqoCqlAAVVVUBFVVVVqqqqqqqqqqqqpVVVVVVVVVVVVaqqqqAwAA4AVUAVWgACqqqqp//////////////////////////////////qsAAOIKqgCqQABVVVVUKqgABVAVwo1UIQNBVawA6wKoLAqgKoIQVVUDAADgBVUBVYAAKqqqqj/4AAP4PwAD+AADwf/wAD8D+DwPwD8AAH//UwAA4gqqAKoAABVVVVQVWqqkqSpVU6VVUtWqaqqVKVK0pSpUpVVqqgMAAOAFVQFVgAAqqqqqP/gAAPg+AAHgAAPB/8AADwP4PA8A/AAAf/+rAADiCqqAqgAAFVVVVCqpX1patKVBSoKrSVXVAUtSqS1KVKlH6lVVAwAA4AVVQFWAACqqqqo/+BuAeDwf/8B+A8H/gPwHA/g8DAP4HqB//1MAAOIKqoAqAAAVVVVUFVq/5WkpWqrV1VLVq6uqpSlStKKlUr/1aqoDAADhFVVAVYAAKqqqqj/4FUB4OB//wP8Dwf+B/gcD+DwAD/AlUH//qwAA4rqqoCoAABVVVVQqqV/aWrqgAMqrK0tVlVdXUqktVUq1f/pVVQMAAOF1VUAVgAAqqqqqf/gawHg4AADA/wPB/4H+BwP4PAAf8Dqwf/9TAADguqqgKkAAVVVVVFVav6VpKVVU1dVS1KurqqWpUrSipVK/9WqqAwAA4X1VUBWgAKqqqqp/+AAA+DwfwMB+A8B/gPwHA/A8CAf4HWB//qcAAPC+qqgKUAFVVVVUKqlVWVq0uulKnqtKlZVVS1SlLUlSqUfqVVQHAADxfUFUBagCqqqCqH/4AAf4PA+B4AADwAHAAA8AADwOAfwAAH/9VwAA8L6AKApVVVVUAVRVWqqiqSpVUqVUUtRUSqqVKKq0pSpUpUVqqA8AAPBfABQFqqqqqgKo//gcA/g/AAf4AQPBgfAAPwIAfA+AfgAQf/qvAAD4vgAKAlVVVVQBUKqpX1lQFQqFVKErQUGsAOsCAK1KgCsAMFVQDwAA+F4ABQGqqqqoAqj/+BUB///gP/8PA//////////8D///////9V8AAPgsAAKCVVVVVAFRVVq/paqqqqqqqlKqqlVVVVVVVKVVVVqqqqAfAAD8XAAFAaqqqqgCof/4GwH///////8D//////////wP///////qvwAA/CwAAoBVVVVUAVKqqV9ZVFVVVVVVK1VVqqqqqqqtSqqqpVVVQD8AAP4UAAFAKqqqqAKj//gAA/g//////wP//////////A///////9U/AAD+CgACoBVVVVQFRVVaqqK1WqqqqqpSqqpVVVVVVVSlVVVaqqqAfwAA/hYAAVAqqqqqAoP/+AAH8B//////A//////////8D///////qn8AAP8LAAKgFVVVVRUCqqgABVKVVVVVVStVVaqqqqqqrAqqqqVVVQD/AAD/BYAFUAqqqqqqj//////4P/////+H//////////////////9U/wAA/4KAAqgFVVVQFRVVVVVaqqqqqqqqqqqqVVVVVVVVVVVVWqqoAf8AAP+BQAVUAqqoAAIf////////////////////////////////+qv/AAD/wrAKqgFVUAAAKqqqqqVVVVVVVVVVVVWqqqqqqqqqqqqlVVAH/wAA/+FcFVWAqoAAAA/////////////////////////////////1T/8AAP/wr+qqQFUB//wFVVVVWqqqqqqqqqqqqlVVVVVVVVVVVVqqgB//AAD/+Ff1VaAoB///gf///////////////////////////////qq//wAA//wr+qpAUB/4D8CqqqqlVVVVVVVVVVVVqqqqqqqqqqqqpVQAf/8AAP/+Ff1VoCB/gAHwf//////////////////////////////VVP//AAD//wqCqlAA/wAAeBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH//wAA//+FAFWoAfwAADwFVVVaqqqqqqqqqqqqVVVVVVVVVVVVWqqn//8AAP//wIAKVAP4AAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///AAD//+BAD6oH8AAAD4KqqqVVVVVVVVVVVVWqqqqqqqqqqqqlVD///wAA///4IAfVD/AAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH///8AAP///BQH+h/gAAADwVVVWqqqqqqqqqqqqlVVVVVVVVVVVVqH////AAD///4Cq/wfwAAAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf////wAA////AFWoP8AH/AHg//////////////////////////////////8AAP///8AKVH+A///g8P//////////////////////////////////AAD////wACh8g/////B//////////////////////////////////wAA/////AAA/g/////4f/////////////////////////////////8AAP/////gAP8f///B+H//////////////////////////////////AAD//////gH/j///gPw//////////////////////////////////wAA///////h/+///5j8P/////////////////////////////////8AAP//////4f////+I/D//////////////////////////////////AAD//////+H///H/w/4//////////////////////////////////wAA///////D///h///+H/////////////////////////////////8AAP//////w///wP///h//////////////////////////////////AAD//////8P//4D///4f/////////////////////////////////wAA///////D//+A///+H/////////////////////////////////8AAP//////w///AP///h//////////////////////////////////AAD//////8P//wH/+H4f/////////////////////////////////wAA///////D//8B//B+H/////////////////////////////////8AAP//////4//+Af/gfh//////////////////////////////////AAD//////+P//gH/4H4f/////////////////////////////////wAA///////h//4D/+B+P/////////////////////////////////8AAP//////4f/+A//AfD//////////////////////////////////AAD//////+H//gf/wPw//////////////////////////////////wAA///////h//+H/8D8P/////////////////////////////////8AAP//////8P//D/+B+H//////////////////////////////////AAD///////D//x//gfh//////////////////////////////////wAA///////4f////6Pwf/////////////////////////////////8AAP//////+H/////H8P//////////////////////////////////AAD///////g/////h+D//////////////////////////////////wAA///////4H////9/A//////////////////////////////////8AAP///////B//////wf//////////////////////////////////AAD///////4P/////4P//////////////////////////////////wAA///////+B/////8D//////////////////////////////////8AAP///////wP////+B///////////////////////////////////AAD///////+B/////A///////////////////////////////////wAA////////AP////gf//////////////////////////////////8AAP///////gA////gP///////////////////////////////////AAD///////wYD///gH///////////////////////////////////wAA///////8HAH//gH///////////////////////////////////8AAP//////+D4Af/AD////////////////////////////////////AAD///////B/AAAAB////////////////////////////////////wAA///////g/8AAAB////////////////////////////////////8AAP/////wAf/wAAB/////////////////////////////////////AAD/////4Af//AAf/////////////////////////////////////wAA/////+Af//wf//////////////////////////////////////8AAP/////gP//+H///////////////////////////////////////AAD/////8H///h///////////////////////////////////////wAA//////////8f//////////////////////////////////////8AAP//////////D///////////////////////////////////////AAD//////////4f//////////////////////////////////////wAA///////////D//////////////////////////////////////8AAP//////////4MP/////////////////////////////////////AAD///////////AB/////////////////////////////////////wAA///////////8Af////////////////////////////////////8AAP///////////4H/////////////////////////////////////AAD////////////D/////////////////////////////////////wAA//////////////////////////////////////////////////8AAP//////////////////////////////////////////////////AAA=" + } + }, + { + "headerText": { + "headerTextLines": "
BIEDRONKA "CODZIENNIE NISKIE CENY" 5834
34-325 ŁODYGOWICE UL. BESKIDZKA 8
JERONIMO MARTINS POLSKA S.A.
62-025 KOSTRZYN UL.ŻNIWNA 5
" + } + }, + { + "headerData": { + "tin": "7791011327", + "docNumber": 608827, + "date": "2026-07-30T17:21:19.000Z", + "CPS": 56 + } + } + ], + "body": [ + { + "sellLine": { + "name": "Tonic A", + "vatId": "A", + "price": 549, + "total": 549, + "quantity": "1", + "isStorno": false + } + }, + { + "sellLine": { + "name": "Citrus mix A", + "vatId": "A", + "price": 549, + "total": 549, + "quantity": "1", + "isStorno": false + } + }, + { + "sellLine": { + "name": "NapGazCocaCoMIX2,5l A", + "vatId": "A", + "price": 899, + "total": 1798, + "quantity": "2", + "isStorno": false + } + }, + { + "sellLine": { + "name": "NapGazCocaCoMIX2,5l A", + "vatId": "A", + "price": 899, + "total": 1798, + "quantity": "2", + "isStorno": false + } + }, + { + "sellLine": { + "name": "OsheeVitamMagnez1,1l C", + "vatId": "C", + "price": 449, + "total": 449, + "quantity": "1", + "isStorno": false + } + }, + { + "sellLine": { + "name": "Polaris NGaz 1,5l A", + "vatId": "A", + "price": 119, + "total": 714, + "quantity": "6", + "isStorno": false + } + }, + { + "sellLine": { + "name": "Tost Maślany 500g C", + "vatId": "C", + "price": 399, + "total": 1596, + "quantity": "4", + "isStorno": false + } + }, + { + "sellLine": { + "name": "WódkaB Bocian 0,7l A", + "vatId": "A", + "price": 5399, + "total": 10798, + "quantity": "2", + "isStorno": false + } + }, + { + "discountLine": { + "base": 10798, + "value": 2800, + "isDiscount": true, + "isPercent": false, + "isStorno": false, + "vatId": "A" + } + }, + { + "sellLine": { + "name": "OlejRzepZOliwWyb1l C", + "vatId": "C", + "price": 999, + "total": 999, + "quantity": "1", + "isStorno": false + } + }, + { + "sellLine": { + "name": "GranolaMleczCzek350g C", + "vatId": "C", + "price": 649, + "total": 649, + "quantity": "1", + "isStorno": false + } + }, + { + "sellLine": { + "name": "PodpałNatur64Kostki A", + "vatId": "A", + "price": 399, + "total": 399, + "quantity": "1", + "isStorno": false + } + }, + { + "sellLine": { + "name": "GranolaFitNesOat300g C", + "vatId": "C", + "price": 999, + "total": 999, + "quantity": "1", + "isStorno": false + } + }, + { + "sellLine": { + "name": "GranolaFitNesQui300g C", + "vatId": "C", + "price": 999, + "total": 999, + "quantity": "1", + "isStorno": false + } + }, + { + "sellLine": { + "name": "DO MOJEJ IDĘ A", + "vatId": "A", + "price": 399, + "total": 798, + "quantity": "2", + "isStorno": false + } + }, + { + "sellLine": { + "name": "KiełbBrockaSokołkg C", + "vatId": "C", + "price": 3290, + "total": 2866, + "quantity": "0,871", + "isStorno": false + } + }, + { + "discountLine": { + "base": 2866, + "value": 697, + "isDiscount": true, + "isPercent": false, + "isStorno": false, + "vatId": "C" + } + }, + { + "sellLine": { + "name": "KiełbBrockaSokołkg C", + "vatId": "C", + "price": 3290, + "total": 2737, + "quantity": "0,832", + "isStorno": false + } + }, + { + "discountLine": { + "base": 2737, + "value": 665, + "isDiscount": true, + "isPercent": false, + "isStorno": false, + "vatId": "C" + } + }, + { + "sellLine": { + "name": "GuancialeCubGB150g C", + "vatId": "C", + "price": 749, + "total": 3745, + "quantity": "5", + "isStorno": false + } + }, + { + "sellLine": { + "name": "Bagietka 175g C", + "vatId": "C", + "price": 379, + "total": 1137, + "quantity": "3", + "isStorno": false + } + }, + { + "sellLine": { + "name": "MasloEkstra200g C", + "vatId": "C", + "price": 599, + "total": 599, + "quantity": "1", + "isStorno": false + } + }, + { + "sellLine": { + "name": "NapGazKinleyMix1l A", + "vatId": "A", + "price": 749, + "total": 749, + "quantity": "1", + "isStorno": false + } + }, + { + "sellLine": { + "name": "Cebula 1kg opak C", + "vatId": "C", + "price": 499, + "total": 499, + "quantity": "1", + "isStorno": false + } + }, + { + "sellLine": { + "name": "MuszSarKam185g B", + "vatId": "B", + "price": 369, + "total": 738, + "quantity": "2", + "isStorno": false + } + }, + { + "sellLine": { + "name": "DIP ORIENTALNY CZOSN B", + "vatId": "B", + "price": 299, + "total": 299, + "quantity": "1", + "isStorno": false + } + }, + { + "sellLine": { + "name": "PrzypDoSoAmerKami20g B", + "vatId": "B", + "price": 299, + "total": 299, + "quantity": "1", + "isStorno": false + } + }, + { + "sellLine": { + "name": "KetPikantnyPud 990 g B", + "vatId": "B", + "price": 1299, + "total": 1299, + "quantity": "1", + "isStorno": false + } + }, + { + "sellLine": { + "name": "SosCzosMadXXL860g B", + "vatId": "B", + "price": 1099, + "total": 1099, + "quantity": "1", + "isStorno": false + } + }, + { + "sellLine": { + "name": "ZapalniczkSolebo1szt A", + "vatId": "A", + "price": 129, + "total": 129, + "quantity": "1", + "isStorno": false + } + }, + { + "sellLine": { + "name": "Ręcznik Queen 2 4 A", + "vatId": "A", + "price": 849, + "total": 849, + "quantity": "1", + "isStorno": false + } + }, + { + "sellLine": { + "name": "Jaja W Wyb L 10szt C", + "vatId": "C", + "price": 1399, + "total": 2798, + "quantity": "2", + "isStorno": false + } + }, + { + "sellLine": { + "name": "MięsZnogiKu b k kg C", + "vatId": "C", + "price": 2199, + "total": 2038, + "quantity": "0,927", + "isStorno": false + } + }, + { + "discountLine": { + "base": 2038, + "value": 741, + "isDiscount": true, + "isPercent": false, + "isStorno": false, + "vatId": "C" + } + }, + { + "sellLine": { + "name": "Ser Gouda Świat1kg C", + "vatId": "C", + "price": 2989, + "total": 2989, + "quantity": "1", + "isStorno": false + } + }, + { + "sellLine": { + "name": "MixSałatRoszpoVf150g C", + "vatId": "C", + "price": 549, + "total": 549, + "quantity": "1", + "isStorno": false + } + }, + { + "sellLine": { + "name": "Limonka szt C", + "vatId": "C", + "price": 199, + "total": 398, + "quantity": "2", + "isStorno": false + } + }, + { + "sellLine": { + "name": "Cytryna 500 g C", + "vatId": "C", + "price": 599, + "total": 599, + "quantity": "1", + "isStorno": false + } + }, + { + "sellLine": { + "name": "PomidorPaprycz500g C", + "vatId": "C", + "price": 1299, + "total": 1299, + "quantity": "1", + "isStorno": false + } + }, + { + "sellLine": { + "name": "Banan Luz C", + "vatId": "C", + "price": 699, + "total": 598, + "quantity": "0,856", + "isStorno": false + } + }, + { + "sellLine": { + "name": "OgórekSzkLuz C", + "vatId": "C", + "price": 899, + "total": 340, + "quantity": "0,378", + "isStorno": false + } + }, + { + "sellLine": { + "name": "BorówkaAmeryk 300g C", + "vatId": "C", + "price": 1498, + "total": 1498, + "quantity": "1", + "isStorno": false + } + }, + { + "sellLine": { + "name": "ChipsyCr130-140g C", + "vatId": "C", + "price": 799, + "total": 799, + "quantity": "1", + "isStorno": false + } + }, + { + "discountLine": { + "base": 799, + "value": 243, + "isDiscount": true, + "isPercent": false, + "isStorno": false, + "vatId": "C" + } + }, + { + "sellLine": { + "name": "ChipsyTopFromage150g C", + "vatId": "C", + "price": 495, + "total": 495, + "quantity": "1", + "isStorno": false + } + }, + { + "discountLine": { + "base": 495, + "value": 150, + "isDiscount": true, + "isPercent": false, + "isStorno": false, + "vatId": "C" + } + }, + { + "sellLine": { + "name": "ChipsyLay s120g C", + "vatId": "C", + "price": 869, + "total": 869, + "quantity": "1", + "isStorno": false + } + }, + { + "discountLine": { + "base": 869, + "value": 264, + "isDiscount": true, + "isPercent": false, + "isStorno": false, + "vatId": "C" + } + }, + { + "sellLine": { + "name": "ChipsyCrun130-140g C", + "vatId": "C", + "price": 799, + "total": 799, + "quantity": "1", + "isStorno": false + } + }, + { + "discountLine": { + "base": 799, + "value": 242, + "isDiscount": true, + "isPercent": false, + "isStorno": false, + "vatId": "C" + } + }, + { + "sellLine": { + "name": "PrażynkiNietKet100g C", + "vatId": "C", + "price": 499, + "total": 499, + "quantity": "1", + "isStorno": false + } + }, + { + "discountLine": { + "base": 499, + "value": 152, + "isDiscount": true, + "isPercent": false, + "isStorno": false, + "vatId": "C" + } + }, + { + "sellLine": { + "name": "ChipyCrun130-140g C", + "vatId": "C", + "price": 799, + "total": 799, + "quantity": "1", + "isStorno": false + } + }, + { + "discountLine": { + "base": 799, + "value": 243, + "isDiscount": true, + "isPercent": false, + "isStorno": false, + "vatId": "C" + } + }, + { + "sellLine": { + "name": "PizzaKurPiecz600g C", + "vatId": "C", + "price": 1699, + "total": 1699, + "quantity": "1", + "isStorno": false + } + }, + { + "discountLine": { + "base": 1699, + "value": 300, + "isDiscount": true, + "isPercent": false, + "isStorno": false, + "vatId": "C" + } + }, + { + "sellLine": { + "name": "PizRodzSzynSer600g C", + "vatId": "C", + "price": 1699, + "total": 1699, + "quantity": "1", + "isStorno": false + } + }, + { + "discountLine": { + "base": 1699, + "value": 300, + "isDiscount": true, + "isPercent": false, + "isStorno": false, + "vatId": "C" + } + }, + { + "sellLine": { + "name": "SzynkaKonserwoKW200 C", + "vatId": "C", + "price": 699, + "total": 2097, + "quantity": "3", + "isStorno": false + } + }, + { + "discountLine": { + "base": 2097, + "value": 699, + "isDiscount": true, + "isPercent": false, + "isStorno": false, + "vatId": "C" + } + }, + { + "sellLine": { + "name": "MakaronPennePa500g C", + "vatId": "C", + "price": 339, + "total": 1017, + "quantity": "3", + "isStorno": false + } + }, + { + "discountLine": { + "base": 1017, + "value": 339, + "isDiscount": true, + "isPercent": false, + "isStorno": false, + "vatId": "C" + } + }, + { + "sellLine": { + "name": "PIWO HEINEKEN 0,5B A", + "vatId": "A", + "price": 479, + "total": 5748, + "quantity": "12", + "isStorno": false + } + }, + { + "discountLine": { + "base": 5748, + "value": 2874, + "isDiscount": true, + "isPercent": false, + "isStorno": false, + "vatId": "A" + } + }, + { "discountSummary": { "discounts": 10709 } }, + { + "vatSummary": { + "currency": "PLN", + "vatRatesSummary": [ + { + "vatId": "A", + "vatRate": 2300, + "vatSale": 19204, + "vatAmount": 3591 + }, + { "vatId": "B", "vatRate": 800, "vatSale": 3734, "vatAmount": 277 }, + { "vatId": "C", "vatRate": 500, "vatSale": 36117, "vatAmount": 1720 } + ] + } + }, + { + "sumInCurrency": { + "fiscalTotal": 59055, + "totalWithPacks": 59755, + "currency": "PLN", + "printBig": true, + "printable": true + } + }, + { "section": { "type": 0 } }, + { "section": { "type": 1 } }, + { + "pack": { + "name": "But Plastik kaucja", + "price": 50, + "quantity": "14", + "total": 700, + "isNegative": false + } + }, + { "section": { "type": 100, "amount": 700, "currency": "PLN" } }, + { "section": { "type": 101, "amount": 59755, "currency": "PLN" } }, + { "section": { "type": 3 } }, + { + "payment": { + "type": "2", + "amount": 59755, + "name": "VISA CARD 07 1", + "currency": "PLN" + } + }, + { + "fiscalFooter": { + "billNumber": 419, + "uniqueNumber": "EAZ2202160196", + "cashNumber": "Kasa 1", + "cashier": "Kasjer nr 27", + "CPS": 56, + "date": "2026-07-30T17:21:19.000Z" + } + }, + { + "addLine": { + "id": 0, + "data": "
Nr transakcji:4320
", + "width": 80, + "CPS": 56 + } + }, + { + "addLine": { + "id": 30, + "data": "
Numer:5834260730432001
", + "width": 80, + "CPS": 56 + } + }, + { + "addLine": { + "id": 41, + "data": "
Udzielono łącznie opustów                   107,09
", + "width": 80, + "CPS": 56 + } + }, + { + "addLine": { + "id": 41, + "data": "
w tym:
", + "width": 80, + "CPS": 56 + } + }, + { + "addLine": { + "id": 41, + "data": "
  Promocje                                  107,09
", + "width": 80, + "CPS": 56 + } + }, + { + "addLine": { + "id": 6, + "data": "
Numer karty:99001*****936
", + "width": 80, + "CPS": 56 + } + }, + { "barcode": { "id": 5, "data": "MTAwMDA1ODM0NDMyMDI0MDIwMTgxOA==" } }, + { "sysNumber": { "data": "4320", "width": 80, "CPS": 56 } } + ], + "sign": "9884906F1B375741B11083CB694BE25B79F66E001EC24307395DDD98CA3AE1114C0573D08A7DE7320E4152D5FF8AB34402368AAA97C72535F2F6CC439DCAA0A3529BCA76E53EC7217CE7995AE266638C320478CB91D10961CA80013181A2B978030A3691302848AD3F7E2443E8490E939A28312E428920EE795DADF80256E0F3A6BA1ABF28D5FB4D7CB1A860EF29CAC51DEDA13E81B1E1C99AC82E7DCA60C92967123B24AD1645169BF869BA2D8839B2E65E03F10A3F0A8D356D580EBF42CABC2432EE01C0320B35AF8B6DC015B09E943528F38E885157BD9BE0D2502822B830C230B9980819EE974434E3DB58E6431CD8884E38297917F432F521D38BF93849" +} diff --git a/billsplit/test/helpers.dart b/billsplit/test/helpers.dart new file mode 100644 index 0000000..597fc2a --- /dev/null +++ b/billsplit/test/helpers.dart @@ -0,0 +1,78 @@ +import 'dart:io'; + +import 'package:billsplit/state/app_state.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:provider/provider.dart'; + +const _pickerChannel = MethodChannel('miguelruivo.flutter.plugins.filepicker'); +const _pathChannel = MethodChannel('plugins.flutter.io/path_provider'); + +/// Mocks the file_picker method channel. +/// +/// `pickResult`: bytes returned by pickFiles (null → user cancelled). +/// `saveResult`: path returned by saveFile (null → user cancelled). +/// `saveThrows`: saveFile raises a PlatformException. +void mockFilePicker({ + Uint8List? pickResult, + String? pickName, + String? saveResult, + bool saveThrows = false, +}) { + TestWidgetsFlutterBinding.ensureInitialized(); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(_pickerChannel, (call) async { + if (call.method == 'save') { + if (saveThrows) { + throw PlatformException(code: 'boom', message: 'disk full'); + } + return saveResult; + } + // pickFiles ('custom'/'any'/...) + if (pickResult == null) return null; + return >[ + { + 'path': '/tmp/${pickName ?? 'picked.json'}', + 'name': pickName ?? 'picked.json', + 'size': pickResult.length, + 'bytes': pickResult, + }, + ]; + }); +} + +/// Points path_provider's documents directory at [dir]. +void mockPathProvider(Directory dir) { + TestWidgetsFlutterBinding.ensureInitialized(); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(_pathChannel, (call) async => dir.path); +} + +/// Clears both channel mocks. +void clearChannelMocks() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + ..setMockMethodCallHandler(_pickerChannel, null) + ..setMockMethodCallHandler(_pathChannel, null); +} + +/// Creates an [AppState] persisted under a fresh temp directory. +AppState tempState() { + final dir = Directory.systemTemp.createTempSync('billsplit_ui'); + addTearDown(() => dir.deleteSync(recursive: true)); + return AppState(overrideDir: dir)..loaded = true; +} + +/// Pumps [home] inside the app shell with [state] provided. +Future pumpApp(WidgetTester tester, AppState state, Widget home) { + return tester.pumpWidget( + ChangeNotifierProvider.value( + value: state, + child: MaterialApp(home: home), + ), + ); +} + +/// Fires the debounced save timer so tests end with no pending timers. +Future flushSaves(WidgetTester tester) => + tester.pump(const Duration(milliseconds: 450)); diff --git a/billsplit/test/models_test.dart b/billsplit/test/models_test.dart new file mode 100644 index 0000000..0cfbb72 --- /dev/null +++ b/billsplit/test/models_test.dart @@ -0,0 +1,78 @@ +import 'dart:convert'; + +import 'package:billsplit/domain/models.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('formatPln handles zero, cents padding and negatives', () { + expect(formatPln(0), '0,00 zł'); + expect(formatPln(5), '0,05 zł'); + expect(formatPln(123456), '1234,56 zł'); + expect(formatPln(-7), '-0,07 zł'); + }); + + test('Ids are unique and ordered enough', () { + final a = Ids.next(); + final b = Ids.next(); + expect(a, isNot(b)); + }); + + test('Person/Group/Assignment/Item/Receipt JSON roundtrips', () { + final p = Person(id: 'p1', name: 'Ala', drinks: false); + expect(Person.fromJson(p.toJson()).drinks, isFalse); + + final g = Group(id: 'g1', name: 'Meat', memberIds: {'p1'}); + expect(Group.fromJson(g.toJson()).memberIds, {'p1'}); + + final a = Assignment(mode: AssignMode.group, groupId: 'g1'); + final a2 = Assignment.fromJson(a.toJson()); + expect(a2.mode, AssignMode.group); + expect(a2.groupId, 'g1'); + + final i = Item( + id: 'i1', + name: 'Piwo', + totalGr: 500, + qty: 2, + unitPriceGr: 250, + discountGr: -100, + category: Categories.alcohol, + assignment: Assignment(mode: AssignMode.custom, customIds: {'p1'}), + ); + final i2 = Item.fromJson( + jsonDecode(jsonEncode(i.toJson())) as Map, + ); + expect(i2.assignment.customIds, {'p1'}); + expect(i2.discountGr, -100); + + final r = Receipt( + id: 'r1', + title: 't', + store: 's', + items: [i], + participantIds: {'p1'}, + paidTotalGr: 500, + ); + final r2 = Receipt.fromJson( + jsonDecode(jsonEncode(r.toJson())) as Map, + ); + expect(r2.itemsTotalGr, 500); + expect(r2.participantIds, {'p1'}); + expect(r2.store, 's'); + }); + + test('fromJson tolerates missing optional fields', () { + final p = Person.fromJson(const {'id': 'x', 'name': 'n'}); + expect(p.drinks, isTrue); + final g = Group.fromJson(const {'id': 'x', 'name': 'n'}); + expect(g.memberIds, isEmpty); + final a = Assignment.fromJson(const {}); + expect(a.mode, AssignMode.everyone); + final i = Item.fromJson(const {'id': 'x', 'name': 'n', 'totalGr': 1}); + expect(i.qty, 1); + expect(i.category, Categories.general); + final r = Receipt.fromJson(const {'id': 'x', 'title': 't', 'date': 'bad'}); + expect(r.items, isEmpty); + expect(r.paidTotalGr, isNull); + }); +} diff --git a/billsplit/test/split_engine_test.dart b/billsplit/test/split_engine_test.dart new file mode 100644 index 0000000..9451f67 --- /dev/null +++ b/billsplit/test/split_engine_test.dart @@ -0,0 +1,189 @@ +import 'dart:io'; +import 'dart:math'; + +import 'package:billsplit/domain/eparagon_parser.dart'; +import 'package:billsplit/domain/models.dart'; +import 'package:billsplit/domain/split_engine.dart'; +import 'package:flutter_test/flutter_test.dart'; + +List makePeople(int n, {Set nonDrinkers = const {}}) => [ + for (var i = 0; i < n; i++) + Person(id: 'p$i', name: 'P$i', drinks: !nonDrinkers.contains(i)), + ]; + +void main() { + test('splitAmount: exact sum, deterministic, rotation moves the extra', () { + final shares = splitAmount(100, ['b', 'a', 'c']); // rotation 0 + expect(shares.values.reduce((a, b) => a + b), 100); + expect(shares['a'], 34); // sorted order, start at index 0 + expect(shares['b'], 33); + expect(shares['c'], 33); + final rotated = splitAmount(100, ['b', 'a', 'c'], rotation: 1); + expect(rotated.values.reduce((a, b) => a + b), 100); + expect(rotated['b'], 34); // start shifted by one + // Same inputs -> same output (deterministic). + expect(splitAmount(100, ['b', 'a', 'c'], rotation: 1), rotated); + }); + + test('splitAmount: negative totals (returns) also sum exactly', () { + final shares = splitAmount(-100, ['a', 'b', 'c']); + expect(shares.values.reduce((a, b) => a + b), -100); + expect(shares.values.every((v) => v == -33 || v == -34), isTrue); + }); + + test('splitAmount fuzz: sum invariant over random cases', () { + final rnd = Random(42); + for (var i = 0; i < 500; i++) { + final n = 1 + rnd.nextInt(9); + final total = rnd.nextInt(100000) - 5000; + final ids = List.generate(n, (j) => 'p$j'); + final shares = splitAmount(total, ids, rotation: rnd.nextInt(1000)); + expect( + shares.values.fold(0, (a, b) => a + b), + total, + reason: 'total=$total n=$n', + ); + final values = shares.values.toList(); + expect( + values.reduce(max) - values.reduce(min), + lessThanOrEqualTo(1), + reason: 'shares differ by more than 1 grosz', + ); + } + }); + + test('modes resolve correctly and intersect with receipt participants', () { + final people = makePeople(4, nonDrinkers: {3}); + final group = Group(id: 'g1', name: 'Meat', memberIds: {'p0', 'p2'}); + final receipt = Receipt( + id: 'r', + title: 't', + participantIds: {'p0', 'p1', 'p3'}, // p2 not on this receipt + ); + Item mk(Assignment a) => + Item(id: Ids.next(), name: 'x', totalGr: 300, assignment: a); + + expect( + resolveParticipants(mk(Assignment()), receipt, people, [group]) + .map((p) => p.id), + ['p0', 'p1', 'p3'], + ); + expect( + resolveParticipants( + mk(Assignment(mode: AssignMode.drinkers)), + receipt, + people, + [group], + ).map((p) => p.id), + ['p0', 'p1'], + ); + expect( + resolveParticipants( + mk(Assignment(mode: AssignMode.nonDrinkers)), + receipt, + people, + [group], + ).map((p) => p.id), + ['p3'], + ); + // Group intersected with receipt pool: p2 excluded. + expect( + resolveParticipants( + mk(Assignment(mode: AssignMode.group, groupId: 'g1')), + receipt, + people, + [group], + ).map((p) => p.id), + ['p0'], + ); + expect( + resolveParticipants( + mk(Assignment(mode: AssignMode.custom, customIds: {'p1', 'p2'})), + receipt, + people, + [group], + ).map((p) => p.id), + ['p1'], + ); + // Missing group -> unassigned. + expect( + resolveParticipants( + mk(Assignment(mode: AssignMode.group, groupId: 'gone')), + receipt, + people, + [group], + ), + isEmpty, + ); + }); + + test('real receipt: 8 people, all drinkers — reconciles to 597.55', () { + final receipt = parseEParagon( + File('test/fixtures/sample_receipt.json').readAsStringSync(), + ); + final people = makePeople(8); + receipt.participantIds.addAll(people.map((p) => p.id)); + + final split = computeSplit(receipt, people, []); + expect(split.unassigned, isEmpty); + // Exact reconciliation is the hard invariant. + expect(split.assignedTotalGr, 59755); + // 59755 / 8 = 7469.375 -> everyone lands within a few grosze of that; + // remainder rotation spreads the extras instead of stacking them on the + // alphabetically-first person. + final totals = split.perPerson.values.map((t) => t.totalGr).toList(); + for (final t in totals) { + expect(t, inInclusiveRange(7469 - 15, 7470 + 15)); + } + // Alcohol: 79.98 + 28.74 = 108.72 split over 8 -> 13.59 each ±2 gr. + final alcohol = split.perPerson.values.map((t) => t.alcoholGr).toList(); + expect(alcohol.fold(0, (a, b) => a + b), 10872); + for (final a in alcohol) { + expect(a, inInclusiveRange(1357, 1361)); + } + }); + + test('real receipt: 2 non-drinkers pay no alcohol, totals still exact', () { + final receipt = parseEParagon( + File('test/fixtures/sample_receipt.json').readAsStringSync(), + ); + final people = makePeople(8, nonDrinkers: {6, 7}); + receipt.participantIds.addAll(people.map((p) => p.id)); + + final split = computeSplit(receipt, people, []); + expect(split.assignedTotalGr, 59755); + expect(split.perPerson['p6']!.alcoholGr, 0); + expect(split.perPerson['p7']!.alcoholGr, 0); + // 10872 over 6 drinkers = 1812 each ±2 gr, summing exactly. + final drinkerAlcohol = [ + for (var i = 0; i < 6; i++) split.perPerson['p$i']!.alcoholGr, + ]; + expect(drinkerAlcohol.fold(0, (a, b) => a + b), 10872); + for (final a in drinkerAlcohol) { + expect(a, inInclusiveRange(1810, 1814)); + } + // Non-drinkers pay strictly less. + expect( + split.perPerson['p7']!.totalGr, + lessThan(split.perPerson['p0']!.totalGr), + ); + }); + + test('items with no possible participants are reported, not lost', () { + final people = makePeople(2); // both drinkers + final receipt = Receipt(id: 'r', title: 't', participantIds: {'p0', 'p1'}); + receipt.items.add( + Item( + id: 'i1', + name: 'water', + totalGr: 500, + assignment: Assignment(mode: AssignMode.nonDrinkers), + ), + ); + receipt.items.add(Item(id: 'i2', name: 'bread', totalGr: 300)); + + final split = computeSplit(receipt, people, []); + expect(split.unassigned.map((i) => i.id), ['i1']); + expect(split.assignedTotalGr, 300); + }); +} diff --git a/billsplit/test/ui_flows_test.dart b/billsplit/test/ui_flows_test.dart new file mode 100644 index 0000000..1b24602 --- /dev/null +++ b/billsplit/test/ui_flows_test.dart @@ -0,0 +1,536 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:billsplit/domain/models.dart'; +import 'package:billsplit/main.dart' as app; +import 'package:billsplit/state/app_state.dart'; +import 'package:billsplit/ui/home_screen.dart'; +import 'package:billsplit/ui/people_screen.dart'; +import 'package:billsplit/ui/receipt_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'helpers.dart'; + +Receipt demoReceipt(AppState s, {bool withUnassigned = false}) { + final r = Receipt( + id: 'r1', + title: 'Demo', + store: 'Biedronka', + paidTotalGr: 1000, + items: [ + Item(id: 'i1', name: 'Bread', totalGr: 500), + Item( + id: 'i2', + name: 'Beer', + totalGr: 300, + category: Categories.alcohol, + assignment: Assignment(mode: AssignMode.drinkers), + ), + Item( + id: 'i3', + name: 'Tonic', + totalGr: 100, + category: Categories.mixer, + ), + Item( + id: 'i4', + name: 'Kaucja', + totalGr: 100, + category: Categories.deposit, + ), + if (withUnassigned) + Item( + id: 'i5', + name: 'Ghost', + totalGr: 50, + assignment: Assignment(mode: AssignMode.custom), + ), + ], + ); + s.addReceipt(r); + return r; +} + +void main() { + tearDown(clearChannelMocks); + + group('main()', () { + testWidgets('boots with the real load path', (tester) async { + final dir = Directory.systemTemp.createTempSync('billsplit_main'); + addTearDown(() => dir.deleteSync(recursive: true)); + mockPathProvider(dir); + app.main(); + await tester.pump(); + await tester.pump(); + expect(find.text('BillSplit'), findsOneWidget); + }); + }); + + group('home', () { + testWidgets('import cancel does nothing', (tester) async { + mockFilePicker(); + final s = tempState(); + await pumpApp(tester, s, const HomeScreen()); + await tester.tap(find.text('Import e-paragon')); + await tester.pumpAndSettle(); + expect(s.receipts, isEmpty); + }); + + testWidgets('import parse error shows message', (tester) async { + mockFilePicker(pickResult: Uint8List.fromList('zzz'.codeUnits)); + final s = tempState(); + await pumpApp(tester, s, const HomeScreen()); + await tester.tap(find.text('Import e-paragon')); + await tester.pumpAndSettle(); + expect(find.textContaining('neither valid JSON'), findsOneWidget); + await flushSaves(tester); + }); + + testWidgets('import invalid UTF-8 hits the generic handler', + (tester) async { + mockFilePicker(pickResult: Uint8List.fromList([0xFF, 0xFE, 0x00])); + final s = tempState(); + await pumpApp(tester, s, const HomeScreen()); + await tester.tap(find.text('Import e-paragon')); + await tester.pumpAndSettle(); + expect(find.textContaining('Import failed'), findsOneWidget); + }); + + testWidgets('import success navigates; warning shows a snackbar', + (tester) async { + final fixture = + File('test/fixtures/sample_receipt.json').readAsBytesSync(); + mockFilePicker(pickResult: fixture); + final s = tempState()..addPerson('Ala'); + await pumpApp(tester, s, const HomeScreen()); + await tester.tap(find.text('Import e-paragon')); + await tester.pumpAndSettle(); + expect(s.receipts, hasLength(1)); + expect(find.byType(ReceiptScreen), findsOneWidget); + await flushSaves(tester); + }); + + testWidgets('import surfaces parser warnings', (tester) async { + final payload = { + 'dokument': { + 'paragon': { + 'pozycja': [ + { + 'towar': {'nazwa': 'Chleb', 'ilosc': 1, 'brutto': 500}, + }, + ], + 'podsum': {'sumaBrutto': 999}, + }, + }, + }; + final body = base64Url.encode(utf8.encode(jsonEncode(payload))); + final wrapper = jsonEncode({'data': 'h.$body.s'}); + mockFilePicker(pickResult: Uint8List.fromList(utf8.encode(wrapper))); + final s = tempState(); + await pumpApp(tester, s, const HomeScreen()); + await tester.tap(find.text('Import e-paragon')); + await tester.pumpAndSettle(); + expect(find.textContaining('differs'), findsOneWidget); + await flushSaves(tester); + }); + + testWidgets('manual create: cancel, empty and valid', (tester) async { + final s = tempState(); + await pumpApp(tester, s, const HomeScreen()); + + await tester.tap(find.text('New receipt')); + await tester.pumpAndSettle(); + await tester.tap(find.text('Cancel')); + await tester.pumpAndSettle(); + expect(s.receipts, isEmpty); + + await tester.tap(find.text('New receipt')); + await tester.pumpAndSettle(); + await tester.tap(find.text('Create')); + await tester.pumpAndSettle(); + expect(s.receipts, isEmpty); + + await tester.tap(find.text('New receipt')); + await tester.pumpAndSettle(); + await tester.enterText(find.byType(TextField), 'Pizza night'); + await tester.tap(find.text('Create')); + await tester.pumpAndSettle(); + expect(s.receipts.single.title, 'Pizza night'); + expect(find.byType(ReceiptScreen), findsOneWidget); + await tester.pageBack(); + await tester.pumpAndSettle(); + + await tester.tap(find.text('New receipt')); + await tester.pumpAndSettle(); + await tester.enterText(find.byType(TextField), 'Kebab'); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); + expect(s.receipts.first.title, 'Kebab'); + await flushSaves(tester); + }); + + testWidgets('list tile navigates; dismiss asks and deletes', + (tester) async { + final s = tempState(); + demoReceipt(s); + await pumpApp(tester, s, const HomeScreen()); + await flushSaves(tester); + + expect(find.textContaining('4 items'), findsOneWidget); + await tester.tap(find.text('Demo')); + await tester.pumpAndSettle(); + expect(find.byType(ReceiptScreen), findsOneWidget); + await tester.pageBack(); + await tester.pumpAndSettle(); + + await tester.drag(find.byType(Dismissible), const Offset(-600, 0)); + await tester.pumpAndSettle(); + await tester.tap(find.text('Cancel')); + await tester.pumpAndSettle(); + expect(s.receipts, hasLength(1)); + + await tester.drag(find.byType(Dismissible), const Offset(-600, 0)); + await tester.pumpAndSettle(); + await tester.tap(find.text('Delete')); + await tester.pumpAndSettle(); + expect(s.receipts, isEmpty); + await flushSaves(tester); + }); + + testWidgets('people button opens roster', (tester) async { + final s = tempState(); + await pumpApp(tester, s, const HomeScreen()); + await tester.tap(find.byIcon(Icons.group)); + await tester.pumpAndSettle(); + expect(find.byType(PeopleScreen), findsOneWidget); + }); + }); + + group('people', () { + testWidgets('add person dialog: toggle, cancel, empty, valid', + (tester) async { + final s = tempState(); + await pumpApp(tester, s, const PeopleScreen()); + expect(find.textContaining('Nobody yet'), findsOneWidget); + + await tester.tap(find.text('Add person')); + await tester.pumpAndSettle(); + await tester.tap(find.byType(Switch)); + await tester.pumpAndSettle(); + await tester.tap(find.text('Cancel')); + await tester.pumpAndSettle(); + expect(s.people, isEmpty); + + await tester.tap(find.text('Add person')); + await tester.pumpAndSettle(); + await tester.tap(find.text('Add')); + await tester.pumpAndSettle(); + expect(s.people, isEmpty); + + await tester.tap(find.text('Add person')); + await tester.pumpAndSettle(); + await tester.enterText(find.byType(TextField), 'Kasia'); + await tester.tap(find.byType(Switch)); + await tester.pumpAndSettle(); + await tester.tap(find.text('Add')); + await tester.pumpAndSettle(); + expect(s.people.single.name, 'Kasia'); + expect(s.people.single.drinks, isFalse); + await flushSaves(tester); + }); + + testWidgets('drinks switch, membership early-return, delete flows', + (tester) async { + final s = tempState(); + final p = s.addPerson('Ala'); + await pumpApp(tester, s, const PeopleScreen()); + await flushSaves(tester); + + await tester.tap(find.byType(Switch).first); + await tester.pumpAndSettle(); + expect(p.drinks, isFalse); + + // No groups yet → tapping the person is a no-op branch. + await tester.tap(find.text('Ala')); + await tester.pumpAndSettle(); + expect(find.text('Group membership'), findsNothing); + + await tester.tap(find.byIcon(Icons.delete_outline).first); + await tester.pumpAndSettle(); + await tester.tap(find.text('Cancel')); + await tester.pumpAndSettle(); + expect(s.people, hasLength(1)); + + await tester.tap(find.byIcon(Icons.delete_outline).first); + await tester.pumpAndSettle(); + await tester.tap(find.text('Remove')); + await tester.pumpAndSettle(); + expect(s.people, isEmpty); + await flushSaves(tester); + }); + + testWidgets('groups: add, membership sheet, delete', (tester) async { + final s = tempState()..addPerson('Ala'); + await pumpApp(tester, s, const PeopleScreen()); + await flushSaves(tester); + + await tester.tap(find.text('Add group')); + await tester.pumpAndSettle(); + await tester.tap(find.text('Cancel')); + await tester.pumpAndSettle(); + expect(s.groups, isEmpty); + + await tester.tap(find.text('Add group')); + await tester.pumpAndSettle(); + await tester.enterText(find.byType(TextField), 'Meat'); + await tester.tap(find.text('Add')); + await tester.pumpAndSettle(); + expect(s.groups.single.name, 'Meat'); + expect(find.textContaining('No members'), findsOneWidget); + + await tester.tap(find.text('Ala')); + await tester.pumpAndSettle(); + expect(find.byType(CheckboxListTile), findsOneWidget); + await tester.tap(find.byType(CheckboxListTile)); + await tester.pumpAndSettle(); + expect(s.groups.single.memberIds, hasLength(1)); + await tester.tap(find.byType(CheckboxListTile)); + await tester.pumpAndSettle(); + expect(s.groups.single.memberIds, isEmpty); + await tester.tapAt(const Offset(10, 10)); // close sheet + await tester.pumpAndSettle(); + + await tester.tap(find.byIcon(Icons.delete_outline).last); + await tester.pumpAndSettle(); + expect(s.groups, isEmpty); + await flushSaves(tester); + }); + }); + + group('receipt screen', () { + testWidgets('unknown id shows deleted note', (tester) async { + final s = tempState(); + await pumpApp(tester, s, const ReceiptScreen(receiptId: 'nope')); + expect(find.text('Receipt deleted.'), findsOneWidget); + }); + + testWidgets('tabs on narrow, panes on wide', (tester) async { + final s = tempState()..addPerson('Ala'); + demoReceipt(s); + await pumpApp(tester, s, const ReceiptScreen(receiptId: 'r1')); + await flushSaves(tester); + expect(find.byType(TabBar), findsOneWidget); + await tester.tap(find.text('Totals')); + await tester.pumpAndSettle(); + expect(find.textContaining('grosz-exact'), findsOneWidget); + + tester.view.physicalSize = const Size(1600, 900); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.reset); + await tester.pumpAndSettle(); + expect(find.byType(TabBar), findsNothing); + expect(find.byType(VerticalDivider), findsOneWidget); + }); + + testWidgets('participants: empty roster snackbar, then editing', + (tester) async { + final s = tempState(); + demoReceipt(s); + await pumpApp(tester, s, const ReceiptScreen(receiptId: 'r1')); + await flushSaves(tester); + await tester.tap(find.byIcon(Icons.group)); + await tester.pumpAndSettle(); + expect(find.textContaining('Add people first'), findsOneWidget); + + final p = s.addPerson('Ala'); + s.addPerson('Bob'); + await tester.pumpAndSettle(); + await tester.tap(find.byIcon(Icons.group)); + await tester.pumpAndSettle(); + expect(find.text('Who is on this receipt?'), findsOneWidget); + await tester.tap(find.byType(CheckboxListTile).first); + await tester.pumpAndSettle(); + expect(s.receipts.single.participantIds.contains(p.id), isTrue); + await tester.tap(find.byType(CheckboxListTile).first); + await tester.pumpAndSettle(); + await tester.tapAt(const Offset(10, 10)); + await tester.pumpAndSettle(); + await flushSaves(tester); + }); + + testWidgets('export: no participants, success, cancel, failure', + (tester) async { + final s = tempState(); + demoReceipt(s); + await pumpApp(tester, s, const ReceiptScreen(receiptId: 'r1')); + await flushSaves(tester); + + await tester.tap(find.byIcon(Icons.table_view)); + await tester.pumpAndSettle(); + expect(find.textContaining('Add participants'), findsOneWidget); + + s.addPerson('Ala'); + s.receipts.single.participantIds.addAll(s.people.map((p) => p.id)); + mockFilePicker(saveResult: '/tmp/out.xlsx'); + await tester.pump(const Duration(seconds: 5)); // expire prior snackbar + await tester.pumpAndSettle(); + await tester.tap(find.byIcon(Icons.table_view)); + await tester.pumpAndSettle(); + expect(find.textContaining('Saved: out.xlsx'), findsOneWidget); + + mockFilePicker(); + await tester.pump(const Duration(seconds: 5)); + await tester.pumpAndSettle(); + await tester.tap(find.byIcon(Icons.table_view)); + await tester.pumpAndSettle(); + + mockFilePicker(saveThrows: true); + await tester.pump(const Duration(seconds: 5)); + await tester.pumpAndSettle(); + await tester.tap(find.byIcon(Icons.table_view)); + await tester.pumpAndSettle(); + expect(find.textContaining('Export failed'), findsOneWidget); + await flushSaves(tester); + }); + + testWidgets('items list: labels, unassigned, edit dialog paths', + (tester) async { + final s = tempState(); + final p = s.addPerson('Ala'); + s.addPerson('Bob', drinks: false); + final g = s.addGroup('Meat')..memberIds.add(p.id); + final r = demoReceipt(s, withUnassigned: true); + r.items[0].assignment = Assignment(mode: AssignMode.group, groupId: g.id); + r.items[2].assignment = Assignment(mode: AssignMode.nonDrinkers); + r.items[3].assignment = Assignment( + mode: AssignMode.group, + groupId: 'gone', + ); + await pumpApp(tester, s, const ReceiptScreen(receiptId: 'r1')); + await flushSaves(tester); + + expect(find.textContaining('Group: Meat'), findsOneWidget); + expect(find.textContaining('Group: ?'), findsOneWidget); + expect(find.textContaining('Drinkers'), findsOneWidget); + expect(find.textContaining('Non-drinkers'), findsOneWidget); + expect(find.textContaining('UNASSIGNED'), findsOneWidget); + + await tester.tap(find.text('Totals')); + await tester.pumpAndSettle(); + expect(find.textContaining('excluded from totals'), findsOneWidget); + await tester.tap(find.text('Items')); + await tester.pumpAndSettle(); + + // Edit via long-press: invalid total keeps dialog open, then fix+save. + await tester.longPress(find.text('Bread')); + await tester.pumpAndSettle(); + await tester.enterText( + find.widgetWithText(TextField, 'Total (zł)'), + 'abc', + ); + await tester.tap(find.text('Save')); + await tester.pumpAndSettle(); + expect(find.text('Save'), findsOneWidget); // still open + await tester.enterText( + find.widgetWithText(TextField, 'Total (zł)'), + '6,00', + ); + await tester.enterText(find.widgetWithText(TextField, 'Name'), 'Rolls'); + await tester.tap(find.text('Save')); + await tester.pumpAndSettle(); + expect(r.items[0].totalGr, 600); + expect(r.items[0].name, 'Rolls'); + + // Delete an item from the edit dialog. + await tester.longPress(find.text('Ghost')); + await tester.pumpAndSettle(); + await tester.tap(find.text('Delete')); + await tester.pumpAndSettle(); + expect(r.items.any((i) => i.name == 'Ghost'), isFalse); + + // Add a new alcohol item via the FAB: cancel first, then create. + await tester.tap(find.byType(FloatingActionButton)); + await tester.pumpAndSettle(); + await tester.tap(find.text('Cancel')); + await tester.pumpAndSettle(); + + await tester.tap(find.byType(FloatingActionButton)); + await tester.pumpAndSettle(); + await tester.enterText(find.widgetWithText(TextField, 'Name'), 'Wine'); + await tester.enterText( + find.widgetWithText(TextField, 'Total (zł)'), + '30', + ); + await tester.tap(find.byType(DropdownButtonFormField)); + await tester.pumpAndSettle(); + await tester.tap(find.text('Alcohol').last); + await tester.pumpAndSettle(); + await tester.tap(find.text('Save')); + await tester.pumpAndSettle(); + final wine = r.items.singleWhere((i) => i.name == 'Wine'); + expect(wine.totalGr, 3000); + expect(wine.assignment.mode, AssignMode.drinkers); + + await tester.tap(find.byType(FloatingActionButton)); + await tester.pumpAndSettle(); + await tester.enterText(find.widgetWithText(TextField, 'Name'), 'Chips'); + await tester.enterText( + find.widgetWithText(TextField, 'Total (zł)'), + '5', + ); + await tester.tap(find.text('Save')); + await tester.pumpAndSettle(); + expect( + r.items.singleWhere((i) => i.name == 'Chips').assignment.mode, + AssignMode.everyone, + ); + await flushSaves(tester); + }); + + testWidgets('assignment sheet: modes, materialization, nobody', + (tester) async { + final s = tempState(); + final ala = s.addPerson('Ala'); + s.addPerson('Bob'); + final g = s.addGroup('Meat')..memberIds.add(ala.id); + final r = demoReceipt(s); + await pumpApp(tester, s, const ReceiptScreen(receiptId: 'r1')); + await flushSaves(tester); + + await tester.tap(find.text('Bread')); + await tester.pumpAndSettle(); + final item = r.items[0]; + + await tester.tap(find.widgetWithText(ChoiceChip, 'Drinkers')); + await tester.pumpAndSettle(); + expect(item.assignment.mode, AssignMode.drinkers); + await tester.tap(find.widgetWithText(ChoiceChip, 'Non-drinkers')); + await tester.pumpAndSettle(); + expect(item.assignment.mode, AssignMode.nonDrinkers); + expect(find.textContaining('Nobody selected'), findsOneWidget); + await tester.tap(find.widgetWithText(ChoiceChip, 'Meat')); + await tester.pumpAndSettle(); + expect(item.assignment.mode, AssignMode.group); + expect(item.assignment.groupId, g.id); + await tester.tap(find.widgetWithText(ChoiceChip, 'Everyone')); + await tester.pumpAndSettle(); + expect(item.assignment.mode, AssignMode.everyone); + + // Toggling a person on a symbolic mode materializes to custom. + await tester.tap(find.byType(CheckboxListTile).first); + await tester.pumpAndSettle(); + expect(item.assignment.mode, AssignMode.custom); + expect(item.assignment.customIds, hasLength(1)); + await tester.tap(find.byType(CheckboxListTile).last); + await tester.pumpAndSettle(); + expect(item.assignment.customIds, isEmpty); + expect(find.textContaining('Nobody selected'), findsOneWidget); + await tester.tap(find.byType(CheckboxListTile).first); + await tester.pumpAndSettle(); + expect(item.assignment.customIds, hasLength(1)); + await flushSaves(tester); + }); + }); +} diff --git a/billsplit/test/widget_test.dart b/billsplit/test/widget_test.dart new file mode 100644 index 0000000..e37c0ab --- /dev/null +++ b/billsplit/test/widget_test.dart @@ -0,0 +1,16 @@ +import 'package:billsplit/main.dart'; +import 'package:billsplit/state/app_state.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('app builds and shows empty state', (tester) async { + // Real file IO never completes inside the fake-async test zone, so skip + // load() and mark the state ready directly; this is a pure widget smoke + // test (persistence is exercised by the domain tests / at runtime). + final state = AppState()..loaded = true; + await tester.pumpWidget(BillSplitApp(state: state)); + await tester.pump(); + expect(find.text('BillSplit'), findsOneWidget); + expect(find.textContaining('No receipts yet'), findsOneWidget); + }); +} diff --git a/billsplit/test/xlsx_export_test.dart b/billsplit/test/xlsx_export_test.dart new file mode 100644 index 0000000..f9be9de --- /dev/null +++ b/billsplit/test/xlsx_export_test.dart @@ -0,0 +1,31 @@ +import 'dart:io'; + +import 'package:billsplit/domain/eparagon_parser.dart'; +import 'package:billsplit/domain/models.dart'; +import 'package:billsplit/domain/xlsx_export.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('exports a non-trivial xlsx for the real receipt', () { + final receipt = parseEParagon( + File('test/fixtures/sample_receipt.json').readAsStringSync(), + ); + final people = [ + for (var i = 0; i < 8; i++) + Person(id: 'p$i', name: 'Person ${i + 1}', drinks: i < 6), + ]; + receipt.participantIds.addAll(people.map((p) => p.id)); + + final bytes = exportXlsx(receipt: receipt, roster: people, groups: []); + expect(bytes.length, greaterThan(4000)); + // PK zip magic. + expect(bytes[0], 0x50); + expect(bytes[1], 0x4B); + + // Write out for external validation (recalc via LibreOffice + openpyxl). + final out = File('build/test_export.xlsx') + ..createSync(recursive: true) + ..writeAsBytesSync(bytes); + expect(out.existsSync(), isTrue); + }); +} diff --git a/billsplit/test/xlsx_extra_test.dart b/billsplit/test/xlsx_extra_test.dart new file mode 100644 index 0000000..9d5516f --- /dev/null +++ b/billsplit/test/xlsx_extra_test.dart @@ -0,0 +1,36 @@ +import 'package:billsplit/domain/models.dart'; +import 'package:billsplit/domain/xlsx_export.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('export handles no store/paid, unassigned items and non-drinkers', () { + final people = [ + Person(id: 'a', name: 'Ala'), + Person(id: 'b', name: 'Bob', drinks: false), + ]; + final receipt = Receipt( + id: 'r', + title: 'Manual', + participantIds: {'a', 'b'}, + items: [ + Item(id: 'i1', name: 'Bread', totalGr: 500), + Item( + id: 'i2', + name: 'Beer', + totalGr: 300, + category: Categories.alcohol, + assignment: Assignment(mode: AssignMode.drinkers), + ), + Item( + id: 'i3', + name: 'Ghost', + totalGr: 100, + assignment: Assignment(mode: AssignMode.custom), + ), + ], + ); + final bytes = exportXlsx(receipt: receipt, roster: people, groups: []); + expect(bytes.length, greaterThan(1000)); + expect(bytes[0], 0x50); + }); +} diff --git a/billsplit/windows/.gitignore b/billsplit/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/billsplit/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/billsplit/windows/CMakeLists.txt b/billsplit/windows/CMakeLists.txt new file mode 100644 index 0000000..bd40246 --- /dev/null +++ b/billsplit/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(billsplit LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "billsplit") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/billsplit/windows/flutter/CMakeLists.txt b/billsplit/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/billsplit/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/billsplit/windows/flutter/generated_plugin_registrant.cc b/billsplit/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..8b6d468 --- /dev/null +++ b/billsplit/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void RegisterPlugins(flutter::PluginRegistry* registry) { +} diff --git a/billsplit/windows/flutter/generated_plugin_registrant.h b/billsplit/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/billsplit/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/billsplit/windows/flutter/generated_plugins.cmake b/billsplit/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..3ad69c6 --- /dev/null +++ b/billsplit/windows/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/billsplit/windows/runner/CMakeLists.txt b/billsplit/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/billsplit/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/billsplit/windows/runner/Runner.rc b/billsplit/windows/runner/Runner.rc new file mode 100644 index 0000000..9f9b0e5 --- /dev/null +++ b/billsplit/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "dev.kuhy" "\0" + VALUE "FileDescription", "billsplit" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "billsplit" "\0" + VALUE "LegalCopyright", "Copyright (C) 2026 dev.kuhy. All rights reserved." "\0" + VALUE "OriginalFilename", "billsplit.exe" "\0" + VALUE "ProductName", "billsplit" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/billsplit/windows/runner/flutter_window.cpp b/billsplit/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..955ee30 --- /dev/null +++ b/billsplit/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/billsplit/windows/runner/flutter_window.h b/billsplit/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/billsplit/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/billsplit/windows/runner/main.cpp b/billsplit/windows/runner/main.cpp new file mode 100644 index 0000000..4dc011e --- /dev/null +++ b/billsplit/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"billsplit", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/billsplit/windows/runner/resource.h b/billsplit/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/billsplit/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/billsplit/windows/runner/resources/app_icon.ico b/billsplit/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c04e20caf6370ebb9253ad831cc31de4a9c965f6 GIT binary patch literal 33772 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 literal 0 HcmV?d00001 diff --git a/billsplit/windows/runner/runner.exe.manifest b/billsplit/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..153653e --- /dev/null +++ b/billsplit/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/billsplit/windows/runner/utils.cpp b/billsplit/windows/runner/utils.cpp new file mode 100644 index 0000000..3cb7146 --- /dev/null +++ b/billsplit/windows/runner/utils.cpp @@ -0,0 +1,69 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + // First, find the length of the string with a safe upper bound (CWE-126). + // UNICODE_STRING_MAX_CHARS (32767) is the maximum length of a UNICODE_STRING. + int input_length = static_cast(wcsnlen(utf16_string, UNICODE_STRING_MAX_CHARS)); + // Now use that bounded length to determine the required buffer size. + // When an explicit length is passed, WideCharToMultiByte does not include + // the null terminator in its returned size. + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, nullptr, 0, nullptr, nullptr); + std::string utf8_string; + if (target_length == 0 || static_cast(target_length) > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/billsplit/windows/runner/utils.h b/billsplit/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/billsplit/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/billsplit/windows/runner/win32_window.cpp b/billsplit/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/billsplit/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/billsplit/windows/runner/win32_window.h b/billsplit/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/billsplit/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/python_pkg/billsplit_coverage/__init__.py b/python_pkg/billsplit_coverage/__init__.py new file mode 100644 index 0000000..3f7e006 --- /dev/null +++ b/python_pkg/billsplit_coverage/__init__.py @@ -0,0 +1,13 @@ +"""Coverage gate for the ``billsplit/`` Flutter app. + +``flutter test --coverage`` reports success while leaving whole files untouched, +so this package re-reads the lcov report it produces and fails unless every +``lib/`` file is present *and* fully covered. It lives here rather than beside +the app because every Python file in this repository belongs under +``python_pkg/``; the app itself holds only Dart. + +Usage: + PYTHONPATH=~/testsAndMisc python3 -m python_pkg.billsplit_coverage +""" + +from __future__ import annotations diff --git a/python_pkg/billsplit_coverage/__main__.py b/python_pkg/billsplit_coverage/__main__.py new file mode 100644 index 0000000..9e95907 --- /dev/null +++ b/python_pkg/billsplit_coverage/__main__.py @@ -0,0 +1,14 @@ +"""Module entry point for the billsplit coverage gate. + +Usage: + PYTHONPATH=~/testsAndMisc python3 -m python_pkg.billsplit_coverage +""" + +from __future__ import annotations + +import sys + +from python_pkg.billsplit_coverage.checker import main + +if __name__ == "__main__": + sys.exit(main()) diff --git a/python_pkg/billsplit_coverage/checker.py b/python_pkg/billsplit_coverage/checker.py new file mode 100644 index 0000000..d076671 --- /dev/null +++ b/python_pkg/billsplit_coverage/checker.py @@ -0,0 +1,130 @@ +"""Fail unless lcov reports 100% line coverage for every ``billsplit/lib`` file. + +Two separate checks, because they catch different mistakes: a file present in +the report but with unhit lines, and a file missing from the report entirely. +The second is the one that catches a newly added Dart file nothing imports — +partial coverage is visible in the report, absence is not. + +The default project root is resolved from this file's location rather than the +working directory, so the CI job can invoke the module from anywhere in the +repository. +""" + +from __future__ import annotations + +import argparse +import pathlib +import sys +from typing import Final + +# python_pkg/billsplit_coverage/checker.py -> repo root -> billsplit/ +_DEFAULT_PROJECT: Final[pathlib.Path] = ( + pathlib.Path(__file__).resolve().parents[2] / "billsplit" +) + +EXIT_INCOMPLETE: Final[int] = 1 +EXIT_NO_REPORT: Final[int] = 2 + + +def parse_lcov(lcov: pathlib.Path) -> dict[str, list[int]]: + """Map each file in an lcov report to its uncovered line numbers. + + Parameters: + lcov (pathlib.Path): Path to an ``lcov.info`` report. + + Returns: + dict[str, list[int]]: Source path to the lines with a zero hit count. A + fully covered file maps to an empty list, which is what lets the caller + tell "covered completely" apart from "absent from the report". + """ + uncovered: dict[str, list[int]] = {} + current: str | None = None + for line in lcov.read_text(encoding="utf-8").splitlines(): + if line.startswith("SF:"): + # lcov quotes the path verbatim; normalise Windows separators. + current = line[3:].replace("\\", "/") + uncovered.setdefault(current, []) + elif line.startswith("DA:") and current is not None: + number, hits = line[3:].split(",") + if int(hits) == 0: + uncovered[current].append(int(number)) + return uncovered + + +def find_failures( + uncovered: dict[str, list[int]], + project: pathlib.Path, +) -> list[str]: + """Describe every coverage gap, as lines ready to print. + + Parameters: + uncovered (dict[str, list[int]]): Output of :func:`parse_lcov`. + project (pathlib.Path): Flutter project root holding ``lib/``. + + Returns: + list[str]: One message per gap; empty when coverage is complete. + """ + failures = [ + f"{path}: uncovered lines {misses}" + for path, misses in sorted(uncovered.items()) + if misses + ] + lib_files = { + str(path.relative_to(project)).replace("\\", "/") + for path in (project / "lib").rglob("*.dart") + } + failures.extend( + f"{path}: not executed by any test (0% coverage)" + for path in sorted(lib_files - set(uncovered)) + ) + return failures + + +def check(project: pathlib.Path) -> int: + """Run the gate for one Flutter project and report to stdout/stderr. + + Parameters: + project (pathlib.Path): Flutter project root, i.e. the directory holding + ``lib/`` and ``coverage/lcov.info``. + + Returns: + int: 0 when coverage is complete, :data:`EXIT_INCOMPLETE` when it is not, + :data:`EXIT_NO_REPORT` when the lcov report is missing. + """ + lcov = project / "coverage" / "lcov.info" + if not lcov.exists(): + sys.stderr.write(f"{lcov} missing — run: flutter test --coverage\n") + return EXIT_NO_REPORT + + uncovered = parse_lcov(lcov) + failures = find_failures(uncovered, project) + if failures: + sys.stderr.write("COVERAGE < 100%:\n") + for failure in failures: + sys.stderr.write(f" {failure}\n") + return EXIT_INCOMPLETE + + sys.stdout.write(f"100% line coverage across {len(uncovered)} lib files.\n") + return 0 + + +def main(argv: list[str] | None = None) -> int: + """Parse arguments and run the gate. + + Parameters: + argv (list[str] | None): Argument list, or None to read ``sys.argv``. + + Returns: + int: Process exit status. + """ + parser = argparse.ArgumentParser( + description="Fail unless billsplit has 100% line coverage.", + ) + parser.add_argument( + "--project", + type=pathlib.Path, + default=_DEFAULT_PROJECT, + help="Flutter project root (defaults to the billsplit/ app in this repo)", + ) + args = parser.parse_args(argv) + return check(args.project) diff --git a/python_pkg/billsplit_coverage/tests/__init__.py b/python_pkg/billsplit_coverage/tests/__init__.py new file mode 100644 index 0000000..b20c235 --- /dev/null +++ b/python_pkg/billsplit_coverage/tests/__init__.py @@ -0,0 +1,3 @@ +"""Tests for the billsplit coverage gate.""" + +from __future__ import annotations diff --git a/python_pkg/billsplit_coverage/tests/test_checker.py b/python_pkg/billsplit_coverage/tests/test_checker.py new file mode 100644 index 0000000..cba107f --- /dev/null +++ b/python_pkg/billsplit_coverage/tests/test_checker.py @@ -0,0 +1,191 @@ +"""Tests for the billsplit coverage gate.""" + +from __future__ import annotations + +import runpy +import sys +from typing import TYPE_CHECKING +from unittest.mock import MagicMock, patch + +import pytest + +from python_pkg.billsplit_coverage import checker + +if TYPE_CHECKING: + import pathlib + + +def make_project( + root: pathlib.Path, + lcov: str | None = None, + dart_files: tuple[str, ...] = ("lib/main.dart",), +) -> pathlib.Path: + """Build a throwaway Flutter project tree. + + Parameters: + root (pathlib.Path): Directory to build the project in. + lcov (str | None): Contents of ``coverage/lcov.info``; None writes no + report at all, which is the missing-report case. + dart_files (tuple[str, ...]): Project-relative Dart files to create. + + Returns: + pathlib.Path: The project root. + """ + for relative in dart_files: + path = root / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("void main() {}\n", encoding="utf-8") + if lcov is not None: + report = root / "coverage" / "lcov.info" + report.parent.mkdir(parents=True, exist_ok=True) + report.write_text(lcov, encoding="utf-8") + return root + + +class TestParseLcov: + def test_records_only_unhit_lines(self, tmp_path: pathlib.Path) -> None: + report = tmp_path / "lcov.info" + report.write_text( + "SF:lib/main.dart\nDA:1,3\nDA:2,0\nDA:9,0\nend_of_record\n", + encoding="utf-8", + ) + assert checker.parse_lcov(report) == {"lib/main.dart": [2, 9]} + + def test_fully_covered_file_maps_to_empty_list( + self, tmp_path: pathlib.Path + ) -> None: + """Present-and-covered must be distinguishable from absent.""" + report = tmp_path / "lcov.info" + report.write_text("SF:lib/a.dart\nDA:1,1\nend_of_record\n", encoding="utf-8") + assert checker.parse_lcov(report) == {"lib/a.dart": []} + + def test_normalises_windows_separators(self, tmp_path: pathlib.Path) -> None: + report = tmp_path / "lcov.info" + report.write_text("SF:lib\\ui\\home.dart\nDA:1,1\n", encoding="utf-8") + assert "lib/ui/home.dart" in checker.parse_lcov(report) + + def test_da_before_any_sf_is_ignored(self, tmp_path: pathlib.Path) -> None: + """A DA: line with no preceding SF: has no file to attribute to.""" + report = tmp_path / "lcov.info" + report.write_text("DA:1,0\nSF:lib/a.dart\nDA:2,0\n", encoding="utf-8") + assert checker.parse_lcov(report) == {"lib/a.dart": [2]} + + def test_unrelated_lines_are_skipped(self, tmp_path: pathlib.Path) -> None: + report = tmp_path / "lcov.info" + report.write_text( + "TN:\nSF:lib/a.dart\nLF:2\nLH:1\nDA:1,0\nend_of_record\n", + encoding="utf-8", + ) + assert checker.parse_lcov(report) == {"lib/a.dart": [1]} + + def test_empty_report_yields_nothing(self, tmp_path: pathlib.Path) -> None: + report = tmp_path / "lcov.info" + report.write_text("", encoding="utf-8") + assert checker.parse_lcov(report) == {} + + +class TestFindFailures: + def test_no_gaps_returns_empty(self, tmp_path: pathlib.Path) -> None: + project = make_project(tmp_path, lcov="") + assert checker.find_failures({"lib/main.dart": []}, project) == [] + + def test_reports_uncovered_lines(self, tmp_path: pathlib.Path) -> None: + project = make_project(tmp_path, lcov="") + failures = checker.find_failures({"lib/main.dart": [2, 7]}, project) + assert failures == ["lib/main.dart: uncovered lines [2, 7]"] + + def test_reports_file_absent_from_report(self, tmp_path: pathlib.Path) -> None: + project = make_project( + tmp_path, lcov="", dart_files=("lib/main.dart", "lib/orphan.dart") + ) + failures = checker.find_failures({"lib/main.dart": []}, project) + assert failures == ["lib/orphan.dart: not executed by any test (0% coverage)"] + + def test_reports_both_kinds_together(self, tmp_path: pathlib.Path) -> None: + project = make_project( + tmp_path, lcov="", dart_files=("lib/main.dart", "lib/orphan.dart") + ) + failures = checker.find_failures({"lib/main.dart": [4]}, project) + assert failures == [ + "lib/main.dart: uncovered lines [4]", + "lib/orphan.dart: not executed by any test (0% coverage)", + ] + + def test_finds_dart_files_in_subdirectories(self, tmp_path: pathlib.Path) -> None: + project = make_project( + tmp_path, lcov="", dart_files=("lib/domain/split_engine.dart",) + ) + failures = checker.find_failures({}, project) + assert failures == [ + "lib/domain/split_engine.dart: not executed by any test (0% coverage)" + ] + + +class TestCheck: + def test_missing_report_exits_two( + self, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture[str] + ) -> None: + project = make_project(tmp_path) + assert checker.check(project) == checker.EXIT_NO_REPORT + assert "flutter test --coverage" in capsys.readouterr().err + + def test_incomplete_coverage_exits_one_and_names_files( + self, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture[str] + ) -> None: + project = make_project( + tmp_path, + lcov="SF:lib/main.dart\nDA:1,1\nDA:2,0\n", + dart_files=("lib/main.dart", "lib/orphan.dart"), + ) + assert checker.check(project) == checker.EXIT_INCOMPLETE + err = capsys.readouterr().err + assert "COVERAGE < 100%:" in err + assert "lib/main.dart: uncovered lines [2]" in err + assert "lib/orphan.dart: not executed by any test" in err + + def test_full_coverage_exits_zero( + self, tmp_path: pathlib.Path, capsys: pytest.CaptureFixture[str] + ) -> None: + project = make_project(tmp_path, lcov="SF:lib/main.dart\nDA:1,1\n") + assert checker.check(project) == 0 + assert "100% line coverage across 1 lib files." in capsys.readouterr().out + + +class TestMain: + def test_project_flag_selects_the_tree(self, tmp_path: pathlib.Path) -> None: + project = make_project(tmp_path, lcov="SF:lib/main.dart\nDA:1,1\n") + assert checker.main(["--project", str(project)]) == 0 + + def test_defaults_to_the_repo_billsplit_app(self) -> None: + """With no --project, the gate resolves billsplit/ from this file.""" + with patch.object(checker, "check", return_value=0) as mock_check: + assert checker.main([]) == 0 + assert mock_check.call_args[0][0].name == "billsplit" + + def test_propagates_failure_status(self, tmp_path: pathlib.Path) -> None: + project = make_project(tmp_path) + assert checker.main(["--project", str(project)]) == checker.EXIT_NO_REPORT + + +class TestModuleEntry: + def test_main_called_when_run_as_module(self) -> None: + """python3 -m python_pkg.billsplit_coverage runs main() and exits.""" + mock_main = MagicMock(return_value=0) + # runpy warns if __main__ is already imported, and the suite may run in + # a random order, so another test could have imported it first. + sys.modules.pop("python_pkg.billsplit_coverage.__main__", None) + with ( + patch("python_pkg.billsplit_coverage.checker.main", mock_main), + pytest.raises(SystemExit) as excinfo, + ): + runpy.run_module("python_pkg.billsplit_coverage", run_name="__main__") + assert excinfo.value.code == 0 + mock_main.assert_called_once() + + def test_main_not_called_on_plain_import(self) -> None: + """Importing __main__ must not run the gate as a side effect.""" + mock_main = MagicMock() + with patch("python_pkg.billsplit_coverage.checker.main", mock_main): + sys.modules.pop("python_pkg.billsplit_coverage.__main__", None) + runpy.run_module("python_pkg.billsplit_coverage.__main__") + mock_main.assert_not_called()