├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature-request.yml │ └── user-experience.yml ├── PULL_REQUEST_TEMPLATE.md ├── issue-label-bot.yaml ├── no-response.yaml ├── settings.yaml ├── stale.yaml └── workflows │ ├── deploy-to-playstore.yaml │ ├── github-page.yaml │ ├── release-candidate.yaml │ ├── release.yaml │ └── test.yaml ├── .gitignore ├── .vscode ├── arb.schema.json ├── launch.json └── settings.json ├── LICENSE ├── Makefile ├── Pipfile ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── .project ├── .settings │ └── org.eclipse.buildship.core.prefs ├── Gemfile ├── README.md ├── app │ ├── .classpath │ ├── .project │ ├── .settings │ │ └── org.eclipse.buildship.core.prefs │ ├── build.gradle │ └── src │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── evanlu │ │ │ │ └── possystem │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-hdpi │ │ │ └── splash.png │ │ │ ├── drawable-mdpi │ │ │ └── splash.png │ │ │ ├── drawable-v21 │ │ │ ├── background.png │ │ │ ├── ic_launcher_background.xml │ │ │ ├── ic_launcher_foreground.xml │ │ │ └── launch_background.xml │ │ │ ├── drawable-xhdpi │ │ │ └── splash.png │ │ │ ├── drawable-xxhdpi │ │ │ └── splash.png │ │ │ ├── drawable-xxxhdpi │ │ │ └── splash.png │ │ │ ├── drawable │ │ │ ├── background.png │ │ │ └── launch_background.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── values-b+zh │ │ │ └── strings.xml │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ ├── values-v31 │ │ │ └── styles.xml │ │ │ └── values │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── fastlane │ ├── Appfile │ ├── Fastfile │ ├── README.md │ ├── metadata │ │ └── android │ │ │ ├── en-US │ │ │ ├── changelogs │ │ │ │ ├── 20900007.txt │ │ │ │ ├── 20901004.txt │ │ │ │ ├── 21000003.txt │ │ │ │ ├── 21001002.txt │ │ │ │ ├── 21002004.txt │ │ │ │ ├── 21003003.txt │ │ │ │ ├── 21004002.txt │ │ │ │ ├── 21005002.txt │ │ │ │ └── default.txt │ │ │ ├── full_description.txt │ │ │ ├── images │ │ │ │ ├── featureGraphic.jpeg │ │ │ │ ├── icon.png │ │ │ │ └── phoneScreenshots │ │ │ │ │ ├── 1_analysis_chart.png │ │ │ │ │ ├── 2_analysis_chart_dark.png │ │ │ │ │ ├── 3_menu_product.png │ │ │ │ │ ├── 4_order_action.png │ │ │ │ │ ├── 5_order_details.png │ │ │ │ │ ├── 6_more.png │ │ │ │ │ ├── 7_inventory.png │ │ │ │ │ └── 8_printer.png │ │ │ ├── short_description.txt │ │ │ ├── title.txt │ │ │ └── video.txt │ │ │ └── zh-TW │ │ │ ├── changelogs │ │ │ ├── 10104.txt │ │ │ ├── 20000006.txt │ │ │ ├── 20100010.txt │ │ │ ├── 20200002.txt │ │ │ ├── 20201002.txt │ │ │ ├── 20300004.txt │ │ │ ├── 20301002.txt │ │ │ ├── 20400003.txt │ │ │ ├── 20400005.txt │ │ │ ├── 20401004.txt │ │ │ ├── 20401006.txt │ │ │ ├── 20402002.txt │ │ │ ├── 20403002.txt │ │ │ ├── 20404004.txt │ │ │ ├── 20405002.txt │ │ │ ├── 20500016.txt │ │ │ ├── 20501002.txt │ │ │ ├── 20502002.txt │ │ │ ├── 20503003.txt │ │ │ ├── 20504003.txt │ │ │ ├── 20505002.txt │ │ │ ├── 20506002.txt │ │ │ ├── 20600003.txt │ │ │ ├── 20601002.txt │ │ │ ├── 20602002.txt │ │ │ ├── 20603003.txt │ │ │ ├── 20700008.txt │ │ │ ├── 20701002.txt │ │ │ ├── 20702005.txt │ │ │ ├── 20703002.txt │ │ │ ├── 20800005.txt │ │ │ ├── 20801004.txt │ │ │ ├── 20900007.txt │ │ │ ├── 20901004.txt │ │ │ ├── 21000003.txt │ │ │ ├── 21001002.txt │ │ │ ├── 21002004.txt │ │ │ ├── 21003003.txt │ │ │ ├── 21004002.txt │ │ │ ├── 21005002.txt │ │ │ └── default.txt │ │ │ ├── full_description.txt │ │ │ ├── images │ │ │ ├── featureGraphic.jpeg │ │ │ ├── icon.png │ │ │ └── phoneScreenshots │ │ │ │ ├── 1_analysis_chart.png │ │ │ │ ├── 2_analysis_chart_dark.png │ │ │ │ ├── 3_menu_product.png │ │ │ │ ├── 4_order_action.png │ │ │ │ ├── 5_order_details.png │ │ │ │ ├── 6_more.png │ │ │ │ ├── 7_inventory.png │ │ │ │ └── 8_printer.png │ │ │ ├── short_description.txt │ │ │ ├── title.txt │ │ │ └── video.txt │ ├── translate-changelog.sh │ └── translate-prompt.txt ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── settings.gradle └── settings_aar.gradle ├── assets ├── feature_request_please.gif ├── food_placeholder.png ├── google_sheet_icon.svg ├── google_signin_button.svg ├── l10n │ ├── en │ │ ├── analysis.yaml │ │ ├── cashier.yaml │ │ ├── global.yaml │ │ ├── menu.yaml │ │ ├── order.yaml │ │ ├── order_attribute.yaml │ │ ├── printer.yaml │ │ ├── setting.yaml │ │ ├── stock.yaml │ │ └── transit.yaml │ └── zh │ │ ├── analysis.yaml │ │ ├── cashier.yaml │ │ ├── global.yaml │ │ ├── menu.yaml │ │ ├── order.yaml │ │ ├── order_attribute.yaml │ │ ├── printer.yaml │ │ ├── setting.yaml │ │ ├── stock.yaml │ │ └── transit.yaml ├── logo.png └── web │ └── tutorial-gs-copy-url.gif ├── devtools_options.yaml ├── docs ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── PRIVACY_POLICY.md ├── README.md ├── README.zh.md ├── SECURITY.md ├── SUPPORT.md ├── about │ ├── contribute.md │ ├── contribute.zh.md │ ├── structure.md │ └── structure.zh.md ├── images │ ├── cheese-burger.png │ ├── favicon.ico │ ├── ham-burger.jpg │ ├── index-introduction.png │ ├── index-introduction.zh.png │ ├── lang │ │ ├── en-US │ │ │ └── rwd.png │ │ └── zh-TW │ │ │ └── rwd.png │ ├── logo.png │ └── veggie-burger.webp ├── maintenance │ ├── bump-dependencies.md │ ├── bump-dependencies.zh.md │ ├── deployment.md │ ├── deployment.zh.md │ ├── development.md │ └── development.zh.md ├── overrides │ └── partials │ │ └── comments.html ├── untranslated.json └── unused.txt ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-1024x1024@1x.png │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ └── Icon-App-83.5x83.5@2x.png │ ├── LaunchBackground.imageset │ │ ├── Contents.json │ │ └── background.png │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── Runner-Bridging-Header.h ├── l10n.yaml ├── lib ├── app.dart ├── components │ ├── bottom_sheet_actions.dart │ ├── choice_chip_with_help.dart │ ├── dialog │ │ ├── confirm_dialog.dart │ │ ├── delete_dialog.dart │ │ ├── dialog_page.dart │ │ ├── responsive_dialog.dart │ │ ├── single_text_dialog.dart │ │ └── slider_text_dialog.dart │ ├── imageable_container.dart │ ├── item_loader.dart │ ├── linkify.dart │ ├── loading_wrapper.dart │ ├── meta_block.dart │ ├── models │ │ ├── order_attribute_value_widget.dart │ │ └── order_loader.dart │ ├── scaffold │ │ ├── item_modal.dart │ │ └── reorderable_scaffold.dart │ ├── scrollable_draggable_sheet.dart │ ├── search_bar_wrapper.dart │ ├── sign_in_button.dart │ ├── slidable_item_list.dart │ ├── slivers │ │ └── sliver_image_app_bar.dart │ ├── style │ │ ├── buttons.dart │ │ ├── card_info_text.dart │ │ ├── circular_loading.dart │ │ ├── date_range_picker.dart │ │ ├── empty_body.dart │ │ ├── footer.dart │ │ ├── gradient_button.dart │ │ ├── gradient_scroll_hint.dart │ │ ├── head_tail_tile.dart │ │ ├── hint_text.dart │ │ ├── image_holder.dart │ │ ├── info_popup.dart │ │ ├── outlined_text.dart │ │ ├── percentile_bar.dart │ │ ├── pop_button.dart │ │ ├── route_buttons.dart │ │ ├── search_bar_inline.dart │ │ ├── single_row_warp.dart │ │ ├── slide_to_delete.dart │ │ ├── snackbar.dart │ │ ├── snackbar_actions.dart │ │ └── text_divider.dart │ └── tutorial.dart ├── constants │ ├── app_themes.dart │ ├── constant.dart │ └── icons.dart ├── debug │ ├── debug_page.dart │ ├── random_gen_order.dart │ └── rerun_migration.dart ├── firebase_compatible_options.dart ├── helpers │ ├── analysis │ │ └── ema_calculator.dart │ ├── breakpoint.dart │ ├── exporter │ │ ├── data_exporter.dart │ │ ├── google_sheet_exporter.dart │ │ └── plain_text_exporter.dart │ ├── formatter │ │ ├── formatter.dart │ │ ├── google_sheet_formatter.dart │ │ └── plain_text_formatter.dart │ ├── launcher.dart │ ├── logger.dart │ ├── setup_example.dart │ ├── util.dart │ └── validator.dart ├── l10n │ ├── app_en.arb │ └── app_zh.arb ├── main.dart ├── models │ ├── analysis │ │ ├── analysis.dart │ │ ├── chart.dart │ │ └── chart_object.dart │ ├── menu │ │ ├── catalog.dart │ │ ├── product.dart │ │ ├── product_ingredient.dart │ │ └── product_quantity.dart │ ├── model.dart │ ├── model_object.dart │ ├── objects │ │ ├── cashier_object.dart │ │ ├── menu_object.dart │ │ ├── order_attribute_object.dart │ │ ├── order_object.dart │ │ └── stock_object.dart │ ├── order │ │ ├── cart_product.dart │ │ ├── order_attribute.dart │ │ └── order_attribute_option.dart │ ├── printer.dart │ ├── repository.dart │ ├── repository │ │ ├── cart.dart │ │ ├── cashier.dart │ │ ├── menu.dart │ │ ├── order_attributes.dart │ │ ├── quantities.dart │ │ ├── replenisher.dart │ │ ├── seller.dart │ │ ├── stashed_orders.dart │ │ └── stock.dart │ ├── stock │ │ ├── ingredient.dart │ │ ├── quantity.dart │ │ └── replenishment.dart │ └── xfile.dart ├── routes.dart ├── services │ ├── auth.dart │ ├── bluetooth.dart │ ├── cache.dart │ ├── database.dart │ ├── database_migration_actions.dart │ ├── database_migrations.dart │ ├── image_dumper.dart │ └── storage.dart ├── settings │ ├── checkout_warning.dart │ ├── collect_events_setting.dart │ ├── currency_setting.dart │ ├── language_setting.dart │ ├── order_awakening_setting.dart │ ├── setting.dart │ ├── settings_provider.dart │ └── theme_setting.dart ├── translator.dart └── ui │ ├── analysis │ ├── analysis_view.dart │ ├── history_page.dart │ └── widgets │ │ ├── chart_card_view.dart │ │ ├── chart_modal.dart │ │ ├── chart_range_page.dart │ │ ├── chart_reorder.dart │ │ ├── goals_card_view.dart │ │ ├── history_calendar_view.dart │ │ ├── history_order_list.dart │ │ ├── history_order_modal.dart │ │ └── reloadable_card.dart │ ├── cashier │ ├── cashier_view.dart │ ├── changer_modal.dart │ ├── surplus_page.dart │ └── widgets │ │ ├── changer_custom_view.dart │ │ ├── changer_favorite_view.dart │ │ └── unit_list_tile.dart │ ├── home │ ├── elf_page.dart │ ├── home_page.dart │ ├── mobile_more_view.dart │ ├── settings_page.dart │ └── widgets │ │ ├── feature_slider.dart │ │ └── feature_switch.dart │ ├── image_gallery_page.dart │ ├── menu │ ├── menu_page.dart │ ├── product_page.dart │ └── widgets │ │ ├── catalog_modal.dart │ │ ├── catalog_reorder.dart │ │ ├── menu_catalog_list.dart │ │ ├── menu_product_list.dart │ │ ├── product_ingredient_modal.dart │ │ ├── product_ingredient_reorder.dart │ │ ├── product_ingredient_view.dart │ │ ├── product_modal.dart │ │ ├── product_quantity_modal.dart │ │ └── product_reorder.dart │ ├── order │ ├── cart │ │ ├── cart_actions.dart │ │ ├── cart_metadata_view.dart │ │ ├── cart_product_list.dart │ │ ├── cart_product_selector.dart │ │ ├── cart_product_state_selector.dart │ │ └── cart_snapshot.dart │ ├── checkout │ │ ├── checkout_attribute_view.dart │ │ ├── checkout_cashier_calculator.dart │ │ ├── checkout_cashier_snapshot.dart │ │ └── stashed_order_list_view.dart │ ├── order_checkout_page.dart │ ├── order_page.dart │ └── widgets │ │ ├── checkout_receipt_dialog.dart │ │ ├── draggable_sheet_view.dart │ │ ├── order_catalog_list_view.dart │ │ ├── order_object_view.dart │ │ ├── order_product_list_view.dart │ │ ├── orientated_view.dart │ │ └── printer_button_view.dart │ ├── order_attr │ ├── order_attribute_page.dart │ └── widgets │ │ ├── order_attribute_modal.dart │ │ ├── order_attribute_option_modal.dart │ │ ├── order_attribute_option_reorder.dart │ │ ├── order_attribute_reorder.dart │ │ └── order_attribute_tile.dart │ ├── printer │ ├── printer_modal.dart │ ├── printer_page.dart │ ├── printer_settings_modal.dart │ └── widgets │ │ ├── printer_info_dialog.dart │ │ ├── printer_receipt_view.dart │ │ └── printer_view.dart │ ├── stock │ ├── quantities_page.dart │ ├── replenishment_page.dart │ ├── stock_view.dart │ └── widgets │ │ ├── replenishment_apply.dart │ │ ├── replenishment_modal.dart │ │ ├── stock_ingredient_list_tile.dart │ │ ├── stock_ingredient_modal.dart │ │ ├── stock_ingredient_restock_modal.dart │ │ ├── stock_quantity_list.dart │ │ └── stock_quantity_modal.dart │ └── transit │ ├── google_sheet │ ├── export_basic_view.dart │ ├── export_order_view.dart │ ├── import_basic_view.dart │ ├── order_formatter.dart │ ├── order_setting_page.dart │ ├── order_table.dart │ ├── sheet_namer.dart │ ├── sheet_preview_page.dart │ ├── sheet_selector.dart │ ├── spreadsheet_selector.dart │ └── views.dart │ ├── plain_text │ ├── export_basic_view.dart │ ├── export_order_view.dart │ ├── import_basic_view.dart │ └── views.dart │ ├── previews │ ├── ingredient_preview_page.dart │ ├── order_attribute_preview_page.dart │ ├── preview_page.dart │ ├── product_preview_page.dart │ ├── quantity_preview_page.dart │ └── replenishment_preview_page.dart │ ├── transit_order_list.dart │ ├── transit_order_range.dart │ ├── transit_page.dart │ └── transit_station.dart ├── mkdocs.yaml ├── pubspec.lock ├── pubspec.yaml ├── release.config.json ├── scripts ├── Tra-Chi.ttf ├── bump-after.sh ├── check-unused-lang.sh └── paint_introduce.py └── test ├── app_test.dart ├── components ├── analysis_card_test.dart ├── bottom_sheet_actions_test.dart ├── imageable_container_test.dart ├── linkify_test.dart ├── loading_wrapper_test.dart ├── scrollable_draggable_sheet_test.dart ├── slidable_item_list_test.dart ├── snackbar_test.dart └── tutorial_test.dart ├── debug ├── random_gen_order_test.dart └── rerun_migration_test.dart ├── helpers ├── exporter │ └── google_sheet_exporter_test.dart ├── formatter │ ├── google_sheet_formatter_test.dart │ └── plain_text_formatter_test.dart ├── logger_test.dart ├── setup_example_test.dart ├── util_test.dart └── validator_test.dart ├── image_gallery_page_test.dart ├── mocks ├── mock_auth.dart ├── mock_auth.mocks.dart ├── mock_bluetooth.dart ├── mock_bluetooth.mocks.dart ├── mock_cache.dart ├── mock_cache.mocks.dart ├── mock_database.dart ├── mock_database.mocks.dart ├── mock_google_api.dart ├── mock_google_api.mocks.dart ├── mock_helpers.dart ├── mock_helpers.mocks.dart ├── mock_storage.dart └── mock_storage.mocks.dart ├── models ├── initialize_test.dart ├── model_test.dart ├── object_test.dart ├── repository_test.dart └── xfile_test.dart ├── services ├── auth_test.dart ├── auth_test.mocks.dart ├── cache_test.dart ├── cache_test.mocks.dart ├── database_migration_actions_test.dart ├── database_test.dart ├── database_test.mocks.dart └── storage_test.dart ├── settings ├── currency_settting_test.dart └── language_setting_test.dart ├── test_helpers ├── breakpoint_mocker.dart ├── file_mocker.dart ├── firebase_mocker.dart ├── order_setter.dart └── translator.dart └── ui ├── analysis ├── analysis_view_test.dart ├── history_page_test.dart └── widgets │ ├── chart_card_view_test.dart │ ├── chart_range_page_test.dart │ ├── goals_card_view_test.dart │ └── history_order_list_test.dart ├── cashier ├── cashier_view_test.dart └── changer_modal_test.dart ├── home ├── home_page_test.dart └── settings_page_test.dart ├── menu ├── catalog_view_test.dart ├── menu_page_test.dart └── product_page_test.dart ├── order ├── order_actions_test.dart ├── order_checkout_page_test.dart ├── order_page_test.dart ├── order_with_printer_test.dart └── stashed_order_test.dart ├── order_attr └── order_attribute_page_test.dart ├── printer └── printer_page_test.dart ├── stock ├── quantities_page_test.dart ├── replenishment_page_test.dart └── stock_view_test.dart └── transit ├── google_sheet ├── export_basic_test.dart ├── export_order_test.dart ├── import_basic_test.dart └── select_spreadsheet_test.dart ├── plain_text ├── export_basic_test.dart ├── export_order_test.dart └── import_basic_test.dart ├── transit_order_list_test.dart └── transit_page_test.dart /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: ❗ Security Issues 3 | url: https://en.wikipedia.org/wiki/Responsible_disclosure 4 | about: Do not disclose any security issues through issues or public discussions. Try to contact the team privately, such as via email (evanlu361425@gmail.com). We will discuss and resolve such issues together. Thank you! 5 | - name: 👻 Others 6 | url: https://github.com/evan361425/flutter-pos-system/discussions 7 | about: All other issues can be discussed here. 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: 💡 Feature Proposal 2 | description: Suggest ideas or improvements for the POS system 3 | labels: 4 | - "enhancement" 5 | body: 6 | - type: textarea 7 | id: expected 8 | attributes: 9 | label: What do you expect this feature to look like? 10 | description: How should it operate and display? 11 | validations: 12 | required: true 13 | - type: textarea 14 | id: problems 15 | attributes: 16 | label: Is this feature related to a problem you are experiencing? 17 | description: Whenever I want to..., I find that I can't... 18 | - type: textarea 19 | id: alternative 20 | attributes: 21 | label: How do you manage without this feature? 22 | - type: textarea 23 | id: more 24 | attributes: 25 | label: Any additional comments? 26 | - type: checkboxes 27 | id: checks 28 | attributes: 29 | label: Before opening a new issue 30 | options: 31 | - label: I have checked other [issues](https://github.com/evan361425/flutter-pos-system/issues) 32 | required: true 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/user-experience.yml: -------------------------------------------------------------------------------- 1 | name: 😺 User Experience 2 | description: What's not user-friendly in the POS system? 3 | labels: 4 | - "ux" 5 | body: 6 | - type: textarea 7 | id: feature 8 | attributes: 9 | label: Which feature is not user-friendly? 10 | description: Which page's feature, and why is it not user-friendly? 11 | validations: 12 | required: true 13 | - type: textarea 14 | id: expected 15 | attributes: 16 | label: What do you expect this feature to look like? 17 | description: How should it operate and display? 18 | validations: 19 | required: true 20 | - type: textarea 21 | id: more 22 | attributes: 23 | label: Any additional comments? 24 | - type: checkboxes 25 | id: checks 26 | attributes: 27 | label: Before opening a new issue 28 | options: 29 | - label: I have checked other [issues](https://github.com/evan361425/flutter-pos-system/issues) 30 | required: true 31 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Issue 2 | 3 | - enter your issue number here (with the hashtag `#`) 4 | 5 | ## Altered code 6 | 7 | - enter how your new code works in this PR. 8 | -------------------------------------------------------------------------------- /.github/issue-label-bot.yaml: -------------------------------------------------------------------------------- 1 | # follow by https://github.com/apps/issue-label-bot 2 | label-alias: 3 | bug: "bug" 4 | feature_request: "enhancement" 5 | question: "question" 6 | -------------------------------------------------------------------------------- /.github/no-response.yaml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-no-response - https://github.com/probot/no-response 2 | 3 | # Number of days of inactivity before an Issue is closed for lack of response 4 | daysUntilClose: 14 5 | # Label requiring a response 6 | responseRequiredLabel: more-information-needed 7 | # Comment to post when closing an Issue for lack of response. Set to `false` to disable 8 | closeComment: > 9 | This issue has been automatically closed because there has been no response to our request for more information from the original author. With only the information that is currently in the issue, we don't have enough information to take action. Please reach out if you have or find the answers we need so that we can investigate further. 10 | -------------------------------------------------------------------------------- /.github/settings.yaml: -------------------------------------------------------------------------------- 1 | # Repository settings set via https://github.com/probot/settings 2 | 3 | repository: 4 | has_issues: true 5 | has_wiki: false 6 | has_projects: true 7 | has_downloads: true 8 | 9 | labels: 10 | - name: help wanted 11 | oldname: help-wanted 12 | color: 0e8a16 13 | - name: more-information-needed 14 | color: d93f0b 15 | - name: bug 16 | color: b60205 17 | - name: enhancement 18 | color: 1d76db 19 | - name: good first issue 20 | color: 5319e7 21 | -------------------------------------------------------------------------------- /.github/stale.yaml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-stale - https://github.com/probot/stale 2 | 3 | # Number of days of inactivity before an Issue or Pull Request becomes stale 4 | daysUntilStale: 60 5 | 6 | # Number of days of inactivity before a stale Issue or Pull Request is closed 7 | daysUntilClose: 7 8 | 9 | # Issues or Pull Requests with these labels will never be considered stale 10 | exemptLabels: 11 | - pinned 12 | - security 13 | 14 | # Label to use when marking as stale 15 | staleLabel: wontfix 16 | 17 | # Comment to post when marking as stale. Set to `false` to disable 18 | markComment: > 19 | This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. 20 | # Comment to post when closing a stale Issue or Pull Request. Set to `false` to disable 21 | closeComment: false 22 | # Limit to only `issues` or `pulls` 23 | # only: issues 24 | -------------------------------------------------------------------------------- /.github/workflows/github-page.yaml: -------------------------------------------------------------------------------- 1 | name: github pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - "docs/**" 9 | - "!docs/untranslated.json" 10 | - "!docs/unused.txt" 11 | - mkdocs.yaml 12 | pull_request: 13 | paths: 14 | - "docs/**" 15 | - "!docs/untranslated.json" 16 | - "!docs/unused.txt" 17 | - mkdocs.yaml 18 | workflow_dispatch: 19 | 20 | jobs: 21 | deploy: 22 | name: GitHub Pages 23 | runs-on: ubuntu-20.04 24 | steps: 25 | - name: Checkout Repository 26 | uses: actions/checkout@v4 27 | with: 28 | fetch-depth: 0 29 | persist-credentials: true 30 | 31 | - uses: actions/setup-python@v4 32 | with: 33 | python-version: "3.10" 34 | 35 | - name: Install dependencies 36 | run: | 37 | pip install mkdocs 38 | pip install mkdocs-material 39 | pip install mkdocs-git-revision-date-plugin 40 | pip install mkdocs-static-i18n 41 | pip install mdx_truly_sane_lists 42 | 43 | - name: Build 44 | if: ${{ github.ref != 'refs/heads/master' }} 45 | run: mkdocs build 46 | 47 | - name: Deploy 48 | if: ${{ github.ref == 'refs/heads/master' }} 49 | run: mkdocs gh-deploy 50 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | # Controls when the action will run. 4 | on: 5 | # Triggers the workflow on push or pull request events but only for the master branch 6 | push: 7 | branches: 8 | - master 9 | paths-ignore: 10 | - "**.md" 11 | - "docs/**" 12 | - "scripts/**" 13 | - "android/**" 14 | - "pubspec.yaml" 15 | - "pubspec.lock" 16 | - "mkdocs.yaml" 17 | - "Makefile" 18 | - ".github/**" 19 | pull_request: 20 | 21 | # Allows you to run this workflow manually from the Actions tab 22 | workflow_dispatch: 23 | 24 | jobs: 25 | code-checking: 26 | name: Check code format and testing 27 | runs-on: ubuntu-latest 28 | if: ${{ !startsWith(github.head_ref , 'refs/heads/ci/') }} 29 | steps: 30 | - uses: actions/checkout@v4 31 | 32 | # Setup the flutter environment. 33 | - uses: subosito/flutter-action@v2 34 | with: 35 | flutter-version: "3.27.x" 36 | cache: true 37 | channel: "stable" 38 | 39 | # Get flutter dependencies. 40 | - name: Install dependencies 41 | env: 42 | GH_READ_PAT: ${{ secrets.GH_READ_PAT }} 43 | run: | 44 | echo "https://oauth:$GH_READ_PAT@github.com" > ~/.git-credentials 45 | git config --global credential.helper store 46 | flutter pub get 47 | 48 | # Statically analyze the Dart code for any errors. 49 | - name: Linter 50 | run: flutter analyze . 51 | 52 | - name: Format 53 | run: dart format --set-exit-if-changed --show none --line-length 120 . 54 | 55 | # Run widget tests for our flutter project. 56 | - name: Testing 57 | if: github.event_name == 'pull_request' 58 | run: flutter test 59 | 60 | - name: Testing with Coverage 61 | if: github.event_name != 'pull_request' 62 | run: flutter test --coverage 63 | 64 | - name: Coverage to codecov 65 | if: github.event_name != 'pull_request' 66 | uses: codecov/codecov-action@v3 67 | with: 68 | file: coverage/lcov.info 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | .venv 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Web related 36 | lib/generated_plugin_registrant.dart 37 | 38 | # Symbolication related 39 | app.*.symbols 40 | 41 | # Obfuscation related 42 | app.*.map.json 43 | 44 | # Android Studio will place build artifacts here 45 | /android/app/src/debug 46 | /android/app/profile 47 | /android/app/release 48 | 49 | # Sensitive data 50 | google-services.json 51 | services.*.tar 52 | 53 | # mocks 54 | # *.mocks.dart 55 | 56 | coverage/ 57 | android/build/* 58 | site/ 59 | Pipfile.lock 60 | /*.key 61 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "pos-system", 9 | "request": "launch", 10 | "type": "dart", 11 | "program": "lib/main.dart", 12 | "args": [ 13 | "--flavor", 14 | "dev", 15 | "--dart-define=appFlavor=debug", 16 | "--dart-define=logLevel=debug" 17 | ] 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "autofocus", 4 | "avator", 5 | "crashlytics", 6 | "cupertino", 7 | "firestore", 8 | "Formattable", 9 | "fullscreen", 10 | "googleapis", 11 | "Imageable", 12 | "Ingr", 13 | "Linkify", 14 | "loadmore", 15 | "LTRB", 16 | "mockito", 17 | "possystem", 18 | "pubspec", 19 | "reloadable", 20 | "reorderable", 21 | "sembast", 22 | "signin", 23 | "sqflite", 24 | "sublist", 25 | "syncfusion", 26 | "unfocus", 27 | "upgrader", 28 | "vsync", 29 | "wakelock", 30 | "xfile" 31 | ], 32 | "files.associations": { 33 | "*.arb": "json" 34 | }, 35 | "yaml.schemas": { 36 | ".vscode/arb.schema.json": ["/assets/l10n/**/*.yaml"] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | Pillow = "*" 8 | 9 | [dev-packages] 10 | 11 | [requires] 12 | python_version = "3.12" 13 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | analyzer: 13 | exclude: 14 | - "test/mocks/**.mocks.dart" 15 | 16 | linter: 17 | # The lint rules applied to this project can be customized in the 18 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 19 | # included above or to enable additional rules. A list of all available lints 20 | # and their documentation is published at 21 | # https://dart-lang.github.io/linter/lints/index.html. 22 | # 23 | # Instead of disabling a lint rule for the entire project in the 24 | # section below, it can also be suppressed for a single line of code 25 | # or a specific dart file by using the `// ignore: name_of_lint` and 26 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 27 | # producing the lint. 28 | rules: 29 | lines_longer_than_80_chars: false 30 | prefer_const_constructors: true 31 | prefer_const_literals_to_create_immutables: true 32 | prefer_const_declarations: true 33 | # unnecessary_overrides: false 34 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 35 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 36 | # Additional information about this file can be found at 37 | # https://dart.dev/guides/language/analysis-options 38 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | 13 | fastlane-keys.json 14 | *.jks 15 | *.lockfile 16 | fastlane/report.xml 17 | -------------------------------------------------------------------------------- /android/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | android 4 | Project android created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.buildship.core.gradleprojectbuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.buildship.core.gradleprojectnature 16 | 17 | 18 | 19 | 1624699301756 20 | 21 | 30 22 | 23 | org.eclipse.core.resources.regexFilterMatcher 24 | node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /android/.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | arguments= 2 | auto.sync=false 3 | build.scans.enabled=false 4 | connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER) 5 | connection.project.dir= 6 | eclipse.preferences.version=1 7 | gradle.user.home= 8 | java.home=/Library/Java/JavaVirtualMachines/adoptopenjdk-11.jdk/Contents/Home 9 | jvm.arguments= 10 | offline.mode=false 11 | override.workspace.settings=true 12 | show.console.view=true 13 | show.executions.view=true 14 | -------------------------------------------------------------------------------- /android/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "fastlane", ">= 2.220.0" 4 | -------------------------------------------------------------------------------- /android/README.md: -------------------------------------------------------------------------------- 1 | # Android 2 | 3 | How to build? 4 | 5 | ```bash 6 | flutter build -v apk --release --flavor dev 7 | ``` 8 | -------------------------------------------------------------------------------- /android/app/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /android/app/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | app 4 | Project app created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | org.eclipse.buildship.core.gradleprojectbuilder 15 | 16 | 17 | 18 | 19 | 20 | org.eclipse.jdt.core.javanature 21 | org.eclipse.buildship.core.gradleprojectnature 22 | 23 | 24 | 25 | 1624699301764 26 | 27 | 30 28 | 29 | org.eclipse.core.resources.regexFilterMatcher 30 | node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /android/app/.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | connection.project.dir=.. 2 | eclipse.preferences.version=1 3 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/evanlu/possystem/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.evanlu.possystem 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/android/app/src/main/res/drawable-hdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/android/app/src/main/res/drawable-mdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/android/app/src/main/res/drawable-v21/background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 14 | 17 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/android/app/src/main/res/drawable-xhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/android/app/src/main/res/drawable-xxhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/android/app/src/main/res/drawable-xxxhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/android/app/src/main/res/drawable/background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-b+zh/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | POS 系統 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 16 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-v31/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | POS System 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.9.25' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 10 | } 11 | } 12 | 13 | allprojects { 14 | repositories { 15 | google() 16 | mavenCentral() 17 | } 18 | } 19 | 20 | rootProject.buildDir = '../build' 21 | 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | tasks.register("clean", Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /android/fastlane/Appfile: -------------------------------------------------------------------------------- 1 | package_name("com.evanlu.possystem") # e.g. com.krausefx.app 2 | -------------------------------------------------------------------------------- /android/fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ---- 3 | 4 | # Installation 5 | 6 | Make sure you have the latest version of the Xcode command line tools installed: 7 | 8 | ```sh 9 | xcode-select --install 10 | ``` 11 | 12 | For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) 13 | 14 | # Available Actions 15 | 16 | ## Android 17 | 18 | ### android test 19 | 20 | ```sh 21 | [bundle exec] fastlane android test 22 | ``` 23 | 24 | Test locally 25 | 26 | ### android internal 27 | 28 | ```sh 29 | [bundle exec] fastlane android internal 30 | ``` 31 | 32 | Submit a new Internal Build to Play Store 33 | 34 | ### android beta 35 | 36 | ```sh 37 | [bundle exec] fastlane android beta 38 | ``` 39 | 40 | Submit a new Beta Build to Crashlytics Beta 41 | 42 | ### android promote_to_production 43 | 44 | ```sh 45 | [bundle exec] fastlane android promote_to_production 46 | ``` 47 | 48 | Promote beta track to prod 49 | 50 | ---- 51 | 52 | This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. 53 | 54 | More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). 55 | 56 | The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 57 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/20900007.txt: -------------------------------------------------------------------------------- 1 | 🚀 Features 2 | 3 | • Allow drawing chart to analysis orders #204 : evan361425 4 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/20901004.txt: -------------------------------------------------------------------------------- 1 | 🚀 Features 2 | 3 | • Multiple languages #205 : evan361425 4 | • Replenish by money #206 : evan361425 5 | • Product's ingredients are order-able : evan361425 6 | 7 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/21000003.txt: -------------------------------------------------------------------------------- 1 | 🚀 Features 2 | 3 | • Responsible width design #211 : evan361425 4 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/21001002.txt: -------------------------------------------------------------------------------- 1 | Caught some bugs, time to clean up! 2 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/21002004.txt: -------------------------------------------------------------------------------- 1 | ## 🚀 Features 2 | 3 | - Support bluetooth printers #212 : evan361425 -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/21003003.txt: -------------------------------------------------------------------------------- 1 | ## 🚀 Features 2 | 3 | • allow add notes when ordering 4 | • display order ID in records 5 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/21004002.txt: -------------------------------------------------------------------------------- 1 | 🐛 Fixes 2 | 3 | - Correct analytics -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/21005002.txt: -------------------------------------------------------------------------------- 1 | Caught some bugs, time to clean up! -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/default.txt: -------------------------------------------------------------------------------- 1 | Caught some bugs, time to clean up! 2 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | Order and Checkout System 2 | This is an offline point-of-sale (POS) system that uses local files as its database. 3 | This allows users to operate the system without an internet connection, and it not only stores order data but also allows for the configuration of inventory management and other functions. 4 | This system is completely open source (free). If you are a programmer or anyone who wants to make this product better, you are welcome to visit the relevant website: 5 | 6 | https://github.com/evan361425/flutter-pos-system 7 | 8 | ♦ Features: 9 | 10 | • Menu: Users can directly edit the menu, including the catalog, price, cost, and ingredients of each product. 11 | • Inventory Tracking: Set inventory levels for each dish. Each order is automatically subtracted from the inventory. 12 | • Ordering: Includes useful features such as order queue and quick order amount calculation. 13 | • Cash Register: Helps reconcile daily orders and calculate the correct amount of cash due after an order is placed. 14 | • Customer Details: Customize customer options such as takeout, dine-in, gender, age, etc. 15 | • Data Backup: Back up order, menu, and other information to Google Sheets. 16 | • Chart Analysis: Create custom charts for intuitive analysis and statistics. 17 | • Printer: Print receipts via Bluetooth connection. 18 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/images/featureGraphic.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/android/fastlane/metadata/android/en-US/images/featureGraphic.jpeg -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/android/fastlane/metadata/android/en-US/images/icon.png -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/images/phoneScreenshots/1_analysis_chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/android/fastlane/metadata/android/en-US/images/phoneScreenshots/1_analysis_chart.png -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/images/phoneScreenshots/2_analysis_chart_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/android/fastlane/metadata/android/en-US/images/phoneScreenshots/2_analysis_chart_dark.png -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/images/phoneScreenshots/3_menu_product.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/android/fastlane/metadata/android/en-US/images/phoneScreenshots/3_menu_product.png -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/images/phoneScreenshots/4_order_action.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/android/fastlane/metadata/android/en-US/images/phoneScreenshots/4_order_action.png -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/images/phoneScreenshots/5_order_details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/android/fastlane/metadata/android/en-US/images/phoneScreenshots/5_order_details.png -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/images/phoneScreenshots/6_more.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/android/fastlane/metadata/android/en-US/images/phoneScreenshots/6_more.png -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/images/phoneScreenshots/7_inventory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/android/fastlane/metadata/android/en-US/images/phoneScreenshots/7_inventory.png -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/images/phoneScreenshots/8_printer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/android/fastlane/metadata/android/en-US/images/phoneScreenshots/8_printer.png -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | offline point-of-sale (POS) system that has inventory system and cash register. 2 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/title.txt: -------------------------------------------------------------------------------- 1 | POS System - Order & Checkout 2 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/video.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/android/fastlane/metadata/android/en-US/video.txt -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/10104.txt: -------------------------------------------------------------------------------- 1 | 修正部分錯誤 -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/20000006.txt: -------------------------------------------------------------------------------- 1 | 點餐結帳的系統。 2 | 以本地端的檔案作為資料庫。讓使用者不需要連網也能操作,不僅有點餐後的資料,也可自由配置貨存管理等。 3 | 4 | 本系統是完全開源(免費),若你是程式設計師或任何想讓本產品更好的人,歡迎來相關網站: 5 | https://github.com/evan361425/flutter-pos-system 6 | 7 | 功能簡介 : 8 | 菜單 9 | 可以直接編輯菜單,包括各項餐點的名稱、種類、售價、成本、內容物等。 10 | 庫存追蹤 11 | 在菜單中設定各項餐點的庫存。每次點餐都可以計算所剩的庫存。 12 | 點餐 13 | 使用者也可以用此系統來做點餐。並搭配許多功能。。 14 | 15 | 尚未實作功能: 16 | 17 | - 客製化顧客細項 18 | 可客製化顧客的選項。例如,外帶、內用、性別、年齡等。 19 | - 連結Google表單 20 | 會建立表單,並用此表單編輯、儲存您的菜單。點餐時的資料也會記錄起來以利分析。 21 | - 報表 22 | 結算當日的點餐,並做出報表。方便使用者直接觀察分析。 23 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/20100010.txt: -------------------------------------------------------------------------------- 1 | 🚀 功能 2 | 3 | - 新增收銀機功能 4 | - 長按時會有回饋 5 | - 使用 Material 預設外觀設計 6 | 7 | 🐛 修正 8 | 9 | - 點餐的「變更數量」現在不能設定為 0 10 | - 點餐頁面開啟鍵盤時會讓畫面跑掉 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/20200002.txt: -------------------------------------------------------------------------------- 1 | ## 🚀 功能 2 | 3 | - 菜單頁可以直接導回首頁 #65 4 | - 點餐時不關閉螢幕 #66 5 | - 菜單頁可以搜尋 #69 6 | - 搜尋時會先顯示搜尋過的產品 #77 7 | - 菜單頁顯示教學 #82 8 | - 允許產品負數價格 #83 9 | - 快顯圖提供關閉按鈕 #85 10 | - 搜尋成份時可以導到成份頁 #86 11 | - 點餐頁提供面板外觀 #88 12 | - 改用提示而非教學 #90 13 | 14 | ## 👻 其他 15 | 16 | - 收銀機套用和菜單相同的模組 #68 17 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/20201002.txt: -------------------------------------------------------------------------------- 1 | ## 🚀 功能 2 | 3 | - 把提示另外包一個套件,並整合進本專案中 #101 : evan361425 4 | 5 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/20300004.txt: -------------------------------------------------------------------------------- 1 | ## 🚀 功能 2 | 3 | - 客戶設定 #107 : evan361425 4 | - 長按餐點可以操作「使所選物」 #104 : evan361425 5 | - 使用 l10n 來做多語言 #120 : evan361425 6 | - 在結帳時看得到點餐的餐點 #111 : evan361425 7 | 8 | ## ⚙️ 重構 9 | 10 | - 客戶設定使用 SQLite 來做儲存而非 Document,未來方便轉換 #76 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/20301002.txt: -------------------------------------------------------------------------------- 1 | ## 🐛 修正 2 | 3 | - 點餐時,成分的錯誤狀態 #125 : evan361425 4 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/20400003.txt: -------------------------------------------------------------------------------- 1 | 🚀 功能 2 | 3 | • 在結餘時允許設置現金 #129 : evan361425 4 | • 設置產品的圖片 #132 : evan361425 5 | 6 | 🐛 修正 7 | 8 | • 把提示放進各個頁面中,才不會一次一大堆提示 #127 : evan361425 9 | • 結帳換錢時,收銀機不會拿超過它擁有的錢 #130 : evan361425 10 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/20400005.txt: -------------------------------------------------------------------------------- 1 | 🚀 功能 2 | 3 | • 在結餘時允許設置現金 #129 : evan361425 4 | • 設置產品的圖片 #132 : evan361425 5 | 6 | 🐛 修正 7 | 8 | • 把提示放進各個頁面中,才不會一次一大堆提示 #127 : evan361425 9 | • 結帳換錢時,收銀機不會拿超過它擁有的錢 #130 : evan361425 10 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/20401004.txt: -------------------------------------------------------------------------------- 1 | 🚀 功能 2 | 3 | • 增加「建議」頁面 #142 : evan361425 4 | 5 | 👻 其他 6 | 7 | • 使用底部做主要導航 #142 : evan361425 8 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/20401006.txt: -------------------------------------------------------------------------------- 1 | 🚀 功能 2 | 3 | • 增加「建議」頁面 #142 : evan361425 4 | 5 | 👻 其他 6 | 7 | • 使用底部做主要導航 #142 : evan361425 8 | • 當收銀機錢不夠了,使用者會被通知 #117 : evan361425 9 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/20402002.txt: -------------------------------------------------------------------------------- 1 | 🚀 功能 2 | 3 | • 允許設定是否關閉收銀機的提示 #148 : evan361425 4 | 5 | 🐛 修正 6 | 7 | • 避免小螢幕在結餘時看不到完整資訊 : evan361425 8 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/20403002.txt: -------------------------------------------------------------------------------- 1 | 👻 其他 2 | 3 | • 更新各種套件 #150 : evan361425 4 | • 測試環境中自動增加訂單 #151 : evan361425 5 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/20404004.txt: -------------------------------------------------------------------------------- 1 | 👻 其他 2 | 3 | • 新增淨利於分析頁中 #155 : evan361425 4 | • 允許資料庫測試 #156 : evan361425 5 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/20405002.txt: -------------------------------------------------------------------------------- 1 | 修正點餐頁面的型別轉換錯誤 2 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/20500016.txt: -------------------------------------------------------------------------------- 1 | 🚀 功能 2 | 3 | • 新增 Google Sheet 匯出匯入功能 #161 : evan361425 4 | 5 | 👻 其他 6 | 7 | • 設定頁顯示版本資訊 8 | • 新增「隱私權政策」和「使用條款」 9 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/20501002.txt: -------------------------------------------------------------------------------- 1 | 🚀 功能 2 | 3 | • 允許一次匯入 : evan361425 4 | 5 | 👻 其他 6 | 7 | • 保持之前登入的紀錄 8 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/20502002.txt: -------------------------------------------------------------------------------- 1 | 🚀 功能 2 | 3 | • 提供搜集錯誤資訊時的不同處理方式 : evan361425 4 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/20503003.txt: -------------------------------------------------------------------------------- 1 | 🐛 修正 2 | 3 | • 當點完餐後分析頁會自動更新 #160 : evan361425 4 | 5 | 👻 其他 6 | 7 | • 修正顧客設定的 Schema,改為使用 Storage #165 : evan361425 8 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/20504003.txt: -------------------------------------------------------------------------------- 1 | 🚀 功能 2 | 3 | 在今年的最後幾天,更新了版本😆 4 | 5 | • 現在終於可以刪掉訂單記錄了!感謝網友提醒(許願?) 6 | • 現在計算機可以使用「加」、「減」和「乘」了! 7 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/20505002.txt: -------------------------------------------------------------------------------- 1 | 🚀 功能 2 | 3 | • 教學模式 #168 : evan361425 4 | • 使用列條表示庫存和收銀 e299e32 : evan361425 5 | • 點餐時按上一頁會關閉菜單而非跳出 d9ca7db : evan361425 6 | • 點餐後會顯示該產品的產品種類 f8e4415 : evan361425 7 | 8 | 🐛 修正 9 | 10 | • 開啟其他滑動按鈕時自動關閉已經打開的 c1ea1ce : evan361425 11 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/20506002.txt: -------------------------------------------------------------------------------- 1 | 🐛 修正 2 | 3 | • 在產品中顯示更多訂單細節 #170 : evan361425 4 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/20600003.txt: -------------------------------------------------------------------------------- 1 | 🚀 功能 2 | 3 | • 使用 Material 3 作為設計基礎 #173 4 | • 庫存的 Bar 改用 Slider #175 5 | • 提交表單時自動聚焦於錯誤的欄位 #179 6 | • 點餐向左向右切,可以改種類 #180 7 | • 點餐的顧客設定跟結帳用 tab #180 8 | 9 | 🐛 修正 10 | 11 | • 點餐顯示,如果沒有成份,就不要顯示 #171 12 | • 計算機打勾之後關掉計算機,而非送出 #176 13 | 14 | 👻 其他 15 | 16 | • 新增產品種類到訂單記錄中 #172 17 | • 使用 SpotlightAnt #174 18 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/20601002.txt: -------------------------------------------------------------------------------- 1 | 🐛 修正 2 | 3 | • 部分翻譯修正 4 | • 移除 card 和 expansion tile 的搭配 5 | • 點單時,不要顯示預設的份量 6 | • 點網址後要跳出應用程式,讓瀏覽器處理 7 | • Google 表單匯入全部時要提醒 8 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/20602002.txt: -------------------------------------------------------------------------------- 1 | • 修正小蟲 🐛 2 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/20603003.txt: -------------------------------------------------------------------------------- 1 | 🚀 功能 2 | 3 | • 允許管理圖片 #185 : evan361425 4 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/20700008.txt: -------------------------------------------------------------------------------- 1 | 🚀 功能 2 | 3 | • 新增純文字匯出匯入選項 #189 : evan361425 4 | 5 | 👻 其他 6 | 7 | • 提供更直觀的說明按鈕 #187 : evan361425 8 | • Flutter 升版到 3.10.6 9 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/20701002.txt: -------------------------------------------------------------------------------- 1 | 🚀 功能 2 | 3 | • 允許發布佈告欄 4 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/20702005.txt: -------------------------------------------------------------------------------- 1 | 🐛 修正 2 | 3 | • 各種隱藏在細節的問題,包括但不局限於:打折使用物品原價而非當下的價錢、在圖示和頁面中新增說明等等。 4 | 5 | 👻 其他 6 | 7 | • 允許透過 link 導航到指定位置 #196 : evan361425 8 | 9 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/20703002.txt: -------------------------------------------------------------------------------- 1 | 2 | 🐛 修正 3 | 4 | • 調整 link 的位置 5 | • 升級依賴 6 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/20800005.txt: -------------------------------------------------------------------------------- 1 | 🚀 功能 2 | 3 | • 允許管理暫存訂單 #198 : evan361425 4 | • 使用滑動來刪除資料 5 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/20801004.txt: -------------------------------------------------------------------------------- 1 | 🚀 功能 2 | 3 | • 升級點餐時的酷炫面板 #199 : evan361425 4 | 5 | 🐛 修正 6 | 7 | • 修正各種 UI 介面錯誤 8 | 9 | 10 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/20900007.txt: -------------------------------------------------------------------------------- 1 | 🚀 功能 2 | 3 | • 允許使用者畫圖分析訂單 #204 : evan361425 4 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/20901004.txt: -------------------------------------------------------------------------------- 1 | 好的 2 | 3 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/21000003.txt: -------------------------------------------------------------------------------- 1 | 🚀 功能 2 | 3 | • 回應式寬度設計 #211 : evan361425 4 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/21001002.txt: -------------------------------------------------------------------------------- 1 | 抓到一些害蟲,是時候修理一下了! 2 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/21002004.txt: -------------------------------------------------------------------------------- 1 | 🚀 新功能 2 | 3 | • 支援藍牙出單機 #212 : evan361425 -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/21003003.txt: -------------------------------------------------------------------------------- 1 | 🚀 新增功能 2 | 3 | • 點餐時允許備注。 4 | • 用 ID 標注每筆訂單。 5 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/21004002.txt: -------------------------------------------------------------------------------- 1 | 🐛 修正 2 | 3 | - 確保統計資料正確 -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/21005002.txt: -------------------------------------------------------------------------------- 1 | 修復了一些錯誤,進行了整理! 2 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/changelogs/default.txt: -------------------------------------------------------------------------------- 1 | 抓了一些臭蟲,打掃一下! 2 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/full_description.txt: -------------------------------------------------------------------------------- 1 | 點餐結帳的系統。 2 | 以本地端的檔案作為資料庫。讓使用者不需要連網也能操作,不僅有點餐後的資料,也可自由配置貨存管理等。 3 | 4 | 本系統是完全開源(免費),若你是程式設計師或任何想讓本產品更好的人,歡迎來相關網站: 5 | https://github.com/evan361425/flutter-pos-system 6 | 7 | ♦ 功能簡介 8 | 9 | • 菜單 - 可以直接編輯菜單,包括各項餐點的種類、售價、成本、內容物等。 10 | • 庫存追蹤 - 設定各項餐點的庫存。每次點餐都可以計算所剩的庫存。 11 | • 點餐 - 搭配暫存、快速點餐金額等有用小功能。 12 | • 收銀機 - 幫助我們結餘當日的訂單,並且在點單後計算現金應有的數量。 13 | • 顧客細項 - 可客製化顧客的選項。例如,外帶、內用、性別、年齡等。 14 | • 資料備份 - 可以把訂單、菜單等資訊備份,匯出至 Google 表單。 15 | • 圖表分析,自訂圖表進行直觀的分析和統計。 16 | • 出單機:允許透過藍牙列印訂單內容。 17 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/images/featureGraphic.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/android/fastlane/metadata/android/zh-TW/images/featureGraphic.jpeg -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/android/fastlane/metadata/android/zh-TW/images/icon.png -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/images/phoneScreenshots/1_analysis_chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/android/fastlane/metadata/android/zh-TW/images/phoneScreenshots/1_analysis_chart.png -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/images/phoneScreenshots/2_analysis_chart_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/android/fastlane/metadata/android/zh-TW/images/phoneScreenshots/2_analysis_chart_dark.png -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/images/phoneScreenshots/3_menu_product.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/android/fastlane/metadata/android/zh-TW/images/phoneScreenshots/3_menu_product.png -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/images/phoneScreenshots/4_order_action.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/android/fastlane/metadata/android/zh-TW/images/phoneScreenshots/4_order_action.png -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/images/phoneScreenshots/5_order_details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/android/fastlane/metadata/android/zh-TW/images/phoneScreenshots/5_order_details.png -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/images/phoneScreenshots/6_more.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/android/fastlane/metadata/android/zh-TW/images/phoneScreenshots/6_more.png -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/images/phoneScreenshots/7_inventory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/android/fastlane/metadata/android/zh-TW/images/phoneScreenshots/7_inventory.png -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/images/phoneScreenshots/8_printer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/android/fastlane/metadata/android/zh-TW/images/phoneScreenshots/8_printer.png -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/short_description.txt: -------------------------------------------------------------------------------- 1 | 用來點餐結帳的開源程式,並且不需要網路連線即可操作。 2 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/title.txt: -------------------------------------------------------------------------------- 1 | POS 系統 - 點餐結帳 2 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/zh-TW/video.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/android/fastlane/metadata/android/zh-TW/video.txt -------------------------------------------------------------------------------- /android/fastlane/translate-prompt.txt: -------------------------------------------------------------------------------- 1 | You are an AI assistant that help translate the following product changelog from English to %s. 2 | You should only use text format not markdown. 3 | Ensuring that each bullet point maintains the same prefix `•` and each title remains the origin emoji, for example `🚀`. 4 | You should not give me any other information or anything explanation. 5 | Here is the changelog: 6 | %s 7 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G 2 | 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | settings.ext.flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | }() 9 | 10 | includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | 18 | plugins { 19 | id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false 20 | } 21 | } 22 | 23 | plugins { 24 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 25 | id "com.android.application" version "8.1.4" apply false 26 | id "org.jetbrains.kotlin.android" version "1.9.25" apply false 27 | id "com.google.gms.google-services" version "4.4.0" apply false 28 | id "com.google.firebase.firebase-perf" version "1.4.1" apply false 29 | id "com.google.firebase.crashlytics" version "2.9.9" apply false 30 | } 31 | 32 | include ':app' 33 | -------------------------------------------------------------------------------- /android/settings_aar.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /assets/feature_request_please.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/assets/feature_request_please.gif -------------------------------------------------------------------------------- /assets/food_placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/assets/food_placeholder.png -------------------------------------------------------------------------------- /assets/l10n/en/setting.yaml: -------------------------------------------------------------------------------- 1 | $prefix: setting 2 | tab: Settings 3 | version: 4 | - 'Version: {version}' 5 | - Display the app version 6 | - version: 7 | welcome: 8 | - Hi, {name} 9 | - Display user's name 10 | - name: 11 | logoutBtn: Log Out 12 | elf: 13 | title: Suggestions 14 | description: Provide feedback using Google Forms 15 | content: |- 16 | Feel like something's missing here? 17 | Feel free to [give suggestions](https://forms.gle/s8V5SXuqhA1u3zmt7). 18 | You can also check out [upcoming features](https://github.com/evan361425/flutter-pos-system/milestones). 19 | feature: 20 | title: Other Settings 21 | description: Appearance, Language, Tips 22 | theme: 23 | title: Theme 24 | name: 25 | - dark: Dark Mode 26 | light: Light Mode 27 | system: Follow System 28 | - Appearance of the app 29 | - name: 30 | language: 31 | title: Language 32 | checkoutWarning: 33 | title: Cash Registry Warnings 34 | name: 35 | - showAll: Show All 36 | onlyNotEnough: Show Only When Not Enough 37 | hideAll: Hide All 38 | - Whether to display cash registry warnings 39 | - name: 40 | tip: 41 | - showAll: |- 42 | Show warning when using smaller denominations to give change. 43 | For example, if $5 is not enough, start using 5 $1 bills for change. 44 | onlyNotEnough: Show warning when cash registry not enough money. 45 | hideAll: Won't display any warnings during ordering. 46 | - name: 47 | orderAwakening: 48 | title: 49 | - Keep Screen On During Ordering 50 | - Keep the screen on during ordering, even when idle 51 | description: If disabled, the screen will turn off based on system settings during ordering. 52 | report: 53 | title: Collect Error Messages and Events 54 | description: Send error messages when the app encounters issues, helping the app improve 55 | -------------------------------------------------------------------------------- /assets/l10n/zh/cashier.yaml: -------------------------------------------------------------------------------- 1 | $prefix: cashier 2 | tab: 收銀 3 | unitLabel: 4 | - 幣值:{unit} 5 | - unit: 6 | counter: 7 | label: 8 | - 數量 9 | - 設定幣值數量時的標籤 10 | toDefault: 11 | title: 設為預設 12 | tutorial: 13 | title: 收銀機預設狀態 14 | content: |- 15 | 在下面設定完收銀機各幣值的數量後, 16 | 按這裡設定預設狀態! 17 | 設定好的數量就會是各個幣值狀態條的「最大值」。 18 | dialog: 19 | title: 調整收銀臺預設? 20 | content: |- 21 | 這將會把目前的收銀機狀態設定為預設狀態。 22 | 此動作將會覆蓋掉先前的設定。 23 | changer: 24 | title: 換錢 25 | button: 套用 26 | tutorial: 27 | title: 收銀機換錢 28 | content: |- 29 | 一百塊換成 10 個十塊之類。 30 | 幫助快速調整收銀機狀態。 31 | error: 32 | noSelection: 請選擇要套用的組合 33 | notEnough: "{unit} 元不夠換" 34 | invalidHead: "{count} 個 {unit} 元沒辦法換" 35 | invalidBody: "{count} 個 {unit} 元" 36 | favorite: 37 | tab: 常用 38 | hint: 選完後請點選「套用」來使用該組合 39 | emptyBody: 這裡可以幫助你快速轉換不同幣值 40 | item: 41 | from: 用 {count} 個 {unit} 元換 42 | to: "{count} 個 {unit} 元" 43 | custom: 44 | tab: 自訂 45 | addBtn: 新增常用 46 | count: 47 | label: 數量 48 | unit: 49 | label: 幣值 50 | addBtn: 新增幣種 51 | divider: 52 | from: 拿 53 | to: 換 54 | surplus: 55 | title: 結餘 56 | button: 結餘 57 | tutorial: 58 | title: 每日結餘 59 | content: |- 60 | 結餘可以幫助我們在每天打烊時, 61 | 計算現有金額和預設金額的差異。 62 | error: 63 | emptyDefault: 尚未設定預設狀態 64 | tableHint: 若你確認收銀機的金錢都沒問題之後就可以完成結餘囉! 65 | columnName: 66 | - unit: 單位 67 | currentCount: 現有 68 | diffCount: 差異 69 | defaultCount: 預設 70 | counter: 71 | label: 幣值{unit}的數量 72 | shortLabel: 數量 73 | currentTotal: 74 | label: 現有總額 75 | helper: |- 76 | 現在收銀機應該要有的總額。 77 | 若你發現現金和這值對不上,想一想今天有沒有用收銀機的錢買東西? 78 | diffTotal: 79 | label: 差額 80 | helper: |- 81 | 和收銀機最一開始的總額的差額。 82 | 這可以快速幫你了解今天收銀機多了多少錢唷。 83 | -------------------------------------------------------------------------------- /assets/l10n/zh/global.yaml: -------------------------------------------------------------------------------- 1 | appTitle: POS 系統 2 | act: 3 | success: 執行成功 4 | error: 錯誤 5 | moreInfo: 說明 6 | singleChoice: 一次只能選擇一種 7 | multiChoices: 可以選擇多種 8 | totalCount: 9 | - other: 總共 {count} 項 10 | searchCount: 搜尋到 {count} 個結果 11 | title: 12 | - analysis: 分析 13 | stock: 庫存 14 | cashier: 收銀 15 | settings: 設定 16 | menu: 菜單 17 | printers: 出單機 18 | transit: 資料轉移 19 | orderAttributes: 顧客設定 20 | stockQuantities: 份量 21 | elf: 建議 22 | more: 更多 23 | debug: Debug 24 | dialog: 25 | deletionTitle: 刪除確認通知 26 | deletionContent: |- 27 | 確定要刪除「{name}」嗎? 28 | 29 | {more}此動作將無法復原! 30 | image: 31 | holder: 32 | create: 點選以新增圖片 33 | update: 點擊以更新圖片 34 | btn: 35 | crop: 裁切 36 | gallery: 37 | title: 圖片管理 38 | empty: 點擊開始匯入你的第一張照片! 39 | action: 40 | create: 新增圖片 41 | delete: 刪除 42 | snackbar: 43 | deleteFailed: 有一個或多個圖片沒有刪成功。 44 | selection: 45 | title: 選擇相片 46 | deleteConfirm: |- 47 | 將會刪除 {count} 個圖片 48 | 刪除之後會讓相關產品顯示不到圖片 49 | emptyBody: 50 | title: 哎呀!這裡還是空的 51 | action: 立即設定 52 | btn: 53 | navTo: 查看 54 | signInWith: 55 | google: 使用 Google 登入 56 | semantics: 57 | percentileBar: 目前佔總數的 {percent} 58 | invalid: 59 | integer: 60 | type: "{field}必須是整數" 61 | number: 62 | type: "{field}必須是數字" 63 | positive: "{field}不能為負數" 64 | maximum: "{field}不能超過 {maximum}" 65 | minimum: "{field}不能低於 {minimum}" 66 | string: 67 | empty: "{field}不能為空" 68 | maximum: "{field}不能超過 {maximum} 個字" 69 | singleMonth: 單月 70 | singleWeek: 單週 71 | twoWeeks: 雙週 72 | -------------------------------------------------------------------------------- /assets/l10n/zh/printer.yaml: -------------------------------------------------------------------------------- 1 | $prefix: printer 2 | title: 出單機管理 3 | description: 藍牙連線、出單設定 4 | headerInfo: 出單機 5 | _title: 6 | $prefix: title 7 | create: 新增出單機 8 | update: 編輯出單機 9 | settings: 設定格式 10 | btn: 11 | connect: 建立連線 12 | disconnect: 中斷連線 13 | testPrint: 列印測試 14 | retry: 重新連線 15 | print: 列印 16 | status: 17 | success: 成功連結出單機 18 | connecting: 連線中 19 | standby: 尚未進行連線 20 | printed: 列印完成 21 | name: 22 | - good: 正常 23 | writeFailed: 上次列印失敗 24 | paperNotFound: 缺紙 25 | tooHot: 出單機過熱 26 | lowBattery: 電量不足 27 | printing: 列印中 28 | unknown: 未知 29 | signal: 30 | name: 31 | - good: 良好 32 | normal: 一般 33 | weak: 微弱 34 | scan: 35 | ing: 搜尋藍牙設備中... 36 | count: 搜尋到 {count} 個裝置 37 | retry: 重新搜尋 38 | notFound: 找不到裝置? 39 | error: 40 | notSelect: 尚未選擇裝置 41 | notSupport: 42 | title: 裝置不相容 43 | content: |- 44 | 目前尚未支援此裝置,你可以[聯絡我們](mailto:evanlu361425@gmail.com)以取得協助。 45 | bluetoothOff: 藍牙未開啟 46 | disconnected: 出單機已斷線 47 | timeout: 出單機連線逾時 48 | canceled: 出單機連線請求被中斷 49 | timeoutMore: |- 50 | 可以嘗試以下操作: 51 | • 確認裝置是否開啟(通常裝置會閃爍) 52 | • 確認裝置是否在範圍內 53 | • 重新開啟藍牙 54 | name: 55 | label: 出單機名稱 56 | hint: 例如:廚房的出單機 57 | helper: 位置:{address} 58 | autoConn: 59 | label: 自動連線 60 | helper: 當進入訂單頁時自動連線 61 | meta: 62 | connected: 已連線 63 | exist: 已建立,無法新增 64 | helper: 打開藍牙並確保出單機就在你旁邊 65 | settings: 66 | title: 設定出單機格式 67 | padding: 68 | label: 窄間距 69 | helper: 單子跟單子之間的空白會變少,較省紙張,但是撕紙時要小心 70 | more: 其他更多設定,敬請期待! 71 | receipt: 72 | title: 交易明細 73 | column: 74 | name: 品項 75 | price: 單價 76 | count: 數量 77 | total: 小計 78 | time: 時間 79 | discount: 80 | label: 折扣 81 | origin: 原單價 82 | addOns: 83 | label: 附加 84 | adjustment: 調整金額 85 | total: 總價 86 | paid: 付額 87 | price: 總價 88 | change: 找錢 89 | info: 90 | title: 出單機資訊 91 | name: 名稱 92 | address: 位置 93 | signal: 訊號強度 94 | status: 狀態 95 | -------------------------------------------------------------------------------- /assets/l10n/zh/setting.yaml: -------------------------------------------------------------------------------- 1 | $prefix: setting 2 | tab: 設定 3 | version: 版本:{version} 4 | welcome: HI,{name} 5 | logoutBtn: 登出 6 | elf: 7 | title: 建議 8 | description: 使用 Google 表單提供回饋 9 | content: |- 10 | 覺得這裡還少了什麼嗎? 11 | 歡迎[提供建議](https://forms.gle/R1vZDk9ztQLScUdb9)。 12 | 也可以來看看[排程中的功能](https://github.com/evan361425/flutter-pos-system/milestones)。 13 | feature: 14 | title: 其他設定 15 | description: 外觀、語言、提示 16 | theme: 17 | title: 調色盤 18 | name: 19 | - dark: 暗色模式 20 | light: 日光模式 21 | system: 跟隨系統 22 | language: 23 | title: 語言 24 | checkoutWarning: 25 | title: 收銀機提示 26 | name: 27 | - showAll: 全部顯示 28 | onlyNotEnough: 僅不夠時顯示 29 | hideAll: 全部隱藏 30 | tip: 31 | - showAll: |- 32 | 若使用小錢去找,顯示提示。 33 | 例如 5 塊錢不夠了,開始用 5 個 1 塊去找錢 34 | onlyNotEnough: 當零錢不夠找的時候,顯示提示。 35 | hideAll: 當點餐時,收銀機不會顯示任何提示 36 | orderAwakening: 37 | title: 點餐時不關閉螢幕 38 | description: 若取消,則會根據系統設定時間關閉螢幕 39 | report: 40 | title: 收集錯誤訊息和事件 41 | description: 當應用程式發生錯誤時,寄送錯誤訊息,以幫助應用程式成長 42 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/assets/logo.png -------------------------------------------------------------------------------- /assets/web/tutorial-gs-copy-url.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/assets/web/tutorial-gs-copy-url.gif -------------------------------------------------------------------------------- /devtools_options.yaml: -------------------------------------------------------------------------------- 1 | description: This file stores settings for Dart & Flutter DevTools. 2 | documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states 3 | extensions: 4 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Detailed in [here](https://evan361425.github.io/flutter-pos-system/docs/contribute). 2 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # POS System 2 | 3 | ![Introduction](https://evan361425.github.io/flutter-pos-system/images/index-introduction.png) 4 | 5 | Key features of this POS system: 6 | 7 | - Inventory system: Monitor and manage ingredient inventory. 8 | - Customer information: Capture customer demographics (e.g., age, gender) for analytics. 9 | - Cash register: Simplify daily balance calculations. 10 | - Transit: Export and backup orders, menus, and other data for external use. 11 | - Analytics: Create custom line charts and pie charts for analysis. 12 | - Printer: Print receipts via Bluetooth connection. 13 | 14 | Design Principles: 15 | 16 | - Offline usage: Use the system even without an internet connection. 17 | - Privacy-focused: No personal data is stored remotely, only on your device. 18 | - Responsive Width Design. 19 | 20 | ## Download 21 | 22 | - **Android**: Download from [Google Play](https://play.google.com/store/apps/details?id=com.evanlu.possystem). 23 | - **iOS**: Coming soon. 24 | 25 | ## Contribute 26 | 27 | Want to help make POS System even better? We'd love your help! 28 | POS System is an open-source project that's built one contribution at a time. 29 | Check out the [documentation](https://evan361425.github.io/flutter-pos-system/about/contribute) 30 | to learn how you can make POS System better! 31 | 32 | ## Learn More 33 | 34 | To help you quickly get an [overview of the project's structure](https://evan361425.github.io/flutter-pos-system/about/structure), 35 | we also provide some simple documentation to help you 36 | get involved in the project more quickly. 37 | It's also a great starting point for Flutter beginners! 38 | 39 | ## License 40 | 41 | Please refer to the [LICENSE](LICENSE)。 42 | -------------------------------------------------------------------------------- /docs/README.zh.md: -------------------------------------------------------------------------------- 1 | # POS 系統 2 | 3 | ![Introduction](images/index-introduction.zh.png) 4 | 5 | 本 POS 系統是一個開源的 Flutter 應用程式,專為小型餐飲業設計,其功能包括: 6 | 7 | - 庫存系統:幫助你紀錄現有成份庫存,追蹤每種食材的數量,並在存量不足時收到通知; 8 | - 顧客資訊:可以在點餐時備註使用者的資訊,例如年齡和性別,幫助分析; 9 | - 收銀機:方便做每日結餘,還有幫助統計現有現金數量; 10 | - 匯出與備份:可以將訂單、菜單等資料匯出到應用程式之外; 11 | - 分析:可以根據您的需求,製作客製化的折線圖、圓餅圖以利分析; 12 | - 出單機:允許透過藍牙列印訂單內容。 13 | 14 | 設計核心: 15 | 16 | - 離線使用:即使沒有網路連線,也能照常使用 POS 系統; 17 | - 不會遠端紀錄個資:所有資料都儲存在您的手機裡,不會傳輸到伺服器; 18 | - 支援大螢幕的操作。 19 | 20 | ## 下載 21 | 22 | - Android 可以至 [Google Play](https://play.google.com/store/apps/details?id=com.evanlu.possystem) 下載。 23 | - iOS 要再等等,已排程準備。 24 | 25 | ## 開發 & 貢獻 26 | 27 | 想要幫助 POS 系統更完善?我們很樂意你的幫忙! 28 | POS 系統是一個開源的專案,並且透過大家一點一點的幫助建構出來的。 29 | 查看 [Development](https://evan361425.github.io/flutter-pos-system/zh/maintenance/development/) 30 | 開始在本地端建置和測試。 31 | 查看 [Contributing](https://evan361425.github.io/flutter-pos-system/zh/about/contribute/) 暸解如何讓 POS 系統更好! 32 | 33 | ## 深入瞭解 34 | 35 | 為了加速你暸解本系統的概略架構,我們也提供了一些簡單的[說明文件](https://evan361425.github.io/flutter-pos-system/zh/about/structure.md), 36 | 希望可以讓你更快融入本專案,對於 Flutter 初學者來說,亦是一個不錯的開始! 37 | -------------------------------------------------------------------------------- /docs/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | This is an mobile app project which means we can only fix vulnerability in latest minor version and wish user to update their version... 6 | 7 | ## Reporting a Vulnerability 8 | 9 | Please contact me by email and **DO NOT** open issue or PR for security vulnerability. 10 | 11 | I should reply you in one to two days and work with you for the fix (also with a big hug for your dedication🤗). 12 | -------------------------------------------------------------------------------- /docs/SUPPORT.md: -------------------------------------------------------------------------------- 1 | 相關說明請[至此](https://evan361425.github.io/flutter-pos-system/docs/contribute#你需要任何幫助嗎)查看 2 | -------------------------------------------------------------------------------- /docs/images/cheese-burger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/docs/images/cheese-burger.png -------------------------------------------------------------------------------- /docs/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/docs/images/favicon.ico -------------------------------------------------------------------------------- /docs/images/ham-burger.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/docs/images/ham-burger.jpg -------------------------------------------------------------------------------- /docs/images/index-introduction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/docs/images/index-introduction.png -------------------------------------------------------------------------------- /docs/images/index-introduction.zh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/docs/images/index-introduction.zh.png -------------------------------------------------------------------------------- /docs/images/lang/en-US/rwd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/docs/images/lang/en-US/rwd.png -------------------------------------------------------------------------------- /docs/images/lang/zh-TW/rwd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/docs/images/lang/zh-TW/rwd.png -------------------------------------------------------------------------------- /docs/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/docs/images/logo.png -------------------------------------------------------------------------------- /docs/images/veggie-burger.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/docs/images/veggie-burger.webp -------------------------------------------------------------------------------- /docs/maintenance/bump-dependencies.md: -------------------------------------------------------------------------------- 1 | # Update Dependencies 2 | 3 | There are three types: 4 | 5 | - dependencies: Direct dependencies 6 | - dev_dependencies: Development environment dependencies 7 | - transitive: Dependencies of the dependencies 8 | 9 | ## How to Check Which Packages Need Updating 10 | 11 | ```bash 12 | $ make outdated 13 | Showing outdated packages. 14 | [*] indicates versions that are not the latest available. 15 | 16 | Package Name Current Upgradable Resolvable Latest 17 | 18 | direct dependencies: 19 | some_package *3.2.0 *3.2.0 *3.2.0 4.0.0 20 | 21 | dev_dependencies: 22 | dev_package *1.0.0 *1.0.0 *1.1.0 1.1.0 23 | ``` 24 | 25 | Note the following: 26 | 27 | - `Current`: The current version 28 | - `Upgradable`: The highest version upgradable according to [version constraints](https://dart.dev/tools/pub/dependencies#version-constraints) 29 | - `Resolvable`: The highest version upgradable without conflicting with the current environment (mainly Dart/Flutter version) 30 | - `Latest`: The latest version of the package 31 | 32 | ## How to Upgrade 33 | 34 | After identifying the version to upgrade to: 35 | 36 | flutter pub upgrade some_package 37 | 38 | This method also upgrades transitive dependencies. 39 | 40 | ## After Updating 41 | 42 | Remember to rerun the mock process, as new versions of packages may introduce new APIs: 43 | 44 | make mock 45 | -------------------------------------------------------------------------------- /docs/maintenance/bump-dependencies.zh.md: -------------------------------------------------------------------------------- 1 | # 更新相依套件 2 | 3 | 分三種: 4 | 5 | - dependencies,直接依賴的套件 6 | - dev_dependencies,開發環境依賴的套件 7 | - transitive,依賴套件的依賴套件 8 | 9 | ## 如何查找哪些套件需要更新 10 | 11 | ```bash 12 | $ make outdated 13 | Showing outdated packages. 14 | [*] indicates versions that are not the latest available. 15 | 16 | Package Name Current Upgradable Resolvable Latest 17 | 18 | direct dependencies: 19 | some_package *3.2.0 *3.2.0 *3.2.0 4.0.0 20 | 21 | dev_dependencies: 22 | dev_package *1.0.0 *1.0.0 *1.1.0 1.1.0 23 | ``` 24 | 25 | 但要注意幾件事: 26 | 27 | - `Current` 代表現在的版本 28 | - `Upgradable` 代表依據[版本限制](https://dart.dev/tools/pub/dependencies#version-constraints)所能升級的最高版本 29 | - `Resolvable` 代表在和現有環境(主要是 dart/flutter 版本)不衝突的情況下可升級的最高版本 30 | - `Latest` 代表這個套件目前最新的版本 31 | 32 | ## 如何升級 33 | 34 | 根據上面得到想要升級的版本後 35 | 36 | flutter pub upgrade some_package 37 | 38 | 這樣的方式可以同時升級 Transitive 的套件。 39 | 40 | ## 更新之後 41 | 42 | 請記得重新跑一次 Mock,因為新版本的套件可能會有新的 API: 43 | 44 | make mock 45 | -------------------------------------------------------------------------------- /docs/maintenance/deployment.md: -------------------------------------------------------------------------------- 1 | # Deployment Process 2 | 3 | Divided into three environments (or `lane` in Fastlane): 4 | 5 | - `internal`: For internal testing. 6 | - `beta`: For external testing. The same files will be pushed to `promote_to_production`. 7 | - `promote_to_production`: Pushes the `beta` version to production. 8 | 9 | Deployment steps for each environment are as follows: 10 | 11 | - `internal`: run `make bump-dev` and there will be two different input: 12 | 1. If we are bumping for new version, enter new tag, ex. `1.2.3`. 13 | 2. If we are bumping build code only, enter empty text. 14 | - `beta`: run `make bump`. 15 | - `promote_to_production`: publish the 16 | [draft release](https://github.com/evan361425/flutter-pos-system/releases) on GitHub. 17 | -------------------------------------------------------------------------------- /docs/maintenance/deployment.zh.md: -------------------------------------------------------------------------------- 1 | # 部署流程 2 | 3 | 分為三個環境(或者在 Fastlane 中稱為 `lane`): 4 | 5 | - `internal`:內部測試用。 6 | - `beta`:對外的測試,同樣的檔案會推展到 `promote_to_production`。 7 | - `promote_to_production`:把 `beta` 的版本推到線上。 8 | 9 | 分別的部署方式如下: 10 | 11 | - `internal`:執行 `make bump-dev` 後,可能有兩種輸入: 12 | 1. 如果要使用新的版本,則輸入該版號,例如 `1.2.3`; 13 | 2. 如果要沿用版本號,但是要更新建置號,則輸入空白文字即可。 14 | - `beta`:執行 `make bump`。 15 | - `promote_to_production`:把 GitHub 的 16 | [draft release](https://github.com/evan361425/flutter-pos-system/releases) publish 出來。 17 | -------------------------------------------------------------------------------- /docs/overrides/partials/comments.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /docs/untranslated.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": [ 3 | "orderCheckoutAttributeNoteTitle", 4 | "orderCheckoutAttributeNoteHint" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /docs/unused.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/docs/unused.txt -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "background.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "LaunchImage.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "LaunchImage@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "LaunchImage@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | 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. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | possystem 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | UIStatusBarHidden 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /l10n.yaml: -------------------------------------------------------------------------------- 1 | # https://docs.flutter.dev/development/accessibility-and-localization/internationalization#configuring-the-l10nyaml-file 2 | arb-dir: lib/l10n 3 | template-arb-file: app_zh.arb 4 | output-localization-file: app_localizations.dart 5 | preferred-supported-locales: 6 | - "en" 7 | - "zh" 8 | # - "zh_TW" 9 | # - "zh_Hant" 10 | # - "zh_Hant_TW" 11 | use-deferred-loading: true 12 | untranslated-messages-file: docs/untranslated.json 13 | -------------------------------------------------------------------------------- /lib/components/dialog/confirm_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:possystem/components/style/pop_button.dart'; 3 | 4 | class ConfirmDialog extends StatelessWidget { 5 | const ConfirmDialog({ 6 | super.key, 7 | required this.title, 8 | this.content, 9 | }); 10 | 11 | final String title; 12 | final Widget? content; 13 | 14 | static Future show( 15 | BuildContext context, { 16 | required String title, 17 | String? content, 18 | Widget? body, 19 | }) async { 20 | final result = await showAdaptiveDialog( 21 | context: context, 22 | builder: (_) => ConfirmDialog( 23 | title: title, 24 | content: body ?? (content == null ? null : Text(content)), 25 | ), 26 | ); 27 | 28 | return result ?? false; 29 | } 30 | 31 | @override 32 | Widget build(BuildContext context) { 33 | final local = MaterialLocalizations.of(context); 34 | return AlertDialog.adaptive( 35 | title: Text(title), 36 | content: content == null ? null : SingleChildScrollView(child: content), 37 | actions: [ 38 | PopButton( 39 | key: const Key('confirm_dialog.cancel'), 40 | title: local.cancelButtonLabel, 41 | ), 42 | FilledButton( 43 | key: const Key('confirm_dialog.confirm'), 44 | onPressed: () => Navigator.of(context).pop(true), 45 | child: Text(local.okButtonLabel), 46 | ), 47 | ], 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/components/dialog/dialog_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// A dialog page with Material entrance and exit animations, modal barrier color, 4 | /// and modal barrier behavior (dialog is dismissible with a tap on the barrier). 5 | /// 6 | /// exported from https://croxx5f.hashnode.dev/adding-modal-routes-to-your-gorouter 7 | class MaterialDialogPage extends Page { 8 | final Offset? anchorPoint; 9 | final Color? barrierColor; 10 | final bool barrierDismissible; 11 | final String? barrierLabel; 12 | final bool useSafeArea; 13 | final CapturedThemes? themes; 14 | final TraversalEdgeBehavior? traversalEdgeBehavior; 15 | 16 | /// only if constant child is allowed, otherwise use [builder] 17 | final Widget? child; 18 | 19 | /// if child is constant, use [child] instead 20 | final WidgetBuilder? builder; 21 | 22 | const MaterialDialogPage({ 23 | this.child, 24 | this.builder, 25 | this.anchorPoint, 26 | this.barrierColor = Colors.black54, 27 | this.barrierDismissible = true, 28 | this.barrierLabel, 29 | this.useSafeArea = true, 30 | this.themes, 31 | this.traversalEdgeBehavior, 32 | super.key, 33 | super.name, 34 | super.arguments, 35 | super.restorationId, 36 | }) : assert(child != null || builder != null); 37 | 38 | @override 39 | Route createRoute(BuildContext context) => DialogRoute( 40 | context: context, 41 | settings: this, 42 | builder: builder ?? (context) => child!, 43 | anchorPoint: anchorPoint, 44 | barrierColor: barrierColor, 45 | barrierDismissible: barrierDismissible, 46 | barrierLabel: barrierLabel, 47 | useSafeArea: useSafeArea, 48 | themes: themes, 49 | traversalEdgeBehavior: traversalEdgeBehavior, 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /lib/components/loading_wrapper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class LoadingWrapper extends StatefulWidget { 4 | final Widget child; 5 | 6 | final bool isLoading; 7 | 8 | const LoadingWrapper({ 9 | super.key, 10 | required this.child, 11 | this.isLoading = false, 12 | }); 13 | 14 | @override 15 | State createState() => LoadingWrapperState(); 16 | } 17 | 18 | class LoadingWrapperState extends State { 19 | late bool _isLoading; 20 | 21 | String? _status; 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return Stack(children: [ 26 | widget.child, 27 | if (_isLoading) 28 | Positioned.fill( 29 | child: Container( 30 | color: Theme.of(context).colorScheme.surface.withAlpha(30), 31 | child: Column( 32 | mainAxisAlignment: MainAxisAlignment.center, 33 | children: [ 34 | const CircularProgressIndicator(), 35 | if (_status != null) 36 | DefaultTextStyle( 37 | style: Theme.of(context).textTheme.headlineSmall!, 38 | child: Text(_status!), 39 | ), 40 | ], 41 | ), 42 | ), 43 | ), 44 | ]); 45 | } 46 | 47 | void startLoading([String? status]) { 48 | setState(() { 49 | _status = status; 50 | _isLoading = true; 51 | }); 52 | } 53 | 54 | void setStatus(String? status) { 55 | setState(() { 56 | _status = status; 57 | }); 58 | } 59 | 60 | void finishLoading() { 61 | setState(() { 62 | _status = null; 63 | _isLoading = false; 64 | }); 65 | } 66 | 67 | @override 68 | void initState() { 69 | super.initState(); 70 | _isLoading = widget.isLoading; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/components/meta_block.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:possystem/components/style/hint_text.dart'; 3 | 4 | class MetaBlock { 5 | static TextSpan span() { 6 | return const TextSpan(text: string); 7 | } 8 | 9 | static const string = ' • '; 10 | 11 | /// Divide strings with [MetaBlock] 12 | /// 13 | /// return null if [emptyText] is not provided and [data] is empty 14 | static Widget? withString( 15 | BuildContext context, 16 | Iterable data, { 17 | TextStyle? textStyle, 18 | String? emptyText, 19 | int? maxLines, 20 | TextOverflow textOverflow = TextOverflow.ellipsis, 21 | }) { 22 | if (data.isNotEmpty) { 23 | final children = data 24 | .expand((value) => [ 25 | TextSpan(text: value), 26 | MetaBlock.span(), 27 | ]) 28 | .toList(); 29 | // remove last block 30 | children.removeLast(); 31 | 32 | return RichText( 33 | overflow: textOverflow, 34 | maxLines: maxLines, 35 | text: TextSpan( 36 | children: children, 37 | // disable parent text style 38 | style: textStyle ?? Theme.of(context).textTheme.bodyMedium, 39 | ), 40 | ); 41 | } else if (emptyText != null) { 42 | return RichText(text: HintText.inSpan(context, emptyText)); 43 | } else { 44 | return null; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/components/models/order_attribute_value_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:possystem/components/style/hint_text.dart'; 3 | import 'package:possystem/helpers/util.dart'; 4 | import 'package:possystem/models/objects/order_attribute_object.dart'; 5 | import 'package:possystem/translator.dart'; 6 | 7 | class OrderAttributeValueWidget { 8 | static Widget? build(OrderAttributeMode? mode, num? value) { 9 | if (value == null || mode == null || mode == OrderAttributeMode.statOnly) { 10 | return null; 11 | } 12 | 13 | final name = string(mode, value); 14 | return name == '' ? HintText(S.orderAttributeValueEmpty) : Text(name); 15 | } 16 | 17 | static String string(OrderAttributeMode mode, num value) { 18 | final modeValue = value; 19 | if (mode == OrderAttributeMode.changeDiscount) { 20 | final value = modeValue.toInt(); 21 | return value == 0 ? S.orderAttributeValueFree : 'x $value%'; 22 | } else { 23 | final value = modeValue.toCurrency(); 24 | return modeValue == 0 25 | ? '' 26 | : modeValue > 0 27 | ? '+ \$$value' 28 | : '- \$$value'; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/components/slivers/sliver_image_app_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:possystem/components/style/image_holder.dart'; 3 | import 'package:possystem/models/model.dart'; 4 | 5 | class SliverImageAppBar extends StatelessWidget { 6 | final ModelImage model; 7 | 8 | final List? actions; 9 | 10 | const SliverImageAppBar({ 11 | super.key, 12 | required this.model, 13 | this.actions, 14 | }); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | final background = ImageHolder( 19 | image: model.image, 20 | padding: const EdgeInsets.fromLTRB(0, 36, 0, 0), 21 | // required for the gradient 22 | title: '', 23 | onImageError: () => model.saveImage(null), 24 | ); 25 | 26 | return SliverAppBar( 27 | expandedHeight: 250.0, 28 | pinned: true, 29 | leading: const CloseButton(), 30 | flexibleSpace: FlexibleSpaceBar( 31 | title: Text( 32 | model.name, 33 | style: TextStyle( 34 | color: Theme.of(context).textTheme.bodyMedium!.color, 35 | ), 36 | ), 37 | background: background, 38 | ), 39 | actions: actions, 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/components/style/card_info_text.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CardInfoText extends StatelessWidget { 4 | final Widget child; 5 | 6 | const CardInfoText({ 7 | super.key, 8 | required this.child, 9 | }); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Card( 14 | shape: RoundedRectangleBorder( 15 | side: BorderSide(color: Theme.of(context).colorScheme.outline), 16 | borderRadius: const BorderRadius.all(Radius.circular(12)), 17 | ), 18 | child: ConstrainedBox( 19 | constraints: const BoxConstraints(minHeight: 100), 20 | child: Padding( 21 | padding: const EdgeInsets.all(8.0), 22 | child: Center( 23 | child: child, 24 | ), 25 | ), 26 | ), 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/components/style/circular_loading.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CircularLoading extends StatelessWidget { 4 | const CircularLoading({super.key, this.size}); 5 | 6 | final double? size; 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return Center( 11 | child: SizedBox( 12 | height: size ?? 20, 13 | width: size ?? 20, 14 | child: const Center( 15 | child: CircularProgressIndicator.adaptive(strokeWidth: 2), 16 | ), 17 | ), 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/components/style/date_range_picker.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:possystem/settings/language_setting.dart'; 3 | 4 | /// Show a date range picker dialog but with a slightly different design. 5 | /// 6 | /// Human usually think 5/1~5/2 is two days. 7 | /// Machine usually think 5/1~5/2 is one day (5/1 0:0 ~ 5/2 0:0). 8 | /// So we need to convert between human and machine by adding a day to the end. 9 | Future showMyDateRangePicker(BuildContext context, DateTimeRange range) async { 10 | final end = range.end.subtract(const Duration(days: 1)); 11 | final now = DateTime.now(); 12 | // TODO: using fullscreen and dialog 13 | final result = await showDateRangePicker( 14 | context: context, 15 | initialDateRange: DateTimeRange( 16 | start: range.start, 17 | // must be greater than [lastDate] 18 | end: end.microsecondsSinceEpoch > now.microsecondsSinceEpoch ? now : end, 19 | ), 20 | initialEntryMode: DatePickerEntryMode.calendarOnly, 21 | firstDate: DateTime(2021, 1), 22 | lastDate: now, 23 | locale: LanguageSetting.instance.language.locale, 24 | 25 | /// TODO: should fix this bug 26 | /// Wrapping the design, because the background will use a slightly 27 | /// transparent primary color when selecting a date, which will reduce 28 | /// the expected contrast, making it difficult to see, so adjust the color 29 | /// of onPrimary. 30 | builder: (context, dialog) { 31 | final theme = Theme.of(context); 32 | final colorScheme = theme.colorScheme.copyWith( 33 | onPrimary: theme.textTheme.bodyMedium?.color, 34 | ); 35 | return Theme( 36 | data: theme.copyWith(colorScheme: colorScheme), 37 | child: dialog ?? const SizedBox.shrink(), 38 | ); 39 | }, 40 | ); 41 | 42 | if (result != null) { 43 | return DateTimeRange( 44 | start: result.start, 45 | end: result.end.add(const Duration(days: 1)), 46 | ); 47 | } 48 | 49 | return null; 50 | } 51 | -------------------------------------------------------------------------------- /lib/components/style/empty_body.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:go_router/go_router.dart'; 3 | import 'package:possystem/translator.dart'; 4 | 5 | class EmptyBody extends StatelessWidget { 6 | /// title of the empty body, default: Oops! It's empty here. 7 | final String? title; 8 | 9 | /// content of the empty body 10 | final String? content; 11 | 12 | /// navigate to the route when the button is pressed, either this or [onPressed] must be provided 13 | final String? routeName; 14 | 15 | /// path parameters for the route 16 | final Map pathParameters; 17 | 18 | final VoidCallback? onPressed; 19 | 20 | const EmptyBody({ 21 | super.key, 22 | this.title, 23 | this.content, 24 | this.routeName, 25 | this.pathParameters = const {}, 26 | this.onPressed, 27 | }) : assert(routeName != null || onPressed != null, 'Either routeName or onPressed must be provided'); 28 | 29 | @override 30 | Widget build(BuildContext context) { 31 | return SizedBox( 32 | height: 300, 33 | child: Column( 34 | mainAxisAlignment: MainAxisAlignment.center, 35 | crossAxisAlignment: CrossAxisAlignment.center, 36 | children: [ 37 | Text( 38 | title ?? S.emptyBodyTitle, 39 | style: Theme.of(context).textTheme.titleLarge, 40 | ), 41 | if (content != null) 42 | Padding( 43 | padding: const EdgeInsets.fromLTRB(16, 8.0, 16.0, 8.0), 44 | child: Text(content!), 45 | ), 46 | TextButton( 47 | key: const Key('empty_body'), 48 | onPressed: onPressed ?? () => context.pushNamed(routeName!, pathParameters: pathParameters), 49 | child: Text(S.emptyBodyAction), 50 | ), 51 | ], 52 | ), 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/components/style/footer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:possystem/components/linkify.dart'; 3 | import 'package:possystem/components/meta_block.dart'; 4 | 5 | class Footer extends StatelessWidget { 6 | const Footer({super.key}); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return Wrap(alignment: WrapAlignment.center, crossAxisAlignment: WrapCrossAlignment.center, children: [ 11 | TextButton( 12 | onPressed: _links[0].launch, 13 | child: Text(_links[0].text), 14 | ), 15 | const Text(MetaBlock.string), 16 | TextButton( 17 | onPressed: _links[1].launch, 18 | child: Text(_links[1].text), 19 | ), 20 | ]); 21 | } 22 | } 23 | 24 | const _links = [ 25 | LinkifyData('Privacy Policy', 'https://evan361425.github.io/flutter-pos-system/PRIVACY_POLICY/'), 26 | LinkifyData('License', 'https://evan361425.github.io/flutter-pos-system/LICENSE/'), 27 | ]; 28 | -------------------------------------------------------------------------------- /lib/components/style/gradient_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class GradientButton extends StatelessWidget { 4 | final VoidCallback onPressed; 5 | 6 | final List colors; 7 | 8 | final Widget child; 9 | 10 | const GradientButton({ 11 | super.key, 12 | required this.onPressed, 13 | required this.colors, 14 | required this.child, 15 | }); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return ElevatedButton( 20 | onPressed: onPressed, 21 | style: ElevatedButton.styleFrom(padding: const EdgeInsets.all(0.0)), 22 | child: Ink( 23 | decoration: BoxDecoration( 24 | gradient: LinearGradient( 25 | begin: Alignment.topLeft, 26 | end: Alignment.bottomRight, 27 | colors: colors, 28 | tileMode: TileMode.clamp, 29 | ), 30 | ), 31 | child: Container( 32 | constraints: const BoxConstraints(minWidth: 88.0, minHeight: 36.0), 33 | alignment: Alignment.center, 34 | child: child, 35 | ), 36 | ), 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/components/style/gradient_scroll_hint.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// GradientScrollHint help to show a gradient hint when the content is scrollable. 4 | class GradientScrollHint extends StatelessWidget { 5 | final Axis direction; 6 | 7 | final bool isDialog; 8 | 9 | const GradientScrollHint({ 10 | super.key, 11 | this.direction = Axis.horizontal, 12 | this.isDialog = false, 13 | }); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | final theme = Theme.of(context); 18 | final color = isDialog ? theme.dialogBackgroundColor : theme.scaffoldBackgroundColor; 19 | return IgnorePointer( 20 | child: DecoratedBox( 21 | decoration: BoxDecoration( 22 | gradient: LinearGradient( 23 | begin: direction == Axis.horizontal ? Alignment.centerRight : Alignment.topCenter, 24 | end: direction == Axis.horizontal ? Alignment.centerLeft : Alignment.bottomCenter, 25 | colors: [color.withAlpha(0), color], 26 | ), 27 | ), 28 | ), 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/components/style/head_tail_tile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class HeadTailTile extends StatelessWidget { 4 | final String head; 5 | 6 | final String? tail; 7 | 8 | final Widget? tailWidget; 9 | 10 | final Widget? subtitle; 11 | 12 | const HeadTailTile({ 13 | super.key, 14 | required this.head, 15 | this.tail, 16 | this.tailWidget, 17 | this.subtitle, 18 | }) : assert(tailWidget != null || tail != null); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | final child = Row( 23 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 24 | children: [ 25 | Text(head), 26 | tailWidget ?? Text(tail!), 27 | ], 28 | ); 29 | return Padding( 30 | padding: const EdgeInsetsDirectional.only( 31 | start: 16.0, 32 | end: 24.0, 33 | top: 8.0, 34 | bottom: 8.0, 35 | ), 36 | child: subtitle == null ? child : Column(children: [child, subtitle!]), 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/components/style/hint_text.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class HintText extends StatelessWidget { 4 | final String text; 5 | 6 | final TextOverflow? overflow; 7 | 8 | final TextAlign? textAlign; 9 | 10 | const HintText( 11 | this.text, { 12 | super.key, 13 | this.overflow, 14 | this.textAlign, 15 | }); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | final theme = Theme.of(context); 20 | final style = theme.textTheme.bodySmall!.copyWith( 21 | color: theme.hintColor, 22 | inherit: true, 23 | ); 24 | 25 | return Text( 26 | text, 27 | style: style, 28 | overflow: overflow, 29 | textAlign: textAlign, 30 | ); 31 | } 32 | 33 | static TextSpan inSpan(BuildContext context, String text) { 34 | final theme = Theme.of(context); 35 | final style = theme.textTheme.bodySmall!.copyWith( 36 | color: theme.hintColor, 37 | inherit: true, 38 | ); 39 | 40 | return TextSpan(text: text, style: style); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/components/style/info_popup.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class InfoPopup extends StatelessWidget { 4 | final String message; 5 | 6 | const InfoPopup( 7 | this.message, { 8 | super.key, 9 | }); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Tooltip( 14 | message: message, 15 | triggerMode: TooltipTriggerMode.tap, 16 | showDuration: const Duration(seconds: 30), 17 | margin: const EdgeInsets.symmetric(horizontal: 16.0), 18 | child: const Icon(Icons.help_outline_outlined), 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/components/style/pop_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:go_router/go_router.dart'; 3 | import 'package:possystem/routes.dart'; 4 | 5 | class PopButton extends StatelessWidget { 6 | final String? title; 7 | 8 | final VoidCallback? onPressed; 9 | 10 | const PopButton({ 11 | super.key, 12 | this.title, 13 | this.onPressed, 14 | }); 15 | 16 | static safePop(BuildContext context, {String path = Routes.base, T? value}) { 17 | if (context.mounted) { 18 | context.canPop() ? context.pop(value) : context.go(path); 19 | } 20 | } 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | cb() => onPressed == null ? safePop(context) : onPressed!(); 25 | 26 | if (title != null) { 27 | return TextButton( 28 | onPressed: cb, 29 | child: Text(title!), 30 | ); 31 | } 32 | 33 | return BackButton( 34 | key: const Key('pop'), 35 | onPressed: cb, 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/components/style/search_bar_inline.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:possystem/constants/icons.dart'; 3 | 4 | class SearchBarInline extends StatelessWidget { 5 | final String? text; 6 | final String? labelText; 7 | final String? hintText; 8 | final String? Function(String?)? validator; 9 | final void Function() onTap; 10 | 11 | /// using controller for dynamically change the initialValue 12 | final TextEditingController textController; 13 | 14 | SearchBarInline({ 15 | super.key, 16 | this.text, 17 | this.validator, 18 | this.labelText, 19 | this.hintText, 20 | required this.onTap, 21 | }) : textController = TextEditingController(text: text); 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return Material( 26 | child: TextFormField( 27 | readOnly: true, 28 | enableInteractiveSelection: false, 29 | controller: textController, 30 | onTap: onTap, 31 | validator: validator, 32 | decoration: InputDecoration( 33 | floatingLabelBehavior: FloatingLabelBehavior.always, 34 | border: const OutlineInputBorder(borderSide: BorderSide()), 35 | isDense: true, 36 | labelText: labelText, 37 | hintText: hintText, 38 | focusedBorder: Theme.of(context).inputDecorationTheme.focusedBorder, 39 | errorMaxLines: 2, 40 | prefixIcon: const Icon(KIcons.search), 41 | ), 42 | ), 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/components/style/single_row_warp.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:possystem/constants/constant.dart'; 3 | 4 | class SingleRowWrap extends StatelessWidget { 5 | final List children; 6 | 7 | final Color? color; 8 | 9 | const SingleRowWrap({ 10 | super.key, 11 | required this.children, 12 | this.color, 13 | }); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return Material( 18 | elevation: 1.0, 19 | color: color, 20 | shadowColor: color, 21 | child: SingleChildScrollView( 22 | scrollDirection: Axis.horizontal, 23 | child: Padding( 24 | padding: const EdgeInsets.symmetric(horizontal: kHorizontalSpacing), 25 | child: Wrap( 26 | spacing: kInternalSpacing, 27 | children: children, 28 | ), 29 | ), 30 | ), 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/components/style/slide_to_delete.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:possystem/components/dialog/delete_dialog.dart'; 3 | import 'package:possystem/constants/icons.dart'; 4 | 5 | class SlideToDelete extends StatelessWidget { 6 | final T item; 7 | 8 | final Widget child; 9 | 10 | final Future Function() deleteCallback; 11 | 12 | final Widget? Function(BuildContext context)? warningContentBuilder; 13 | 14 | final Widget? warningContent; 15 | 16 | const SlideToDelete({ 17 | super.key, 18 | required this.item, 19 | required this.child, 20 | required this.deleteCallback, 21 | this.warningContentBuilder, 22 | this.warningContent, 23 | }); 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return Dismissible( 28 | key: ObjectKey(item), 29 | background: Container( 30 | alignment: AlignmentDirectional.centerEnd, 31 | color: const Color(0xFFC62828), 32 | child: const Padding( 33 | padding: EdgeInsets.only(right: 10.0), 34 | child: Icon(KIcons.delete, color: Colors.white), 35 | ), 36 | ), 37 | direction: DismissDirection.endToStart, 38 | onDismissed: (direction) => deleteCallback(), 39 | confirmDismiss: warningContent == null && warningContentBuilder == null 40 | ? null 41 | : (direction) => DeleteDialog.show( 42 | context, 43 | deleteCallback: deleteCallback, 44 | warningContent: warningContent ?? warningContentBuilder!(context), 45 | ), 46 | child: child, 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/components/style/snackbar_actions.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:possystem/helpers/launcher.dart'; 3 | import 'package:possystem/helpers/logger.dart'; 4 | 5 | class LauncherSnackbarAction extends SnackBarAction { 6 | LauncherSnackbarAction({ 7 | super.key, 8 | required super.label, 9 | required String link, 10 | required String logCode, 11 | }) : super(onPressed: () { 12 | Log.ger('launch_snackbar_action', {'code': logCode, 'link': link}); 13 | Launcher.launch(link).ignore(); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /lib/components/style/text_divider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:possystem/constants/constant.dart'; 3 | 4 | class TextDivider extends StatelessWidget { 5 | final String label; 6 | 7 | const TextDivider({ 8 | super.key, 9 | required this.label, 10 | }); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Padding( 15 | padding: const EdgeInsets.symmetric(vertical: kHorizontalSpacing), 16 | child: Row(children: [ 17 | const Expanded( 18 | child: Divider( 19 | indent: kInternalSpacing, 20 | endIndent: kInternalSpacing, 21 | ), 22 | ), 23 | Text(label), 24 | const Expanded( 25 | child: Divider( 26 | indent: kInternalSpacing, 27 | endIndent: kInternalSpacing, 28 | ), 29 | ), 30 | ]), 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/constants/app_themes.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// Want to build your own theme? 4 | /// https://github.com/rxlabz/panache 5 | class AppThemes { 6 | static final ThemeData lightTheme = ThemeData( 7 | useMaterial3: true, 8 | colorSchemeSeed: const Color(0xFF448AFF), 9 | )..setGradientColors(const [ 10 | // Plum Plate 11 | Color(0xFFB0CDFF), 12 | Color(0xFFD8E5FB), 13 | Color(0xFFC3F9FE), 14 | ]); 15 | 16 | static final ThemeData darkTheme = ThemeData.dark(useMaterial3: true) 17 | ..setGradientColors(const [ 18 | Color(0xFF1F1B24), 19 | Color(0xFF2e2356), 20 | ]); 21 | } 22 | 23 | extension GradientColorsTheme on ThemeData { 24 | static final Map> _gradientColors = {}; 25 | 26 | void setGradientColors(List colors) { 27 | _gradientColors[brightness] = colors; 28 | } 29 | 30 | List get gradientColors { 31 | return _gradientColors[brightness]!; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/constants/constant.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | 3 | const double kTopSpacing = 12.0; 4 | const double kHorizontalSpacing = 10.0; 5 | const double kInternalSpacing = 5.5; 6 | const double kInternalLargeSpacing = 12.0; 7 | const double kFABSpacing = 76.0; 8 | const double kDialogBottomSpacing = 24.0; 9 | const bool isLocalTest = String.fromEnvironment('appFlavor') == 'debug'; 10 | const bool isInternalTest = String.fromEnvironment('appFlavor') == 'dev'; 11 | const bool isProd = String.fromEnvironment('appFlavor') == 'prod'; 12 | 13 | /// The time to show the warning message when the bluetooth is not found. 14 | const Duration btSearchWarningTime = kDebugMode ? Duration(milliseconds: 10) : Duration(minutes: 1); 15 | -------------------------------------------------------------------------------- /lib/constants/icons.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class KIcons { 4 | static const add = Icons.add_outlined; 5 | static const cancel = Icons.cancel_outlined; 6 | static const delete = Icons.delete_outlined; 7 | static const reorder = Icons.switch_access_shortcut_outlined; 8 | static const modal = Icons.text_fields_outlined; 9 | static const image = Icons.image_outlined; 10 | 11 | static const entryRemove = Icons.remove_circle_outlined; 12 | static const entryMore = Icons.more_vert_outlined; 13 | static const entryAdd = Icons.add_circle_outline_outlined; 14 | 15 | static const more = Icons.more_horiz_outlined; 16 | static const edit = Icons.edit_outlined; 17 | static const search = Icons.search_outlined; 18 | static const preview = Icons.remove_red_eye_outlined; 19 | 20 | static const warn = Icons.warning_amber_outlined; 21 | } 22 | -------------------------------------------------------------------------------- /lib/debug/debug_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:possystem/components/style/pop_button.dart'; 3 | import 'package:possystem/debug/random_gen_order.dart'; 4 | import 'package:possystem/debug/rerun_migration.dart'; 5 | import 'package:possystem/services/cache.dart'; 6 | 7 | class DebugPage extends StatelessWidget { 8 | const DebugPage({super.key}); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return Scaffold( 13 | appBar: AppBar(title: const Text('Debug'), leading: const PopButton()), 14 | body: ListView( 15 | key: const Key('debug.list'), 16 | children: [ 17 | ListTile( 18 | title: const Text('Generate orders'), 19 | trailing: const Icon(Icons.add_outlined), 20 | onTap: goGenerateRandomOrders(context), 21 | ), 22 | ListTile( 23 | title: const Text('Cache Reset'), 24 | trailing: const Icon(Icons.clear_all_outlined), 25 | onTap: Cache.instance.reset, 26 | ), 27 | const ListTile( 28 | title: Text('Migrate DB Again'), 29 | trailing: Icon(Icons.refresh_outlined), 30 | onTap: rerunMigration, 31 | ) 32 | ], 33 | ), 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/debug/rerun_migration.dart: -------------------------------------------------------------------------------- 1 | import 'package:possystem/services/database.dart'; 2 | 3 | void rerunMigration() async { 4 | await Database.execMigrationAction( 5 | Database.instance.db, 6 | Database.latestVersion, 7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /lib/helpers/analysis/ema_calculator.dart: -------------------------------------------------------------------------------- 1 | class EMACalculator { 2 | final double weightFactor; 3 | 4 | final int length; 5 | 6 | const EMACalculator(this.length) : weightFactor = 2 / (length + 1); 7 | 8 | double calculate(Iterable data) { 9 | double carry = 0; 10 | 11 | for (final value in data) { 12 | carry = feed(value, carry); 13 | } 14 | 15 | return carry; 16 | } 17 | 18 | double feed(num value, double carry) { 19 | if (carry == 0) { 20 | return value.toDouble(); 21 | } 22 | 23 | return value * weightFactor + carry * (1 - weightFactor); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/helpers/exporter/data_exporter.dart: -------------------------------------------------------------------------------- 1 | abstract class DataExporter { 2 | const DataExporter(); 3 | } 4 | -------------------------------------------------------------------------------- /lib/helpers/exporter/plain_text_exporter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | import 'package:possystem/helpers/formatter/formatter.dart'; 3 | import 'package:possystem/helpers/formatter/plain_text_formatter.dart'; 4 | 5 | import 'data_exporter.dart'; 6 | 7 | class PlainTextExporter extends DataExporter { 8 | final PlainTextFormatter formatter; 9 | 10 | const PlainTextExporter({this.formatter = const PlainTextFormatter()}); 11 | 12 | Future export(Formattable able) { 13 | final text = formatter.getRows(able).map((row) => row.join('\n')).join('\n\n'); 14 | return exportToClipboard(text); 15 | } 16 | 17 | Future exportToClipboard(String text) { 18 | return Clipboard.setData(ClipboardData(text: text)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/helpers/launcher.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:url_launcher/url_launcher.dart' as l; 3 | import 'package:url_launcher/url_launcher_string.dart'; 4 | 5 | class Launcher { 6 | @visibleForTesting 7 | static late String lastUrl; 8 | 9 | static Future launch(String url) { 10 | assert(() { 11 | lastUrl = url; 12 | return true; 13 | }()); 14 | return l.launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/models/analysis/analysis.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:possystem/models/analysis/chart.dart'; 3 | import 'package:possystem/models/analysis/chart_object.dart'; 4 | import 'package:possystem/models/repository.dart'; 5 | import 'package:possystem/services/storage.dart'; 6 | 7 | class Analysis extends ChangeNotifier with Repository, RepositoryStorage, RepositoryOrderable { 8 | static late Analysis instance; 9 | 10 | @override 11 | final Stores storageStore = Stores.analysis; 12 | 13 | Analysis() { 14 | instance = this; 15 | } 16 | 17 | @override 18 | Chart buildItem(String id, Map value) { 19 | return Chart.fromObject(ChartObject.build({'id': id, ...value})); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/models/model_object.dart: -------------------------------------------------------------------------------- 1 | /// Use to help I/O with DB. 2 | abstract class ModelObject { 3 | /// It is able to be constant most time. 4 | const ModelObject(); 5 | 6 | /// Help diff from other and get the different properties. 7 | Map diff(T model); 8 | 9 | /// To map format for DB I/O. 10 | Map toMap(); 11 | } 12 | -------------------------------------------------------------------------------- /lib/models/repository/order_attributes.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:possystem/models/objects/order_attribute_object.dart'; 3 | import 'package:possystem/models/order/order_attribute.dart'; 4 | import 'package:possystem/services/storage.dart'; 5 | 6 | import '../repository.dart'; 7 | 8 | class OrderAttributes extends ChangeNotifier 9 | with Repository, RepositoryOrderable, RepositoryStorage { 10 | static late OrderAttributes instance; 11 | 12 | @override 13 | final Stores storageStore = Stores.orderAttributes; 14 | 15 | OrderAttributes() { 16 | instance = this; 17 | } 18 | 19 | List get notEmptyItems => itemList.where((item) => item.isNotEmpty).toList(); 20 | 21 | @override 22 | OrderAttribute buildItem(String id, Map value) { 23 | return OrderAttribute.fromObject(OrderAttributeObject.build({ 24 | 'id': id, 25 | ...value, 26 | })); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/models/repository/quantities.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:possystem/models/objects/stock_object.dart'; 3 | import 'package:possystem/models/repository.dart'; 4 | import 'package:possystem/models/stock/quantity.dart'; 5 | import 'package:possystem/services/storage.dart'; 6 | 7 | class Quantities extends ChangeNotifier 8 | with Repository, RepositoryStorage, RepositorySearchable { 9 | static late Quantities instance; 10 | 11 | @override 12 | final Stores storageStore = Stores.quantities; 13 | 14 | Quantities() { 15 | instance = this; 16 | } 17 | 18 | @override 19 | Quantity buildItem(String id, Map value) { 20 | return Quantity.fromObject( 21 | QuantityObject.build({ 22 | 'id': id, 23 | ...value, 24 | }), 25 | ); 26 | } 27 | 28 | @override 29 | Future commitStaged({bool save = true, bool reset = true}) { 30 | // Avoid reset since it will effect Menu 31 | return super.commitStaged(save: save, reset: false); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/models/repository/replenisher.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:possystem/models/objects/stock_object.dart'; 3 | import 'package:possystem/models/repository.dart'; 4 | import 'package:possystem/models/repository/stock.dart'; 5 | import 'package:possystem/models/stock/replenishment.dart'; 6 | import 'package:possystem/services/storage.dart'; 7 | 8 | class Replenisher extends ChangeNotifier with Repository, RepositoryStorage { 9 | static late Replenisher instance; 10 | 11 | @override 12 | final Stores storageStore = Stores.replenisher; 13 | 14 | Replenisher() { 15 | instance = this; 16 | } 17 | 18 | @override 19 | void abortStaged() { 20 | super.abortStaged(); 21 | Stock.instance.abortStaged(); 22 | } 23 | 24 | @override 25 | Replenishment buildItem(String id, Map value) { 26 | return Replenishment.fromObject( 27 | ReplenishmentObject.build({ 28 | 'id': id, 29 | ...value, 30 | }), 31 | ); 32 | } 33 | 34 | @override 35 | Future commitStaged({bool save = true, bool reset = true}) async { 36 | await Stock.instance.commitStaged(reset: false); 37 | await super.commitStaged(); 38 | } 39 | } 40 | 41 | enum ReplenishBy { quantity, price } 42 | -------------------------------------------------------------------------------- /lib/models/repository/stashed_orders.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:possystem/models/objects/order_object.dart'; 3 | import 'package:possystem/services/database.dart'; 4 | 5 | /// Help I/O from stashed order DB. 6 | class StashedOrders extends ChangeNotifier { 7 | static const table = 'order_stash'; 8 | 9 | static final instance = StashedOrders(); 10 | 11 | /// Stash the order to recover later. 12 | /// 13 | /// It will also save the order attributes. 14 | Future stash(OrderObject order) async { 15 | await Database.instance.push(table, order.toStashMap()); 16 | notifyListeners(); 17 | } 18 | 19 | /// Get the stashed orders. 20 | Future> getItems({ 21 | int offset = 0, 22 | int? limit = 10, 23 | }) async { 24 | final rows = await Database.instance.query( 25 | table, 26 | orderBy: 'createdAt desc', 27 | limit: limit, 28 | offset: offset, 29 | ); 30 | 31 | return rows.map((e) => OrderObject.fromStashMap(e)).toList(); 32 | } 33 | 34 | /// Get the stashed orders. 35 | Future getMetrics() async { 36 | final rows = await Database.instance.query( 37 | table, 38 | columns: ['COUNT(*) count'], 39 | ); 40 | 41 | return StashedOrderMetrics.fromMap(rows.isEmpty ? {} : rows[0]); 42 | } 43 | 44 | /// Get specific stashed order by id and delete that entry. 45 | Future delete(int id) async { 46 | await Database.instance.delete(table, id); 47 | notifyListeners(); 48 | } 49 | } 50 | 51 | /// Metrics from [StashedOrders.getMetrics] 52 | class StashedOrderMetrics { 53 | /// Total count of stashed orders. 54 | final int count; 55 | 56 | const StashedOrderMetrics._({ 57 | required this.count, 58 | }); 59 | 60 | /// Directly from DB data. 61 | factory StashedOrderMetrics.fromMap(Map map) { 62 | return StashedOrderMetrics._( 63 | count: map['count'] as int? ?? 0, 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/models/repository/stock.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:possystem/models/objects/order_object.dart'; 3 | import 'package:possystem/models/objects/stock_object.dart'; 4 | import 'package:possystem/models/stock/ingredient.dart'; 5 | import 'package:possystem/services/storage.dart'; 6 | 7 | import '../repository.dart'; 8 | 9 | class Stock extends ChangeNotifier 10 | with Repository, RepositoryStorage, RepositorySearchable { 11 | static late Stock instance; 12 | 13 | @override 14 | final Stores storageStore = Stores.stock; 15 | 16 | Stock() { 17 | instance = this; 18 | } 19 | 20 | Future applyAmounts( 21 | Map amounts, { 22 | onlyAmount = false, 23 | }) async { 24 | final updateData = {}; 25 | 26 | amounts.forEach((id, amount) { 27 | if (amount != 0) { 28 | getItem(id)?.getUpdateData(amount, onlyAmount: onlyAmount).forEach((key, value) { 29 | updateData[key] = value; 30 | }); 31 | } 32 | }); 33 | 34 | if (updateData.isEmpty) return Future.value(); 35 | 36 | // should use [saveBatch] instead 37 | await Storage.instance.set(Stores.stock, updateData); 38 | 39 | notifyListeners(); 40 | } 41 | 42 | @override 43 | Ingredient buildItem(String id, Map value) { 44 | return Ingredient.fromObject( 45 | IngredientObject.build({ 46 | 'id': id, 47 | ...value, 48 | }), 49 | ); 50 | } 51 | 52 | /// Update amounts by order. 53 | Future order(OrderObject data) async { 54 | final amounts = {}; 55 | 56 | data.applyToStock(amounts, add: false); 57 | 58 | return applyAmounts(amounts, onlyAmount: true); 59 | } 60 | 61 | @override 62 | Future commitStaged({bool save = true, bool reset = true}) { 63 | // Avoid reset since it will effect Menu and Replenishment 64 | return super.commitStaged(save: save, reset: false); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/models/stock/quantity.dart: -------------------------------------------------------------------------------- 1 | import 'package:possystem/models/model.dart'; 2 | import 'package:possystem/models/objects/stock_object.dart'; 3 | import 'package:possystem/models/repository/quantities.dart'; 4 | import 'package:possystem/services/storage.dart'; 5 | 6 | class Quantity extends Model with ModelStorage, ModelSearchable { 7 | /// between 0 ~ 1 8 | num defaultProportion; 9 | 10 | @override 11 | final Stores storageStore = Stores.quantities; 12 | 13 | Quantity({ 14 | super.id, 15 | super.status = ModelStatus.normal, 16 | super.name = 'quantity', 17 | this.defaultProportion = 1, 18 | }); 19 | 20 | factory Quantity.fromObject(QuantityObject object) => Quantity( 21 | id: object.id, 22 | name: object.name!, 23 | defaultProportion: object.defaultProportion!, 24 | ); 25 | 26 | factory Quantity.fromRow(Quantity? ori, List row) { 27 | final p = row.length > 1 ? num.tryParse(row[1]) ?? 1 : 1; 28 | final status = 29 | ori == null ? ModelStatus.staged : (p == ori.defaultProportion ? ModelStatus.normal : ModelStatus.updated); 30 | 31 | return Quantity( 32 | id: ori?.id, 33 | name: row[0], 34 | defaultProportion: p, 35 | status: status, 36 | ); 37 | } 38 | 39 | @override 40 | Quantities get repository => Quantities.instance; 41 | 42 | @override 43 | QuantityObject toObject() => QuantityObject( 44 | id: id, 45 | name: name, 46 | defaultProportion: defaultProportion, 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /lib/models/xfile.dart: -------------------------------------------------------------------------------- 1 | import 'package:file/file.dart'; 2 | import 'package:file/local.dart'; 3 | import 'package:path_provider/path_provider.dart'; 4 | 5 | class XFile { 6 | static FileSystem fs = const LocalFileSystem(); 7 | 8 | final String path; 9 | 10 | const XFile(this.path); 11 | 12 | Directory get dir => fs.directory(path); 13 | 14 | File get file => fs.file(path); 15 | 16 | Future copy(String newPath) => file.copy(newPath); 17 | 18 | static Future createDir(String folder) async { 19 | final directory = await getRootPath(); 20 | 21 | final path = fs.path.join(directory, folder); 22 | 23 | return XFile(path).dir.create(); 24 | } 25 | 26 | static Future getRootPath() async { 27 | return fs is LocalFileSystem ? (await getApplicationDocumentsDirectory()).path : ''; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/services/bluetooth.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:packages/bluetooth.dart' as bt; 4 | 5 | typedef BluetoothSignal = bt.BluetoothSignal; 6 | typedef PrinterStatus = bt.PrinterStatus; 7 | typedef PrinterDensity = bt.PrinterDensity; 8 | typedef BluetoothException = bt.BluetoothException; 9 | typedef BluetoothExceptionCode = bt.BluetoothExceptionCode; 10 | typedef BluetoothExceptionFrom = bt.BluetoothExceptionFrom; 11 | typedef BluetoothOffException = bt.BluetoothOffException; 12 | 13 | class Bluetooth { 14 | static Bluetooth instance = Bluetooth(); 15 | 16 | final bt.Bluetooth blue; 17 | 18 | Bluetooth({bt.Bluetooth? blue}) : blue = blue ?? bt.Bluetooth.i; 19 | 20 | /// Timeout in 3 minutes 21 | Stream> startScan() => blue.startScan(); 22 | 23 | Future stopScan() => blue.stopScan(); 24 | } 25 | -------------------------------------------------------------------------------- /lib/services/cache.dart: -------------------------------------------------------------------------------- 1 | import 'package:shared_preferences/shared_preferences.dart'; 2 | 3 | class Cache { 4 | static Cache instance = Cache(); 5 | 6 | late SharedPreferences service; 7 | 8 | bool _initialized = false; 9 | 10 | T? get(String name) { 11 | if (T == bool) { 12 | return service.getBool(name) as T?; 13 | } else if (T == String) { 14 | return service.getString(name) as T?; 15 | } else if (T == int) { 16 | return service.getInt(name) as T?; 17 | } else if (T == double) { 18 | return service.getDouble(name) as T?; 19 | } else { 20 | throw ArgumentError(); 21 | } 22 | } 23 | 24 | Future initialize() async { 25 | if (_initialized) return; 26 | _initialized = true; 27 | 28 | service = await SharedPreferences.getInstance(); 29 | 30 | final version = service.getInt('version'); 31 | if (version == null) { 32 | await service.setInt('version', 1); 33 | } 34 | } 35 | 36 | Future reset() { 37 | return service.clear(); 38 | } 39 | 40 | Future set(String key, T value) { 41 | if (T == bool) { 42 | return service.setBool(key, value as bool); 43 | } else if (T == String) { 44 | return service.setString(key, value as String); 45 | } else if (T == int) { 46 | return service.setInt(key, value as int); 47 | } else if (T == double) { 48 | return service.setDouble(key, value as double); 49 | } else { 50 | throw ArgumentError(); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/services/image_dumper.dart: -------------------------------------------------------------------------------- 1 | import 'package:image/image.dart'; 2 | import 'package:image_cropper/image_cropper.dart'; 3 | import 'package:image_picker/image_picker.dart' hide XFile; 4 | import 'package:possystem/models/xfile.dart'; 5 | import 'package:possystem/translator.dart'; 6 | 7 | class ImageDumper { 8 | static ImageDumper instance = const ImageDumper._(); 9 | 10 | static ImageCropper cropper = ImageCropper(); 11 | 12 | const ImageDumper._(); 13 | 14 | /// After pick, it is always JPEG image 15 | Future pick() async { 16 | // Pick an image 17 | final image = await ImagePicker().pickImage(source: ImageSource.gallery); 18 | 19 | if (image == null) return null; 20 | 21 | final result = await cropper.cropImage( 22 | sourcePath: image.path, 23 | maxHeight: 512, 24 | maxWidth: 512, 25 | aspectRatio: const CropAspectRatio(ratioX: 1, ratioY: 1), 26 | uiSettings: [AndroidUiSettings(toolbarTitle: S.imageBtnCrop)], 27 | ); 28 | 29 | return result == null ? null : XFile(result.path); 30 | } 31 | 32 | Future resize( 33 | XFile image, 34 | String destination, { 35 | int? width, 36 | int? height, 37 | }) async { 38 | final decodedImage = decodeImage(await image.file.readAsBytes()); 39 | if (decodedImage == null) return null; 40 | 41 | final dst = XFile(destination); 42 | 43 | await dst.file.writeAsBytes(encodeJpg(copyResize( 44 | decodedImage, 45 | width: width, 46 | height: height, 47 | ))); 48 | 49 | return dst; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/settings/checkout_warning.dart: -------------------------------------------------------------------------------- 1 | import 'package:possystem/models/repository/cart.dart'; 2 | import 'package:possystem/settings/setting.dart'; 3 | 4 | class CheckoutWarningSetting extends Setting { 5 | static final instance = CheckoutWarningSetting._(); 6 | 7 | static const defaultValue = CheckoutWarningTypes.showAll; 8 | 9 | CheckoutWarningSetting._() { 10 | value = defaultValue; 11 | } 12 | 13 | // history reason for calling cashier 14 | @override 15 | String get key => 'feat.cashierWarning'; 16 | 17 | @override 18 | void initialize() { 19 | value = CheckoutWarningTypes.values[service.get(key) ?? defaultValue.index]; 20 | } 21 | 22 | @override 23 | Future updateRemotely(CheckoutWarningTypes data) { 24 | return service.set(key, value.index); 25 | } 26 | 27 | CheckoutStatus shouldShow(CheckoutStatus status) { 28 | if (status == CheckoutStatus.ok || value == CheckoutWarningTypes.hideAll) { 29 | return CheckoutStatus.ok; 30 | } 31 | 32 | if (status != CheckoutStatus.cashierNotEnough && value == CheckoutWarningTypes.onlyNotEnough) { 33 | return CheckoutStatus.ok; 34 | } 35 | 36 | return status; 37 | } 38 | } 39 | 40 | enum CheckoutWarningTypes { 41 | /// show all warning 42 | /// 43 | /// when using small amount of money, it will show warning 44 | showAll, 45 | 46 | /// only show when cashier has not enough money 47 | onlyNotEnough, 48 | 49 | /// hide all warning 50 | hideAll, 51 | } 52 | -------------------------------------------------------------------------------- /lib/settings/collect_events_setting.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_analytics/firebase_analytics.dart'; 2 | import 'package:firebase_in_app_messaging/firebase_in_app_messaging.dart'; 3 | import 'package:possystem/helpers/logger.dart'; 4 | import 'package:possystem/settings/setting.dart'; 5 | 6 | class CollectEventsSetting extends Setting { 7 | static final instance = CollectEventsSetting._(); 8 | 9 | static const defaultValue = true; 10 | 11 | CollectEventsSetting._() { 12 | value = defaultValue; 13 | } 14 | 15 | @override 16 | String get key => 'feat.collectEvents'; 17 | 18 | @override 19 | void initialize() { 20 | value = service.get(key) ?? defaultValue; 21 | } 22 | 23 | @override 24 | Future updateRemotely(bool data) async { 25 | Log.allowSendEvents = data; 26 | 27 | // Do it first to make testing easier, because the rest future will not 28 | // complete. 29 | await service.set(key, data); 30 | await Future.wait([ 31 | FirebaseInAppMessaging.instance.setAutomaticDataCollectionEnabled(data), 32 | FirebaseAnalytics.instance.setAnalyticsCollectionEnabled(data), 33 | ]); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/settings/language_setting.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:collection/collection.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:possystem/settings/setting.dart'; 6 | 7 | /// Language setting allow given null language which means system default. 8 | class LanguageSetting extends Setting { 9 | Language? _systemLanguage; 10 | 11 | static final instance = LanguageSetting._(); 12 | 13 | LanguageSetting._() { 14 | value = null; 15 | } 16 | 17 | @override 18 | final String key = 'language'; 19 | 20 | @override 21 | bool get registryForApp => true; 22 | 23 | /// Set system language for fallback. 24 | /// 25 | /// This is not idempotent, it will only set once. 26 | set systemLanguage(String locale) { 27 | _systemLanguage ??= parseLanguage(locale)!; 28 | } 29 | 30 | Language get language => value ?? _systemLanguage ?? Language.en; 31 | 32 | @override 33 | void initialize() { 34 | value = parseLanguage(service.get(key)); 35 | notifyListeners(); 36 | } 37 | 38 | @override 39 | Future updateRemotely(Language? data) { 40 | return service.set(key, data?.locale.toString() ?? ''); 41 | } 42 | 43 | Language? parseLanguage(String? value) { 44 | if (value == null || value.isEmpty) return null; 45 | 46 | final codes = value.split('_'); 47 | 48 | return Language.values.firstWhereOrNull((e) => e.locale.languageCode == codes[0]); 49 | } 50 | } 51 | 52 | enum Language { 53 | zhTW(Locale('zh', 'TW'), '繁體中文'), 54 | en(Locale('en'), 'English'); 55 | 56 | final Locale locale; 57 | 58 | final String title; 59 | 60 | const Language(this.locale, this.title); 61 | } 62 | -------------------------------------------------------------------------------- /lib/settings/order_awakening_setting.dart: -------------------------------------------------------------------------------- 1 | import 'package:possystem/settings/setting.dart'; 2 | 3 | class OrderAwakeningSetting extends Setting { 4 | static final instance = OrderAwakeningSetting._(); 5 | 6 | static const defaultValue = true; 7 | 8 | OrderAwakeningSetting._() { 9 | value = defaultValue; 10 | } 11 | 12 | @override 13 | String get key => 'feat.orderAwakening'; 14 | 15 | @override 16 | void initialize() { 17 | value = service.get(key) ?? defaultValue; 18 | } 19 | 20 | @override 21 | Future updateRemotely(bool data) { 22 | return service.set(key, data); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/settings/setting.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:possystem/helpers/logger.dart'; 3 | import 'package:possystem/services/cache.dart'; 4 | 5 | abstract class Setting extends ChangeNotifier { 6 | late T value; 7 | 8 | String get key; 9 | 10 | String get logKey => key.replaceAll('.', '_'); 11 | 12 | /// Whether the app should be rebuilt when the setting is changed 13 | /// 14 | /// e.g. theme, language 15 | bool get registryForApp => false; 16 | 17 | Cache get service => Cache.instance; 18 | 19 | void initialize(); 20 | 21 | Future update(T data) async { 22 | if (value == data) return; 23 | 24 | Log.ger('user_setting', {'key': logKey, 'value': data.toString()}); 25 | value = data; 26 | 27 | notifyListeners(); 28 | 29 | await updateRemotely(data); 30 | Log.out('finish setting', 'user_setting'); 31 | } 32 | 33 | Future updateRemotely(T data); 34 | } 35 | -------------------------------------------------------------------------------- /lib/settings/settings_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:possystem/settings/checkout_warning.dart'; 3 | import 'package:possystem/settings/collect_events_setting.dart'; 4 | 5 | import 'currency_setting.dart'; 6 | import 'language_setting.dart'; 7 | import 'order_awakening_setting.dart'; 8 | import 'setting.dart'; 9 | import 'theme_setting.dart'; 10 | 11 | class SettingsProvider extends ChangeNotifier { 12 | static SettingsProvider instance = SettingsProvider._(); 13 | 14 | final settings = List.from([ 15 | LanguageSetting.instance, 16 | ThemeSetting.instance, 17 | CurrencySetting.instance, 18 | OrderAwakeningSetting.instance, 19 | CheckoutWarningSetting.instance, 20 | CollectEventsSetting.instance, 21 | ], growable: false); 22 | 23 | SettingsProvider._(); 24 | 25 | void initialize() { 26 | for (var setting in settings) { 27 | setting.initialize(); 28 | if (setting.registryForApp) { 29 | setting.addListener(notifyListeners); 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/settings/theme_setting.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:possystem/settings/setting.dart'; 3 | 4 | class ThemeSetting extends Setting { 5 | static final instance = ThemeSetting._(); 6 | 7 | static const defaultValue = ThemeMode.system; 8 | 9 | ThemeSetting._() { 10 | value = defaultValue; 11 | } 12 | 13 | @override 14 | String get key => 'theme'; 15 | 16 | @override 17 | bool get registryForApp => true; 18 | 19 | @override 20 | void initialize() { 21 | value = ThemeMode.values[service.get(key) ?? defaultValue.index]; 22 | } 23 | 24 | @override 25 | Future updateRemotely(ThemeMode data) { 26 | return service.set(key, value.index); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/translator.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 2 | import 'package:flutter_gen/gen_l10n/app_localizations_en.dart'; 3 | import 'package:intl/date_symbol_data_local.dart'; 4 | import 'package:intl/intl.dart'; 5 | 6 | AppLocalizations S = setAppLocalizations(AppLocalizationsEn()); 7 | 8 | AppLocalizations setAppLocalizations(AppLocalizations localizations) { 9 | S = localizations; 10 | Intl.systemLocale = localizations.localeName; 11 | Intl.defaultLocale = localizations.localeName; 12 | initializeDateFormatting(localizations.localeName); 13 | return localizations; 14 | } 15 | -------------------------------------------------------------------------------- /lib/ui/analysis/widgets/chart_reorder.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:possystem/components/scaffold/reorderable_scaffold.dart'; 3 | import 'package:possystem/models/analysis/analysis.dart'; 4 | import 'package:possystem/models/analysis/chart.dart'; 5 | import 'package:possystem/translator.dart'; 6 | 7 | class ChartReorder extends StatelessWidget { 8 | const ChartReorder({super.key}); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return ReorderableScaffold( 13 | items: Analysis.instance.itemList, 14 | title: S.analysisChartTitleReorder, 15 | handleSubmit: (List items) => Analysis.instance.reorderItems(items), 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/ui/analysis/widgets/history_order_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:go_router/go_router.dart'; 3 | import 'package:intl/intl.dart'; 4 | import 'package:possystem/components/meta_block.dart'; 5 | import 'package:possystem/components/models/order_loader.dart'; 6 | import 'package:possystem/models/objects/order_object.dart'; 7 | import 'package:possystem/routes.dart'; 8 | import 'package:possystem/translator.dart'; 9 | 10 | class HistoryOrderList extends StatelessWidget { 11 | final ValueNotifier notifier; 12 | 13 | const HistoryOrderList({ 14 | super.key, 15 | required this.notifier, 16 | }); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return OrderLoader( 21 | builder: _buildOrder, 22 | ranger: notifier, 23 | ); 24 | } 25 | 26 | Widget _buildOrder(BuildContext context, OrderObject order) { 27 | final subtitle = MetaBlock.withString(context, [ 28 | S.analysisHistoryOrderListMetaId(order.id.toString()), 29 | S.analysisHistoryOrderListMetaPaid(order.paid), 30 | S.analysisHistoryOrderListMetaPrice(order.price), 31 | S.analysisHistoryOrderListMetaProfit(order.profit), 32 | ]); 33 | 34 | return ListTile( 35 | key: Key('history.order.${order.id}'), 36 | leading: Padding( 37 | padding: const EdgeInsets.only(top: 8.0), 38 | child: Text(DateFormat.Hm(S.localeName).format(order.createdAt)), 39 | ), 40 | title: MetaBlock.withString( 41 | context, 42 | order.products 43 | .map((product) => product.count == 1 ? product.productName : '${product.productName} * ${product.count}'), 44 | ), 45 | subtitle: subtitle, 46 | onTap: () => context.pushNamed( 47 | Routes.historyOrder, 48 | pathParameters: {'id': order.id?.toString() ?? ''}, 49 | ), 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/ui/cashier/widgets/unit_list_tile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:possystem/components/dialog/slider_text_dialog.dart'; 3 | import 'package:possystem/components/style/percentile_bar.dart'; 4 | import 'package:possystem/helpers/util.dart'; 5 | import 'package:possystem/helpers/validator.dart'; 6 | import 'package:possystem/models/objects/cashier_object.dart'; 7 | import 'package:possystem/models/repository/cashier.dart'; 8 | import 'package:possystem/translator.dart'; 9 | 10 | class UnitListTile extends StatelessWidget { 11 | final CashierUnitObject item; 12 | final int index; 13 | 14 | const UnitListTile({ 15 | super.key, 16 | required this.item, 17 | required this.index, 18 | }); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | final max = Cashier.instance.defaultAt(index)?.count ?? 0; 23 | return ListTile( 24 | title: Text(S.cashierUnitLabel(item.unit.toCurrencyLong())), 25 | subtitle: PercentileBar(item.count, max), 26 | onTap: () => _setUnitCount(context, item.unit, max, item.count), 27 | ); 28 | } 29 | 30 | Future _setUnitCount( 31 | BuildContext context, 32 | num unit, 33 | num max, 34 | int value, 35 | ) async { 36 | final result = await showDialog( 37 | context: context, 38 | builder: (BuildContext context) => SliderTextDialog( 39 | value: value, 40 | max: max.toDouble(), 41 | title: Text(S.cashierUnitLabel(unit.toCurrency())), 42 | validator: Validator.positiveInt(S.cashierCounterLabel), 43 | decoration: InputDecoration(label: Text(S.cashierCounterLabel)), 44 | ), 45 | ); 46 | 47 | if (result != null) { 48 | await Cashier.instance.setUnitCount(unit, int.tryParse(result) ?? 0); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/ui/home/elf_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:possystem/components/linkify.dart'; 3 | import 'package:possystem/constants/constant.dart'; 4 | import 'package:possystem/translator.dart'; 5 | 6 | class ElfPage extends StatelessWidget { 7 | const ElfPage({super.key}); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | // vertical center 12 | return Center( 13 | child: SingleChildScrollView( 14 | child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ 15 | Container( 16 | decoration: const BoxDecoration( 17 | // moon white 18 | color: Color(0xFFF4F6F0), 19 | shape: BoxShape.circle, 20 | ), 21 | child: Image.asset( 22 | 'assets/feature_request_please.gif', 23 | key: const Key('elf_page'), 24 | ), 25 | ), 26 | const SizedBox(height: 14.0), 27 | Linkify.fromString( 28 | S.settingElfContent, 29 | textAlign: TextAlign.center, 30 | ), 31 | const SizedBox(height: kFABSpacing), 32 | ]), 33 | ), 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/ui/home/widgets/feature_switch.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class FeatureSwitch extends StatefulWidget { 4 | final bool value; 5 | 6 | final Function(bool) onChanged; 7 | 8 | final bool autofocus; 9 | 10 | const FeatureSwitch({ 11 | super.key, 12 | required this.value, 13 | required this.onChanged, 14 | this.autofocus = false, 15 | }); 16 | 17 | @override 18 | State createState() => _FeatureSwitchState(); 19 | } 20 | 21 | class _FeatureSwitchState extends State { 22 | late bool isEnable; 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return Switch( 27 | value: isEnable, 28 | autofocus: widget.autofocus, 29 | onChanged: (value) { 30 | widget.onChanged(value); 31 | setState(() => isEnable = value); 32 | }); 33 | } 34 | 35 | @override 36 | void initState() { 37 | isEnable = widget.value; 38 | super.initState(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/ui/menu/widgets/catalog_reorder.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:possystem/components/scaffold/reorderable_scaffold.dart'; 3 | import 'package:possystem/models/menu/catalog.dart'; 4 | import 'package:possystem/models/repository/menu.dart'; 5 | import 'package:possystem/translator.dart'; 6 | 7 | class CatalogReorder extends StatelessWidget { 8 | const CatalogReorder({super.key}); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return ReorderableScaffold( 13 | items: Menu.instance.itemList, 14 | title: S.menuCatalogTitleReorder, 15 | handleSubmit: (List items) => Menu.instance.reorderItems(items), 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/ui/menu/widgets/product_ingredient_reorder.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:possystem/components/scaffold/reorderable_scaffold.dart'; 3 | import 'package:possystem/models/menu/product.dart'; 4 | import 'package:possystem/models/menu/product_ingredient.dart'; 5 | import 'package:possystem/translator.dart'; 6 | 7 | class ProductIngredientReorder extends StatelessWidget { 8 | final Product product; 9 | 10 | const ProductIngredientReorder(this.product, {super.key}); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return ReorderableScaffold( 15 | items: product.itemList, 16 | title: S.menuIngredientTitleReorder, 17 | handleSubmit: (List items) => product.reorderItems(items), 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/ui/menu/widgets/product_reorder.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:possystem/components/scaffold/reorderable_scaffold.dart'; 3 | import 'package:possystem/models/menu/catalog.dart'; 4 | import 'package:possystem/models/menu/product.dart'; 5 | import 'package:possystem/translator.dart'; 6 | 7 | class ProductReorder extends StatelessWidget { 8 | final Catalog catalog; 9 | 10 | const ProductReorder(this.catalog, {super.key}); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return ReorderableScaffold( 15 | items: catalog.itemList, 16 | title: S.menuProductTitleReorder, 17 | handleSubmit: (List items) => catalog.reorderItems(items), 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/ui/order/cart/cart_metadata_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:possystem/components/meta_block.dart'; 3 | import 'package:possystem/helpers/util.dart'; 4 | import 'package:possystem/models/repository/cart.dart'; 5 | import 'package:possystem/translator.dart'; 6 | import 'package:possystem/ui/order/cart/cart_actions.dart'; 7 | import 'package:provider/provider.dart'; 8 | 9 | class CartMetadataView extends StatelessWidget { 10 | const CartMetadataView({super.key}); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | final cart = context.watch(); 15 | 16 | return Row(children: [ 17 | const SizedBox(width: 16.0), 18 | const CartActions(), 19 | const SizedBox(width: 16.0), 20 | Expanded( 21 | key: const Key('cart.metadata'), 22 | child: MetaBlock.withString(context, [ 23 | S.orderCartMetaTotalCount(cart.productCount), 24 | S.orderCartMetaTotalPrice(cart.productsPrice.toCurrency()), 25 | ])!, 26 | ), 27 | const SizedBox(width: 16.0), 28 | ]); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/ui/order/cart/cart_product_selector.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:possystem/models/repository/cart.dart'; 3 | import 'package:possystem/translator.dart'; 4 | 5 | /// Select all or toggle all products in the cart. 6 | class CartProductSelector extends StatelessWidget { 7 | const CartProductSelector({super.key}); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Row(children: [ 12 | const SizedBox(width: 16.0), 13 | Expanded( 14 | child: OutlinedButton( 15 | key: const Key('cart.select_all'), 16 | style: OutlinedButton.styleFrom( 17 | shape: RoundedRectangleBorder( 18 | borderRadius: BorderRadius.circular(8), 19 | ), 20 | ), 21 | onPressed: () => Cart.instance.toggleAll(true), 22 | child: Text(S.orderCartActionSelectAll), 23 | ), 24 | ), 25 | const SizedBox(width: 4.0), 26 | Expanded( 27 | child: OutlinedButton( 28 | key: const Key('cart.toggle_all'), 29 | style: OutlinedButton.styleFrom( 30 | shape: RoundedRectangleBorder( 31 | borderRadius: BorderRadius.circular(8), 32 | ), 33 | ), 34 | onPressed: () => Cart.instance.toggleAll(null), 35 | child: Text(S.orderCartActionToggle), 36 | ), 37 | ), 38 | const SizedBox(width: 16.0), 39 | ]); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/ui/order/widgets/orientated_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class OrientatedView extends StatelessWidget { 4 | final Widget row1; 5 | 6 | final Widget row2; 7 | 8 | final Widget row3_1; 9 | final Widget row3_2; 10 | final Widget row3_3; 11 | 12 | final Widget row4; 13 | 14 | const OrientatedView({ 15 | super.key, 16 | required this.row1, 17 | required this.row2, 18 | required this.row3_1, 19 | required this.row3_2, 20 | required this.row3_3, 21 | required this.row4, 22 | }); 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return Row( 27 | // crossAxisAlignment: CrossAxisAlignment.stretch, 28 | children: [ 29 | ConstrainedBox( 30 | constraints: const BoxConstraints(maxWidth: 400.0), 31 | child: Column( 32 | children: [ 33 | Expanded(child: wrapRow3(context)), 34 | row4, 35 | ], 36 | ), 37 | ), 38 | Expanded( 39 | child: Column( 40 | crossAxisAlignment: CrossAxisAlignment.stretch, 41 | children: [ 42 | ColoredBox( 43 | color: Theme.of(context).colorScheme.surface, 44 | child: row1, 45 | ), 46 | Expanded(child: row2), 47 | ], 48 | ), 49 | ), 50 | ], 51 | ); 52 | } 53 | 54 | Widget wrapRow3(BuildContext context) { 55 | return Card( 56 | shape: const RoundedRectangleBorder( 57 | borderRadius: BorderRadius.all(Radius.circular(6.0)), 58 | ), 59 | color: Theme.of(context).colorScheme.surface, 60 | child: Column( 61 | crossAxisAlignment: CrossAxisAlignment.stretch, 62 | children: [ 63 | row3_1, 64 | row3_2, 65 | row3_3, 66 | ], 67 | ), 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/ui/order_attr/widgets/order_attribute_option_reorder.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:possystem/components/scaffold/reorderable_scaffold.dart'; 3 | import 'package:possystem/models/order/order_attribute.dart'; 4 | import 'package:possystem/models/order/order_attribute_option.dart'; 5 | import 'package:possystem/translator.dart'; 6 | 7 | class OrderAttributeOptionReorder extends StatelessWidget { 8 | final OrderAttribute attribute; 9 | 10 | const OrderAttributeOptionReorder({ 11 | super.key, 12 | required this.attribute, 13 | }); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return ReorderableScaffold( 18 | items: attribute.itemList, 19 | title: S.orderAttributeOptionTitleReorder, 20 | handleSubmit: (items) => attribute.reorderItems(items), 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/ui/order_attr/widgets/order_attribute_reorder.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:possystem/components/scaffold/reorderable_scaffold.dart'; 3 | import 'package:possystem/models/order/order_attribute.dart'; 4 | import 'package:possystem/models/repository/order_attributes.dart'; 5 | import 'package:possystem/translator.dart'; 6 | 7 | class OrderAttributeReorder extends StatelessWidget { 8 | const OrderAttributeReorder({super.key}); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return ReorderableScaffold( 13 | items: OrderAttributes.instance.itemList, 14 | title: S.orderAttributeTitleReorder, 15 | handleSubmit: (List items) => OrderAttributes.instance.reorderItems(items), 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/ui/printer/printer_settings_modal.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:go_router/go_router.dart'; 3 | import 'package:possystem/components/scaffold/item_modal.dart'; 4 | import 'package:possystem/components/style/hint_text.dart'; 5 | import 'package:possystem/constants/constant.dart'; 6 | import 'package:possystem/models/printer.dart'; 7 | import 'package:possystem/services/bluetooth.dart'; 8 | import 'package:possystem/translator.dart'; 9 | 10 | class PrinterSettingsModal extends StatefulWidget { 11 | const PrinterSettingsModal({super.key}); 12 | 13 | @override 14 | State createState() => _PrinterSettingsModalState(); 15 | } 16 | 17 | class _PrinterSettingsModalState extends State with ItemModal { 18 | late PrinterDensity density; 19 | 20 | @override 21 | String get title => S.printerSettingsTitle; 22 | 23 | @override 24 | List buildFormFields() { 25 | return [ 26 | SwitchListTile( 27 | title: Text(S.printerSettingsPaddingLabel), 28 | subtitle: Text(S.printerSettingsPaddingHelper), 29 | value: density == PrinterDensity.tight, 30 | onChanged: (value) => setState(() => density = value ? PrinterDensity.tight : PrinterDensity.normal), 31 | ), 32 | const SizedBox(height: kInternalLargeSpacing), 33 | Center(child: HintText(S.printerSettingsMore)), 34 | ]; 35 | } 36 | 37 | @override 38 | initState() { 39 | density = Printers.instance.density; 40 | super.initState(); 41 | } 42 | 43 | @override 44 | Future updateItem() async { 45 | Printers.instance.density = density; 46 | await Printers.instance.saveProperties(); 47 | 48 | if (mounted && context.canPop()) { 49 | context.pop(); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/ui/stock/quantities_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:possystem/components/style/empty_body.dart'; 3 | import 'package:possystem/components/style/route_buttons.dart'; 4 | import 'package:possystem/constants/icons.dart'; 5 | import 'package:possystem/models/repository/quantities.dart'; 6 | import 'package:possystem/routes.dart'; 7 | import 'package:possystem/translator.dart'; 8 | 9 | import 'widgets/stock_quantity_list.dart'; 10 | 11 | class QuantitiesPage extends StatelessWidget { 12 | const QuantitiesPage({super.key}); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return ListenableBuilder( 17 | key: const Key('quantities_page'), 18 | listenable: Quantities.instance, 19 | builder: (context, child) => _buildBody(context), 20 | ); 21 | } 22 | 23 | Widget _buildBody(BuildContext context) { 24 | if (Quantities.instance.isEmpty) { 25 | return EmptyBody( 26 | content: S.stockQuantityEmptyBody, 27 | routeName: Routes.quantityCreate, 28 | ); 29 | } 30 | 31 | return StockQuantityList( 32 | quantities: Quantities.instance.itemList, 33 | leading: Row(children: [ 34 | Expanded( 35 | child: RouteElevatedIconButton( 36 | key: const Key('quantity.add'), 37 | route: Routes.quantityCreate, 38 | label: S.stockQuantityTitleCreate, 39 | icon: const Icon(KIcons.add), 40 | ), 41 | ), 42 | ]), 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/ui/stock/widgets/replenishment_apply.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:go_router/go_router.dart'; 3 | import 'package:possystem/components/dialog/responsive_dialog.dart'; 4 | import 'package:possystem/components/style/card_info_text.dart'; 5 | import 'package:possystem/models/stock/replenishment.dart'; 6 | import 'package:possystem/translator.dart'; 7 | 8 | class ReplenishmentPreviewPage extends StatelessWidget { 9 | final Replenishment item; 10 | 11 | const ReplenishmentPreviewPage(this.item, {super.key}); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return ResponsiveDialog( 16 | title: Text(item.name), 17 | action: TextButton( 18 | key: const Key('repl.apply'), 19 | onPressed: () async { 20 | await item.apply(); 21 | if (context.mounted && context.canPop()) { 22 | context.pop(true); 23 | } 24 | }, 25 | child: Text(S.stockReplenishmentApplyConfirmButton), 26 | ), 27 | content: Column(children: [ 28 | CardInfoText(child: Text(S.stockReplenishmentApplyConfirmHint)), 29 | DataTable(columns: [ 30 | DataColumn(label: Text(S.stockReplenishmentApplyConfirmColumn('name'))), 31 | DataColumn(numeric: true, label: Text(S.stockReplenishmentApplyConfirmColumn('amount'))) 32 | ], rows: [ 33 | for (final entry in item.ingredientData.entries) 34 | DataRow(cells: [ 35 | DataCell(Text(entry.key.name)), 36 | DataCell(Text(entry.value.toString())), 37 | ]), 38 | ]), 39 | ]), 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/ui/transit/google_sheet/views.dart: -------------------------------------------------------------------------------- 1 | export 'export_basic_view.dart'; 2 | export 'export_order_view.dart'; 3 | export 'import_basic_view.dart'; 4 | -------------------------------------------------------------------------------- /lib/ui/transit/plain_text/views.dart: -------------------------------------------------------------------------------- 1 | export 'export_basic_view.dart'; 2 | export 'export_order_view.dart'; 3 | export 'import_basic_view.dart'; 4 | -------------------------------------------------------------------------------- /lib/ui/transit/previews/ingredient_preview_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:possystem/components/meta_block.dart'; 3 | import 'package:possystem/models/stock/ingredient.dart'; 4 | import 'package:possystem/translator.dart'; 5 | 6 | import 'preview_page.dart'; 7 | 8 | class IngredientPreviewPage extends PreviewPage { 9 | const IngredientPreviewPage({ 10 | super.key, 11 | required super.items, 12 | }); 13 | 14 | @override 15 | Widget getItem(BuildContext context, Ingredient item) { 16 | return ListTile( 17 | title: ImporterColumnStatus( 18 | name: item.name, 19 | status: item.statusName, 20 | ), 21 | subtitle: MetaBlock.withString(context, [ 22 | S.transitImportPreviewIngredientMetaAmount(item.currentAmount), 23 | S.transitImportPreviewIngredientMetaMaxAmount(item.totalAmount == null ? 0 : 1, item.totalAmount ?? 0), 24 | ]), 25 | ); 26 | } 27 | 28 | @override 29 | Widget getHeader(BuildContext context) { 30 | return Text(S.transitImportPreviewIngredientHeader); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/ui/transit/previews/order_attribute_preview_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:possystem/components/meta_block.dart'; 3 | import 'package:possystem/components/models/order_attribute_value_widget.dart'; 4 | import 'package:possystem/components/style/outlined_text.dart'; 5 | import 'package:possystem/models/order/order_attribute.dart'; 6 | import 'package:possystem/translator.dart'; 7 | 8 | import 'preview_page.dart'; 9 | 10 | class OrderAttributePreviewPage extends PreviewPage { 11 | const OrderAttributePreviewPage({ 12 | super.key, 13 | required super.items, 14 | }); 15 | 16 | @override 17 | Widget getItem(BuildContext context, OrderAttribute item) { 18 | final mode = S.orderAttributeModeName(item.mode.name); 19 | final defaultName = item.defaultOption?.name ?? S.orderAttributeMetaNoDefault; 20 | return ExpansionTile( 21 | title: ImporterColumnStatus( 22 | name: item.name, 23 | status: item.statusName, 24 | ), 25 | subtitle: MetaBlock.withString(context, [ 26 | S.orderAttributeMetaMode(mode), 27 | S.orderAttributeMetaDefault(defaultName), 28 | ]), 29 | expandedCrossAxisAlignment: CrossAxisAlignment.stretch, 30 | children: [ 31 | for (final option in item.items) 32 | ListTile( 33 | title: Text(option.name), 34 | subtitle: OrderAttributeValueWidget.build(option.mode, option.modeValue), 35 | trailing: option.isDefault ? OutlinedText(S.orderAttributeOptionMetaDefault) : null, 36 | ), 37 | ], 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/ui/transit/previews/quantity_preview_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:possystem/components/meta_block.dart'; 3 | import 'package:possystem/models/stock/quantity.dart'; 4 | import 'package:possystem/translator.dart'; 5 | 6 | import 'preview_page.dart'; 7 | 8 | class QuantityPreviewPage extends PreviewPage { 9 | const QuantityPreviewPage({ 10 | super.key, 11 | required super.items, 12 | }); 13 | 14 | @override 15 | Widget getItem(BuildContext context, Quantity item) { 16 | return ListTile( 17 | title: ImporterColumnStatus( 18 | name: item.name, 19 | status: item.statusName, 20 | ), 21 | subtitle: MetaBlock.withString(context, [ 22 | S.stockQuantityMetaProportion(item.defaultProportion), 23 | ]), 24 | ); 25 | } 26 | 27 | @override 28 | Widget getHeader(BuildContext context) { 29 | return Text(S.transitImportPreviewQuantityHeader); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/ui/transit/previews/replenishment_preview_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:possystem/components/meta_block.dart'; 3 | import 'package:possystem/models/repository/stock.dart'; 4 | import 'package:possystem/models/stock/replenishment.dart'; 5 | import 'package:possystem/translator.dart'; 6 | 7 | import 'preview_page.dart'; 8 | 9 | class ReplenishmentPreviewPage extends PreviewPage { 10 | const ReplenishmentPreviewPage({ 11 | super.key, 12 | required super.items, 13 | }); 14 | 15 | @override 16 | Widget getItem( 17 | BuildContext context, 18 | Replenishment item, 19 | ) { 20 | return ExpansionTile( 21 | title: ImporterColumnStatus( 22 | name: item.name, 23 | status: item.statusName, 24 | ), 25 | subtitle: Text(S.stockReplenishmentMetaAffect(item.data.length)), 26 | childrenPadding: const EdgeInsets.symmetric(horizontal: 8.0), 27 | children: _getData(context, item).toList(), 28 | ); 29 | } 30 | 31 | Iterable _getData(BuildContext context, Replenishment item) sync* { 32 | for (var entry in item.data.entries) { 33 | final ingredient = (Stock.instance.getItem(entry.key) ?? Stock.instance.getStaged(entry.key)); 34 | final amount = (entry.value > 0 ? '+' : '') + entry.value.toString(); 35 | 36 | yield MetaBlock.withString(context, [ 37 | ingredient?.name ?? 'unknown', 38 | amount, 39 | ])!; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/ui/transit/transit_order_range.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:possystem/components/style/date_range_picker.dart'; 3 | import 'package:possystem/helpers/util.dart'; 4 | import 'package:possystem/translator.dart'; 5 | 6 | class TransitOrderRange extends StatefulWidget { 7 | final ValueNotifier notifier; 8 | 9 | const TransitOrderRange({ 10 | super.key, 11 | required this.notifier, 12 | }); 13 | 14 | @override 15 | State createState() => _TransitOrderRangeState(); 16 | } 17 | 18 | class _TransitOrderRangeState extends State { 19 | @override 20 | Widget build(BuildContext context) { 21 | return ListTile( 22 | key: const Key('btn.edit_range'), 23 | title: Text(S.transitOrderMetaRange(range.format(S.localeName))), 24 | subtitle: Text(S.transitOrderMetaRangeDays(range.duration.inDays)), 25 | onTap: pickRange, 26 | trailing: const Icon(Icons.date_range_outlined), 27 | ); 28 | } 29 | 30 | DateTimeRange get range => widget.notifier.value; 31 | 32 | void pickRange() async { 33 | final result = await showMyDateRangePicker(context, range); 34 | 35 | if (result != null) { 36 | _updateRange(result.start, result.end); 37 | } 38 | } 39 | 40 | void _updateRange(DateTime start, DateTime end) { 41 | setState(() { 42 | widget.notifier.value = DateTimeRange(start: start, end: end); 43 | }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /release.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "categories": [ 3 | { 4 | "title": "## 🚀 Features", 5 | "labels": ["enhancement"] 6 | }, 7 | { 8 | "title": "## 🐛 Fixes", 9 | "labels": ["bug"] 10 | }, 11 | { 12 | "title": "## 📦 Others", 13 | "labels": [] 14 | } 15 | ], 16 | "ignore_labels": ["ignore"], 17 | "sort": "ASC", 18 | "pr_template": "- ${{TITLE}} #${{NUMBER}} : ${{AUTHOR}}", 19 | "empty_template": "Caught some bugs, time to clean up!", 20 | "max_tags_to_fetch": 10, 21 | "max_pull_requests": 20, 22 | "max_back_track_time_days": 365, 23 | "exclude_merge_branches": [], 24 | "tag_resolver": { 25 | "method": "semver" 26 | }, 27 | "base_branches": ["master"] 28 | } 29 | -------------------------------------------------------------------------------- /scripts/Tra-Chi.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evan361425/flutter-pos-system/c91f99036ce680833911877a2efd3a6579889dc3/scripts/Tra-Chi.ttf -------------------------------------------------------------------------------- /scripts/bump-after.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | repository=evan361425/flutter-pos-system 6 | 7 | function trim_last_newline() { 8 | printf '%s' "$1" 9 | } 10 | 11 | function find_release() { 12 | local tag=$1 13 | 14 | gh api "repos/$repository/releases" \ 15 | | jq -c '.[] | select( .name | contains("'"$tag"'"))' 16 | } 17 | 18 | function get_key() { 19 | local key 20 | set +e 21 | key="$(cat .gen-ai.key 2> /dev/null)" 22 | set -e 23 | 24 | if [ -z "$key" ]; then 25 | echo "Please create a file named .gen-ai.key with your Google Gemini API key." 26 | exit 1 27 | fi 28 | 29 | trim_last_newline "$key" 30 | } 31 | 32 | function main() { 33 | local tag=$1 buildCode=$2 release tmpl 34 | tmpl="android/fastlane/metadata/android/%s/changelogs/$buildCode.txt" 35 | 36 | release=$(find_release "$tag") 37 | 38 | trim_last_newline "$(echo "$release" | jq -r .body)" \ 39 | | tr -d '\r' \ 40 | | sed 's/## //g' \ 41 | > "$(printf "$tmpl" "en-US")" 42 | 43 | bash android/fastlane/translate-changelog.sh "$tmpl" "$(get_key)" 44 | } 45 | 46 | main "$@" 47 | -------------------------------------------------------------------------------- /scripts/check-unused-lang.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | dst_file=docs/unused.txt 4 | tmp_file=tmp_unused 5 | 6 | # reset destination file 7 | true > $dst_file 8 | 9 | echo "Start check unused lang" 10 | 11 | char='a' 12 | 13 | printf 'Finish process char...' 14 | 15 | while read -r key; do 16 | rm -f $tmp_file 17 | 18 | # shellcheck disable=SC2038 19 | find ./lib -type f -name '*.dart' \ 20 | | xargs -I '{}' -P 8 bash -c "\ 21 | test ! -f $tmp_file && grep -i -q $key {} && touch $tmp_file 22 | " 23 | 24 | if test ! -f "$tmp_file"; then 25 | echo "$key" | tee -a $dst_file 26 | fi 27 | 28 | first_char=${key:0:1} 29 | if test ! "$first_char" = "$char"; then 30 | printf "%s" "$char" 31 | char=$first_char 32 | fi 33 | done <<< "$(jq -r 'keys[]' lib/l10n/app_zh.arb | grep -v '^@')" 34 | 35 | rm -f $tmp_file 36 | 37 | printf "\nDone!\n" 38 | -------------------------------------------------------------------------------- /test/app_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_core/firebase_core.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mockito/mockito.dart'; 4 | import 'package:possystem/app.dart'; 5 | import 'package:possystem/models/printer.dart'; 6 | import 'package:possystem/models/repository/menu.dart'; 7 | import 'package:possystem/models/repository/order_attributes.dart'; 8 | import 'package:possystem/settings/settings_provider.dart'; 9 | import 'package:provider/provider.dart'; 10 | 11 | import 'mocks/mock_cache.dart'; 12 | import 'test_helpers/firebase_mocker.dart'; 13 | 14 | void main() { 15 | testWidgets('MyApp should execute onGenerateTitle', (tester) async { 16 | when(cache.get(any)).thenReturn(null); 17 | when(cache.get('tutorial.home.menu')).thenReturn(true); 18 | when(cache.get('tutorial.home.exporter')).thenReturn(true); 19 | when(cache.get('tutorial.home.order_attr')).thenReturn(true); 20 | await Firebase.initializeApp(); 21 | 22 | final app = MultiProvider( 23 | providers: [ 24 | ChangeNotifierProvider.value(value: SettingsProvider.instance), 25 | ChangeNotifierProvider.value(value: Menu()), 26 | ChangeNotifierProvider.value(value: OrderAttributes()), 27 | ChangeNotifierProvider.value(value: Printers()), 28 | ], 29 | builder: (_, __) => const App(), 30 | ); 31 | 32 | await tester.pumpWidget(app); 33 | await tester.pump(const Duration(milliseconds: 50)); 34 | }); 35 | 36 | setUpAll(() { 37 | initializeCache(); 38 | setupFirebaseAuthMocks(); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /test/components/bottom_sheet_actions_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:possystem/components/bottom_sheet_actions.dart'; 4 | 5 | import '../test_helpers/translator.dart'; 6 | 7 | void main() { 8 | group('Widget BottomSheetActions', () { 9 | testWidgets('should cancelable', (WidgetTester tester) async { 10 | await tester.pumpWidget(MaterialApp(home: _ExampleWidget())); 11 | 12 | await tester.tap(find.text('hi')); 13 | await tester.pumpAndSettle(); 14 | await tester.tap(find.byIcon(Icons.cancel_outlined)); 15 | await tester.pumpAndSettle(); 16 | 17 | expect(find.byIcon(Icons.cancel_outlined), findsNothing); 18 | }); 19 | 20 | setUpAll(() { 21 | initializeTranslator(); 22 | }); 23 | }); 24 | } 25 | 26 | class _ExampleWidget extends StatelessWidget { 27 | @override 28 | Widget build(BuildContext context) { 29 | return TextButton( 30 | onPressed: () => showCircularBottomSheet(context, actions: []), 31 | child: const Text('hi'), 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/components/imageable_container_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:possystem/components/imageable_container.dart'; 4 | 5 | import '../test_helpers/breakpoint_mocker.dart'; 6 | 7 | void main() { 8 | group('Imageable Container', () { 9 | testWidgets('should render correctly', (tester) async { 10 | deviceAs(Device.mobile, tester); 11 | 12 | final controller = ImageableManger.instance.create(); 13 | 14 | await tester.pumpWidget(MaterialApp( 15 | theme: ThemeData.light(), 16 | home: Scaffold( 17 | body: ImageableContainer( 18 | controller: controller, 19 | children: const [ 20 | Text('Hello World'), 21 | ], 22 | ), 23 | ), 24 | )); 25 | await tester.pumpAndSettle(); 26 | 27 | final image = await tester.runAsync(() => controller.toImage(widths: [128])); 28 | expect(image, isNotNull); 29 | 30 | const width = 128 ~/ 8; 31 | final data = image!.first.toGrayScale().toBitMap(invert: false, mirrored: false).bytes; 32 | var line = ''; 33 | for (var i = 0; i < data.length ~/ width; i++) { 34 | for (var j = 0; j < width; j++) { 35 | line += data[i * width + j].toRadixString(2).padLeft(8, '0'); 36 | } 37 | line += '\n'; 38 | } 39 | 40 | expect(line.split('\n').first.length, 128); 41 | }); 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /test/components/linkify_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:possystem/components/linkify.dart'; 4 | import 'package:possystem/helpers/launcher.dart'; 5 | 6 | void main() { 7 | group('Widget Linkify', () { 8 | testWidgets('should launch', (tester) async { 9 | WidgetsFlutterBinding.ensureInitialized(); 10 | const link = 'any-link'; 11 | 12 | const LinkifyData('test', link).launch(); 13 | 14 | expect(Launcher.lastUrl, equals(link)); 15 | }); 16 | 17 | testWidgets('mailto and http should be ok', (tester) async { 18 | WidgetsFlutterBinding.ensureInitialized(); 19 | 20 | final widget = Linkify.fromString(''' 21 | and [Hello Mail](mailto:abc@mail.com) 22 | and [Hello HTTP](http://example.com) 23 | and [Hello HTTPS](https://example.com) 24 | '''); 25 | final data = widget.data.toList(); 26 | 27 | expect(data[1].link, equals('mailto:abc@mail.com')); 28 | expect(data[1].text, equals('Hello Mail')); 29 | expect(data[3].link, equals('http://example.com')); 30 | expect(data[3].text, equals('Hello HTTP')); 31 | expect(data[5].link, equals('https://example.com')); 32 | expect(data[5].text, equals('Hello HTTPS')); 33 | }); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /test/components/loading_wrapper_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:possystem/components/loading_wrapper.dart'; 4 | 5 | void main() { 6 | group('Widget LoadingWrapper', () { 7 | testWidgets('should show status', (tester) async { 8 | final loader = GlobalKey(); 9 | await tester.pumpWidget(Material( 10 | child: MaterialApp( 11 | home: LoadingWrapper( 12 | key: loader, 13 | isLoading: true, 14 | child: const Text('hi'), 15 | ), 16 | ), 17 | )); 18 | 19 | loader.currentState?.setStatus('Hello World'); 20 | await tester.pump(); 21 | 22 | expect(find.text('Hello World'), findsOneWidget); 23 | }); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /test/components/tutorial_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:mockito/mockito.dart'; 4 | import 'package:possystem/components/tutorial.dart'; 5 | 6 | import '../mocks/mock_cache.dart'; 7 | 8 | void main() { 9 | group('Tutorial', () { 10 | testWidgets('should setup cache after tutorial', (tester) async { 11 | when(cache.get(any)).thenReturn(null); 12 | when(cache.set(any, true)).thenAnswer((_) => Future.value(true)); 13 | 14 | const widgets = Column( 15 | children: [ 16 | Tutorial( 17 | id: '1', 18 | title: 'title1', 19 | message: 'message1', 20 | child: Text('1'), 21 | ), 22 | ], 23 | ); 24 | 25 | await tester.pumpWidget(const MaterialApp( 26 | home: TutorialWrapper(child: Scaffold(body: widgets)), 27 | )); 28 | await tester.pumpAndSettle(); 29 | 30 | // show spotlight 31 | await tester.pump(const Duration(milliseconds: 5)); 32 | verify(cache.get('tutorial.1')); 33 | 34 | await tester.tapAt(const Offset(100, 100)); 35 | verify(cache.set('tutorial.1', true)); 36 | }); 37 | }); 38 | 39 | setUpAll(() { 40 | Tutorial.debug = true; 41 | initializeCache(); 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /test/debug/rerun_migration_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:possystem/debug/rerun_migration.dart'; 3 | import 'package:possystem/services/database.dart'; 4 | import 'package:possystem/services/database_migration_actions.dart'; 5 | 6 | import '../services/database_test.mocks.dart'; 7 | 8 | void main() { 9 | group('DEBUG', () { 10 | test('Rerun the migration test', () async { 11 | dbMigrationActions.remove(Database.latestVersion); 12 | Database.instance.db = MockDatabase(); 13 | 14 | rerunMigration(); 15 | }); 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /test/helpers/logger_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_core/firebase_core.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:possystem/helpers/logger.dart'; 4 | 5 | import '../test_helpers/firebase_mocker.dart'; 6 | 7 | void main() { 8 | group('Logger', () { 9 | testWidgets('Should do things on crashlytics', (tester) async { 10 | final crashlytics = setupFirebaseCrashlyticsMocks(); 11 | await Firebase.initializeApp(); 12 | 13 | Log.err(Exception('hi'), 'there', null, true); 14 | await tester.pumpAndSettle(); 15 | 16 | expect(crashlytics.methodCalls[0], equals('Crashlytics#recordError')); 17 | }); 18 | 19 | testWidgets('Should do things on analytics', (tester) async { 20 | final analytics = setupFirebaseAnalyticsMocks(); 21 | await Firebase.initializeApp(); 22 | 23 | Log.ger('hi', {'test': '1', 'key': 2}, true); 24 | await tester.pumpAndSettle(); 25 | 26 | expect(analytics['methods']![0], equals('Analytics#logEvent')); 27 | }); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /test/helpers/setup_example_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:mockito/mockito.dart'; 3 | import 'package:possystem/helpers/setup_example.dart'; 4 | import 'package:possystem/models/repository/menu.dart'; 5 | import 'package:possystem/models/repository/order_attributes.dart'; 6 | import 'package:possystem/models/repository/quantities.dart'; 7 | import 'package:possystem/models/repository/stock.dart'; 8 | 9 | import '../mocks/mock_storage.dart'; 10 | import '../test_helpers/translator.dart'; 11 | 12 | void main() { 13 | group('Setup Menu', () { 14 | test('Should add once', () async { 15 | when(storage.add(any, any, any)).thenAnswer((_) => Future.value()); 16 | 17 | await setupExampleMenu(); 18 | await setupExampleOrderAttrs(); 19 | verify(storage.add(any, any, any)); 20 | 21 | await setupExampleMenu(); 22 | await setupExampleOrderAttrs(); 23 | verifyNever(storage.add(any, any, any)); 24 | }); 25 | 26 | setUpAll(() { 27 | initializeStorage(); 28 | initializeTranslator(); 29 | Menu(); 30 | Stock(); 31 | Quantities(); 32 | OrderAttributes(); 33 | }); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /test/helpers/util_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:intl/date_symbol_data_local.dart'; 4 | import 'package:possystem/helpers/util.dart'; 5 | 6 | void main() { 7 | group('Util', () { 8 | test('#uuidV4', () { 9 | final id1 = Util.uuidV4(); 10 | final id2 = Util.uuidV4(); 11 | expect(id1, isNot(equals(id2))); 12 | }); 13 | 14 | test('#toUTC', () { 15 | expect( 16 | Util.toUTC(now: DateTime.utc(2021, 6, 14, 2, 59, 33)), 17 | equals(1623639573), 18 | ); 19 | 20 | final date = Util.toUTC(hour: 0); 21 | final utc = DateTime.now().timeZoneOffset.inSeconds + date; 22 | // should be 0 o'clock 23 | expect(utc / 86400, equals((utc / 86400).floor())); 24 | }); 25 | 26 | test('#fromUTC', () { 27 | final date = Util.fromUTC(1623639573).toUtc(); 28 | expect(date.year, equals(2021)); 29 | expect(date.month, equals(6)); 30 | expect(date.day, equals(14)); 31 | expect(date.hour, equals(2)); 32 | expect(date.minute, equals(59)); 33 | expect(date.second, equals(33)); 34 | }); 35 | 36 | test('#formatCompact', () { 37 | initializeDateFormatting('en', null); 38 | DateTimeRange range = Util.getDateRange(now: DateTime.utc(2021, 6, 14), days: 3); 39 | 40 | expect(range.formatCompact('en'), equals('20210614 - 20210616')); 41 | }); 42 | 43 | testWidgets('#handleSnapshot error', (WidgetTester tester) async { 44 | Object? gotten; 45 | final f = Util.handleSnapshot((context, data) => const SizedBox.shrink(), onError: (err) => gotten = err); 46 | const err = AsyncSnapshot.withError(ConnectionState.done, 'test'); 47 | 48 | await tester.pumpWidget(MaterialApp( 49 | home: Builder(builder: (BuildContext context) => f(context, err)), 50 | )); 51 | 52 | expect(find.text('test'), findsOneWidget); 53 | expect(gotten, equals('test')); 54 | }); 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /test/mocks/mock_auth.dart: -------------------------------------------------------------------------------- 1 | import 'package:http/http.dart'; 2 | import 'package:mockito/annotations.dart'; 3 | import 'package:possystem/services/auth.dart'; 4 | 5 | import 'mock_auth.mocks.dart'; 6 | 7 | final auth = MockAuth(); 8 | 9 | @GenerateMocks([Auth, Client]) 10 | void initializeAuth() { 11 | Auth.instance = auth; 12 | } 13 | -------------------------------------------------------------------------------- /test/mocks/mock_bluetooth.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:mockito/annotations.dart'; 5 | import 'package:mockito/mockito.dart'; 6 | import 'package:packages/bluetooth.dart' as bt; 7 | import 'package:possystem/components/imageable_container.dart'; 8 | import 'package:possystem/services/bluetooth.dart'; 9 | 10 | import 'mock_bluetooth.mocks.dart'; 11 | 12 | final blue = MockBluetooth(); 13 | 14 | @GenerateMocks([ 15 | bt.Bluetooth, 16 | bt.Printer, 17 | bt.BluetoothDevice, 18 | bt.PrinterManufactory, 19 | ImageableManger, 20 | ImageableController, 21 | ]) 22 | void initializeBlue() { 23 | Bluetooth.instance = Bluetooth(blue: blue); 24 | } 25 | 26 | MockImageableController prepareImageable([Future?>? result]) { 27 | final manger = ImageableManger.instance = MockImageableManger(); 28 | final controller = MockImageableController(); 29 | when(manger.create()).thenReturn(controller); 30 | when(controller.key).thenReturn(GlobalKey()); 31 | when(controller.toImage(widths: anyNamed('widths'))) 32 | .thenAnswer((_) => result ?? Future.value([ConvertibleImage(Uint8List(4), width: 1)])); 33 | 34 | return controller; 35 | } 36 | 37 | void resetImageable() { 38 | ImageableManger.instance = ImageableManger(); 39 | } 40 | -------------------------------------------------------------------------------- /test/mocks/mock_cache.dart: -------------------------------------------------------------------------------- 1 | import 'package:mockito/annotations.dart'; 2 | import 'package:possystem/services/cache.dart'; 3 | 4 | import 'mock_cache.mocks.dart'; 5 | 6 | final cache = MockCache(); 7 | 8 | @GenerateMocks([Cache]) 9 | void initializeCache() { 10 | Cache.instance = cache; 11 | } 12 | -------------------------------------------------------------------------------- /test/mocks/mock_database.dart: -------------------------------------------------------------------------------- 1 | // database must seperate with storage, since there have same Database dependecy 2 | 3 | import 'package:mockito/annotations.dart'; 4 | import 'package:possystem/services/database.dart'; 5 | import 'package:sqflite/sqflite.dart' show DatabaseExecutor, Batch; 6 | 7 | import 'mock_database.mocks.dart'; 8 | 9 | final database = MockDatabase(); 10 | 11 | @GenerateMocks([Database, DatabaseExecutor, Batch]) 12 | void _initialize() { 13 | Database.instance = database; 14 | _finished = true; 15 | } 16 | 17 | var _finished = false; 18 | void initializeDatabase() { 19 | if (!_finished) { 20 | _initialize(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/mocks/mock_google_api.dart: -------------------------------------------------------------------------------- 1 | import 'package:googleapis/drive/v3.dart' as gd; 2 | import 'package:googleapis/sheets/v4.dart' as gs; 3 | import 'package:mockito/annotations.dart'; 4 | 5 | import 'mock_google_api.mocks.dart'; 6 | 7 | @GenerateMocks([gd.DriveApi, gd.FilesResource]) 8 | CustomMockDriveApi getMockDriveApi() { 9 | return CustomMockDriveApi(); 10 | } 11 | 12 | @GenerateMocks([ 13 | gs.SheetsApi, 14 | gs.SpreadsheetsResource, 15 | gs.SpreadsheetsValuesResource, 16 | ]) 17 | CustomMockSheetsApi getMockSheetsApi() { 18 | return CustomMockSheetsApi(); 19 | } 20 | 21 | class CustomMockDriveApi extends MockDriveApi { 22 | final MockFilesResource mockFiles = MockFilesResource(); 23 | 24 | @override 25 | MockFilesResource get files => mockFiles; 26 | } 27 | 28 | class CustomMockSheetsApi extends MockSheetsApi { 29 | final mockSpreadsheets = CustomMockSpreadsheetsResource(); 30 | 31 | @override 32 | CustomMockSpreadsheetsResource get spreadsheets => mockSpreadsheets; 33 | } 34 | 35 | class CustomMockSpreadsheetsResource extends MockSpreadsheetsResource { 36 | final mockSpreadsheetsValues = MockSpreadsheetsValuesResource(); 37 | 38 | @override 39 | MockSpreadsheetsValuesResource get values => mockSpreadsheetsValues; 40 | } 41 | -------------------------------------------------------------------------------- /test/mocks/mock_helpers.dart: -------------------------------------------------------------------------------- 1 | import 'package:image_cropper/image_cropper.dart'; 2 | import 'package:mockito/annotations.dart'; 3 | 4 | import 'package:possystem/services/image_dumper.dart'; 5 | import 'mock_helpers.mocks.dart'; 6 | 7 | final imageCropper = MockImageCropper(); 8 | 9 | @GenerateMocks([ImageCropper]) 10 | void initializeImageCropper() { 11 | ImageDumper.cropper = imageCropper; 12 | } 13 | -------------------------------------------------------------------------------- /test/mocks/mock_storage.dart: -------------------------------------------------------------------------------- 1 | import 'package:mockito/annotations.dart'; 2 | import 'package:possystem/services/storage.dart'; 3 | 4 | import 'mock_storage.mocks.dart'; 5 | 6 | final storage = MockStorage(); 7 | 8 | @GenerateMocks([Storage]) 9 | void _initializeStorage() { 10 | Storage.instance = storage; 11 | _finished = true; 12 | } 13 | 14 | var _finished = false; 15 | void initializeStorage() { 16 | if (!_finished) { 17 | _initializeStorage(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/models/object_test.dart: -------------------------------------------------------------------------------- 1 | void main() {} 2 | -------------------------------------------------------------------------------- /test/models/xfile_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:file/local.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:possystem/models/xfile.dart'; 5 | // ignore: depend_on_referenced_packages 6 | import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; 7 | // ignore: depend_on_referenced_packages 8 | import 'package:path_provider_platform_interface/src/method_channel_path_provider.dart'; 9 | 10 | import '../test_helpers/file_mocker.dart'; 11 | 12 | void main() { 13 | group('XFile', () { 14 | testWidgets('#getRootPath', (tester) async { 15 | XFile.fs = const LocalFileSystem(); 16 | final provider = MethodChannelPathProvider(); 17 | PathProviderPlatform.instance = provider; 18 | 19 | tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( 20 | provider.methodChannel, 21 | (MethodCall methodCall) { 22 | expect(methodCall.method, 'getApplicationDocumentsDirectory'); 23 | return Future.value(''); 24 | }, 25 | ); 26 | 27 | await XFile.getRootPath(); 28 | 29 | initializeFileSystem(); 30 | }); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /test/settings/currency_settting_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:mockito/mockito.dart'; 3 | import 'package:possystem/settings/currency_setting.dart'; 4 | 5 | import '../mocks/mock_cache.dart'; 6 | 7 | void main() { 8 | group('Currency Setting', () { 9 | test('set', () { 10 | when(cache.set(any, any)).thenAnswer((_) => Future.value(true)); 11 | 12 | CurrencySetting.instance.updateRemotely(CurrencyTypes.usd); 13 | 14 | verify(cache.set('currency', 1)); 15 | }); 16 | 17 | test('initialize', () { 18 | when(cache.get(any)).thenReturn(1); 19 | 20 | CurrencySetting.instance.initialize(); 21 | 22 | expect(CurrencySetting.instance.isInt, false); 23 | }); 24 | 25 | setUpAll(() { 26 | initializeCache(); 27 | }); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /test/settings/language_setting_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:possystem/settings/language_setting.dart'; 3 | 4 | void main() { 5 | group('Language Setting', () { 6 | test('Parse language', () { 7 | final l = LanguageSetting.instance; 8 | expect(l.parseLanguage(''), isNull); 9 | expect(l.parseLanguage('something'), equals(null)); 10 | expect(l.parseLanguage('zh'), equals(Language.zhTW)); 11 | expect(l.parseLanguage('zh_TW'), equals(Language.zhTW)); 12 | expect(l.parseLanguage('zh_Hant'), equals(Language.zhTW)); 13 | expect(l.parseLanguage('zh_Hant_TW'), equals(Language.zhTW)); 14 | }); 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /test/test_helpers/breakpoint_mocker.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | void deviceAs(Device device, WidgetTester tester) { 5 | tester.view.physicalSize = Size(device.width, device.height); 6 | 7 | // resets the screen to its original size after the test end 8 | addTearDown(tester.view.resetPhysicalSize); 9 | } 10 | 11 | enum Device { 12 | compact(500, 1200), 13 | mobile(800, 1500), 14 | landscape(1024, 768), 15 | desktop(1800, 900); 16 | 17 | final double width; 18 | final double height; 19 | 20 | const Device(double width, double height) 21 | // devicePixelRatio = 3.0 22 | : width = width * 3.0, 23 | height = height * 3.0; 24 | } 25 | -------------------------------------------------------------------------------- /test/test_helpers/translator.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_gen/gen_l10n/app_localizations_en.dart'; 2 | import 'package:intl/date_symbol_data_local.dart'; 3 | import 'package:intl/intl.dart'; 4 | import 'package:possystem/translator.dart'; 5 | 6 | var _initialized = false; 7 | 8 | void initializeTranslator() { 9 | if (!_initialized) { 10 | _initialized = true; 11 | S = AppLocalizationsEn(); 12 | Intl.systemLocale = S.localeName; 13 | Intl.defaultLocale = S.localeName; 14 | initializeDateFormatting(S.localeName); 15 | } 16 | } 17 | --------------------------------------------------------------------------------