Repository: simplezhli/flutter_deer Branch: master Commit: 94d0620d2825 Files: 347 Total size: 1.5 MB Directory structure: gitextract_mfrrwjd0/ ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ └── deer-issue-template.md │ └── workflows/ │ ├── flutter-drive.yml │ └── flutter-web-deploy.yml ├── .gitignore ├── .metadata ├── LICENSE ├── README-EN.md ├── README.md ├── analysis_options.yaml ├── android/ │ ├── app/ │ │ ├── build.gradle │ │ ├── key.properties │ │ ├── proguard-rules.pro │ │ ├── src/ │ │ │ ├── debug/ │ │ │ │ └── AndroidManifest.xml │ │ │ ├── main/ │ │ │ │ ├── AndroidManifest.xml │ │ │ │ ├── java/ │ │ │ │ │ └── com/ │ │ │ │ │ └── weilu/ │ │ │ │ │ └── deer/ │ │ │ │ │ ├── DeerPickerProvider.java │ │ │ │ │ ├── FileProvider7.java │ │ │ │ │ ├── InstallAPKPlugin.java │ │ │ │ │ ├── MainActivity.java │ │ │ │ │ └── MyApp.java │ │ │ │ └── res/ │ │ │ │ ├── drawable/ │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-anydpi-v26/ │ │ │ │ │ └── ic_launcher.xml │ │ │ │ ├── values/ │ │ │ │ │ ├── colors.xml │ │ │ │ │ └── styles.xml │ │ │ │ ├── values-night/ │ │ │ │ │ └── colors.xml │ │ │ │ ├── values-v27/ │ │ │ │ │ └── styles.xml │ │ │ │ └── xml/ │ │ │ │ ├── file_paths.xml │ │ │ │ └── network_security_config.xml │ │ │ └── profile/ │ │ │ └── AndroidManifest.xml │ │ └── test.jks │ ├── build.gradle │ ├── gradle/ │ │ └── wrapper/ │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ └── settings.gradle ├── assets/ │ ├── data/ │ │ ├── bank.json │ │ ├── bank_2.json │ │ ├── city.json │ │ ├── sort_0.json │ │ ├── sort_1.json │ │ └── sort_2.json │ └── lottie/ │ └── bunny_new_mouth.json ├── devtools_options.yaml ├── docs/ │ ├── Android问题汇总.md │ ├── CHANGELOG.md │ ├── Web问题汇总.md │ └── iOS问题汇总.md ├── integration_test/ │ ├── goods_test.dart │ ├── integration_test.dart │ └── login_test.dart ├── ios/ │ ├── .gitignore │ ├── Flutter/ │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Podfile │ ├── Runner/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ ├── LaunchImage.imageset/ │ │ │ │ ├── Contents.json │ │ │ │ └── README.md │ │ │ └── flutter_dash_black.imageset/ │ │ │ └── Contents.json │ │ ├── Base.lproj/ │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── Runner-Bridging-Header.h │ ├── 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 │ └── RunnerTests/ │ └── RunnerTests.swift ├── l10n.yaml ├── lib/ │ ├── account/ │ │ ├── account_router.dart │ │ ├── models/ │ │ │ ├── bank_entity.dart │ │ │ ├── city_entity.dart │ │ │ └── withdrawal_account_model.dart │ │ ├── page/ │ │ │ ├── account_page.dart │ │ │ ├── account_record_list_page.dart │ │ │ ├── add_withdrawal_account_page.dart │ │ │ ├── bank_select_page.dart │ │ │ ├── city_select_page.dart │ │ │ ├── withdrawal_account_list_page.dart │ │ │ ├── withdrawal_account_page.dart │ │ │ ├── withdrawal_page.dart │ │ │ ├── withdrawal_password_page.dart │ │ │ ├── withdrawal_record_list_page.dart │ │ │ └── withdrawal_result_page.dart │ │ └── widgets/ │ │ ├── rise_number_text.dart │ │ ├── sms_verify_dialog.dart │ │ ├── withdrawal_account_item.dart │ │ └── withdrawal_password_setting.dart │ ├── demo/ │ │ ├── demo_page.dart │ │ ├── focus/ │ │ │ └── focus_demo_page.dart │ │ ├── lottie/ │ │ │ ├── bunny.dart │ │ │ └── lottie_demo.dart │ │ ├── navigator/ │ │ │ ├── book_entity.dart │ │ │ ├── books_app_state.dart │ │ │ ├── books_main.dart │ │ │ ├── delegate/ │ │ │ │ ├── inner_router_delegate.dart │ │ │ │ └── router_delegate.dart │ │ │ ├── parser/ │ │ │ │ └── route_information_parser.dart │ │ │ └── screen/ │ │ │ ├── app_shell.dart │ │ │ ├── book_details_screen.dart │ │ │ ├── books_list_screen.dart │ │ │ └── setting_screen.dart │ │ ├── overlay/ │ │ │ ├── bottom_navigation/ │ │ │ │ └── my_bottom_navigation_bar.dart │ │ │ ├── overlay_main.dart │ │ │ ├── page/ │ │ │ │ ├── overlay_demo_page.dart │ │ │ │ └── test_page.dart │ │ │ └── route/ │ │ │ ├── application.dart │ │ │ └── my_navigator_observer.dart │ │ ├── ripple/ │ │ │ └── ripples_animation_page.dart │ │ ├── scratcher/ │ │ │ └── scratch_card_demo_page.dart │ │ └── widgets/ │ │ └── neumorphic.dart │ ├── generated/ │ │ └── json/ │ │ ├── bank_entity.g.dart │ │ ├── base/ │ │ │ ├── json_convert_content.dart │ │ │ └── json_field.dart │ │ ├── city_entity.g.dart │ │ ├── goods_sort_entity.g.dart │ │ ├── search_entity.g.dart │ │ └── user_entity.g.dart │ ├── goods/ │ │ ├── goods_router.dart │ │ ├── models/ │ │ │ ├── goods_item_entity.dart │ │ │ ├── goods_size_model.dart │ │ │ └── goods_sort_entity.dart │ │ ├── page/ │ │ │ ├── goods_edit_page.dart │ │ │ ├── goods_list_page.dart │ │ │ ├── goods_page.dart │ │ │ ├── goods_search_page.dart │ │ │ ├── goods_size_edit_page.dart │ │ │ ├── goods_size_page.dart │ │ │ └── qr_code_scanner_page.dart │ │ ├── provider/ │ │ │ ├── goods_page_provider.dart │ │ │ └── goods_sort_provider.dart │ │ └── widgets/ │ │ ├── goods_add_menu.dart │ │ ├── goods_delete_bottom_sheet.dart │ │ ├── goods_item.dart │ │ ├── goods_size_dialog.dart │ │ ├── goods_sort_bottom_sheet.dart │ │ ├── goods_sort_menu.dart │ │ └── menu_reveal.dart │ ├── home/ │ │ ├── home_page.dart │ │ ├── provider/ │ │ │ └── home_provider.dart │ │ ├── splash_page.dart │ │ └── webview_page.dart │ ├── l10n/ │ │ ├── deer_localizations.dart │ │ ├── deer_localizations_en.dart │ │ ├── deer_localizations_zh.dart │ │ ├── intl_en.arb │ │ └── intl_zh.arb │ ├── login/ │ │ ├── login_router.dart │ │ ├── page/ │ │ │ ├── login_page.dart │ │ │ ├── register_page.dart │ │ │ ├── reset_password_page.dart │ │ │ ├── sms_login_page.dart │ │ │ └── update_password_page.dart │ │ └── widgets/ │ │ └── my_text_field.dart │ ├── main.dart │ ├── mvp/ │ │ ├── base_page.dart │ │ ├── base_page_presenter.dart │ │ ├── base_presenter.dart │ │ ├── i_lifecycle.dart │ │ ├── mvps.dart │ │ └── power_presenter.dart │ ├── net/ │ │ ├── base_entity.dart │ │ ├── dio_utils.dart │ │ ├── error_handle.dart │ │ ├── http_api.dart │ │ ├── intercept.dart │ │ └── net.dart │ ├── order/ │ │ ├── iview/ │ │ │ └── order_search_iview.dart │ │ ├── models/ │ │ │ └── search_entity.dart │ │ ├── order_router.dart │ │ ├── page/ │ │ │ ├── order_info_page.dart │ │ │ ├── order_list_page.dart │ │ │ ├── order_page.dart │ │ │ ├── order_search_page.dart │ │ │ └── order_track_page.dart │ │ ├── presenter/ │ │ │ └── order_search_presenter.dart │ │ ├── provider/ │ │ │ ├── base_list_provider.dart │ │ │ └── order_page_provider.dart │ │ └── widgets/ │ │ ├── order_item.dart │ │ ├── order_tag_item.dart │ │ └── pay_type_dialog.dart │ ├── res/ │ │ ├── colors.dart │ │ ├── constant.dart │ │ ├── dimens.dart │ │ ├── gaps.dart │ │ ├── resources.dart │ │ └── styles.dart │ ├── routers/ │ │ ├── fluro_navigator.dart │ │ ├── i_router.dart │ │ ├── not_found_page.dart │ │ ├── routers.dart │ │ └── web_page_transitions.dart │ ├── setting/ │ │ ├── page/ │ │ │ ├── about_page.dart │ │ │ ├── account_manager_page.dart │ │ │ ├── locale_page.dart │ │ │ ├── setting_page.dart │ │ │ └── theme_page.dart │ │ ├── provider/ │ │ │ ├── locale_provider.dart │ │ │ └── theme_provider.dart │ │ ├── setting_router.dart │ │ └── widgets/ │ │ ├── exit_dialog.dart │ │ └── update_dialog.dart │ ├── shop/ │ │ ├── iview/ │ │ │ └── shop_iview.dart │ │ ├── models/ │ │ │ ├── freight_config_model.dart │ │ │ └── user_entity.dart │ │ ├── page/ │ │ │ ├── freight_config_page.dart │ │ │ ├── input_text_page.dart │ │ │ ├── message_page.dart │ │ │ ├── select_address_page.dart │ │ │ ├── shop_page.dart │ │ │ └── shop_setting_page.dart │ │ ├── presenter/ │ │ │ └── shop_presenter.dart │ │ ├── provider/ │ │ │ └── user_provider.dart │ │ ├── shop_router.dart │ │ └── widgets/ │ │ ├── pay_type_dialog.dart │ │ ├── price_input_dialog.dart │ │ ├── range_price_input_dialog.dart │ │ └── send_type_dialog.dart │ ├── statistics/ │ │ ├── page/ │ │ │ ├── goods_statistics_page.dart │ │ │ ├── order_statistics_page.dart │ │ │ └── statistics_page.dart │ │ ├── statistics_router.dart │ │ └── widgets/ │ │ └── selected_date.dart │ ├── store/ │ │ ├── page/ │ │ │ ├── store_audit_page.dart │ │ │ └── store_audit_result_page.dart │ │ └── store_router.dart │ ├── util/ │ │ ├── app_navigator.dart │ │ ├── change_notifier_manage.dart │ │ ├── date_utils.dart │ │ ├── device_utils.dart │ │ ├── handle_error_utils.dart │ │ ├── image_utils.dart │ │ ├── input_formatter/ │ │ │ ├── fix_ios_input_formatter.dart │ │ │ └── number_text_input_formatter.dart │ │ ├── log_utils.dart │ │ ├── other_utils.dart │ │ ├── screen_utils.dart │ │ ├── theme_utils.dart │ │ ├── toast_utils.dart │ │ └── version_utils.dart │ └── widgets/ │ ├── base_dialog.dart │ ├── bezier_chart/ │ │ ├── bezier_chart.dart │ │ ├── bezier_chart_config.dart │ │ ├── bezier_chart_widget.dart │ │ ├── bezier_line.dart │ │ └── my_single_child_scroll_view.dart │ ├── click_item.dart │ ├── double_tap_back_exit_app.dart │ ├── fractionally_aligned_sized_box.dart │ ├── load_image.dart │ ├── my_app_bar.dart │ ├── my_button.dart │ ├── my_card.dart │ ├── my_flexible_space_bar.dart │ ├── my_refresh_list.dart │ ├── my_scroll_view.dart │ ├── my_search_bar.dart │ ├── pie_chart/ │ │ ├── pie_chart.dart │ │ └── pie_data.dart │ ├── popup_window.dart │ ├── progress_dialog.dart │ ├── selected_image.dart │ ├── selected_item.dart │ ├── state_layout.dart │ └── text_field_item.dart ├── macos/ │ ├── .gitignore │ ├── Flutter/ │ │ ├── Flutter-Debug.xcconfig │ │ ├── Flutter-Release.xcconfig │ │ └── GeneratedPluginRegistrant.swift │ ├── Podfile │ ├── Runner/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ └── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ ├── Base.lproj/ │ │ │ └── MainMenu.xib │ │ ├── Configs/ │ │ │ ├── AppInfo.xcconfig │ │ │ ├── Debug.xcconfig │ │ │ ├── Release.xcconfig │ │ │ └── Warnings.xcconfig │ │ ├── DebugProfile.entitlements │ │ ├── Info.plist │ │ ├── MainFlutterWindow.swift │ │ └── Release.entitlements │ ├── Runner.xcodeproj/ │ │ ├── project.pbxproj │ │ ├── project.xcworkspace/ │ │ │ └── xcshareddata/ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata/ │ │ └── xcschemes/ │ │ └── Runner.xcscheme │ └── Runner.xcworkspace/ │ ├── contents.xcworkspacedata │ └── xcshareddata/ │ └── IDEWorkspaceChecks.plist ├── pubspec.yaml ├── shader/ │ ├── README.md │ └── flutter_01.sksl.json ├── test/ │ ├── accessibility_test.dart │ ├── account/ │ │ └── account_accessibility_test.dart │ ├── goods/ │ │ └── goods_accessibility_test.dart │ ├── login/ │ │ └── login_accessibility_test.dart │ ├── net/ │ │ └── dio_test.dart │ ├── order/ │ │ └── order_accessibility_test.dart │ ├── setting/ │ │ └── setting_accessibility_test.dart │ ├── shop/ │ │ └── shop_accessibility_test.dart │ ├── statistics/ │ │ └── statistic_accessibility_test.dart │ ├── store/ │ │ └── store_accessibility_test.dart │ └── widget_test.dart ├── test_driver/ │ ├── account/ │ │ ├── account.dart │ │ └── account_test.dart │ ├── driver.dart │ ├── driver_test.dart │ ├── goods/ │ │ ├── goods.dart │ │ └── goods_test.dart │ ├── home/ │ │ ├── splash_page.dart │ │ └── splash_page_test.dart │ ├── login/ │ │ ├── login_page.dart │ │ └── login_page_test.dart │ ├── order/ │ │ ├── order.dart │ │ └── order_test.dart │ ├── setting/ │ │ ├── setting.dart │ │ └── setting_test.dart │ ├── shop/ │ │ ├── shop.dart │ │ └── shop_test.dart │ ├── statistic/ │ │ ├── statistic.dart │ │ └── statistic_test.dart │ ├── store/ │ │ ├── store.dart │ │ └── store_test.dart │ └── tools/ │ └── test_utils.dart ├── web/ │ ├── index.html │ ├── index1.html │ └── manifest.json └── windows/ ├── .gitignore ├── CMakeLists.txt ├── flutter/ │ ├── CMakeLists.txt │ ├── generated_plugin_registrant.cc │ ├── generated_plugin_registrant.h │ └── generated_plugins.cmake └── runner/ ├── CMakeLists.txt ├── Runner.rc ├── flutter_window.cpp ├── flutter_window.h ├── main.cpp ├── resource.h ├── run_loop.cpp ├── run_loop.h ├── runner.exe.manifest ├── utils.cpp ├── utils.h ├── win32_window.cpp └── win32_window.h ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: https://github.com/simplezhli/flutter_deer/blob/master/preview/jikeshijian.jpg ================================================ FILE: .github/ISSUE_TEMPLATE/deer-issue-template.md ================================================ --- name: deer issue template about: 请确保使用最新的代码并详细阅读了readme和查找了已有的issue,避免提出重复的问题。 title: '' labels: '' assignees: '' --- ### 运行环境 ### - [x] 电脑系统:如:`Windows 11` - [x] 设备型号:如:`小米13` - [x] 设备系统版本:如 `Android 13` - [x] Flutter 版本:如 `3.13.0` ### 具体问题描述 ### #### 问题截图 #### #### 异常日志 #### ================================================ FILE: .github/workflows/flutter-drive.yml ================================================ # https://medium.com/flutter-community/run-flutter-driver-tests-on-github-actions-13c639c7e4ab # https://github.com/nazarcybulskij/CI_CD_Flutter_Demo # https://weilu.blog.csdn.net/article/details/114744416 # Name of your workflow. name: flutter_deer driver # Trigger the workflow on push or pull request. on: [push, pull_request] # A workflow run is made up of one or more jobs. jobs: # id of job, a string that is unique to the "jobs" node above. drive_ios: # Creates a build matrix for your jobs. You can define different # variations of an environment to run each job in. strategy: # A set of different configurations of the virtual environment. matrix: device: - "iPhone 17 Pro Max" # When set to true, GitHub cancels all in-progress jobs if any matrix job # fails. fail-fast: false # The type of machine to run the job on. runs-on: macos-latest # Contains a sequence of tasks. steps: # A name for your step to display on GitHub. # - name: "List all simulators" # run: "xcrun instruments -s" # - name: "Start Simulator" # run: | # UDID=$( # xcrun instruments -s | # awk \ # -F ' *[][]' \ # -v 'device=${{ matrix.device }}' \ # '$1 == device { print $2 }' # ) # xcrun simctl boot "${UDID:?No Simulator with this name found}" - name: "Start Simulator" # https://github.com/futureware-tech/simulator-action uses: futureware-tech/simulator-action@v2 with: model: ${{ matrix.device }} erase_before_boot: true shutdown_after_job: true # The branch or tag ref that triggered the workflow will be checked out. # https://github.com/marketplace/actions/checkout - uses: actions/checkout@v3 # Sets up a flutter environment. # https://github.com/marketplace/actions/flutter-action - uses: subosito/flutter-action@v2 with: flutter-version: '3.41.0' channel: 'stable' # or: 'dev' or 'beta' - run: "flutter clean" - name: "Run Flutter Driver tests" run: "flutter drive --target=test_driver/driver.dart --no-enable-impeller" #https://github.com/flutter/flutter/issues/128391 drive_android: # The type of machine to run the job on. runs-on: macos-latest # creates a build matrix for your jobs strategy: # set of different configurations of the virtual environment. matrix: api-level: [35] target: [google_apis] steps: - uses: actions/checkout@v3 - name: set up JDK 17 uses: actions/setup-java@v3 with: distribution: "oracle" java-version: "17" - uses: subosito/flutter-action@v2 with: flutter-version: '3.41.0' channel: 'stable' # or: 'dev' or 'beta' - name: "Run Flutter Driver tests" # GitHub Action for installing, configuring and running Android Emulators (work only Mac OS) # https://github.com/marketplace/actions/android-emulator-runner uses: reactivecircus/android-emulator-runner@v2 with: api-level: ${{ matrix.api-level }} target: ${{ matrix.target }} arch: arm64-v8a profile: Nexus 6 script: "flutter drive --target=test_driver/driver.dart" accessibility_test: #The type of machine to run the job on. [windows,macos, ubuntu , self-hosted] runs-on: macos-latest #sequence of tasks called steps: # The branch or tag ref that triggered the workflow will be checked out. # https://github.com/actions/checkout - uses: actions/checkout@v3 # Setup a flutter environment. # https://github.com/marketplace/actions/flutter-action - uses: subosito/flutter-action@v2 with: flutter-version: '3.41.0' channel: 'stable' - run: "flutter pub get" - name: "Run Flutter Accessibility Tests" run: "flutter test test/accessibility_test.dart" ================================================ FILE: .github/workflows/flutter-web-deploy.yml ================================================ name: flutter_deer web deploy # push 提交中修改`pubspec.yaml`触发此workflow。 # 为了避免每次部署,这里使用一个不存在的文件名。 on: push: paths: - 'pubspec1.yaml' jobs: web_build_and_deploy: runs-on: macos-latest steps: - uses: actions/checkout@v2.3.1 - uses: subosito/flutter-action@v2 with: flutter-version: '3.35.1' channel: 'stable' architecture: x64 - name: "Web Build 🔧" run: | flutter pub get flutter build web - name: "Web Deploy 🚀" # https://github.com/JamesIves/github-pages-deploy-action uses: JamesIves/github-pages-deploy-action@4.0.0 with: token: '${{ secrets.GITHUB_TOKEN }}' branch: gh-pages folder: build/web ================================================ FILE: .gitignore ================================================ # Miscellaneous *.class *.log *.pyc *.swp *.lock .DS_Store .atom/ .buildlog/ .history .svn/ # IntelliJ related *.iml *.ipr *.iws .idea/ # Visual Studio Code related .vscode/ # Flutter/Dart/Pub related **/doc/api/ .dart_tool/ .flutter-plugins .flutter-plugins-dependencies .packages .pub-cache/ .pub/ /build/ # Android related **/android/**/gradle-wrapper.jar **/android/.gradle **/android/captures/ **/android/gradlew **/android/gradlew.bat **/android/local.properties **/android/**/GeneratedPluginRegistrant.java **/android/app/.cxx # iOS/XCode related **/ios/**/*.mode1v3 **/ios/**/*.mode2v3 **/ios/**/*.moved-aside **/ios/**/*.pbxuser **/ios/**/*.perspectivev3 **/ios/**/*sync/ **/ios/**/.sconsign.dblite **/ios/**/.tags* **/ios/**/.vagrant/ **/ios/**/DerivedData/ **/ios/**/Icon? **/ios/**/Pods/ **/ios/**/.symlinks/ **/ios/**/profile **/ios/**/xcuserdata **/ios/.generated/ **/ios/Flutter/App.framework **/ios/Flutter/Flutter.framework **/ios/Flutter/Generated.xcconfig **/ios/Flutter/app.flx **/ios/Flutter/app.zip **/ios/Flutter/flutter_assets/ **/ios/ServiceDefinitions.json **/ios/Runner/GeneratedPluginRegistrant.* **/ios/Flutter/flutter_export_environment.sh **/ios/Flutter/.last_build_id # Web related lib/generated_plugin_registrant.dart # Exceptions to above rules. !**/ios/**/default.mode1v3 !**/ios/**/default.mode2v3 !**/ios/**/default.pbxuser !**/ios/**/default.perspectivev3 !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages /ios/build/ ================================================ FILE: .metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled and should not be manually edited. version: revision: "b45fa18946ecc2d9b4009952c636ba7e2ffbb787" channel: "stable" project_type: app # Tracks metadata for the flutter migrate command migration: platforms: - platform: root create_revision: b45fa18946ecc2d9b4009952c636ba7e2ffbb787 base_revision: b45fa18946ecc2d9b4009952c636ba7e2ffbb787 - platform: ios create_revision: b45fa18946ecc2d9b4009952c636ba7e2ffbb787 base_revision: b45fa18946ecc2d9b4009952c636ba7e2ffbb787 # User provided section # List of Local paths (relative to this file) that should be # ignored by the migrate tool. # # Files that are not part of the templates will be ignored by default. unmanaged_files: - 'lib/main.dart' - 'ios/Runner.xcodeproj/project.pbxproj' ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README-EN.md ================================================ # Flutter Deer ## English | [中文](README.md) This project is an exercise in learning Flutter for personal growth and development. To achieve specific design outcomes and meet the demands of daily development, one may employ the methods of configuring, modifying, combining pre-existing components, and customizing. The design plans for this project can be found in the "design" directory. You may utilize these plans to practice with a specific goal in mind. Any implementation is solely based on personal comprehension and learning. Should there be any superior implementation strategies, I welcome the opportunity for discussion. ## Preview Some of the page effects are as follows: | ![](./preview/Screenshot_1.png) | ![](./preview/Screenshot_2.png) | ![](./preview/Screenshot_3.png) | ![](./preview/Screenshot_4.png) | | :--------------------------------: | :---------------------------------: | :-------------------------------: | :-------------------------------: | | ![](./preview/Screenshot_5.png) | ![](./preview/Screenshot_6.png) | ![](./preview/Screenshot_7.png) | ![](./preview/Screenshot_8.png) | | ![](./preview/Screenshot_9.png) | ![](./preview/Screenshot_10.png) | ![](./preview/Screenshot_11.png) | ![](./preview/Screenshot_12.png) | | ![](./preview/Screenshot_13.png) | ![](./preview/Screenshot_14.png) | ![](./preview/Screenshot_15.png) | ![](./preview/Screenshot_17.png) | | ![](./preview/Screenshot_18.png) | ![](./preview/Screenshot_19.png) | ![](./preview/Screenshot_20.png) | ![](./preview/Screenshot_21.png) | | ![](./preview/Screenshot_22.jpg) | ![](./preview/Screenshot_23.jpg) | ![](./preview/Screenshot_24.jpg) | ![](./preview/Screenshot_25.jpg) | | ![](./preview/Screenshot_26.jpg) | ![](./preview/Screenshot_27.jpg) | ![](./preview/lottie.gif) | | **If you find this project satisfactory, kindly show your support by giving it a Star or Fork. Rest assured, this project is being continuously maintained and any issues can be brought to our attention by submitting an Issue.** ## Realizing the content. * MVP pattern * State management using `provider` (version 6.x) * Network request encapsulation based on `dio` (version 5.x) * Integration testing and accessibility testing * Support for dark mode * Localization(Thanks to @ghedwards) * Implementation of complex scrolling effects using `Sliver` series components * Location selection using AMap (supports Web) * Encapsulation of common widgets handling * Pull-to-refresh and load-more functionality * Application update check * PopupWindow * QR code scanning functionality (using the qr_code_scanner plugin) * Menu switching animations (circular expansion, 3D flip) * Swipe-to-delete * City selection * Three-level linkage selection similar to JD's city selection * Various custom dialogs * Sticky header for lists * Password input keyboard * Verification code input box * Custom simple calendar * Line chart and [pie charts](https://dartpad.cn/d06f8f737d6eb2d87978eb2d14b87864) * Modularized route management * More demos (ripple animation, scratch card, lottie) * More detailed optimizations You may download and experience it specifically by accessing the following links: For the Android version, kindly click on the link provided: [Download here](https://www.pgyer.com/oEm8me), and enter the password: `111111`. As for iOS, you will need to download and run the code on your own. For web experience, please visit: https://simplezhli.github.io/flutter_deer/ ## The project's operational environment. [![flutter_deer driver](https://github.com/simplezhli/flutter_deer/actions/workflows/flutter-drive.yml/badge.svg?branch=master)](https://github.com/simplezhli/flutter_deer/actions/workflows/flutter-drive.yml) 1. Flutter version 3.41.0 2. Dart version 3.11.0 ## Precautions to be taken. - In debug mode, there may be some lagging, which is considered a normal occurrence. A satisfactory experience requires the creation of a release package. To create a release version for iOS, execute the command `flutter build ios`. For Android, execute the command `flutter build apk`. - If there are any issues with the project's execution, please refer to the [iOS issue summary](./docs/iOS问题汇总.md) and [Android issue summary](./docs/Android问题汇总.md) for possible solutions. - Due to certain plugin limitations, this project is only available for preview on Windows and macOS. Those interested may run and experience it themselves. - To view the functionality demonstration, execute the integration test command `flutter drive --target=test_driver/driver.dart`. - Due to the abundance of pages, it may be difficult to match the design at first. However, I have added the relative path of the design in the code comments, which can be searched or located for the corresponding page. I hope this will be helpful to you. - This project uses the [FlutterJsonBeanFactory](https://github.com/zhangruiyu/FlutterJsonBeanFactory) plugin to generate Beans. - Web performance may be slower due to large resource files such as js and deployment on Github. ## Summary of Experience - [Flutter开发中的一些Tips(一)](https://weilu.blog.csdn.net/article/details/90546727) - [Flutter开发中的一些Tips(二)](https://weilu.blog.csdn.net/article/details/94849020) - [Flutter开发中的一些Tips(三)](https://weilu.blog.csdn.net/article/details/100108123) - [Flutter适配深色模式(DarkMode)](https://weilu.blog.csdn.net/article/details/102531559) - [说说Flutter中的RepaintBoundary](https://weilu.blog.csdn.net/article/details/103452637) - [说说Flutter中的Semantics](https://weilu.blog.csdn.net/article/details/103823259) - [说说Flutter中最熟悉的陌生人 —— Key](https://weilu.blog.csdn.net/article/details/104745624) - [说说Flutter中的无名英雄 —— Focus](https://weilu.blog.csdn.net/article/details/107132031) - [Flutter性能优化实践 —— UI篇](https://weilu.blog.csdn.net/article/details/106046434) - [玩玩Flutter的拖拽——实现一款万能遥控器](https://weilu.blog.csdn.net/article/details/105237677) - [玩玩Flutter Web —— 实现高德地图插件](https://weilu.blog.csdn.net/article/details/106465792) - [在GitHub Actions上进行Flutter 的测试和部署](https://weilu.blog.csdn.net/article/details/114744416) - [Flutter动画曲线Curves 效果一览](https://weilu.blog.csdn.net/article/details/95632571) - [Flutter状态管理之Riverpod](https://weilu.blog.csdn.net/article/details/108352306) - [【译】正确操作Dart中的字符串](https://weilu.blog.csdn.net/article/details/107857569) - [【译】学习Flutter中新的Navigator和Router系统](https://weilu.blog.csdn.net/article/details/108902282) ## Tripartite library used | library | Functionality | | -------------------------- | --------------- | | [dio](https://github.com/cfug/dio) | **Networking library** | | [provider](https://github.com/rrousselGit/provider) | **State management** | | [flutter_2d_amap](https://github.com/simplezhli/flutter_2d_amap) | **2D map from Amap** | | [cached_network_image](https://github.com/renefloor/flutter_cached_network_image) | **Image loading** | | [fluro](https://github.com/theyakka/fluro) | **Routing management** | | [flutter_oktoast](https://github.com/OpenFlutter/flutter_oktoast) | **Toast notifications** | | [common_utils](https://github.com/Sky24n/common_utils) | **Common Dart utility library** | | [flutter_slidable](https://github.com/letsar/flutter_slidable) | **Swipe-to-delete** | | [flustars](https://github.com/Sky24n/flustars) | **Common Flutter utility library** | | [flutter_swiper](https://github.com/best-flutter/flutter_swiper) | **Flutter carousel component** | | [url_launcher](https://github.com/flutter/plugins/tree/master/packages/url_launcher) | **Plugin for launching URLs** | | [image_picker](https://github.com/flutter/plugins/tree/master/packages/image_picker) | **Plugin for selecting images** | | [rxdart](https://github.com/ReactiveX/rxdart) | **Reactive extensions for Dart** | | [webview_flutter](https://github.com/flutter/plugins/tree/master/packages/webview_flutter) | **WebView plugin** | | [keyboard_actions](https://github.com/diegoveloper/flutter_keyboard_actions) | **Handling keyboard events** | | [azlistview](https://github.com/flutterchina/azlistview) | **City selection list** | | [date_utils](https://github.com/apptreesoftware/date_utils) | **Common date utility classes** | | [bezier_chart](https://github.com/aeyrium/bezier-chart) | **Bezier chart** | | [sprintf](https://github.com/Naddiseo/dart-sprintf) | **String formatting** | | [qr_code_scanner](https://github.com/juliuscanute/qr_code_scanner) | **Scanning QR codes** | | [intl](https://github.com/dart-lang/intl) | **Localization** | | [device_info_plus](https://github.com/fluttercommunity/plus_plugins/tree/main/packages/device_info_plus) | **Getting device information** | | [vibration](https://github.com/benjamindean/flutter_vibration) | **Vibration** | | [lottie](https://github.com/xvrh/lottie-flutter) | **Animation effects** | For details, please refer to the [pubspec.yaml](https://github.com/simplezhli/flutter_deer/blob/master/pubspec.yaml) file. ## Plan: * [x] Web support. * [x] Migrate to null-safety. * [ ] Migrate to Navigator 2.0. ## Thanks For - [flutter_wanandroid](https://github.com/Sky24n/flutter_wanandroid) ## License Copyright 2019 simplezhli Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # Flutter Deer ## [English](README-EN.md) | 中文 本项目为个人学习Flutter的练习项目。 通过设置、修改、组合自带部件以及自定义来实现具体的设计效果,满足日常开发的需求。 本项目设计图见design目录,你可以通过我提供的设计图有目标的去练习。所有的实现仅是个人的学习理解,如果有更好的实现方案欢迎交流。 ## 预览 部分页面效果如下: | ![](./preview/Screenshot_1.png) | ![](./preview/Screenshot_2.png) | ![](./preview/Screenshot_3.png) | ![](./preview/Screenshot_4.png) | | :--------------------------------: | :---------------------------------: | :-------------------------------: | :-------------------------------: | | ![](./preview/Screenshot_5.png) | ![](./preview/Screenshot_6.png) | ![](./preview/Screenshot_7.png) | ![](./preview/Screenshot_8.png) | | ![](./preview/Screenshot_9.png) | ![](./preview/Screenshot_10.png) | ![](./preview/Screenshot_11.png) | ![](./preview/Screenshot_12.png) | | ![](./preview/Screenshot_13.png) | ![](./preview/Screenshot_14.png) | ![](./preview/Screenshot_15.png) | ![](./preview/Screenshot_17.png) | | ![](./preview/Screenshot_18.png) | ![](./preview/Screenshot_19.png) | ![](./preview/Screenshot_20.png) | ![](./preview/Screenshot_21.png) | | ![](./preview/Screenshot_22.jpg) | ![](./preview/Screenshot_23.jpg) | ![](./preview/Screenshot_24.jpg) | ![](./preview/Screenshot_25.jpg) | | ![](./preview/Screenshot_26.jpg) | ![](./preview/Screenshot_27.jpg) | ![](./preview/lottie.gif) | | **觉得还可以的话,来个Star、Fork支持一波!本项目持续维护中,有问题欢迎提Issue。** ## 实现内容(已迁移到空安全) * mvp模式 * 使用`provider` (6.x 版本)做状态管理 * 基于`dio` (5.x 版本)的网络请求封装 * 完整的集成测试、可访问性测试。 * 支持深色模式 * 本地化(感谢 @ghedwards) * 使用`Sliver` 系列组件实现复杂滚动效果 * 使用高德地图定位选择地址(支持Web) * 通用Widget的处理封装 * 下拉刷新 + 上拉加载更多 * 应用检查更新 * PopupWindow * 扫码功能(qr_code_scanner插件) * 菜单切换动画(圆形扩散、3D翻转) * 侧滑删除 * 城市选择 * 类似京东选择城市的三级联动 * 各种自定义Dialog * 列表头部吸顶 * 密码输入键盘 * 验证码输入框 * 自定义简易日历 * 曲线图及[饼状图](https://dartpad.cn/d06f8f737d6eb2d87978eb2d14b87864) * 模块化路由管理 * 更多Demo(水波纹动画、刮刮卡、lottie) * 更多的细节优化 具体可以下载体验: Android版安装包:[点击去下载](https://github.com/simplezhli/flutter_deer/releases)。 iOS需要自行下载代码运行。 Web体验地址:https://simplezhli.github.io/flutter_deer/ ## 项目运行环境 [![flutter_deer driver](https://github.com/simplezhli/flutter_deer/actions/workflows/flutter-drive.yml/badge.svg?branch=master)](https://github.com/simplezhli/flutter_deer/actions/workflows/flutter-drive.yml) 1. Flutter version 3.41.0 2. Dart version 3.11.0 ## 注意事项 - `debug`模式下会有部分卡顿现象,这属于正常现象。良好的体验需要打`release` 包。 iOS可以执行命令`flutter build ios` 以创建`release`版本。 Android可以执行命令`flutter build apk` 以创建`release`版本。 - 项目运行有问题可以在[iOS问题汇总](./docs/iOS问题汇总.md)、[Android问题汇总](./docs/Android问题汇总.md)中尝试寻找解决办法。 - 由于部分插件的原因,本项目在Windows、macOS仅做预览(主要为原生功能方面,UI问题不大)。有兴趣的可自行运行体验。 - 可以执行集成测试命令`flutter drive --target=test_driver/driver.dart` 查看功能演示。 - 因为页面有点多,一开始可能会导致页面无法与设计图对应上。我在代码注释中有添加设计图的相对路径,可以搜索或查找到对应页面,希望对你有帮助。 - 本项目使用[FlutterJsonBeanFactory](https://github.com/zhangruiyu/FlutterJsonBeanFactory)插件来生成Bean。 - Web受制于js等资源过大和部署在Github上,访问会慢一些。 ## 心得总结(推荐阅读) - [Flutter开发中的一些Tips(一)](https://weilu.blog.csdn.net/article/details/90546727) - [Flutter开发中的一些Tips(二)](https://weilu.blog.csdn.net/article/details/94849020) - [Flutter开发中的一些Tips(三)](https://weilu.blog.csdn.net/article/details/100108123) - [Flutter适配深色模式(DarkMode)](https://weilu.blog.csdn.net/article/details/102531559) - [说说Flutter中的RepaintBoundary](https://weilu.blog.csdn.net/article/details/103452637) - [说说Flutter中的Semantics](https://weilu.blog.csdn.net/article/details/103823259) - [说说Flutter中最熟悉的陌生人 —— Key](https://weilu.blog.csdn.net/article/details/104745624) - [说说Flutter中的无名英雄 —— Focus](https://weilu.blog.csdn.net/article/details/107132031) - [Flutter性能优化实践 —— UI篇](https://weilu.blog.csdn.net/article/details/106046434) - [玩玩Flutter的拖拽——实现一款万能遥控器](https://weilu.blog.csdn.net/article/details/105237677) - [玩玩Flutter Web —— 实现高德地图插件](https://weilu.blog.csdn.net/article/details/106465792) - [在GitHub Actions上进行Flutter 的测试和部署](https://weilu.blog.csdn.net/article/details/114744416) - [Flutter动画曲线Curves 效果一览](https://weilu.blog.csdn.net/article/details/95632571) - [Flutter状态管理之Riverpod](https://weilu.blog.csdn.net/article/details/108352306) - [【译】正确操作Dart中的字符串](https://weilu.blog.csdn.net/article/details/107857569) - [【译】学习Flutter中新的Navigator和Router系统](https://weilu.blog.csdn.net/article/details/108902282) - [【译】Flutter 2.2中的新功能](https://weilu.blog.csdn.net/article/details/117061293) ## 使用到的三方库 | 库 | 功能 | | -------------------------- | --------------- | | [dio](https://github.com/cfug/dio) | **网络库** | | [provider](https://github.com/rrousselGit/provider) | **状态管理** | | [flutter_2d_amap](https://github.com/simplezhli/flutter_2d_amap) | **高德2D地图** | | [cached_network_image](https://github.com/renefloor/flutter_cached_network_image) | **图片加载** | | [fluro](https://github.com/theyakka/fluro) | **路由管理** | | [flutter_oktoast](https://github.com/OpenFlutter/flutter_oktoast) | **Toast** | | [common_utils](https://github.com/Sky24n/common_utils) | **Dart 常用工具类库** | | [flutter_slidable](https://github.com/letsar/flutter_slidable) | **侧滑删除** | | [flustars](https://github.com/Sky24n/flustars) | **Flutter 常用工具类库** | | [flutter_swiper](https://github.com/best-flutter/flutter_swiper) | **Flutter 轮播组件** | | [url_launcher](https://github.com/flutter/plugins/tree/master/packages/url_launcher) | **启动URL的插件** | | [image_picker](https://github.com/flutter/plugins/tree/master/packages/image_picker) | **图片选择插件** | | [rxdart](https://github.com/ReactiveX/rxdart) | **Dart的响应式扩展** | | [webview_flutter](https://github.com/flutter/plugins/tree/master/packages/webview_flutter) | **WebView插件** | | [keyboard_actions](https://github.com/diegoveloper/flutter_keyboard_actions) | **处理键盘事件** | | [azlistview](https://github.com/flutterchina/azlistview) | **城市选择列表** | | [date_utils](https://github.com/apptreesoftware/date_utils) | **常用的日期工具类** | | [bezier_chart](https://github.com/aeyrium/bezier-chart) | **曲线图表** | | [sprintf](https://github.com/Naddiseo/dart-sprintf) | **格式化String** | | [qr_code_scanner](https://github.com/juliuscanute/qr_code_scanner) | **扫码功能** | | [intl](https://github.com/dart-lang/intl) | **本地化** | | [device_info_plus](https://github.com/fluttercommunity/plus_plugins/tree/main/packages/device_info_plus) | **获取设备信息** | | [vibration](https://github.com/benjamindean/flutter_vibration) | **振动** | | [lottie](https://github.com/xvrh/lottie-flutter) | **动画效果** | 详细内容可以参看[pubspec.yaml](https://github.com/simplezhli/flutter_deer/blob/master/pubspec.yaml)文件 ## 后续计划: * [x] 添加地图功能,具体实现插件见 [flutter_2d_amap](https://github.com/simplezhli/flutter_2d_amap) * [x] 下拉刷新 + 上拉加载更多 * [x] 引入状态管理,预计使用 [provider](https://github.com/rrousselGit/provider) * [x] 页面添加设计图路径注释,方便寻找对应的设计图。 * [x] 添加集成测试。 * [x] 深色模式支持。 * [x] 添加`Semantics`(语义) * [x] Web端支持。 * [x] 迁移到空安全。(安装包减少135KB,10.3M -> 10.1M) * [ ] 迁移至Navigator 2.0。 ## 已知存在问题: - 部分使用的到的三方库没有适配3.0.0,flutter_swiper(flutter_swiper_null_safety_flutter3替代)、flustars(flustars_flutter3替代)、azlistview(升级scrollable_positioned_list)。 - 3.10.0 已知存在问题(#105203 #113595) - 2.0.0 已知存在问题(#68571 #73351 #74890 #79773 #79931) - ListView在没有设置分割线的情况下,个别Item之间存在大约1像素的间隔([像素对齐问题](https://github.com/flutter/flutter/issues/14288))。 - 其他历史问题见docs目录下的问题汇总。 ## Thanks For - [flutter_wanandroid](https://github.com/Sky24n/flutter_wanandroid) ## License Copyright 2019 simplezhli Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: analysis_options.yaml ================================================ # Specify analysis options. # # For a list of lints, see: https://dart.dev/tools/linter-rules # For guidelines on configuring static analysis, see: # https://dart.dev/tools/analysis # # There are other similar analysis options files in the flutter repos, # which should be kept in sync with this file: # # - analysis_options.yaml (this file) # - https://github.com/flutter/engine/blob/main/analysis_options.yaml # - https://github.com/flutter/packages/blob/main/analysis_options.yaml # # This file contains the analysis options used for code in the flutter/flutter # repository. analyzer: language: strict-casts: true strict-inference: true strict-raw-types: true errors: # allow deprecated members (we do this because otherwise we have to annotate # every member in every test, assert, etc, when we or the Dart SDK deprecates # something (https://github.com/flutter/flutter/issues/143312) deprecated_member_use: ignore deprecated_member_use_from_same_package: ignore # Turned off until null-safe rollout is complete. # unnecessary_null_comparison: ignore exclude: # the following two are relative to the stocks example and the flutter package respectively # see https://github.com/dart-lang/sdk/issues/28463 - "lib/l10n/**" - "lib/generated/json/**" - "lib/widgets/bezier_chart/**" # - "test/**" - "test_driver/**" formatter: page_width: 100 linter: rules: # This list is derived from the list of all available lints located at # https://github.com/dart-lang/sdk/blob/main/pkg/linter/example/all.yaml - always_declare_return_types - always_put_control_body_on_new_line # - always_put_required_named_parameters_first # we prefer having parameters in the same order as fields https://github.com/flutter/flutter/issues/10219 # - always_specify_types # - always_use_package_imports # we do this commonly - annotate_overrides - annotate_redeclares # - avoid_annotating_with_dynamic # conflicts with always_specify_types - avoid_bool_literals_in_conditional_expressions # - avoid_catches_without_on_clauses # blocked on https://github.com/dart-lang/linter/issues/3023 # - avoid_catching_errors # blocked on https://github.com/dart-lang/linter/issues/4998 # - avoid_classes_with_only_static_members - avoid_double_and_int_checks - avoid_dynamic_calls - avoid_empty_else - avoid_equals_and_hash_code_on_mutable_classes - avoid_escaping_inner_quotes - avoid_field_initializers_in_const_classes # - avoid_final_parameters # incompatible with prefer_final_parameters - avoid_function_literals_in_foreach_calls # - avoid_implementing_value_types - avoid_init_to_null - avoid_js_rounded_ints # - avoid_multiple_declarations_per_line # seems to be a stylistic choice we don't subscribe to - avoid_null_checks_in_equality_operators # - avoid_positional_boolean_parameters # would have been nice to enable this but by now there's too many places that break it - avoid_print # - avoid_private_typedef_functions # we prefer having typedef (discussion in https://github.com/flutter/flutter/pull/16356) - avoid_redundant_argument_values - avoid_relative_lib_imports - avoid_renaming_method_parameters - avoid_return_types_on_setters - avoid_returning_null_for_void # - avoid_returning_this # there are enough valid reasons to return `this` that this lint ends up with too many false positives - avoid_setters_without_getters - avoid_shadowing_type_parameters - avoid_single_cascade_in_expression_statements - avoid_slow_async_io - avoid_type_to_string - avoid_types_as_parameter_names # - avoid_types_on_closure_parameters # conflicts with always_specify_types - avoid_unnecessary_containers - avoid_unused_constructor_parameters - avoid_void_async # - avoid_web_libraries_in_flutter # we use web libraries in web-specific code, and our tests prevent us from using them elsewhere - await_only_futures - camel_case_extensions - camel_case_types - cancel_subscriptions # - cascade_invocations # doesn't match the typical style of this repo - cast_nullable_to_non_nullable # - close_sinks # not reliable enough # - comment_references # blocked on https://github.com/dart-lang/linter/issues/1142 - conditional_uri_does_not_exist # - constant_identifier_names # needs an opt-out https://github.com/dart-lang/linter/issues/204 - control_flow_in_finally - curly_braces_in_flow_control_structures - depend_on_referenced_packages - deprecated_consistency # - diagnostic_describe_all_properties # enabled only at the framework level (packages/flutter/lib) - directives_ordering # - do_not_use_environment # there are appropriate times to use the environment, especially in our tests and build logic - empty_catches - empty_constructor_bodies - empty_statements - eol_at_end_of_file - exhaustive_cases - file_names - flutter_style_todos - hash_and_equals - implementation_imports - implicit_call_tearoffs - implicit_reopen - invalid_case_patterns - invalid_runtime_check_with_js_interop_types # - invariant_booleans # too many false positives: https://github.com/dart-lang/linter/issues/811 # - join_return_with_assignment # not required by flutter style - leading_newlines_in_multiline_strings - library_names - library_prefixes # - library_private_types_in_public_api # - lines_longer_than_80_chars # required by flutter style - literal_only_boolean_expressions - missing_code_block_language_in_doc_comment - missing_whitespace_between_adjacent_strings - no_adjacent_strings_in_list - no_default_cases - no_duplicate_case_values - no_leading_underscores_for_library_prefixes - no_leading_underscores_for_local_identifiers - no_literal_bool_comparisons - no_logic_in_create_state - no_self_assignments - no_wildcard_variable_uses # - no_runtimeType_toString # ok in tests; we enable this only in packages/ - non_constant_identifier_names - noop_primitive_operations - null_check_on_nullable_type_parameter - null_closures # - omit_local_variable_types # opposite of always_specify_types # - one_member_abstracts # too many false positives - only_throw_errors # this does get disabled in a few places where we have legacy code that uses strings et al - overridden_fields - package_names - package_prefixed_library_names # - parameter_assignments # we do this commonly - prefer_adjacent_string_concatenation - prefer_asserts_in_initializer_lists # - prefer_asserts_with_message # not required by flutter style - prefer_collection_literals - prefer_conditional_assignment - prefer_const_constructors - prefer_const_constructors_in_immutables - prefer_const_declarations - prefer_const_literals_to_create_immutables # - prefer_constructors_over_static_methods # far too many false positives - prefer_contains # - prefer_double_quotes # opposite of prefer_single_quotes # - prefer_expression_function_bodies # conflicts with ./docs/contributing/Style-guide-for-Flutter-repo.md#consider-using--for-short-functions-and-methods - prefer_final_fields - prefer_final_in_for_each - prefer_final_locals # - prefer_final_parameters # we should enable this one day when it can be auto-fixed (https://github.com/dart-lang/linter/issues/3104), see also parameter_assignments - prefer_for_elements_to_map_fromIterable - prefer_foreach - prefer_function_declarations_over_variables - prefer_generic_function_type_aliases - prefer_if_elements_to_conditional_expressions - prefer_if_null_operators - prefer_initializing_formals - prefer_inlined_adds # - prefer_int_literals # conflicts with ./docs/contributing/Style-guide-for-Flutter-repo.md#use-double-literals-for-double-constants - prefer_interpolation_to_compose_strings - prefer_is_empty - prefer_is_not_empty - prefer_is_not_operator - prefer_iterable_whereType # - prefer_mixin # Has false positives, see https://github.com/dart-lang/linter/issues/3018 # - prefer_null_aware_method_calls # "call()" is confusing to people new to the language since it's not documented anywhere # - prefer_relative_imports - prefer_single_quotes - prefer_spread_collections - prefer_typing_uninitialized_variables - prefer_void_to_null - provide_deprecation_message # - public_member_api_docs # enabled on a case-by-case basis; see e.g. packages/analysis_options.yaml - recursive_getters # - require_trailing_commas # blocked on https://github.com/dart-lang/sdk/issues/47441 - secure_pubspec_urls - sized_box_for_whitespace - sized_box_shrink_expand - slash_for_doc_comments - sort_child_properties_last - sort_constructors_first # - sort_pub_dependencies # prevents separating pinned transitive dependencies - sort_unnamed_constructors_first - test_types_in_equals - throw_in_finally - tighten_type_of_initializing_formals # - type_annotate_public_apis # subset of always_specify_types - type_init_formals - type_literal_in_constant_pattern # - unawaited_futures # too many false positives, especially with the way AnimationController works - unnecessary_await_in_return - unnecessary_brace_in_string_interps - unnecessary_const - unnecessary_constructor_name # - unnecessary_final # conflicts with prefer_final_locals - unnecessary_getters_setters # - unnecessary_lambdas # has false positives: https://github.com/dart-lang/linter/issues/498 - unnecessary_late - unnecessary_library_directive # - unnecessary_library_name # blocked on blocked on https://github.com/dart-lang/dartdoc/issues/3882 - unnecessary_new - unnecessary_null_aware_assignments - unnecessary_null_aware_operator_on_extension_on_nullable - unnecessary_null_checks - unnecessary_null_in_if_null_operators - unnecessary_nullable_for_final_variable_declarations - unnecessary_overrides - unnecessary_parenthesis # - unnecessary_raw_strings # what's "necessary" is a matter of opinion; consistency across strings can help readability more than this lint - unnecessary_statements - unnecessary_string_escapes - unnecessary_string_interpolations - unnecessary_this - unnecessary_to_list_in_spreads - unreachable_from_main - unrelated_type_equality_checks - use_build_context_synchronously - use_colored_box # - use_decorated_box # not yet tested - use_enums - use_full_hex_values_for_flutter_colors - use_function_type_syntax_for_parameters - use_if_null_to_convert_nulls_to_bools - use_is_even_rather_than_modulo - use_key_in_widget_constructors - use_late_for_private_fields_and_variables - use_named_constants - use_raw_strings - use_rethrow_when_possible - use_setters_to_change_properties - use_super_parameters # - use_string_buffers # has false positives: https://github.com/dart-lang/sdk/issues/34182 - use_test_throws_matchers # - use_to_and_as_if_applicable # has false positives, so we prefer to catch this by code-review - use_truncating_division - valid_regexps - void_checks ================================================ FILE: android/app/build.gradle ================================================ plugins { id "com.android.application" id "kotlin-android" id "dev.flutter.flutter-gradle-plugin" } def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { localPropertiesFile.withReader('UTF-8') { reader -> localProperties.load(reader) } } def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' } def flutterVersionName = localProperties.getProperty('flutter.versionName') if (flutterVersionName == null) { flutterVersionName = '1.0' } def keystorePropertiesFile = rootProject.file("app/key.properties") def keystoreProperties = new Properties() keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) android { compileSdkVersion 36 ndkVersion = "28.2.13676358" defaultConfig { applicationId "com.weilu.deer" minSdkVersion flutter.minSdkVersion targetSdkVersion 35 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } signingConfigs { release { keyAlias keystoreProperties['keyAlias'] keyPassword keystoreProperties['keyPassword'] storeFile file(keystoreProperties['storeFile']) storePassword keystoreProperties['storePassword'] } } buildTypes { debug { signingConfig signingConfigs.release } release { // flutter build apk // Signing with the debug keys for now, so `flutter run --release` works. signingConfig signingConfigs.release // https://github.com/flutter/flutter/issues/47462 #48015 // #58967 gradle 4.0.0 shrinkResources false zipAlignEnabled false minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } namespace 'com.weilu.deer' lint { disable 'InvalidPackage' } } flutter { source '../..' } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.0.21" } ================================================ FILE: android/app/key.properties ================================================ storePassword=111111 keyPassword=111111 keyAlias=key storeFile=../app/test.jks ================================================ FILE: android/app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile #定位 -keep class com.amap.api.location.**{*;} -keep class com.amap.api.fence.**{*;} -keep class com.loc.**{*;} -keep class com.autonavi.aps.amapapi.model.**{*;} #搜索 -keep class com.amap.api.services.**{*;} #2D地图 -keep class com.amap.api.maps2d.**{*;} -keep class com.amap.api.mapcore2d.**{*;} ================================================ FILE: android/app/src/debug/AndroidManifest.xml ================================================ ================================================ FILE: android/app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: android/app/src/main/java/com/weilu/deer/DeerPickerProvider.java ================================================ package com.weilu.deer; import androidx.core.content.FileProvider; /** * @author weilu * 作者:weilu on 2019/8/08 15:15 */ public class DeerPickerProvider extends FileProvider { } ================================================ FILE: android/app/src/main/java/com/weilu/deer/FileProvider7.java ================================================ package com.weilu.deer; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Build; import java.io.File; import androidx.core.content.FileProvider; /** * @author weilu * 作者:weilu on 2017/6/20 14:44 */ public class FileProvider7 { public static Uri getUriForFile(Context context, File file) { Uri fileUri = null; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { fileUri = getUriForFile24(context, file); } else { fileUri = Uri.fromFile(file); } return fileUri; } private static Uri getUriForFile24(Context context, File file) { return FileProvider.getUriForFile(context, context.getPackageName() + ".Deer", file); } public static void setIntentDataAndType(Context context, Intent intent, String type, File file, boolean writeAble) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { intent.setDataAndType(getUriForFile(context, file), type); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); if (writeAble) { intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); } } else { intent.setDataAndType(Uri.fromFile(file), type); } } public static void setIntentData(Context context, Intent intent, File file, boolean writeAble) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { intent.setData(getUriForFile(context, file)); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); if (writeAble) { intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); } } else { intent.setData(Uri.fromFile(file)); } } } ================================================ FILE: android/app/src/main/java/com/weilu/deer/InstallAPKPlugin.java ================================================ package com.weilu.deer; import android.app.Activity; import android.content.Intent; import java.io.File; import androidx.annotation.NonNull; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodChannel; /** * @author weilu */ public class InstallAPKPlugin implements FlutterPlugin { private MethodChannel channel; private final Activity mActivity; public InstallAPKPlugin(Activity activity) { this.mActivity = activity; } @Override public void onAttachedToEngine(@NonNull FlutterPlugin.FlutterPluginBinding binding) { setupMethodChannel(binding.getBinaryMessenger()); } @Override public void onDetachedFromEngine(@NonNull FlutterPlugin.FlutterPluginBinding binding) { tearDownChannel(); } private void setupMethodChannel(BinaryMessenger messenger) { channel = new MethodChannel(messenger, "version"); channel.setMethodCallHandler((methodCall, result) -> { if ("install".equals(methodCall.method)) { String path = methodCall.argument("path"); openFile(path); } else { result.notImplemented(); } }); } /** * 安装 文件(APK) */ private void openFile(String path) { Intent intents = new Intent(); intents.setAction(Intent.ACTION_VIEW); intents.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); FileProvider7.setIntentDataAndType(mActivity, intents, "application/vnd.android.package-archive", new File(path), false); mActivity.startActivity(intents); } private void tearDownChannel() { channel.setMethodCallHandler(null); channel = null; } } ================================================ FILE: android/app/src/main/java/com/weilu/deer/MainActivity.java ================================================ package com.weilu.deer; import android.graphics.Color; import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import io.flutter.embedding.android.FlutterActivity; import io.flutter.embedding.engine.FlutterEngine; /** * @author weilu */ public class MainActivity extends FlutterActivity { @Override public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { super.configureFlutterEngine(flutterEngine); flutterEngine.getPlugins().add(new InstallAPKPlugin(this)); } @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); /// 设置状态栏透明,导航栏沉浸。 // getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); getWindow().setStatusBarColor(Color.TRANSPARENT); } } ================================================ FILE: android/app/src/main/java/com/weilu/deer/MyApp.java ================================================ package com.weilu.deer; import android.app.Application; /** * @Description: * @Author: weilu * @Time: 2019/8/5 0005 17:08. */ public class MyApp extends Application { @Override public void onCreate() { super.onCreate(); } } ================================================ FILE: android/app/src/main/res/drawable/launch_background.xml ================================================ ================================================ FILE: android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml ================================================ ================================================ FILE: android/app/src/main/res/values/colors.xml ================================================ #FFFFFFFF ================================================ FILE: android/app/src/main/res/values/styles.xml ================================================ ================================================ FILE: android/app/src/main/res/values-night/colors.xml ================================================ #FF18191A ================================================ FILE: android/app/src/main/res/values-v27/styles.xml ================================================ ================================================ FILE: android/app/src/main/res/xml/file_paths.xml ================================================ ================================================ FILE: android/app/src/main/res/xml/network_security_config.xml ================================================ ================================================ FILE: android/app/src/profile/AndroidManifest.xml ================================================ ================================================ FILE: android/build.gradle ================================================ allprojects { repositories { maven { url 'https://maven.aliyun.com/repository/public' } maven { url 'https://maven.aliyun.com/repository/gradle-plugin' } maven { url 'https://maven.aliyun.com/repository/central' } maven { url 'https://maven.aliyun.com/repository/google' } google() mavenCentral() maven { url 'https://jitpack.io' } } } rootProject.buildDir = '../build' subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" } subprojects { project.evaluationDependsOn(':app') } tasks.register("clean", Delete) { delete rootProject.buildDir } ================================================ FILE: android/gradle/wrapper/gradle-wrapper.properties ================================================ #Sat Nov 23 15:18:48 CST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: android/gradle.properties ================================================ android.useAndroidX=true android.enableJetifier=true # 提升编译速度配置 https://blog.csdn.net/weixin_33943347/article/details/91361727 org.gradle.daemon=true org.gradle.configureondemand=true org.gradle.parallel=true org.gradle.jvmargs=-Xmx3072m -XX:+UseParallelGC -XX:MaxPermSize=1024m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 # 开启gradle缓存 org.gradle.caching=true #开启 kotlin 的增量和并行编译 kotlin.incremental=true kotlin.incremental.java=true kotlin.incremental.js=true kotlin.caching.enabled=true kotlin.parallel.tasks.in.project=true android.defaults.buildfeatures.buildconfig=true android.nonTransitiveRClass=false android.nonFinalResIds=false ================================================ FILE: android/settings.gradle ================================================ pluginManagement { def flutterSdkPath = { def properties = new Properties() file("local.properties").withInputStream { properties.load(it) } def flutterSdkPath = properties.getProperty("flutter.sdk") assert flutterSdkPath != null, "flutter.sdk not set in local.properties" return flutterSdkPath }() includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") repositories { google() mavenCentral() gradlePluginPortal() } } plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" version '8.12.3' apply false id "org.jetbrains.kotlin.android" version "2.0.21" apply false } include ":app" ================================================ FILE: assets/data/bank.json ================================================ [ { "id": 1, "bankName": "民生银行", "firstLetter": "M" }, { "id": 2, "bankName": "工商银行", "firstLetter": "G" }, { "id": 3, "bankName": "农业银行", "firstLetter": "N" }, { "id": 4, "bankName": "中国银行", "firstLetter": "Z" }, { "id": 5, "bankName": "建设银行", "firstLetter": "J" }, { "id": 6, "bankName": "交通银行", "firstLetter": "J" }, { "id": 7, "bankName": "中信银行", "firstLetter": "Z" }, { "id": 8, "bankName": "招商银行", "firstLetter": "Z" }, { "id": 9, "bankName": "兴业银行", "firstLetter": "X" }, { "id": 10, "bankName": "浦发银行", "firstLetter": "P" }, { "id": 16, "bankName": "光大银行", "firstLetter": "G" }, { "id": 17, "bankName": "华夏银行", "firstLetter": "H" }, { "id": 18, "bankName": "广发银行", "firstLetter": "G" }, { "id": 19, "bankName": "平安银行", "firstLetter": "P" }, { "id": 20, "bankName": "北京银行", "firstLetter": "B" }, { "id": 43, "bankName": "上海银行", "firstLetter": "S" }, { "id": 44, "bankName": "南京银行", "firstLetter": "N" }, { "id": 48, "bankName": "杭州银行", "firstLetter": "H" }, { "id": 49, "bankName": "宁波银行", "firstLetter": "N" }, { "id": 54, "bankName": "浙江稠州商业银行", "firstLetter": "Z" }, { "id": 83, "bankName": "汉口银行", "firstLetter": "H" }, { "id": 84, "bankName": "长沙银行", "firstLetter": "C" }, { "id": 116, "bankName": "浙商银行", "firstLetter": "Z" }, { "id": 118, "bankName": "渤海银行", "firstLetter": "B" }, { "id": 127, "bankName": "上海农商银行", "firstLetter": "S" }, { "id": 128, "bankName": "北京农商行", "firstLetter": "B" }, { "id": 143, "bankName": "中国邮储银行", "firstLetter": "Y" } ] ================================================ FILE: assets/data/bank_2.json ================================================ [ { "id": 13035, "bankName": "西安市城南支行", "firstLetter": "X" }, { "id": 13036, "bankName": "西安经济技术开发区支行", "firstLetter": "X" }, { "id": 13037, "bankName": "西安市东新街支行", "firstLetter": "X" }, { "id": 13038, "bankName": "西安南大街支行", "firstLetter": "X" }, { "id": 13039, "bankName": "西安市解放路支行", "firstLetter": "X" }, { "id": 13040, "bankName": "西安市太华路支行", "firstLetter": "X" }, { "id": 13041, "bankName": "西安民乐园支行", "firstLetter": "X" }, { "id": 13042, "bankName": "西安市东大街支行", "firstLetter": "X" }, { "id": 13043, "bankName": "西安南院门支行", "firstLetter": "X" }, { "id": 13044, "bankName": "西安东关支行", "firstLetter": "X" }, { "id": 13045, "bankName": "西安和平路支行", "firstLetter": "X" }, { "id": 13046, "bankName": "西安互助路支行", "firstLetter": "X" }, { "id": 13047, "bankName": "西安市北大街支行", "firstLetter": "X" }, { "id": 13048, "bankName": "西安西大街支行", "firstLetter": "X" }, { "id": 13049, "bankName": "西安星火路支行", "firstLetter": "X" }, { "id": 13050, "bankName": "西安市南关支行", "firstLetter": "X" }, { "id": 13051, "bankName": "西安小寨支行", "firstLetter": "X" }, { "id": 13052, "bankName": "西安雁塔路支行", "firstLetter": "X" }, { "id": 13053, "bankName": "西安含光路支行", "firstLetter": "X" }, { "id": 13054, "bankName": "西安铁路局支行", "firstLetter": "X" }, { "id": 13055, "bankName": "西安电子工业区支行", "firstLetter": "X" }, { "id": 13056, "bankName": "西安大雁塔支行", "firstLetter": "X" }, { "id": 13057, "bankName": "西安市土门支行", "firstLetter": "X" }, { "id": 13058, "bankName": "西安市纺织城支行", "firstLetter": "X" }, { "id": 13059, "bankName": "西安市韩森寨支行", "firstLetter": "X" }, { "id": 13060, "bankName": "西安咸宁路支行", "firstLetter": "X" }, { "id": 13061, "bankName": "西安市韩森寨支行万寿路分理处", "firstLetter": "X" }, { "id": 13062, "bankName": "西安市阎良区支行", "firstLetter": "X" }, { "id": 13063, "bankName": "西安市周至县支行", "firstLetter": "X" }, { "id": 13064, "bankName": "西安市户县支行", "firstLetter": "X" }, { "id": 13065, "bankName": "西安市临潼区支行", "firstLetter": "X" }, { "id": 13066, "bankName": "西安市高新技术开发区支行", "firstLetter": "X" }, { "id": 13067, "bankName": "西安劳动南路支行", "firstLetter": "X" }, { "id": 13068, "bankName": "西安市未央支行", "firstLetter": "X" }, { "id": 13069, "bankName": "西安徐家湾支行", "firstLetter": "X" }, { "id": 17731, "bankName": "西安莲湖路支行", "firstLetter": "X" }, { "id": 17747, "bankName": "陕西省西安凤城八路分理处", "firstLetter": "S" }, { "id": 17748, "bankName": "西安长乐中路支行", "firstLetter": "X" }, { "id": 17749, "bankNumber": "102791013020", "bankName": "陕西省西安咸宁中路支行", "firstLetter": "S" }, { "id": 17750, "bankName": "西安万寿南路分理处", "firstLetter": "X" }, { "id": 17759, "bankName": "陕西省西安紫薇田园都市分理处", "firstLetter": "S" } ] ================================================ FILE: assets/data/city.json ================================================ [ { "name": "阿拉善盟", "cityCode": "0483", "firstCharacter": "A" }, { "name": "鞍山市", "cityCode": "0412", "firstCharacter": "A" }, { "name": "安庆市", "cityCode": "0556", "firstCharacter": "A" }, { "name": "安阳市", "cityCode": "0372", "firstCharacter": "A" }, { "name": "阿坝藏族羌族自治州", "cityCode": "0837", "firstCharacter": "A" }, { "name": "安顺市", "cityCode": "0853", "firstCharacter": "A" }, { "name": "安康市", "cityCode": "0915", "firstCharacter": "A" }, { "name": "阿克苏地区", "cityCode": "0997", "firstCharacter": "A" }, { "name": "阿勒泰地区", "cityCode": "0906", "firstCharacter": "A" }, { "name": "北京市", "cityCode": "010", "firstCharacter": "B" }, { "name": "保定市", "cityCode": "0312", "firstCharacter": "B" }, { "name": "包头市", "cityCode": "0472", "firstCharacter": "B" }, { "name": "巴彦淖尔市", "cityCode": "0478", "firstCharacter": "B" }, { "name": "本溪市", "cityCode": "0414", "firstCharacter": "B" }, { "name": "白山市", "cityCode": "0439", "firstCharacter": "B" }, { "name": "白城市", "cityCode": "0436", "firstCharacter": "B" }, { "name": "蚌埠市", "cityCode": "0552", "firstCharacter": "B" }, { "name": "亳州市", "cityCode": "0558", "firstCharacter": "B" }, { "name": "滨州市", "cityCode": "0543", "firstCharacter": "B" }, { "name": "北海市", "cityCode": "0779", "firstCharacter": "B" }, { "name": "百色市", "cityCode": "0776", "firstCharacter": "B" }, { "name": "巴中市", "cityCode": "0827", "firstCharacter": "B" }, { "name": "毕节市", "cityCode": "0857", "firstCharacter": "B" }, { "name": "保山市", "cityCode": "0875", "firstCharacter": "B" }, { "name": "宝鸡市", "cityCode": "0917", "firstCharacter": "B" }, { "name": "白银市", "cityCode": "0943", "firstCharacter": "B" }, { "name": "博尔塔拉蒙古自治州", "cityCode": "0909", "firstCharacter": "B" }, { "name": "巴音郭楞蒙古自治州", "cityCode": "0996", "firstCharacter": "B" }, { "name": "承德市", "cityCode": "0314", "firstCharacter": "C" }, { "name": "沧州市", "cityCode": "0317", "firstCharacter": "C" }, { "name": "长治市", "cityCode": "0355", "firstCharacter": "C" }, { "name": "赤峰市", "cityCode": "0476", "firstCharacter": "C" }, { "name": "朝阳市", "cityCode": "0421", "firstCharacter": "C" }, { "name": "长春市", "cityCode": "0431", "firstCharacter": "C" }, { "name": "常州市", "cityCode": "0519", "firstCharacter": "C" }, { "name": "滁州市", "cityCode": "0550", "firstCharacter": "C" }, { "name": "池州市", "cityCode": "0566", "firstCharacter": "C" }, { "name": "长沙市", "cityCode": "0731", "firstCharacter": "C" }, { "name": "常德市", "cityCode": "0736", "firstCharacter": "C" }, { "name": "潮州市", "cityCode": "0768", "firstCharacter": "C" }, { "name": "崇左市", "cityCode": "0771", "firstCharacter": "C" }, { "name": "重庆市", "cityCode": "023", "firstCharacter": "C" }, { "name": "成都市", "cityCode": "028", "firstCharacter": "C" }, { "name": "楚雄彝族自治州", "cityCode": "0878", "firstCharacter": "C" }, { "name": "昌吉回族自治州", "cityCode": "0994", "firstCharacter": "C" }, { "name": "嘉义市", "cityCode": "05", "firstCharacter": "C" }, { "name": "彰化县", "cityCode": "04", "firstCharacter": "C" }, { "name": "嘉义县", "cityCode": "05", "firstCharacter": "C" }, { "name": "大同市", "cityCode": "0352", "firstCharacter": "D" }, { "name": "大连市", "cityCode": "0411", "firstCharacter": "D" }, { "name": "丹东市", "cityCode": "0415", "firstCharacter": "D" }, { "name": "大庆市", "cityCode": "0459", "firstCharacter": "D" }, { "name": "德州市", "cityCode": "0534", "firstCharacter": "D" }, { "name": "东莞市", "cityCode": "0769", "firstCharacter": "D" }, { "name": "德阳市", "cityCode": "0838", "firstCharacter": "D" }, { "name": "达州市", "cityCode": "0818", "firstCharacter": "D" }, { "name": "大理白族自治州", "cityCode": "0872", "firstCharacter": "D" }, { "name": "德宏傣族景颇族自治州", "cityCode": "0692", "firstCharacter": "D" }, { "name": "迪庆藏族自治州", "cityCode": "0887", "firstCharacter": "D" }, { "name": "定西市", "cityCode": "0932", "firstCharacter": "D" }, { "name": "鄂州市", "cityCode": "0711", "firstCharacter": "E" }, { "name": "恩施土家族苗族自治州", "cityCode": "0718", "firstCharacter": "E" }, { "name": "抚顺市", "cityCode": "024", "firstCharacter": "F" }, { "name": "阜阳市", "cityCode": "0558", "firstCharacter": "F" }, { "name": "福州市", "cityCode": "0591", "firstCharacter": "F" }, { "name": "抚州市", "cityCode": "0794", "firstCharacter": "F" }, { "name": "佛山市", "cityCode": "0757", "firstCharacter": "F" }, { "name": "防城港市", "cityCode": "0770", "firstCharacter": "F" }, { "name": "赣州市", "cityCode": "0797", "firstCharacter": "G" }, { "name": "广州市", "cityCode": "020", "firstCharacter": "G" }, { "name": "桂林市", "cityCode": "0773", "firstCharacter": "G" }, { "name": "贵港市", "cityCode": "0775", "firstCharacter": "G" }, { "name": "广元市", "cityCode": "0839", "firstCharacter": "G" }, { "name": "广安市", "cityCode": "0826", "firstCharacter": "G" }, { "name": "贵阳市", "cityCode": "0851", "firstCharacter": "G" }, { "name": "甘南藏族自治州", "cityCode": "0941", "firstCharacter": "G" }, { "name": "邯郸市", "cityCode": "0310", "firstCharacter": "H" }, { "name": "衡水市", "cityCode": "0318", "firstCharacter": "H" }, { "name": "呼和浩特市", "cityCode": "0471", "firstCharacter": "H" }, { "name": "呼伦贝尔市", "cityCode": "0470", "firstCharacter": "H" }, { "name": "兴安盟", "cityCode": "0482", "firstCharacter": "H" }, { "name": "葫芦岛市", "cityCode": "0429", "firstCharacter": "H" }, { "name": "哈尔滨市", "cityCode": "0451", "firstCharacter": "H" }, { "name": "鹤岗市", "cityCode": "0468", "firstCharacter": "H" }, { "name": "黑河市", "cityCode": "0456", "firstCharacter": "H" }, { "name": "淮安市", "cityCode": "0517", "firstCharacter": "H" }, { "name": "杭州市", "cityCode": "0571", "firstCharacter": "H" }, { "name": "湖州市", "cityCode": "0572", "firstCharacter": "H" }, { "name": "合肥市", "cityCode": "0551", "firstCharacter": "H" }, { "name": "淮南市", "cityCode": "0554", "firstCharacter": "H" }, { "name": "淮北市", "cityCode": "0561", "firstCharacter": "H" }, { "name": "黄山市", "cityCode": "0559", "firstCharacter": "H" }, { "name": "菏泽市", "cityCode": "0530", "firstCharacter": "H" }, { "name": "鹤壁市", "cityCode": "0392", "firstCharacter": "H" }, { "name": "黄石市", "cityCode": "0714", "firstCharacter": "H" }, { "name": "黄冈市", "cityCode": "0713", "firstCharacter": "H" }, { "name": "衡阳市", "cityCode": "0734", "firstCharacter": "H" }, { "name": "怀化市", "cityCode": "0745", "firstCharacter": "H" }, { "name": "海口市", "cityCode": "0898", "firstCharacter": "H" }, { "name": "汉中市", "cityCode": "0916", "firstCharacter": "H" }, { "name": "伊犁哈萨克自治州", "cityCode": "0999", "firstCharacter": "I" }, { "name": "晋城市", "cityCode": "0356", "firstCharacter": "J" }, { "name": "晋中市", "cityCode": "0354", "firstCharacter": "J" }, { "name": "吉林市", "cityCode": "0432", "firstCharacter": "J" }, { "name": "金华市", "cityCode": "0579", "firstCharacter": "J" }, { "name": "济南市", "cityCode": "0531", "firstCharacter": "J" }, { "name": "焦作市", "cityCode": "0391", "firstCharacter": "J" }, { "name": "开封市", "cityCode": "0378", "firstCharacter": "K" }, { "name": "昆明市", "cityCode": "0871", "firstCharacter": "K" }, { "name": "克孜勒苏柯尔克孜自治州", "cityCode": "0908", "firstCharacter": "K" }, { "name": "九龙", "cityCode": "00852", "firstCharacter": "K" }, { "name": "临汾市", "cityCode": "0357", "firstCharacter": "L" }, { "name": "丽水市", "cityCode": "0578", "firstCharacter": "L" }, { "name": "临沂市", "cityCode": "0539", "firstCharacter": "L" }, { "name": "洛阳市", "cityCode": "0379", "firstCharacter": "L" }, { "name": "拉萨市", "cityCode": "0891", "firstCharacter": "L" }, { "name": "牡丹江市", "cityCode": "0453", "firstCharacter": "M" }, { "name": "马鞍山市", "cityCode": "0555", "firstCharacter": "M" }, { "name": "茂名市", "cityCode": "0668", "firstCharacter": "M" }, { "name": "南京市", "cityCode": "025", "firstCharacter": "N" }, { "name": "南通市", "cityCode": "0513", "firstCharacter": "N" }, { "name": "宁波市", "cityCode": "0574", "firstCharacter": "N" }, { "name": "莆田市", "cityCode": "0594", "firstCharacter": "P" }, { "name": "平顶山市", "cityCode": "0375", "firstCharacter": "P" }, { "name": "衢州市", "cityCode": "0570", "firstCharacter": "Q" }, { "name": "泉州市", "cityCode": "0595", "firstCharacter": "Q" }, { "name": "青岛市", "cityCode": "0532", "firstCharacter": "Q" }, { "name": "庆阳市", "cityCode": "0934", "firstCharacter": "Q" }, { "name": "日照市", "cityCode": "0633", "firstCharacter": "R" }, { "name": "石家庄市", "cityCode": "0311", "firstCharacter": "S" }, { "name": "朔州市", "cityCode": "0349", "firstCharacter": "S" }, { "name": "沈阳市", "cityCode": "024", "firstCharacter": "S" }, { "name": "苏州市", "cityCode": "0512", "firstCharacter": "S" }, { "name": "十堰市", "cityCode": "0719", "firstCharacter": "S" }, { "name": "三沙市", "cityCode": "0898", "firstCharacter": "S" }, { "name": "石嘴山市", "cityCode": "0952", "firstCharacter": "S" }, { "name": "天津市", "cityCode": "022", "firstCharacter": "T" }, { "name": "唐山市", "cityCode": "0315", "firstCharacter": "T" }, { "name": "太原市", "cityCode": "0351", "firstCharacter": "T" }, { "name": "台州市", "cityCode": "0576", "firstCharacter": "T" }, { "name": "吐鲁番地区", "cityCode": "0995", "firstCharacter": "T" }, { "name": "乌兰察布市", "cityCode": "0474", "firstCharacter": "W" }, { "name": "乌鲁木齐市", "cityCode": "0991", "firstCharacter": "W" }, { "name": "潍坊市", "cityCode": "0536", "firstCharacter": "W" }, { "name": "威海市", "cityCode": "0631", "firstCharacter": "W" }, { "name": "武汉市", "cityCode": "0022222", "firstCharacter": "W" }, { "name": "邢台市", "cityCode": "0319", "firstCharacter": "X" }, { "name": "忻州市", "cityCode": "0350", "firstCharacter": "X" }, { "name": "信阳市", "cityCode": "0376", "firstCharacter": "X" }, { "name": "阳泉市", "cityCode": "0353", "firstCharacter": "Y" }, { "name": "运城市", "cityCode": "0359", "firstCharacter": "Y" }, { "name": "营口市", "cityCode": "0417", "firstCharacter": "Y" }, { "name": "宜昌市", "cityCode": "0717", "firstCharacter": "Y" }, { "name": "岳阳市", "cityCode": "0730", "firstCharacter": "Y" }, { "name": "玉溪市", "cityCode": "0877", "firstCharacter": "Y" }, { "name": "舟山群岛新区", "cityCode": "0580", "firstCharacter": "Z" }, { "name": "郑州市", "cityCode": "0371", "firstCharacter": "Z" }, { "name": "肇庆市", "cityCode": "0758", "firstCharacter": "Z" }, { "name": "张掖市", "cityCode": "0936", "firstCharacter": "Z" } ] ================================================ FILE: assets/data/sort_0.json ================================================ [ { "id": "1", "name": "超市便利" }, { "id": "2", "name": "生鲜果蔬" }, { "id": "3", "name": "零食小吃" }, { "id": "4", "name": "美食餐饮" }, { "id": "5", "name": "鲜花烘培" }, { "id": "6", "name": "生活服务" }, { "id": "7", "name": "其他" }, { "id": "8", "name": "综合" }, { "id": "10", "name": "美容个护" }, { "id": "11", "name": "家居生活" }, { "id": "12", "name": "服饰箱包" }, { "id": "13", "name": "母婴玩具" }, { "id": "15", "name": "海淘进口" }, { "id": "755", "name": "快递代收" }, { "id": "756", "name": "食品保健" }, { "id": "764", "name": "家居生活" }, { "id": "769", "name": "米面杂粮" }, { "id": "786", "name": "水果生鲜" }, { "id": "807", "name": "社区健身" }, { "id": "811", "name": "艺术礼品" }, { "id": "814", "name": "今日特卖" }, { "id": "816", "name": "周边旅游" }, { "id": "820", "name": "家装建材" }, { "id": "823", "name": "虚拟商品" }, { "id": "14057", "name": "生活用品" }, { "id": "14181", "name": "手机数码" } ] ================================================ FILE: assets/data/sort_1.json ================================================ [ { "id": "15677", "name": "厨房用具" }, { "id": "15690", "name": "精美餐具" }, { "id": "15698", "name": "家纺" }, { "id": "15717", "name": "家具" }, { "id": "15740", "name": "灯具" }, { "id": "15755", "name": "生活日用" }, { "id": "15765", "name": "宠物用品" }, { "id": "15773", "name": "家装建材" }, { "id": "15795", "name": "赠品" }, { "id": "15797", "name": "家装软饰" }, { "id": "15814", "name": "收纳用品" }, { "id": "26541", "name": "演出票务" }, { "id": "26551", "name": "健康体检" }, { "id": "26554", "name": "教育培训" }, { "id": "26561", "name": "汽车保养" }, { "id": "26563", "name": "影视会员" }, { "id": "26565", "name": "摄影、摄像" } ] ================================================ FILE: assets/data/sort_2.json ================================================ [ { "id": "15691", "name": "酒具/杯具" }, { "id": "15692", "name": "水具" }, { "id": "15693", "name": "筷勺/刀叉" }, { "id": "15694", "name": "碗碟" }, { "id": "15695", "name": "组合套装" }, { "id": "15696", "name": "美食工具" }, { "id": "15697", "name": "茶具/咖啡具" } ] ================================================ FILE: assets/lottie/bunny_new_mouth.json ================================================ { "assets": [], "layers": [ { "ddd": 0, "ind": 0, "ty": 4, "nm": "left_hand_mask", "td": 1, "ks": { "o": { "k": 100 }, "r": { "k": 0 }, "p": { "k": [ 500, 500, 0 ] }, "a": { "k": [ 476.25, 476.25, 0 ] }, "s": { "k": [ 100, 100, 100 ] } }, "ao": 0, "shapes": [ { "ty": "gr", "it": [ { "ind": 0, "ty": "sh", "ks": { "k": { "i": [ [ 0, 262.888 ], [ 262.888, 0 ], [ 0, -262.888 ], [ -262.888, 0 ] ], "o": [ [ 0, -262.888 ], [ -262.888, 0 ], [ 0, 262.888 ], [ 262.888, 0 ] ], "v": [ [ 476, 0 ], [ 0, -476 ], [ -476, 0 ], [ 0, 476 ] ], "c": true } }, "nm": "Path 1", "mn": "ADBE Vector Shape - Group" }, { "ty": "fl", "fillEnabled": true, "c": { "k": [ 0.62, 0.79, 0.81, 1 ] }, "o": { "k": 100 }, "nm": "Fill 1", "mn": "ADBE Vector Graphic - Fill" }, { "ty": "tr", "p": { "k": [ 476.25, 476.25 ], "ix": 2 }, "a": { "k": [ 0, 0 ], "ix": 1 }, "s": { "k": [ 100, 100 ], "ix": 3 }, "r": { "k": 0, "ix": 6 }, "o": { "k": 100, "ix": 7 }, "sk": { "k": 0, "ix": 4 }, "sa": { "k": 0, "ix": 5 }, "nm": "Transform" } ], "nm": "Group 1", "np": 2, "mn": "ADBE Vector Group" } ], "ip": 0, "op": 78, "st": 0, "bm": 0, "sr": 1 }, { "ddd": 0, "ind": 1, "ty": 4, "nm": "hand_left", "tt": 1, "ks": { "o": { "k": 100 }, "r": { "k": 0 }, "p": { "k": [ { "i": { "x": 0.69, "y": 1 }, "o": { "x": 1, "y": 0 }, "n": "0p69_1_1_0", "t": 29, "s": [ 208.794, 1161.368, 0 ], "e": [ 312.794, 745.368, 0 ], "to": [ 17.3333339691162, -69.3333358764648, 0 ], "ti": [ -17.3333339691162, 69.3333358764648, 0 ] }, { "i": { "x": 0.21, "y": 0.21 }, "o": { "x": 1, "y": 1 }, "n": "0p21_0p21_1_1", "t": 39, "s": [ 312.794, 745.368, 0 ], "e": [ 312.794, 745.368, 0 ], "to": [ 0, 0, 0 ], "ti": [ 0, 0, 0 ] }, { "i": { "x": 0.21, "y": 1 }, "o": { "x": 0.13, "y": 0 }, "n": "0p21_1_0p13_0", "t": 44, "s": [ 312.794, 745.368, 0 ], "e": [ 208.794, 1161.368, 0 ], "to": [ -17.3333339691162, 69.3333358764648, 0 ], "ti": [ 0, 0, 0 ] }, { "i": { "x": 0.667, "y": 1 }, "o": { "x": 0.13, "y": 0 }, "n": "0p667_1_0p13_0", "t": 54, "s": [ 208.794, 1161.368, 0 ], "e": [ 312.794, 745.368, 0 ], "to": [ 0, 0, 0 ], "ti": [ -17.3333339691162, 69.3333358764648, 0 ] }, { "i": { "x": 0.69, "y": 0.69 }, "o": { "x": 0.333, "y": 0.333 }, "n": "0p69_0p69_0p333_0p333", "t": 59, "s": [ 312.794, 745.368, 0 ], "e": [ 312.794, 745.368, 0 ], "to": [ 0, 0, 0 ], "ti": [ 0, 0, 0 ] }, { "i": { "x": 0.69, "y": 1 }, "o": { "x": 0.86, "y": 0 }, "n": "0p69_1_0p86_0", "t": 67.904, "s": [ 312.794, 745.368, 0 ], "e": [ 208.794, 1161.368, 0 ], "to": [ -17.3333339691162, 69.3333358764648, 0 ], "ti": [ 17.3333339691162, -69.3333358764648, 0 ] }, { "t": 76 } ] }, "a": { "k": [ 109.858, 208.134, 0 ] }, "s": { "k": [ 100, 100, 100 ] } }, "ao": 0, "shapes": [ { "ty": "gr", "it": [ { "ind": 0, "ty": "sh", "ks": { "k": { "i": [ [ -0.087, 0.362 ], [ -0.849, 3.519 ], [ -1.155, 4.776 ], [ -0.76, 3.148 ], [ -1.315, 5.442 ], [ -1.023, 4.221 ], [ -1.171, 4.85 ], [ -0.84, 3.481 ], [ -1.157, 4.774 ], [ -0.926, 3.814 ], [ -1.17, 4.85 ], [ -0.839, 3.482 ], [ -1.158, 4.775 ], [ -0.922, 3.814 ], [ -1.154, 4.776 ], [ -1.023, 4.22 ], [ -1.17, 4.85 ], [ -0.839, 3.481 ], [ -1.158, 4.774 ], [ -0.924, 3.814 ], [ -1.171, 4.85 ], [ -0.839, 3.481 ], [ -1.156, 4.775 ], [ -0.758, 3.149 ], [ -1.37, 5.821 ], [ -0.54, 5.338 ], [ -0.42, 1.913 ], [ 0.047, 0.178 ], [ 0.021, 1.028 ], [ 0.052, 0.414 ], [ 0.518, 2.7 ], [ 1.959, 4.408 ], [ 1.197, 2.003 ], [ 2.498, 2.793 ], [ 2.407, 2.031 ], [ 3.596, 2.031 ], [ 3.788, 1.248 ], [ 4.518, 0.388 ], [ 2.353, -0.169 ], [ 2.464, -0.317 ], [ 2.752, -0.768 ], [ 4.156, -2.244 ], [ 3.729, -3.237 ], [ 1.854, -2.234 ], [ 2.259, -4.752 ], [ 0.72, -2.134 ], [ 0.626, -2.636 ], [ 1.193, -4.923 ], [ 1.681, -7 ], [ 1.637, -6.696 ], [ 1.23, -5.228 ], [ 1.408, -5.811 ], [ 1.766, -7.293 ], [ 1.244, -5.147 ], [ 1.426, -5.886 ], [ 1.767, -7.253 ], [ 0.872, -3.591 ], [ 1.171, -4.85 ], [ 0.831, -3.443 ], [ 1.184, -4.886 ], [ 0.907, -3.739 ], [ 1.332, -5.517 ], [ 0.838, -3.481 ], [ 1.315, -5.442 ], [ 0.686, -2.797 ], [ -40.643, -16.932 ] ], "o": [ [ 0.85, -3.518 ], [ 1.152, -4.776 ], [ 0.762, -3.147 ], [ 1.314, -5.443 ], [ 1.02, -4.221 ], [ 1.176, -4.848 ], [ 0.84, -3.48 ], [ 1.151, -4.775 ], [ 0.924, -3.814 ], [ 1.176, -4.848 ], [ 0.84, -3.481 ], [ 1.151, -4.776 ], [ 0.924, -3.812 ], [ 1.155, -4.775 ], [ 1.02, -4.221 ], [ 1.175, -4.849 ], [ 0.84, -3.48 ], [ 1.152, -4.775 ], [ 0.924, -3.814 ], [ 1.177, -4.848 ], [ 0.84, -3.481 ], [ 1.152, -4.776 ], [ 0.762, -3.147 ], [ 1.399, -5.813 ], [ 1.221, -5.184 ], [ 0.195, -1.936 ], [ 0.04, -0.182 ], [ -0.265, -1.021 ], [ -0.009, -0.421 ], [ -0.338, -2.718 ], [ -0.911, -4.753 ], [ -0.947, -2.131 ], [ -1.924, -3.218 ], [ -2.087, -2.334 ], [ -3.168, -2.671 ], [ -3.468, -1.959 ], [ -4.328, -1.427 ], [ -2.296, -0.196 ], [ -2.467, 0.177 ], [ -2.844, 0.368 ], [ -4.542, 1.269 ], [ -4.358, 2.353 ], [ -2.197, 1.908 ], [ -3.354, 4.041 ], [ -0.967, 2.035 ], [ -0.865, 2.564 ], [ -1.172, 4.929 ], [ -1.696, 6.996 ], [ -1.611, 6.703 ], [ -1.276, 5.217 ], [ -1.368, 5.821 ], [ -1.766, 7.293 ], [ -1.247, 5.146 ], [ -1.424, 5.886 ], [ -1.758, 7.256 ], [ -0.874, 3.59 ], [ -1.178, 4.848 ], [ -0.832, 3.443 ], [ -1.179, 4.888 ], [ -0.905, 3.739 ], [ -1.336, 5.516 ], [ -0.841, 3.48 ], [ -1.31, 5.444 ], [ -0.676, 2.799 ], [ 34.022, 26.956 ], [ 0.087, -0.362 ] ], "v": [ [ 17.125, 196.379 ], [ 19.69, 185.829 ], [ 23.133, 171.498 ], [ 25.45, 162.064 ], [ 29.37, 145.732 ], [ 32.463, 133.076 ], [ 35.968, 118.525 ], [ 38.499, 108.085 ], [ 41.944, 93.755 ], [ 44.748, 82.322 ], [ 48.253, 67.771 ], [ 50.784, 57.331 ], [ 54.229, 43 ], [ 57.032, 31.568 ], [ 60.466, 17.234 ], [ 63.557, 4.579 ], [ 67.062, -9.973 ], [ 69.591, -20.413 ], [ 73.04, -34.742 ], [ 75.841, -46.176 ], [ 79.347, -60.727 ], [ 81.876, -71.167 ], [ 85.324, -85.497 ], [ 87.637, -94.933 ], [ 91.803, -112.38 ], [ 95.063, -128.023 ], [ 95.547, -133.832 ], [ 95.546, -134.402 ], [ 95.415, -137.487 ], [ 95.409, -138.749 ], [ 94.28, -146.888 ], [ 89.919, -160.601 ], [ 86.662, -166.785 ], [ 80.106, -175.871 ], [ 73.416, -182.468 ], [ 63.265, -189.504 ], [ 52.348, -194.232 ], [ 39.068, -197.004 ], [ 32.109, -197.297 ], [ 24.709, -196.721 ], [ 16.32, -195.009 ], [ 3.285, -189.717 ], [ -8.791, -181.263 ], [ -14.825, -174.988 ], [ -23.31, -161.822 ], [ -25.88, -155.575 ], [ -28.075, -147.761 ], [ -31.685, -132.998 ], [ -36.743, -112.002 ], [ -41.57, -91.892 ], [ -45.367, -76.234 ], [ -49.575, -58.795 ], [ -54.878, -36.917 ], [ -58.609, -21.478 ], [ -62.883, -3.82 ], [ -68.164, 17.945 ], [ -70.814, 28.708 ], [ -74.32, 43.259 ], [ -76.828, 53.586 ], [ -80.358, 68.25 ], [ -83.1, 79.462 ], [ -87.092, 96.012 ], [ -89.624, 106.451 ], [ -93.542, 122.784 ], [ -95.593, 131.176 ], [ 16.864, 197.466 ] ], "c": true } }, "nm": "Path 1", "mn": "ADBE Vector Shape - Group" }, { "ty": "fl", "fillEnabled": true, "c": { "k": [ 0.94, 0.94, 0.94, 1 ] }, "o": { "k": 100 }, "nm": "Fill 1", "mn": "ADBE Vector Graphic - Fill" }, { "ty": "tr", "p": { "k": [ 108.871, 212.877 ], "ix": 2 }, "a": { "k": [ 0, 0 ], "ix": 1 }, "s": { "k": [ 100, 100 ], "ix": 3 }, "r": { "k": 0, "ix": 6 }, "o": { "k": 100, "ix": 7 }, "sk": { "k": 0, "ix": 4 }, "sa": { "k": 0, "ix": 5 }, "nm": "Transform" } ], "nm": "Group 1", "np": 2, "mn": "ADBE Vector Group" }, { "ty": "gr", "it": [ { "ind": 0, "ty": "sh", "ks": { "k": { "i": [ [ -2.786, 11.518 ], [ -3.055, 12.631 ], [ -3.136, 12.966 ], [ -3.461, 14.304 ], [ -3.307, 13.672 ], [ -2.958, 12.223 ], [ -2.635, 10.886 ], [ -3.051, 12.632 ], [ -0.322, 4.6 ], [ -0.29, 0.427 ], [ -0.07, 3.445 ], [ 0.031, 0.271 ], [ -0.008, 0.346 ], [ 0.138, 1.291 ], [ 3.501, 7.461 ], [ 7.346, 6.858 ], [ 6.083, 3.294 ], [ 5.005, 1.469 ], [ 5.277, 0.165 ], [ 0.422, 0.008 ], [ 0.358, 0.007 ], [ 0.311, 0.236 ], [ 0, 0 ], [ 0.34, -0.041 ], [ 0.303, 0.006 ], [ 0.426, -0.167 ], [ 1.784, -0.213 ], [ 4.574, -1.424 ], [ 7.008, -5.122 ], [ 5.118, -7.737 ], [ 2.11, -8.664 ], [ 2.446, -10.105 ], [ 3.411, -14.078 ], [ 2.887, -11.923 ], [ 2.614, -10.81 ], [ 3.056, -12.63 ], [ 2.623, -10.848 ], [ 3.468, -14.34 ], [ 1.587, -6.55 ], [ -4.407, -3.492 ], [ -0.677, 2.799 ], [ -1.31, 5.444 ], [ -0.841, 3.48 ], [ -1.337, 5.516 ], [ -0.906, 3.739 ], [ -1.18, 4.888 ], [ -0.831, 3.443 ], [ -1.178, 4.848 ], [ -0.874, 3.59 ], [ -1.758, 7.256 ], [ -1.423, 5.887 ], [ -1.246, 5.146 ], [ -1.767, 7.293 ], [ -1.369, 5.821 ], [ -1.275, 5.217 ], [ -1.61, 6.703 ], [ -1.696, 6.996 ], [ -1.172, 4.929 ], [ -0.866, 2.564 ], [ -0.967, 2.035 ], [ -3.355, 4.041 ], [ -2.197, 1.908 ], [ -4.358, 2.353 ], [ -4.543, 1.269 ], [ -2.845, 0.367 ], [ -2.467, 0.177 ], [ -2.295, -0.196 ], [ -4.329, -1.427 ], [ -3.468, -1.959 ], [ -3.168, -2.671 ], [ -2.087, -2.334 ], [ -1.924, -3.218 ], [ -0.947, -2.13 ], [ -0.912, -4.753 ], [ -0.339, -2.718 ], [ -0.009, -0.421 ], [ -0.264, -1.021 ], [ 0.04, -0.182 ], [ 0.196, -1.936 ], [ 1.221, -5.184 ], [ 1.399, -5.813 ], [ 0.761, -3.148 ], [ 1.151, -4.776 ], [ 0.841, -3.48 ], [ 1.176, -4.848 ], [ 0.924, -3.814 ], [ 1.151, -4.775 ], [ 0.839, -3.48 ], [ 1.175, -4.85 ], [ 1.02, -4.221 ], [ 1.155, -4.775 ], [ 0.924, -3.813 ], [ 1.151, -4.776 ], [ 0.84, -3.481 ], [ 1.177, -4.848 ], [ 0.925, -3.814 ], [ 1.151, -4.775 ], [ 0.84, -3.48 ], [ 1.176, -4.848 ], [ 1.02, -4.22 ], [ 1.314, -5.442 ], [ 0.762, -3.147 ], [ 1.152, -4.776 ], [ 0.85, -3.518 ], [ 0.087, -0.362 ], [ -4.789, -1.816 ], [ 0, 0 ] ], "o": [ [ 3.055, -12.632 ], [ 3.136, -12.966 ], [ 3.461, -14.304 ], [ 3.309, -13.672 ], [ 2.957, -12.222 ], [ 2.635, -10.884 ], [ 3.058, -12.631 ], [ 1.077, -4.46 ], [ 0.214, -0.44 ], [ 0.07, -3.445 ], [ -0.207, -0.25 ], [ 0.007, -0.346 ], [ 0.042, -1.299 ], [ -0.88, -8.221 ], [ -4.248, -9.053 ], [ -5.069, -4.733 ], [ -4.591, -2.489 ], [ -5.034, -1.476 ], [ -0.422, -0.009 ], [ -0.358, -0.008 ], [ -0.336, -0.055 ], [ 0, 0 ], [ -0.323, 0.224 ], [ -0.303, -0.007 ], [ -0.433, 0.031 ], [ -1.791, 0.153 ], [ -4.754, 0.569 ], [ -8.293, 2.582 ], [ -7.495, 5.478 ], [ -4.922, 7.439 ], [ -2.461, 10.1 ], [ -3.408, 14.078 ], [ -2.888, 11.924 ], [ -2.617, 10.811 ], [ -3.054, 12.63 ], [ -2.625, 10.847 ], [ -3.467, 14.34 ], [ -1.584, 6.55 ], [ 4.278, 3.642 ], [ 0.686, -2.797 ], [ 1.315, -5.442 ], [ 0.837, -3.481 ], [ 1.333, -5.517 ], [ 0.906, -3.739 ], [ 1.183, -4.886 ], [ 0.831, -3.443 ], [ 1.172, -4.85 ], [ 0.872, -3.591 ], [ 1.766, -7.254 ], [ 1.426, -5.886 ], [ 1.245, -5.146 ], [ 1.766, -7.293 ], [ 1.407, -5.811 ], [ 1.229, -5.228 ], [ 1.637, -6.696 ], [ 1.682, -7 ], [ 1.194, -4.923 ], [ 0.627, -2.636 ], [ 0.72, -2.134 ], [ 2.259, -4.752 ], [ 1.854, -2.234 ], [ 3.728, -3.237 ], [ 4.156, -2.244 ], [ 2.752, -0.768 ], [ 2.464, -0.317 ], [ 2.353, -0.169 ], [ 4.518, 0.388 ], [ 3.787, 1.248 ], [ 3.597, 2.031 ], [ 2.408, 2.031 ], [ 2.498, 2.793 ], [ 1.198, 2.003 ], [ 1.96, 4.409 ], [ 0.517, 2.7 ], [ 0.051, 0.414 ], [ 0.022, 1.028 ], [ 0.047, 0.178 ], [ -0.421, 1.913 ], [ -0.54, 5.337 ], [ -1.37, 5.82 ], [ -0.757, 3.149 ], [ -1.157, 4.775 ], [ -0.839, 3.48 ], [ -1.17, 4.851 ], [ -0.925, 3.814 ], [ -1.158, 4.774 ], [ -0.84, 3.481 ], [ -1.171, 4.85 ], [ -1.023, 4.22 ], [ -1.153, 4.776 ], [ -0.922, 3.814 ], [ -1.157, 4.775 ], [ -0.839, 3.481 ], [ -1.171, 4.85 ], [ -0.925, 3.814 ], [ -1.157, 4.774 ], [ -0.839, 3.481 ], [ -1.171, 4.85 ], [ -1.023, 4.221 ], [ -1.316, 5.443 ], [ -0.761, 3.149 ], [ -1.155, 4.775 ], [ -0.848, 3.519 ], [ -0.088, 0.362 ], [ 4.714, 1.964 ], [ 0, 0 ], [ 2.789, -11.518 ] ], "v": [ [ 41.848, 159.493 ], [ 51.007, 121.597 ], [ 60.422, 82.7 ], [ 70.799, 39.788 ], [ 80.73, -1.226 ], [ 89.593, -37.897 ], [ 97.511, -70.549 ], [ 106.671, -108.445 ], [ 109.181, -121.959 ], [ 109.398, -123.344 ], [ 109.608, -133.679 ], [ 109.506, -134.493 ], [ 109.527, -135.532 ], [ 109.389, -139.419 ], [ 102.719, -162.942 ], [ 85.432, -186.885 ], [ 68.712, -198.905 ], [ 54.295, -204.815 ], [ 38.875, -207.592 ], [ 37.61, -207.617 ], [ 36.536, -207.639 ], [ 35.524, -207.778 ], [ 31.62, -207.857 ], [ 30.597, -207.759 ], [ 29.688, -207.778 ], [ 28.388, -207.717 ], [ 23.015, -207.247 ], [ 9.016, -204.266 ], [ -13.915, -192.662 ], [ -32.81, -172.808 ], [ -43.335, -148.634 ], [ -50.671, -118.321 ], [ -60.899, -76.087 ], [ -69.566, -40.317 ], [ -77.412, -7.885 ], [ -86.575, 30.007 ], [ -94.456, 62.547 ], [ -104.849, 105.568 ], [ -109.608, 125.217 ], [ -96.579, 135.919 ], [ -94.528, 127.527 ], [ -90.61, 111.194 ], [ -88.079, 100.755 ], [ -84.086, 84.205 ], [ -81.344, 72.993 ], [ -77.814, 58.329 ], [ -75.307, 48.002 ], [ -71.8, 33.451 ], [ -69.15, 22.688 ], [ -63.87, 0.923 ], [ -59.596, -16.736 ], [ -55.865, -32.174 ], [ -50.561, -54.052 ], [ -46.353, -71.491 ], [ -42.557, -87.149 ], [ -37.73, -107.259 ], [ -32.672, -128.255 ], [ -29.062, -143.018 ], [ -26.866, -150.832 ], [ -24.296, -157.079 ], [ -15.811, -170.245 ], [ -9.777, -176.52 ], [ 2.298, -184.974 ], [ 15.334, -190.266 ], [ 23.723, -191.978 ], [ 31.122, -192.554 ], [ 38.081, -192.261 ], [ 51.362, -189.489 ], [ 62.278, -184.761 ], [ 72.429, -177.725 ], [ 79.119, -171.128 ], [ 85.675, -162.042 ], [ 88.932, -155.859 ], [ 93.294, -142.145 ], [ 94.423, -134.006 ], [ 94.428, -132.744 ], [ 94.559, -129.659 ], [ 94.561, -129.089 ], [ 94.076, -123.28 ], [ 90.817, -107.637 ], [ 86.65, -90.19 ], [ 84.338, -80.754 ], [ 80.89, -66.424 ], [ 78.36, -55.985 ], [ 74.855, -41.433 ], [ 72.053, -29.999 ], [ 68.605, -15.67 ], [ 66.076, -5.23 ], [ 62.57, 9.322 ], [ 59.479, 21.977 ], [ 56.045, 36.311 ], [ 53.242, 47.743 ], [ 49.797, 62.074 ], [ 47.267, 72.514 ], [ 43.761, 87.065 ], [ 40.957, 98.498 ], [ 37.512, 112.828 ], [ 34.982, 123.268 ], [ 31.476, 137.819 ], [ 28.384, 150.474 ], [ 24.464, 166.806 ], [ 22.146, 176.241 ], [ 18.703, 190.572 ], [ 16.139, 201.122 ], [ 15.878, 202.209 ], [ 30.131, 207.884 ], [ 33.483, 194.044 ] ], "c": true } }, "nm": "Path 1", "mn": "ADBE Vector Shape - Group" }, { "ty": "fl", "fillEnabled": true, "c": { "k": [ 1, 0.74, 0.75, 1 ] }, "o": { "k": 100 }, "nm": "Fill 1", "mn": "ADBE Vector Graphic - Fill" }, { "ty": "tr", "p": { "k": [ 109.858, 208.134 ], "ix": 2 }, "a": { "k": [ 0, 0 ], "ix": 1 }, "s": { "k": [ 100, 100 ], "ix": 3 }, "r": { "k": 0, "ix": 6 }, "o": { "k": 100, "ix": 7 }, "sk": { "k": 0, "ix": 4 }, "sa": { "k": 0, "ix": 5 }, "nm": "Transform" } ], "nm": "Group 2", "np": 2, "mn": "ADBE Vector Group" } ], "ip": 0, "op": 78, "st": 0, "bm": 0, "sr": 1 }, { "ddd": 0, "ind": 2, "ty": 4, "nm": "right_hand_mask", "td": 1, "ks": { "o": { "k": 100 }, "r": { "k": 0 }, "p": { "k": [ 500, 500, 0 ] }, "a": { "k": [ 476.25, 476.25, 0 ] }, "s": { "k": [ 100, 100, 100 ] } }, "ao": 0, "shapes": [ { "ty": "gr", "it": [ { "ind": 0, "ty": "sh", "ks": { "k": { "i": [ [ 0, 262.888 ], [ 262.888, 0 ], [ 0, -262.888 ], [ -262.888, 0 ] ], "o": [ [ 0, -262.888 ], [ -262.888, 0 ], [ 0, 262.888 ], [ 262.888, 0 ] ], "v": [ [ 476, 0 ], [ 0, -476 ], [ -476, 0 ], [ 0, 476 ] ], "c": true } }, "nm": "Path 1", "mn": "ADBE Vector Shape - Group" }, { "ty": "fl", "fillEnabled": true, "c": { "k": [ 0.62, 0.79, 0.81, 1 ] }, "o": { "k": 100 }, "nm": "Fill 1", "mn": "ADBE Vector Graphic - Fill" }, { "ty": "tr", "p": { "k": [ 476.25, 476.25 ], "ix": 2 }, "a": { "k": [ 0, 0 ], "ix": 1 }, "s": { "k": [ 100, 100 ], "ix": 3 }, "r": { "k": 0, "ix": 6 }, "o": { "k": 100, "ix": 7 }, "sk": { "k": 0, "ix": 4 }, "sa": { "k": 0, "ix": 5 }, "nm": "Transform" } ], "nm": "Group 1", "np": 2, "mn": "ADBE Vector Group" } ], "ip": 0, "op": 78, "st": 0, "bm": 0, "sr": 1 }, { "ddd": 0, "ind": 3, "ty": 4, "nm": "hand_right", "tt": 1, "ks": { "o": { "k": 100 }, "r": { "k": 0 }, "p": { "k": [ { "i": { "x": 0.69, "y": 1 }, "o": { "x": 1, "y": 0 }, "n": "0p69_1_1_0", "t": 29, "s": [ 819.292, 1149.194, 0 ], "e": [ 689.292, 745.194, 0 ], "to": [ -21.6666660308838, -67.3333358764648, 0 ], "ti": [ 21.6666660308838, 67.3333358764648, 0 ] }, { "i": { "x": 0.21, "y": 0.21 }, "o": { "x": 1, "y": 1 }, "n": "0p21_0p21_1_1", "t": 39, "s": [ 689.292, 745.194, 0 ], "e": [ 689.292, 745.194, 0 ], "to": [ 0, 0, 0 ], "ti": [ 0, 0, 0 ] }, { "i": { "x": 0.21, "y": 1 }, "o": { "x": 0.13, "y": 0 }, "n": "0p21_1_0p13_0", "t": 44, "s": [ 689.292, 745.194, 0 ], "e": [ 819.292, 1149.194, 0 ], "to": [ 21.6666660308838, 67.3333358764648, 0 ], "ti": [ 0, 0, 0 ] }, { "i": { "x": 0.515, "y": 1 }, "o": { "x": 0.13, "y": 0 }, "n": "0p515_1_0p13_0", "t": 54, "s": [ 819.292, 1149.194, 0 ], "e": [ 689.292, 745.194, 0 ], "to": [ 0, 0, 0 ], "ti": [ 13.3333330154419, 40.3333320617676, 0 ] }, { "i": { "x": 0.69, "y": 1 }, "o": { "x": 0.95, "y": 0 }, "n": "0p69_1_0p95_0", "t": 59, "s": [ 689.292, 745.194, 0 ], "e": [ 739.292, 907.194, 0 ], "to": [ -13.3333330154419, -40.3333320617676, 0 ], "ti": [ -21.6666660308838, -67.3333358764648, 0 ] }, { "i": { "x": 0.69, "y": 1 }, "o": { "x": 0.86, "y": 0 }, "n": "0p69_1_0p86_0", "t": 67.904, "s": [ 739.292, 907.194, 0 ], "e": [ 819.292, 1149.194, 0 ], "to": [ 21.6666660308838, 67.3333358764648, 0 ], "ti": [ -13.3333330154419, -40.3333320617676, 0 ] }, { "t": 76 } ] }, "a": { "k": [ 109.772, 207.96, 0 ] }, "s": { "k": [ 100, 100, 100 ] } }, "ao": 0, "shapes": [ { "ty": "gr", "it": [ { "ind": 0, "ty": "sh", "ks": { "k": { "i": [ [ 0.622, 2.577 ], [ 1.31, 5.443 ], [ 0.841, 3.48 ], [ 1.336, 5.515 ], [ 0.906, 3.739 ], [ 1.18, 4.887 ], [ 0.831, 3.443 ], [ 1.178, 4.847 ], [ 0.874, 3.59 ], [ 1.758, 7.255 ], [ 1.423, 5.887 ], [ 1.246, 5.145 ], [ 1.766, 7.293 ], [ 1.369, 5.822 ], [ 1.276, 5.217 ], [ 1.611, 6.703 ], [ 1.695, 6.996 ], [ 1.172, 4.929 ], [ 0.865, 2.564 ], [ 0.966, 2.035 ], [ 3.356, 4.041 ], [ 2.197, 1.907 ], [ 4.358, 2.353 ], [ 4.542, 1.269 ], [ 2.845, 0.367 ], [ 2.467, 0.177 ], [ 2.296, -0.196 ], [ 4.328, -1.426 ], [ 3.468, -1.959 ], [ 3.168, -2.671 ], [ 2.087, -2.334 ], [ 1.924, -3.218 ], [ 0.947, -2.131 ], [ 0.911, -4.753 ], [ 0.338, -2.718 ], [ 0.009, -0.421 ], [ 0.265, -1.02 ], [ -0.041, -0.182 ], [ -0.197, -1.935 ], [ -1.22, -5.185 ], [ -1.399, -5.813 ], [ -0.762, -3.148 ], [ -1.152, -4.777 ], [ -0.84, -3.48 ], [ -1.176, -4.849 ], [ -0.924, -3.814 ], [ -1.151, -4.776 ], [ -0.84, -3.48 ], [ -1.175, -4.85 ], [ -1.02, -4.221 ], [ -1.155, -4.775 ], [ -0.923, -3.813 ], [ -1.151, -4.776 ], [ -0.841, -3.48 ], [ -1.176, -4.849 ], [ -0.924, -3.814 ], [ -1.15, -4.776 ], [ -0.841, -3.48 ], [ -1.176, -4.849 ], [ -1.021, -4.22 ], [ -1.315, -5.442 ], [ -0.762, -3.148 ], [ -1.152, -4.776 ], [ -0.851, -3.518 ], [ 0, 0 ], [ -33.986, 27.051 ] ], "o": [ [ -1.315, -5.442 ], [ -0.838, -3.481 ], [ -1.332, -5.517 ], [ -0.907, -3.74 ], [ -1.183, -4.886 ], [ -0.831, -3.444 ], [ -1.172, -4.85 ], [ -0.872, -3.591 ], [ -1.766, -7.254 ], [ -1.426, -5.886 ], [ -1.245, -5.146 ], [ -1.767, -7.293 ], [ -1.408, -5.812 ], [ -1.229, -5.227 ], [ -1.637, -6.696 ], [ -1.681, -7 ], [ -1.195, -4.923 ], [ -0.627, -2.637 ], [ -0.721, -2.134 ], [ -2.259, -4.752 ], [ -1.853, -2.234 ], [ -3.728, -3.237 ], [ -4.156, -2.244 ], [ -2.752, -0.769 ], [ -2.464, -0.317 ], [ -2.353, -0.169 ], [ -4.518, 0.388 ], [ -3.788, 1.248 ], [ -3.596, 2.031 ], [ -2.407, 2.03 ], [ -2.498, 2.793 ], [ -1.197, 2.003 ], [ -1.96, 4.408 ], [ -0.518, 2.699 ], [ -0.052, 0.414 ], [ -0.022, 1.028 ], [ -0.047, 0.179 ], [ 0.42, 1.913 ], [ 0.54, 5.338 ], [ 1.37, 5.82 ], [ 0.757, 3.149 ], [ 1.156, 4.775 ], [ 0.839, 3.48 ], [ 1.17, 4.851 ], [ 0.925, 3.814 ], [ 1.158, 4.774 ], [ 0.84, 3.481 ], [ 1.17, 4.85 ], [ 1.023, 4.22 ], [ 1.154, 4.776 ], [ 0.922, 3.814 ], [ 1.158, 4.775 ], [ 0.839, 3.481 ], [ 1.17, 4.851 ], [ 0.926, 3.814 ], [ 1.157, 4.774 ], [ 0.84, 3.481 ], [ 1.17, 4.85 ], [ 1.024, 4.221 ], [ 1.315, 5.443 ], [ 0.76, 3.149 ], [ 1.155, 4.775 ], [ 0.848, 3.518 ], [ 0, 0 ], [ 40.632, -17.027 ], [ -0.632, -2.576 ] ], "v": [ [ 93.624, 122.973 ], [ 89.706, 106.64 ], [ 87.174, 96.201 ], [ 83.182, 79.651 ], [ 80.438, 68.439 ], [ 76.909, 53.775 ], [ 74.401, 43.448 ], [ 70.895, 28.897 ], [ 68.245, 18.134 ], [ 62.964, -3.631 ], [ 58.69, -21.29 ], [ 54.961, -36.728 ], [ 49.656, -58.606 ], [ 45.448, -76.046 ], [ 41.651, -91.703 ], [ 36.825, -111.813 ], [ 31.768, -132.809 ], [ 28.156, -147.572 ], [ 25.962, -155.386 ], [ 23.391, -161.633 ], [ 14.906, -174.799 ], [ 8.872, -181.074 ], [ -3.203, -189.528 ], [ -16.238, -194.82 ], [ -24.628, -196.532 ], [ -32.027, -197.108 ], [ -38.987, -196.815 ], [ -52.266, -194.044 ], [ -63.183, -189.315 ], [ -73.334, -182.279 ], [ -80.024, -175.682 ], [ -86.581, -166.596 ], [ -89.837, -160.413 ], [ -94.198, -146.699 ], [ -95.328, -138.56 ], [ -95.333, -137.298 ], [ -95.464, -134.214 ], [ -95.466, -133.643 ], [ -94.98, -127.835 ], [ -91.722, -112.191 ], [ -87.555, -94.744 ], [ -85.242, -85.308 ], [ -81.794, -70.978 ], [ -79.266, -60.539 ], [ -75.76, -45.987 ], [ -72.958, -34.553 ], [ -69.51, -20.224 ], [ -66.98, -9.784 ], [ -63.475, 4.768 ], [ -60.384, 17.423 ], [ -56.951, 31.757 ], [ -54.147, 43.189 ], [ -50.703, 57.52 ], [ -48.172, 67.959 ], [ -44.667, 82.511 ], [ -41.862, 93.944 ], [ -38.417, 108.274 ], [ -35.886, 118.714 ], [ -32.381, 133.265 ], [ -29.289, 145.92 ], [ -25.368, 162.252 ], [ -23.052, 171.687 ], [ -19.609, 186.018 ], [ -17.044, 196.568 ], [ -16.873, 197.277 ], [ 95.511, 130.699 ] ], "c": true } }, "nm": "Path 1", "mn": "ADBE Vector Shape - Group" }, { "ty": "fl", "fillEnabled": true, "c": { "k": [ 0.94, 0.94, 0.94, 1 ] }, "o": { "k": 100 }, "nm": "Fill 1", "mn": "ADBE Vector Graphic - Fill" }, { "ty": "tr", "p": { "k": [ 110.763, 212.689 ], "ix": 2 }, "a": { "k": [ 0, 0 ], "ix": 1 }, "s": { "k": [ 100, 100 ], "ix": 3 }, "r": { "k": 0, "ix": 6 }, "o": { "k": 100, "ix": 7 }, "sk": { "k": 0, "ix": 4 }, "sa": { "k": 0, "ix": 5 }, "nm": "Transform" } ], "nm": "Group 1", "np": 2, "mn": "ADBE Vector Group" }, { "ty": "gr", "it": [ { "ind": 0, "ty": "sh", "ks": { "k": { "i": [ [ 3.468, 14.34 ], [ 2.625, 10.848 ], [ 3.055, 12.63 ], [ 2.618, 10.81 ], [ 2.889, 11.924 ], [ 3.407, 14.078 ], [ 2.461, 10.1 ], [ 4.922, 7.438 ], [ 7.495, 5.479 ], [ 8.293, 2.582 ], [ 4.755, 0.569 ], [ 1.791, 0.154 ], [ 0.433, 0.03 ], [ 0.304, -0.006 ], [ 0.322, 0.223 ], [ 1.301, -0.027 ], [ 0.336, -0.054 ], [ 0.358, -0.008 ], [ 0.422, -0.008 ], [ 5.034, -1.477 ], [ 4.592, -2.488 ], [ 5.07, -4.733 ], [ 4.249, -9.054 ], [ 0.88, -8.221 ], [ -0.042, -1.299 ], [ -0.008, -0.346 ], [ 0.207, -0.25 ], [ -0.07, -3.445 ], [ -0.215, -0.439 ], [ -1.077, -4.46 ], [ -3.057, -12.631 ], [ -2.635, -10.885 ], [ -2.957, -12.223 ], [ -3.307, -13.672 ], [ -3.46, -14.303 ], [ -3.136, -12.966 ], [ -3.054, -12.632 ], [ -2.789, -11.518 ], [ -1.089, -4.498 ], [ -4.712, 1.975 ], [ 0, 0 ], [ 0.848, 3.519 ], [ 1.156, 4.776 ], [ 0.76, 3.148 ], [ 1.316, 5.442 ], [ 1.024, 4.221 ], [ 1.17, 4.849 ], [ 0.84, 3.482 ], [ 1.157, 4.775 ], [ 0.926, 3.813 ], [ 1.17, 4.85 ], [ 0.839, 3.482 ], [ 1.157, 4.776 ], [ 0.922, 3.813 ], [ 1.154, 4.775 ], [ 1.022, 4.22 ], [ 1.171, 4.849 ], [ 0.84, 3.482 ], [ 1.158, 4.775 ], [ 0.925, 3.813 ], [ 1.169, 4.85 ], [ 0.839, 3.481 ], [ 1.156, 4.776 ], [ 0.756, 3.148 ], [ 1.37, 5.821 ], [ 0.54, 5.338 ], [ 0.42, 1.913 ], [ -0.047, 0.179 ], [ -0.021, 1.028 ], [ -0.052, 0.414 ], [ -0.518, 2.699 ], [ -1.96, 4.408 ], [ -1.197, 2.003 ], [ -2.497, 2.793 ], [ -2.407, 2.03 ], [ -3.597, 2.032 ], [ -3.788, 1.248 ], [ -4.518, 0.388 ], [ -2.353, -0.169 ], [ -2.464, -0.318 ], [ -2.752, -0.769 ], [ -4.156, -2.244 ], [ -3.728, -3.238 ], [ -1.853, -2.233 ], [ -2.259, -4.752 ], [ -0.721, -2.134 ], [ -0.627, -2.637 ], [ -1.195, -4.923 ], [ -1.681, -7 ], [ -1.638, -6.697 ], [ -1.229, -5.228 ], [ -1.408, -5.811 ], [ -1.766, -7.293 ], [ -1.245, -5.147 ], [ -1.426, -5.886 ], [ -1.765, -7.254 ], [ -0.872, -3.591 ], [ -1.172, -4.85 ], [ -0.831, -3.443 ], [ -1.184, -4.885 ], [ -0.906, -3.739 ], [ -1.332, -5.517 ], [ -0.838, -3.481 ], [ -1.315, -5.442 ], [ -0.632, -2.575 ], [ -4.275, 3.657 ], [ 1.527, 6.314 ] ], "o": [ [ -2.623, -10.848 ], [ -3.056, -12.63 ], [ -2.614, -10.81 ], [ -2.886, -11.924 ], [ -3.41, -14.078 ], [ -2.446, -10.105 ], [ -2.11, -8.664 ], [ -5.118, -7.738 ], [ -7.008, -5.122 ], [ -4.575, -1.424 ], [ -1.784, -0.213 ], [ -0.427, -0.167 ], [ -0.302, 0.006 ], [ -0.34, -0.041 ], [ -1.302, 0.026 ], [ -0.312, 0.235 ], [ -0.359, 0.007 ], [ -0.422, 0.009 ], [ -5.276, 0.165 ], [ -5.005, 1.468 ], [ -6.083, 3.295 ], [ -7.344, 6.859 ], [ -3.501, 7.461 ], [ -0.137, 1.291 ], [ 0.007, 0.347 ], [ -0.031, 0.271 ], [ 0.07, 3.446 ], [ 0.291, 0.427 ], [ 0.322, 4.601 ], [ 3.051, 12.633 ], [ 2.635, 10.886 ], [ 2.959, 12.222 ], [ 3.308, 13.671 ], [ 3.461, 14.304 ], [ 3.137, 12.966 ], [ 3.056, 12.632 ], [ 2.786, 11.518 ], [ 1.088, 4.497 ], [ 4.787, -1.826 ], [ 0, 0 ], [ -0.85, -3.517 ], [ -1.152, -4.776 ], [ -0.761, -3.147 ], [ -1.314, -5.443 ], [ -1.02, -4.221 ], [ -1.175, -4.848 ], [ -0.84, -3.481 ], [ -1.15, -4.775 ], [ -0.924, -3.813 ], [ -1.176, -4.849 ], [ -0.84, -3.481 ], [ -1.15, -4.776 ], [ -0.924, -3.812 ], [ -1.154, -4.776 ], [ -1.02, -4.222 ], [ -1.176, -4.849 ], [ -0.84, -3.481 ], [ -1.151, -4.775 ], [ -0.924, -3.813 ], [ -1.175, -4.849 ], [ -0.841, -3.481 ], [ -1.152, -4.776 ], [ -0.762, -3.147 ], [ -1.4, -5.814 ], [ -1.221, -5.184 ], [ -0.197, -1.936 ], [ -0.041, -0.181 ], [ 0.265, -1.021 ], [ 0.009, -0.421 ], [ 0.338, -2.718 ], [ 0.911, -4.753 ], [ 0.948, -2.131 ], [ 1.924, -3.218 ], [ 2.088, -2.334 ], [ 3.168, -2.671 ], [ 3.467, -1.959 ], [ 4.328, -1.427 ], [ 2.296, -0.196 ], [ 2.468, 0.177 ], [ 2.845, 0.367 ], [ 4.542, 1.268 ], [ 4.358, 2.353 ], [ 2.198, 1.907 ], [ 3.356, 4.041 ], [ 0.967, 2.035 ], [ 0.865, 2.565 ], [ 1.172, 4.928 ], [ 1.695, 6.996 ], [ 1.611, 6.703 ], [ 1.275, 5.216 ], [ 1.37, 5.821 ], [ 1.766, 7.293 ], [ 1.246, 5.146 ], [ 1.422, 5.886 ], [ 1.758, 7.256 ], [ 0.874, 3.589 ], [ 1.177, 4.848 ], [ 0.831, 3.443 ], [ 1.18, 4.888 ], [ 0.905, 3.74 ], [ 1.336, 5.516 ], [ 0.841, 3.48 ], [ 1.31, 5.444 ], [ 0.622, 2.578 ], [ 4.404, -3.506 ], [ -1.53, -6.314 ], [ -3.468, -14.339 ] ], "v": [ [ 94.541, 62.721 ], [ 86.66, 30.181 ], [ 77.497, -7.711 ], [ 69.651, -40.142 ], [ 60.984, -75.913 ], [ 50.757, -118.147 ], [ 43.421, -148.46 ], [ 32.895, -172.633 ], [ 14, -192.488 ], [ -8.93, -204.091 ], [ -22.93, -207.073 ], [ -28.302, -207.543 ], [ -29.603, -207.603 ], [ -30.512, -207.585 ], [ -31.534, -207.682 ], [ -35.438, -207.603 ], [ -36.45, -207.465 ], [ -37.524, -207.443 ], [ -38.79, -207.418 ], [ -54.209, -204.64 ], [ -68.627, -198.731 ], [ -85.347, -186.711 ], [ -102.634, -162.767 ], [ -109.304, -139.245 ], [ -109.441, -135.358 ], [ -109.42, -134.319 ], [ -109.522, -133.505 ], [ -109.313, -123.17 ], [ -109.095, -121.785 ], [ -106.585, -108.271 ], [ -97.425, -70.375 ], [ -89.508, -37.722 ], [ -80.645, -1.051 ], [ -70.714, 39.962 ], [ -60.337, 82.874 ], [ -50.922, 121.771 ], [ -41.763, 159.667 ], [ -33.397, 194.218 ], [ -30.13, 207.71 ], [ -15.883, 202.005 ], [ -16.053, 201.296 ], [ -18.618, 190.746 ], [ -22.061, 176.415 ], [ -24.378, 166.981 ], [ -28.298, 150.649 ], [ -31.391, 137.993 ], [ -34.896, 123.443 ], [ -37.427, 113.002 ], [ -40.872, 98.672 ], [ -43.676, 87.24 ], [ -47.181, 72.688 ], [ -49.712, 62.248 ], [ -53.156, 47.917 ], [ -55.96, 36.486 ], [ -59.394, 22.152 ], [ -62.484, 9.496 ], [ -65.99, -5.055 ], [ -68.52, -15.496 ], [ -71.968, -29.825 ], [ -74.77, -41.258 ], [ -78.274, -55.81 ], [ -80.804, -66.25 ], [ -84.252, -80.58 ], [ -86.564, -90.015 ], [ -90.731, -107.463 ], [ -93.99, -123.106 ], [ -94.475, -128.915 ], [ -94.474, -129.485 ], [ -94.343, -132.57 ], [ -94.337, -133.832 ], [ -93.208, -141.97 ], [ -88.847, -155.684 ], [ -85.59, -161.868 ], [ -79.034, -170.954 ], [ -72.344, -177.55 ], [ -62.192, -184.587 ], [ -51.275, -189.315 ], [ -37.996, -192.087 ], [ -31.037, -192.38 ], [ -23.638, -191.803 ], [ -15.248, -190.091 ], [ -2.213, -184.8 ], [ 9.863, -176.345 ], [ 15.896, -170.071 ], [ 24.382, -156.905 ], [ 26.952, -150.658 ], [ 29.147, -142.843 ], [ 32.758, -128.081 ], [ 37.815, -107.085 ], [ 42.643, -86.974 ], [ 46.438, -71.317 ], [ 50.647, -53.878 ], [ 55.951, -32 ], [ 59.682, -16.561 ], [ 63.955, 1.097 ], [ 69.235, 22.863 ], [ 71.886, 33.625 ], [ 75.393, 48.176 ], [ 77.899, 58.503 ], [ 81.43, 73.167 ], [ 84.172, 84.379 ], [ 88.164, 100.929 ], [ 90.696, 111.368 ], [ 94.614, 127.701 ], [ 96.502, 135.427 ], [ 109.522, 124.682 ], [ 104.935, 105.742 ] ], "c": true } }, "nm": "Path 1", "mn": "ADBE Vector Shape - Group" }, { "ty": "fl", "fillEnabled": true, "c": { "k": [ 1, 0.74, 0.75, 1 ] }, "o": { "k": 100 }, "nm": "Fill 1", "mn": "ADBE Vector Graphic - Fill" }, { "ty": "tr", "p": { "k": [ 109.772, 207.96 ], "ix": 2 }, "a": { "k": [ 0, 0 ], "ix": 1 }, "s": { "k": [ 100, 100 ], "ix": 3 }, "r": { "k": 0, "ix": 6 }, "o": { "k": 100, "ix": 7 }, "sk": { "k": 0, "ix": 4 }, "sa": { "k": 0, "ix": 5 }, "nm": "Transform" } ], "nm": "Group 2", "np": 2, "mn": "ADBE Vector Group" } ], "ip": 0, "op": 78, "st": 0, "bm": 0, "sr": 1 }, { "ddd": 0, "ind": 4, "ty": 4, "nm": "mouth_smile", "ks": { "o": { "k": 100 }, "r": { "k": 0 }, "p": { "k": [ 500.75, 815.75, 0 ] }, "a": { "k": [ 0.5, -0.61, 0 ] }, "s": { "k": [ 100, 100, 100 ] } }, "ao": 0, "shapes": [ { "ty": "gr", "it": [ { "ind": 0, "ty": "sh", "ks": { "k": [ { "i": { "x": 0, "y": 1 }, "o": { "x": 1, "y": 0 }, "n": "0_1_1_0", "t": 32, "s": [ { "i": [ [ -0.081, 0.416 ], [ 0, 0.614 ], [ 0.017, 0.114 ], [ 4.432, 1.114 ], [ 0.455, 0.099 ], [ 0.63, 0 ], [ 0.118, -0.012 ], [ 1.098, -0.552 ], [ 5.005, -1.792 ], [ 17.596, -1.761 ], [ 5.449, -0.216 ], [ 10.022, 0.979 ], [ 5.427, 0.928 ], [ 9.789, 3.142 ], [ 6.284, 3.051 ], [ 0.79, 0.255 ], [ 0.915, 0.206 ], [ 0.63, 0 ], [ 0.098, -0.016 ], [ 1.538, -3.764 ], [ 0.246, -0.951 ], [ 0, -0.58 ], [ -0.022, -0.149 ], [ -2.832, -1.687 ], [ -1.661, -0.753 ], [ -10.509, -2.813 ], [ -8.427, -1.306 ], [ -5.769, -0.425 ], [ -5.447, -0.289 ], [ -0.241, -0.027 ], [ -3.43, 0 ], [ -0.312, 0.015 ], [ -4.293, 0.255 ], [ -5.555, 0.778 ], [ -6.003, 1.316 ], [ -9.441, 3.427 ], [ -5.316, 2.752 ], [ -0.737, 3.161 ] ], "o": [ [ 0, -0.614 ], [ -0.035, -0.111 ], [ -0.645, -4.395 ], [ -0.451, -0.113 ], [ -0.63, 0 ], [ -0.116, 0.032 ], [ -1.23, 0.122 ], [ -4.743, 2.385 ], [ -16.622, 5.95 ], [ -5.424, 0.543 ], [ -10.064, 0.399 ], [ -5.48, -0.536 ], [ -10.152, -1.736 ], [ -6.661, -2.138 ], [ -0.75, -0.364 ], [ -0.888, -0.287 ], [ -0.63, 0 ], [ -0.095, 0.034 ], [ -4.089, 0.654 ], [ -0.369, 0.903 ], [ 0, 0.58 ], [ 0.038, 0.147 ], [ 0.472, 3.212 ], [ 1.56, 0.929 ], [ 9.893, 4.481 ], [ 8.226, 2.203 ], [ 5.716, 0.886 ], [ 5.438, 0.4 ], [ 0.242, 0.013 ], [ 3.43, 0 ], [ 0.311, -0.029 ], [ 4.296, -0.209 ], [ 5.604, -0.333 ], [ 6.089, -0.852 ], [ 9.827, -2.154 ], [ 5.635, -2.045 ], [ 2.928, -1.516 ], [ 0.096, -0.413 ] ], "v": [ [ -0.25, -37.45 ], [ -0.25, -39.292 ], [ -0.352, -39.627 ], [ -8.441, -48.41 ], [ -9.805, -48.71 ], [ -11.695, -48.71 ], [ -12.043, -48.617 ], [ -15.549, -47.635 ], [ -30.201, -41.435 ], [ -81.575, -30.008 ], [ -97.904, -28.937 ], [ -128.04, -29.897 ], [ -144.399, -32.098 ], [ -174.316, -39.388 ], [ -193.782, -47.058 ], [ -196.066, -48.073 ], [ -198.805, -48.71 ], [ -200.695, -48.71 ], [ -200.982, -48.612 ], [ -209.494, -42.035 ], [ -210.25, -39.19 ], [ -210.25, -37.45 ], [ -210.139, -37.008 ], [ -205.218, -29.585 ], [ -200.329, -27.133 ], [ -169.658, -16.368 ], [ -144.675, -11.119 ], [ -127.458, -9.034 ], [ -111.119, -8.149 ], [ -110.395, -8.07 ], [ -100.105, -8.07 ], [ -99.172, -8.154 ], [ -86.283, -8.786 ], [ -69.554, -10.561 ], [ -51.415, -13.81 ], [ -22.49, -22.107 ], [ -5.996, -29.157 ], [ -0.494, -36.201 ] ], "c": true } ], "e": [ { "i": [ [ -0.044, 0.566 ], [ 0, 0.797 ], [ 0.009, 0.092 ], [ 0.473, 1.052 ], [ 2.267, 1.723 ], [ 3.546, 1.383 ], [ 4.958, 1.036 ], [ 5.038, 0.506 ], [ 3.295, 0.201 ], [ 3.27, 0.122 ], [ 0.157, 0.015 ], [ 2.008, 0 ], [ 0.123, -0.004 ], [ 2.525, -0.137 ], [ 3.032, -0.3 ], [ 3.359, -0.467 ], [ 3.986, -0.994 ], [ 4.243, -1.98 ], [ 2.404, -2.298 ], [ 0.547, -2.511 ], [ 0.082, -0.477 ], [ 0, -0.797 ], [ -0.009, -0.122 ], [ -0.09, -0.954 ], [ -0.77, -2.696 ], [ -4.189, -5.385 ], [ -7.486, -4.304 ], [ -9.579, -1.491 ], [ -2.987, -0.129 ], [ -0.602, -0.035 ], [ -1.278, 0 ], [ -0.169, 0.005 ], [ -3.481, 0.546 ], [ -4.049, 1.347 ], [ -6.01, 4.45 ], [ -3.397, 4.433 ], [ -1.64, 6.947 ], [ -0.167, 2.53 ] ], "o": [ [ 0, -0.797 ], [ -0.023, -0.091 ], [ -0.106, -1.121 ], [ -1.087, -2.417 ], [ -2.906, -2.208 ], [ -4.636, -1.808 ], [ -4.922, -1.029 ], [ -3.281, -0.33 ], [ -3.263, -0.199 ], [ -0.158, -0.006 ], [ -2.008, 0 ], [ -0.123, 0.014 ], [ -2.53, 0.082 ], [ -3.048, 0.165 ], [ -3.381, 0.335 ], [ -4.103, 0.571 ], [ -4.648, 1.159 ], [ -3.171, 1.48 ], [ -1.997, 1.909 ], [ -0.103, 0.473 ], [ 0, 0.797 ], [ 0.02, 0.121 ], [ 0.072, 0.955 ], [ 0.261, 2.763 ], [ 1.783, 6.242 ], [ 4.941, 6.352 ], [ 8.015, 4.608 ], [ 2.94, 0.458 ], [ 0.602, 0.026 ], [ 1.278, 0 ], [ 0.169, -0.017 ], [ 3.538, -0.113 ], [ 4.291, -0.673 ], [ 7.471, -2.485 ], [ 4.723, -3.497 ], [ 4.574, -5.97 ], [ 0.587, -2.486 ], [ 0.037, -0.566 ] ], "v": [ [ -36.789, -43.354 ], [ -36.789, -45.744 ], [ -36.856, -46.019 ], [ -37.73, -49.279 ], [ -42.925, -55.398 ], [ -52.719, -60.643 ], [ -67.157, -64.804 ], [ -82.094, -67.138 ], [ -91.959, -67.967 ], [ -101.766, -68.346 ], [ -102.238, -68.39 ], [ -108.262, -68.39 ], [ -108.631, -68.349 ], [ -116.22, -68.095 ], [ -125.351, -67.445 ], [ -135.461, -66.212 ], [ -147.59, -63.828 ], [ -161, -59.264 ], [ -169.508, -53.749 ], [ -173.463, -47.173 ], [ -173.711, -45.744 ], [ -173.711, -43.354 ], [ -173.652, -42.991 ], [ -173.44, -40.125 ], [ -171.868, -31.94 ], [ -162.909, -14.495 ], [ -144.266, 1.498 ], [ -117.864, 10.635 ], [ -108.971, 11.506 ], [ -107.167, 11.61 ], [ -103.333, 11.61 ], [ -102.827, 11.56 ], [ -92.3, 10.584 ], [ -79.789, 7.549 ], [ -59.57, -2.862 ], [ -47.388, -14.757 ], [ -38.06, -34.133 ], [ -36.92, -41.656 ] ], "c": true } ] }, { "i": { "x": 0, "y": 1 }, "o": { "x": 0.333, "y": 0 }, "n": "0_1_0p333_0", "t": 37, "s": [ { "i": [ [ -0.044, 0.566 ], [ 0, 0.797 ], [ 0.009, 0.092 ], [ 0.473, 1.052 ], [ 2.267, 1.723 ], [ 3.546, 1.383 ], [ 4.958, 1.036 ], [ 5.038, 0.506 ], [ 3.295, 0.201 ], [ 3.27, 0.122 ], [ 0.157, 0.015 ], [ 2.008, 0 ], [ 0.123, -0.004 ], [ 2.525, -0.137 ], [ 3.032, -0.3 ], [ 3.359, -0.467 ], [ 3.986, -0.994 ], [ 4.243, -1.98 ], [ 2.404, -2.298 ], [ 0.547, -2.511 ], [ 0.082, -0.477 ], [ 0, -0.797 ], [ -0.009, -0.122 ], [ -0.09, -0.954 ], [ -0.77, -2.696 ], [ -4.189, -5.385 ], [ -7.486, -4.304 ], [ -9.579, -1.491 ], [ -2.987, -0.129 ], [ -0.602, -0.035 ], [ -1.278, 0 ], [ -0.169, 0.005 ], [ -3.481, 0.546 ], [ -4.049, 1.347 ], [ -6.01, 4.45 ], [ -3.397, 4.433 ], [ -1.64, 6.947 ], [ -0.167, 2.53 ] ], "o": [ [ 0, -0.797 ], [ -0.023, -0.091 ], [ -0.106, -1.121 ], [ -1.087, -2.417 ], [ -2.906, -2.208 ], [ -4.636, -1.808 ], [ -4.922, -1.029 ], [ -3.281, -0.33 ], [ -3.263, -0.199 ], [ -0.158, -0.006 ], [ -2.008, 0 ], [ -0.123, 0.014 ], [ -2.53, 0.082 ], [ -3.048, 0.165 ], [ -3.381, 0.335 ], [ -4.103, 0.571 ], [ -4.648, 1.159 ], [ -3.171, 1.48 ], [ -1.997, 1.909 ], [ -0.103, 0.473 ], [ 0, 0.797 ], [ 0.02, 0.121 ], [ 0.072, 0.955 ], [ 0.261, 2.763 ], [ 1.783, 6.242 ], [ 4.941, 6.352 ], [ 8.015, 4.608 ], [ 2.94, 0.458 ], [ 0.602, 0.026 ], [ 1.278, 0 ], [ 0.169, -0.017 ], [ 3.538, -0.113 ], [ 4.291, -0.673 ], [ 7.471, -2.485 ], [ 4.723, -3.497 ], [ 4.574, -5.97 ], [ 0.587, -2.486 ], [ 0.037, -0.566 ] ], "v": [ [ -36.789, -43.354 ], [ -36.789, -45.744 ], [ -36.856, -46.019 ], [ -37.73, -49.279 ], [ -42.925, -55.398 ], [ -52.719, -60.643 ], [ -67.157, -64.804 ], [ -82.094, -67.138 ], [ -91.959, -67.967 ], [ -101.766, -68.346 ], [ -102.238, -68.39 ], [ -108.262, -68.39 ], [ -108.631, -68.349 ], [ -116.22, -68.095 ], [ -125.351, -67.445 ], [ -135.461, -66.212 ], [ -147.59, -63.828 ], [ -161, -59.264 ], [ -169.508, -53.749 ], [ -173.463, -47.173 ], [ -173.711, -45.744 ], [ -173.711, -43.354 ], [ -173.652, -42.991 ], [ -173.44, -40.125 ], [ -171.868, -31.94 ], [ -162.909, -14.495 ], [ -144.266, 1.498 ], [ -117.864, 10.635 ], [ -108.971, 11.506 ], [ -107.167, 11.61 ], [ -103.333, 11.61 ], [ -102.827, 11.56 ], [ -92.3, 10.584 ], [ -79.789, 7.549 ], [ -59.57, -2.862 ], [ -47.388, -14.757 ], [ -38.06, -34.133 ], [ -36.92, -41.656 ] ], "c": true } ], "e": [ { "i": [ [ -0.044, 0.566 ], [ 0, 0.797 ], [ 0.009, 0.092 ], [ 0.473, 1.052 ], [ 2.267, 1.723 ], [ 3.546, 1.383 ], [ 4.958, 1.036 ], [ 5.038, 0.506 ], [ 3.295, 0.201 ], [ 3.27, 0.122 ], [ 0.157, 0.015 ], [ 2.008, 0 ], [ 0.123, -0.004 ], [ 2.525, -0.137 ], [ 3.032, -0.3 ], [ 3.359, -0.467 ], [ 3.986, -0.994 ], [ 4.243, -1.98 ], [ 2.404, -2.298 ], [ 0.547, -2.511 ], [ 0.082, -0.477 ], [ 0, -0.797 ], [ -0.009, -0.122 ], [ -0.09, -0.954 ], [ -0.77, -2.696 ], [ -4.189, -5.385 ], [ -7.486, -4.304 ], [ -9.579, -1.491 ], [ -2.987, -0.129 ], [ -0.602, -0.035 ], [ -1.278, 0 ], [ -0.169, 0.005 ], [ -3.481, 0.546 ], [ -4.049, 1.347 ], [ -6.01, 4.45 ], [ -3.397, 4.433 ], [ -1.64, 6.947 ], [ -0.167, 2.53 ] ], "o": [ [ 0, -0.797 ], [ -0.023, -0.091 ], [ -0.106, -1.121 ], [ -1.087, -2.417 ], [ -2.906, -2.208 ], [ -4.636, -1.808 ], [ -4.922, -1.029 ], [ -3.281, -0.33 ], [ -3.263, -0.199 ], [ -0.158, -0.006 ], [ -2.008, 0 ], [ -0.123, 0.014 ], [ -2.53, 0.082 ], [ -3.048, 0.165 ], [ -3.381, 0.335 ], [ -4.103, 0.571 ], [ -4.648, 1.159 ], [ -3.171, 1.48 ], [ -1.997, 1.909 ], [ -0.103, 0.473 ], [ 0, 0.797 ], [ 0.02, 0.121 ], [ 0.072, 0.955 ], [ 0.261, 2.763 ], [ 1.783, 6.242 ], [ 4.941, 6.352 ], [ 8.015, 4.608 ], [ 2.94, 0.458 ], [ 0.602, 0.026 ], [ 1.278, 0 ], [ 0.169, -0.017 ], [ 3.538, -0.113 ], [ 4.291, -0.673 ], [ 7.471, -2.485 ], [ 4.723, -3.497 ], [ 4.574, -5.97 ], [ 0.587, -2.486 ], [ 0.037, -0.566 ] ], "v": [ [ -36.789, -43.354 ], [ -36.789, -45.744 ], [ -36.856, -46.019 ], [ -37.73, -49.279 ], [ -42.925, -55.398 ], [ -52.719, -60.643 ], [ -67.157, -64.804 ], [ -82.094, -67.138 ], [ -91.959, -67.967 ], [ -101.766, -68.346 ], [ -102.238, -68.39 ], [ -108.262, -68.39 ], [ -108.631, -68.349 ], [ -116.22, -68.095 ], [ -125.351, -67.445 ], [ -135.461, -66.212 ], [ -147.59, -63.828 ], [ -161, -59.264 ], [ -169.508, -53.749 ], [ -173.463, -47.173 ], [ -173.711, -45.744 ], [ -173.711, -43.354 ], [ -173.652, -42.991 ], [ -173.44, -40.125 ], [ -171.868, -31.94 ], [ -162.909, -14.495 ], [ -144.266, 1.498 ], [ -117.864, 10.635 ], [ -108.971, 11.506 ], [ -107.167, 11.61 ], [ -103.333, 11.61 ], [ -102.827, 11.56 ], [ -92.3, 10.584 ], [ -79.789, 7.549 ], [ -59.57, -2.862 ], [ -47.388, -14.757 ], [ -38.06, -34.133 ], [ -36.92, -41.656 ] ], "c": true } ] }, { "i": { "x": 0.667, "y": 1 }, "o": { "x": 0.333, "y": 0 }, "n": "0p667_1_0p333_0", "t": 44, "s": [ { "i": [ [ -0.044, 0.566 ], [ 0, 0.797 ], [ 0.009, 0.092 ], [ 0.473, 1.052 ], [ 2.267, 1.723 ], [ 3.546, 1.383 ], [ 4.958, 1.036 ], [ 5.038, 0.506 ], [ 3.295, 0.201 ], [ 3.27, 0.122 ], [ 0.157, 0.015 ], [ 2.008, 0 ], [ 0.123, -0.004 ], [ 2.525, -0.137 ], [ 3.032, -0.3 ], [ 3.359, -0.467 ], [ 3.986, -0.994 ], [ 4.243, -1.98 ], [ 2.404, -2.298 ], [ 0.547, -2.511 ], [ 0.082, -0.477 ], [ 0, -0.797 ], [ -0.009, -0.122 ], [ -0.09, -0.954 ], [ -0.77, -2.696 ], [ -4.189, -5.385 ], [ -7.486, -4.304 ], [ -9.579, -1.491 ], [ -2.987, -0.129 ], [ -0.602, -0.035 ], [ -1.278, 0 ], [ -0.169, 0.005 ], [ -3.481, 0.546 ], [ -4.049, 1.347 ], [ -6.01, 4.45 ], [ -3.397, 4.433 ], [ -1.64, 6.947 ], [ -0.167, 2.53 ] ], "o": [ [ 0, -0.797 ], [ -0.023, -0.091 ], [ -0.106, -1.121 ], [ -1.087, -2.417 ], [ -2.906, -2.208 ], [ -4.636, -1.808 ], [ -4.922, -1.029 ], [ -3.281, -0.33 ], [ -3.263, -0.199 ], [ -0.158, -0.006 ], [ -2.008, 0 ], [ -0.123, 0.014 ], [ -2.53, 0.082 ], [ -3.048, 0.165 ], [ -3.381, 0.335 ], [ -4.103, 0.571 ], [ -4.648, 1.159 ], [ -3.171, 1.48 ], [ -1.997, 1.909 ], [ -0.103, 0.473 ], [ 0, 0.797 ], [ 0.02, 0.121 ], [ 0.072, 0.955 ], [ 0.261, 2.763 ], [ 1.783, 6.242 ], [ 4.941, 6.352 ], [ 8.015, 4.608 ], [ 2.94, 0.458 ], [ 0.602, 0.026 ], [ 1.278, 0 ], [ 0.169, -0.017 ], [ 3.538, -0.113 ], [ 4.291, -0.673 ], [ 7.471, -2.485 ], [ 4.723, -3.497 ], [ 4.574, -5.97 ], [ 0.587, -2.486 ], [ 0.037, -0.566 ] ], "v": [ [ -36.789, -43.354 ], [ -36.789, -45.744 ], [ -36.856, -46.019 ], [ -37.73, -49.279 ], [ -42.925, -55.398 ], [ -52.719, -60.643 ], [ -67.157, -64.804 ], [ -82.094, -67.138 ], [ -91.959, -67.967 ], [ -101.766, -68.346 ], [ -102.238, -68.39 ], [ -108.262, -68.39 ], [ -108.631, -68.349 ], [ -116.22, -68.095 ], [ -125.351, -67.445 ], [ -135.461, -66.212 ], [ -147.59, -63.828 ], [ -161, -59.264 ], [ -169.508, -53.749 ], [ -173.463, -47.173 ], [ -173.711, -45.744 ], [ -173.711, -43.354 ], [ -173.652, -42.991 ], [ -173.44, -40.125 ], [ -171.868, -31.94 ], [ -162.909, -14.495 ], [ -144.266, 1.498 ], [ -117.864, 10.635 ], [ -108.971, 11.506 ], [ -107.167, 11.61 ], [ -103.333, 11.61 ], [ -102.827, 11.56 ], [ -92.3, 10.584 ], [ -79.789, 7.549 ], [ -59.57, -2.862 ], [ -47.388, -14.757 ], [ -38.06, -34.133 ], [ -36.92, -41.656 ] ], "c": true } ], "e": [ { "i": [ [ -0.081, 0.416 ], [ 0, 0.614 ], [ 0.017, 0.114 ], [ 4.432, 1.114 ], [ 0.455, 0.099 ], [ 0.63, 0 ], [ 0.118, -0.012 ], [ 1.098, -0.552 ], [ 5.005, -1.792 ], [ 17.596, -1.761 ], [ 5.449, -0.216 ], [ 10.022, 0.979 ], [ 5.427, 0.928 ], [ 9.789, 3.142 ], [ 6.284, 3.051 ], [ 0.79, 0.255 ], [ 0.915, 0.206 ], [ 0.63, 0 ], [ 0.098, -0.016 ], [ 1.538, -3.764 ], [ 0.246, -0.951 ], [ 0, -0.58 ], [ -0.022, -0.149 ], [ -2.832, -1.687 ], [ -1.661, -0.753 ], [ -10.509, -2.813 ], [ -8.427, -1.306 ], [ -5.769, -0.425 ], [ -5.447, -0.289 ], [ -0.241, -0.027 ], [ -3.43, 0 ], [ -0.312, 0.015 ], [ -4.293, 0.255 ], [ -5.555, 0.778 ], [ -6.003, 1.316 ], [ -9.441, 3.427 ], [ -5.316, 2.752 ], [ -0.737, 3.161 ] ], "o": [ [ 0, -0.614 ], [ -0.035, -0.111 ], [ -0.645, -4.395 ], [ -0.451, -0.113 ], [ -0.63, 0 ], [ -0.116, 0.032 ], [ -1.23, 0.122 ], [ -4.743, 2.385 ], [ -16.622, 5.95 ], [ -5.424, 0.543 ], [ -10.064, 0.399 ], [ -5.48, -0.536 ], [ -10.152, -1.736 ], [ -6.661, -2.138 ], [ -0.75, -0.364 ], [ -0.888, -0.287 ], [ -0.63, 0 ], [ -0.095, 0.034 ], [ -4.089, 0.654 ], [ -0.369, 0.903 ], [ 0, 0.58 ], [ 0.038, 0.147 ], [ 0.472, 3.212 ], [ 1.56, 0.929 ], [ 9.893, 4.481 ], [ 8.226, 2.203 ], [ 5.716, 0.886 ], [ 5.438, 0.4 ], [ 0.242, 0.013 ], [ 3.43, 0 ], [ 0.311, -0.029 ], [ 4.296, -0.209 ], [ 5.604, -0.333 ], [ 6.089, -0.852 ], [ 9.827, -2.154 ], [ 5.635, -2.045 ], [ 2.928, -1.516 ], [ 0.096, -0.413 ] ], "v": [ [ -0.25, -37.45 ], [ -0.25, -39.292 ], [ -0.352, -39.627 ], [ -8.441, -48.41 ], [ -9.805, -48.71 ], [ -11.695, -48.71 ], [ -12.043, -48.617 ], [ -15.549, -47.635 ], [ -30.201, -41.435 ], [ -81.575, -30.008 ], [ -97.904, -28.937 ], [ -128.04, -29.897 ], [ -144.399, -32.098 ], [ -174.316, -39.388 ], [ -193.782, -47.058 ], [ -196.066, -48.073 ], [ -198.805, -48.71 ], [ -200.695, -48.71 ], [ -200.982, -48.612 ], [ -209.494, -42.035 ], [ -210.25, -39.19 ], [ -210.25, -37.45 ], [ -210.139, -37.008 ], [ -205.218, -29.585 ], [ -200.329, -27.133 ], [ -169.658, -16.368 ], [ -144.675, -11.119 ], [ -127.458, -9.034 ], [ -111.119, -8.149 ], [ -110.395, -8.07 ], [ -100.105, -8.07 ], [ -99.172, -8.154 ], [ -86.283, -8.786 ], [ -69.554, -10.561 ], [ -51.415, -13.81 ], [ -22.49, -22.107 ], [ -5.996, -29.157 ], [ -0.494, -36.201 ] ], "c": true } ] }, { "i": { "x": 0.667, "y": 1 }, "o": { "x": 1, "y": 0 }, "n": "0p667_1_1_0", "t": 48, "s": [ { "i": [ [ -0.081, 0.416 ], [ 0, 0.614 ], [ 0.017, 0.114 ], [ 4.432, 1.114 ], [ 0.455, 0.099 ], [ 0.63, 0 ], [ 0.118, -0.012 ], [ 1.098, -0.552 ], [ 5.005, -1.792 ], [ 17.596, -1.761 ], [ 5.449, -0.216 ], [ 10.022, 0.979 ], [ 5.427, 0.928 ], [ 9.789, 3.142 ], [ 6.284, 3.051 ], [ 0.79, 0.255 ], [ 0.915, 0.206 ], [ 0.63, 0 ], [ 0.098, -0.016 ], [ 1.538, -3.764 ], [ 0.246, -0.951 ], [ 0, -0.58 ], [ -0.022, -0.149 ], [ -2.832, -1.687 ], [ -1.661, -0.753 ], [ -10.509, -2.813 ], [ -8.427, -1.306 ], [ -5.769, -0.425 ], [ -5.447, -0.289 ], [ -0.241, -0.027 ], [ -3.43, 0 ], [ -0.312, 0.015 ], [ -4.293, 0.255 ], [ -5.555, 0.778 ], [ -6.003, 1.316 ], [ -9.441, 3.427 ], [ -5.316, 2.752 ], [ -0.737, 3.161 ] ], "o": [ [ 0, -0.614 ], [ -0.035, -0.111 ], [ -0.645, -4.395 ], [ -0.451, -0.113 ], [ -0.63, 0 ], [ -0.116, 0.032 ], [ -1.23, 0.122 ], [ -4.743, 2.385 ], [ -16.622, 5.95 ], [ -5.424, 0.543 ], [ -10.064, 0.399 ], [ -5.48, -0.536 ], [ -10.152, -1.736 ], [ -6.661, -2.138 ], [ -0.75, -0.364 ], [ -0.888, -0.287 ], [ -0.63, 0 ], [ -0.095, 0.034 ], [ -4.089, 0.654 ], [ -0.369, 0.903 ], [ 0, 0.58 ], [ 0.038, 0.147 ], [ 0.472, 3.212 ], [ 1.56, 0.929 ], [ 9.893, 4.481 ], [ 8.226, 2.203 ], [ 5.716, 0.886 ], [ 5.438, 0.4 ], [ 0.242, 0.013 ], [ 3.43, 0 ], [ 0.311, -0.029 ], [ 4.296, -0.209 ], [ 5.604, -0.333 ], [ 6.089, -0.852 ], [ 9.827, -2.154 ], [ 5.635, -2.045 ], [ 2.928, -1.516 ], [ 0.096, -0.413 ] ], "v": [ [ -0.25, -37.45 ], [ -0.25, -39.292 ], [ -0.352, -39.627 ], [ -8.441, -48.41 ], [ -9.805, -48.71 ], [ -11.695, -48.71 ], [ -12.043, -48.617 ], [ -15.549, -47.635 ], [ -30.201, -41.435 ], [ -81.575, -30.008 ], [ -97.904, -28.937 ], [ -128.04, -29.897 ], [ -144.399, -32.098 ], [ -174.316, -39.388 ], [ -193.782, -47.058 ], [ -196.066, -48.073 ], [ -198.805, -48.71 ], [ -200.695, -48.71 ], [ -200.982, -48.612 ], [ -209.494, -42.035 ], [ -210.25, -39.19 ], [ -210.25, -37.45 ], [ -210.139, -37.008 ], [ -205.218, -29.585 ], [ -200.329, -27.133 ], [ -169.658, -16.368 ], [ -144.675, -11.119 ], [ -127.458, -9.034 ], [ -111.119, -8.149 ], [ -110.395, -8.07 ], [ -100.105, -8.07 ], [ -99.172, -8.154 ], [ -86.283, -8.786 ], [ -69.554, -10.561 ], [ -51.415, -13.81 ], [ -22.49, -22.107 ], [ -5.996, -29.157 ], [ -0.494, -36.201 ] ], "c": true } ], "e": [ { "i": [ [ -0.081, 0.416 ], [ 0, 0.614 ], [ 0.017, 0.114 ], [ 4.432, 1.114 ], [ 0.455, 0.099 ], [ 0.63, 0 ], [ 0.118, -0.012 ], [ 1.098, -0.552 ], [ 5.005, -1.792 ], [ 17.596, -1.761 ], [ 5.449, -0.216 ], [ 10.022, 0.979 ], [ 5.427, 0.928 ], [ 9.789, 3.142 ], [ 6.284, 3.051 ], [ 0.79, 0.255 ], [ 0.915, 0.206 ], [ 0.63, 0 ], [ 0.098, -0.016 ], [ 1.538, -3.764 ], [ 0.246, -0.951 ], [ 0, -0.58 ], [ -0.022, -0.149 ], [ -2.832, -1.687 ], [ -1.661, -0.753 ], [ -10.509, -2.813 ], [ -8.427, -1.306 ], [ -5.769, -0.425 ], [ -5.447, -0.289 ], [ -0.241, -0.027 ], [ -3.43, 0 ], [ -0.312, 0.015 ], [ -4.293, 0.255 ], [ -5.555, 0.778 ], [ -6.003, 1.316 ], [ -9.441, 3.427 ], [ -5.316, 2.752 ], [ -0.737, 3.161 ] ], "o": [ [ 0, -0.614 ], [ -0.035, -0.111 ], [ -0.645, -4.395 ], [ -0.451, -0.113 ], [ -0.63, 0 ], [ -0.116, 0.032 ], [ -1.23, 0.122 ], [ -4.743, 2.385 ], [ -16.622, 5.95 ], [ -5.424, 0.543 ], [ -10.064, 0.399 ], [ -5.48, -0.536 ], [ -10.152, -1.736 ], [ -6.661, -2.138 ], [ -0.75, -0.364 ], [ -0.888, -0.287 ], [ -0.63, 0 ], [ -0.095, 0.034 ], [ -4.089, 0.654 ], [ -0.369, 0.903 ], [ 0, 0.58 ], [ 0.038, 0.147 ], [ 0.472, 3.212 ], [ 1.56, 0.929 ], [ 9.893, 4.481 ], [ 8.226, 2.203 ], [ 5.716, 0.886 ], [ 5.438, 0.4 ], [ 0.242, 0.013 ], [ 3.43, 0 ], [ 0.311, -0.029 ], [ 4.296, -0.209 ], [ 5.604, -0.333 ], [ 6.089, -0.852 ], [ 9.827, -2.154 ], [ 5.635, -2.045 ], [ 2.928, -1.516 ], [ 0.096, -0.413 ] ], "v": [ [ -0.25, -37.45 ], [ -0.25, -39.292 ], [ -0.352, -39.627 ], [ -8.441, -48.41 ], [ -9.805, -48.71 ], [ -11.695, -48.71 ], [ -12.043, -48.617 ], [ -15.549, -47.635 ], [ -30.201, -41.435 ], [ -81.575, -30.008 ], [ -97.904, -28.937 ], [ -128.04, -29.897 ], [ -144.399, -32.098 ], [ -174.316, -39.388 ], [ -193.782, -47.058 ], [ -196.066, -48.073 ], [ -198.805, -48.71 ], [ -200.695, -48.71 ], [ -200.982, -48.612 ], [ -209.494, -42.035 ], [ -210.25, -39.19 ], [ -210.25, -37.45 ], [ -210.139, -37.008 ], [ -205.218, -29.585 ], [ -200.329, -27.133 ], [ -169.658, -16.368 ], [ -144.675, -11.119 ], [ -127.458, -9.034 ], [ -111.119, -8.149 ], [ -110.395, -8.07 ], [ -100.105, -8.07 ], [ -99.172, -8.154 ], [ -86.283, -8.786 ], [ -69.554, -10.561 ], [ -51.415, -13.81 ], [ -22.49, -22.107 ], [ -5.996, -29.157 ], [ -0.494, -36.201 ] ], "c": true } ] }, { "i": { "x": 0, "y": 1 }, "o": { "x": 1, "y": 0 }, "n": "0_1_1_0", "t": 55, "s": [ { "i": [ [ -0.081, 0.416 ], [ 0, 0.614 ], [ 0.017, 0.114 ], [ 4.432, 1.114 ], [ 0.455, 0.099 ], [ 0.63, 0 ], [ 0.118, -0.012 ], [ 1.098, -0.552 ], [ 5.005, -1.792 ], [ 17.596, -1.761 ], [ 5.449, -0.216 ], [ 10.022, 0.979 ], [ 5.427, 0.928 ], [ 9.789, 3.142 ], [ 6.284, 3.051 ], [ 0.79, 0.255 ], [ 0.915, 0.206 ], [ 0.63, 0 ], [ 0.098, -0.016 ], [ 1.538, -3.764 ], [ 0.246, -0.951 ], [ 0, -0.58 ], [ -0.022, -0.149 ], [ -2.832, -1.687 ], [ -1.661, -0.753 ], [ -10.509, -2.813 ], [ -8.427, -1.306 ], [ -5.769, -0.425 ], [ -5.447, -0.289 ], [ -0.241, -0.027 ], [ -3.43, 0 ], [ -0.312, 0.015 ], [ -4.293, 0.255 ], [ -5.555, 0.778 ], [ -6.003, 1.316 ], [ -9.441, 3.427 ], [ -5.316, 2.752 ], [ -0.737, 3.161 ] ], "o": [ [ 0, -0.614 ], [ -0.035, -0.111 ], [ -0.645, -4.395 ], [ -0.451, -0.113 ], [ -0.63, 0 ], [ -0.116, 0.032 ], [ -1.23, 0.122 ], [ -4.743, 2.385 ], [ -16.622, 5.95 ], [ -5.424, 0.543 ], [ -10.064, 0.399 ], [ -5.48, -0.536 ], [ -10.152, -1.736 ], [ -6.661, -2.138 ], [ -0.75, -0.364 ], [ -0.888, -0.287 ], [ -0.63, 0 ], [ -0.095, 0.034 ], [ -4.089, 0.654 ], [ -0.369, 0.903 ], [ 0, 0.58 ], [ 0.038, 0.147 ], [ 0.472, 3.212 ], [ 1.56, 0.929 ], [ 9.893, 4.481 ], [ 8.226, 2.203 ], [ 5.716, 0.886 ], [ 5.438, 0.4 ], [ 0.242, 0.013 ], [ 3.43, 0 ], [ 0.311, -0.029 ], [ 4.296, -0.209 ], [ 5.604, -0.333 ], [ 6.089, -0.852 ], [ 9.827, -2.154 ], [ 5.635, -2.045 ], [ 2.928, -1.516 ], [ 0.096, -0.413 ] ], "v": [ [ -0.25, -37.45 ], [ -0.25, -39.292 ], [ -0.352, -39.627 ], [ -8.441, -48.41 ], [ -9.805, -48.71 ], [ -11.695, -48.71 ], [ -12.043, -48.617 ], [ -15.549, -47.635 ], [ -30.201, -41.435 ], [ -81.575, -30.008 ], [ -97.904, -28.937 ], [ -128.04, -29.897 ], [ -144.399, -32.098 ], [ -174.316, -39.388 ], [ -193.782, -47.058 ], [ -196.066, -48.073 ], [ -198.805, -48.71 ], [ -200.695, -48.71 ], [ -200.982, -48.612 ], [ -209.494, -42.035 ], [ -210.25, -39.19 ], [ -210.25, -37.45 ], [ -210.139, -37.008 ], [ -205.218, -29.585 ], [ -200.329, -27.133 ], [ -169.658, -16.368 ], [ -144.675, -11.119 ], [ -127.458, -9.034 ], [ -111.119, -8.149 ], [ -110.395, -8.07 ], [ -100.105, -8.07 ], [ -99.172, -8.154 ], [ -86.283, -8.786 ], [ -69.554, -10.561 ], [ -51.415, -13.81 ], [ -22.49, -22.107 ], [ -5.996, -29.157 ], [ -0.494, -36.201 ] ], "c": true } ], "e": [ { "i": [ [ -0.044, 0.566 ], [ 0, 0.797 ], [ 0.009, 0.092 ], [ 0.473, 1.052 ], [ 2.267, 1.723 ], [ 3.546, 1.383 ], [ 4.958, 1.036 ], [ 5.038, 0.506 ], [ 3.295, 0.201 ], [ 3.27, 0.122 ], [ 0.157, 0.015 ], [ 2.008, 0 ], [ 0.123, -0.004 ], [ 2.525, -0.137 ], [ 3.032, -0.3 ], [ 3.359, -0.467 ], [ 3.986, -0.994 ], [ 4.243, -1.98 ], [ 2.404, -2.298 ], [ 0.547, -2.511 ], [ 0.082, -0.477 ], [ 0, -0.797 ], [ -0.009, -0.122 ], [ -0.09, -0.954 ], [ -0.77, -2.696 ], [ -4.189, -5.385 ], [ -7.486, -4.304 ], [ -9.579, -1.491 ], [ -2.987, -0.129 ], [ -0.602, -0.035 ], [ -1.278, 0 ], [ -0.169, 0.005 ], [ -3.481, 0.546 ], [ -4.049, 1.347 ], [ -6.01, 4.45 ], [ -3.397, 4.433 ], [ -1.64, 6.947 ], [ -0.167, 2.53 ] ], "o": [ [ 0, -0.797 ], [ -0.023, -0.091 ], [ -0.106, -1.121 ], [ -1.087, -2.417 ], [ -2.906, -2.208 ], [ -4.636, -1.808 ], [ -4.922, -1.029 ], [ -3.281, -0.33 ], [ -3.263, -0.199 ], [ -0.158, -0.006 ], [ -2.008, 0 ], [ -0.123, 0.014 ], [ -2.53, 0.082 ], [ -3.048, 0.165 ], [ -3.381, 0.335 ], [ -4.103, 0.571 ], [ -4.648, 1.159 ], [ -3.171, 1.48 ], [ -1.997, 1.909 ], [ -0.103, 0.473 ], [ 0, 0.797 ], [ 0.02, 0.121 ], [ 0.072, 0.955 ], [ 0.261, 2.763 ], [ 1.783, 6.242 ], [ 4.941, 6.352 ], [ 8.015, 4.608 ], [ 2.94, 0.458 ], [ 0.602, 0.026 ], [ 1.278, 0 ], [ 0.169, -0.017 ], [ 3.538, -0.113 ], [ 4.291, -0.673 ], [ 7.471, -2.485 ], [ 4.723, -3.497 ], [ 4.574, -5.97 ], [ 0.587, -2.486 ], [ 0.037, -0.566 ] ], "v": [ [ -36.789, -43.354 ], [ -36.789, -45.744 ], [ -36.856, -46.019 ], [ -37.73, -49.279 ], [ -42.925, -55.398 ], [ -52.719, -60.643 ], [ -67.157, -64.804 ], [ -82.094, -67.138 ], [ -91.959, -67.967 ], [ -101.766, -68.346 ], [ -102.238, -68.39 ], [ -108.262, -68.39 ], [ -108.631, -68.349 ], [ -116.22, -68.095 ], [ -125.351, -67.445 ], [ -135.461, -66.212 ], [ -147.59, -63.828 ], [ -161, -59.264 ], [ -169.508, -53.749 ], [ -173.463, -47.173 ], [ -173.711, -45.744 ], [ -173.711, -43.354 ], [ -173.652, -42.991 ], [ -173.44, -40.125 ], [ -171.868, -31.94 ], [ -162.909, -14.495 ], [ -144.266, 1.498 ], [ -117.864, 10.635 ], [ -108.971, 11.506 ], [ -107.167, 11.61 ], [ -103.333, 11.61 ], [ -102.827, 11.56 ], [ -92.3, 10.584 ], [ -79.789, 7.549 ], [ -59.57, -2.862 ], [ -47.388, -14.757 ], [ -38.06, -34.133 ], [ -36.92, -41.656 ] ], "c": true } ] }, { "i": { "x": 1, "y": 1 }, "o": { "x": 0.333, "y": 0 }, "n": "1_1_0p333_0", "t": 58, "s": [ { "i": [ [ -0.044, 0.566 ], [ 0, 0.797 ], [ 0.009, 0.092 ], [ 0.473, 1.052 ], [ 2.267, 1.723 ], [ 3.546, 1.383 ], [ 4.958, 1.036 ], [ 5.038, 0.506 ], [ 3.295, 0.201 ], [ 3.27, 0.122 ], [ 0.157, 0.015 ], [ 2.008, 0 ], [ 0.123, -0.004 ], [ 2.525, -0.137 ], [ 3.032, -0.3 ], [ 3.359, -0.467 ], [ 3.986, -0.994 ], [ 4.243, -1.98 ], [ 2.404, -2.298 ], [ 0.547, -2.511 ], [ 0.082, -0.477 ], [ 0, -0.797 ], [ -0.009, -0.122 ], [ -0.09, -0.954 ], [ -0.77, -2.696 ], [ -4.189, -5.385 ], [ -7.486, -4.304 ], [ -9.579, -1.491 ], [ -2.987, -0.129 ], [ -0.602, -0.035 ], [ -1.278, 0 ], [ -0.169, 0.005 ], [ -3.481, 0.546 ], [ -4.049, 1.347 ], [ -6.01, 4.45 ], [ -3.397, 4.433 ], [ -1.64, 6.947 ], [ -0.167, 2.53 ] ], "o": [ [ 0, -0.797 ], [ -0.023, -0.091 ], [ -0.106, -1.121 ], [ -1.087, -2.417 ], [ -2.906, -2.208 ], [ -4.636, -1.808 ], [ -4.922, -1.029 ], [ -3.281, -0.33 ], [ -3.263, -0.199 ], [ -0.158, -0.006 ], [ -2.008, 0 ], [ -0.123, 0.014 ], [ -2.53, 0.082 ], [ -3.048, 0.165 ], [ -3.381, 0.335 ], [ -4.103, 0.571 ], [ -4.648, 1.159 ], [ -3.171, 1.48 ], [ -1.997, 1.909 ], [ -0.103, 0.473 ], [ 0, 0.797 ], [ 0.02, 0.121 ], [ 0.072, 0.955 ], [ 0.261, 2.763 ], [ 1.783, 6.242 ], [ 4.941, 6.352 ], [ 8.015, 4.608 ], [ 2.94, 0.458 ], [ 0.602, 0.026 ], [ 1.278, 0 ], [ 0.169, -0.017 ], [ 3.538, -0.113 ], [ 4.291, -0.673 ], [ 7.471, -2.485 ], [ 4.723, -3.497 ], [ 4.574, -5.97 ], [ 0.587, -2.486 ], [ 0.037, -0.566 ] ], "v": [ [ -36.789, -43.354 ], [ -36.789, -45.744 ], [ -36.856, -46.019 ], [ -37.73, -49.279 ], [ -42.925, -55.398 ], [ -52.719, -60.643 ], [ -67.157, -64.804 ], [ -82.094, -67.138 ], [ -91.959, -67.967 ], [ -101.766, -68.346 ], [ -102.238, -68.39 ], [ -108.262, -68.39 ], [ -108.631, -68.349 ], [ -116.22, -68.095 ], [ -125.351, -67.445 ], [ -135.461, -66.212 ], [ -147.59, -63.828 ], [ -161, -59.264 ], [ -169.508, -53.749 ], [ -173.463, -47.173 ], [ -173.711, -45.744 ], [ -173.711, -43.354 ], [ -173.652, -42.991 ], [ -173.44, -40.125 ], [ -171.868, -31.94 ], [ -162.909, -14.495 ], [ -144.266, 1.498 ], [ -117.864, 10.635 ], [ -108.971, 11.506 ], [ -107.167, 11.61 ], [ -103.333, 11.61 ], [ -102.827, 11.56 ], [ -92.3, 10.584 ], [ -79.789, 7.549 ], [ -59.57, -2.862 ], [ -47.388, -14.757 ], [ -38.06, -34.133 ], [ -36.92, -41.656 ] ], "c": true } ], "e": [ { "i": [ [ -0.029, 0.566 ], [ 0, 0.797 ], [ 0.006, 0.092 ], [ 0.309, 1.052 ], [ 1.481, 1.723 ], [ 2.317, 1.383 ], [ 3.239, 1.036 ], [ 3.292, 0.506 ], [ 2.153, 0.201 ], [ 2.136, 0.122 ], [ 0.103, 0.015 ], [ 1.312, 0 ], [ 0.081, -0.004 ], [ 1.65, -0.137 ], [ 1.981, -0.3 ], [ 2.195, -0.467 ], [ 2.604, -0.994 ], [ 2.772, -1.98 ], [ 1.571, -2.298 ], [ 0.357, -2.511 ], [ 0.054, -0.477 ], [ 0, -0.797 ], [ -0.006, -0.122 ], [ -0.059, -0.954 ], [ -0.503, -2.696 ], [ -2.737, -5.385 ], [ -4.891, -4.304 ], [ -6.259, -1.491 ], [ -1.952, -0.129 ], [ -0.393, -0.035 ], [ -0.835, 0 ], [ -0.111, 0.005 ], [ -2.274, 0.546 ], [ -2.646, 1.347 ], [ -3.927, 4.45 ], [ -2.219, 4.434 ], [ -1.071, 6.947 ], [ -0.109, 2.53 ] ], "o": [ [ 0, -0.797 ], [ -0.015, -0.091 ], [ -0.069, -1.121 ], [ -0.71, -2.417 ], [ -1.898, -2.208 ], [ -3.029, -1.807 ], [ -3.216, -1.029 ], [ -2.144, -0.33 ], [ -2.132, -0.199 ], [ -0.103, -0.006 ], [ -1.312, 0 ], [ -0.08, 0.014 ], [ -1.653, 0.082 ], [ -1.992, 0.166 ], [ -2.209, 0.335 ], [ -2.681, 0.571 ], [ -3.037, 1.159 ], [ -2.072, 1.48 ], [ -1.305, 1.909 ], [ -0.067, 0.473 ], [ 0, 0.797 ], [ 0.013, 0.121 ], [ 0.047, 0.955 ], [ 0.171, 2.763 ], [ 1.165, 6.242 ], [ 3.229, 6.352 ], [ 5.237, 4.608 ], [ 1.921, 0.458 ], [ 0.393, 0.026 ], [ 0.835, 0 ], [ 0.11, -0.017 ], [ 2.311, -0.113 ], [ 2.803, -0.672 ], [ 4.881, -2.485 ], [ 3.086, -3.497 ], [ 2.989, -5.97 ], [ 0.383, -2.486 ], [ 0.024, -0.566 ] ], "v": [ [ -60.519, -43.354 ], [ -60.519, -45.744 ], [ -60.563, -46.019 ], [ -61.134, -49.279 ], [ -64.529, -55.398 ], [ -70.927, -60.643 ], [ -80.361, -64.804 ], [ -90.121, -67.138 ], [ -96.566, -67.967 ], [ -102.974, -68.346 ], [ -103.282, -68.39 ], [ -107.218, -68.39 ], [ -107.459, -68.349 ], [ -112.417, -68.095 ], [ -118.384, -67.445 ], [ -124.989, -66.212 ], [ -132.914, -63.828 ], [ -141.676, -59.264 ], [ -147.235, -53.749 ], [ -149.818, -47.173 ], [ -149.981, -45.744 ], [ -149.981, -43.354 ], [ -149.942, -42.992 ], [ -149.804, -40.125 ], [ -148.776, -31.94 ], [ -142.922, -14.495 ], [ -130.742, 1.498 ], [ -113.492, 10.635 ], [ -107.682, 11.506 ], [ -106.502, 11.61 ], [ -103.997, 11.61 ], [ -103.667, 11.56 ], [ -96.789, 10.584 ], [ -88.614, 7.549 ], [ -75.404, -2.862 ], [ -67.444, -14.757 ], [ -61.35, -34.133 ], [ -60.605, -41.656 ] ], "c": true } ] }, { "i": { "x": 1, "y": 1 }, "o": { "x": 0, "y": 0 }, "n": "1_1_0_0", "t": 65, "s": [ { "i": [ [ -0.029, 0.566 ], [ 0, 0.797 ], [ 0.006, 0.092 ], [ 0.309, 1.052 ], [ 1.481, 1.723 ], [ 2.317, 1.383 ], [ 3.239, 1.036 ], [ 3.292, 0.506 ], [ 2.153, 0.201 ], [ 2.136, 0.122 ], [ 0.103, 0.015 ], [ 1.312, 0 ], [ 0.081, -0.004 ], [ 1.65, -0.137 ], [ 1.981, -0.3 ], [ 2.195, -0.467 ], [ 2.604, -0.994 ], [ 2.772, -1.98 ], [ 1.571, -2.298 ], [ 0.357, -2.511 ], [ 0.054, -0.477 ], [ 0, -0.797 ], [ -0.006, -0.122 ], [ -0.059, -0.954 ], [ -0.503, -2.696 ], [ -2.737, -5.385 ], [ -4.891, -4.304 ], [ -6.259, -1.491 ], [ -1.952, -0.129 ], [ -0.393, -0.035 ], [ -0.835, 0 ], [ -0.111, 0.005 ], [ -2.274, 0.546 ], [ -2.646, 1.347 ], [ -3.927, 4.45 ], [ -2.219, 4.434 ], [ -1.071, 6.947 ], [ -0.109, 2.53 ] ], "o": [ [ 0, -0.797 ], [ -0.015, -0.091 ], [ -0.069, -1.121 ], [ -0.71, -2.417 ], [ -1.898, -2.208 ], [ -3.029, -1.807 ], [ -3.216, -1.029 ], [ -2.144, -0.33 ], [ -2.132, -0.199 ], [ -0.103, -0.006 ], [ -1.312, 0 ], [ -0.08, 0.014 ], [ -1.653, 0.082 ], [ -1.992, 0.166 ], [ -2.209, 0.335 ], [ -2.681, 0.571 ], [ -3.037, 1.159 ], [ -2.072, 1.48 ], [ -1.305, 1.909 ], [ -0.067, 0.473 ], [ 0, 0.797 ], [ 0.013, 0.121 ], [ 0.047, 0.955 ], [ 0.171, 2.763 ], [ 1.165, 6.242 ], [ 3.229, 6.352 ], [ 5.237, 4.608 ], [ 1.921, 0.458 ], [ 0.393, 0.026 ], [ 0.835, 0 ], [ 0.11, -0.017 ], [ 2.311, -0.113 ], [ 2.803, -0.672 ], [ 4.881, -2.485 ], [ 3.086, -3.497 ], [ 2.989, -5.97 ], [ 0.383, -2.486 ], [ 0.024, -0.566 ] ], "v": [ [ -60.519, -43.354 ], [ -60.519, -45.744 ], [ -60.563, -46.019 ], [ -61.134, -49.279 ], [ -64.529, -55.398 ], [ -70.927, -60.643 ], [ -80.361, -64.804 ], [ -90.121, -67.138 ], [ -96.566, -67.967 ], [ -102.974, -68.346 ], [ -103.282, -68.39 ], [ -107.218, -68.39 ], [ -107.459, -68.349 ], [ -112.417, -68.095 ], [ -118.384, -67.445 ], [ -124.989, -66.212 ], [ -132.914, -63.828 ], [ -141.676, -59.264 ], [ -147.235, -53.749 ], [ -149.818, -47.173 ], [ -149.981, -45.744 ], [ -149.981, -43.354 ], [ -149.942, -42.992 ], [ -149.804, -40.125 ], [ -148.776, -31.94 ], [ -142.922, -14.495 ], [ -130.742, 1.498 ], [ -113.492, 10.635 ], [ -107.682, 11.506 ], [ -106.502, 11.61 ], [ -103.997, 11.61 ], [ -103.667, 11.56 ], [ -96.789, 10.584 ], [ -88.614, 7.549 ], [ -75.404, -2.862 ], [ -67.444, -14.757 ], [ -61.35, -34.133 ], [ -60.605, -41.656 ] ], "c": true } ], "e": [ { "i": [ [ -0.029, 0.566 ], [ 0, 0.797 ], [ 0.006, 0.092 ], [ 0.309, 1.052 ], [ 1.481, 1.723 ], [ 2.317, 1.383 ], [ 3.239, 1.036 ], [ 3.292, 0.506 ], [ 2.153, 0.201 ], [ 2.136, 0.122 ], [ 0.103, 0.015 ], [ 1.312, 0 ], [ 0.081, -0.004 ], [ 1.65, -0.137 ], [ 1.981, -0.3 ], [ 2.195, -0.467 ], [ 2.604, -0.994 ], [ 2.772, -1.98 ], [ 1.571, -2.298 ], [ 0.357, -2.511 ], [ 0.054, -0.477 ], [ 0, -0.797 ], [ -0.006, -0.122 ], [ -0.059, -0.954 ], [ -0.503, -2.696 ], [ -2.737, -5.385 ], [ -4.891, -4.304 ], [ -6.259, -1.491 ], [ -1.952, -0.129 ], [ -0.393, -0.035 ], [ -0.835, 0 ], [ -0.111, 0.005 ], [ -2.274, 0.546 ], [ -2.646, 1.347 ], [ -3.927, 4.45 ], [ -2.219, 4.434 ], [ -1.071, 6.947 ], [ -0.109, 2.53 ] ], "o": [ [ 0, -0.797 ], [ -0.015, -0.091 ], [ -0.069, -1.121 ], [ -0.71, -2.417 ], [ -1.898, -2.208 ], [ -3.029, -1.807 ], [ -3.216, -1.029 ], [ -2.144, -0.33 ], [ -2.132, -0.199 ], [ -0.103, -0.006 ], [ -1.312, 0 ], [ -0.08, 0.014 ], [ -1.653, 0.082 ], [ -1.992, 0.166 ], [ -2.209, 0.335 ], [ -2.681, 0.571 ], [ -3.037, 1.159 ], [ -2.072, 1.48 ], [ -1.305, 1.909 ], [ -0.067, 0.473 ], [ 0, 0.797 ], [ 0.013, 0.121 ], [ 0.047, 0.955 ], [ 0.171, 2.763 ], [ 1.165, 6.242 ], [ 3.229, 6.352 ], [ 5.237, 4.608 ], [ 1.921, 0.458 ], [ 0.393, 0.026 ], [ 0.835, 0 ], [ 0.11, -0.017 ], [ 2.311, -0.113 ], [ 2.803, -0.672 ], [ 4.881, -2.485 ], [ 3.086, -3.497 ], [ 2.989, -5.97 ], [ 0.383, -2.486 ], [ 0.024, -0.566 ] ], "v": [ [ -60.519, -43.354 ], [ -60.519, -45.744 ], [ -60.563, -46.019 ], [ -61.134, -49.279 ], [ -64.529, -55.398 ], [ -70.927, -60.643 ], [ -80.361, -64.804 ], [ -90.121, -67.138 ], [ -96.566, -67.967 ], [ -102.974, -68.346 ], [ -103.282, -68.39 ], [ -107.218, -68.39 ], [ -107.459, -68.349 ], [ -112.417, -68.095 ], [ -118.384, -67.445 ], [ -124.989, -66.212 ], [ -132.914, -63.828 ], [ -141.676, -59.264 ], [ -147.235, -53.749 ], [ -149.818, -47.173 ], [ -149.981, -45.744 ], [ -149.981, -43.354 ], [ -149.942, -42.992 ], [ -149.804, -40.125 ], [ -148.776, -31.94 ], [ -142.922, -14.495 ], [ -130.742, 1.498 ], [ -113.492, 10.635 ], [ -107.682, 11.506 ], [ -106.502, 11.61 ], [ -103.997, 11.61 ], [ -103.667, 11.56 ], [ -96.789, 10.584 ], [ -88.614, 7.549 ], [ -75.404, -2.862 ], [ -67.444, -14.757 ], [ -61.35, -34.133 ], [ -60.605, -41.656 ] ], "c": true } ] }, { "i": { "x": 0, "y": 1 }, "o": { "x": 0, "y": 0 }, "n": "0_1_0_0", "t": 68.25, "s": [ { "i": [ [ -0.029, 0.566 ], [ 0, 0.797 ], [ 0.006, 0.092 ], [ 0.309, 1.052 ], [ 1.481, 1.723 ], [ 2.317, 1.383 ], [ 3.239, 1.036 ], [ 3.292, 0.506 ], [ 2.153, 0.201 ], [ 2.136, 0.122 ], [ 0.103, 0.015 ], [ 1.312, 0 ], [ 0.081, -0.004 ], [ 1.65, -0.137 ], [ 1.981, -0.3 ], [ 2.195, -0.467 ], [ 2.604, -0.994 ], [ 2.772, -1.98 ], [ 1.571, -2.298 ], [ 0.357, -2.511 ], [ 0.054, -0.477 ], [ 0, -0.797 ], [ -0.006, -0.122 ], [ -0.059, -0.954 ], [ -0.503, -2.696 ], [ -2.737, -5.385 ], [ -4.891, -4.304 ], [ -6.259, -1.491 ], [ -1.952, -0.129 ], [ -0.393, -0.035 ], [ -0.835, 0 ], [ -0.111, 0.005 ], [ -2.274, 0.546 ], [ -2.646, 1.347 ], [ -3.927, 4.45 ], [ -2.219, 4.434 ], [ -1.071, 6.947 ], [ -0.109, 2.53 ] ], "o": [ [ 0, -0.797 ], [ -0.015, -0.091 ], [ -0.069, -1.121 ], [ -0.71, -2.417 ], [ -1.898, -2.208 ], [ -3.029, -1.807 ], [ -3.216, -1.029 ], [ -2.144, -0.33 ], [ -2.132, -0.199 ], [ -0.103, -0.006 ], [ -1.312, 0 ], [ -0.08, 0.014 ], [ -1.653, 0.082 ], [ -1.992, 0.166 ], [ -2.209, 0.335 ], [ -2.681, 0.571 ], [ -3.037, 1.159 ], [ -2.072, 1.48 ], [ -1.305, 1.909 ], [ -0.067, 0.473 ], [ 0, 0.797 ], [ 0.013, 0.121 ], [ 0.047, 0.955 ], [ 0.171, 2.763 ], [ 1.165, 6.242 ], [ 3.229, 6.352 ], [ 5.237, 4.608 ], [ 1.921, 0.458 ], [ 0.393, 0.026 ], [ 0.835, 0 ], [ 0.11, -0.017 ], [ 2.311, -0.113 ], [ 2.803, -0.672 ], [ 4.881, -2.485 ], [ 3.086, -3.497 ], [ 2.989, -5.97 ], [ 0.383, -2.486 ], [ 0.024, -0.566 ] ], "v": [ [ -60.519, -43.354 ], [ -60.519, -45.744 ], [ -60.563, -46.019 ], [ -61.134, -49.279 ], [ -64.529, -55.398 ], [ -70.927, -60.643 ], [ -80.361, -64.804 ], [ -90.121, -67.138 ], [ -96.566, -67.967 ], [ -102.974, -68.346 ], [ -103.282, -68.39 ], [ -107.218, -68.39 ], [ -107.459, -68.349 ], [ -112.417, -68.095 ], [ -118.384, -67.445 ], [ -124.989, -66.212 ], [ -132.914, -63.828 ], [ -141.676, -59.264 ], [ -147.235, -53.749 ], [ -149.818, -47.173 ], [ -149.981, -45.744 ], [ -149.981, -43.354 ], [ -149.942, -42.992 ], [ -149.804, -40.125 ], [ -148.776, -31.94 ], [ -142.922, -14.495 ], [ -130.742, 1.498 ], [ -113.492, 10.635 ], [ -107.682, 11.506 ], [ -106.502, 11.61 ], [ -103.997, 11.61 ], [ -103.667, 11.56 ], [ -96.789, 10.584 ], [ -88.614, 7.549 ], [ -75.404, -2.862 ], [ -67.444, -14.757 ], [ -61.35, -34.133 ], [ -60.605, -41.656 ] ], "c": true } ], "e": [ { "i": [ [ -0.044, 0.566 ], [ 0, 0.797 ], [ 0.009, 0.092 ], [ 0.473, 1.052 ], [ 2.267, 1.723 ], [ 3.546, 1.383 ], [ 4.958, 1.036 ], [ 5.038, 0.506 ], [ 3.295, 0.201 ], [ 3.27, 0.122 ], [ 0.157, 0.015 ], [ 2.008, 0 ], [ 0.123, -0.004 ], [ 2.525, -0.137 ], [ 3.032, -0.3 ], [ 3.359, -0.467 ], [ 3.986, -0.994 ], [ 4.243, -1.98 ], [ 2.404, -2.298 ], [ 0.547, -2.511 ], [ 0.082, -0.477 ], [ 0, -0.797 ], [ -0.009, -0.122 ], [ -0.09, -0.954 ], [ -0.77, -2.696 ], [ -4.189, -5.385 ], [ -7.486, -4.304 ], [ -9.579, -1.491 ], [ -2.987, -0.129 ], [ -0.602, -0.035 ], [ -1.278, 0 ], [ -0.169, 0.005 ], [ -3.481, 0.546 ], [ -4.049, 1.347 ], [ -6.01, 4.45 ], [ -3.397, 4.433 ], [ -1.64, 6.947 ], [ -0.167, 2.53 ] ], "o": [ [ 0, -0.797 ], [ -0.023, -0.091 ], [ -0.106, -1.121 ], [ -1.087, -2.417 ], [ -2.906, -2.208 ], [ -4.636, -1.808 ], [ -4.922, -1.029 ], [ -3.281, -0.33 ], [ -3.263, -0.199 ], [ -0.158, -0.006 ], [ -2.008, 0 ], [ -0.123, 0.014 ], [ -2.53, 0.082 ], [ -3.048, 0.165 ], [ -3.381, 0.335 ], [ -4.103, 0.571 ], [ -4.648, 1.159 ], [ -3.171, 1.48 ], [ -1.997, 1.909 ], [ -0.103, 0.473 ], [ 0, 0.797 ], [ 0.02, 0.121 ], [ 0.072, 0.955 ], [ 0.261, 2.763 ], [ 1.783, 6.242 ], [ 4.941, 6.352 ], [ 8.015, 4.608 ], [ 2.94, 0.458 ], [ 0.602, 0.026 ], [ 1.278, 0 ], [ 0.169, -0.017 ], [ 3.538, -0.113 ], [ 4.291, -0.673 ], [ 7.471, -2.485 ], [ 4.723, -3.497 ], [ 4.574, -5.97 ], [ 0.587, -2.486 ], [ 0.037, -0.566 ] ], "v": [ [ -36.789, -43.354 ], [ -36.789, -45.744 ], [ -36.856, -46.019 ], [ -37.73, -49.279 ], [ -42.925, -55.398 ], [ -52.719, -60.643 ], [ -67.157, -64.804 ], [ -82.094, -67.138 ], [ -91.959, -67.967 ], [ -101.766, -68.346 ], [ -102.238, -68.39 ], [ -108.262, -68.39 ], [ -108.631, -68.349 ], [ -116.22, -68.095 ], [ -125.351, -67.445 ], [ -135.461, -66.212 ], [ -147.59, -63.828 ], [ -161, -59.264 ], [ -169.508, -53.749 ], [ -173.463, -47.173 ], [ -173.711, -45.744 ], [ -173.711, -43.354 ], [ -173.652, -42.991 ], [ -173.44, -40.125 ], [ -171.868, -31.94 ], [ -162.909, -14.495 ], [ -144.266, 1.498 ], [ -117.864, 10.635 ], [ -108.971, 11.506 ], [ -107.167, 11.61 ], [ -103.333, 11.61 ], [ -102.827, 11.56 ], [ -92.3, 10.584 ], [ -79.789, 7.549 ], [ -59.57, -2.862 ], [ -47.388, -14.757 ], [ -38.06, -34.133 ], [ -36.92, -41.656 ] ], "c": true } ] }, { "i": { "x": 0.308, "y": 1 }, "o": { "x": 0.703, "y": 0 }, "n": "0p308_1_0p703_0", "t": 71.144, "s": [ { "i": [ [ -0.044, 0.566 ], [ 0, 0.797 ], [ 0.009, 0.092 ], [ 0.473, 1.052 ], [ 2.267, 1.723 ], [ 3.546, 1.383 ], [ 4.958, 1.036 ], [ 5.038, 0.506 ], [ 3.295, 0.201 ], [ 3.27, 0.122 ], [ 0.157, 0.015 ], [ 2.008, 0 ], [ 0.123, -0.004 ], [ 2.525, -0.137 ], [ 3.032, -0.3 ], [ 3.359, -0.467 ], [ 3.986, -0.994 ], [ 4.243, -1.98 ], [ 2.404, -2.298 ], [ 0.547, -2.511 ], [ 0.082, -0.477 ], [ 0, -0.797 ], [ -0.009, -0.122 ], [ -0.09, -0.954 ], [ -0.77, -2.696 ], [ -4.189, -5.385 ], [ -7.486, -4.304 ], [ -9.579, -1.491 ], [ -2.987, -0.129 ], [ -0.602, -0.035 ], [ -1.278, 0 ], [ -0.169, 0.005 ], [ -3.481, 0.546 ], [ -4.049, 1.347 ], [ -6.01, 4.45 ], [ -3.397, 4.433 ], [ -1.64, 6.947 ], [ -0.167, 2.53 ] ], "o": [ [ 0, -0.797 ], [ -0.023, -0.091 ], [ -0.106, -1.121 ], [ -1.087, -2.417 ], [ -2.906, -2.208 ], [ -4.636, -1.808 ], [ -4.922, -1.029 ], [ -3.281, -0.33 ], [ -3.263, -0.199 ], [ -0.158, -0.006 ], [ -2.008, 0 ], [ -0.123, 0.014 ], [ -2.53, 0.082 ], [ -3.048, 0.165 ], [ -3.381, 0.335 ], [ -4.103, 0.571 ], [ -4.648, 1.159 ], [ -3.171, 1.48 ], [ -1.997, 1.909 ], [ -0.103, 0.473 ], [ 0, 0.797 ], [ 0.02, 0.121 ], [ 0.072, 0.955 ], [ 0.261, 2.763 ], [ 1.783, 6.242 ], [ 4.941, 6.352 ], [ 8.015, 4.608 ], [ 2.94, 0.458 ], [ 0.602, 0.026 ], [ 1.278, 0 ], [ 0.169, -0.017 ], [ 3.538, -0.113 ], [ 4.291, -0.673 ], [ 7.471, -2.485 ], [ 4.723, -3.497 ], [ 4.574, -5.97 ], [ 0.587, -2.486 ], [ 0.037, -0.566 ] ], "v": [ [ -36.789, -43.354 ], [ -36.789, -45.744 ], [ -36.856, -46.019 ], [ -37.73, -49.279 ], [ -42.925, -55.398 ], [ -52.719, -60.643 ], [ -67.157, -64.804 ], [ -82.094, -67.138 ], [ -91.959, -67.967 ], [ -101.766, -68.346 ], [ -102.238, -68.39 ], [ -108.262, -68.39 ], [ -108.631, -68.349 ], [ -116.22, -68.095 ], [ -125.351, -67.445 ], [ -135.461, -66.212 ], [ -147.59, -63.828 ], [ -161, -59.264 ], [ -169.508, -53.749 ], [ -173.463, -47.173 ], [ -173.711, -45.744 ], [ -173.711, -43.354 ], [ -173.652, -42.991 ], [ -173.44, -40.125 ], [ -171.868, -31.94 ], [ -162.909, -14.495 ], [ -144.266, 1.498 ], [ -117.864, 10.635 ], [ -108.971, 11.506 ], [ -107.167, 11.61 ], [ -103.333, 11.61 ], [ -102.827, 11.56 ], [ -92.3, 10.584 ], [ -79.789, 7.549 ], [ -59.57, -2.862 ], [ -47.388, -14.757 ], [ -38.06, -34.133 ], [ -36.92, -41.656 ] ], "c": true } ], "e": [ { "i": [ [ -0.081, 0.416 ], [ 0, 0.614 ], [ 0.017, 0.114 ], [ 4.432, 1.114 ], [ 0.455, 0.099 ], [ 0.63, 0 ], [ 0.118, -0.012 ], [ 1.098, -0.552 ], [ 5.005, -1.792 ], [ 17.596, -1.761 ], [ 5.449, -0.216 ], [ 10.022, 0.979 ], [ 5.427, 0.928 ], [ 9.789, 3.142 ], [ 6.284, 3.051 ], [ 0.79, 0.255 ], [ 0.915, 0.206 ], [ 0.63, 0 ], [ 0.098, -0.016 ], [ 1.538, -3.764 ], [ 0.246, -0.951 ], [ 0, -0.58 ], [ -0.022, -0.149 ], [ -2.832, -1.687 ], [ -1.661, -0.753 ], [ -10.509, -2.813 ], [ -8.427, -1.306 ], [ -5.769, -0.425 ], [ -5.447, -0.289 ], [ -0.241, -0.027 ], [ -3.43, 0 ], [ -0.312, 0.015 ], [ -4.293, 0.255 ], [ -5.555, 0.778 ], [ -6.003, 1.316 ], [ -9.441, 3.427 ], [ -5.316, 2.752 ], [ -0.737, 3.161 ] ], "o": [ [ 0, -0.614 ], [ -0.035, -0.111 ], [ -0.645, -4.395 ], [ -0.451, -0.113 ], [ -0.63, 0 ], [ -0.116, 0.032 ], [ -1.23, 0.122 ], [ -4.743, 2.385 ], [ -16.622, 5.95 ], [ -5.424, 0.543 ], [ -10.064, 0.399 ], [ -5.48, -0.536 ], [ -10.152, -1.736 ], [ -6.661, -2.138 ], [ -0.75, -0.364 ], [ -0.888, -0.287 ], [ -0.63, 0 ], [ -0.095, 0.034 ], [ -4.089, 0.654 ], [ -0.369, 0.903 ], [ 0, 0.58 ], [ 0.038, 0.147 ], [ 0.472, 3.212 ], [ 1.56, 0.929 ], [ 9.893, 4.481 ], [ 8.226, 2.203 ], [ 5.716, 0.886 ], [ 5.438, 0.4 ], [ 0.242, 0.013 ], [ 3.43, 0 ], [ 0.311, -0.029 ], [ 4.296, -0.209 ], [ 5.604, -0.333 ], [ 6.089, -0.852 ], [ 9.827, -2.154 ], [ 5.635, -2.045 ], [ 2.928, -1.516 ], [ 0.096, -0.413 ] ], "v": [ [ -0.25, -37.45 ], [ -0.25, -39.292 ], [ -0.352, -39.627 ], [ -8.441, -48.41 ], [ -9.805, -48.71 ], [ -11.695, -48.71 ], [ -12.043, -48.617 ], [ -15.549, -47.635 ], [ -30.201, -41.435 ], [ -81.575, -30.008 ], [ -97.904, -28.937 ], [ -128.04, -29.897 ], [ -144.399, -32.098 ], [ -174.316, -39.388 ], [ -193.782, -47.058 ], [ -196.066, -48.073 ], [ -198.805, -48.71 ], [ -200.695, -48.71 ], [ -200.982, -48.612 ], [ -209.494, -42.035 ], [ -210.25, -39.19 ], [ -210.25, -37.45 ], [ -210.139, -37.008 ], [ -205.218, -29.585 ], [ -200.329, -27.133 ], [ -169.658, -16.368 ], [ -144.675, -11.119 ], [ -127.458, -9.034 ], [ -111.119, -8.149 ], [ -110.395, -8.07 ], [ -100.105, -8.07 ], [ -99.172, -8.154 ], [ -86.283, -8.786 ], [ -69.554, -10.561 ], [ -51.415, -13.81 ], [ -22.49, -22.107 ], [ -5.996, -29.157 ], [ -0.494, -36.201 ] ], "c": true } ] }, { "t": 75 } ] }, "nm": "Path 1", "mn": "ADBE Vector Shape - Group" }, { "ty": "fl", "fillEnabled": true, "c": { "k": [ 0, 0, 0, 1 ] }, "o": { "k": 100 }, "nm": "Fill 1", "mn": "ADBE Vector Graphic - Fill" }, { "ty": "tr", "p": { "k": [ 105.25, 28.39 ], "ix": 2 }, "a": { "k": [ 0, 0 ], "ix": 1 }, "s": { "k": [ 100, 100 ], "ix": 3 }, "r": { "k": 0, "ix": 6 }, "o": { "k": 100, "ix": 7 }, "sk": { "k": 0, "ix": 4 }, "sa": { "k": 0, "ix": 5 }, "nm": "Transform" } ], "nm": "Group 1", "np": 2, "mn": "ADBE Vector Group" } ], "ip": 0, "op": 78, "st": 0, "bm": 0, "sr": 1 }, { "ddd": 0, "ind": 5, "ty": 4, "nm": "nose", "ks": { "o": { "k": 100 }, "r": { "k": 0 }, "p": { "k": [ 503.498, 680.02, 0 ] }, "a": { "k": [ 42.233, 29.174, 0 ] }, "s": { "k": [ 100, 100, 100 ] } }, "ao": 0, "shapes": [ { "ty": "gr", "it": [ { "ind": 0, "ty": "sh", "ks": { "k": { "i": [ [ -3.557, 23.651 ], [ -14.41, -16.492 ], [ 16.97, -2.701 ] ], "o": [ [ 2.439, -16.225 ], [ 14.41, 16.492 ], [ -16.971, 2.7 ] ], "v": [ [ -38.426, -12.003 ], [ 27.573, -12.432 ], [ -1.301, 26.225 ] ], "c": true } }, "nm": "Path 1", "mn": "ADBE Vector Shape - Group" }, { "ty": "fl", "fillEnabled": true, "c": { "k": [ 0, 0, 0, 1 ] }, "o": { "k": 100 }, "nm": "Fill 1", "mn": "ADBE Vector Graphic - Fill" }, { "ty": "tr", "p": { "k": [ 42.233, 29.175 ], "ix": 2 }, "a": { "k": [ 0, 0 ], "ix": 1 }, "s": { "k": [ 100, 100 ], "ix": 3 }, "r": { "k": 0, "ix": 6 }, "o": { "k": 100, "ix": 7 }, "sk": { "k": 0, "ix": 4 }, "sa": { "k": 0, "ix": 5 }, "nm": "Transform" } ], "nm": "Group 1", "np": 2, "mn": "ADBE Vector Group" } ], "ip": 0, "op": 78, "st": 0, "bm": 0, "sr": 1 }, { "ddd": 0, "ind": 6, "ty": 3, "nm": "eye_pupil_controller", "ks": { "o": { "k": 0 }, "r": { "k": 0 }, "p": { "k": [ { "i": { "x": 0.833, "y": 0.833 }, "o": { "x": 0.167, "y": 0 }, "n": "0p833_0p833_0p167_0", "t": 0, "s": [ 481, 519.5, 0 ], "e": [ 459.5, 527, 0 ], "to": [ 0, 0, 0 ], "ti": [ -4.03295516967773, -4.07727336883545, 0 ] }, { "i": { "x": 0.833, "y": 0.833 }, "o": { "x": 0.167, "y": 0.167 }, "n": "0p833_0p833_0p167_0p167", "t": 4, "s": [ 459.5, 527, 0 ], "e": [ 481.875, 531.25, 0 ], "to": [ 3.79166674613953, 3.83333325386047, 0 ], "ti": [ -11.1676235198975, 0.20281778275967, 0 ] }, { "i": { "x": 0.833, "y": 0.833 }, "o": { "x": 0.167, "y": 0.167 }, "n": "0p833_0p833_0p167_0p167", "t": 13, "s": [ 481.875, 531.25, 0 ], "e": [ 506.5, 524, 0 ], "to": [ 11.9174766540527, -0.21643604338169, 0 ], "ti": [ -6.04166650772095, 4.625, 0 ] }, { "i": { "x": 0.667, "y": 1 }, "o": { "x": 0.167, "y": 0.167 }, "n": "0p667_1_0p167_0p167", "t": 22, "s": [ 506.5, 524, 0 ], "e": [ 481, 519.5, 0 ], "to": [ 5.39564085006714, -4.13045644760132, 0 ], "ti": [ 0, 0, 0 ] }, { "i": { "x": 0.667, "y": 0.667 }, "o": { "x": 0.167, "y": 0.167 }, "n": "0p667_0p667_0p167_0p167", "t": 29, "s": [ 481, 519.5, 0 ], "e": [ 481, 519.5, 0 ], "to": [ 0, 0, 0 ], "ti": [ 0, 0, 0 ] }, { "i": { "x": 0.667, "y": 0.667 }, "o": { "x": 0.167, "y": 0.167 }, "n": "0p667_0p667_0p167_0p167", "t": 59, "s": [ 481, 519.5, 0 ], "e": [ 481, 519.5, 0 ], "to": [ 0, 0, 0 ], "ti": [ 0, 0, 0 ] }, { "i": { "x": 0.667, "y": 1 }, "o": { "x": 0.167, "y": 0 }, "n": "0p667_1_0p167_0", "t": 60.667, "s": [ 481, 519.5, 0 ], "e": [ 483.5, 535, 0 ], "to": [ 0, 0, 0 ], "ti": [ 0, 0, 0 ] }, { "i": { "x": 0.667, "y": 1 }, "o": { "x": 0.167, "y": 0 }, "n": "0p667_1_0p167_0", "t": 69, "s": [ 483.5, 535, 0 ], "e": [ 481, 519.5, 0 ], "to": [ 0, 0, 0 ], "ti": [ 0, 0, 0 ] }, { "i": { "x": 0.667, "y": 0.667 }, "o": { "x": 0.167, "y": 0.167 }, "n": "0p667_0p667_0p167_0p167", "t": 71.682, "s": [ 481, 519.5, 0 ], "e": [ 481, 519.5, 0 ], "to": [ 0, 0, 0 ], "ti": [ 0, 0, 0 ] }, { "t": 74 } ] }, "a": { "k": [ 0, 0, 0 ] }, "s": { "k": [ 100, 100, 100 ] } }, "ao": 0, "ip": 0, "op": 78, "st": 0, "bm": 0, "sr": 1 }, { "ddd": 0, "ind": 7, "ty": 4, "nm": "eye_pupil_left", "parent": 6, "ks": { "o": { "k": 100 }, "r": { "k": 0 }, "p": { "k": [ -136.393, 117.431, 0 ] }, "a": { "k": [ 15.684, 15.18, 0 ] }, "s": { "k": [ 170, 170, 100 ] } }, "ao": 0, "shapes": [ { "ty": "gr", "it": [ { "ind": 0, "ty": "sh", "ks": { "k": { "i": [ [ -0.518, 0.056 ], [ -3.048, -3.18 ], [ -0.381, -3.103 ], [ 2.85, -3.249 ], [ 3.341, -0.639 ], [ 3.717, 2.785 ], [ 0.788, 3.507 ], [ -0.239, 1.647 ], [ -3.323, 2.447 ], [ -2.342, 0.372 ] ], "o": [ [ 4.772, 0.078 ], [ 2.154, 2.248 ], [ 0.529, 4.306 ], [ -2.252, 2.566 ], [ -4.542, 0.869 ], [ -2.871, -2.151 ], [ -0.363, -1.619 ], [ 0.597, -4.113 ], [ 1.905, -1.403 ], [ 0.89, -0.142 ] ], "v": [ [ -0.538, -14.93 ], [ 11.057, -10.14 ], [ 14.905, -2.099 ], [ 11.371, 9.246 ], [ 2.932, 14.061 ], [ -9.513, 11.235 ], [ -15.01, 2.721 ], [ -15.194, -2.185 ], [ -9.237, -11.996 ], [ -2.849, -14.672 ] ], "c": true } }, "nm": "Path 1", "mn": "ADBE Vector Shape - Group" }, { "ty": "fl", "fillEnabled": true, "c": { "k": [ 1, 1, 1, 1 ] }, "o": { "k": 100 }, "nm": "Fill 1", "mn": "ADBE Vector Graphic - Fill" }, { "ty": "tr", "p": { "k": [ 15.683, 15.18 ], "ix": 2 }, "a": { "k": [ 0, 0 ], "ix": 1 }, "s": { "k": [ 100, 100 ], "ix": 3 }, "r": { "k": 0, "ix": 6 }, "o": { "k": 100, "ix": 7 }, "sk": { "k": 0, "ix": 4 }, "sa": { "k": 0, "ix": 5 }, "nm": "Transform" } ], "nm": "Group 1", "np": 2, "mn": "ADBE Vector Group" } ], "ip": 0, "op": 78, "st": 0, "bm": 0, "sr": 1 }, { "ddd": 0, "ind": 8, "ty": 4, "nm": "eye_pupil_right", "parent": 6, "ks": { "o": { "k": 100 }, "r": { "k": 0 }, "p": { "k": [ 172.459, 117.431, 0 ] }, "a": { "k": [ 15.684, 15.18, 0 ] }, "s": { "k": [ 170, 170, 100 ] } }, "ao": 0, "shapes": [ { "ty": "gr", "it": [ { "ind": 0, "ty": "sh", "ks": { "k": { "i": [ [ -0.519, 0.056 ], [ -3.048, -3.18 ], [ -0.38, -3.103 ], [ 2.851, -3.249 ], [ 3.341, -0.639 ], [ 3.717, 2.785 ], [ 0.787, 3.507 ], [ -0.24, 1.647 ], [ -3.323, 2.447 ], [ -2.341, 0.372 ] ], "o": [ [ 4.771, 0.078 ], [ 2.154, 2.248 ], [ 0.53, 4.306 ], [ -2.252, 2.566 ], [ -4.542, 0.869 ], [ -2.87, -2.151 ], [ -0.364, -1.619 ], [ 0.597, -4.113 ], [ 1.904, -1.403 ], [ 0.89, -0.142 ] ], "v": [ [ -0.538, -14.93 ], [ 11.058, -10.14 ], [ 14.904, -2.099 ], [ 11.371, 9.246 ], [ 2.933, 14.061 ], [ -9.514, 11.235 ], [ -15.01, 2.721 ], [ -15.194, -2.185 ], [ -9.236, -11.996 ], [ -2.85, -14.672 ] ], "c": true } }, "nm": "Path 1", "mn": "ADBE Vector Shape - Group" }, { "ty": "fl", "fillEnabled": true, "c": { "k": [ 1, 1, 1, 1 ] }, "o": { "k": 100 }, "nm": "Fill 1", "mn": "ADBE Vector Graphic - Fill" }, { "ty": "tr", "p": { "k": [ 15.684, 15.18 ], "ix": 2 }, "a": { "k": [ 0, 0 ], "ix": 1 }, "s": { "k": [ 100, 100 ], "ix": 3 }, "r": { "k": 0, "ix": 6 }, "o": { "k": 100, "ix": 7 }, "sk": { "k": 0, "ix": 4 }, "sa": { "k": 0, "ix": 5 }, "nm": "Transform" } ], "nm": "Group 1", "np": 2, "mn": "ADBE Vector Group" } ], "ip": 0, "op": 78, "st": 0, "bm": 0, "sr": 1 }, { "ddd": 0, "ind": 9, "ty": 4, "nm": "eyeball_left", "ks": { "o": { "k": 100 }, "r": { "k": 0 }, "p": { "k": [ 345.573, 632.847, 0 ] }, "a": { "k": [ 65.824, 65.826, 0 ] }, "s": { "k": [ 100, 100, 100 ] } }, "ao": 0, "shapes": [ { "ty": "gr", "it": [ { "ind": 0, "ty": "sh", "ks": { "k": { "i": [ [ 36.218, 0 ], [ 0, -36.212 ], [ -36.219, 0 ], [ 0, 36.219 ] ], "o": [ [ -36.219, 0 ], [ 0, 36.219 ], [ 36.218, 0 ], [ 0, -36.212 ] ], "v": [ [ 0.002, -65.576 ], [ -65.574, -0.003 ], [ 0.002, 65.576 ], [ 65.574, -0.003 ] ], "c": true } }, "nm": "Path 1", "mn": "ADBE Vector Shape - Group" }, { "ind": 1, "ty": "sh", "ks": { "k": { "i": [ [ -27.336, 0 ], [ 0, -27.335 ], [ 27.334, 0 ], [ 0, 27.338 ] ], "o": [ [ 27.334, 0 ], [ 0, 27.338 ], [ -27.336, 0 ], [ 0, -27.335 ] ], "v": [ [ 0.002, -49.576 ], [ 49.574, -0.003 ], [ 0.002, 49.576 ], [ -49.574, -0.003 ] ], "c": true } }, "nm": "Path 2", "mn": "ADBE Vector Shape - Group" }, { "ty": "mm", "mm": 1, "nm": "Merge Paths 1", "mn": "ADBE Vector Filter - Merge" }, { "ty": "fl", "fillEnabled": true, "c": { "k": [ 1, 1, 1, 1 ] }, "o": { "k": 100 }, "nm": "Fill 1", "mn": "ADBE Vector Graphic - Fill" }, { "ty": "tr", "p": { "k": [ 65.824, 65.826 ], "ix": 2 }, "a": { "k": [ 0, 0 ], "ix": 1 }, "s": { "k": [ 100, 100 ], "ix": 3 }, "r": { "k": 0, "ix": 6 }, "o": { "k": 100, "ix": 7 }, "sk": { "k": 0, "ix": 4 }, "sa": { "k": 0, "ix": 5 }, "nm": "Transform" } ], "nm": "Group 1", "np": 4, "mn": "ADBE Vector Group" }, { "ty": "gr", "it": [ { "ind": 0, "ty": "sh", "ks": { "k": { "i": [ [ 31.746, 0 ], [ 0, 31.749 ], [ -31.748, 0 ], [ 0, -31.745 ] ], "o": [ [ -31.748, 0 ], [ 0, -31.745 ], [ 31.746, 0 ], [ 0, 31.749 ] ], "v": [ [ 0.002, 57.576 ], [ -57.575, -0.003 ], [ 0.002, -57.576 ], [ 57.575, -0.003 ] ], "c": true } }, "nm": "Path 1", "mn": "ADBE Vector Shape - Group" }, { "ty": "fl", "fillEnabled": true, "c": { "k": [ 0, 0, 0, 1 ] }, "o": { "k": 100 }, "nm": "Fill 1", "mn": "ADBE Vector Graphic - Fill" }, { "ty": "tr", "p": { "k": [ 65.824, 65.826 ], "ix": 2 }, "a": { "k": [ 0, 0 ], "ix": 1 }, "s": { "k": [ 100, 100 ], "ix": 3 }, "r": { "k": 0, "ix": 6 }, "o": { "k": 100, "ix": 7 }, "sk": { "k": 0, "ix": 4 }, "sa": { "k": 0, "ix": 5 }, "nm": "Transform" } ], "nm": "Group 2", "np": 2, "mn": "ADBE Vector Group" } ], "ip": 0, "op": 78, "st": 0, "bm": 0, "sr": 1 }, { "ddd": 0, "ind": 10, "ty": 4, "nm": "eyeball_right", "ks": { "o": { "k": 100 }, "r": { "k": 0 }, "p": { "k": [ 654.426, 632.847, 0 ] }, "a": { "k": [ 65.824, 65.826, 0 ] }, "s": { "k": [ 100, 100, 100 ] } }, "ao": 0, "shapes": [ { "ty": "gr", "it": [ { "ind": 0, "ty": "sh", "ks": { "k": { "i": [ [ 36.218, 0 ], [ 0, -36.212 ], [ -36.219, 0 ], [ 0, 36.219 ] ], "o": [ [ -36.219, 0 ], [ 0, 36.219 ], [ 36.218, 0 ], [ 0, -36.212 ] ], "v": [ [ 0.002, -65.576 ], [ -65.574, -0.003 ], [ 0.002, 65.576 ], [ 65.574, -0.003 ] ], "c": true } }, "nm": "Path 1", "mn": "ADBE Vector Shape - Group" }, { "ind": 1, "ty": "sh", "ks": { "k": { "i": [ [ -27.337, 0 ], [ 0, -27.335 ], [ 27.334, 0 ], [ 0, 27.338 ] ], "o": [ [ 27.334, 0 ], [ 0, 27.338 ], [ -27.337, 0 ], [ 0, -27.335 ] ], "v": [ [ 0.002, -49.576 ], [ 49.574, -0.003 ], [ 0.002, 49.576 ], [ -49.574, -0.003 ] ], "c": true } }, "nm": "Path 2", "mn": "ADBE Vector Shape - Group" }, { "ty": "mm", "mm": 1, "nm": "Merge Paths 1", "mn": "ADBE Vector Filter - Merge" }, { "ty": "fl", "fillEnabled": true, "c": { "k": [ 1, 1, 1, 1 ] }, "o": { "k": 100 }, "nm": "Fill 1", "mn": "ADBE Vector Graphic - Fill" }, { "ty": "tr", "p": { "k": [ 65.824, 65.826 ], "ix": 2 }, "a": { "k": [ 0, 0 ], "ix": 1 }, "s": { "k": [ 100, 100 ], "ix": 3 }, "r": { "k": 0, "ix": 6 }, "o": { "k": 100, "ix": 7 }, "sk": { "k": 0, "ix": 4 }, "sa": { "k": 0, "ix": 5 }, "nm": "Transform" } ], "nm": "Group 1", "np": 4, "mn": "ADBE Vector Group" }, { "ty": "gr", "it": [ { "ind": 0, "ty": "sh", "ks": { "k": { "i": [ [ 31.745, 0 ], [ 0, 31.749 ], [ -31.748, 0 ], [ 0, -31.745 ] ], "o": [ [ -31.748, 0 ], [ 0, -31.745 ], [ 31.745, 0 ], [ 0, 31.749 ] ], "v": [ [ 0.002, 57.576 ], [ -57.574, -0.003 ], [ 0.002, -57.576 ], [ 57.574, -0.003 ] ], "c": true } }, "nm": "Path 1", "mn": "ADBE Vector Shape - Group" }, { "ty": "fl", "fillEnabled": true, "c": { "k": [ 0, 0, 0, 1 ] }, "o": { "k": 100 }, "nm": "Fill 1", "mn": "ADBE Vector Graphic - Fill" }, { "ty": "tr", "p": { "k": [ 65.824, 65.826 ], "ix": 2 }, "a": { "k": [ 0, 0 ], "ix": 1 }, "s": { "k": [ 100, 100 ], "ix": 3 }, "r": { "k": 0, "ix": 6 }, "o": { "k": 100, "ix": 7 }, "sk": { "k": 0, "ix": 4 }, "sa": { "k": 0, "ix": 5 }, "nm": "Transform" } ], "nm": "Group 2", "np": 2, "mn": "ADBE Vector Group" } ], "ip": 0, "op": 78, "st": 0, "bm": 0, "sr": 1 }, { "ddd": 0, "ind": 11, "ty": 4, "nm": "cheek_left", "ks": { "o": { "k": 100 }, "r": { "k": 0 }, "p": { "k": [ 268.359, 718.298, 0 ] }, "a": { "k": [ 37.624, 37.625, 0 ] }, "s": { "k": [ 100, 100, 100 ] } }, "ao": 0, "shapes": [ { "ty": "gr", "it": [ { "ind": 0, "ty": "sh", "ks": { "k": { "i": [ [ 0, -20.645 ], [ 20.64, 0 ], [ 0, 20.637 ], [ -20.645, 0 ] ], "o": [ [ 0, 20.637 ], [ -20.645, 0 ], [ 0, -20.645 ], [ 20.64, 0 ] ], "v": [ [ 37.374, 0 ], [ 0.002, 37.375 ], [ -37.374, 0 ], [ 0.002, -37.375 ] ], "c": true } }, "nm": "Path 1", "mn": "ADBE Vector Shape - Group" }, { "ty": "fl", "fillEnabled": true, "c": { "k": [ 1, 0.74, 0.75, 1 ] }, "o": { "k": 100 }, "nm": "Fill 1", "mn": "ADBE Vector Graphic - Fill" }, { "ty": "tr", "p": { "k": [ 37.624, 37.626 ], "ix": 2 }, "a": { "k": [ 0, 0 ], "ix": 1 }, "s": { "k": [ 100, 100 ], "ix": 3 }, "r": { "k": 0, "ix": 6 }, "o": { "k": 100, "ix": 7 }, "sk": { "k": 0, "ix": 4 }, "sa": { "k": 0, "ix": 5 }, "nm": "Transform" } ], "nm": "Group 1", "np": 2, "mn": "ADBE Vector Group" } ], "ip": 0, "op": 78, "st": 0, "bm": 0, "sr": 1 }, { "ddd": 0, "ind": 12, "ty": 4, "nm": "cheek_right", "ks": { "o": { "k": 100 }, "r": { "k": 0 }, "p": { "k": [ 731.64, 718.298, 0 ] }, "a": { "k": [ 37.622, 37.625, 0 ] }, "s": { "k": [ 100, 100, 100 ] } }, "ao": 0, "shapes": [ { "ty": "gr", "it": [ { "ind": 0, "ty": "sh", "ks": { "k": { "i": [ [ 0, -20.645 ], [ 20.645, 0 ], [ 0, 20.637 ], [ -20.637, 0 ] ], "o": [ [ 0, 20.637 ], [ -20.637, 0 ], [ 0, -20.645 ], [ 20.645, 0 ] ], "v": [ [ 37.372, 0 ], [ -0.004, 37.375 ], [ -37.372, 0 ], [ -0.004, -37.375 ] ], "c": true } }, "nm": "Path 1", "mn": "ADBE Vector Shape - Group" }, { "ty": "fl", "fillEnabled": true, "c": { "k": [ 1, 0.74, 0.75, 1 ] }, "o": { "k": 100 }, "nm": "Fill 1", "mn": "ADBE Vector Graphic - Fill" }, { "ty": "tr", "p": { "k": [ 37.622, 37.626 ], "ix": 2 }, "a": { "k": [ 0, 0 ], "ix": 1 }, "s": { "k": [ 100, 100 ], "ix": 3 }, "r": { "k": 0, "ix": 6 }, "o": { "k": 100, "ix": 7 }, "sk": { "k": 0, "ix": 4 }, "sa": { "k": 0, "ix": 5 }, "nm": "Transform" } ], "nm": "Group 1", "np": 2, "mn": "ADBE Vector Group" } ], "ip": 0, "op": 78, "st": 0, "bm": 0, "sr": 1 }, { "ddd": 0, "ind": 13, "ty": 4, "nm": "face", "ks": { "o": { "k": 100 }, "r": { "k": 0 }, "p": { "k": [ 499.999, 618.729, 0 ] }, "a": { "k": [ 321.693, 269.521, 0 ] }, "s": { "k": [ 100, 100, 100 ] } }, "ao": 0, "shapes": [ { "ty": "gr", "it": [ { "ind": 0, "ty": "sh", "ks": { "k": { "i": [ [ -16.966, -150.969 ], [ 183.631, 0 ], [ -19.111, 170.049 ], [ -151.907, 0 ] ], "o": [ [ 19.111, 170.049 ], [ -183.64, 0 ], [ 16.966, -150.969 ], [ 151.905, 0 ] ], "v": [ [ 302.333, 0.828 ], [ 0.003, 269.271 ], [ -302.332, 0.828 ], [ 0.003, -269.271 ] ], "c": true } }, "nm": "Path 1", "mn": "ADBE Vector Shape - Group" }, { "ty": "fl", "fillEnabled": true, "c": { "k": [ 0.94, 0.94, 0.94, 1 ] }, "o": { "k": 100 }, "nm": "Fill 1", "mn": "ADBE Vector Graphic - Fill" }, { "ty": "tr", "p": { "k": [ 321.693, 269.521 ], "ix": 2 }, "a": { "k": [ 0, 0 ], "ix": 1 }, "s": { "k": [ 100, 100 ], "ix": 3 }, "r": { "k": 0, "ix": 6 }, "o": { "k": 100, "ix": 7 }, "sk": { "k": 0, "ix": 4 }, "sa": { "k": 0, "ix": 5 }, "nm": "Transform" } ], "nm": "Group 1", "np": 2, "mn": "ADBE Vector Group" } ], "ip": 0, "op": 78, "st": 0, "bm": 0, "sr": 1 }, { "ddd": 0, "ind": 14, "ty": 4, "nm": "ear_right", "ks": { "o": { "k": 100 }, "r": { "k": 0 }, "p": { "k": [ 763.547, 347.608, 0 ] }, "a": { "k": [ 121.241, 208.634, 0 ] }, "s": { "k": [ 100, 100, 100 ] } }, "ao": 0, "shapes": [ { "ty": "gr", "it": [ { "ind": 0, "ty": "sh", "ks": { "k": { "i": [ [ 0, 0 ], [ -36.779, 4.6 ], [ 71.08, -141.307 ] ], "o": [ [ 0, 0 ], [ 36.786, -4.592 ], [ 0, 0 ] ], "v": [ [ -58.273, 77.218 ], [ 7.089, -105.401 ], [ -12.807, 109.993 ] ], "c": true } }, "nm": "Path 1", "mn": "ADBE Vector Shape - Group" }, { "ty": "fl", "fillEnabled": true, "c": { "k": [ 1, 0.74, 0.75, 1 ] }, "o": { "k": 100 }, "nm": "Fill 1", "mn": "ADBE Vector Graphic - Fill" }, { "ty": "tr", "p": { "k": [ 88.503, 211.259 ], "ix": 2 }, "a": { "k": [ 0, 0 ], "ix": 1 }, "s": { "k": [ 100, 100 ], "ix": 3 }, "r": { "k": 0, "ix": 6 }, "o": { "k": 100, "ix": 7 }, "sk": { "k": 0, "ix": 4 }, "sa": { "k": 0, "ix": 5 }, "nm": "Transform" } ], "nm": "Group 1", "np": 2, "mn": "ADBE Vector Group" }, { "ty": "gr", "it": [ { "ind": 0, "ty": "sh", "ks": { "k": { "i": [ [ 0, 0 ], [ -81.399, 7.085 ], [ 168.251, -305.441 ] ], "o": [ [ 0, 0 ], [ 81.407, -7.093 ], [ 0, 0 ] ], "v": [ [ -120.991, 60.422 ], [ -19.402, -201.291 ], [ -47.26, 208.384 ] ], "c": true } }, "nm": "Path 1", "mn": "ADBE Vector Shape - Group" }, { "ty": "fl", "fillEnabled": true, "c": { "k": [ 0.94, 0.94, 0.94, 1 ] }, "o": { "k": 100 }, "nm": "Fill 1", "mn": "ADBE Vector Graphic - Fill" }, { "ty": "tr", "p": { "k": [ 121.241, 208.634 ], "ix": 2 }, "a": { "k": [ 0, 0 ], "ix": 1 }, "s": { "k": [ 100, 100 ], "ix": 3 }, "r": { "k": 0, "ix": 6 }, "o": { "k": 100, "ix": 7 }, "sk": { "k": 0, "ix": 4 }, "sa": { "k": 0, "ix": 5 }, "nm": "Transform" } ], "nm": "Group 2", "np": 2, "mn": "ADBE Vector Group" } ], "ip": 0, "op": 78, "st": 0, "bm": 0, "sr": 1 }, { "ddd": 0, "ind": 15, "ty": 4, "nm": "ear_left", "ks": { "o": { "k": 100 }, "r": { "k": 0 }, "p": { "k": [ 236.452, 347.608, 0 ] }, "a": { "k": [ 121.236, 208.634, 0 ] }, "s": { "k": [ 100, 100, 100 ] } }, "ao": 0, "shapes": [ { "ty": "gr", "it": [ { "ind": 0, "ty": "sh", "ks": { "k": { "i": [ [ 0, 0 ], [ 36.779, 4.6 ], [ -71.08, -141.307 ] ], "o": [ [ 0, 0 ], [ -36.779, -4.592 ], [ 0, 0 ] ], "v": [ [ 58.275, 77.218 ], [ -7.091, -105.401 ], [ 12.805, 109.993 ] ], "c": true } }, "nm": "Path 1", "mn": "ADBE Vector Shape - Group" }, { "ty": "fl", "fillEnabled": true, "c": { "k": [ 1, 0.74, 0.75, 1 ] }, "o": { "k": 100 }, "nm": "Fill 1", "mn": "ADBE Vector Graphic - Fill" }, { "ty": "tr", "p": { "k": [ 153.974, 211.259 ], "ix": 2 }, "a": { "k": [ 0, 0 ], "ix": 1 }, "s": { "k": [ 100, 100 ], "ix": 3 }, "r": { "k": 0, "ix": 6 }, "o": { "k": 100, "ix": 7 }, "sk": { "k": 0, "ix": 4 }, "sa": { "k": 0, "ix": 5 }, "nm": "Transform" } ], "nm": "Group 1", "np": 2, "mn": "ADBE Vector Group" }, { "ty": "gr", "it": [ { "ind": 0, "ty": "sh", "ks": { "k": { "i": [ [ 0, 0 ], [ 81.41, 7.085 ], [ -168.248, -305.441 ] ], "o": [ [ 0, 0 ], [ -81.399, -7.093 ], [ 0, 0 ] ], "v": [ [ 120.986, 60.422 ], [ 19.392, -201.291 ], [ 47.262, 208.384 ] ], "c": true } }, "nm": "Path 1", "mn": "ADBE Vector Shape - Group" }, { "ty": "fl", "fillEnabled": true, "c": { "k": [ 0.94, 0.94, 0.94, 1 ] }, "o": { "k": 100 }, "nm": "Fill 1", "mn": "ADBE Vector Graphic - Fill" }, { "ty": "tr", "p": { "k": [ 121.236, 208.634 ], "ix": 2 }, "a": { "k": [ 0, 0 ], "ix": 1 }, "s": { "k": [ 100, 100 ], "ix": 3 }, "r": { "k": 0, "ix": 6 }, "o": { "k": 100, "ix": 7 }, "sk": { "k": 0, "ix": 4 }, "sa": { "k": 0, "ix": 5 }, "nm": "Transform" } ], "nm": "Group 2", "np": 2, "mn": "ADBE Vector Group" } ], "ip": 0, "op": 78, "st": 0, "bm": 0, "sr": 1 }, { "ddd": 0, "ind": 16, "ty": 4, "nm": "body", "ks": { "o": { "k": 100 }, "r": { "k": 0 }, "p": { "k": [ 499.697, 811.329, 0 ] }, "a": { "k": [ 233.907, 164.921, 0 ] }, "s": { "k": [ 100, 100, 100 ] } }, "ao": 0, "shapes": [ { "ty": "gr", "it": [ { "ind": 0, "ty": "sh", "ks": { "k": { "i": [ [ 55.823, 51.688 ], [ 0, 0 ], [ 24.473, -75.204 ], [ -85.039, 0 ], [ -68.954, 38.859 ] ], "o": [ [ -126.866, -117.468 ], [ 0, 0 ], [ 69.094, 39.071 ], [ 84.791, 0 ], [ -14.957, -44.737 ] ], "v": [ [ 133.898, -47.203 ], [ -164.471, -30.757 ], [ -233.657, 103.292 ], [ 0.303, 164.671 ], [ 233.657, 103.636 ] ], "c": true } }, "nm": "Path 1", "mn": "ADBE Vector Shape - Group" }, { "ty": "fl", "fillEnabled": true, "c": { "k": [ 0.94, 0.94, 0.94, 1 ] }, "o": { "k": 100 }, "nm": "Fill 1", "mn": "ADBE Vector Graphic - Fill" }, { "ty": "tr", "p": { "k": [ 233.907, 164.921 ], "ix": 2 }, "a": { "k": [ 0, 0 ], "ix": 1 }, "s": { "k": [ 100, 100 ], "ix": 3 }, "r": { "k": 0, "ix": 6 }, "o": { "k": 100, "ix": 7 }, "sk": { "k": 0, "ix": 4 }, "sa": { "k": 0, "ix": 5 }, "nm": "Transform" } ], "nm": "Group 1", "np": 2, "mn": "ADBE Vector Group" } ], "ip": 0, "op": 78, "st": 0, "bm": 0, "sr": 1 }, { "ddd": 0, "ind": 17, "ty": 4, "nm": "bg", "ks": { "o": { "k": 100 }, "r": { "k": 0 }, "p": { "k": [ 500, 500, 0 ] }, "a": { "k": [ 476.25, 476.25, 0 ] }, "s": { "k": [ 100, 100, 100 ] } }, "ao": 0, "shapes": [ { "ty": "gr", "it": [ { "ind": 0, "ty": "sh", "ks": { "k": { "i": [ [ 0, 262.888 ], [ 262.888, 0 ], [ 0, -262.888 ], [ -262.888, 0 ] ], "o": [ [ 0, -262.888 ], [ -262.888, 0 ], [ 0, 262.888 ], [ 262.888, 0 ] ], "v": [ [ 476, 0 ], [ 0, -476 ], [ -476, 0 ], [ 0, 476 ] ], "c": true } }, "nm": "Path 1", "mn": "ADBE Vector Shape - Group" }, { "ty": "fl", "fillEnabled": true, "c": { "k": [ 0.62, 0.79, 0.81, 1 ] }, "o": { "k": 100 }, "nm": "Fill 1", "mn": "ADBE Vector Graphic - Fill" }, { "ty": "tr", "p": { "k": [ 476.25, 476.25 ], "ix": 2 }, "a": { "k": [ 0, 0 ], "ix": 1 }, "s": { "k": [ 100, 100 ], "ix": 3 }, "r": { "k": 0, "ix": 6 }, "o": { "k": 100, "ix": 7 }, "sk": { "k": 0, "ix": 4 }, "sa": { "k": 0, "ix": 5 }, "nm": "Transform" } ], "nm": "Group 1", "np": 2, "mn": "ADBE Vector Group" } ], "ip": 0, "op": 78, "st": 0, "bm": 0, "sr": 1 } ], "v": "4.5.3", "ddd": 0, "ip": 0, "op": 78, "fr": 25, "w": 1000, "h": 1000 } ================================================ FILE: devtools_options.yaml ================================================ description: This file stores settings for Dart & Flutter DevTools. documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states extensions: ================================================ FILE: docs/Android问题汇总.md ================================================ # Android 问题汇总 ## Could not resolve io.flutter:flutter_embedding_xxx ### 大致异常如下: ```java Could not resolve all files for configuration ':app:debugCompileClasspath'. > Could not resolve io.flutter:flutter_embedding_debug:1.0.0-6bc433c6b6b5b98dcf4cc11aff31cdee90849f32. ... ``` ### 解决方法: 修改`flutter\packages\flutter_tools\gradle`下的`flutter.gradle`文件。 替换`MAVEN_REPO`为: http://download.flutter.io 或者修改flutter项目`android`目录的`build.gradle`文件,在`repositories`中添加: ```java maven { url `http://download.flutter.io` } ``` 参考:https://github.com/flutter/flutter/issues/39729 ## 关于打包 默认使用`flutter build apk`命令,包含32、64位。 添加`--target-platform`可指定平台,比如`android-arm`或`android-arm64`,来减小包体积。 还可以使用`--split-debug-info`标志省略调试信息,来减小包体积。(注意使用此方式无法获取可读的堆栈信息) 完整举例子: ``` flutter build apk --target-platform android-arm64 --obfuscate --split-debug-info=/flutter_deer/ ``` ## 历史问题 - 3.10.0已知问题(~~#124546~~ ~~#126560~~ ~~#131319~~ ~~#73388~~)。 - 1.22.0已知问题(~~#67262~~ ~~#67213~~)。 - 1.17.0已知问题(~~#25767~~ ~~#47191~~)。 - 1.12.13已知问题(~~#47804~~ ~~#47270~~ ~~#47635~~ ~~#47137~~ ~~#47462~~ ~~#47021~~ ~~#39494~~)。 - 1.12.13已修复。~~在1.9.1上,TextField在语言环境为中文时,[光标与输入文字不居中显示](https://github.com/flutter/flutter/issues/40248),可暂时使用`textBaseline: TextBaseline.alphabetic` 处理此问提。~~ - 1.9.1已支持,使用`keyboardType: TextInputType.visiblePassword`即可。~~输入框在不设置`obscureText`属性的情况下(false),[无法弹出密码模式键盘](https://github.com/flutter/flutter/issues/31738),可暂时使用`BlacklistingTextInputFormatter`去除可能会输入的中文。~~ ================================================ FILE: docs/CHANGELOG.md ================================================ # Change Log: ## 1.3.3 * 适配Android 14。 * Flutter SDK升至3.24.0。 ## 1.3.2 * Android导航栏颜色优化。 ## 1.3.1 * Flutter SDK升至3.16.5。 ## 1.3.0 * 适配Android 13。 * Flutter SDK升至3.10.0。 ## 1.2.3 * Flutter SDK升至3.7.0。 ## 1.2.2 * 适配Android 12。 * Flutter SDK升至3.0.0。 ## 1.2.1 * 适配Android 11。 ## 1.2.0 * 迁移到空安全。 * 修复曲线图长按滑动不展示提示框问题。 ## 1.1.7 * 深色模式适配导航栏,优化启动效果。 * 适配Android 11键盘弹出动画。 * 商品列表进入编辑页时添加Hero动画。 * 添加lottie使用demo。 ## 1.1.6 * 密码键盘添加输入振动。 * 迁移到新的本地化方案。 * 设置页添加Deer Web入口。 * 适配dio 4.0版本。 * 更换扫码插件。 ## 1.1.5 * Web支持基本完成。 * 迁移废弃的FlatButton为TextButton。 * 添加多语言切换。 * Flutter SDK升至2.0.0。 ## 1.1.4 * 添加navigator示例。 * 添加金额数字输入限制。 * Flutter SDK升至1.22.3。 ## 1.1.3 * 设置页添加Demo入口。 * 代码优化。 ## 1.1.2 * 编译版本Flutter 1.17.3。 * 避免动画执行打断。 * unfocus方式调整。 * 扫码插件更新。 * 适配Android 10。 ## 1.1.1 * logo更换。 * 添加Android 8.0自适应图标。 * 动画、代码优化。 ## 1.1.0 * 提现账号页添加3D翻转动画。 * 侧滑删除操作优化。 * PageView 滑动卡顿优化。 * 添加accessibility test。 ## 1.0.9 * 文字大小不受手机系统设置影响。 * 商品分类选择状态保留。 * Flutter版本升至1.12.13+hotfix.8。 * 优化布局,提升性能。 ## 1.0.8 * 启动页适配深色模式。 * 更好的无障碍支持。 ## 1.0.7 * 优化商品列表 ================================================ FILE: docs/Web问题汇总.md ================================================ # Web 问题汇总(flutter 2.10.3) ## service worker flutter 2.2中新的`service worker`加载机制目前发现兼容不够,部分浏览器无法正常工作。(web/index1.html) ## CanvasKit渲染(默认PC浏览器) ### 中文文字、表情等加载延迟导致乱码现象 主要原因是完整的字体表情包太大,不能是一次加载完成,按需加载过程导致此类现象。 具体问题跟进可以关注: - [[web] Emojis take a few seconds to render on canvaskit ](https://github.com/flutter/flutter/issues/76248) - [[Web] [CanvasKit][Feature Request]: Load fonts as soon as detecting browser locale](https://github.com/flutter/flutter/issues/77023) ## 指定渲染引擎 ``` flutter run -d chrome --release --web-renderer html // 或 flutter run -d chrome --release --web-renderer canvaskit ``` > 总结:HTML渲染相较于CanvasKit渲染,UI还原度差一些,但综合性能相对较好。 ## 已解决问题 ### 使用`Transform`。 在变换Widget时,添加的`LinearGradient`没有渐变效果。。。(例子见lib/account/widgets/withdrawal_account_item.dart) 目前处理方法是添加`RepaintBoundary`。 ### 按钮的大小在移动端与Web端不同 可以`ThemeData`中全局指定`visualDensity`属性为`VisualDensity.standard`。 详情见[Buttons not respecting default dimensions](https://github.com/flutter/flutter/issues/77142) ## 已修复问题 ### ~~使用`DecorationImage`的`colorFilter`属性。~~ `ColorFilter.mode`中的color为null时,Web报错NoSuchMethodError: invalid member on null: 'red' 。 2.2.0上`ColorFilter.mode`中的color以不能设为null。 其他相关问题: [[web] wrong image filter on web app built with HTML renderer](https://github.com/flutter/flutter/issues/76966) ### ~~使用`Locale`报空安全错误~~ 2.2.0已修复,详情见[[Web]: App throws null safety errors on Locale using latest stable, but works on Master](https://github.com/flutter/flutter/issues/79351) ### ~~HTML渲染使用`TextOverflow.ellipsis`属性~~ 现象如下: - 文字没有超出,后面出现红色省略号。 - 文字超出,未出现省略号。 其他相关问题: - [[canvaskit] font renders missing glyph when text overflow is ellipsis](https://github.com/flutter/flutter/issues/76473) ================================================ FILE: docs/iOS问题汇总.md ================================================ # iOS 问题汇总 ## CDN: trunk Repo update failed ### 问题如下: ```java [!] CDN: trunk Repo update failed - xx error(s): CDN: trunk URL couldn't be downloaded: https://raw.githubusercontent.com/CocoaPods/Specs/master/Specs/... ... ``` ### 原因: CocoaPods 1.8后将CDN切换为默认的spec repo源。 ### 解决方法: 1.Podfile文件中添加source源: ``` source 'https://github.com/CocoaPods/Specs.git' ``` 或者指定为国内的镜像: ``` source 'https://mirrors.tuna.tsinghua.edu.cn/git/CocoaPods/Specs.git' ``` 2.执行`pod repo remove trunk`移除trunk源。 参考:https://www.jianshu.com/p/bf1cbe49cb5d ## CDN: trunk URL couldn't be downloaded 在host文件中添加: ``` 151.101.76.133 raw.githubusercontent.com ``` 如果这个ip无效,可以根据这个[查询真实IP](https://www.cnblogs.com/ljcgood66/p/12852044.html)自行查询。 参考:https://www.ioiox.com/archives/62.html https://mirrors.tuna.tsinghua.edu.cn/help/CocoaPods/ ## 历史问题 - [~~iOS使用Let's Encript证书,App卡死~~](https://github.com/flutterchina/dio/issues/703#issuecomment-748737446) - 1.17.0已知问题(~~#38323~~ ~~#47191~~)。 - 1.17.0已修复。~~在iOS手机上开启深色模式时,[无法将状态栏文字修改为黑色](https://github.com/flutter/flutter/issues/41067)。~~ ================================================ FILE: integration_test/goods_test.dart ================================================ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_deer/goods/page/goods_edit_page.dart'; import 'package:flutter_deer/goods/page/goods_page.dart'; import 'package:flutter_deer/goods/page/goods_size_page.dart'; import 'package:flutter_deer/main.dart'; import 'package:flutter_deer/res/constant.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; /// flutter drive --driver integration_test/integration_test.dart --target integration_test/goods_test.dart void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('商品部分:', () { Constant.isDriverTest = true; tearDown(() { debugPrint('< Success'); }); testWidgets('商品页测试',(WidgetTester tester) async { runApp(MyApp(home: const GoodsPage())); await tester.pumpAndSettle(); await tester.tap(find.text('待售')); await tester.pumpAndSettle(); final Finder pageView = find.byKey(const Key('pageView')); await tester.drag(pageView, const Offset(400, 0)); await tester.pumpAndSettle(); await tester.tap(find.text('全部商品')); await tester.pumpAndSettle(); await tester.tap(find.text('休闲食品')); await tester.pumpAndSettle(); //进入搜索页 await tester.tap(find.byKey(const Key('search'))); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('search_back'))); await tester.pumpAndSettle(); //添加商品 await tester.tap(find.byKey(const Key('add'))); await tester.pumpAndSettle(); await tester.tap(find.text('添加商品')); await tester.pumpAndSettle(); await tester.tap(find.byTooltip('Back')); await tester.pumpAndSettle(); // 商品菜单 await tester.tap(find.byKey(const Key('goods_menu_item_2'))); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('goods_operation_item_2'))); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('goods_delete_item_2'))); await tester.pumpAndSettle(); await tester.tap(find.text('确认删除')); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('goods_menu_item_1'))); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('goods_edit_item_1'))); await tester.pumpAndSettle(); }); testWidgets('商品编辑页测试',(WidgetTester tester) async { runApp(MyApp(home: const GoodsEditPage())); await tester.pumpAndSettle(); await tester.drag(find.byKey(const Key('goods_edit_page')), const Offset(0, -500)); await tester.pumpAndSettle(); await tester.tap(find.text('商品类型')); await tester.pumpAndSettle(); await tester.tap(find.text('生鲜果蔬')); await tester.pumpAndSettle(); await tester.tap(find.text('厨房用具')); await tester.pumpAndSettle(); await tester.tap(find.text('碗碟')); await tester.pumpAndSettle(); }, timeout: const Timeout(Duration(seconds: 30))); testWidgets('商品规格页测试',(WidgetTester tester) async { runApp(MyApp(home: const GoodsSizePage())); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('hint'))); await tester.pumpAndSettle(); final richText = find.byKey(const Key('name_edit')).first; fireOnTap(richText, '编辑'); await tester.pumpAndSettle(); await tester.tap(find.text('取消')); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('2'))); await tester.pumpAndSettle(); await tester.tap(find.byTooltip('Back')); await tester.pumpAndSettle(); // 侧滑删除 await tester.drag(find.byKey(const Key('2')), const Offset(-100, 0)); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('delete_2'))); await tester.pumpAndSettle(); await tester.tap(find.byTooltip('Back')); await tester.pumpAndSettle(); await tester.tap(find.byTooltip('Back')); await tester.pumpAndSettle(); }, timeout: const Timeout(Duration(seconds: 30))); }); } /// https://github.com/flutter/flutter/issues/56023 /// Runs the onTap handler for the [TextSpan] which matches the search-string. void fireOnTap(Finder finder, String text) { final Element element = finder.evaluate().single; final RenderParagraph paragraph = element.renderObject! as RenderParagraph; // The children are the individual TextSpans which have GestureRecognizers paragraph.text.visitChildren((InlineSpan span) { if (span is TextSpan) { if (span.text != text) { return true; // continue iterating. } (span.recognizer! as TapGestureRecognizer).onTap!(); } return false; // stop iterating, we found the one. }); } ================================================ FILE: integration_test/integration_test.dart ================================================ import 'package:integration_test/integration_test_driver.dart'; Future main() => integrationDriver(); ================================================ FILE: integration_test/login_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/login/page/login_page.dart'; import 'package:flutter_deer/login/page/register_page.dart'; import 'package:flutter_deer/main.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; /// flutter drive --driver integration_test/integration_test.dart --target integration_test/login_test.dart void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('登录部分:', () { tearDown(() { debugPrint('< Success'); }); testWidgets('登录页按钮点击',(WidgetTester tester) async { runApp(MyApp(home: const LoginPage())); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('actionName'))); await tester.pumpAndSettle(); await tester.tap(find.byTooltip('Back')); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('forgotPassword'))); await tester.pumpAndSettle(); await tester.tap(find.byTooltip('Back')); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('noAccountRegister'))); }); testWidgets('注册页测试',(WidgetTester tester) async { runApp(MyApp(home: const RegisterPage())); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('getVerificationCode')));/// 无法成功触发事件,需要输入手机号 final Finder textField = find.byKey(const Key('phone')); await tester.enterText(textField, '15000000000'); // 输入内容 await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('getVerificationCode'))); final Finder textField2 = find.byKey(const Key('vcode')); await tester.enterText(textField2, '123456'); await tester.pumpAndSettle(); final Finder textField3 = find.byKey(const Key('password')); await tester.enterText(textField3, '111111'); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('register'))); // 点击注册 // 清除输入框文字 await tester.pumpAndSettle(); expect(find.text('111111'), findsOneWidget); await tester.tap(find.byKey(const Key('password_delete'))); expect(find.text('111111'), findsNothing); await tester.pumpAndSettle(); await tester.tap(find.byTooltip('Back')); }, timeout: const Timeout(Duration(seconds: 30))); testWidgets('登录页测试',(WidgetTester tester) async { runApp(MyApp(home: const LoginPage())); await tester.pumpAndSettle(); final Finder textField = find.byKey(const Key('phone')); await tester.enterText(textField, '15000000000'); await tester.pumpAndSettle(); final Finder textField2 = find.byKey(const Key('password')); await tester.enterText(textField2, '111111'); await tester.pumpAndSettle(); // 点击密码可见两次 await tester.tap(find.byKey(const Key('password_showPwd'))); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('password_showPwd'))); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('login'))); // 点击登录 }, timeout: const Timeout(Duration(seconds: 30))); }); } ================================================ FILE: ios/.gitignore ================================================ **/dgph *.mode1v3 *.mode2v3 *.moved-aside *.pbxuser *.perspectivev3 **/*sync/ .sconsign.dblite .tags* **/.vagrant/ **/DerivedData/ Icon? **/Pods/ **/.symlinks/ profile xcuserdata **/.generated/ Flutter/App.framework Flutter/Flutter.framework Flutter/Flutter.podspec Flutter/Generated.xcconfig Flutter/ephemeral/ Flutter/app.flx Flutter/app.zip Flutter/flutter_assets/ Flutter/flutter_export_environment.sh ServiceDefinitions.json Runner/GeneratedPluginRegistrant.* # Exceptions to above rules. !default.mode1v3 !default.mode2v3 !default.pbxuser !default.perspectivev3 ================================================ FILE: ios/Flutter/AppFrameworkInfo.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable App CFBundleIdentifier io.flutter.flutter.app CFBundleInfoDictionaryVersion 6.0 CFBundleName App CFBundlePackageType FMWK CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1.0 ================================================ FILE: ios/Flutter/Debug.xcconfig ================================================ #include "Generated.xcconfig" #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" ================================================ FILE: ios/Flutter/Release.xcconfig ================================================ #include "Generated.xcconfig" #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" ================================================ FILE: ios/Podfile ================================================ # Uncomment this line to define a global platform for your project platform :ios, '13.0' # source 'https://github.com/CocoaPods/Specs.git' # source 'https://mirrors.tuna.tsinghua.edu.cn/git/CocoaPods/Specs.git' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' project 'Runner', { 'Debug' => :debug, 'Profile' => :release, 'Release' => :release, } def flutter_root generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) unless File.exist?(generated_xcode_build_settings_path) raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" end File.foreach(generated_xcode_build_settings_path) do |line| matches = line.match(/FLUTTER_ROOT\=(.*)/) return matches[1].strip if matches end raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" end require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) flutter_ios_podfile_setup target 'Runner' do use_frameworks! :linkage => :static flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) target 'RunnerTests' do inherit! :search_paths end end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) target.build_configurations.each do |config| if config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'].to_f < 12.0 config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0' end # https://github.com/flutter/flutter/issues/90504 #84562 # config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64 i386' end end end ================================================ FILE: ios/Runner/AppDelegate.swift ================================================ import UIKit import Flutter @main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { GeneratedPluginRegistrant.register(with: self) let controller:FlutterViewController = window?.rootViewController as! FlutterViewController let versionChannel = FlutterMethodChannel(name: "version", binaryMessenger: controller as! FlutterBinaryMessenger) versionChannel.setMethodCallHandler { (call, result) in if "jumpAppStore" == call.method { let urlString = "https://itunes.apple.com/us/app/id444934666" let url = URL(string: urlString) if UIApplication.shared.canOpenURL(url!) { if #available(iOS 10.0, *) { UIApplication.shared.open(url!, options: [:], completionHandler: nil) } else { UIApplication.shared.openURL(url!) } } } else { result(FlutterMethodNotImplemented) } } return super.application(application, didFinishLaunchingWithOptions: launchOptions) } } ================================================ FILE: ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "size" : "20x20", "idiom" : "iphone", "filename" : "Icon-App-20x20@2x.png", "scale" : "2x" }, { "size" : "20x20", "idiom" : "iphone", "filename" : "Icon-App-20x20@3x.png", "scale" : "3x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@1x.png", "scale" : "1x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@2x.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@3x.png", "scale" : "3x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "Icon-App-40x40@2x.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "Icon-App-40x40@3x.png", "scale" : "3x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "Icon-App-60x60@2x.png", "scale" : "2x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "Icon-App-60x60@3x.png", "scale" : "3x" }, { "size" : "20x20", "idiom" : "ipad", "filename" : "Icon-App-20x20@1x.png", "scale" : "1x" }, { "size" : "20x20", "idiom" : "ipad", "filename" : "Icon-App-20x20@2x.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "ipad", "filename" : "Icon-App-29x29@1x.png", "scale" : "1x" }, { "size" : "29x29", "idiom" : "ipad", "filename" : "Icon-App-29x29@2x.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "ipad", "filename" : "Icon-App-40x40@1x.png", "scale" : "1x" }, { "size" : "40x40", "idiom" : "ipad", "filename" : "Icon-App-40x40@2x.png", "scale" : "2x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "Icon-App-76x76@1x.png", "scale" : "1x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "Icon-App-76x76@2x.png", "scale" : "2x" }, { "size" : "83.5x83.5", "idiom" : "ipad", "filename" : "Icon-App-83.5x83.5@2x.png", "scale" : "2x" }, { "size" : "1024x1024", "idiom" : "ios-marketing", "filename" : "Icon-App-1024x1024@1x.png", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "LaunchImage.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "LaunchImage@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "LaunchImage@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md ================================================ # Launch Screen Assets You can customize the launch screen with your own desired assets by replacing the image files in this directory. You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. ================================================ FILE: ios/Runner/Assets.xcassets/flutter_dash_black.imageset/Contents.json ================================================ { "images" : [ { "filename" : "flutter_dash_black_1x.png", "idiom" : "universal", "scale" : "1x" }, { "filename" : "flutter_dash_black_2x.png", "idiom" : "universal", "scale" : "2x" }, { "filename" : "flutter_dash_black_3x.png", "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "template-rendering-intent" : "template" } } ================================================ FILE: ios/Runner/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: ios/Runner/Base.lproj/Main.storyboard ================================================ ================================================ FILE: ios/Runner/Info.plist ================================================ CFBundleAllowMixedLocalizations CFBundleDevelopmentRegion zh_CN CFBundleLocalizations en zh_CN NSCameraUsageDescription “Flutter Deer”请求在您使用期间获取您的相机权限,使用扫描条形码等功能 NSLocationWhenInUseUsageDescription “Flutter Deer”请求在您使用期间获取定位权限,这将带来更好的用户体验 NSPhotoLibraryUsageDescription “Flutter Deer”请求在您使用期间获取您的相册权限 io.flutter.embedded_views_preview CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName Flutter Deer CFBundlePackageType APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) CFBundleSignature ???? CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIViewControllerBasedStatusBarAppearance CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents ================================================ FILE: ios/Runner/Runner-Bridging-Header.h ================================================ #import "GeneratedPluginRegistrant.h" ================================================ FILE: ios/Runner.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 28CA5DF32ED2502B09C1B5B5 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 23E83CCE70FB1DC8687346B7 /* Pods_RunnerTests.framework */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; CEE44467A522F07798BADB83 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7482934083A212EB1C22ECB5 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 97C146E61CF9000F007C117D /* Project object */; proxyType = 1; remoteGlobalIDString = 97C146ED1CF9000F007C117D; remoteInfo = Runner; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 100B55C9A72F2B3AC421762C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 23E83CCE70FB1DC8687346B7 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 7482934083A212EB1C22ECB5 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 8ECD0D1A9223A475629BEA03 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; B0BE96799F1FD60D31676059 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; C0E376174B5BC02BA30538F1 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; DCFD1919E54EE05891C3C64B /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; F470C531425BBD2CC373C47A /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( CEE44467A522F07798BADB83 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; F70BD244216EA273847021A6 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 28CA5DF32ED2502B09C1B5B5 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 2549A0620386D486F08127CA /* Pods */ = { isa = PBXGroup; children = ( DCFD1919E54EE05891C3C64B /* Pods-Runner.debug.xcconfig */, 100B55C9A72F2B3AC421762C /* Pods-Runner.release.xcconfig */, B0BE96799F1FD60D31676059 /* Pods-Runner.profile.xcconfig */, F470C531425BBD2CC373C47A /* Pods-RunnerTests.debug.xcconfig */, C0E376174B5BC02BA30538F1 /* Pods-RunnerTests.release.xcconfig */, 8ECD0D1A9223A475629BEA03 /* Pods-RunnerTests.profile.xcconfig */, ); name = Pods; path = Pods; sourceTree = ""; }; 331C8082294A63A400263BE5 /* RunnerTests */ = { isa = PBXGroup; children = ( 331C807B294A618700263BE5 /* RunnerTests.swift */, ); path = RunnerTests; sourceTree = ""; }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, ); name = Flutter; sourceTree = ""; }; 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, 2549A0620386D486F08127CA /* Pods */, B512157F1DA3852EEAB02C39 /* Frameworks */, ); sourceTree = ""; }; 97C146EF1CF9000F007C117D /* Products */ = { isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, 331C8081294A63A400263BE5 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; }; 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, ); path = Runner; sourceTree = ""; }; B512157F1DA3852EEAB02C39 /* Frameworks */ = { isa = PBXGroup; children = ( 7482934083A212EB1C22ECB5 /* Pods_Runner.framework */, 23E83CCE70FB1DC8687346B7 /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 331C8080294A63A400263BE5 /* RunnerTests */ = { isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( F213357E348FD666B41F58DB /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, F70BD244216EA273847021A6 /* Frameworks */, ); buildRules = ( ); dependencies = ( 331C8086294A63A400263BE5 /* PBXTargetDependency */, ); name = RunnerTests; productName = RunnerTests; productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( D9FB969B7990732944954166 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 04431B192DCA9096E0D9DFE8 /* [CP] Copy Pods Resources */, ); buildRules = ( ); dependencies = ( ); name = Runner; productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 331C8080294A63A400263BE5 = { CreatedOnToolsVersion = 14.0; TestTargetID = 97C146ED1CF9000F007C117D; }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1100; }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 97C146E51CF9000F007C117D; productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, 331C8080294A63A400263BE5 /* RunnerTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 331C807F294A63A400263BE5 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 04431B192DCA9096E0D9DFE8 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; showEnvVarsInLog = 0; }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Run Script"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; D9FB969B7990732944954166 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; F213357E348FD666B41F58DB /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 331C807D294A63A400263BE5 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 97C146ED1CF9000F007C117D /* Runner */; targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 97C146FB1CF9000F007C117D /* Base */, ); name = Main.storyboard; sourceTree = ""; }; 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( 97C147001CF9000F007C117D /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Profile; }; 249021D4217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.weilu.flutterDeer; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = F470C531425BBD2CC373C47A /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.weilu.flutterDeer.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; }; name = Debug; }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = C0E376174B5BC02BA30538F1 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.weilu.flutterDeer.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; }; name = Release; }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 8ECD0D1A9223A475629BEA03 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.weilu.flutterDeer.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; }; name = Profile; }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Release; }; 97C147061CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.weilu.flutterDeer; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; 97C147071CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.weilu.flutterDeer; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 331C8088294A63A400263BE5 /* Debug */, 331C8089294A63A400263BE5 /* Release */, 331C808A294A63A400263BE5 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147031CF9000F007C117D /* Debug */, 97C147041CF9000F007C117D /* Release */, 249021D3217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147061CF9000F007C117D /* Debug */, 97C147071CF9000F007C117D /* Release */, 249021D4217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } ================================================ FILE: ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ PreviewsEnabled ================================================ FILE: ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme ================================================ ================================================ FILE: ios/Runner.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ PreviewsEnabled ================================================ FILE: ios/RunnerTests/RunnerTests.swift ================================================ import Flutter import UIKit import XCTest class RunnerTests: XCTestCase { func testExample() { // If you add code to the Runner application, consider adding tests here. // See https://developer.apple.com/documentation/xctest for more information about using XCTest. } } ================================================ FILE: l10n.yaml ================================================ # 文档:https://flutter.dev/docs/development/accessibility-and-localization/internationalization arb-dir: lib/l10n template-arb-file: intl_en.arb output-localization-file: deer_localizations.dart output-class: DeerLocalizations use-deferred-loading: false ================================================ FILE: lib/account/account_router.dart ================================================ import 'package:fluro/fluro.dart'; import 'package:flutter_deer/routers/i_router.dart'; import 'page/account_page.dart'; import 'page/account_record_list_page.dart'; import 'page/add_withdrawal_account_page.dart'; import 'page/bank_select_page.dart'; import 'page/city_select_page.dart'; import 'page/withdrawal_account_list_page.dart'; import 'page/withdrawal_account_page.dart'; import 'page/withdrawal_page.dart'; import 'page/withdrawal_password_page.dart'; import 'page/withdrawal_record_list_page.dart'; import 'page/withdrawal_result_page.dart'; class AccountRouter implements IRouterProvider{ static String accountPage = '/account'; static String accountRecordListPage = '/account/recordList'; static String addWithdrawalAccountPage = '/account/addWithdrawal'; static String bankSelectPage = '/account/bankSelect'; static String citySelectPage = '/account/citySelect'; static String withdrawalAccountListPage = '/account/withdrawalAccountList'; static String withdrawalAccountPage = '/account/withdrawalAccount'; static String withdrawalPage = '/account/withdrawal'; static String withdrawalPasswordPage = '/account/withdrawalPassword'; static String withdrawalRecordListPage = '/account/withdrawalRecordList'; static String withdrawalResultPage = '/account/withdrawalResult'; @override void initRouter(FluroRouter router) { router.define(accountPage, handler: Handler(handlerFunc: (_, __) => const AccountPage())); router.define(accountRecordListPage, handler: Handler(handlerFunc: (_, __) => const AccountRecordListPage())); router.define(addWithdrawalAccountPage, handler: Handler(handlerFunc: (_, __) => const AddWithdrawalAccountPage())); router.define(bankSelectPage, handler: Handler(handlerFunc: (_, Map> params) { final int type = int.parse(params['type']?.first ?? '0'); return BankSelectPage(type: type); })); router.define(citySelectPage, handler: Handler(handlerFunc: (_, __) => const CitySelectPage())); router.define(withdrawalAccountListPage, handler: Handler(handlerFunc: (_, __) => const WithdrawalAccountListPage())); router.define(withdrawalAccountPage, handler: Handler(handlerFunc: (_, __) => const WithdrawalAccountPage())); router.define(withdrawalPage, handler: Handler(handlerFunc: (_, __) => const WithdrawalPage())); router.define(withdrawalPasswordPage, handler: Handler(handlerFunc: (_, __) => const WithdrawalPasswordPage())); router.define(withdrawalRecordListPage, handler: Handler(handlerFunc: (_, __) => const WithdrawalRecordListPage())); router.define(withdrawalResultPage, handler: Handler(handlerFunc: (_, __) => const WithdrawalResultPage())); } } ================================================ FILE: lib/account/models/bank_entity.dart ================================================ import 'package:azlistview/azlistview.dart'; import 'package:flutter_deer/generated/json/bank_entity.g.dart'; import 'package:flutter_deer/generated/json/base/json_field.dart'; @JsonSerializable() class BankEntity with ISuspensionBean { BankEntity({this.id, this.bankName, this.firstLetter}); factory BankEntity.fromJson(Map json) => $BankEntityFromJson(json); Map toJson() => $BankEntityToJson(this); int? id; String? bankName; String? firstLetter; @override String getSuspensionTag() { return firstLetter ?? ''; } } ================================================ FILE: lib/account/models/city_entity.dart ================================================ import 'package:azlistview/azlistview.dart'; import 'package:flutter_deer/generated/json/base/json_field.dart'; import 'package:flutter_deer/generated/json/city_entity.g.dart'; @JsonSerializable() class CityEntity with ISuspensionBean { CityEntity(); factory CityEntity.fromJson(Map json) => $CityEntityFromJson(json); Map toJson() => $CityEntityToJson(this); late String name; late String cityCode; late String firstCharacter; @override String getSuspensionTag() { return firstCharacter; } } ================================================ FILE: lib/account/models/withdrawal_account_model.dart ================================================ class WithdrawalAccountModel { WithdrawalAccountModel(this.name, this.typeName, this.type, this.code); WithdrawalAccountModel.fromJsonMap(Map map): name = map['name'] as String, typeName = map['typeName'] as String, type = map['type'] as int, code = map['code'] as String; String name; String typeName; int type; String code; Map toJson() { final Map data = {}; data['name'] = name; data['typeName'] = typeName; data['type'] = type; data['code'] = code; return data; } } ================================================ FILE: lib/account/page/account_page.dart ================================================ import 'package:common_utils/common_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_deer/account/widgets/rise_number_text.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/util/image_utils.dart'; import 'package:flutter_deer/widgets/click_item.dart'; import 'package:flutter_deer/widgets/my_app_bar.dart'; import '../account_router.dart'; /// design/6店铺-账户/index.html#artboard2 class AccountPage extends StatefulWidget { const AccountPage({super.key}); @override _AccountPageState createState() => _AccountPageState(); } class _AccountPageState extends State { @override Widget build(BuildContext context) { return Scaffold( appBar: const MyAppBar( centerTitle: '资金管理', ), body: SingleChildScrollView( child: Column( children: [ Gaps.vGap5, _buildCard(), Gaps.vGap5, ClickItem( title: '提现', onTap: () => NavigatorUtils.push(context, AccountRouter.withdrawalPage), ), ClickItem( title: '提现记录', onTap: () => NavigatorUtils.push(context, AccountRouter.withdrawalRecordListPage), ), ClickItem( title: '提现密码', onTap: () => NavigatorUtils.push(context, AccountRouter.withdrawalPasswordPage), ), ], ), ) ); } Widget _buildCard() { return AspectRatio( aspectRatio: 1.85, child: Container( margin: const EdgeInsets.symmetric(horizontal: 6.0), padding: const EdgeInsets.all(6.0), decoration: BoxDecoration( image: DecorationImage( image: ImageUtils.getAssetImage('account/bg'), fit: BoxFit.fill, ), ), child: const Column( children: [ _AccountMoney( title: '当前余额(元)', money: '30.12', alignment: MainAxisAlignment.end, moneyTextStyle: TextStyle(color: Colors.white, fontSize: 32.0, fontWeight: FontWeight.bold, fontFamily: 'RobotoThin'), ), Expanded( child: Row( children: [ _AccountMoney(title: '累计结算金额', money: '20000'), _AccountMoney(title: '累计发放佣金', money: '0.02'), ], ), ), ], ), ), ); } } class _AccountMoney extends StatelessWidget { const _AccountMoney({ required this.title, required this.money, this.alignment, this.moneyTextStyle }); final String title; final String money; final MainAxisAlignment? alignment; final TextStyle? moneyTextStyle; @override Widget build(BuildContext context) { return Expanded( child: MergeSemantics( child: Column( mainAxisAlignment: alignment ?? MainAxisAlignment.center, children: [ /// 横向撑开Column,扩大语义区域 const SizedBox(width: double.infinity), Text(title, style: const TextStyle(color: Colours.text_disabled, fontSize: Dimens.font_sp12)), Gaps.vGap8, RiseNumberText( NumUtil.getDoubleByValueStr(money) ?? 0, style: moneyTextStyle ?? const TextStyle( color: Colours.text_disabled, fontSize: Dimens.font_sp14, fontWeight: FontWeight.bold, fontFamily: 'RobotoThin' ) ), ], ), ), ); } } ================================================ FILE: lib/account/page/account_record_list_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/util/theme_utils.dart'; import 'package:flutter_deer/widgets/my_app_bar.dart'; import '../../order/page/order_page.dart'; /// design/6店铺-账户/index.html#artboard1 class AccountRecordListPage extends StatefulWidget { const AccountRecordListPage({super.key}); @override _AccountRecordListPageState createState() => _AccountRecordListPageState(); } class _AccountRecordListPageState extends State { @override Widget build(BuildContext context) { return Scaffold( appBar: const MyAppBar( centerTitle: '账户流水', ), body: CustomScrollView( slivers: [ for (int i = 0; i < 8; i++) _buildGroup(i) ], ), ); } Widget _buildGroup(int index) { return SliverMainAxisGroup( slivers: [ SliverPersistentHeader( pinned: true, delegate: SliverAppBarDelegate( Container( alignment: Alignment.centerLeft, width: double.infinity, color: ThemeUtils.getStickyHeaderColor(context), padding: const EdgeInsets.only(left: 16.0), child: Text('2021/06/0${index + 1}'), ) , 34.0, ), ), SliverList( delegate: SliverChildBuilderDelegate((_, index) { return _buildItem(index); }, childCount: index + 1, ), ), ], ); } Widget _buildItem(int i) { return Container( height: 72.0, width: double.infinity, padding: const EdgeInsets.all(15.0), decoration: BoxDecoration( border: Border( bottom: Divider.createBorderSide(context, width: 0.8), ), ), child: IndexedSemantics( index: i, child: Stack( children: [ Text(i.isEven ? '采购订单结算营收' : '提现'), Positioned( top: 0.0, right: 0.0, child: Text(i.isEven ? '+10.00' : '-10.00', style: i.isEven ? TextStyle( color: Theme.of(context).colorScheme.error, fontWeight: FontWeight.bold, ) : TextStyles.textBold14, ), ), Positioned( bottom: 0.0, left: 0.0, child: Text(i.isEven ? '18:20:10' : '08:20:11', style: Theme.of(context).textTheme.titleSmall), ), Positioned( bottom: 0.0, right: 0.0, child: Text('余额:20.00', style: Theme.of(context).textTheme.titleSmall), ), ], ), ), ); } } ================================================ FILE: lib/account/page/add_withdrawal_account_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/account/account_router.dart'; import 'package:flutter_deer/account/models/bank_entity.dart'; import 'package:flutter_deer/account/models/city_entity.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/util/other_utils.dart'; import 'package:flutter_deer/util/theme_utils.dart'; import 'package:flutter_deer/widgets/my_app_bar.dart'; import 'package:flutter_deer/widgets/my_button.dart'; import 'package:flutter_deer/widgets/my_scroll_view.dart'; import 'package:flutter_deer/widgets/selected_item.dart'; import 'package:flutter_deer/widgets/text_field_item.dart'; /// design/6店铺-账户/index.html#artboard29 class AddWithdrawalAccountPage extends StatefulWidget { const AddWithdrawalAccountPage({super.key}); @override _AddWithdrawalAccountPageState createState() => _AddWithdrawalAccountPageState(); } class _AddWithdrawalAccountPageState extends State { bool _isWechat = false; String _accountType = '银行卡(对私账户)'; String _city = ''; String _bank = ''; String _bank1 = ''; @override Widget build(BuildContext context) { final TextStyle? style = Theme.of(context).textTheme.titleSmall?.copyWith(fontSize: Dimens.font_sp14); final List children = [ Gaps.vGap5, SelectedItem( title: '账号类型', content: _accountType, onTap: () => _showSelectAccountTypeDialog(), ), Visibility( maintainState: true, /// 是为了保留填写信息,其实就是Offstage,这里只是展示另一种方法。 visible: !_isWechat, child: Column( children: [ const TextFieldItem( title: '持 卡 人', hintText: '填写您的真实姓名', ), const TextFieldItem( title: '银行卡号', keyboardType: TextInputType.number, hintText: '填写银行卡号', ), SelectedItem( title: '开 户 地', content: _city.isEmpty ? '选择开户城市' : _city, style: _city.isEmpty ? style : null, onTap: () { NavigatorUtils.pushResult(context, AccountRouter.citySelectPage, (Object result) { setState(() { final CityEntity model = result as CityEntity; _city = model.name; }); }); }, ), SelectedItem( title: '银行名称', content: _bank.isEmpty ? '选择开户银行' : _bank, style: _bank.isEmpty ? style : null, onTap: () { NavigatorUtils.pushResult(context, '${AccountRouter.bankSelectPage}?type=0', (Object result) { setState(() { final BankEntity model = result as BankEntity; _bank = model.bankName.nullSafe; }); }); }, ), SelectedItem( title: '支行名称', content: _bank1.isEmpty ? '选择开户支行' : _bank1, style: _bank1.isEmpty ? style : null, onTap: () { NavigatorUtils.pushResult(context, '${AccountRouter.bankSelectPage}?type=1', (Object result) { setState(() { final BankEntity model = result as BankEntity; _bank1 = model.bankName.nullSafe; }); }); }, ), ], ), ), Padding( padding: const EdgeInsets.only(top: 8.0, left: 16.0), child: Text( _isWechat ? '绑定本机当前登录的微信号' : '绑定持卡人本人的银行卡', style: Theme.of(context).textTheme.titleSmall, ), ), ]; return Scaffold( resizeToAvoidBottomInset: false, appBar: const MyAppBar( title: '添加账号', ), body: MyScrollView( bottomButton: Padding( padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 8.0), child: MyButton( onPressed: () => NavigatorUtils.goBackWithParams(context, 'add'), text: '确定', ), ), children: children ), ); } void _dialogSelect(bool flag) { setState(() { _isWechat = flag; }); NavigatorUtils.goBack(context); } /// design/6店铺-账户/index.html#artboard30 void _showSelectAccountTypeDialog() { /// 关闭输入法,避免弹出 FocusManager.instance.primaryFocus?.unfocus(); showElasticDialog( context: context, builder: (BuildContext context) { const OutlinedBorder buttonShape = RoundedRectangleBorder(); final Widget content = Column( children: [ const Text( '账号类型', style: TextStyles.textBold18, ), Gaps.vGap16, Gaps.line, Expanded( child: TextButton( child: const Text('微信'), onPressed: () { _accountType = '微信'; _dialogSelect(true); }, ), ), Gaps.line, Expanded( child: TextButton( child: const Text('银行卡(对私账户)'), onPressed: () { _accountType = '银行卡(对私账户)'; _dialogSelect(false); }, ), ), Gaps.line, Expanded( child: TextButton( child: const Text('银行卡(对公账户)'), onPressed: () { _accountType = '银行卡(对公账户)'; _dialogSelect(false); }, ), ), ], ); final Widget decoration = Container( decoration: BoxDecoration( color: context.dialogBackgroundColor, borderRadius: BorderRadius.circular(8.0), ), width: 270.0, height: 190.0, padding: const EdgeInsets.only(top: 24.0), child: TextButtonTheme( data: TextButtonThemeData( style: TextButton.styleFrom( // 文字颜色 foregroundColor: Theme.of(context).primaryColor, // 按钮大小 minimumSize: Size.infinite, // 修改默认圆角 shape: buttonShape, ), ), child: content, ), ); return Material( type: MaterialType.transparency, child: Center( child: decoration, ), ); }, ); } } ================================================ FILE: lib/account/page/bank_select_page.dart ================================================ import 'dart:convert'; import 'package:azlistview/azlistview.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_deer/account/models/bank_entity.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/util/other_utils.dart'; import 'package:flutter_deer/util/theme_utils.dart'; import 'package:flutter_deer/widgets/load_image.dart'; import 'package:flutter_deer/widgets/my_app_bar.dart'; /// design/6店铺-账户/index.html#artboard33 class BankSelectPage extends StatefulWidget { const BankSelectPage({super.key, this.type = 0}); final int type; @override _BankSelectPageState createState() => _BankSelectPageState(); } class _BankSelectPageState extends State { final List _bankList = []; final List _bankNameList = [ '工商银行', '建设银行', '中国银行', '农业银行', '招商银行', '交通银行', '中信银行', '民生银行', '兴业银行', '浦发银行' ]; final List _bankLogoList = [ 'gongshang', 'jianhang', 'zhonghang', 'nonghang', 'zhaohang', 'jiaohang', 'zhongxin', 'minsheng', 'xingye', 'pufa' ]; List _indexBarData = []; @override void initState() { super.initState(); _loadData(); } void _loadData() { // 获取城市列表 rootBundle.loadString(widget.type == 0 ? 'assets/data/bank.json' : 'assets/data/bank_2.json').then((String value) { final List list = json.decode(value) as List; list.forEach(_addBank); SuspensionUtil.sortListBySuspensionTag(_bankList); SuspensionUtil.setShowSuspensionStatus(_bankList); _indexBarData = _bankList.map((BankEntity e) { if (e.isShowSuspension) { return e.firstLetter.nullSafe; } else { return ''; } }).where((String element) => element.isNotEmpty).toList(); if (widget.type == 0) { // add header. _bankList.insert(0, BankEntity(firstLetter: '常用')); _indexBarData.insert(0, '常用'); } setState(() { }); }); } void _addBank(dynamic value) { _bankList.add(BankEntity.fromJson(value as Map)); } @override Widget build(BuildContext context) { return Scaffold( appBar: MyAppBar( title: widget.type == 0 ? '开户银行' : '选择支行', ), body: SafeArea( child: AzListView( data: _bankList, itemCount: _bankList.length, itemBuilder: (_, int index) { if (index == 0 && widget.type == 0) { return _buildHeader(); } return _buildListItem(index); }, indexBarItemHeight: 25, indexBarData: _indexBarData, indexBarOptions: IndexBarOptions( needRebuild: true, indexHintWidth: 96, indexHintHeight: 96, indexHintTextStyle: const TextStyle(fontSize: 26.0, color: Colors.white), textStyle: Theme.of(context).textTheme.titleSmall!, downTextStyle: context.isDark ? TextStyles.textSize12 : const TextStyle(fontSize: 12.0, color: Colors.black), ), ), ), ); } Widget _buildHeader() { return SizedBox( height: 430, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.only(top: 5.0, bottom: 5.0, left: 16.0), child: Text('常用', style: Theme.of(context).textTheme.titleSmall), ), Expanded( child: ListView.builder( physics: const NeverScrollableScrollPhysics(), itemExtent: 40.0, itemCount: _bankNameList.length, itemBuilder: (_, int index) { return InkWell( onTap: () => NavigatorUtils.goBackWithParams(context, BankEntity(id: 0, bankName: _bankNameList[index], firstLetter: '')), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Row( children: [ LoadAssetImage('account/${_bankLogoList[index]}',width: 24.0), Gaps.hGap8, Text(_bankNameList[index]), ], ), ), ); } ), ) ], ), ); } Widget _buildListItem(int index) { final BankEntity model = _bankList[index]; return InkWell( onTap: () => NavigatorUtils.goBackWithParams(context, model), child: Container( padding: const EdgeInsets.only(left: 16.0, right: 34.0), height: 40.0, child: Container( decoration: BoxDecoration( border: (model.isShowSuspension && model.id != 17749) ? Border( top: Divider.createBorderSide(context, width: 0.6), ) : null ), child: Row( children: [ Opacity( opacity: model.isShowSuspension ? 1 : 0, child: SizedBox( width: 28.0, child: Text(model.firstLetter.nullSafe), ) ), Expanded( child: Text(model.bankName.nullSafe), ) ], ), ), ), ); } } ================================================ FILE: lib/account/page/city_select_page.dart ================================================ import 'dart:convert'; import 'package:azlistview/azlistview.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_deer/account/models/city_entity.dart'; import 'package:flutter_deer/res/constant.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/util/theme_utils.dart'; import 'package:flutter_deer/widgets/my_app_bar.dart'; /// design/6店铺-账户/index.html#artboard34 class CitySelectPage extends StatefulWidget { const CitySelectPage({super.key}); @override _CitySelectPageState createState() => _CitySelectPageState(); } class _CitySelectPageState extends State { final List _cityList = []; List _indexBarData = []; @override void initState() { super.initState(); _loadData(); } Future _loadData() async { // 获取城市列表 // loadString源码中有对json大小进行判断,json过大会使用compute处理,集成测试无法使用compute,所以这里修改源码单独判断处理。 String jsonStr; if (Constant.isDriverTest) { jsonStr = await _loadString('assets/data/city.json'); } else { jsonStr = await rootBundle.loadString('assets/data/city.json'); } final List list = json.decode(jsonStr) as List; list.forEach(_addCity); SuspensionUtil.setShowSuspensionStatus(_cityList); _indexBarData = _cityList.map((CityEntity e) { if (e.isShowSuspension) { return e.firstCharacter; } else { return ''; } }).where((String element) => element.isNotEmpty).toList(); setState(() { }); } void _addCity(dynamic value) { _cityList.add(CityEntity.fromJson(value as Map)); } /// rootBundle.loadString源码修改 Future _loadString(String key) async { final ByteData data = await rootBundle.load(key); return utf8.decode(data.buffer.asUint8List()); } @override Widget build(BuildContext context) { return Scaffold( appBar: const MyAppBar( title: '开户地点', ), body: SafeArea( child: AzListView( data: _cityList, itemCount: _cityList.length, itemBuilder: (_, int index) => _buildListItem(index), indexBarItemHeight: 18, indexBarData: _indexBarData, indexBarOptions: IndexBarOptions( needRebuild: true, indexHintWidth: 96, indexHintHeight: 96, indexHintTextStyle: const TextStyle(fontSize: 26.0, color: Colors.white), textStyle: Theme.of(context).textTheme.titleSmall!, downTextStyle: context.isDark ? TextStyles.textSize12 : const TextStyle(fontSize: 12.0, color: Colors.black), ), ), ), ); } Widget _buildListItem(int index) { final CityEntity model = _cityList[index]; return InkWell( onTap: () => NavigatorUtils.goBackWithParams(context, model), child: Container( padding: const EdgeInsets.only(left: 16.0, right: 34.0), height: 40.0, child: Container( decoration: BoxDecoration( border: (model.isShowSuspension && model.cityCode != '0483') ? Border( top: Divider.createBorderSide(context, width: 0.6), ) : null ), child: Row( children: [ Opacity( opacity: model.isShowSuspension ? 1 : 0, child: SizedBox( width: 28.0, child: Text(model.firstCharacter), ) ), Expanded( child: Text(model.name), ) ], ), ), ), ); } } ================================================ FILE: lib/account/page/withdrawal_account_list_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/account/models/withdrawal_account_model.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/widgets/load_image.dart'; import 'package:flutter_deer/widgets/my_app_bar.dart'; import '../account_router.dart'; /// design/6店铺-账户/index.html#artboard7 class WithdrawalAccountListPage extends StatefulWidget { const WithdrawalAccountListPage({super.key}); @override _WithdrawalAccountListPageState createState() => _WithdrawalAccountListPageState(); } class _WithdrawalAccountListPageState extends State { final int _selectIndex = 0; final List _list = []; @override void initState() { super.initState(); _list.clear(); _list.add(WithdrawalAccountModel('尾号5236 李艺', '工商银行', 0, '123')); _list.add(WithdrawalAccountModel('唯鹿', '微信', 1, '')); } @override Widget build(BuildContext context) { return Scaffold( appBar: MyAppBar( centerTitle: '选择账号', actionName: '添加', onPressed: () => NavigatorUtils.push(context, AccountRouter.addWithdrawalAccountPage) ), body: ListView.separated( itemCount: _list.length, separatorBuilder: (_, index) => const Divider(height: 0.6), itemBuilder: (_, index) => _buildItem(index), ), ); } Widget _buildItem(int index) { return InkWell( onTap: () => NavigatorUtils.goBackWithParams(context, _list[index]), child: Container( padding: const EdgeInsets.symmetric(horizontal: 16.0), width: double.infinity, height: 74.0, alignment: Alignment.center, child: Row( children: [ LoadAssetImage(_list[index].type == 0 ? 'account/yhk' : 'account/wechat', width: 24.0), Gaps.hGap16, Expanded( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(_list[index].typeName), Gaps.vGap8, Text(_list[index].name, style: TextStyles.textSize12), ], ), ), Visibility( visible: _selectIndex == index, child: const LoadAssetImage( 'account/selected', height: 24.0, width: 24.0, ), ) ], ), ), ); } } ================================================ FILE: lib/account/page/withdrawal_account_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/account/models/withdrawal_account_model.dart'; import 'package:flutter_deer/account/widgets/withdrawal_account_item.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/widgets/my_app_bar.dart'; import 'package:flutter_deer/widgets/my_button.dart'; import 'package:flutter_deer/widgets/state_layout.dart'; import '../account_router.dart'; /// design/6店铺-账户/index.html#artboard26 class WithdrawalAccountPage extends StatefulWidget { const WithdrawalAccountPage({super.key}); @override _WithdrawalAccountPageState createState() => _WithdrawalAccountPageState(); } class _WithdrawalAccountPageState extends State { final List _list = []; final GlobalKey _listKey = GlobalKey(); final Duration _kDuration = const Duration(milliseconds: 300); @override void initState() { super.initState(); _list.clear(); _list.add(WithdrawalAccountModel('唯鹿', '微信', 1, '')); _list.add(WithdrawalAccountModel('李*', '工商银行', 0, '**** **** **** 5236')); _list.add(WithdrawalAccountModel('李*', '工商银行', 0, '**** **** **** 2165')); } @override Widget build(BuildContext context) { return Scaffold( appBar: MyAppBar( centerTitle: '提现账号', actionName: '添加', onPressed: () { NavigatorUtils.pushResult(context, AccountRouter.addWithdrawalAccountPage, (result) { _insertItem(0); }); } ), body: _list.isEmpty ? const StateLayout(type: StateType.account) : AnimatedList( key: _listKey, padding: const EdgeInsets.only(top: 8.0), initialItemCount: _list.length, itemBuilder: (_, index, animation) => sizeItem(_list[index], index, animation), ), ); } Widget sizeItem(WithdrawalAccountModel data, int index, Animation animation) { /// item插入、移除动画 return SizeTransition( axisAlignment: 1.0, sizeFactor: animation, child: WithdrawalAccountItem( key: ObjectKey(data), /// 这里注意必须添加key,原因见: https://weilu.blog.csdn.net/article/details/104745624 data: data, onLongPress: () => _showDeleteBottomSheet(index), ), ); } void _removeItem(int index) { /// 先移除数据 final WithdrawalAccountModel item = _list.removeAt(index); _listKey.currentState?.removeItem( index, (_, animation) => sizeItem(item, 0, animation), /// 构建移除Widget duration: _kDuration, ); if (_list.isEmpty) { Future.delayed(_kDuration, () { if (mounted) { setState(() { }); } }); } } void _insertItem(int index) { final WithdrawalAccountModel item = WithdrawalAccountModel('weilu_deer', '微信', 1, ''); _list.insert(index, item); if (_list.length == 1) { setState(() { }); } else { _listKey.currentState?.insertItem( index, duration: _kDuration, ); } } void _showDeleteBottomSheet(int index) { showModalBottomSheet( context: context, builder: (BuildContext context) { return Material( child: SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox( height: 52.0, child: Center( child: Text( '是否确认解绑,防止错误操作', style: TextStyles.textSize16, ), ), ), Gaps.line, MyButton( minHeight: 54.0, textColor: Theme.of(context).colorScheme.error, text: '确认解绑', backgroundColor: Colors.transparent, onPressed: () { _removeItem(index); NavigatorUtils.goBack(context); }, ), Gaps.line, MyButton( minHeight: 54.0, textColor: Colours.text_gray, text: '取消', backgroundColor: Colors.transparent, onPressed: () { NavigatorUtils.goBack(context); }, ), ], ), ), ); }, ); } } ================================================ FILE: lib/account/page/withdrawal_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/account/models/withdrawal_account_model.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/util/input_formatter/number_text_input_formatter.dart'; import 'package:flutter_deer/util/theme_utils.dart'; import 'package:flutter_deer/widgets/load_image.dart'; import 'package:flutter_deer/widgets/my_app_bar.dart'; import 'package:flutter_deer/widgets/my_button.dart'; import 'package:flutter_deer/widgets/my_scroll_view.dart'; import '../account_router.dart'; /// design/6店铺-账户/index.html#artboard3 class WithdrawalPage extends StatefulWidget { const WithdrawalPage({super.key}); @override _WithdrawalPageState createState() => _WithdrawalPageState(); } class _WithdrawalPageState extends State { final TextEditingController _controller = TextEditingController(); int _withdrawalType = 0; bool _clickable = false; WithdrawalAccountModel _data = WithdrawalAccountModel('尾号5236 李艺', '工商银行', 0, '123'); @override void initState() { super.initState(); _controller.addListener(_verify); } @override void dispose() { _controller.removeListener(_verify); _controller.dispose(); super.dispose(); } void _verify() { final price = _controller.text; if (price.isEmpty || double.parse(price) < 1) { setState(() { _clickable = false; }); return; } setState(() { _clickable = true; }); } @override Widget build(BuildContext context) { return PopScope( onPopInvoked: (_) { /// 拦截返回,关闭键盘,否则会造成上一页面短暂的组件溢出 FocusManager.instance.primaryFocus?.unfocus(); }, child: Scaffold( appBar: const MyAppBar( title: '提现', ), body: MyScrollView( padding: const EdgeInsets.symmetric(horizontal: 16.0), children: [ Gaps.vGap5, InkWell( onTap: () { NavigatorUtils.pushResult(context, AccountRouter.withdrawalAccountListPage, (result) { setState(() { _data = result as WithdrawalAccountModel; }); }); }, child: Container( width: double.infinity, height: 74.0, alignment: Alignment.center, child: Row( children: [ LoadAssetImage(_data.type == 0 ? 'account/yhk' : 'account/wechat', width: 24.0), Gaps.hGap16, Expanded( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(_data.typeName), Gaps.vGap8, Text(_data.name, style: Theme.of(context).textTheme.titleSmall), ], ), ), Images.arrowRight ], ), ), ), Gaps.vGap16, const Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('提现金额', style: TextStyles.textBold14), Text('单笔2万,单日2万', style: TextStyle(fontSize: Dimens.font_sp12, color: Colours.orange)) ], ), Gaps.vGap8, Row( children: [ Container( width: 15.0, height: 40.0, padding: const EdgeInsets.symmetric(vertical: 8.0), child: LoadAssetImage('account/rmb', color: ThemeUtils.getIconColor(context),), ), Gaps.hGap8, Expanded( child: TextField( maxLength: 10, controller: _controller, keyboardType: const TextInputType.numberWithOptions(decimal: true), inputFormatters: [UsNumberTextInputFormatter()], style: const TextStyle( fontSize: 32.0, fontWeight: FontWeight.bold, ), decoration: const InputDecoration( contentPadding: EdgeInsets.only(bottom: 8.0), hintStyle: TextStyle( fontSize: Dimens.font_sp14, fontWeight: FontWeight.normal, color: Colours.text_gray_c, ), hintText: '不能少于1元', counterText: '', border: InputBorder.none, ), ), ), ], ), Gaps.line, Gaps.vGap8, Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('最多可提现70元', style: Theme.of(context).textTheme.titleSmall), GestureDetector( onTap: () { _controller.text = '70'; }, child: SizedBox( height: 48.0, child: Text('全部提现', style: TextStyle( fontSize: Dimens.font_sp12, color: Theme.of(context).primaryColor, )), ) ) ], ), const Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('转出方式', style: TextStyles.textBold14), LoadAssetImage('account/sm', width: 16.0) ], ), _buildWithdrawalType(0), Gaps.line, _buildWithdrawalType(1), Gaps.vGap24, MyButton( key: const Key('提现'), onPressed: _clickable ? () { NavigatorUtils.push(context, AccountRouter.withdrawalResultPage); } : null, text: '提现', ), ], ), ), ); } Widget _buildWithdrawalType(int type) { return InkWell( onTap: () { setState(() { _withdrawalType = type; }); }, child: SizedBox( width: double.infinity, height: 74.0, child: Stack( children: [ Positioned( top: 18.0, left: 0.0, child: LoadAssetImage(_withdrawalType == type ? 'account/txxz' : 'account/txwxz', width: 16.0), ), Positioned( top: 16.0, left: 24.0, right: 0.0, child: Text(type == 0 ? '快速到账' : '普通到账'), ), Positioned( bottom: 16.0, left: 24.0, right: 0.0, child: RichText( text: type == 0 ? TextSpan( text: '手续费按', style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: Dimens.font_sp12), children: const [ TextSpan(text: '0.3%', style: TextStyle(color: Colours.orange)), TextSpan(text: '收取'), ], ) : TextSpan( text: '预计', style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: Dimens.font_sp12), children: const [ TextSpan(text: 'T+1天到账(免手续费,T为工作日)', style: TextStyle(color: Colours.orange)), ], ), ) ), ], ), ), ); } } ================================================ FILE: lib/account/page/withdrawal_password_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/account/widgets/sms_verify_dialog.dart'; import 'package:flutter_deer/account/widgets/withdrawal_password_setting.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/util/other_utils.dart'; import 'package:flutter_deer/widgets/base_dialog.dart'; import 'package:flutter_deer/widgets/click_item.dart'; import 'package:flutter_deer/widgets/my_app_bar.dart'; /// design/6店铺-账户/index.html#artboard20 class WithdrawalPasswordPage extends StatefulWidget { const WithdrawalPasswordPage({super.key}); @override _WithdrawalPasswordPageState createState() => _WithdrawalPasswordPageState(); } class _WithdrawalPasswordPageState extends State { @override Widget build(BuildContext context) { return Scaffold( appBar: const MyAppBar( centerTitle: '提现密码', ), body: Column( children: [ Gaps.vGap5, ClickItem( title: '修改密码', onTap: () { showModalBottomSheet( context: context, /// 禁止拖动关闭 enableDrag: false, /// 使用true则高度不受16分之9的最高限制 isScrollControlled: true, builder: (_) => const WithdrawalPasswordSetting() ); } ), ClickItem( title: '忘记密码', onTap: () => _showHintDialog() ), ], ), ); } void _showHintDialog() { showElasticDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return BaseDialog( hiddenTitle: true, child: const Padding( padding: EdgeInsets.symmetric(horizontal: 16.0), child: Text('为了您的账户安全需先进行短信验证并设置提现密码。', textAlign: TextAlign.center), ), onPressed: () { NavigatorUtils.goBack(context); _showVerifyDialog(); }, ); }, ); } void _showVerifyDialog() { showDialog( context: context, barrierDismissible: false, builder: (_) => const SMSVerifyDialog() ); } } ================================================ FILE: lib/account/page/withdrawal_record_list_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/util/theme_utils.dart'; import 'package:flutter_deer/widgets/my_app_bar.dart'; import '../../order/page/order_page.dart'; /// design/6店铺-账户/index.html#artboard19 class WithdrawalRecordListPage extends StatefulWidget { const WithdrawalRecordListPage({super.key}); @override _WithdrawalRecordListPageState createState() => _WithdrawalRecordListPageState(); } class _WithdrawalRecordListPageState extends State { @override Widget build(BuildContext context) { return Scaffold( appBar: const MyAppBar( title: '提现记录', ), body: CustomScrollView( slivers: [ for (int i = 0; i < 8; i++) _buildGroup(i) ], ), ); } Widget _buildGroup(int index) { return SliverMainAxisGroup( slivers: [ SliverPersistentHeader( pinned: true, delegate: SliverAppBarDelegate( Container( alignment: Alignment.centerLeft, width: double.infinity, color: ThemeUtils.getStickyHeaderColor(context), padding: const EdgeInsets.only(left: 16.0), child: Text('2021/06/0${index + 1}'), ) , 34.0, ), ), SliverList( delegate: SliverChildBuilderDelegate((_, index) { return _buildItem(index); }, childCount: index + 1, ), ), ], ); } Widget _buildItem(int i) { final Widget content = Stack( children: [ Text(i.isEven ? '微信(唯鹿)' : '工商(尾号:4562 李一)'), const Positioned( top: 0.0, right: 0.0, child: Text('-10.00', style: TextStyles.textBold14), ), Positioned( bottom: 0.0, left: 0.0, child: Text(i.isEven ? '12:40:20' : '12:50:20', style: Theme.of(context).textTheme.titleSmall), ), Positioned( bottom: 0.0, right: 0.0, child: Text( i.isEven ? '审核失败' : '待审核', style: i.isEven ? TextStyle( fontSize: Dimens.font_sp12, color: Theme.of(context).colorScheme.error, ) : const TextStyle( fontSize: Dimens.font_sp12, color: Colours.orange, ), ), ), ], ); return MergeSemantics( child: Container( height: 72.0, width: double.infinity, padding: const EdgeInsets.all(15.0), decoration: BoxDecoration( border: Border( bottom: Divider.createBorderSide(context, width: 0.8), ), ), child: content, ), ); } } ================================================ FILE: lib/account/page/withdrawal_result_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/widgets/load_image.dart'; import 'package:flutter_deer/widgets/my_app_bar.dart'; import 'package:flutter_deer/widgets/my_button.dart'; /// design/6店铺-账户/index.html#artboard5 class WithdrawalResultPage extends StatefulWidget { const WithdrawalResultPage({super.key}); @override _WithdrawalResultPageState createState() => _WithdrawalResultPageState(); } class _WithdrawalResultPageState extends State { @override Widget build(BuildContext context) { return Scaffold( appBar: const MyAppBar( title: '提现结果', ), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ Gaps.vGap50, const LoadAssetImage('account/sqsb', width: 80.0, height: 80.0, ), Gaps.vGap12, const Text( '提现申请提交失败,请重新提交', style: TextStyles.textSize16, ), Gaps.vGap8, Text( '2021-02-21 15:20:10', style: Theme.of(context).textTheme.titleSmall, ), Gaps.vGap8, Text( '5秒后返回提现页面', style: Theme.of(context).textTheme.titleSmall, ), Gaps.vGap24, MyButton( onPressed: () => NavigatorUtils.goBack(context), text: '返回', ) ], ), ), ); } } ================================================ FILE: lib/account/widgets/rise_number_text.dart ================================================ import 'package:flutter/material.dart'; // 简易实现数字滚动效果 class RiseNumberText extends StatefulWidget { const RiseNumberText(this.number,{ super.key, this.style, this.duration = 1200 }); final num number; final TextStyle? style; final int duration; @override _RiseNumberTextState createState() => _RiseNumberTextState(); } class _RiseNumberTextState extends State with SingleTickerProviderStateMixin { late Animation _animation; late AnimationController _controller; num _fromNumber = 0; @override void initState() { super.initState(); _controller = AnimationController(duration: Duration(milliseconds: widget.duration), vsync: this); final curve = CurvedAnimation(parent: _controller, curve: Curves.linear); _animation = Tween(begin: 0, end: 1).animate(curve); _controller.forward(from: 0); } @override void didUpdateWidget(RiseNumberText oldWidget) { super.didUpdateWidget(oldWidget); // 数据变化时执行动画 if (oldWidget.number != widget.number) { _fromNumber = oldWidget.number; _controller.forward(from: 0); } } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _animation, builder: (_, __) { // 数字默认从0增长。数据变化时,由之前数字为基础变化。 return Text( (_fromNumber + (_animation.value * (widget.number - _fromNumber))).toStringAsFixed(2), style: widget.style, ); }, ); } } ================================================ FILE: lib/account/widgets/sms_verify_dialog.dart ================================================ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/util/device_utils.dart'; import 'package:flutter_deer/util/screen_utils.dart'; import 'package:flutter_deer/util/theme_utils.dart'; import 'package:flutter_deer/util/toast_utils.dart'; import 'package:flutter_deer/widgets/load_image.dart'; import 'package:flutter_deer/widgets/my_button.dart'; /// design/6店铺-账户/index.html#artboard23 /// 骚操作:借腹生子 class SMSVerifyDialog extends StatefulWidget { const SMSVerifyDialog({super.key}); @override _SMSVerifyDialogState createState() => _SMSVerifyDialogState(); } class _SMSVerifyDialogState extends State { /// 倒计时秒数 final int _second = 60; /// 当前秒数 late int _currentSecond; StreamSubscription? _subscription; bool _clickable = true; final FocusNode _focusNode = FocusNode(); final TextEditingController _controller = TextEditingController(); final List _codeList = ['', '', '', '', '', '']; @override void initState() { super.initState(); // _controller.addListener(() { // if (_controller.text.isEmpty) { // return; // } // // 点击EditableText将光标放置在后端 // _controller.value = TextEditingValue( // text: _controller.text, // selection: TextSelection.collapsed(offset: _controller.text.length), // ); // }); } @override void dispose() { _subscription?.cancel(); _focusNode.dispose(); _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final Color textColor = Theme.of(context).primaryColor; final Widget child = Column( children: [ Stack( children: [ Container( width: double.infinity, height: 56.0, alignment: Alignment.center, padding: const EdgeInsets.only(top: 22.0), child: const Text( '短信验证', style: TextStyles.textBold18, ), ), Positioned( top: 0.0, right: 0.0, child: Semantics( label: '关闭', child: GestureDetector( onTap: () => NavigatorUtils.goBack(context), child: const Padding( padding: EdgeInsets.only(top: 16.0, right: 16.0), child: LoadAssetImage('goods/icon_dialog_close', width: 16.0, key: Key('dialog_close'),), ), ), ), ) ], ), const Padding( padding: EdgeInsets.symmetric(horizontal: 16.0), child: Text('本次操作需短信验证,验证码会发送至您的注册手机 15000000000', textAlign: TextAlign.center), ), Gaps.vGap16, Expanded( child: Stack( children: [ EditableText( controller: _controller, focusNode: _focusNode, keyboardType: TextInputType.number, /// 指定键盘外观,仅iOS有效 keyboardAppearance: Brightness.dark, /// 只能为数字、6位 inputFormatters: [FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(6)], // 隐藏光标与字体颜色,达到隐藏输入框的目的 cursorColor: Colors.transparent, cursorWidth: 0, textAlign: TextAlign.center, backgroundCursorColor: Colors.transparent, style: const TextStyle(color: Colors.transparent, fontSize: Dimens.font_sp18), onChanged: (v) { for (var i = 0; i < _codeList.length; i ++) { if (i < v.length) { _codeList[i] = v.substring(i, i + 1); } else { _codeList[i] = ''; } } if (v.length == _codeList.length) { Toast.show('验证码:${_controller.text}'); for (var i = 0; i < _codeList.length; i ++) { _codeList[i] = ''; } /// https://github.com/flutter/flutter/issues/47191 /// https://github.com/flutter/flutter/pull/57264 /// 1.19.0已修复,小于此版本需添加addPostFrameCallback处理,否则会错误触发onChanged。 SchedulerBinding.instance.addPostFrameCallback((_) { _controller.clear(); }); } setState(() {}); }, ), Semantics( label: '点击输入', child: GestureDetector( onTap: () { /// 一直怼,会有概率造成键盘抖动,加一个键盘时候弹出判断 if (MediaQuery.of(context).viewInsets.bottom < 10) { final focusScope = FocusScope.of(context); focusScope.unfocus(); Future.delayed(Duration.zero, () => focusScope.requestFocus(_focusNode)); } }, child: Container( key: const Key('vcode'), color: Colors.transparent, padding: const EdgeInsets.symmetric(horizontal: 24.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: List.generate(_codeList.length, (i) => _buildInputWidget(i, textColor)), ), ), ), ), ], ), ), Gaps.vGap16, Gaps.line, MyButton( text: _clickable ? '获取验证码' : '已发送($_currentSecond s)', textColor: textColor, disabledTextColor: Colours.text_gray, backgroundColor: Colors.transparent, disabledBackgroundColor: Colors.transparent, onPressed: _clickable ? () { setState(() { _currentSecond = _second; _clickable = false; }); _subscription = Stream.periodic(const Duration(seconds: 1), (i) => i).take(_second).listen((i) { setState(() { _currentSecond = _second - i - 1; _clickable = _currentSecond < 1; }); }); }: null, ), ], ); Widget body = Container( decoration: BoxDecoration( color: context.dialogBackgroundColor, borderRadius: BorderRadius.circular(8.0), ), width: 280.0, height: 210.0, child: child, ); /// 判断原因见BaseDialog注释 if (Device.getAndroidSdkInt() >= 30) { body = Container( alignment: Alignment.center, height: context.height - MediaQuery.of(context).viewInsets.bottom, child: body, ); } else { body = AnimatedContainer( alignment: Alignment.center, height: context.height - MediaQuery.of(context).viewInsets.bottom, duration: const Duration(milliseconds: 120), curve: Curves.easeInCubic, child: body, ); } return Scaffold(//创建透明层 backgroundColor: Colors.transparent,//透明类型 body: body, ); } Widget _buildInputWidget(int p, Color textColor) { return Container( height: 32.0, width: 32.0, alignment: Alignment.center, decoration: BoxDecoration( border: Border.all(width: 0.6, color: _codeList[p].isNotEmpty ? textColor : Colours.text_gray_c), borderRadius: BorderRadius.circular(4.0), ), child: Text(_codeList[p], style: const TextStyle(fontSize: Dimens.font_sp18),) ); } } ================================================ FILE: lib/account/widgets/withdrawal_account_item.dart ================================================ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_deer/account/models/withdrawal_account_model.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/util/theme_utils.dart'; import 'package:flutter_deer/util/toast_utils.dart'; import 'package:flutter_deer/widgets/load_image.dart'; class WithdrawalAccountItem extends StatefulWidget { const WithdrawalAccountItem({ super.key, required this.data, required this.onLongPress, }); final WithdrawalAccountModel data; final GestureLongPressCallback onLongPress; @override _WithdrawalAccountItemState createState() => _WithdrawalAccountItemState(); } /// 3D翻转动画 https://medium.com/flutterpub/flutter-flip-card-animation-with-3d-effect-4284af04f5a class _WithdrawalAccountItemState extends State with SingleTickerProviderStateMixin { late AnimationController _animationController; late Animation _animation; AnimationStatus _animationStatus = AnimationStatus.dismissed; @override void initState() { super.initState(); _animationController = AnimationController(vsync: this, duration: const Duration(seconds: 1)); _animation = Tween(end: 1.0, begin: 0).animate(_animationController) ..addStatusListener((status) { _animationStatus = status; }); } @override void dispose() { _animationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final Widget front = Stack( children: [ Positioned( top: 25.0, left: 24.0, child: Container( height: 40.0, width: 40.0, padding: const EdgeInsets.all(8.0), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20.0), ), child: LoadAssetImage(widget.data.type == 1 ? 'account/wechat' : 'account/yhk'), ), ), Positioned( top: 22.0, left: 72.0, child: Text(widget.data.typeName, style: const TextStyle(color: Colors.white, fontSize: Dimens.font_sp18)), ), Positioned( top: 48.0, left: 72.0, child: Text(widget.data.name, style: const TextStyle(color: Colors.white, fontSize: Dimens.font_sp12)), ), Positioned( bottom: 24.0, left: 72.0, child: Text(widget.data.code, style: const TextStyle(color: Colors.white, fontSize: Dimens.font_sp18, letterSpacing: 1.0)), ), ], ); final Widget back = Center( child: GestureDetector( onTap: () => Toast.show('提现'), child: Container( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 5.0), decoration: BoxDecoration( border: Border.all(width: 0.6, color: Colors.white), borderRadius: BorderRadius.circular(4.0), ), child: Transform( // 文字翻转,保证文字的方向 alignment: FractionalOffset.center, transform: Matrix4.identity() ..setEntry(3, 2, 0.002) ..rotateX(pi), child: const Text('提现', style: TextStyle(color: Colors.white, fontSize: Dimens.font_sp16) ), ), ), ), ); return Container( height: 152.0, padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 22.0), child: AnimatedBuilder( animation: _animation, builder: (_, child) { return Transform( alignment: FractionalOffset.center, transform: Matrix4.identity() ..setEntry(3, 2, 0.002) ..rotateX(pi * _animation.value), child: AccountCard( type: widget.data.type, child: InkWell( // 长按删除账号 onLongPress: () => widget.onLongPress(), onTap: () { /// 避免动画中重复执行 if (_animationStatus == AnimationStatus.dismissed) { _animationController.forward(); } if (_animationStatus == AnimationStatus.completed) { _animationController.reverse(); } }, child: _animation.value <= 0.5 ? front : back, ), ), ); }, ), ); } } class AccountCard extends StatefulWidget { const AccountCard({ super.key, required this.child, required this.type }); final Widget child; final int type; @override _AccountCardState createState() => _AccountCardState(); } class _AccountCardState extends State { @override Widget build(BuildContext context) { /// 添加RepaintBoundary原因见docs/Web问题汇总.md return RepaintBoundary( child: DecoratedBox( decoration: BoxDecoration( borderRadius: BorderRadius.circular(8.0), boxShadow: context.isDark ? null : [ BoxShadow( color: widget.type == 1 ? const Color(0x804EE07A) : Colours.shadow_blue, offset: const Offset(0.0, 2.0), blurRadius: 8.0, ), ], gradient: LinearGradient( colors: widget.type == 1 ? const [Color(0xFF40E6AE), Color(0xFF2DE062)] : const [Color(0xFF57C4FA), Colours.app_main], ), ), child: widget.child, ), ); } } ================================================ FILE: lib/account/widgets/withdrawal_password_setting.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/util/device_utils.dart'; import 'package:flutter_deer/util/screen_utils.dart'; import 'package:flutter_deer/util/theme_utils.dart'; import 'package:flutter_deer/util/toast_utils.dart'; import 'package:flutter_deer/widgets/load_image.dart'; import 'package:vibration/vibration.dart'; /// design/6店铺-账户/index.html#artboard13 class WithdrawalPasswordSetting extends StatefulWidget { const WithdrawalPasswordSetting({super.key}); @override _WithdrawalPasswordSettingState createState() => _WithdrawalPasswordSettingState(); } class _WithdrawalPasswordSettingState extends State { int _index = 0; final _list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0]; final List _codeList = ['', '', '', '', '', '']; @override Widget build(BuildContext context) { return Container( color: context.dialogBackgroundColor, height: context.height * 7 / 10.0, child: Column( children: [ Stack( children: [ Container( width: double.infinity, alignment: Alignment.center, padding: const EdgeInsets.symmetric(vertical: 16.0), child: const Text( '设置提现密码', style: TextStyles.textBold18, ), ), Positioned( right: 16.0, top: 16.0, bottom: 16.0, child: Semantics( label: '关闭', child: GestureDetector( onTap: () => NavigatorUtils.goBack(context), child: const LoadAssetImage( 'goods/icon_dialog_close', key: Key('close'), width: 16.0, height: 16.0, ), ), ), ), ], ), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( height: 45.0, margin: const EdgeInsets.only(left: 16.0, right: 16.0), decoration: BoxDecoration( border: Border.all(width: 0.6, color: Colours.text_gray_c), borderRadius: BorderRadius.circular(4.0), ), child: Row( children: List.generate(_codeList.length, (i) => _buildInputWidget(i)) ), ), Gaps.vGap10, Text('提现密码不可为连续、重复的数字。', style: Theme.of(context).textTheme.titleSmall), ], ), ), Gaps.line, Container( color: Theme.of(context).dividerTheme.color, child: GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, childAspectRatio: 1.953, mainAxisSpacing: 0.6, crossAxisSpacing: 0.6, ), itemCount: 12, itemBuilder: (_, index) => _buildButton(index) ), ), ], ) ); } Widget _buildButton(int index) { final color = context.isDark ? Colours.dark_bg_gray : Colours.dark_button_text; return Material( color: (index == 9 || index == 11) ? color : null, child: InkWell( child: Center( child: index == 11 ? Semantics( label: '删除', child: const LoadAssetImage('account/del', width: 32.0), ) : index == 9 ? Semantics( label: '无效', child: Gaps.empty, ) : Text( _list[index].toString(), style: const TextStyle(fontSize: 26.0), ), ), onTap: () async { if (index == 9) { return; } if (index == 11) { if (_index == 0) { return; } _codeList[_index - 1] = ''; _index--; setState(() { }); return; } _codeList[_index] = _list[index].toString(); _index++; if (_index == _codeList.length) { var code = ''; for (var i = 0; i < _codeList.length; i ++) { code = code + _codeList[i]; } Toast.show('密码:$code'); _index = 0; for (var i = 0; i < _codeList.length; i ++) { _codeList[i] = ''; } } setState(() { }); /// 点击时给予振动反馈 if (!Device.isDesktop && await Vibration.hasVibrator()) { Vibration.vibrate(duration: 10); } }, ), ); } Widget _buildInputWidget(int p) { return Expanded( child: Container( alignment: Alignment.center, decoration: BoxDecoration( border: p != 5 ? Border( right: Divider.createBorderSide(context, color: Colours.text_gray_c, width: 0.6), ) : null, ), child: Text(_codeList[p].isEmpty ? '' : '●', style: TextStyles.textSize12,), ), ); } } ================================================ FILE: lib/demo/demo_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_deer/demo/focus/focus_demo_page.dart'; import 'package:flutter_deer/demo/lottie/lottie_demo.dart'; import 'package:flutter_deer/demo/navigator/books_main.dart'; import 'package:flutter_deer/demo/overlay/overlay_main.dart'; import 'package:flutter_deer/demo/ripple/ripples_animation_page.dart'; import 'package:flutter_deer/demo/scratcher/scratch_card_demo_page.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/util/app_navigator.dart'; import 'package:flutter_deer/widgets/click_item.dart'; import 'package:flutter_deer/widgets/my_app_bar.dart'; class DemoPage extends StatefulWidget { const DemoPage({super.key}); @override _DemoPageState createState() => _DemoPageState(); } class _DemoPageState extends State { @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { /// 显示状态栏和导航栏(使用QuickActions进入demo页) SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: [SystemUiOverlay.top, SystemUiOverlay.bottom]); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: const MyAppBar( centerTitle: 'Demo', ), body: Column( children: [ Gaps.vGap5, ClickItem( title: 'Overlay', onTap: () => AppNavigator.push(context, OverlayDemo()), ), ClickItem( title: 'Focus', onTap: () => AppNavigator.push(context, const FocusDemoPage(title: 'Focus Demo')), ), ClickItem( title: 'RipplesAnimation', onTap: () => AppNavigator.push(context, const RipplesAnimationPage()), ), ClickItem( title: 'Navigator 2.0', onTap: () => AppNavigator.push(context, const NestedRouterDemo()), ), ClickItem( title: 'ScratchCard', onTap: () => AppNavigator.push(context, const ScratchCardDemoPage()), ), ClickItem( title: 'Lottie', onTap: () => AppNavigator.push(context, const LottieDemo()), ), ], ), ); } } ================================================ FILE: lib/demo/focus/focus_demo_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/util/theme_utils.dart'; /// 博客:https://weilu.blog.csdn.net/article/details/107132031 class FocusDemoPage extends StatefulWidget { const FocusDemoPage({super.key, required this.title}); final String title; @override _FocusDemoPageState createState() => _FocusDemoPageState(); } class _FocusDemoPageState extends State { final FocusNode _focusNode = FocusNode(); @override void dispose() { _focusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { debugPrint('${widget.title} build'); return Scaffold( appBar: AppBar( backgroundColor: context.isDark ? Colours.dark_bg_color : Colors.blue, title: Text(widget.title), ), body: Column( children: [ TextField( focusNode: _focusNode, ), OutlinedButton( child: const Text('打印FocusTree'), onPressed: () { // 关闭软键盘四种方式 // SystemChannels.textInput.invokeMethod('TextInput.hide'); // FocusScope.of(context).requestFocus(FocusNode()); // FocusScope.of(context).unfocus(); // _focusNode.unfocus(); // FocusManager.instance.primaryFocus.unfocus(); WidgetsBinding.instance.addPostFrameCallback((_) { debugDumpFocusTree(); }); }, ), ElevatedButton( child: const Text('Push TestPage'), onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (BuildContext context) => const FocusDemoPage(title: 'Test Page'), ), ); }, ), ], ), ); } } ================================================ FILE: lib/demo/lottie/bunny.dart ================================================ import 'package:flutter/material.dart'; class Bunny { Bunny(this.controller) { setNeutralState(); } AnimationController controller; /// 各种状态过渡的起始帧数 static const List _neutral_to_tracking = [4, 22]; static const List _tracking_to_neutral = [0, 0]; static const List _neutral_to_shy = [29, 39]; static const List _shy_to_neutral = [44, 54]; static const List _neutral_to_peek = [76, 68]; static const List _peek_to_neutral = [68, 76]; static const List _shy_to_peek = [59, 68]; static const List _peek_to_shy = [68, 59]; BunnyState currentState = BunnyState.neutral; void setNeutralState() { switch(currentState) { case BunnyState.neutral: return; case BunnyState.tracking: setMinMaxFrame(_tracking_to_neutral); break; case BunnyState.shy: setMinMaxFrame(_shy_to_neutral); break; case BunnyState.peek: setMinMaxFrame(_peek_to_neutral); break; } currentState = BunnyState.neutral; } void setShyState() { switch(currentState) { case BunnyState.neutral: case BunnyState.tracking: setMinMaxFrame(_neutral_to_shy); break; case BunnyState.shy: return; case BunnyState.peek: setMinMaxFrame(_peek_to_shy); break; } currentState = BunnyState.shy; } void setPeekState() { switch(currentState) { case BunnyState.neutral: case BunnyState.tracking: setMinMaxFrame(_neutral_to_peek); break; case BunnyState.shy: setMinMaxFrame(_shy_to_peek); break; case BunnyState.peek: return; } currentState = BunnyState.peek; } void setTrackingState() { switch(currentState) { case BunnyState.neutral: setMinMaxFrame(_tracking_to_neutral); break; case BunnyState.tracking: return; case BunnyState.shy: setMinMaxFrame(_shy_to_neutral); break; case BunnyState.peek: setMinMaxFrame(_peek_to_neutral); break; } currentState = BunnyState.tracking; } void setEyesPosition(double progress) { if (currentState != BunnyState.tracking) { setMinMaxFrame(_tracking_to_neutral); currentState = BunnyState.tracking; return; } if (progress > 1) { return; } final double frame = (_neutral_to_tracking[1] - _neutral_to_tracking[0]) * progress; controller.animateTo(framesToPercentage(frame.toInt() + _neutral_to_tracking[0]), duration: Duration.zero); } void setMinMaxFrame(List frames) { /// 移动至起始帧 controller.animateTo(framesToPercentage(frames[0]), duration: Duration.zero); /// 动画至结束帧 controller.animateTo(framesToPercentage(frames[1])); } /// 共77帧。将已知帧数转为百分比 double framesToPercentage(int frame) { return frame / 77; } } enum BunnyState { /// 默认状态 neutral, /// 跟踪(文字输入) tracking, /// 害羞(密码不可见) shy, /// 偷看(密码可见) peek } ================================================ FILE: lib/demo/lottie/lottie_demo.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_deer/demo/lottie/bunny.dart'; import 'package:lottie/lottie.dart'; /// Android版实现:https://github.com/omarsahl/Flopsy /// 感谢Flopsy项目提供的思路及素材 class LottieDemo extends StatefulWidget { const LottieDemo({super.key,}); @override _LottieDemoState createState() => _LottieDemoState(); } const Color _primaryColor = Color(0xFFFFBCBF); const Color _backgroundColor = Color(0xFF37474F); const Color _textColor = Color(0xFFCCCCCC); class _LottieDemoState extends State with TickerProviderStateMixin { late AnimationController _controller; late Bunny _bunny; @override void initState() { super.initState(); _controller = AnimationController(vsync: this); _controller.stop(); _bunny = Bunny(_controller); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { /// 屏幕宽度减去左右各16的padding,计算出输入框宽度。 final double textFieldWidth = MediaQuery.of(context).size.width - 32; final Widget content = Scaffold( appBar: AppBar( systemOverlayStyle: SystemUiOverlayStyle.light, backgroundColor: _backgroundColor, title: const Text('Lottie Demo', style: TextStyle(color: _textColor),), iconTheme: const IconThemeData(color: _textColor), ), backgroundColor: _backgroundColor, body: SingleChildScrollView( child: Column( children: [ const SizedBox(height: 32.0), Lottie.asset( 'assets/lottie/bunny_new_mouth.json', width: 250, height: 250, controller: _controller, fit: BoxFit.fill, onLoaded: (composition) { setState(() { // 计算帧数 composition.endFrame - composition.startFrame; /// 设置动画时长 _controller.duration = composition.duration; }); }, ), _MyTextField( labelText: 'Email', keyboardType: TextInputType.emailAddress, onHasFocus: (isObscure) { /// 获取焦点,开始文字跟踪状态 _bunny.setTrackingState(); }, onChanged: (text) { /// 计算输入文字宽度占输入框宽度的比例 _bunny.setEyesPosition(_getTextSize(text) / textFieldWidth); }, ), _MyTextField( labelText: 'Password', keyboardType: TextInputType.visiblePassword, obscureText: true, onHasFocus: (isObscure) { /// 获取焦点,设置状态 if (isObscure) { _bunny.setShyState(); } else { _bunny.setPeekState(); } }, onObscureText: (isObscure) { if (isObscure) { _bunny.setShyState(); } else { _bunny.setPeekState(); } }, ), ], ), ), ); return Theme( data: ThemeData( primaryColor: _primaryColor, textSelectionTheme: TextSelectionThemeData( selectionColor: _primaryColor.withAlpha(70), selectionHandleColor: _primaryColor, // 覆盖`selectionHandleColor`不起作用 https://github.com/flutter/flutter/issues/74890 cursorColor: _primaryColor, ), colorScheme: ColorScheme.fromSwatch().copyWith(secondary: _primaryColor), ), child: content, ); } /// 获取文字宽度 double _getTextSize(String text) { final TextPainter textPainter = TextPainter( text: TextSpan(text: text, style: const TextStyle(fontSize: 16.0,)), maxLines: 1, textDirection: TextDirection.ltr, ) ..layout(); return textPainter.size.width; } } class _MyTextField extends StatefulWidget { const _MyTextField({ required this.labelText, this.obscureText = false, this.keyboardType, this.onHasFocus, this.onObscureText, this.onChanged }); final String labelText; final bool obscureText; final TextInputType? keyboardType; /// 获取焦点监听 final void Function(bool isObscure)? onHasFocus; /// 密码可见监听 final void Function(bool isObscure)? onObscureText; /// 文字输入监听 final void Function(String text)? onChanged; @override _MyTextFieldState createState() => _MyTextFieldState(); } class _MyTextFieldState extends State<_MyTextField> { bool _isObscure = true; final FocusNode _focusNode = FocusNode(); @override void initState() { super.initState(); _focusNode.addListener(_refresh); } void _refresh() { if (_focusNode.hasFocus && widget.onHasFocus != null) { widget.onHasFocus?.call(_isObscure); } setState(() { }); } @override void dispose() { _focusNode.removeListener(_refresh); _focusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(16.0), child: Listener( onPointerDown: (e) => FocusScope.of(context).requestFocus(_focusNode), child: TextField( focusNode: _focusNode, style: const TextStyle( color: _textColor, fontSize: 16.0, ), textInputAction: TextInputAction.next, decoration: InputDecoration( labelText: widget.labelText, labelStyle: TextStyle( color: _focusNode.hasFocus ? _primaryColor : _textColor, ), contentPadding: const EdgeInsets.only(left: 8.0), enabledBorder: const UnderlineInputBorder( borderSide: BorderSide( color: _textColor, ), ), focusedBorder: const UnderlineInputBorder( borderSide: BorderSide( color: _primaryColor, ), ), suffixIcon: widget.obscureText ? IconButton( icon: Icon( _isObscure ? Icons.visibility_off : Icons.visibility, color: _focusNode.hasFocus ? _primaryColor : _textColor, ), onPressed: () { setState(() { _isObscure = !_isObscure; }); if (widget.onObscureText != null) { widget.onObscureText?.call(_isObscure); } }, ) : null, ), keyboardType: widget.keyboardType, obscureText: widget.obscureText ? _isObscure : widget.obscureText, onChanged: widget.onChanged, ), ), ); } } ================================================ FILE: lib/demo/navigator/book_entity.dart ================================================ class Book { Book(this.title, this.author); final String title; final String author; } // Routes abstract class BookRoutePath {} class BooksListPath extends BookRoutePath {} class BooksSettingsPath extends BookRoutePath {} class BooksDetailsPath extends BookRoutePath { BooksDetailsPath(this.id); final int id; } ================================================ FILE: lib/demo/navigator/books_app_state.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/demo/navigator/book_entity.dart'; class BooksAppState extends ChangeNotifier { BooksAppState() : _selectedIndex = 0; late int _selectedIndex; Book? _selectedBook; final List books = [ Book('Stranger in a Strange Land', 'Robert A. Heinlein'), Book('Foundation', 'Isaac Asimov'), Book('Fahrenheit 451', 'Ray Bradbury'), ]; int get selectedIndex => _selectedIndex; set selectedIndex(int idx) { _selectedIndex = idx; if (_selectedIndex == 1) { // Remove this line if you want to keep the selected book when navigating // between "settings" and "home" which book was selected when Settings is // tapped. selectedBook = null; } notifyListeners(); } Book? get selectedBook => _selectedBook; set selectedBook(Book? book) { _selectedBook = book; notifyListeners(); } int getSelectedBookById() { if (!books.contains(_selectedBook)) { return 0; } return books.indexOf(_selectedBook!); } void setSelectedBookById(int id) { if (id < 0 || id > books.length - 1) { return; } _selectedBook = books[id]; notifyListeners(); } } ================================================ FILE: lib/demo/navigator/books_main.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/demo/navigator/delegate/router_delegate.dart'; import 'package:flutter_deer/demo/navigator/parser/route_information_parser.dart'; /// https://gist.github.com/johnpryan/bbca91e23bbb4d39247fa922533be7c9 /// https://weilu.blog.csdn.net/article/details/108902282 class NestedRouterDemo extends StatefulWidget { const NestedRouterDemo({super.key}); @override _NestedRouterDemoState createState() => _NestedRouterDemoState(); } class _NestedRouterDemoState extends State { final BookRouterDelegate _routerDelegate = BookRouterDelegate(); final BookRouteInformationParser _routeInformationParser = BookRouteInformationParser(); @override Widget build(BuildContext context) { return MaterialApp.router( title: 'Books App', routerDelegate: _routerDelegate, routeInformationParser: _routeInformationParser, ); } } ================================================ FILE: lib/demo/navigator/delegate/inner_router_delegate.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/demo/navigator/book_entity.dart'; import 'package:flutter_deer/demo/navigator/books_app_state.dart'; import 'package:flutter_deer/demo/navigator/screen/book_details_screen.dart'; import 'package:flutter_deer/demo/navigator/screen/books_list_screen.dart'; import 'package:flutter_deer/demo/navigator/screen/setting_screen.dart'; class InnerRouterDelegate extends RouterDelegate with ChangeNotifier, PopNavigatorRouterDelegateMixin { InnerRouterDelegate(this._appState); @override final GlobalKey navigatorKey = GlobalKey(); BooksAppState get appState => _appState; BooksAppState _appState; set appState(BooksAppState value) { if (value == _appState) { return; } _appState = value; notifyListeners(); } @override Widget build(BuildContext context) { return Navigator( key: navigatorKey, pages: [ if (appState.selectedIndex == 0) ...[ FadeAnimationPage( child: BooksListScreen( books: appState.books, onTapped: _handleBookTapped, ), key: const ValueKey('BooksListPage'), ), if (appState.selectedBook != null) MaterialPage( key: ValueKey(appState.selectedBook), child: BookDetailsScreen(book: appState.selectedBook!), ), ] else const FadeAnimationPage( child: SettingsScreen(), key: ValueKey('SettingsPage'), ), ], onPopPage: (route, dynamic result) { appState.selectedBook = null; notifyListeners(); return route.didPop(result); }, ); } @override Future setNewRoutePath(BookRoutePath configuration) async { // This is not required for inner router delegate because it does not // parse route assert(false); } void _handleBookTapped(Book book) { appState.selectedBook = book; notifyListeners(); } } class FadeAnimationPage extends Page { const FadeAnimationPage({super.key, required this.child}); final Widget child; @override Route createRoute(BuildContext context) { return PageRouteBuilder( settings: this, pageBuilder: (context, animation, animation2) { final curveTween = CurveTween(curve: Curves.easeIn); return FadeTransition( opacity: animation.drive(curveTween), child: child, ); }, ); } } ================================================ FILE: lib/demo/navigator/delegate/router_delegate.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/demo/navigator/book_entity.dart'; import 'package:flutter_deer/demo/navigator/books_app_state.dart'; import 'package:flutter_deer/demo/navigator/screen/app_shell.dart'; class BookRouterDelegate extends RouterDelegate with ChangeNotifier, PopNavigatorRouterDelegateMixin { BookRouterDelegate() : navigatorKey = GlobalKey() { appState.addListener(notifyListeners); } @override final GlobalKey navigatorKey; BooksAppState appState = BooksAppState(); @override BookRoutePath get currentConfiguration { if (appState.selectedIndex == 1) { return BooksSettingsPath(); } else { if (appState.selectedBook == null) { return BooksListPath(); } else { return BooksDetailsPath(appState.getSelectedBookById()); } } } @override Widget build(BuildContext context) { return Navigator( key: navigatorKey, pages: [ MaterialPage( child: AppShell(appState: appState), ), ], onPopPage: (route, dynamic result) { if (!route.didPop(result)) { return false; } if (appState.selectedBook != null) { appState.selectedBook = null; } notifyListeners(); return true; }, ); } @override Future setNewRoutePath(BookRoutePath configuration) async { if (configuration is BooksListPath) { appState.selectedIndex = 0; appState.selectedBook = null; } else if (configuration is BooksSettingsPath) { appState.selectedIndex = 1; } else if (configuration is BooksDetailsPath) { appState.setSelectedBookById(configuration.id); } } } ================================================ FILE: lib/demo/navigator/parser/route_information_parser.dart ================================================ import 'package:flutter/material.dart'; import '../book_entity.dart'; class BookRouteInformationParser extends RouteInformationParser { @override Future parseRouteInformation( RouteInformation routeInformation) async { final uri = routeInformation.uri; if (uri.pathSegments.isNotEmpty && uri.pathSegments.first == 'settings') { return BooksSettingsPath(); } else { if (uri.pathSegments.length >= 2) { if (uri.pathSegments[0] == 'book') { return BooksDetailsPath(int.tryParse(uri.pathSegments[1])!); } } return BooksListPath(); } } @override RouteInformation? restoreRouteInformation(BookRoutePath configuration) { if (configuration is BooksListPath) { return RouteInformation(uri: Uri.parse('/home')); } if (configuration is BooksSettingsPath) { return RouteInformation(uri: Uri.parse('/settings')); } if (configuration is BooksDetailsPath) { return RouteInformation(uri: Uri.parse('/book/${configuration.id}')); } return null; } } ================================================ FILE: lib/demo/navigator/screen/app_shell.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/demo/navigator/books_app_state.dart'; import 'package:flutter_deer/demo/navigator/delegate/inner_router_delegate.dart'; // Widget that contains the AdaptiveNavigationScaffold class AppShell extends StatefulWidget { const AppShell({ super.key, required this.appState, }); final BooksAppState appState; @override _AppShellState createState() => _AppShellState(); } class _AppShellState extends State { late InnerRouterDelegate _routerDelegate; late ChildBackButtonDispatcher _backButtonDispatcher; @override void initState() { super.initState(); _routerDelegate = InnerRouterDelegate(widget.appState); } @override void didUpdateWidget(covariant AppShell oldWidget) { super.didUpdateWidget(oldWidget); _routerDelegate.appState = widget.appState; } @override void didChangeDependencies() { super.didChangeDependencies(); // Defer back button dispatching to the child router _backButtonDispatcher = Router.of(context) .backButtonDispatcher! .createChildBackButtonDispatcher(); } @override Widget build(BuildContext context) { final appState = widget.appState; // Claim priority, If there are parallel sub router, you will need // to pick which one should take priority; _backButtonDispatcher.takePriority(); return Scaffold( appBar: AppBar( title: const Text('Books App'), ), body: Router( routerDelegate: _routerDelegate, backButtonDispatcher: _backButtonDispatcher, ), bottomNavigationBar: BottomNavigationBar( items: const [ BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'), BottomNavigationBarItem( icon: Icon(Icons.settings), label: 'Settings'), ], currentIndex: appState.selectedIndex, onTap: (newIndex) { appState.selectedIndex = newIndex; }, ), ); } } ================================================ FILE: lib/demo/navigator/screen/book_details_screen.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/demo/navigator/book_entity.dart'; class BookDetailsScreen extends StatelessWidget { const BookDetailsScreen({ super.key, required this.book, }); final Book book; @override Widget build(BuildContext context) { return Scaffold( body: Padding( padding: const EdgeInsets.all(8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ TextButton( onPressed: () { Navigator.of(context).pop(); }, child: const Text('Back'), ), Text(book.title, style: Theme.of(context).textTheme.titleLarge), Text(book.author, style: Theme.of(context).textTheme.titleMedium), ], ), ), ); } } ================================================ FILE: lib/demo/navigator/screen/books_list_screen.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/demo/navigator/book_entity.dart'; class BooksListScreen extends StatelessWidget { const BooksListScreen({ super.key, required this.books, required this.onTapped, }); final List books; final ValueChanged onTapped; @override Widget build(BuildContext context) { return Scaffold( body: ListView( children: [ for (final book in books) ListTile( title: Text(book.title), subtitle: Text(book.author), onTap: () => onTapped(book), ) ], ), ); } } ================================================ FILE: lib/demo/navigator/screen/setting_screen.dart ================================================ import 'package:flutter/material.dart'; class SettingsScreen extends StatelessWidget { const SettingsScreen({super.key}); @override Widget build(BuildContext context) { return const Scaffold( body: Center( child: Text('Settings screen'), ), ); } } ================================================ FILE: lib/demo/overlay/bottom_navigation/my_bottom_navigation_bar.dart ================================================ import 'package:flutter/material.dart'; class MyBottomNavigationBar extends StatefulWidget { const MyBottomNavigationBar({ super.key, this.selectedPosition = 0, this.isShowIndicator = true, required this.selectedCallback, }); /// 选中下标 final int selectedPosition; final bool isShowIndicator; final void Function(int selectedPosition) selectedCallback; @override _MyBottomNavigationBarState createState() => _MyBottomNavigationBarState(); } class _MyBottomNavigationBarState extends State with TickerProviderStateMixin { /// BottomNavigationBar高度 double barHeight = 56.0; /// 指示器高度 double indicatorHeight = 44.0; /// 选中图标颜色 Color selectedIconColor = Colors.blue; /// 默认图标颜色 Color normalIconColor = Colors.grey; /// 选中下标 int selectedPosition = 0; /// 记录上一次的选中下标 int previousSelectedPosition = 0; /// 选中图标高度 double selectedIconHeight = 38.0; /// 默认图标高度 double normalIconHeight = 32.0; /// 图标 List iconList = [Icons.image, Icons.add, Icons.access_alarms, Icons.settings]; double itemWidth = 0; late AnimationController controller; late Animation animation; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { itemWidth = (context.size!.width - barHeight) / 3; setState(() {}); }); /// 设置动画时长 controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 333)); if (widget.isShowIndicator) { selectedPosition = widget.selectedPosition; previousSelectedPosition = widget.selectedPosition; } animation = Tween(begin: selectedPosition.toDouble(), end: selectedPosition.toDouble()) .animate(CurvedAnimation(parent: controller, curve: Curves.linear)); } @override Widget build(BuildContext context) { final children = []; /// 背景 final background = Container( height: barHeight, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(barHeight / 2), boxShadow: const [ BoxShadow(color: Colors.grey, offset: Offset(0.0, 1.0), blurRadius: 4.0), ], ), ); children.add(background); if (itemWidth == 0) { return Stack(children: children,); } if (widget.isShowIndicator) { /// 指示器 children.add(Positioned( left: 6.0 + animation.value * itemWidth, top: (barHeight - indicatorHeight) / 2, child: Container( width: indicatorHeight, height: indicatorHeight, decoration: const BoxDecoration( shape: BoxShape.circle, color: Colors.white, boxShadow: [ BoxShadow(color: Colors.grey, blurRadius: 1.0), ], ), ), )); } for (var i = 0; i < iconList.length; i++) { /// 图标中心点计算 final rect = Rect.fromCenter( center: Offset(28.0 + (i * itemWidth), 28.0), width: (i == selectedPosition && widget.isShowIndicator) ? selectedIconHeight : normalIconHeight, height: (i == selectedPosition && widget.isShowIndicator) ? selectedIconHeight : normalIconHeight, ); children.add(Positioned.fromRect( rect: rect, child: GestureDetector( child: Container( decoration: BoxDecoration( shape: BoxShape.circle, color: (i == selectedPosition && widget.isShowIndicator) ? selectedIconColor : normalIconColor, ), child: Icon(iconList[i], color: Colors.white,), ), onTap: () { _selectedPosition(i); }, ), )); } return Stack(children: children,); } void _selectedPosition(int position) { if (!widget.isShowIndicator) { previousSelectedPosition = position; } else { previousSelectedPosition = selectedPosition; } selectedPosition = position; /// 执行动画 animation = Tween(begin: previousSelectedPosition.toDouble(), end: selectedPosition.toDouble()) .animate(CurvedAnimation(parent: controller, curve: Curves.linear)); animation.addListener(() { setState(() {}); }); controller.forward(from: 0.0); widget.selectedCallback(selectedPosition); } @override void dispose() { controller.dispose(); super.dispose(); } } ================================================ FILE: lib/demo/overlay/overlay_main.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/demo/overlay/page/overlay_demo_page.dart'; import 'package:flutter_deer/demo/overlay/route/application.dart'; import 'package:flutter_deer/demo/overlay/route/my_navigator_observer.dart'; class OverlayDemo extends StatelessWidget { OverlayDemo({super.key}) { Application.navigatorObserver = MyNavigatorObserver(); } // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const OverlayDemoPage(), navigatorObservers: [ Application.navigatorObserver ], ); } } ================================================ FILE: lib/demo/overlay/page/overlay_demo_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/demo/overlay/bottom_navigation/my_bottom_navigation_bar.dart'; import 'package:flutter_deer/demo/overlay/page/test_page.dart'; import 'package:flutter_deer/demo/overlay/route/application.dart'; /// 需求说明: 底部固定悬浮BottomNavigationBar,点击切换时有移动动画。 /// 进入二级页面图标全灰,返回一级页面返回之前状态。 /// 二级页面内点击按钮,直接返回一级页面。 /// /// 本例包含自定义BottomNavigationBar,路由监听及Overlay悬浮用法。 class OverlayDemoPage extends StatefulWidget { const OverlayDemoPage({super.key}); @override _OverlayDemoPageState createState() => _OverlayDemoPageState(); } class _OverlayDemoPageState extends State { OverlayEntry? _overlayEntry; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { _overlayEntry = OverlayEntry( builder: (context) => _buildBottomNavigation(context), ); /// 添加悬浮 Overlay.of(context).insert(_overlayEntry!); }); } @override void dispose() { /// 移除悬浮 _overlayEntry?.remove(); _overlayEntry = null; super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Overlay Demo'), ), body: ColoredBox( color: Colors.amber, child: Center( child: GestureDetector( child: const Padding( padding: EdgeInsets.symmetric(horizontal: 26.0), child: Text('功能说明:\n1.底部固定悬浮BottomNavigationBar点击切换时有移动动画。\n2.进入二级页面图标全灰,返回一级页面返回之前状态。\n3.二级页面内点击按钮,直接返回一级页面。\n\n点击文字进入下一页->', style: TextStyle(fontSize: 15.0), ), ), onTap: () { Navigator.push( context, MaterialPageRoute( builder: (BuildContext context) => const TestPage(), ), ); }, ) ), ) ); } Widget _buildBottomNavigation(BuildContext context) { final double width = MediaQuery.of(context).size.width; return Positioned( left: width * 0.2, right: width * 0.2, bottom: 20.0, child: SafeArea( child: MyBottomNavigationBar( /// 是否显示指示器 isShowIndicator: Application.navigatorObserver.list.isEmpty, selectedCallback: (position) { /// 返回主页 void removeRoute(Route route) { Navigator.removeRoute(context, route); } Application.navigatorObserver.list.forEach(removeRoute); /// 手动清空 Application.navigatorObserver.list = []; }, ), ), ); } } ================================================ FILE: lib/demo/overlay/page/test_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/demo/widgets/neumorphic.dart'; class TestPage extends StatefulWidget { const TestPage({super.key}); @override _TestPageState createState() => _TestPageState(); } class _TestPageState extends State { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Test Page'), ), backgroundColor: Colors.blueGrey.shade200, body: Center( child: NeumorphicContainer( child: GestureDetector( child: Text( '点击跳转', style: TextStyle( fontSize: 18.0, color: Colors.white, fontWeight: FontWeight.bold, shadows: [ const Shadow( offset: Offset(3, 3), color: Colors.black38, blurRadius: 10, ), Shadow( offset: const Offset(-3, -3), color: Colors.white.withOpacity(0.85), blurRadius: 10, ) ], ), ), onTap: () { Navigator.push( context, MaterialPageRoute( builder: (BuildContext context) => const TestPage(), ), ); }, ), ), ) ); } } ================================================ FILE: lib/demo/overlay/route/application.dart ================================================ import 'package:flutter_deer/demo/overlay/route/my_navigator_observer.dart'; class Application { static late MyNavigatorObserver navigatorObserver; } ================================================ FILE: lib/demo/overlay/route/my_navigator_observer.dart ================================================ import 'package:flutter/material.dart'; /// 记录路由,便于清空路由栈 class MyNavigatorObserver extends NavigatorObserver { List> list = []; @override void didPush(Route route, Route? previousRoute) { /// 首页不添加 if (route.settings.name != '/') { list.add(route); debugPrint(list.length.toString()); } } @override void didPop(Route route, Route? previousRoute) { list.remove(route); debugPrint(list.length.toString()); } } ================================================ FILE: lib/demo/ripple/ripples_animation_page.dart ================================================ import 'dart:math' as math show sin, pi, sqrt; import 'package:flutter/material.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/util/theme_utils.dart'; /// https://medium.com/flutterdevs/ripple-animation-in-flutter-3421cbd66a18 class RipplesAnimationPage extends StatefulWidget { const RipplesAnimationPage({ super.key, this.size = 80.0, this.color = Colors.red, }); final double size; final Color color; @override _RipplesAnimationState createState() => _RipplesAnimationState(); } class _RipplesAnimationState extends State with TickerProviderStateMixin { late AnimationController _controller; @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(milliseconds: 2000), vsync: this, )..repeat(); } @override void dispose() { _controller.dispose(); super.dispose(); } Widget _button() { return Center( child: ClipRRect( borderRadius: BorderRadius.circular(widget.size), child: DecoratedBox( decoration: BoxDecoration( gradient: RadialGradient( colors: [ widget.color, Color.lerp(widget.color, Colors.black, .05)! ], ), ), child: ScaleTransition( scale: Tween(begin: 0.95, end: 1.0).animate( CurvedAnimation( parent: _controller, curve: const PulsateCurve(), ), ), child: const Icon(Icons.speaker_phone, size: 44, color: Colors.white,), ), ), ), ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: context.isDark ? Colours.dark_bg_color : Colors.blue, title: const Text('Ripple Demo'), ), body: Center( child: CustomPaint( painter: CirclePainter( _controller, color: widget.color, ), child: SizedBox( width: widget.size * 4.125, height: widget.size * 4.125, child: _button(), ), ), ), ); } } class CirclePainter extends CustomPainter { CirclePainter(this._animation, { required this.color, }) : super(repaint: _animation); final Color color; final Animation _animation; void circle(Canvas canvas, Rect rect, double value) { final double opacity = (1.0 - (value / 4.0)).clamp(0.0, 1.0); final double size = rect.width / 2; final double area = size * size; final double radius = math.sqrt(area * value / 4); final Paint paint = Paint()..color = color.withOpacity(opacity); canvas.drawCircle(rect.center, radius, paint); } @override void paint(Canvas canvas, Size size) { final Rect rect = Rect.fromLTRB(0.0, 0.0, size.width, size.height); for (int wave = 3; wave >= 0; wave--) { circle(canvas, rect, wave + _animation.value); } } @override bool shouldRepaint(CirclePainter oldDelegate) => true; } class PulsateCurve extends Curve { const PulsateCurve(); @override double transform(double t) { if (t == 0 || t == 1) { return 0.01; } return math.sin(t * math.pi); } } ================================================ FILE: lib/demo/scratcher/scratch_card_demo_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/res/colors.dart'; import 'package:flutter_deer/res/gaps.dart'; import 'package:flutter_deer/util/theme_utils.dart'; import 'package:flutter_deer/widgets/load_image.dart'; import 'package:scratcher/scratcher.dart'; class ScratchCardDemoPage extends StatefulWidget { const ScratchCardDemoPage({super.key}); @override _ScratchCardDemoPageState createState() => _ScratchCardDemoPageState(); } class _ScratchCardDemoPageState extends State { final GlobalKey scratchKey = GlobalKey(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: context.isDark ? Colours.dark_bg_color : Colors.blue, title: const Text('ScratchCard Demo'), ), body: Column( children: [ Gaps.vGap16, Scratcher( key: scratchKey, brushSize: 20, threshold: 50, color: Colors.grey, onChange: (value) => debugPrint('Scratch progress: ${value.toStringAsFixed(2)}%'), onThreshold: () { /// 这里设置刮开50%,就揭开所有。 debugPrint('Threshold reached!'); scratchKey.currentState!.reveal( duration: const Duration(milliseconds: 1000), ); }, child: Container( padding: const EdgeInsets.symmetric(vertical: 20), color: Colors.white, height: 200, width: 300, child: const LoadAssetImage('logo',), ), ), Gaps.vGap50, Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ OutlinedButton( child: const Text('Reset'), onPressed: () { scratchKey.currentState!.reset( duration: const Duration(milliseconds: 2000), ); }, ), ElevatedButton( child: const Text('Reveal'), onPressed: () { scratchKey.currentState!.reveal( duration: const Duration(milliseconds: 2000), ); }, ), ], ), ], ), ); } } ================================================ FILE: lib/demo/widgets/neumorphic.dart ================================================ import 'package:flutter/material.dart'; /// https://medium.com/flutter-community/neumorphic-designs-in-flutter-eab9a4de2059 class NeumorphicContainer extends StatefulWidget { NeumorphicContainer({ super.key, required this.child, this.bevel = 10.0, this.color, }) : blurOffset = Offset(bevel / 2, bevel / 2); final Widget child; final double bevel; final Offset blurOffset; final Color? color; @override _NeumorphicContainerState createState() => _NeumorphicContainerState(); } class _NeumorphicContainerState extends State { bool _isPressed = false; void _onPointerDown(PointerDownEvent event) { setState(() { _isPressed = true; }); } void _onPointerUp(PointerUpEvent event) { setState(() { _isPressed = false; }); } @override Widget build(BuildContext context) { final Color color = widget.color ?? Colors.blueGrey.shade200; return Listener( onPointerDown: _onPointerDown, onPointerUp: _onPointerUp, child: AnimatedContainer( duration: const Duration(milliseconds: 150), padding: const EdgeInsets.all(24.0), decoration: BoxDecoration( borderRadius: BorderRadius.circular(widget.bevel * 10), gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ if (_isPressed) color else color.mix(Colors.black, .1), if (_isPressed) color.mix(Colors.black, .05) else color, if (_isPressed) color.mix(Colors.black, .05) else color, color.mix(Colors.white, _isPressed ? .2 : .5), ], stops: const [0.0, 0.3, 0.6, 1.0], ), boxShadow: _isPressed ? null : [ BoxShadow( blurRadius: widget.bevel, offset: -widget.blurOffset, color: color.mix(Colors.white, .6), ), BoxShadow( blurRadius: widget.bevel, offset: widget.blurOffset, color: color.mix(Colors.black, .3), ), ], ), child: widget.child, ), ); } } extension ColorUtils on Color { Color mix(Color another, double amount) { return Color.lerp(this, another, amount)!; } } ================================================ FILE: lib/generated/json/bank_entity.g.dart ================================================ import 'package:flutter_deer/generated/json/base/json_convert_content.dart'; import 'package:flutter_deer/account/models/bank_entity.dart'; import 'package:azlistview/azlistview.dart'; BankEntity $BankEntityFromJson(Map json) { final BankEntity bankEntity = BankEntity(); final int? id = jsonConvert.convert(json['id']); if (id != null) { bankEntity.id = id; } final String? bankName = jsonConvert.convert(json['bankName']); if (bankName != null) { bankEntity.bankName = bankName; } final String? firstLetter = jsonConvert.convert(json['firstLetter']); if (firstLetter != null) { bankEntity.firstLetter = firstLetter; } return bankEntity; } Map $BankEntityToJson(BankEntity entity) { final Map data = {}; data['id'] = entity.id; data['bankName'] = entity.bankName; data['firstLetter'] = entity.firstLetter; return data; } ================================================ FILE: lib/generated/json/base/json_convert_content.dart ================================================ // ignore_for_file: non_constant_identifier_names // ignore_for_file: camel_case_types // ignore_for_file: prefer_single_quotes // This file is automatically generated. DO NOT EDIT, all your changes would be lost. import 'package:flutter_deer/account/models/bank_entity.dart'; import 'package:flutter_deer/generated/json/bank_entity.g.dart'; import 'package:flutter_deer/account/models/city_entity.dart'; import 'package:flutter_deer/generated/json/city_entity.g.dart'; import 'package:flutter_deer/goods/models/goods_sort_entity.dart'; import 'package:flutter_deer/generated/json/goods_sort_entity.g.dart'; import 'package:flutter_deer/order/models/search_entity.dart'; import 'package:flutter_deer/generated/json/search_entity.g.dart'; import 'package:flutter_deer/shop/models/user_entity.dart'; import 'package:flutter_deer/generated/json/user_entity.g.dart'; JsonConvert jsonConvert = JsonConvert(); class JsonConvert { T? convert(dynamic value) { if (value == null) { return null; } return asT(value); } List? convertList(List? value) { if (value == null) { return null; } try { return value.map((dynamic e) => asT(e)).toList(); } catch (e, stackTrace) { print('asT<$T> $e $stackTrace'); return []; } } List? convertListNotNull(dynamic value) { if (value == null) { return null; } try { return (value as List).map((dynamic e) => asT(e)!).toList(); } catch (e, stackTrace) { print('asT<$T> $e $stackTrace'); return []; } } T? asT(dynamic value) { if (value is T) { return value; } final String type = T.toString(); try { final String valueS = value.toString(); if (type == "String") { return valueS as T; } else if (type == "int") { final int? intValue = int.tryParse(valueS); if (intValue == null) { return double.tryParse(valueS)?.toInt() as T?; } else { return intValue as T; } } else if (type == "double") { return double.parse(valueS) as T; } else if (type == "DateTime") { return DateTime.parse(valueS) as T; } else if (type == "bool") { if (valueS == '0' || valueS == '1') { return (valueS == '1') as T; } return (valueS == 'true') as T; } else { return JsonConvert.fromJsonAsT(value); } } catch (e, stackTrace) { print('asT<$T> $e $stackTrace'); return null; } } //Go back to a single instance by type static M? _fromJsonSingle(Map json) { final String type = M.toString(); if(type == (BankEntity).toString()){ return BankEntity.fromJson(json) as M; } if(type == (CityEntity).toString()){ return CityEntity.fromJson(json) as M; } if(type == (GoodsSortEntity).toString()){ return GoodsSortEntity.fromJson(json) as M; } if(type == (SearchEntity).toString()){ return SearchEntity.fromJson(json) as M; } if(type == (SearchItems).toString()){ return SearchItems.fromJson(json) as M; } if(type == (SearchItemsOwner).toString()){ return SearchItemsOwner.fromJson(json) as M; } if(type == (SearchItemsLicense).toString()){ return SearchItemsLicense.fromJson(json) as M; } if(type == (UserEntity).toString()){ return UserEntity.fromJson(json) as M; } print("$type not found"); return null; } //list is returned by type static M? _getListChildType(List data) { if([] is M){ return data.map((e) => BankEntity.fromJson(e)).toList() as M; } if([] is M){ return data.map((e) => CityEntity.fromJson(e)).toList() as M; } if([] is M){ return data.map((e) => GoodsSortEntity.fromJson(e)).toList() as M; } if([] is M){ return data.map((e) => SearchEntity.fromJson(e)).toList() as M; } if([] is M){ return data.map((e) => SearchItems.fromJson(e)).toList() as M; } if([] is M){ return data.map((e) => SearchItemsOwner.fromJson(e)).toList() as M; } if([] is M){ return data.map((e) => SearchItemsLicense.fromJson(e)).toList() as M; } if([] is M){ return data.map((e) => UserEntity.fromJson(e)).toList() as M; } print("${M.toString()} not found"); return null; } static M? fromJsonAsT(dynamic json) { if(json == null){ return null; } if (json is List) { return _getListChildType(json); } else { return _fromJsonSingle(json as Map); } } } ================================================ FILE: lib/generated/json/base/json_field.dart ================================================ // ignore_for_file: non_constant_identifier_names // ignore_for_file: camel_case_types // ignore_for_file: prefer_single_quotes // This file is automatically generated. DO NOT EDIT, all your changes would be lost. class JsonSerializable{ const JsonSerializable(); } class JSONField { //Specify the parse field name final String? name; //Whether to participate in toJson final bool? serialize; //Whether to participate in fromMap final bool? deserialize; const JSONField({this.name, this.serialize, this.deserialize}); } ================================================ FILE: lib/generated/json/city_entity.g.dart ================================================ import 'package:flutter_deer/generated/json/base/json_convert_content.dart'; import 'package:flutter_deer/account/models/city_entity.dart'; import 'package:azlistview/azlistview.dart'; CityEntity $CityEntityFromJson(Map json) { final CityEntity cityEntity = CityEntity(); final String? name = jsonConvert.convert(json['name']); if (name != null) { cityEntity.name = name; } final String? cityCode = jsonConvert.convert(json['cityCode']); if (cityCode != null) { cityEntity.cityCode = cityCode; } final String? firstCharacter = jsonConvert.convert(json['firstCharacter']); if (firstCharacter != null) { cityEntity.firstCharacter = firstCharacter; } return cityEntity; } Map $CityEntityToJson(CityEntity entity) { final Map data = {}; data['name'] = entity.name; data['cityCode'] = entity.cityCode; data['firstCharacter'] = entity.firstCharacter; return data; } ================================================ FILE: lib/generated/json/goods_sort_entity.g.dart ================================================ import 'package:flutter_deer/generated/json/base/json_convert_content.dart'; import 'package:flutter_deer/goods/models/goods_sort_entity.dart'; GoodsSortEntity $GoodsSortEntityFromJson(Map json) { final GoodsSortEntity goodsSortEntity = GoodsSortEntity(); final String? id = jsonConvert.convert(json['id']); if (id != null) { goodsSortEntity.id = id; } final String? name = jsonConvert.convert(json['name']); if (name != null) { goodsSortEntity.name = name; } return goodsSortEntity; } Map $GoodsSortEntityToJson(GoodsSortEntity entity) { final Map data = {}; data['id'] = entity.id; data['name'] = entity.name; return data; } ================================================ FILE: lib/generated/json/search_entity.g.dart ================================================ import 'package:flutter_deer/generated/json/base/json_convert_content.dart'; import 'package:flutter_deer/order/models/search_entity.dart'; SearchEntity $SearchEntityFromJson(Map json) { final SearchEntity searchEntity = SearchEntity(); final int? totalCount = jsonConvert.convert(json['total_count']); if (totalCount != null) { searchEntity.totalCount = totalCount; } final bool? incompleteResults = jsonConvert.convert(json['incomplete_results']); if (incompleteResults != null) { searchEntity.incompleteResults = incompleteResults; } final List? items = jsonConvert.convertListNotNull(json['items']); if (items != null) { searchEntity.items = items; } return searchEntity; } Map $SearchEntityToJson(SearchEntity entity) { final Map data = {}; data['total_count'] = entity.totalCount; data['incomplete_results'] = entity.incompleteResults; data['items'] = entity.items?.map((v) => v.toJson()).toList(); return data; } SearchItems $SearchItemsFromJson(Map json) { final SearchItems searchItems = SearchItems(); final int? id = jsonConvert.convert(json['id']); if (id != null) { searchItems.id = id; } final String? nodeId = jsonConvert.convert(json['node_id']); if (nodeId != null) { searchItems.nodeId = nodeId; } final String? name = jsonConvert.convert(json['name']); if (name != null) { searchItems.name = name; } final String? fullName = jsonConvert.convert(json['full_name']); if (fullName != null) { searchItems.fullName = fullName; } final bool? private = jsonConvert.convert(json['private']); if (private != null) { searchItems.private = private; } final SearchItemsOwner? owner = jsonConvert.convert(json['owner']); if (owner != null) { searchItems.owner = owner; } final String? htmlUrl = jsonConvert.convert(json['html_url']); if (htmlUrl != null) { searchItems.htmlUrl = htmlUrl; } final String? description = jsonConvert.convert(json['description']); if (description != null) { searchItems.description = description; } final bool? fork = jsonConvert.convert(json['fork']); if (fork != null) { searchItems.fork = fork; } final String? url = jsonConvert.convert(json['url']); if (url != null) { searchItems.url = url; } final String? forksUrl = jsonConvert.convert(json['forks_url']); if (forksUrl != null) { searchItems.forksUrl = forksUrl; } final String? keysUrl = jsonConvert.convert(json['keys_url']); if (keysUrl != null) { searchItems.keysUrl = keysUrl; } final String? collaboratorsUrl = jsonConvert.convert(json['collaborators_url']); if (collaboratorsUrl != null) { searchItems.collaboratorsUrl = collaboratorsUrl; } final String? teamsUrl = jsonConvert.convert(json['teams_url']); if (teamsUrl != null) { searchItems.teamsUrl = teamsUrl; } final String? hooksUrl = jsonConvert.convert(json['hooks_url']); if (hooksUrl != null) { searchItems.hooksUrl = hooksUrl; } final String? issueEventsUrl = jsonConvert.convert(json['issue_events_url']); if (issueEventsUrl != null) { searchItems.issueEventsUrl = issueEventsUrl; } final String? eventsUrl = jsonConvert.convert(json['events_url']); if (eventsUrl != null) { searchItems.eventsUrl = eventsUrl; } final String? assigneesUrl = jsonConvert.convert(json['assignees_url']); if (assigneesUrl != null) { searchItems.assigneesUrl = assigneesUrl; } final String? branchesUrl = jsonConvert.convert(json['branches_url']); if (branchesUrl != null) { searchItems.branchesUrl = branchesUrl; } final String? tagsUrl = jsonConvert.convert(json['tags_url']); if (tagsUrl != null) { searchItems.tagsUrl = tagsUrl; } final String? blobsUrl = jsonConvert.convert(json['blobs_url']); if (blobsUrl != null) { searchItems.blobsUrl = blobsUrl; } final String? gitTagsUrl = jsonConvert.convert(json['git_tags_url']); if (gitTagsUrl != null) { searchItems.gitTagsUrl = gitTagsUrl; } final String? gitRefsUrl = jsonConvert.convert(json['git_refs_url']); if (gitRefsUrl != null) { searchItems.gitRefsUrl = gitRefsUrl; } final String? treesUrl = jsonConvert.convert(json['trees_url']); if (treesUrl != null) { searchItems.treesUrl = treesUrl; } final String? statusesUrl = jsonConvert.convert(json['statuses_url']); if (statusesUrl != null) { searchItems.statusesUrl = statusesUrl; } final String? languagesUrl = jsonConvert.convert(json['languages_url']); if (languagesUrl != null) { searchItems.languagesUrl = languagesUrl; } final String? stargazersUrl = jsonConvert.convert(json['stargazers_url']); if (stargazersUrl != null) { searchItems.stargazersUrl = stargazersUrl; } final String? contributorsUrl = jsonConvert.convert(json['contributors_url']); if (contributorsUrl != null) { searchItems.contributorsUrl = contributorsUrl; } final String? subscribersUrl = jsonConvert.convert(json['subscribers_url']); if (subscribersUrl != null) { searchItems.subscribersUrl = subscribersUrl; } final String? subscriptionUrl = jsonConvert.convert(json['subscription_url']); if (subscriptionUrl != null) { searchItems.subscriptionUrl = subscriptionUrl; } final String? commitsUrl = jsonConvert.convert(json['commits_url']); if (commitsUrl != null) { searchItems.commitsUrl = commitsUrl; } final String? gitCommitsUrl = jsonConvert.convert(json['git_commits_url']); if (gitCommitsUrl != null) { searchItems.gitCommitsUrl = gitCommitsUrl; } final String? commentsUrl = jsonConvert.convert(json['comments_url']); if (commentsUrl != null) { searchItems.commentsUrl = commentsUrl; } final String? issueCommentUrl = jsonConvert.convert(json['issue_comment_url']); if (issueCommentUrl != null) { searchItems.issueCommentUrl = issueCommentUrl; } final String? contentsUrl = jsonConvert.convert(json['contents_url']); if (contentsUrl != null) { searchItems.contentsUrl = contentsUrl; } final String? compareUrl = jsonConvert.convert(json['compare_url']); if (compareUrl != null) { searchItems.compareUrl = compareUrl; } final String? mergesUrl = jsonConvert.convert(json['merges_url']); if (mergesUrl != null) { searchItems.mergesUrl = mergesUrl; } final String? archiveUrl = jsonConvert.convert(json['archive_url']); if (archiveUrl != null) { searchItems.archiveUrl = archiveUrl; } final String? downloadsUrl = jsonConvert.convert(json['downloads_url']); if (downloadsUrl != null) { searchItems.downloadsUrl = downloadsUrl; } final String? issuesUrl = jsonConvert.convert(json['issues_url']); if (issuesUrl != null) { searchItems.issuesUrl = issuesUrl; } final String? pullsUrl = jsonConvert.convert(json['pulls_url']); if (pullsUrl != null) { searchItems.pullsUrl = pullsUrl; } final String? milestonesUrl = jsonConvert.convert(json['milestones_url']); if (milestonesUrl != null) { searchItems.milestonesUrl = milestonesUrl; } final String? notificationsUrl = jsonConvert.convert(json['notifications_url']); if (notificationsUrl != null) { searchItems.notificationsUrl = notificationsUrl; } final String? labelsUrl = jsonConvert.convert(json['labels_url']); if (labelsUrl != null) { searchItems.labelsUrl = labelsUrl; } final String? releasesUrl = jsonConvert.convert(json['releases_url']); if (releasesUrl != null) { searchItems.releasesUrl = releasesUrl; } final String? deploymentsUrl = jsonConvert.convert(json['deployments_url']); if (deploymentsUrl != null) { searchItems.deploymentsUrl = deploymentsUrl; } final String? createdAt = jsonConvert.convert(json['created_at']); if (createdAt != null) { searchItems.createdAt = createdAt; } final String? updatedAt = jsonConvert.convert(json['updated_at']); if (updatedAt != null) { searchItems.updatedAt = updatedAt; } final String? pushedAt = jsonConvert.convert(json['pushed_at']); if (pushedAt != null) { searchItems.pushedAt = pushedAt; } final String? gitUrl = jsonConvert.convert(json['git_url']); if (gitUrl != null) { searchItems.gitUrl = gitUrl; } final String? sshUrl = jsonConvert.convert(json['ssh_url']); if (sshUrl != null) { searchItems.sshUrl = sshUrl; } final String? cloneUrl = jsonConvert.convert(json['clone_url']); if (cloneUrl != null) { searchItems.cloneUrl = cloneUrl; } final String? svnUrl = jsonConvert.convert(json['svn_url']); if (svnUrl != null) { searchItems.svnUrl = svnUrl; } final String? homepage = jsonConvert.convert(json['homepage']); if (homepage != null) { searchItems.homepage = homepage; } final int? size = jsonConvert.convert(json['size']); if (size != null) { searchItems.size = size; } final int? stargazersCount = jsonConvert.convert(json['stargazers_count']); if (stargazersCount != null) { searchItems.stargazersCount = stargazersCount; } final int? watchersCount = jsonConvert.convert(json['watchers_count']); if (watchersCount != null) { searchItems.watchersCount = watchersCount; } final String? language = jsonConvert.convert(json['language']); if (language != null) { searchItems.language = language; } final bool? hasIssues = jsonConvert.convert(json['has_issues']); if (hasIssues != null) { searchItems.hasIssues = hasIssues; } final bool? hasProjects = jsonConvert.convert(json['has_projects']); if (hasProjects != null) { searchItems.hasProjects = hasProjects; } final bool? hasDownloads = jsonConvert.convert(json['has_downloads']); if (hasDownloads != null) { searchItems.hasDownloads = hasDownloads; } final bool? hasWiki = jsonConvert.convert(json['has_wiki']); if (hasWiki != null) { searchItems.hasWiki = hasWiki; } final bool? hasPages = jsonConvert.convert(json['has_pages']); if (hasPages != null) { searchItems.hasPages = hasPages; } final int? forksCount = jsonConvert.convert(json['forks_count']); if (forksCount != null) { searchItems.forksCount = forksCount; } final bool? archived = jsonConvert.convert(json['archived']); if (archived != null) { searchItems.archived = archived; } final bool? disabled = jsonConvert.convert(json['disabled']); if (disabled != null) { searchItems.disabled = disabled; } final int? openIssuesCount = jsonConvert.convert(json['open_issues_count']); if (openIssuesCount != null) { searchItems.openIssuesCount = openIssuesCount; } final SearchItemsLicense? license = jsonConvert.convert(json['license']); if (license != null) { searchItems.license = license; } final int? forks = jsonConvert.convert(json['forks']); if (forks != null) { searchItems.forks = forks; } final int? openIssues = jsonConvert.convert(json['open_issues']); if (openIssues != null) { searchItems.openIssues = openIssues; } final int? watchers = jsonConvert.convert(json['watchers']); if (watchers != null) { searchItems.watchers = watchers; } final String? defaultBranch = jsonConvert.convert(json['default_branch']); if (defaultBranch != null) { searchItems.defaultBranch = defaultBranch; } final double? score = jsonConvert.convert(json['score']); if (score != null) { searchItems.score = score; } return searchItems; } Map $SearchItemsToJson(SearchItems entity) { final Map data = {}; data['id'] = entity.id; data['node_id'] = entity.nodeId; data['name'] = entity.name; data['full_name'] = entity.fullName; data['private'] = entity.private; data['owner'] = entity.owner?.toJson(); data['html_url'] = entity.htmlUrl; data['description'] = entity.description; data['fork'] = entity.fork; data['url'] = entity.url; data['forks_url'] = entity.forksUrl; data['keys_url'] = entity.keysUrl; data['collaborators_url'] = entity.collaboratorsUrl; data['teams_url'] = entity.teamsUrl; data['hooks_url'] = entity.hooksUrl; data['issue_events_url'] = entity.issueEventsUrl; data['events_url'] = entity.eventsUrl; data['assignees_url'] = entity.assigneesUrl; data['branches_url'] = entity.branchesUrl; data['tags_url'] = entity.tagsUrl; data['blobs_url'] = entity.blobsUrl; data['git_tags_url'] = entity.gitTagsUrl; data['git_refs_url'] = entity.gitRefsUrl; data['trees_url'] = entity.treesUrl; data['statuses_url'] = entity.statusesUrl; data['languages_url'] = entity.languagesUrl; data['stargazers_url'] = entity.stargazersUrl; data['contributors_url'] = entity.contributorsUrl; data['subscribers_url'] = entity.subscribersUrl; data['subscription_url'] = entity.subscriptionUrl; data['commits_url'] = entity.commitsUrl; data['git_commits_url'] = entity.gitCommitsUrl; data['comments_url'] = entity.commentsUrl; data['issue_comment_url'] = entity.issueCommentUrl; data['contents_url'] = entity.contentsUrl; data['compare_url'] = entity.compareUrl; data['merges_url'] = entity.mergesUrl; data['archive_url'] = entity.archiveUrl; data['downloads_url'] = entity.downloadsUrl; data['issues_url'] = entity.issuesUrl; data['pulls_url'] = entity.pullsUrl; data['milestones_url'] = entity.milestonesUrl; data['notifications_url'] = entity.notificationsUrl; data['labels_url'] = entity.labelsUrl; data['releases_url'] = entity.releasesUrl; data['deployments_url'] = entity.deploymentsUrl; data['created_at'] = entity.createdAt; data['updated_at'] = entity.updatedAt; data['pushed_at'] = entity.pushedAt; data['git_url'] = entity.gitUrl; data['ssh_url'] = entity.sshUrl; data['clone_url'] = entity.cloneUrl; data['svn_url'] = entity.svnUrl; data['homepage'] = entity.homepage; data['size'] = entity.size; data['stargazers_count'] = entity.stargazersCount; data['watchers_count'] = entity.watchersCount; data['language'] = entity.language; data['has_issues'] = entity.hasIssues; data['has_projects'] = entity.hasProjects; data['has_downloads'] = entity.hasDownloads; data['has_wiki'] = entity.hasWiki; data['has_pages'] = entity.hasPages; data['forks_count'] = entity.forksCount; data['archived'] = entity.archived; data['disabled'] = entity.disabled; data['open_issues_count'] = entity.openIssuesCount; data['license'] = entity.license?.toJson(); data['forks'] = entity.forks; data['open_issues'] = entity.openIssues; data['watchers'] = entity.watchers; data['default_branch'] = entity.defaultBranch; data['score'] = entity.score; return data; } SearchItemsOwner $SearchItemsOwnerFromJson(Map json) { final SearchItemsOwner searchItemsOwner = SearchItemsOwner(); final String? login = jsonConvert.convert(json['login']); if (login != null) { searchItemsOwner.login = login; } final int? id = jsonConvert.convert(json['id']); if (id != null) { searchItemsOwner.id = id; } final String? nodeId = jsonConvert.convert(json['node_id']); if (nodeId != null) { searchItemsOwner.nodeId = nodeId; } final String? avatarUrl = jsonConvert.convert(json['avatar_url']); if (avatarUrl != null) { searchItemsOwner.avatarUrl = avatarUrl; } final String? gravatarId = jsonConvert.convert(json['gravatar_id']); if (gravatarId != null) { searchItemsOwner.gravatarId = gravatarId; } final String? url = jsonConvert.convert(json['url']); if (url != null) { searchItemsOwner.url = url; } final String? htmlUrl = jsonConvert.convert(json['html_url']); if (htmlUrl != null) { searchItemsOwner.htmlUrl = htmlUrl; } final String? followersUrl = jsonConvert.convert(json['followers_url']); if (followersUrl != null) { searchItemsOwner.followersUrl = followersUrl; } final String? followingUrl = jsonConvert.convert(json['following_url']); if (followingUrl != null) { searchItemsOwner.followingUrl = followingUrl; } final String? gistsUrl = jsonConvert.convert(json['gists_url']); if (gistsUrl != null) { searchItemsOwner.gistsUrl = gistsUrl; } final String? starredUrl = jsonConvert.convert(json['starred_url']); if (starredUrl != null) { searchItemsOwner.starredUrl = starredUrl; } final String? subscriptionsUrl = jsonConvert.convert(json['subscriptions_url']); if (subscriptionsUrl != null) { searchItemsOwner.subscriptionsUrl = subscriptionsUrl; } final String? organizationsUrl = jsonConvert.convert(json['organizations_url']); if (organizationsUrl != null) { searchItemsOwner.organizationsUrl = organizationsUrl; } final String? reposUrl = jsonConvert.convert(json['repos_url']); if (reposUrl != null) { searchItemsOwner.reposUrl = reposUrl; } final String? eventsUrl = jsonConvert.convert(json['events_url']); if (eventsUrl != null) { searchItemsOwner.eventsUrl = eventsUrl; } final String? receivedEventsUrl = jsonConvert.convert(json['received_events_url']); if (receivedEventsUrl != null) { searchItemsOwner.receivedEventsUrl = receivedEventsUrl; } final String? type = jsonConvert.convert(json['type']); if (type != null) { searchItemsOwner.type = type; } final bool? siteAdmin = jsonConvert.convert(json['site_admin']); if (siteAdmin != null) { searchItemsOwner.siteAdmin = siteAdmin; } return searchItemsOwner; } Map $SearchItemsOwnerToJson(SearchItemsOwner entity) { final Map data = {}; data['login'] = entity.login; data['id'] = entity.id; data['node_id'] = entity.nodeId; data['avatar_url'] = entity.avatarUrl; data['gravatar_id'] = entity.gravatarId; data['url'] = entity.url; data['html_url'] = entity.htmlUrl; data['followers_url'] = entity.followersUrl; data['following_url'] = entity.followingUrl; data['gists_url'] = entity.gistsUrl; data['starred_url'] = entity.starredUrl; data['subscriptions_url'] = entity.subscriptionsUrl; data['organizations_url'] = entity.organizationsUrl; data['repos_url'] = entity.reposUrl; data['events_url'] = entity.eventsUrl; data['received_events_url'] = entity.receivedEventsUrl; data['type'] = entity.type; data['site_admin'] = entity.siteAdmin; return data; } SearchItemsLicense $SearchItemsLicenseFromJson(Map json) { final SearchItemsLicense searchItemsLicense = SearchItemsLicense(); final String? key = jsonConvert.convert(json['key']); if (key != null) { searchItemsLicense.key = key; } final String? name = jsonConvert.convert(json['name']); if (name != null) { searchItemsLicense.name = name; } final String? spdxId = jsonConvert.convert(json['spdx_id']); if (spdxId != null) { searchItemsLicense.spdxId = spdxId; } final String? url = jsonConvert.convert(json['url']); if (url != null) { searchItemsLicense.url = url; } final String? nodeId = jsonConvert.convert(json['node_id']); if (nodeId != null) { searchItemsLicense.nodeId = nodeId; } return searchItemsLicense; } Map $SearchItemsLicenseToJson(SearchItemsLicense entity) { final Map data = {}; data['key'] = entity.key; data['name'] = entity.name; data['spdx_id'] = entity.spdxId; data['url'] = entity.url; data['node_id'] = entity.nodeId; return data; } ================================================ FILE: lib/generated/json/user_entity.g.dart ================================================ import 'package:flutter_deer/generated/json/base/json_convert_content.dart'; import 'package:flutter_deer/shop/models/user_entity.dart'; UserEntity $UserEntityFromJson(Map json) { final UserEntity userEntity = UserEntity(); final String? avatarUrl = jsonConvert.convert(json['avatar_url']); if (avatarUrl != null) { userEntity.avatarUrl = avatarUrl; } final String? name = jsonConvert.convert(json['name']); if (name != null) { userEntity.name = name; } final int? id = jsonConvert.convert(json['id']); if (id != null) { userEntity.id = id; } final String? blog = jsonConvert.convert(json['blog']); if (blog != null) { userEntity.blog = blog; } return userEntity; } Map $UserEntityToJson(UserEntity entity) { final Map data = {}; data['avatar_url'] = entity.avatarUrl; data['name'] = entity.name; data['id'] = entity.id; data['blog'] = entity.blog; return data; } ================================================ FILE: lib/goods/goods_router.dart ================================================ import 'package:common_utils/common_utils.dart'; import 'package:fluro/fluro.dart'; import 'package:flutter_deer/goods/page/qr_code_scanner_page.dart'; import 'package:flutter_deer/routers/i_router.dart'; import 'page/goods_edit_page.dart'; import 'page/goods_page.dart'; import 'page/goods_search_page.dart'; import 'page/goods_size_edit_page.dart'; import 'page/goods_size_page.dart'; class GoodsRouter implements IRouterProvider{ static String goodsPage = '/goods'; static String goodsEditPage = '/goods/edit'; static String goodsSearchPage = '/goods/search'; static String goodsSizePage = '/goods/size'; static String goodsSizeEditPage = '/goods/sizeEdit'; static String qrCodeScannerPage = '/goods/qrCodeScanner'; @override void initRouter(FluroRouter router) { router.define(goodsPage, handler: Handler(handlerFunc: (_, __) => const GoodsPage())); router.define(goodsEditPage, handler: Handler(handlerFunc: (_, Map> params) { final bool isAdd = params['isAdd']?.first == 'true'; final bool isScan = params['isScan']?.first == 'true'; final String url = EncryptUtil.decodeBase64(params['url']?.first ?? ''); final String heroTag = params['heroTag']?.first ?? 'heroTag'; return GoodsEditPage(isAdd: isAdd, isScan: isScan, goodsImageUrl: url, heroTag: heroTag,); })); router.define(goodsSearchPage, handler: Handler(handlerFunc: (_, __) => const GoodsSearchPage())); router.define(goodsSizePage, handler: Handler(handlerFunc: (_, __) => const GoodsSizePage())); router.define(goodsSizeEditPage, handler: Handler(handlerFunc: (_, __) => const GoodsSizeEditPage())); router.define(qrCodeScannerPage, handler: Handler(handlerFunc: (_, __) => const QrCodeScannerPage())); } } ================================================ FILE: lib/goods/models/goods_item_entity.dart ================================================ class GoodsItemEntity { GoodsItemEntity({required this.icon, required this.title, required this.type}); GoodsItemEntity.fromJson(Map json) { icon = json['icon'] as String; title = json['title'] as String; type = json['type'] as int; } late String icon; late String title; late int type; Map toJson() { final Map data = {}; data['icon'] = icon; data['title'] = title; data['type'] = type; return data; } } ================================================ FILE: lib/goods/models/goods_size_model.dart ================================================ class GoodsSizeModel { GoodsSizeModel(this.icon, this.sizeName, this.stock, this.price, this.minSaleNum, this.reducePrice, this.charges, this.currencyPrice); GoodsSizeModel.fromJsonMap(Map map): icon = map['icon'] as String, sizeName = map['sizeName'] as String, stock = map['stock'] as int, price = map['price'] as String, minSaleNum = map['minSaleNum'] as int, reducePrice = map['reducePrice'] as String, charges = map['charges'] as String, currencyPrice = map['currencyPrice'] as String; String icon; String sizeName; int stock; String price; int minSaleNum; String reducePrice; String charges; String currencyPrice; Map toJson() { final Map data = {}; data['icon'] = icon; data['sizeName'] = sizeName; data['stock'] = stock; data['price'] = price; data['minSaleNum'] = minSaleNum; data['reducePrice'] = reducePrice; data['charges'] = charges; data['xPrice'] = currencyPrice; return data; } } ================================================ FILE: lib/goods/models/goods_sort_entity.dart ================================================ import 'package:flutter_deer/generated/json/base/json_field.dart'; import 'package:flutter_deer/generated/json/goods_sort_entity.g.dart'; @JsonSerializable() class GoodsSortEntity { GoodsSortEntity(); factory GoodsSortEntity.fromJson(Map json) => $GoodsSortEntityFromJson(json); Map toJson() => $GoodsSortEntityToJson(this); late String id; late String name; } ================================================ FILE: lib/goods/page/goods_edit_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/goods/provider/goods_sort_provider.dart'; import 'package:flutter_deer/goods/widgets/goods_sort_bottom_sheet.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/util/device_utils.dart'; import 'package:flutter_deer/util/theme_utils.dart'; import 'package:flutter_deer/util/toast_utils.dart'; import 'package:flutter_deer/widgets/click_item.dart'; import 'package:flutter_deer/widgets/load_image.dart'; import 'package:flutter_deer/widgets/my_app_bar.dart'; import 'package:flutter_deer/widgets/my_button.dart'; import 'package:flutter_deer/widgets/my_scroll_view.dart'; import 'package:flutter_deer/widgets/selected_image.dart'; import 'package:flutter_deer/widgets/text_field_item.dart'; import '../goods_router.dart'; /// design/4商品/index.html#artboard5 class GoodsEditPage extends StatefulWidget { const GoodsEditPage({ super.key, this.isAdd = true, this.isScan = false, this.heroTag, this.goodsImageUrl }); final bool isAdd; final bool isScan; final String? heroTag; final String? goodsImageUrl; @override _GoodsEditPageState createState() => _GoodsEditPageState(); } class _GoodsEditPageState extends State { String? _goodsSortName; final TextEditingController _codeController = TextEditingController(); @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { if (widget.isScan) { _scan(); } }); } Future _scan() async { if (Device.isMobile) { NavigatorUtils.unfocus(); // 延时保证键盘收起,否则进入扫码页会黑屏 Future.delayed(const Duration(milliseconds: 500), (){ if (!mounted) { return; } NavigatorUtils.pushResult(context, GoodsRouter.qrCodeScannerPage, (Object code) { _codeController.text = code.toString(); }); }); } else { Toast.show('当前平台暂不支持'); } } @override Widget build(BuildContext context) { return Scaffold( appBar: MyAppBar( centerTitle: widget.isAdd ? '添加商品' : '编辑商品', ), body: MyScrollView( key: const Key('goods_edit_page'), padding: const EdgeInsets.symmetric(vertical: 16.0), bottomButton: Padding( padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 8.0), child: MyButton( onPressed: () => NavigatorUtils.goBack(context), text: '提交', ), ), children: [ Gaps.vGap5, const Padding( padding: EdgeInsets.only(left: 16.0), child: Text( '基本信息', style: TextStyles.textBold18, ), ), Gaps.vGap16, Center( child: SelectedImage( heroTag: widget.heroTag, url: widget.goodsImageUrl, size: 96.0, ), ), Gaps.vGap8, Center( child: Text( '点击添加商品图片', style: Theme.of(context).textTheme.titleSmall?.copyWith(fontSize: Dimens.font_sp14), ), ), Gaps.vGap16, const TextFieldItem( title: '商品名称', hintText: '填写商品名称', ), const TextFieldItem( title: '商品简介', hintText: '填写简短描述', ), const TextFieldItem( title: '折后价格', keyboardType: TextInputType.numberWithOptions(decimal: true), hintText: '填写商品单品折后价格', ), Stack( alignment: Alignment.centerRight, children: [ TextFieldItem( controller: _codeController, title: '商品条码', hintText: '选填', ), Positioned( right: 0.0, child: Semantics( label: '扫码', child: GestureDetector( onTap: _scan, child: Padding( padding: const EdgeInsets.all(16.0), child: context.isDark ? const LoadAssetImage('goods/icon_sm', width: 16.0, height: 16.0) : const LoadAssetImage('goods/scanning', width: 16.0, height: 16.0), ), ), ), ) ], ), const TextFieldItem( title: '商品说明', hintText: '选填', ), Gaps.vGap32, const Padding( padding: EdgeInsets.only(left: 16.0), child: Text( '折扣立减', style: TextStyles.textBold18, ), ), Gaps.vGap16, const TextFieldItem( title: '立减金额', keyboardType: TextInputType.numberWithOptions(decimal: true) ), const TextFieldItem( title: '折扣金额', keyboardType: TextInputType.numberWithOptions(decimal: true) ), Gaps.vGap32, const Padding( padding: EdgeInsets.only(left: 16.0), child: Text( '类型规格', style: TextStyles.textBold18, ), ), Gaps.vGap16, ClickItem( title: '商品类型', content: _goodsSortName ?? '选择商品类型', onTap: () => _showBottomSheet(), ), ClickItem( title: '商品规格', content: '对规格进行编辑', onTap: () => NavigatorUtils.push(context, GoodsRouter.goodsSizePage), ), Gaps.vGap8, ], ) ); } final GoodsSortProvider _provider = GoodsSortProvider(); @override void dispose() { _provider.dispose(); _codeController.dispose(); super.dispose(); } void _showBottomSheet() { showModalBottomSheet( context: context, /// 使用true则高度不受16分之9的最高限制 isScrollControlled: true, builder: (BuildContext context) { return GoodsSortBottomSheet( provider: _provider, onSelected: (_, name) { setState(() { _goodsSortName = name; }); }, ); }, ); } } ================================================ FILE: lib/goods/page/goods_list_page.dart ================================================ import 'package:common_utils/common_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_deer/goods/models/goods_item_entity.dart'; import 'package:flutter_deer/goods/provider/goods_page_provider.dart'; import 'package:flutter_deer/res/constant.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/util/toast_utils.dart'; import 'package:flutter_deer/widgets/my_refresh_list.dart'; import 'package:flutter_deer/widgets/state_layout.dart'; import 'package:provider/provider.dart'; import '../goods_router.dart'; import '../widgets/goods_delete_bottom_sheet.dart'; import '../widgets/goods_item.dart'; class GoodsListPage extends StatefulWidget { const GoodsListPage({ super.key, required this.index }); final int index; @override _GoodsListPageState createState() => _GoodsListPageState(); } class _GoodsListPageState extends State with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin { int _selectIndex = -1; late Animation _animation; late AnimationController _controller; List _list = []; AnimationStatus _animationStatus = AnimationStatus.dismissed; @override void initState() { super.initState(); // 初始化动画控制 _controller = AnimationController(duration: const Duration(milliseconds: 450), vsync: this); // 动画曲线 final curvedAnimation = CurvedAnimation(parent: _controller, curve: Curves.easeOutSine); _animation = Tween(begin: 0.0, end: 1.1).animate(curvedAnimation) ..addStatusListener((status) { _animationStatus = status; }); //Item数量 _maxPage = widget.index == 0 ? 1 : (widget.index == 1 ? 2 : 3); _onRefresh(); } @override void dispose() { _controller.dispose(); super.dispose(); } final List _imgList = [ 'https://hbimg.b0.upaiyun.com/29cdf569b916ec8b952804a93b0a77e8c9baf61a58e0b-A0orbz_fw658', if (Constant.isDriverTest) 'https://hbimg.huabanimg.com/a3947661524be662da9f40d95dddc73c66196816633e1-9bUI91_fw658' else 'https://xxx', // 可以使用一张无效链接,触发缺省、异常显示图片 'https://hbimg.huabanimg.com/528c11bba65e2b8c0b6ae56f05e66b68f78f545f4ff26-tkM2Lx_fw658', 'https://img2.baidu.com/it/u=272387872,295674292&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', 'https://img0.baidu.com/it/u=2202484983,917817934&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', 'https://img0.baidu.com/it/u=2329453320,961102964&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500' ]; Future _onRefresh() async { await Future.delayed(const Duration(seconds: 2), () { setState(() { _page = 1; _list = List.generate(widget.index == 0 ? 3 : 10, (i) => GoodsItemEntity(icon: _imgList[i % 6], title: '八月十五中秋月饼礼盒', type: i % 3)); }); _setGoodsCount(_list.length); }); } Future _loadMore() async { await Future.delayed(const Duration(seconds: 2), () { setState(() { _list.addAll(List.generate(10, (i) => GoodsItemEntity(icon: _imgList[i % 6], title: '八月十五中秋月饼礼盒', type: i % 3))); _page ++; }); _setGoodsCount(_list.length); }); } void _setGoodsCount(int count) { // Provider.of(context, listen: false).setGoodsCount(count); /// 与上方等价,provider 4.1.0添加的拓展方法 context.read().setGoodsCount(count); } int _page = 1; late int _maxPage; StateType _stateType = StateType.loading; @override Widget build(BuildContext context) { super.build(context); return DeerListView( itemCount: _list.length, stateType: _stateType, onRefresh: _onRefresh, loadMore: _loadMore, hasMore: _page < _maxPage, itemBuilder: (_, index) { final String heroTag = 'goodsImg${widget.index}-$index'; return GoodsItem( index: index, heroTag: heroTag, selectIndex: _selectIndex, item: _list[index], animation: _animation, onTapMenu: () { /// 点击其他item时,重置状态 if (_selectIndex != index) { _animationStatus = AnimationStatus.dismissed; } /// 避免动画中重复执行 if (_animationStatus == AnimationStatus.dismissed) { // 开始执行动画 _controller.forward(from: 0.0); } setState(() { _selectIndex = index; }); }, onTapMenuClose: () { if (_animationStatus == AnimationStatus.completed) { _controller.reverse(from: 1.1); } _selectIndex = -1; }, onTapEdit: () { setState(() { _selectIndex = -1; }); final String url = EncryptUtil.encodeBase64(_list[index].icon); NavigatorUtils.push(context, '${GoodsRouter.goodsEditPage}?isAdd=false&url=$url&heroTag=$heroTag'); }, onTapOperation: () { Toast.show('下架'); }, onTapDelete: () { _controller.reverse(from: 1.1); _selectIndex = -1; _showDeleteBottomSheet(index); }, ); } ); } @override bool get wantKeepAlive => true; void _showDeleteBottomSheet(int index) { showModalBottomSheet( context: context, builder: (BuildContext context) { return GoodsDeleteBottomSheet( onTapDelete: () { setState(() { _list.removeAt(index); if (_list.isEmpty) { _stateType = StateType.goods; } }); _setGoodsCount(_list.length); }, ); }, ); } } ================================================ FILE: lib/goods/page/goods_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/goods/goods_router.dart'; import 'package:flutter_deer/goods/page/goods_list_page.dart'; import 'package:flutter_deer/goods/provider/goods_page_provider.dart'; import 'package:flutter_deer/goods/widgets/goods_add_menu.dart'; import 'package:flutter_deer/goods/widgets/goods_sort_menu.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/util/theme_utils.dart'; import 'package:flutter_deer/util/toast_utils.dart'; import 'package:flutter_deer/widgets/load_image.dart'; import 'package:flutter_deer/widgets/popup_window.dart'; import 'package:provider/provider.dart'; /// design/4商品/index.html class GoodsPage extends StatefulWidget { const GoodsPage({super.key}); @override _GoodsPageState createState() => _GoodsPageState(); } class _GoodsPageState extends State with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin { final List _sortList = ['全部商品', '个人护理', '饮料', '沐浴洗护', '厨房用具', '休闲食品', '生鲜水果', '酒水', '家庭清洁']; TabController? _tabController; final PageController _pageController = PageController(); final GlobalKey _addKey = GlobalKey(); final GlobalKey _bodyKey = GlobalKey(); final GlobalKey _buttonKey = GlobalKey(); GoodsPageProvider provider = GoodsPageProvider(); @override void initState() { super.initState(); _tabController = TabController(vsync: this, length: 3); } @override void dispose() { _tabController?.dispose(); super.dispose(); } /// https://github.com/flutter/flutter/issues/72908 @override // ignore: must_call_super void didChangeDependencies() { } @override Widget build(BuildContext context) { super.build(context); final Color? iconColor = ThemeUtils.getIconColor(context); return ChangeNotifierProvider( create: (_) => provider, child: Scaffold( appBar: AppBar( actions: [ IconButton( tooltip: '搜索商品', onPressed: () => NavigatorUtils.push(context, GoodsRouter.goodsSearchPage), icon: LoadAssetImage( 'goods/search', key: const Key('search'), width: 24.0, height: 24.0, color: iconColor, ), ), IconButton( tooltip: '添加商品', key: _addKey, onPressed: _showAddMenu, icon: LoadAssetImage( 'goods/add', key: const Key('add'), width: 24.0, height: 24.0, color: iconColor, ), ) ], ), body: Column( key: _bodyKey, crossAxisAlignment: CrossAxisAlignment.start, children: [ Semantics( container: true, label: '选择商品类型', child: GestureDetector( key: _buttonKey, /// 使用Selector避免同provider数据变化导致此处不必要的刷新 child: Selector( selector: (_, provider) => provider.sortIndex, /// 精准判断刷新条件(provider 4.0新属性) // shouldRebuild: (previous, next) => previous != next, builder: (_, sortIndex, __) { // 只会触发sortIndex变化的刷新 return Row( mainAxisSize: MainAxisSize.min, children: [ Gaps.hGap16, Text( _sortList[sortIndex], style: TextStyles.textBold24, ), Gaps.hGap8, LoadAssetImage('goods/expand', width: 16.0, height: 16.0, color: iconColor,) ], ); }, ), onTap: () => _showSortMenu(), ), ), Gaps.vGap24, Padding( padding: const EdgeInsets.only(left: 16.0), child: TabBar( onTap: (index) { if (!mounted) { return; } _pageController.jumpToPage(index); }, isScrollable: true, controller: _tabController, labelStyle: TextStyles.textBold18, indicatorSize: TabBarIndicatorSize.label, labelPadding: EdgeInsets.zero, unselectedLabelColor: context.isDark ? Colours.text_gray : Colours.text, labelColor: Theme.of(context).primaryColor, indicatorPadding: const EdgeInsets.only(right: 98.0 - 36.0), // 隐藏点击效果 overlayColor: MaterialStateProperty.resolveWith((Set states) { return Colors.transparent; }, ), tabs: const [ _TabView('在售', 0), _TabView('待售', 1), _TabView('下架', 2), ], ), ), Gaps.line, Expanded( child: PageView.builder( key: const Key('pageView'), itemCount: 3, onPageChanged: _onPageChange, controller: _pageController, itemBuilder: (_, int index) => GoodsListPage(index: index) ), ) ], ), ), ); } void _onPageChange(int index) { _tabController?.animateTo(index); provider.setIndex(index); } /// design/4商品/index.html#artboard3 void _showSortMenu() { // 获取点击控件的坐标 final RenderBox button = _buttonKey.currentContext!.findRenderObject()! as RenderBox; final RenderBox body = _bodyKey.currentContext!.findRenderObject()! as RenderBox; showPopupWindow( context: context, offset: const Offset(0.0, 12.0), anchor: button, child: GoodsSortMenu( data: _sortList, height: body.size.height - button.size.height, sortIndex: provider.sortIndex, onSelected: (index, name) { provider.setSortIndex(index); Toast.show('选择分类: $name'); }, ), ); } /// design/4商品/index.html#artboard4 void _showAddMenu() { final RenderBox button = _addKey.currentContext!.findRenderObject()! as RenderBox; showPopupWindow( context: context, isShowBg: true, offset: Offset(button.size.width - 8.0, -12.0), anchor: button, child: const GoodsAddMenu(), ); } @override bool get wantKeepAlive => true; } class _TabView extends StatelessWidget { const _TabView(this.tabName, this.index); final String tabName; final int index; @override Widget build(BuildContext context) { return Tab( child: SizedBox( width: 98.0, child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(tabName), Consumer( builder: (_, provider, child) { return Visibility( visible: !(provider.goodsCountList[index] == 0 || provider.index != index), child: Padding( padding: const EdgeInsets.only(top: 1.0), child: Text(' (${provider.goodsCountList[index]}件)', style: const TextStyle(fontSize: Dimens.font_sp12), ), ), ); }, ), ], ), ), ); } } ================================================ FILE: lib/goods/page/goods_search_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/util/toast_utils.dart'; import 'package:flutter_deer/widgets/my_search_bar.dart'; class GoodsSearchPage extends StatefulWidget { const GoodsSearchPage({super.key}); @override _GoodsSearchPageState createState() => _GoodsSearchPageState(); } class _GoodsSearchPageState extends State { @override Widget build(BuildContext context) { return Scaffold( appBar: MySearchBar( hintText: '请输入商品名称查询', onPressed: (text) => Toast.show('搜索内容:$text'), ), body: Container(), ); } } ================================================ FILE: lib/goods/page/goods_size_edit_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/widgets/my_app_bar.dart'; import 'package:flutter_deer/widgets/my_button.dart'; import 'package:flutter_deer/widgets/my_scroll_view.dart'; import 'package:flutter_deer/widgets/selected_image.dart'; import 'package:flutter_deer/widgets/text_field_item.dart'; /// design/4商品/index.html#artboard14 class GoodsSizeEditPage extends StatefulWidget { const GoodsSizeEditPage({super.key}); @override _GoodsSizeEditPageState createState() => _GoodsSizeEditPageState(); } class _GoodsSizeEditPageState extends State { @override Widget build(BuildContext context) { return Scaffold( appBar: const MyAppBar( centerTitle: '规格分类', ), body: MyScrollView( padding: const EdgeInsets.symmetric(vertical: 16.0), bottomButton: Padding( padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 8.0), child: MyButton( onPressed: () => NavigatorUtils.goBack(context), text: '确定', ), ), children: [ Gaps.vGap5, const Padding( padding: EdgeInsets.only(left: 16.0), child: Text('基本信息', style: TextStyles.textBold18), ), Gaps.vGap16, const Center( child: SelectedImage( size: 96.0, ), ), Gaps.vGap8, Center( child: Text( '点击添加分类图片', style: Theme.of(context).textTheme.titleSmall?.copyWith(fontSize: Dimens.font_sp14), ), ), Gaps.vGap16, const TextFieldItem( title: '分类名称', hintText: '填写该分类名称', ), const TextFieldItem( title: '折后价格', keyboardType: TextInputType.numberWithOptions(decimal: true), hintText: '填写该分类折后价格', ), const TextFieldItem( title: '库存数量', hintText: '填写该分类库存数量', keyboardType: TextInputType.number ), const TextFieldItem( title: '佣金金额', keyboardType: TextInputType.numberWithOptions(decimal: true) ), const TextFieldItem( title: '起购数量', keyboardType: TextInputType.number ), Gaps.vGap32, const Padding( padding: EdgeInsets.only(left: 16.0), child: Text('折扣立减', style: TextStyles.textBold18), ), Gaps.vGap16, const TextFieldItem( title: '立减金额', keyboardType: TextInputType.number, ), const TextFieldItem( title: '抵扣金额', keyboardType: TextInputType.number, ), Gaps.vGap8, ], ) ); } } ================================================ FILE: lib/goods/page/goods_size_page.dart ================================================ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_deer/goods/models/goods_size_model.dart'; import 'package:flutter_deer/goods/widgets/goods_size_dialog.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/util/device_utils.dart'; import 'package:flutter_deer/util/image_utils.dart'; import 'package:flutter_deer/util/other_utils.dart'; import 'package:flutter_deer/util/toast_utils.dart'; import 'package:flutter_deer/widgets/load_image.dart'; import 'package:flutter_deer/widgets/my_app_bar.dart'; import 'package:flutter_deer/widgets/my_button.dart'; import 'package:flutter_deer/widgets/popup_window.dart'; import 'package:flutter_deer/widgets/state_layout.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import '../goods_router.dart'; /// design/4商品/index.html#artboard9 class GoodsSizePage extends StatefulWidget { const GoodsSizePage({super.key}); @override _GoodsSizePageState createState() => _GoodsSizePageState(); } class _GoodsSizePageState extends State { bool _isEdit = false; String _sizeName = '商品规格名称'; final GlobalKey _hintKey = GlobalKey(); final List _goodsSizeList = []; @override void initState() { super.initState(); _goodsSizeList.clear(); _goodsSizeList.add(GoodsSizeModel('goods/goods_size_1', '黑色', 1000, '50.0', 2, '2', '2', '2')); _goodsSizeList.add(GoodsSizeModel('goods/goods_size_2', '银色', 100, '51.0', 1, '', '2', '1')); _goodsSizeList.add(GoodsSizeModel('goods/goods_size_1', '黑色1', 1050, '50.0', 2, '20', '2', '')); _goodsSizeList.add(GoodsSizeModel('goods/goods_size_2', '银色1', 1000, '55.0', 2, '', '10', '2')); _goodsSizeList.add(GoodsSizeModel('goods/goods_size_1', '黑色2', 500, '56', 2, '2', '2', '2')); _goodsSizeList.add(GoodsSizeModel('goods/goods_size_2', '银色2', 110, '51.0', 2, '2', '1', '')); _goodsSizeList.add(GoodsSizeModel('goods/goods_size_1', '黑色3', 10, '50.0', 2, '2', '2.5', '')); // 获取Build完成状态监听 WidgetsBinding.instance.addPostFrameCallback((_) { _showHint(); }); } /// design/4商品/index.html#artboard18 void _showHint() { final RenderBox hint = _hintKey.currentContext!.findRenderObject()! as RenderBox; showPopupWindow( context: context, isShowBg: true, offset: const Offset(50.0, 150.0), anchor: hint, child: Semantics( label: '弹出引导页', hint: '向左滑动可删除列表,点击可关闭', button: true, child: Container( key: const Key('hint'), width: 200.0, height: 147.0, decoration: BoxDecoration( image: DecorationImage( image: ImageUtils.getAssetImage('goods/ydss'), fit: BoxFit.fitWidth, ), ), ), ), ); } @override Widget build(BuildContext context) { return Scaffold( appBar: MyAppBar( key: _hintKey, title: '商品规格', actionName: '保存', onPressed: () { Toast.show('保存'); NavigatorUtils.goBack(context); }, ), resizeToAvoidBottomInset: false, body: SafeArea( child: Column( children: [ Gaps.vGap16, Text( _sizeName, style: TextStyles.textBold24, ), Gaps.vGap8, RichText( key: const Key('name_edit'), text: TextSpan( text: '先对名称进行', style: Theme.of(context).textTheme.titleSmall?.copyWith(fontSize: Dimens.font_sp14), children: [ TextSpan( text: '编辑', semanticsLabel: '编辑', style: TextStyle(color: Theme.of(context).primaryColor), recognizer: TapGestureRecognizer() ..onTap = () { _showGoodsSizeDialog(); }, ), ], ), ), Gaps.vGap32, Expanded( child: _goodsSizeList.isEmpty ? const StateLayout( type: StateType.goods, hintText: '暂无商品规格', ) : SlidableAutoCloseBehavior( child: ListView.builder( itemCount: _goodsSizeList.length, itemExtent: 107.0, itemBuilder: (_, index) => _buildGoodsSizeItem(index), ), ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: MyButton( onPressed: _isEdit ? () { NavigatorUtils.push(context, GoodsRouter.goodsSizeEditPage); } : null, text: '添加', ), ) ], ), ), ); } /// design/4商品/index.html#artboard19 Widget _buildGoodsSizeItem(int index) { // item Widget widget = Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ LoadAssetImage(_goodsSizeList[index].icon, width: 72.0, height: 72.0), Gaps.hGap8, Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( _goodsSizeList[index].sizeName, maxLines: 1, overflow: TextOverflow.ellipsis, ), Text( '库存${_goodsSizeList[index].stock}', style: TextStyles.textSize12, ), ], ), Gaps.vGap4, Row( children: [ Offstage( offstage: _goodsSizeList[index].reducePrice.isEmpty, child: _buildGoodsTag(Theme.of(context).colorScheme.error, '立减${_goodsSizeList[index].reducePrice}元'), ), Opacity( opacity: _goodsSizeList[index].currencyPrice.isEmpty ? 0.0 : 1.0, child: _buildGoodsTag(Theme.of(context).primaryColor, '金币抵扣${_goodsSizeList[index].currencyPrice}元'), ) ], ), const Spacer(), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(Utils.formatPrice(_goodsSizeList[index].price)), const SizedBox(width: 50.0,), Text( '佣金${_goodsSizeList[index].charges}元', style: TextStyles.textSize12, ), Text( '起购${_goodsSizeList[index].minSaleNum}件', style: TextStyles.textSize12, ), ], ) ], ), ), ], ); // item装饰 widget = InkWell( onTap: () { NavigatorUtils.push(context, GoodsRouter.goodsSizeEditPage); }, child: Padding( padding: const EdgeInsets.only(left: 16.0, top: 16.0), child: DecoratedBox( decoration: BoxDecoration( border: Border( bottom: Divider.createBorderSide(context, width: 0.8), ), ), child: Padding( padding: const EdgeInsets.only(right: 16.0, bottom: 16.0), child: widget, ), ), ), ); // 侧滑删除 return Slidable( key: Key(index.toString()), endActionPane: ActionPane( motion: const DrawerMotion(), extentRatio: 0.20, children: [ CustomSlidableAction( backgroundColor: Theme.of(context).colorScheme.error, child: Semantics( label: '删除', child: LoadAssetImage( 'goods/goods_delete', key: Key('delete_$index'), width: 24.0, ), ), onPressed: (context) { setState(() { _goodsSizeList.removeAt(index); }); }, ), ], ), child: widget, ); } Widget _buildGoodsTag(Color color, String text) { return Container( padding: const EdgeInsets.symmetric(horizontal: 4.0), margin: const EdgeInsets.only(right: 4.0), decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(2.0), ), height: 16.0, alignment: Alignment.center, child: Text( text, style: TextStyle(color: Colors.white, fontSize: Dimens.font_sp10, height: Device.isAndroid ? 1.1 : null,), ), ); } void _showGoodsSizeDialog() { showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return GoodsSizeDialog( onPressed: (name) { setState(() { _sizeName = name; _isEdit = true; }); }, ); }, ); } } ================================================ FILE: lib/goods/page/qr_code_scanner_page.dart ================================================ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/widgets/my_app_bar.dart'; import 'package:qr_code_scanner/qr_code_scanner.dart'; class QrCodeScannerPage extends StatefulWidget { const QrCodeScannerPage({super.key}); @override _QrCodeScannerPageState createState() => _QrCodeScannerPageState(); } class _QrCodeScannerPageState extends State { final GlobalKey qrKey = GlobalKey(debugLabel: 'QR'); QRViewController? controller; /// In order to get hot reload to work we need to pause the camera if the platform /// is android, or resume the camera if the platform is iOS. @override void reassemble() { super.reassemble(); if (Platform.isAndroid) { controller?.pauseCamera(); controller?.resumeCamera(); } else if (Platform.isIOS) { controller?.resumeCamera(); } } @override Widget build(BuildContext context) { final scanArea = (MediaQuery.of(context).size.width < 400 || MediaQuery.of(context).size.height < 400) ? 250.0 : 300.0; return Scaffold( body: Stack( children: [ Positioned.fill( child: QRView( key: qrKey, onQRViewCreated: _onQRViewCreated, overlay: QrScannerOverlayShape( borderRadius: 10, borderLength: 20, borderWidth: 5, cutOutSize: scanArea, ), ), ), Positioned( bottom: 60, left: 0, right: 0, child: Center( child: IconButton( icon: const Icon(Icons.highlight_outlined, size: 32, color: Colors.white,), onPressed: () { controller?.toggleFlash(); }, ), ), ), const Positioned( top: 0, left: 0, right: 0, child: MyAppBar( backgroundColor: Colors.transparent, backImgColor: Colors.white, ), ), ], ), ); } void _onQRViewCreated(QRViewController? controller) { setState(() { this.controller = controller; if (Platform.isAndroid) { controller?.pauseCamera(); controller?.resumeCamera(); } else if (Platform.isIOS) { controller?.resumeCamera(); } }); controller?.scannedDataStream.listen((scanData) { /// 避免扫描结果多次回调 controller.dispose(); if (!mounted) { return; } NavigatorUtils.goBackWithParams(context, scanData.code ?? ''); }); } @override void dispose() { controller?.dispose(); super.dispose(); } } ================================================ FILE: lib/goods/provider/goods_page_provider.dart ================================================ import 'package:flutter/material.dart'; class GoodsPageProvider extends ChangeNotifier { /// Tab的下标 int _index = 0; int get index => _index; /// 商品数量 final List _goodsCountList = [0, 0, 0]; List get goodsCountList => _goodsCountList; /// 选中商品分类下标 int _sortIndex = 0; int get sortIndex => _sortIndex; void setSortIndex(int sortIndex) { _sortIndex = sortIndex; notifyListeners(); } void setIndex(int index) { _index = index; notifyListeners(); } void setGoodsCount(int count) { _goodsCountList[index] = count; notifyListeners(); } } ================================================ FILE: lib/goods/provider/goods_sort_provider.dart ================================================ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_deer/generated/json/base/json_convert_content.dart'; import 'package:flutter_deer/goods/models/goods_sort_entity.dart'; class GoodsSortProvider extends ChangeNotifier { int _index = 0; int get index => _index; // TabBar初始化3个,其中两个文字置空。 final List _myTabs = [const Tab(text: '请选择'), const Tab(text: ''), const Tab(text: '')]; List get myTabs => _myTabs; List _mGoodsSort = []; List _mGoodsSort1 = []; List _mGoodsSort2 = []; /// 当前列表数据 List _mList = []; List get mList => _mList; /// 三级联动选择的position final List _positions = [0, 0, 0]; List get positions => _positions; void setIndex(int index) { _index = index; notifyListeners(); } void indexIncrement() { _index ++; } void setList(int index) { switch(index) { case 0: _mList = _mGoodsSort; break; case 1: _mList = _mGoodsSort1; break; case 2: _mList = _mGoodsSort2; break; } } void setListAndChangeTab() { switch(index) { case 1: _mList = _mGoodsSort1; _myTabs[1] = const Tab(text: '请选择'); _myTabs[2] = const Tab(text: ''); break; case 2: _mList = _mGoodsSort2; _myTabs[2] = const Tab(text: '请选择'); break; case 3: _mList = _mGoodsSort2; break; } notifyListeners(); } void initData() { if (_mList.isNotEmpty) { return; } // 模拟数据,数据为固定的三个列表 rootBundle.loadString('assets/data/sort_0.json').then((String value) { _mGoodsSort = JsonConvert.fromJsonAsT>(json.decode(value)) ?? []; _mList = _mGoodsSort; notifyListeners(); }); rootBundle.loadString('assets/data/sort_1.json').then((String value) { _mGoodsSort1 = JsonConvert.fromJsonAsT>(json.decode(value)) ?? []; }); rootBundle.loadString('assets/data/sort_2.json').then((String value) { _mGoodsSort2 = JsonConvert.fromJsonAsT>(json.decode(value)) ?? []; }); } } ================================================ FILE: lib/goods/widgets/goods_add_menu.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/goods/goods_router.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/util/theme_utils.dart'; import 'package:flutter_deer/widgets/load_image.dart'; class GoodsAddMenu extends StatefulWidget { const GoodsAddMenu({ super.key, }); @override _GoodsAddMenuState createState() => _GoodsAddMenuState(); } class _GoodsAddMenuState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _scaleAnimation; @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(milliseconds: 200), vsync: this, ); _scaleAnimation = Tween(begin: 0.0, end: 1.0).animate(_controller); _controller.forward(); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final Color backgroundColor = context.backgroundColor; final Color? iconColor = ThemeUtils.getIconColor(context); final Widget body = Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Padding( padding: const EdgeInsets.only(right: 12.0), child: LoadAssetImage('goods/jt', width: 8.0, height: 4.0, color: ThemeUtils.getDarkColor(context, Colours.dark_bg_color), ), ), SizedBox( width: 120.0, height: 40.0, child: TextButton.icon( onPressed: () { NavigatorUtils.push(context, '${GoodsRouter.goodsEditPage}?isAdd=true&isScan=true', replace: true); }, icon: LoadAssetImage('goods/scanning', width: 16.0, height: 16.0, color: iconColor,), label: const Text('扫码添加'), style: TextButton.styleFrom( foregroundColor: Theme.of(context).textTheme.bodyMedium?.color, disabledForegroundColor: Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.12), backgroundColor: backgroundColor, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only(topLeft: Radius.circular(8.0), topRight: Radius.circular(8.0)), ), ), ), ), Container(width: 120.0, height: 0.6, color: Colours.line), SizedBox( width: 120.0, height: 40.0, child: TextButton.icon( onPressed: () { NavigatorUtils.push(context, '${GoodsRouter.goodsEditPage}?isAdd=true', replace: true); }, icon: LoadAssetImage('goods/add2', width: 16.0, height: 16.0, color: iconColor,), label: const Text('添加商品'), style: TextButton.styleFrom( foregroundColor: Theme.of(context).textTheme.bodyMedium?.color, disabledForegroundColor: Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.12), backgroundColor: backgroundColor, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only(bottomLeft: Radius.circular(8.0), bottomRight: Radius.circular(8.0)), ), ), ), ), ], ); return AnimatedBuilder( animation: _scaleAnimation, builder: (_, child) { return Transform.scale( scale: _scaleAnimation.value, alignment: Alignment.topRight, child: child, ); }, child: body, ); } } ================================================ FILE: lib/goods/widgets/goods_delete_bottom_sheet.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/widgets/my_button.dart'; /// design/4商品/index.html#artboard2 class GoodsDeleteBottomSheet extends StatelessWidget { const GoodsDeleteBottomSheet({ super.key, required this.onTapDelete, }); final VoidCallback onTapDelete; @override Widget build(BuildContext context) { return Material( child: SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox( height: 52.0, child: Center( child: Text( '是否确认删除,防止错误操作', style: TextStyles.textSize16, ), ), ), Gaps.line, MyButton( minHeight: 54.0, textColor: Theme.of(context).colorScheme.error, text: '确认删除', backgroundColor: Colors.transparent, onPressed: () { NavigatorUtils.goBack(context); onTapDelete(); }, ), Gaps.line, MyButton( minHeight: 54.0, textColor: Colours.text_gray, text: '取消', backgroundColor: Colors.transparent, onPressed: () { NavigatorUtils.goBack(context); }, ), ], ), ), ); } } ================================================ FILE: lib/goods/widgets/goods_item.dart ================================================ import 'package:common_utils/common_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_deer/goods/models/goods_item_entity.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/util/device_utils.dart'; import 'package:flutter_deer/util/other_utils.dart'; import 'package:flutter_deer/util/theme_utils.dart'; import 'package:flutter_deer/widgets/load_image.dart'; import 'package:flutter_deer/widgets/my_button.dart'; import 'menu_reveal.dart'; /// design/4商品/index.html#artboard1 class GoodsItem extends StatelessWidget { const GoodsItem({ super.key, required this.item, required this.index, required this.selectIndex, required this.onTapMenu, required this.onTapEdit, required this.onTapOperation, required this.onTapDelete, required this.onTapMenuClose, required this.animation, required this.heroTag, }); final GoodsItemEntity item; final int index; final int selectIndex; final VoidCallback onTapMenu; final VoidCallback onTapEdit; final VoidCallback onTapOperation; final VoidCallback onTapDelete; final VoidCallback onTapMenuClose; final Animation animation; final String heroTag; @override Widget build(BuildContext context) { final Row child = Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ ExcludeSemantics( child: Hero( tag: heroTag, child: LoadImage(item.icon, width: 72.0, height: 72.0), ), ), Gaps.hGap8, Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( item.type % 3 != 0 ? '八月十五中秋月饼礼盒' : '八月十五中秋月饼礼盒八月十五中秋月饼礼盒', maxLines: 1, overflow: TextOverflow.ellipsis, ), Gaps.vGap4, Row( children: [ Visibility( // 默认为占位替换,类似于gone visible: item.type % 3 == 0, child: _GoodsItemTag( text: '立减', color: Theme.of(context).colorScheme.error, ), ), Opacity( // 修改透明度实现隐藏,类似于invisible opacity: item.type % 2 != 0 ? 0.0 : 1.0, child: _GoodsItemTag( text: '金币抵扣', color: Theme.of(context).primaryColor, ), ) ], ), Gaps.vGap16, Text(Utils.formatPrice('20.00', format: MoneyFormat.NORMAL)) ], ), ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Semantics( /// container属性为true,防止上方ExcludeSemantics去除此处语义 container: true, label: '商品操作菜单', child: GestureDetector( onTap: onTapMenu, child: Container( key: Key('goods_menu_item_$index'), width: 44.0, height: 44.0, color: Colors.transparent, padding: const EdgeInsets.only(left: 28.0, bottom: 28.0), child: const LoadAssetImage('goods/ellipsis'), ), ), ), Padding( padding: const EdgeInsets.only(top: 10.0), child: Text( '特产美味', style: Theme.of(context).textTheme.titleSmall, ), ) ], ) ], ); return Stack( children: [ // item间的分隔线 Padding( padding: const EdgeInsets.only(left: 16.0, top: 16.0), child: DecoratedBox( decoration: BoxDecoration( border: Border( bottom: Divider.createBorderSide(context, width: 0.8), ), ), child: Padding( padding: const EdgeInsets.only(right: 16.0, bottom: 16.0), child: child, ), ), ), if (selectIndex != index) Gaps.empty else _buildGoodsMenu(context), ], ); } Widget _buildGoodsMenu(BuildContext context) { return Positioned.fill( child: AnimatedBuilder( animation: animation, child: _buildGoodsMenuContent(context), builder: (_, Widget? child) { return MenuReveal( revealPercent: animation.value, child: child! ); } ), ); } Widget _buildGoodsMenuContent(BuildContext context) { final bool isDark = context.isDark; final Color buttonColor = isDark ? Colours.dark_text : Colors.white; return InkWell( onTap: onTapMenuClose, child: ColoredBox( color: isDark ? const Color(0xB34D4D4D) : const Color(0x4D000000), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Gaps.hGap15, MyButton( key: Key('goods_edit_item_$index'), text: '编辑', fontSize: Dimens.font_sp16, radius: 24.0, minWidth: 56.0, minHeight: 56.0, padding: const EdgeInsets.symmetric(horizontal: 12.0), textColor: isDark ? Colours.dark_button_text : Colors.white, backgroundColor: isDark ? Colours.dark_app_main : Colours.app_main, onPressed: onTapEdit, ), MyButton( key: Key('goods_operation_item_$index'), text: '下架', fontSize: Dimens.font_sp16, radius: 24.0, minWidth: 56.0, minHeight: 56.0, padding: const EdgeInsets.symmetric(horizontal: 12.0), textColor: Colours.text, backgroundColor: buttonColor, onPressed: onTapOperation, ), MyButton( key: Key('goods_delete_item_$index'), text: '删除', fontSize: Dimens.font_sp16, radius: 24.0, minWidth: 56.0, minHeight: 56.0, padding: const EdgeInsets.symmetric(horizontal: 12.0), textColor: Colours.text, backgroundColor: buttonColor, onPressed: onTapDelete, ), Gaps.hGap15, ], ), ), ); } } class _GoodsItemTag extends StatelessWidget { const _GoodsItemTag({ required this.color, required this.text, }); final Color? color; final String text; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 4.0), margin: const EdgeInsets.only(right: 4.0), decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(2.0), ), height: 16.0, alignment: Alignment.center, child: Text( text, style: TextStyle( color: Colors.white, fontSize: Dimens.font_sp10, height: Device.isAndroid ? 1.1 : null, ), ), ); } } ================================================ FILE: lib/goods/widgets/goods_size_dialog.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/util/theme_utils.dart'; import 'package:flutter_deer/widgets/base_dialog.dart'; /// design/4商品/index.html#artboard10 class GoodsSizeDialog extends StatefulWidget { const GoodsSizeDialog({ super.key, this.onPressed, }); final void Function(String)? onPressed; @override _GoodsSizeDialog createState() => _GoodsSizeDialog(); } class _GoodsSizeDialog extends State { final TextEditingController _controller = TextEditingController(); @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return BaseDialog( title: '规格名称', child: Container( height: 34.0, alignment: Alignment.center, margin: const EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 0.0), decoration: BoxDecoration( color: ThemeUtils.getDialogTextFieldColor(context), borderRadius: BorderRadius.circular(2.0), ), child: TextField( autofocus: true, controller: _controller, decoration: const InputDecoration( isDense: true, contentPadding: EdgeInsets.symmetric(horizontal: 16.0), border: InputBorder.none, hintText: '输入文字…', //hintStyle: TextStyles.textGrayC14, ), ), ), onPressed: () { NavigatorUtils.goBack(context); widget.onPressed?.call(_controller.text); }, ); } } ================================================ FILE: lib/goods/widgets/goods_sort_bottom_sheet.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/goods/provider/goods_sort_provider.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/util/other_utils.dart'; import 'package:flutter_deer/util/screen_utils.dart'; import 'package:flutter_deer/util/theme_utils.dart'; import 'package:flutter_deer/widgets/load_image.dart'; import 'package:provider/provider.dart'; /// design/4商品/index.html#artboard20 class GoodsSortBottomSheet extends StatefulWidget { const GoodsSortBottomSheet({ super.key, required this.provider, required this.onSelected, }); final void Function(String, String) onSelected; /// 临时状态 final GoodsSortProvider provider; @override GoodsSortBottomSheetState createState() => GoodsSortBottomSheetState(); } class GoodsSortBottomSheetState extends State with SingleTickerProviderStateMixin { TabController? _tabController; final ScrollController _controller = ScrollController(); @override void initState() { super.initState(); _tabController = TabController(vsync: this, length: 3); WidgetsBinding.instance.addPostFrameCallback((_) { widget.provider.initData(); _tabController?.animateTo(widget.provider.index, duration: Duration.zero); }); } @override void dispose() { _tabController?.dispose(); _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Material( child: SizedBox( height: context.height * 11.0 / 16.0, /// 为保留状态,选择ChangeNotifierProvider.value,销毁自己手动处理(见 goods_edit_page.dart :dispose()) child: ChangeNotifierProvider.value( value: widget.provider, child: Consumer( builder: (_, provider, child) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ child!, Gaps.line, TabBar( controller: _tabController, isScrollable: true, onTap: (index) { if (provider.myTabs[index].text.nullSafe.isEmpty) { // 拦截点击事件 _tabController?.animateTo(provider.index); return; } provider.setList(index); provider.setIndex(index); _controller.animateTo( provider.positions[provider.index] * 48.0, duration: const Duration(milliseconds: 10), curve: Curves.ease, ); }, indicatorSize: TabBarIndicatorSize.label, unselectedLabelColor: context.isDark ? Colours.text_gray : Colours.text, labelColor: Theme.of(context).primaryColor, // 隐藏点击效果 overlayColor: MaterialStateProperty.resolveWith((Set states) { return Colors.transparent; },), tabs: provider.myTabs, ), Gaps.line, Expanded( child: ListView.builder( controller: _controller, itemExtent: 48.0, itemBuilder: (_, index) { return _buildItem(provider, index); }, itemCount: provider.mList.length, ), ) ], ); }, child: Stack( children: [ Container( width: double.infinity, alignment: Alignment.center, padding: const EdgeInsets.symmetric(vertical: 16.0), child: const Text( '商品分类', style: TextStyles.textBold16, ), ), Positioned( right: 16.0, top: 16.0, bottom: 16.0, child: InkWell( onTap: () => NavigatorUtils.goBack(context), child: const SizedBox( height: 16.0, width: 16.0, child: LoadAssetImage('goods/icon_dialog_close'), ), ), ) ], ), ), ), ), ); } Widget _buildItem(GoodsSortProvider provider, int index) { final bool flag = provider.mList[index].name == provider.myTabs[provider.index].text; return InkWell( child: Container( padding: const EdgeInsets.symmetric(horizontal: 16.0), alignment: Alignment.centerLeft, child: Row( children: [ Text( provider.mList[index].name, style: flag ? TextStyle( fontSize: Dimens.font_sp14, color: Theme.of(context).primaryColor, ) : null,), Gaps.hGap8, Visibility( visible: flag, child: const LoadAssetImage('goods/xz', height: 16.0, width: 16.0), ) ], ), ), onTap: () { provider.myTabs[provider.index] = Tab(text: provider.mList[index].name); provider.positions[provider.index] = index; provider.indexIncrement(); provider.setListAndChangeTab(); if (provider.index > 2) { provider.setIndex(2); widget.onSelected(provider.mList[index].id, provider.mList[index].name); NavigatorUtils.goBack(context); } _controller.animateTo(0.0, duration: const Duration(milliseconds: 100), curve: Curves.ease); _tabController?.animateTo(provider.index); }, ); } } ================================================ FILE: lib/goods/widgets/goods_sort_menu.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/util/theme_utils.dart'; class GoodsSortMenu extends StatefulWidget { const GoodsSortMenu({ super.key, required this.data, required this.sortIndex, required this.height, required this.onSelected, }); final List data; final int sortIndex; final double height; final void Function(int, String) onSelected; @override _GoodsSortMenuState createState() => _GoodsSortMenuState(); } class _GoodsSortMenuState extends State with SingleTickerProviderStateMixin { late final AnimationController _controller = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, )..forward(); late final Animation _animation = CurvedAnimation( parent: _controller, curve: Curves.easeInCubic, reverseCurve: Curves.easeOutCubic, ); @override void initState() { super.initState(); _animation.addStatusListener(_statusListener); } void _statusListener(AnimationStatus status) { if (status == AnimationStatus.dismissed) { /// 菜单动画停止,关闭菜单。 NavigatorUtils.goBack(context); } } @override void dispose() { _animation.removeStatusListener(_statusListener); _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final Color backgroundColor = context.backgroundColor; final Widget listView = ListView.builder( physics: const ClampingScrollPhysics(), itemCount: widget.data.length + 1, itemBuilder: (_, int index) { return index == widget.data.length ? Container( color: backgroundColor, height: 12.0, ) : _buildItem(index, backgroundColor); }, ); return FadeTransition( opacity: _animation, child: Container( color: const Color(0x99000000), height: widget.height - 12.0, child: ScaleTransition( scale: _animation, alignment: Alignment.topCenter, child: listView, ), ), ); } Widget _buildItem(int index, Color backgroundColor) { final TextStyle textStyle = TextStyle( fontSize: Dimens.font_sp14, color: Theme.of(context).primaryColor, ); return Material( color: backgroundColor, child: InkWell( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( widget.data[index], style: index == widget.sortIndex ? textStyle : null, ), Text( '($index)', style: index == widget.sortIndex ? textStyle : null, ), ], ), ), onTap: () { widget.onSelected(index, widget.data[index]); _controller.reverse(); }, ), ); } } ================================================ FILE: lib/goods/widgets/menu_reveal.dart ================================================ import 'dart:math'; import 'package:flutter/material.dart'; //https://github.com/alibaba/flutter-go/blob/master/lib/views/fourth_page/page_reveal.dart class MenuReveal extends StatelessWidget { const MenuReveal({ super.key, required this.revealPercent, required this.child }); final double revealPercent; final Widget child; @override Widget build(BuildContext context) { return ClipOval( clipper: CircleRevealClipper(revealPercent), child: child, ); } } class CircleRevealClipper extends CustomClipper { CircleRevealClipper(this.revealPercent); final double revealPercent; @override Rect getClip(Size size) { // 右上角的点击点为圆心 final Offset epicenter = Offset(size.width - 25.0, 25.0); final double theta = atan(epicenter.dy / epicenter.dx); final double distanceToCorner = (epicenter.dy) / sin(theta); final double radius = distanceToCorner * revealPercent; final double diameter = 2 * radius; return Rect.fromLTWH(epicenter.dx - radius, epicenter.dy - radius, diameter, diameter); } @override bool shouldReclip(CustomClipper oldClipper) { return true; } } ================================================ FILE: lib/home/home_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/goods/page/goods_page.dart'; import 'package:flutter_deer/home/provider/home_provider.dart'; import 'package:flutter_deer/order/page/order_page.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/shop/page/shop_page.dart'; import 'package:flutter_deer/statistics/page/statistics_page.dart'; import 'package:flutter_deer/util/theme_utils.dart'; import 'package:flutter_deer/widgets/double_tap_back_exit_app.dart'; import 'package:flutter_deer/widgets/load_image.dart'; import 'package:provider/provider.dart'; class Home extends StatefulWidget { const Home({super.key}); @override _HomeState createState() => _HomeState(); } class _HomeState extends State with RestorationMixin{ static const double _imageSize = 25.0; late List _pageList; final List _appBarTitles = ['订单', '商品', '统计', '店铺']; final PageController _pageController = PageController(); HomeProvider provider = HomeProvider(); List? _list; List? _listDark; @override void initState() { super.initState(); initData(); } @override void dispose() { _pageController.dispose(); super.dispose(); } void initData() { _pageList = [ const OrderPage(), const GoodsPage(), const StatisticsPage(), const ShopPage(), ]; } List _buildBottomNavigationBarItem() { if (_list == null) { const tabImages = [ [ LoadAssetImage('home/icon_order', width: _imageSize, color: Colours.unselected_item_color,), LoadAssetImage('home/icon_order', width: _imageSize, color: Colours.app_main,), ], [ LoadAssetImage('home/icon_commodity', width: _imageSize, color: Colours.unselected_item_color,), LoadAssetImage('home/icon_commodity', width: _imageSize, color: Colours.app_main,), ], [ LoadAssetImage('home/icon_statistics', width: _imageSize, color: Colours.unselected_item_color,), LoadAssetImage('home/icon_statistics', width: _imageSize, color: Colours.app_main,), ], [ LoadAssetImage('home/icon_shop', width: _imageSize, color: Colours.unselected_item_color,), LoadAssetImage('home/icon_shop', width: _imageSize, color: Colours.app_main,), ] ]; _list = List.generate(tabImages.length, (i) { return BottomNavigationBarItem( icon: tabImages[i][0], activeIcon: tabImages[i][1], label: _appBarTitles[i], tooltip: _appBarTitles[i], ); }); } return _list!; } List _buildDarkBottomNavigationBarItem() { if (_listDark == null) { const tabImagesDark = [ [ LoadAssetImage('home/icon_order', width: _imageSize), LoadAssetImage('home/icon_order', width: _imageSize, color: Colours.dark_app_main,), ], [ LoadAssetImage('home/icon_commodity', width: _imageSize), LoadAssetImage('home/icon_commodity', width: _imageSize, color: Colours.dark_app_main,), ], [ LoadAssetImage('home/icon_statistics', width: _imageSize), LoadAssetImage('home/icon_statistics', width: _imageSize, color: Colours.dark_app_main,), ], [ LoadAssetImage('home/icon_shop', width: _imageSize), LoadAssetImage('home/icon_shop', width: _imageSize, color: Colours.dark_app_main,), ] ]; _listDark = List.generate(tabImagesDark.length, (i) { return BottomNavigationBarItem( icon: tabImagesDark[i][0], activeIcon: tabImagesDark[i][1], label: _appBarTitles[i], tooltip: _appBarTitles[i], ); }); } return _listDark!; } @override Widget build(BuildContext context) { final bool isDark = context.isDark; return ChangeNotifierProvider( create: (_) => provider, child: DoubleTapBackExitApp( child: Scaffold( bottomNavigationBar: Consumer( builder: (_, provider, __) { return BottomNavigationBar( backgroundColor: context.backgroundColor, items: isDark ? _buildDarkBottomNavigationBarItem() : _buildBottomNavigationBarItem(), type: BottomNavigationBarType.fixed, currentIndex: provider.value, elevation: 5.0, iconSize: 21.0, selectedFontSize: Dimens.font_sp10, unselectedFontSize: Dimens.font_sp10, selectedItemColor: Theme.of(context).primaryColor, unselectedItemColor: isDark ? Colours.dark_unselected_item_color : Colours.unselected_item_color, onTap: (index) => _pageController.jumpToPage(index), ); }, ), // 使用PageView的原因参看 https://zhuanlan.zhihu.com/p/58582876 body: PageView( physics: const NeverScrollableScrollPhysics(), // 禁止滑动 controller: _pageController, onPageChanged: (int index) => provider.value = index, children: _pageList, ) ), ), ); } @override String? get restorationId => 'home'; @override void restoreState(RestorationBucket? oldBucket, bool initialRestore) { registerForRestoration(provider, 'BottomNavigationBarCurrentIndex'); } } ================================================ FILE: lib/home/provider/home_provider.dart ================================================ import 'package:flutter/material.dart'; class HomeProvider extends RestorableInt { HomeProvider() : super(0); } ================================================ FILE: lib/home/splash_page.dart ================================================ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_deer/demo/demo_page.dart'; import 'package:flutter_deer/login/login_router.dart'; import 'package:flutter_deer/res/constant.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/util/app_navigator.dart'; import 'package:flutter_deer/util/device_utils.dart'; import 'package:flutter_deer/util/image_utils.dart'; import 'package:flutter_deer/util/theme_utils.dart'; import 'package:flutter_deer/widgets/fractionally_aligned_sized_box.dart'; import 'package:flutter_deer/widgets/load_image.dart'; import 'package:flutter_swiper_null_safety_flutter3/flutter_swiper_null_safety_flutter3.dart'; import 'package:quick_actions/quick_actions.dart'; import 'package:rxdart/rxdart.dart'; import 'package:sp_util/sp_util.dart'; class SplashPage extends StatefulWidget { const SplashPage({super.key}); @override _SplashPageState createState() => _SplashPageState(); } class _SplashPageState extends State { int _status = 0; final List _guideList = ['app_start_1', 'app_start_2', 'app_start_3']; StreamSubscription? _subscription; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) async { /// 两种初始化方案,另一种见 main.dart /// 两种方法各有优劣 await SpUtil.getInstance(); await Device.initDeviceInfo(); if (SpUtil.getBool(Constant.keyGuide, defValue: true)!) { /// 预先缓存图片,避免直接使用时因为首次加载造成闪动 void precacheImages(String image) { precacheImage(ImageUtils.getAssetImage(image, format: ImageFormat.webp), context); } _guideList.forEach(precacheImages); } _initSplash(); }); if (Device.isAndroid) { const QuickActions quickActions = QuickActions(); quickActions.initialize((String shortcutType) async { if (shortcutType == 'demo') { AppNavigator.pushReplacement(context, const DemoPage()); _subscription?.cancel(); } }); } } @override void dispose() { _subscription?.cancel(); super.dispose(); } void _initGuide() { setState(() { _status = 1; }); } void _initSplash() { _subscription = Stream.value(1).delay(const Duration(milliseconds: 1500)).listen((_) { if (SpUtil.getBool(Constant.keyGuide, defValue: true)! || Constant.isDriverTest) { SpUtil.putBool(Constant.keyGuide, false); _initGuide(); } else { _goLogin(); } }); } void _goLogin() { NavigatorUtils.push(context, LoginRouter.loginPage, replace: true); } @override Widget build(BuildContext context) { return Material( color: context.backgroundColor, child: _status == 0 ? const FractionallyAlignedSizedBox( heightFactor: 0.3, widthFactor: 0.33, leftFactor: 0.33, bottomFactor: 0, child: LoadAssetImage('logo') ) : Swiper( key: const Key('swiper'), itemCount: _guideList.length, loop: false, itemBuilder: (_, index) { return LoadAssetImage( _guideList[index], key: Key(_guideList[index]), fit: BoxFit.cover, width: double.infinity, height: double.infinity, format: ImageFormat.webp, ); }, onTap: (index) { if (index == _guideList.length - 1) { _goLogin(); } }, ) ); } } ================================================ FILE: lib/home/webview_page.dart ================================================ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_deer/res/gaps.dart'; import 'package:flutter_deer/widgets/my_app_bar.dart'; import 'package:webview_flutter/webview_flutter.dart'; class WebViewPage extends StatefulWidget { const WebViewPage({ super.key, required this.title, required this.url, }); final String title; final String url; @override _WebViewPageState createState() => _WebViewPageState(); } class _WebViewPageState extends State { late final WebViewController _controller; int _progressValue = 0; @override void initState() { super.initState(); _controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setNavigationDelegate( NavigationDelegate( onProgress: (int progress) { if (!mounted) { return; } debugPrint('WebView is loading (progress : $progress%)'); setState(() { _progressValue = progress; }); }, ), ) ..loadRequest(Uri.parse(widget.url)); } @override Widget build(BuildContext context) { return WillPopScope( onWillPop: () async { final bool canGoBack = await _controller.canGoBack(); if (canGoBack) { // 网页可以返回时,优先返回上一页 await _controller.goBack(); return Future.value(false); } return Future.value(true); }, child: Scaffold( appBar: MyAppBar( centerTitle: widget.title, ), body: Stack( children: [ WebViewWidget( controller: _controller, ), if (_progressValue != 100) LinearProgressIndicator( value: _progressValue / 100, backgroundColor: Colors.transparent, minHeight: 2, ) else Gaps.empty, ], ), ), ); } } ================================================ FILE: lib/l10n/deer_localizations.dart ================================================ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:intl/intl.dart' as intl; import 'deer_localizations_en.dart'; import 'deer_localizations_zh.dart'; // ignore_for_file: type=lint /// Callers can lookup localized strings with an instance of DeerLocalizations /// returned by `DeerLocalizations.of(context)`. /// /// Applications need to include `DeerLocalizations.delegate()` in their app's /// `localizationDelegates` list, and the locales they support in the app's /// `supportedLocales` list. For example: /// /// ```dart /// import 'l10n/deer_localizations.dart'; /// /// return MaterialApp( /// localizationsDelegates: DeerLocalizations.localizationsDelegates, /// supportedLocales: DeerLocalizations.supportedLocales, /// home: MyApplicationHome(), /// ); /// ``` /// /// ## Update pubspec.yaml /// /// Please make sure to update your pubspec.yaml to include the following /// packages: /// /// ```yaml /// dependencies: /// # Internationalization support. /// flutter_localizations: /// sdk: flutter /// intl: any # Use the pinned version from flutter_localizations /// /// # Rest of dependencies /// ``` /// /// ## iOS Applications /// /// iOS applications define key application metadata, including supported /// locales, in an Info.plist file that is built into the application bundle. /// To configure the locales supported by your app, you’ll need to edit this /// file. /// /// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. /// Then, in the Project Navigator, open the Info.plist file under the Runner /// project’s Runner folder. /// /// Next, select the Information Property List item, select Add Item from the /// Editor menu, then select Localizations from the pop-up menu. /// /// Select and expand the newly-created Localizations item then, for each /// locale your application supports, add a new item and select the locale /// you wish to add from the pop-up menu in the Value field. This list should /// be consistent with the languages listed in the DeerLocalizations.supportedLocales /// property. abstract class DeerLocalizations { DeerLocalizations(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString()); final String localeName; static DeerLocalizations? of(BuildContext context) { return Localizations.of(context, DeerLocalizations); } static const LocalizationsDelegate delegate = _DeerLocalizationsDelegate(); /// A list of this localizations delegate along with the default localizations /// delegates. /// /// Returns a list of localizations delegates containing this delegate along with /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, /// and GlobalWidgetsLocalizations.delegate. /// /// Additional delegates can be added by appending to this list in /// MaterialApp. This list does not have to be used at all if a custom list /// of delegates is preferred or required. static const List> localizationsDelegates = >[ delegate, GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, GlobalWidgetsLocalizations.delegate, ]; /// A list of this localizations delegate's supported locales. static const List supportedLocales = [Locale('en'), Locale('zh')]; /// Title for the application /// /// In en, this message translates to: /// **'Flutter Deer'** String get title; /// Title for the Login page /// /// In en, this message translates to: /// **'Verification Code Login'** String get verificationCodeLogin; /// Password Login /// /// In en, this message translates to: /// **'Password Login'** String get passwordLogin; /// Login /// /// In en, this message translates to: /// **'Login'** String get login; /// Forgot Password /// /// In en, this message translates to: /// **'Forgot Password'** String get forgotPasswordLink; /// Please enter the password /// /// In en, this message translates to: /// **'Please enter the password'** String get inputPasswordHint; /// Please input username /// /// In en, this message translates to: /// **'Please input username'** String get inputUsernameHint; /// No account yet? Register now /// /// In en, this message translates to: /// **'No account yet? Register now'** String get noAccountRegisterLink; /// Register /// /// In en, this message translates to: /// **'Register'** String get register; /// Open your account /// /// In en, this message translates to: /// **'Open your account'** String get openYourAccount; /// Please enter phone number /// /// In en, this message translates to: /// **'Please enter phone number'** String get inputPhoneHint; /// Please enter verification code /// /// In en, this message translates to: /// **'Please enter verification code'** String get inputVerificationCodeHint; /// Please input valid mobile phone number /// /// In en, this message translates to: /// **'Please input valid mobile phone number'** String get inputPhoneInvalid; /// Not really sent, just log in! /// /// In en, this message translates to: /// **'Not really sent, just log in!'** String get verificationButton; /// Get verification code /// /// In en, this message translates to: /// **'Get code'** String get getVerificationCode; /// No description provided for @confirm. /// /// In en, this message translates to: /// **'Confirm'** String get confirm; /// Reset login password /// /// In en, this message translates to: /// **'Reset Login Password'** String get resetLoginPassword; /// Registered Tips /// /// In en, this message translates to: /// **'Unregistered mobile phone number, please '** String get registeredTips; } class _DeerLocalizationsDelegate extends LocalizationsDelegate { const _DeerLocalizationsDelegate(); @override Future load(Locale locale) { return SynchronousFuture(lookupDeerLocalizations(locale)); } @override bool isSupported(Locale locale) => ['en', 'zh'].contains(locale.languageCode); @override bool shouldReload(_DeerLocalizationsDelegate old) => false; } DeerLocalizations lookupDeerLocalizations(Locale locale) { // Lookup logic when only language code is specified. switch (locale.languageCode) { case 'en': return DeerLocalizationsEn(); case 'zh': return DeerLocalizationsZh(); } throw FlutterError( 'DeerLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' 'an issue with the localizations generation tool. Please file an issue ' 'on GitHub with a reproducible sample app and the gen-l10n configuration ' 'that was used.'); } ================================================ FILE: lib/l10n/deer_localizations_en.dart ================================================ // ignore: unused_import import 'package:intl/intl.dart' as intl; import 'deer_localizations.dart'; // ignore_for_file: type=lint /// The translations for English (`en`). class DeerLocalizationsEn extends DeerLocalizations { DeerLocalizationsEn([String locale = 'en']) : super(locale); @override String get title => 'Flutter Deer'; @override String get verificationCodeLogin => 'Verification Code Login'; @override String get passwordLogin => 'Password Login'; @override String get login => 'Login'; @override String get forgotPasswordLink => 'Forgot Password'; @override String get inputPasswordHint => 'Please enter the password'; @override String get inputUsernameHint => 'Please input username'; @override String get noAccountRegisterLink => 'No account yet? Register now'; @override String get register => 'Register'; @override String get openYourAccount => 'Open your account'; @override String get inputPhoneHint => 'Please enter phone number'; @override String get inputVerificationCodeHint => 'Please enter verification code'; @override String get inputPhoneInvalid => 'Please input valid mobile phone number'; @override String get verificationButton => 'Not really sent, just log in!'; @override String get getVerificationCode => 'Get code'; @override String get confirm => 'Confirm'; @override String get resetLoginPassword => 'Reset Login Password'; @override String get registeredTips => 'Unregistered mobile phone number, please '; } ================================================ FILE: lib/l10n/deer_localizations_zh.dart ================================================ // ignore: unused_import import 'package:intl/intl.dart' as intl; import 'deer_localizations.dart'; // ignore_for_file: type=lint /// The translations for Chinese (`zh`). class DeerLocalizationsZh extends DeerLocalizations { DeerLocalizationsZh([String locale = 'zh']) : super(locale); @override String get title => 'Flutter Deer'; @override String get verificationCodeLogin => '验证码登录'; @override String get passwordLogin => '密码登录'; @override String get login => '登录'; @override String get forgotPasswordLink => '忘记密码'; @override String get inputPasswordHint => '请输入密码'; @override String get inputUsernameHint => '请输入账号'; @override String get noAccountRegisterLink => '还没账号?快去注册'; @override String get register => '注册'; @override String get openYourAccount => '开启你的账号'; @override String get inputPhoneHint => '请输入手机号'; @override String get inputVerificationCodeHint => '请输入验证码'; @override String get inputPhoneInvalid => '请输入有效的手机号'; @override String get verificationButton => '并没有真正发送哦,直接登录吧!'; @override String get getVerificationCode => '获取验证码'; @override String get confirm => '确认'; @override String get resetLoginPassword => '重置登录密码'; @override String get registeredTips => '提示:未注册账号的手机号,请先'; } ================================================ FILE: lib/l10n/intl_en.arb ================================================ { "@@last_modified": "2020-05-29T16:55:56.054100", "title": "Flutter Deer", "@title": { "description": "Title for the application", "type": "text", "placeholders": {} }, "verificationCodeLogin": "Verification Code Login", "@verificationCodeLogin": { "description": "Title for the Login page", "type": "text", "placeholders": {} }, "passwordLogin": "Password Login", "@passwordLogin": { "description": "Password Login", "type": "text", "placeholders": {} }, "login": "Login", "@login": { "description": "Login", "type": "text", "placeholders": {} }, "forgotPasswordLink": "Forgot Password", "@forgotPasswordLink": { "description": "Forgot Password", "type": "text", "placeholders": {} }, "inputPasswordHint": "Please enter the password", "@inputPasswordHint": { "description": "Please enter the password", "type": "text", "placeholders": {} }, "inputUsernameHint": "Please input username", "@inputUsernameHint": { "description": "Please input username", "type": "text", "placeholders": {} }, "noAccountRegisterLink": "No account yet? Register now", "@noAccountRegisterLink": { "description": "No account yet? Register now", "type": "text", "placeholders": {} }, "register": "Register", "@register": { "description": "Register", "type": "text", "placeholders": {} }, "openYourAccount": "Open your account", "@openYourAccount": { "description": "Open your account", "type": "text", "placeholders": {} }, "inputPhoneHint": "Please enter phone number", "@inputPhoneHint": { "description": "Please enter phone number", "type": "text", "placeholders": {} }, "inputVerificationCodeHint": "Please enter verification code", "@inputVerificationCodeHint": { "description": "Please enter verification code", "type": "text", "placeholders": {} }, "inputPhoneInvalid": "Please input valid mobile phone number", "@inputPhoneInvalid": { "description": "Please input valid mobile phone number", "type": "text", "placeholders": {} }, "verificationButton": "Not really sent, just log in!", "@verificationButton": { "description": "Not really sent, just log in!", "type": "text", "placeholders": {} }, "getVerificationCode": "Get code", "@getVerificationCode": { "description": "Get verification code", "type": "text", "placeholders": {} }, "confirm": "Confirm", "@confirm": { "type": "text", "placeholders": {} }, "resetLoginPassword": "Reset Login Password", "@resetLoginPassword": { "description": "Reset login password", "type": "text", "placeholders": {} }, "registeredTips": "Unregistered mobile phone number, please ", "@registeredTips": { "description": "Registered Tips", "type": "text", "placeholders": {} } } ================================================ FILE: lib/l10n/intl_zh.arb ================================================ { "title": "Flutter Deer", "verificationCodeLogin": "验证码登录", "passwordLogin": "密码登录", "login": "登录", "forgotPasswordLink": "忘记密码", "inputPasswordHint": "请输入密码", "inputUsernameHint": "请输入账号", "noAccountRegisterLink": "还没账号?快去注册", "register": "注册", "openYourAccount": "开启你的账号", "inputPhoneHint": "请输入手机号", "inputVerificationCodeHint": "请输入验证码", "inputPhoneInvalid": "请输入有效的手机号", "verificationButton": "并没有真正发送哦,直接登录吧!", "getVerificationCode": "获取验证码", "confirm": "确认", "resetLoginPassword": "重置登录密码", "registeredTips": "提示:未注册账号的手机号,请先" } ================================================ FILE: lib/login/login_router.dart ================================================ import 'package:fluro/fluro.dart'; import 'package:flutter_deer/routers/i_router.dart'; import 'page/login_page.dart'; import 'page/register_page.dart'; import 'page/reset_password_page.dart'; import 'page/sms_login_page.dart'; import 'page/update_password_page.dart'; class LoginRouter implements IRouterProvider{ static String loginPage = '/login'; static String registerPage = '/login/register'; static String smsLoginPage = '/login/smsLogin'; static String resetPasswordPage = '/login/resetPassword'; static String updatePasswordPage = '/login/updatePassword'; @override void initRouter(FluroRouter router) { router.define(loginPage, handler: Handler(handlerFunc: (_, __) => const LoginPage())); router.define(registerPage, handler: Handler(handlerFunc: (_, __) => const RegisterPage())); router.define(smsLoginPage, handler: Handler(handlerFunc: (_, __) => const SMSLoginPage())); router.define(resetPasswordPage, handler: Handler(handlerFunc: (_, __) => const ResetPasswordPage())); router.define(updatePasswordPage, handler: Handler(handlerFunc: (_, __) => const UpdatePasswordPage())); } } ================================================ FILE: lib/login/page/login_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_deer/login/widgets/my_text_field.dart'; import 'package:flutter_deer/res/constant.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/store/store_router.dart'; import 'package:flutter_deer/util/change_notifier_manage.dart'; import 'package:flutter_deer/util/other_utils.dart'; import 'package:flutter_deer/widgets/my_app_bar.dart'; import 'package:flutter_deer/widgets/my_button.dart'; import 'package:flutter_deer/widgets/my_scroll_view.dart'; import 'package:sp_util/sp_util.dart'; import '../../l10n/deer_localizations.dart'; import '../login_router.dart'; /// design/1注册登录/index.html class LoginPage extends StatefulWidget { const LoginPage({super.key}); @override _LoginPageState createState() => _LoginPageState(); } class _LoginPageState extends State with ChangeNotifierMixin { //定义一个controller final TextEditingController _nameController = TextEditingController(); final TextEditingController _passwordController = TextEditingController(); final FocusNode _nodeText1 = FocusNode(); final FocusNode _nodeText2 = FocusNode(); bool _clickable = false; @override Map?>? changeNotifier() { final List callbacks = [_verify]; return ?>{ _nameController: callbacks, _passwordController: callbacks, _nodeText1: null, _nodeText2: null, }; } @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { /// 显示状态栏和导航栏 SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: [SystemUiOverlay.top, SystemUiOverlay.bottom]); }); _nameController.text = SpUtil.getString(Constant.phone).nullSafe; } void _verify() { final String name = _nameController.text; final String password = _passwordController.text; bool clickable = true; if (name.isEmpty || name.length < 11) { clickable = false; } if (password.isEmpty || password.length < 6) { clickable = false; } /// 状态不一样再刷新,避免不必要的setState if (clickable != _clickable) { setState(() { _clickable = clickable; }); } } void _login() { SpUtil.putString(Constant.phone, _nameController.text); NavigatorUtils.push(context, StoreRouter.auditPage); } @override Widget build(BuildContext context) { return Scaffold( appBar: MyAppBar( isBack: false, actionName: DeerLocalizations.of(context)!.verificationCodeLogin, onPressed: () { NavigatorUtils.push(context, LoginRouter.smsLoginPage); }, ), body: MyScrollView( keyboardConfig: Utils.getKeyboardActionsConfig(context, [_nodeText1, _nodeText2]), padding: const EdgeInsets.only(left: 16.0, right: 16.0, top: 20.0), children: _buildBody, ), ); } List get _buildBody => [ Text( DeerLocalizations.of(context)!.passwordLogin, style: TextStyles.textBold26, ), Gaps.vGap16, MyTextField( key: const Key('phone'), focusNode: _nodeText1, controller: _nameController, maxLength: 11, keyboardType: TextInputType.phone, hintText: DeerLocalizations.of(context)!.inputUsernameHint, ), Gaps.vGap8, MyTextField( key: const Key('password'), keyName: 'password', focusNode: _nodeText2, isInputPwd: true, controller: _passwordController, keyboardType: TextInputType.visiblePassword, hintText: DeerLocalizations.of(context)!.inputPasswordHint, ), Gaps.vGap24, MyButton( key: const Key('login'), onPressed: _clickable ? _login : null, text: DeerLocalizations.of(context)!.login, ), Container( height: 40.0, alignment: Alignment.centerRight, child: GestureDetector( child: Text( DeerLocalizations.of(context)!.forgotPasswordLink, key: const Key('forgotPassword'), style: Theme.of(context).textTheme.titleSmall, ), onTap: () => NavigatorUtils.push(context, LoginRouter.resetPasswordPage), ), ), Gaps.vGap16, Container( alignment: Alignment.center, child: GestureDetector( child: Text( DeerLocalizations.of(context)!.noAccountRegisterLink, key: const Key('noAccountRegister'), style: TextStyle( color: Theme.of(context).primaryColor ), ), onTap: () => NavigatorUtils.push(context, LoginRouter.registerPage), ) ) ]; } ================================================ FILE: lib/login/page/register_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/login/widgets/my_text_field.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/util/change_notifier_manage.dart'; import 'package:flutter_deer/util/other_utils.dart'; import 'package:flutter_deer/util/toast_utils.dart'; import 'package:flutter_deer/widgets/my_app_bar.dart'; import 'package:flutter_deer/widgets/my_button.dart'; import 'package:flutter_deer/widgets/my_scroll_view.dart'; import '../../l10n/deer_localizations.dart'; /// design/1注册登录/index.html#artboard11 class RegisterPage extends StatefulWidget { const RegisterPage({super.key}); @override _RegisterPageState createState() => _RegisterPageState(); } class _RegisterPageState extends State with ChangeNotifierMixin { //定义一个controller final TextEditingController _nameController = TextEditingController(); final TextEditingController _vCodeController = TextEditingController(); final TextEditingController _passwordController = TextEditingController(); final FocusNode _nodeText1 = FocusNode(); final FocusNode _nodeText2 = FocusNode(); final FocusNode _nodeText3 = FocusNode(); bool _clickable = false; @override Map?>? changeNotifier() { final List callbacks = [_verify]; return ?>{ _nameController: callbacks, _vCodeController: callbacks, _passwordController: callbacks, _nodeText1: null, _nodeText2: null, _nodeText3: null, }; } void _verify() { final String name = _nameController.text; final String vCode = _vCodeController.text; final String password = _passwordController.text; bool clickable = true; if (name.isEmpty || name.length < 11) { clickable = false; } if (vCode.isEmpty || vCode.length < 6) { clickable = false; } if (password.isEmpty || password.length < 6) { clickable = false; } if (clickable != _clickable) { setState(() { _clickable = clickable; }); } } void _register() { Toast.show('点击注册'); } @override Widget build(BuildContext context) { return Scaffold( appBar: MyAppBar( title: DeerLocalizations.of(context)!.register, ), body: MyScrollView( keyboardConfig: Utils.getKeyboardActionsConfig(context, [_nodeText1, _nodeText2, _nodeText3]), crossAxisAlignment: CrossAxisAlignment.center, padding: const EdgeInsets.only(left: 16.0, right: 16.0, top: 20.0), children: _buildBody(), ), ); } List _buildBody() { return [ Text( DeerLocalizations.of(context)!.openYourAccount, style: TextStyles.textBold26, ), Gaps.vGap16, MyTextField( key: const Key('phone'), focusNode: _nodeText1, controller: _nameController, maxLength: 11, keyboardType: TextInputType.phone, hintText: DeerLocalizations.of(context)!.inputPhoneHint, ), Gaps.vGap8, MyTextField( key: const Key('vcode'), focusNode: _nodeText2, controller: _vCodeController, keyboardType: TextInputType.number, getVCode: () async { if (_nameController.text.length == 11) { Toast.show(DeerLocalizations.of(context)!.verificationButton); /// 一般可以在这里发送真正的请求,请求成功返回true return true; } else { Toast.show(DeerLocalizations.of(context)!.inputPhoneInvalid); return false; } }, maxLength: 6, hintText: DeerLocalizations.of(context)!.inputVerificationCodeHint, ), Gaps.vGap8, MyTextField( key: const Key('password'), keyName: 'password', focusNode: _nodeText3, isInputPwd: true, controller: _passwordController, keyboardType: TextInputType.visiblePassword, hintText: DeerLocalizations.of(context)!.inputPasswordHint, ), Gaps.vGap24, MyButton( key: const Key('register'), onPressed: _clickable ? _register : null, text: DeerLocalizations.of(context)!.register, ) ]; } } ================================================ FILE: lib/login/page/reset_password_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/login/widgets/my_text_field.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/util/change_notifier_manage.dart'; import 'package:flutter_deer/util/other_utils.dart'; import 'package:flutter_deer/util/toast_utils.dart'; import 'package:flutter_deer/widgets/my_app_bar.dart'; import 'package:flutter_deer/widgets/my_button.dart'; import 'package:flutter_deer/widgets/my_scroll_view.dart'; import '../../l10n/deer_localizations.dart'; /// design/1注册登录/index.html#artboard9 class ResetPasswordPage extends StatefulWidget { const ResetPasswordPage({super.key}); @override _ResetPasswordPageState createState() => _ResetPasswordPageState(); } class _ResetPasswordPageState extends State with ChangeNotifierMixin { //定义一个controller final TextEditingController _nameController = TextEditingController(); final TextEditingController _vCodeController = TextEditingController(); final TextEditingController _passwordController = TextEditingController(); final FocusNode _nodeText1 = FocusNode(); final FocusNode _nodeText2 = FocusNode(); final FocusNode _nodeText3 = FocusNode(); bool _clickable = false; @override Map?>? changeNotifier() { final List callbacks = [_verify]; return ?>{ _nameController: callbacks, _vCodeController: callbacks, _passwordController: callbacks, _nodeText1: null, _nodeText2: null, _nodeText3: null, }; } void _verify() { final String name = _nameController.text; final String vCode = _vCodeController.text; final String password = _passwordController.text; bool clickable = true; if (name.isEmpty || name.length < 11) { clickable = false; } if (vCode.isEmpty || vCode.length < 6) { clickable = false; } if (password.isEmpty || password.length < 6) { clickable = false; } if (clickable != _clickable) { setState(() { _clickable = clickable; }); } } void _reset() { Toast.show(DeerLocalizations.of(context)!.confirm); } @override Widget build(BuildContext context) { return Scaffold( appBar: MyAppBar( title: DeerLocalizations.of(context)!.forgotPasswordLink, ), body: MyScrollView( keyboardConfig: Utils.getKeyboardActionsConfig(context, [_nodeText1, _nodeText2, _nodeText3]), crossAxisAlignment: CrossAxisAlignment.center, padding: const EdgeInsets.only(left: 16.0, right: 16.0, top: 20.0), children: _buildBody(), ), ); } List _buildBody() { return [ Text( DeerLocalizations.of(context)!.resetLoginPassword, style: TextStyles.textBold26, ), Gaps.vGap16, MyTextField( focusNode: _nodeText1, controller: _nameController, maxLength: 11, keyboardType: TextInputType.phone, hintText: DeerLocalizations.of(context)!.inputPhoneHint, ), Gaps.vGap8, MyTextField( focusNode: _nodeText2, controller: _vCodeController, keyboardType: TextInputType.number, getVCode: () { return Future.value(true); }, maxLength: 6, hintText: DeerLocalizations.of(context)!.inputVerificationCodeHint, ), Gaps.vGap8, MyTextField( focusNode: _nodeText3, isInputPwd: true, controller: _passwordController, keyboardType: TextInputType.visiblePassword, hintText: DeerLocalizations.of(context)!.inputPasswordHint, ), Gaps.vGap24, MyButton( onPressed: _clickable ? _reset : null, text: DeerLocalizations.of(context)!.confirm, ) ]; } } ================================================ FILE: lib/login/page/sms_login_page.dart ================================================ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_deer/login/widgets/my_text_field.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/util/change_notifier_manage.dart'; import 'package:flutter_deer/util/other_utils.dart'; import 'package:flutter_deer/util/toast_utils.dart'; import 'package:flutter_deer/widgets/my_app_bar.dart'; import 'package:flutter_deer/widgets/my_button.dart'; import 'package:flutter_deer/widgets/my_scroll_view.dart'; import '../../l10n/deer_localizations.dart'; import '../login_router.dart'; /// design/1注册登录/index.html#artboard4 class SMSLoginPage extends StatefulWidget { const SMSLoginPage({super.key}); @override _SMSLoginPageState createState() => _SMSLoginPageState(); } class _SMSLoginPageState extends State with ChangeNotifierMixin { final TextEditingController _phoneController = TextEditingController(); final TextEditingController _vCodeController = TextEditingController(); final FocusNode _nodeText1 = FocusNode(); final FocusNode _nodeText2 = FocusNode(); bool _clickable = false; @override Map?>? changeNotifier() { final List callbacks = [_verify]; return ?>{ _phoneController: callbacks, _vCodeController: callbacks, _nodeText1: null, _nodeText2: null, }; } void _verify() { final String name = _phoneController.text; final String vCode = _vCodeController.text; bool clickable = true; if (name.isEmpty || name.length < 11) { clickable = false; } if (vCode.isEmpty || vCode.length < 6) { clickable = false; } if (clickable != _clickable) { setState(() { _clickable = clickable; }); } } void _login() { Toast.show('去登录......'); } @override Widget build(BuildContext context) { return Scaffold( appBar: const MyAppBar(), body: MyScrollView( keyboardConfig: Utils.getKeyboardActionsConfig(context, [_nodeText1, _nodeText2]), padding: const EdgeInsets.only(left: 16.0, right: 16.0, top: 20.0), children: _buildBody(), ), ); } List _buildBody() { return [ Text( DeerLocalizations.of(context)!.verificationCodeLogin, style: TextStyles.textBold26, ), Gaps.vGap16, MyTextField( focusNode: _nodeText1, controller: _phoneController, maxLength: 11, keyboardType: TextInputType.phone, hintText: DeerLocalizations.of(context)!.inputPhoneHint, ), Gaps.vGap8, MyTextField( focusNode: _nodeText2, controller: _vCodeController, maxLength: 6, keyboardType: TextInputType.number, hintText: DeerLocalizations.of(context)!.inputVerificationCodeHint, getVCode: () { Toast.show(DeerLocalizations.of(context)!.getVerificationCode); return Future.value(true); }, ), Gaps.vGap8, Container( alignment: Alignment.centerLeft, child: RichText( text: TextSpan( text: DeerLocalizations.of(context)!.registeredTips, style: Theme.of(context).textTheme.titleSmall?.copyWith(fontSize: Dimens.font_sp14), children: [ TextSpan( text: DeerLocalizations.of(context)!.register, style: TextStyle( color: Theme.of(context).colorScheme.error, ), recognizer: TapGestureRecognizer() ..onTap = () { NavigatorUtils.push(context, LoginRouter.registerPage); }, ), TextSpan(text: Utils.getCurrLocale() == 'zh' ? '。' : '.',), ], ), ), ), Gaps.vGap24, MyButton( onPressed: _clickable ? _login : null, text: DeerLocalizations.of(context)!.login, ), Container( height: 40.0, alignment: Alignment.centerRight, child: GestureDetector( child: Text( DeerLocalizations.of(context)!.forgotPasswordLink, style: Theme.of(context).textTheme.titleSmall, ), onTap: () => NavigatorUtils.push(context, LoginRouter.resetPasswordPage), ), ) ]; } } ================================================ FILE: lib/login/page/update_password_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/login/widgets/my_text_field.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/util/change_notifier_manage.dart'; import 'package:flutter_deer/util/other_utils.dart'; import 'package:flutter_deer/util/toast_utils.dart'; import 'package:flutter_deer/widgets/my_app_bar.dart'; import 'package:flutter_deer/widgets/my_button.dart'; import 'package:flutter_deer/widgets/my_scroll_view.dart'; /// design/1注册登录/index.html#artboard13 class UpdatePasswordPage extends StatefulWidget { const UpdatePasswordPage({super.key}); @override _UpdatePasswordPageState createState() => _UpdatePasswordPageState(); } class _UpdatePasswordPageState extends State with ChangeNotifierMixin { //定义一个controller final TextEditingController _oldPwdController = TextEditingController(); final TextEditingController _newPwdController = TextEditingController(); final FocusNode _nodeText1 = FocusNode(); final FocusNode _nodeText2 = FocusNode(); bool _clickable = false; @override Map?>? changeNotifier() { final List callbacks = [_verify]; return ?>{ _oldPwdController: callbacks, _newPwdController: callbacks, _nodeText1: null, _nodeText2: null, }; } void _verify() { final String oldPwd = _oldPwdController.text; final String newPwd = _newPwdController.text; bool clickable = true; if (oldPwd.isEmpty || oldPwd.length < 6) { clickable = false; } if (newPwd.isEmpty || newPwd.length < 6) { clickable = false; } if (clickable != _clickable) { setState(() { _clickable = clickable; }); } } void _confirm() { Toast.show('修改成功!'); NavigatorUtils.goBack(context); } @override Widget build(BuildContext context) { return Scaffold( appBar: const MyAppBar( title: '修改密码', ), body: MyScrollView( keyboardConfig: Utils.getKeyboardActionsConfig(context, [_nodeText1, _nodeText2]), crossAxisAlignment: CrossAxisAlignment.center, padding: const EdgeInsets.only(left: 16.0, right: 16.0, top: 20.0), children: [ const Text( '重置登录密码', style: TextStyles.textBold26, ), Gaps.vGap8, Text( '设置账号 15000000000', style: Theme.of(context).textTheme.titleSmall?.copyWith(fontSize: Dimens.font_sp12), ), Gaps.vGap32, MyTextField( isInputPwd: true, focusNode: _nodeText1, controller: _oldPwdController, keyboardType: TextInputType.visiblePassword, hintText: '请确认旧密码', ), Gaps.vGap8, MyTextField( isInputPwd: true, focusNode: _nodeText2, controller: _newPwdController, keyboardType: TextInputType.visiblePassword, hintText: '请输入新密码', ), Gaps.vGap24, MyButton( onPressed: _clickable ? _confirm : null, text: '确认', ) ], ), ); } } ================================================ FILE: lib/login/widgets/my_text_field.dart ================================================ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/util/device_utils.dart'; import 'package:flutter_deer/widgets/load_image.dart'; import 'package:flutter_deer/widgets/my_button.dart'; import '../../l10n/deer_localizations.dart'; /// 登录模块的输入框封装 class MyTextField extends StatefulWidget { const MyTextField({ super.key, required this.controller, this.maxLength = 16, this.autoFocus = false, this.keyboardType = TextInputType.text, this.hintText = '', this.focusNode, this.isInputPwd = false, this.getVCode, this.keyName }); final TextEditingController controller; final int maxLength; final bool autoFocus; final TextInputType keyboardType; final String hintText; final FocusNode? focusNode; final bool isInputPwd; final Future Function()? getVCode; /// 用于集成测试寻找widget final String? keyName; @override _MyTextFieldState createState() => _MyTextFieldState(); } class _MyTextFieldState extends State { bool _isShowPwd = false; bool _isShowDelete = false; bool _clickable = true; /// 倒计时秒数 final int _second = 30; /// 当前秒数 late int _currentSecond; StreamSubscription? _subscription; @override void initState() { /// 获取初始化值 _isShowDelete = widget.controller.text.isNotEmpty; /// 监听输入改变 widget.controller.addListener(isEmpty); super.initState(); } void isEmpty() { final bool isNotEmpty = widget.controller.text.isNotEmpty; /// 状态不一样在刷新,避免重复不必要的setState if (isNotEmpty != _isShowDelete) { setState(() { _isShowDelete = isNotEmpty; }); } } @override void dispose() { _subscription?.cancel(); widget.controller.removeListener(isEmpty); super.dispose(); } Future _getVCode() async { final bool isSuccess = await widget.getVCode!(); if (isSuccess) { setState(() { _currentSecond = _second; _clickable = false; }); _subscription = Stream.periodic(const Duration(seconds: 1), (int i) => i).take(_second).listen((int i) { setState(() { _currentSecond = _second - i - 1; _clickable = _currentSecond < 1; }); }); } } @override Widget build(BuildContext context) { final ThemeData themeData = Theme.of(context); final bool isDark = themeData.brightness == Brightness.dark; Widget textField = TextField( focusNode: widget.focusNode, maxLength: widget.maxLength, obscureText: widget.isInputPwd && !_isShowPwd, autofocus: widget.autoFocus, controller: widget.controller, textInputAction: TextInputAction.done, keyboardType: widget.keyboardType, // 数字、手机号限制格式为0到9, 密码限制不包含汉字 inputFormatters: (widget.keyboardType == TextInputType.number || widget.keyboardType == TextInputType.phone) ? [FilteringTextInputFormatter.allow(RegExp('[0-9]'))] : [FilteringTextInputFormatter.deny(RegExp('[\u4e00-\u9fa5]'))], decoration: InputDecoration( contentPadding: const EdgeInsets.symmetric(vertical: 16.0), hintText: widget.hintText, counterText: '', focusedBorder: UnderlineInputBorder( borderSide: BorderSide( color: themeData.primaryColor, width: 0.8, ), ), enabledBorder: UnderlineInputBorder( borderSide: BorderSide( color: Theme.of(context).dividerTheme.color!, width: 0.8, ), ), ), ); /// 个别Android机型(华为、vivo)的密码安全键盘不弹出问题(已知小米正常),临时修复方法:https://github.com/flutter/flutter/issues/68571 (issues/61446) /// 怀疑是安全键盘与三方输入法之间的切换冲突问题。 if (Device.isAndroid) { textField = Listener( onPointerDown: (e) => FocusScope.of(context).requestFocus(widget.focusNode), child: textField, ); } Widget? clearButton; if (_isShowDelete) { clearButton = Semantics( label: '清空', hint: '清空输入框', child: GestureDetector( child: LoadAssetImage('login/qyg_shop_icon_delete', key: Key('${widget.keyName}_delete'), width: 18.0, height: 40.0, ), onTap: () => widget.controller.text = '', ), ); } late Widget pwdVisible; if (widget.isInputPwd) { pwdVisible = Semantics( label: '密码可见开关', hint: '密码是否可见', child: GestureDetector( child: LoadAssetImage( _isShowPwd ? 'login/qyg_shop_icon_display' : 'login/qyg_shop_icon_hide', key: Key('${widget.keyName}_showPwd'), width: 18.0, height: 40.0, ), onTap: () { setState(() { _isShowPwd = !_isShowPwd; }); }, ), ); } late Widget getVCodeButton; if (widget.getVCode != null) { getVCodeButton = MyButton( key: const Key('getVerificationCode'), onPressed: _clickable ? _getVCode : null, fontSize: Dimens.font_sp12, text: _clickable ? DeerLocalizations.of(context)!.getVerificationCode : '($_currentSecond s)', textColor: themeData.primaryColor, disabledTextColor: isDark ? Colours.dark_text : Colors.white, backgroundColor: Colors.transparent, disabledBackgroundColor: isDark ? Colours.dark_text_gray : Colours.text_gray_c, radius: 1.0, minHeight: 26.0, minWidth: 76.0, padding: const EdgeInsets.symmetric(horizontal: 8.0), side: BorderSide( color: _clickable ? themeData.primaryColor : Colors.transparent, width: 0.8, ), ); } return Stack( alignment: Alignment.centerRight, children: [ textField, Row( mainAxisSize: MainAxisSize.min, children: [ /// _isShowDelete参数动态变化,为了不破坏树结构使用Visibility,false时放一个空Widget。 /// 对于其他参数,为初始配置参数,基本可以确定树结构,就不做空Widget处理。 Visibility( visible: _isShowDelete, child: clearButton ?? Gaps.empty, ), if (widget.isInputPwd) Gaps.hGap15, if (widget.isInputPwd) pwdVisible, if (widget.getVCode != null) Gaps.hGap15, if (widget.getVCode != null) getVCodeButton, ], ) ], ); } } ================================================ FILE: lib/main.dart ================================================ import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_deer/demo/demo_page.dart'; import 'package:flutter_deer/home/splash_page.dart'; import 'package:flutter_deer/net/dio_utils.dart'; import 'package:flutter_deer/net/intercept.dart'; import 'package:flutter_deer/res/constant.dart'; import 'package:flutter_deer/routers/not_found_page.dart'; import 'package:flutter_deer/routers/routers.dart'; import 'package:flutter_deer/setting/provider/locale_provider.dart'; import 'package:flutter_deer/setting/provider/theme_provider.dart'; import 'package:flutter_deer/util/device_utils.dart'; import 'package:flutter_deer/util/handle_error_utils.dart'; import 'package:flutter_deer/util/log_utils.dart'; import 'package:oktoast/oktoast.dart'; import 'package:provider/provider.dart'; import 'package:quick_actions/quick_actions.dart'; import 'package:sp_util/sp_util.dart'; import 'package:url_strategy/url_strategy.dart'; import 'package:window_manager/window_manager.dart'; import '../../l10n/deer_localizations.dart'; Future main() async { // debugProfileBuildsEnabled = true; // debugPaintLayerBordersEnabled = true; // debugProfilePaintsEnabled = true; // debugRepaintRainbowEnabled = true; if (Constant.inProduction) { /// Release环境时不打印debugPrint内容 debugPrint = (String? message, {int? wrapWidth}) {}; } /// 异常处理 handleError(() async { /// 确保初始化完成 WidgetsFlutterBinding.ensureInitialized(); if (Device.isDesktop) { await WindowManager.instance.ensureInitialized(); windowManager.waitUntilReadyToShow().then((_) async { /// 隐藏标题栏及操作按钮 // await windowManager.setTitleBarStyle( // TitleBarStyle.hidden, // windowButtonVisibility: false, // ); /// 设置桌面端窗口大小 await windowManager.setSize(const Size(400, 800)); await windowManager.setMinimumSize(const Size(400, 800)); /// 居中显示 await windowManager.center(); await windowManager.show(); await windowManager.setPreventClose(false); await windowManager.setSkipTaskbar(false); }); } /// 去除URL中的“#”(hash),仅针对Web。默认为setHashUrlStrategy /// 注意本地部署和远程部署时`web/index.html`中的base标签,https://github.com/flutter/flutter/issues/69760 setPathUrlStrategy(); /// sp初始化 await SpUtil.getInstance(); /// 1.22 预览功能: 在输入频率与显示刷新率不匹配情况下提供平滑的滚动效果 // GestureBinding.instance?.resamplingEnabled = true; runApp(MyApp()); }); /// 隐藏状态栏,导航栏。为启动页、引导页设置全屏显示。完成后还原。 SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); // TODO(weilu): 启动体验不佳。状态栏、导航栏在冷启动开始的一瞬间为黑色,且无法通过隐藏、修改颜色等方式进行处理。。。 // 相关问题跟踪:https://github.com/flutter/flutter/issues/73351 } class MyApp extends StatelessWidget { MyApp({super.key, this.home, this.theme}) { Log.init(); initDio(); Routes.initRoutes(); initQuickActions(); } final Widget? home; final ThemeData? theme; static GlobalKey navigatorKey = GlobalKey(); void initDio() { final List interceptors = []; /// 统一添加身份验证请求头 interceptors.add(AuthInterceptor()); /// 刷新Token interceptors.add(TokenInterceptor()); /// 打印Log(生产模式去除) if (!Constant.inProduction) { interceptors.add(LoggingInterceptor()); } /// 适配数据(根据自己的数据结构,可自行选择添加) interceptors.add(AdapterInterceptor()); configDio( baseUrl: 'https://api.github.com/', interceptors: interceptors, ); } void initQuickActions() { if (Device.isMobile) { const QuickActions quickActions = QuickActions(); if (Device.isIOS) { // Android每次是重新启动activity,所以放在了splash_page处理。 // 总体来说使用不方便,这种动态的方式在安卓中局限性高。这里仅做练习使用。 quickActions.initialize((String shortcutType) async { if (shortcutType == 'demo') { navigatorKey.currentState?.push(MaterialPageRoute( builder: (BuildContext context) => const DemoPage(), )); } }); } quickActions.setShortcutItems([ const ShortcutItem( type: 'demo', localizedTitle: 'Demo', icon: 'flutter_dash_black' ), ]); } } @override Widget build(BuildContext context) { final Widget app = MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => ThemeProvider()), ChangeNotifierProvider(create: (_) => LocaleProvider()) ], child: Consumer2( builder: (_, ThemeProvider provider, LocaleProvider localeProvider, __) { return _buildMaterialApp(provider, localeProvider); }, ), ); /// Toast 配置 return OKToast( backgroundColor: Colors.black54, textPadding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 10.0), radius: 20.0, position: ToastPosition.bottom, child: app ); } Widget _buildMaterialApp(ThemeProvider provider, LocaleProvider localeProvider) { return MaterialApp( title: 'Flutter Deer', // showPerformanceOverlay: true, //显示性能标签 // debugShowCheckedModeBanner: false, // 去除右上角debug的标签 // checkerboardRasterCacheImages: true, // showSemanticsDebugger: true, // 显示语义视图 // checkerboardOffscreenLayers: true, // 检查离屏渲染 theme: theme ?? provider.getTheme(), darkTheme: provider.getTheme(isDarkMode: true), themeMode: provider.getThemeMode(), home: home ?? const SplashPage(), onGenerateRoute: Routes.router.generator, localizationsDelegates: DeerLocalizations.localizationsDelegates, supportedLocales: DeerLocalizations.supportedLocales, locale: localeProvider.locale, navigatorKey: navigatorKey, builder: (BuildContext context, Widget? child) { /// 保证文字大小不受手机系统设置影响 https://www.kikt.top/posts/flutter/layout/dynamic-text/ return MediaQuery( data: MediaQuery.of(context).copyWith(textScaler: TextScaler.noScaling), child: child!, ); }, /// 因为使用了fluro,这里设置主要针对Web onUnknownRoute: (_) { return MaterialPageRoute( builder: (BuildContext context) => const NotFoundPage(), ); }, restorationScopeId: 'app', ); } } ================================================ FILE: lib/mvp/base_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/mvp/base_presenter.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/util/log_utils.dart'; import 'package:flutter_deer/util/toast_utils.dart'; import 'package:flutter_deer/widgets/progress_dialog.dart'; import 'mvps.dart'; mixin BasePageMixin on State implements IMvpView { P? presenter; P createPresenter(); @override BuildContext getContext() { return context; } @override void closeProgress() { if (mounted && _isShowDialog) { _isShowDialog = false; NavigatorUtils.goBack(context); } } bool _isShowDialog = false; @override void showProgress() { /// 避免重复弹出 if (mounted && !_isShowDialog) { _isShowDialog = true; try { showDialog( context: context, barrierDismissible: false, barrierColor: const Color(0x00FFFFFF), // 默认dialog背景色为半透明黑色,这里修改为透明(1.20添加属性) builder:(_) { return WillPopScope( onWillPop: () async { // 拦截到返回键,证明dialog被手动关闭 _isShowDialog = false; return Future.value(true); }, child: buildProgress(), ); }, ); } catch(e) { /// 异常原因主要是页面没有build完成就调用Progress。 debugPrint(e.toString()); } } } @override void showToast(String string) { Toast.show(string); } /// 可自定义Progress Widget buildProgress() => const ProgressDialog(hintText: '正在加载...'); @override void didChangeDependencies() { presenter?.didChangeDependencies(); Log.d('$T ==> didChangeDependencies'); super.didChangeDependencies(); } @override void dispose() { presenter?.dispose(); Log.d('$T ==> dispose'); super.dispose(); } @override void deactivate() { presenter?.deactivate(); Log.d('$T ==> deactivate'); super.deactivate(); } @override void didUpdateWidget(T oldWidget) { presenter?.didUpdateWidgets(oldWidget); Log.d('$T ==> didUpdateWidgets'); super.didUpdateWidget(oldWidget); } @override void initState() { Log.d('$T ==> initState'); presenter = createPresenter(); presenter?.view = this; presenter?.initState(); super.initState(); } } ================================================ FILE: lib/mvp/base_page_presenter.dart ================================================ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:flutter_deer/mvp/base_presenter.dart'; import 'package:flutter_deer/net/net.dart'; import 'mvps.dart'; class BasePagePresenter extends BasePresenter { BasePagePresenter() { _cancelToken = CancelToken(); } late CancelToken _cancelToken; @override void dispose() { /// 销毁时,将请求取消 if (!_cancelToken.isCancelled) { _cancelToken.cancel(); } } /// 返回Future 适用于刷新,加载更多 Future requestNetwork(Method method, { required String url, bool isShow = true, bool isClose = true, NetSuccessCallback? onSuccess, NetErrorCallback? onError, dynamic params, Map? queryParameters, CancelToken? cancelToken, Options? options, }) { if (isShow) { view.showProgress(); } return DioUtils.instance.requestNetwork(method, url, params: params, queryParameters: queryParameters, options: options, cancelToken: cancelToken?? _cancelToken, onSuccess: (data) { if (isClose) { view.closeProgress(); } onSuccess?.call(data); }, onError: (code, msg) { _onError(code, msg, onError); }, ); } void asyncRequestNetwork(Method method, { required String url, bool isShow = true, bool isClose = true, NetSuccessCallback? onSuccess, NetErrorCallback? onError, dynamic params, Map? queryParameters, CancelToken? cancelToken, Options? options, }) { if (isShow) { view.showProgress(); } DioUtils.instance.asyncRequestNetwork(method, url, params: params, queryParameters: queryParameters, options: options, cancelToken: cancelToken?? _cancelToken, onSuccess: (data) { if (isClose) { view.closeProgress(); } onSuccess?.call(data); }, onError: (code, msg) { _onError(code, msg, onError); }, ); } /// 上传图片实现 Future uploadImg(File image) async { String imgPath = ''; try{ final String path = image.path; final String name = path.substring(path.lastIndexOf('/') + 1); final FormData formData = FormData.fromMap({ 'uploadIcon': await MultipartFile.fromFile(path, filename: name) }); await requestNetwork(Method.post, url: HttpApi.upload, params: formData, onSuccess: (data) { imgPath = data ?? ''; } ); } catch(e) { view.showToast('图片上传失败!'); } return imgPath; } void _onError(int code, String msg, NetErrorCallback? onError) { /// 异常时直接关闭加载圈,不受isClose影响 view.closeProgress(); if (code != ExceptionHandle.cancel_error) { view.showToast(msg); } /// 页面如果dispose,则不回调onError if (onError != null) { onError(code, msg); } } } ================================================ FILE: lib/mvp/base_presenter.dart ================================================ import 'mvps.dart'; class BasePresenter extends IPresenter { late V view; @override void deactivate() {} @override void didChangeDependencies() {} @override void didUpdateWidgets(W oldWidget) {} @override void dispose() {} @override void initState() {} } ================================================ FILE: lib/mvp/i_lifecycle.dart ================================================ abstract class ILifecycle { void initState(); void didChangeDependencies(); void didUpdateWidgets(W oldWidget); void deactivate(); void dispose(); } ================================================ FILE: lib/mvp/mvps.dart ================================================ import 'package:flutter/material.dart'; import 'i_lifecycle.dart'; abstract class IMvpView { BuildContext getContext(); /// 显示Progress void showProgress(); /// 关闭Progress void closeProgress(); /// 展示Toast void showToast(String string); } abstract class IPresenter extends ILifecycle {} ================================================ FILE: lib/mvp/power_presenter.dart ================================================ import 'package:flutter_deer/mvp/base_page.dart'; import 'package:flutter_deer/mvp/base_page_presenter.dart'; import 'package:flutter_deer/mvp/base_presenter.dart'; /// 管理多个Presenter,实现复用。 class PowerPresenter extends BasePresenter { PowerPresenter(BasePageMixin state) { _state = state; } late BasePageMixin _state; List _presenters = []; void requestPresenter(List presenters) { _presenters = presenters; _presenters.forEach(_requestPresenter); } void _requestPresenter(BasePagePresenter presenter) { presenter.view = _state; } @override void deactivate() { _presenters.forEach(_deactivate); } void _deactivate(BasePagePresenter presenter) { presenter.deactivate(); } @override void didChangeDependencies() { _presenters.forEach(_didChangeDependencies); } void _didChangeDependencies(BasePagePresenter presenter) { presenter.didChangeDependencies(); } @override void didUpdateWidgets(W oldWidget) { void didUpdateWidgets(BasePagePresenter presenter) { presenter.didUpdateWidgets(oldWidget); } _presenters.forEach(didUpdateWidgets); } @override void dispose() { _presenters.forEach(_dispose); } void _dispose(BasePagePresenter presenter) { presenter.dispose(); } @override void initState() { _presenters.forEach(_initState); } void _initState(BasePagePresenter presenter) { presenter.initState(); } } ================================================ FILE: lib/net/base_entity.dart ================================================ import 'package:flutter_deer/generated/json/base/json_convert_content.dart'; import 'package:flutter_deer/res/constant.dart'; class BaseEntity { BaseEntity(this.code, this.message, this.data); BaseEntity.fromJson(Map json) { code = json[Constant.code] as int?; message = json[Constant.message] as String; if (json.containsKey(Constant.data)) { data = _generateOBJ(json[Constant.data] as Object?); } } int? code; late String message; T? data; T? _generateOBJ(Object? json) { if (json == null) { return null; } if (T.toString() == 'String') { return json.toString() as T; } else if (T.toString() == 'Map') { return json as T; } else { /// List类型数据由fromJsonAsT判断处理 return JsonConvert.fromJsonAsT(json); } } } ================================================ FILE: lib/net/dio_utils.dart ================================================ import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_deer/res/constant.dart'; import 'package:flutter_deer/util/log_utils.dart'; import 'base_entity.dart'; import 'error_handle.dart'; /// 默认dio配置 Duration _connectTimeout = const Duration(seconds: 15); Duration _receiveTimeout = const Duration(seconds: 15); Duration _sendTimeout = const Duration(seconds: 10); String _baseUrl = ''; List _interceptors = []; /// 初始化Dio配置 void configDio({ Duration? connectTimeout, Duration? receiveTimeout, Duration? sendTimeout, String? baseUrl, List? interceptors, }) { _connectTimeout = connectTimeout ?? _connectTimeout; _receiveTimeout = receiveTimeout ?? _receiveTimeout; _sendTimeout = sendTimeout ?? _sendTimeout; _baseUrl = baseUrl ?? _baseUrl; _interceptors = interceptors ?? _interceptors; } typedef NetSuccessCallback = void Function(T data); typedef NetSuccessListCallback = void Function(List data); typedef NetErrorCallback = void Function(int code, String msg); /// @weilu https://github.com/simplezhli class DioUtils { factory DioUtils() => _singleton; DioUtils._() { final BaseOptions options = BaseOptions( connectTimeout: _connectTimeout, receiveTimeout: _receiveTimeout, sendTimeout: _sendTimeout, /// dio默认json解析,这里指定返回UTF8字符串,自己处理解析。(可也以自定义Transformer实现) responseType: ResponseType.plain, validateStatus: (_) { // 不使用http状态码判断状态,使用AdapterInterceptor来处理(适用于标准REST风格) return true; }, baseUrl: _baseUrl, // contentType: Headers.formUrlEncodedContentType, // 适用于post form表单提交 ); _dio = Dio(options); /// Fiddler抓包代理配置 https://www.jianshu.com/p/d831b1f7c45b // _dio.httpClientAdapter = IOHttpClientAdapter()..onHttpClientCreate = (HttpClient client) { // client.findProxy = (uri) { // //proxy all request to localhost:8888 // return 'PROXY 10.41.0.132:8888'; // }; // return client; // }; /// 添加拦截器 void addInterceptor(Interceptor interceptor) { _dio.interceptors.add(interceptor); } _interceptors.forEach(addInterceptor); } static final DioUtils _singleton = DioUtils._(); static DioUtils get instance => DioUtils(); static late Dio _dio; Dio get dio => _dio; // 数据返回格式统一,统一处理异常 Future> _request(String method, String url, { Object? data, Map? queryParameters, CancelToken? cancelToken, Options? options, }) async { final Response response = await _dio.request( url, data: data, queryParameters: queryParameters, options: _checkOptions(method, options), cancelToken: cancelToken, ); try { final String data = response.data.toString(); /// 集成测试无法使用 isolate https://github.com/flutter/flutter/issues/24703 /// 使用compute条件:数据大于10KB(粗略使用10 * 1024)且当前不是集成测试(后面可能会根据Web环境进行调整) /// 主要目的减少不必要的性能开销 final bool isCompute = !Constant.isDriverTest && data.length > 10 * 1024; debugPrint('isCompute:$isCompute'); final Map map = isCompute ? await compute(parseData, data) : parseData(data); return BaseEntity.fromJson(map); } catch(e) { debugPrint(e.toString()); return BaseEntity(ExceptionHandle.parse_error, '数据解析错误!', null); } } Options _checkOptions(String method, Options? options) { options ??= Options(); options.method = method; return options; } Future requestNetwork(Method method, String url, { NetSuccessCallback? onSuccess, NetErrorCallback? onError, Object? params, Map? queryParameters, CancelToken? cancelToken, Options? options, }) { return _request(method.value, url, data: params, queryParameters: queryParameters, options: options, cancelToken: cancelToken, ).then((BaseEntity result) { if (result.code == 0) { onSuccess?.call(result.data); } else { _onError(result.code, result.message, onError); } }, onError: (dynamic e) { _cancelLogPrint(e, url); final NetError error = ExceptionHandle.handleException(e); _onError(error.code, error.msg, onError); }); } /// 统一处理(onSuccess返回T对象,onSuccessList返回 List) void asyncRequestNetwork(Method method, String url, { NetSuccessCallback? onSuccess, NetErrorCallback? onError, Object? params, Map? queryParameters, CancelToken? cancelToken, Options? options, }) { Stream.fromFuture(_request(method.value, url, data: params, queryParameters: queryParameters, options: options, cancelToken: cancelToken, )).asBroadcastStream() .listen((result) { if (result.code == 0) { if (onSuccess != null) { onSuccess(result.data); } } else { _onError(result.code, result.message, onError); } }, onError: (dynamic e) { _cancelLogPrint(e, url); final NetError error = ExceptionHandle.handleException(e); _onError(error.code, error.msg, onError); }); } void _cancelLogPrint(dynamic e, String url) { if (e is DioException && CancelToken.isCancel(e)) { Log.e('取消请求接口: $url'); } } void _onError(int? code, String msg, NetErrorCallback? onError) { if (code == null) { code = ExceptionHandle.unknown_error; msg = '未知异常'; } Log.e('接口请求异常: code: $code, mag: $msg'); onError?.call(code, msg); } } Map parseData(String data) { return json.decode(data) as Map; } enum Method { get, post, put, patch, delete, head } /// 使用拓展枚举替代 switch判断取值 /// https://zhuanlan.zhihu.com/p/98545689 extension MethodExtension on Method { String get value => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD'][index]; } ================================================ FILE: lib/net/error_handle.dart ================================================ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; class ExceptionHandle { static const int success = 200; static const int success_not_content = 204; static const int not_modified = 304; static const int unauthorized = 401; static const int forbidden = 403; static const int not_found = 404; static const int net_error = 1000; static const int parse_error = 1001; static const int socket_error = 1002; static const int http_error = 1003; static const int connect_timeout_error = 1004; static const int send_timeout_error = 1005; static const int receive_timeout_error = 1006; static const int cancel_error = 1007; static const int unknown_error = 9999; static final Map _errorMap = { net_error : NetError(net_error, '网络异常,请检查你的网络!'), parse_error : NetError(parse_error, '数据解析错误!'), socket_error : NetError(socket_error, '网络异常,请检查你的网络!'), http_error : NetError(http_error, '服务器异常,请稍后重试!'), connect_timeout_error : NetError(connect_timeout_error, '连接超时!'), send_timeout_error : NetError(send_timeout_error, '请求超时!'), receive_timeout_error : NetError(receive_timeout_error, '响应超时!'), cancel_error : NetError(cancel_error, '取消请求'), unknown_error : NetError(unknown_error, '未知异常'), }; static NetError handleException(dynamic error) { debugPrint(error.toString()); if (error is DioException) { if (error.type.errorCode == 0) { return _handleException(error.error); } else { return _errorMap[error.type.errorCode]!; } } else { return _handleException(error); } } static NetError _handleException(dynamic error) { int errorCode = unknown_error; if (error is SocketException) { errorCode = socket_error; } if (error is HttpException) { errorCode = http_error; } if (error is FormatException) { errorCode = parse_error; } return _errorMap[errorCode]!; } } class NetError{ NetError(this.code, this.msg); int code; String msg; } extension DioErrorTypeExtension on DioExceptionType { int get errorCode => [ ExceptionHandle.connect_timeout_error, ExceptionHandle.send_timeout_error, ExceptionHandle.receive_timeout_error, 0, 0, ExceptionHandle.cancel_error, 0, ExceptionHandle.unknown_error, ][index]; } ================================================ FILE: lib/net/http_api.dart ================================================ class HttpApi{ static const String users = 'users/simplezhli'; static const String search = 'search/repositories'; static const String subscriptions = 'users/simplezhli/subscriptions'; static const String upload = 'uuc/upload-inco'; } ================================================ FILE: lib/net/intercept.dart ================================================ import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:flutter_deer/res/constant.dart'; import 'package:flutter_deer/util/device_utils.dart'; import 'package:flutter_deer/util/log_utils.dart'; import 'package:flutter_deer/util/other_utils.dart'; import 'package:sp_util/sp_util.dart'; import 'package:sprintf/sprintf.dart'; import 'dio_utils.dart'; import 'error_handle.dart'; class AuthInterceptor extends Interceptor { @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { final String accessToken = SpUtil.getString(Constant.accessToken).nullSafe; if (accessToken.isNotEmpty) { options.headers['Authorization'] = 'token $accessToken'; } if (!Device.isWeb) { // https://developer.github.com/v3/#user-agent-required options.headers['User-Agent'] = 'Mozilla/5.0'; } super.onRequest(options, handler); } } class TokenInterceptor extends QueuedInterceptor { Dio? _tokenDio; Future getToken() async { final Map params = {}; params['refresh_token'] = SpUtil.getString(Constant.refreshToken).nullSafe; try { _tokenDio ??= Dio(); _tokenDio!.options = DioUtils.instance.dio.options; final Response response = await _tokenDio!.post('lgn/refreshToken', data: params); if (response.statusCode == ExceptionHandle.success) { return (json.decode(response.data.toString()) as Map)['access_token'] as String; } } catch(e) { Log.e('刷新Token失败!'); } return null; } @override Future onResponse(Response response, ResponseInterceptorHandler handler) async { //401代表token过期 if (response.statusCode == ExceptionHandle.unauthorized) { Log.d('-----------自动刷新Token------------'); final String? accessToken = await getToken(); // 获取新的accessToken Log.e('-----------NewToken: $accessToken ------------'); SpUtil.putString(Constant.accessToken, accessToken.nullSafe); if (accessToken != null) { // 重新请求失败接口 final RequestOptions request = response.requestOptions; request.headers['Authorization'] = 'Bearer $accessToken'; final Options options = Options( headers: request.headers, method: request.method, ); try { Log.e('----------- 重新请求接口 ------------'); /// 避免重复执行拦截器,使用tokenDio final Response response = await _tokenDio!.request(request.path, data: request.data, queryParameters: request.queryParameters, cancelToken: request.cancelToken, options: options, onReceiveProgress: request.onReceiveProgress, ); return handler.next(response); } on DioException catch (e) { return handler.reject(e); } } } super.onResponse(response, handler); } } class LoggingInterceptor extends Interceptor{ late DateTime _startTime; late DateTime _endTime; @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { _startTime = DateTime.now(); Log.d('----------Start----------'); if (options.queryParameters.isEmpty) { Log.d('RequestUrl: ${options.baseUrl}${options.path}'); } else { Log.d('RequestUrl: ${options.baseUrl}${options.path}?${Transformer.urlEncodeMap(options.queryParameters)}'); } Log.d('RequestMethod: ${options.method}'); Log.d('RequestHeaders:${options.headers}'); Log.d('RequestContentType: ${options.contentType}'); Log.d('RequestData: ${options.data}'); super.onRequest(options, handler); } @override void onResponse(Response response, ResponseInterceptorHandler handler) { _endTime = DateTime.now(); final int duration = _endTime.difference(_startTime).inMilliseconds; if (response.statusCode == ExceptionHandle.success) { Log.d('ResponseCode: ${response.statusCode}'); } else { Log.e('ResponseCode: ${response.statusCode}'); } // 输出结果 Log.json(response.data.toString()); Log.d('----------End: $duration 毫秒----------'); super.onResponse(response, handler); } @override void onError(DioException err, ErrorInterceptorHandler handler) { Log.d('----------Error-----------'); super.onError(err, handler); } } class AdapterInterceptor extends Interceptor{ static const String _kMsg = 'msg'; static const String _kSlash = "'"; static const String _kMessage = 'message'; static const String _kDefaultText = '无返回信息'; static const String _kNotFound = '未找到查询信息'; static const String _kFailureFormat = '{"code":%d,"message":"%s"}'; static const String _kSuccessFormat = '{"code":0,"data":%s,"message":""}'; @override void onResponse(Response response, ResponseInterceptorHandler handler) { final Response r = adapterData(response); super.onResponse(r, handler); } @override void onError(DioException err, ErrorInterceptorHandler handler) { if (err.response != null) { adapterData(err.response!); } super.onError(err, handler); } Response adapterData(Response response) { String result; String content = response.data?.toString() ?? ''; /// 成功时,直接格式化返回 if (response.statusCode == ExceptionHandle.success || response.statusCode == ExceptionHandle.success_not_content) { if (content.isEmpty) { content = _kDefaultText; } result = sprintf(_kSuccessFormat, [content]); response.statusCode = ExceptionHandle.success; } else { if (response.statusCode == ExceptionHandle.not_found) { /// 错误数据格式化后,按照成功数据返回 result = sprintf(_kFailureFormat, [response.statusCode, _kNotFound]); response.statusCode = ExceptionHandle.success; } else { if (content.isEmpty) { // 一般为网络断开等异常 result = content; } else { String msg; try { content = content.replaceAll(r'\', ''); if (_kSlash == content.substring(0, 1)) { content = content.substring(1, content.length - 1); } final Map map = json.decode(content) as Map; if (map.containsKey(_kMessage)) { msg = map[_kMessage] as String; } else if (map.containsKey(_kMsg)) { msg = map[_kMsg] as String; } else { msg = '未知异常'; } result = sprintf(_kFailureFormat, [response.statusCode, msg]); // 401 token失效时,单独处理,其他一律为成功 if (response.statusCode == ExceptionHandle.unauthorized) { response.statusCode = ExceptionHandle.unauthorized; } else { response.statusCode = ExceptionHandle.success; } } catch (e) { // Log.d('异常信息:$e'); // 解析异常直接按照返回原数据处理(一般为返回500,503 HTML页面代码) result = sprintf(_kFailureFormat, [response.statusCode, '服务器异常(${response.statusCode})']); } } } } response.data = result; return response; } } ================================================ FILE: lib/net/net.dart ================================================ export 'dio_utils.dart'; export 'error_handle.dart'; export 'http_api.dart'; ================================================ FILE: lib/order/iview/order_search_iview.dart ================================================ import 'package:flutter_deer/mvp/mvps.dart'; import 'package:flutter_deer/order/models/search_entity.dart'; import 'package:flutter_deer/order/provider/base_list_provider.dart'; abstract class OrderSearchIMvpView implements IMvpView { BaseListProvider get provider; } ================================================ FILE: lib/order/models/search_entity.dart ================================================ import 'package:flutter_deer/generated/json/base/json_field.dart'; import 'package:flutter_deer/generated/json/search_entity.g.dart'; @JsonSerializable() class SearchEntity { SearchEntity(); factory SearchEntity.fromJson(Map json) => $SearchEntityFromJson(json); Map toJson() => $SearchEntityToJson(this); @JSONField(name: 'total_count') int? totalCount; @JSONField(name: 'incomplete_results') bool? incompleteResults; List? items; } @JsonSerializable() class SearchItems { SearchItems(); factory SearchItems.fromJson(Map json) => $SearchItemsFromJson(json); Map toJson() => $SearchItemsToJson(this); int? id; @JSONField(name: 'node_id') String? nodeId; String? name; @JSONField(name: 'full_name') String? fullName; bool? private; SearchItemsOwner? owner; @JSONField(name: 'html_url') String? htmlUrl; String? description; bool? fork; String? url; @JSONField(name: 'forks_url') String? forksUrl; @JSONField(name: 'keys_url') String? keysUrl; @JSONField(name: 'collaborators_url') String? collaboratorsUrl; @JSONField(name: 'teams_url') String? teamsUrl; @JSONField(name: 'hooks_url') String? hooksUrl; @JSONField(name: 'issue_events_url') String? issueEventsUrl; @JSONField(name: 'events_url') String? eventsUrl; @JSONField(name: 'assignees_url') String? assigneesUrl; @JSONField(name: 'branches_url') String? branchesUrl; @JSONField(name: 'tags_url') String? tagsUrl; @JSONField(name: 'blobs_url') String? blobsUrl; @JSONField(name: 'git_tags_url') String? gitTagsUrl; @JSONField(name: 'git_refs_url') String? gitRefsUrl; @JSONField(name: 'trees_url') String? treesUrl; @JSONField(name: 'statuses_url') String? statusesUrl; @JSONField(name: 'languages_url') String? languagesUrl; @JSONField(name: 'stargazers_url') String? stargazersUrl; @JSONField(name: 'contributors_url') String? contributorsUrl; @JSONField(name: 'subscribers_url') String? subscribersUrl; @JSONField(name: 'subscription_url') String? subscriptionUrl; @JSONField(name: 'commits_url') String? commitsUrl; @JSONField(name: 'git_commits_url') String? gitCommitsUrl; @JSONField(name: 'comments_url') String? commentsUrl; @JSONField(name: 'issue_comment_url') String? issueCommentUrl; @JSONField(name: 'contents_url') String? contentsUrl; @JSONField(name: 'compare_url') String? compareUrl; @JSONField(name: 'merges_url') String? mergesUrl; @JSONField(name: 'archive_url') String? archiveUrl; @JSONField(name: 'downloads_url') String? downloadsUrl; @JSONField(name: 'issues_url') String? issuesUrl; @JSONField(name: 'pulls_url') String? pullsUrl; @JSONField(name: 'milestones_url') String? milestonesUrl; @JSONField(name: 'notifications_url') String? notificationsUrl; @JSONField(name: 'labels_url') String? labelsUrl; @JSONField(name: 'releases_url') String? releasesUrl; @JSONField(name: 'deployments_url') String? deploymentsUrl; @JSONField(name: 'created_at') String? createdAt; @JSONField(name: 'updated_at') String? updatedAt; @JSONField(name: 'pushed_at') String? pushedAt; @JSONField(name: 'git_url') String? gitUrl; @JSONField(name: 'ssh_url') String? sshUrl; @JSONField(name: 'clone_url') String? cloneUrl; @JSONField(name: 'svn_url') String? svnUrl; String? homepage; int? size; @JSONField(name: 'stargazers_count') int? stargazersCount; @JSONField(name: 'watchers_count') int? watchersCount; String? language; @JSONField(name: 'has_issues') bool? hasIssues; @JSONField(name: 'has_projects') bool? hasProjects; @JSONField(name: 'has_downloads') bool? hasDownloads; @JSONField(name: 'has_wiki') bool? hasWiki; @JSONField(name: 'has_pages') bool? hasPages; @JSONField(name: 'forks_count') int? forksCount; bool? archived; bool? disabled; @JSONField(name: 'open_issues_count') int? openIssuesCount; SearchItemsLicense? license; int? forks; @JSONField(name: 'open_issues') int? openIssues; int? watchers; @JSONField(name: 'default_branch') String? defaultBranch; double? score; } @JsonSerializable() class SearchItemsOwner { SearchItemsOwner(); factory SearchItemsOwner.fromJson(Map json) => $SearchItemsOwnerFromJson(json); Map toJson() => $SearchItemsOwnerToJson(this); String? login; int? id; @JSONField(name: 'node_id') String? nodeId; @JSONField(name: 'avatar_url') String? avatarUrl; @JSONField(name: 'gravatar_id') String? gravatarId; String? url; @JSONField(name: 'html_url') String? htmlUrl; @JSONField(name: 'followers_url') String? followersUrl; @JSONField(name: 'following_url') String? followingUrl; @JSONField(name: 'gists_url') String? gistsUrl; @JSONField(name: 'starred_url') String? starredUrl; @JSONField(name: 'subscriptions_url') String? subscriptionsUrl; @JSONField(name: 'organizations_url') String? organizationsUrl; @JSONField(name: 'repos_url') String? reposUrl; @JSONField(name: 'events_url') String? eventsUrl; @JSONField(name: 'received_events_url') String? receivedEventsUrl; String? type; @JSONField(name: 'site_admin') bool? siteAdmin; } @JsonSerializable() class SearchItemsLicense { SearchItemsLicense(); factory SearchItemsLicense.fromJson(Map json) => $SearchItemsLicenseFromJson(json); Map toJson() => $SearchItemsLicenseToJson(this); String? key; String? name; @JSONField(name: 'spdx_id') String? spdxId; String? url; @JSONField(name: 'node_id') String? nodeId; } ================================================ FILE: lib/order/order_router.dart ================================================ import 'package:fluro/fluro.dart'; import 'package:flutter_deer/routers/i_router.dart'; import 'page/order_info_page.dart'; import 'page/order_page.dart'; import 'page/order_search_page.dart'; import 'page/order_track_page.dart'; class OrderRouter implements IRouterProvider{ static String orderPage = '/order'; static String orderInfoPage = '/order/info'; static String orderSearchPage = '/order/search'; static String orderTrackPage = '/order/track'; @override void initRouter(FluroRouter router) { router.define(orderPage, handler: Handler(handlerFunc: (_, __) => const OrderPage())); router.define(orderInfoPage, handler: Handler(handlerFunc: (_, __) => const OrderInfoPage())); router.define(orderSearchPage, handler: Handler(handlerFunc: (_, __) => const OrderSearchPage())); router.define(orderTrackPage, handler: Handler(handlerFunc: (_, __) => const OrderTrackPage())); } } ================================================ FILE: lib/order/page/order_info_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/util/device_utils.dart'; import 'package:flutter_deer/util/other_utils.dart'; import 'package:flutter_deer/util/theme_utils.dart'; import 'package:flutter_deer/widgets/load_image.dart'; import 'package:flutter_deer/widgets/my_app_bar.dart'; import 'package:flutter_deer/widgets/my_button.dart'; import 'package:flutter_deer/widgets/my_scroll_view.dart'; import '../order_router.dart'; /// design/3订单/index.html#artboard10 class OrderInfoPage extends StatefulWidget { const OrderInfoPage({super.key}); @override _OrderInfoPageState createState() => _OrderInfoPageState(); } class _OrderInfoPageState extends State { @override Widget build(BuildContext context) { final Color red = Theme.of(context).colorScheme.error; final bool isDark = context.isDark; final Widget bottomMenu = Container( height: 60.0, padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Theme( data: Theme.of(context).copyWith( buttonTheme: const ButtonThemeData( height: 44.0, ), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: MyButton( backgroundColor: isDark ? Colours.dark_material_bg : const Color(0xFFE1EAFA), textColor: isDark ? Colours.dark_text : Colours.app_main, text: '拒单', minHeight: 45, onPressed: () {}, ), ), Gaps.hGap16, Expanded( child: MyButton( text: '接单', minHeight: 45, onPressed: () {}, ), ) ], ), ), ); final List children = [ const Text( '暂未接单', style: TextStyles.textBold24, ), Gaps.vGap32, const Text( '客户信息', style: TextStyles.textBold18, ), Gaps.vGap16, Row( children: [ const ClipOval( child: LoadAssetImage('order/icon_avatar', width: 44.0, height: 44.0), ), Gaps.hGap8, const Expanded( // 合并Text的语义 child: MergeSemantics( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('郭李'), Gaps.vGap8, Text('15000000000'), ], ), ), ), Gaps.vLine, //Gaps.hGap16, Semantics( label: '拨打电话', child: GestureDetector( child: const Padding( padding: EdgeInsets.only(left: 20.0), child: LoadAssetImage('order/icon_phone', width: 24.0, height: 44.0), ), onTap: () => _showCallPhoneDialog('15000000000'), ), ) ], ), Gaps.vGap10, const Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ LoadAssetImage('order/icon_address', width: 16.0, height: 20.0), Gaps.hGap4, Expanded(child: Text('西安市雁塔区 鱼化寨街道唐兴路唐兴数码3楼318', maxLines: 2)), ], ), Gaps.vGap32, const Text( '商品信息', style: TextStyles.textBold18, ), ListView.builder( // 如果滚动视图在滚动方向无界约束,那么shrinkWrap必须为true shrinkWrap: true, // 禁用ListView滑动,使用外层的ScrollView滑动 physics: const NeverScrollableScrollPhysics(), itemCount: 2, itemBuilder: (_, index) => _buildOrderGoodsItem(index), ), Gaps.vGap8, _buildGoodsInfoItem('共2件商品', Utils.formatPrice('50.00')), _buildGoodsInfoItem('配送费', Utils.formatPrice('5.00')), _buildGoodsInfoItem('立减', Utils.formatPrice('-2.50'), contentTextColor: red), _buildGoodsInfoItem('优惠券', Utils.formatPrice('-2.50'), contentTextColor: red), _buildGoodsInfoItem('金币抵扣', Utils.formatPrice('-2.50'), contentTextColor: red), _buildGoodsInfoItem('佣金', Utils.formatPrice('-1.0'), contentTextColor: red), Gaps.line, Gaps.vGap8, _buildGoodsInfoItem('合计', Utils.formatPrice('46.50')), Gaps.vGap8, Gaps.line, Gaps.vGap32, const Text( '订单信息', style: TextStyles.textBold18, ), Gaps.vGap12, _buildOrderInfoItem('订单编号:', '1256324856942'), _buildOrderInfoItem('下单时间:', '2021/08/26 12:20'), _buildOrderInfoItem('支付方式:', '在线支付/支付宝'), _buildOrderInfoItem('配送方式:', '送货上门'), _buildOrderInfoItem('客户备注:', '无'), ]; return Scaffold( appBar: MyAppBar( actionName: '订单跟踪', onPressed: () { NavigatorUtils.push(context, OrderRouter.orderTrackPage); }, ), body: MyScrollView( key: const Key('order_info'), padding: const EdgeInsets.symmetric(horizontal: 16.0), bottomButton: bottomMenu, children: children, ) ); } Widget _buildOrderInfoItem(String title, String content) { return MergeSemantics( child: Container( padding: const EdgeInsets.symmetric(vertical: 4.0), child: Row( children: [ Text(title, style: Theme.of(context).textTheme.titleSmall?.copyWith(fontSize: Dimens.font_sp14)), Gaps.hGap8, Text(content) ], ), ), ); } Widget _buildOrderGoodsItem(int index) { final Widget item = Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Padding( padding: EdgeInsets.only(top: 5.0), child: LoadAssetImage('order/icon_goods', width: 56.0, height: 56.0), ), Gaps.hGap8, Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( index.isEven ? '泊泉雅花瓣·浪漫亲肤玫瑰沐浴乳' : '日本纳鲁火多橙饮', maxLines: 1, overflow: TextOverflow.ellipsis, ), Gaps.vGap4, Text(index.isEven ? '玫瑰香 520ml' : '125ml', style: Theme.of(context).textTheme.titleSmall), Gaps.vGap8, Row( children: [ _buildGoodsTag(Theme.of(context).colorScheme.error, '立减2.50元'), Gaps.hGap4, Offstage( offstage: index % 2 != 0, child: _buildGoodsTag(Theme.of(context).primaryColor, '抵扣2.50元'), ) ], ) ], ), ), Gaps.hGap8, const Text('x1', style: TextStyles.textSize12), Gaps.hGap32, Text(Utils.formatPrice('25'), style: TextStyles.textBold14), ], ); return DecoratedBox( decoration: BoxDecoration( border: Border( bottom: Divider.createBorderSide(context, width: 0.8), ), ), child: Padding( padding: const EdgeInsets.symmetric(vertical: 16.0), child: item ), ); } Widget _buildGoodsTag(Color color, String text) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8.0), decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(2.0), ), height: 16.0, alignment: Alignment.center, child: Text( text, style: TextStyle(color: Colors.white, fontSize: Dimens.font_sp10, height: Device.isAndroid ? 1.1 : null,), ), ); } Widget _buildGoodsInfoItem(String title, String content, {Color? contentTextColor}) { return MergeSemantics( child: Container( padding: const EdgeInsets.symmetric(vertical: 8.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(title), Text(content, style: TextStyle( color: contentTextColor ?? Theme.of(context).textTheme.bodyMedium?.color, fontWeight: FontWeight.bold )) ], ), ), ); } void _showCallPhoneDialog(String phone) { showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return AlertDialog( title: const Text('提示'), content: Text('是否拨打:$phone ?'), actions: [ TextButton( onPressed: () => NavigatorUtils.goBack(context), child: const Text('取消'), ), TextButton( onPressed: () { Utils.launchTelURL(phone); NavigatorUtils.goBack(context); }, style: ButtonStyle( // 按下高亮颜色 overlayColor: MaterialStateProperty.all(Theme.of(context).colorScheme.error.withOpacity(0.2)), ), child: Text('拨打', style: TextStyle(color: Theme.of(context).colorScheme.error),), ), ], ); } ); } } ================================================ FILE: lib/order/page/order_list_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/order/provider/order_page_provider.dart'; import 'package:flutter_deer/order/widgets/order_item.dart'; import 'package:flutter_deer/order/widgets/order_tag_item.dart'; import 'package:flutter_deer/util/change_notifier_manage.dart'; import 'package:flutter_deer/widgets/my_refresh_list.dart'; import 'package:flutter_deer/widgets/state_layout.dart'; import 'package:provider/provider.dart'; class OrderListPage extends StatefulWidget { const OrderListPage({ super.key, required this.index, }); final int index; @override _OrderListPageState createState() => _OrderListPageState(); } class _OrderListPageState extends State with AutomaticKeepAliveClientMixin, ChangeNotifierMixin{ final ScrollController _controller = ScrollController(); final StateType _stateType = StateType.loading; /// 是否正在加载数据 bool _isLoading = false; final int _maxPage = 3; int _page = 1; int _index = 0; List _list = []; @override void initState() { super.initState(); _index = widget.index; _onRefresh(); } @override Map?>? changeNotifier() { return {_controller: null}; } @override Widget build(BuildContext context) { super.build(context); return NotificationListener( onNotification: (ScrollNotification note) { if (note.metrics.pixels == note.metrics.maxScrollExtent) { _loadMore(); } return true; }, child: RefreshIndicator( onRefresh: _onRefresh, displacement: 120.0, /// 默认40, 多添加的80为Header高度 child: Consumer( builder: (_, provider, child) { return CustomScrollView( /// 这里指定controller可以与外层NestedScrollView的滚动分离,避免一处滑动,5个Tab中的列表同步滑动。 /// 这种方法的缺点是会重新layout列表 controller: _index != provider.index ? _controller : null, key: PageStorageKey('$_index'), slivers: [ SliverOverlapInjector( ///SliverAppBar的expandedHeight高度,避免重叠 handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), ), child!, ], ); }, child: SliverPadding( padding: const EdgeInsets.symmetric(horizontal: 16.0), sliver: _list.isEmpty ? SliverFillRemaining(child: StateLayout(type: _stateType)) : SliverList( delegate: SliverChildBuilderDelegate((BuildContext context, int index) { return index < _list.length ? (index % 5 == 0 ? const OrderTagItem(date: '2021年2月5日', orderTotal: 4) : OrderItem(key: Key('order_item_$index'), index: index, tabIndex: _index,) ) : MoreWidget(_list.length, _hasMore(), 10); }, childCount: _list.length + 1), ), ), ), ), ); } Future _onRefresh() async { await Future.delayed(const Duration(seconds: 2), () { setState(() { _page = 1; _list = List.generate(10, (i) => 'newItem:$i'); }); }); } bool _hasMore() { return _page < _maxPage; } Future _loadMore() async { if (_isLoading) { return; } if (!_hasMore()) { return; } _isLoading = true; await Future.delayed(const Duration(seconds: 2), () { setState(() { _list.addAll(List.generate(10, (i) => 'newItem:$i')); _page ++; _isLoading = false; }); }); } @override bool get wantKeepAlive => true; } ================================================ FILE: lib/order/page/order_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/order/page/order_list_page.dart'; import 'package:flutter_deer/order/provider/order_page_provider.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/util/image_utils.dart'; import 'package:flutter_deer/util/screen_utils.dart'; import 'package:flutter_deer/util/theme_utils.dart'; import 'package:flutter_deer/widgets/load_image.dart'; import 'package:flutter_deer/widgets/my_card.dart'; import 'package:flutter_deer/widgets/my_flexible_space_bar.dart'; import 'package:provider/provider.dart'; import '../order_router.dart'; /// design/3订单/index.html class OrderPage extends StatefulWidget { const OrderPage({super.key}); @override _OrderPageState createState() => _OrderPageState(); } class _OrderPageState extends State with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin { @override bool get wantKeepAlive => true; TabController? _tabController; OrderPageProvider provider = OrderPageProvider(); int _lastReportedPage = 0; @override void initState() { super.initState(); _tabController = TabController(vsync: this, length: 5); WidgetsBinding.instance.addPostFrameCallback((_) { /// 预先缓存剩余切换图片 _preCacheImage(); }); } void _preCacheImage() { precacheImage(ImageUtils.getAssetImage('order/xdd_n'), context); precacheImage(ImageUtils.getAssetImage('order/dps_s'), context); precacheImage(ImageUtils.getAssetImage('order/dwc_s'), context); precacheImage(ImageUtils.getAssetImage('order/ywc_s'), context); precacheImage(ImageUtils.getAssetImage('order/yqx_s'), context); } @override void dispose() { _tabController?.dispose(); super.dispose(); } /// https://github.com/simplezhli/flutter_deer/issues/194 @override // ignore: must_call_super void didChangeDependencies() { } bool isDark = false; @override Widget build(BuildContext context) { super.build(context); isDark = context.isDark; return ChangeNotifierProvider( create: (_) => provider, child: Scaffold( body: Stack( children: [ /// 像素对齐问题的临时解决方法 SafeArea( child: SizedBox( height: 105, width: double.infinity, child: isDark ? null : const DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient(colors: [Colours.gradient_blue, Color(0xFF4647FA)]), ), ), ), ), NestedScrollView( key: const Key('order_list'), physics: const ClampingScrollPhysics(), headerSliverBuilder: (context, innerBoxIsScrolled) => _sliverBuilder(context), body: NotificationListener( onNotification: (ScrollNotification notification) { /// PageView的onPageChanged是监听ScrollUpdateNotification,会造成滑动中卡顿。这里修改为监听滚动结束再更新、 if (notification.depth == 0 && notification is ScrollEndNotification) { final PageMetrics metrics = notification.metrics as PageMetrics; final int currentPage = (metrics.page ?? 0).round(); if (currentPage != _lastReportedPage) { _lastReportedPage = currentPage; _onPageChange(currentPage); } } return false; }, child: PageView.builder( key: const Key('pageView'), itemCount: 5, controller: _pageController, itemBuilder: (_, index) => OrderListPage(index: index), ), ), ), ], ), ), ); } List _sliverBuilder(BuildContext context) { return [ SliverOverlapAbsorber( handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), sliver: SliverAppBar( systemOverlayStyle: isDark ? ThemeUtils.light : ThemeUtils.dark, actions: [ IconButton( onPressed: () { NavigatorUtils.push(context, OrderRouter.orderSearchPage); }, tooltip: '搜索', icon: LoadAssetImage('order/icon_search', width: 22.0, height: 22.0, color: ThemeUtils.getIconColor(context), ), ) ], backgroundColor: Colors.transparent, elevation: 0.0, centerTitle: true, expandedHeight: 100.0, // 不随着滑动隐藏标题 pinned: true, // 固定在顶部 flexibleSpace: MyFlexibleSpaceBar( background: isDark ? Container(height: 113.0, color: Colours.dark_bg_color,) : LoadAssetImage('order/order_bg', width: context.width, height: 113.0, fit: BoxFit.fill, ), centerTitle: true, titlePadding: const EdgeInsetsDirectional.only(start: 16.0, bottom: 14.0), collapseMode: CollapseMode.pin, title: Text('订单', style: TextStyle(color: ThemeUtils.getIconColor(context)),), ), ), ), SliverPersistentHeader( pinned: true, delegate: SliverAppBarDelegate( DecoratedBox( decoration: BoxDecoration( color: isDark ? Colours.dark_bg_color : null, image: isDark ? null : DecorationImage( image: ImageUtils.getAssetImage('order/order_bg1'), fit: BoxFit.fill, ), ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: MyCard( child: Container( height: 80.0, padding: const EdgeInsets.only(top: 8.0), child: TabBar( labelPadding: EdgeInsets.zero, controller: _tabController, labelColor: context.isDark ? Colours.dark_text : Colours.text, unselectedLabelColor: context.isDark ? Colours.dark_text_gray : Colours.text, labelStyle: TextStyles.textBold14, unselectedLabelStyle: const TextStyle( fontSize: Dimens.font_sp14, ), indicatorColor: Colors.transparent, tabs: const [ _TabView(0, '新订单'), _TabView(1, '待配送'), _TabView(2, '待完成'), _TabView(3, '已完成'), _TabView(4, '已取消'), ], onTap: (index) { if (!mounted) { return; } _pageController.jumpToPage(index); }, ), ), ), ), ), 80.0, ), ), ]; } final PageController _pageController = PageController(); Future _onPageChange(int index) async { provider.setIndex(index); /// 这里没有指示器,所以缩短过渡动画时间,减少不必要的刷新 _tabController?.animateTo(index, duration: Duration.zero); } } List> img = [ ['order/xdd_s', 'order/xdd_n'], ['order/dps_s', 'order/dps_n'], ['order/dwc_s', 'order/dwc_n'], ['order/ywc_s', 'order/ywc_n'], ['order/yqx_s', 'order/yqx_n'] ]; List> darkImg = [ ['order/dark/icon_xdd_s', 'order/dark/icon_xdd_n'], ['order/dark/icon_dps_s', 'order/dark/icon_dps_n'], ['order/dark/icon_dwc_s', 'order/dark/icon_dwc_n'], ['order/dark/icon_ywc_s', 'order/dark/icon_ywc_n'], ['order/dark/icon_yqx_s', 'order/dark/icon_yqx_n'] ]; class _TabView extends StatelessWidget { const _TabView(this.index, this.text); final int index; final String text; @override Widget build(BuildContext context) { final List> imgList = context.isDark ? darkImg : img; return Stack( children: [ Container( width: 46.0, padding: const EdgeInsets.symmetric(vertical: 8.0), child: Column( children: [ /// 使用context.select替代Consumer LoadAssetImage(context.select((value) => value.index) == index ? imgList[index][0] : imgList[index][1], width: 24.0, height: 24.0,), Gaps.vGap4, Text(text), ], ), ), Positioned( right: 0.0, child: index < 3 ? DecoratedBox( decoration: BoxDecoration( color: Theme.of(context).colorScheme.error, borderRadius: BorderRadius.circular(11.0), ), child: const Padding( padding: EdgeInsets.symmetric(horizontal: 5.5, vertical: 2.0), child: Text('10', style: TextStyle(color: Colors.white, fontSize: Dimens.font_sp12),), ), ) : Gaps.empty, ) ], ); } } class SliverAppBarDelegate extends SliverPersistentHeaderDelegate { SliverAppBarDelegate(this.widget, this.height); final Widget widget; final double height; // minHeight 和 maxHeight 的值设置为相同时,header就不会收缩了 @override double get minExtent => height; @override double get maxExtent => height; @override Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { return widget; } @override bool shouldRebuild(SliverAppBarDelegate oldDelegate) { return true; } } ================================================ FILE: lib/order/page/order_search_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/mvp/base_page.dart'; import 'package:flutter_deer/mvp/power_presenter.dart'; import 'package:flutter_deer/order/iview/order_search_iview.dart'; import 'package:flutter_deer/order/models/search_entity.dart'; import 'package:flutter_deer/order/presenter/order_search_presenter.dart'; import 'package:flutter_deer/order/provider/base_list_provider.dart'; import 'package:flutter_deer/shop/iview/shop_iview.dart'; import 'package:flutter_deer/shop/models/user_entity.dart'; import 'package:flutter_deer/shop/presenter/shop_presenter.dart'; import 'package:flutter_deer/util/other_utils.dart'; import 'package:flutter_deer/widgets/my_refresh_list.dart'; import 'package:flutter_deer/widgets/my_search_bar.dart'; import 'package:flutter_deer/widgets/state_layout.dart'; import 'package:provider/provider.dart'; /// design/3订单/index.html#artboard8 class OrderSearchPage extends StatefulWidget { const OrderSearchPage({super.key}); @override _OrderSearchPageState createState() => _OrderSearchPageState(); } class _OrderSearchPageState extends State with BasePageMixin> implements OrderSearchIMvpView, ShopIMvpView { @override BaseListProvider provider = BaseListProvider(); late String _keyword; int _page = 1; @override void initState() { /// 默认为加载中状态,本页面场景默认为空 provider.stateType = StateType.empty; super.initState(); } @override Widget build(BuildContext context) { return ChangeNotifierProvider>( create: (_) => provider, child: Scaffold( appBar: MySearchBar( hintText: '请输入手机号或姓名查询', onPressed: (text) { if (text.isEmpty) { showToast('搜索关键字不能为空!'); return; } _keyword = text; provider.setStateType(StateType.loading); _page = 1; _orderSearchPresenter.search(_keyword, _page, true); }, ), body: Consumer>( builder: (_, provider, __) { return DeerListView( key: const Key('order_search_list'), itemCount: provider.list.length, stateType: provider.stateType, onRefresh: _onRefresh, loadMore: _loadMore, itemExtent: 50.0, hasMore: provider.hasMore, itemBuilder: (_, index) { return Container( padding: const EdgeInsets.symmetric(horizontal: 16.0), alignment: Alignment.centerLeft, child: Text(provider.list[index].name.nullSafe), ); }, ); } ), ), ); } Future _onRefresh() async { _page = 1; await _orderSearchPresenter.search(_keyword, _page, false); } Future _loadMore() async { _page++; await _orderSearchPresenter.search(_keyword, _page, false); } late OrderSearchPresenter _orderSearchPresenter; late ShopPagePresenter _shopPagePresenter; @override PowerPresenter createPresenter() { final PowerPresenter powerPresenter = PowerPresenter(this); _orderSearchPresenter = OrderSearchPresenter(); _shopPagePresenter = ShopPagePresenter(); powerPresenter.requestPresenter([_orderSearchPresenter, _shopPagePresenter]); return powerPresenter; } @override bool get isAccessibilityTest => false; @override void setUser(UserEntity? user) { showToast(user?.name ?? ''); } } ================================================ FILE: lib/order/page/order_track_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/widgets/my_app_bar.dart'; import 'package:flutter_deer/widgets/my_scroll_view.dart'; /// design/3订单/index.html#artboard10 class OrderTrackPage extends StatefulWidget { const OrderTrackPage({super.key}); @override _OrderTrackPageState createState() => _OrderTrackPageState(); } class _OrderTrackPageState extends State { @override Widget build(BuildContext context) { return Scaffold( appBar: const MyAppBar( centerTitle: '订单跟踪', ), body: MyScrollView( children: [ Padding( padding: const EdgeInsets.only(top: 21.0, left: 16.0, right: 16.0), child: Row( children: [ const Text('订单编号:'), // 可选择文本组件(复制) Semantics( label: '长按复制订单编号', child: const SelectableText('14562364879', maxLines: 1,), ), ], ) ), Stepper( physics: const BouncingScrollPhysics(), currentStep: 4 - 1, controlsBuilder: (_, __) { return Gaps.empty; //操作按钮置空 }, steps: List.generate(4, (i) => _buildStep(i)), ) ], ) ); } final List _titleList = ['订单已完成', '开始配送', '等待配送', '收到新订单']; final List _timeList = ['2019/08/30 13:30', '2019/08/30 11:30', '2019/08/30 9:30', '2019/08/30 9:00']; Step _buildStep(int index) { final Color primaryColor = Theme.of(context).primaryColor; return Step( title: Padding( padding: const EdgeInsets.only(top: 15.0), child: Text(_titleList[index], style: index == 0 ? TextStyle( fontSize: Dimens.font_sp14, color: primaryColor, ) : Theme.of(context).textTheme.bodyMedium), ), subtitle: Text(_timeList[index], style: index == 0 ? TextStyle( fontSize: Dimens.font_sp12, color: primaryColor, ) : Theme.of(context).textTheme.titleSmall), content: const Text(''), isActive: index == 0, state: index == 0 ? StepState.complete : StepState.indexed, ); } } ================================================ FILE: lib/order/presenter/order_search_presenter.dart ================================================ import 'package:flutter_deer/mvp/base_page_presenter.dart'; import 'package:flutter_deer/net/net.dart'; import 'package:flutter_deer/order/iview/order_search_iview.dart'; import 'package:flutter_deer/order/models/search_entity.dart'; import 'package:flutter_deer/widgets/state_layout.dart'; class OrderSearchPresenter extends BasePagePresenter { Future search(String text, int page, bool isShowDialog) { final Map params = {}; params['q'] = text; params['page'] = page.toString(); params['l'] = 'Dart'; return requestNetwork(Method.get, url: HttpApi.search, queryParameters: params, isShow: isShowDialog, onSuccess: (data) { if (data != null && data.items != null) { /// 一页30条数据,等于30条认为有下一页 /// 具体的处理逻辑根据具体的接口情况处理,这部分可以抽离出来 view.provider.hasMore = data.items!.length == 30; if (page == 1) { /// 刷新 view.provider.list.clear(); if (data.items!.isEmpty) { view.provider.setStateType(StateType.order); } else { view.provider.addAll(data.items!); } } else { view.provider.addAll(data.items!); } } else { /// 加载失败 view.provider.hasMore = false; view.provider.setStateType(StateType.network); } }, onError: (_, __) { /// 加载失败 view.provider.hasMore = false; view.provider.setStateType(StateType.network); } ); } } ================================================ FILE: lib/order/provider/base_list_provider.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/widgets/state_layout.dart'; class BaseListProvider extends ChangeNotifier { final List _list = []; List get list => _list; bool hasMore = true; StateType stateType = StateType.loading; void setStateType(StateType stateType) { this.stateType = stateType; notifyListeners(); } void add(T data) { _list.add(data); notifyListeners(); } void addAll(List data) { _list.addAll(data); notifyListeners(); } void insert(int i, T data) { _list.insert(i, data); notifyListeners(); } void insertAll(int i, List data) { _list.insertAll(i, data); notifyListeners(); } void remove(T data) { _list.remove(data); notifyListeners(); } void removeAt(int i) { _list.removeAt(i); notifyListeners(); } void clear() { _list.clear(); notifyListeners(); } void refresh() { notifyListeners(); } } ================================================ FILE: lib/order/provider/order_page_provider.dart ================================================ import 'package:flutter/material.dart'; class OrderPageProvider extends ChangeNotifier { /// Tab的下标 int _index = 0; int get index => _index; void refresh() { notifyListeners(); } void setIndex(int index) { _index = index; notifyListeners(); } } ================================================ FILE: lib/order/widgets/order_item.dart ================================================ import 'package:common_utils/common_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_deer/order/widgets/pay_type_dialog.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/util/other_utils.dart'; import 'package:flutter_deer/util/theme_utils.dart'; import 'package:flutter_deer/util/toast_utils.dart'; import 'package:flutter_deer/widgets/my_card.dart'; import '../order_router.dart'; const List orderLeftButtonText = ['拒单', '拒单', '订单跟踪', '订单跟踪', '订单跟踪']; const List orderRightButtonText = ['接单', '开始配送', '完成', '', '']; class OrderItem extends StatelessWidget { const OrderItem({ super.key, required this.tabIndex, required this.index, }); final int tabIndex; final int index; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(top: 8.0), child: MyCard( child: Padding( padding: const EdgeInsets.all(16.0), child: InkWell( onTap: () => NavigatorUtils.push(context, OrderRouter.orderInfoPage), child: _buildContent(context), ), ), ) ); } Widget _buildContent(BuildContext context) { final TextStyle? textTextStyle = Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: Dimens.font_sp12); final bool isDark = context.isDark; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Expanded( child: Text('15000000000(郭李)'), ), Text( '货到付款', style: TextStyle( fontSize: Dimens.font_sp12, color: Theme.of(context).colorScheme.error, ), ), ], ), Gaps.vGap8, Text( '西安市雁塔区 鱼化寨街道唐兴路唐兴数码3楼318', style: Theme.of(context).textTheme.titleSmall, ), Gaps.vGap8, Gaps.line, Gaps.vGap8, RichText( text: TextSpan( style: textTextStyle, children: [ const TextSpan(text: '清凉一度抽纸'), TextSpan(text: ' x1', style: Theme.of(context).textTheme.titleSmall), ], ), ), Gaps.vGap8, RichText( text: TextSpan( style: textTextStyle, children: [ const TextSpan(text: '清凉一度抽纸'), TextSpan(text: ' x2', style: Theme.of(context).textTheme.titleSmall), ], ), ), Gaps.vGap12, Row( children: [ Expanded( child: RichText( text: TextSpan( style: textTextStyle, children: [ TextSpan(text: Utils.formatPrice('20.00', format: MoneyFormat.NORMAL)), TextSpan(text: ' 共3件商品', style: Theme.of(context).textTheme.titleSmall?.copyWith(fontSize: Dimens.font_sp10)), ], ), ), ), const Text( '2021.02.05 10:00', style: TextStyles.textSize12, ), ], ), Gaps.vGap8, Gaps.line, Gaps.vGap8, Row( children: [ OrderItemButton( key: Key('order_button_1_$index'), text: '联系客户', textColor: isDark ? Colours.dark_text : Colours.text, bgColor: isDark ? Colours.dark_material_bg : Colours.bg_gray, onTap: () => _showCallPhoneDialog(context, '15000000000'), ), const Expanded( child: Gaps.empty, ), OrderItemButton( key: Key('order_button_2_$index'), text: orderLeftButtonText[tabIndex], textColor: isDark ? Colours.dark_text : Colours.text, bgColor: isDark ? Colours.dark_material_bg : Colours.bg_gray, onTap: () { if (tabIndex >= 2) { NavigatorUtils.push(context, OrderRouter.orderTrackPage); } }, ), if (orderRightButtonText[tabIndex].isEmpty) Gaps.empty else Gaps.hGap10, if (orderRightButtonText[tabIndex].isEmpty) Gaps.empty else OrderItemButton( key: Key('order_button_3_$index'), text: orderRightButtonText[tabIndex], textColor: isDark ? Colours.dark_button_text : Colors.white, bgColor: isDark ? Colours.dark_app_main : Colours.app_main, onTap: () { if (tabIndex == 2) { _showPayTypeDialog(context); } }, ), ], ) ], ); } void _showCallPhoneDialog(BuildContext context, String phone) { showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return AlertDialog( title: const Text('提示'), content: Text('是否拨打:$phone ?'), actions: [ TextButton( onPressed: () => NavigatorUtils.goBack(context), child: const Text('取消'), ), TextButton( onPressed: () { Utils.launchTelURL(phone); NavigatorUtils.goBack(context); }, style: ButtonStyle( // 按下高亮颜色 overlayColor: MaterialStateProperty.all(Theme.of(context).colorScheme.error.withOpacity(0.2)), ), child: Text('拨打', style: TextStyle(color: Theme.of(context).colorScheme.error),), ), ], ); }, ); } void _showPayTypeDialog(BuildContext context) { showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return PayTypeDialog( onPressed: (index, type) { Toast.show('收款类型:$type'); }, ); }, ); } } class OrderItemButton extends StatelessWidget { const OrderItemButton({ super.key, this.bgColor, this.textColor, required this.text, this.onTap }); final Color? bgColor; final Color? textColor; final GestureTapCallback? onTap; final String text; @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: Container( alignment: Alignment.center, padding: const EdgeInsets.symmetric(horizontal: 14.0), decoration: BoxDecoration( color: bgColor, borderRadius: BorderRadius.circular(4.0), ), constraints: const BoxConstraints( minWidth: 64.0, maxHeight: 30.0, minHeight: 30.0, ), child: Text(text, style: TextStyle(fontSize: Dimens.font_sp14, color: textColor),), ), ); } } ================================================ FILE: lib/order/widgets/order_tag_item.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/util/theme_utils.dart'; import 'package:flutter_deer/widgets/load_image.dart'; import 'package:flutter_deer/widgets/my_card.dart'; class OrderTagItem extends StatelessWidget { const OrderTagItem({ super.key, required this.date, required this.orderTotal, }); final String date; final int orderTotal; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(top: 8.0), child: MyCard( child: Container( height: 34.0, padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Row( children: [ if (context.isDark) const LoadAssetImage('order/icon_calendar_dark', width: 14.0, height: 14.0) else const LoadAssetImage('order/icon_calendar', width: 14.0, height: 14.0), Gaps.hGap10, Text(date), const Expanded(child: Gaps.empty), Text('$orderTotal单') ], ), ) ), ); } } ================================================ FILE: lib/order/widgets/pay_type_dialog.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/widgets/base_dialog.dart'; import 'package:flutter_deer/widgets/load_image.dart'; /// design/3订单/index.html#artboard5 class PayTypeDialog extends StatefulWidget { const PayTypeDialog({ super.key, this.onPressed, }); final void Function(int, String)? onPressed; @override _PayTypeDialog createState() => _PayTypeDialog(); } class _PayTypeDialog extends State { int _value = 0; final _list = ['未收款', '支付宝', '微信', '现金']; Widget _buildItem(int index) { return Material( type: MaterialType.transparency, child: InkWell( child: SizedBox( height: 42.0, child: Row( children: [ Gaps.hGap16, Expanded( child: Text( _list[index], style: _value == index ? TextStyle( fontSize: Dimens.font_sp14, color: Theme.of(context).primaryColor, ) : null, ), ), Visibility( visible: _value == index, child: const LoadAssetImage('order/ic_check', width: 16.0, height: 16.0)), Gaps.hGap16, ], ), ), onTap: () { if (mounted) { setState(() { _value = index; }); } }, ), ); } @override Widget build(BuildContext context) { return BaseDialog( title: '收款方式', child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisSize: MainAxisSize.min, children: List.generate(_list.length, (i) => _buildItem(i)) ), onPressed: () { NavigatorUtils.goBack(context); widget.onPressed?.call(_value, _list[_value]); }, ); } } ================================================ FILE: lib/res/colors.dart ================================================ import 'package:flutter/material.dart'; class Colours { static const Color app_main = Color(0xFF4688FA); static const Color dark_app_main = Color(0xFF3F7AE0); static const Color bg_color = Color(0xfff1f1f1); static const Color dark_bg_color = Color(0xFF18191A); static const Color material_bg = Color(0xFFFFFFFF); static const Color dark_material_bg = Color(0xFF303233); static const Color text = Color(0xFF333333); static const Color dark_text = Color(0xFFB8B8B8); static const Color text_gray = Color(0xFF999999); static const Color dark_text_gray = Color(0xFF666666); static const Color text_gray_c = Color(0xFFcccccc); static const Color dark_button_text = Color(0xFFF2F2F2); static const Color bg_gray = Color(0xFFF6F6F6); static const Color dark_bg_gray = Color(0xFF1F1F1F); static const Color line = Color(0xFFEEEEEE); static const Color dark_line = Color(0xFF3A3C3D); static const Color red = Color(0xFFFF4759); static const Color dark_red = Color(0xFFE03E4E); static const Color text_disabled = Color(0xFFD4E2FA); static const Color dark_text_disabled = Color(0xFFCEDBF2); static const Color button_disabled = Color(0xFF96BBFA); static const Color dark_button_disabled = Color(0xFF83A5E0); static const Color unselected_item_color = Color(0xffbfbfbf); static const Color dark_unselected_item_color = Color(0xFF4D4D4D); static const Color bg_gray_ = Color(0xFFFAFAFA); static const Color dark_bg_gray_ = Color(0xFF242526); static const Color gradient_blue = Color(0xFF5793FA); static const Color shadow_blue = Color(0x805793FA); static const Color orange = Color(0xFFFF8547); } ================================================ FILE: lib/res/constant.dart ================================================ import 'package:flutter/foundation.dart'; class Constant { /// App运行在Release环境时,inProduction为true;当App运行在Debug和Profile环境时,inProduction为false static const bool inProduction = kReleaseMode; static bool isDriverTest = false; static bool isUnitTest = false; static const String data = 'data'; static const String message = 'message'; static const String code = 'code'; static const String keyGuide = 'keyGuide'; static const String phone = 'phone'; static const String accessToken = 'accessToken'; static const String refreshToken = 'refreshToken'; static const String theme = 'AppTheme'; static const String locale = 'locale'; } ================================================ FILE: lib/res/dimens.dart ================================================ class Dimens { static const double font_sp10 = 10.0; static const double font_sp12 = 12.0; static const double font_sp14 = 14.0; static const double font_sp15 = 15.0; static const double font_sp16 = 16.0; static const double font_sp18 = 18.0; static const double gap_dp4 = 4; static const double gap_dp5 = 5; static const double gap_dp8 = 8; static const double gap_dp10 = 10; static const double gap_dp12 = 12; static const double gap_dp15 = 15; static const double gap_dp16 = 16; static const double gap_dp24 = 24; static const double gap_dp32 = 32; static const double gap_dp50 = 50; } ================================================ FILE: lib/res/gaps.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/res/resources.dart'; /// 间隔 /// 官方做法:https://github.com/flutter/flutter/pull/54394 class Gaps { /// 水平间隔 static const Widget hGap4 = SizedBox(width: Dimens.gap_dp4); static const Widget hGap5 = SizedBox(width: Dimens.gap_dp5); static const Widget hGap8 = SizedBox(width: Dimens.gap_dp8); static const Widget hGap10 = SizedBox(width: Dimens.gap_dp10); static const Widget hGap12 = SizedBox(width: Dimens.gap_dp12); static const Widget hGap15 = SizedBox(width: Dimens.gap_dp15); static const Widget hGap16 = SizedBox(width: Dimens.gap_dp16); static const Widget hGap32 = SizedBox(width: Dimens.gap_dp32); /// 垂直间隔 static const Widget vGap4 = SizedBox(height: Dimens.gap_dp4); static const Widget vGap5 = SizedBox(height: Dimens.gap_dp5); static const Widget vGap8 = SizedBox(height: Dimens.gap_dp8); static const Widget vGap10 = SizedBox(height: Dimens.gap_dp10); static const Widget vGap12 = SizedBox(height: Dimens.gap_dp12); static const Widget vGap15 = SizedBox(height: Dimens.gap_dp15); static const Widget vGap16 = SizedBox(height: Dimens.gap_dp16); static const Widget vGap24 = SizedBox(height: Dimens.gap_dp24); static const Widget vGap32 = SizedBox(height: Dimens.gap_dp32); static const Widget vGap50 = SizedBox(height: Dimens.gap_dp50); // static Widget line = const SizedBox( // height: 0.6, // width: double.infinity, // child: const DecoratedBox(decoration: BoxDecoration(color: Colours.line)), // ); static const Widget line = Divider(); static const Widget vLine = SizedBox( width: 0.6, height: 24.0, child: VerticalDivider(), ); static const Widget empty = SizedBox.shrink(); /// 补充一种空Widget实现 https://github.com/letsar/nil /// https://github.com/flutter/flutter/issues/78159 } ================================================ FILE: lib/res/resources.dart ================================================ import 'package:flutter/widgets.dart'; import 'package:flutter_deer/widgets/load_image.dart'; export 'colors.dart'; export 'dimens.dart'; export 'gaps.dart'; export 'styles.dart'; class Images { static const Widget arrowRight = LoadAssetImage('ic_arrow_right', height: 16.0, width: 16.0); } ================================================ FILE: lib/res/styles.dart ================================================ import 'package:flutter/material.dart'; import 'colors.dart'; import 'dimens.dart'; class TextStyles { static const TextStyle textSize12 = TextStyle( fontSize: Dimens.font_sp12, ); static const TextStyle textSize16 = TextStyle( fontSize: Dimens.font_sp16, ); static const TextStyle textBold14 = TextStyle( fontSize: Dimens.font_sp14, fontWeight: FontWeight.bold ); static const TextStyle textBold16 = TextStyle( fontSize: Dimens.font_sp16, fontWeight: FontWeight.bold ); static const TextStyle textBold18 = TextStyle( fontSize: Dimens.font_sp18, fontWeight: FontWeight.bold ); static const TextStyle textBold24 = TextStyle( fontSize: 24.0, fontWeight: FontWeight.bold ); static const TextStyle textBold26 = TextStyle( fontSize: 26.0, fontWeight: FontWeight.bold ); static const TextStyle textGray14 = TextStyle( fontSize: Dimens.font_sp14, color: Colours.text_gray, ); static const TextStyle textDarkGray14 = TextStyle( fontSize: Dimens.font_sp14, color: Colours.dark_text_gray, ); static const TextStyle textWhite14 = TextStyle( fontSize: Dimens.font_sp14, color: Colors.white, ); static const TextStyle text = TextStyle( fontSize: Dimens.font_sp14, color: Colours.text, // https://github.com/flutter/flutter/issues/40248 textBaseline: TextBaseline.alphabetic ); static const TextStyle textDark = TextStyle( fontSize: Dimens.font_sp14, color: Colours.dark_text, textBaseline: TextBaseline.alphabetic ); static const TextStyle textGray12 = TextStyle( fontSize: Dimens.font_sp12, color: Colours.text_gray, fontWeight: FontWeight.normal ); static const TextStyle textDarkGray12 = TextStyle( fontSize: Dimens.font_sp12, color: Colours.dark_text_gray, fontWeight: FontWeight.normal ); static const TextStyle textHint14 = TextStyle( fontSize: Dimens.font_sp14, color: Colours.dark_unselected_item_color ); } ================================================ FILE: lib/routers/fluro_navigator.dart ================================================ import 'package:fluro/fluro.dart'; import 'package:flutter/material.dart'; import 'routers.dart'; /// fluro的路由跳转工具类 class NavigatorUtils { static void push(BuildContext context, String path, {bool replace = false, bool clearStack = false, Object? arguments}) { unfocus(); Routes.router.navigateTo(context, path, replace: replace, clearStack: clearStack, transition: TransitionType.native, routeSettings: RouteSettings( arguments: arguments, ), ); } static void pushResult(BuildContext context, String path, void Function(Object) function, {bool replace = false, bool clearStack = false, Object? arguments}) { unfocus(); Routes.router.navigateTo(context, path, replace: replace, clearStack: clearStack, transition: TransitionType.native, routeSettings: RouteSettings( arguments: arguments, ), ).then((Object? result) { // 页面返回result为null if (result == null) { return; } function(result); }).catchError((dynamic error) { debugPrint('$error'); }); } /// 返回 static void goBack(BuildContext context) { unfocus(); Navigator.pop(context); } /// 带参数返回 static void goBackWithParams(BuildContext context, Object result) { unfocus(); Navigator.pop(context, result); } /// 跳到WebView页 static void goWebViewPage(BuildContext context, String title, String url) { //fluro 不支持传中文,需转换 push(context, '${Routes.webViewPage}?title=${Uri.encodeComponent(title)}&url=${Uri.encodeComponent(url)}'); } static void unfocus() { // 使用下面的方式,会触发不必要的build。 // FocusScope.of(context).unfocus(); // https://github.com/flutter/flutter/issues/47128#issuecomment-627551073 FocusManager.instance.primaryFocus?.unfocus(); } } ================================================ FILE: lib/routers/i_router.dart ================================================ import 'package:fluro/fluro.dart'; abstract class IRouterProvider { void initRouter(FluroRouter router); } ================================================ FILE: lib/routers/not_found_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/widgets/my_app_bar.dart'; import 'package:flutter_deer/widgets/state_layout.dart'; class NotFoundPage extends StatelessWidget { const NotFoundPage({super.key}); @override Widget build(BuildContext context) { return const Scaffold( appBar: MyAppBar( centerTitle: '页面不存在', ), body: StateLayout( type: StateType.account, hintText: '页面不存在', ), ); } } ================================================ FILE: lib/routers/routers.dart ================================================ import 'package:fluro/fluro.dart'; import 'package:flutter/material.dart'; import 'package:flutter_deer/account/account_router.dart'; import 'package:flutter_deer/goods/goods_router.dart'; import 'package:flutter_deer/home/home_page.dart'; import 'package:flutter_deer/home/webview_page.dart'; import 'package:flutter_deer/login/login_router.dart'; import 'package:flutter_deer/order/order_router.dart'; import 'package:flutter_deer/routers/i_router.dart'; import 'package:flutter_deer/routers/not_found_page.dart'; import 'package:flutter_deer/setting/setting_router.dart'; import 'package:flutter_deer/shop/shop_router.dart'; import 'package:flutter_deer/statistics/statistics_router.dart'; import 'package:flutter_deer/store/store_router.dart'; class Routes { static String home = '/home'; static String webViewPage = '/webView'; static final List _listRouter = []; static final FluroRouter router = FluroRouter(); static void initRoutes() { /// 指定路由跳转错误返回页 router.notFoundHandler = Handler( handlerFunc: (BuildContext? context, Map> params) { debugPrint('未找到目标页'); return const NotFoundPage(); }); router.define(home, handler: Handler( handlerFunc: (BuildContext? context, Map> params) => const Home())); router.define(webViewPage, handler: Handler(handlerFunc: (_, params) { final String title = params['title']?.first ?? ''; final String url = params['url']?.first ?? ''; return WebViewPage(title: title, url: url); })); _listRouter.clear(); /// 各自路由由各自模块管理,统一在此添加初始化 _listRouter.add(ShopRouter()); _listRouter.add(LoginRouter()); _listRouter.add(GoodsRouter()); _listRouter.add(OrderRouter()); _listRouter.add(StoreRouter()); _listRouter.add(AccountRouter()); _listRouter.add(SettingRouter()); _listRouter.add(StatisticsRouter()); /// 初始化路由 void initRouter(IRouterProvider routerProvider) { routerProvider.initRouter(router); } _listRouter.forEach(initRouter); } } ================================================ FILE: lib/routers/web_page_transitions.dart ================================================ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; /// https://medium.com/flutter/improving-perceived-performance-with-image-placeholders-precaching-and-disabled-navigation-6b3601087a2b /// 对于Web应用程序,为了提高性能,可以禁用页面过渡动画。 class NoTransitionsOnWeb extends PageTransitionsTheme { @override Widget buildTransitions( route, context, animation, secondaryAnimation, child, ) { if (kIsWeb) { return child; } return super.buildTransitions( route, context, animation, secondaryAnimation, child, ); } } ================================================ FILE: lib/setting/page/about_page.dart ================================================ import 'dart:async'; import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/util/device_utils.dart'; import 'package:flutter_deer/util/other_utils.dart'; import 'package:flutter_deer/widgets/click_item.dart'; import 'package:flutter_deer/widgets/my_app_bar.dart'; class AboutPage extends StatefulWidget { const AboutPage({super.key}); @override _AboutPageState createState() => _AboutPageState(); } class _AboutPageState extends State { final List _styles = [ FlutterLogoStyle.stacked, FlutterLogoStyle.markOnly, FlutterLogoStyle.horizontal ]; final List _curves = [ Curves.ease, Curves.easeIn, Curves.easeInOutCubic, Curves.easeInOut, Curves.easeInQuad, Curves.easeInCirc, Curves.easeInBack, Curves.easeInOutExpo, Curves.easeInToLinear, Curves.easeOutExpo, Curves.easeInOutSine, Curves.easeOutSine, ]; // 取随机颜色 Color _randomColor() { final int red = Random.secure().nextInt(255); final int greed = Random.secure().nextInt(255); final int blue = Random.secure().nextInt(255); return Color.fromARGB(255, red, greed, blue); } Timer? _countdownTimer; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) async { // 2s定时器 _countdownTimer = Timer.periodic(const Duration(seconds: 2), (_) { // https://www.jianshu.com/p/e4106b829bff if (!mounted) { return; } setState(() { }); }); }); } @override void dispose() { _countdownTimer?.cancel(); _countdownTimer = null; super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: const MyAppBar( title: '关于我们', ), body: Column( children: [ Gaps.vGap50, FlutterLogo( size: 100.0, textColor: _randomColor(), style: _styles[Random.secure().nextInt(3)], curve: _curves[Random.secure().nextInt(12)], ), Gaps.vGap10, ClickItem( title: 'Github', content: 'Go Star', onTap: () => _launchWebURL('Flutter Deer', 'https://github.com/simplezhli/flutter_deer') ), ClickItem( title: '作者博客', onTap: () => _launchWebURL('作者博客', 'https://weilu.blog.csdn.net') ), ], ), ); } void _launchWebURL(String title, String url) { if (Device.isMobile) { NavigatorUtils.goWebViewPage(context, title, url); } else { Utils.launchWebURL(url); } } } ================================================ FILE: lib/setting/page/account_manager_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/login/login_router.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/widgets/click_item.dart'; import 'package:flutter_deer/widgets/load_image.dart'; import 'package:flutter_deer/widgets/my_app_bar.dart'; /// design/8设置/index.html#artboard1 class AccountManagerPage extends StatefulWidget { const AccountManagerPage({super.key}); @override _AccountManagerPageState createState() => _AccountManagerPageState(); } class _AccountManagerPageState extends State { @override Widget build(BuildContext context) { return Scaffold( appBar: const MyAppBar( centerTitle: '账号管理', ), body: Column( children: [ Stack( children: [ ClickItem( title: '店铺logo', onTap: () {} ), const Positioned( top: 8.0, bottom: 8.0, right: 40.0, child: LoadAssetImage('shop/tx', width: 34.0), ) ], ), ClickItem( title: '修改密码', content: '用于密码登录', onTap: () => NavigatorUtils.push(context, LoginRouter.updatePasswordPage) ), const ClickItem( title: '绑定账号', content: '15000000000', ), ], ), ); } } ================================================ FILE: lib/setting/page/locale_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/res/constant.dart'; import 'package:flutter_deer/setting/provider/locale_provider.dart'; import 'package:flutter_deer/util/toast_utils.dart'; import 'package:flutter_deer/widgets/my_app_bar.dart'; import 'package:provider/provider.dart'; import 'package:sp_util/sp_util.dart'; class LocalePage extends StatefulWidget { const LocalePage({super.key}); @override _LocalePageState createState() => _LocalePageState(); } class _LocalePageState extends State { final List _list = ['跟随系统', '中文', 'English']; @override Widget build(BuildContext context) { final String? locale = SpUtil.getString(Constant.locale); String localeMode; switch(locale) { case 'zh': localeMode = _list[1]; break; case 'en': localeMode = _list[2]; break; default: localeMode = _list[0]; break; } return Scaffold( appBar: const MyAppBar( title: '多语言', ), body: ListView.separated( itemCount: _list.length, separatorBuilder: (_, __) => const Divider(), itemBuilder: (_, int index) { return InkWell( onTap: () { final String locale = index == 0 ? '' : (index == 1 ? 'zh' : 'en'); context.read().setLocale(locale); Toast.show('当前功能仅登录模块有效'); setState(() {}); }, child: Container( alignment: Alignment.centerLeft, padding: const EdgeInsets.symmetric(horizontal: 16.0), height: 50.0, child: Row( children: [ Expanded( child: Text(_list[index]), ), Opacity( opacity: localeMode == _list[index] ? 1 : 0, child: const Icon(Icons.done, color: Colors.blue), ) ], ), ), ); }, ), ); } } ================================================ FILE: lib/setting/page/setting_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/demo/demo_page.dart'; import 'package:flutter_deer/res/constant.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/setting/provider/locale_provider.dart'; import 'package:flutter_deer/setting/provider/theme_provider.dart'; import 'package:flutter_deer/setting/widgets/exit_dialog.dart'; import 'package:flutter_deer/setting/widgets/update_dialog.dart'; import 'package:flutter_deer/util/app_navigator.dart'; import 'package:flutter_deer/util/device_utils.dart'; import 'package:flutter_deer/widgets/click_item.dart'; import 'package:flutter_deer/widgets/my_app_bar.dart'; import 'package:provider/provider.dart'; import 'package:sp_util/sp_util.dart'; import '../setting_router.dart'; /// design/8设置/index.html class SettingPage extends StatefulWidget { const SettingPage({super.key}); @override _SettingPageState createState() => _SettingPageState(); } class _SettingPageState extends State { @override Widget build(BuildContext context) { return Scaffold( appBar: const MyAppBar( centerTitle: '设置', ), body: Consumer2( builder: (_, ThemeProvider provider, LocaleProvider localeProvider, __) { return Column( children: [ Gaps.vGap5, ClickItem( title: '账号管理', onTap: () => NavigatorUtils.push(context, SettingRouter.accountManagerPage), ), if (Device.isMobile) ClickItem( title: '清除缓存', content: '23.5MB', onTap: () {}, ), ClickItem( title: '夜间模式', content: _getCurrentTheme(), onTap: () => NavigatorUtils.push(context, SettingRouter.themePage), ), ClickItem( title: '多语言', content: _getCurrentLocale(), onTap: () => NavigatorUtils.push(context, SettingRouter.localePage), ), if (Device.isMobile) ClickItem( title: '检查更新', onTap: _showUpdateDialog, ), ClickItem( title: '关于我们', onTap: () => NavigatorUtils.push(context, SettingRouter.aboutPage), ), ClickItem( title: '退出当前账号', onTap: _showExitDialog, ), if (Device.isMobile) ClickItem( title: 'Deer Web版', onTap: () => NavigatorUtils.goWebViewPage(context, 'Flutter Deer', 'https://simplezhli.github.io/flutter_deer/'), ), ClickItem( title: '其他Demo', onTap: () => AppNavigator.push(context, const DemoPage()), ), ], ); }, ), ); } String _getCurrentTheme() { final String? theme = SpUtil.getString(Constant.theme); String themeMode; switch(theme) { case 'Dark': themeMode = '开启'; break; case 'Light': themeMode = '关闭'; break; default: themeMode = '跟随系统'; break; } return themeMode; } String _getCurrentLocale() { final String? locale = SpUtil.getString(Constant.locale); String localeMode; switch(locale) { case 'zh': localeMode = '中文'; break; case 'en': localeMode = 'English'; break; default: localeMode = '跟随系统'; break; } return localeMode; } void _showExitDialog() { showDialog( context: context, builder: (_) => const ExitDialog() ); } void _showUpdateDialog() { showDialog( context: context, barrierDismissible: false, builder: (_) => const UpdateDialog() ); } } ================================================ FILE: lib/setting/page/theme_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/res/constant.dart'; import 'package:flutter_deer/setting/provider/theme_provider.dart'; import 'package:flutter_deer/widgets/my_app_bar.dart'; import 'package:provider/provider.dart'; import 'package:sp_util/sp_util.dart'; class ThemePage extends StatefulWidget { const ThemePage({super.key}); @override _ThemePageState createState() => _ThemePageState(); } class _ThemePageState extends State { final List _list = ['跟随系统', '开启', '关闭']; @override Widget build(BuildContext context) { final String? theme = SpUtil.getString(Constant.theme); String themeMode; switch(theme) { case 'Dark': themeMode = _list[1]; break; case 'Light': themeMode = _list[2]; break; default: themeMode = _list[0]; break; } return Scaffold( appBar: const MyAppBar( title: '夜间模式', ), body: ListView.separated( itemCount: _list.length, separatorBuilder: (_, __) => const Divider(), itemBuilder: (_, int index) { return InkWell( onTap: () { final ThemeMode themeMode = index == 0 ? ThemeMode.system : (index == 1 ? ThemeMode.dark : ThemeMode.light); // Provider.of(context, listen: false).setTheme(themeMode); /// 与上方等价,provider 4.1.0添加的拓展方法 context.read().setTheme(themeMode); setState(() {}); }, child: Container( alignment: Alignment.centerLeft, padding: const EdgeInsets.symmetric(horizontal: 16.0), height: 50.0, child: Row( children: [ Expanded( child: Text(_list[index]), ), Opacity( opacity: themeMode == _list[index] ? 1 : 0, child: const Icon(Icons.done, color: Colors.blue), ) ], ), ), ); }, ), ); } } ================================================ FILE: lib/setting/provider/locale_provider.dart ================================================ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_deer/res/constant.dart'; import 'package:sp_util/sp_util.dart'; class LocaleProvider extends ChangeNotifier { Locale? get locale { final String locale = SpUtil.getString(Constant.locale) ?? ''; switch(locale) { case 'zh': return const Locale('zh', 'CN'); case 'en': return const Locale('en', 'US'); default: return null; } } void setLocale(String locale) { SpUtil.putString(Constant.locale, locale); notifyListeners(); } } ================================================ FILE: lib/setting/provider/theme_provider.dart ================================================ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_deer/res/constant.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/routers/web_page_transitions.dart'; import 'package:sp_util/sp_util.dart'; import '../../util/theme_utils.dart'; extension ThemeModeExtension on ThemeMode { String get value => ['System', 'Light', 'Dark'][index]; } class ThemeProvider extends ChangeNotifier { void syncTheme() { final String theme = SpUtil.getString(Constant.theme) ?? ''; if (theme.isNotEmpty && theme != ThemeMode.system.value) { notifyListeners(); } } void setTheme(ThemeMode themeMode) { SpUtil.putString(Constant.theme, themeMode.value); notifyListeners(); } ThemeMode getThemeMode(){ final String theme = SpUtil.getString(Constant.theme) ?? ''; switch(theme) { case 'Dark': return ThemeMode.dark; case 'Light': return ThemeMode.light; default: return ThemeMode.system; } } ThemeData getTheme({bool isDarkMode = false}) { return ThemeData( useMaterial3: false, primaryColor: isDarkMode ? Colours.dark_app_main : Colours.app_main, colorScheme: ColorScheme.fromSwatch().copyWith( brightness: isDarkMode ? Brightness.dark : Brightness.light, secondary: isDarkMode ? Colours.dark_app_main : Colours.app_main, error: isDarkMode ? Colours.dark_red : Colours.red, ), // Tab指示器颜色 indicatorColor: isDarkMode ? Colours.dark_app_main : Colours.app_main, // 页面背景色 scaffoldBackgroundColor: isDarkMode ? Colours.dark_bg_color : Colors.white, // 主要用于Material背景色 canvasColor: isDarkMode ? Colours.dark_material_bg : Colors.white, // 文字选择色(输入框选择文字等) // textSelectionColor: Colours.app_main.withAlpha(70), // textSelectionHandleColor: Colours.app_main, // 稳定版:1.23 变更(https://flutter.dev/docs/release/breaking-changes/text-selection-theme) textSelectionTheme: TextSelectionThemeData( selectionColor: Colours.app_main.withAlpha(70), selectionHandleColor: Colours.app_main, cursorColor: Colours.app_main, ), textTheme: TextTheme( // TextField输入文字颜色 titleMedium: isDarkMode ? TextStyles.textDark : TextStyles.text, // Text文字样式 bodyMedium: isDarkMode ? TextStyles.textDark : TextStyles.text, titleSmall: isDarkMode ? TextStyles.textDarkGray12 : TextStyles.textGray12, ), inputDecorationTheme: InputDecorationTheme( hintStyle: isDarkMode ? TextStyles.textHint14 : TextStyles.textDarkGray14, ), appBarTheme: AppBarTheme( elevation: 0.0, color: isDarkMode ? Colours.dark_bg_color : Colors.white, systemOverlayStyle: isDarkMode ? ThemeUtils.light : ThemeUtils.dark, ), dividerTheme: DividerThemeData( color: isDarkMode ? Colours.dark_line : Colours.line, space: 0.6, thickness: 0.6 ), cupertinoOverrideTheme: CupertinoThemeData( brightness: isDarkMode ? Brightness.dark : Brightness.light, ), pageTransitionsTheme: NoTransitionsOnWeb(), visualDensity: VisualDensity.standard, // https://github.com/flutter/flutter/issues/77142 ); } } ================================================ FILE: lib/setting/setting_router.dart ================================================ import 'package:fluro/fluro.dart'; import 'package:flutter_deer/routers/i_router.dart'; import 'package:flutter_deer/setting/page/locale_page.dart'; import 'package:flutter_deer/setting/page/theme_page.dart'; import 'page/about_page.dart'; import 'page/account_manager_page.dart'; import 'page/setting_page.dart'; class SettingRouter implements IRouterProvider{ static String settingPage = '/setting'; static String aboutPage = '/setting/about'; static String themePage = '/setting/theme'; static String localePage = '/setting/locale'; static String accountManagerPage = '/setting/accountManager'; @override void initRouter(FluroRouter router) { router.define(settingPage, handler: Handler(handlerFunc: (_, __) => const SettingPage())); router.define(aboutPage, handler: Handler(handlerFunc: (_, __) => const AboutPage())); router.define(themePage, handler: Handler(handlerFunc: (_, __) => const ThemePage())); router.define(localePage, handler: Handler(handlerFunc: (_, __) => const LocalePage())); router.define(accountManagerPage, handler: Handler(handlerFunc: (_, __) => const AccountManagerPage())); } } ================================================ FILE: lib/setting/widgets/exit_dialog.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/login/login_router.dart'; import 'package:flutter_deer/res/styles.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/widgets/base_dialog.dart'; class ExitDialog extends StatefulWidget { const ExitDialog({ super.key, }); @override _ExitDialog createState() => _ExitDialog(); } class _ExitDialog extends State { @override Widget build(BuildContext context) { return BaseDialog( title: '提示', child: const Padding( padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Text('您确定要退出登录吗?', style: TextStyles.textSize16), ), onPressed: () { NavigatorUtils.push(context, LoginRouter.loginPage, clearStack: true); }, ); } } ================================================ FILE: lib/setting/widgets/update_dialog.dart ================================================ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:flustars_flutter3/flustars_flutter3.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/util/image_utils.dart'; import 'package:flutter_deer/util/other_utils.dart'; import 'package:flutter_deer/util/theme_utils.dart'; import 'package:flutter_deer/util/toast_utils.dart'; import 'package:flutter_deer/util/version_utils.dart'; import 'package:flutter_deer/widgets/my_button.dart'; class UpdateDialog extends StatefulWidget { const UpdateDialog({super.key}); @override _UpdateDialogState createState() => _UpdateDialogState(); } class _UpdateDialogState extends State { final CancelToken _cancelToken = CancelToken(); bool _isDownload = false; double _value = 0; @override void dispose() { if (!_cancelToken.isCancelled && _value != 1) { _cancelToken.cancel(); } super.dispose(); } @override Widget build(BuildContext context) { final Color primaryColor = Theme.of(context).primaryColor; return PopScope( canPop: false, /// 使用false禁止返回键返回,达到强制升级目的 child: Scaffold( resizeToAvoidBottomInset: false, backgroundColor: Colors.transparent, body: Center( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Container( height: 120.0, width: 280.0, decoration: BoxDecoration( borderRadius: const BorderRadius.only(topLeft: Radius.circular(8.0), topRight: Radius.circular(8.0)), image: DecorationImage( image: ImageUtils.getAssetImage('update_head', format: ImageFormat.jpg), fit: BoxFit.cover, ), ), ), Container( width: 280.0, decoration: BoxDecoration( color: context.dialogBackgroundColor, borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(8.0), bottomRight: Radius.circular(8.0)) ), padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 15.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('新版本更新', style: TextStyles.textSize16), Gaps.vGap10, const Text('1.又双叒修复了一大堆bug。\n\n2.祭天了多名程序猿。'), Gaps.vGap15, if (_isDownload) LinearProgressIndicator( backgroundColor: Colours.line, valueColor: AlwaysStoppedAnimation(primaryColor), value: _value, ) else _buildButton(context), ], ), ), ], ), ) ), ); } Widget _buildButton(BuildContext context) { final Color primaryColor = Theme.of(context).primaryColor; return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ SizedBox( width: 110.0, height: 36.0, child: MyButton( text: '残忍拒绝', fontSize: Dimens.font_sp16, textColor: primaryColor, disabledTextColor: Colors.white, disabledBackgroundColor: Colours.text_gray_c, radius: 18.0, side: BorderSide( color: primaryColor, width: 0.8, ), backgroundColor: Colors.transparent, onPressed: () { NavigatorUtils.goBack(context); }, ), ), SizedBox( width: 110.0, height: 36.0, child: MyButton( text: '立即更新', fontSize: Dimens.font_sp16, onPressed: () { if (defaultTargetPlatform == TargetPlatform.iOS) { NavigatorUtils.goBack(context); VersionUtils.jumpAppStore(); } else { setState(() { _isDownload = true; }); _download(); } }, textColor: Colors.white, backgroundColor: primaryColor, disabledTextColor: Colors.white, disabledBackgroundColor: Colours.text_gray_c, radius: 18.0, ), ) ], ); } ///下载apk Future _download() async { try { setInitDir(initStorageDir: true); await DirectoryUtil.getInstance(); DirectoryUtil.createStorageDirSync(category: 'Download'); final String path = DirectoryUtil.getStoragePath(fileName: 'deer', category: 'Download', format: 'apk').nullSafe; final File file = File(path); /// 链接可能会失效 await Dio().download('http://imtt.dd.qq.com/16891/apk/FF9625F40FD26F015F4CDED37B6B66AE.apk', file.path, cancelToken: _cancelToken, onReceiveProgress: (int count, int total) { if (total != -1) { _value = count / total; setState(() { }); if (count == total) { NavigatorUtils.goBack(context); VersionUtils.install(path); } } }, ); } catch (e) { Toast.show('下载失败!'); debugPrint(e.toString()); setState(() { _isDownload = false; }); } } } ================================================ FILE: lib/shop/iview/shop_iview.dart ================================================ import 'package:flutter_deer/mvp/mvps.dart'; import 'package:flutter_deer/shop/models/user_entity.dart'; abstract class ShopIMvpView implements IMvpView { void setUser(UserEntity? user); bool get isAccessibilityTest; } ================================================ FILE: lib/shop/models/freight_config_model.dart ================================================ class FreightConfigModel { FreightConfigModel(this.min, this.max, this.type, this.isAdd, this.price); FreightConfigModel.fromJsonMap(Map map): min = map['min'] as String, max = map['max'] as String, type = map['type'] as int, isAdd = map['isAdd'] as bool, price = map['price'] as String; String min; String max; int type; bool isAdd; String price; Map toJson() { final Map data = {}; data['min'] = min; data['max'] = max; data['type'] = type; data['isAdd'] = isAdd; data['price'] = price; return data; } } ================================================ FILE: lib/shop/models/user_entity.dart ================================================ import 'package:flutter_deer/generated/json/base/json_field.dart'; import 'package:flutter_deer/generated/json/user_entity.g.dart'; @JsonSerializable() class UserEntity { UserEntity(); factory UserEntity.fromJson(Map json) => $UserEntityFromJson(json); Map toJson() => $UserEntityToJson(this); @JSONField(name: 'avatar_url') String? avatarUrl; String? name; int? id; String? blog; } ================================================ FILE: lib/shop/page/freight_config_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/shop/models/freight_config_model.dart'; import 'package:flutter_deer/shop/widgets/price_input_dialog.dart'; import 'package:flutter_deer/shop/widgets/range_price_input_dialog.dart'; import 'package:flutter_deer/util/theme_utils.dart'; import 'package:flutter_deer/util/toast_utils.dart'; import 'package:flutter_deer/widgets/load_image.dart'; import 'package:flutter_deer/widgets/my_app_bar.dart'; import 'package:flutter_deer/widgets/my_button.dart'; import 'package:flutter_deer/widgets/my_card.dart'; /// design/7店铺-店铺配置/index.html class FreightConfigPage extends StatefulWidget { const FreightConfigPage({super.key}); @override _FreightConfigPageState createState() => _FreightConfigPageState(); } class _FreightConfigPageState extends State { final List _list = []; @override void initState() { super.initState(); _reset(); } void _reset() { _list.clear(); _list.add(FreightConfigModel('0', '', 1, false, '')); _list.add(FreightConfigModel('', '', 1, true, '')); _list.add(FreightConfigModel('', '-1', 1, false, '')); } @override Widget build(BuildContext context) { return Scaffold( resizeToAvoidBottomInset: false, appBar: MyAppBar( title: '运费比例配置', actionName: '重置', onPressed: () { setState(() { _reset(); }); }, ), body: SafeArea( child: Stack( children: [ Positioned( left: 16.0, right: 16.0, bottom: 8.0, child: MyButton( onPressed: () { NavigatorUtils.goBack(context); }, text: '完成', ), ), Positioned( top: 0.0, left: 0.0, right: 0.0, bottom: 64.0, child: ListView.builder( itemExtent: 114.0, padding: const EdgeInsets.only(left: 16.0, right: 16.0), itemBuilder: (_, index) => _buildItem(index), itemCount: _list.length, ), ), ], ), ), ); } // 暂时没有对输入数据进行校验 Widget _buildItem(int index) { return _list[index].isAdd ? Semantics( label: '添加区间', child: GestureDetector( onTap: () { final FreightConfigModel config = _list[index - 1]; if (config.max.isNotEmpty && config.min.isNotEmpty) { setState(() { _list.insert(_list.length - 2, FreightConfigModel('', '', 1, false, '')); }); } else { Toast.show('请先完善上一个区间金额!'); return; } }, child: Container( key: const Key('add'), margin: const EdgeInsets.only(bottom: 8.0), padding: const EdgeInsets.symmetric(vertical: 32.0), decoration: BoxDecoration( color: context.isDark ? Colours.dark_bg_gray : Colours.bg_gray, borderRadius: BorderRadius.circular(8.0), ), child: const LoadAssetImage('shop/tj',), ), ), ) : Padding( padding: const EdgeInsets.only(bottom: 8.0), child: MyCard( child: Padding( padding: const EdgeInsets.all(15.0), child: Column( children: [ Row( children: [ Text(index == 0 ? '订单金额小于' : (index == _list.length - 1 ? '订单金额不小于' : '订单金额区间')), Expanded( child: Semantics( label: '填写订单金额', child: InkWell( onTap: () { if (index == 0 || index == _list.length - 1) { _showOrderPriceInputDialog(index); } else { _showRangePriceInputDialog(index); } }, child: Text( _getPriceText(index).isEmpty ? '订单金额' : _getPriceText(index), key: Key('订单金额$index'), textAlign: TextAlign.end, style: _getPriceText(index).isEmpty ? Theme.of(context).textTheme.titleSmall?.copyWith(fontSize: Dimens.font_sp14) : null, ), ), )), Gaps.hGap5, const Text('元'), ], ), const Spacer(), Gaps.line, const Spacer(), Row( children: [ Semantics( label: '选择比率', child: InkWell( onTap: () { setState(() { _list[index].type = 1; }); }, child: Row( mainAxisSize: MainAxisSize.min, children: [ LoadAssetImage(_list[index].type == 1 ? 'shop/xzyf' : 'shop/wxzyf', width: 16.0,), Gaps.hGap4, const Text('比率'), ], ), ), ), Gaps.hGap16, Semantics( label: '选择金额', child: InkWell( onTap: () { setState(() { _list[index].type = 0; }); }, child: Row( mainAxisSize: MainAxisSize.min, children: [ LoadAssetImage(_list[index].type == 0 ? 'shop/xzyf' : 'shop/wxzyf', width: 16.0), Gaps.hGap4, const Text('金额'), ], ), ), ), Expanded( child: Semantics( label: '填写${_list[index].type == 1 ? '运费比率' : '运费金额'}', child: InkWell( onTap: () => _showFreightInputDialog(index), child: Text( _list[index].price.isEmpty ? (_list[index].type == 1 ? '运费比率' : '运费金额'): _list[index].price, textAlign: TextAlign.end, style: _list[index].price.isEmpty ? Theme.of(context).textTheme.titleSmall?.copyWith(fontSize: Dimens.font_sp14) : null, ), ), )), Gaps.hGap5, Text(_list[index].type == 1 ? '%' : '元'), ], ) ], ), ), ), ); } void _showOrderPriceInputDialog(int index) { showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return PriceInputDialog( title: '订单金额', onPressed: (value) { setState(() { if (index == 0) { _list[index].max = value; } else { _list[index].min = value; } }); }, ); } ); } void _showRangePriceInputDialog(int index) { showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return RangePriceInputDialog( title: '订单金额', onPressed: (min, max) { setState(() { _list[index].min = min; _list[index].max = max; }); }, ); } ); } void _showFreightInputDialog(int index) { showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return PriceInputDialog( title: _list[index].type == 1 ? '运费比率' : '运费金额', inputMaxPrice: _list[index].type == 1 ? 100 : 100000, onPressed: (value) { setState(() { _list[index].price = value; }); }, ); } ); } String _getPriceText(int index) { if (index == 0) { if (_list[index].max.isEmpty) { return ''; } else { return _list[index].max; } } else if (index == _list.length - 1) { if (_list[index].min.isEmpty) { return ''; } else { return _list[index].min; } } else { if (_list[index].min.isEmpty || _list[index].max.isEmpty) { return ''; } else { return '${_list[index].min}~${_list[index].max}'; } } } } ================================================ FILE: lib/shop/page/input_text_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/widgets/my_app_bar.dart'; /// design/7店铺-店铺配置/index.html#artboard13 class InputTextPage extends StatefulWidget { const InputTextPage({ super.key, required this.title, this.content, this.hintText, this.keyboardType = TextInputType.text, }); final String title; final String? content; final String? hintText; final TextInputType? keyboardType; @override _InputTextPageState createState() => _InputTextPageState(); } class _InputTextPageState extends State { final TextEditingController _controller = TextEditingController(); List? _inputFormatters; late int _maxLength; @override void initState() { super.initState(); _controller.text = widget.content ?? ''; _maxLength = widget.keyboardType == TextInputType.phone ? 11 : 30; _inputFormatters = widget.keyboardType == TextInputType.phone ? [FilteringTextInputFormatter.allow(RegExp('[0-9]'))] : null; } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: MyAppBar( title: widget.title, actionName: '完成', onPressed: () { NavigatorUtils.goBackWithParams(context, _controller.text); }, ), body: Padding( padding: const EdgeInsets.only(top: 21.0, left: 16.0, right: 16.0, bottom: 16.0), child: Semantics( multiline: true, maxValueLength: _maxLength, child: TextField( maxLength: _maxLength, maxLines: 5, autofocus: true, controller: _controller, keyboardType: widget.keyboardType, inputFormatters: _inputFormatters, decoration: InputDecoration( hintText: widget.hintText, border: InputBorder.none, ), ), ), ), ); } } ================================================ FILE: lib/shop/page/message_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/widgets/my_app_bar.dart'; import 'package:flutter_deer/widgets/my_card.dart'; /// design/8设置/index.html#artboard2 class MessagePage extends StatefulWidget { const MessagePage({super.key}); @override _MessagePageState createState() => _MessagePageState(); } class _MessagePageState extends State { final ScrollController _scrollController = ScrollController(); @override void dispose() { _scrollController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: MyAppBar( centerTitle: '消息', actionName: '全部已读', onPressed: () {}, ), body: Scrollbar( // 加个滚动条 controller: _scrollController, child: ListView.builder( itemCount: 20, controller: _scrollController, physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 28.0), itemBuilder: (_, __) => _MessageItem(), ), ), ); } } class _MessageItem extends StatefulWidget { @override _MessageItemState createState() => _MessageItemState(); } class _MessageItemState extends State<_MessageItem> { @override Widget build(BuildContext context) { return Column( children: [ Gaps.vGap15, Text('2021-5-31 17:19:36', style: Theme.of(context).textTheme.titleSmall), Gaps.vGap8, MyCard( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Expanded(child: Text('系统通知')), Container( margin: const EdgeInsets.only(right: 4.0), height: 8.0, width: 8.0, decoration: BoxDecoration( color: Colours.app_main, borderRadius: BorderRadius.circular(4.0), ), ), Images.arrowRight, ], ), Gaps.vGap8, Gaps.line, Gaps.vGap8, const Text('供货商由于[商品缺货]原因,取消了采购订单。', style: TextStyles.textSize12), ], ), ), ) ], ); } } ================================================ FILE: lib/shop/page/select_address_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_2d_amap/flutter_2d_amap.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/util/other_utils.dart'; import 'package:flutter_deer/util/toast_utils.dart'; import 'package:flutter_deer/widgets/my_button.dart'; import 'package:flutter_deer/widgets/my_search_bar.dart'; class AddressSelectPage extends StatefulWidget { const AddressSelectPage({super.key}); @override _AddressSelectPageState createState() => _AddressSelectPageState(); } class _AddressSelectPageState extends State { List _list = []; int _index = 0; final ScrollController _controller = ScrollController(); AMap2DController? _aMap2DController; @override void dispose() { _controller.dispose(); super.dispose(); } @override void initState() { super.initState(); Flutter2dAMap.updatePrivacy(true); /// 配置key Flutter2dAMap.setApiKey( iOSKey: '4327916279bf45a044bb53b947442387', webKey: 'c9446a164fd1245dd110b54114095303', ); } @override Widget build(BuildContext context) { return Scaffold( resizeToAvoidBottomInset: false, appBar: MySearchBar( hintText: '搜索地址', onPressed: (text) { _controller.animateTo(0.0, duration: const Duration(milliseconds: 10), curve: Curves.ease); _index = 0; _aMap2DController?.search(text); }, ), body: SafeArea( child: Column( children: [ Expanded( flex: 9, child: AMap2DView( onPoiSearched: (result) { _controller.animateTo(0.0, duration: const Duration(milliseconds: 10), curve: Curves.ease); _index = 0; _list = result; setState(() { }); }, onAMap2DViewCreated: (controller) { _aMap2DController = controller; }, ), ), Expanded( flex: 11, child: // _list.isEmpty ? // Container( // alignment: Alignment.center, // child: CircularProgressIndicator(), // ) : ListView.separated( controller: _controller, itemCount: _list.length, separatorBuilder: (_, index) => const Divider(), itemBuilder: (_, index) { return _AddressItem( isSelected: _index == index, date: _list[index], onTap: () { _index = index; _aMap2DController?.move(_list[index].latitude.nullSafe, _list[index].longitude.nullSafe); setState(() { }); }, ); }, ), ), MyButton( onPressed: () { if (_list.isEmpty) { Toast.show('未选择地址!'); return; } NavigatorUtils.goBackWithParams(context, _list[_index]); }, text: '确认选择地址', ) ], ), ), ); } } class _AddressItem extends StatelessWidget { const _AddressItem({ required this.date, this.isSelected = false, this.onTap, }); final PoiSearch date; final bool isSelected; final GestureTapCallback? onTap; @override Widget build(BuildContext context) { return InkWell( onTap: onTap, child: Container( alignment: Alignment.centerLeft, padding: const EdgeInsets.symmetric(horizontal: 16.0), height: 50.0, child: Row( children: [ Expanded( child: Text( '${date.provinceName.nullSafe} ${date.cityName.nullSafe} ${date.adName.nullSafe} ${date.title.nullSafe}', ), ), Visibility( visible: isSelected, child: const Icon(Icons.done, color: Colors.blue), ) ], ), ), ); } } ================================================ FILE: lib/shop/page/shop_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/account/account_router.dart'; import 'package:flutter_deer/mvp/base_page.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/setting/setting_router.dart'; import 'package:flutter_deer/shop/iview/shop_iview.dart'; import 'package:flutter_deer/shop/models/user_entity.dart'; import 'package:flutter_deer/shop/presenter/shop_presenter.dart'; import 'package:flutter_deer/shop/provider/user_provider.dart'; import 'package:flutter_deer/shop/shop_router.dart'; import 'package:flutter_deer/util/image_utils.dart'; import 'package:flutter_deer/util/theme_utils.dart'; import 'package:flutter_deer/widgets/load_image.dart'; import 'package:provider/provider.dart'; /// design/6店铺-账户/index.html#artboard0 class ShopPage extends StatefulWidget { const ShopPage({ super.key, this.isAccessibilityTest = false, }); final bool isAccessibilityTest; @override _ShopPageState createState() => _ShopPageState(); } class _ShopPageState extends State with BasePageMixin, AutomaticKeepAliveClientMixin implements ShopIMvpView { final List _menuTitle = ['账户流水', '资金管理', '提现账号']; final List _menuImage = ['zhls', 'zjgl', 'txzh']; final List _menuDarkImage = ['dark_zhls', 'dark_zjgl', 'dark_txzh']; UserProvider provider = UserProvider(); @override void setUser(UserEntity? user) { provider.setUser(user); } @override bool get isAccessibilityTest => widget.isAccessibilityTest; @override Widget build(BuildContext context) { super.build(context); final Color? iconColor = ThemeUtils.getIconColor(context); final Widget line = Container( height: 0.6, width: double.infinity, margin: const EdgeInsets.only(left: 16.0), child: Gaps.line, ); return ChangeNotifierProvider( create: (_) => provider, child: Scaffold( appBar: AppBar( actions: [ IconButton( tooltip: '消息', onPressed: () { NavigatorUtils.push(context, ShopRouter.messagePage); }, icon: LoadAssetImage( 'shop/message', key: const Key('message'), width: 24.0, height: 24.0, color: iconColor, ), ), IconButton( tooltip: '设置', onPressed: () { NavigatorUtils.push(context, SettingRouter.settingPage); }, icon: LoadAssetImage( 'shop/setting', key: const Key('setting'), width: 24.0, height: 24.0, color: iconColor, ), ) ], ), body: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Gaps.vGap12, Consumer( builder: (_, provider, child) { final Widget header = Stack( children: [ const SizedBox(width: double.infinity, height: 56.0), const Text( '官方直营店', style: TextStyles.textBold24, ), Positioned( right: 0.0, child: CircleAvatar( radius: 28.0, backgroundColor: Colors.transparent, backgroundImage: ImageUtils.getImageProvider(provider.user?.avatarUrl, holderImg: 'shop/tx'), ), ), child!, ], ); return Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: MergeSemantics( child: header, ), ); }, child: const Positioned( top: 38.0, left: 0.0, child: Row( children: [ LoadAssetImage('shop/zybq', width: 40.0, height: 16.0,), Gaps.hGap8, Text('店铺账号:15000000000', style: TextStyles.textSize12) ], ), ), ), Gaps.vGap24, line, Gaps.vGap24, const MergeSemantics( child: Padding( padding: EdgeInsets.symmetric(horizontal: 16.0), child: Text( '账户', style: TextStyles.textBold18, ), ), ), _ShopFunctionModule( data: _menuTitle, image: _menuImage, darkImage: _menuDarkImage, onItemClick: (index) { if (index == 0) { NavigatorUtils.push(context, AccountRouter.accountRecordListPage); } else if (index == 1) { NavigatorUtils.push(context, AccountRouter.accountPage); } else if (index == 2) { NavigatorUtils.push(context, AccountRouter.withdrawalAccountPage); } }, ), line, Gaps.vGap24, const MergeSemantics( child: Padding( padding: EdgeInsets.symmetric(horizontal: 16.0), child: Text( '店铺', style: TextStyles.textBold18, ), ), ), /// 使用Flexible防止溢出 Flexible( child: _ShopFunctionModule( data: const ['店铺设置'], image: const ['dpsz'], darkImage: const ['dark_dpsz'], onItemClick: (index) { NavigatorUtils.push(context, ShopRouter.shopSettingPage); }, ), ), ], ), ), ); } @override bool get wantKeepAlive => true; @override ShopPagePresenter createPresenter() => ShopPagePresenter(); } class _ShopFunctionModule extends StatelessWidget { const _ShopFunctionModule({ required this.onItemClick, required this.data, required this.image, required this.darkImage, }); final void Function(int index) onItemClick; final List data; final List image; final List darkImage; @override Widget build(BuildContext context) { return GridView.builder( shrinkWrap: true, padding: const EdgeInsets.fromLTRB(8.0, 0, 8.0, 12.0), physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 4, childAspectRatio: 1.18, ), itemCount: data.length, itemBuilder: (_, index) { return InkWell( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ LoadAssetImage(context.isDark ? 'shop/${darkImage[index]}' : 'shop/${image[index]}', width: 32.0), Gaps.vGap4, Text( data[index], style: TextStyles.textSize12, ) ], ), onTap: () { onItemClick(index); }, ); }, ); } } ================================================ FILE: lib/shop/page/shop_setting_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_2d_amap/flutter_2d_amap.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/shop/shop_router.dart'; import 'package:flutter_deer/shop/widgets/pay_type_dialog.dart'; import 'package:flutter_deer/shop/widgets/price_input_dialog.dart'; import 'package:flutter_deer/shop/widgets/send_type_dialog.dart'; import 'package:flutter_deer/util/other_utils.dart'; import 'package:flutter_deer/widgets/click_item.dart'; import 'package:flutter_deer/widgets/my_app_bar.dart'; import 'package:flutter_deer/widgets/my_button.dart'; import 'package:flutter_deer/widgets/my_scroll_view.dart'; /// design/7店铺-店铺配置/index.html#artboard17 class ShopSettingPage extends StatefulWidget { const ShopSettingPage({super.key}); @override _ShopSettingPageState createState() => _ShopSettingPageState(); } class _ShopSettingPageState extends State { bool _check = false; List _selectValue = [0]; int _sendType = 0; String _sendPrice = '0.00'; String _freePrice = '0.00'; String _phone = ''; String _shopIntroduction = '零食铺子坚果饮料美酒佳肴…'; String _securityService = '假一赔十'; String _address = '陕西省 西安市 长安区 郭杜镇郭北村韩林路圣方医院斜对面'; @override Widget build(BuildContext context) { return Scaffold( // 防止键盘弹出,提交按钮升起。。。 resizeToAvoidBottomInset: false, appBar: const MyAppBar(), body: MyScrollView( padding: const EdgeInsets.symmetric(vertical: 16.0), bottomButton: Padding( padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 8.0), child: MyButton( text: '提交', onPressed: () => NavigatorUtils.goBack(context), ), ), children: [ Gaps.vGap5, Row( children: [ Gaps.hGap16, Text( _check ? '正在营业' : '暂停营业', style: TextStyles.textBold24, ), const Spacer(), Semantics( label: '店铺营业开关', child: Switch.adaptive( value: _check, onChanged: (bool val) { setState(() { _check = val; }); }, ), ), Gaps.hGap4, ], ), Gaps.vGap32, const Padding( padding: EdgeInsets.only(left: 16.0), child: Text('基础设置', style: TextStyles.textBold18), ), Gaps.vGap16, ClickItem( title: '店铺简介', content: _shopIntroduction, onTap: () { _goInputTextPage(context, '店铺简介', '这里有一段完美的简介…', _shopIntroduction, (result) { setState(() { _shopIntroduction = result.toString(); }); },); }, ), ClickItem( title: '保障服务', content: _securityService, onTap: () { _goInputTextPage(context, '保障服务', '这里有一段完美的说明…', _securityService, (result) { setState(() { _securityService = result.toString(); }); },); }, ), ClickItem( title: '支付方式', content: _getPayType(), onTap: _showPayTypeDialog, ), Gaps.vGap32, const Padding( padding: EdgeInsets.only(left: 16.0), child: Text('运费设置', style: TextStyles.textBold18), ), Gaps.vGap16, ClickItem( title: '运费配置', content: _sendType == 0 ? '运费满免配置' : '运费比例配置', onTap: _showSendTypeDialog, ), Visibility( visible: _sendType != 1, child: ClickItem( title: '运费满免', content: _freePrice, onTap: () { _showInputDialog('配送费满免', (value) { setState(() { _freePrice = value; }); }); }, ), ), Visibility( visible: _sendType != 1, child: ClickItem( title: '配送费用', content: _sendPrice, onTap: () { _showInputDialog('配送费用', (value) { setState(() { _sendPrice = value; }); }); }, ), ), Visibility( visible: _sendType != 0, child: ClickItem( maxLines: 10, title: '运费比例', content: '1、订单金额<20元,配送费为订单金额的1%\n2、订单金额≥20元,配送费为订单金额的1%', onTap: () => NavigatorUtils.push(context, ShopRouter.freightConfigPage), ), ), Gaps.vGap32, const Padding( padding: EdgeInsets.only(left: 16.0), child: Text('联系信息', style: TextStyles.textBold18,), ), Gaps.vGap16, ClickItem( title: '联系电话', content: _phone, onTap: () { _goInputTextPage(context, '联系电话', '这里有一串神秘的数字…', _phone, (result) { setState(() { _phone = result.toString(); }); }, keyboardType: TextInputType.phone,); }, ), ClickItem( maxLines: 2, title: '店铺地址', content: _address, onTap: () { NavigatorUtils.pushResult(context, ShopRouter.addressSelectPage, (result) { setState(() { final PoiSearch model = result as PoiSearch; _address = '${model.provinceName.nullSafe} ${model.cityName.nullSafe} ${model.adName.nullSafe} ${model.title.nullSafe}'; }); }); }, ), Gaps.vGap8, ], ) ); } String _getPayType() { String payType = ''; for (final int s in _selectValue) { if (s == 0) { payType = '$payType在线支付+'; } else if (s == 1) { payType = '$payType对公转账+'; } else if (s == 2) { payType = '$payType货到付款+'; } } return payType.substring(0, payType.length - 1); } void _goInputTextPage(BuildContext context, String title, String hintText, String content, void Function(Object?) function, {TextInputType? keyboardType}) { NavigatorUtils.pushResult(context, ShopRouter.inputTextPage, function, arguments: InputTextPageArgumentsData( title: title, hintText: hintText, content: content, keyboardType: keyboardType, ) ); } void _showInputDialog(String title, void Function(String) onPressed) { showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return PriceInputDialog( title: title, onPressed: onPressed, ); }, ); } void _showPayTypeDialog() { showElasticDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return PayTypeDialog( value: _selectValue, onPressed: (value) { setState(() { _selectValue = value.cast(); }); }, ); }, ); } void _showSendTypeDialog() { showElasticDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return SendTypeDialog( onPressed: (i, value) { setState(() { _sendType = i; }); }, ); }, ); } } class InputTextPageArgumentsData { InputTextPageArgumentsData({ required this.title, this.content, this.hintText, this.keyboardType, }); late String title; late String? content; late String? hintText; late TextInputType? keyboardType; } ================================================ FILE: lib/shop/presenter/shop_presenter.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/account/models/city_entity.dart'; import 'package:flutter_deer/mvp/base_page_presenter.dart'; import 'package:flutter_deer/net/net.dart'; import 'package:flutter_deer/shop/iview/shop_iview.dart'; import 'package:flutter_deer/shop/models/user_entity.dart'; class ShopPagePresenter extends BasePagePresenter { @override void initState() { WidgetsBinding.instance.addPostFrameCallback((_) { if (view.isAccessibilityTest) { return; } /// 接口请求例子 /// get请求参数queryParameters post请求参数params asyncRequestNetwork(Method.get, url: HttpApi.users, onSuccess: (data) { view.setUser(data); }, ); }); } void testListData() { /// 测试返回List类型数据解析 asyncRequestNetwork>(Method.get, url: HttpApi.subscriptions, onSuccess: (data) { }, ); } } ================================================ FILE: lib/shop/provider/user_provider.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/shop/models/user_entity.dart'; class UserProvider extends ChangeNotifier { UserEntity? _user; UserEntity? get user => _user; void setUser(UserEntity? user) { _user = user; notifyListeners(); } } ================================================ FILE: lib/shop/shop_router.dart ================================================ import 'package:fluro/fluro.dart'; import 'package:flutter_deer/routers/i_router.dart'; import 'page/freight_config_page.dart'; import 'page/input_text_page.dart'; import 'page/message_page.dart'; import 'page/select_address_page.dart'; import 'page/shop_page.dart'; import 'page/shop_setting_page.dart'; class ShopRouter implements IRouterProvider{ static String shopPage = '/shop'; static String shopSettingPage = '/shop/shopSetting'; static String messagePage = '/shop/message'; static String freightConfigPage = '/shop/freightConfig'; static String addressSelectPage = '/shop/addressSelect'; static String inputTextPage = '/shop/inputText'; @override void initRouter(FluroRouter router) { router.define(shopPage, handler: Handler(handlerFunc: (_, __) => const ShopPage())); router.define(shopSettingPage, handler: Handler(handlerFunc: (_, __) => const ShopSettingPage())); router.define(messagePage, handler: Handler(handlerFunc: (_, __) => const MessagePage())); router.define(freightConfigPage, handler: Handler(handlerFunc: (_, __) => const FreightConfigPage())); router.define(addressSelectPage, handler: Handler(handlerFunc: (_, __) => const AddressSelectPage())); router.define(inputTextPage, handler: Handler(handlerFunc: (context, params) { /// 类参数 final args = context!.settings!.arguments! as InputTextPageArgumentsData; return InputTextPage( title: args.title, hintText: args.hintText, content: args.content, keyboardType: args.keyboardType, ); })); } } ================================================ FILE: lib/shop/widgets/pay_type_dialog.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/util/toast_utils.dart'; import 'package:flutter_deer/widgets/base_dialog.dart'; import 'package:flutter_deer/widgets/load_image.dart'; /// design/7店铺-店铺配置/index.html#artboard10 class PayTypeDialog extends StatefulWidget { const PayTypeDialog({ super.key, this.value, required this.onPressed, }); final List? value; final void Function(List) onPressed; @override _PayTypeDialog createState() => _PayTypeDialog(); } class _PayTypeDialog extends State { late List _selectValue; final List _list = ['线上支付', '对公转账', '货到付款']; Widget _buildItem(int index) { _selectValue = widget.value ?? [0]; return Material( type: MaterialType.transparency, child: InkWell( child: SizedBox( height: 42.0, child: Row( children: [ Gaps.hGap16, Expanded( child: Text(_list[index]), ), LoadAssetImage(_selectValue.contains(index) ? 'shop/xz' : 'shop/xztm', width: 16.0, height: 16.0), Gaps.hGap16, ], ), ), onTap: () { if (mounted) { if (index == 0) { Toast.show('线上支付为必选项'); return; } setState(() { if (_selectValue.contains(index)) { _selectValue.remove(index); } else { _selectValue.add(index); } }); } }, ), ); } @override Widget build(BuildContext context) { return BaseDialog( title: '支付方式(多选)', child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisSize: MainAxisSize.min, children: List.generate(_list.length, (i) => _buildItem(i)) ), onPressed: () { NavigatorUtils.goBack(context); widget.onPressed(_selectValue); }, ); } } ================================================ FILE: lib/shop/widgets/price_input_dialog.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/util/input_formatter/number_text_input_formatter.dart'; import 'package:flutter_deer/util/theme_utils.dart'; import 'package:flutter_deer/util/toast_utils.dart'; import 'package:flutter_deer/widgets/base_dialog.dart'; /// design/7店铺-店铺配置/index.html#artboard3 class PriceInputDialog extends StatefulWidget { const PriceInputDialog({ super.key, this.title, this.inputMaxPrice = 100000, required this.onPressed, }); final String? title; final double inputMaxPrice; final void Function(String) onPressed; @override _PriceInputDialog createState() => _PriceInputDialog(); } class _PriceInputDialog extends State { final TextEditingController _controller = TextEditingController(); @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return BaseDialog( title: widget.title, child: Container( height: 34.0, alignment: Alignment.center, margin: const EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 0.0), decoration: BoxDecoration( color: ThemeUtils.getDialogTextFieldColor(context), borderRadius: BorderRadius.circular(2.0), ), child: TextField( key: const Key('price_input'), autofocus: true, controller: _controller, //style: TextStyles.textDark14, keyboardType: const TextInputType.numberWithOptions(decimal: true), // 金额限制数字格式 inputFormatters: [UsNumberTextInputFormatter(max: widget.inputMaxPrice)], decoration: InputDecoration( isDense: true, contentPadding: const EdgeInsets.symmetric(horizontal: 16.0), border: InputBorder.none, hintText: '输入${widget.title}', //hintStyle: TextStyles.textGrayC14, ), ), ), onPressed: () { if (_controller.text.isEmpty) { Toast.show('请输入${widget.title}'); return; } NavigatorUtils.goBack(context); widget.onPressed(_controller.text); }, ); } } ================================================ FILE: lib/shop/widgets/range_price_input_dialog.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/util/input_formatter/number_text_input_formatter.dart'; import 'package:flutter_deer/util/theme_utils.dart'; import 'package:flutter_deer/util/toast_utils.dart'; import 'package:flutter_deer/widgets/base_dialog.dart'; /// design/7店铺-店铺配置/index.html#artboard1 class RangePriceInputDialog extends StatefulWidget { const RangePriceInputDialog({ super.key, this.title, required this.onPressed, }); final String? title; final void Function(String, String) onPressed; @override _RangePriceInputDialog createState() => _RangePriceInputDialog(); } class _RangePriceInputDialog extends State { final TextEditingController _controller = TextEditingController(); final TextEditingController _controller1 = TextEditingController(); @override void dispose() { _controller.dispose(); _controller1.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return BaseDialog( title: widget.title, child: Container( height: 34.0, margin: const EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 0.0), decoration: BoxDecoration( color: ThemeUtils.getDialogTextFieldColor(context), borderRadius: BorderRadius.circular(2.0), ), child: Row( children: [ Expanded( child: _buildTextField(_controller), ), Container( alignment: Alignment.center, padding: const EdgeInsets.symmetric(horizontal: 12.0), color: context.dialogBackgroundColor, height: double.infinity, child: const Text('至') ), Expanded( child: _buildTextField(_controller1), ), ], ), ), onPressed: () { if (_controller.text.isEmpty || _controller1.text.isEmpty) { Toast.show('请输入${widget.title}'); return; } if (double.parse(_controller.text) >= double.parse(_controller1.text)) { Toast.show('最小金额不能大于最大金额!'); return; } NavigatorUtils.goBack(context); widget.onPressed(_controller.text, _controller1.text); }, ); } Widget _buildTextField(TextEditingController controller) { return TextField( autofocus: true, //style: TextStyles.textDark14, controller: controller, keyboardType: const TextInputType.numberWithOptions(decimal: true), // 金额限制数字格式 inputFormatters: [UsNumberTextInputFormatter()], decoration: const InputDecoration( isDense: true, contentPadding: EdgeInsets.symmetric(horizontal: 16.0), border: InputBorder.none, //hintStyle: TextStyles.textGray14, ), ); } } ================================================ FILE: lib/shop/widgets/send_type_dialog.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/widgets/base_dialog.dart'; import 'package:flutter_deer/widgets/load_image.dart'; /// design/7店铺-店铺配置/index.html#artboard9 class SendTypeDialog extends StatefulWidget { const SendTypeDialog({ super.key, required this.onPressed, }); final void Function(int, String) onPressed; @override _SendTypeDialog createState() => _SendTypeDialog(); } class _SendTypeDialog extends State { int _value = 0; final _list = ['运费满免配置', '运费比例配置']; Widget _buildItem(int index) { return Material( type: MaterialType.transparency, child: InkWell( child: SizedBox( height: 42.0, child: Row( children: [ Gaps.hGap16, Expanded( child: Text( _list[index], style: _value == index ? TextStyle( fontSize: Dimens.font_sp14, color: Theme.of(context).primaryColor, ) : null, ), ), Visibility( visible: _value == index, child: const LoadAssetImage('order/ic_check', width: 16.0, height: 16.0)), Gaps.hGap16, ], ), ), onTap: () { if (mounted) { setState(() { _value = index; }); } }, ), ); } @override Widget build(BuildContext context) { return BaseDialog( title: '运费配置', child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisSize: MainAxisSize.min, children: List.generate(_list.length, (i) => _buildItem(i)) ), onPressed: () { NavigatorUtils.goBack(context); widget.onPressed(_value, _list[_value]); }, ); } } ================================================ FILE: lib/statistics/page/goods_statistics_page.dart ================================================ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/statistics/widgets/selected_date.dart'; import 'package:flutter_deer/util/date_utils.dart' as date; import 'package:flutter_deer/util/image_utils.dart'; import 'package:flutter_deer/util/theme_utils.dart'; import 'package:flutter_deer/widgets/load_image.dart'; import 'package:flutter_deer/widgets/my_app_bar.dart'; import 'package:flutter_deer/widgets/my_card.dart'; import 'package:flutter_deer/widgets/pie_chart/pie_chart.dart'; import 'package:flutter_deer/widgets/pie_chart/pie_data.dart'; /// design/5统计/index.html#artboard11 class GoodsStatisticsPage extends StatefulWidget { const GoodsStatisticsPage({super.key}); @override _GoodsStatisticsPageState createState() => _GoodsStatisticsPageState(); } class _GoodsStatisticsPageState extends State { late DateTime _initialDay; int _selectedIndex = 2; /// false 待配货 true 已配货 bool _type = false; @override void initState() { super.initState(); _initialDay = DateTime.now(); } @override Widget build(BuildContext context) { final Widget time = Row( children: [ _buildSelectedText(_initialDay.year.toString(), 0), Gaps.hGap12, Gaps.vLine, Gaps.hGap12, _buildSelectedText('${_initialDay.month}月', 1), Gaps.hGap12, Gaps.vLine, Gaps.hGap12, _buildSelectedText(_type ? '${date.DateUtils.previousWeekToString(_initialDay)} -${date.DateUtils.apiDayFormat2(_initialDay)}' : '${_initialDay.day}日', 2), ], ); return Scaffold( appBar: MyAppBar( actionName: _type ? '待配货' : '已配货', onPressed: () { setState(() { _type = !_type; }); }, ), body: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 15.0), key: const Key('goods_statistics_list'), child: SafeArea( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Gaps.vGap4, Text(_type ? '已配货' : '待配货', style: TextStyles.textBold24), Gaps.vGap32, if (_type) time else MergeSemantics(child: time,), Gaps.vGap8, _buildChart(), const Text('热销商品排行', style: TextStyles.textBold18), ListView.builder( physics: const ClampingScrollPhysics(), padding: const EdgeInsets.only(top: 16.0), shrinkWrap: true, itemCount: 10, itemExtent: 76.0, itemBuilder: (context, index) => _buildItem(index), ), ], ), ), ), ); } Widget _buildChart() { return AspectRatio( aspectRatio: 1.30, // 百分比布局 child: FractionallySizedBox( heightFactor: 0.8, child: PieChart( name: _type ? '已配货' : '待配货', data: _getRandomData(), ), ), ); } List data = []; List data1 = []; // 数据为前十名数据与剩余数量 List _getRandomData() { if (data.isEmpty) { for (int i = 0; i < 9; i++) { final PieData pieData = PieData(); pieData.name = '商品$i'; pieData.number = Random.secure().nextInt(1000); data.add(pieData); } for (int i = 0; i < 11; i++) { final PieData pieData = PieData(); if (i == 10) { pieData.name = '其他'; pieData.number = Random.secure().nextInt(1000); pieData.color = Colours.text_gray_c; } else { pieData.name = '商品$i'; pieData.number = Random.secure().nextInt(1000); } data1.add(pieData); } } if (_type) { return data; } else { return data1; } } Widget _buildItem(int index) { return Padding( padding: const EdgeInsets.only(bottom: 8.0), child: MyCard( child: Padding( padding: const EdgeInsets.fromLTRB(8.0, 16.0, 16.0, 16.0), child: Row( children: [ if (index <= 2) LoadAssetImage('statistic/${index == 0 ? 'champion' : index == 1 ? 'runnerup' : 'thirdplace'}', width: 40.0,) else Container( alignment: Alignment.center, width: 18.0, height: 18.0, margin: const EdgeInsets.symmetric(horizontal: 11.0), decoration: BoxDecoration( shape: BoxShape.circle, color: PieChart.colorList[index] ), child: Text('${index + 1}', style: const TextStyle(color: Colors.white, fontSize: Dimens.font_sp12, fontWeight: FontWeight.bold)), ), Gaps.hGap4, Container( height: 36.0, width: 36.0, decoration: BoxDecoration( borderRadius: BorderRadius.circular(4.0), border: Border.all(color: const Color(0xFFF7F8FA), width: 0.6), image: DecorationImage( image: ImageUtils.getAssetImage('order/icon_goods'), fit: BoxFit.fitWidth, ), ), ), Gaps.hGap8, Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('那鲁火多饮料', maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontWeight: FontWeight.bold, fontSize: Dimens.font_sp12)), Text('250ml', style: Theme.of(context).textTheme.titleSmall), ], ), ), Gaps.hGap8, Visibility( visible: !_type, child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('100件', style: Theme.of(context).textTheme.titleSmall), Text('未支付', style: Theme.of(context).textTheme.titleSmall), ], ), ), Gaps.hGap16, Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: _type ? MainAxisAlignment.center : MainAxisAlignment.spaceBetween, children: [ Text('400件', style: Theme.of(context).textTheme.titleSmall), Visibility(visible: !_type, child: Text('已支付', style: Theme.of(context).textTheme.titleSmall)), ], ), ], ), ), ), ); } Widget _buildSelectedText(String text, int index) { final Color unSelectedTextColor = context.isDark ? Colors.white : Colours.dark_text_gray; return SelectedDateButton( text, fontSize: Dimens.font_sp15, selected: _type && _selectedIndex == index, unSelectedTextColor: unSelectedTextColor, onTap: _type ? () { setState(() { _selectedIndex = index; }); } : null, ); } } ================================================ FILE: lib/statistics/page/order_statistics_page.dart ================================================ import 'dart:math'; import 'package:common_utils/common_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/statistics/widgets/selected_date.dart'; import 'package:flutter_deer/util/date_utils.dart' as date; import 'package:flutter_deer/util/image_utils.dart'; import 'package:flutter_deer/util/theme_utils.dart'; import 'package:flutter_deer/widgets/bezier_chart/bezier_chart.dart'; import 'package:flutter_deer/widgets/load_image.dart'; import 'package:flutter_deer/widgets/my_app_bar.dart'; import 'package:flutter_deer/widgets/my_card.dart'; /// design/5统计/index.html#artboard1 /// design/5统计/index.html#artboard6 class OrderStatisticsPage extends StatefulWidget { const OrderStatisticsPage(this.index, {super.key}); final int index; @override _OrderStatisticsPageState createState() => _OrderStatisticsPageState(); } class _OrderStatisticsPageState extends State with TickerProviderStateMixin { int _selectedIndex = 2; late DateTime _initialDay; late Iterable _weeksDays; late List _currentMonthsDays; // 周视图中选择的日期 late int _selectedWeekDay; // 月视图中选择的日期 late DateTime _selectedDay; // 年视图中选择的月份 late int _selectedMonth; final List _monthList = []; bool _isExpanded = true; late Color _unSelectedTextColor; static const List _weeks = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']; @override void initState() { super.initState(); _initialDay = DateTime.now(); _selectedWeekDay = _initialDay.day; _selectedDay = _initialDay; _selectedMonth = _initialDay.month; _weeksDays = date.DateUtils.daysInRange(date.DateUtils.previousWeek(_initialDay), date.DateUtils.nextDay(_initialDay)).toList().sublist(1, 8); _currentMonthsDays = date.DateUtils.daysInMonth(_initialDay); _monthList.clear(); for (int i = 1; i < 13; i ++) { _monthList.add(i); } } @override Widget build(BuildContext context) { _unSelectedTextColor = context.isDark ? Colors.white : Colours.dark_text_gray; return Scaffold( appBar: MyAppBar( centerTitle: widget.index == 1 ? '订单统计' : '交易额统计', ), body: SingleChildScrollView( child: SafeArea( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Gaps.vGap16, Row( children: [ Gaps.hGap16, _buildButton(_initialDay.year.toString(), const Key('year'), 0), Gaps.hGap12, Gaps.vLine, Gaps.hGap12, _buildButton('${_initialDay.month}月', const Key('month'), 1), Gaps.hGap12, Gaps.vLine, Gaps.hGap12, _buildButton('${date.DateUtils.previousWeekToString(_initialDay)} -${date.DateUtils.apiDayFormat2(_initialDay)}', const Key('day'), 2), ], ), Gaps.vGap16, Flexible( child: Container( color: ThemeUtils.getStickyHeaderColor(context), padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: _selectedIndex != 1 ? 4.0 : 0.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ // AnimatedCrossFade( // firstChild: _buildCalendar(), // secondChild: _buildCalendar(), // firstCurve: const Interval(0.0, 0.0, curve: Curves.fastOutSlowIn), // secondCurve: const Interval(0.0, 0.0, curve: Curves.fastOutSlowIn), // sizeCurve: Curves.decelerate, // crossFadeState: _isExpanded ? CrossFadeState.showSecond : CrossFadeState.showFirst, // duration: const Duration(milliseconds: 300), // ), AnimatedSize( curve: Curves.decelerate, duration: const Duration(milliseconds: 300), child: _buildCalendar(), ), if (_selectedIndex == 1) InkWell( onTap: () { setState(() { _isExpanded = !_isExpanded; }); }, child: Semantics( label: _isExpanded ? '收起' : '展开', child: Container( height: 27.0, alignment: Alignment.topCenter, child: LoadAssetImage('statistic/${_isExpanded ? 'up' : 'down'}', width: 16.0, color: ThemeUtils.getIconColor(context),), ), ), ) ], ), ), ), Padding( padding: const EdgeInsets.only(left: 16.0, right: 16.0, top: 32.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(widget.index == 1 ? '订单走势' : '交易额走势', style: TextStyles.textBold18), Gaps.vGap16, _buildChart(Colours.app_main, Colours.shadow_blue, widget.index == 1 ? '全部订单' : '交易额(元)', '3000'), if (widget.index == 1) Column( children: [ Gaps.vGap16, _buildChart(const Color(0xFFFFAA33), const Color(0x80FFAA33), '完成订单', '2000'), Gaps.vGap16, _buildChart(Theme.of(context).colorScheme.error, const Color(0x80FF4759), '取消订单', '1000'), Gaps.vGap16, ], ) ], ), ), ], ), ), ), ); } Widget _buildButton(String text, Key key, int index) { return SelectedDateButton( text, key: key, fontSize: Dimens.font_sp15, selected: _selectedIndex == index, unSelectedTextColor: _unSelectedTextColor, onTap: () { setState(() { _selectedIndex = index; }); }, ); } Widget _buildChart(Color color, Color shadowColor, String title, String count) { final Column body = Column( children: [ Gaps.vGap16, Row( children: [ Gaps.hGap16, Text(title, style: const TextStyle(color: Colors.white)), const Spacer(), Text(count, style: const TextStyle(color: Colors.white)), Gaps.hGap16, ], ), Gaps.vGap4, Expanded( child: BezierChart( bezierChartScale: BezierChartScale.CUSTOM, xAxisCustomValues: const [0, 5, 10, 15, 20, 25, 30], footerValueBuilder: (double value) => '', bubbleLabelValueBuilder: (double value) => '\n', series: [ BezierLine( dataPointStrokeColor: color, label: widget.index == 1 ? '单' : '元', data: _getRandomData(), ), ], config: BezierChartConfig( footerHeight: 16, showVerticalIndicator: false, backgroundColor: color, ), ), ), ], ); return AspectRatio( aspectRatio: 3, child: MyCard( color: color, shadowColor: shadowColor, child: Container( //padding: const EdgeInsets.symmetric(horizontal: 16.0), decoration: BoxDecoration( image: DecorationImage( image: ImageUtils.getAssetImage('statistic/chart_fg'), fit: BoxFit.fill, ), ), child: body, ), ), ); } List> data = []; List> data1 = []; List> data2 = []; // 数据变化图标会刷新,否则不会 List> _getRandomData() { if (data.isEmpty) { for (int i = 0; i < 7; i++) { data.add(DataPoint(value: Random.secure().nextInt(3000).toDouble(), xAxis: (i * 5).toDouble())); } for (int i = 0; i < 7; i++) { data1.add(DataPoint(value: Random.secure().nextInt(3000).toDouble(), xAxis: (i * 5).toDouble())); } for (int i = 0; i < 7; i++) { data2.add(DataPoint(value: Random.secure().nextInt(3000).toDouble(), xAxis: (i * 5).toDouble())); } } if (_selectedIndex == 0) { return data; } else if (_selectedIndex == 1) { return data1; } else { return data2; } } Widget _buildCalendar() { List children = []; if (_selectedIndex == 0) { children = _builderYearCalendar(); } else if (_selectedIndex == 1) { children = _builderMonthCalendar(); } else if (_selectedIndex == 2) { children = _builderWeekCalendar(); } return GridView.count( physics: const ClampingScrollPhysics(), shrinkWrap: true, crossAxisCount: 7, children: children, ); } List _buildWeeks() { final List widgets = []; void addWidget(String str) { widgets.add(Center( child: Text(str, style: Theme.of(context).textTheme.titleSmall), )); } _weeks.forEach(addWidget); return widgets; } List _builderMonthCalendar() { final List dayWidgets = []; List list; if (_isExpanded) { list = _currentMonthsDays; } else { list = date.DateUtils.daysInWeek(_selectedDay); } dayWidgets.addAll(_buildWeeks()); void addButton(DateTime day) { dayWidgets.add( Center( child: SelectedDateButton( day.day.toString().padLeft(2, '0'), // 不足2位左边补0 selected: day.day == _selectedDay.day && !date.DateUtils.isExtraDay(day, _initialDay), // 不是本月的日期与超过当前日期的不可点击 enable: day.day <= _initialDay.day && !date.DateUtils.isExtraDay(day, _initialDay), unSelectedTextColor: _unSelectedTextColor, /// 日历中的具体日期添加完整语义 semanticsLabel: DateUtil.formatDate(day, format: DateFormats.zh_y_mo_d), onTap: () { setState(() { _selectedDay = day; }); }, ), ), ); } list.forEach(addButton); return dayWidgets; } List _builderYearCalendar() { final List monthWidgets = []; void addButton(int month) { monthWidgets.add( Center( child: SelectedDateButton( '$month月', selected: month == _selectedMonth, enable: month <= _initialDay.month, unSelectedTextColor: _unSelectedTextColor, onTap: () { setState(() { _selectedMonth = month; }); }, ), ), ); } _monthList.forEach(addButton); return monthWidgets; } List _builderWeekCalendar() { final List dayWidgets = []; void addButton(DateTime day) { dayWidgets.add( Center( child: SelectedDateButton( day.day.toString().padLeft(2, '0'), selected: day.day == _selectedWeekDay, unSelectedTextColor: _unSelectedTextColor, semanticsLabel: DateUtil.formatDate(day, format: DateFormats.zh_y_mo_d), onTap: () { setState(() { _selectedWeekDay = day.day; }); }, ), ), ); } _weeksDays.forEach(addButton); return dayWidgets; } } ================================================ FILE: lib/statistics/page/statistics_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/order/page/order_page.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/statistics/statistics_router.dart'; import 'package:flutter_deer/util/image_utils.dart'; import 'package:flutter_deer/util/screen_utils.dart'; import 'package:flutter_deer/util/theme_utils.dart'; import 'package:flutter_deer/widgets/load_image.dart'; import 'package:flutter_deer/widgets/my_card.dart'; import 'package:flutter_deer/widgets/my_flexible_space_bar.dart'; /// design/5统计/index.html class StatisticsPage extends StatefulWidget { const StatisticsPage({super.key}); @override _StatisticsPageState createState() => _StatisticsPageState(); } class _StatisticsPageState extends State { @override Widget build(BuildContext context) { return Scaffold( body: CustomScrollView( key: const Key('statistic_list'), physics: const ClampingScrollPhysics(), slivers: _sliverBuilder(), ), ); } bool isDark = false; List _sliverBuilder() { isDark = context.isDark; return [ SliverAppBar( systemOverlayStyle: isDark ? ThemeUtils.light : ThemeUtils.dark, backgroundColor: Colors.transparent, elevation: 0.0, centerTitle: true, expandedHeight: 100.0, pinned: true, flexibleSpace: MyFlexibleSpaceBar( background: isDark ? Container(height: 115.0, color: Colours.dark_bg_color,) : LoadAssetImage('statistic/statistic_bg', width: context.width, height: 115.0, fit: BoxFit.fill, ), centerTitle: true, titlePadding: const EdgeInsetsDirectional.only(start: 16.0, bottom: 14.0), collapseMode: CollapseMode.pin, title: Text('统计', style: TextStyle(color: ThemeUtils.getIconColor(context)),), ), ), SliverPersistentHeader( pinned: true, delegate: SliverAppBarDelegate( DecoratedBox( decoration: BoxDecoration( color: isDark ? Colours.dark_bg_color : null, image: isDark ? null : DecorationImage( image: ImageUtils.getAssetImage('statistic/statistic_bg1'), fit: BoxFit.fill, ), ), child: Container( margin: const EdgeInsets.symmetric(horizontal: 16.0), alignment: Alignment.center, height: 120.0, child: const MyCard( child: Row( children: [ _StatisticsTab('新订单(单)', 'xdd', '80'), _StatisticsTab('待配送(单)', 'dps', '80'), _StatisticsTab('今日交易额(元)', 'jrjye', '8000.00'), ], ), ), ), ) , 120.0, ), ), const SliverToBoxAdapter( child: Padding( padding: EdgeInsets.symmetric(horizontal: 16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Gaps.vGap32, Text('数据走势', style: TextStyles.textBold18), Gaps.vGap16, _StatisticsItem('订单统计', 'sjzs', 1), Gaps.vGap8, _StatisticsItem('交易额统计', 'jyetj', 2), Gaps.vGap8, _StatisticsItem('商品统计', 'sptj', 3), ], ), ), ) ]; } } class _StatisticsItem extends StatelessWidget { const _StatisticsItem(this.title, this.img, this.index); final String title; final String img; final int index; @override Widget build(BuildContext context) { return AspectRatio( aspectRatio: 2.14, child: GestureDetector( onTap: () { if (index == 1 || index == 2) { NavigatorUtils.push(context, '${StatisticsRouter.orderStatisticsPage}?index=$index'); } else { NavigatorUtils.push(context, StatisticsRouter.goodsStatisticsPage); } }, child: MyCard( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 16.0), child: Column( children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(title, style: TextStyles.textBold14), const LoadAssetImage('statistic/icon_selected', height: 16.0, width: 16.0) ], ), ), Expanded(child: LoadAssetImage('statistic/$img', fit: BoxFit.fill)) ], ), ), ), ), ); } } class _StatisticsTab extends StatelessWidget { const _StatisticsTab(this.title, this.img, this.content); final String title; final String img; final String content; @override Widget build(BuildContext context) { return Expanded( child: MergeSemantics( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ LoadAssetImage('statistic/$img', width: 40.0, height: 40.0), Gaps.vGap4, Text(title, style: Theme.of(context).textTheme.titleSmall), Gaps.vGap8, Text(content, style: const TextStyle(fontSize: Dimens.font_sp18)), ], ), ), ); } } ================================================ FILE: lib/statistics/statistics_router.dart ================================================ import 'package:fluro/fluro.dart'; import 'package:flutter_deer/routers/i_router.dart'; import 'page/goods_statistics_page.dart'; import 'page/order_statistics_page.dart'; class StatisticsRouter implements IRouterProvider{ static String orderStatisticsPage = '/statistics/order'; static String goodsStatisticsPage = '/statistics/goods'; @override void initRouter(FluroRouter router) { router.define(orderStatisticsPage, handler: Handler(handlerFunc: (_, params) { final int index = int.parse(params['index']?.first ?? '0'); return OrderStatisticsPage(index); })); router.define(goodsStatisticsPage, handler: Handler(handlerFunc: (_, __) => const GoodsStatisticsPage())); } } ================================================ FILE: lib/statistics/widgets/selected_date.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/util/theme_utils.dart'; class SelectedDateButton extends StatelessWidget { const SelectedDateButton(this.text,{ super.key, this.fontSize = 14.0, this.selected = false, required this.unSelectedTextColor, this.enable = true, this.onTap, this.semanticsLabel }); final String text; final double fontSize; final bool selected; final Color unSelectedTextColor; final GestureTapCallback? onTap; final bool enable; final String? semanticsLabel; @override Widget build(BuildContext context) { Widget child = _buildText(); if (enable) { child = InkWell( borderRadius: BorderRadius.circular(16.0), onTap: onTap, child: Container( constraints: BoxConstraints( maxWidth: fontSize > 14 ? double.infinity : 32.0, // 日历按钮32 * 32 minWidth: 32.0, maxHeight: 32.0, minHeight: 32.0, ), padding: EdgeInsets.symmetric(horizontal: fontSize > 14 ? 10.0 : 0.0), decoration: selected ? BoxDecoration( borderRadius: BorderRadius.circular(20.0), boxShadow: context.isDark ? null : const [ BoxShadow(color: Colours.shadow_blue, offset: Offset(0.0, 2.0), blurRadius: 8.0), ], gradient: const LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Color(0xFF5758FA), Colours.gradient_blue], ), ) : null, alignment: Alignment.center, child: child, ), ); } return child; } Widget _buildText() { if (text.endsWith('月') || text.endsWith('日')) { return RichText( text: TextSpan( children: [ TextSpan(text: text.substring(0, text.length - 1), style: TextStyle(color: getTextColor(), fontSize: fontSize)), TextSpan(text: text.substring(text.length - 1), style: TextStyle(color: getTextColor(), fontSize: fontSize - 4.0)), ], ), ); } else { return Text(text, semanticsLabel: semanticsLabel, style: TextStyle(color: getTextColor(), fontSize: fontSize), ); } } Color getTextColor() { return enable ? (selected ? Colors.white : unSelectedTextColor) : Colours.text_gray_c; } } ================================================ FILE: lib/store/page/store_audit_page.dart ================================================ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_2d_amap/flutter_2d_amap.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/shop/shop_router.dart'; import 'package:flutter_deer/store/store_router.dart'; import 'package:flutter_deer/util/other_utils.dart'; import 'package:flutter_deer/util/theme_utils.dart'; import 'package:flutter_deer/widgets/my_app_bar.dart'; import 'package:flutter_deer/widgets/my_button.dart'; import 'package:flutter_deer/widgets/my_scroll_view.dart'; import 'package:flutter_deer/widgets/selected_image.dart'; import 'package:flutter_deer/widgets/selected_item.dart'; import 'package:flutter_deer/widgets/text_field_item.dart'; import 'package:image_picker/image_picker.dart'; import 'package:keyboard_actions/keyboard_actions.dart'; /// design/2店铺审核/index.html class StoreAuditPage extends StatefulWidget { const StoreAuditPage({super.key}); @override _StoreAuditPageState createState() => _StoreAuditPageState(); } class _StoreAuditPageState extends State { final GlobalKey _imageGlobalKey = GlobalKey(); final FocusNode _nodeText1 = FocusNode(); final FocusNode _nodeText2 = FocusNode(); final FocusNode _nodeText3 = FocusNode(); final ImagePicker picker = ImagePicker(); String _address = '陕西省 西安市 雁塔区 高新六路201号'; KeyboardActionsConfig _buildConfig(BuildContext context) { return KeyboardActionsConfig( keyboardActionsPlatform: KeyboardActionsPlatform.IOS, keyboardBarColor: ThemeUtils.getKeyboardActionsColor(context), actions: [ KeyboardActionsItem( focusNode: _nodeText1, displayDoneButton: false, ), KeyboardActionsItem( focusNode: _nodeText2, displayDoneButton: false, ), KeyboardActionsItem( focusNode: _nodeText3, toolbarButtons: [ (node) { return GestureDetector( onTap: () => node.unfocus(), child: Padding( padding: const EdgeInsets.only(right: 16.0), child: Text(Utils.getCurrLocale() == 'zh' ? '关闭' : 'Close'), ), ); }, ], ), ], ); } @override Widget build(BuildContext context) { return Scaffold( appBar: const MyAppBar( centerTitle: '店铺审核资料', ), body: MyScrollView( padding: const EdgeInsets.symmetric(vertical: 16.0), keyboardConfig: _buildConfig(context), tapOutsideToDismiss: true, bottomButton: Padding( padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 8.0), child: MyButton( onPressed: () { debugPrint('文件路径:${_imageGlobalKey.currentState?.pickedFile?.path}'); NavigatorUtils.push(context, StoreRouter.auditResultPage); }, text: '提交', ), ), children: _buildBody(), ), /// 同时存在底部按钮与keyboardConfig配置时,为保证Android与iOS平台软键盘弹出高度正常,添加下面的代码。 resizeToAvoidBottomInset: defaultTargetPlatform != TargetPlatform.iOS, ); } List _buildBody() { return [ Gaps.vGap5, const Padding( padding: EdgeInsets.only(left: 16.0), child: Text('店铺资料', style: TextStyles.textBold18), ), Gaps.vGap16, Center( child: SelectedImage( key: _imageGlobalKey, ), ), Gaps.vGap10, Center( child: Text( '店主手持身份证或营业执照', style: Theme.of(context).textTheme.titleSmall?.copyWith(fontSize: Dimens.font_sp14), ), ), Gaps.vGap16, TextFieldItem( focusNode: _nodeText1, title: '店铺名称', hintText: '填写店铺名称' ), SelectedItem( title: '主营范围', content: _sortName, onTap: () => _showBottomSheet() ), SelectedItem( title: '店铺地址', content: _address, onTap: () { NavigatorUtils.pushResult(context, ShopRouter.addressSelectPage, (result) { setState(() { final PoiSearch model = result as PoiSearch; _address = '${model.provinceName.nullSafe} ${model.cityName.nullSafe} ${model.adName.nullSafe} ${model.title.nullSafe}'; }); }); } ), Gaps.vGap32, const Padding( padding:EdgeInsets.only(left: 16.0), child: Text('店主信息', style: TextStyles.textBold18), ), Gaps.vGap16, TextFieldItem( focusNode: _nodeText2, title: '店主姓名', hintText: '填写店主姓名' ), TextFieldItem( focusNode: _nodeText3, keyboardType: TextInputType.phone, title: '联系电话', hintText: '填写店主联系电话' ) ]; } String _sortName = ''; final List _list = ['水果生鲜', '家用电器', '休闲食品', '茶酒饮料', '美妆个护', '粮油调味', '家庭清洁', '厨具用品', '儿童玩具', '床上用品']; void _showBottomSheet() { showModalBottomSheet( context: context, builder: (BuildContext context) { // 可滑动ListView关闭BottomSheet return DraggableScrollableSheet( key: const Key('goods_sort'), initialChildSize: 0.7, minChildSize: 0.65, expand: false, builder: (_, scrollController) { return ListView.builder( controller: scrollController, itemExtent: 48.0, itemBuilder: (_, index) { return InkWell( child: Container( padding: const EdgeInsets.symmetric(horizontal: 16.0), alignment: Alignment.centerLeft, child: Text(_list[index]), ), onTap: () { setState(() { _sortName = _list[index]; }); NavigatorUtils.goBack(context); }, ); }, itemCount: _list.length, ); }, ); }, ); } } ================================================ FILE: lib/store/page/store_audit_result_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/routers/routers.dart'; import 'package:flutter_deer/widgets/load_image.dart'; import 'package:flutter_deer/widgets/my_app_bar.dart'; import 'package:flutter_deer/widgets/my_button.dart'; /// design/2店铺审核/index.html#artboard2 class StoreAuditResultPage extends StatefulWidget { const StoreAuditResultPage({super.key}); @override _StoreAuditResultPageState createState() => _StoreAuditResultPageState(); } class _StoreAuditResultPageState extends State { @override Widget build(BuildContext context) { return Scaffold( appBar: const MyAppBar( title: '审核结果', ), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ Gaps.vGap50, const LoadAssetImage('store/icon_success', width: 80.0, height: 80.0, ), Gaps.vGap12, const Text( '恭喜,店铺资料审核成功', style: TextStyles.textSize16, ), Gaps.vGap8, Text( '2021-02-21 15:20:10', style: Theme.of(context).textTheme.titleSmall, ), Gaps.vGap8, Text( '预计完成时间:02月28日', style: Theme.of(context).textTheme.titleSmall, ), Gaps.vGap24, MyButton( onPressed: () { NavigatorUtils.push(context, Routes.home, clearStack: true); }, text: '进入', ) ], ), ), ); } } ================================================ FILE: lib/store/store_router.dart ================================================ import 'package:fluro/fluro.dart'; import 'package:flutter_deer/routers/i_router.dart'; import 'page/store_audit_page.dart'; import 'page/store_audit_result_page.dart'; class StoreRouter implements IRouterProvider{ static String auditPage = '/store/audit'; static String auditResultPage = '/store/auditResult'; @override void initRouter(FluroRouter router) { router.define(auditPage, handler: Handler(handlerFunc: (_, __) => const StoreAuditPage())); router.define(auditResultPage, handler: Handler(handlerFunc: (_, __) => const StoreAuditResultPage())); } } ================================================ FILE: lib/util/app_navigator.dart ================================================ import 'package:flutter/material.dart'; /// Navigator工具类 /// 更推荐使用'routers/fluro_navigator.dart' class AppNavigator { static void push(BuildContext context, Widget scene) { Navigator.push( context, MaterialPageRoute( builder: (BuildContext context) => scene, ), ); } /// 替换页面 当新的页面进入后,之前的页面将执行dispose方法 static void pushReplacement(BuildContext context, Widget scene) { Navigator.pushReplacement( context, MaterialPageRoute( builder: (BuildContext context) => scene, ), ); } /// 指定页面加入到路由中,然后将其他所有的页面全部pop static void pushAndRemoveUntil(BuildContext context, Widget scene) { Navigator.pushAndRemoveUntil( context, MaterialPageRoute( builder: (BuildContext context) => scene, ), (route) => false ); } static void pushResult(BuildContext context, Widget scene, void Function(Object?) function) { Navigator.push( context, MaterialPageRoute( builder: (BuildContext context) => scene, ), ).then((dynamic result) { // 页面返回result为null if (result == null) { return; } function(result); }).catchError((dynamic error) { debugPrint('$error'); }); } /// 返回 static void goBack(BuildContext context) { Navigator.pop(context); } /// 带参数返回 static void goBackWithParams(BuildContext context, dynamic result) { Navigator.pop(context, result); } } ================================================ FILE: lib/util/change_notifier_manage.dart ================================================ import 'package:flutter/widgets.dart'; /// @weilu https://github.com/simplezhli/flutter_deer /// /// 便于管理ChangeNotifier,不用重复写模板代码。 /// 之前: /// ```dart /// class TestPageState extends State { /// final TextEditingController _controller = TextEditingController(); /// final FocusNode _nodeText = FocusNode(); /// /// @override /// void initState() { /// _controller.addListener(callback); /// super.initState(); /// } /// /// @override /// void dispose() { /// _controller.removeListener(callback); /// _controller.dispose(); /// _nodeText.dispose(); /// super.dispose(); /// } /// } /// ``` /// 使用示例: /// ```dart /// class TestPageState extends State with ChangeNotifierMixin { /// final TextEditingController _controller = TextEditingController(); /// final FocusNode _nodeText = FocusNode(); /// /// @override /// Map?>? changeNotifier() { /// return { /// _controller: [callback], /// _nodeText: null, /// }; /// } /// } /// ``` mixin ChangeNotifierMixin on State { Map?>? _map; Map?>? changeNotifier(); @override void initState() { _map = changeNotifier(); /// 遍历数据,如果callbacks不为空则添加监听 _map?.forEach((changeNotifier, callbacks) { if (callbacks != null && callbacks.isNotEmpty) { void addListener(VoidCallback callback) { changeNotifier?.addListener(callback); } callbacks.forEach(addListener); } }); super.initState(); } @override void dispose() { _map?.forEach((changeNotifier, callbacks) { if (callbacks != null && callbacks.isNotEmpty) { void removeListener(VoidCallback callback) { changeNotifier?.removeListener(callback); } callbacks.forEach(removeListener); } changeNotifier?.dispose(); }); super.dispose(); } } ================================================ FILE: lib/util/date_utils.dart ================================================ import 'package:intl/intl.dart'; /// date_utils(version:0.1.0+2) : https://github.com/apptreesoftware/date_utils /// @Author: aleksanderwozniak /// @GitHub: https://github.com/aleksanderwozniak/table_calendar /// @Description: Date Util. class DateUtils { static final DateFormat _monthFormat = DateFormat('MMMM yyyy'); static final DateFormat _dayFormat = DateFormat('dd'); static final DateFormat _firstDayFormat = DateFormat('MMM dd'); static final DateFormat _fullDayFormat = DateFormat('EEE MMM dd, yyyy'); static final DateFormat _apiDayFormat = DateFormat('yyyy-MM-dd'); static final DateFormat _apiDayFormat2 = DateFormat('yy.MM.dd'); static String formatMonth(DateTime d) => _monthFormat.format(d); static String formatDay(DateTime d) => _dayFormat.format(d); static String formatFirstDay(DateTime d) => _firstDayFormat.format(d); static String fullDayFormat(DateTime d) => _fullDayFormat.format(d); static String apiDayFormat(DateTime d) => _apiDayFormat.format(d); static String apiDayFormat2(DateTime d) => _apiDayFormat2.format(d); static const List weekdays = [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ]; /// 周一开始 static List daysInMonth(DateTime month) { final first = firstDayOfMonth(month); final daysBefore = first.weekday - 1; var firstToDisplay = first.subtract(Duration(days: daysBefore)); if (firstToDisplay.hour == 23) { firstToDisplay = firstToDisplay.add(const Duration(hours: 1)); } var last = lastDayOfMonth(month); if (last.hour == 23) { last = last.add(const Duration(hours: 1)); } var daysAfter = 7 - last.weekday; daysAfter++; var lastToDisplay = last.add(Duration(days: daysAfter)); if (lastToDisplay.hour == 1) { lastToDisplay = lastToDisplay.subtract(const Duration(hours: 1)); } return daysInRange(firstToDisplay, lastToDisplay).toList(); } static bool isFirstDayOfMonth(DateTime day) { return isSameDay(firstDayOfMonth(day), day); } static bool isLastDayOfMonth(DateTime day) { return isSameDay(lastDayOfMonth(day), day); } static DateTime firstDayOfMonth(DateTime month) { return DateTime(month.year, month.month); } static DateTime firstDayOfWeek(DateTime day) { /// Handle Daylight Savings by setting hour to 12:00 Noon /// rather than the default of Midnight day = DateTime.utc(day.year, day.month, day.day, 12); /// Weekday is on a 1-7 scale Monday - Sunday, /// This Calendar works from Sunday - Monday final decreaseNum = day.weekday % 7; return day.subtract(Duration(days: decreaseNum)); } static DateTime lastDayOfWeek(DateTime day) { /// Handle Daylight Savings by setting hour to 12:00 Noon /// rather than the default of Midnight day = DateTime.utc(day.year, day.month, day.day, 12); /// Weekday is on a 1-7 scale Monday - Sunday, /// This Calendar's Week starts on Sunday final increaseNum = day.weekday % 7; return day.add(Duration(days: 7 - increaseNum)); } /// The last day of a given month static DateTime lastDayOfMonth(DateTime month) { final beginningNextMonth = (month.month < 12) ? DateTime(month.year, month.month + 1) : DateTime(month.year + 1); return beginningNextMonth.subtract(const Duration(days: 1)); } /// Returns a [DateTime] for each day the given range. /// /// [start] inclusive /// [end] exclusive static Iterable daysInRange(DateTime start, DateTime end) sync* { var i = start; var offset = start.timeZoneOffset; while (i.isBefore(end)) { yield i; i = i.add(const Duration(days: 1)); final timeZoneDiff = i.timeZoneOffset - offset; if (timeZoneDiff.inSeconds != 0) { offset = i.timeZoneOffset; i = i.subtract(Duration(seconds: timeZoneDiff.inSeconds)); } } } /// Whether or not two times are on the same day. static bool isSameDay(DateTime a, DateTime b) { return a.year == b.year && a.month == b.month && a.day == b.day; } static bool isSameWeek(DateTime a, DateTime b) { /// Handle Daylight Savings by setting hour to 12:00 Noon /// rather than the default of Midnight a = DateTime.utc(a.year, a.month, a.day); b = DateTime.utc(b.year, b.month, b.day); final diff = a.toUtc().difference(b.toUtc()).inDays; if (diff.abs() >= 7) { return false; } final min = a.isBefore(b) ? a : b; final max = a.isBefore(b) ? b : a; final result = max.weekday % 7 - min.weekday % 7 >= 0; return result; } static DateTime previousMonth(DateTime m) { var year = m.year; var month = m.month; if (month == 1) { year--; month = 12; } else { month--; } return DateTime(year, month); } static DateTime nextMonth(DateTime m) { var year = m.year; var month = m.month; if (month == 12) { year++; month = 1; } else { month++; } return DateTime(year, month); } static DateTime previousWeek(DateTime w) { return w.subtract(const Duration(days: 7)); } static DateTime nextWeek(DateTime w) { return w.add(const Duration(days: 7)); } static String previousWeekToString(DateTime w) { return apiDayFormat2(w.subtract(const Duration(days: 6))); } static DateTime nextDay(DateTime w) { return w.add(const Duration(days: 1)); } static List daysInWeek(DateTime week) { final first = _firstDayOfWeek(week); final last = _lastDayOfWeek(week); final days = daysInRange(first, last); return days.map((day) => DateTime(day.year, day.month, day.day)).toList(); } static DateTime _firstDayOfWeek(DateTime day) { day = DateTime.utc(day.year, day.month, day.day, 12); final decreaseNum = day.weekday - 1; return day.subtract(Duration(days: decreaseNum)); } static DateTime _lastDayOfWeek(DateTime day) { day = DateTime.utc(day.year, day.month, day.day, 12); final increaseNum = day.weekday - 1; return day.add(Duration(days: 7 - increaseNum)); } static bool isExtraDay(DateTime day, DateTime now) { return _isExtraDayBefore(day, now) || _isExtraDayAfter(day, now); } static bool _isExtraDayBefore(DateTime day, DateTime now) { return day.month < now.month; } static bool _isExtraDayAfter(DateTime day, DateTime now) { return day.month > now.month; } } ================================================ FILE: lib/util/device_utils.dart ================================================ import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_deer/res/constant.dart'; /// https://medium.com/gskinner-team/flutter-simplify-platform-screen-size-detection-4cb6fc4f7ed1 class Device { static bool get isDesktop => !isWeb && (isWindows || isLinux || isMacOS); static bool get isMobile => isAndroid || isIOS; static bool get isWeb => kIsWeb; static bool get isWindows => !isWeb && Platform.isWindows; static bool get isLinux => !isWeb && Platform.isLinux; static bool get isMacOS => !isWeb && Platform.isMacOS; static bool get isAndroid => !isWeb && Platform.isAndroid; static bool get isFuchsia => !isWeb && Platform.isFuchsia; static bool get isIOS => !isWeb && Platform.isIOS; static late AndroidDeviceInfo _androidInfo; static Future initDeviceInfo() async { if (isAndroid) { final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); _androidInfo = await deviceInfo.androidInfo; } } /// 使用前记得初始化 static int getAndroidSdkInt() { if (Constant.isDriverTest) { return -1; } if (isAndroid) { return _androidInfo.version.sdkInt; } else { return -1; } } } ================================================ FILE: lib/util/handle_error_utils.dart ================================================ import 'dart:async'; import 'package:flutter/widgets.dart'; import 'package:flutter_deer/res/constant.dart'; /// 捕获全局异常,进行统一处理。 void handleError(void Function() body) { /// 重写Flutter异常回调 FlutterError.onError FlutterError.onError = (FlutterErrorDetails details) { if (!Constant.inProduction) { // debug时,直接将异常信息打印。 FlutterError.dumpErrorToConsole(details); } else { // release时,将异常交由zone统一处理。 Zone.current.handleUncaughtError(details.exception, details.stack!); } }; /// 使用runZonedGuarded捕获Flutter未捕获的异常 runZonedGuarded(body, (Object error, StackTrace stackTrace) async { await _reportError(error, stackTrace); }); } Future _reportError(Object error, StackTrace stackTrace) async { if (!Constant.inProduction) { debugPrintStack( stackTrace: stackTrace, label: error.toString(), maxFrames: 100, ); } else { /// 将异常信息收集并上传到服务器。可以直接使用类似`flutter_bugly`插件处理异常上报。 } } ================================================ FILE: lib/util/image_utils.dart ================================================ import 'package:cached_network_image/cached_network_image.dart'; import 'package:common_utils/common_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; class ImageUtils { static ImageProvider getAssetImage(String name, {ImageFormat format = ImageFormat.png}) { return AssetImage(getImgPath(name, format: format)); } static String getImgPath(String name, {ImageFormat format = ImageFormat.png}) { return 'assets/images/$name.${format.value}'; } static ImageProvider getImageProvider(String? imageUrl, {String holderImg = 'none'}) { if (TextUtil.isEmpty(imageUrl)) { return AssetImage(getImgPath(holderImg)); } return CachedNetworkImageProvider(imageUrl!); } } enum ImageFormat { png, jpg, gif, webp } extension ImageFormatExtension on ImageFormat { String get value => ['png', 'jpg', 'gif', 'webp'][index]; } ================================================ FILE: lib/util/input_formatter/fix_ios_input_formatter.dart ================================================ import 'dart:io'; import 'package:flutter/services.dart'; /// https://github.com/flutter/flutter/issues/25511 /// 主要针对TextInput有设置maxLength且在iOS平台使用原生输入法输入中文时崩溃问题。 /// 使用方法: /// TextField( /// inputFormatters: [FixIOSTextInputFormatter()], /// ) /// 使用后问题是输入的拼音不展示。 /// /// 1.22已修复:https://github.com/flutter/flutter/pull/63754 @Deprecated('1.22已修复') class FixIOSTextInputFormatter extends TextInputFormatter { @override TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) { if (Platform.isIOS) { // ios Composing变化也执行format,因为在拼音阶段没有执行LengthLimitingTextInputFormatter,从拼音到汉字需要重新执行 if (newValue.composing.isValid) { // ios拼音阶段不执行长度限制的format return TextEditingValue.empty; } } return TextEditingValue( text: newValue.text, selection: TextSelection.collapsed(offset: newValue.selection.end), ); } } ================================================ FILE: lib/util/input_formatter/number_text_input_formatter.dart ================================================ import 'package:flutter/services.dart'; /// 数字、小数格式化(默认两位小数) class UsNumberTextInputFormatter extends TextInputFormatter { UsNumberTextInputFormatter({ this.digit = 2, this.max = 1000000 }); /// 允许输入的小数位数,-1代表不限制位数 final int digit; /// 允许输入的最大值 final double max; static const double _kDefaultDouble = 0.001; double _strToFloat(String str, [double defaultValue = _kDefaultDouble]) { try { return double.parse(str); } catch (e) { return defaultValue; } } ///获取目前的小数位数 int _getValueDigit(String value) { if (value.contains('.')) { return value.split('.')[1].length; } else { return -1; } } @override TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) { String value = newValue.text; int selectionIndex = newValue.selection.end; if (value == '.') { value = '0.'; selectionIndex++; } else if (value != '' && value != _kDefaultDouble.toString() && _strToFloat(value) == _kDefaultDouble || _getValueDigit(value) > digit || _strToFloat(value) > max) { value = oldValue.text; selectionIndex = oldValue.selection.end; } return TextEditingValue( text: value, selection: TextSelection.collapsed(offset: selectionIndex), ); } } ================================================ FILE: lib/util/log_utils.dart ================================================ import 'dart:convert' as convert; import 'package:common_utils/common_utils.dart'; import 'package:flutter_deer/res/constant.dart'; /// 输出Log工具类 class Log { static const String tag = 'DEER-LOG'; static void init() { LogUtil.init(isDebug: !Constant.inProduction, maxLen: 512); } static void d(String msg, {String tag = tag}) { if (!Constant.inProduction) { LogUtil.v(msg, tag: tag); } } static void e(String msg, {String tag = tag}) { if (!Constant.inProduction) { LogUtil.e(msg, tag: tag); } } static void json(String msg, {String tag = tag}) { if (!Constant.inProduction) { try { final dynamic data = convert.json.decode(msg); if (data is Map) { _printMap(data); } else if (data is List) { _printList(data); } else { LogUtil.v(msg, tag: tag); } } catch(e) { LogUtil.e(msg, tag: tag); } } } // https://github.com/Milad-Akarie/pretty_dio_logger static void _printMap(Map data, {String tag = tag, int tabs = 1, bool isListItem = false, bool isLast = false}) { final bool isRoot = tabs == 1; final String initialIndent = _indent(tabs); tabs++; if (isRoot || isListItem) { LogUtil.v('$initialIndent{', tag: tag); } data.keys.toList().asMap().forEach((index, dynamic key) { final bool isLast = index == data.length - 1; dynamic value = data[key]; if (value is String) { value = '"$value"'; } if (value is Map) { if (value.isEmpty) { LogUtil.v('${_indent(tabs)} $key: $value${!isLast ? ',' : ''}', tag: tag); } else { LogUtil.v('${_indent(tabs)} $key: {', tag: tag); _printMap(value, tabs: tabs); } } else if (value is List) { if (value.isEmpty || value.length > 50) { LogUtil.v('${_indent(tabs)} $key: $value', tag: tag); } else { LogUtil.v('${_indent(tabs)} $key: [', tag: tag); _printList(value, tabs: tabs); LogUtil.v('${_indent(tabs)} ]${isLast ? '' : ','}', tag: tag); } } else { final msg = value.toString().replaceAll('\n', ''); LogUtil.v('${_indent(tabs)} $key: $msg${!isLast ? ',' : ''}', tag: tag); } }); LogUtil.v('$initialIndent}${isListItem && !isLast ? ',' : ''}', tag: tag); } static void _printList(List list, {String tag = tag, int tabs = 1}) { list.asMap().forEach((i, dynamic e) { final bool isLast = i == list.length - 1; if (e is Map) { if (_canFlattenMap(e, list)) { LogUtil.v('${_indent(tabs)} $e${!isLast ? ',' : ''}', tag: tag); } else { _printMap(e, tabs: tabs + 1, isListItem: true, isLast: isLast); } } else { LogUtil.v('${_indent(tabs + 2)} $e${isLast ? '' : ','}', tag: tag); } }); } /// 避免一秒内输出过多行数的日志被限制显示 /// Single process limit 250/s drop 66 lines. static bool _canFlattenMap(Map map, List list) { return list.length * map.length > 100; } static String _indent([int tabCount = 1]) => ' ' * tabCount; } ================================================ FILE: lib/util/other_utils.dart ================================================ import 'dart:ui'; import 'package:common_utils/common_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_deer/res/constant.dart'; import 'package:flutter_deer/util/theme_utils.dart'; import 'package:flutter_deer/util/toast_utils.dart'; import 'package:keyboard_actions/keyboard_actions_config.dart'; import 'package:keyboard_actions/keyboard_actions_item.dart'; import 'package:sp_util/sp_util.dart'; import 'package:url_launcher/url_launcher.dart'; class Utils { /// 打开链接 static Future launchWebURL(String url) async { final Uri uri = Uri.parse(url); if (await canLaunchUrl(uri)) { await launchUrl(uri); } else { Toast.show('打开链接失败!'); } } /// 调起拨号页 static Future launchTelURL(String phone) async { final Uri uri = Uri.parse('tel:$phone'); if (await canLaunchUrl(uri)) { await launchUrl(uri); } else { Toast.show('拨号失败!'); } } static String formatPrice(String price, {MoneyFormat format = MoneyFormat.END_INTEGER}){ return MoneyUtil.changeYWithUnit(NumUtil.getDoubleByValueStr(price) ?? 0, MoneyUnit.YUAN, format: format); } static KeyboardActionsConfig getKeyboardActionsConfig(BuildContext context, List list) { return KeyboardActionsConfig( keyboardBarColor: ThemeUtils.getKeyboardActionsColor(context), actions: List.generate(list.length, (i) => KeyboardActionsItem( focusNode: list[i], toolbarButtons: [ (node) { return GestureDetector( onTap: () => node.unfocus(), child: Padding( padding: const EdgeInsets.only(right: 16.0), child: Text(getCurrLocale() == 'zh' ? '关闭' : 'Close'), ), ); }, ], )), ); } static String? getCurrLocale() { final String locale = SpUtil.getString(Constant.locale)!; if (locale == '') { return PlatformDispatcher.instance.locale.languageCode; } return locale; } } Future showElasticDialog({ required BuildContext context, bool barrierDismissible = true, required WidgetBuilder builder, }) { return showGeneralDialog( context: context, pageBuilder: (BuildContext buildContext, Animation animation, Animation secondaryAnimation) { final Widget pageChild = Builder(builder: builder); return SafeArea( child: pageChild, ); }, barrierDismissible: barrierDismissible, barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, barrierColor: Colors.black54, transitionDuration: const Duration(milliseconds: 550), transitionBuilder: _buildDialogTransitions, ); } Widget _buildDialogTransitions(BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { return FadeTransition( opacity: CurvedAnimation( parent: animation, curve: Curves.easeOut, ), child: SlideTransition( position: Tween( begin: const Offset(0.0, 0.3), end: Offset.zero ).animate(CurvedAnimation( parent: animation, curve: const ElasticOutCurve(0.85), reverseCurve: Curves.easeOutBack, )), child: child, ), ); } /// String 空安全处理 extension StringExtension on String? { String get nullSafe => this ?? ''; } ================================================ FILE: lib/util/screen_utils.dart ================================================ import 'dart:io'; import 'dart:math'; import 'package:flutter/widgets.dart'; /// https://medium.com/gskinner-team/flutter-simplify-platform-screen-size-detection-4cb6fc4f7ed1 /// /// bool isLandscape = Screen.isLandscape(context) /// bool isLargePhone = Screen.diagonal(context) > 720; /// bool isTablet = Screen.diagonalInches(context) >= 7; /// bool isNarrow = Screen.widthInches(context) < 3.5; class Screen { static double get _ppi => (Platform.isAndroid || Platform.isIOS)? 150 : 96; static bool isLandscape(BuildContext context) => MediaQuery.of(context).orientation == Orientation.landscape; //PIXELS static Size size(BuildContext context) => MediaQuery.of(context).size; static double width(BuildContext context) => size(context).width; static double height(BuildContext context) => size(context).height; static double diagonal(BuildContext context) { final Size s = size(context); return sqrt((s.width * s.width) + (s.height * s.height)); } //INCHES static Size inches(BuildContext context) { final Size pxSize = size(context); return Size(pxSize.width / _ppi, pxSize.height/ _ppi); } static double widthInches(BuildContext context) => inches(context).width; static double heightInches(BuildContext context) => inches(context).height; static double diagonalInches(BuildContext context) => diagonal(context) / _ppi; } extension MediaQueryExtension on BuildContext { Size get size => Screen.size(this); double get height => Screen.size(this).height; double get width => Screen.size(this).width; } ================================================ FILE: lib/util/theme_utils.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_deer/res/resources.dart'; class ThemeUtils { static bool isDark(BuildContext context) { return Theme.of(context).brightness == Brightness.dark; } static Color? getDarkColor(BuildContext context, Color darkColor) { return isDark(context) ? darkColor : null; } static Color? getIconColor(BuildContext context) { return isDark(context) ? Colours.dark_text : null; } static Color getStickyHeaderColor(BuildContext context) { return isDark(context) ? Colours.dark_bg_gray_ : Colours.bg_gray_; } static Color getDialogTextFieldColor(BuildContext context) { return isDark(context) ? Colours.dark_bg_gray_ : Colours.bg_gray; } static Color? getKeyboardActionsColor(BuildContext context) { return isDark(context) ? Colours.dark_bg_color : Colors.grey[200]; } static const SystemUiOverlayStyle light = SystemUiOverlayStyle( statusBarColor: Colors.transparent, systemNavigationBarColor: Colours.dark_bg_color, systemNavigationBarIconBrightness: Brightness.light, statusBarIconBrightness: Brightness.light, statusBarBrightness: Brightness.dark, ); static const SystemUiOverlayStyle dark = SystemUiOverlayStyle( statusBarColor: Colors.transparent, systemNavigationBarColor: Colors.white, systemNavigationBarIconBrightness: Brightness.dark, statusBarIconBrightness: Brightness.dark, statusBarBrightness: Brightness.light, ); } extension ThemeExtension on BuildContext { bool get isDark => ThemeUtils.isDark(this); Color get backgroundColor => Theme.of(this).scaffoldBackgroundColor; Color get dialogBackgroundColor => Theme.of(this).canvasColor; } ================================================ FILE: lib/util/toast_utils.dart ================================================ import 'package:oktoast/oktoast.dart'; /// Toast工具类 class Toast { static void show(String? msg, {int duration = 2000}) { if (msg == null) { return; } showToast( msg, duration: Duration(milliseconds: duration), dismissOtherToast: true ); } static void cancelToast() { dismissAllToast(); } } ================================================ FILE: lib/util/version_utils.dart ================================================ import 'package:flutter/services.dart'; class VersionUtils { static const MethodChannel _kChannel = MethodChannel('version'); /// 应用安装 static void install(String path) { _kChannel.invokeMethod('install', {'path': path}); } /// AppStore跳转 static void jumpAppStore() { _kChannel.invokeMethod('jumpAppStore'); } } ================================================ FILE: lib/widgets/base_dialog.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/routers/fluro_navigator.dart'; import 'package:flutter_deer/util/device_utils.dart'; import 'package:flutter_deer/widgets/my_button.dart'; /// 自定义dialog的模板 class BaseDialog extends StatelessWidget { const BaseDialog({ super.key, this.title, this.onPressed, this.hiddenTitle = false, required this.child }); final String? title; final VoidCallback? onPressed; final Widget child; final bool hiddenTitle; @override Widget build(BuildContext context) { final Widget dialogTitle = Visibility( visible: !hiddenTitle, child: Padding( padding: const EdgeInsets.only(bottom: 8.0), child: Text( hiddenTitle ? '' : title ?? '', style: TextStyles.textBold18, ), ), ); final Widget bottomButton = Row( children: [ _DialogButton( text: '取消', textColor: Colours.text_gray, onPressed: () => NavigatorUtils.goBack(context), ), const SizedBox( height: 48.0, width: 0.6, child: VerticalDivider(), ), _DialogButton( text: '确定', textColor: Theme.of(context).primaryColor, onPressed: onPressed, ), ], ); final Widget content = Material( borderRadius: BorderRadius.circular(8.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ Gaps.vGap24, dialogTitle, Flexible(child: child), Gaps.vGap8, Gaps.line, bottomButton, ], ), ); final Widget body = MediaQuery.removeViewInsets( removeLeft: true, removeTop: true, removeRight: true, removeBottom: true, context: context, child: Center( child: SizedBox( width: 270.0, child: content, ), ), ); /// Android 11添加了键盘弹出动画,这与我添加的过渡动画冲突(原先iOS、Android 没有相关过渡动画,相关问题跟踪:https://github.com/flutter/flutter/issues/19279)。 /// 因为在Android 11上,viewInsets的值在键盘弹出过程中是变化的(以前只有开始结束的值)。 /// 所以解决方法就是在Android 11及以上系统中使用Padding代替AnimatedPadding。 if (Device.getAndroidSdkInt() >= 30) { return Padding( padding: MediaQuery.of(context).viewInsets, child: body, ); } else { return AnimatedPadding( padding: MediaQuery.of(context).viewInsets, duration: const Duration(milliseconds: 120), curve: Curves.easeInCubic, // easeOutQuad child: body, ); } } } class _DialogButton extends StatelessWidget { const _DialogButton({ required this.text, this.textColor, this.onPressed, }); final String text; final Color? textColor; final VoidCallback? onPressed; @override Widget build(BuildContext context) { return Expanded( child: MyButton( text: text, textColor: textColor, onPressed: onPressed, backgroundColor: Colors.transparent, ), ); } } ================================================ FILE: lib/widgets/bezier_chart/bezier_chart.dart ================================================ library bezier_chart; export 'bezier_chart_config.dart'; export 'bezier_chart_widget.dart'; export 'bezier_line.dart'; /// 曲线图表(修改版本:1.0.17+1) https://github.com/aeyrium/bezier-chart ================================================ FILE: lib/widgets/bezier_chart/bezier_chart_config.dart ================================================ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; /// Type of Bezier line Chart enum BezierChartScale { HOURLY, WEEKLY, MONTHLY, YEARLY, ///numbers sorted in an increasing way. CUSTOM, } enum BezierChartAggregation { AVERAGE, SUM, FIRST, COUNT, MAX, MIN, } ///`BezierChartConfig` allows the customization of the `BezierChart` widget class BezierChartConfig { ///`true` if you want to display the vertical indicator final bool showVerticalIndicator; final Color verticalIndicatorColor; ///`width` of the line used for the vertical indicator final double verticalIndicatorStrokeWidth; ///`true` if you want to keep the info indicator in a fixed position final bool verticalIndicatorFixedPosition; ///`true` if you want to display the vertical line in full height final bool verticalLineFullHeight; ///Color of the bubble indicator, it's white by default final Color bubbleIndicatorColor; ///TextStyle for the title displayed inside the bubble indicator final TextStyle bubbleIndicatorTitleStyle; ///TextStyle for the value displayed inside the bubble indicator final TextStyle bubbleIndicatorValueStyle; ///NumberFormat for the value displayed inside the bubble indicator final NumberFormat? bubbleIndicatorValueFormat; ///TextStyle for the label displayed inside the bubble indicator final TextStyle bubbleIndicatorLabelStyle; ///Color of the background of the chart final Color backgroundColor; ///Gradient of the background of the chart final LinearGradient? backgroundGradient; ///`true` if you want to display the value of the Y axis, [false] by default final bool displayYAxis; ///If [displayYAxis] is true, then you can set a positive value to display the steps of Y axis values ///e.g 1: stepsYAxis : 5 , if your maxValue is 100, then the Y values should be: [0,5,10,15 .... 100] ///e.g 2: stepsYAxis : 10 , if your maxValue is 100, then the Y values should be: [10,20,30,40 .... 100] final int? stepsYAxis; ///`true` if you want to start the values of Y axis from the minimum value of your Y values. final bool startYAxisFromNonZeroValue; ///TextStyle of the text of the Y Axis values final TextStyle? yAxisTextStyle; ///TextStyle of the text of the X Axis values final TextStyle? xAxisTextStyle; ///Height of the footer final double footerHeight; ///`true` if you want to display the data points final bool showDataPoints; ///`true` if you want to snap between each data point final bool snap; ///`true` if you want to enable pinch Zoom for `bezierChartScale` of date types /// Pinch and zoom is used to switch beetwen charts of date types final bool pinchZoom; ///If the `contentWidth` is upper than the current width then the content will be scrollable (only valid for `bezierChartScale` = `CUSTOM`) final double? contentWidth; ///`true` if you want to display a vertical line on each X data point, it only works when there is one `BezierLine`. final bool displayLinesXAxis; ///Color for the vertical line in each X point, only works when `displayLinesXAxis` is true final Color xLinesColor; ///`true` if you want do display the dot when there is no value specified (The values inside `onMissingValue`) final bool displayDataPointWhenNoValue; ///The physics for horizontal ScrollView final ScrollPhysics physics; ///`true` if you want do update bubble info on tap action instead of long press. This option will disable tap to hide bubble action final bool updatePositionOnTap; BezierChartConfig({ this.verticalIndicatorStrokeWidth = 2.0, this.verticalIndicatorColor = Colors.black, this.showVerticalIndicator = true, this.showDataPoints = true, this.displayYAxis = false, this.snap = true, this.backgroundColor = Colors.transparent, this.xAxisTextStyle, this.yAxisTextStyle, this.footerHeight = 35.0, this.contentWidth, this.pinchZoom = true, this.bubbleIndicatorColor = Colors.white, this.backgroundGradient, this.verticalIndicatorFixedPosition = false, this.startYAxisFromNonZeroValue = true, this.displayLinesXAxis = false, this.stepsYAxis, this.xLinesColor = Colors.grey, this.displayDataPointWhenNoValue = true, this.bubbleIndicatorLabelStyle = const TextStyle( color: Colors.grey, fontWeight: FontWeight.w700, fontSize: 9, ), this.bubbleIndicatorTitleStyle = const TextStyle( color: Colors.grey, fontWeight: FontWeight.w600, fontSize: 9.5, ), this.bubbleIndicatorValueStyle = const TextStyle( color: Colors.black, fontWeight: FontWeight.bold, fontSize: 11, ), this.bubbleIndicatorValueFormat, this.physics = const AlwaysScrollableScrollPhysics(), this.updatePositionOnTap = false, bool? verticalLineFullHeight, }) : this.verticalLineFullHeight = verticalLineFullHeight ?? verticalIndicatorFixedPosition; } ================================================ FILE: lib/widgets/bezier_chart/bezier_chart_widget.dart ================================================ import 'dart:math'; import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'bezier_line.dart'; import 'bezier_chart_config.dart'; import 'package:intl/intl.dart' as intl; import 'my_single_child_scroll_view.dart'; typedef FooterValueBuilder = String Function(double value); typedef FooterDateTimeBuilder = String Function( DateTime value, BezierChartScale scaleType); class BezierChart extends StatefulWidget { ///Chart configuration final BezierChartConfig config; ///Type of Chart final BezierChartScale bezierChartScale; ///Aggregation of Chart final BezierChartAggregation bezierChartAggregation; ///This value is required only if the `BezierChartScale` is `BezierChartScale.CUSTOM` ///and these values must be sorted in increasing way (These will be showed in the Axis X). final List xAxisCustomValues; ///[Optional] This callback only works if the `BezierChartScale` is `BezierChartScale.CUSTOM` otherwise it will be ignored ///This is used to display a custom footer value based on the current 'x' value final FooterValueBuilder? footerValueBuilder; ///[Optional] This callback only works if the `BezierChartScale` is `BezierChartScale.CUSTOM` otherwise it will be ignored ///This is used to display a custom bubble label value based on the current 'x' value final FooterValueBuilder? bubbleLabelValueBuilder; ///[Optional] This callback only works if the `BezierChartScale` is Date type otherwise it will be ignored ///This is used to display a custom footer value based on the current 'x' value final FooterDateTimeBuilder? footerDateTimeBuilder; ///[Optional] This callback only works if the `BezierChartScale` is Date type otherwise it will be ignored ///This is used to display a custom bubble label value based on the current 'x' value final FooterDateTimeBuilder? bubbleLabelDateTimeBuilder; ///[Optional] This callback notify when the display indicator is visible or not final ValueChanged? onIndicatorVisible; ///[Optional] This callback will display the current `double` value selected by the indicator ///Only works when the `BezierChartScale` is not `BezierChartScale.CUSTOM` final ValueChanged? onValueSelected; ///[Optional] This callback will display the current `DateTime` selected by the indicator ///Only works when the `BezierChartScale` is date type final ValueChanged? onDateTimeSelected; ///This value is required only if the `BezierChartScale` is not `BezierChartScale.CUSTOM` final DateTime? fromDate; ///This value is required only if the `BezierChartScale` is not `BezierChartScale.CUSTOM` final DateTime? toDate; ///This value represents the date selected to display the info in the Chart ///For `BezierChartScale.HOURLY` it will use year, month, day and hour ///For `BezierChartScale.WEEKLY` it will use year, month and day ///For `BezierChartScale.MONTHLY` it will use year, month ///For `BezierChartScale.YEARLY` it will use year final DateTime? selectedDate; ///This value represents the value selected to display the info in the Chart ///It's only for `BezierChartScale.CUSTOM` final double? selectedValue; ///Beziers used in the Axis Y final List series; ///Notify if the `BezierChartScale` changed, it only works with date scales. final ValueChanged? onScaleChanged; BezierChart({ Key? key, required this.config, required this.xAxisCustomValues, this.footerValueBuilder, this.bubbleLabelValueBuilder, this.footerDateTimeBuilder, this.bubbleLabelDateTimeBuilder, this.fromDate, this.toDate, this.selectedDate, this.onIndicatorVisible, this.onDateTimeSelected, this.onValueSelected, this.selectedValue, this.bezierChartAggregation = BezierChartAggregation.SUM, required this.bezierChartScale, required this.series, this.onScaleChanged, }) : assert( (bezierChartScale == BezierChartScale.CUSTOM) || bezierChartScale != BezierChartScale.CUSTOM, 'The xAxisCustomValues and series must not be null', ), assert( bezierChartScale == BezierChartScale.CUSTOM && _isSorted(xAxisCustomValues) || bezierChartScale != BezierChartScale.CUSTOM, 'The xAxisCustomValues must be sorted in increasing way', ), assert( bezierChartScale == BezierChartScale.CUSTOM && _compareLengths(xAxisCustomValues.length, series) || bezierChartScale != BezierChartScale.CUSTOM, 'xAxisCustomValues lenght must be equals to series length', ), assert( (bezierChartScale == BezierChartScale.CUSTOM && _areAllPositive(xAxisCustomValues) && _checkCustomValues(series)) || bezierChartScale != BezierChartScale.CUSTOM, 'xAxisCustomValues and series must be positives', ), assert( (((bezierChartScale != BezierChartScale.CUSTOM) && fromDate != null && toDate != null) || (bezierChartScale == BezierChartScale.CUSTOM && fromDate == null && toDate == null)), 'fromDate and toDate must not be null', ), assert( (((bezierChartScale != BezierChartScale.CUSTOM) && toDate!.isAfter(fromDate!)) || (bezierChartScale == BezierChartScale.CUSTOM && fromDate == null && toDate == null)), 'toDate must be after of fromDate', ), super(key: key); @override BezierChartState createState() => BezierChartState(); } @visibleForTesting class BezierChartState extends State with SingleTickerProviderStateMixin { late AnimationController _animationController; late ScrollController _scrollController; GlobalKey _keyScroll = GlobalKey(); ///Track the current position when dragging the indicator Offset? _verticalIndicatorPosition; bool _displayIndicator = false; ///padding for leading and trailing of the chart // TODO(weilu): 修改处 horizontalPadding = 50.0; final double horizontalPadding = 11.0; ///spacing between each datapoint double horizontalSpacing = 60.0; ///List of `DataPoint`s used to display all the values for the `X` axis List _xAxisDataPoints = []; ///List of `BezierLine`s used to display all lines, each line contains a list of `DataPoint`s List computedSeries = []; ///Current scale when use pinch/zoom double _currentScale = 1.0; ///This value allow us to get the last scale used when start the pinch/zoom again late double _previousScale; ///The current chart scale late BezierChartScale _currentBezierChartScale; double _lastValueSnapped = double.infinity; bool get isPinchZoomActive => (_touchFingers > 1 && widget.config.pinchZoom); ///When we only have 1 axis we don't need to much span to change the date type chart` bool get isOnlyOneAxis => _xAxisDataPoints.length <= 1; double _contentWidth = 0.0; bool _isScrollable = false; ///Calculate all of the values related to the Y axis late List _yValues; ///Values from valueBuilder late List _tempYValues; DateTime? _dateTimeSelected; double? _valueSelected; GlobalKey _keyLastYAxisItem = GlobalKey(); double _yAxisWidth = 0.0; ///Refresh the position of the vertical/bubble void _refreshPosition(details) { if (_animationController.status == AnimationStatus.completed && _displayIndicator) { return _updatePosition(details.globalPosition); } } ///Update and refresh the position based on the current screen void _updatePosition(Offset globalPosition) { final RenderBox renderBox = context.findRenderObject() as RenderBox; final position = renderBox.globalToLocal(globalPosition); return setState( () { final fixedPosition = Offset( position.dx + _scrollController.offset - horizontalPadding, position.dy); _verticalIndicatorPosition = fixedPosition; }, ); } ///After long press this method is called to display the bubble indicator if is not visible ///An animation and snap sound are triggered _onDisplayIndicator(details, {bool updatePosition = true}) { if (!_displayIndicator) { _displayIndicator = true; _animationController.forward( from: 0.0, ); if (widget.onIndicatorVisible != null) { widget.onIndicatorVisible!(true); } } _onDataPointSnap(double.maxFinite); if (updatePosition) _updatePosition(details.globalPosition); } ///Hide the vertical/bubble indicator and refresh the widget _onHideIndicator() { if (_displayIndicator) { if (widget.onIndicatorVisible != null) { widget.onIndicatorVisible!(false); } _animationController.reverse(from: 1.0).whenCompleteOrCancel( () { setState( () { _displayIndicator = false; }, ); }, ); } } ///When the current indicator reach any data point a feedback is triggered void _onDataPointSnap(double value) { bool isIOS = Theme.of(context).platform == TargetPlatform.iOS; if (_lastValueSnapped != value && widget.config.snap) { if (isIOS) { HapticFeedback.heavyImpact(); } else { Feedback.forTap(context); } _lastValueSnapped = value; } } _checkMissingValues(DateTime newDate) { for (BezierLine line in widget.series) { if (line.onMissingValue != null) { final newValue = line.onMissingValue!(newDate); if (!_tempYValues.contains(newValue)) _tempYValues.add(newValue); //if there is no missingvalue specified we should use 0 as minimum value to avoid overlap } else if (widget.config.startYAxisFromNonZeroValue && line.onMissingValue == null) { if (!_tempYValues.contains(0)) _tempYValues.add(0); } } } ///Building the data points for the `X` axis based on the `_currentBezierChartScale` void _buildXDataPoints() { _xAxisDataPoints = []; _tempYValues = []; final scale = _currentBezierChartScale; if (scale == BezierChartScale.CUSTOM) { _xAxisDataPoints = widget.xAxisCustomValues .map((val) => DataPoint(value: val, xAxis: val)) .toList(); } else if (scale == BezierChartScale.HOURLY) { final hours = widget.toDate!.difference(widget.fromDate!).inHours; for (int i = 0; i < hours; i++) { final tempDate = widget.fromDate!.add( Duration( hours: (i + 1), ), ); final newDate = DateTime( tempDate.year, tempDate.month, tempDate.day, tempDate.hour, 0); _xAxisDataPoints.add( DataPoint(value: (i * 5).toDouble(), xAxis: newDate), ); _checkMissingValues(newDate); } } else if (scale == BezierChartScale.WEEKLY) { final days = _convertToDateOnly(widget.toDate!) .difference(_convertToDateOnly(widget.fromDate!)) .inDays; for (int i = 0; i <= days; i++) { final newDate = widget.fromDate!.add( Duration( days: (i), ), ); _xAxisDataPoints.add( DataPoint(value: (i * 5).toDouble(), xAxis: newDate), ); _checkMissingValues(newDate); } } else if (scale == BezierChartScale.MONTHLY) { DateTime startDate = DateTime( widget.fromDate!.year, widget.fromDate!.month, ); DateTime endDate = DateTime( widget.toDate!.year, widget.toDate!.month, ); for (int i = 0; (startDate.isBefore(endDate) || areEqualDates(startDate, endDate)); i++) { _xAxisDataPoints.add( DataPoint(value: (i * 5).toDouble(), xAxis: startDate), ); _checkMissingValues(startDate); startDate = DateTime(startDate.year, startDate.month + 1); } } else if (scale == BezierChartScale.YEARLY) { DateTime startDate = DateTime( widget.fromDate!.year, ); DateTime endDate = DateTime( widget.toDate!.year, ); for (int i = 0; (startDate.isBefore(endDate) || areEqualDates(startDate, endDate)); i++) { _xAxisDataPoints.add( DataPoint(value: (i * 5).toDouble(), xAxis: startDate), ); _checkMissingValues(startDate); startDate = DateTime( startDate.year + 1, ); } } } DateTime _convertToDateOnly(DateTime date) { int year = date.year; int month = date.month; int day = date.day; return DateTime(year, month, day); } ///Calculating the size of the content based on the parent constraints and on the `_currentBezierChartScale` double _buildContentWidth(BoxConstraints constraints) { final scale = _currentBezierChartScale; if (scale == BezierChartScale.CUSTOM) { return widget.config.contentWidth ?? constraints.maxWidth - 2 * horizontalPadding; } else { if (scale == BezierChartScale.HOURLY) { horizontalSpacing = constraints.maxWidth / 7; return _xAxisDataPoints.length * (horizontalSpacing * _currentScale) - horizontalPadding / 2; } else if (scale == BezierChartScale.WEEKLY) { horizontalSpacing = constraints.maxWidth / 7; return _xAxisDataPoints.length * (horizontalSpacing * _currentScale) - horizontalPadding / 2; } else if (scale == BezierChartScale.MONTHLY) { horizontalSpacing = constraints.maxWidth / 12; return _xAxisDataPoints.length * (horizontalSpacing * _currentScale) - horizontalPadding / 2; } else if (scale == BezierChartScale.YEARLY) { if (_xAxisDataPoints.length > 12) { horizontalSpacing = constraints.maxWidth / 12; } else if (_xAxisDataPoints.length < 6) { horizontalSpacing = constraints.maxWidth / 6; } else { horizontalSpacing = constraints.maxWidth / _xAxisDataPoints.length; } return _xAxisDataPoints.length * (horizontalSpacing * _currentScale) - horizontalPadding; } return 0.0; } } ///When the widget finish rendering for the first time _onLayoutDone(_) { _yAxisWidth = _keyLastYAxisItem.currentContext?.size?.width ?? 0; //Move to selected position if ((widget.selectedDate != null && _currentBezierChartScale != BezierChartScale.CUSTOM) || (widget.selectedValue != null && _currentBezierChartScale == BezierChartScale.CUSTOM)) { int index = -1; if (_currentBezierChartScale == BezierChartScale.WEEKLY) { index = _xAxisDataPoints.indexWhere( (dp) => areEqualDates((dp.xAxis as DateTime), widget.selectedDate!)); } else if (_currentBezierChartScale == BezierChartScale.HOURLY) { index = _xAxisDataPoints.indexWhere((dp) => (dp.xAxis as DateTime).year == widget.selectedDate!.year && (dp.xAxis as DateTime).month == widget.selectedDate!.month && (dp.xAxis as DateTime).day == widget.selectedDate!.day && (dp.xAxis as DateTime).hour == widget.selectedDate!.hour); } else if (_currentBezierChartScale == BezierChartScale.MONTHLY) { index = _xAxisDataPoints.indexWhere((dp) => (dp.xAxis as DateTime).year == widget.selectedDate!.year && (dp.xAxis as DateTime).month == widget.selectedDate!.month); } else if (_currentBezierChartScale == BezierChartScale.YEARLY) { index = _xAxisDataPoints.indexWhere( (dp) => (dp.xAxis as DateTime).year == widget.selectedDate!.year); } else if (_currentBezierChartScale == BezierChartScale.CUSTOM) { index = _xAxisDataPoints .indexWhere((dp) => (dp.xAxis as double) == widget.selectedValue); } //If it's a valid index then scroll to the date selected based on the current position if (index >= 0) { Offset fixedPosition; if (_currentBezierChartScale == BezierChartScale.CUSTOM) { final space = (_contentWidth / _xAxisDataPoints.length); fixedPosition = Offset(isOnlyOneAxis ? 0.0 : (index * space) + space / 2, 0.0); _scrollController.jumpTo((index * space)); setState( () { _verticalIndicatorPosition = fixedPosition; _onDisplayIndicator( LongPressMoveUpdateDetails( globalPosition: fixedPosition, offsetFromOrigin: fixedPosition, ), updatePosition: false, ); }, ); } else { final jumpToX = (index * horizontalSpacing) - horizontalPadding / 2 - _keyScroll.currentContext!.size!.width / 2; _scrollController.jumpTo(jumpToX); fixedPosition = Offset( isOnlyOneAxis ? 0.0 : (index * horizontalSpacing + 2 * horizontalPadding) - _scrollController.offset, 0.0); _verticalIndicatorPosition = fixedPosition; _onDisplayIndicator( LongPressMoveUpdateDetails( globalPosition: fixedPosition, offsetFromOrigin: fixedPosition, ), ); } } } _checkIfNeedScroll(); if (_isScrollable) { setState(() {}); } } _checkIfNeedScroll() { if (_contentWidth > _keyScroll.currentContext!.size!.width - horizontalPadding * 2) { _isScrollable = true; } } ///Calculating the new series based on the `_currentBezierChartScale` _computeSeries() { computedSeries = []; _yValues = []; //fill data series for DateTime scale type if (_currentBezierChartScale == BezierChartScale.MONTHLY || _currentBezierChartScale == BezierChartScale.YEARLY || _currentBezierChartScale == BezierChartScale.WEEKLY || _currentBezierChartScale == BezierChartScale.HOURLY) { for (BezierLine line in widget.series) { Map> tmpMap = Map(); for (DataPoint dataPoint in line.data) { String key; if (_currentBezierChartScale == BezierChartScale.MONTHLY) { key = '${dataPoint.xAxis.year},${dataPoint.xAxis.month.toString().padLeft(2, '0')}'; } else if (_currentBezierChartScale == BezierChartScale.YEARLY) { key = '${dataPoint.xAxis.year}'; } else if (_currentBezierChartScale == BezierChartScale.WEEKLY) { key = '${dataPoint.xAxis.year},${dataPoint.xAxis.month.toString().padLeft(2, '0')},${dataPoint.xAxis.day.toString().padLeft(2, '0')}'; } else { key = '${dataPoint.xAxis.year},${dataPoint.xAxis.month.toString().padLeft(2, '0')},${dataPoint.xAxis.day.toString().padLeft(2, '0')},${dataPoint.xAxis.hour.toString().padLeft(2, '0')}'; } //support aggregations for y axis if (!tmpMap.containsKey(key)) { tmpMap[key] = []; } tmpMap[key]?.add(dataPoint.value); } Map valueMap = Map(); if (widget.bezierChartAggregation == BezierChartAggregation.SUM) { valueMap = tmpMap.map((k, v) => MapEntry( k, v.reduce( (c1, c2) => double.parse((c1 + c2).toStringAsFixed(2))))); } else if (widget.bezierChartAggregation == BezierChartAggregation.FIRST) { valueMap = tmpMap.map((k, v) => MapEntry(k, v.reduce((c1, c2) => c1))); } else if (widget.bezierChartAggregation == BezierChartAggregation.AVERAGE) { valueMap = tmpMap.map( (k, v) => MapEntry(k, v.reduce((c1, c2) => c1 + c2) / v.length)); } else if (widget.bezierChartAggregation == BezierChartAggregation.COUNT) { valueMap = tmpMap.map((k, v) => MapEntry(k, v.length.toDouble())); } else if (widget.bezierChartAggregation == BezierChartAggregation.MAX) { valueMap = tmpMap.map( (k, v) => MapEntry(k, v.reduce((c1, c2) => c1 > c2 ? c1 : c2))); } else if (widget.bezierChartAggregation == BezierChartAggregation.MIN) { valueMap = tmpMap.map( (k, v) => MapEntry(k, v.reduce((c1, c2) => c1 < c2 ? c1 : c2))); } List> newDataPoints = []; valueMap.keys.forEach( (key) { final value = valueMap[key]; if (!_yValues.contains(value)) _yValues.add(value!); ///Sum all the values corresponding to each month and create a new data serie if (_currentBezierChartScale == BezierChartScale.HOURLY) { List split = key.split(','); int year = int.parse(split[0]); int month = int.parse(split[1]); int day = int.parse(split[2]); int hour = int.parse(split[3]); final date = DateTime(year, month, day, hour, 0); newDataPoints.add( DataPoint( value: value!, xAxis: date, ), ); } ///Sum all the values corresponding to each month and create a new data serie else if (_currentBezierChartScale == BezierChartScale.MONTHLY) { List split = key.split(','); int year = int.parse(split[0]); int month = int.parse(split[1]); final date = DateTime(year, month); newDataPoints.add( DataPoint( value: value!, xAxis: date, ), ); } else if (_currentBezierChartScale == BezierChartScale.WEEKLY) { List split = key.split(','); int year = int.parse(split[0]); int month = int.parse(split[1]); int day = int.parse(split[2]); final date = DateTime(year, month, day, 0); newDataPoints.add( DataPoint( value: value!, xAxis: date, ), ); } else { ///Sum all the values corresponding to each year and create a new data serie int year = int.parse(key); final date = DateTime(year); newDataPoints.add( DataPoint( value: value!, xAxis: date, ), ); } }, ); BezierLine newBezierLine = BezierLine.copy( bezierLine: BezierLine( lineColor: line.lineColor, label: line.label, lineStrokeWidth: line.lineStrokeWidth, onMissingValue: line.onMissingValue, dataPointFillColor: line.dataPointFillColor, dataPointStrokeColor: line.dataPointStrokeColor, data: newDataPoints, ), ); computedSeries.add(newBezierLine); } } else { for (BezierLine line in widget.series) { for (double val in line.data.map((dp) => dp.value).toList()) { if (!_yValues.contains(val)) _yValues.add(val); } } computedSeries = widget.series; } for (double temp in _tempYValues) { if (!_yValues.contains(temp)) _yValues.add(temp); } //sort yValues _yValues.sort((val1, val2) => (val1 > val2) ? 1 : -1); } ///Pinch and zoom based on the scale reported by the gesture detector _onPinchZoom(double scale) { scale = double.parse(scale.toStringAsFixed(1)); if (isPinchZoomActive) { BezierChartScale lastScale = BezierChartScale.WEEKLY; if (_currentBezierChartScale == BezierChartScale.MONTHLY) { lastScale = BezierChartScale.MONTHLY; } else if (_currentBezierChartScale == BezierChartScale.YEARLY) { lastScale = BezierChartScale.YEARLY; } //when the scale is below 1 then we'll try to change the chart scale depending of the `_currentBezierChartScale` if (scale < 1) { if (_currentBezierChartScale == BezierChartScale.WEEKLY) { _currentBezierChartScale = BezierChartScale.MONTHLY; _previousScale = 1.5; } else if (_currentBezierChartScale == BezierChartScale.MONTHLY) { _currentBezierChartScale = BezierChartScale.YEARLY; } _currentScale = 1.0; setState( () { _buildXDataPoints(); _computeSeries(); _checkIfNeedScroll(); }, ); _notifyScaleChanged(lastScale); return; //if the scale is greater than 1.5 then we'll try to change the chart scale depending of the `_currentBezierChartScale` } else if (scale > 1.5 || (isOnlyOneAxis && scale > 1.2)) { if (_currentBezierChartScale == BezierChartScale.YEARLY) { _currentBezierChartScale = BezierChartScale.MONTHLY; _currentScale = 1.0; _previousScale = 1.0 / scale; setState( () { _buildXDataPoints(); _computeSeries(); _checkIfNeedScroll(); }, ); _notifyScaleChanged(lastScale); } else if (_currentBezierChartScale == BezierChartScale.MONTHLY) { _currentBezierChartScale = BezierChartScale.WEEKLY; _currentScale = 1.0; _previousScale = 1.0 / scale; setState( () { _buildXDataPoints(); _computeSeries(); _checkIfNeedScroll(); }, ); _notifyScaleChanged(lastScale); return; } } else { if (scale > 2.5) scale = 2.5; if (scale != _currentScale) { setState( () { _currentScale = scale; }, ); } } } } void _notifyScaleChanged(BezierChartScale lastScale) { if (widget.onScaleChanged != null && lastScale != _currentBezierChartScale) { widget.onScaleChanged!(_currentBezierChartScale); } } bool areSeriesDifferent = false; @override void didUpdateWidget(BezierChart oldWidget) { /// Rebuild data points and series in case: /// 1. if the BezierChartScale is different from the old one /// 2. if the series are different /// 3. if either fromDate or toDate are different // areSeriesDifferent = false; // // if (oldWidget.series.length != widget.series.length) { // areSeriesDifferent = true; // } else { // if (oldWidget.series.length == widget.series.length) { // for (int i = 0; i < oldWidget.series.length; i++) { // final size1 = oldWidget.series[i]; // final size2 = widget.series[i]; // if (size1.data.length != size2.data.length) { // areSeriesDifferent = true; // break; // } // } // } // // if (!areSeriesDifferent) { // for (int i = 0; i < oldWidget.series.length; i++) { // final line1 = oldWidget.series[i]; // final line2 = widget.series[i]; // if (line1 != line2) { // areSeriesDifferent = true; // break; // } // } // } // } // // if (oldWidget.bezierChartScale != widget.bezierChartScale || // areSeriesDifferent || // oldWidget.fromDate != widget.fromDate || // oldWidget.toDate != widget.toDate) { // _currentBezierChartScale = widget.bezierChartScale; // _buildXDataPoints(); // _computeSeries(); // } // TODO(weilu): 修改处 if (oldWidget.series != widget.series) { _currentBezierChartScale = widget.bezierChartScale; _buildXDataPoints(); _computeSeries(); } super.didUpdateWidget(oldWidget); } @override void initState() { _currentBezierChartScale = widget.bezierChartScale; _scrollController = ScrollController(); _animationController = AnimationController( vsync: this, duration: Duration( milliseconds: 300, ), ); _buildXDataPoints(); _computeSeries(); WidgetsBinding.instance.addPostFrameCallback(_onLayoutDone); super.initState(); } @override void dispose() { _animationController.dispose(); _scrollController.dispose(); super.dispose(); } int _touchFingers = 0; @override Widget build(BuildContext context) { //using `Listener` to fix the issue with single touch for multitouch gesture like pinch/zoom //https://github.com/flutter/flutter/issues/13102 return Semantics( label: '长按查看详细数据', child: Container( decoration: BoxDecoration( // TODO(weilu): 修改处 // color: widget.config.backgroundGradient != null // ? null // : widget.config.backgroundColor, gradient: widget.config.backgroundGradient, ), alignment: Alignment.center, child: Listener( onPointerDown: (_) { _touchFingers++; if (_touchFingers > 1) { setState(() {}); } }, onPointerUp: (_) { _touchFingers--; if (_touchFingers < 2) { setState(() {}); } }, child: GestureDetector( onLongPressStart: widget.config.updatePositionOnTap ? null : (isPinchZoomActive ? null : _onDisplayIndicator), onLongPressMoveUpdate: isPinchZoomActive ? null : _refreshPosition, onScaleStart: (_) { _previousScale = _currentScale; }, onScaleUpdate: _currentBezierChartScale != BezierChartScale.CUSTOM && //Hourly chart doesn't support pinch/zoom for now _currentBezierChartScale != BezierChartScale.HOURLY && !_displayIndicator ? (details) => _onPinchZoom(_previousScale * details.scale) : null, onTap: widget.config.updatePositionOnTap ? null : (isPinchZoomActive ? null : _onHideIndicator), onTapDown: widget.config.updatePositionOnTap ? (isPinchZoomActive ? null : _refreshPosition) : null, child: LayoutBuilder( builder: (context, constraints) { _contentWidth = _buildContentWidth(constraints); final items = []; final maxHeight = constraints.biggest.height * 0.75; items.add( MySingleChildScrollView( controller: _scrollController, physics: isPinchZoomActive || !_isScrollable ? const NeverScrollableScrollPhysics() : widget.config.physics, key: _keyScroll, scrollDirection: Axis.horizontal, padding: EdgeInsets.symmetric(horizontal: horizontalPadding), child: Align( alignment: Alignment(0.0, 0.7), child: CustomPaint( size: Size( _contentWidth, maxHeight, ), painter: _BezierChartPainter( shouldRepaintChart: areSeriesDifferent, config: widget.config, maxYValue: _yValues.last, minYValue: _yValues.first, bezierChartScale: _currentBezierChartScale, verticalIndicatorPosition: _verticalIndicatorPosition, series: computedSeries, showIndicator: _displayIndicator, animation: CurvedAnimation( parent: _animationController, curve: Interval( 0.0, 1.0, curve: Curves.elasticOut, ), ), xAxisDataPoints: _xAxisDataPoints, onDataPointSnap: _onDataPointSnap, maxWidth: MediaQuery.of(context).size.width, scrollOffset: _scrollController.hasClients ? _scrollController.offset : 0.0, footerValueBuilder: widget.footerValueBuilder, bubbleLabelValueBuilder: widget.bubbleLabelValueBuilder, footerDateTimeBuilder: widget.footerDateTimeBuilder, bubbleLabelDateTimeBuilder: widget.bubbleLabelDateTimeBuilder, onValueSelected: (val) { if (widget.onValueSelected != null) { if (_valueSelected == null) { _valueSelected = val; widget.onValueSelected!(_valueSelected!); } else { if (_valueSelected != val) { _valueSelected = val; widget.onValueSelected!(_valueSelected!); } } } }, onDateTimeSelected: (val) { if (widget.onDateTimeSelected != null) { if (_dateTimeSelected == null) { _dateTimeSelected = val; widget.onDateTimeSelected!(_dateTimeSelected!); } else { if (_dateTimeSelected != val) { _dateTimeSelected = val; widget.onDateTimeSelected!(_dateTimeSelected!); } } } }, ), ), ), ), ); if (widget.config.displayYAxis) { if (_yValues.isNotEmpty) { //add a background container for the Y Axis items.add(Positioned( left: 0, top: 0, bottom: 0, child: Container( width: _yAxisWidth + 10, decoration: widget.config.backgroundGradient != null ? BoxDecoration( gradient: widget.config.backgroundGradient) : null, color: widget.config.backgroundGradient != null ? null : widget.config.backgroundColor, ), )); } final fontSize = widget.config.yAxisTextStyle?.fontSize ?? 8.0; final maxValue = _yValues.last - (widget.config.startYAxisFromNonZeroValue ? _yValues.first : 0.0); final steps = widget.config.stepsYAxis != null && widget.config.stepsYAxis! > 0 ? widget.config.stepsYAxis : null; _addYItem(double value, {Key? key}) { items.add( Positioned( bottom: _getRealValue( value - (widget.config.startYAxisFromNonZeroValue ? _yValues.first : 0.0), maxHeight - widget.config.footerHeight, maxValue) + widget.config.footerHeight + fontSize / 2, left: 10.0, child: Text( formatAsIntOrDouble(value), key: key, style: widget.config.yAxisTextStyle ?? TextStyle(color: Colors.white, fontSize: fontSize), ), ), ); } if (steps != null) { final max = _yValues.last; final min = widget.config.startYAxisFromNonZeroValue ? _yValues.first.ceil() : 0; for (int i = min; i < max + steps; i++) { if (i % steps == 0) { bool isLast = (i + steps) > max && (i + steps) >= (max + steps); _addYItem(i.toDouble(), key: isLast ? _keyLastYAxisItem : null); } } } else { for (double val in _yValues) { _addYItem(val, key: val == _yValues.last ? _keyLastYAxisItem : null); } } } return Stack( children: items, ); }, ), ), ), ), ); } } ///return the real value of canvas _getRealValue(double value, double maxConstraint, double maxValue) => maxConstraint * value / (maxValue == 0 ? 1 : maxValue); //BezierChart class _BezierChartPainter extends CustomPainter { final BezierChartConfig config; final Offset? verticalIndicatorPosition; final List series; final List xAxisDataPoints; double _maxValueY = 0.0; double _maxValueX = 0.0; List<_CustomValue> _currentCustomValues = []; DataPoint? _currentXDataPoint; final double radiusDotIndicatorMain = 7; final double radiusDotIndicatorItems = 3.5; final bool showIndicator; final Animation animation; final ValueChanged? onDataPointSnap; final BezierChartScale bezierChartScale; final double maxWidth; final double scrollOffset; bool footerDrawed = false; final FooterValueBuilder? footerValueBuilder; final FooterValueBuilder? bubbleLabelValueBuilder; final FooterDateTimeBuilder? footerDateTimeBuilder; final FooterDateTimeBuilder? bubbleLabelDateTimeBuilder; final double maxYValue; final double minYValue; final ValueChanged? onValueSelected; final ValueChanged? onDateTimeSelected; final bool shouldRepaintChart; _BezierChartPainter({ required this.shouldRepaintChart, required this.config, this.verticalIndicatorPosition, required this.series, required this.showIndicator, required this.xAxisDataPoints, required this.animation, required this.bezierChartScale, this.onDataPointSnap, required this.maxWidth, this.footerValueBuilder, this.bubbleLabelValueBuilder, required this.scrollOffset, this.footerDateTimeBuilder, this.bubbleLabelDateTimeBuilder, required this.maxYValue, required this.minYValue, this.onDateTimeSelected, this.onValueSelected, }) : super(repaint: animation) { _maxValueY = _getMaxValueY(); _maxValueX = _getMaxValueX(); } ///return the max value of the Axis X double _getMaxValueX() { double x = double.negativeInfinity; for (DataPoint dp in xAxisDataPoints) { if (dp.value > x) x = dp.value; } return x; } ///return the max value of the Axis Y double _getMaxValueY() { /* double y = double.negativeInfinity; for (BezierLine line in series) { for (DataPoint dp in line.data) { if (dp.value > y) y = dp.value; } }*/ if (maxYValue == 0.0) return 1.0; return maxYValue - (config.startYAxisFromNonZeroValue ? minYValue : 0.0); } @override void paint(Canvas canvas, Size size) { final height = size.height - config.footerHeight; Paint paintVerticalIndicator = Paint(); try { paintVerticalIndicator ..color = config.verticalIndicatorColor ..strokeWidth = config.verticalIndicatorStrokeWidth ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.square; } catch (ex) { print('err: $ex'); } Paint paintControlPoints = Paint()..strokeCap = StrokeCap.round; double verticalX = 0.0; //fixing verticalIndicator outbounds if (verticalIndicatorPosition != null) { verticalX = verticalIndicatorPosition!.dx; if (verticalIndicatorPosition!.dx < 0) { verticalX = 0.0; } else if (verticalIndicatorPosition!.dx > size.width) { verticalX = size.width; } } //variables for the last item on the list (this is required to display the indicator) late Offset p0, p1, p2, p3; void _drawBezierLinePath(BezierLine line) { Path path = Path(); List dataPoints = []; TextPainter textPainterXAxis = TextPainter( textAlign: TextAlign.center, textDirection: TextDirection.ltr, ); TextStyle xAxisTextStyle = config.xAxisTextStyle ?? TextStyle( color: Colors.white, fontWeight: FontWeight.w400, fontSize: 11, ); Paint paintLine = Paint() ..color = line.lineColor ..strokeWidth = line.lineStrokeWidth ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round; Paint paintXLines = Paint() ..color = config.xLinesColor ..strokeWidth = 1.0 ..style = PaintingStyle.stroke; _AxisValue? lastPoint; //display each data point for (int i = 0; i < xAxisDataPoints.length; i++) { double value = 0.0; double axisX = xAxisDataPoints[i].value; final double valueX = _getRealValue( axisX, size.width, _maxValueX, ); //Only calculate and display the necessary data to improve the performance of the scrolling final range = maxWidth * 10; if (scrollOffset - range >= valueX || scrollOffset + range <= valueX) { continue; } bool isMissingValue = false; if (bezierChartScale == BezierChartScale.CUSTOM) { value = line.data[i].value; } else { //search from axis for (DataPoint dp in line.data) { final dateTime = (xAxisDataPoints[i].xAxis as DateTime); if (bezierChartScale == BezierChartScale.HOURLY) { if (areEqualDatesIncludingHour(dateTime, dp.xAxis)) { value = dp.value; axisX = xAxisDataPoints[i].value; break; } } else { if (areEqualDates(dateTime, dp.xAxis)) { value = dp.value; axisX = xAxisDataPoints[i].value; break; } } } if (value == 0) { if (line.onMissingValue != null) { isMissingValue = true; value = line.onMissingValue!(xAxisDataPoints[i].xAxis as DateTime); } } } final double axisY = value; final double valueY = height - _getRealValue( axisY - (config.startYAxisFromNonZeroValue ? minYValue : 0.0), height, _maxValueY, ); if (config.displayLinesXAxis && series.length == 1) { canvas.drawLine( Offset(valueX, height), Offset(valueX, valueY), paintXLines); } if (lastPoint == null) { lastPoint = _AxisValue(x: valueX, y: valueY); path.moveTo(valueX, valueY); } final double controlPointX = lastPoint.x + (valueX - lastPoint.x) / 2; path.cubicTo( controlPointX, lastPoint.y, controlPointX, valueY, valueX, valueY); if (isMissingValue) { if (config.displayDataPointWhenNoValue) { dataPoints.add(Offset(valueX, valueY)); } } else { dataPoints.add(Offset(valueX, valueY)); } if (verticalIndicatorPosition != null && verticalX >= lastPoint.x && verticalX <= valueX) { //points to draw the info p0 = Offset(lastPoint.x, height - lastPoint.y); p1 = Offset(controlPointX, height - lastPoint.y); p2 = Offset(controlPointX, height - valueY); p3 = Offset(valueX, height - valueY); } if (verticalIndicatorPosition != null) { //get current information double nextX = double.infinity; double lastX = double.negativeInfinity; if (xAxisDataPoints.length > (i + 1)) { nextX = _getRealValue( xAxisDataPoints[i + 1].value, size.width, _maxValueX, ); } if (i > 0) { lastX = _getRealValue( xAxisDataPoints[i - 1].value, size.width, _maxValueX, ); } //if vertical indicator is in range then display the bubble info if (verticalX >= valueX - (valueX - lastX) / 2 && verticalX <= valueX + (nextX - valueX) / 2) { _currentXDataPoint = xAxisDataPoints[i]; if (_currentCustomValues.length < series.length) { bool isDouble = (xAxisDataPoints[i].xAxis is double); if (isDouble) { if (onValueSelected != null) { onValueSelected!(xAxisDataPoints[i].xAxis); } } else { if (onDateTimeSelected != null) { onDateTimeSelected!(xAxisDataPoints[i].xAxis); } } onDataPointSnap!(xAxisDataPoints[i].value); _currentCustomValues.add( _CustomValue( value: '${formatAsIntOrDouble(axisY)}', label: line.label, color: line.lineColor, ), ); } } } lastPoint = _AxisValue(x: valueX, y: valueY); //draw footer textPainterXAxis.text = TextSpan( text: _getFooterText(xAxisDataPoints[i]), style: xAxisTextStyle, ); textPainterXAxis.layout(); textPainterXAxis.paint( canvas, Offset(valueX - textPainterXAxis.width / 2, height + textPainterXAxis.height / 1.5), ); } //only draw the footer for the first line because it is the same for all the lines if (!footerDrawed) footerDrawed = true; canvas.drawPath(path, paintLine); if (config.showDataPoints) { //draw data points //Data points won't work until Flutter team fix this issue : https://github.com/flutter/flutter/issues/32218 // if (!kIsWeb) { // TODO(weilu): web已支持 canvas.drawPoints( PointMode.points, dataPoints, paintControlPoints ..style = PaintingStyle.stroke ..strokeWidth = 9 ..color = line.dataPointStrokeColor); canvas.drawPoints( PointMode.points, dataPoints, paintControlPoints ..style = PaintingStyle.fill ..strokeWidth = line.lineStrokeWidth * 2 ..color = line.dataPointFillColor, ); // } } } final reversedSeries = series.reversed; for (BezierLine line in reversedSeries) { _drawBezierLinePath(line); } if (verticalIndicatorPosition != null && showIndicator) { if (config.snap) { if (_currentXDataPoint != null) { verticalX = _getRealValue( _currentXDataPoint!.value, size.width, _maxValueX, ); } else { verticalX = 0.0; } } final yValue = _getYValues( p0, p1, p2, p3, (verticalX - p0.dx) / (p3.dx - p0.dx), ); double infoWidth = 0; //base value, modified based on the label text double infoHeight = 30; //bubble indicator padding // TODO 28 final horizontalPadding = 18.0; // TODO 42 double offsetInfo = 37 + ((_currentCustomValues.length - 1.0) * 10.0); final centerForCircle = Offset(verticalX, height - yValue); final center = config.verticalIndicatorFixedPosition ? Offset(verticalX, offsetInfo) : centerForCircle; if (config.showVerticalIndicator) { canvas.drawLine( Offset(verticalX, height), Offset(verticalX, config.verticalLineFullHeight ? 0.0 : center.dy), paintVerticalIndicator, ); } //draw point canvas.drawCircle( centerForCircle, radiusDotIndicatorMain, Paint() ..color = series.reversed.toList().last.dataPointFillColor ..strokeWidth = 4.0, ); //calculate the total lenght of the lines List textValues = []; List centerCircles = []; // TODO(weilu): 修改处 infoHeight / (8.75) double space = 10 - ((infoHeight / (4)) * _currentCustomValues.length); infoHeight = infoHeight + (_currentCustomValues.length - 1) * (infoHeight / 3); for (_CustomValue customValue in _currentCustomValues.reversed.toList()) { textValues.add( TextSpan( text: '${customValue.value} ', style: config.bubbleIndicatorValueStyle.copyWith(fontSize: 11), children: [ TextSpan( text: '${customValue.label}\n', style: config.bubbleIndicatorLabelStyle.copyWith(fontSize: 9), ), ], ), ); centerCircles.add( // Offset(center.dx - infoWidth / 2 + radiusDotIndicatorItems * 1.5, Offset( center.dx, center.dy - offsetInfo - radiusDotIndicatorItems + space + (_currentCustomValues.length == 1 ? 1 : 0)), ); space += 12.5; } //Calculate Text size TextPainter textPainter = TextPainter( textAlign: TextAlign.center, text: TextSpan( text: _getInfoTitleText(), // TODO(weilu): 修改处 9.5 style: config.bubbleIndicatorTitleStyle.copyWith(fontSize: 5.0), children: textValues, ), textDirection: TextDirection.ltr, ); textPainter.layout(); infoWidth = textPainter.width + radiusDotIndicatorItems * 2 + horizontalPadding; ///Draw Bubble Indicator Info /// Draw shadow bubble info if (animation.isCompleted) { Path path = Path(); path.moveTo(center.dx - infoWidth / 2 + 4, center.dy - offsetInfo + infoHeight / 1.8); path.lineTo(center.dx + infoWidth / 2 + 4, center.dy - offsetInfo + infoHeight / 1.8); path.lineTo(center.dx + infoWidth / 2 + 4, center.dy - offsetInfo - infoHeight / 3); //path.close(); // canvas.drawShadow(path, Colors.black, 20.0, false); canvas.drawPath(path, paintControlPoints..color = Colors.black12); } final paintInfo = Paint() ..color = config.bubbleIndicatorColor ..style = PaintingStyle.fill; //Draw Bubble info canvas.drawRRect( RRect.fromRectAndRadius( _fromCenter( center: Offset( center.dx, (center.dy - offsetInfo * animation.value), ), width: infoWidth, height: infoHeight, ), Radius.circular(5), ), paintInfo, ); //Draw triangle Bubble final double triangleSize = 6; Path pathArrow = Path(); pathArrow.moveTo(center.dx - triangleSize, center.dy - offsetInfo * animation.value + infoHeight / 2.1); pathArrow.lineTo( center.dx, center.dy - offsetInfo * animation.value + infoHeight / 2.1 + triangleSize * 1.5); pathArrow.lineTo(center.dx + triangleSize, center.dy - offsetInfo * animation.value + infoHeight / 2.1); pathArrow.close(); canvas.drawPath( pathArrow, paintInfo, ); //End triangle if (animation.isCompleted) { //Paint Text , title and description textPainter.paint( canvas, Offset( center.dx - textPainter.width / 2 + 6, // TODO 0 center.dy - offsetInfo - infoHeight / 2.5, ), ); //draw circle indicators and text for (int z = 0; z < _currentCustomValues.length; z++) { _CustomValue customValue = _currentCustomValues[z]; Offset centerIndicator = centerCircles.reversed.toList()[z]; Offset fixedCenter = Offset( centerIndicator.dx - infoWidth / 2 + radiusDotIndicatorItems + 6, // TODO 4 centerIndicator.dy); canvas.drawCircle( fixedCenter, radiusDotIndicatorItems, Paint() ..color = customValue.color ..style = PaintingStyle.fill); canvas.drawCircle( fixedCenter, radiusDotIndicatorItems, Paint() ..color = Colors.black ..strokeWidth = 0.5 ..style = PaintingStyle.stroke); } } } } String _getInfoTitleText() { final scale = bezierChartScale; if (bubbleLabelValueBuilder != null && scale == BezierChartScale.CUSTOM) { return bubbleLabelValueBuilder!(_currentXDataPoint!.value); } if (bubbleLabelDateTimeBuilder != null && scale != BezierChartScale.CUSTOM) { return bubbleLabelDateTimeBuilder!( _currentXDataPoint!.xAxis as DateTime, scale); } if (scale == BezierChartScale.CUSTOM) { return "${formatAsIntOrDouble(_currentXDataPoint!.value)}\n"; } else if (scale == BezierChartScale.HOURLY) { final dateFormat = intl.DateFormat('dd/MM HH:mm'); final date = _currentXDataPoint!.xAxis as DateTime; final now = DateTime.now(); if (areEqualDatesIncludingHour(date, now)) { return 'Now\n'; } else { return '${dateFormat.format(_currentXDataPoint!.xAxis)}\n'; } } else if (scale == BezierChartScale.WEEKLY) { final dateFormat = intl.DateFormat('EEE d'); final date = _currentXDataPoint!.xAxis as DateTime; final now = DateTime.now(); if (areEqualDates(date, now)) { return 'Current\n'; } else { return '${dateFormat.format(_currentXDataPoint!.xAxis)}\n'; } } else if (scale == BezierChartScale.MONTHLY) { final dateFormat = intl.DateFormat('MMM y'); final date = _currentXDataPoint!.xAxis as DateTime; final now = DateTime.now(); if (date.year == now.year && now.month == date.month) { return 'Current Month\n'; } else { return '${dateFormat.format(_currentXDataPoint!.xAxis)}\n'; } } else if (scale == BezierChartScale.YEARLY) { final dateFormat = intl.DateFormat('y'); final date = _currentXDataPoint!.xAxis as DateTime; final now = DateTime.now(); if (date.year == now.year) { return 'Current Year\n'; } else { return '${dateFormat.format(_currentXDataPoint!.xAxis)}\n'; } } return ''; } String _getFooterText(DataPoint dataPoint) { final scale = bezierChartScale; if (footerValueBuilder != null && scale == BezierChartScale.CUSTOM) { return footerValueBuilder!(dataPoint.value); } if (footerDateTimeBuilder != null && scale != BezierChartScale.CUSTOM) { return footerDateTimeBuilder!(dataPoint.xAxis as DateTime, scale); } if (scale == BezierChartScale.CUSTOM) { return '${formatAsIntOrDouble(dataPoint.value)}\n'; } else if (scale == BezierChartScale.HOURLY) { final dateFormat = intl.DateFormat('HH:mm\n'); return '${dateFormat.format(dataPoint.xAxis as DateTime)}'; } else if (scale == BezierChartScale.WEEKLY) { final dateFormat = intl.DateFormat('EEE\nd'); return '${dateFormat.format(dataPoint.xAxis as DateTime)}'; } else if (scale == BezierChartScale.MONTHLY) { final dateFormat = intl.DateFormat('MMM'); final dateFormatYear = intl.DateFormat('y'); final year = dateFormatYear.format(dataPoint.xAxis as DateTime).substring(2); return "${dateFormat.format(dataPoint.xAxis as DateTime)}\n'$year"; } else if (scale == BezierChartScale.YEARLY) { final dateFormat = intl.DateFormat('y'); return '${dateFormat.format(dataPoint.xAxis as DateTime)}'; } return ''; } _getYValues(Offset p0, Offset p1, Offset p2, Offset p3, double t) { if (t.isNaN) { t = 1.0; } //P0 = (X0,Y0) //P1 = (X1,Y1) //P2 = (X2,Y2) //P3 = (X3,Y3) //X(t) = (1-t)^3 * X0 + 3*(1-t)^2 * t * X1 + 3*(1-t) * t^2 * X2 + t^3 * X3 //Y(t) = (1-t)^3 * Y0 + 3*(1-t)^2 * t * Y1 + 3*(1-t) * t^2 * Y2 + t^3 * Y3 //source: https://stackoverflow.com/questions/8217346/cubic-bezier-curves-get-y-for-given-x final y0 = p0.dy; // x0 = p0.dx; final y1 = p1.dy; //x1 = p1.dx, final y2 = p2.dy; //x2 = p2.dx, final y3 = p3.dy; //x3 = p3.dx, //print('p0: $p0, p1: $p1, p2: $p2, p3: $p3 , t: $t'); final y = pow(1 - t, 3) * y0 + 3 * pow(1 - t, 2) * t * y1 + 3 * (1 - t) * pow(t, 2) * y2 + pow(t, 3) * y3; return y; } Rect _fromCenter({required Offset center, required double width, required double height}) => Rect.fromLTRB( center.dx - width / 2, center.dy - height / 2, center.dx + width / 2, center.dy + height / 2, ); @override bool shouldRepaint(_BezierChartPainter oldDelegate) => // shouldRepaintChart || // oldDelegate.verticalIndicatorPosition != verticalIndicatorPosition || // oldDelegate.scrollOffset != scrollOffset || // oldDelegate.showIndicator != showIndicator; // TODO(weilu): 修改处 oldDelegate.series != series || oldDelegate.verticalIndicatorPosition != verticalIndicatorPosition; } class _AxisValue { final double x; final double y; const _AxisValue({ required this.x, required this.y, }); } bool _compareLengths(int currentValue, List val2) { for (BezierLine line in val2) { if (currentValue != line.data.length) { return false; } } return true; } bool _isSorted(List list, [int Function(double, double)? compare]) { if (list.length < 2) return true; compare ??= (double a, double b) => a.compareTo(b); double prev = list.first; for (var i = 1; i < list.length; i++) { double next = list[i]; if (compare(prev, next) > 0) return false; prev = next; } return true; } bool _checkCustomValues(List list) { for (BezierLine line in list) { if (!_areAllPositive( line.data.map((dp) => dp.value), )) return false; } return true; } bool _areAllPositive(Iterable list) { for (double val in list) { if (val < 0) return false; } return true; } ///This method remove the decimals if the value doesn't have decimals String formatAsIntOrDouble(double str) { final values = str.toString().split('.'); if (values.length > 1) { final int intDecimal = int.parse(values[1]); if (intDecimal == 0) { return str.toInt().toString(); } } return str.toString(); } class _CustomValue { final String value; final String label; final Color color; _CustomValue({ required this.value, required this.label, required this.color, }); } bool areEqualDates(DateTime dateTime1, DateTime dateTime2) => dateTime1.year == dateTime2.year && dateTime1.month == dateTime2.month && dateTime1.day == dateTime2.day; bool areEqualDatesIncludingHour(DateTime dateTime1, DateTime dateTime2) => dateTime1.year == dateTime2.year && dateTime1.month == dateTime2.month && dateTime1.day == dateTime2.day && dateTime1.hour == dateTime2.hour; ================================================ FILE: lib/widgets/bezier_chart/bezier_line.dart ================================================ import 'package:flutter/material.dart'; typedef MissingValueBuilder = double Function(DateTime value); ///This Bezier line is used to display your data class BezierLine { ///Line color for each data point final Color lineColor; ///Color for the dot fill on each data point final Color dataPointFillColor; ///Color for the dot stroke on each data point final Color dataPointStrokeColor; ///`width` of the bezier line final double lineStrokeWidth; ///List of data points used to build the bezier line final List data; ///This builder is only valid for `bezierChartScale` of date types ///It uses the double value returned by the function based on the current `DateTime` received as parameter final MissingValueBuilder? onMissingValue; ///Label used in the bubble info indicator final String label; const BezierLine({ this.lineColor = Colors.white, this.lineStrokeWidth = 2.0, this.label = '', this.onMissingValue, Color? dataPointFillColor, Color? dataPointStrokeColor, required this.data, }) : dataPointFillColor = dataPointFillColor ?? lineColor, dataPointStrokeColor = dataPointStrokeColor ?? lineColor; factory BezierLine.copy({required BezierLine bezierLine}) { return BezierLine( lineColor: bezierLine.lineColor, lineStrokeWidth: bezierLine.lineStrokeWidth, label: bezierLine.label, dataPointFillColor: bezierLine.dataPointFillColor, dataPointStrokeColor: bezierLine.dataPointStrokeColor, onMissingValue: bezierLine.onMissingValue, data: bezierLine.data, ); } @override bool operator ==(Object other) => identical(this, other) || other is BezierLine && runtimeType == other.runtimeType && label == other.label && lineColor == other.lineColor && lineStrokeWidth == other.lineStrokeWidth && dataPointFillColor == other.dataPointFillColor && dataPointStrokeColor == other.dataPointStrokeColor && hashCode == other.hashCode; @override int get hashCode => data .map((val) => val.value.toString()) .reduce((val1, val2) => '$val1$val2') .hashCode; } ///This class represent each value `Y` per `X` axis class DataPoint { ///The value `Y` final double value; ///The `X` Axis value, it supports `double` and `DateTime` for now final T xAxis; const DataPoint({ required this.value, required this.xAxis, }); @override String toString() => 'value: $value, xAxis: $xAxis'; } ================================================ FILE: lib/widgets/bezier_chart/my_single_child_scroll_view.dart ================================================ import 'dart:math' as math; import 'package:flutter/widgets.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/gestures.dart' show DragStartBehavior; class MySingleChildScrollView extends StatelessWidget { /// Creates a box in which a single widget can be scrolled. const MySingleChildScrollView({ Key? key, this.scrollDirection = Axis.vertical, this.reverse = false, this.padding, bool? primary, this.physics, this.controller, required this.child, this.dragStartBehavior = DragStartBehavior.start, }) : assert( !(controller != null && primary == true), 'Primary ScrollViews obtain their ScrollController via inheritance from a PrimaryScrollController widget. ' 'You cannot both set primary to true and pass an explicit controller.'), primary = primary ?? controller == null && identical(scrollDirection, Axis.vertical), super(key: key); /// The axis along which the scroll view scrolls. /// /// Defaults to [Axis.vertical]. final Axis scrollDirection; /// Whether the scroll view scrolls in the reading direction. /// /// For example, if the reading direction is left-to-right and /// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from /// left to right when [reverse] is false and from right to left when /// [reverse] is true. /// /// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view /// scrolls from top to bottom when [reverse] is false and from bottom to top /// when [reverse] is true. /// /// Defaults to false. final bool reverse; /// The amount of space by which to inset the child. final EdgeInsetsGeometry? padding; /// An object that can be used to control the position to which this scroll /// view is scrolled. /// /// Must be null if [primary] is true. /// /// A [ScrollController] serves several purposes. It can be used to control /// the initial scroll position (see [ScrollController.initialScrollOffset]). /// It can be used to control whether the scroll view should automatically /// save and restore its scroll position in the [PageStorage] (see /// [ScrollController.keepScrollOffset]). It can be used to read the current /// scroll position (see [ScrollController.offset]), or change it (see /// [ScrollController.animateTo]). final ScrollController? controller; /// Whether this is the primary scroll view associated with the parent /// [PrimaryScrollController]. /// /// On iOS, this identifies the scroll view that will scroll to top in /// response to a tap in the status bar. /// /// Defaults to true when [scrollDirection] is vertical and [controller] is /// not specified. final bool primary; /// How the scroll view should respond to user input. /// /// For example, determines how the scroll view continues to animate after the /// user stops dragging the scroll view. /// /// Defaults to matching platform conventions. final ScrollPhysics? physics; /// The widget that scrolls. /// /// {@macro flutter.widgets.child} final Widget child; /// {@macro flutter.widgets.scrollable.dragStartBehavior} final DragStartBehavior dragStartBehavior; AxisDirection _getDirection(BuildContext context) { return getAxisDirectionFromAxisReverseAndDirectionality( context, scrollDirection, reverse); } @override Widget build(BuildContext context) { final AxisDirection axisDirection = _getDirection(context); Widget contents = child; if (padding != null) contents = Padding(padding: padding!, child: contents); final ScrollController? scrollController = primary ? PrimaryScrollController.of(context) : controller; final Scrollable scrollable = Scrollable( dragStartBehavior: dragStartBehavior, axisDirection: axisDirection, controller: scrollController, physics: physics, viewportBuilder: (BuildContext context, ViewportOffset offset) { return _SingleChildViewport( axisDirection: axisDirection, offset: offset, child: contents, ); }, ); return primary && scrollController != null ? PrimaryScrollController.none(child: scrollable) : scrollable; } } class _SingleChildViewport extends SingleChildRenderObjectWidget { const _SingleChildViewport({ Key? key, this.axisDirection = AxisDirection.down, required this.offset, Widget? child, }) : super(key: key, child: child); final AxisDirection axisDirection; final ViewportOffset offset; @override _RenderSingleChildViewport createRenderObject(BuildContext context) { return _RenderSingleChildViewport( axisDirection: axisDirection, offset: offset, ); } @override void updateRenderObject( BuildContext context, _RenderSingleChildViewport renderObject) { // Order dependency: The offset setter reads the axis direction. renderObject ..axisDirection = axisDirection ..offset = offset; } } class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMixin implements RenderAbstractViewport { _RenderSingleChildViewport({ AxisDirection axisDirection = AxisDirection.down, required ViewportOffset offset, double cacheExtent = RenderAbstractViewport.defaultCacheExtent, RenderBox? child, }) : _axisDirection = axisDirection, _offset = offset, _cacheExtent = cacheExtent { this.child = child; } AxisDirection get axisDirection => _axisDirection; AxisDirection _axisDirection; set axisDirection(AxisDirection value) { if (value == _axisDirection) return; _axisDirection = value; markNeedsLayout(); } Axis get axis => axisDirectionToAxis(axisDirection); ViewportOffset get offset => _offset; ViewportOffset _offset; set offset(ViewportOffset value) { if (value == _offset) return; if (attached) _offset.removeListener(_hasScrolled); _offset = value; if (attached) _offset.addListener(_hasScrolled); markNeedsLayout(); } /// {@macro flutter.rendering.viewport.cacheExtent} double get cacheExtent => _cacheExtent; double _cacheExtent; set cacheExtent(double value) { if (value == _cacheExtent) return; _cacheExtent = value; markNeedsLayout(); } void _hasScrolled() { markNeedsPaint(); markNeedsSemanticsUpdate(); } @override void setupParentData(RenderObject child) { // We don't actually use the offset argument in BoxParentData, so let's // avoid allocating it at all. if (child.parentData is! ParentData) child.parentData = ParentData(); } @override void attach(PipelineOwner owner) { super.attach(owner); _offset.addListener(_hasScrolled); } @override void detach() { _offset.removeListener(_hasScrolled); super.detach(); } @override bool get isRepaintBoundary => true; double get _viewportExtent { assert(hasSize); switch (axis) { case Axis.horizontal: return size.width; case Axis.vertical: return size.height; } } double get _minScrollExtent { assert(hasSize); return 0.0; } double get _maxScrollExtent { assert(hasSize); if (child == null) return 0.0; switch (axis) { case Axis.horizontal: return math.max(0.0, child!.size.width - size.width); case Axis.vertical: return math.max(0.0, child!.size.height - size.height); } } BoxConstraints _getInnerConstraints(BoxConstraints constraints) { switch (axis) { case Axis.horizontal: return constraints.heightConstraints(); case Axis.vertical: return constraints.widthConstraints(); } } @override double computeMinIntrinsicWidth(double height) { if (child != null) return child!.getMinIntrinsicWidth(height); return 0.0; } @override double computeMaxIntrinsicWidth(double height) { if (child != null) return child!.getMaxIntrinsicWidth(height); return 0.0; } @override double computeMinIntrinsicHeight(double width) { if (child != null) return child!.getMinIntrinsicHeight(width); return 0.0; } @override double computeMaxIntrinsicHeight(double width) { if (child != null) return child!.getMaxIntrinsicHeight(width); return 0.0; } // We don't override computeDistanceToActualBaseline(), because we // want the default behavior (returning null). Otherwise, as you // scroll, it would shift in its parent if the parent was baseline-aligned, // which makes no sense. @override void performLayout() { if (child == null) { size = constraints.smallest; } else { child!.layout(_getInnerConstraints(constraints), parentUsesSize: true); size = constraints.constrain(child!.size); } offset.applyViewportDimension(_viewportExtent); offset.applyContentDimensions(_minScrollExtent, _maxScrollExtent); } Offset get _paintOffset => _paintOffsetForPosition(offset.pixels); Offset _paintOffsetForPosition(double position) { switch (axisDirection) { case AxisDirection.up: return Offset(0.0, position - child!.size.height + size.height); case AxisDirection.down: return Offset(0.0, -position); case AxisDirection.left: return Offset(position - child!.size.width + size.width, 0.0); case AxisDirection.right: return Offset(-position, 0.0); } } bool _shouldClipAtPaintOffset(Offset paintOffset) { assert(child != null); return paintOffset < Offset.zero || !(Offset.zero & size).contains((paintOffset & child!.size).bottomRight); } @override void paint(PaintingContext context, Offset offset) { if (child != null) { final Offset paintOffset = _paintOffset; void paintContents(PaintingContext context, Offset offset) { context.paintChild(child!, offset + paintOffset); } if (_shouldClipAtPaintOffset(paintOffset)) { final Rect clipRect = Rect.fromLTWH(0.0, -size.height / 2, size.width, size.height * 1.5); context.pushClipRect(needsCompositing, offset, clipRect, paintContents); } else { paintContents(context, offset); } } } @override void applyPaintTransform(RenderBox child, Matrix4 transform) { final Offset paintOffset = _paintOffset; transform.translate(paintOffset.dx, paintOffset.dy); } @override Rect? describeApproximatePaintClip(RenderObject child) { if (_shouldClipAtPaintOffset(_paintOffset)) return Offset.zero & size; return null; } @override bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { if (child != null) { final Offset transformed = position + -_paintOffset; return child!.hitTest(result, position: transformed); } return false; } @override RevealedOffset getOffsetToReveal(RenderObject target, double alignment, {Rect? rect, Axis? axis,}) { axis = this.axis; rect ??= target.paintBounds; if (target is! RenderBox) return RevealedOffset(offset: offset.pixels, rect: rect); final RenderBox targetBox = target; final Matrix4 transform = targetBox.getTransformTo(this); final Rect bounds = MatrixUtils.transformRect(transform, rect); final Size contentSize = child!.size; double leadingScrollOffset; double targetMainAxisExtent; double mainAxisExtent; switch (axisDirection) { case AxisDirection.up: mainAxisExtent = size.height; leadingScrollOffset = contentSize.height - bounds.bottom; targetMainAxisExtent = bounds.height; break; case AxisDirection.right: mainAxisExtent = size.width; leadingScrollOffset = bounds.left; targetMainAxisExtent = bounds.width; break; case AxisDirection.down: mainAxisExtent = size.height; leadingScrollOffset = bounds.top; targetMainAxisExtent = bounds.height; break; case AxisDirection.left: mainAxisExtent = size.width; leadingScrollOffset = contentSize.width - bounds.right; targetMainAxisExtent = bounds.width; break; } final double targetOffset = leadingScrollOffset - (mainAxisExtent - targetMainAxisExtent) * alignment; final Rect targetRect = bounds.shift(_paintOffsetForPosition(targetOffset)); return RevealedOffset(offset: targetOffset, rect: targetRect); } @override void showOnScreen({ RenderObject? descendant, Rect? rect, Duration duration = Duration.zero, Curve curve = Curves.ease, }) { if (!offset.allowImplicitScrolling) { return super.showOnScreen( descendant: descendant, rect: rect, duration: duration, curve: curve, ); } final Rect? newRect = RenderViewportBase.showInViewport( descendant: descendant, viewport: this, offset: offset, rect: rect, duration: duration, curve: curve, ); super.showOnScreen( rect: newRect, duration: duration, curve: curve, ); } @override Rect describeSemanticsClip(RenderObject child) { switch (axis) { case Axis.vertical: return Rect.fromLTRB( semanticBounds.left, semanticBounds.top - cacheExtent, semanticBounds.right, semanticBounds.bottom + cacheExtent, ); case Axis.horizontal: return Rect.fromLTRB( semanticBounds.left - cacheExtent, semanticBounds.top, semanticBounds.right + cacheExtent, semanticBounds.bottom, ); } } } ================================================ FILE: lib/widgets/click_item.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/res/resources.dart'; class ClickItem extends StatelessWidget { const ClickItem({ super.key, this.onTap, required this.title, this.content = '', this.textAlign = TextAlign.start, this.maxLines = 1 }); final GestureTapCallback? onTap; final String title; final String content; final TextAlign textAlign; final int maxLines; @override Widget build(BuildContext context) { Widget child = Row( //为了数字类文字居中 crossAxisAlignment: maxLines == 1 ? CrossAxisAlignment.center : CrossAxisAlignment.start, children: [ Text(title), const Spacer(), Gaps.hGap16, Expanded( flex: 4, child: Text( content, maxLines: maxLines, textAlign: maxLines == 1 ? TextAlign.right : textAlign, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.titleSmall?.copyWith(fontSize: Dimens.font_sp14), ), ), Gaps.hGap8, Opacity( // 无点击事件时,隐藏箭头图标 opacity: onTap == null ? 0 : 1, child: Padding( padding: EdgeInsets.only(top: maxLines == 1 ? 0.0 : 2.0), child: Images.arrowRight, ), ) ], ); /// 分隔线 child = Container( margin: const EdgeInsets.only(left: 15.0), padding: const EdgeInsets.fromLTRB(0, 15.0, 15.0, 15.0), constraints: const BoxConstraints( minHeight: 50.0, ), width: double.infinity, decoration: BoxDecoration( border: Border( bottom: Divider.createBorderSide(context, width: 0.6), ), ), child: child, ); return InkWell( onTap: onTap, child: child, ); } } ================================================ FILE: lib/widgets/double_tap_back_exit_app.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_deer/util/toast_utils.dart'; /// 双击返回退出 class DoubleTapBackExitApp extends StatefulWidget { const DoubleTapBackExitApp({ super.key, required this.child, this.duration = const Duration(milliseconds: 2500), }); final Widget child; /// 两次点击返回按钮的时间间隔 final Duration duration; @override _DoubleTapBackExitAppState createState() => _DoubleTapBackExitAppState(); } class _DoubleTapBackExitAppState extends State { DateTime? _lastTime; @override Widget build(BuildContext context) { return WillPopScope( onWillPop: _isExit, child: widget.child, ); } Future _isExit() async { if (_lastTime == null || DateTime.now().difference(_lastTime!) > widget.duration) { _lastTime = DateTime.now(); Toast.show('再次点击退出应用'); return Future.value(false); } Toast.cancelToast(); /// 不推荐使用 `dart:io` 的 exit(0) await SystemNavigator.pop(); return Future.value(true); } } ================================================ FILE: lib/widgets/fractionally_aligned_sized_box.dart ================================================ import 'package:flutter/widgets.dart'; /// https://github.com/letsar/flutter_slidable /// A widget that positions its child to a fraction of the total available space. class FractionallyAlignedSizedBox extends StatelessWidget { /// Creates a widget that positions its child to a fraction of the total available space. /// /// Only two out of the three horizontal values ([leftFactor], [rightFactor], /// [widthFactor]), and only two out of the three vertical values ([topFactor], /// [bottomFactor], [heightFactor]), can be set. In each case, at least one of /// the three must be null. /// /// If non-null, the [widthFactor] and [heightFactor] arguments must be /// non-negative. const FractionallyAlignedSizedBox({ super.key, required this.child, this.leftFactor, this.topFactor, this.rightFactor, this.bottomFactor, this.widthFactor, this.heightFactor, }) : assert( leftFactor == null || rightFactor == null || widthFactor == null), assert( topFactor == null || bottomFactor == null || heightFactor == null), assert(widthFactor == null || widthFactor >= 0.0), assert(heightFactor == null || heightFactor >= 0.0); /// The relative distance that the child's left edge is inset from the left of the parent. final double? leftFactor; /// The relative distance that the child's top edge is inset from the top of the parent. final double? topFactor; /// The relative distance that the child's right edge is inset from the right of the parent. final double? rightFactor; /// The relative distance that the child's bottom edge is inset from the bottom of the parent. final double? bottomFactor; /// The child's width relative to its parent's width. final double? widthFactor; /// The child's height relative to its parent's height. final double? heightFactor; /// The widget below this widget in the tree. final Widget child; @override Widget build(BuildContext context) { double dx = 0; double dy = 0; double? width = widthFactor; double? height = heightFactor; if (widthFactor == null) { final left = leftFactor ?? 0; final right = rightFactor ?? 0; width = 1 - left - right; if (width != 1) { dx = left / (1.0 - width); } } if (heightFactor == null) { final top = topFactor ?? 0; final bottom = bottomFactor ?? 0; height = 1 - top - bottom; if (height != 1) { dy = top / (1.0 - height); } } if (widthFactor != null && widthFactor != 1) { if (leftFactor != null) { dx = leftFactor! / (1 - widthFactor!); } else if (leftFactor == null && rightFactor != null) { dx = (1 - widthFactor! - rightFactor!) / (1 - widthFactor!); } } if (heightFactor != null && heightFactor != 1) { if (topFactor != null) { dy = topFactor! / (1 - heightFactor!); } else if (topFactor == null && bottomFactor != null) { dy = (1 - heightFactor! - bottomFactor!) / (1 - heightFactor!); } } return Align( alignment: FractionalOffset( dx, dy, ), child: FractionallySizedBox( widthFactor: width, heightFactor: height, child: child, ), ); } } ================================================ FILE: lib/widgets/load_image.dart ================================================ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_deer/util/image_utils.dart'; /// 图片加载(支持本地与网络图片) class LoadImage extends StatelessWidget { const LoadImage(this.image, { super.key, this.width, this.height, this.fit = BoxFit.cover, this.format = ImageFormat.png, this.holderImg = 'none', this.cacheWidth, this.cacheHeight, }); final String image; final double? width; final double? height; final BoxFit fit; final ImageFormat format; final String holderImg; final int? cacheWidth; final int? cacheHeight; @override Widget build(BuildContext context) { if (image.isEmpty || image.startsWith('http')) { final Widget holder = LoadAssetImage(holderImg, height: height, width: width, fit: fit); return CachedNetworkImage( imageUrl: image, placeholder: (_, __) => holder, errorWidget: (_, __, dynamic error) => holder, width: width, height: height, fit: fit, memCacheWidth: cacheWidth, memCacheHeight: cacheHeight, ); } else { return LoadAssetImage(image, height: height, width: width, fit: fit, format: format, cacheWidth: cacheWidth, cacheHeight: cacheHeight, ); } } } /// 加载本地资源图片 class LoadAssetImage extends StatelessWidget { const LoadAssetImage(this.image, { super.key, this.width, this.height, this.cacheWidth, this.cacheHeight, this.fit, this.format = ImageFormat.png, this.color }); final String image; final double? width; final double? height; final int? cacheWidth; final int? cacheHeight; final BoxFit? fit; final ImageFormat format; final Color? color; @override Widget build(BuildContext context) { return Image.asset( ImageUtils.getImgPath(image, format: format), height: height, width: width, cacheWidth: cacheWidth, cacheHeight: cacheHeight, fit: fit, color: color, /// 忽略图片语义 excludeFromSemantics: true, ); } } ================================================ FILE: lib/widgets/my_app_bar.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/util/theme_utils.dart'; import 'package:flutter_deer/widgets/my_button.dart'; /// 自定义AppBar class MyAppBar extends StatelessWidget implements PreferredSizeWidget { const MyAppBar({ super.key, this.backgroundColor, this.title = '', this.centerTitle = '', this.actionName = '', this.backImg = 'assets/images/ic_back_black.png', this.backImgColor, this.onPressed, this.isBack = true }); final Color? backgroundColor; final String title; final String centerTitle; final String backImg; final Color? backImgColor; final String actionName; final VoidCallback? onPressed; final bool isBack; @override Widget build(BuildContext context) { final Color bgColor = backgroundColor ?? context.backgroundColor; final SystemUiOverlayStyle overlayStyle = ThemeData.estimateBrightnessForColor(bgColor) == Brightness.dark ? ThemeUtils.light : ThemeUtils.dark; final Widget action = actionName.isNotEmpty ? Positioned( right: 0.0, child: Theme( data: Theme.of(context).copyWith( buttonTheme: const ButtonThemeData( padding: EdgeInsets.symmetric(horizontal: 16.0), minWidth: 60.0, ), ), child: MyButton( key: const Key('actionName'), fontSize: Dimens.font_sp14, minWidth: null, text: actionName, textColor: context.isDark ? Colours.dark_text : Colours.text, backgroundColor: Colors.transparent, onPressed: onPressed, ), ), ) : Gaps.empty; final Widget back = isBack ? IconButton( onPressed: () async { FocusManager.instance.primaryFocus?.unfocus(); final isBack = await Navigator.maybePop(context); if (!isBack) { await SystemNavigator.pop(); } }, tooltip: 'Back', padding: const EdgeInsets.all(12.0), icon: Image.asset( backImg, color: backImgColor ?? ThemeUtils.getIconColor(context), ), ) : Gaps.empty; final Widget titleWidget = Semantics( namesRoute: true, header: true, child: Container( alignment: centerTitle.isEmpty ? Alignment.centerLeft : Alignment.center, width: double.infinity, margin: const EdgeInsets.symmetric(horizontal: 48.0), child: Text( title.isEmpty ? centerTitle : title, style: const TextStyle(fontSize: Dimens.font_sp18,), ), ), ); return AnnotatedRegion( value: overlayStyle, child: Material( color: bgColor, child: SafeArea( child: Stack( alignment: Alignment.centerLeft, children: [ titleWidget, back, action, ], ), ), ), ); } @override Size get preferredSize => const Size.fromHeight(48.0); } ================================================ FILE: lib/widgets/my_button.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/util/theme_utils.dart'; /// 默认字号18,白字蓝底,高度48 class MyButton extends StatelessWidget { const MyButton({ super.key, this.text = '', this.fontSize = Dimens.font_sp18, this.textColor, this.disabledTextColor, this.backgroundColor, this.disabledBackgroundColor, this.minHeight = 48.0, this.minWidth = double.infinity, this.padding = const EdgeInsets.symmetric(horizontal: 16.0), this.radius = 2.0, this.side = BorderSide.none, required this.onPressed, }); final String text; final double fontSize; final Color? textColor; final Color? disabledTextColor; final Color? backgroundColor; final Color? disabledBackgroundColor; final double? minHeight; final double? minWidth; final VoidCallback? onPressed; final EdgeInsetsGeometry padding; final double radius; final BorderSide side; @override Widget build(BuildContext context) { final bool isDark = context.isDark; return TextButton( onPressed: onPressed, style: ButtonStyle( // 文字颜色 foregroundColor: MaterialStateProperty.resolveWith((states) { if (states.contains(MaterialState.disabled)) { return disabledTextColor ?? (isDark ? Colours.dark_text_disabled : Colours.text_disabled); } return textColor ?? (isDark ? Colours.dark_button_text : Colors.white); }, ), // 背景颜色 backgroundColor: MaterialStateProperty.resolveWith((states) { if (states.contains(MaterialState.disabled)) { return disabledBackgroundColor ?? (isDark ? Colours.dark_button_disabled : Colours.button_disabled); } return backgroundColor ?? (isDark ? Colours.dark_app_main : Colours.app_main); }), // 水波纹 overlayColor: MaterialStateProperty.resolveWith((states) { return (textColor ?? (isDark ? Colours.dark_button_text : Colors.white)).withOpacity(0.12); }), // 按钮最小大小 minimumSize: (minWidth == null || minHeight == null) ? null : MaterialStateProperty.all(Size(minWidth!, minHeight!)), padding: MaterialStateProperty.all(padding), shape: MaterialStateProperty.all( RoundedRectangleBorder( borderRadius: BorderRadius.circular(radius), ), ), side: MaterialStateProperty.all(side), ), child: Text(text, style: TextStyle(fontSize: fontSize),) ); } } ================================================ FILE: lib/widgets/my_card.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/res/colors.dart'; import 'package:flutter_deer/util/theme_utils.dart'; class MyCard extends StatelessWidget { const MyCard({ super.key, required this.child, this.color, this.shadowColor }); final Widget child; final Color? color; final Color? shadowColor; @override Widget build(BuildContext context) { final bool isDark = context.isDark; final Color backgroundColor = color ?? (isDark ? Colours.dark_bg_gray_ : Colors.white); final Color sColor = isDark ? Colors.transparent : (shadowColor ?? const Color(0x80DCE7FA)); return DecoratedBox( decoration: BoxDecoration( color: backgroundColor, borderRadius: BorderRadius.circular(8.0), boxShadow: [ BoxShadow(color: sColor, offset: const Offset(0.0, 2.0), blurRadius: 8.0), ], ), child: child, ); } } ================================================ FILE: lib/widgets/my_flexible_space_bar.dart ================================================ // Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/material.dart'; /// The part of a material design [AppBar] that expands and collapses. /// /// Most commonly used in in the [SliverAppBar.flexibleSpace] field, a flexible /// space bar expands and contracts as the app scrolls so that the [AppBar] /// reaches from the top of the app to the top of the scrolling contents of the /// app. /// /// The widget that sizes the [AppBar] must wrap it in the widget returned by /// [FlexibleSpaceBar.createSettings], to convey sizing information down to the /// [FlexibleSpaceBar]. /// /// See also: /// /// * [SliverAppBar], which implements the expanding and contracting. /// * [AppBar], which is used by [SliverAppBar]. /// * class MyFlexibleSpaceBar extends StatefulWidget { /// Creates a flexible space bar. /// /// Most commonly used in the [AppBar.flexibleSpace] field. const MyFlexibleSpaceBar({ super.key, this.title, this.background, this.centerTitle, this.titlePadding, this.collapseMode = CollapseMode.parallax, }); /// The primary contents of the flexible space bar when expanded. /// /// Typically a [Text] widget. final Widget? title; /// Shown behind the [title] when expanded. /// /// Typically an [Image] widget with [Image.fit] set to [BoxFit.cover]. final Widget? background; /// Whether the title should be centered. /// /// By default this property is true if the current target platform /// is [TargetPlatform.iOS], false otherwise. final bool? centerTitle; /// Collapse effect while scrolling. /// /// Defaults to [MyCollapseMode.parallax]. final CollapseMode collapseMode; /// Defines how far the [title] is inset from either the widget's /// bottom-left or its center. /// /// Typically this property is used to adjust how far the title is /// is inset from the bottom-left and it is specified along with /// [centerTitle] false. /// /// By default the value of this property is /// `EdgeInsetsDirectional.only(start: 72, bottom: 16)` if the title is /// not centered, `EdgeInsetsDirectional.only(start 0, bottom: 16)` otherwise. final EdgeInsetsGeometry? titlePadding; /// Wraps a widget that contains an [AppBar] to convey sizing information down /// to the [FlexibleSpaceBar]. /// /// Used by [Scaffold] and [SliverAppBar]. /// /// `toolbarOpacity` affects how transparent the text within the toolbar /// appears. `minExtent` sets the minimum height of the resulting /// [FlexibleSpaceBar] when fully collapsed. `maxExtent` sets the maximum /// height of the resulting [FlexibleSpaceBar] when fully expanded. /// `currentExtent` sets the scale of the [FlexibleSpaceBar.background] and /// [FlexibleSpaceBar.title] widgets of [FlexibleSpaceBar] upon /// initialization. /// /// See also: /// /// * [FlexibleSpaceBarSettings] which creates a settings object that can be /// used to specify these settings to a [FlexibleSpaceBar]. static Widget createSettings({ double? toolbarOpacity, double? minExtent, double? maxExtent, required double currentExtent, required Widget child, }) { return FlexibleSpaceBarSettings( toolbarOpacity: toolbarOpacity ?? 1.0, minExtent: minExtent ?? currentExtent, maxExtent: maxExtent ?? currentExtent, currentExtent: currentExtent, child: child, ); } @override _FlexibleSpaceBarState createState() => _FlexibleSpaceBarState(); } class _FlexibleSpaceBarState extends State { bool _getEffectiveCenterTitle(ThemeData theme) { if (widget.centerTitle != null) { return widget.centerTitle!; } switch (theme.platform) { case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: return false; case TargetPlatform.iOS: case TargetPlatform.macOS: return true; } } Alignment _getTitleAlignment(bool effectiveCenterTitle) { if (effectiveCenterTitle) { return Alignment.bottomCenter; } final TextDirection textDirection = Directionality.of(context); switch (textDirection) { case TextDirection.rtl: return Alignment.bottomRight; case TextDirection.ltr: return Alignment.bottomLeft; } } double? _getCollapsePadding(double t, FlexibleSpaceBarSettings settings) { switch (widget.collapseMode) { case CollapseMode.pin: return -(settings.maxExtent - settings.currentExtent); case CollapseMode.none: return 0.0; case CollapseMode.parallax: final double deltaExtent = settings.maxExtent - settings.minExtent; return -Tween(begin: 0.0, end: deltaExtent / 4.0).transform(t); } } final GlobalKey _key = GlobalKey(); double _offset = 0; @override void initState() { //监听Widget是否绘制完毕 WidgetsBinding.instance.addPostFrameCallback((_) { final RenderBox? renderBoxRed = _key.currentContext!.findRenderObject() as RenderBox?; _offset = renderBoxRed!.size.width / 2; }); super.initState(); } @override Widget build(BuildContext context) { final Size size = MediaQuery.of(context).size; final FlexibleSpaceBarSettings settings = context.dependOnInheritedWidgetOfExactType()!; final List children = []; final double deltaExtent = settings.maxExtent - settings.minExtent; // 0.0 -> Expanded // 1.0 -> Collapsed to toolbar final double t = (1.0 - (settings.currentExtent - settings.minExtent) / deltaExtent).clamp(0.0, 1.0); // background image if (widget.background != null) { children.add(Positioned( top: _getCollapsePadding(t, settings), left: 0.0, right: 0.0, height: settings.maxExtent, child: widget.background!, )); } if (widget.title != null) { final ThemeData theme = Theme.of(context); Widget title; switch (theme.platform) { case TargetPlatform.iOS: case TargetPlatform.macOS: title = widget.title!; break; case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: title = Semantics( namesRoute: true, child: widget.title, ); break; } title = Container( key: _key, child: title, ); final double opacity = settings.toolbarOpacity; if (opacity > 0.0) { TextStyle titleStyle = theme.primaryTextTheme.titleLarge!; titleStyle = titleStyle.copyWith( color: titleStyle.color!.withOpacity(opacity), fontWeight: t != 0 ? FontWeight.normal : FontWeight.bold ); final bool effectiveCenterTitle = _getEffectiveCenterTitle(theme); final EdgeInsetsGeometry padding = widget.titlePadding ?? EdgeInsetsDirectional.only( start: effectiveCenterTitle ? 0.0 : 72.0, bottom: 16.0, ); final double scaleValue = Tween(begin: 1.5, end: 1.0).transform(t); final double width = (size.width - 32.0) / 2 - _offset; final Matrix4 scaleTransform = Matrix4.identity() ..scale(scaleValue, scaleValue, 1.0)..translate(t * width); final Alignment titleAlignment = _getTitleAlignment(false); children.add(Container( padding: padding, child: Transform( alignment: titleAlignment, transform: scaleTransform, child: Align( alignment: titleAlignment, child: DefaultTextStyle( style: titleStyle, child: title, ), ), ), )); } } return ClipRect(child: Stack(children: children)); } } ================================================ FILE: lib/widgets/my_refresh_list.dart ================================================ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/util/theme_utils.dart'; import 'package:flutter_deer/widgets/state_layout.dart'; /// 封装下拉刷新与加载更多 class DeerListView extends StatefulWidget { const DeerListView({ super.key, required this.itemCount, required this.itemBuilder, required this.onRefresh, this.loadMore, this.hasMore = false, this.stateType = StateType.empty, this.pageSize = 10, this.padding, this.itemExtent, }); final RefreshCallback onRefresh; final LoadMoreCallback? loadMore; final int itemCount; final bool hasMore; final IndexedWidgetBuilder itemBuilder; final StateType stateType; /// 一页的数量,默认为10 final int pageSize; /// padding属性使用时注意会破坏原有的SafeArea,需要自行计算bottom大小 final EdgeInsetsGeometry? padding; final double? itemExtent; @override _DeerListViewState createState() => _DeerListViewState(); } typedef RefreshCallback = Future Function(); typedef LoadMoreCallback = Future Function(); class _DeerListViewState extends State { /// 是否正在加载数据 bool _isLoading = false; @override Widget build(BuildContext context) { final Widget child = RefreshIndicator( onRefresh: widget.onRefresh, child: widget.itemCount == 0 ? StateLayout(type: widget.stateType) : ListView.builder( itemCount: widget.loadMore == null ? widget.itemCount : widget.itemCount + 1, padding: widget.padding, itemExtent: widget.itemExtent, itemBuilder: (BuildContext context, int index) { /// 不需要加载更多则不需要添加FootView if (widget.loadMore == null) { return widget.itemBuilder(context, index); } else { return index < widget.itemCount ? widget.itemBuilder(context, index) : MoreWidget(widget.itemCount, widget.hasMore, widget.pageSize); } }, ), ); return SafeArea( child: NotificationListener( onNotification: (ScrollNotification note) { /// 确保是垂直方向滚动,且滑动至底部 if (note.metrics.pixels == note.metrics.maxScrollExtent && note.metrics.axis == Axis.vertical) { _loadMore(); } return true; }, child: child, ), ); } Future _loadMore() async { if (widget.loadMore == null) { return; } if (_isLoading) { return; } if (!widget.hasMore) { return; } _isLoading = true; await widget.loadMore?.call(); _isLoading = false; } } class MoreWidget extends StatelessWidget { const MoreWidget(this.itemCount, this.hasMore, this.pageSize, {super.key}); final int itemCount; final bool hasMore; final int pageSize; @override Widget build(BuildContext context) { final TextStyle style = context.isDark ? TextStyles.textGray14 : const TextStyle(color: Color(0x8A000000)); return Padding( padding: const EdgeInsets.symmetric(vertical: 10.0), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ if (hasMore) const CupertinoActivityIndicator(), if (hasMore) Gaps.hGap5, /// 只有一页的时候,就不显示FooterView了 Text(hasMore ? '正在加载中...' : (itemCount < pageSize ? '' : '没有了呦~'), style: style), ], ), ); } } ================================================ FILE: lib/widgets/my_scroll_view.dart ================================================ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:keyboard_actions/keyboard_actions.dart'; /// 本项目通用的布局(SingleChildScrollView) /// 1.底部存在按钮 /// 2.底部没有按钮 class MyScrollView extends StatelessWidget { /// 注意:同时存在底部按钮与keyboardConfig配置时,为保证软键盘弹出高度正常。需要在`Scaffold`使用 `resizeToAvoidBottomInset: defaultTargetPlatform != TargetPlatform.iOS,` /// 除非Android与iOS平台均使用keyboard_actions const MyScrollView({ super.key, required this.children, this.padding, this.physics = const BouncingScrollPhysics(), this.crossAxisAlignment = CrossAxisAlignment.start, this.bottomButton, this.keyboardConfig, this.tapOutsideToDismiss = false, this.overScroll = 16.0, }); final List children; final EdgeInsetsGeometry? padding; final ScrollPhysics physics; final CrossAxisAlignment crossAxisAlignment; final Widget? bottomButton; final KeyboardActionsConfig? keyboardConfig; /// 键盘外部按下将其关闭 final bool tapOutsideToDismiss; /// 默认弹起位置在TextField的文字下面,可以添加此属性继续向上滑动一段距离。用来露出完整的TextField。 final double overScroll; @override Widget build(BuildContext context) { Widget contents = Column( crossAxisAlignment: crossAxisAlignment, children: children, ); if (defaultTargetPlatform == TargetPlatform.iOS && keyboardConfig != null) { /// iOS 键盘处理 if (padding != null) { contents = Padding( padding: padding!, child: contents ); } contents = KeyboardActions( isDialog: bottomButton != null, overscroll: overScroll, config: keyboardConfig!, tapOutsideBehavior: tapOutsideToDismiss ? TapOutsideBehavior.opaqueDismiss : TapOutsideBehavior.none, child: contents ); } else { contents = SingleChildScrollView( padding: padding, physics: physics, child: contents, ); } if (bottomButton != null) { contents = Column( children: [ Expanded( child: contents ), SafeArea( child: bottomButton! ) ], ); } return contents; } } ================================================ FILE: lib/widgets/my_search_bar.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/util/theme_utils.dart'; import 'package:flutter_deer/widgets/my_button.dart'; import 'load_image.dart'; /// 搜索页的AppBar class MySearchBar extends StatefulWidget implements PreferredSizeWidget { const MySearchBar({ super.key, this.hintText = '', this.backImg = 'assets/images/ic_back_black.png', this.onPressed, }); final String backImg; final String hintText; final void Function(String)? onPressed; @override _MySearchBarState createState() => _MySearchBarState(); @override Size get preferredSize => const Size.fromHeight(48.0); } class _MySearchBarState extends State { final TextEditingController _controller = TextEditingController(); final FocusNode _focus = FocusNode(); @override void dispose() { _focus.dispose(); _controller.dispose(); super.dispose(); } // @override // void initState() { // WidgetsBinding.instance!.addPostFrameCallback((_) async { // SystemChannels.textInput.invokeMethod('TextInput.updateConfig', const TextInputConfiguration().toJson()); // SystemChannels.textInput.invokeMethod('TextInput.hide'); // }); // super.initState(); // } @override Widget build(BuildContext context) { final bool isDark = context.isDark; final Color iconColor = isDark ? Colours.dark_text_gray : Colours.text_gray_c; final Widget back = Semantics( label: '返回', child: SizedBox( width: 48.0, height: 48.0, child: InkWell( onTap: () { _focus.unfocus(); Navigator.maybePop(context); }, borderRadius: BorderRadius.circular(24.0), child: Padding( key: const Key('search_back'), padding: const EdgeInsets.all(12.0), child: Image.asset( widget.backImg, color: isDark ? Colours.dark_text : Colours.text, ), ), ), ), ); /// 使用2.0.0新增CupertinoSearchTextField 实现, 需添加依赖 cupertino_icons: ^1.0.2 // final Widget textField1 = Expanded(child: Container( // height: 32.0, // child: CupertinoSearchTextField( // key: const Key('search_text_field'), // controller: _controller, // focusNode: _focus, // placeholder: widget.hintText, // placeholderStyle: Theme.of(context).inputDecorationTheme.hintStyle, // padding: const EdgeInsetsDirectional.fromSTEB(3.8, 0, 5, 0), // prefixInsets: const EdgeInsetsDirectional.fromSTEB(8, 0, 0, 0), // suffixInsets: const EdgeInsetsDirectional.fromSTEB(0, 0, 8, 0), // style: Theme.of(context).textTheme.subtitle1, // itemSize: 16.0, // itemColor: iconColor, // decoration: BoxDecoration( // color: isDark ? Colours.dark_material_bg : Colours.bg_gray, // borderRadius: BorderRadius.circular(4.0), // ), // onSubmitted: (String val) { // _focus.unfocus(); // // 点击软键盘的动作按钮时的回调 // widget.onPressed(val); // }, // ) // )); final Widget textField = Expanded( child: Container( height: 32.0, decoration: BoxDecoration( color: isDark ? Colours.dark_material_bg : Colours.bg_gray, borderRadius: BorderRadius.circular(4.0), ), child: TextField( key: const Key('search_text_field'), // autofocus: true, controller: _controller, focusNode: _focus, textInputAction: TextInputAction.search, onSubmitted: (String val) { _focus.unfocus(); // 点击软键盘的动作按钮时的回调 widget.onPressed?.call(val); }, decoration: InputDecoration( contentPadding: const EdgeInsets.only(left: -8.0, right: -16.0, bottom: 14.0), border: InputBorder.none, icon: Padding( padding: const EdgeInsets.only(top: 8.0, bottom: 8.0, left: 8.0), child: LoadAssetImage('order/order_search', color: iconColor,), ), hintText: widget.hintText, suffixIcon: GestureDetector( child: Semantics( label: '清空', child: Padding( padding: const EdgeInsets.only(left: 16.0, top: 8.0, bottom: 8.0), child: LoadAssetImage('order/order_delete', color: iconColor), ), ), onTap: () { /// https://github.com/flutter/flutter/issues/35848 SchedulerBinding.instance.addPostFrameCallback((_) { _controller.text = ''; }); }, ), ), ), ), ); final Widget search = MyButton( minHeight: 32.0, minWidth: 44.0, fontSize: Dimens.font_sp14, radius: 4.0, padding: const EdgeInsets.symmetric(horizontal: 8.0), text: '搜索', onPressed:() { _focus.unfocus(); widget.onPressed?.call(_controller.text); }, ); return AnnotatedRegion( value: isDark ? ThemeUtils.light : ThemeUtils.dark, child: Material( color: context.backgroundColor, child: SafeArea( child: Row( children: [ back, textField, Gaps.hGap8, search, Gaps.hGap16, ], ), ), ), ); } } ================================================ FILE: lib/widgets/pie_chart/pie_chart.dart ================================================ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/semantics.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/util/theme_utils.dart'; import 'package:flutter_deer/widgets/pie_chart/pie_data.dart'; ///环形图 参考:https://github.com/apgapg/pie_chart class PieChart extends StatefulWidget { const PieChart({ super.key, required this.data, required this.name }); final List data; final String name; static const List colorList = [ Color(0xFFFFD147), Color(0xFFA9DAF2), Color(0xFFFAAF64), Color(0xFF7087FA), Color(0xFFA0E65C), Color(0xFF5CE6A1), Color(0xFFA364FA), Color(0xFFDA61F2), Color(0xFFFA64AE), Color(0xFFFA6464), ]; @override _PieChartState createState() => _PieChartState(); } class _PieChartState extends State with SingleTickerProviderStateMixin { late int count; late Animation animation; late AnimationController controller; late List oldData; @override void initState() { super.initState(); controller = AnimationController(duration: const Duration(milliseconds: 800), vsync: this); final Animation curve = CurvedAnimation(parent: controller, curve: Curves.decelerate); animation = Tween(begin: 0, end: 1).animate(curve); controller.forward(from: 0); } @override void didUpdateWidget(PieChart oldWidget) { super.didUpdateWidget(oldWidget); // 数据变化时执行动画 if (oldData != widget.data) { controller.forward(from: 0); } } @override void dispose() { controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { oldData = widget.data; count = 0; for (int i = 0; i < widget.data.length; i++) { count += widget.data[i].number; } final Color bgColor = context.backgroundColor; final Color shadowColor = context.isDark ? Colours.dark_bg_gray : const Color(0x80C8DAFA); return Container( decoration: BoxDecoration( shape: BoxShape.circle, color: bgColor, boxShadow: [ BoxShadow(color: shadowColor, offset: const Offset(0.0, 4.0), blurRadius: 8.0), ], ), child: RepaintBoundary( child: AnimatedBuilder( animation: animation, builder: (_, Widget? child) { return CustomPaint( painter: PieChartPainter( widget.data, animation.value, bgColor, widget.name, count ), child: child, ); }, child: Center( child: ExcludeSemantics( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(widget.name, style: TextStyles.textBold16), Gaps.vGap4, Text('$count件') ], ), ), ) ), ), ); } } class PieChartPainter extends CustomPainter { PieChartPainter(this.data, double angleFactor, this.bgColor, this.name, this.count) { if (data.isEmpty) { return; } int count = 0; for (int i = 0; i < data.length; i++) { count += data[i].number; } PieData? pieData; if (data.length == 11) { // 获取“其他”数据 pieData = data[10]; pieData.percentage = pieData.number / count; pieData.color = Colours.text_gray_c; // 移除“其他”后,按数量排序 data.removeAt(10); } data.sort((PieData left,PieData right) => right.number.compareTo(left.number)); // 由大到小给予颜色 for (int i = 0; i < data.length; i++) { data[i].color = PieChart.colorList[i]; data[i].percentage = data[i].number / count; // 排序后的数据输出 // print(data[i].toString()); } if (pieData != null) { data.add(pieData); } _mPaint = Paint(); totalAngle = angleFactor * math.pi * 2; } late Rect mCircle; late Paint _mPaint; // 半径 late double mRadius; late List data; late double totalAngle; // 起始角度 late double prevAngle; late Color bgColor; // 总数量 late int count; // 图表名称 late String name; @override void paint(Canvas canvas, Size size) { if (data.isEmpty) { return; } prevAngle = -math.pi; mRadius = math.min(size.width, size.height) / 2 - 4; // 圆心 final Offset offset = Offset(size.width / 2, size.height / 2); mCircle = Rect.fromCircle(center: offset, radius: mRadius); for (int i = 0; i < data.length; i++) { _mPaint..color = data[i].color ..style = PaintingStyle.fill; canvas.drawArc(mCircle, prevAngle, totalAngle * data[i].percentage, true, _mPaint); prevAngle = prevAngle + totalAngle * data[i].percentage; } // 为了文字不被覆盖,在绘制完扇形后绘制文字 prevAngle = -math.pi; for (int i = 0; i < data.length; i++) { //计算扇形中心点的坐标 final double x = (size.height * 0.74 / 2) * math.cos(prevAngle + (totalAngle * data[i].percentage / 2)); final double y = (size.height * 0.74 / 2) * math.sin(prevAngle + (totalAngle * data[i].percentage / 2)); // 保留一位小数 final String percentage = '${(data[i].percentage * 100).toStringAsFixed(1)}%'; drawPercentage(canvas, percentage, x, y, size); prevAngle = prevAngle + totalAngle * data[i].percentage; } canvas.save(); _mPaint..color = bgColor ..style = PaintingStyle.fill; canvas.drawCircle(offset, mRadius * 0.52, _mPaint); _mPaint..color = const Color(0x80FFFFFF) ..style = PaintingStyle.stroke ..strokeWidth = 8.0; canvas.drawCircle(offset, mRadius * 0.52, _mPaint); canvas.restore(); } void drawPercentage(Canvas context, String percentage, double x, double y, Size size) { final TextSpan span = TextSpan( style: const TextStyle(color: Colors.white, fontSize: Dimens.font_sp12), text: percentage); final TextPainter tp = TextPainter( text: span, textAlign: TextAlign.left, textDirection: TextDirection.rtl); tp.layout(); tp.paint(context, Offset(size.width / 2 + x - (tp.width / 2), size.height / 2 + y - (tp.height / 2))); } @override bool shouldRepaint(PieChartPainter oldDelegate) { // 由于动画需要重绘,所以返true。避免重绘,交由RepaintBoundary处理。你也可以判断动画是否执行完成来处理时候重绘 return true; } @override SemanticsBuilderCallback get semanticsBuilder => _buildSemantics; /// 给饼状图上的各个扇形区域添加语义节点(为了便于阅读,将节点区域改为矩形) List _buildSemantics(Size size) { final List nodes = []; final double height = size.height / data.length; for (int i = 0; i < data.length; i++) { final String percentage = '${(data[i].percentage * 100).toStringAsFixed(1)}%'; final CustomPainterSemantics node = CustomPainterSemantics( rect: Rect.fromLTRB( 0, height * i, size.width, height * i + height, ), properties: SemanticsProperties( sortKey: OrdinalSortKey(i.toDouble()), label: '$name${'$count件'}${data[i].name}占比$percentage', readOnly: true, textDirection: TextDirection.ltr, ), tags: const { SemanticsTag('pieChart-label'), }, ); nodes.add(node); } return nodes; } } ================================================ FILE: lib/widgets/pie_chart/pie_data.dart ================================================ import 'package:flutter/material.dart'; class PieData { /// 颜色 late Color color; /// 百分比 late num percentage; /// 数量 late int number; /// 名称 late String name; @override String toString() => 'name: $name, color: $color, ' 'number: $number, percentage: $percentage'; } ================================================ FILE: lib/widgets/popup_window.dart ================================================ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; ///create by elileo on 2018/12/21 ///https://github.com/elileo1/flutter_travel_friends/blob/master/lib/widget/PopupWindow.dart /// /// weilu update: /// 1.去除了IntrinsicWidth限制,添加了默认蒙版。 /// 2.简化position计算。 const Duration _kWindowDuration = Duration.zero; const double _kWindowCloseIntervalEnd = 2.0 / 3.0; const double _kWindowScreenPadding = 0.001; ///弹窗方法 Future showPopupWindow({ required BuildContext context, required RenderBox anchor, required Widget child, Offset? offset, String? semanticLabel, bool isShowBg = false, }) { switch (defaultTargetPlatform) { case TargetPlatform.iOS: case TargetPlatform.macOS: break; case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: semanticLabel ??= MaterialLocalizations.of(context).popupMenuLabel; } final RenderBox? overlay = Overlay.of(context).context.findRenderObject() as RenderBox?; // 默认位置锚点下方 final Offset defaultOffset = Offset(0, anchor.size.height); if (offset == null) { offset = defaultOffset; } else { offset = offset + defaultOffset; } // 获得控件左下方的坐标 final a = anchor.localToGlobal(offset, ancestor: overlay); // 获得控件右下方的坐标 final b = anchor.localToGlobal(anchor.size.bottomLeft(offset), ancestor: overlay); final RelativeRect position = RelativeRect.fromRect( Rect.fromPoints(a, b), Offset.zero & overlay!.size, ); return Navigator.push(context, _PopupWindowRoute( position: position, child: child, semanticLabel: semanticLabel, barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, isShowBg: isShowBg )); } ///自定义弹窗路由:参照_PopupMenuRoute修改的 class _PopupWindowRoute extends PopupRoute { _PopupWindowRoute({ super.settings, required this.child, required this.position, required this.barrierLabel, required this.semanticLabel, required this.isShowBg, }); final Widget child; final RelativeRect position; final String? semanticLabel; final bool isShowBg; @override Color? get barrierColor => null; @override bool get barrierDismissible => true; @override final String barrierLabel; @override Duration get transitionDuration => _kWindowDuration; @override Animation createAnimation() { return CurvedAnimation( parent: super.createAnimation(), curve: Curves.linear, reverseCurve: const Interval(0.0, _kWindowCloseIntervalEnd)); } @override Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) { final Widget win = _PopupWindow( route: this, semanticLabel: semanticLabel, ); return MediaQuery.removePadding( context: context, removeTop: true, removeBottom: true, removeLeft: true, removeRight: true, child: Builder( builder: (BuildContext context) { return GestureDetector( onTap: () => Navigator.pop(context), child: Material( type: MaterialType.transparency, child: Container( width: double.infinity, height: double.infinity, color: isShowBg ? const Color(0x99000000) : null, child: CustomSingleChildLayout( delegate: _PopupWindowLayoutDelegate( position, Directionality.of(context) ), child: win, ), ), ), ); }, ), ); } } ///自定义弹窗控件:对自定义的弹窗内容进行再包装,添加长宽、动画等约束条件 class _PopupWindow extends StatelessWidget { const _PopupWindow({ super.key, required this.route, required this.semanticLabel, }); final _PopupWindowRoute route; final String? semanticLabel; @override Widget build(BuildContext context) { const double length = 10.0; const double unit = 1.0 / (length + 1.5); // 1.0 for the width and 0.5 for the last item's fade. final CurveTween opacity = CurveTween(curve: const Interval(0.0, 1.0 / 3.0)); final CurveTween width = CurveTween(curve: const Interval(0.0, unit)); final CurveTween height = CurveTween(curve: const Interval(0.0, unit * length)); final Widget child = SingleChildScrollView( child: route.child, ); return AnimatedBuilder( animation: route.animation!, builder: (BuildContext context, Widget? child) { return Opacity( opacity: opacity.evaluate(route.animation!), child: Align( alignment: AlignmentDirectional.topEnd, widthFactor: width.evaluate(route.animation!), heightFactor: height.evaluate(route.animation!), child: Semantics( scopesRoute: true, namesRoute: true, explicitChildNodes: true, label: semanticLabel, child: child, ), ), ); }, child: child, ); } } ///自定义委托内容:子控件大小及其位置计算 class _PopupWindowLayoutDelegate extends SingleChildLayoutDelegate { _PopupWindowLayoutDelegate( this.position, this.textDirection); final RelativeRect position; final TextDirection textDirection; @override BoxConstraints getConstraintsForChild(BoxConstraints constraints) { // The menu can be at most the size of the overlay minus 8.0 pixels in each // direction. return BoxConstraints.loose(constraints.biggest - const Offset(_kWindowScreenPadding * 2.0, _kWindowScreenPadding * 2.0) as Size); } @override Offset getPositionForChild(Size size, Size childSize) { // size: The size of the overlay. // childSize: The size of the menu, when fully open, as determined by // getConstraintsForChild. // Find the ideal vertical position. double y = position.top; // Find the ideal horizontal position. double x; if (position.left > position.right) { // Menu button is closer to the right edge, so grow to the left, aligned to the right edge. x = size.width - position.right - childSize.width; } else if (position.left < position.right) { // Menu button is closer to the left edge, so grow to the right, aligned to the left edge. x = position.left; } else { // Menu button is equidistant from both edges, so grow in reading direction. switch (textDirection) { case TextDirection.rtl: x = size.width - position.right - childSize.width; break; case TextDirection.ltr: x = position.left; break; } } // Avoid going outside an area defined as the rectangle 8.0 pixels from the // edge of the screen in every direction. if (x < _kWindowScreenPadding) { x = _kWindowScreenPadding; } else if (x + childSize.width > size.width - _kWindowScreenPadding) { x = size.width - childSize.width - _kWindowScreenPadding; } if (y < _kWindowScreenPadding) { y = _kWindowScreenPadding; } else if (y + childSize.height > size.height - _kWindowScreenPadding) { y = size.height - childSize.height - _kWindowScreenPadding; } return Offset(x, y); } @override bool shouldRelayout(_PopupWindowLayoutDelegate oldDelegate) { return position != oldDelegate.position; } } ================================================ FILE: lib/widgets/progress_dialog.dart ================================================ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_deer/res/resources.dart'; /// 加载中的弹框 class ProgressDialog extends Dialog { const ProgressDialog({ super.key, this.hintText = '', }); final String hintText; @override Widget build(BuildContext context) { final Widget progress = Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const CupertinoActivityIndicator(radius: 14.0, color: Colors.grey,), Gaps.vGap8, Text(hintText, style: const TextStyle(color: Colors.white),) ], ); return Material( type: MaterialType.transparency, child: Center( child: Container( height: 88.0, width: 120.0, decoration: const ShapeDecoration( color: Color(0xFF3A3A3A), shape: RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(8.0)), ), ), child: progress, ), ), ); } } ================================================ FILE: lib/widgets/selected_image.dart ================================================ import 'dart:io'; import 'package:common_utils/common_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/util/device_utils.dart'; import 'package:flutter_deer/util/image_utils.dart'; import 'package:flutter_deer/util/theme_utils.dart'; import 'package:flutter_deer/util/toast_utils.dart'; import 'package:image_picker/image_picker.dart'; class SelectedImage extends StatefulWidget { const SelectedImage({ super.key, this.url, this.heroTag, this.size = 80.0, }); final String? url; final String? heroTag; final double size; @override SelectedImageState createState() => SelectedImageState(); } class SelectedImageState extends State { final ImagePicker _picker = ImagePicker(); ImageProvider? _imageProvider; XFile? pickedFile; Future _getImage() async { try { pickedFile = await _picker.pickImage(source: ImageSource.gallery, maxWidth: 800); if (pickedFile != null) { if (Device.isWeb) { _imageProvider = NetworkImage(pickedFile!.path); } else { _imageProvider = FileImage(File(pickedFile!.path)); } } else { _imageProvider = null; } setState(() { }); } catch (e) { if (e is MissingPluginException) { Toast.show('当前平台暂不支持!'); } else { Toast.show('没有权限,无法打开相册!'); } } } @override Widget build(BuildContext context) { final ColorFilter colorFilter = ColorFilter.mode( ThemeUtils.isDark(context) ? Colours.dark_unselected_item_color : Colours.text_gray, BlendMode.srcIn ); Widget image = Container( width: widget.size, height: widget.size, decoration: BoxDecoration( // 图片圆角展示 borderRadius: BorderRadius.circular(16.0), image: DecorationImage( image: _imageProvider ?? ImageUtils.getImageProvider(widget.url, holderImg: 'store/icon_zj'), fit: BoxFit.cover, colorFilter: _imageProvider == null && TextUtil.isEmpty(widget.url) ? colorFilter : null ), ), ); if (widget.heroTag != null && !Device.isWeb) { image = Hero(tag: widget.heroTag!, child: image); } return Semantics( label: '选择图片', hint: '跳转相册选择图片', child: InkWell( borderRadius: BorderRadius.circular(16.0), onTap: _getImage, child: image, ), ); } } ================================================ FILE: lib/widgets/selected_item.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/res/resources.dart'; class SelectedItem extends StatelessWidget { const SelectedItem({ super.key, this.onTap, required this.title, this.content = '', this.textAlign = TextAlign.start, this.style }); final GestureTapCallback? onTap; final String title; final String content; final TextAlign textAlign; final TextStyle? style; @override Widget build(BuildContext context) { return InkWell( onTap: onTap, child: Container( height: 50.0, margin: const EdgeInsets.only(right: 8.0, left: 16.0), width: double.infinity, decoration: BoxDecoration( border: Border( bottom: Divider.createBorderSide(context, width: 0.6), ), ), child: Row( children: [ Text(title), Gaps.hGap16, Expanded( child: Text( content, maxLines: 2, textAlign: textAlign, overflow: TextOverflow.ellipsis, style: style ), ), Gaps.hGap8, Images.arrowRight ], ), ), ); } } ================================================ FILE: lib/widgets/state_layout.dart ================================================ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/util/theme_utils.dart'; import 'package:flutter_deer/widgets/load_image.dart'; /// design/9暂无状态页面/index.html#artboard3 class StateLayout extends StatelessWidget { const StateLayout({ super.key, required this.type, this.hintText }); final StateType type; final String? hintText; @override Widget build(BuildContext context) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ if (type == StateType.loading) const CupertinoActivityIndicator(radius: 16.0) else if (type != StateType.empty) Opacity( opacity: context.isDark ? 0.5 : 1, child: LoadAssetImage( 'state/${type.img}', width: 120, ), ), const SizedBox(width: double.infinity, height: Dimens.gap_dp16,), Text( hintText ?? type.hintText, style: Theme.of(context).textTheme.titleSmall?.copyWith(fontSize: Dimens.font_sp14), ), Gaps.vGap50, ], ); } } enum StateType { /// 订单 order, /// 商品 goods, /// 无网络 network, /// 消息 message, /// 无提现账号 account, /// 加载中 loading, /// 空 empty } extension StateTypeExtension on StateType { String get img => [ 'zwdd', 'zwsp', 'zwwl', 'zwxx', 'zwzh', '', ''] [index]; String get hintText => [ '暂无订单', '暂无商品', '无网络连接', '暂无消息', '马上添加提现账号吧', '', '' ][index]; } ================================================ FILE: lib/widgets/text_field_item.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_deer/res/resources.dart'; import 'package:flutter_deer/util/input_formatter/number_text_input_formatter.dart'; /// 封装输入框 class TextFieldItem extends StatelessWidget { const TextFieldItem({ super.key, this.controller, required this.title, this.keyboardType = TextInputType.text, this.hintText = '', this.focusNode, }); final TextEditingController? controller; final String title; final String hintText; final TextInputType keyboardType; final FocusNode? focusNode; @override Widget build(BuildContext context) { final Row child = Row( children: [ Text(title), Gaps.hGap16, Expanded( child: Semantics( label: hintText.isEmpty ? '请输入$title' : hintText, child: TextField( focusNode: focusNode, keyboardType: keyboardType, inputFormatters: _getInputFormatters(), controller: controller, //style: TextStyles.textDark14, decoration: InputDecoration( hintText: hintText, border: InputBorder.none, //去掉下划线 //hintStyle: TextStyles.textGrayC14 ), ), ), ), Gaps.hGap16 ], ); return Container( height: 50.0, margin: const EdgeInsets.only(left: 16.0), width: double.infinity, decoration: BoxDecoration( border: Border( bottom: Divider.createBorderSide(context, width: 0.6), ), ), child: child, ); } List? _getInputFormatters() { if (keyboardType == const TextInputType.numberWithOptions(decimal: true)) { return [UsNumberTextInputFormatter()]; } if (keyboardType == TextInputType.number || keyboardType == TextInputType.phone) { return [FilteringTextInputFormatter.digitsOnly]; } return null; } } ================================================ FILE: macos/.gitignore ================================================ # Flutter-related **/Flutter/ephemeral/ **/Pods/ # Xcode-related **/dgph **/xcuserdata/ ================================================ FILE: macos/Flutter/Flutter-Debug.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" ================================================ FILE: macos/Flutter/Flutter-Release.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" ================================================ FILE: macos/Flutter/GeneratedPluginRegistrant.swift ================================================ // // Generated file. Do not edit. // import FlutterMacOS import Foundation import device_info_plus import file_selector_macos import path_provider_foundation import screen_retriever_macos import shared_preferences_foundation import sqflite_darwin import url_launcher_macos import webview_flutter_wkwebview import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin")) WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) } ================================================ FILE: macos/Podfile ================================================ platform :osx, '10.15' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' project 'Runner', { 'Debug' => :debug, 'Profile' => :release, 'Release' => :release, } def flutter_root generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) unless File.exist?(generated_xcode_build_settings_path) raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" end File.foreach(generated_xcode_build_settings_path) do |line| matches = line.match(/FLUTTER_ROOT\=(.*)/) return matches[1].strip if matches end raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" end require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) flutter_macos_podfile_setup target 'Runner' do use_frameworks! flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) target 'RunnerTests' do inherit! :search_paths end end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_macos_build_settings(target) target.build_configurations.each do |config| if config.build_settings['MACOSX_DEPLOYMENT_TARGET'].to_f < 10.9 config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '10.15' end # https://github.com/flutter/flutter/issues/94914 config.build_settings['ONLY_ACTIVE_ARCH'] = 'NO' end end end ================================================ FILE: macos/Runner/AppDelegate.swift ================================================ import Cocoa import FlutterMacOS @main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { return true } } ================================================ FILE: macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "size" : "16x16", "idiom" : "mac", "filename" : "app_icon_16.png", "scale" : "1x" }, { "size" : "16x16", "idiom" : "mac", "filename" : "app_icon_32.png", "scale" : "2x" }, { "size" : "32x32", "idiom" : "mac", "filename" : "app_icon_32.png", "scale" : "1x" }, { "size" : "32x32", "idiom" : "mac", "filename" : "app_icon_64.png", "scale" : "2x" }, { "size" : "128x128", "idiom" : "mac", "filename" : "app_icon_128.png", "scale" : "1x" }, { "size" : "128x128", "idiom" : "mac", "filename" : "app_icon_256.png", "scale" : "2x" }, { "size" : "256x256", "idiom" : "mac", "filename" : "app_icon_256.png", "scale" : "1x" }, { "size" : "256x256", "idiom" : "mac", "filename" : "app_icon_512.png", "scale" : "2x" }, { "size" : "512x512", "idiom" : "mac", "filename" : "app_icon_512.png", "scale" : "1x" }, { "size" : "512x512", "idiom" : "mac", "filename" : "app_icon_1024.png", "scale" : "2x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: macos/Runner/Base.lproj/MainMenu.xib ================================================ ================================================ FILE: macos/Runner/Configs/AppInfo.xcconfig ================================================ // Application-level settings for the Runner target. // // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the // future. If not, the values below would default to using the project name when this becomes a // 'flutter create' template. // The application's name. By default this is also the title of the Flutter window. PRODUCT_NAME = flutter_deer // The application's bundle identifier PRODUCT_BUNDLE_IDENTIFIER = com.weilu.flutterDeer // The copyright displayed in application information PRODUCT_COPYRIGHT = Copyright © 2025 com.weilu. All rights reserved. ================================================ FILE: macos/Runner/Configs/Debug.xcconfig ================================================ #include "../../Flutter/Flutter-Debug.xcconfig" #include "Warnings.xcconfig" ================================================ FILE: macos/Runner/Configs/Release.xcconfig ================================================ #include "../../Flutter/Flutter-Release.xcconfig" #include "Warnings.xcconfig" ================================================ FILE: macos/Runner/Configs/Warnings.xcconfig ================================================ WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings GCC_WARN_UNDECLARED_SELECTOR = YES CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE CLANG_WARN__DUPLICATE_METHOD_MATCH = YES CLANG_WARN_PRAGMA_PACK = YES CLANG_WARN_STRICT_PROTOTYPES = YES CLANG_WARN_COMMA = YES GCC_WARN_STRICT_SELECTOR_MATCH = YES CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES GCC_WARN_SHADOW = YES CLANG_WARN_UNREACHABLE_CODE = YES ================================================ FILE: macos/Runner/DebugProfile.entitlements ================================================ com.apple.security.app-sandbox com.apple.security.cs.allow-jit com.apple.security.network.server com.apple.security.network.client com.apple.security.files.user-selected.read-only ================================================ FILE: macos/Runner/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright $(PRODUCT_COPYRIGHT) NSMainNibFile MainMenu NSPrincipalClass NSApplication ================================================ FILE: macos/Runner/MainFlutterWindow.swift ================================================ import Cocoa import FlutterMacOS class MainFlutterWindow: NSWindow { override func awakeFromNib() { let flutterViewController = FlutterViewController() let windowFrame = self.frame self.contentViewController = flutterViewController self.setFrame(windowFrame, display: true) RegisterGeneratedPlugins(registry: flutterViewController) super.awakeFromNib() } } ================================================ FILE: macos/Runner/Release.entitlements ================================================ com.apple.security.app-sandbox com.apple.security.network.server com.apple.security.network.client com.apple.security.files.user-selected.read-only ================================================ FILE: macos/Runner.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { isa = PBXAggregateTarget; buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; buildPhases = ( 33CC111E2044C6BF0003C045 /* ShellScript */, ); dependencies = ( ); name = "Flutter Assemble"; productName = FLX; }; /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; 94D18224249FFFD282DBAF54 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 33EACC6A96021441DF721B4F /* Pods_RunnerTests.framework */; }; EB1002DD48F6C5D501E82863 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0E1A0A1DCBCBE19BC59DB44 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 33CC10E52044A3C60003C045 /* Project object */; proxyType = 1; remoteGlobalIDString = 33CC10EC2044A3C60003C045; remoteInfo = Runner; }; 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 33CC10E52044A3C60003C045 /* Project object */; proxyType = 1; remoteGlobalIDString = 33CC111A2044C6BA0003C045; remoteInfo = FLX; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ 33CC110E2044A8840003C045 /* Bundle Framework */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); name = "Bundle Framework"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 2982D5AFA86B8C8E5D6798B1 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; 33CC10ED2044A3C60003C045 /* flutter_deer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = flutter_deer.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; 33EACC6A96021441DF721B4F /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B95E46B745242AE5ECD61AF /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 85C524DFFFAAE6E5DF1F2BA0 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; ACE4782403BA557877997DAD /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; AF66F61280DE416811E629C0 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; C0E1A0A1DCBCBE19BC59DB44 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CBB31A81010853A79639CDA8 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 331C80D2294CF70F00263BE5 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 94D18224249FFFD282DBAF54 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 33CC10EA2044A3C60003C045 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( EB1002DD48F6C5D501E82863 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 331C80D6294CF71000263BE5 /* RunnerTests */ = { isa = PBXGroup; children = ( 331C80D7294CF71000263BE5 /* RunnerTests.swift */, ); path = RunnerTests; sourceTree = ""; }; 33BA886A226E78AF003329D5 /* Configs */ = { isa = PBXGroup; children = ( 33E5194F232828860026EE4D /* AppInfo.xcconfig */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, ); path = Configs; sourceTree = ""; }; 33CC10E42044A3C60003C045 = { isa = PBXGroup; children = ( 33FAB671232836740065AC1E /* Runner */, 33CEB47122A05771004F2AC0 /* Flutter */, 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, 6F6437355A43A892334BF9AB /* Pods */, ); sourceTree = ""; }; 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( 33CC10ED2044A3C60003C045 /* flutter_deer.app */, 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; }; 33CC11242044D66E0003C045 /* Resources */ = { isa = PBXGroup; children = ( 33CC10F22044A3C60003C045 /* Assets.xcassets */, 33CC10F42044A3C60003C045 /* MainMenu.xib */, 33CC10F72044A3C60003C045 /* Info.plist */, ); name = Resources; path = ..; sourceTree = ""; }; 33CEB47122A05771004F2AC0 /* Flutter */ = { isa = PBXGroup; children = ( 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, ); path = Flutter; sourceTree = ""; }; 33FAB671232836740065AC1E /* Runner */ = { isa = PBXGroup; children = ( 33CC10F02044A3C60003C045 /* AppDelegate.swift */, 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, 33E51913231747F40026EE4D /* DebugProfile.entitlements */, 33E51914231749380026EE4D /* Release.entitlements */, 33CC11242044D66E0003C045 /* Resources */, 33BA886A226E78AF003329D5 /* Configs */, ); path = Runner; sourceTree = ""; }; 6F6437355A43A892334BF9AB /* Pods */ = { isa = PBXGroup; children = ( AF66F61280DE416811E629C0 /* Pods-Runner.debug.xcconfig */, 2982D5AFA86B8C8E5D6798B1 /* Pods-Runner.release.xcconfig */, 3B95E46B745242AE5ECD61AF /* Pods-Runner.profile.xcconfig */, CBB31A81010853A79639CDA8 /* Pods-RunnerTests.debug.xcconfig */, ACE4782403BA557877997DAD /* Pods-RunnerTests.release.xcconfig */, 85C524DFFFAAE6E5DF1F2BA0 /* Pods-RunnerTests.profile.xcconfig */, ); name = Pods; path = Pods; sourceTree = ""; }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( C0E1A0A1DCBCBE19BC59DB44 /* Pods_Runner.framework */, 33EACC6A96021441DF721B4F /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 331C80D4294CF70F00263BE5 /* RunnerTests */ = { isa = PBXNativeTarget; buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( 57472F94D0F393DF2E27323C /* [CP] Check Pods Manifest.lock */, 331C80D1294CF70F00263BE5 /* Sources */, 331C80D2294CF70F00263BE5 /* Frameworks */, 331C80D3294CF70F00263BE5 /* Resources */, ); buildRules = ( ); dependencies = ( 331C80DA294CF71000263BE5 /* PBXTargetDependency */, ); name = RunnerTests; productName = RunnerTests; productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; 33CC10EC2044A3C60003C045 /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( C78B187BCE44972DE87A4057 /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, 33DA59D61687FF3F7A81A448 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); dependencies = ( 33CC11202044C79F0003C045 /* PBXTargetDependency */, ); name = Runner; productName = Runner; productReference = 33CC10ED2044A3C60003C045 /* flutter_deer.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 33CC10E52044A3C60003C045 /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 0920; LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 331C80D4294CF70F00263BE5 = { CreatedOnToolsVersion = 14.0; TestTargetID = 33CC10EC2044A3C60003C045; }; 33CC10EC2044A3C60003C045 = { CreatedOnToolsVersion = 9.2; LastSwiftMigration = 1100; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.Sandbox = { enabled = 1; }; }; }; 33CC111A2044C6BA0003C045 = { CreatedOnToolsVersion = 9.2; ProvisioningStyle = Manual; }; }; }; buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 33CC10E42044A3C60003C045; productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 33CC10EC2044A3C60003C045 /* Runner */, 331C80D4294CF70F00263BE5 /* RunnerTests */, 33CC111A2044C6BA0003C045 /* Flutter Assemble */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 331C80D3294CF70F00263BE5 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 33CC10EB2044A3C60003C045 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( ); outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; }; 33CC111E2044C6BF0003C045 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( Flutter/ephemeral/FlutterInputs.xcfilelist, ); inputPaths = ( Flutter/ephemeral/tripwire, ); outputFileListPaths = ( Flutter/ephemeral/FlutterOutputs.xcfilelist, ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; 33DA59D61687FF3F7A81A448 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; 57472F94D0F393DF2E27323C /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; C78B187BCE44972DE87A4057 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 331C80D1294CF70F00263BE5 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 33CC10E92044A3C60003C045 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 33CC10EC2044A3C60003C045 /* Runner */; targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; }; 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { isa = PBXVariantGroup; children = ( 33CC10F52044A3C60003C045 /* Base */, ); name = MainMenu.xib; path = Runner; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 331C80DB294CF71000263BE5 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = CBB31A81010853A79639CDA8 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.weilu.flutterDeer.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flutter_deer.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flutter_deer"; }; name = Debug; }; 331C80DC294CF71000263BE5 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = ACE4782403BA557877997DAD /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.weilu.flutterDeer.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flutter_deer.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flutter_deer"; }; name = Release; }; 331C80DD294CF71000263BE5 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 85C524DFFFAAE6E5DF1F2BA0 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.weilu.flutterDeer.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flutter_deer.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flutter_deer"; }; name = Profile; }; 338D0CE9231458BD00FA5F75 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = Profile; }; 338D0CEA231458BD00FA5F75 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; name = Profile; }; 338D0CEB231458BD00FA5F75 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Manual; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Profile; }; 33CC10F92044A3C60003C045 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; 33CC10FA2044A3C60003C045 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = Release; }; 33CC10FC2044A3C60003C045 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Debug; }; 33CC10FD2044A3C60003C045 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; name = Release; }; 33CC111C2044C6BA0003C045 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Manual; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; }; 33CC111D2044C6BA0003C045 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 331C80DB294CF71000263BE5 /* Debug */, 331C80DC294CF71000263BE5 /* Release */, 331C80DD294CF71000263BE5 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 33CC10F92044A3C60003C045 /* Debug */, 33CC10FA2044A3C60003C045 /* Release */, 338D0CE9231458BD00FA5F75 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 33CC10FC2044A3C60003C045 /* Debug */, 33CC10FD2044A3C60003C045 /* Release */, 338D0CEA231458BD00FA5F75 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { isa = XCConfigurationList; buildConfigurations = ( 33CC111C2044C6BA0003C045 /* Debug */, 33CC111D2044C6BA0003C045 /* Release */, 338D0CEB231458BD00FA5F75 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 33CC10E52044A3C60003C045 /* Project object */; } ================================================ FILE: macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme ================================================ ================================================ FILE: macos/Runner.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: pubspec.yaml ================================================ name: flutter_deer description: Flutter Deer version: 1.3.3+22 # 唯鹿 homepage: https://weilu.blog.csdn.net/ publish_to: 'none' environment: sdk: ">=3.4.0 <4.0.0" flutter: ">=3.19.0" dependencies: flutter: sdk: flutter flutter_localizations: sdk: flutter # 去除网页URL中的“#”(hash) https://flutter.cn/docs/development/ui/navigation/url-strategies url_strategy: 0.3.0 # Localization https://github.com/dart-lang/intl intl: ^0.20.2 # Toast插件 https://github.com/OpenFlutter/flutter_oktoast oktoast: ^3.4.0 # 网络库 https://github.com/cfug/dio dio: ^5.9.0 # https://github.com/ReactiveX/rxdart rxdart: ^0.28.0 # Dart 常用工具类库 https://github.com/Sky24n/common_utils common_utils: 2.1.0 sp_util: 2.0.3 # Flutter 常用工具类库 https://github.com/Sky24n/flustars flustars_flutter3: ^3.0.0 # flustars很久不维护,可以使用这个替代 # Flutter 轮播图 https://github.com/mdddj/flutter_swiper_null_safety flutter_swiper_null_safety_flutter3: ^4.0.3 # flutter_swiper很久不维护,可以使用这个替代 # 启动URL的插件(支持Web) https://github.com/flutter/packages/tree/main/packages/url_launcher url_launcher: 6.3.0 # 图片选择插件 https://github.com/flutter/packages/tree/main/packages/image_picker image_picker: 1.2.0 # 侧滑删除 https://github.com/letsar/flutter_slidable flutter_slidable: ^4.0.0 # WebView插件 https://github.com/flutter/packages/tree/main/packages/webview_flutter webview_flutter: 4.13.0 webview_flutter_wkwebview: 3.23.4 # 处理键盘事件 https://github.com/diegoveloper/flutter_keyboard_actions keyboard_actions: ^4.2.1 # 城市选择列表 https://github.com/flutterchina/azlistview azlistview: ^2.0.0 # 路由框架 https://github.com/theyakka/fluro fluro: ^2.0.5 # 图片缓存 https://github.com/renefloor/flutter_cached_network_image cached_network_image: ^3.4.0 # 格式化String https://github.com/Naddiseo/dart-sprintf sprintf: ^7.0.0 # 状态管理 https://github.com/rrousselGit/provider provider: ^6.1.2 # 扫码 https://github.com/juliuscanute/qr_code_scanner qr_code_scanner: git: url: 'https://github.com/NeverOvO/qr_code_scanner.git' ref: '3fe7b88' # App Shortcuts https://github.com/flutter/packages/tree/main/packages/quick_actions quick_actions: 1.1.0 # 振动 https://github.com/benjamindean/flutter_vibration vibration: 3.1.3 vibration_web: 1.6.8 # 获取当前设备信息 https://github.com/fluttercommunity/plus_plugins/tree/main/packages/device_info_plus device_info_plus: 11.5.0 # 桌面应用调整窗口的大小和位置 https://github.com/leanflutter/window_manager window_manager: 0.5.1 # 高德2D地图插件(支持Web) https://github.com/simplezhli/flutter_2d_amap flutter_2d_amap: git: ref: '2f9657e4' url: 'https://github.com/simplezhli/flutter_2d_amap.git' # demo 用到的库 # 刮刮卡 https://github.com/vintage/scratcher scratcher: ^2.5.0 # 动画效果 https://github.com/xvrh/lottie-flutter lottie: ^3.3.0 win32: 5.15.0 # https://github.com/simplezhli/flutter_deer/issues/187 dependency_overrides: scrollable_positioned_list: ^0.3.2 dev_dependencies: # Widget测试 flutter_test: sdk: flutter # 集成测试 flutter_driver: sdk: flutter integration_test: sdk: flutter # 黄金测试 # flutter_goldens: # sdk: flutter # 单元测试 test: ^1.16.8 # For information on the generic Dart part of this file, see the # following page: https:/w.dartlang.org/tools/pub/pubspec # The following section is specific to Flutter. flutter: generate: true # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true assets: - assets/data/ - assets/images/ - assets/images/home/ - assets/images/store/ - assets/images/state/ - assets/images/order/ - assets/images/order/dark/ - assets/images/login/ - assets/images/goods/ - assets/images/shop/ - assets/images/account/ - assets/images/statistic/ - assets/lottie/ # To add custom fonts to your application, add a fonts section here, # in this "flutter" section. Each entry in this list should have a # "family" key with the font family name, and a "fonts" key with a # list giving the asset and other descriptors for the font. For # example: fonts: - family: RobotoThin fonts: - asset: assets/fonts/Roboto-Thin.ttf # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages ================================================ FILE: shader/README.md ================================================ ## shader 详细使用方法见文档:https://flutter.cn/docs/perf/rendering/shader 本项目提供我捕获的`SkSL着色`json文件(Android),可在项目根目录执行: ``` flutter build apk --bundle-sksl-path shader/flutter_01.sksl.json ``` 经测试在低性能设备(华为平板C5 麒麟659)有明显效果。 ================================================ FILE: shader/flutter_01.sksl.json ================================================ {"platform":"android","name":"MIX 2S","engineRevision":"c8e3b9485386425213e2973126d6f57e7ed83c54","data":{"CAZAAAACBAAAAAAAAAACKAAAAAJQAAIA7777777777776EYAAEAP777777777777AAAAAABAABMAAAAAAAAAAAAAAABAAAAAGQAFKAA":"AgAAAExTS1NQAQAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0OwppbiBmbG9hdDIgcG9zaXRpb247CmluIGZsb2F0MiBsb2NhbENvb3JkOwpub3BlcnNwZWN0aXZlIG91dCBmbG9hdDIgdmxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZsb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAYQIAACNleHRlbnNpb24gR0xfTlZfc2hhZGVyX25vcGVyc3BlY3RpdmVfaW50ZXJwb2xhdGlvbjogcmVxdWlyZQp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTA7Cm5vcGVyc3BlY3RpdmUgaW4gZmxvYXQyIHZsb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCQlmbG9hdDIgdGV4Q29vcmQ7CgkJdGV4Q29vcmQgPSB2bG9jYWxDb29yZF9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTAsIHRleENvb3JkKSAqIG91dHB1dENvbG9yX1N0YWdlMCk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","CAZAAAECA4AAAAAAAAABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAGAAAAAAAAAAAAAAAEAAAAAYAAVIA":"AgAAAExTS1MMAgAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0OwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gZmxvYXQ0IGluQ2lyY2xlRWRnZTsKbm9wZXJzcGVjdGl2ZSBvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpub3BlcnNwZWN0aXZlIG91dCBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCgl2aW5DaXJjbGVFZGdlX1N0YWdlMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1N0YWdlMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAClAgAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCm5vcGVyc3BlY3RpdmUgaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpub3BlcnNwZWN0aXZlIGluIGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJCWZsb2F0NCBjaXJjbGVFZGdlOwoJCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1N0YWdlMDsKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgkJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCQloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgkJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChlZGdlQWxwaGEpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UBAAAAAAAAAA==","CAZAAAECA4AAAAAAAAABKAADAAKQAAYACUAAGAATAAAQAFIAAMABKAADAAOAAEIAEAAC6AAAAAAAAAAAAAAAEAAAAAYAAVIA":"AgAAAExTS1NrBAAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0OwppbiBmbG9hdDQgcmFkaWlfc2VsZWN0b3I7CmluIGZsb2F0NCBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzOwppbiBmbG9hdDQgc2tldzsKaW4gZmxvYXQyIHRyYW5zbGF0ZTsKaW4gZmxvYXQ0IHJhZGlpX3g7CmluIGZsb2F0NCByYWRpaV95OwppbiBoYWxmNCBjb2xvcjsKbm9wZXJzcGVjdGl2ZSBvdXQgaGFsZjQgdmNvbG9yX1N0YWdlMDsKbm9wZXJzcGVjdGl2ZSBvdXQgZmxvYXQ0IHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgcmFkaWk7CglyYWRpaS54ID0gZG90KHJhZGlpX3NlbGVjdG9yLCByYWRpaV94KTsKCXJhZGlpLnkgPSBkb3QocmFkaWlfc2VsZWN0b3IsIHJhZGlpX3kpOwoJYm9vbCBpc19hcmNfc2VjdGlvbiA9IChyYWRpaS54ID4gMCk7CglyYWRpaSA9IGFicyhyYWRpaSk7CglmbG9hdDIgdmVydGV4cG9zID0gY29ybmVyICsgcmFkaXVzX291dHNldCAqIHJhZGlpOwoJZmxvYXQyeDIgc2tld21hdHJpeCA9IGZsb2F0MngyKHNrZXcueHksIHNrZXcuencpOwoJZmxvYXQyIGRldmNvb3JkID0gdmVydGV4cG9zICogc2tld21hdHJpeCArIHRyYW5zbGF0ZTsKCWlmIChpc19hcmNfc2VjdGlvbikgCgl7CgkJdmFyY2Nvb3JkX1N0YWdlMC54eSA9IDEgLSBhYnMocmFkaXVzX291dHNldCk7CgkJZmxvYXQyeDIgZGVyaXZhdGl2ZXMgPSBpbnZlcnNlKHNrZXdtYXRyaXgpOwoJCXZhcmNjb29yZF9TdGFnZTAuencgPSBkZXJpdmF0aXZlcyAqICh2YXJjY29vcmRfU3RhZ2UwLnh5L3JhZGlpICogY29ybmVyICogMik7Cgl9CgllbHNlIAoJewoJCXZhcmNjb29yZF9TdGFnZTAgPSBmbG9hdDQoMCk7Cgl9Cglza19Qb3NpdGlvbiA9IGZsb2F0NChkZXZjb29yZC54ICwgZGV2Y29vcmQueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAHwCAAAjZXh0ZW5zaW9uIEdMX05WX3NoYWRlcl9ub3BlcnNwZWN0aXZlX2ludGVycG9sYXRpb246IHJlcXVpcmUKbm9wZXJzcGVjdGl2ZSBpbiBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpub3BlcnNwZWN0aXZlIGluIGZsb2F0NCB2YXJjY29vcmRfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7CgkJaWYgKGZsb2F0MigwKSAhPSB2YXJjY29vcmRfU3RhZ2UwLnh5KSAKCQl7CgkJCWZsb2F0IGZuID0gZG90KHZhcmNjb29yZF9TdGFnZTAueHksIHZhcmNjb29yZF9TdGFnZTAueHkpIC0gMTsKCQkJaWYgKGZuID4gMCkgCgkJCXsKCQkJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDApOwoJCQl9CgkJfQoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAHAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAAAQAAABza2V3CQAAAHRyYW5zbGF0ZQAAAAcAAAByYWRpaV94AAcAAAByYWRpaV95AAUAAABjb2xvcgAAAAEAAAAAAAAA","CAZAAAECA4AAAAIAAAABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAGAAAAAAAAAAAAAAAEAAAAAYAAVIA":"AgAAAExTS1MMAgAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0OwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gZmxvYXQ0IGluQ2lyY2xlRWRnZTsKbm9wZXJzcGVjdGl2ZSBvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpub3BlcnNwZWN0aXZlIG91dCBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCgl2aW5DaXJjbGVFZGdlX1N0YWdlMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1N0YWdlMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAA5AwAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCm5vcGVyc3BlY3RpdmUgaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpub3BlcnNwZWN0aXZlIGluIGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJCWZsb2F0NCBjaXJjbGVFZGdlOwoJCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1N0YWdlMDsKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgkJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCQloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgkJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCQloYWxmIGRpc3RhbmNlVG9Jbm5lckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqIChkIC0gY2lyY2xlRWRnZS53KSk7CgkJaGFsZiBpbm5lckFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb0lubmVyRWRnZSk7CgkJZWRnZUFscGhhICo9IGlubmVyQWxwaGE7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","CAZAAAACA4AAAAYAAAAAAAAAAAAQAAAACMAACAA4AAIQADYACQAAAAAAAAMAALAAAAAAAAAAAAAAAAQAAAACYACVAA":"AgAAAExTS1OHAwAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0Owp1bmlmb3JtIGZsb2F0MiB1QXRsYXNEaW1lbnNpb25zSW52X1N0YWdlMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIHVzaG9ydDIgaW5UZXh0dXJlQ29vcmRzOwpub3BlcnNwZWN0aXZlIG91dCBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IG91dCBpbnQgdlRleEluZGV4X1N0YWdlMDsKbm9wZXJzcGVjdGl2ZSBvdXQgZmxvYXQyIHZJbnRUZXh0dXJlQ29vcmRzX1N0YWdlMDsKbm9wZXJzcGVjdGl2ZSBvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEaXN0YW5jZUZpZWxkUGF0aAoJaW50MiBzaWduZWRDb29yZHMgPSBpbnQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7CglmbG9hdDIgdW5vcm1UZXhDb29yZHMgPSBmbG9hdDIoc2lnbmVkQ29vcmRzLngvMiwgc2lnbmVkQ29vcmRzLnkvMik7CglpbnQgdGV4SWR4ID0gMDsKCXZUZXh0dXJlQ29vcmRzX1N0YWdlMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzRGltZW5zaW9uc0ludl9TdGFnZTA7Cgl2VGV4SW5kZXhfU3RhZ2UwID0gdGV4SWR4OwoJdkludFRleHR1cmVDb29yZHNfU3RhZ2UwID0gdW5vcm1UZXhDb29yZHM7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCWZsb2F0MiBfdG1wXzFfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAHkDAAAjZXh0ZW5zaW9uIEdMX05WX3NoYWRlcl9ub3BlcnNwZWN0aXZlX2ludGVycG9sYXRpb246IHJlcXVpcmUKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwpub3BlcnNwZWN0aXZlIGluIGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TdGFnZTA7CmZsYXQgaW4gaW50IHZUZXhJbmRleF9TdGFnZTA7Cm5vcGVyc3BlY3RpdmUgaW4gZmxvYXQyIHZJbnRUZXh0dXJlQ29vcmRzX1N0YWdlMDsKbm9wZXJzcGVjdGl2ZSBpbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgRGlzdGFuY2VGaWVsZFBhdGgKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgkJZmxvYXQyIHV2ID0gdlRleHR1cmVDb29yZHNfU3RhZ2UwOwoJCWhhbGY0IHRleENvbG9yOwoJCXsKCQkJdGV4Q29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwLCB1dik7CgkJfQoJCWhhbGYgZGlzdGFuY2UgPSA3Ljk2ODc1Kih0ZXhDb2xvci5yIC0gMC41MDE5NjA3ODQzMSk7CgkJaGFsZiBhZndpZHRoOwoJCWFmd2lkdGggPSBhYnMoMC42NSpoYWxmKGRGZHkodkludFRleHR1cmVDb29yZHNfU3RhZ2UwLnkpKSk7CgkJaGFsZiB2YWwgPSBzbW9vdGhzdGVwKC1hZndpZHRoLCBhZndpZHRoLCBkaXN0YW5jZSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQodmFsKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAPAAAAaW5UZXh0dXJlQ29vcmRzAAEAAAAAAAAA","CAZAAAACAYAAAAAIAAABGAABAD7777777777777777776FAABUAAAAAAAAAAAAAAAIAAAABEABKQA":"AgAAAExTS1MhAQAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0OwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQoX3RtcF8wX2luUG9zaXRpb24ueCAsIF90bXBfMF9pblBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAAAAAAAAAAAAAAAAAJcBAAAjZXh0ZW5zaW9uIEdMX05WX3NoYWRlcl9ub3BlcnNwZWN0aXZlX2ludGVycG9sYXRpb246IHJlcXVpcmUKdW5pZm9ybSBoYWxmNCB1Q29sb3JfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHVDb2xvcl9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAABAAAACgAAAGluUG9zaXRpb24AAAEAAAAAAAAA","CAZAAAACAYAABAAIAAABGAABAD777777777777YZAAAAAFAABUAAAAAAAAAAAAAAAIAAAABEABKQA":"AgAAAExTS1OCAQAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0OwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZiBpbkNvdmVyYWdlOwpub3BlcnNwZWN0aXZlIG91dCBoYWxmIHZpbkNvdmVyYWdlX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJdmluQ292ZXJhZ2VfU3RhZ2UwID0gaW5Db3ZlcmFnZTsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAPcBAAAjZXh0ZW5zaW9uIEdMX05WX3NoYWRlcl9ub3BlcnNwZWN0aXZlX2ludGVycG9sYXRpb246IHJlcXVpcmUKdW5pZm9ybSBoYWxmNCB1Q29sb3JfU3RhZ2UwOwpub3BlcnNwZWN0aXZlIGluIGhhbGYgdmluQ292ZXJhZ2VfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIERlZmF1bHRHZW9tZXRyeVByb2Nlc3NvcgoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHVDb2xvcl9TdGFnZTA7CgkJaGFsZiBhbHBoYSA9IDEuMDsKCQlhbHBoYSA9IHZpbkNvdmVyYWdlX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChhbHBoYSk7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACgAAAGluUG9zaXRpb24AAAoAAABpbkNvdmVyYWdlAAABAAAAAAAAAA==","CAZAAAACAUAABEAAAAABGAABAAOAAAYABQAEAAAAAAAAAAAAAAAAEAAAAAOAAVIA":"AgAAAExTS1N8AQAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0OwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5IYWlyUXVhZEVkZ2U7Cm5vcGVyc3BlY3RpdmUgb3V0IGhhbGY0IHZIYWlyUXVhZEVkZ2VfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkCgl2SGFpclF1YWRFZGdlX1N0YWdlMCA9IGluSGFpclF1YWRFZGdlOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAACuAwAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gaGFsZjQgdUNvbG9yX1N0YWdlMDsKdW5pZm9ybSBoYWxmIHVDb3ZlcmFnZV9TdGFnZTA7Cm5vcGVyc3BlY3RpdmUgaW4gaGFsZjQgdkhhaXJRdWFkRWRnZV9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZAoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHVDb2xvcl9TdGFnZTA7CgkJaGFsZiBlZGdlQWxwaGE7CgkJaGFsZjIgZHV2ZHggPSBoYWxmMihkRmR4KHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaGFsZjIgZ0YgPSBoYWxmMigyLjAgKiB2SGFpclF1YWRFZGdlX1N0YWdlMC54ICogZHV2ZHgueCAtIGR1dmR4LnksICAgICAgICAgICAgICAgMi4wICogdkhhaXJRdWFkRWRnZV9TdGFnZTAueCAqIGR1dmR5LnggLSBkdXZkeS55KTsKCQllZGdlQWxwaGEgPSBoYWxmKHZIYWlyUXVhZEVkZ2VfU3RhZ2UwLnggKiB2SGFpclF1YWRFZGdlX1N0YWdlMC54IC0gdkhhaXJRdWFkRWRnZV9TdGFnZTAueSk7CgkJZWRnZUFscGhhID0gc3FydChlZGdlQWxwaGEgKiBlZGdlQWxwaGEgLyBkb3QoZ0YsIGdGKSk7CgkJZWRnZUFscGhhID0gbWF4KDEuMCAtIGVkZ2VBbHBoYSwgMC4wKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCh1Q292ZXJhZ2VfU3RhZ2UwICogZWRnZUFscGhhKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACgAAAGluUG9zaXRpb24AAA4AAABpbkhhaXJRdWFkRWRnZQAAAQAAAAAAAAA=","CAZAAAECAUAAAAAAAAABGAABAAOAAEIADQAAGAAQABLQAAAAAAAAAAAAAABAAAAAEAAFKAA":"AgAAAExTS1PHAQAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0OwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gaGFsZjQgaW5RdWFkRWRnZTsKbm9wZXJzcGVjdGl2ZSBvdXQgaGFsZjQgdlF1YWRFZGdlX1N0YWdlMDsKbm9wZXJzcGVjdGl2ZSBvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkRWRnZQoJdlF1YWRFZGdlX1N0YWdlMCA9IGluUXVhZEVkZ2U7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAA1wMAACNleHRlbnNpb24gR0xfTlZfc2hhZGVyX25vcGVyc3BlY3RpdmVfaW50ZXJwb2xhdGlvbjogcmVxdWlyZQpub3BlcnNwZWN0aXZlIGluIGhhbGY0IHZRdWFkRWRnZV9TdGFnZTA7Cm5vcGVyc3BlY3RpdmUgaW4gaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIFF1YWRFZGdlCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJCWhhbGYgZWRnZUFscGhhOwoJCWhhbGYyIGR1dmR4ID0gaGFsZjIoZEZkeCh2UXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaGFsZjIgZHV2ZHkgPSBoYWxmMihkRmR5KHZRdWFkRWRnZV9TdGFnZTAueHkpKTsKCQlpZiAodlF1YWRFZGdlX1N0YWdlMC56ID4gMC4wICYmIHZRdWFkRWRnZV9TdGFnZTAudyA+IDAuMCkgCgkJewoJCQllZGdlQWxwaGEgPSBtaW4obWluKHZRdWFkRWRnZV9TdGFnZTAueiwgdlF1YWRFZGdlX1N0YWdlMC53KSArIDAuNSwgMS4wKTsKCQl9CgkJZWxzZSAKCQl7CgkJCWhhbGYyIGdGID0gaGFsZjIoMi4wKnZRdWFkRWRnZV9TdGFnZTAueCpkdXZkeC54IC0gZHV2ZHgueSwgICAgICAgICAgICAgICAyLjAqdlF1YWRFZGdlX1N0YWdlMC54KmR1dmR5LnggLSBkdXZkeS55KTsKCQkJZWRnZUFscGhhID0gKHZRdWFkRWRnZV9TdGFnZTAueCp2UXVhZEVkZ2VfU3RhZ2UwLnggLSB2UXVhZEVkZ2VfU3RhZ2UwLnkpOwoJCQllZGdlQWxwaGEgPSBzYXR1cmF0ZSgwLjUgLSBlZGdlQWxwaGEgLyBsZW5ndGgoZ0YpKTsKCQl9CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IACgAAAGluUXVhZEVkZ2UAAAEAAAAAAAAA","CAZAAAACAYAABEAJAAABGAABAAOAAEIA777777YZAAAAAFAABUAAAAAAAAAAAAAAAIAAAABEABKQA":"AgAAAExTS1O7AQAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0OwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gaGFsZiBpbkNvdmVyYWdlOwpub3BlcnNwZWN0aXZlIG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEZWZhdWx0R2VvbWV0cnlQcm9jZXNzb3IKCWhhbGY0IGNvbG9yID0gaW5Db2xvcjsKCWNvbG9yID0gY29sb3IgKiBpbkNvdmVyYWdlOwoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAoAEAACNleHRlbnNpb24gR0xfTlZfc2hhZGVyX25vcGVyc3BlY3RpdmVfaW50ZXJwb2xhdGlvbjogcmVxdWlyZQpub3BlcnNwZWN0aXZlIGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgRGVmYXVsdEdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAKAAAAaW5Db3ZlcmFnZQAAAQAAAAAAAAA=","CAZAAAECAUAAAEYAAEABYAARAANQAAQAAAAAAAAMABCQAAAAAAAAAAAAAABAAAAAEAAFKAA":"AgAAAExTS1PeAQAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0OwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gaGFsZjMgaW5TaGFkb3dQYXJhbXM7Cm5vcGVyc3BlY3RpdmUgb3V0IGhhbGYzIHZpblNoYWRvd1BhcmFtc19TdGFnZTA7Cm5vcGVyc3BlY3RpdmUgb3V0IGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUlJlY3RTaGFkb3cKCXZpblNoYWRvd1BhcmFtc19TdGFnZTAgPSBpblNoYWRvd1BhcmFtczsKCXZpbkNvbG9yX1N0YWdlMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQoX3RtcF8wX2luUG9zaXRpb24ueCAsIF90bXBfMF9pblBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAAAAAAAAAAAAAAAAyQIAACNleHRlbnNpb24gR0xfTlZfc2hhZGVyX25vcGVyc3BlY3RpdmVfaW50ZXJwb2xhdGlvbjogcmVxdWlyZQp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTA7Cm5vcGVyc3BlY3RpdmUgaW4gaGFsZjMgdmluU2hhZG93UGFyYW1zX1N0YWdlMDsKbm9wZXJzcGVjdGl2ZSBpbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUlJlY3RTaGFkb3cKCQloYWxmMyBzaGFkb3dQYXJhbXM7CgkJc2hhZG93UGFyYW1zID0gdmluU2hhZG93UGFyYW1zX1N0YWdlMDsKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgkJaGFsZiBkID0gbGVuZ3RoKHNoYWRvd1BhcmFtcy54eSk7CgkJZmxvYXQyIHV2ID0gZmxvYXQyKHNoYWRvd1BhcmFtcy56ICogKDEuMCAtIGQpLCAwLjUpOwoJCWhhbGYgZmFjdG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMCwgdXYpLmE7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZmFjdG9yKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAOAAAAaW5TaGFkb3dQYXJhbXMAAAEAAAAAAAAA","CAZAAAECAYAAAAAAAAAACAAAAAJQAAIADQABCAAPAAKAAAAAAAABIAAYAAAAAAAAAAAAAAACAAAAAKAAKUAA":"AgAAAExTS1PcAgAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0Owp1bmlmb3JtIGZsb2F0MiB1QXRsYXNTaXplSW52X1N0YWdlMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIHVzaG9ydDIgaW5UZXh0dXJlQ29vcmRzOwpub3BlcnNwZWN0aXZlIG91dCBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IG91dCBpbnQgdlRleEluZGV4X1N0YWdlMDsKbm9wZXJzcGVjdGl2ZSBvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBUZXh0dXJlCglpbnQyIHNpZ25lZENvb3JkcyA9IGludDIoaW5UZXh0dXJlQ29vcmRzLngsIGluVGV4dHVyZUNvb3Jkcy55KTsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihzaWduZWRDb29yZHMueC8yLCBzaWduZWRDb29yZHMueS8yKTsKCWludCB0ZXhJZHggPSAwOwoJdlRleHR1cmVDb29yZHNfU3RhZ2UwID0gdW5vcm1UZXhDb29yZHMgKiB1QXRsYXNTaXplSW52X1N0YWdlMDsKCXZUZXhJbmRleF9TdGFnZTAgPSB0ZXhJZHg7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KGluUG9zaXRpb24ueCAsIGluUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAbQIAACNleHRlbnNpb24gR0xfTlZfc2hhZGVyX25vcGVyc3BlY3RpdmVfaW50ZXJwb2xhdGlvbjogcmVxdWlyZQp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTA7Cm5vcGVyc3BlY3RpdmUgaW4gZmxvYXQyIHZUZXh0dXJlQ29vcmRzX1N0YWdlMDsKZmxhdCBpbiBpbnQgdlRleEluZGV4X1N0YWdlMDsKbm9wZXJzcGVjdGl2ZSBpbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgVGV4dHVyZQoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZpbkNvbG9yX1N0YWdlMDsKCQloYWxmNCB0ZXhDb2xvcjsKCQl7CgkJCXRleENvbG9yID0gc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMCwgdlRleHR1cmVDb29yZHNfU3RhZ2UwKTsKCQl9CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gdGV4Q29sb3I7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADwAAAGluVGV4dHVyZUNvb3JkcwABAAAAAAAAAA==","CAZAAAMCBEAAAAIAAEABKAADAAKQAAYACUAAGAAVAABQAEYAAEABKAADAAKQAAYADQABCABEAAXQAAAAAAAHSAAAAAYAABAAAQAAAAAAAAAAAAQAAAAEAACVAA":"AgAAAExTS1MkCQAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0OwppbiBmbG9hdDQgcmFkaWlfc2VsZWN0b3I7CmluIGZsb2F0NCBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzOwppbiBmbG9hdDQgYWFfYmxvYXRfYW5kX2NvdmVyYWdlOwppbiBmbG9hdDQgc2tldzsKaW4gZmxvYXQyIHRyYW5zbGF0ZTsKaW4gZmxvYXQ0IHJhZGlpX3g7CmluIGZsb2F0NCByYWRpaV95OwppbiBoYWxmNCBjb2xvcjsKbm9wZXJzcGVjdGl2ZSBvdXQgaGFsZjQgdmNvbG9yX1N0YWdlMDsKbm9wZXJzcGVjdGl2ZSBvdXQgZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglpZiAoYW55KGdyZWF0ZXJUaGFuKGFhX2Jsb2F0cmFkaXVzLCBmbG9hdDIoMSkpKSkgCgl7CgkJY29ybmVyID0gbWF4KGFicyhjb3JuZXIpLCBhYV9ibG9hdHJhZGl1cykgKiBzaWduKGNvcm5lcik7CgkJY292ZXJhZ2UgLz0gbWF4KGFhX2Jsb2F0cmFkaXVzLngsIDEpICogbWF4KGFhX2Jsb2F0cmFkaXVzLnksIDEpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjI1KSkpIAoJewoJCXJhZGlpID0gYWFfYmxvYXRyYWRpdXM7CgkJcmFkaXVzX291dHNldCA9IGZsb29yKGFicyhyYWRpdXNfb3V0c2V0KSkgKiByYWRpdXNfb3V0c2V0OwoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24ueHkgKiBhYV9ibG9hdHJhZGl1czsKCWZsb2F0MiB2ZXJ0ZXhwb3MgPSBjb3JuZXIgKyByYWRpdXNfb3V0c2V0ICogcmFkaWkgKyBhYV9vdXRzZXQ7CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKDAsIGNvdmVyYWdlKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7Cgl9Cglza19Qb3NpdGlvbiA9IGZsb2F0NChkZXZjb29yZC54ICwgZGV2Y29vcmQueSwgMCwgMSk7Cn0KAAEBAAAAAAAAAQEAiwQAACNleHRlbnNpb24gR0xfTlZfc2hhZGVyX25vcGVyc3BlY3RpdmVfaW50ZXJwb2xhdGlvbjogcmVxdWlyZQp1bmlmb3JtIGZsb2F0NCB1aW5uZXJSZWN0X1N0YWdlMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxOwpub3BlcnNwZWN0aXZlIGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm5vcGVyc3BlY3RpdmUgaW4gZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgR3JGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfU3RhZ2UwLngsIHk9dmFyY2Nvb3JkX1N0YWdlMC55OwoJCWhhbGYgY292ZXJhZ2U7CgkJaWYgKDAgPT0geF9wbHVzXzEpIAoJCXsKCQkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJCX0KCQllbHNlIAoJCXsKCQkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCQlmbiA9IGZtYSh5LHksIGZuKTsKCQkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJCWhhbGYgZCA9IGhhbGYoZm4vZm53aWR0aCk7CgkJCWNvdmVyYWdlID0gY2xhbXAoLjUgLSBkLCAwLCAxKTsKCQl9CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoY292ZXJhZ2UpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBDaXJjdWxhclJSZWN0CgkJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1N0YWdlMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCQlmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfU3RhZ2UxLlJCOwoJCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TdGFnZTEueCAtIGxlbmd0aChkeHkpKSk7CgkJb3V0cHV0X1N0YWdlMSA9IG91dHB1dENvdmVyYWdlX1N0YWdlMCAqIGFscGhhOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dF9TdGFnZTE7Cgl9Cn0KAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAgAAAAOAAAAcmFkaWlfc2VsZWN0b3IAABkAAABjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzAAAAFQAAAGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZQAAAAQAAABza2V3CQAAAHRyYW5zbGF0ZQAAAAcAAAByYWRpaV94AAcAAAByYWRpaV95AAUAAABjb2xvcgAAAAEAAAAAAAAA","CAZAAAICBEAAAAAAAAAEOAAAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFQAAAAAAAA6IAAAACYAAEAACAAAAAAAAAAAACAAAAAPAAKUAA":"AgAAAExTS1M6AQAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0OwppbiBmbG9hdDIgcG9zaXRpb247CmluIGhhbGY0IGNvbG9yOwpub3BlcnNwZWN0aXZlIG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZjb2xvcl9TdGFnZTAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKfQoAAAABAQAAAAAAAAEBADADAAAjZXh0ZW5zaW9uIEdMX05WX3NoYWRlcl9ub3BlcnNwZWN0aXZlX2ludGVycG9sYXRpb246IHJlcXVpcmUKdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TdGFnZTE7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1N0YWdlMTsKbm9wZXJzcGVjdGl2ZSBpbiBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIENpcmN1bGFyUlJlY3QKCQlmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TdGFnZTEuUkI7CgkJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgkJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMS54IC0gbGVuZ3RoKGR4eSkpKTsKCQlvdXRwdXRfU3RhZ2UxID0gb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwICogYWxwaGE7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0X1N0YWdlMTsKCX0KfQoAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAQAAAAAAAAA=","CAZAAAICBEAAAAAAAAAEOAQAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFQAAAAAAAA6IAAAACYAAEAACAAAAAAAAAAAACAAAAAPAAKUAA":"AgAAAExTS1M6AQAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0OwppbiBmbG9hdDIgcG9zaXRpb247CmluIGhhbGY0IGNvbG9yOwpub3BlcnNwZWN0aXZlIG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZjb2xvcl9TdGFnZTAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKfQoAAAABAQAAAAAAAAEBADADAAAjZXh0ZW5zaW9uIEdMX05WX3NoYWRlcl9ub3BlcnNwZWN0aXZlX2ludGVycG9sYXRpb246IHJlcXVpcmUKdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TdGFnZTE7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1N0YWdlMTsKbm9wZXJzcGVjdGl2ZSBpbiBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIENpcmN1bGFyUlJlY3QKCQlmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TdGFnZTEuUkI7CgkJZmxvYXQyIGR4eSA9IG1heChtYXgoZHh5MCwgZHh5MSksIDAuMCk7CgkJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMS54IC0gbGVuZ3RoKGR4eSkpKTsKCQlvdXRwdXRfU3RhZ2UxID0gb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwICogYWxwaGE7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0X1N0YWdlMTsKCX0KfQoAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAAAQAAAAAAAAA=","CAZAAAACBAAAAAIAAEABKAADAAKQAAYACUAAGAAVAABQAEYAAEABKAADAAKQAAYADQABCABEAAXQAAAAAAAAAAAAAABAAAAAGQAFKAA":"AgAAAExTS1MkCQAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0OwppbiBmbG9hdDQgcmFkaWlfc2VsZWN0b3I7CmluIGZsb2F0NCBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzOwppbiBmbG9hdDQgYWFfYmxvYXRfYW5kX2NvdmVyYWdlOwppbiBmbG9hdDQgc2tldzsKaW4gZmxvYXQyIHRyYW5zbGF0ZTsKaW4gZmxvYXQ0IHJhZGlpX3g7CmluIGZsb2F0NCByYWRpaV95OwppbiBoYWxmNCBjb2xvcjsKbm9wZXJzcGVjdGl2ZSBvdXQgaGFsZjQgdmNvbG9yX1N0YWdlMDsKbm9wZXJzcGVjdGl2ZSBvdXQgZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglpZiAoYW55KGdyZWF0ZXJUaGFuKGFhX2Jsb2F0cmFkaXVzLCBmbG9hdDIoMSkpKSkgCgl7CgkJY29ybmVyID0gbWF4KGFicyhjb3JuZXIpLCBhYV9ibG9hdHJhZGl1cykgKiBzaWduKGNvcm5lcik7CgkJY292ZXJhZ2UgLz0gbWF4KGFhX2Jsb2F0cmFkaXVzLngsIDEpICogbWF4KGFhX2Jsb2F0cmFkaXVzLnksIDEpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjI1KSkpIAoJewoJCXJhZGlpID0gYWFfYmxvYXRyYWRpdXM7CgkJcmFkaXVzX291dHNldCA9IGZsb29yKGFicyhyYWRpdXNfb3V0c2V0KSkgKiByYWRpdXNfb3V0c2V0OwoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24ueHkgKiBhYV9ibG9hdHJhZGl1czsKCWZsb2F0MiB2ZXJ0ZXhwb3MgPSBjb3JuZXIgKyByYWRpdXNfb3V0c2V0ICogcmFkaWkgKyBhYV9vdXRzZXQ7CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKDAsIGNvdmVyYWdlKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7Cgl9Cglza19Qb3NpdGlvbiA9IGZsb2F0NChkZXZjb29yZC54ICwgZGV2Y29vcmQueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAAQMAACNleHRlbnNpb24gR0xfTlZfc2hhZGVyX25vcGVyc3BlY3RpdmVfaW50ZXJwb2xhdGlvbjogcmVxdWlyZQpub3BlcnNwZWN0aXZlIGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm5vcGVyc3BlY3RpdmUgaW4gZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgR3JGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfU3RhZ2UwLngsIHk9dmFyY2Nvb3JkX1N0YWdlMC55OwoJCWhhbGYgY292ZXJhZ2U7CgkJaWYgKDAgPT0geF9wbHVzXzEpIAoJCXsKCQkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJCX0KCQllbHNlIAoJCXsKCQkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCQlmbiA9IGZtYSh5LHksIGZuKTsKCQkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJCWhhbGYgZCA9IGhhbGYoZm4vZm53aWR0aCk7CgkJCWNvdmVyYWdlID0gY2xhbXAoLjUgLSBkLCAwLCAxKTsKCQl9CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoY292ZXJhZ2UpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAIAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAABUAAABhYV9ibG9hdF9hbmRfY292ZXJhZ2UAAAAEAAAAc2tldwkAAAB0cmFuc2xhdGUAAAAHAAAAcmFkaWlfeAAHAAAAcmFkaWlfeQAFAAAAY29sb3IAAAABAAAAAAAAAA==","CAZAAAACBAAAAAIAAAABKAADAAKQAAYACUAAGAAVAABQAEYAAEABKAADAAKQAAYADQABCABEAAXQAAAAAAAAAAAAAABAAAAAGQAFKAA":"AgAAAExTS1OOCQAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0OwppbiBmbG9hdDQgcmFkaWlfc2VsZWN0b3I7CmluIGZsb2F0NCBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzOwppbiBmbG9hdDQgYWFfYmxvYXRfYW5kX2NvdmVyYWdlOwppbiBmbG9hdDQgc2tldzsKaW4gZmxvYXQyIHRyYW5zbGF0ZTsKaW4gZmxvYXQ0IHJhZGlpX3g7CmluIGZsb2F0NCByYWRpaV95OwppbiBoYWxmNCBjb2xvcjsKbm9wZXJzcGVjdGl2ZSBvdXQgaGFsZjQgdmNvbG9yX1N0YWdlMDsKbm9wZXJzcGVjdGl2ZSBvdXQgZmxvYXQ0IHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglpZiAoYW55KGdyZWF0ZXJUaGFuKGFhX2Jsb2F0cmFkaXVzLCBmbG9hdDIoMSkpKSkgCgl7CgkJY29ybmVyID0gbWF4KGFicyhjb3JuZXIpLCBhYV9ibG9hdHJhZGl1cykgKiBzaWduKGNvcm5lcik7CgkJY292ZXJhZ2UgLz0gbWF4KGFhX2Jsb2F0cmFkaXVzLngsIDEpICogbWF4KGFhX2Jsb2F0cmFkaXVzLnksIDEpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjI1KSkpIAoJewoJCXJhZGlpID0gYWFfYmxvYXRyYWRpdXM7CgkJcmFkaXVzX291dHNldCA9IGZsb29yKGFicyhyYWRpdXNfb3V0c2V0KSkgKiByYWRpdXNfb3V0c2V0OwoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24ueHkgKiBhYV9ibG9hdHJhZGl1czsKCWZsb2F0MiB2ZXJ0ZXhwb3MgPSBjb3JuZXIgKyByYWRpdXNfb3V0c2V0ICogcmFkaWkgKyBhYV9vdXRzZXQ7CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKDAsIGNvdmVyYWdlKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7CgkJZmxvYXQyeDIgZGVyaXZhdGl2ZXMgPSBpbnZlcnNlKHNrZXdtYXRyaXgpOwoJCXZhcmNjb29yZF9TdGFnZTAuencgPSBkZXJpdmF0aXZlcyAqIChhcmNjb29yZC9yYWRpaSAqIDIpOwoJfQoJc2tfUG9zaXRpb24gPSBmbG9hdDQoZGV2Y29vcmQueCAsIGRldmNvb3JkLnksIDAsIDEpOwp9CgAAAAAAAAAAAAAAAAAAPwMAACNleHRlbnNpb24gR0xfTlZfc2hhZGVyX25vcGVyc3BlY3RpdmVfaW50ZXJwb2xhdGlvbjogcmVxdWlyZQpub3BlcnNwZWN0aXZlIGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm5vcGVyc3BlY3RpdmUgaW4gZmxvYXQ0IHZhcmNjb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgR3JGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfU3RhZ2UwLngsIHk9dmFyY2Nvb3JkX1N0YWdlMC55OwoJCWhhbGYgY292ZXJhZ2U7CgkJaWYgKDAgPT0geF9wbHVzXzEpIAoJCXsKCQkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJCX0KCQllbHNlIAoJCXsKCQkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCQlmbiA9IGZtYSh5LHksIGZuKTsKCQkJZmxvYXQgZ3g9dmFyY2Nvb3JkX1N0YWdlMC56LCBneT12YXJjY29vcmRfU3RhZ2UwLnc7CgkJCWZsb2F0IGZud2lkdGggPSBhYnMoZ3gpICsgYWJzKGd5KTsKCQkJaGFsZiBkID0gaGFsZihmbi9mbndpZHRoKTsKCQkJY292ZXJhZ2UgPSBjbGFtcCguNSAtIGQsIDAsIDEpOwoJCX0KCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChjb3ZlcmFnZSk7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAIAAAADgAAAHJhZGlpX3NlbGVjdG9yAAAZAAAAY29ybmVyX2FuZF9yYWRpdXNfb3V0c2V0cwAAABUAAABhYV9ibG9hdF9hbmRfY292ZXJhZ2UAAAAEAAAAc2tldwkAAAB0cmFuc2xhdGUAAAAHAAAAcmFkaWlfeAAHAAAAcmFkaWlfeQAFAAAAY29sb3IAAAABAAAAAAAAAA==","CAZAAAECA4AAAAAAAAAEOAQAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFQAAAAAAAAAAAAAAAEAAAAAYAAVIA":"AgAAAExTS1M6AQAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0OwppbiBmbG9hdDIgcG9zaXRpb247CmluIGhhbGY0IGNvbG9yOwpub3BlcnNwZWN0aXZlIG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZjb2xvcl9TdGFnZTAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAKYBAAAjZXh0ZW5zaW9uIEdMX05WX3NoYWRlcl9ub3BlcnNwZWN0aXZlX2ludGVycG9sYXRpb246IHJlcXVpcmUKbm9wZXJzcGVjdGl2ZSBpbiBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAEAAAAAAAAA","CAZAAAACBAAAAHYBAEACKAAAAAJQAAIA7777777777776EYAAEAP777777777777AAAAAABAABMAAAAAAAAAAAAAAABAAAAAGQAFKAA":"AgAAAExTS1NQAQAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0OwppbiBmbG9hdDIgcG9zaXRpb247CmluIGZsb2F0MiBsb2NhbENvb3JkOwpub3BlcnNwZWN0aXZlIG91dCBmbG9hdDIgdmxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZsb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAYQcAACNleHRlbnNpb24gR0xfTlZfc2hhZGVyX25vcGVyc3BlY3RpdmVfaW50ZXJwb2xhdGlvbjogcmVxdWlyZQp1bmlmb3JtIGhhbGYgdVNyY1RGX1N0YWdlMFs3XTsKdW5pZm9ybSBoYWxmM3gzIHVDb2xvclhmb3JtX1N0YWdlMDsKdW5pZm9ybSBoYWxmIHVEc3RURl9TdGFnZTBbN107CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMDsKbm9wZXJzcGVjdGl2ZSBpbiBmbG9hdDIgdmxvY2FsQ29vcmRfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmIHNyY190Zl9TdGFnZTAoaGFsZiB4KSAKewoJaGFsZiBHID0gdVNyY1RGX1N0YWdlMFswXTsKCWhhbGYgQSA9IHVTcmNURl9TdGFnZTBbMV07CgloYWxmIEIgPSB1U3JjVEZfU3RhZ2UwWzJdOwoJaGFsZiBDID0gdVNyY1RGX1N0YWdlMFszXTsKCWhhbGYgRCA9IHVTcmNURl9TdGFnZTBbNF07CgloYWxmIEUgPSB1U3JjVEZfU3RhZ2UwWzVdOwoJaGFsZiBGID0gdVNyY1RGX1N0YWdlMFs2XTsKCWhhbGYgcyA9IHNpZ24oeCk7Cgl4ID0gYWJzKHgpOwoJeCA9ICh4IDwgRCkgPyAoQyAqIHgpICsgRiA6IHBvdyhBICogeCArIEIsIEcpICsgRTsKCXJldHVybiBzICogeDsKfQpoYWxmIGRzdF90Zl9TdGFnZTAoaGFsZiB4KSAKewoJaGFsZiBHID0gdURzdFRGX1N0YWdlMFswXTsKCWhhbGYgQSA9IHVEc3RURl9TdGFnZTBbMV07CgloYWxmIEIgPSB1RHN0VEZfU3RhZ2UwWzJdOwoJaGFsZiBDID0gdURzdFRGX1N0YWdlMFszXTsKCWhhbGYgRCA9IHVEc3RURl9TdGFnZTBbNF07CgloYWxmIEUgPSB1RHN0VEZfU3RhZ2UwWzVdOwoJaGFsZiBGID0gdURzdFRGX1N0YWdlMFs2XTsKCWhhbGYgcyA9IHNpZ24oeCk7Cgl4ID0gYWJzKHgpOwoJeCA9ICh4IDwgRCkgPyAoQyAqIHgpICsgRiA6IHBvdyhBICogeCArIEIsIEcpICsgRTsKCXJldHVybiBzICogeDsKfQpoYWxmNCBnYW11dF94Zm9ybV9TdGFnZTAoaGFsZjQgY29sb3IpIAp7Cgljb2xvci5yZ2IgPSAodUNvbG9yWGZvcm1fU3RhZ2UwICogY29sb3IucmdiKTsKCXJldHVybiBjb2xvcjsKfQpoYWxmNCBjb2xvcl94Zm9ybV9TdGFnZTAoaGFsZjQgY29sb3IpIAp7Cgljb2xvciA9IHVucHJlbXVsKGNvbG9yKTsKCWNvbG9yLnIgPSBzcmNfdGZfU3RhZ2UwKGhhbGYoY29sb3IucikpOwoJY29sb3IuZyA9IHNyY190Zl9TdGFnZTAoaGFsZihjb2xvci5nKSk7Cgljb2xvci5iID0gc3JjX3RmX1N0YWdlMChoYWxmKGNvbG9yLmIpKTsKCWNvbG9yID0gZ2FtdXRfeGZvcm1fU3RhZ2UwKGhhbGY0KGNvbG9yKSk7Cgljb2xvci5yID0gZHN0X3RmX1N0YWdlMChoYWxmKGNvbG9yLnIpKTsKCWNvbG9yLmcgPSBkc3RfdGZfU3RhZ2UwKGhhbGYoY29sb3IuZykpOwoJY29sb3IuYiA9IGRzdF90Zl9TdGFnZTAoaGFsZihjb2xvci5iKSk7Cgljb2xvci5yZ2IgKj0gY29sb3IuYTsKCXJldHVybiBoYWxmNChjb2xvcik7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCQlmbG9hdDIgdGV4Q29vcmQ7CgkJdGV4Q29vcmQgPSB2bG9jYWxDb29yZF9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gKGNvbG9yX3hmb3JtX1N0YWdlMChzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwLCB0ZXhDb29yZCkpICogb3V0cHV0Q29sb3JfU3RhZ2UwKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","CAZAAAECA4AAAAAAAEABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAGAAAAAAAAAAAAAAAEAAAAAYAAVIA":"AgAAAExTS1NiAgAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0Owp1bmlmb3JtIGZsb2F0NCB1bG9jYWxNYXRyaXhfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gZmxvYXQ0IGluQ2lyY2xlRWRnZTsKbm9wZXJzcGVjdGl2ZSBvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpub3BlcnNwZWN0aXZlIG91dCBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCgl2aW5DaXJjbGVFZGdlX1N0YWdlMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1N0YWdlMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gdWxvY2FsTWF0cml4X1N0YWdlMC54eiAqIGluUG9zaXRpb24gKyB1bG9jYWxNYXRyaXhfU3RhZ2UwLnl3OwoJc2tfUG9zaXRpb24gPSBmbG9hdDQoX3RtcF8wX2luUG9zaXRpb24ueCAsIF90bXBfMF9pblBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAAAAAAAAAAAAAAAApQIAACNleHRlbnNpb24gR0xfTlZfc2hhZGVyX25vcGVyc3BlY3RpdmVfaW50ZXJwb2xhdGlvbjogcmVxdWlyZQpub3BlcnNwZWN0aXZlIGluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1N0YWdlMDsKbm9wZXJzcGVjdGl2ZSBpbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCQlmbG9hdDQgY2lyY2xlRWRnZTsKCQljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgkJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","CAZAAAACBAAAAAAAAAAGKAAAAAJQAAIA777777Y4AAIQAEYAAEAP777777777777AAAAAABAABMAAAAAAAAAAAAAAABAAAAAGQAFKAA":"AgAAAExTS1OfAQAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0OwppbiBmbG9hdDIgcG9zaXRpb247CmluIGhhbGY0IGNvbG9yOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKbm9wZXJzcGVjdGl2ZSBvdXQgaGFsZjQgdmNvbG9yX1N0YWdlMDsKbm9wZXJzcGVjdGl2ZSBvdXQgZmxvYXQyIHZsb2NhbENvb3JkX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7Cgl2bG9jYWxDb29yZF9TdGFnZTAgPSBsb2NhbENvb3JkOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQocG9zaXRpb24ueCAsIHBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAAAAAAAAAAAAAACMAgAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMDsKbm9wZXJzcGVjdGl2ZSBpbiBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpub3BlcnNwZWN0aXZlIGluIGZsb2F0MiB2bG9jYWxDb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdDIgdGV4Q29vcmQ7CgkJdGV4Q29vcmQgPSB2bG9jYWxDb29yZF9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTAsIHRleENvb3JkKSAqIG91dHB1dENvbG9yX1N0YWdlMCk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAMAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAAKAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","CAZAAAMCBMAAAAIAAEABKAADAAKQAAYACUAAGAAVAABQAEYAAEABKAADAAKQAAYADQABCABEAAXQAAAAAAAHSAAAAAYAABAAAQAAAAABAAAAA6IAAAAEAAAVAACAAAAAAAAAAAACAAAAAUAAKUAA":"AgAAAExTS1MkCQAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0OwppbiBmbG9hdDQgcmFkaWlfc2VsZWN0b3I7CmluIGZsb2F0NCBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzOwppbiBmbG9hdDQgYWFfYmxvYXRfYW5kX2NvdmVyYWdlOwppbiBmbG9hdDQgc2tldzsKaW4gZmxvYXQyIHRyYW5zbGF0ZTsKaW4gZmxvYXQ0IHJhZGlpX3g7CmluIGZsb2F0NCByYWRpaV95OwppbiBoYWxmNCBjb2xvcjsKbm9wZXJzcGVjdGl2ZSBvdXQgaGFsZjQgdmNvbG9yX1N0YWdlMDsKbm9wZXJzcGVjdGl2ZSBvdXQgZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglpZiAoYW55KGdyZWF0ZXJUaGFuKGFhX2Jsb2F0cmFkaXVzLCBmbG9hdDIoMSkpKSkgCgl7CgkJY29ybmVyID0gbWF4KGFicyhjb3JuZXIpLCBhYV9ibG9hdHJhZGl1cykgKiBzaWduKGNvcm5lcik7CgkJY292ZXJhZ2UgLz0gbWF4KGFhX2Jsb2F0cmFkaXVzLngsIDEpICogbWF4KGFhX2Jsb2F0cmFkaXVzLnksIDEpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjI1KSkpIAoJewoJCXJhZGlpID0gYWFfYmxvYXRyYWRpdXM7CgkJcmFkaXVzX291dHNldCA9IGZsb29yKGFicyhyYWRpdXNfb3V0c2V0KSkgKiByYWRpdXNfb3V0c2V0OwoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24ueHkgKiBhYV9ibG9hdHJhZGl1czsKCWZsb2F0MiB2ZXJ0ZXhwb3MgPSBjb3JuZXIgKyByYWRpdXNfb3V0c2V0ICogcmFkaWkgKyBhYV9vdXRzZXQ7CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKDAsIGNvdmVyYWdlKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7Cgl9Cglza19Qb3NpdGlvbiA9IGZsb2F0NChkZXZjb29yZC54ICwgZGV2Y29vcmQueSwgMCwgMSk7Cn0KAAEBAAAAAAAAAQEAJAgAACNleHRlbnNpb24gR0xfTlZfc2hhZGVyX25vcGVyc3BlY3RpdmVfaW50ZXJwb2xhdGlvbjogcmVxdWlyZQp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fU3RhZ2UxOwp1bmlmb3JtIGZsb2F0NCB1aW5uZXJSZWN0X1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxX2MwOwpub3BlcnNwZWN0aXZlIGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm5vcGVyc3BlY3RpdmUgaW4gZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxX2MwLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMV9jMC5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMV9jMC54IC0gbGVuZ3RoKGR4eSkpKTsKCV9vdXRwdXQgPSBfaW5wdXQgKiBhbHBoYTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgR3JGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfU3RhZ2UwLngsIHk9dmFyY2Nvb3JkX1N0YWdlMC55OwoJCWhhbGYgY292ZXJhZ2U7CgkJaWYgKDAgPT0geF9wbHVzXzEpIAoJCXsKCQkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJCX0KCQllbHNlIAoJCXsKCQkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCQlmbiA9IGZtYSh5LHksIGZuKTsKCQkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJCWhhbGYgZCA9IGhhbGYoZm4vZm53aWR0aCk7CgkJCWNvdmVyYWdlID0gY2xhbXAoLjUgLSBkLCAwLCAxKTsKCQl9CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoY292ZXJhZ2UpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBBQVJlY3RFZmZlY3QKCQlmbG9hdDQgcHJldlJlY3QgPSBmbG9hdDQoLTEuMDAwMDAwLCAtMS4wMDAwMDAsIC0xLjAwMDAwMCwgLTEuMDAwMDAwKTsKCQloYWxmIGFscGhhOwoJCUBzd2l0Y2ggKDEpIAoJCXsKCQkJY2FzZSAwOiAgICBjYXNlIDI6ICAgICAgICBhbHBoYSA9IGhhbGYoYWxsKGdyZWF0ZXJUaGFuKGZsb2F0NChza19GcmFnQ29vcmQueHksIHVyZWN0VW5pZm9ybV9TdGFnZTEuencpLCBmbG9hdDQodXJlY3RVbmlmb3JtX1N0YWdlMS54eSwgc2tfRnJhZ0Nvb3JkLnh5KSkpID8gMSA6IDApOwoJCQlicmVhazsKCQkJZGVmYXVsdDogICAgICAgIGhhbGYgeFN1YiwgeVN1YjsKCQkJeFN1YiA9IG1pbihoYWxmKHNrX0ZyYWdDb29yZC54IC0gdXJlY3RVbmlmb3JtX1N0YWdlMS54KSwgMC4wKTsKCQkJeFN1YiArPSBtaW4oaGFsZih1cmVjdFVuaWZvcm1fU3RhZ2UxLnogLSBza19GcmFnQ29vcmQueCksIDAuMCk7CgkJCXlTdWIgPSBtaW4oaGFsZihza19GcmFnQ29vcmQueSAtIHVyZWN0VW5pZm9ybV9TdGFnZTEueSksIDAuMCk7CgkJCXlTdWIgKz0gbWluKGhhbGYodXJlY3RVbmlmb3JtX1N0YWdlMS53IC0gc2tfRnJhZ0Nvb3JkLnkpLCAwLjApOwoJCQlhbHBoYSA9ICgxLjAgKyBtYXgoeFN1YiwgLTEuMCkpICogKDEuMCArIG1heCh5U3ViLCAtMS4wKSk7CgkJfQoJCUBpZiAoMSA9PSAyIHx8IDEgPT0gMykgCgkJewoJCQlhbHBoYSA9IDEuMCAtIGFscGhhOwoJCX0KCQloYWxmNCBpbnB1dENvbG9yID0gQ2lyY3VsYXJSUmVjdF9TdGFnZTFfYzAob3V0cHV0Q292ZXJhZ2VfU3RhZ2UwKTsKCQlvdXRwdXRfU3RhZ2UxID0gaW5wdXRDb2xvciAqIGFscGhhOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dF9TdGFnZTE7Cgl9Cn0KAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAACAAAAA4AAAByYWRpaV9zZWxlY3RvcgAAGQAAAGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMAAAAVAAAAYWFfYmxvYXRfYW5kX2NvdmVyYWdlAAAABAAAAHNrZXcJAAAAdHJhbnNsYXRlAAAABwAAAHJhZGlpX3gABwAAAHJhZGlpX3kABQAAAGNvbG9yAAAAAQAAAAAAAAA=","CAZAAAICBMAAAAAAAEABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAGAAAAAAAA6IAAAACYAAEAACAAAAAAEAAAADZAAAAAPAACUAAIAAAAAAAAAAAAIAAAACMABKQA":"AgAAAExTS1NiAgAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0Owp1bmlmb3JtIGZsb2F0NCB1bG9jYWxNYXRyaXhfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gZmxvYXQ0IGluQ2lyY2xlRWRnZTsKbm9wZXJzcGVjdGl2ZSBvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpub3BlcnNwZWN0aXZlIG91dCBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCgl2aW5DaXJjbGVFZGdlX1N0YWdlMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1N0YWdlMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gdWxvY2FsTWF0cml4X1N0YWdlMC54eiAqIGluUG9zaXRpb24gKyB1bG9jYWxNYXRyaXhfU3RhZ2UwLnl3OwoJc2tfUG9zaXRpb24gPSBmbG9hdDQoX3RtcF8wX2luUG9zaXRpb24ueCAsIF90bXBfMF9pblBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAAEBAAAAAAAAAQEAyAcAACNleHRlbnNpb24gR0xfTlZfc2hhZGVyX25vcGVyc3BlY3RpdmVfaW50ZXJwb2xhdGlvbjogcmVxdWlyZQp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fU3RhZ2UxOwp1bmlmb3JtIGZsb2F0NCB1aW5uZXJSZWN0X1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxX2MwOwpub3BlcnNwZWN0aXZlIGluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1N0YWdlMDsKbm9wZXJzcGVjdGl2ZSBpbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgZHh5MCA9IHVpbm5lclJlY3RfU3RhZ2UxX2MwLkxUIC0gc2tfRnJhZ0Nvb3JkLnh5OwoJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMV9jMC5SQjsKCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJaGFsZiBhbHBoYSA9IGhhbGYoc2F0dXJhdGUodXJhZGl1c1BsdXNIYWxmX1N0YWdlMV9jMC54IC0gbGVuZ3RoKGR4eSkpKTsKCV9vdXRwdXQgPSBfaW5wdXQgKiBhbHBoYTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCQlmbG9hdDQgY2lyY2xlRWRnZTsKCQljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgkJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgQUFSZWN0RWZmZWN0CgkJZmxvYXQ0IHByZXZSZWN0ID0gZmxvYXQ0KC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDAsIC0xLjAwMDAwMCk7CgkJaGFsZiBhbHBoYTsKCQlAc3dpdGNoICgxKSAKCQl7CgkJCWNhc2UgMDogICAgY2FzZSAyOiAgICAgICAgYWxwaGEgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fU3RhZ2UxLnp3KSwgZmxvYXQ0KHVyZWN0VW5pZm9ybV9TdGFnZTEueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCQkJYnJlYWs7CgkJCWRlZmF1bHQ6ICAgICAgICBoYWxmIHhTdWIsIHlTdWI7CgkJCXhTdWIgPSBtaW4oaGFsZihza19GcmFnQ29vcmQueCAtIHVyZWN0VW5pZm9ybV9TdGFnZTEueCksIDAuMCk7CgkJCXhTdWIgKz0gbWluKGhhbGYodXJlY3RVbmlmb3JtX1N0YWdlMS56IC0gc2tfRnJhZ0Nvb3JkLngpLCAwLjApOwoJCQl5U3ViID0gbWluKGhhbGYoc2tfRnJhZ0Nvb3JkLnkgLSB1cmVjdFVuaWZvcm1fU3RhZ2UxLnkpLCAwLjApOwoJCQl5U3ViICs9IG1pbihoYWxmKHVyZWN0VW5pZm9ybV9TdGFnZTEudyAtIHNrX0ZyYWdDb29yZC55KSwgMC4wKTsKCQkJYWxwaGEgPSAoMS4wICsgbWF4KHhTdWIsIC0xLjApKSAqICgxLjAgKyBtYXgoeVN1YiwgLTEuMCkpOwoJCX0KCQlAaWYgKDEgPT0gMiB8fCAxID09IDMpIAoJCXsKCQkJYWxwaGEgPSAxLjAgLSBhbHBoYTsKCQl9CgkJaGFsZjQgaW5wdXRDb2xvciA9IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKG91dHB1dENvdmVyYWdlX1N0YWdlMCk7CgkJb3V0cHV0X1N0YWdlMSA9IGlucHV0Q29sb3IgKiBhbHBoYTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","CAZAAAICBEAAAAAAAEABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAGAAAAAAAA6IAAAACYAAEAACAAAAAAAAAAAACAAAAAPAAKUAA":"AgAAAExTS1NiAgAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0Owp1bmlmb3JtIGZsb2F0NCB1bG9jYWxNYXRyaXhfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gZmxvYXQ0IGluQ2lyY2xlRWRnZTsKbm9wZXJzcGVjdGl2ZSBvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpub3BlcnNwZWN0aXZlIG91dCBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCgl2aW5DaXJjbGVFZGdlX1N0YWdlMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1N0YWdlMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gdWxvY2FsTWF0cml4X1N0YWdlMC54eiAqIGluUG9zaXRpb24gKyB1bG9jYWxNYXRyaXhfU3RhZ2UwLnl3OwoJc2tfUG9zaXRpb24gPSBmbG9hdDQoX3RtcF8wX2luUG9zaXRpb24ueCAsIF90bXBfMF9pblBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAAEBAAAAAAAAAQEALwQAACNleHRlbnNpb24gR0xfTlZfc2hhZGVyX25vcGVyc3BlY3RpdmVfaW50ZXJwb2xhdGlvbjogcmVxdWlyZQp1bmlmb3JtIGZsb2F0NCB1aW5uZXJSZWN0X1N0YWdlMTsKdW5pZm9ybSBoYWxmMiB1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxOwpub3BlcnNwZWN0aXZlIGluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1N0YWdlMDsKbm9wZXJzcGVjdGl2ZSBpbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCQlmbG9hdDQgY2lyY2xlRWRnZTsKCQljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgkJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgQ2lyY3VsYXJSUmVjdAoJCWZsb2F0MiBkeHkwID0gdWlubmVyUmVjdF9TdGFnZTEuTFQgLSBza19GcmFnQ29vcmQueHk7CgkJZmxvYXQyIGR4eTEgPSBza19GcmFnQ29vcmQueHkgLSB1aW5uZXJSZWN0X1N0YWdlMS5SQjsKCQlmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCQloYWxmIGFscGhhID0gaGFsZihzYXR1cmF0ZSh1cmFkaXVzUGx1c0hhbGZfU3RhZ2UxLnggLSBsZW5ndGgoZHh5KSkpOwoJCW91dHB1dF9TdGFnZTEgPSBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgKiBhbHBoYTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UBAAAAAAAAAA==","CAZAAAECA4AAAAAAAIABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAGAAAAAAAAAAAAAAAEAAAAAYAAVIA":"AgAAAExTS1NRAgAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0Owp1bmlmb3JtIGZsb2F0M3gzIHVsb2NhbE1hdHJpeF9TdGFnZTA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBmbG9hdDQgaW5DaXJjbGVFZGdlOwpub3BlcnNwZWN0aXZlIG91dCBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TdGFnZTA7Cm5vcGVyc3BlY3RpdmUgb3V0IGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCXZpbkNpcmNsZUVkZ2VfU3RhZ2UwID0gaW5DaXJjbGVFZGdlOwoJdmluQ29sb3JfU3RhZ2UwID0gaW5Db2xvcjsKCWZsb2F0MiBfdG1wXzBfaW5Qb3NpdGlvbiA9IGluUG9zaXRpb247CglmbG9hdDIgX3RtcF8xX2luUG9zaXRpb24gPSAodWxvY2FsTWF0cml4X1N0YWdlMCAqIGluUG9zaXRpb24ueHkxKS54eTsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAAClAgAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCm5vcGVyc3BlY3RpdmUgaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpub3BlcnNwZWN0aXZlIGluIGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJCWZsb2F0NCBjaXJjbGVFZGdlOwoJCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1N0YWdlMDsKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgkJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCQloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgkJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChlZGdlQWxwaGEpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UBAAAAAAAAAA==","CAZAAAECA4AAAAAAAAAEOAAAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFQAAAAAAAAAAAAAAAEAAAAAYAAVIA":"AgAAAExTS1M6AQAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0OwppbiBmbG9hdDIgcG9zaXRpb247CmluIGhhbGY0IGNvbG9yOwpub3BlcnNwZWN0aXZlIG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZjb2xvcl9TdGFnZTAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAAAAAKYBAAAjZXh0ZW5zaW9uIEdMX05WX3NoYWRlcl9ub3BlcnNwZWN0aXZlX2ludGVycG9sYXRpb246IHJlcXVpcmUKbm9wZXJzcGVjdGl2ZSBpbiBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgUAAABjb2xvcgAAAAEAAAAAAAAA","GMZQAAECA4AAAAAAAAABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAGAAAAAAAAAAAAAAAEAAAAAYAAVIA":"AgAAAExTS1MMAgAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0OwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gZmxvYXQ0IGluQ2lyY2xlRWRnZTsKbm9wZXJzcGVjdGl2ZSBvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpub3BlcnNwZWN0aXZlIG91dCBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCgl2aW5DaXJjbGVFZGdlX1N0YWdlMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1N0YWdlMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCAxKTsKfQoAAAAAAAAAAAAAAADJAgAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCm5vcGVyc3BlY3RpdmUgaW4gZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpub3BlcnNwZWN0aXZlIGluIGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJCWZsb2F0NCBjaXJjbGVFZGdlOwoJCWNpcmNsZUVkZ2UgPSB2aW5DaXJjbGVFZGdlX1N0YWdlMDsKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgkJZmxvYXQgZCA9IGxlbmd0aChjaXJjbGVFZGdlLnh5KTsKCQloYWxmIGRpc3RhbmNlVG9PdXRlckVkZ2UgPSBoYWxmKGNpcmNsZUVkZ2UueiAqICgxLjAgLSBkKSk7CgkJaGFsZiBlZGdlQWxwaGEgPSBzYXR1cmF0ZShkaXN0YW5jZVRvT3V0ZXJFZGdlKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChlZGdlQWxwaGEpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCQlza19GcmFnQ29sb3IgPSBza19GcmFnQ29sb3IuYWFhYTsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UBAAAAAAAAAA==","GMZQCAACBMAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFQAAAAAAAAAAAAAAAAAAAAAWAASYABAAAAAAAAAAAAPAAHAAAIAAAAAAAAAAAAIAAAACMABKQA":"AgAAAExTS1OoAQAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0Owp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxOwppbiBmbG9hdDIgcG9zaXRpb247CmluIGZsb2F0MiBsb2NhbENvb3JkOwpub3BlcnNwZWN0aXZlIG91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAgPSAoKCh1bWF0cml4X1N0YWdlMSkpICogbG9jYWxDb29yZC54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAADkDAAAjZXh0ZW5zaW9uIEdMX05WX3NoYWRlcl9ub3BlcnNwZWN0aXZlX2ludGVycG9sYXRpb246IHJlcXVpcmUKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxOwpub3BlcnNwZWN0aXZlIGluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTEsIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCkgKiBfaW5wdXQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgTWF0cml4RWZmZWN0CgkJb3V0cHV0X1N0YWdlMSA9IFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCk7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCQlza19GcmFnQ29sb3IgPSBza19GcmFnQ29sb3IuYWFhYTsKCX0KfQoAAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","GMZQCAACBMAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFQAAAAAAAAAIAAEAAAAAAAAWAASYABAAAAAABAAAQAPAAHAAAIAAAAAAAAAAAAIAAAACMABKQA":"AgAAAExTS1OoAQAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0Owp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxOwppbiBmbG9hdDIgcG9zaXRpb247CmluIGZsb2F0MiBsb2NhbENvb3JkOwpub3BlcnNwZWN0aXZlIG91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAgPSAoKCh1bWF0cml4X1N0YWdlMSkpICogbG9jYWxDb29yZC54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAJ8EAAAjZXh0ZW5zaW9uIEdMX05WX3NoYWRlcl9ub3BlcnNwZWN0aXZlX2ludGVycG9sYXRpb246IHJlcXVpcmUKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMTsKdW5pZm9ybSBmbG9hdDQgdWNsYW1wX1N0YWdlMV9jMDsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxOwpub3BlcnNwZWN0aXZlIGluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKCWZsb2F0MiBzdWJzZXRDb29yZDsKCXN1YnNldENvb3JkLnggPSBpbkNvb3JkLng7CglzdWJzZXRDb29yZC55ID0gaW5Db29yZC55OwoJZmxvYXQyIGNsYW1wZWRDb29yZDsKCWNsYW1wZWRDb29yZC54ID0gY2xhbXAoc3Vic2V0Q29vcmQueCwgdWNsYW1wX1N0YWdlMV9jMC54LCB1Y2xhbXBfU3RhZ2UxX2MwLnopOwoJY2xhbXBlZENvb3JkLnkgPSBjbGFtcChzdWJzZXRDb29yZC55LCB1Y2xhbXBfU3RhZ2UxX2MwLnksIHVjbGFtcF9TdGFnZTFfYzAudyk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxLCBjbGFtcGVkQ29vcmQpOwoJX291dHB1dCA9IF9pbnB1dCAqIHRleHR1cmVDb2xvcjsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIE1hdHJpeEVmZmVjdAoJCW91dHB1dF9TdGFnZTEgPSBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMChvdXRwdXRDb2xvcl9TdGFnZTApOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7CgkJc2tfRnJhZ0NvbG9yID0gc2tfRnJhZ0NvbG9yLmFhYWE7Cgl9Cn0KAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","GMZQCAACBUAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFQAAAAAAAAAAAAEAAAAAAAAWAASYABEAAAAAAAAAQAPAAHAAAKAAAAAGAAAAAAAAACACMAAYAABAAAAAAAAAAAABAAAAALQAFKAA":"AgAAAExTS1NQAQAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0OwppbiBmbG9hdDIgcG9zaXRpb247CmluIGZsb2F0MiBsb2NhbENvb3JkOwpub3BlcnNwZWN0aXZlIG91dCBmbG9hdDIgdkxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZMb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAA+xMAACNleHRlbnNpb24gR0xfTlZfc2hhZGVyX25vcGVyc3BlY3RpdmVfaW50ZXJwb2xhdGlvbjogcmVxdWlyZQp1bmlmb3JtIGhhbGYyIHVJbmNyZW1lbnRfU3RhZ2UxOwp1bmlmb3JtIGhhbGY0IHVLZXJuZWxfU3RhZ2UxWzddOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwOwp1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTE7Cm5vcGVyc3BlY3RpdmUgaW4gZmxvYXQyIHZMb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IGNsYW1wKHN1YnNldENvb3JkLngsIHVjbGFtcF9TdGFnZTFfYzBfYzAueCwgdWNsYW1wX1N0YWdlMV9jMF9jMC56KTsKCWNsYW1wZWRDb29yZC55ID0gc3Vic2V0Q29vcmQueTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTEsIGNsYW1wZWRDb29yZCk7Cglfb3V0cHV0ID0gX2lucHV0ICogdGV4dHVyZUNvbG9yOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX291dHB1dDsKCV9vdXRwdXQgPSBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsICgodW1hdHJpeF9TdGFnZTFfYzApICogX2Nvb3Jkcy54eTEpLnh5KTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIEdhdXNzaWFuQ29udm9sdXRpb24KCQlmbG9hdDIgX2Nvb3JkcyA9IHZMb2NhbENvb3JkX1N0YWdlMC54eTsKCQlvdXRwdXRfU3RhZ2UxID0gaGFsZjQoMCwgMCwgMCwgMCk7CgkJZmxvYXQyIGNvb3JkID0gX2Nvb3JkcyAtIDEyLjAgKiB1SW5jcmVtZW50X1N0YWdlMTsKCQlmbG9hdDIgY29vcmRTYW1wbGVkID0gaGFsZjIoMCwgMCk7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVswXS55OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVsxXS54OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVsxXS53OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVsyXS56OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVszXS55OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVs0XS54OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVs0XS53OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVs1XS56OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNl0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQlvdXRwdXRfU3RhZ2UxICo9IG91dHB1dENvbG9yX1N0YWdlMDsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJCXNrX0ZyYWdDb2xvciA9IHNrX0ZyYWdDb2xvci5hYWFhOwoJfQp9CgAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","GMZQCAACBUAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFQAAAAAAAAAIAAAAAAAAAAAWAASYABEAAAAABAAAAAPAAHAAAKAAAAAGAAAAAAEAAAACMAAYAABAAAAAAAAAAAABAAAAALQAFKAA":"AgAAAExTS1NQAQAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0OwppbiBmbG9hdDIgcG9zaXRpb247CmluIGZsb2F0MiBsb2NhbENvb3JkOwpub3BlcnNwZWN0aXZlIG91dCBmbG9hdDIgdkxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZMb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAA+xMAACNleHRlbnNpb24gR0xfTlZfc2hhZGVyX25vcGVyc3BlY3RpdmVfaW50ZXJwb2xhdGlvbjogcmVxdWlyZQp1bmlmb3JtIGhhbGYyIHVJbmNyZW1lbnRfU3RhZ2UxOwp1bmlmb3JtIGhhbGY0IHVLZXJuZWxfU3RhZ2UxWzddOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwOwp1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTE7Cm5vcGVyc3BlY3RpdmUgaW4gZmxvYXQyIHZMb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IHN1YnNldENvb3JkLng7CgljbGFtcGVkQ29vcmQueSA9IGNsYW1wKHN1YnNldENvb3JkLnksIHVjbGFtcF9TdGFnZTFfYzBfYzAueSwgdWNsYW1wX1N0YWdlMV9jMF9jMC53KTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTEsIGNsYW1wZWRDb29yZCk7Cglfb3V0cHV0ID0gX2lucHV0ICogdGV4dHVyZUNvbG9yOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX291dHB1dDsKCV9vdXRwdXQgPSBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsICgodW1hdHJpeF9TdGFnZTFfYzApICogX2Nvb3Jkcy54eTEpLnh5KTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIEdhdXNzaWFuQ29udm9sdXRpb24KCQlmbG9hdDIgX2Nvb3JkcyA9IHZMb2NhbENvb3JkX1N0YWdlMC54eTsKCQlvdXRwdXRfU3RhZ2UxID0gaGFsZjQoMCwgMCwgMCwgMCk7CgkJZmxvYXQyIGNvb3JkID0gX2Nvb3JkcyAtIDEyLjAgKiB1SW5jcmVtZW50X1N0YWdlMTsKCQlmbG9hdDIgY29vcmRTYW1wbGVkID0gaGFsZjIoMCwgMCk7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVswXS55OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVsxXS54OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVsxXS53OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVsyXS56OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVszXS55OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVs0XS54OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVs0XS53OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVs1XS56OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNl0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQlvdXRwdXRfU3RhZ2UxICo9IG91dHB1dENvbG9yX1N0YWdlMDsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJCXNrX0ZyYWdDb2xvciA9IHNrX0ZyYWdDb2xvci5hYWFhOwoJfQp9CgAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZAAAMCBMAAAAAACAAAAAAAAAAAAAAACMAACAH777777777777RQADPAAAAAAAAAAAAAAAAAAAAAJAAJMAASAAAAAAAAAAAGQADQAAFAAAAAAAAAAAEAACEAACAAAAAAAAAAAACAAAAAUAAKUAA":"AgAAAExTS1NcAQAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0Owp1bmlmb3JtIGZsb2F0NCB1dmlld01hdHJpeF9TdGFnZTA7CmluIGZsb2F0MiBwb3NpdGlvbjsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgVmVydGljZXNHUAoJZmxvYXQyIF90bXBfMF9wb3NpdGlvbiA9IHV2aWV3TWF0cml4X1N0YWdlMC54eiAqIHBvc2l0aW9uICsgdXZpZXdNYXRyaXhfU3RhZ2UwLnl3OwoJc2tfUG9zaXRpb24gPSBmbG9hdDQoX3RtcF8wX3Bvc2l0aW9uLnggLCBfdG1wXzBfcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAEBAAAAAAAAAQEAxgYAACNleHRlbnNpb24gR0xfTlZfc2hhZGVyX25vcGVyc3BlY3RpdmVfaW50ZXJwb2xhdGlvbjogcmVxdWlyZQp1bmlmb3JtIGhhbGY0IHVDb2xvcl9TdGFnZTA7CnVuaWZvcm0gaGFsZiB1Y29ybmVyUmFkaXVzX1N0YWdlMTsKdW5pZm9ybSBmbG9hdDQgdXByb3h5UmVjdF9TdGFnZTE7CnVuaWZvcm0gaGFsZiB1Ymx1clJhZGl1c19TdGFnZTE7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMTsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTEsIF9jb29yZHMpICogX2lucHV0KTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0LCAoKHVtYXRyaXhfU3RhZ2UxX2MwKSAqIF9jb29yZHMueHkxKS54eSk7CglyZXR1cm4gX291dHB1dDsKfQp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIFZlcnRpY2VzR1AKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB1Q29sb3JfU3RhZ2UwOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBSUmVjdEJsdXJFZmZlY3QKCQloYWxmMiB0cmFuc2xhdGVkRnJhZ1BvcyA9IGhhbGYyKHNrX0ZyYWdDb29yZC54eSAtIHVwcm94eVJlY3RfU3RhZ2UxLnh5KTsKCQloYWxmMiBwcm94eUNlbnRlciA9IGhhbGYyKCh1cHJveHlSZWN0X1N0YWdlMS56dyAtIHVwcm94eVJlY3RfU3RhZ2UxLnh5KSAqIDAuNSk7CgkJaGFsZiBlZGdlU2l6ZSA9ICgyLjAgKiB1Ymx1clJhZGl1c19TdGFnZTEgKyB1Y29ybmVyUmFkaXVzX1N0YWdlMSkgKyAwLjU7CgkJdHJhbnNsYXRlZEZyYWdQb3MgLT0gcHJveHlDZW50ZXI7CgkJaGFsZjIgZnJhZ0RpcmVjdGlvbiA9IHNpZ24odHJhbnNsYXRlZEZyYWdQb3MpOwoJCXRyYW5zbGF0ZWRGcmFnUG9zID0gYWJzKHRyYW5zbGF0ZWRGcmFnUG9zKTsKCQl0cmFuc2xhdGVkRnJhZ1BvcyAtPSBwcm94eUNlbnRlciAtIGVkZ2VTaXplOwoJCXRyYW5zbGF0ZWRGcmFnUG9zID0gbWF4KHRyYW5zbGF0ZWRGcmFnUG9zLCAwLjApOwoJCXRyYW5zbGF0ZWRGcmFnUG9zICo9IGZyYWdEaXJlY3Rpb247CgkJdHJhbnNsYXRlZEZyYWdQb3MgKz0gaGFsZjIoZWRnZVNpemUpOwoJCWhhbGYyIHByb3h5RGltcyA9IGhhbGYyKDIuMCAqIGVkZ2VTaXplKTsKCQloYWxmMiB0ZXhDb29yZCA9IHRyYW5zbGF0ZWRGcmFnUG9zIC8gcHJveHlEaW1zOwoJCWhhbGY0IGlucHV0Q29sb3IgPSBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7CgkJb3V0cHV0X1N0YWdlMSA9IGlucHV0Q29sb3IgKiBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBmbG9hdDIodGV4Q29vcmQpKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAAAQAAAAgAAABwb3NpdGlvbgEAAAAAAAAA","GMZQAAEBA4AAAAAAAEABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAGAAAAAAAAAAAAAAACAAAAAYAACAA":"AgAAAExTS1NiAgAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0Owp1bmlmb3JtIGZsb2F0NCB1bG9jYWxNYXRyaXhfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gZmxvYXQ0IGluQ2lyY2xlRWRnZTsKbm9wZXJzcGVjdGl2ZSBvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpub3BlcnNwZWN0aXZlIG91dCBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCgl2aW5DaXJjbGVFZGdlX1N0YWdlMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1N0YWdlMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gdWxvY2FsTWF0cml4X1N0YWdlMC54eiAqIGluUG9zaXRpb24gKyB1bG9jYWxNYXRyaXhfU3RhZ2UwLnl3OwoJc2tfUG9zaXRpb24gPSBmbG9hdDQoX3RtcF8wX2luUG9zaXRpb24ueCAsIF90bXBfMF9pblBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAAAAAAAAAAAAAAAAuAIAACNleHRlbnNpb24gR0xfTlZfc2hhZGVyX25vcGVyc3BlY3RpdmVfaW50ZXJwb2xhdGlvbjogcmVxdWlyZQpub3BlcnNwZWN0aXZlIGluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1N0YWdlMDsKbm9wZXJzcGVjdGl2ZSBpbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCQlmbG9hdDQgY2lyY2xlRWRnZTsKCQljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgkJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogQ292ZXJhZ2UgU2V0IE9wCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJCXNrX0ZyYWdDb2xvciA9IHNrX0ZyYWdDb2xvci5hYWFhOwoJfQp9CgAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","GMZQCAABBMAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFQAAAAAAAAAAAAAAAAAAAAAWAASYABAAAAAAAAAAAAPAAHAAAIAAAAAAAAAAAAIAAAACMABKQA":"AgAAAExTS1OoAQAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0Owp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxOwppbiBmbG9hdDIgcG9zaXRpb247CmluIGZsb2F0MiBsb2NhbENvb3JkOwpub3BlcnNwZWN0aXZlIG91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAgPSAoKCh1bWF0cml4X1N0YWdlMSkpICogbG9jYWxDb29yZC54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAADkDAAAjZXh0ZW5zaW9uIEdMX05WX3NoYWRlcl9ub3BlcnNwZWN0aXZlX2ludGVycG9sYXRpb246IHJlcXVpcmUKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxOwpub3BlcnNwZWN0aXZlIGluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTEsIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCkgKiBfaW5wdXQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSBoYWxmNCgxKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgTWF0cml4RWZmZWN0CgkJb3V0cHV0X1N0YWdlMSA9IFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvbG9yX1N0YWdlMCk7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCQlza19GcmFnQ29sb3IgPSBza19GcmFnQ29sb3IuYWFhYTsKCX0KfQoAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZAAAICBAAAAAAAAAAACAAAAAJQAAIADQABCAAPAAKAAAAAAAABIAAYAAAAAAAAAEAAAABEAAOQABAAAAAAAAAAAABAAAAAGQAFKAA":"AgAAAExTS1PcAgAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0Owp1bmlmb3JtIGZsb2F0MiB1QXRsYXNTaXplSW52X1N0YWdlMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIHVzaG9ydDIgaW5UZXh0dXJlQ29vcmRzOwpub3BlcnNwZWN0aXZlIG91dCBmbG9hdDIgdlRleHR1cmVDb29yZHNfU3RhZ2UwOwpmbGF0IG91dCBpbnQgdlRleEluZGV4X1N0YWdlMDsKbm9wZXJzcGVjdGl2ZSBvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBUZXh0dXJlCglpbnQyIHNpZ25lZENvb3JkcyA9IGludDIoaW5UZXh0dXJlQ29vcmRzLngsIGluVGV4dHVyZUNvb3Jkcy55KTsKCWZsb2F0MiB1bm9ybVRleENvb3JkcyA9IGZsb2F0MihzaWduZWRDb29yZHMueC8yLCBzaWduZWRDb29yZHMueS8yKTsKCWludCB0ZXhJZHggPSAwOwoJdlRleHR1cmVDb29yZHNfU3RhZ2UwID0gdW5vcm1UZXhDb29yZHMgKiB1QXRsYXNTaXplSW52X1N0YWdlMDsKCXZUZXhJbmRleF9TdGFnZTAgPSB0ZXhJZHg7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQyIF90bXBfMF9pblBvc2l0aW9uID0gaW5Qb3NpdGlvbjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KGluUG9zaXRpb24ueCAsIGluUG9zaXRpb24ueSwgMCwgMSk7Cn0KAAEBAAAAAAAAAQEAyAQAACNleHRlbnNpb24gR0xfTlZfc2hhZGVyX25vcGVyc3BlY3RpdmVfaW50ZXJwb2xhdGlvbjogcmVxdWlyZQp1bmlmb3JtIGZsb2F0NCB1Y2lyY2xlX1N0YWdlMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwpub3BlcnNwZWN0aXZlIGluIGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TdGFnZTA7CmZsYXQgaW4gaW50IHZUZXhJbmRleF9TdGFnZTA7Cm5vcGVyc3BlY3RpdmUgaW4gaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIFRleHR1cmUKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2aW5Db2xvcl9TdGFnZTA7CgkJaGFsZjQgdGV4Q29sb3I7CgkJewoJCQl0ZXhDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTAsIHZUZXh0dXJlQ29vcmRzX1N0YWdlMCk7CgkJfQoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IHRleENvbG9yOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBDaXJjbGVFZmZlY3QKCQlmbG9hdDIgcHJldkNlbnRlcjsKCQlmbG9hdCBwcmV2UmFkaXVzID0gLTEuMDAwMDAwOwoJCWhhbGYgZDsKCQlAaWYgKDEgPT0gMiB8fCAxID09IDMpIAoJCXsKCQkJZCA9IGhhbGYoKGxlbmd0aCgodWNpcmNsZV9TdGFnZTEueHkgLSBza19GcmFnQ29vcmQueHkpICogdWNpcmNsZV9TdGFnZTEudykgLSAxLjApICogdWNpcmNsZV9TdGFnZTEueik7CgkJfQoJCWVsc2UgCgkJewoJCQlkID0gaGFsZigoMS4wIC0gbGVuZ3RoKCh1Y2lyY2xlX1N0YWdlMS54eSAtIHNrX0ZyYWdDb29yZC54eSkgKiB1Y2lyY2xlX1N0YWdlMS53KSkgKiB1Y2lyY2xlX1N0YWdlMS56KTsKCQl9CgkJaGFsZjQgaW5wdXRDb2xvciA9IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCQlAaWYgKDEgPT0gMSB8fCAxID09IDMpIAoJCXsKCQkJb3V0cHV0X1N0YWdlMSA9IGlucHV0Q29sb3IgKiBjbGFtcChkLCAwLjAsIDEuMCk7CgkJfQoJCWVsc2UgCgkJewoJCQlvdXRwdXRfU3RhZ2UxID0gZCA+IDAuNSA/IGlucHV0Q29sb3IgOiBoYWxmNCgwLjApOwoJCX0KCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADwAAAGluVGV4dHVyZUNvb3JkcwABAAAAAAAAAA==","CAZACAECCMAAAAAAAAAGOAAAAAJQAAIA777777Y4AAIQAEYAAEAP777777777777EAAFQAAAAAAAAAAAAAACYABFAACAAAAAAAAAAAAAAAAAAOAAJMAAQAAAAAAAAAAAJAADQAAEAAAAAAAAAAAAAPAAHQADYAB4AAAAAADAAA6AABAAAAAAKAAAAAAAAAAAAAAAAAAAHQADYAB4AA6AAAAAACAAABYAAQAAAAAAAAAAAAQAAAAJAACVAA":"AgAAAExTS1MDAgAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0Owp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MxX2MwOwppbiBmbG9hdDIgcG9zaXRpb247CmluIGhhbGY0IGNvbG9yOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKbm9wZXJzcGVjdGl2ZSBvdXQgaGFsZjQgdmNvbG9yX1N0YWdlMDsKbm9wZXJzcGVjdGl2ZSBvdXQgZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzFfYzApKSAqIGxvY2FsQ29vcmQueHkxKS54eTsKCX0KfQoAAAAAAAAAAAAAAAAA/gYAACNleHRlbnNpb24gR0xfTlZfc2hhZGVyX25vcGVyc3BlY3RpdmVfaW50ZXJwb2xhdGlvbjogcmVxdWlyZQp1bmlmb3JtIGhhbGY0IHVjb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzFfYzA7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMTsKbm9wZXJzcGVjdGl2ZSBpbiBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpub3BlcnNwZWN0aXZlIGluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IENvbnN0Q29sb3JQcm9jZXNzb3JfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CglAc3dpdGNoICgwKSAKCXsKCQljYXNlIDA6ICAgICAgICAKCQl7CgkJCV9vdXRwdXQgPSB1Y29sb3JfU3RhZ2UxX2MwOwoJCQlicmVhazsKCQl9CgkJY2FzZSAxOiAgICAgICAgCgkJewoJCQloYWxmNCBpbnB1dENvbG9yID0gX2lucHV0OwoJCQlfb3V0cHV0ID0gaW5wdXRDb2xvciAqIHVjb2xvcl9TdGFnZTFfYzA7CgkJCWJyZWFrOwoJCX0KCQljYXNlIDI6ICAgICAgICAKCQl7CgkJCWhhbGYgaW5wdXRBbHBoYSA9IF9pbnB1dC53OwoJCQlfb3V0cHV0ID0gaW5wdXRBbHBoYSAqIHVjb2xvcl9TdGFnZTFfYzA7CgkJCWJyZWFrOwoJCX0KCX0KCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MxX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTEsIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCkgKiBfaW5wdXQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MxX2MwX2MwKF9pbnB1dCk7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBPdmVycmlkZUlucHV0RnJhZ21lbnRQcm9jZXNzb3JfU3RhZ2UxX2MxKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmNCBjb25zdENvbG9yOwoJQGlmIChmYWxzZSkgCgl7CgkJY29uc3RDb2xvciA9IGhhbGY0KDApOwoJfQoJZWxzZSAKCXsKCQljb25zdENvbG9yID0gaGFsZjQoMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDApOwoJfQoJX291dHB1dCA9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzFfYzAoY29uc3RDb2xvcik7CglyZXR1cm4gX291dHB1dDsKfQp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZjb2xvcl9TdGFnZTA7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIENvbXBvc2UKCQkvLyBTa01vZGUgWGZlciBNb2RlOiBTcmNJbgoJCW91dHB1dF9TdGFnZTEgPSBibGVuZF9zcmNfaW4oQ29uc3RDb2xvclByb2Nlc3Nvcl9TdGFnZTFfYzAoaGFsZjQoMSkpLCBPdmVycmlkZUlucHV0RnJhZ21lbnRQcm9jZXNzb3JfU3RhZ2UxX2MxKG91dHB1dENvbG9yX1N0YWdlMCkpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","GMZQCAABBMAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFQAAAAAAAAAIAAEAAAAAAAAWAASYABAAAAAABAAAQAPAAHAAAIAAAAAAAAAAAAIAAAACMABKQA":"AgAAAExTS1OoAQAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0Owp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxOwppbiBmbG9hdDIgcG9zaXRpb247CmluIGZsb2F0MiBsb2NhbENvb3JkOwpub3BlcnNwZWN0aXZlIG91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKCXsKCQl2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAgPSAoKCh1bWF0cml4X1N0YWdlMSkpICogbG9jYWxDb29yZC54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAJ8EAAAjZXh0ZW5zaW9uIEdMX05WX3NoYWRlcl9ub3BlcnNwZWN0aXZlX2ludGVycG9sYXRpb246IHJlcXVpcmUKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMTsKdW5pZm9ybSBmbG9hdDQgdWNsYW1wX1N0YWdlMV9jMDsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxOwpub3BlcnNwZWN0aXZlIGluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgaW5Db29yZCA9IHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKCWZsb2F0MiBzdWJzZXRDb29yZDsKCXN1YnNldENvb3JkLnggPSBpbkNvb3JkLng7CglzdWJzZXRDb29yZC55ID0gaW5Db29yZC55OwoJZmxvYXQyIGNsYW1wZWRDb29yZDsKCWNsYW1wZWRDb29yZC54ID0gY2xhbXAoc3Vic2V0Q29vcmQueCwgdWNsYW1wX1N0YWdlMV9jMC54LCB1Y2xhbXBfU3RhZ2UxX2MwLnopOwoJY2xhbXBlZENvb3JkLnkgPSBjbGFtcChzdWJzZXRDb29yZC55LCB1Y2xhbXBfU3RhZ2UxX2MwLnksIHVjbGFtcF9TdGFnZTFfYzAudyk7CgloYWxmNCB0ZXh0dXJlQ29sb3IgPSBzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxLCBjbGFtcGVkQ29vcmQpOwoJX291dHB1dCA9IF9pbnB1dCAqIHRleHR1cmVDb2xvcjsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIE1hdHJpeEVmZmVjdAoJCW91dHB1dF9TdGFnZTEgPSBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMChvdXRwdXRDb2xvcl9TdGFnZTApOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dF9TdGFnZTEgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7CgkJc2tfRnJhZ0NvbG9yID0gc2tfRnJhZ0NvbG9yLmFhYWE7Cgl9Cn0KAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24KAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","GMZQCAABBUAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFQAAAAAAAAAAAAEAAAAAAAAWAASYABEAAAAAAAAAQAPAAHAAAKAAAAAGAAAAAAAAACACMAAYAABAAAAAAAAAAAABAAAAALQAFKAA":"AgAAAExTS1NQAQAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0OwppbiBmbG9hdDIgcG9zaXRpb247CmluIGZsb2F0MiBsb2NhbENvb3JkOwpub3BlcnNwZWN0aXZlIG91dCBmbG9hdDIgdkxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZMb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAA+xMAACNleHRlbnNpb24gR0xfTlZfc2hhZGVyX25vcGVyc3BlY3RpdmVfaW50ZXJwb2xhdGlvbjogcmVxdWlyZQp1bmlmb3JtIGhhbGYyIHVJbmNyZW1lbnRfU3RhZ2UxOwp1bmlmb3JtIGhhbGY0IHVLZXJuZWxfU3RhZ2UxWzddOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwOwp1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTE7Cm5vcGVyc3BlY3RpdmUgaW4gZmxvYXQyIHZMb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IGNsYW1wKHN1YnNldENvb3JkLngsIHVjbGFtcF9TdGFnZTFfYzBfYzAueCwgdWNsYW1wX1N0YWdlMV9jMF9jMC56KTsKCWNsYW1wZWRDb29yZC55ID0gc3Vic2V0Q29vcmQueTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTEsIGNsYW1wZWRDb29yZCk7Cglfb3V0cHV0ID0gX2lucHV0ICogdGV4dHVyZUNvbG9yOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX291dHB1dDsKCV9vdXRwdXQgPSBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsICgodW1hdHJpeF9TdGFnZTFfYzApICogX2Nvb3Jkcy54eTEpLnh5KTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIEdhdXNzaWFuQ29udm9sdXRpb24KCQlmbG9hdDIgX2Nvb3JkcyA9IHZMb2NhbENvb3JkX1N0YWdlMC54eTsKCQlvdXRwdXRfU3RhZ2UxID0gaGFsZjQoMCwgMCwgMCwgMCk7CgkJZmxvYXQyIGNvb3JkID0gX2Nvb3JkcyAtIDEyLjAgKiB1SW5jcmVtZW50X1N0YWdlMTsKCQlmbG9hdDIgY29vcmRTYW1wbGVkID0gaGFsZjIoMCwgMCk7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVswXS55OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVsxXS54OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVsxXS53OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVsyXS56OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVszXS55OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVs0XS54OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVs0XS53OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVs1XS56OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNl0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQlvdXRwdXRfU3RhZ2UxICo9IG91dHB1dENvbG9yX1N0YWdlMDsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJCXNrX0ZyYWdDb2xvciA9IHNrX0ZyYWdDb2xvci5hYWFhOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","GMZQCAABBUAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFQAAAAAAAAAIAAAAAAAAAAAWAASYABEAAAAABAAAAAPAAHAAAKAAAAAGAAAAAAEAAAACMAAYAABAAAAAAAAAAAABAAAAALQAFKAA":"AgAAAExTS1NQAQAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0OwppbiBmbG9hdDIgcG9zaXRpb247CmluIGZsb2F0MiBsb2NhbENvb3JkOwpub3BlcnNwZWN0aXZlIG91dCBmbG9hdDIgdkxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZMb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAA+xMAACNleHRlbnNpb24gR0xfTlZfc2hhZGVyX25vcGVyc3BlY3RpdmVfaW50ZXJwb2xhdGlvbjogcmVxdWlyZQp1bmlmb3JtIGhhbGYyIHVJbmNyZW1lbnRfU3RhZ2UxOwp1bmlmb3JtIGhhbGY0IHVLZXJuZWxfU3RhZ2UxWzddOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwOwp1bmlmb3JtIGZsb2F0NCB1Y2xhbXBfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTE7Cm5vcGVyc3BlY3RpdmUgaW4gZmxvYXQyIHZMb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7CglmbG9hdDIgaW5Db29yZCA9IF9jb29yZHM7CglmbG9hdDIgc3Vic2V0Q29vcmQ7CglzdWJzZXRDb29yZC54ID0gaW5Db29yZC54OwoJc3Vic2V0Q29vcmQueSA9IGluQ29vcmQueTsKCWZsb2F0MiBjbGFtcGVkQ29vcmQ7CgljbGFtcGVkQ29vcmQueCA9IHN1YnNldENvb3JkLng7CgljbGFtcGVkQ29vcmQueSA9IGNsYW1wKHN1YnNldENvb3JkLnksIHVjbGFtcF9TdGFnZTFfYzBfYzAueSwgdWNsYW1wX1N0YWdlMV9jMF9jMC53KTsKCWhhbGY0IHRleHR1cmVDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTEsIGNsYW1wZWRDb29yZCk7Cglfb3V0cHV0ID0gX2lucHV0ICogdGV4dHVyZUNvbG9yOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX291dHB1dDsKCV9vdXRwdXQgPSBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMF9jMChfaW5wdXQsICgodW1hdHJpeF9TdGFnZTFfYzApICogX2Nvb3Jkcy54eTEpLnh5KTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIEdhdXNzaWFuQ29udm9sdXRpb24KCQlmbG9hdDIgX2Nvb3JkcyA9IHZMb2NhbENvb3JkX1N0YWdlMC54eTsKCQlvdXRwdXRfU3RhZ2UxID0gaGFsZjQoMCwgMCwgMCwgMCk7CgkJZmxvYXQyIGNvb3JkID0gX2Nvb3JkcyAtIDEyLjAgKiB1SW5jcmVtZW50X1N0YWdlMTsKCQlmbG9hdDIgY29vcmRTYW1wbGVkID0gaGFsZjIoMCwgMCk7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVswXS55OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVsxXS54OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVsxXS53OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVsyXS56OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVszXS55OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVs0XS54OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVs0XS53OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVs1XS56OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNl0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQlvdXRwdXRfU3RhZ2UxICo9IG91dHB1dENvbG9yX1N0YWdlMDsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJCXNrX0ZyYWdDb2xvciA9IHNrX0ZyYWdDb2xvci5hYWFhOwoJfQp9CgAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZAAAICBMAAAAAAAAAGOAAAAAJQAAIA777777Y4AAIQAEYAAEAP777777777777EAAFQAAAAAAAAAAAAAAAAAAAAAWAASYABAAAAAAAAAAAAPAAHAAAIAAAAAAAAAAAAIAAAACMABKQA":"AgAAAExTS1P3AQAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0Owp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxOwppbiBmbG9hdDIgcG9zaXRpb247CmluIGhhbGY0IGNvbG9yOwppbiBmbG9hdDIgbG9jYWxDb29yZDsKbm9wZXJzcGVjdGl2ZSBvdXQgaGFsZjQgdmNvbG9yX1N0YWdlMDsKbm9wZXJzcGVjdGl2ZSBvdXQgZmxvYXQyIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTEpKSAqIGxvY2FsQ29vcmQueHkxKS54eTsKCX0KfQoAAAAAAAAAAAAAAAAAQAMAACNleHRlbnNpb24gR0xfTlZfc2hhZGVyX25vcGVyc3BlY3RpdmVfaW50ZXJwb2xhdGlvbjogcmVxdWlyZQp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTE7Cm5vcGVyc3BlY3RpdmUgaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKbm9wZXJzcGVjdGl2ZSBpbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IChzYW1wbGUodVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxLCB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTApICogX2lucHV0KTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgTWF0cml4RWZmZWN0CgkJb3V0cHV0X1N0YWdlMSA9IFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvdmVyYWdlX1N0YWdlMCk7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0X1N0YWdlMTsKCX0KfQoAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACAAAAHBvc2l0aW9uBQAAAGNvbG9yAAAACgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZAAAICBEAAAAAAAAAEOAAAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFQAAAAAAAAAIAAAACYAA5AACAAAAAAAAAAAACAAAAAPAAKUAA":"AgAAAExTS1M6AQAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0OwppbiBmbG9hdDIgcG9zaXRpb247CmluIGhhbGY0IGNvbG9yOwpub3BlcnNwZWN0aXZlIG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZjb2xvcl9TdGFnZTAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKfQoAAAABAQAAAAAAAAEBAAEEAAAjZXh0ZW5zaW9uIEdMX05WX3NoYWRlcl9ub3BlcnNwZWN0aXZlX2ludGVycG9sYXRpb246IHJlcXVpcmUKdW5pZm9ybSBmbG9hdDQgdWNpcmNsZV9TdGFnZTE7Cm5vcGVyc3BlY3RpdmUgaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2Y29sb3JfU3RhZ2UwOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBDaXJjbGVFZmZlY3QKCQlmbG9hdDIgcHJldkNlbnRlcjsKCQlmbG9hdCBwcmV2UmFkaXVzID0gLTEuMDAwMDAwOwoJCWhhbGYgZDsKCQlAaWYgKDEgPT0gMiB8fCAxID09IDMpIAoJCXsKCQkJZCA9IGhhbGYoKGxlbmd0aCgodWNpcmNsZV9TdGFnZTEueHkgLSBza19GcmFnQ29vcmQueHkpICogdWNpcmNsZV9TdGFnZTEudykgLSAxLjApICogdWNpcmNsZV9TdGFnZTEueik7CgkJfQoJCWVsc2UgCgkJewoJCQlkID0gaGFsZigoMS4wIC0gbGVuZ3RoKCh1Y2lyY2xlX1N0YWdlMS54eSAtIHNrX0ZyYWdDb29yZC54eSkgKiB1Y2lyY2xlX1N0YWdlMS53KSkgKiB1Y2lyY2xlX1N0YWdlMS56KTsKCQl9CgkJaGFsZjQgaW5wdXRDb2xvciA9IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCQlAaWYgKDEgPT0gMSB8fCAxID09IDMpIAoJCXsKCQkJb3V0cHV0X1N0YWdlMSA9IGlucHV0Q29sb3IgKiBjbGFtcChkLCAwLjAsIDEuMCk7CgkJfQoJCWVsc2UgCgkJewoJCQlvdXRwdXRfU3RhZ2UxID0gZCA+IDAuNSA/IGlucHV0Q29sb3IgOiBoYWxmNCgwLjApOwoJCX0KCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAABAAAAAAAAAA==","CAZAAAICBEAAAAAAAEABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAGAAAAAAAAAIAAAACYAA5AACAAAAAAAAAAAACAAAAAPAAKUAA":"AgAAAExTS1NiAgAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0Owp1bmlmb3JtIGZsb2F0NCB1bG9jYWxNYXRyaXhfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gZmxvYXQ0IGluQ2lyY2xlRWRnZTsKbm9wZXJzcGVjdGl2ZSBvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpub3BlcnNwZWN0aXZlIG91dCBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCgl2aW5DaXJjbGVFZGdlX1N0YWdlMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1N0YWdlMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gdWxvY2FsTWF0cml4X1N0YWdlMC54eiAqIGluUG9zaXRpb24gKyB1bG9jYWxNYXRyaXhfU3RhZ2UwLnl3OwoJc2tfUG9zaXRpb24gPSBmbG9hdDQoX3RtcF8wX2luUG9zaXRpb24ueCAsIF90bXBfMF9pblBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAAEBAAAAAAAAAQEAAAUAACNleHRlbnNpb24gR0xfTlZfc2hhZGVyX25vcGVyc3BlY3RpdmVfaW50ZXJwb2xhdGlvbjogcmVxdWlyZQp1bmlmb3JtIGZsb2F0NCB1Y2lyY2xlX1N0YWdlMTsKbm9wZXJzcGVjdGl2ZSBpbiBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TdGFnZTA7Cm5vcGVyc3BlY3RpdmUgaW4gaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCgkJZmxvYXQ0IGNpcmNsZUVkZ2U7CgkJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZpbkNvbG9yX1N0YWdlMDsKCQlmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJCWhhbGYgZGlzdGFuY2VUb091dGVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKDEuMCAtIGQpKTsKCQloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIENpcmNsZUVmZmVjdAoJCWZsb2F0MiBwcmV2Q2VudGVyOwoJCWZsb2F0IHByZXZSYWRpdXMgPSAtMS4wMDAwMDA7CgkJaGFsZiBkOwoJCUBpZiAoMSA9PSAyIHx8IDEgPT0gMykgCgkJewoJCQlkID0gaGFsZigobGVuZ3RoKCh1Y2lyY2xlX1N0YWdlMS54eSAtIHNrX0ZyYWdDb29yZC54eSkgKiB1Y2lyY2xlX1N0YWdlMS53KSAtIDEuMCkgKiB1Y2lyY2xlX1N0YWdlMS56KTsKCQl9CgkJZWxzZSAKCQl7CgkJCWQgPSBoYWxmKCgxLjAgLSBsZW5ndGgoKHVjaXJjbGVfU3RhZ2UxLnh5IC0gc2tfRnJhZ0Nvb3JkLnh5KSAqIHVjaXJjbGVfU3RhZ2UxLncpKSAqIHVjaXJjbGVfU3RhZ2UxLnopOwoJCX0KCQloYWxmNCBpbnB1dENvbG9yID0gb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJCUBpZiAoMSA9PSAxIHx8IDEgPT0gMykgCgkJewoJCQlvdXRwdXRfU3RhZ2UxID0gaW5wdXRDb2xvciAqIGNsYW1wKGQsIDAuMCwgMS4wKTsKCQl9CgkJZWxzZSAKCQl7CgkJCW91dHB1dF9TdGFnZTEgPSBkID4gMC41ID8gaW5wdXRDb2xvciA6IGhhbGY0KDAuMCk7CgkJfQoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dF9TdGFnZTE7Cgl9Cn0KAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAAAwAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlAQAAAAAAAAA=","CAZAAAMCBEAAAAIAAEABKAADAAKQAAYACUAAGAAVAABQAEYAAEABKAADAAKQAAYADQABCABEAAXQAAAAAAAACAAAAAYAAFIAAQAAAAAAAAAAAAQAAAAEAACVAA":"AgAAAExTS1MkCQAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0OwppbiBmbG9hdDQgcmFkaWlfc2VsZWN0b3I7CmluIGZsb2F0NCBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzOwppbiBmbG9hdDQgYWFfYmxvYXRfYW5kX2NvdmVyYWdlOwppbiBmbG9hdDQgc2tldzsKaW4gZmxvYXQyIHRyYW5zbGF0ZTsKaW4gZmxvYXQ0IHJhZGlpX3g7CmluIGZsb2F0NCByYWRpaV95OwppbiBoYWxmNCBjb2xvcjsKbm9wZXJzcGVjdGl2ZSBvdXQgaGFsZjQgdmNvbG9yX1N0YWdlMDsKbm9wZXJzcGVjdGl2ZSBvdXQgZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglpZiAoYW55KGdyZWF0ZXJUaGFuKGFhX2Jsb2F0cmFkaXVzLCBmbG9hdDIoMSkpKSkgCgl7CgkJY29ybmVyID0gbWF4KGFicyhjb3JuZXIpLCBhYV9ibG9hdHJhZGl1cykgKiBzaWduKGNvcm5lcik7CgkJY292ZXJhZ2UgLz0gbWF4KGFhX2Jsb2F0cmFkaXVzLngsIDEpICogbWF4KGFhX2Jsb2F0cmFkaXVzLnksIDEpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjI1KSkpIAoJewoJCXJhZGlpID0gYWFfYmxvYXRyYWRpdXM7CgkJcmFkaXVzX291dHNldCA9IGZsb29yKGFicyhyYWRpdXNfb3V0c2V0KSkgKiByYWRpdXNfb3V0c2V0OwoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24ueHkgKiBhYV9ibG9hdHJhZGl1czsKCWZsb2F0MiB2ZXJ0ZXhwb3MgPSBjb3JuZXIgKyByYWRpdXNfb3V0c2V0ICogcmFkaWkgKyBhYV9vdXRzZXQ7CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKDAsIGNvdmVyYWdlKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7Cgl9Cglza19Qb3NpdGlvbiA9IGZsb2F0NChkZXZjb29yZC54ICwgZGV2Y29vcmQueSwgMCwgMSk7Cn0KAAEBAAAAAAAAAQEAagYAACNleHRlbnNpb24gR0xfTlZfc2hhZGVyX25vcGVyc3BlY3RpdmVfaW50ZXJwb2xhdGlvbjogcmVxdWlyZQp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fU3RhZ2UxOwpub3BlcnNwZWN0aXZlIGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm5vcGVyc3BlY3RpdmUgaW4gZmxvYXQyIHZhcmNjb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgR3JGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfU3RhZ2UwLngsIHk9dmFyY2Nvb3JkX1N0YWdlMC55OwoJCWhhbGYgY292ZXJhZ2U7CgkJaWYgKDAgPT0geF9wbHVzXzEpIAoJCXsKCQkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJCX0KCQllbHNlIAoJCXsKCQkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCQlmbiA9IGZtYSh5LHksIGZuKTsKCQkJZmxvYXQgZm53aWR0aCA9IGZ3aWR0aChmbik7CgkJCWhhbGYgZCA9IGhhbGYoZm4vZm53aWR0aCk7CgkJCWNvdmVyYWdlID0gY2xhbXAoLjUgLSBkLCAwLCAxKTsKCQl9CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoY292ZXJhZ2UpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBBQVJlY3RFZmZlY3QKCQlmbG9hdDQgcHJldlJlY3QgPSBmbG9hdDQoLTEuMDAwMDAwLCAtMS4wMDAwMDAsIC0xLjAwMDAwMCwgLTEuMDAwMDAwKTsKCQloYWxmIGFscGhhOwoJCUBzd2l0Y2ggKDEpIAoJCXsKCQkJY2FzZSAwOiAgICBjYXNlIDI6ICAgICAgICBhbHBoYSA9IGhhbGYoYWxsKGdyZWF0ZXJUaGFuKGZsb2F0NChza19GcmFnQ29vcmQueHksIHVyZWN0VW5pZm9ybV9TdGFnZTEuencpLCBmbG9hdDQodXJlY3RVbmlmb3JtX1N0YWdlMS54eSwgc2tfRnJhZ0Nvb3JkLnh5KSkpID8gMSA6IDApOwoJCQlicmVhazsKCQkJZGVmYXVsdDogICAgICAgIGhhbGYgeFN1YiwgeVN1YjsKCQkJeFN1YiA9IG1pbihoYWxmKHNrX0ZyYWdDb29yZC54IC0gdXJlY3RVbmlmb3JtX1N0YWdlMS54KSwgMC4wKTsKCQkJeFN1YiArPSBtaW4oaGFsZih1cmVjdFVuaWZvcm1fU3RhZ2UxLnogLSBza19GcmFnQ29vcmQueCksIDAuMCk7CgkJCXlTdWIgPSBtaW4oaGFsZihza19GcmFnQ29vcmQueSAtIHVyZWN0VW5pZm9ybV9TdGFnZTEueSksIDAuMCk7CgkJCXlTdWIgKz0gbWluKGhhbGYodXJlY3RVbmlmb3JtX1N0YWdlMS53IC0gc2tfRnJhZ0Nvb3JkLnkpLCAwLjApOwoJCQlhbHBoYSA9ICgxLjAgKyBtYXgoeFN1YiwgLTEuMCkpICogKDEuMCArIG1heCh5U3ViLCAtMS4wKSk7CgkJfQoJCUBpZiAoMSA9PSAyIHx8IDEgPT0gMykgCgkJewoJCQlhbHBoYSA9IDEuMCAtIGFscGhhOwoJCX0KCQloYWxmNCBpbnB1dENvbG9yID0gb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJCW91dHB1dF9TdGFnZTEgPSBpbnB1dENvbG9yICogYWxwaGE7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0X1N0YWdlMTsKCX0KfQoAAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAgAAAAOAAAAcmFkaWlfc2VsZWN0b3IAABkAAABjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzAAAAFQAAAGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZQAAAAQAAABza2V3CQAAAHRyYW5zbGF0ZQAAAAcAAAByYWRpaV94AAcAAAByYWRpaV95AAUAAABjb2xvcgAAAAEAAAAAAAAA","CAZAAAICBEAAAAAAAEABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAGAAAAAAAAAIAAAACYAAVAACAAAAAAAAAAAACAAAAAPAAKUAA":"AgAAAExTS1NiAgAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0Owp1bmlmb3JtIGZsb2F0NCB1bG9jYWxNYXRyaXhfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gZmxvYXQ0IGluQ2lyY2xlRWRnZTsKbm9wZXJzcGVjdGl2ZSBvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpub3BlcnNwZWN0aXZlIG91dCBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCgl2aW5DaXJjbGVFZGdlX1N0YWdlMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1N0YWdlMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gdWxvY2FsTWF0cml4X1N0YWdlMC54eiAqIGluUG9zaXRpb24gKyB1bG9jYWxNYXRyaXhfU3RhZ2UwLnl3OwoJc2tfUG9zaXRpb24gPSBmbG9hdDQoX3RtcF8wX2luUG9zaXRpb24ueCAsIF90bXBfMF9pblBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAAEBAAAAAAAAAQEADgYAACNleHRlbnNpb24gR0xfTlZfc2hhZGVyX25vcGVyc3BlY3RpdmVfaW50ZXJwb2xhdGlvbjogcmVxdWlyZQp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fU3RhZ2UxOwpub3BlcnNwZWN0aXZlIGluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1N0YWdlMDsKbm9wZXJzcGVjdGl2ZSBpbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCQlmbG9hdDQgY2lyY2xlRWRnZTsKCQljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgkJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoZWRnZUFscGhhKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgQUFSZWN0RWZmZWN0CgkJZmxvYXQ0IHByZXZSZWN0ID0gZmxvYXQ0KC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDAsIC0xLjAwMDAwMCk7CgkJaGFsZiBhbHBoYTsKCQlAc3dpdGNoICgxKSAKCQl7CgkJCWNhc2UgMDogICAgY2FzZSAyOiAgICAgICAgYWxwaGEgPSBoYWxmKGFsbChncmVhdGVyVGhhbihmbG9hdDQoc2tfRnJhZ0Nvb3JkLnh5LCB1cmVjdFVuaWZvcm1fU3RhZ2UxLnp3KSwgZmxvYXQ0KHVyZWN0VW5pZm9ybV9TdGFnZTEueHksIHNrX0ZyYWdDb29yZC54eSkpKSA/IDEgOiAwKTsKCQkJYnJlYWs7CgkJCWRlZmF1bHQ6ICAgICAgICBoYWxmIHhTdWIsIHlTdWI7CgkJCXhTdWIgPSBtaW4oaGFsZihza19GcmFnQ29vcmQueCAtIHVyZWN0VW5pZm9ybV9TdGFnZTEueCksIDAuMCk7CgkJCXhTdWIgKz0gbWluKGhhbGYodXJlY3RVbmlmb3JtX1N0YWdlMS56IC0gc2tfRnJhZ0Nvb3JkLngpLCAwLjApOwoJCQl5U3ViID0gbWluKGhhbGYoc2tfRnJhZ0Nvb3JkLnkgLSB1cmVjdFVuaWZvcm1fU3RhZ2UxLnkpLCAwLjApOwoJCQl5U3ViICs9IG1pbihoYWxmKHVyZWN0VW5pZm9ybV9TdGFnZTEudyAtIHNrX0ZyYWdDb29yZC55KSwgMC4wKTsKCQkJYWxwaGEgPSAoMS4wICsgbWF4KHhTdWIsIC0xLjApKSAqICgxLjAgKyBtYXgoeVN1YiwgLTEuMCkpOwoJCX0KCQlAaWYgKDEgPT0gMiB8fCAxID09IDMpIAoJCXsKCQkJYWxwaGEgPSAxLjAgLSBhbHBoYTsKCQl9CgkJaGFsZjQgaW5wdXRDb2xvciA9IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCQlvdXRwdXRfU3RhZ2UxID0gaW5wdXRDb2xvciAqIGFscGhhOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dF9TdGFnZTE7Cgl9Cn0KAAAAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAAwAAABpbkNpcmNsZUVkZ2UBAAAAAAAAAA==","CAZAAAMCBEAAAHYBAEACKAAAAAJQAAIA7777777777776EYAAEAP777777777777AAAAAABAABMAAAAAAAAACAAAAAYAAHIAAQAAAAAAAAAAAAQAAAAEAACVAA":"AgAAAExTS1NQAQAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0OwppbiBmbG9hdDIgcG9zaXRpb247CmluIGZsb2F0MiBsb2NhbENvb3JkOwpub3BlcnNwZWN0aXZlIG91dCBmbG9hdDIgdmxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZsb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAEBAAAAAAAAAQEAvAkAACNleHRlbnNpb24gR0xfTlZfc2hhZGVyX25vcGVyc3BlY3RpdmVfaW50ZXJwb2xhdGlvbjogcmVxdWlyZQp1bmlmb3JtIGhhbGYgdVNyY1RGX1N0YWdlMFs3XTsKdW5pZm9ybSBoYWxmM3gzIHVDb2xvclhmb3JtX1N0YWdlMDsKdW5pZm9ybSBoYWxmIHVEc3RURl9TdGFnZTBbN107CnVuaWZvcm0gZmxvYXQ0IHVjaXJjbGVfU3RhZ2UxOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTA7Cm5vcGVyc3BlY3RpdmUgaW4gZmxvYXQyIHZsb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZiBzcmNfdGZfU3RhZ2UwKGhhbGYgeCkgCnsKCWhhbGYgRyA9IHVTcmNURl9TdGFnZTBbMF07CgloYWxmIEEgPSB1U3JjVEZfU3RhZ2UwWzFdOwoJaGFsZiBCID0gdVNyY1RGX1N0YWdlMFsyXTsKCWhhbGYgQyA9IHVTcmNURl9TdGFnZTBbM107CgloYWxmIEQgPSB1U3JjVEZfU3RhZ2UwWzRdOwoJaGFsZiBFID0gdVNyY1RGX1N0YWdlMFs1XTsKCWhhbGYgRiA9IHVTcmNURl9TdGFnZTBbNl07CgloYWxmIHMgPSBzaWduKHgpOwoJeCA9IGFicyh4KTsKCXggPSAoeCA8IEQpID8gKEMgKiB4KSArIEYgOiBwb3coQSAqIHggKyBCLCBHKSArIEU7CglyZXR1cm4gcyAqIHg7Cn0KaGFsZiBkc3RfdGZfU3RhZ2UwKGhhbGYgeCkgCnsKCWhhbGYgRyA9IHVEc3RURl9TdGFnZTBbMF07CgloYWxmIEEgPSB1RHN0VEZfU3RhZ2UwWzFdOwoJaGFsZiBCID0gdURzdFRGX1N0YWdlMFsyXTsKCWhhbGYgQyA9IHVEc3RURl9TdGFnZTBbM107CgloYWxmIEQgPSB1RHN0VEZfU3RhZ2UwWzRdOwoJaGFsZiBFID0gdURzdFRGX1N0YWdlMFs1XTsKCWhhbGYgRiA9IHVEc3RURl9TdGFnZTBbNl07CgloYWxmIHMgPSBzaWduKHgpOwoJeCA9IGFicyh4KTsKCXggPSAoeCA8IEQpID8gKEMgKiB4KSArIEYgOiBwb3coQSAqIHggKyBCLCBHKSArIEU7CglyZXR1cm4gcyAqIHg7Cn0KaGFsZjQgZ2FtdXRfeGZvcm1fU3RhZ2UwKGhhbGY0IGNvbG9yKSAKewoJY29sb3IucmdiID0gKHVDb2xvclhmb3JtX1N0YWdlMCAqIGNvbG9yLnJnYik7CglyZXR1cm4gY29sb3I7Cn0KaGFsZjQgY29sb3JfeGZvcm1fU3RhZ2UwKGhhbGY0IGNvbG9yKSAKewoJY29sb3IgPSB1bnByZW11bChjb2xvcik7Cgljb2xvci5yID0gc3JjX3RmX1N0YWdlMChoYWxmKGNvbG9yLnIpKTsKCWNvbG9yLmcgPSBzcmNfdGZfU3RhZ2UwKGhhbGYoY29sb3IuZykpOwoJY29sb3IuYiA9IHNyY190Zl9TdGFnZTAoaGFsZihjb2xvci5iKSk7Cgljb2xvciA9IGdhbXV0X3hmb3JtX1N0YWdlMChoYWxmNChjb2xvcikpOwoJY29sb3IuciA9IGRzdF90Zl9TdGFnZTAoaGFsZihjb2xvci5yKSk7Cgljb2xvci5nID0gZHN0X3RmX1N0YWdlMChoYWxmKGNvbG9yLmcpKTsKCWNvbG9yLmIgPSBkc3RfdGZfU3RhZ2UwKGhhbGYoY29sb3IuYikpOwoJY29sb3IucmdiICo9IGNvbG9yLmE7CglyZXR1cm4gaGFsZjQoY29sb3IpOwp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gaGFsZjQoMSk7CgkJZmxvYXQyIHRleENvb3JkOwoJCXRleENvb3JkID0gdmxvY2FsQ29vcmRfU3RhZ2UwOwoJCW91dHB1dENvbG9yX1N0YWdlMCA9IChjb2xvcl94Zm9ybV9TdGFnZTAoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMCwgdGV4Q29vcmQpKSAqIG91dHB1dENvbG9yX1N0YWdlMCk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQoMSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIENpcmNsZUVmZmVjdAoJCWZsb2F0MiBwcmV2Q2VudGVyOwoJCWZsb2F0IHByZXZSYWRpdXMgPSAtMS4wMDAwMDA7CgkJaGFsZiBkOwoJCUBpZiAoMSA9PSAyIHx8IDEgPT0gMykgCgkJewoJCQlkID0gaGFsZigobGVuZ3RoKCh1Y2lyY2xlX1N0YWdlMS54eSAtIHNrX0ZyYWdDb29yZC54eSkgKiB1Y2lyY2xlX1N0YWdlMS53KSAtIDEuMCkgKiB1Y2lyY2xlX1N0YWdlMS56KTsKCQl9CgkJZWxzZSAKCQl7CgkJCWQgPSBoYWxmKCgxLjAgLSBsZW5ndGgoKHVjaXJjbGVfU3RhZ2UxLnh5IC0gc2tfRnJhZ0Nvb3JkLnh5KSAqIHVjaXJjbGVfU3RhZ2UxLncpKSAqIHVjaXJjbGVfU3RhZ2UxLnopOwoJCX0KCQloYWxmNCBpbnB1dENvbG9yID0gb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJCUBpZiAoMSA9PSAxIHx8IDEgPT0gMykgCgkJewoJCQlvdXRwdXRfU3RhZ2UxID0gaW5wdXRDb2xvciAqIGNsYW1wKGQsIDAuMCwgMS4wKTsKCQl9CgkJZWxzZSAKCQl7CgkJCW91dHB1dF9TdGFnZTEgPSBkID4gMC41ID8gaW5wdXRDb2xvciA6IGhhbGY0KDAuMCk7CgkJfQoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dF9TdGFnZTE7Cgl9Cn0KAAEBAAEAAAABAAAAAQAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","CAZAAAACAYAAAAQMAAABGAABAAOAAEIACMAACAATAAAQAFAABYAAAAAAAAAAAAAAAIAAAABEABKQA":"AgAAAExTS1NfAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdXZpZXdNYXRyaXhfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gZmxvYXQyIGluRWxsaXBzZU9mZnNldHMwOwppbiBmbG9hdDIgaW5FbGxpcHNlT2Zmc2V0czE7Cm91dCBmbG9hdDIgdkVsbGlwc2VPZmZzZXRzMF9TdGFnZTA7Cm91dCBmbG9hdDIgdkVsbGlwc2VPZmZzZXRzMV9TdGFnZTA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIERJRWxsaXBzZUdlb21ldHJ5UHJvY2Vzc29yCgl2RWxsaXBzZU9mZnNldHMwX1N0YWdlMCA9IGluRWxsaXBzZU9mZnNldHMwOwoJdkVsbGlwc2VPZmZzZXRzMV9TdGFnZTAgPSBpbkVsbGlwc2VPZmZzZXRzMTsKCXZpbkNvbG9yX1N0YWdlMCA9IGluQ29sb3I7CglmbG9hdDMgX3RtcF8wX2luUG9zaXRpb24gPSAodXZpZXdNYXRyaXhfU3RhZ2UwICogaW5Qb3NpdGlvbi54eTEpOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQoX3RtcF8wX2luUG9zaXRpb24ueCAsIF90bXBfMF9pblBvc2l0aW9uLnksIDAsIF90bXBfMF9pblBvc2l0aW9uLnopOwp9CgAAAAAAAAAAAAAAAADgAwAAaW4gZmxvYXQyIHZFbGxpcHNlT2Zmc2V0czBfU3RhZ2UwOwppbiBmbG9hdDIgdkVsbGlwc2VPZmZzZXRzMV9TdGFnZTA7CmluIGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBESUVsbGlwc2VHZW9tZXRyeVByb2Nlc3NvcgoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZpbkNvbG9yX1N0YWdlMDsKCQlmbG9hdDIgc2NhbGVkT2Zmc2V0ID0gdkVsbGlwc2VPZmZzZXRzMF9TdGFnZTAueHk7CgkJZmxvYXQgdGVzdCA9IGRvdChzY2FsZWRPZmZzZXQsIHNjYWxlZE9mZnNldCkgLSAxLjA7CgkJZmxvYXQyIGR1dmR4ID0gZEZkeCh2RWxsaXBzZU9mZnNldHMwX1N0YWdlMC54eSk7CgkJZmxvYXQyIGR1dmR5ID0gZEZkeSh2RWxsaXBzZU9mZnNldHMwX1N0YWdlMC54eSk7CgkJZmxvYXQyIGdyYWQgPSBmbG9hdDIodkVsbGlwc2VPZmZzZXRzMF9TdGFnZTAueCpkdXZkeC54ICsgdkVsbGlwc2VPZmZzZXRzMF9TdGFnZTAueSpkdXZkeC55LCAgICAgICAgICAgICAgICAgICAgIHZFbGxpcHNlT2Zmc2V0czBfU3RhZ2UwLngqZHV2ZHkueCArIHZFbGxpcHNlT2Zmc2V0czBfU3RhZ2UwLnkqZHV2ZHkueSk7CgkJZmxvYXQgZ3JhZF9kb3QgPSA0LjAqZG90KGdyYWQsIGdyYWQpOwoJCWdyYWRfZG90ID0gbWF4KGdyYWRfZG90LCAxLjE3NTVlLTM4KTsKCQlmbG9hdCBpbnZsZW4gPSBpbnZlcnNlc3FydChncmFkX2RvdCk7CgkJZmxvYXQgZWRnZUFscGhhID0gc2F0dXJhdGUoMC41LXRlc3QqaW52bGVuKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChoYWxmKGVkZ2VBbHBoYSkpOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCX0KfQoAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAAEAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yABEAAABpbkVsbGlwc2VPZmZzZXRzMAAAABEAAABpbkVsbGlwc2VPZmZzZXRzMQAAAAEAAAAAAAAA","CAZAAAACA4AAAAAAAMAACAAAAAAQAAAACMAACAA4AAIQADYACQAAAAAAAAMAALAAAAAAAAAAAAAAAAQAAAACYACVAA":"AgAAAExTS1NKAwAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQyIHVBdGxhc0RpbWVuc2lvbnNJbnZfU3RhZ2UwOwp1bmlmb3JtIGZsb2F0M3gzIHV2aWV3TWF0cml4X1N0YWdlMDsKaW4gZmxvYXQyIGluUG9zaXRpb247CmluIGhhbGY0IGluQ29sb3I7CmluIHVzaG9ydDIgaW5UZXh0dXJlQ29vcmRzOwpvdXQgZmxvYXQyIHZUZXh0dXJlQ29vcmRzX1N0YWdlMDsKZmxhdCBvdXQgaW50IHZUZXhJbmRleF9TdGFnZTA7Cm91dCBmbG9hdDIgdkludFRleHR1cmVDb29yZHNfU3RhZ2UwOwpvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBEaXN0YW5jZUZpZWxkUGF0aAoJaW50MiBzaWduZWRDb29yZHMgPSBpbnQyKGluVGV4dHVyZUNvb3Jkcy54LCBpblRleHR1cmVDb29yZHMueSk7CglmbG9hdDIgdW5vcm1UZXhDb29yZHMgPSBmbG9hdDIoc2lnbmVkQ29vcmRzLngvMiwgc2lnbmVkQ29vcmRzLnkvMik7CglpbnQgdGV4SWR4ID0gMDsKCXZUZXh0dXJlQ29vcmRzX1N0YWdlMCA9IHVub3JtVGV4Q29vcmRzICogdUF0bGFzRGltZW5zaW9uc0ludl9TdGFnZTA7Cgl2VGV4SW5kZXhfU3RhZ2UwID0gdGV4SWR4OwoJdkludFRleHR1cmVDb29yZHNfU3RhZ2UwID0gdW5vcm1UZXhDb29yZHM7Cgl2aW5Db2xvcl9TdGFnZTAgPSBpbkNvbG9yOwoJZmxvYXQzIF90bXBfMF9pblBvc2l0aW9uID0gKHV2aWV3TWF0cml4X1N0YWdlMCAqIGluUG9zaXRpb24ueHkxKTsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KF90bXBfMF9pblBvc2l0aW9uLnggLCBfdG1wXzBfaW5Qb3NpdGlvbi55LCAwLCBfdG1wXzBfaW5Qb3NpdGlvbi56KTsKfQoAAAAAAAAAAAAAAAAAAMYEAAB1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTA7CmluIGZsb2F0MiB2VGV4dHVyZUNvb3Jkc19TdGFnZTA7CmZsYXQgaW4gaW50IHZUZXhJbmRleF9TdGFnZTA7CmluIGZsb2F0MiB2SW50VGV4dHVyZUNvb3Jkc19TdGFnZTA7CmluIGhhbGY0IHZpbkNvbG9yX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBEaXN0YW5jZUZpZWxkUGF0aAoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZpbkNvbG9yX1N0YWdlMDsKCQlmbG9hdDIgdXYgPSB2VGV4dHVyZUNvb3Jkc19TdGFnZTA7CgkJaGFsZjQgdGV4Q29sb3I7CgkJewoJCQl0ZXhDb2xvciA9IHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTAsIHV2KTsKCQl9CgkJaGFsZiBkaXN0YW5jZSA9IDcuOTY4NzUqKHRleENvbG9yLnIgLSAwLjUwMTk2MDc4NDMxKTsKCQloYWxmIGFmd2lkdGg7CgkJaGFsZjIgZGlzdF9ncmFkID0gaGFsZjIoZEZkeChkaXN0YW5jZSksIGRGZHkoZGlzdGFuY2UpKTsKCQloYWxmIGRnX2xlbjIgPSBkb3QoZGlzdF9ncmFkLCBkaXN0X2dyYWQpOwoJCWlmIChkZ19sZW4yIDwgMC4wMDAxKSAKCQl7CgkJCWRpc3RfZ3JhZCA9IGhhbGYyKDAuNzA3MSwgMC43MDcxKTsKCQl9CgkJZWxzZSAKCQl7CgkJCWRpc3RfZ3JhZCA9IGRpc3RfZ3JhZCpoYWxmKGludmVyc2VzcXJ0KGRnX2xlbjIpKTsKCQl9CgkJaGFsZjIgSmR4ID0gaGFsZjIoZEZkeCh2SW50VGV4dHVyZUNvb3Jkc19TdGFnZTApKTsKCQloYWxmMiBKZHkgPSBoYWxmMihkRmR5KHZJbnRUZXh0dXJlQ29vcmRzX1N0YWdlMCkpOwoJCWhhbGYyIGdyYWQgPSBoYWxmMihkaXN0X2dyYWQueCpKZHgueCArIGRpc3RfZ3JhZC55KkpkeS54LCAgICAgICAgICAgICAgICAgICBkaXN0X2dyYWQueCpKZHgueSArIGRpc3RfZ3JhZC55KkpkeS55KTsKCQlhZndpZHRoID0gMC42NSpsZW5ndGgoZ3JhZCk7CgkJaGFsZiB2YWwgPSBzbW9vdGhzdGVwKC1hZndpZHRoLCBhZndpZHRoLCBkaXN0YW5jZSk7CgkJb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwID0gaGFsZjQodmFsKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAADAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yAA8AAABpblRleHR1cmVDb29yZHMAAQAAAAAAAAA=","CAZACAACB4AAAAAAAEABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAGAAAAAAAAKAAIYAAIAAAAAYAANAABAAAAABYAA4AABAAAAAAAAAAABCAAHQAAQAAAAAAAAAAAAB4AA6AAPAAHQAAAAAALQADYAAEAAAAAAAAAAAAEAAAABWAAVIA":"AgAAAExTS1McAwAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0Owp1bmlmb3JtIGZsb2F0NCB1bG9jYWxNYXRyaXhfU3RhZ2UwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gZmxvYXQ0IGluQ2lyY2xlRWRnZTsKbm9wZXJzcGVjdGl2ZSBvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpub3BlcnNwZWN0aXZlIG91dCBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm5vcGVyc3BlY3RpdmUgb3V0IGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCgl2aW5DaXJjbGVFZGdlX1N0YWdlMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1N0YWdlMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gdWxvY2FsTWF0cml4X1N0YWdlMC54eiAqIGluUG9zaXRpb24gKyB1bG9jYWxNYXRyaXhfU3RhZ2UwLnl3OwoJc2tfUG9zaXRpb24gPSBmbG9hdDQoX3RtcF8wX2luUG9zaXRpb24ueCAsIF90bXBfMF9pblBvc2l0aW9uLnksIDAsIDEpOwoJewoJCXZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMCA9ICgoKHVtYXRyaXhfU3RhZ2UxX2MwX2MwKSkgKiBfdG1wXzFfaW5Qb3NpdGlvbi54eTEpLnh5OwoJfQp9CgAAAAAAAAAAAAAAAI0IAAAjZXh0ZW5zaW9uIEdMX05WX3NoYWRlcl9ub3BlcnNwZWN0aXZlX2ludGVycG9sYXRpb246IHJlcXVpcmUKdW5pZm9ybSBoYWxmNCB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKdW5pZm9ybSBoYWxmNCB1cmlnaHRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CnVuaWZvcm0gaGFsZjQgdXN0YXJ0X1N0YWdlMV9jMF9jMTsKdW5pZm9ybSBoYWxmNCB1ZW5kX1N0YWdlMV9jMF9jMTsKbm9wZXJzcGVjdGl2ZSBpbiBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TdGFnZTA7Cm5vcGVyc3BlY3RpdmUgaW4gaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwpub3BlcnNwZWN0aXZlIGluIGZsb2F0MiB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CmhhbGY0IExpbmVhckdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZiB0ID0gaGFsZih2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAueCkgKyA5Ljk5OTk5OTc0NzM3ODc1MTZlLTA2OwoJX291dHB1dCA9IGhhbGY0KHQsIDEuMCwgMC4wLCAwLjApOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJX291dHB1dCA9IExpbmVhckdyYWRpZW50TGF5b3V0X1N0YWdlMV9jMF9jMF9jMChfaW5wdXQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgU2luZ2xlSW50ZXJ2YWxHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGYgdCA9IF9pbnB1dC54OwoJX291dHB1dCA9ICgxLjAgLSB0KSAqIHVzdGFydF9TdGFnZTFfYzBfYzEgKyB0ICogdWVuZF9TdGFnZTFfYzBfYzE7CglyZXR1cm4gX291dHB1dDsKfQpoYWxmNCBDbGFtcGVkR3JhZGllbnRFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmNCB0ID0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMF9jMChoYWxmNCgxKSk7CglpZiAoIXRydWUgJiYgdC55IDwgMC4wKSAKCXsKCQlfb3V0cHV0ID0gaGFsZjQoMC4wKTsKCX0KCWVsc2UgaWYgKHQueCA8IDAuMCkgCgl7CgkJX291dHB1dCA9IHVsZWZ0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSBpZiAodC54ID4gMS4wKSAKCXsKCQlfb3V0cHV0ID0gdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwoJfQoJZWxzZSAKCXsKCQlfb3V0cHV0ID0gU2luZ2xlSW50ZXJ2YWxHcmFkaWVudENvbG9yaXplcl9TdGFnZTFfYzBfYzEodCk7Cgl9CglAaWYgKGZhbHNlKSAKCXsKCQlfb3V0cHV0Lnh5eiAqPSBfb3V0cHV0Lnc7Cgl9CglyZXR1cm4gX291dHB1dDsKfQp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCgkJZmxvYXQ0IGNpcmNsZUVkZ2U7CgkJY2lyY2xlRWRnZSA9IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZpbkNvbG9yX1N0YWdlMDsKCQlmbG9hdCBkID0gbGVuZ3RoKGNpcmNsZUVkZ2UueHkpOwoJCWhhbGYgZGlzdGFuY2VUb091dGVyRWRnZSA9IGhhbGYoY2lyY2xlRWRnZS56ICogKDEuMCAtIGQpKTsKCQloYWxmIGVkZ2VBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9PdXRlckVkZ2UpOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIE92ZXJyaWRlSW5wdXRGcmFnbWVudFByb2Nlc3NvcgoJCWhhbGY0IGNvbnN0Q29sb3I7CgkJQGlmIChmYWxzZSkgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMCk7CgkJfQoJCWVsc2UgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDApOwoJCX0KCQlvdXRwdXRfU3RhZ2UxID0gQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChjb25zdENvbG9yKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","CAZAAAACBAAAAAAAAAACCAAAAAKAAAQA7777777777776EYAAEAP777777777777AAAAAABAABMAAAAAAAAAAAAAAABAAAAAGQAFKAA":"AgAAAExTS1MOAQAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CmluIGZsb2F0MyBwb3NpdGlvbjsKaW4gZmxvYXQyIGxvY2FsQ29vcmQ7Cm91dCBmbG9hdDIgdmxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZsb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgcG9zaXRpb24ueik7Cn0KAAAAAAAAAAAAAAAAAAAWAgAAdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UwOwppbiBmbG9hdDIgdmxvY2FsQ29vcmRfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJCW91dHB1dENvbG9yX1N0YWdlMCA9IGhhbGY0KDEpOwoJCWZsb2F0MiB0ZXhDb29yZDsKCQl0ZXhDb29yZCA9IHZsb2NhbENvb3JkX1N0YWdlMDsKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSAoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMCwgdGV4Q29vcmQpICogb3V0cHV0Q29sb3JfU3RhZ2UwKTsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl9Cn0KAAAAAAAAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZACAACBUAAABYAAAABGAABAAOAAEIADQAAGAAQABLQAAAAAAABQACGAACAAAAAEAADIAAIAAAAAKAAHAAAIAAAAAAAAAAAGQAB4AAEAAAAAAAAAAAAAPAAHQADYAB4AAAAAACMAA6AABAAAAAAAAAAAABAAAAALQAFKAA":"AgAAAExTS1PiAgAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0Owp1bmlmb3JtIGZsb2F0M3gzIHVsb2NhbE1hdHJpeF9TdGFnZTA7CnVuaWZvcm0gZmxvYXQzeDMgdW1hdHJpeF9TdGFnZTFfYzBfYzA7CmluIGZsb2F0MiBpblBvc2l0aW9uOwppbiBoYWxmNCBpbkNvbG9yOwppbiBoYWxmNCBpblF1YWRFZGdlOwpub3BlcnNwZWN0aXZlIG91dCBoYWxmNCB2UXVhZEVkZ2VfU3RhZ2UwOwpub3BlcnNwZWN0aXZlIG91dCBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm5vcGVyc3BlY3RpdmUgb3V0IGZsb2F0MyB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIFF1YWRFZGdlCgl2UXVhZEVkZ2VfU3RhZ2UwID0gaW5RdWFkRWRnZTsKCXZpbkNvbG9yX1N0YWdlMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQzIF90bXBfMV9pblBvc2l0aW9uID0gKHVsb2NhbE1hdHJpeF9TdGFnZTAgKiBpblBvc2l0aW9uLnh5MSk7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChfdG1wXzBfaW5Qb3NpdGlvbi54ICwgX3RtcF8wX2luUG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCh1bWF0cml4X1N0YWdlMV9jMF9jMCkpICogX3RtcF8xX2luUG9zaXRpb247Cgl9Cn0KAAAAAAAAAAAAAAAAAAD9CQAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjQgdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIGhhbGY0IHVzdGFydF9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWVuZF9TdGFnZTFfYzBfYzE7Cm5vcGVyc3BlY3RpdmUgaW4gaGFsZjQgdlF1YWRFZGdlX1N0YWdlMDsKbm9wZXJzcGVjdGl2ZSBpbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm5vcGVyc3BlY3RpdmUgaW4gZmxvYXQzIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgTGluZWFyR3JhZGllbnRMYXlvdXRfU3RhZ2UxX2MwX2MwX2MwKGhhbGY0IF9pbnB1dCkgCnsKCWZsb2F0MiBfY29vcmRzID0gdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwLnh5IC8gdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwLno7CgloYWxmNCBfb3V0cHV0OwoJaGFsZiB0ID0gaGFsZihfY29vcmRzLngpICsgOS45OTk5OTk3NDczNzg3NTE2ZS0wNjsKCV9vdXRwdXQgPSBoYWxmNCh0LCAxLjAsIDAuMCwgMC4wKTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCV9vdXRwdXQgPSBMaW5lYXJHcmFkaWVudExheW91dF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0KTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IFNpbmdsZUludGVydmFsR3JhZGllbnRDb2xvcml6ZXJfU3RhZ2UxX2MwX2MxKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBfaW5wdXQueDsKCV9vdXRwdXQgPSAoMS4wIC0gdCkgKiB1c3RhcnRfU3RhZ2UxX2MwX2MxICsgdCAqIHVlbmRfU3RhZ2UxX2MwX2MxOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQoMSkpOwoJaWYgKCF0cnVlICYmIHQueSA8IDAuMCkgCgl7CgkJX291dHB1dCA9IGhhbGY0KDAuMCk7Cgl9CgllbHNlIGlmICh0LnggPCAwLjApIAoJewoJCV9vdXRwdXQgPSB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKCX0KCWVsc2UgaWYgKHQueCA+IDEuMCkgCgl7CgkJX291dHB1dCA9IHVyaWdodEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKCX0KCWVsc2UgCgl7CgkJX291dHB1dCA9IFNpbmdsZUludGVydmFsR3JhZGllbnRDb2xvcml6ZXJfU3RhZ2UxX2MwX2MxKHQpOwoJfQoJQGlmIChmYWxzZSkgCgl7CgkJX291dHB1dC54eXogKj0gX291dHB1dC53OwoJfQoJcmV0dXJuIF9vdXRwdXQ7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkRWRnZQoJCW91dHB1dENvbG9yX1N0YWdlMCA9IHZpbkNvbG9yX1N0YWdlMDsKCQloYWxmIGVkZ2VBbHBoYTsKCQloYWxmMiBkdXZkeCA9IGhhbGYyKGRGZHgodlF1YWRFZGdlX1N0YWdlMC54eSkpOwoJCWhhbGYyIGR1dmR5ID0gaGFsZjIoZEZkeSh2UXVhZEVkZ2VfU3RhZ2UwLnh5KSk7CgkJaWYgKHZRdWFkRWRnZV9TdGFnZTAueiA+IDAuMCAmJiB2UXVhZEVkZ2VfU3RhZ2UwLncgPiAwLjApIAoJCXsKCQkJZWRnZUFscGhhID0gbWluKG1pbih2UXVhZEVkZ2VfU3RhZ2UwLnosIHZRdWFkRWRnZV9TdGFnZTAudykgKyAwLjUsIDEuMCk7CgkJfQoJCWVsc2UgCgkJewoJCQloYWxmMiBnRiA9IGhhbGYyKDIuMCp2UXVhZEVkZ2VfU3RhZ2UwLngqZHV2ZHgueCAtIGR1dmR4LnksICAgICAgICAgICAgICAgMi4wKnZRdWFkRWRnZV9TdGFnZTAueCpkdXZkeS54IC0gZHV2ZHkueSk7CgkJCWVkZ2VBbHBoYSA9ICh2UXVhZEVkZ2VfU3RhZ2UwLngqdlF1YWRFZGdlX1N0YWdlMC54IC0gdlF1YWRFZGdlX1N0YWdlMC55KTsKCQkJZWRnZUFscGhhID0gc2F0dXJhdGUoMC41IC0gZWRnZUFscGhhIC8gbGVuZ3RoKGdGKSk7CgkJfQoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIE92ZXJyaWRlSW5wdXRGcmFnbWVudFByb2Nlc3NvcgoJCWhhbGY0IGNvbnN0Q29sb3I7CgkJQGlmIChmYWxzZSkgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMCk7CgkJfQoJCWVsc2UgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDApOwoJCX0KCQlvdXRwdXRfU3RhZ2UxID0gQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChjb25zdENvbG9yKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IACgAAAGluUXVhZEVkZ2UAAAEAAAAAAAAA","CAZAAAMCA4AAAAQMAAABGAABAAOAAEIACMAACAATAAAQAFAABYAAAAAAAAQQAAAAEAACMAAEAAAAAAAAAAAAEAAAAAYAAVIA":"AgAAAExTS1NfAgAAdW5pZm9ybSBmbG9hdDQgc2tfUlRBZGp1c3Q7CnVuaWZvcm0gZmxvYXQzeDMgdXZpZXdNYXRyaXhfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gZmxvYXQyIGluRWxsaXBzZU9mZnNldHMwOwppbiBmbG9hdDIgaW5FbGxpcHNlT2Zmc2V0czE7Cm91dCBmbG9hdDIgdkVsbGlwc2VPZmZzZXRzMF9TdGFnZTA7Cm91dCBmbG9hdDIgdkVsbGlwc2VPZmZzZXRzMV9TdGFnZTA7Cm91dCBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIERJRWxsaXBzZUdlb21ldHJ5UHJvY2Vzc29yCgl2RWxsaXBzZU9mZnNldHMwX1N0YWdlMCA9IGluRWxsaXBzZU9mZnNldHMwOwoJdkVsbGlwc2VPZmZzZXRzMV9TdGFnZTAgPSBpbkVsbGlwc2VPZmZzZXRzMTsKCXZpbkNvbG9yX1N0YWdlMCA9IGluQ29sb3I7CglmbG9hdDMgX3RtcF8wX2luUG9zaXRpb24gPSAodXZpZXdNYXRyaXhfU3RhZ2UwICogaW5Qb3NpdGlvbi54eTEpOwoJc2tfUG9zaXRpb24gPSBmbG9hdDQoX3RtcF8wX2luUG9zaXRpb24ueCAsIF90bXBfMF9pblBvc2l0aW9uLnksIDAsIF90bXBfMF9pblBvc2l0aW9uLnopOwp9CgAAAQEAAAAAAAABAQB/BgAAdW5pZm9ybSBoYWxmMyB1ZWRnZXNfU3RhZ2UxWzRdOwppbiBmbG9hdDIgdkVsbGlwc2VPZmZzZXRzMF9TdGFnZTA7CmluIGZsb2F0MiB2RWxsaXBzZU9mZnNldHMxX1N0YWdlMDsKaW4gaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIERJRWxsaXBzZUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJCWZsb2F0MiBzY2FsZWRPZmZzZXQgPSB2RWxsaXBzZU9mZnNldHMwX1N0YWdlMC54eTsKCQlmbG9hdCB0ZXN0ID0gZG90KHNjYWxlZE9mZnNldCwgc2NhbGVkT2Zmc2V0KSAtIDEuMDsKCQlmbG9hdDIgZHV2ZHggPSBkRmR4KHZFbGxpcHNlT2Zmc2V0czBfU3RhZ2UwLnh5KTsKCQlmbG9hdDIgZHV2ZHkgPSBkRmR5KHZFbGxpcHNlT2Zmc2V0czBfU3RhZ2UwLnh5KTsKCQlmbG9hdDIgZ3JhZCA9IGZsb2F0Mih2RWxsaXBzZU9mZnNldHMwX1N0YWdlMC54KmR1dmR4LnggKyB2RWxsaXBzZU9mZnNldHMwX1N0YWdlMC55KmR1dmR4LnksICAgICAgICAgICAgICAgICAgICAgdkVsbGlwc2VPZmZzZXRzMF9TdGFnZTAueCpkdXZkeS54ICsgdkVsbGlwc2VPZmZzZXRzMF9TdGFnZTAueSpkdXZkeS55KTsKCQlmbG9hdCBncmFkX2RvdCA9IDQuMCpkb3QoZ3JhZCwgZ3JhZCk7CgkJZ3JhZF9kb3QgPSBtYXgoZ3JhZF9kb3QsIDEuMTc1NWUtMzgpOwoJCWZsb2F0IGludmxlbiA9IGludmVyc2VzcXJ0KGdyYWRfZG90KTsKCQlmbG9hdCBlZGdlQWxwaGEgPSBzYXR1cmF0ZSgwLjUtdGVzdCppbnZsZW4pOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGhhbGYoZWRnZUFscGhhKSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIENvbnZleFBvbHkKCQloYWxmIGFscGhhID0gMS4wOwoJCWhhbGYgZWRnZTsKCQllZGdlID0gZG90KHVlZGdlc19TdGFnZTFbMF0sIGhhbGYzKGhhbGYoc2tfRnJhZ0Nvb3JkLngpLCBoYWxmKHNrX0ZyYWdDb29yZC55KSwgMSkpOwoJCWVkZ2UgPSBzYXR1cmF0ZShlZGdlKTsKCQlhbHBoYSAqPSBlZGdlOwoJCWVkZ2UgPSBkb3QodWVkZ2VzX1N0YWdlMVsxXSwgaGFsZjMoaGFsZihza19GcmFnQ29vcmQueCksIGhhbGYoc2tfRnJhZ0Nvb3JkLnkpLCAxKSk7CgkJZWRnZSA9IHNhdHVyYXRlKGVkZ2UpOwoJCWFscGhhICo9IGVkZ2U7CgkJZWRnZSA9IGRvdCh1ZWRnZXNfU3RhZ2UxWzJdLCBoYWxmMyhoYWxmKHNrX0ZyYWdDb29yZC54KSwgaGFsZihza19GcmFnQ29vcmQueSksIDEpKTsKCQllZGdlID0gc2F0dXJhdGUoZWRnZSk7CgkJYWxwaGEgKj0gZWRnZTsKCQllZGdlID0gZG90KHVlZGdlc19TdGFnZTFbM10sIGhhbGYzKGhhbGYoc2tfRnJhZ0Nvb3JkLngpLCBoYWxmKHNrX0ZyYWdDb29yZC55KSwgMSkpOwoJCWVkZ2UgPSBzYXR1cmF0ZShlZGdlKTsKCQlhbHBoYSAqPSBlZGdlOwoJCW91dHB1dF9TdGFnZTEgPSBvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgKiBhbHBoYTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgAAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAAEAAAACgAAAGluUG9zaXRpb24AAAcAAABpbkNvbG9yABEAAABpbkVsbGlwc2VPZmZzZXRzMAAAABEAAABpbkVsbGlwc2VPZmZzZXRzMQAAAAEAAAAAAAAA","CAZAAAICBMAAAAAAAAAFOAAAAAJQAAIA777777Y4AAIQAFAAAIAP777777777777EAAFQAAAAAAAAAAAAAAAAAAAAAWAASYADAAAAAAAAAAAAPAAHAAAIAAAAAAAAAAAAIAAAACMABKQA":"AgAAAExTS1PuAQAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0Owp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxOwppbiBmbG9hdDIgcG9zaXRpb247CmluIGhhbGY0IGNvbG9yOwppbiBmbG9hdDMgbG9jYWxDb29yZDsKbm9wZXJzcGVjdGl2ZSBvdXQgaGFsZjQgdmNvbG9yX1N0YWdlMDsKbm9wZXJzcGVjdGl2ZSBvdXQgZmxvYXQzIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKdm9pZCBtYWluKCkgCnsKCS8vIFByaW1pdGl2ZSBQcm9jZXNzb3IgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgl2Y29sb3JfU3RhZ2UwID0gY29sb3I7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCh1bWF0cml4X1N0YWdlMSkpICogbG9jYWxDb29yZDsKCX0KfQoAAAAAAAAAAAAAAAAAAH4DAAAjZXh0ZW5zaW9uIEdMX05WX3NoYWRlcl9ub3BlcnNwZWN0aXZlX2ludGVycG9sYXRpb246IHJlcXVpcmUKdW5pZm9ybSBmbG9hdDN4MyB1bWF0cml4X1N0YWdlMTsKdW5pZm9ybSBzYW1wbGVyMkQgdVRleHR1cmVTYW1wbGVyXzBfU3RhZ2UxOwpub3BlcnNwZWN0aXZlIGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm5vcGVyc3BlY3RpdmUgaW4gZmxvYXQzIHZUcmFuc2Zvcm1lZENvb3Jkc18wX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0KSAKewoJZmxvYXQyIF9jb29yZHMgPSB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAueHkgLyB2VHJhbnNmb3JtZWRDb29yZHNfMF9TdGFnZTAuejsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTEsIF9jb29yZHMpICogX2lucHV0KTsKCXJldHVybiBfb3V0cHV0Owp9CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgUXVhZFBlckVkZ2VBQUdlb21ldHJ5UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNCgxKTsKCX0KCWhhbGY0IG91dHB1dF9TdGFnZTE7Cgl7CgkJLy8gU3RhZ2UgMSwgTWF0cml4RWZmZWN0CgkJb3V0cHV0X1N0YWdlMSA9IFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwKG91dHB1dENvdmVyYWdlX1N0YWdlMCk7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0X1N0YWdlMTsKCX0KfQoAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAMAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAAKAAAAbG9jYWxDb29yZAAAAQAAAAAAAAA=","CAZAAAMCBEAAAAIAAAABKAADAAKQAAYACUAAGAAVAABQAEYAAEABKAADAAKQAAYADQABCABEAAXQAAAAAAAACAAAAAYAAFIAAQAAAAAAAAAAAAQAAAAEAACVAA":"AgAAAExTS1OOCQAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0OwppbiBmbG9hdDQgcmFkaWlfc2VsZWN0b3I7CmluIGZsb2F0NCBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzOwppbiBmbG9hdDQgYWFfYmxvYXRfYW5kX2NvdmVyYWdlOwppbiBmbG9hdDQgc2tldzsKaW4gZmxvYXQyIHRyYW5zbGF0ZTsKaW4gZmxvYXQ0IHJhZGlpX3g7CmluIGZsb2F0NCByYWRpaV95OwppbiBoYWxmNCBjb2xvcjsKbm9wZXJzcGVjdGl2ZSBvdXQgaGFsZjQgdmNvbG9yX1N0YWdlMDsKbm9wZXJzcGVjdGl2ZSBvdXQgZmxvYXQ0IHZhcmNjb29yZF9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIEdyRmlsbFJSZWN0T3A6OlByb2Nlc3NvcgoJdmNvbG9yX1N0YWdlMCA9IGNvbG9yOwoJZmxvYXQyIGNvcm5lciA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMueHk7CglmbG9hdDIgcmFkaXVzX291dHNldCA9IGNvcm5lcl9hbmRfcmFkaXVzX291dHNldHMuenc7CglmbG9hdDIgYWFfYmxvYXRfZGlyZWN0aW9uID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLnh5OwoJZmxvYXQgY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UuejsKCWZsb2F0IGlzX2xpbmVhcl9jb3ZlcmFnZSA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS53OwoJZmxvYXQyIHBpeGVsbGVuZ3RoID0gaW52ZXJzZXNxcnQoZmxvYXQyKGRvdChza2V3Lnh6LCBza2V3Lnh6KSwgZG90KHNrZXcueXcsIHNrZXcueXcpKSk7CglmbG9hdDQgbm9ybWFsaXplZF9heGlzX2RpcnMgPSBza2V3ICogcGl4ZWxsZW5ndGgueHl4eTsKCWZsb2F0MiBheGlzd2lkdGhzID0gKGFicyhub3JtYWxpemVkX2F4aXNfZGlycy54eSkgKyBhYnMobm9ybWFsaXplZF9heGlzX2RpcnMuencpKTsKCWZsb2F0MiBhYV9ibG9hdHJhZGl1cyA9IGF4aXN3aWR0aHMgKiBwaXhlbGxlbmd0aCAqIC41OwoJZmxvYXQ0IHJhZGlpX2FuZF9uZWlnaGJvcnMgPSByYWRpaV9zZWxlY3RvciogZmxvYXQ0eDQocmFkaWlfeCwgcmFkaWlfeSwgcmFkaWlfeC55eHd6LCByYWRpaV95Lnd6eXgpOwoJZmxvYXQyIHJhZGlpID0gcmFkaWlfYW5kX25laWdoYm9ycy54eTsKCWZsb2F0MiBuZWlnaGJvcl9yYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMuenc7CglpZiAoYW55KGdyZWF0ZXJUaGFuKGFhX2Jsb2F0cmFkaXVzLCBmbG9hdDIoMSkpKSkgCgl7CgkJY29ybmVyID0gbWF4KGFicyhjb3JuZXIpLCBhYV9ibG9hdHJhZGl1cykgKiBzaWduKGNvcm5lcik7CgkJY292ZXJhZ2UgLz0gbWF4KGFhX2Jsb2F0cmFkaXVzLngsIDEpICogbWF4KGFhX2Jsb2F0cmFkaXVzLnksIDEpOwoJCXJhZGlpID0gZmxvYXQyKDApOwoJfQoJaWYgKGFueShsZXNzVGhhbihyYWRpaSwgYWFfYmxvYXRyYWRpdXMgKiAxLjI1KSkpIAoJewoJCXJhZGlpID0gYWFfYmxvYXRyYWRpdXM7CgkJcmFkaXVzX291dHNldCA9IGZsb29yKGFicyhyYWRpdXNfb3V0c2V0KSkgKiByYWRpdXNfb3V0c2V0OwoJCWlzX2xpbmVhcl9jb3ZlcmFnZSA9IDE7Cgl9CgllbHNlIAoJewoJCXJhZGlpID0gY2xhbXAocmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCW5laWdoYm9yX3JhZGlpID0gY2xhbXAobmVpZ2hib3JfcmFkaWksIHBpeGVsbGVuZ3RoLCAyIC0gcGl4ZWxsZW5ndGgpOwoJCWZsb2F0MiBzcGFjaW5nID0gMiAtIHJhZGlpIC0gbmVpZ2hib3JfcmFkaWk7CgkJZmxvYXQyIGV4dHJhX3BhZCA9IG1heChwaXhlbGxlbmd0aCAqIC4wNjI1IC0gc3BhY2luZywgZmxvYXQyKDApKTsKCQlyYWRpaSAtPSBleHRyYV9wYWQgKiAuNTsKCX0KCWZsb2F0MiBhYV9vdXRzZXQgPSBhYV9ibG9hdF9kaXJlY3Rpb24ueHkgKiBhYV9ibG9hdHJhZGl1czsKCWZsb2F0MiB2ZXJ0ZXhwb3MgPSBjb3JuZXIgKyByYWRpdXNfb3V0c2V0ICogcmFkaWkgKyBhYV9vdXRzZXQ7CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKDAsIGNvdmVyYWdlKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7CgkJZmxvYXQyeDIgZGVyaXZhdGl2ZXMgPSBpbnZlcnNlKHNrZXdtYXRyaXgpOwoJCXZhcmNjb29yZF9TdGFnZTAuencgPSBkZXJpdmF0aXZlcyAqIChhcmNjb29yZC9yYWRpaSAqIDIpOwoJfQoJc2tfUG9zaXRpb24gPSBmbG9hdDQoZGV2Y29vcmQueCAsIGRldmNvb3JkLnksIDAsIDEpOwp9CgAAAAEBAAAAAAAAAQEAqAYAACNleHRlbnNpb24gR0xfTlZfc2hhZGVyX25vcGVyc3BlY3RpdmVfaW50ZXJwb2xhdGlvbjogcmVxdWlyZQp1bmlmb3JtIGZsb2F0NCB1cmVjdFVuaWZvcm1fU3RhZ2UxOwpub3BlcnNwZWN0aXZlIGluIGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm5vcGVyc3BlY3RpdmUgaW4gZmxvYXQ0IHZhcmNjb29yZF9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgR3JGaWxsUlJlY3RPcDo6UHJvY2Vzc29yCgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmNvbG9yX1N0YWdlMDsKCQlmbG9hdCB4X3BsdXNfMT12YXJjY29vcmRfU3RhZ2UwLngsIHk9dmFyY2Nvb3JkX1N0YWdlMC55OwoJCWhhbGYgY292ZXJhZ2U7CgkJaWYgKDAgPT0geF9wbHVzXzEpIAoJCXsKCQkJY292ZXJhZ2UgPSBoYWxmKHkpOwoJCX0KCQllbHNlIAoJCXsKCQkJZmxvYXQgZm4gPSB4X3BsdXNfMSAqICh4X3BsdXNfMSAtIDIpOwoJCQlmbiA9IGZtYSh5LHksIGZuKTsKCQkJZmxvYXQgZ3g9dmFyY2Nvb3JkX1N0YWdlMC56LCBneT12YXJjY29vcmRfU3RhZ2UwLnc7CgkJCWZsb2F0IGZud2lkdGggPSBhYnMoZ3gpICsgYWJzKGd5KTsKCQkJaGFsZiBkID0gaGFsZihmbi9mbndpZHRoKTsKCQkJY292ZXJhZ2UgPSBjbGFtcCguNSAtIGQsIDAsIDEpOwoJCX0KCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChjb3ZlcmFnZSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIEFBUmVjdEVmZmVjdAoJCWZsb2F0NCBwcmV2UmVjdCA9IGZsb2F0NCgtMS4wMDAwMDAsIC0xLjAwMDAwMCwgLTEuMDAwMDAwLCAtMS4wMDAwMDApOwoJCWhhbGYgYWxwaGE7CgkJQHN3aXRjaCAoMSkgCgkJewoJCQljYXNlIDA6ICAgIGNhc2UgMjogICAgICAgIGFscGhhID0gaGFsZihhbGwoZ3JlYXRlclRoYW4oZmxvYXQ0KHNrX0ZyYWdDb29yZC54eSwgdXJlY3RVbmlmb3JtX1N0YWdlMS56dyksIGZsb2F0NCh1cmVjdFVuaWZvcm1fU3RhZ2UxLnh5LCBza19GcmFnQ29vcmQueHkpKSkgPyAxIDogMCk7CgkJCWJyZWFrOwoJCQlkZWZhdWx0OiAgICAgICAgaGFsZiB4U3ViLCB5U3ViOwoJCQl4U3ViID0gbWluKGhhbGYoc2tfRnJhZ0Nvb3JkLnggLSB1cmVjdFVuaWZvcm1fU3RhZ2UxLngpLCAwLjApOwoJCQl4U3ViICs9IG1pbihoYWxmKHVyZWN0VW5pZm9ybV9TdGFnZTEueiAtIHNrX0ZyYWdDb29yZC54KSwgMC4wKTsKCQkJeVN1YiA9IG1pbihoYWxmKHNrX0ZyYWdDb29yZC55IC0gdXJlY3RVbmlmb3JtX1N0YWdlMS55KSwgMC4wKTsKCQkJeVN1YiArPSBtaW4oaGFsZih1cmVjdFVuaWZvcm1fU3RhZ2UxLncgLSBza19GcmFnQ29vcmQueSksIDAuMCk7CgkJCWFscGhhID0gKDEuMCArIG1heCh4U3ViLCAtMS4wKSkgKiAoMS4wICsgbWF4KHlTdWIsIC0xLjApKTsKCQl9CgkJQGlmICgxID09IDIgfHwgMSA9PSAzKSAKCQl7CgkJCWFscGhhID0gMS4wIC0gYWxwaGE7CgkJfQoJCWhhbGY0IGlucHV0Q29sb3IgPSBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7CgkJb3V0cHV0X1N0YWdlMSA9IGlucHV0Q29sb3IgKiBhbHBoYTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAgAAAAOAAAAcmFkaWlfc2VsZWN0b3IAABkAAABjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzAAAAFQAAAGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZQAAAAQAAABza2V3CQAAAHRyYW5zbGF0ZQAAAAcAAAByYWRpaV94AAcAAAByYWRpaV95AAUAAABjb2xvcgAAAAEAAAAAAAAA","GMZQCAABBUAAAAAAAAACOAAAAAJQAAIA7777777777776EYAAEAP777777777777EAAFQAAAAAAAAAAAAAAAAAAAAAWAASYABEAAAAAAAAAAAPAAHAAAKAAAAAGAAAAAAAAAAACMAAYAABAAAAAAAAAAAABAAAAALQAFKAA":"AgAAAExTS1NQAQAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0OwppbiBmbG9hdDIgcG9zaXRpb247CmluIGZsb2F0MiBsb2NhbENvb3JkOwpub3BlcnNwZWN0aXZlIG91dCBmbG9hdDIgdkxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZMb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAAAAAAAAAAAAAAAuxIAACNleHRlbnNpb24gR0xfTlZfc2hhZGVyX25vcGVyc3BlY3RpdmVfaW50ZXJwb2xhdGlvbjogcmVxdWlyZQp1bmlmb3JtIGhhbGYyIHVJbmNyZW1lbnRfU3RhZ2UxOwp1bmlmb3JtIGhhbGY0IHVLZXJuZWxfU3RhZ2UxWzddOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwOwp1bmlmb3JtIHNhbXBsZXIyRCB1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTE7Cm5vcGVyc3BlY3RpdmUgaW4gZmxvYXQyIHZMb2NhbENvb3JkX1N0YWdlMDsKb3V0IGhhbGY0IHNrX0ZyYWdDb2xvcjsKaGFsZjQgVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTEsIF9jb29yZHMpICogX2lucHV0KTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQgX2lucHV0LCBmbG9hdDIgX2Nvb3JkcykgCnsKCWhhbGY0IF9vdXRwdXQ7Cglfb3V0cHV0ID0gVGV4dHVyZUVmZmVjdF9TdGFnZTFfYzBfYzAoX2lucHV0LCAoKHVtYXRyaXhfU3RhZ2UxX2MwKSAqIF9jb29yZHMueHkxKS54eSk7CglyZXR1cm4gX291dHB1dDsKfQp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJCW91dHB1dENvbG9yX1N0YWdlMCA9IGhhbGY0KDEpOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBHYXVzc2lhbkNvbnZvbHV0aW9uCgkJZmxvYXQyIF9jb29yZHMgPSB2TG9jYWxDb29yZF9TdGFnZTAueHk7CgkJb3V0cHV0X1N0YWdlMSA9IGhhbGY0KDAsIDAsIDAsIDApOwoJCWZsb2F0MiBjb29yZCA9IF9jb29yZHMgLSAxMi4wICogdUluY3JlbWVudF9TdGFnZTE7CgkJZmxvYXQyIGNvb3JkU2FtcGxlZCA9IGhhbGYyKDAsIDApOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMF0ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVswXS56OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzBdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVsxXS55OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzFdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMV0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVsyXS54OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzJdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbMl0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVsyXS53OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbM10ueTsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVszXS56OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzNdLnc7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0ueDsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVs0XS55OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzRdLno7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNF0udzsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVs1XS54OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzVdLnk7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJY29vcmRTYW1wbGVkID0gY29vcmQ7CgkJb3V0cHV0X1N0YWdlMSArPSBNYXRyaXhFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBjb29yZFNhbXBsZWQpICogdUtlcm5lbF9TdGFnZTFbNV0uejsKCQljb29yZCArPSB1SW5jcmVtZW50X1N0YWdlMTsKCQljb29yZFNhbXBsZWQgPSBjb29yZDsKCQlvdXRwdXRfU3RhZ2UxICs9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzAoaGFsZjQoMSksIGNvb3JkU2FtcGxlZCkgKiB1S2VybmVsX1N0YWdlMVs1XS53OwoJCWNvb3JkICs9IHVJbmNyZW1lbnRfU3RhZ2UxOwoJCWNvb3JkU2FtcGxlZCA9IGNvb3JkOwoJCW91dHB1dF9TdGFnZTEgKz0gTWF0cml4RWZmZWN0X1N0YWdlMV9jMChoYWxmNCgxKSwgY29vcmRTYW1wbGVkKSAqIHVLZXJuZWxfU3RhZ2UxWzZdLng7CgkJY29vcmQgKz0gdUluY3JlbWVudF9TdGFnZTE7CgkJb3V0cHV0X1N0YWdlMSAqPSBvdXRwdXRDb2xvcl9TdGFnZTA7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0X1N0YWdlMSAqIG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCQlza19GcmFnQ29sb3IgPSBza19GcmFnQ29sb3IuYWFhYTsKCX0KfQoAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAgAAABwb3NpdGlvbgoAAABsb2NhbENvb3JkAAABAAAAAAAAAA==","CAZAAAICBMAAAAAAAAAEOAAAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFQAAAAAAAAAAAAAAAAAAAAAWAASYAAUAAAAAAAAAAAPAADQAAIAAAAAAAAAAAAIAAAACMABKQA":"AgAAAExTS1M6AQAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0OwppbiBmbG9hdDIgcG9zaXRpb247CmluIGhhbGY0IGNvbG9yOwpub3BlcnNwZWN0aXZlIG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZjb2xvcl9TdGFnZTAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKfQoAAAABAQAAAAAAAAEBABoEAAAjZXh0ZW5zaW9uIEdMX05WX3NoYWRlcl9ub3BlcnNwZWN0aXZlX2ludGVycG9sYXRpb246IHJlcXVpcmUKdW5pZm9ybSBoYWxmNCB1Y2lyY2xlRGF0YV9TdGFnZTE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMTsKbm9wZXJzcGVjdGl2ZSBpbiBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBUZXh0dXJlRWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQsIGZsb2F0MiBfY29vcmRzKSAKewoJaGFsZjQgX291dHB1dDsKCV9vdXRwdXQgPSAoc2FtcGxlKHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMSwgX2Nvb3JkcykgKiBfaW5wdXQpOwoJcmV0dXJuIF9vdXRwdXQ7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2Y29sb3JfU3RhZ2UwOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBDaXJjbGVCbHVyRnJhZ21lbnRQcm9jZXNzb3IKCQk7CgkJaGFsZjIgdmVjID0gaGFsZjIoKHNrX0ZyYWdDb29yZC54eSAtIGZsb2F0Mih1Y2lyY2xlRGF0YV9TdGFnZTEueHkpKSAqIGZsb2F0KHVjaXJjbGVEYXRhX1N0YWdlMS53KSk7CgkJaGFsZiBkaXN0ID0gbGVuZ3RoKHZlYykgKyAoMC41IC0gdWNpcmNsZURhdGFfU3RhZ2UxLnopICogdWNpcmNsZURhdGFfU3RhZ2UxLnc7CgkJaGFsZjQgaW5wdXRDb2xvciA9IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCQlvdXRwdXRfU3RhZ2UxID0gaW5wdXRDb2xvciAqIFRleHR1cmVFZmZlY3RfU3RhZ2UxX2MwKGhhbGY0KDEpLCBmbG9hdDIoaGFsZjIoZGlzdCwgMC41KSkpLnc7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0X1N0YWdlMTsKCX0KfQoAAAABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAABAAAAAAAAAA==","CAZAAAECA4AAABQAAEABGAABAAOAAEIACUAAGAA3AABAAGYAAIAP777777777777EAAAGAAAAAAAAAAAAAAAEAAAAAYAAVIA":"AgAAAExTS1M0AwAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0Owp1bmlmb3JtIGZsb2F0NCB1bG9jYWxNYXRyaXhfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gZmxvYXQ0IGluQ2lyY2xlRWRnZTsKaW4gaGFsZjMgaW5DbGlwUGxhbmU7CmluIGhhbGYzIGluSXNlY3RQbGFuZTsKbm9wZXJzcGVjdGl2ZSBvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpub3BlcnNwZWN0aXZlIG91dCBoYWxmMyB2aW5DbGlwUGxhbmVfU3RhZ2UwOwpub3BlcnNwZWN0aXZlIG91dCBoYWxmMyB2aW5Jc2VjdFBsYW5lX1N0YWdlMDsKbm9wZXJzcGVjdGl2ZSBvdXQgaGFsZjQgdmluQ29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBDaXJjbGVHZW9tZXRyeVByb2Nlc3NvcgoJdmluQ2lyY2xlRWRnZV9TdGFnZTAgPSBpbkNpcmNsZUVkZ2U7Cgl2aW5DbGlwUGxhbmVfU3RhZ2UwID0gaW5DbGlwUGxhbmU7Cgl2aW5Jc2VjdFBsYW5lX1N0YWdlMCA9IGluSXNlY3RQbGFuZTsKCXZpbkNvbG9yX1N0YWdlMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gdWxvY2FsTWF0cml4X1N0YWdlMC54eiAqIGluUG9zaXRpb24gKyB1bG9jYWxNYXRyaXhfU3RhZ2UwLnl3OwoJc2tfUG9zaXRpb24gPSBmbG9hdDQoX3RtcF8wX2luUG9zaXRpb24ueCAsIF90bXBfMF9pblBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAAAAAAAAAAAAADoEAAAjZXh0ZW5zaW9uIEdMX05WX3NoYWRlcl9ub3BlcnNwZWN0aXZlX2ludGVycG9sYXRpb246IHJlcXVpcmUKbm9wZXJzcGVjdGl2ZSBpbiBmbG9hdDQgdmluQ2lyY2xlRWRnZV9TdGFnZTA7Cm5vcGVyc3BlY3RpdmUgaW4gaGFsZjMgdmluQ2xpcFBsYW5lX1N0YWdlMDsKbm9wZXJzcGVjdGl2ZSBpbiBoYWxmMyB2aW5Jc2VjdFBsYW5lX1N0YWdlMDsKbm9wZXJzcGVjdGl2ZSBpbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCQlmbG9hdDQgY2lyY2xlRWRnZTsKCQljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TdGFnZTA7CgkJaGFsZjMgY2xpcFBsYW5lOwoJCWNsaXBQbGFuZSA9IHZpbkNsaXBQbGFuZV9TdGFnZTA7CgkJaGFsZjMgaXNlY3RQbGFuZTsKCQlpc2VjdFBsYW5lID0gdmluSXNlY3RQbGFuZV9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgkJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgkJaGFsZiBjbGlwID0gaGFsZihzYXR1cmF0ZShjaXJjbGVFZGdlLnogKiBkb3QoY2lyY2xlRWRnZS54eSwgY2xpcFBsYW5lLnh5KSArIGNsaXBQbGFuZS56KSk7CgkJY2xpcCAqPSBoYWxmKHNhdHVyYXRlKGNpcmNsZUVkZ2UueiAqIGRvdChjaXJjbGVFZGdlLnh5LCBpc2VjdFBsYW5lLnh5KSArIGlzZWN0UGxhbmUueikpOwoJCWVkZ2VBbHBoYSAqPSBjbGlwOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAEAAAABAAAAAQAAAAAAAAAAAAAABQAAAAoAAABpblBvc2l0aW9uAAAHAAAAaW5Db2xvcgAMAAAAaW5DaXJjbGVFZGdlCwAAAGluQ2xpcFBsYW5lAAwAAABpbklzZWN0UGxhbmUBAAAAAAAAAA==","CAZAAAECA4AAAAIAAEABGAABAAOAAEIACUAAGAH7777777777777777777777777EAAAGAAAAAAAAAAAAAAAEAAAAAYAAVIA":"AgAAAExTS1NiAgAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0Owp1bmlmb3JtIGZsb2F0NCB1bG9jYWxNYXRyaXhfU3RhZ2UwOwppbiBmbG9hdDIgaW5Qb3NpdGlvbjsKaW4gaGFsZjQgaW5Db2xvcjsKaW4gZmxvYXQ0IGluQ2lyY2xlRWRnZTsKbm9wZXJzcGVjdGl2ZSBvdXQgZmxvYXQ0IHZpbkNpcmNsZUVkZ2VfU3RhZ2UwOwpub3BlcnNwZWN0aXZlIG91dCBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7CnZvaWQgbWFpbigpIAp7CgkvLyBQcmltaXRpdmUgUHJvY2Vzc29yIENpcmNsZUdlb21ldHJ5UHJvY2Vzc29yCgl2aW5DaXJjbGVFZGdlX1N0YWdlMCA9IGluQ2lyY2xlRWRnZTsKCXZpbkNvbG9yX1N0YWdlMCA9IGluQ29sb3I7CglmbG9hdDIgX3RtcF8wX2luUG9zaXRpb24gPSBpblBvc2l0aW9uOwoJZmxvYXQyIF90bXBfMV9pblBvc2l0aW9uID0gdWxvY2FsTWF0cml4X1N0YWdlMC54eiAqIGluUG9zaXRpb24gKyB1bG9jYWxNYXRyaXhfU3RhZ2UwLnl3OwoJc2tfUG9zaXRpb24gPSBmbG9hdDQoX3RtcF8wX2luUG9zaXRpb24ueCAsIF90bXBfMF9pblBvc2l0aW9uLnksIDAsIDEpOwp9CgAAAAAAAAAAAAAAAAAAOQMAACNleHRlbnNpb24gR0xfTlZfc2hhZGVyX25vcGVyc3BlY3RpdmVfaW50ZXJwb2xhdGlvbjogcmVxdWlyZQpub3BlcnNwZWN0aXZlIGluIGZsb2F0NCB2aW5DaXJjbGVFZGdlX1N0YWdlMDsKbm9wZXJzcGVjdGl2ZSBpbiBoYWxmNCB2aW5Db2xvcl9TdGFnZTA7Cm91dCBoYWxmNCBza19GcmFnQ29sb3I7CnZvaWQgbWFpbigpIAp7CgloYWxmNCBvdXRwdXRDb2xvcl9TdGFnZTA7CgloYWxmNCBvdXRwdXRDb3ZlcmFnZV9TdGFnZTA7Cgl7CgkJLy8gU3RhZ2UgMCwgQ2lyY2xlR2VvbWV0cnlQcm9jZXNzb3IKCQlmbG9hdDQgY2lyY2xlRWRnZTsKCQljaXJjbGVFZGdlID0gdmluQ2lyY2xlRWRnZV9TdGFnZTA7CgkJb3V0cHV0Q29sb3JfU3RhZ2UwID0gdmluQ29sb3JfU3RhZ2UwOwoJCWZsb2F0IGQgPSBsZW5ndGgoY2lyY2xlRWRnZS54eSk7CgkJaGFsZiBkaXN0YW5jZVRvT3V0ZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoMS4wIC0gZCkpOwoJCWhhbGYgZWRnZUFscGhhID0gc2F0dXJhdGUoZGlzdGFuY2VUb091dGVyRWRnZSk7CgkJaGFsZiBkaXN0YW5jZVRvSW5uZXJFZGdlID0gaGFsZihjaXJjbGVFZGdlLnogKiAoZCAtIGNpcmNsZUVkZ2UudykpOwoJCWhhbGYgaW5uZXJBbHBoYSA9IHNhdHVyYXRlKGRpc3RhbmNlVG9Jbm5lckVkZ2UpOwoJCWVkZ2VBbHBoYSAqPSBpbm5lckFscGhhOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KGVkZ2VBbHBoYSk7Cgl9Cgl7CgkJLy8gWGZlciBQcm9jZXNzb3I6IFBvcnRlciBEdWZmCgkJc2tfRnJhZ0NvbG9yID0gb3V0cHV0Q29sb3JfU3RhZ2UwICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAMAAAAKAAAAaW5Qb3NpdGlvbgAABwAAAGluQ29sb3IADAAAAGluQ2lyY2xlRWRnZQEAAAAAAAAA","CAZAAAICBMAAAAAAAAAEOAAAAAJQAAIA777777Y4AAIQB7777777777777777777EAAFQAAAAAAAA6YAAAACYAAEAACAAAAAPEAAAAD3AAAAAPAAAQAAIAAAAAAAAAAAAIAAAACMABKQA":"AgAAAExTS1M6AQAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0OwppbiBmbG9hdDIgcG9zaXRpb247CmluIGhhbGY0IGNvbG9yOwpub3BlcnNwZWN0aXZlIG91dCBoYWxmNCB2Y29sb3JfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZjb2xvcl9TdGFnZTAgPSBjb2xvcjsKCXNrX1Bvc2l0aW9uID0gZmxvYXQ0KHBvc2l0aW9uLnggLCBwb3NpdGlvbi55LCAwLCAxKTsKfQoAAAABAQAAAAAAAAEBAAAFAAAjZXh0ZW5zaW9uIEdMX05WX3NoYWRlcl9ub3BlcnNwZWN0aXZlX2ludGVycG9sYXRpb246IHJlcXVpcmUKdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TdGFnZTE7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1N0YWdlMTsKdW5pZm9ybSBmbG9hdDQgdWlubmVyUmVjdF9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjIgdXJhZGl1c1BsdXNIYWxmX1N0YWdlMV9jMDsKbm9wZXJzcGVjdGl2ZSBpbiBoYWxmNCB2Y29sb3JfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBDaXJjdWxhclJSZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1N0YWdlMV9jMC5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCWZsb2F0MiBkeHkxID0gc2tfRnJhZ0Nvb3JkLnh5IC0gdWlubmVyUmVjdF9TdGFnZTFfYzAuUkI7CglmbG9hdDIgZHh5ID0gbWF4KG1heChkeHkwLCBkeHkxKSwgMC4wKTsKCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TdGFnZTFfYzAueCAtIGxlbmd0aChkeHkpKSk7CglhbHBoYSA9IDEuMCAtIGFscGhhOwoJX291dHB1dCA9IF9pbnB1dCAqIGFscGhhOwoJcmV0dXJuIF9vdXRwdXQ7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2Y29sb3JfU3RhZ2UwOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBDaXJjdWxhclJSZWN0CgkJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1N0YWdlMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCQlmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfU3RhZ2UxLlJCOwoJCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TdGFnZTEueCAtIGxlbmd0aChkeHkpKSk7CgkJb3V0cHV0X1N0YWdlMSA9IENpcmN1bGFyUlJlY3RfU3RhZ2UxX2MwKG91dHB1dENvdmVyYWdlX1N0YWdlMCkgKiBhbHBoYTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRDb2xvcl9TdGFnZTAgKiBvdXRwdXRfU3RhZ2UxOwoJfQp9CgABAQABAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAAIAAAAcG9zaXRpb24FAAAAY29sb3IAAAABAAAAAAAAAA==","CAZAAAMCBEAAADQBAEACKAAAAAJQAAIA7777777777776EYAAEAP777777777777AAAAAABAABMAAAAAAAAHSAAAAAYAABAAAQAAAAAAAAAAAAQAAAAEAACVAA":"AgAAAExTS1NQAQAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0OwppbiBmbG9hdDIgcG9zaXRpb247CmluIGZsb2F0MiBsb2NhbENvb3JkOwpub3BlcnNwZWN0aXZlIG91dCBmbG9hdDIgdmxvY2FsQ29vcmRfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBRdWFkUGVyRWRnZUFBR2VvbWV0cnlQcm9jZXNzb3IKCXZsb2NhbENvb3JkX1N0YWdlMCA9IGxvY2FsQ29vcmQ7Cglza19Qb3NpdGlvbiA9IGZsb2F0NChwb3NpdGlvbi54ICwgcG9zaXRpb24ueSwgMCwgMSk7Cn0KAAEBAAAAAAAAAQEAuggAACNleHRlbnNpb24gR0xfTlZfc2hhZGVyX25vcGVyc3BlY3RpdmVfaW50ZXJwb2xhdGlvbjogcmVxdWlyZQp1bmlmb3JtIGhhbGYgdVNyY1RGX1N0YWdlMFs3XTsKdW5pZm9ybSBoYWxmM3gzIHVDb2xvclhmb3JtX1N0YWdlMDsKdW5pZm9ybSBoYWxmIHVEc3RURl9TdGFnZTBbN107CnVuaWZvcm0gZmxvYXQ0IHVpbm5lclJlY3RfU3RhZ2UxOwp1bmlmb3JtIGhhbGYyIHVyYWRpdXNQbHVzSGFsZl9TdGFnZTE7CnVuaWZvcm0gc2FtcGxlcjJEIHVUZXh0dXJlU2FtcGxlcl8wX1N0YWdlMDsKbm9wZXJzcGVjdGl2ZSBpbiBmbG9hdDIgdmxvY2FsQ29vcmRfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmIHNyY190Zl9TdGFnZTAoaGFsZiB4KSAKewoJaGFsZiBHID0gdVNyY1RGX1N0YWdlMFswXTsKCWhhbGYgQSA9IHVTcmNURl9TdGFnZTBbMV07CgloYWxmIEIgPSB1U3JjVEZfU3RhZ2UwWzJdOwoJaGFsZiBDID0gdVNyY1RGX1N0YWdlMFszXTsKCWhhbGYgRCA9IHVTcmNURl9TdGFnZTBbNF07CgloYWxmIEUgPSB1U3JjVEZfU3RhZ2UwWzVdOwoJaGFsZiBGID0gdVNyY1RGX1N0YWdlMFs2XTsKCWhhbGYgcyA9IHNpZ24oeCk7Cgl4ID0gYWJzKHgpOwoJeCA9ICh4IDwgRCkgPyAoQyAqIHgpICsgRiA6IHBvdyhBICogeCArIEIsIEcpICsgRTsKCXJldHVybiBzICogeDsKfQpoYWxmIGRzdF90Zl9TdGFnZTAoaGFsZiB4KSAKewoJaGFsZiBHID0gdURzdFRGX1N0YWdlMFswXTsKCWhhbGYgQSA9IHVEc3RURl9TdGFnZTBbMV07CgloYWxmIEIgPSB1RHN0VEZfU3RhZ2UwWzJdOwoJaGFsZiBDID0gdURzdFRGX1N0YWdlMFszXTsKCWhhbGYgRCA9IHVEc3RURl9TdGFnZTBbNF07CgloYWxmIEUgPSB1RHN0VEZfU3RhZ2UwWzVdOwoJaGFsZiBGID0gdURzdFRGX1N0YWdlMFs2XTsKCWhhbGYgcyA9IHNpZ24oeCk7Cgl4ID0gYWJzKHgpOwoJeCA9ICh4IDwgRCkgPyAoQyAqIHgpICsgRiA6IHBvdyhBICogeCArIEIsIEcpICsgRTsKCXJldHVybiBzICogeDsKfQpoYWxmNCBnYW11dF94Zm9ybV9TdGFnZTAoaGFsZjQgY29sb3IpIAp7Cgljb2xvci5yZ2IgPSAodUNvbG9yWGZvcm1fU3RhZ2UwICogY29sb3IucmdiKTsKCXJldHVybiBjb2xvcjsKfQpoYWxmNCBjb2xvcl94Zm9ybV9TdGFnZTAoaGFsZjQgY29sb3IpIAp7Cgljb2xvci5yID0gc3JjX3RmX1N0YWdlMChoYWxmKGNvbG9yLnIpKTsKCWNvbG9yLmcgPSBzcmNfdGZfU3RhZ2UwKGhhbGYoY29sb3IuZykpOwoJY29sb3IuYiA9IHNyY190Zl9TdGFnZTAoaGFsZihjb2xvci5iKSk7Cgljb2xvciA9IGdhbXV0X3hmb3JtX1N0YWdlMChoYWxmNChjb2xvcikpOwoJY29sb3IuciA9IGRzdF90Zl9TdGFnZTAoaGFsZihjb2xvci5yKSk7Cgljb2xvci5nID0gZHN0X3RmX1N0YWdlMChoYWxmKGNvbG9yLmcpKTsKCWNvbG9yLmIgPSBkc3RfdGZfU3RhZ2UwKGhhbGYoY29sb3IuYikpOwoJcmV0dXJuIGhhbGY0KGNvbG9yKTsKfQp2b2lkIG1haW4oKSAKewoJaGFsZjQgb3V0cHV0Q29sb3JfU3RhZ2UwOwoJaGFsZjQgb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJewoJCS8vIFN0YWdlIDAsIFF1YWRQZXJFZGdlQUFHZW9tZXRyeVByb2Nlc3NvcgoJCW91dHB1dENvbG9yX1N0YWdlMCA9IGhhbGY0KDEpOwoJCWZsb2F0MiB0ZXhDb29yZDsKCQl0ZXhDb29yZCA9IHZsb2NhbENvb3JkX1N0YWdlMDsKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSAoY29sb3JfeGZvcm1fU3RhZ2UwKHNhbXBsZSh1VGV4dHVyZVNhbXBsZXJfMF9TdGFnZTAsIHRleENvb3JkKSkgKiBvdXRwdXRDb2xvcl9TdGFnZTApOwoJCW91dHB1dENvdmVyYWdlX1N0YWdlMCA9IGhhbGY0KDEpOwoJfQoJaGFsZjQgb3V0cHV0X1N0YWdlMTsKCXsKCQkvLyBTdGFnZSAxLCBDaXJjdWxhclJSZWN0CgkJZmxvYXQyIGR4eTAgPSB1aW5uZXJSZWN0X1N0YWdlMS5MVCAtIHNrX0ZyYWdDb29yZC54eTsKCQlmbG9hdDIgZHh5MSA9IHNrX0ZyYWdDb29yZC54eSAtIHVpbm5lclJlY3RfU3RhZ2UxLlJCOwoJCWZsb2F0MiBkeHkgPSBtYXgobWF4KGR4eTAsIGR4eTEpLCAwLjApOwoJCWhhbGYgYWxwaGEgPSBoYWxmKHNhdHVyYXRlKHVyYWRpdXNQbHVzSGFsZl9TdGFnZTEueCAtIGxlbmd0aChkeHkpKSk7CgkJb3V0cHV0X1N0YWdlMSA9IG91dHB1dENvdmVyYWdlX1N0YWdlMCAqIGFscGhhOwoJfQoJewoJCS8vIFhmZXIgUHJvY2Vzc29yOiBQb3J0ZXIgRHVmZgoJCXNrX0ZyYWdDb2xvciA9IG91dHB1dENvbG9yX1N0YWdlMCAqIG91dHB1dF9TdGFnZTE7Cgl9Cn0KAAAAAQEAAQAAAAEAAAABAAAAAAAAAAAAAAACAAAACAAAAHBvc2l0aW9uCgAAAGxvY2FsQ29vcmQAAAEAAAAAAAAA","CAZACAACCAAAAAIAAUABKAADAAKQAAYACUAAGAAVAABQAEYAAEABKAADAAKQAAYADQABCAAVAABQAKAAF4AAAAAAAAYAARQAAQAAAABYAA2AACAAAAAEAABYAACAAAAAAAAAAACMAAPAABAAAAAAAAAAAAADYAB4AA6AAPAAAAAAAZAAHQAAIAAAAAAAAAAAAIAAAADUABKQA":"AgAAAExTS1NMCgAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gZmxvYXQ0IHNrX1JUQWRqdXN0Owp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwOwppbiBmbG9hdDQgcmFkaWlfc2VsZWN0b3I7CmluIGZsb2F0NCBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzOwppbiBmbG9hdDQgYWFfYmxvYXRfYW5kX2NvdmVyYWdlOwppbiBmbG9hdDQgc2tldzsKaW4gZmxvYXQyIHRyYW5zbGF0ZTsKaW4gZmxvYXQ0IHJhZGlpX3g7CmluIGZsb2F0NCByYWRpaV95OwppbiBoYWxmNCBjb2xvcjsKaW4gZmxvYXQ0IGxvY2FsX3JlY3Q7Cm5vcGVyc3BlY3RpdmUgb3V0IGhhbGY0IHZjb2xvcl9TdGFnZTA7Cm5vcGVyc3BlY3RpdmUgb3V0IGZsb2F0MiB2YXJjY29vcmRfU3RhZ2UwOwpub3BlcnNwZWN0aXZlIG91dCBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwp2b2lkIG1haW4oKSAKewoJLy8gUHJpbWl0aXZlIFByb2Nlc3NvciBHckZpbGxSUmVjdE9wOjpQcm9jZXNzb3IKCXZjb2xvcl9TdGFnZTAgPSBjb2xvcjsKCWZsb2F0MiBjb3JuZXIgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnh5OwoJZmxvYXQyIHJhZGl1c19vdXRzZXQgPSBjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzLnp3OwoJZmxvYXQyIGFhX2Jsb2F0X2RpcmVjdGlvbiA9IGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZS54eTsKCWZsb2F0IGNvdmVyYWdlID0gYWFfYmxvYXRfYW5kX2NvdmVyYWdlLno7CglmbG9hdCBpc19saW5lYXJfY292ZXJhZ2UgPSBhYV9ibG9hdF9hbmRfY292ZXJhZ2UudzsKCWZsb2F0MiBwaXhlbGxlbmd0aCA9IGludmVyc2VzcXJ0KGZsb2F0Mihkb3Qoc2tldy54eiwgc2tldy54eiksIGRvdChza2V3Lnl3LCBza2V3Lnl3KSkpOwoJZmxvYXQ0IG5vcm1hbGl6ZWRfYXhpc19kaXJzID0gc2tldyAqIHBpeGVsbGVuZ3RoLnh5eHk7CglmbG9hdDIgYXhpc3dpZHRocyA9IChhYnMobm9ybWFsaXplZF9heGlzX2RpcnMueHkpICsgYWJzKG5vcm1hbGl6ZWRfYXhpc19kaXJzLnp3KSk7CglmbG9hdDIgYWFfYmxvYXRyYWRpdXMgPSBheGlzd2lkdGhzICogcGl4ZWxsZW5ndGggKiAuNTsKCWZsb2F0NCByYWRpaV9hbmRfbmVpZ2hib3JzID0gcmFkaWlfc2VsZWN0b3IqIGZsb2F0NHg0KHJhZGlpX3gsIHJhZGlpX3ksIHJhZGlpX3gueXh3eiwgcmFkaWlfeS53enl4KTsKCWZsb2F0MiByYWRpaSA9IHJhZGlpX2FuZF9uZWlnaGJvcnMueHk7CglmbG9hdDIgbmVpZ2hib3JfcmFkaWkgPSByYWRpaV9hbmRfbmVpZ2hib3JzLnp3OwoJaWYgKGFueShncmVhdGVyVGhhbihhYV9ibG9hdHJhZGl1cywgZmxvYXQyKDEpKSkpIAoJewoJCWNvcm5lciA9IG1heChhYnMoY29ybmVyKSwgYWFfYmxvYXRyYWRpdXMpICogc2lnbihjb3JuZXIpOwoJCWNvdmVyYWdlIC89IG1heChhYV9ibG9hdHJhZGl1cy54LCAxKSAqIG1heChhYV9ibG9hdHJhZGl1cy55LCAxKTsKCQlyYWRpaSA9IGZsb2F0MigwKTsKCX0KCWlmIChhbnkobGVzc1RoYW4ocmFkaWksIGFhX2Jsb2F0cmFkaXVzICogMS4yNSkpKSAKCXsKCQlyYWRpaSA9IGFhX2Jsb2F0cmFkaXVzOwoJCXJhZGl1c19vdXRzZXQgPSBmbG9vcihhYnMocmFkaXVzX291dHNldCkpICogcmFkaXVzX291dHNldDsKCQlpc19saW5lYXJfY292ZXJhZ2UgPSAxOwoJfQoJZWxzZSAKCXsKCQlyYWRpaSA9IGNsYW1wKHJhZGlpLCBwaXhlbGxlbmd0aCwgMiAtIHBpeGVsbGVuZ3RoKTsKCQluZWlnaGJvcl9yYWRpaSA9IGNsYW1wKG5laWdoYm9yX3JhZGlpLCBwaXhlbGxlbmd0aCwgMiAtIHBpeGVsbGVuZ3RoKTsKCQlmbG9hdDIgc3BhY2luZyA9IDIgLSByYWRpaSAtIG5laWdoYm9yX3JhZGlpOwoJCWZsb2F0MiBleHRyYV9wYWQgPSBtYXgocGl4ZWxsZW5ndGggKiAuMDYyNSAtIHNwYWNpbmcsIGZsb2F0MigwKSk7CgkJcmFkaWkgLT0gZXh0cmFfcGFkICogLjU7Cgl9CglmbG9hdDIgYWFfb3V0c2V0ID0gYWFfYmxvYXRfZGlyZWN0aW9uLnh5ICogYWFfYmxvYXRyYWRpdXM7CglmbG9hdDIgdmVydGV4cG9zID0gY29ybmVyICsgcmFkaXVzX291dHNldCAqIHJhZGlpICsgYWFfb3V0c2V0OwoJZmxvYXQyIGxvY2FsY29vcmQgPSAobG9jYWxfcmVjdC54eSAqICgxIC0gdmVydGV4cG9zKSArIGxvY2FsX3JlY3QuencgKiAoMSArIHZlcnRleHBvcykpICogLjU7CglmbG9hdDJ4MiBza2V3bWF0cml4ID0gZmxvYXQyeDIoc2tldy54eSwgc2tldy56dyk7CglmbG9hdDIgZGV2Y29vcmQgPSB2ZXJ0ZXhwb3MgKiBza2V3bWF0cml4ICsgdHJhbnNsYXRlOwoJaWYgKDAgIT0gaXNfbGluZWFyX2NvdmVyYWdlKSAKCXsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKDAsIGNvdmVyYWdlKTsKCX0KCWVsc2UgCgl7CgkJZmxvYXQyIGFyY2Nvb3JkID0gMSAtIGFicyhyYWRpdXNfb3V0c2V0KSArIGFhX291dHNldC9yYWRpaSAqIGNvcm5lcjsKCQl2YXJjY29vcmRfU3RhZ2UwLnh5ID0gZmxvYXQyKGFyY2Nvb3JkLngrMSwgYXJjY29vcmQueSk7Cgl9Cglza19Qb3NpdGlvbiA9IGZsb2F0NChkZXZjb29yZC54ICwgZGV2Y29vcmQueSwgMCwgMSk7Cgl7CgkJdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwID0gKCgodW1hdHJpeF9TdGFnZTFfYzBfYzApKSAqIGxvY2FsY29vcmQueHkxKS54eTsKCX0KfQoAAAAAAAAAAAAAAADpCAAAI2V4dGVuc2lvbiBHTF9OVl9zaGFkZXJfbm9wZXJzcGVjdGl2ZV9pbnRlcnBvbGF0aW9uOiByZXF1aXJlCnVuaWZvcm0gaGFsZjQgdWxlZnRCb3JkZXJDb2xvcl9TdGFnZTFfYzA7CnVuaWZvcm0gaGFsZjQgdXJpZ2h0Qm9yZGVyQ29sb3JfU3RhZ2UxX2MwOwp1bmlmb3JtIGZsb2F0M3gzIHVtYXRyaXhfU3RhZ2UxX2MwX2MwOwp1bmlmb3JtIGhhbGY0IHVzdGFydF9TdGFnZTFfYzBfYzE7CnVuaWZvcm0gaGFsZjQgdWVuZF9TdGFnZTFfYzBfYzE7Cm5vcGVyc3BlY3RpdmUgaW4gaGFsZjQgdmNvbG9yX1N0YWdlMDsKbm9wZXJzcGVjdGl2ZSBpbiBmbG9hdDIgdmFyY2Nvb3JkX1N0YWdlMDsKbm9wZXJzcGVjdGl2ZSBpbiBmbG9hdDIgdlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwOwpvdXQgaGFsZjQgc2tfRnJhZ0NvbG9yOwpoYWxmNCBMaW5lYXJHcmFkaWVudExheW91dF9TdGFnZTFfYzBfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCWhhbGYgdCA9IGhhbGYodlRyYW5zZm9ybWVkQ29vcmRzXzBfU3RhZ2UwLngpICsgOS45OTk5OTk3NDczNzg3NTE2ZS0wNjsKCV9vdXRwdXQgPSBoYWxmNCh0LCAxLjAsIDAuMCwgMC4wKTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQgX2lucHV0KSAKewoJaGFsZjQgX291dHB1dDsKCV9vdXRwdXQgPSBMaW5lYXJHcmFkaWVudExheW91dF9TdGFnZTFfYzBfYzBfYzAoX2lucHV0KTsKCXJldHVybiBfb3V0cHV0Owp9CmhhbGY0IFNpbmdsZUludGVydmFsR3JhZGllbnRDb2xvcml6ZXJfU3RhZ2UxX2MwX2MxKGhhbGY0IF9pbnB1dCkgCnsKCWhhbGY0IF9vdXRwdXQ7CgloYWxmIHQgPSBfaW5wdXQueDsKCV9vdXRwdXQgPSAoMS4wIC0gdCkgKiB1c3RhcnRfU3RhZ2UxX2MwX2MxICsgdCAqIHVlbmRfU3RhZ2UxX2MwX2MxOwoJcmV0dXJuIF9vdXRwdXQ7Cn0KaGFsZjQgQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChoYWxmNCBfaW5wdXQpIAp7CgloYWxmNCBfb3V0cHV0OwoJaGFsZjQgdCA9IE1hdHJpeEVmZmVjdF9TdGFnZTFfYzBfYzAoaGFsZjQoMSkpOwoJaWYgKCF0cnVlICYmIHQueSA8IDAuMCkgCgl7CgkJX291dHB1dCA9IGhhbGY0KDAuMCk7Cgl9CgllbHNlIGlmICh0LnggPCAwLjApIAoJewoJCV9vdXRwdXQgPSB1bGVmdEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKCX0KCWVsc2UgaWYgKHQueCA+IDEuMCkgCgl7CgkJX291dHB1dCA9IHVyaWdodEJvcmRlckNvbG9yX1N0YWdlMV9jMDsKCX0KCWVsc2UgCgl7CgkJX291dHB1dCA9IFNpbmdsZUludGVydmFsR3JhZGllbnRDb2xvcml6ZXJfU3RhZ2UxX2MwX2MxKHQpOwoJfQoJQGlmIChmYWxzZSkgCgl7CgkJX291dHB1dC54eXogKj0gX291dHB1dC53OwoJfQoJcmV0dXJuIF9vdXRwdXQ7Cn0Kdm9pZCBtYWluKCkgCnsKCWhhbGY0IG91dHB1dENvbG9yX1N0YWdlMDsKCWhhbGY0IG91dHB1dENvdmVyYWdlX1N0YWdlMDsKCXsKCQkvLyBTdGFnZSAwLCBHckZpbGxSUmVjdE9wOjpQcm9jZXNzb3IKCQlvdXRwdXRDb2xvcl9TdGFnZTAgPSB2Y29sb3JfU3RhZ2UwOwoJCWZsb2F0IHhfcGx1c18xPXZhcmNjb29yZF9TdGFnZTAueCwgeT12YXJjY29vcmRfU3RhZ2UwLnk7CgkJaGFsZiBjb3ZlcmFnZTsKCQlpZiAoMCA9PSB4X3BsdXNfMSkgCgkJewoJCQljb3ZlcmFnZSA9IGhhbGYoeSk7CgkJfQoJCWVsc2UgCgkJewoJCQlmbG9hdCBmbiA9IHhfcGx1c18xICogKHhfcGx1c18xIC0gMik7CgkJCWZuID0gZm1hKHkseSwgZm4pOwoJCQlmbG9hdCBmbndpZHRoID0gZndpZHRoKGZuKTsKCQkJaGFsZiBkID0gaGFsZihmbi9mbndpZHRoKTsKCQkJY292ZXJhZ2UgPSBjbGFtcCguNSAtIGQsIDAsIDEpOwoJCX0KCQlvdXRwdXRDb3ZlcmFnZV9TdGFnZTAgPSBoYWxmNChjb3ZlcmFnZSk7Cgl9CgloYWxmNCBvdXRwdXRfU3RhZ2UxOwoJewoJCS8vIFN0YWdlIDEsIE92ZXJyaWRlSW5wdXRGcmFnbWVudFByb2Nlc3NvcgoJCWhhbGY0IGNvbnN0Q29sb3I7CgkJQGlmIChmYWxzZSkgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMCk7CgkJfQoJCWVsc2UgCgkJewoJCQljb25zdENvbG9yID0gaGFsZjQoMS4wMDAwMDAsIDEuMDAwMDAwLCAxLjAwMDAwMCwgMS4wMDAwMDApOwoJCX0KCQlvdXRwdXRfU3RhZ2UxID0gQ2xhbXBlZEdyYWRpZW50RWZmZWN0X1N0YWdlMV9jMChjb25zdENvbG9yKTsKCX0KCXsKCQkvLyBYZmVyIFByb2Nlc3NvcjogUG9ydGVyIER1ZmYKCQlza19GcmFnQ29sb3IgPSBvdXRwdXRfU3RhZ2UxICogb3V0cHV0Q292ZXJhZ2VfU3RhZ2UwOwoJfQp9CgAAAAAAAAABAAAAAQAAAAEAAAAAAAAAAAAAAAkAAAAOAAAAcmFkaWlfc2VsZWN0b3IAABkAAABjb3JuZXJfYW5kX3JhZGl1c19vdXRzZXRzAAAAFQAAAGFhX2Jsb2F0X2FuZF9jb3ZlcmFnZQAAAAQAAABza2V3CQAAAHRyYW5zbGF0ZQAAAAcAAAByYWRpaV94AAcAAAByYWRpaV95AAUAAABjb2xvcgAAAAoAAABsb2NhbF9yZWN0AAABAAAAAAAAAA=="}} ================================================ FILE: test/accessibility_test.dart ================================================ import 'account/account_accessibility_test.dart' as account_test; import 'goods/goods_accessibility_test.dart' as goods_test; import 'login/login_accessibility_test.dart' as login_test; import 'order/order_accessibility_test.dart' as order_test; import 'setting/setting_accessibility_test.dart' as setting_test; import 'shop/shop_accessibility_test.dart' as shop_test; import 'statistics/statistic_accessibility_test.dart' as statistic_test; import 'store/store_accessibility_test.dart' as store_test; /// 可访问性测试 /// 1.检测页面可点击目标大小是否大于44 * 44 /// 2.检测页面可点击目标是否都有语义,例如图片可点击,但没加语义,就会报错。 /// 3.检测页面文本对比度是否满足最小值的准则,例如文字与背景对比度过低,就会报错。(部分测试) /// /// 测试运行: flutter test test/accessibility_test.dart /// /// 各模块统一运行,也可单独执行子模块测试 void main() { account_test.main(); goods_test.main(); login_test.main(); order_test.main(); setting_test.main(); shop_test.main(); statistic_test.main(); store_test.main(); } ================================================ FILE: test/account/account_accessibility_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/account/page/account_page.dart'; import 'package:flutter_deer/account/page/account_record_list_page.dart'; import 'package:flutter_deer/account/page/add_withdrawal_account_page.dart'; import 'package:flutter_deer/account/page/bank_select_page.dart'; import 'package:flutter_deer/account/page/city_select_page.dart'; import 'package:flutter_deer/account/page/withdrawal_account_list_page.dart'; import 'package:flutter_deer/account/page/withdrawal_account_page.dart'; import 'package:flutter_deer/account/page/withdrawal_page.dart'; import 'package:flutter_deer/account/page/withdrawal_password_page.dart'; import 'package:flutter_deer/account/page/withdrawal_record_list_page.dart'; import 'package:flutter_deer/account/page/withdrawal_result_page.dart'; import 'package:flutter_deer/setting/provider/theme_provider.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { final Map map = {}; map['account_page'] = const AccountPage(); map['account_record_list_page'] = const AccountRecordListPage(); map['add_withdrawal_account_page'] = const AddWithdrawalAccountPage(); map['bank_select_page'] = const BankSelectPage(); map['city_select_page'] = const CitySelectPage(); map['withdrawal_account_list_page'] = const WithdrawalAccountListPage(); map['withdrawal_account_page'] = const WithdrawalAccountPage(); map['withdrawal_page'] = const WithdrawalPage(); map['withdrawal_password_page'] = const WithdrawalPasswordPage(); map['withdrawal_record_list_page'] = const WithdrawalRecordListPage(); map['withdrawal_result_page'] = const WithdrawalResultPage(); group('account => 检测页面可点击目标大小是否大于44 * 44', () { map.forEach((name, page) { testWidgets(name, (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget(MaterialApp(home: page)); await expectLater(tester, meetsGuideline(iOSTapTargetGuideline)); handle.dispose(); }); }); }); /// 例如图片可点击,但没加语义,就会报错。 group('account => 检测页面可点击目标是否都有语义', () { map.forEach((name, page) { testWidgets(name, (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget(MaterialApp(home: page)); await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); handle.dispose(); }); }); }); /// (这个测试仅为示例展示,由于本项目文字与背景遵照设计图,此项测试多处不通过,因此后面的模块不进行此项测试) group('account => 检测页面文本对比度是否满足最小值的准则', () { final List themes = [ ThemeProvider().getTheme(), ThemeProvider().getTheme(isDarkMode: true), ]; const List themeNames = [ 'LightTheme', 'DarkTheme', ]; for (int themeIndex = 0; themeIndex < themes.length; themeIndex += 1) { final ThemeData theme = themes[themeIndex]; final String themeName = themeNames[themeIndex]; map.forEach((name, page) { testWidgets('$name $themeName', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget(MaterialApp(theme: theme, home: page)); await expectLater(tester, meetsGuideline(textContrastGuideline)); handle.dispose(); }, skip: name == 'add_withdrawal_account_page' || name == 'withdrawal_page' || name == 'withdrawal_result_page' ); // https://github.com/flutter/flutter/issues/21647 }); } }); } ================================================ FILE: test/goods/goods_accessibility_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/goods/page/goods_edit_page.dart'; import 'package:flutter_deer/goods/page/goods_page.dart'; import 'package:flutter_deer/goods/page/goods_search_page.dart'; import 'package:flutter_deer/goods/page/goods_size_edit_page.dart'; import 'package:flutter_deer/goods/page/goods_size_page.dart'; import 'package:flutter_deer/setting/provider/theme_provider.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { final Map map = {}; map['goods_page'] = const GoodsPage(); map['goods_edit_page'] = const GoodsEditPage(); map['goods_search_page'] = const GoodsSearchPage(); map['goods_size_page'] = const GoodsSizePage(); map['goods_size_edit_page'] = const GoodsSizeEditPage(); group('goods => 检测页面可点击目标大小是否大于44 * 44', () { final ThemeData themeData = ThemeProvider().getTheme(); map.forEach((name, page) { testWidgets(name, (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget(MaterialApp(home: page, theme: themeData,)); if (name == 'goods_page') { // GoodsListPage 内有一个2秒的延时 await tester.pumpAndSettle(const Duration(seconds: 2)); } await expectLater(tester, meetsGuideline(iOSTapTargetGuideline)); handle.dispose(); }, skip: name == 'goods_search_page' || name == 'goods_size_page'); // https://github.com/flutter/flutter/issues/42455 }); }); group('goods => 检测页面可点击目标是否都有语义', () { final ThemeData themeData = ThemeProvider().getTheme(); map.forEach((name, page) { testWidgets(name, (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget(MaterialApp(home: page, theme: themeData,)); if (name == 'goods_page') { await tester.pumpAndSettle(const Duration(seconds: 2)); } await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); handle.dispose(); }); }); }); } ================================================ FILE: test/login/login_accessibility_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/login/page/login_page.dart'; import 'package:flutter_deer/login/page/register_page.dart'; import 'package:flutter_deer/login/page/reset_password_page.dart'; import 'package:flutter_deer/login/page/sms_login_page.dart'; import 'package:flutter_deer/login/page/update_password_page.dart'; import 'package:flutter_deer/main.dart'; import 'package:flutter_deer/setting/provider/theme_provider.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { final Map map = {}; map['login_page'] = const LoginPage(); map['register_page'] = const RegisterPage(); map['reset_password_page.dart'] = const ResetPasswordPage(); map['sms_login_page.dart'] = const SMSLoginPage(); map['update_password_page.dart'] = const UpdatePasswordPage(); /// 这里就不检测页面可点击目标大小了,因为不符合。。。 group('login => 检测页面可点击目标是否都有语义', () { final ThemeData themeData = ThemeProvider().getTheme(); map.forEach((name, page) { testWidgets(name, (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget(MyApp(home: page, theme: themeData,)); await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); handle.dispose(); }); }); }); } ================================================ FILE: test/net/dio_test.dart ================================================ import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_deer/net/net.dart'; import 'package:flutter_deer/shop/models/user_entity.dart'; import 'package:test/test.dart'; void main() { group('dio_test', () { Dio dio; setUp(() { /// 测试配置 dio = DioUtils.instance.dio; dio.options.baseUrl = 'https://api.github.com/'; }); test('getUsers', () async { await DioUtils.instance.requestNetwork( Method.get, HttpApi.users, onSuccess: (data) { expect(data?.name, '唯鹿'); }, onError: (code, msg) { debugPrint('$code, $msg'); } ); }); }); } ================================================ FILE: test/order/order_accessibility_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/order/page/order_info_page.dart'; import 'package:flutter_deer/order/page/order_page.dart'; import 'package:flutter_deer/order/page/order_track_page.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { final Map map = {}; map['order_page'] = const OrderPage(); map['order_info_page'] = const OrderInfoPage(); map['order_track_page'] = const OrderTrackPage(); group('order => 检测页面可点击目标大小是否大于44 * 44', () { map.forEach((name, page) { testWidgets(name, (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget(MaterialApp(home: page)); await expectLater(tester, meetsGuideline(iOSTapTargetGuideline)); handle.dispose(); }, skip: name == 'order_search_page' || name == 'order_track_page' || name == 'order_page' ); // https://github.com/flutter/flutter/issues/42455 }); }); group('order => 检测页面可点击目标是否都有语义', () { map.forEach((name, page) { testWidgets(name, (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget(MaterialApp(home: page)); if (name == 'order_page') { await tester.pumpAndSettle(const Duration(seconds: 2)); } await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); handle.dispose(); }); }); }); } ================================================ FILE: test/setting/setting_accessibility_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/main.dart'; import 'package:flutter_deer/setting/page/about_page.dart'; import 'package:flutter_deer/setting/page/account_manager_page.dart'; import 'package:flutter_deer/setting/page/setting_page.dart'; import 'package:flutter_deer/setting/page/theme_page.dart'; import 'package:flutter_deer/setting/provider/theme_provider.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { final Map map = {}; map['about_page'] = const AboutPage(); map['account_manager_page'] = const AccountManagerPage(); map['setting_page'] = const SettingPage(); map['theme_page'] = const ThemePage(); group('setting => 检测页面可点击目标大小是否大于44 * 44', () { map.forEach((name, page) { testWidgets(name, (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget(MyApp(home: page)); await expectLater(tester, meetsGuideline(iOSTapTargetGuideline)); handle.dispose(); }); }); }); group('setting => 检测页面可点击目标是否都有语义', () { map.forEach((name, page) { testWidgets(name, (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget(MyApp(home: page)); await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); handle.dispose(); }); }); }); group('setting => 检测页面文本对比度是否满足最小值的准则', () { final List themes = [ ThemeProvider().getTheme(), ThemeProvider().getTheme(isDarkMode: true), ]; const List themeNames = [ 'LightTheme', 'DarkTheme', ]; for (int themeIndex = 0; themeIndex < themes.length; themeIndex += 1) { final ThemeData theme = themes[themeIndex]; final String themeName = themeNames[themeIndex]; map.forEach((name, page) { testWidgets('$name $themeName', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget(MyApp(theme: theme, home: page)); await expectLater(tester, meetsGuideline(textContrastGuideline)); handle.dispose(); }); }); } }); } ================================================ FILE: test/shop/shop_accessibility_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/setting/provider/theme_provider.dart'; import 'package:flutter_deer/shop/page/freight_config_page.dart'; import 'package:flutter_deer/shop/page/input_text_page.dart'; import 'package:flutter_deer/shop/page/message_page.dart'; import 'package:flutter_deer/shop/page/select_address_page.dart'; import 'package:flutter_deer/shop/page/shop_page.dart'; import 'package:flutter_deer/shop/page/shop_setting_page.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { final Map map = {}; map['shop_page'] = const ShopPage(isAccessibilityTest: true,); map['shop_setting_page'] = const ShopSettingPage(); map['select_address_page'] = const AddressSelectPage(); map['message_page'] = const MessagePage(); map['input_text_page'] = const InputTextPage(title: '测试', hintText: '输入测试内容'); map['freight_config_page'] = const FreightConfigPage(); group('shop => 检测页面可点击目标大小是否大于44 * 44', () { final ThemeData themeData = ThemeProvider().getTheme(); map.forEach((name, page) { testWidgets(name, (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget(MaterialApp(home: page, theme: themeData,)); await expectLater(tester, meetsGuideline(iOSTapTargetGuideline)); handle.dispose(); }, skip: name == 'select_address_page' || name == 'freight_config_page'); }); }); group('shop => 检测页面可点击目标是否都有语义', () { final ThemeData themeData = ThemeProvider().getTheme(); map.forEach((name, page) { testWidgets(name, (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget(MaterialApp(home: page, theme: themeData,)); await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); handle.dispose(); }); }); }); } ================================================ FILE: test/statistics/statistic_accessibility_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/setting/provider/theme_provider.dart'; import 'package:flutter_deer/statistics/page/goods_statistics_page.dart'; import 'package:flutter_deer/statistics/page/order_statistics_page.dart'; import 'package:flutter_deer/statistics/page/statistics_page.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { final Map map = {}; map['statistics_page'] = const StatisticsPage(); map['order_statistics_page'] = const OrderStatisticsPage(1); map['goods_statistics_page'] = const GoodsStatisticsPage(); group('statistics => 检测页面可点击目标大小是否大于44 * 44', () { final ThemeData themeData = ThemeProvider().getTheme(); map.forEach((name, page) { testWidgets(name, (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget(MaterialApp(home: page, theme: themeData,)); await expectLater(tester, meetsGuideline(iOSTapTargetGuideline)); handle.dispose(); }, skip: name == 'order_statistics_page'); }); }); group('statistics => 检测页面可点击目标是否都有语义', () { final ThemeData themeData = ThemeProvider().getTheme(); map.forEach((name, page) { testWidgets(name, (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget(MaterialApp(home: page, theme: themeData,)); await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); handle.dispose(); }); }); }); } ================================================ FILE: test/store/store_accessibility_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/setting/provider/theme_provider.dart'; import 'package:flutter_deer/store/page/store_audit_page.dart'; import 'package:flutter_deer/store/page/store_audit_result_page.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { final Map map = {}; map['store_audit_page'] = const StoreAuditPage(); map['store_audit_result_page'] = const StoreAuditResultPage(); group('store => 检测页面可点击目标大小是否大于44 * 44', () { map.forEach((name, page) { testWidgets(name, (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget(MaterialApp(home: page)); await expectLater(tester, meetsGuideline(iOSTapTargetGuideline)); handle.dispose(); }); }); }); group('store => 检测页面可点击目标是否都有语义', () { final ThemeData themeData = ThemeProvider().getTheme(); map.forEach((name, page) { testWidgets(name, (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget(MaterialApp(home: page, theme: themeData,)); await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); handle.dispose(); }); }); }); } ================================================ FILE: test/widget_test.dart ================================================ // This is a basic Flutter widget test. // // To perform an interaction with a widget in your test, use the WidgetTester // utility that Flutter provides. For example, you can send tap and scroll // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. import 'package:flutter/material.dart'; import 'package:flutter_deer/shop/page/message_page.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { testWidgets('widget test', (WidgetTester tester) async { // Build our app and trigger a frame. await tester.pumpWidget(const MaterialApp(home: MessagePage(),)); expect(find.text('消息'), findsOneWidget); }); } ================================================ FILE: test_driver/account/account.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/home/home_page.dart'; import 'package:flutter_deer/main.dart'; import 'package:flutter_deer/res/constant.dart'; import 'package:flutter_driver/driver_extension.dart'; /// 运行 flutter drive --target=test_driver/account/account.dart void main() { enableFlutterDriverExtension(); Constant.isDriverTest = true; runApp(MyApp(home: const Home())); } ================================================ FILE: test_driver/account/account_test.dart ================================================ import 'package:flutter_driver/flutter_driver.dart'; import 'package:test/test.dart'; import '../tools/test_utils.dart'; void main() { group('账户部分:', () { late FlutterDriver driver; setUpAll(() async { driver = await FlutterDriver.connect(); await driver.waitUntilFirstFrameRasterized(); }); tearDown(() { print('< Success'); }); tearDownAll(() async { await driver.close(); }); test('账户流水页测试',() async { await driver.tap(find.byTooltip('店铺')); await delayed(); await driver.tap(find.text('账户流水')); await delayed(); await driver.tap(find.byTooltip('Back')); await delayed(); await driver.tap(find.text('提现账号')); await delayed(); await driver.tap(find.text('微信')); await delayed(); await delayed(); }); test('添加账号页测试',() async { await driver.tap(find.text('添加')); await delayed(); await driver.tap(find.text('账号类型')); await delayed(); await driver.tap(find.text('银行卡(对公账户)')); await delayed(); // 选择城市 await driver.tap(find.text('开 户 地')); await delayed(); await driver.tap(find.text('北京市')); await delayed(); // 选择银行 await driver.tap(find.text('银行名称')); await delayed(); await driver.tap(find.text('建设银行')); await delayed(); await driver.tap(find.text('支行名称')); await delayed(); await driver.tap(find.byTooltip('Back')); await delayed(); await driver.tap(find.byTooltip('Back')); await delayed(); await driver.tap(find.byTooltip('Back')); await delayed(); }); test('资金管理页测试',() async { await driver.tap(find.text('资金管理')); await delayed(); await driver.tap(find.text('提现记录')); await delayed(); await driver.tap(find.byTooltip('Back')); await delayed(); }); test('提现页测试',() async { await driver.tap(find.text('提现')); await delayed(); await driver.tap(find.text('工商银行')); await delayed(); await driver.tap(find.text('微信')); await delayed(); await driver.tap(find.text('全部提现')); await delayed(); await driver.tap(find.byValueKey('提现')); await delayed(); await driver.tap(find.text('返回')); await delayed(); await driver.tap(find.byTooltip('Back')); await delayed(); }); test('提现密码页测试',() async { await driver.tap(find.text('提现密码')); await delayed(); await driver.tap(find.text('修改密码')); await delayed(); await driver.tap(find.text('2')); await driver.tap(find.text('1')); await driver.tap(find.text('6')); await driver.tap(find.text('8')); await driver.tap(find.text('9')); await driver.tap(find.text('0')); await delayed(); await driver.tap(find.byValueKey('close')); await delayed(); await driver.tap(find.text('忘记密码')); await delayed(); await driver.tap(find.text('确定')); await delayed(); await driver.tap(find.text('获取验证码')); await delayed(); await driver.tap(find.byValueKey('vcode')); await delayed(); await driver.enterText('111'); await delayed(); await driver.enterText('111222'); await delayed(); await driver.tap(find.byValueKey('dialog_close')); await delayed(); await driver.tap(find.byTooltip('Back')); await delayed(); await driver.tap(find.byTooltip('Back')); await delayed(); /// 进入设置页,便于执行设置模块测试操作 await driver.tap(find.byValueKey('setting')); await delayed(); }); }); } ================================================ FILE: test_driver/driver.dart ================================================ import 'package:flutter_deer/main.dart' as app; import 'package:flutter_deer/res/constant.dart'; import 'package:flutter_driver/driver_extension.dart'; /// 集成测试运行 flutter drive --profile --target=test_driver/driver.dart (模拟器去除--profile) /// 集成测试暂不支持拖动、长按等方式。 https://github.com/flutter/flutter/issues/27552 /// 其他问题记录:https://github.com/flutter/flutter/issues/12561 void main() { enableFlutterDriverExtension(); Constant.isDriverTest = true; app.main(); } ================================================ FILE: test_driver/driver_test.dart ================================================ import 'account/account_test.dart' as account_test; import 'goods/goods_test.dart' as goods_test; import 'home/splash_page_test.dart' as splash_test; import 'login/login_page_test.dart' as login_test; import 'order/order_test.dart' as order_test; import 'setting/setting_test.dart' as setting_test; import 'shop/shop_test.dart' as shop_test; import 'statistic/statistic_test.dart' as statistic_test; import 'store/store_test.dart' as store_test; /// 集成测试 /// /// 测试运行 flutter drive --profile --target=test_driver/driver.dart /// /// 各模块统一运行,也可单独执行子模块测试 void main() { splash_test.main(); login_test.main(); store_test.main(); order_test.main(); goods_test.main(); statistic_test.main(); shop_test.main(); account_test.main(); setting_test.main(['backHome']); } ================================================ FILE: test_driver/goods/goods.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/home/home_page.dart'; import 'package:flutter_deer/main.dart'; import 'package:flutter_deer/res/constant.dart'; import 'package:flutter_driver/driver_extension.dart'; /// 运行 flutter drive --target=test_driver/goods/goods.dart void main() { enableFlutterDriverExtension(); Constant.isDriverTest = true; runApp(MyApp(home: const Home())); } ================================================ FILE: test_driver/goods/goods_test.dart ================================================ import 'package:flutter_driver/flutter_driver.dart'; import 'package:test/test.dart'; import '../tools/test_utils.dart'; void main() { group('商品部分:', () { late FlutterDriver driver; setUpAll(() async { driver = await FlutterDriver.connect(); await driver.waitUntilFirstFrameRasterized(); }); tearDown(() { print('< Success'); }); tearDownAll(() async { await driver.close(); }); test('商品页测试',() async { await driver.tap(find.byTooltip('商品')); await delayed(); await driver.tap(find.text('待售')); await delayed(); final SerializableFinder pageView = find.byValueKey('pageView'); await driver.waitFor(pageView); await driver.scroll(pageView, 400.0, 0, scrollDuration); await delayed(); await driver.tap(find.text('全部商品')); await delayed(); await driver.tap(find.text('休闲食品')); await delayed(); //进入搜索页 await driver.tap(find.byValueKey('search')); await delayed(); await driver.tap(find.byValueKey('search_back')); await delayed(); //添加商品 await driver.tap(find.byValueKey('add')); await delayed(); await driver.tap(find.text('添加商品')); await delayed(); await driver.tap(find.byTooltip('Back')); await delayed(); // 商品菜单 await driver.tap(find.byValueKey('goods_menu_item_2')); await delayed(); await driver.tap(find.byValueKey('goods_operation_item_2')); await delayed(); await driver.tap(find.byValueKey('goods_delete_item_2')); await delayed(); await driver.tap(find.text('确认删除')); await delayed(); await driver.tap(find.byValueKey('goods_menu_item_1')); await delayed(); await driver.tap(find.byValueKey('goods_edit_item_1')); await delayed(); }); test('商品编辑页测试',() async { await driver.scroll(find.byValueKey('goods_edit_page'), 0, -500, scrollDuration); await delayed(); await driver.tap(find.text('商品类型')); await delayed(); await driver.tap(find.text('生鲜果蔬')); await delayed(); await driver.tap(find.text('厨房用具')); await delayed(); await driver.tap(find.text('碗碟')); await delayed(); }); test('商品规格页测试',() async { await driver.tap(find.text('商品规格')); await delayed(); await driver.tap(find.byValueKey('hint')); await delayed(); /// 在集成测试中不能点击特定的TextSpan,这里测试部分放在integration_test/goods_test.dart 实现 /// https://github.com/flutter/flutter/issues/67123 // await driver.tap(find.byValueKey('name_edit')); // await delayed(); // await driver.tap(find.text('取消')); // await delayed(); await driver.tap(find.byValueKey('2')); await delayed(); await driver.tap(find.byTooltip('Back')); await delayed(); // 侧滑删除 await driver.scroll(find.byValueKey('2'), -100.0, 0, scrollDuration); await delayed(); await driver.tap(find.byValueKey('delete_2')); await delayed(); await driver.tap(find.byTooltip('Back')); await delayed(); await driver.tap(find.byTooltip('Back')); await delayed(); }); }); } ================================================ FILE: test_driver/home/splash_page.dart ================================================ import 'package:flutter_deer/main.dart' as app; import 'package:flutter_driver/driver_extension.dart'; /// 运行 flutter drive --target=test_driver/home/splash_page.dart void main() { enableFlutterDriverExtension(); app.main(); } ================================================ FILE: test_driver/home/splash_page_test.dart ================================================ import 'package:flutter_driver/flutter_driver.dart'; import 'package:test/test.dart'; import '../tools/test_utils.dart'; void main([List args = const []]) { group('引导页:', () { late FlutterDriver driver; // 测试之前连接程序 setUpAll(() async { driver = await FlutterDriver.connect(); await driver.waitUntilFirstFrameRasterized(); }); tearDown(() { print('< Success'); }); // 在测试完成后,关闭程序的连接。 tearDownAll(() async { await driver.close(); }); test('测试引导页滑动',() async { final SerializableFinder swiperFinder = find.byValueKey('swiper'); /// 引导页的第三张图 final SerializableFinder imageFinder = find.byValueKey('app_start_3'); await delayed(); await driver.scrollUntilVisible(swiperFinder, imageFinder, dxScroll: -300); }); test('点击最后一张引导图',() async { final SerializableFinder imageFinder = find.byValueKey('app_start_3'); /// 点击第三张图片 await driver.tap(imageFinder); await delayed(); }); }); } ================================================ FILE: test_driver/login/login_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/login/page/login_page.dart'; import 'package:flutter_deer/main.dart'; import 'package:flutter_driver/driver_extension.dart'; /// 运行 flutter drive --target=test_driver/login/login_page.dart void main() { enableFlutterDriverExtension(); runApp(MyApp(home: const LoginPage())); } ================================================ FILE: test_driver/login/login_page_test.dart ================================================ import 'package:flutter_driver/flutter_driver.dart'; import 'package:test/test.dart'; import '../tools/test_utils.dart'; void main() { group('登录部分:', () { late FlutterDriver driver; setUpAll(() async { driver = await FlutterDriver.connect(); await driver.waitUntilFirstFrameRasterized(); }); tearDown(() { print('< Success'); }); tearDownAll(() async { await driver.close(); }); test('登录页按钮点击',() async { await driver.tap(find.byValueKey('actionName')); await delayed(); await driver.tap(find.byTooltip('Back')); await delayed(); await driver.tap(find.byValueKey('forgotPassword')); await delayed(); await driver.tap(find.byTooltip('Back')); await delayed(); await driver.tap(find.byValueKey('noAccountRegister')); await delayed(); }); test('注册页测试',() async { await driver.tap(find.byValueKey('getVerificationCode'));/// 无法成功触发事件,需要输入手机号 await delayed(); final SerializableFinder textField = find.byValueKey('phone'); await driver.tap(textField); // 点击输入框,给予焦点 await delayed(); await driver.enterText('15000000000'); // 输入内容 await delayed(); await driver.tap(find.byValueKey('getVerificationCode')); await delayed(); final SerializableFinder textField2 = find.byValueKey('vcode'); await driver.tap(textField2); await delayed(); await driver.enterText('123456'); await delayed(); final SerializableFinder textField3 = find.byValueKey('password'); await driver.tap(textField3); await delayed(); await driver.enterText('111111'); await delayed(); await driver.tap(find.byValueKey('register')); // 点击注册 await delayed(); // 清除输入框文字 await driver.tap(find.byValueKey('password_delete')); await delayed(); await driver.tap(find.byTooltip('Back')); await delayed(); }, timeout: const Timeout(Duration(seconds: 30))); test('登录页测试',() async { final SerializableFinder textField = find.byValueKey('phone'); await driver.tap(textField); await delayed(); await driver.enterText('15000000000'); await delayed(); final SerializableFinder textField2 = find.byValueKey('password'); await driver.tap(textField2); await delayed(); await driver.enterText('111111'); await delayed(); // 点击密码可见两次 await driver.tap(find.byValueKey('password_showPwd')); await delayed(); await driver.tap(find.byValueKey('password_showPwd')); await delayed(); await driver.tap(find.byValueKey('login')); // 点击登录 await delayed(); }, timeout: const Timeout(Duration(seconds: 30))); }); } ================================================ FILE: test_driver/order/order.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/home/home_page.dart'; import 'package:flutter_deer/main.dart'; import 'package:flutter_deer/res/constant.dart'; import 'package:flutter_driver/driver_extension.dart'; /// 运行 flutter drive --target=test_driver/order/order.dart void main() { enableFlutterDriverExtension(); Constant.isDriverTest = true; runApp(MyApp(home: const Home())); } ================================================ FILE: test_driver/order/order_test.dart ================================================ import 'package:flutter_driver/flutter_driver.dart'; import 'package:test/test.dart'; import '../tools/test_utils.dart'; void main() { group('订单部分:', () { late FlutterDriver driver; setUpAll(() async { driver = await FlutterDriver.connect(); await driver.waitUntilFirstFrameRasterized(); }); tearDown(() { print('< Success'); }); tearDownAll(() async { await driver.close(); }); test('滑动订单列表',() async { await driver.tap(find.byTooltip('订单')); await delayed(); /// 水平滑动 final SerializableFinder pageView = find.byValueKey('pageView'); await driver.scroll(pageView, -400.0, 0, scrollDuration); await delayed(); await driver.scroll(pageView, -400.0, 0, scrollDuration); await delayed(); final SerializableFinder orderList = find.byValueKey('order_list'); await driver.waitFor(orderList); /// 垂直滑动 await driver.scroll(orderList, 0, -800, scrollDuration); await delayed(); final SerializableFinder orderItem = find.byValueKey('order_item_1'); await driver.scrollUntilVisible(orderList, orderItem, dyScroll: 400); await delayed(); /// 滚动finder所在列表,直到该小部件完全可见。 await driver.scrollIntoView(orderItem); }); test('订单操作',() async { /// 点击订单列表按钮 await driver.tap(find.byValueKey('order_button_1_1')); await delayed(); await driver.tap(find.text('取消')); await delayed(); await driver.tap(find.byValueKey('order_button_3_1')); await delayed(); await driver.tap(find.text('确定')); await driver.scroll(find.byValueKey('order_list'), 0.0, 500.0, scrollDuration); await delayed(); }); test('订单详情页',() async { final SerializableFinder orderItem = find.byValueKey('order_item_2'); await driver.tap(orderItem); await delayed(); await driver.tap(find.text('订单跟踪')); await delayed(); await driver.tap(find.byTooltip('Back')); await delayed(); await driver.scroll(find.byValueKey('order_info'), 0.0, -1000.0, scrollDuration); await delayed(); await driver.tap(find.byTooltip('Back')); await delayed(); }); test('订单搜索页测试',() async { await driver.tap(find.byTooltip('搜索')); await delayed(); await driver.tap(find.byValueKey('search_text_field'), timeout: const Duration(minutes: 1),); await driver.enterText('flutter'); await driver.tap(find.text('搜索')); final SerializableFinder orderList = find.byValueKey('order_search_list'); await driver.waitFor(orderList, timeout: const Duration(minutes: 1),); await driver.scroll(orderList, 0.0, -300.0, scrollDuration); await delayed(); await driver.tap(find.byValueKey('search_back')); await delayed(); }, timeout: const Timeout.factor(5)); }); } ================================================ FILE: test_driver/setting/setting.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/main.dart'; import 'package:flutter_deer/res/constant.dart'; import 'package:flutter_deer/setting/page/setting_page.dart'; import 'package:flutter_driver/driver_extension.dart'; import 'package:sp_util/sp_util.dart'; /// 运行 flutter drive --target=test_driver/setting/setting.dart Future main() async { enableFlutterDriverExtension(); Constant.isDriverTest = true; WidgetsFlutterBinding.ensureInitialized(); /// sp初始化 await SpUtil.getInstance(); runApp(MyApp(home: const SettingPage())); } ================================================ FILE: test_driver/setting/setting_test.dart ================================================ import 'package:flutter_driver/flutter_driver.dart'; import 'package:test/test.dart'; import '../tools/test_utils.dart'; void main([List args = const []]) { group('设置部分:', () { late FlutterDriver driver; setUpAll(() async { driver = await FlutterDriver.connect(); await driver.waitUntilFirstFrameRasterized(); }); tearDown(() { print('< Success'); }); tearDownAll(() async { await driver.close(); }); test('设置页测试',() async { await driver.tap(find.text('账号管理')); await delayed(); await driver.tap(find.text('修改密码')); await delayed(); await driver.tap(find.byTooltip('Back')); await delayed(); await driver.tap(find.byTooltip('Back')); await delayed(); await driver.tap(find.text('检查更新')); await delayed(); await driver.tap(find.text('残忍拒绝')); await delayed(); }); test('关于我们页测试',() async { await driver.tap(find.text('关于我们')); await delayed(); await driver.tap(find.text('作者博客')); await Future.delayed(const Duration(seconds: 3)); await driver.tap(find.byTooltip('Back')); await delayed(); await driver.tap(find.byTooltip('Back')); await delayed(); }); test('夜间模式页测试',() async { await driver.tap(find.text('夜间模式')); await delayed(); await driver.tap(find.text('开启')); await Future.delayed(const Duration(seconds: 4)); await driver.tap(find.byTooltip('Back')); await delayed(); // 查看效果 if (args.contains('backHome')) { await driver.tap(find.byTooltip('Back')); await delayed(); await driver.tap(find.byTooltip('订单')); await delayed(); await driver.tap(find.byTooltip('商品')); await delayed(); await driver.tap(find.byTooltip('统计')); await delayed(); await driver.tap(find.byTooltip('店铺')); await delayed(); await driver.tap(find.byValueKey('setting')); await delayed(); } }); test('多语言页测试',() async { await driver.tap(find.text('多语言')); await delayed(); await driver.tap(find.text('English')); await Future.delayed(const Duration(seconds: 2)); await driver.tap(find.byTooltip('Back')); await delayed(); // 退出后在登录页查看效果 await driver.tap(find.text('退出当前账号')); await delayed(); await driver.tap(find.text('确定')); await delayed(); // 登录页按钮点击 await driver.tap(find.byValueKey('actionName')); await delayed(); await driver.tap(find.byTooltip('Back')); await delayed(); await driver.tap(find.byValueKey('forgotPassword')); await delayed(); await driver.tap(find.byTooltip('Back')); await delayed(); await driver.tap(find.byValueKey('noAccountRegister')); await delayed(); }); }); } ================================================ FILE: test_driver/shop/shop.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/home/home_page.dart'; import 'package:flutter_deer/main.dart'; import 'package:flutter_deer/res/constant.dart'; import 'package:flutter_driver/driver_extension.dart'; /// 运行 flutter drive --target=test_driver/shop/shop.dart void main() { enableFlutterDriverExtension(); Constant.isDriverTest = true; runApp(MyApp(home: const Home())); } ================================================ FILE: test_driver/shop/shop_test.dart ================================================ import 'package:flutter_driver/flutter_driver.dart'; import 'package:test/test.dart'; import '../tools/test_utils.dart'; void main() { group('店铺部分:', () { late FlutterDriver driver; setUpAll(() async { driver = await FlutterDriver.connect(); await driver.waitUntilFirstFrameRasterized(); }); tearDown(() { print('< Success'); }); tearDownAll(() async { await driver.close(); }); test('店铺页测试',() async { await driver.tap(find.byTooltip('店铺')); await delayed(); await driver.tap(find.byValueKey('message'), timeout: const Duration(minutes: 1),); await delayed(); await driver.tap(find.byTooltip('Back')); await delayed(); }, timeout: const Timeout.factor(3)); test('店铺设置页测试',() async { await driver.tap(find.text('店铺设置')); await delayed(); await driver.tap(find.text('店铺简介')); await delayed(); await driver.tap(find.byTooltip('Back')); await delayed(); // 弹框 await driver.tap(find.text('支付方式')); await delayed(); await driver.tap(find.text('货到付款')); await delayed(); await driver.tap(find.text('确定')); await delayed(); await driver.tap(find.text('配送费用')); await delayed(); await driver.tap(find.byValueKey('price_input')); // 点击输入框,给予焦点 await driver.enterText('3.1'); // 输入内容 await delayed(); await driver.tap(find.text('确定')); await delayed(); // 弹框 await driver.tap(find.text('运费配置')); await delayed(); await driver.tap(find.text('运费比例配置')); await delayed(); await driver.tap(find.text('确定')); await delayed(); }); test('运费配置页测试',() async { await driver.tap(find.text('运费比例')); await delayed(); await driver.tap(find.byValueKey('add')); await delayed(); await driver.tap(find.byValueKey('订单金额0')); await delayed(); await driver.tap(find.byValueKey('price_input')); // 点击输入框,给予焦点 await driver.enterText('3'); // 输入内容 await delayed(); await driver.tap(find.text('确定')); await delayed(); await driver.tap(find.byValueKey('add')); await delayed(); await driver.tap(find.text('重置')); await delayed(); await driver.tap(find.text('完成')); await delayed(); await driver.tap(find.byTooltip('Back')); await delayed(); }); }); } ================================================ FILE: test_driver/statistic/statistic.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/home/home_page.dart'; import 'package:flutter_deer/main.dart'; import 'package:flutter_driver/driver_extension.dart'; /// 运行 flutter drive --target=test_driver/statistic/statistic.dart void main() { enableFlutterDriverExtension(); runApp(MyApp(home: const Home())); } ================================================ FILE: test_driver/statistic/statistic_test.dart ================================================ import 'package:flutter_driver/flutter_driver.dart'; import 'package:test/test.dart'; import '../tools/test_utils.dart'; void main() { group('统计部分:', () { late FlutterDriver driver; setUpAll(() async { driver = await FlutterDriver.connect(); await driver.waitUntilFirstFrameRasterized(); }); tearDown(() { print('< Success'); }); tearDownAll(() async { await driver.close(); }); test('统计页测试',() async { await driver.tap(find.byTooltip('统计')); await delayed(); await driver.scroll(find.byValueKey('statistic_list'), 0, -300, scrollDuration); await delayed(); }); test('商品统计页测试',() async { await driver.tap(find.text('商品统计')); await delayed(); await driver.tap(find.byValueKey('actionName')); await delayed(); await driver.tap(find.byValueKey('actionName')); await delayed(); await driver.scroll(find.byValueKey('goods_statistics_list'), 0, -300, scrollDuration); await delayed(); await driver.scroll(find.byValueKey('goods_statistics_list'), 0, 300, scrollDuration); await delayed(); await driver.tap(find.byTooltip('Back')); await delayed(); }); test('订单统计页测试',() async { await driver.scroll(find.byValueKey('statistic_list'), 0, 300, scrollDuration); await delayed(); await driver.tap(find.text('订单统计')); await delayed(); await driver.tap(find.byValueKey('month')); await delayed(); await driver.tap(find.byValueKey('year')); await delayed(); await driver.tap(find.byTooltip('Back')); await delayed(); }); }); } ================================================ FILE: test_driver/store/store.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_deer/main.dart'; import 'package:flutter_deer/store/page/store_audit_page.dart'; import 'package:flutter_driver/driver_extension.dart'; /// 运行 flutter drive --target=test_driver/store/store.dart void main() { enableFlutterDriverExtension(); runApp(MyApp(home: const StoreAuditPage())); } ================================================ FILE: test_driver/store/store_test.dart ================================================ import 'package:flutter_driver/flutter_driver.dart'; import 'package:test/test.dart'; import '../tools/test_utils.dart'; void main() { group('审核部分:', () { late FlutterDriver driver; setUpAll(() async { driver = await FlutterDriver.connect(); await driver.waitUntilFirstFrameRasterized(); }); tearDown(() { print('< Success'); }); tearDownAll(() async { await driver.close(); }); test('店铺审核资料页测试',() async { await driver.tap(find.text('主营范围')); await delayed(); final SerializableFinder sortList = find.byValueKey('goods_sort'); await delayed(); await driver.scroll(sortList, 0.0, -300.0, scrollDuration); await delayed(); await driver.scroll(sortList, 0.0, 100.0, scrollDuration); await driver.tap(find.text('休闲食品')); await delayed(); await driver.tap(find.text('提交')); await delayed(); }); test('审核结果页测试',() async { await delayed(); await driver.tap(find.text('进入')); await delayed(); }); }); } ================================================ FILE: test_driver/tools/test_utils.dart ================================================ Future delayed ({int milliseconds = 666}) async { /// 适当延时,让操作节奏慢下来 return Future.delayed(Duration(milliseconds: milliseconds)); } const Duration scrollDuration = Duration(milliseconds: 300); ================================================ FILE: web/index.html ================================================ flutter_deer ================================================ FILE: web/index1.html ================================================ flutter_deer ================================================ FILE: web/manifest.json ================================================ { "name": "Flutter Deer", "short_name": "Flutter Deer", "start_url": ".", "display": "standalone", "background_color": "#0175C2", "theme_color": "#FFFFFF", "description": "This project is an exercise project for individuals to learn Flutter.", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [ { "src": "icons/Icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "icons/Icon-512.png", "sizes": "512x512", "type": "image/png" } ] } ================================================ FILE: windows/.gitignore ================================================ flutter/ephemeral/ # Visual Studio user-specific files. *.suo *.user *.userosscache *.sln.docstates # Visual Studio build-related files. x64/ x86/ # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ ================================================ FILE: windows/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.15) project(flutter_deer LANGUAGES CXX) set(BINARY_NAME "flutter_deer") cmake_policy(SET CMP0063 NEW) set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") # Configure build options. get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) if(IS_MULTICONFIG) set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" CACHE STRING "" FORCE) else() if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "Flutter build mode" FORCE) set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Profile" "Release") endif() endif() set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") # Use Unicode for all projects. add_definitions(-DUNICODE -D_UNICODE) # Compilation settings that should be applied to most targets. function(APPLY_STANDARD_SETTINGS TARGET) target_compile_features(${TARGET} PUBLIC cxx_std_17) target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") target_compile_options(${TARGET} PRIVATE /EHsc) target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") endfunction() set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") # Flutter library and tool build rules. add_subdirectory(${FLUTTER_MANAGED_DIR}) # Application build add_subdirectory("runner") # Generated plugin build rules, which manage building the plugins and adding # them to the application. include(flutter/generated_plugins.cmake) # === Installation === # Support files are copied into place next to the executable, so that it can # run in place. This is done instead of making a separate bundle (as on Linux) # so that building and running from within Visual Studio will work. set(BUILD_BUNDLE_DIR "$") # Make the "install" step default, as it's required to run. set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) endif() set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" COMPONENT Runtime) install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) if(PLUGIN_BUNDLED_LIBRARIES) install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) endif() # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. set(FLUTTER_ASSET_DIR_NAME "flutter_assets") install(CODE " file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") " COMPONENT Runtime) install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) # Install the AOT library on non-Debug builds only. install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" CONFIGURATIONS Profile;Release COMPONENT Runtime) ================================================ FILE: windows/flutter/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.15) set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") # Configuration provided via flutter tool. include(${EPHEMERAL_DIR}/generated_config.cmake) # TODO: Move the rest of this into files in ephemeral. See # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") # Published to parent scope for install step. set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) list(APPEND FLUTTER_LIBRARY_HEADERS "flutter_export.h" "flutter_windows.h" "flutter_messenger.h" "flutter_plugin_registrar.h" "flutter_texture_registrar.h" ) list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") add_library(flutter INTERFACE) target_include_directories(flutter INTERFACE "${EPHEMERAL_DIR}" ) target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") add_dependencies(flutter flutter_assemble) # === Wrapper === list(APPEND CPP_WRAPPER_SOURCES_CORE "core_implementations.cc" "standard_codec.cc" ) list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") list(APPEND CPP_WRAPPER_SOURCES_PLUGIN "plugin_registrar.cc" ) list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") list(APPEND CPP_WRAPPER_SOURCES_APP "flutter_engine.cc" "flutter_view_controller.cc" ) list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") # Wrapper sources needed for a plugin. add_library(flutter_wrapper_plugin STATIC ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} ) apply_standard_settings(flutter_wrapper_plugin) set_target_properties(flutter_wrapper_plugin PROPERTIES POSITION_INDEPENDENT_CODE ON) set_target_properties(flutter_wrapper_plugin PROPERTIES CXX_VISIBILITY_PRESET hidden) target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) target_include_directories(flutter_wrapper_plugin PUBLIC "${WRAPPER_ROOT}/include" ) add_dependencies(flutter_wrapper_plugin flutter_assemble) # Wrapper sources needed for the runner. add_library(flutter_wrapper_app STATIC ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_APP} ) apply_standard_settings(flutter_wrapper_app) target_link_libraries(flutter_wrapper_app PUBLIC flutter) target_include_directories(flutter_wrapper_app PUBLIC "${WRAPPER_ROOT}/include" ) add_dependencies(flutter_wrapper_app flutter_assemble) # === Flutter tool backend === # _phony_ is a non-existent file to force this command to run every time, # since currently there's no way to get a full input/output list from the # flutter tool. set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) add_custom_command( OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} ${CPP_WRAPPER_SOURCES_APP} ${PHONY_OUTPUT} COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" windows-x64 $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS "${FLUTTER_LIBRARY}" ${FLUTTER_LIBRARY_HEADERS} ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} ${CPP_WRAPPER_SOURCES_APP} ) ================================================ FILE: windows/flutter/generated_plugin_registrant.cc ================================================ // // Generated file. Do not edit. // // clang-format off #include "generated_plugin_registrant.h" #include #include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); WindowManagerPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("WindowManagerPlugin")); } ================================================ FILE: windows/flutter/generated_plugin_registrant.h ================================================ // // Generated file. Do not edit. // // clang-format off #ifndef GENERATED_PLUGIN_REGISTRANT_ #define GENERATED_PLUGIN_REGISTRANT_ #include // Registers Flutter plugins. void RegisterPlugins(flutter::PluginRegistry* registry); #endif // GENERATED_PLUGIN_REGISTRANT_ ================================================ FILE: windows/flutter/generated_plugins.cmake ================================================ # # Generated file, do not edit. # list(APPEND FLUTTER_PLUGIN_LIST file_selector_windows screen_retriever_windows url_launcher_windows window_manager ) list(APPEND FLUTTER_FFI_PLUGIN_LIST ) set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) endforeach(ffi_plugin) ================================================ FILE: windows/runner/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.15) project(runner LANGUAGES CXX) add_executable(${BINARY_NAME} WIN32 "flutter_window.cpp" "main.cpp" "run_loop.cpp" "utils.cpp" "win32_window.cpp" "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" "Runner.rc" "runner.exe.manifest" ) apply_standard_settings(${BINARY_NAME}) target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") add_dependencies(${BINARY_NAME} flutter_assemble) ================================================ FILE: windows/runner/Runner.rc ================================================ // Microsoft Visual C++ generated resource script. // #pragma code_page(65001) #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "winres.h" ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // English (United States) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE BEGIN "resource.h\0" END 2 TEXTINCLUDE BEGIN "#include ""winres.h""\r\n" "\0" END 3 TEXTINCLUDE BEGIN "\r\n" "\0" END #endif // APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Icon // // Icon with lowest ID value placed first to ensure application icon // remains consistent on all systems. IDI_APP_ICON ICON "resources\\app_icon.ico" ///////////////////////////////////////////////////////////////////////////// // // Version // #if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) #define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD #else #define VERSION_AS_NUMBER 1,0,0,0 #endif #if defined(FLUTTER_VERSION) #define VERSION_AS_STRING FLUTTER_VERSION #else #define VERSION_AS_STRING "1.0.0" #endif VS_VERSION_INFO VERSIONINFO FILEVERSION VERSION_AS_NUMBER PRODUCTVERSION VERSION_AS_NUMBER FILEFLAGSMASK VS_FFI_FILEFLAGSMASK #ifdef _DEBUG FILEFLAGS VS_FF_DEBUG #else FILEFLAGS 0x0L #endif FILEOS VOS__WINDOWS32 FILETYPE VFT_APP FILESUBTYPE 0x0L BEGIN BLOCK "StringFileInfo" BEGIN BLOCK "040904e4" BEGIN VALUE "CompanyName", "com.weilu" "\0" VALUE "FileDescription", "A new Flutter project." "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "InternalName", "flutter_deer" "\0" VALUE "LegalCopyright", "Copyright (C) 2021 com.weilu. All rights reserved." "\0" VALUE "OriginalFilename", "flutter_deer.exe" "\0" VALUE "ProductName", "flutter_deer" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" END END BLOCK "VarFileInfo" BEGIN VALUE "Translation", 0x409, 1252 END END #endif // English (United States) resources ///////////////////////////////////////////////////////////////////////////// #ifndef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 3 resource. // ///////////////////////////////////////////////////////////////////////////// #endif // not APSTUDIO_INVOKED ================================================ FILE: windows/runner/flutter_window.cpp ================================================ #include "flutter_window.h" #include #include "flutter/generated_plugin_registrant.h" FlutterWindow::FlutterWindow(RunLoop* run_loop, const flutter::DartProject& project) : run_loop_(run_loop), project_(project) {} FlutterWindow::~FlutterWindow() {} bool FlutterWindow::OnCreate() { if (!Win32Window::OnCreate()) { return false; } RECT frame = GetClientArea(); // The size here must match the window dimensions to avoid unnecessary surface // creation / destruction in the startup path. flutter_controller_ = std::make_unique( frame.right - frame.left, frame.bottom - frame.top, project_); // Ensure that basic setup of the controller was successful. if (!flutter_controller_->engine() || !flutter_controller_->view()) { return false; } RegisterPlugins(flutter_controller_->engine()); run_loop_->RegisterFlutterInstance(flutter_controller_->engine()); SetChildContent(flutter_controller_->view()->GetNativeWindow()); return true; } void FlutterWindow::OnDestroy() { if (flutter_controller_) { run_loop_->UnregisterFlutterInstance(flutter_controller_->engine()); flutter_controller_ = nullptr; } Win32Window::OnDestroy(); } LRESULT FlutterWindow::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { // Give Flutter, including plugins, an opporutunity to handle window messages. if (flutter_controller_) { std::optional result = flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, lparam); if (result) { return *result; } } switch (message) { case WM_FONTCHANGE: flutter_controller_->engine()->ReloadSystemFonts(); break; } return Win32Window::MessageHandler(hwnd, message, wparam, lparam); } ================================================ FILE: windows/runner/flutter_window.h ================================================ #ifndef RUNNER_FLUTTER_WINDOW_H_ #define RUNNER_FLUTTER_WINDOW_H_ #include #include #include #include "run_loop.h" #include "win32_window.h" // A window that does nothing but host a Flutter view. class FlutterWindow : public Win32Window { public: // Creates a new FlutterWindow driven by the |run_loop|, hosting a // Flutter view running |project|. explicit FlutterWindow(RunLoop* run_loop, const flutter::DartProject& project); virtual ~FlutterWindow(); protected: // Win32Window: bool OnCreate() override; void OnDestroy() override; LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept override; private: // The run loop driving events for this window. RunLoop* run_loop_; // The project to run. flutter::DartProject project_; // The Flutter instance hosted by this window. std::unique_ptr flutter_controller_; }; #endif // RUNNER_FLUTTER_WINDOW_H_ ================================================ FILE: windows/runner/main.cpp ================================================ #include #include #include #include "flutter_window.h" #include "run_loop.h" #include "utils.h" int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { // https://leanflutter.org/zh/blog/making-the-app-single-instanced HWND hwnd = ::FindWindow(L"FLUTTER_RUNNER_WIN32_WINDOW", L"flutter_deer"); if (hwnd != NULL) { ::ShowWindow(hwnd, SW_NORMAL); ::SetForegroundWindow(hwnd); return EXIT_FAILURE; } // Attach to console when present (e.g., 'flutter run') or create a // new console when running with a debugger. if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { CreateAndAttachConsole(); } // Initialize COM, so that it is available for use in the library and/or // plugins. ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); RunLoop run_loop; flutter::DartProject project(L"data"); std::vector command_line_arguments = GetCommandLineArguments(); project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); FlutterWindow window(&run_loop, project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); if (!window.CreateAndShow(L"flutter_deer", origin, size)) { return EXIT_FAILURE; } window.SetQuitOnClose(true); run_loop.Run(); ::CoUninitialize(); return EXIT_SUCCESS; } ================================================ FILE: windows/runner/resource.h ================================================ //{{NO_DEPENDENCIES}} // Microsoft Visual C++ generated include file. // Used by Runner.rc // #define IDI_APP_ICON 101 // Next default values for new objects // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 102 #define _APS_NEXT_COMMAND_VALUE 40001 #define _APS_NEXT_CONTROL_VALUE 1001 #define _APS_NEXT_SYMED_VALUE 101 #endif #endif ================================================ FILE: windows/runner/run_loop.cpp ================================================ #include "run_loop.h" #include #include RunLoop::RunLoop() {} RunLoop::~RunLoop() {} void RunLoop::Run() { bool keep_running = true; TimePoint next_flutter_event_time = TimePoint::clock::now(); while (keep_running) { std::chrono::nanoseconds wait_duration = std::max(std::chrono::nanoseconds(0), next_flutter_event_time - TimePoint::clock::now()); ::MsgWaitForMultipleObjects( 0, nullptr, FALSE, static_cast(wait_duration.count() / 1000), QS_ALLINPUT); bool processed_events = false; MSG message; // All pending Windows messages must be processed; MsgWaitForMultipleObjects // won't return again for items left in the queue after PeekMessage. while (::PeekMessage(&message, nullptr, 0, 0, PM_REMOVE)) { processed_events = true; if (message.message == WM_QUIT) { keep_running = false; break; } ::TranslateMessage(&message); ::DispatchMessage(&message); // Allow Flutter to process messages each time a Windows message is // processed, to prevent starvation. next_flutter_event_time = std::min(next_flutter_event_time, ProcessFlutterMessages()); } // If the PeekMessage loop didn't run, process Flutter messages. if (!processed_events) { next_flutter_event_time = std::min(next_flutter_event_time, ProcessFlutterMessages()); } } } void RunLoop::RegisterFlutterInstance( flutter::FlutterEngine* flutter_instance) { flutter_instances_.insert(flutter_instance); } void RunLoop::UnregisterFlutterInstance( flutter::FlutterEngine* flutter_instance) { flutter_instances_.erase(flutter_instance); } RunLoop::TimePoint RunLoop::ProcessFlutterMessages() { TimePoint next_event_time = TimePoint::max(); for (auto instance : flutter_instances_) { std::chrono::nanoseconds wait_duration = instance->ProcessMessages(); if (wait_duration != std::chrono::nanoseconds::max()) { next_event_time = std::min(next_event_time, TimePoint::clock::now() + wait_duration); } } return next_event_time; } ================================================ FILE: windows/runner/run_loop.h ================================================ #ifndef RUNNER_RUN_LOOP_H_ #define RUNNER_RUN_LOOP_H_ #include #include #include // A runloop that will service events for Flutter instances as well // as native messages. class RunLoop { public: RunLoop(); ~RunLoop(); // Prevent copying RunLoop(RunLoop const&) = delete; RunLoop& operator=(RunLoop const&) = delete; // Runs the run loop until the application quits. void Run(); // Registers the given Flutter instance for event servicing. void RegisterFlutterInstance( flutter::FlutterEngine* flutter_instance); // Unregisters the given Flutter instance from event servicing. void UnregisterFlutterInstance( flutter::FlutterEngine* flutter_instance); private: using TimePoint = std::chrono::steady_clock::time_point; // Processes all currently pending messages for registered Flutter instances. TimePoint ProcessFlutterMessages(); std::set flutter_instances_; }; #endif // RUNNER_RUN_LOOP_H_ ================================================ FILE: windows/runner/runner.exe.manifest ================================================ PerMonitorV2 ================================================ FILE: windows/runner/utils.cpp ================================================ #include "utils.h" #include #include #include #include #include void CreateAndAttachConsole() { if (::AllocConsole()) { FILE *unused; if (freopen_s(&unused, "CONOUT$", "w", stdout)) { _dup2(_fileno(stdout), 1); } if (freopen_s(&unused, "CONOUT$", "w", stderr)) { _dup2(_fileno(stdout), 2); } std::ios::sync_with_stdio(); FlutterDesktopResyncOutputStreams(); } } std::vector GetCommandLineArguments() { // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. int argc; wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); if (argv == nullptr) { return std::vector(); } std::vector command_line_arguments; // Skip the first argument as it's the binary name. for (int i = 1; i < argc; i++) { command_line_arguments.push_back(Utf8FromUtf16(argv[i])); } ::LocalFree(argv); return command_line_arguments; } std::string Utf8FromUtf16(const wchar_t* utf16_string) { if (utf16_string == nullptr) { return std::string(); } int target_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -1, nullptr, 0, nullptr, nullptr); if (target_length == 0) { return std::string(); } std::string utf8_string; utf8_string.resize(target_length); int converted_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -1, utf8_string.data(), target_length, nullptr, nullptr); if (converted_length == 0) { return std::string(); } return utf8_string; } ================================================ FILE: windows/runner/utils.h ================================================ #ifndef RUNNER_UTILS_H_ #define RUNNER_UTILS_H_ #include #include // Creates a console for the process, and redirects stdout and stderr to // it for both the runner and the Flutter library. void CreateAndAttachConsole(); // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string // encoded in UTF-8. Returns an empty std::string on failure. std::string Utf8FromUtf16(const wchar_t* utf16_string); // Gets the command line arguments passed in as a std::vector, // encoded in UTF-8. Returns an empty std::vector on failure. std::vector GetCommandLineArguments(); #endif // RUNNER_UTILS_H_ ================================================ FILE: windows/runner/win32_window.cpp ================================================ #include "win32_window.h" #include #include "resource.h" namespace { constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; // The number of Win32Window objects that currently exist. static int g_active_window_count = 0; using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); // Scale helper to convert logical scaler values to physical using passed in // scale factor int Scale(int source, double scale_factor) { return static_cast(source * scale_factor); } // Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. // This API is only needed for PerMonitor V1 awareness mode. void EnableFullDpiSupportIfAvailable(HWND hwnd) { HMODULE user32_module = LoadLibraryA("User32.dll"); if (!user32_module) { return; } auto enable_non_client_dpi_scaling = reinterpret_cast( GetProcAddress(user32_module, "EnableNonClientDpiScaling")); if (enable_non_client_dpi_scaling != nullptr) { enable_non_client_dpi_scaling(hwnd); FreeLibrary(user32_module); } } } // namespace // Manages the Win32Window's window class registration. class WindowClassRegistrar { public: ~WindowClassRegistrar() = default; // Returns the singleton registar instance. static WindowClassRegistrar* GetInstance() { if (!instance_) { instance_ = new WindowClassRegistrar(); } return instance_; } // Returns the name of the window class, registering the class if it hasn't // previously been registered. const wchar_t* GetWindowClass(); // Unregisters the window class. Should only be called if there are no // instances of the window. void UnregisterWindowClass(); private: WindowClassRegistrar() = default; static WindowClassRegistrar* instance_; bool class_registered_ = false; }; WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; const wchar_t* WindowClassRegistrar::GetWindowClass() { if (!class_registered_) { WNDCLASS window_class{}; window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); window_class.lpszClassName = kWindowClassName; window_class.style = CS_HREDRAW | CS_VREDRAW; window_class.cbClsExtra = 0; window_class.cbWndExtra = 0; window_class.hInstance = GetModuleHandle(nullptr); window_class.hIcon = LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); window_class.hbrBackground = 0; window_class.lpszMenuName = nullptr; window_class.lpfnWndProc = Win32Window::WndProc; RegisterClass(&window_class); class_registered_ = true; } return kWindowClassName; } void WindowClassRegistrar::UnregisterWindowClass() { UnregisterClass(kWindowClassName, nullptr); class_registered_ = false; } Win32Window::Win32Window() { ++g_active_window_count; } Win32Window::~Win32Window() { --g_active_window_count; Destroy(); } bool Win32Window::CreateAndShow(const std::wstring& title, const Point& origin, const Size& size) { Destroy(); const wchar_t* window_class = WindowClassRegistrar::GetInstance()->GetWindowClass(); const POINT target_point = {static_cast(origin.x), static_cast(origin.y)}; HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); double scale_factor = dpi / 96.0; HWND window = CreateWindow( window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), Scale(size.width, scale_factor), Scale(size.height, scale_factor), nullptr, nullptr, GetModuleHandle(nullptr), this); if (!window) { return false; } return OnCreate(); } // static LRESULT CALLBACK Win32Window::WndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { if (message == WM_NCCREATE) { auto window_struct = reinterpret_cast(lparam); SetWindowLongPtr(window, GWLP_USERDATA, reinterpret_cast(window_struct->lpCreateParams)); auto that = static_cast(window_struct->lpCreateParams); EnableFullDpiSupportIfAvailable(window); that->window_handle_ = window; } else if (Win32Window* that = GetThisFromHandle(window)) { return that->MessageHandler(window, message, wparam, lparam); } return DefWindowProc(window, message, wparam, lparam); } LRESULT Win32Window::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { switch (message) { case WM_DESTROY: window_handle_ = nullptr; Destroy(); if (quit_on_close_) { PostQuitMessage(0); } return 0; case WM_DPICHANGED: { auto newRectSize = reinterpret_cast(lparam); LONG newWidth = newRectSize->right - newRectSize->left; LONG newHeight = newRectSize->bottom - newRectSize->top; SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, newHeight, SWP_NOZORDER | SWP_NOACTIVATE); return 0; } case WM_SIZE: { RECT rect = GetClientArea(); if (child_content_ != nullptr) { // Size and position the child window. MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, TRUE); } return 0; } case WM_ACTIVATE: if (child_content_ != nullptr) { SetFocus(child_content_); } return 0; } return DefWindowProc(window_handle_, message, wparam, lparam); } void Win32Window::Destroy() { OnDestroy(); if (window_handle_) { DestroyWindow(window_handle_); window_handle_ = nullptr; } if (g_active_window_count == 0) { WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); } } Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { return reinterpret_cast( GetWindowLongPtr(window, GWLP_USERDATA)); } void Win32Window::SetChildContent(HWND content) { child_content_ = content; SetParent(content, window_handle_); RECT frame = GetClientArea(); MoveWindow(content, frame.left, frame.top, frame.right - frame.left, frame.bottom - frame.top, true); SetFocus(child_content_); } RECT Win32Window::GetClientArea() { RECT frame; GetClientRect(window_handle_, &frame); return frame; } HWND Win32Window::GetHandle() { return window_handle_; } void Win32Window::SetQuitOnClose(bool quit_on_close) { quit_on_close_ = quit_on_close; } bool Win32Window::OnCreate() { // No-op; provided for subclasses. return true; } void Win32Window::OnDestroy() { // No-op; provided for subclasses. } ================================================ FILE: windows/runner/win32_window.h ================================================ #ifndef RUNNER_WIN32_WINDOW_H_ #define RUNNER_WIN32_WINDOW_H_ #include #include #include #include // A class abstraction for a high DPI-aware Win32 Window. Intended to be // inherited from by classes that wish to specialize with custom // rendering and input handling class Win32Window { public: struct Point { unsigned int x; unsigned int y; Point(unsigned int x, unsigned int y) : x(x), y(y) {} }; struct Size { unsigned int width; unsigned int height; Size(unsigned int width, unsigned int height) : width(width), height(height) {} }; Win32Window(); virtual ~Win32Window(); // Creates and shows a win32 window with |title| and position and size using // |origin| and |size|. New windows are created on the default monitor. Window // sizes are specified to the OS in physical pixels, hence to ensure a // consistent size to will treat the width height passed in to this function // as logical pixels and scale to appropriate for the default monitor. Returns // true if the window was created successfully. bool CreateAndShow(const std::wstring& title, const Point& origin, const Size& size); // Release OS resources associated with window. void Destroy(); // Inserts |content| into the window tree. void SetChildContent(HWND content); // Returns the backing Window handle to enable clients to set icon and other // window properties. Returns nullptr if the window has been destroyed. HWND GetHandle(); // If true, closing this window will quit the application. void SetQuitOnClose(bool quit_on_close); // Return a RECT representing the bounds of the current client area. RECT GetClientArea(); protected: // Processes and route salient window messages for mouse handling, // size change and DPI. Delegates handling of these to member overloads that // inheriting classes can handle. virtual LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept; // Called when CreateAndShow is called, allowing subclass window-related // setup. Subclasses should return false if setup fails. virtual bool OnCreate(); // Called when Destroy is called. virtual void OnDestroy(); private: friend class WindowClassRegistrar; // OS callback called by message pump. Handles the WM_NCCREATE message which // is passed when the non-client area is being created and enables automatic // non-client DPI scaling so that the non-client area automatically // responsponds to changes in DPI. All other messages are handled by // MessageHandler. static LRESULT CALLBACK WndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept; // Retrieves a class instance pointer for |window| static Win32Window* GetThisFromHandle(HWND const window) noexcept; bool quit_on_close_ = false; // window handle for top level window. HWND window_handle_ = nullptr; // window handle for hosted content. HWND child_content_ = nullptr; }; #endif // RUNNER_WIN32_WINDOW_H_