Repository: mylxsw/aidea Branch: main Commit: cea454f7b48a Files: 400 Total size: 2.4 MB Directory structure: gitextract_69aiun7l/ ├── .github/ │ └── workflows/ │ └── build_windows_app.yml ├── .gitignore ├── .metadata ├── AppRun ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── README.zh-CN.md ├── analysis_options.yaml ├── android/ │ ├── .gitignore │ ├── app/ │ │ ├── build.gradle │ │ ├── proguard-rules.pro │ │ └── src/ │ │ ├── debug/ │ │ │ └── AndroidManifest.xml │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin/ │ │ │ │ └── cc/ │ │ │ │ └── aicode/ │ │ │ │ └── flutter/ │ │ │ │ └── askaide/ │ │ │ │ └── askaide/ │ │ │ │ └── MainActivity.kt │ │ │ └── res/ │ │ │ ├── drawable/ │ │ │ │ └── launch_background.xml │ │ │ ├── drawable-v21/ │ │ │ │ └── launch_background.xml │ │ │ ├── values/ │ │ │ │ └── styles.xml │ │ │ ├── values-night/ │ │ │ │ └── styles.xml │ │ │ ├── values-night-v31/ │ │ │ │ └── styles.xml │ │ │ └── values-v31/ │ │ │ └── styles.xml │ │ └── profile/ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle/ │ │ └── wrapper/ │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ └── settings.gradle ├── askaide.desktop ├── assets/ │ └── lottie/ │ └── empty_status.json ├── build-win-msix.bat ├── build-win.bat ├── devtools_options.yaml ├── docker-build.sh ├── flutter_launcher_icons.yaml ├── install.icns ├── install.iss ├── ios/ │ ├── .gitignore │ ├── Flutter/ │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Podfile │ ├── Runner/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ ├── LaunchBackground.imageset/ │ │ │ │ └── Contents.json │ │ │ └── LaunchImage.imageset/ │ │ │ ├── Contents.json │ │ │ └── README.md │ │ ├── Base.lproj/ │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ ├── Runner-Bridging-Header.h │ │ └── Runner.entitlements │ ├── 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 ├── lib/ │ ├── bloc/ │ │ ├── account_bloc.dart │ │ ├── account_event.dart │ │ ├── account_state.dart │ │ ├── admin_payment_bloc.dart │ │ ├── admin_payment_event.dart │ │ ├── admin_payment_state.dart │ │ ├── admin_room_bloc.dart │ │ ├── admin_room_event.dart │ │ ├── admin_room_state.dart │ │ ├── background_image_bloc.dart │ │ ├── background_image_event.dart │ │ ├── background_image_state.dart │ │ ├── bloc_manager.dart │ │ ├── channel_bloc.dart │ │ ├── channel_event.dart │ │ ├── channel_state.dart │ │ ├── chat_chat_bloc.dart │ │ ├── chat_chat_event.dart │ │ ├── chat_chat_state.dart │ │ ├── chat_event.dart │ │ ├── chat_message_bloc.dart │ │ ├── chat_state.dart │ │ ├── creative_island_bloc.dart │ │ ├── creative_island_event.dart │ │ ├── creative_island_state.dart │ │ ├── free_count_bloc.dart │ │ ├── free_count_event.dart │ │ ├── free_count_state.dart │ │ ├── gallery_bloc.dart │ │ ├── gallery_event.dart │ │ ├── gallery_state.dart │ │ ├── group_chat_bloc.dart │ │ ├── group_chat_event.dart │ │ ├── group_chat_state.dart │ │ ├── model_bloc.dart │ │ ├── model_event.dart │ │ ├── model_state.dart │ │ ├── notify_bloc.dart │ │ ├── notify_event.dart │ │ ├── notify_state.dart │ │ ├── payment_bloc.dart │ │ ├── payment_event.dart │ │ ├── payment_state.dart │ │ ├── room_bloc.dart │ │ ├── room_event.dart │ │ ├── room_state.dart │ │ ├── user_api_keys_bloc.dart │ │ ├── user_api_keys_event.dart │ │ ├── user_api_keys_state.dart │ │ ├── user_bloc.dart │ │ ├── user_event.dart │ │ ├── user_state.dart │ │ ├── version_bloc.dart │ │ ├── version_event.dart │ │ └── version_state.dart │ ├── data/ │ │ └── migrate.dart │ ├── helper/ │ │ ├── ability.dart │ │ ├── cache.dart │ │ ├── chat_token.dart │ │ ├── color.dart │ │ ├── constant.dart │ │ ├── env.dart │ │ ├── error.dart │ │ ├── event.dart │ │ ├── global_store.dart │ │ ├── haptic_feedback.dart │ │ ├── helper.dart │ │ ├── http.dart │ │ ├── image.dart │ │ ├── logger.dart │ │ ├── lru.dart │ │ ├── model.dart │ │ ├── model_resolver.dart │ │ ├── path.dart │ │ ├── platform.dart │ │ ├── queue.dart │ │ ├── tips.dart │ │ └── upload.dart │ ├── lang/ │ │ └── lang.dart │ ├── main.dart │ ├── page/ │ │ ├── admin/ │ │ │ ├── channels.dart │ │ │ ├── channels_add.dart │ │ │ ├── channels_edit.dart │ │ │ ├── dashboard.dart │ │ │ ├── messages.dart │ │ │ ├── models.dart │ │ │ ├── models_add.dart │ │ │ ├── models_edit.dart │ │ │ ├── payments.dart │ │ │ ├── recently_messages.dart │ │ │ ├── rooms.dart │ │ │ ├── user.dart │ │ │ └── users.dart │ │ ├── app_scaffold.dart │ │ ├── auth/ │ │ │ ├── signin_or_signup.dart │ │ │ ├── signin_screen.dart │ │ │ └── signup_screen.dart │ │ ├── balance/ │ │ │ ├── free_statistics.dart │ │ │ ├── payment.dart │ │ │ ├── payment_history.dart │ │ │ ├── price_block.dart │ │ │ ├── quota_usage_details.dart │ │ │ ├── quota_usage_statistics.dart │ │ │ ├── web/ │ │ │ │ ├── payment_element.dart │ │ │ │ └── payment_element_web.dart │ │ │ ├── web_payment_proxy.dart │ │ │ └── web_payment_result.dart │ │ ├── chat/ │ │ │ ├── character_chat.dart │ │ │ ├── character_create.dart │ │ │ ├── character_edit.dart │ │ │ ├── characters.dart │ │ │ ├── component/ │ │ │ │ ├── character_box.dart │ │ │ │ ├── group_avatar.dart │ │ │ │ ├── group_empty.dart │ │ │ │ ├── model_switcher.dart │ │ │ │ └── stop_button.dart │ │ │ ├── group/ │ │ │ │ ├── chat.dart │ │ │ │ ├── create.dart │ │ │ │ └── edit.dart │ │ │ ├── home.dart │ │ │ ├── home_chat.dart │ │ │ └── home_chat_history.dart │ │ ├── component/ │ │ │ ├── account_quota_card.dart │ │ │ ├── advanced_button.dart │ │ │ ├── animated_cursor.dart │ │ │ ├── attached_button_panel.dart │ │ │ ├── audio_player.dart │ │ │ ├── avatar_selector.dart │ │ │ ├── background_container.dart │ │ │ ├── bottom_sheet_box.dart │ │ │ ├── button.dart │ │ │ ├── chat/ │ │ │ │ ├── chat_bubble.dart │ │ │ │ ├── chat_input.dart │ │ │ │ ├── chat_input_button.dart │ │ │ │ ├── chat_preview.dart │ │ │ │ ├── chat_share.dart │ │ │ │ ├── empty.dart │ │ │ │ ├── enhanced_selectable_text.dart │ │ │ │ ├── file_upload.dart │ │ │ │ ├── help_tips.dart │ │ │ │ ├── markdown/ │ │ │ │ │ ├── citation.dart │ │ │ │ │ ├── code.dart │ │ │ │ │ ├── latex/ │ │ │ │ │ │ ├── latex_block_syntax.dart │ │ │ │ │ │ ├── latex_element_builder.dart │ │ │ │ │ │ └── latex_inline_syntax.dart │ │ │ │ │ └── latex.dart │ │ │ │ ├── markdown.dart │ │ │ │ ├── message_state_manager.dart │ │ │ │ ├── role_avatar.dart │ │ │ │ ├── search_result.dart │ │ │ │ ├── thinking_card.dart │ │ │ │ └── voice_record.dart │ │ │ ├── chat_tools_button.dart │ │ │ ├── column_block.dart │ │ │ ├── credit.dart │ │ │ ├── dialog.dart │ │ │ ├── effect/ │ │ │ │ └── glass.dart │ │ │ ├── enhanced_button.dart │ │ │ ├── enhanced_error.dart │ │ │ ├── enhanced_input.dart │ │ │ ├── enhanced_popup_menu.dart │ │ │ ├── enhanced_textfield.dart │ │ │ ├── file_preview.dart │ │ │ ├── gallery_item_share.dart │ │ │ ├── global_alert.dart │ │ │ ├── gradient_style.dart │ │ │ ├── group_list_widget.dart │ │ │ ├── icon_box.dart │ │ │ ├── icon_box_button.dart │ │ │ ├── image.dart │ │ │ ├── image_action.dart │ │ │ ├── image_preview.dart │ │ │ ├── invite_card.dart │ │ │ ├── item_selector.dart │ │ │ ├── item_selector_search.dart │ │ │ ├── loading.dart │ │ │ ├── message_box.dart │ │ │ ├── model_indicator.dart │ │ │ ├── model_item.dart │ │ │ ├── multi_item_selector.dart │ │ │ ├── notify_message.dart │ │ │ ├── pagination.dart │ │ │ ├── password_field.dart │ │ │ ├── prompt_tags_selector.dart │ │ │ ├── random_avatar.dart │ │ │ ├── room_card.dart │ │ │ ├── rotating_widget.dart │ │ │ ├── select_mode_toolbar.dart │ │ │ ├── share.dart │ │ │ ├── sliver_component.dart │ │ │ ├── social_icon.dart │ │ │ ├── take_photo.dart │ │ │ ├── theme/ │ │ │ │ ├── custom_size.dart │ │ │ │ ├── custom_theme.dart │ │ │ │ └── theme.dart │ │ │ ├── transition_resolver.dart │ │ │ ├── verify_code_input.dart │ │ │ ├── video_player.dart │ │ │ ├── weak_text_button.dart │ │ │ └── windows.dart │ │ ├── creative_island/ │ │ │ ├── draw/ │ │ │ │ ├── artistic_qr.dart │ │ │ │ ├── artistic_wordart.dart │ │ │ │ ├── components/ │ │ │ │ │ ├── artistic_style_selector.dart │ │ │ │ │ ├── box.dart │ │ │ │ │ ├── content_preview.dart │ │ │ │ │ ├── creative_item.dart │ │ │ │ │ ├── image_selector.dart │ │ │ │ │ ├── image_size.dart │ │ │ │ │ └── image_style_selector.dart │ │ │ │ ├── data/ │ │ │ │ │ └── draw_history_datasource.dart │ │ │ │ ├── draw_create.dart │ │ │ │ ├── draw_list.dart │ │ │ │ ├── draw_result.dart │ │ │ │ └── image_edit_direct.dart │ │ │ ├── gallery/ │ │ │ │ ├── components/ │ │ │ │ │ └── image_card.dart │ │ │ │ ├── data/ │ │ │ │ │ └── gallery_datasource.dart │ │ │ │ ├── gallery.dart │ │ │ │ └── gallery_item.dart │ │ │ ├── my_creation.dart │ │ │ └── my_creation_item.dart │ │ ├── custom_scaffold.dart │ │ ├── data/ │ │ │ ├── chat_history_datasource.dart │ │ │ ├── group_message_datasource.dart │ │ │ └── notification_datasource.dart │ │ ├── drawer.dart │ │ ├── home.dart │ │ ├── lab/ │ │ │ ├── avatar_selector.dart │ │ │ ├── creative_models.dart │ │ │ ├── draw_board.dart │ │ │ ├── prompt.dart │ │ │ └── user_center.dart │ │ └── setting/ │ │ ├── account_security.dart │ │ ├── article.dart │ │ ├── background_selector.dart │ │ ├── bind_phone_page.dart │ │ ├── change_password.dart │ │ ├── custom_home_models.dart │ │ ├── destroy_account.dart │ │ ├── diagnosis.dart │ │ ├── notification.dart │ │ ├── openai_setting.dart │ │ ├── retrieve_password_screen.dart │ │ ├── setting_screen.dart │ │ └── user_api_keys.dart │ └── repo/ │ ├── api/ │ │ ├── admin/ │ │ │ ├── channels.dart │ │ │ ├── models.dart │ │ │ ├── payment.dart │ │ │ └── users.dart │ │ ├── article.dart │ │ ├── creative.dart │ │ ├── image_model.dart │ │ ├── info.dart │ │ ├── keys.dart │ │ ├── model.dart │ │ ├── notification.dart │ │ ├── page.dart │ │ ├── payment.dart │ │ ├── quota.dart │ │ ├── room_gallery.dart │ │ └── user.dart │ ├── api_server.dart │ ├── cache_repo.dart │ ├── chat_message_repo.dart │ ├── creative_island_repo.dart │ ├── data/ │ │ ├── cache_data.dart │ │ ├── chat_history.dart │ │ ├── chat_message_data.dart │ │ ├── creative_island_data.dart │ │ ├── room_data.dart │ │ └── settings_data.dart │ ├── deepai_repo.dart │ ├── model/ │ │ ├── chat_history.dart │ │ ├── chat_message.dart │ │ ├── creative_island_history.dart │ │ ├── group.dart │ │ ├── message.dart │ │ ├── misc.dart │ │ ├── model.dart │ │ └── room.dart │ ├── openai_repo.dart │ ├── settings_repo.dart │ └── stabilityai_repo.dart ├── linux/ │ ├── .gitignore │ ├── CMakeLists.txt │ ├── flutter/ │ │ ├── CMakeLists.txt │ │ ├── generated_plugin_registrant.cc │ │ ├── generated_plugin_registrant.h │ │ └── generated_plugins.cmake │ ├── main.cc │ ├── my_application.cc │ └── my_application.h ├── 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 ├── nginx.conf ├── pubspec.yaml ├── scripts/ │ ├── go.mod │ ├── go.sum │ ├── macos-icon-replace.sh │ └── main.go ├── web/ │ ├── index.html │ ├── manifest.json │ ├── splash/ │ │ ├── splash.js │ │ └── style.css │ ├── sqflite_sw.js │ └── sqlite3.wasm └── 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 ├── runner.exe.manifest ├── utils.cpp ├── utils.h ├── win32_window.cpp └── win32_window.h ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/build_windows_app.yml ================================================ name: Build Windows app on: push: branches: - main - develop - main-pc jobs: build: runs-on: windows-latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Set up Flutter uses: subosito/flutter-action@v2.10.0 with: flutter-version: "3.19.2" # Set flutter version here - name: Build Windows app #run: flutter build windows --release run: dart run msix:create --release -v --output-path build/windows/runner --output-name AIdea --install-certificate false # - name: Copy dependencies # run: copy .\windows\sqlite3.dll .\build\windows\runner\Release\ - name: Upload artifact uses: actions/upload-artifact@v2 with: name: aidea path: build/windows/runner/AIdea.msix ================================================ FILE: .gitignore ================================================ # Miscellaneous *.class *.log *.pyc *.swp .DS_Store .atom/ .buildlog/ .history .svn/ migrate_working_dir/ # IntelliJ related *.iml *.ipr *.iws .idea/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. #.vscode/ # Flutter/Dart/Pub related **/doc/api/ **/ios/Flutter/.last_build_id .dart_tool/ .flutter-plugins .flutter-plugins-dependencies .packages .pub-cache/ .pub/ /build/ # Symbolication related app.*.symbols # Obfuscation related app.*.map.json # Android Studio will place build artifacts here /android/app/debug /android/app/profile /android/app/release Makefile.local ================================================ 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. version: revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 channel: stable project_type: app # Tracks metadata for the flutter migrate command migration: platforms: - platform: root create_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 base_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 - platform: android create_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 base_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 - platform: ios create_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 base_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 - platform: linux create_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 base_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 - platform: macos create_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 base_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 - platform: web create_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 base_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 - platform: windows create_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 base_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 # 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: AppRun ================================================ #!/bin/sh cd "$(dirname "$0")" exec ./askaide ================================================ FILE: Dockerfile ================================================ FROM nginx:1.25 COPY build/web/ /data/webroot COPY nginx.conf /etc/nginx/conf.d/default.conf ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2023 管宜尧 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ ipa: flutter build ipa --no-tree-shake-icons --release open build/ios/ipa run: flutter run --release build-all: build-android ipa build-dmg rm -fr build/release mkdir -p build/release mv build/app/outputs/flutter-apk/app-release.apk build/release/aidea-android.apk mv build/ios/ipa/askaide.ipa build/release/aidea-ios.ipa mv build/macos/Build/Products/Package/AIdea-Installer.dmg build/release/aidea-macos.dmg open build/release build-android: flutter build apk --release --no-tree-shake-icons build-and-sync-android: build-android mv build/app/outputs/flutter-apk/app-release.apk /Users/mylxsw/ResilioSync/ResilioSync/临时文件/aidea-release.apk build-macos: flutter build macos --no-tree-shake-icons --release codesign -f -s "Developer ID Application: YIYAO GUAN (N95437SZ2A)" build/macos/Build/Products/Release/AIdea.app build-appimage: flutter build linux --no-tree-shake-icons --release mkdir -p aidea_app.AppDir cp -r build/linux/x64/release/bundle/* aidea_app.AppDir cp assets/app.png aidea_app.AppDir/ cp AppRun aidea_app.AppDir/ cp askaide.desktop aidea_app.AppDir/ appimagetool aidea_app.AppDir/ build-dmg: build-macos rm -fr build/macos/Build/Products/Package mkdir -p build/macos/Build/Products/Package && cp -r build/macos/Build/Products/Release/AIdea.app build/macos/Build/Products/Package create-dmg --volname "AIdea Installer" \ --volicon "install.icns" \ --background "background.jpg" \ --window-pos 200 120 \ --window-size 600 320 \ --icon-size 100 \ --icon "AIdea.app" 170 130 \ --hide-extension "AIdea.app" \ --app-drop-link 430 130 \ --sandbox-safe \ --no-internet-enable \ "build/macos/Build/Products/Package/AIdea-Installer.dmg" \ "build/macos/Build/Products/Package" open build/macos/Build/Products/Package/ build-web: #flutter build web --web-renderer canvaskit --release --dart-define=FLUTTER_WEB_CANVASKIT_URL=https://resources.aicode.cc/canvaskit/ flutter build web --web-renderer canvaskit --release cd scripts && go run main.go ../build/web/main.dart.js && cd .. build-web-samehost: flutter build web --web-renderer canvaskit --release --dart-define=API_SERVER_URL=/ cd scripts && go run main.go ../build/web/main.dart.js && cd .. deploy-web: build-web cd build && tar -zcvf web.tar.gz web scp build/web.tar.gz huawei-1:/data/webroot ssh huawei-1 "cd /data/webroot && tar -zxvf web.tar.gz && rm -rf web.tar.gz app && mv web app" rm -fr build/web.tar.gz .PHONY: run build-android build-macos ipa build-web-samehost build-web deploy-web build-dmg build-all build-and-sync-android ================================================ FILE: README.md ================================================ # AIdea - AI Chat, Collaboration & Image Generation English | [中文](./README.zh-CN.md) [![FOSSA Status](https://app.fossa.com/api/projects/custom%2B39727%2Fgithub.com%2Fmylxsw%2Faidea.svg?type=shield&issueType=license)](https://app.fossa.com/projects/custom%2B39727%2Fgithub.com%2Fmylxsw%2Faidea?ref=badge_shield) ![GitHub release (by tag)](https://img.shields.io/github/downloads/mylxsw/aidea/1.0.4/total) ![GitHub](https://img.shields.io/github/license/mylxsw/aidea) mylxsw%2Faidea | Trendshift An APP that integrates mainstream large language models and image generation models, built with Flutter, with fully open-source code. Download & Try: https://aidea.aicode.cc Open Source Repositories: - Client: https://github.com/mylxsw/aidea - Server: https://github.com/mylxsw/aidea-server - Docker Deployment: https://github.com/mylxsw/aidea-docker ## Development & Build Environment The default branch `main` is the v2 version, currently under active development. If you need to self-host, please switch to the [v1.x](https://github.com/mylxsw/aidea/tree/v1.x) branch. ```bash git checkout v1.x ``` To set up a development environment for compiling and packaging the APP, you can refer to the following articles (more articles will be added over time): - [AIdea Development Environment Tutorial (1): Flutter Frontend Setup](https://mp.weixin.qq.com/s/bgAIH6s7t5IREusK_WtpRg) - [AIdea Development Environment Tutorial (2): Golang Server Setup](https://mp.weixin.qq.com/s?__biz=MzA3NTU1NDk4Mg==&mid=2454663711&idx=1&sn=c2c66abc20f8e0900afe645ff1f552ac&chksm=88d55bd6bfa2d2c063ea15a4e8864c197009b49233c710b85725f1aa946836e15a26439c69a7&scene=178&cur_album_id=3204997940193296389#rd) - [AIdea Development Environment Tutorial (3): Windows Build Environment Setup](https://mp.weixin.qq.com/s?__biz=MzA3NTU1NDk4Mg==&mid=2454663731&idx=1&sn=2aa4841daeb8dc4132e8abe63f585996&chksm=88d55bfabfa2d2ecce8224dcf23da6f911d3d8324121d141fd5c0324197c6f4845dd63639ac2&scene=178&cur_album_id=3204997940193296389#rd) - [Flutter App Windows Installer Creation Tutorial](https://mp.weixin.qq.com/s?__biz=MzA3NTU1NDk4Mg==&mid=2454663689&idx=1&sn=73c93edd9ddacb2d4c36061cc79be059&chksm=88d55bc0bfa2d2d6ecaa7979835431467105d9572953f1e96c0f735df3fe60d4f6d6137f041d&scene=178&cur_album_id=3204997940193296389#rd) > Some users encounter build failures, which can be very frustrating. This is not a bug -- it is a known characteristic of Flutter. Build failures are common as the Flutter version changes. > To be safe, you can refer to my local environment configuration: > > ![Local Environment Configuration](./build-environment.png) ## Self-Hosting / Private Deployment If you do not want to use the managed cloud service, you can deploy the server yourself. [See deployment instructions here](https://github.com/mylxsw/aidea-server/blob/main/docs/deploy.md). If you prefer not to set it up yourself, you can contact me for assisted deployment. See [VIP Deployment Service](https://github.com/mylxsw/aidea-server/blob/main/docs/deploy-vip.md) for details. ## Community & Support - WeChat Tech Group: Add WeChat ID `x-prometheus` as a friend for an invitation to the group. - WeChat Official Account: ## Screenshots Mobile ![Mobile](./images/v2-mobile-preview.png) MacOS ![MacOS](./images/v2-macos-preview.png) Windows ![Windows](./images/v2-windows-preview.png) ## License MIT Copyright (c) 2025, mylxsw ================================================ FILE: README.zh-CN.md ================================================ # AIdea - AI 聊天、协作、图像生成 [English](./README.md) | 中文 [![FOSSA Status](https://app.fossa.com/api/projects/custom%2B39727%2Fgithub.com%2Fmylxsw%2Faidea.svg?type=shield&issueType=license)](https://app.fossa.com/projects/custom%2B39727%2Fgithub.com%2Fmylxsw%2Faidea?ref=badge_shield) ![GitHub release (by tag)](https://img.shields.io/github/downloads/mylxsw/aidea/1.0.4/total) ![GitHub](https://img.shields.io/github/license/mylxsw/aidea) mylxsw%2Faidea | Trendshift 一款集成了主流大语言模型以及绘图模型的 APP, 采用 Flutter 开发,代码完全开源。 下载体验地址: https://aidea.aicode.cc 开源代码: - 客户端:https://github.com/mylxsw/aidea - 服务端:https://github.com/mylxsw/aidea-server - Docker 部署:https://github.com/mylxsw/aidea-docker ## 开发、编译运行环境 默认分支 `main` 是 v2 版本,当前正在开发中,如需自己部署,请切换到 [v1.x](https://github.com/mylxsw/aidea/tree/v1.x) 分支。 ```bash git checkout v1.x ``` 搭建开发环境,用来编译和打包 APP,可以参考下面的文章,更多文章后面有时间了会持续更新。 - [AIdea 项目开发环境部署教程(一)前端 Flutter 环境搭建](https://mp.weixin.qq.com/s/bgAIH6s7t5IREusK_WtpRg) - [AIdea 项目开发环境部署教程(二)服务端 Golang 环境搭建](https://mp.weixin.qq.com/s?__biz=MzA3NTU1NDk4Mg==&mid=2454663711&idx=1&sn=c2c66abc20f8e0900afe645ff1f552ac&chksm=88d55bd6bfa2d2c063ea15a4e8864c197009b49233c710b85725f1aa946836e15a26439c69a7&scene=178&cur_album_id=3204997940193296389#rd) - [AIdea 项目开发环境部署教程(三)Windows 编译环境搭建](https://mp.weixin.qq.com/s?__biz=MzA3NTU1NDk4Mg==&mid=2454663731&idx=1&sn=2aa4841daeb8dc4132e8abe63f585996&chksm=88d55bfabfa2d2ecce8224dcf23da6f911d3d8324121d141fd5c0324197c6f4845dd63639ac2&scene=178&cur_album_id=3204997940193296389#rd) - [Flutter 应用 Windows 安装包创建教程](https://mp.weixin.qq.com/s?__biz=MzA3NTU1NDk4Mg==&mid=2454663689&idx=1&sn=73c93edd9ddacb2d4c36061cc79be059&chksm=88d55bc0bfa2d2d6ecaa7979835431467105d9572953f1e96c0f735df3fe60d4f6d6137f041d&scene=178&cur_album_id=3204997940193296389#rd) > 有些小伙伴在编译的时候总是失败,非常令人抓狂,这并不是什么问题,而是 Flutter 特有的特性,随着 Flutter 版本的变化,编译失败是常态。 > 保险起见,你可以参考我的本地环境配置: > > ![本地环境配置](./build-environment.png) ## 私有化部署 如果你不想使用托管的云服务,可以自己部署服务端,[部署请看这里](https://github.com/mylxsw/aidea-server/blob/main/docs/deploy.md)。 不想自己折腾,可以找我来帮你部署,详情参考 [服务器代部署说明](https://github.com/mylxsw/aidea-server/blob/main/docs/deploy-vip.md)。 ## 技术交流 - 微信技术交流群:请添加微信号 `x-prometheus` 为好友,拉你进群。 - 微信公众号 ## 产品截图 移动端 ![移动端](./images/v2-mobile-preview.png) MacOS 端 ![MacOS 端](./images/v2-macos-preview.png) Windows 端 ![Windows 端](./images/v2-windows-preview.png) ## License MIT Copyright (c) 2025, mylxsw ================================================ FILE: analysis_options.yaml ================================================ # This file configures the analyzer, which statically analyzes Dart code to # check for errors, warnings, and lints. # # The issues identified by the analyzer are surfaced in the UI of Dart-enabled # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be # invoked from the command line by running `flutter analyze`. # The following line activates a set of recommended lints for Flutter apps, # packages, and plugins designed to encourage good coding practices. include: package:flutter_lints/flutter.yaml linter: # The lint rules applied to this project can be customized in the # section below to disable rules from the `package:flutter_lints/flutter.yaml` # included above or to enable additional rules. A list of all available lints # and their documentation is published at # https://dart-lang.github.io/linter/lints/index.html. # # Instead of disabling a lint rule for the entire project in the # section below, it can also be suppressed for a single line of code # or a specific dart file by using the `// ignore: name_of_lint` and # `// ignore_for_file: name_of_lint` syntax on the line or in the file # producing the lint. rules: # avoid_print: false # Uncomment to disable the `avoid_print` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options ================================================ FILE: android/.gitignore ================================================ gradle-wrapper.jar /.gradle /captures/ /gradlew /gradlew.bat /local.properties GeneratedPluginRegistrant.java # Remember to never publicly share your keystore. # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app key.properties **/*.keystore **/*.jks ================================================ FILE: android/app/build.gradle ================================================ plugins { id "com.android.application" id "kotlin-android" // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. 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 keystoreProperties = new Properties() def keystorePropertiesFile = rootProject.file('key.properties') if (keystorePropertiesFile.exists()) { keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) } android { namespace 'cc.aicode.flutter.askaide.askaide' compileSdkVersion flutter.compileSdkVersion // ndkVersion flutter.ndkVersion ndkVersion "27.0.12077973" compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8 } defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "cc.aicode.flutter.askaide.askaide" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. // minSdkVersion flutter.minSdkVersion minSdkVersion 23 targetSdkVersion flutter.targetSdkVersion versionCode flutter.versionCode versionName flutterVersionName } signingConfigs { release { keyAlias keystoreProperties['keyAlias'] keyPassword keystoreProperties['keyPassword'] storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null storePassword keystoreProperties['storePassword'] } } buildTypes { release { signingConfig signingConfigs.release } } } flutter { source '../..' } ================================================ FILE: android/app/proguard-rules.pro ================================================ -dontwarn com.stripe.android.pushProvisioning.PushProvisioningActivity$g -dontwarn com.stripe.android.pushProvisioning.PushProvisioningActivityStarter$Args -dontwarn com.stripe.android.pushProvisioning.PushProvisioningActivityStarter$Error -dontwarn com.stripe.android.pushProvisioning.PushProvisioningActivityStarter -dontwarn com.stripe.android.pushProvisioning.PushProvisioningEphemeralKeyProvider ================================================ FILE: android/app/src/debug/AndroidManifest.xml ================================================ ================================================ FILE: android/app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: android/app/src/main/kotlin/cc/aicode/flutter/askaide/askaide/MainActivity.kt ================================================ package cc.aicode.flutter.askaide.askaide import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterFragmentActivity class MainActivity: FlutterFragmentActivity() { } ================================================ FILE: android/app/src/main/res/drawable/launch_background.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable-v21/launch_background.xml ================================================ ================================================ FILE: android/app/src/main/res/values/styles.xml ================================================ ================================================ FILE: android/app/src/main/res/values-night/styles.xml ================================================ ================================================ FILE: android/app/src/main/res/values-night-v31/styles.xml ================================================ ================================================ FILE: android/app/src/main/res/values-v31/styles.xml ================================================ ================================================ FILE: android/app/src/profile/AndroidManifest.xml ================================================ ================================================ FILE: android/build.gradle ================================================ allprojects { repositories { google() mavenCentral() } subprojects { afterEvaluate { project -> if (project.hasProperty('android')) { project.android { if (namespace == null) { namespace project.group } } } if (project.hasProperty("android")) { project.android { compileSdkVersion = 34 } } } } } 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 ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: android/gradle.properties ================================================ org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true android.enableJetifier=true kotlin.jvm.target.validation.mode = IGNORE ================================================ 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.7.2' apply false id "org.jetbrains.kotlin.android" version "1.9.20" apply false } include ":app" ================================================ FILE: askaide.desktop ================================================ [Desktop Entry] Version=1.0 Name=Ask aide Comment=Ask aide! Icon=app Exec=askaide %U Terminal=false Type=Application Categories=Utility; Keywords=Internet; StartupNotify=false ================================================ FILE: assets/lottie/empty_status.json ================================================ {"v":"4.5.9","fr":25,"ip":0,"op":100,"w":248,"h":187,"ddd":0,"assets":[],"layers":[{"ddd":0,"ind":0,"ty":4,"nm":"\rtail","ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[21],"e":[-60]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":12,"s":[-60],"e":[21]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":25,"s":[21],"e":[-60]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":37,"s":[-60],"e":[21]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":50,"s":[21],"e":[-60]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":62,"s":[-60],"e":[21]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":74,"s":[21],"e":[-60]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":87,"s":[-60],"e":[21]},{"t":99}]},"p":{"a":0,"k":[80,44.429,0]},"a":{"a":0,"k":[8.473,9.453,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0],[-1.739,1.217],[0,0],[2.934,-2.055],[0.434,-0.631],[0.467,0.666],[0,0],[-2.054,-2.934],[-1.467,-0.418],[0,0]],"o":[[0,0],[1.521,0.11],[2.933,-2.055],[0,0],[-0.666,0.466],[-0.192,-0.741],[-2.054,-2.934],[0,0],[1.218,1.738],[0,0],[0,0]],"v":[[-0.161,5.232],[-0.161,5.232],[5.32,4.011],[8.702,-2.465],[1.462,-1.499],[-0.177,0.187],[-1.14,-1.958],[-7.614,-5.342],[-6.649,1.901],[-1.916,4.923],[-1.916,4.923]],"c":true}},"nm":"路径 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.769,0.769,0.851,1]},"o":{"a":0,"k":100},"r":1,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[8.952,5.591],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"组 1","np":2,"mn":"ADBE Vector Group"}],"ip":0,"op":750,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":1,"ty":4,"nm":"\reye","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[145.857,82.715,0]},"a":{"a":0,"k":[5.964,5.965,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":42,"s":[100,100,100],"e":[100,1,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":45,"s":[100,1,100],"e":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":48,"s":[100,100,100],"e":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":93,"s":[100,100,100],"e":[100,1,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":96,"s":[100,1,100],"e":[100,100,100]},{"t":99}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[-3.156,0],[0,-3.156],[3.156,0],[0,3.156]],"o":[[3.156,0],[0,3.156],[-3.156,0],[0,-3.156]],"v":[[0,-5.715],[5.714,-0.001],[0,5.715],[-5.714,-0.001]],"c":true}},"nm":"路径 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.671,0.671,0.761,1]},"o":{"a":0,"k":100},"r":1,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[5.964,5.965],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"组 1","np":2,"mn":"ADBE Vector Group"}],"ip":0,"op":750,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":2,"ty":4,"nm":"\reyes white","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[143.714,82.714,0]},"a":{"a":0,"k":[11.679,11.678,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[-6.312,0],[0,6.312],[6.312,0],[0,-6.311]],"o":[[6.312,0],[0,-6.311],[-6.312,0],[0,6.312]],"v":[[0,11.428],[11.429,-0.001],[0,-11.428],[-11.429,-0.001]],"c":true}},"nm":"路径 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"r":1,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[11.679,11.678],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"组 1","np":2,"mn":"ADBE Vector Group"}],"ip":0,"op":750,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":3,"ty":4,"nm":"\rbody","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[123.719,72.664,0]},"a":{"a":0,"k":[60.153,37.965,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[31.5,0],[10.189,-7.981],[1.68,1.422],[0,0],[0,0],[4.418,-6.529],[0,-12.88],[0,0],[-3.015,-6.029],[0,0],[-12.453,0],[-6.911,9.934]],"o":[[-13.946,0],[-1.536,1.204],[0,0],[0,0],[-0.153,2.463],[-6.908,9.775],[0,0],[10.063,-5.593],[0,0],[9.428,-2.285],[20.38,0],[-2.476,-30.863]],"v":[[0.097,-37.715],[-36.872,-24.975],[-42.158,-26.982],[-43.403,-27.495],[-44.57,-27.098],[-48.937,-12.301],[-59.903,22.286],[-59.499,29.285],[-33.617,30.857],[-1.617,30.857],[23.526,37.715],[59.903,17.423]],"c":true}},"nm":"路径 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.769,0.769,0.851,1]},"o":{"a":0,"k":100},"r":1,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[60.153,37.965],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"组 1","np":2,"mn":"ADBE Vector Group"}],"ip":0,"op":750,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":4,"ty":4,"nm":"\rfin","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[104.536,103.099,0]},"a":{"a":0,"k":[24.733,0.25,0]},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":0,"s":[100,100,100],"e":[100,70,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":6,"s":[100,70,100],"e":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":12,"s":[100,100,100],"e":[100,56,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":18,"s":[100,56,100],"e":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":24,"s":[100,100,100],"e":[100,70,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":30,"s":[100,70,100],"e":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":36,"s":[100,100,100],"e":[100,56,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":43,"s":[100,56,100],"e":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":49,"s":[100,100,100],"e":[100,70,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":55,"s":[100,70,100],"e":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":61,"s":[100,100,100],"e":[100,56,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":67,"s":[100,56,100],"e":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":74,"s":[100,100,100],"e":[100,70,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":80,"s":[100,70,100],"e":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":87,"s":[100,100,100],"e":[100,56,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":94,"s":[100,56,100],"e":[100,100,100]},{"t":100}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[15.429,0],[3.714,7.429]],"o":[[-9.429,2.286],[-12,0],[0,0]],"v":[[21,-8.572],[-9,8.572],[-11,-8.572]],"c":true}},"nm":"路径 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.769,0.769,0.851,1]},"o":{"a":0,"k":100},"r":1,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[21.25,8.822],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"组 1","np":2,"mn":"ADBE Vector Group"}],"ip":0,"op":750,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":5,"ty":4,"nm":"\rbelly5","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[88.286,85.293,0]},"a":{"a":0,"k":[24.536,34.528,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[18.68,0.296],[2.348,-3.47],[0,-12.88],[-2.993,-7.135],[-3.347,0],[0,18.935]],"o":[[-0.485,2.668],[-6.908,9.775],[0,8.221],[3.058,0.894],[18.935,0],[0,-18.751]],"v":[[-9.448,-34.279],[-13.32,-24.878],[-24.286,9.708],[-19.636,32.896],[-10,34.279],[24.286,-0.007]],"c":true}},"nm":"路径 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.973,0.973,0.988,1]},"o":{"a":0,"k":100},"r":1,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[24.536,34.528],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"组 1","np":2,"mn":"ADBE Vector Group"}],"ip":0,"op":750,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":6,"ty":4,"nm":"\rbelly4","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[98.286,85.659,0]},"a":{"a":0,"k":[34.536,44.162,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[20.602,3.796],[2.903,-2.274],[1.714,0.858],[6.177,-9.127],[0,-12.88],[-5.829,-9.25],[-5.292,0],[0,24.458]],"o":[[-3.298,1.715],[-2.142,1.678],[-1.714,-0.857],[-6.908,9.775],[0,11.742],[4.705,1.702],[24.458,0],[0,-21.701]],"v":[[-1.929,-43.912],[-11.254,-37.921],[-18.286,-41.231],[-23.32,-25.246],[-34.286,9.341],[-25.071,41.278],[-10,43.912],[34.286,-0.373]],"c":true}},"nm":"路径 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.945,0.945,0.973,1]},"o":{"a":0,"k":100},"r":1,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[34.536,44.162],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"组 1","np":2,"mn":"ADBE Vector Group"}],"ip":0,"op":750,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":7,"ty":4,"nm":"\rbelly3","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[108.286,87.383,0]},"a":{"a":0,"k":[44.535,52.439,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[19.58,8.198],[9.016,-7.063],[1.714,0.857],[6.178,-9.128],[0,-12.88],[-10.446,-10.797],[-6.088,0],[0,29.981]],"o":[[-12.096,0.951],[-2.141,1.677],[-1.715,-0.857],[-6.907,9.775],[0,16.21],[5.464,1.848],[29.982,0],[0,-22.556]],"v":[[10.948,-52.189],[-21.255,-39.644],[-28.286,-42.954],[-33.321,-26.969],[-44.286,7.617],[-27.4,49.323],[-10.001,52.189],[44.286,-2.097]],"c":true}},"nm":"路径 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.91,0.914,0.961,1]},"o":{"a":0,"k":100},"r":1,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[44.535,52.439],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"组 1","np":2,"mn":"ADBE Vector Group"}],"ip":0,"op":750,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":8,"ty":4,"nm":"\rbelly2","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[118.286,92.286,0]},"a":{"a":0,"k":[54.535,57.536,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,35.504],[7.996,10.717],[13.435,0],[10.189,-7.981],[1.714,0.857],[6.177,-9.128],[0,-12.879],[-19.548,-9.89],[-3.867,0]],"o":[[0,-14.399],[-10.001,-7.459],[-13.946,0],[-2.141,1.677],[-1.715,-0.857],[-6.907,9.775],[0,23.406],[3.679,0.655],[35.504,0]],"v":[[54.286,-7],[41.553,-45.409],[5.715,-57.286],[-31.255,-44.547],[-38.286,-47.857],[-43.321,-31.872],[-54.286,2.714],[-21.327,56.27],[-10.001,57.286]],"c":true}},"nm":"路径 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.886,0.886,0.937,1]},"o":{"a":0,"k":100},"r":1,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[54.535,57.536],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"组 1","np":2,"mn":"ADBE Vector Group"}],"ip":0,"op":750,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":9,"ty":4,"nm":"\rbelly1","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[124,95,0]},"a":{"a":0,"k":[60.25,60.25,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[-33.137,0],[0,33.137],[33.137,0],[10.189,-7.981],[1.714,0.857],[6.177,-9.127],[0,-12.88]],"o":[[33.137,0],[0,-33.137],[-13.946,0],[-2.141,1.677],[-1.714,-0.858],[-6.908,9.776],[0,33.137]],"v":[[0,60],[60,0],[0,-60],[-36.969,-47.261],[-44,-50.571],[-49.034,-34.587],[-60,0]],"c":true}},"nm":"路径 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.839,0.843,0.906,1]},"o":{"a":0,"k":100},"r":1,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[60.25,60.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"组 1","np":2,"mn":"ADBE Vector Group"}],"ip":0,"op":750,"st":0,"bm":0,"sr":1}]} ================================================ FILE: build-win-msix.bat ================================================ dart run msix:create --release -v --output-path build/windows/runner --output-name AIdea ================================================ FILE: build-win.bat ================================================ flutter build windows --release ================================================ 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: docker-build.sh ================================================ #!/usr/bin/env bash VERSION=1.0.14 rm -fr build/web flutter build web --web-renderer canvaskit --release --dart-define=API_SERVER_URL=/ docker buildx build --platform=linux/amd64,linux/arm64 -t mylxsw/aidea-web:$VERSION . --push ================================================ FILE: flutter_launcher_icons.yaml ================================================ # flutter pub run flutter_launcher_icons flutter_launcher_icons: image_path: "assets/app.png" android: "launcher_icon" # image_path_android: "assets/icon/icon.png" min_sdk_android: 21 # android min sdk min:16, default 21 # adaptive_icon_background: "assets/icon/background.png" # adaptive_icon_foreground: "assets/icon/foreground.png" # adaptive_icon_monochrome: "assets/icon/monochrome.png" ios: true # image_path_ios: "assets/icon/icon.png" remove_alpha_channel_ios: true # image_path_ios_dark_transparent: "assets/icon/icon_dark.png" # image_path_ios_tinted_grayscale: "assets/icon/icon_tinted.png" # desaturate_tinted_to_grayscale_ios: true web: generate: true # image_path: "path/to/image.png" background_color: "#hexcode" theme_color: "#hexcode" windows: generate: true image_path: "assets/app-macos.png" icon_size: 256 # min:48, max:256, default: 48 macos: generate: true image_path: "assets/app-macos.png" ================================================ FILE: install.iss ================================================ ; Script generated by the Inno Setup Script Wizard. ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "AIdea" #define MyAppVersion "2.0.0" #define MyAppPublisher "Shenzhen Gulu Artificial Intelligence Technology Co., Ltd." #define MyAppURL "https://ai.aicode.cc/" #define MyAppExeName "AIdea.exe" [Setup] ; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) AppId={{F9E7E323-8BD4-46B3-ABEB-20C5CE03F5C7} AppName={#MyAppName} AppVersion={#MyAppVersion} ;AppVerName={#MyAppName} {#MyAppVersion} AppPublisher={#MyAppPublisher} AppPublisherURL={#MyAppURL} AppSupportURL={#MyAppURL} AppUpdatesURL={#MyAppURL} DefaultDirName={autopf}\{#MyAppName} DisableProgramGroupPage=yes ; Uncomment the following line to run in non administrative install mode (install for current user only.) ;PrivilegesRequired=lowest OutputDir=C:\Users\mylxsw\Desktop OutputBaseFilename={#MyAppName}-{#MyAppVersion}-Installer SetupIconFile=D:\Workstation\codes\aidea\app.ico Compression=lzma SolidCompression=yes WizardStyle=modern [Languages] Name: "english"; MessagesFile: "compiler:Default.isl" [Tasks] Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkablealone [Files] Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-core-console-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-core-console-l1-2-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-core-datetime-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-core-debug-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-core-errorhandling-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-core-fibers-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-core-file-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-core-file-l1-2-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-core-file-l2-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-core-handle-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-core-heap-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-core-interlocked-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-core-libraryloader-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-core-localization-l1-2-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-core-memory-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-core-namedpipe-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-core-processenvironment-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-core-processthreads-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-core-processthreads-l1-1-1.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-core-profile-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-core-rtlsupport-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-core-string-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-core-synch-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-core-synch-l1-2-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-core-sysinfo-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-core-timezone-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-core-util-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-crt-conio-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-crt-convert-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-crt-environment-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-crt-filesystem-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-crt-heap-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-crt-locale-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-crt-math-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-crt-multibyte-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-crt-private-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-crt-process-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-crt-runtime-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-crt-stdio-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-crt-string-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-crt-time-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-crt-utility-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-downlevel-kernel32-l2-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\api-ms-win-eventing-provider-l1-1-0.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\audioplayers_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\concrt140.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\d3dcompiler_47.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\file_saver_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\flutter_localization_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\flutter_tts_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\flutter_windows.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\libc++.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\libEGL.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\libGLESv2.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\libmpv-2.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\media_kit_libs_windows_video_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\media_kit_native_event_loop.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\media_kit_video_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\msvcp140.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\msvcp140_1.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\msvcp140_2.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\msvcp140_atomic_wait.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\msvcp140_codecvt_ids.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\record_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\screen_brightness_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\share_plus_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\sqlite3.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\ucrtbase.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\ucrtbased.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\url_launcher_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\vccorlib140.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\vccorlib140d.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\vcruntime140.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\vcruntime140_1.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\vcruntime140_1d.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\vcruntime140d.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\vk_swiftshader.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\vulkan-1.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\zlib.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "D:\Workstation\codes\aidea\build\windows\x64\runner\Release\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs createallsubdirs ; NOTE: Don't use "Flags: ignoreversion" on any shared system files [Icons] Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon [Run] Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent ================================================ 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 MinimumOSVersion 12.0 ================================================ FILE: ios/Flutter/Debug.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" ================================================ FILE: ios/Flutter/Release.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" ================================================ FILE: ios/Podfile ================================================ # Uncomment this line to define a global platform for your project platform :ios, '13.0' # 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! use_modular_headers! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) 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) 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":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"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":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@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/LaunchBackground.imageset/Contents.json ================================================ { "images" : [ { "filename" : "background.png", "idiom" : "universal", "scale" : "1x" }, { "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json ================================================ { "images" : [ { "filename" : "LaunchImage.png", "idiom" : "universal", "scale" : "1x" }, { "filename" : "LaunchImage@2x.png", "idiom" : "universal", "scale" : "2x" }, { "filename" : "LaunchImage@3x.png", "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ 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/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: ios/Runner/Base.lproj/Main.storyboard ================================================ ================================================ FILE: ios/Runner/Info.plist ================================================ CADisableMinimumFrameDurationOnPhone CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName AIdea CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName askaide CFBundlePackageType APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) CFBundleSignature ???? CFBundleVersion $(FLUTTER_BUILD_NUMBER) ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes weixin weixinURLParamsAPI weixinULAPI LSRequiresIPhoneOS LSSupportsOpeningDocumentsInPlace NSAppTransportSecurity NSAllowsArbitraryLoads NSAllowsArbitraryLoadsForMedia NSCameraUsageDescription Used to demonstrate image picker plugin NSDocumentsFolderUsageDescription $(PRODUCT_NAME) needs access to your documents folder to save your files. NSMicrophoneUsageDescription We need to access to the microphone to record audio file NSPhotoLibraryUsageDescription Used to demonstrate image picker plugin NSLocationWhenInUseUsageDescription To enable GPS location access for Exif data UIApplicationSupportsIndirectInputEvents UIFileSharingEnabled UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIViewControllerBasedStatusBarAppearance UIStatusBarHidden ================================================ FILE: ios/Runner/Runner-Bridging-Header.h ================================================ #import "GeneratedPluginRegistrant.h" ================================================ FILE: ios/Runner/Runner.entitlements ================================================ aps-environment development com.apple.developer.applesignin Default com.apple.developer.associated-domains applinks:ai.aicode.cc ================================================ 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 */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 4AB86C8F0973565BB3C184F4 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 16B3CC3FDED8D8CC86722DEC /* Pods_Runner.framework */; }; 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 */; }; A5ECBA6F2A2B177200E3A820 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5ECBA6E2A2B177200E3A820 /* StoreKit.framework */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 16B3CC3FDED8D8CC86722DEC /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; A5D102132A0AA8E200331391 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; A5ECBA6E2A2B177200E3A820 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; A6F16BE481E65688799479DB /* 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 = ""; }; D5F820BBFB52D6BE21AD358D /* 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 = ""; }; EA2292F51AC4CF434D7A04AF /* 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 = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 4AB86C8F0973565BB3C184F4 /* Pods_Runner.framework in Frameworks */, A5ECBA6F2A2B177200E3A820 /* StoreKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 652A687C2637E7BC60520D2C /* Frameworks */ = { isa = PBXGroup; children = ( A5ECBA6E2A2B177200E3A820 /* StoreKit.framework */, 16B3CC3FDED8D8CC86722DEC /* Pods_Runner.framework */, ); name = Frameworks; 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 */, F0FF12F5C5F28587D2089C68 /* Pods */, 652A687C2637E7BC60520D2C /* Frameworks */, ); sourceTree = ""; }; 97C146EF1CF9000F007C117D /* Products */ = { isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, ); name = Products; sourceTree = ""; }; 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( A5D102132A0AA8E200331391 /* Runner.entitlements */, 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 = ""; }; F0FF12F5C5F28587D2089C68 /* Pods */ = { isa = PBXGroup; children = ( D5F820BBFB52D6BE21AD358D /* Pods-Runner.debug.xcconfig */, A6F16BE481E65688799479DB /* Pods-Runner.release.xcconfig */, EA2292F51AC4CF434D7A04AF /* Pods-Runner.profile.xcconfig */, ); path = Pods; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( 5E113495E8126976AA1B9B6D /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 13DFE0107180EC2050B1ABEA /* [CP] Embed Pods Frameworks */, 8C0A97AF55BE760FDB31E756 /* [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 = { LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 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 */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 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 */ 13DFE0107180EC2050B1ABEA /* [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; }; 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"; }; 5E113495E8126976AA1B9B6D /* [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; }; 8C0A97AF55BE760FDB31E756 /* [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; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Run Script"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase 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; 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; 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; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = N95437SZ2A; ENABLE_BITCODE = NO; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", "-l\"WechatOpenSDK\"", "-l\"c++\"", "-l\"sqlite3\"", "-l\"sqlite3.0\"", "-l\"z\"", "-framework", "\"AVFoundation\"", "-framework", "\"AVKit\"", "-framework", "\"AlipaySDK\"", "-framework", "\"CFNetwork\"", "-framework", "\"CoreGraphics\"", "-framework", "\"CoreMotion\"", "-framework", "\"CoreTelephony\"", "-framework", "\"CoreText\"", "-framework", "\"DKImagePickerController\"", "-framework", "\"DKPhotoGallery\"", "-framework", "\"Foundation\"", "-framework", "\"ImageIO\"", "-framework", "\"Mantle\"", "-framework", "\"Photos\"", "-framework", "\"QuartzCore\"", "-framework", "\"SDWebImage\"", "-framework", "\"SDWebImageWebPCoder\"", "-framework", "\"Security\"", "-framework", "\"SwiftyGif\"", "-framework", "\"SystemConfiguration\"", "-framework", "\"UIKit\"", "-framework", "\"WebKit\"", "-framework", "\"audioplayers_darwin\"", "-framework", "\"file_picker\"", "-framework", "\"file_saver\"", "-framework", "\"flutter_localization\"", "-framework", "\"flutter_native_splash\"", "-framework", "\"flutter_tts\"", "-framework", "\"fluwx\"", "-framework", "\"image_gallery_saver\"", "-framework", "\"in_app_purchase_storekit\"", "-framework", "\"libwebp\"", "-framework", "\"path_provider_foundation\"", "-framework", "\"share_plus\"", "-framework", "\"shared_preferences_foundation\"", "-framework", "\"sign_in_with_apple\"", "-framework", "\"sqflite\"", "-framework", "\"tobias\"", "-framework", "\"url_launcher_ios\"", "-weak_framework", "\"LinkPresentation\"", "-ld64", ); PRODUCT_BUNDLE_IDENTIFIER = cc.aicode.flutter.askaide.askaide; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; 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; 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; 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; 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; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = N95437SZ2A; ENABLE_BITCODE = NO; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", "-l\"WechatOpenSDK\"", "-l\"c++\"", "-l\"sqlite3\"", "-l\"sqlite3.0\"", "-l\"z\"", "-framework", "\"AVFoundation\"", "-framework", "\"AVKit\"", "-framework", "\"AlipaySDK\"", "-framework", "\"CFNetwork\"", "-framework", "\"CoreGraphics\"", "-framework", "\"CoreMotion\"", "-framework", "\"CoreTelephony\"", "-framework", "\"CoreText\"", "-framework", "\"DKImagePickerController\"", "-framework", "\"DKPhotoGallery\"", "-framework", "\"Foundation\"", "-framework", "\"ImageIO\"", "-framework", "\"Mantle\"", "-framework", "\"Photos\"", "-framework", "\"QuartzCore\"", "-framework", "\"SDWebImage\"", "-framework", "\"SDWebImageWebPCoder\"", "-framework", "\"Security\"", "-framework", "\"SwiftyGif\"", "-framework", "\"SystemConfiguration\"", "-framework", "\"UIKit\"", "-framework", "\"WebKit\"", "-framework", "\"audioplayers_darwin\"", "-framework", "\"file_picker\"", "-framework", "\"file_saver\"", "-framework", "\"flutter_localization\"", "-framework", "\"flutter_native_splash\"", "-framework", "\"flutter_tts\"", "-framework", "\"fluwx\"", "-framework", "\"image_gallery_saver\"", "-framework", "\"in_app_purchase_storekit\"", "-framework", "\"libwebp\"", "-framework", "\"path_provider_foundation\"", "-framework", "\"share_plus\"", "-framework", "\"shared_preferences_foundation\"", "-framework", "\"sign_in_with_apple\"", "-framework", "\"sqflite\"", "-framework", "\"tobias\"", "-framework", "\"url_launcher_ios\"", "-weak_framework", "\"LinkPresentation\"", "-ld64", ); PRODUCT_BUNDLE_IDENTIFIER = cc.aicode.flutter.askaide.askaide; 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; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = N95437SZ2A; ENABLE_BITCODE = NO; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", "-l\"WechatOpenSDK\"", "-l\"c++\"", "-l\"sqlite3\"", "-l\"sqlite3.0\"", "-l\"z\"", "-framework", "\"AVFoundation\"", "-framework", "\"AVKit\"", "-framework", "\"AlipaySDK\"", "-framework", "\"CFNetwork\"", "-framework", "\"CoreGraphics\"", "-framework", "\"CoreMotion\"", "-framework", "\"CoreTelephony\"", "-framework", "\"CoreText\"", "-framework", "\"DKImagePickerController\"", "-framework", "\"DKPhotoGallery\"", "-framework", "\"Foundation\"", "-framework", "\"ImageIO\"", "-framework", "\"Mantle\"", "-framework", "\"Photos\"", "-framework", "\"QuartzCore\"", "-framework", "\"SDWebImage\"", "-framework", "\"SDWebImageWebPCoder\"", "-framework", "\"Security\"", "-framework", "\"SwiftyGif\"", "-framework", "\"SystemConfiguration\"", "-framework", "\"UIKit\"", "-framework", "\"WebKit\"", "-framework", "\"audioplayers_darwin\"", "-framework", "\"file_picker\"", "-framework", "\"file_saver\"", "-framework", "\"flutter_localization\"", "-framework", "\"flutter_native_splash\"", "-framework", "\"flutter_tts\"", "-framework", "\"fluwx\"", "-framework", "\"image_gallery_saver\"", "-framework", "\"in_app_purchase_storekit\"", "-framework", "\"libwebp\"", "-framework", "\"path_provider_foundation\"", "-framework", "\"share_plus\"", "-framework", "\"shared_preferences_foundation\"", "-framework", "\"sign_in_with_apple\"", "-framework", "\"sqflite\"", "-framework", "\"tobias\"", "-framework", "\"url_launcher_ios\"", "-weak_framework", "\"LinkPresentation\"", "-ld64", ); PRODUCT_BUNDLE_IDENTIFIER = cc.aicode.flutter.askaide.askaide; 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 */ 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: lib/bloc/account_bloc.dart ================================================ import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/http.dart'; import 'package:askaide/repo/api/user.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; part 'account_event.dart'; part 'account_state.dart'; class AccountBloc extends Bloc { final SettingRepository settings; AccountBloc(this.settings) : super(AccountInitial()) { // 加载用户信息 on((event, emit) async { emit(AccountLoading()); final token = settings.get(settingAPIServerToken); if (token != null && token != '') { try { final user = await APIServer().userInfo(cache: event.cache); if (user != null) { emit(AccountLoaded(user)); } else { emit(AccountNeedSignIn()); } } catch (e) { emit(AccountLoaded(null, error: e)); } } else { emit(AccountNeedSignIn()); } }); on((event, emit) async { await settings.set(settingAPIServerToken, ''); await settings.set(settingUserInfo, ''); await HttpClient.cleanCache(); emit(AccountNeedSignIn()); }); on((event, emit) async { try { if (event.realname != null) { await APIServer().updateUserRealname(realname: event.realname!); } if (event.avatarURL != null) { await APIServer().updateUserAvatar(avatarURL: event.avatarURL!); } emit(AccountLoaded(await APIServer().userInfo(cache: false))); } catch (e) { emit(AccountLoaded(await APIServer().userInfo(cache: false), error: e)); } }); } } ================================================ FILE: lib/bloc/account_event.dart ================================================ part of 'account_bloc.dart'; @immutable abstract class AccountEvent {} class AccountLoadEvent extends AccountEvent { final bool cache; AccountLoadEvent({this.cache = true}); } class AccountSignOutEvent extends AccountEvent {} class AccountUpdateEvent extends AccountEvent { final String? realname; final String? avatarURL; AccountUpdateEvent({this.realname, this.avatarURL}); } ================================================ FILE: lib/bloc/account_state.dart ================================================ part of 'account_bloc.dart'; @immutable abstract class AccountState {} class AccountInitial extends AccountState {} class AccountLoading extends AccountState {} class AccountLoaded extends AccountState { final UserInfo? user; final Object? error; AccountLoaded(this.user, {this.error}); } class AccountNeedSignIn extends AccountState {} ================================================ FILE: lib/bloc/admin_payment_bloc.dart ================================================ import 'package:askaide/repo/api/admin/payment.dart'; import 'package:askaide/repo/api/page.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; part 'admin_payment_event.dart'; part 'admin_payment_state.dart'; class AdminPaymentBloc extends Bloc { AdminPaymentBloc() : super(AdminPaymentInitial()) { on((event, emit) async { final histories = await APIServer().adminPaymentHistories( page: event.page, perPage: event.perPage, keyword: event.keyword, ); emit(AdminPaymentHistoriesLoaded(histories)); }); } } ================================================ FILE: lib/bloc/admin_payment_event.dart ================================================ part of 'admin_payment_bloc.dart'; @immutable sealed class AdminPaymentEvent {} class AdminPaymentHistoriesLoadEvent extends AdminPaymentEvent { final int page; final int perPage; final String? keyword; AdminPaymentHistoriesLoadEvent({ this.page = 1, this.perPage = 20, this.keyword, }); } ================================================ FILE: lib/bloc/admin_payment_state.dart ================================================ part of 'admin_payment_bloc.dart'; @immutable sealed class AdminPaymentState {} final class AdminPaymentInitial extends AdminPaymentState {} class AdminPaymentOperationResult extends AdminPaymentState { final bool success; final String message; AdminPaymentOperationResult(this.success, this.message); } class AdminPaymentHistoriesLoaded extends AdminPaymentState { final PagedData histories; AdminPaymentHistoriesLoaded(this.histories); } ================================================ FILE: lib/bloc/admin_room_bloc.dart ================================================ import 'package:askaide/repo/api/page.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/model/message.dart'; import 'package:askaide/repo/model/misc.dart'; import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; part 'admin_room_event.dart'; part 'admin_room_state.dart'; class AdminRoomBloc extends Bloc { AdminRoomBloc() : super(AdminRoomInitial()) { on((event, emit) async { final rooms = await APIServer().adminUserRooms(userId: event.userId); emit(AdminRoomsLoaded(rooms: rooms)); }); on((event, emit) async { final room = await APIServer().adminUserRoom( userId: event.userId, roomId: event.roomId, ); emit(AdminRoomLoaded(room: room)); }); on((event, emit) async { if (event.roomType == 4) { final messages = await APIServer().adminUserRoomGroupMessages( userId: event.userId, roomId: event.roomId); emit(AdminRoomRecentlyMessagesLoaded( messages: messages .map((e) => Message( e.role == 'user' ? Role.sender : Role.receiver, e.message, type: MessageType.text, ts: e.createdAt, model: e.model, quotaConsumed: e.quotaConsumed, tokenConsumed: e.tokenConsumed, refId: e.pid, id: e.id, serverId: e.id, )) .toList())); } else { final messages = await APIServer().adminUserRoomMessages( userId: event.userId, roomId: event.roomId, ); emit(AdminRoomRecentlyMessagesLoaded( messages: messages .map((e) => Message( e.role == 1 ? Role.sender : Role.receiver, e.message, type: MessageType.text, ts: e.createdAt, model: e.model, quotaConsumed: e.quotaConsumed, tokenConsumed: e.tokenConsumed, refId: e.pid, id: e.id, serverId: e.id, userId: e.userId, roomId: e.roomId, )) .toList())); } }); on((event, emit) async { final messages = await APIServer().adminRecentlyMessages( page: event.page, perPage: event.perPage, keyword: event.keyword, ); emit(AdminRecentlyMessagesLoaded(PagedData( data: messages.data .map((e) => Message( e.role == 1 ? Role.sender : Role.receiver, e.message, type: MessageType.text, ts: e.createdAt, model: e.model, quotaConsumed: e.quotaConsumed, tokenConsumed: e.tokenConsumed, refId: e.pid, id: e.id, serverId: e.id, userId: e.userId, roomId: e.roomId, )) .toList(), page: messages.page, perPage: messages.perPage, total: messages.total, lastPage: messages.lastPage, ))); }); } } ================================================ FILE: lib/bloc/admin_room_event.dart ================================================ part of 'admin_room_bloc.dart'; @immutable sealed class AdminRoomEvent {} class AdminRoomsLoadEvent extends AdminRoomEvent { final int userId; AdminRoomsLoadEvent({required this.userId}); } class AdminRoomLoadEvent extends AdminRoomEvent { final int userId; final int roomId; AdminRoomLoadEvent({required this.roomId, required this.userId}); } class AdminRoomRecentlyMessagesLoadEvent extends AdminRoomEvent { final int userId; final int roomId; final int roomType; AdminRoomRecentlyMessagesLoadEvent({ required this.roomId, required this.userId, required this.roomType, }); } class AdminRecentlyMessagesLoadEvent extends AdminRoomEvent { final int page; final int perPage; final String? keyword; AdminRecentlyMessagesLoadEvent({ required this.page, required this.perPage, this.keyword, }); } ================================================ FILE: lib/bloc/admin_room_state.dart ================================================ part of 'admin_room_bloc.dart'; @immutable sealed class AdminRoomState {} final class AdminRoomInitial extends AdminRoomState {} final class AdminRoomsLoaded extends AdminRoomState { final List rooms; AdminRoomsLoaded({required this.rooms}); } final class AdminRoomLoaded extends AdminRoomState { final RoomInServer room; AdminRoomLoaded({required this.room}); } final class AdminRoomRecentlyMessagesLoaded extends AdminRoomState { final List messages; AdminRoomRecentlyMessagesLoaded({required this.messages}); } class AdminRoomOperationResult extends AdminRoomState { final bool success; final String message; AdminRoomOperationResult(this.success, this.message); } class AdminRecentlyMessagesLoaded extends AdminRoomState { final PagedData messages; AdminRecentlyMessagesLoaded(this.messages); } ================================================ FILE: lib/bloc/background_image_bloc.dart ================================================ import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/model/misc.dart'; import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; part 'background_image_event.dart'; part 'background_image_state.dart'; class BackgroundImageBloc extends Bloc { BackgroundImageBloc() : super(BackgroundImageInitial()) { on((event, emit) async { final images = await APIServer().backgrounds(); emit(BackgroundImageLoaded(images)); }); } } ================================================ FILE: lib/bloc/background_image_event.dart ================================================ part of 'background_image_bloc.dart'; @immutable abstract class BackgroundImageEvent {} class BackgroundImageLoadEvent extends BackgroundImageEvent {} ================================================ FILE: lib/bloc/background_image_state.dart ================================================ part of 'background_image_bloc.dart'; @immutable abstract class BackgroundImageState {} class BackgroundImageInitial extends BackgroundImageState {} class BackgroundImageLoaded extends BackgroundImageState { final List images; BackgroundImageLoaded(this.images); } ================================================ FILE: lib/bloc/bloc_manager.dart ================================================ // ignore_for_file: must_call_super import 'package:askaide/bloc/chat_message_bloc.dart'; import 'package:askaide/helper/lru.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class ChatBlocManager { static final ChatBlocManager _singleton = ChatBlocManager._internal(); factory ChatBlocManager() { return _singleton; } ChatBlocManager._internal(); late final ChatMessageBloc Function(int roomId, {int? chatHistoryId}) blocBuilder; init(ChatMessageBloc Function(int roomId, {int? chatHistoryId}) blocBuilder) { this.blocBuilder = blocBuilder; } final LRUCache _blocs = LRUCache(10); ChatMessageBloc getBloc(int roomId, {int? chatHistoryId}) { final key = '$roomId-$chatHistoryId'; if (_blocs.containsKey(key)) { return _blocs.get(key)!; } else { final bloc = blocBuilder(roomId, chatHistoryId: chatHistoryId); _blocs.put(key, bloc); return bloc; } } void dispose() { _blocs.clear(); } } abstract class BlocExt extends Bloc implements Disposable { BlocExt(super.initialState); @override void dispose() { super.close(); } @override Future close() async { return; } } ================================================ FILE: lib/bloc/channel_bloc.dart ================================================ import 'package:askaide/repo/api/admin/channels.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; part 'channel_event.dart'; part 'channel_state.dart'; class ChannelBloc extends Bloc { ChannelBloc() : super(ChannelInitial()) { /// 加载所有渠道 on((event, emit) async { final channels = await APIServer().adminChannels(); emit(ChannelsLoaded(channels)); }); /// 加载单个渠道 on((event, emit) async { final channel = await APIServer().adminChannel(id: event.channelId); emit(ChannelLoaded(channel)); }); /// 创建渠道 on((event, emit) async { try { await APIServer().adminCreateChannel(event.req); emit(ChannelOperationResult(true, '创建成功')); } catch (e) { emit(ChannelOperationResult(false, e.toString())); } }); /// 更新渠道 on((event, emit) async { try { await APIServer().adminUpdateChannel( id: event.channelId, req: event.req, ); emit(ChannelOperationResult(true, '更新成功')); } catch (e) { emit(ChannelOperationResult(false, e.toString())); } }); /// 删除渠道 on((event, emit) async { try { await APIServer().adminDeleteChannel(id: event.channelId); emit(ChannelOperationResult(true, '删除成功')); } catch (e) { emit(ChannelOperationResult(false, e.toString())); } }); } } ================================================ FILE: lib/bloc/channel_event.dart ================================================ part of 'channel_bloc.dart'; @immutable sealed class ChannelEvent {} class ChannelsLoadEvent extends ChannelEvent {} class ChannelLoadEvent extends ChannelEvent { final int channelId; ChannelLoadEvent(this.channelId); } class ChannelCreateEvent extends ChannelEvent { final AdminChannelAddReq req; ChannelCreateEvent(this.req); } class ChannelUpdateEvent extends ChannelEvent { final int channelId; final AdminChannelUpdateReq req; ChannelUpdateEvent(this.channelId, this.req); } class ChannelDeleteEvent extends ChannelEvent { final int channelId; ChannelDeleteEvent(this.channelId); } ================================================ FILE: lib/bloc/channel_state.dart ================================================ part of 'channel_bloc.dart'; @immutable sealed class ChannelState {} final class ChannelInitial extends ChannelState {} class ChannelsLoaded extends ChannelState { final List channels; ChannelsLoaded(this.channels); } class ChannelLoaded extends ChannelState { final AdminChannel channel; ChannelLoaded(this.channel); } class ChannelOperationResult extends ChannelState { final bool success; final String message; ChannelOperationResult(this.success, this.message); } ================================================ FILE: lib/bloc/chat_chat_bloc.dart ================================================ import 'package:askaide/helper/constant.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/chat_message_repo.dart'; import 'package:askaide/repo/model/chat_history.dart'; import 'package:askaide/repo/model/misc.dart'; import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; part 'chat_chat_event.dart'; part 'chat_chat_state.dart'; class ChatChatBloc extends Bloc { final ChatMessageRepository _chatMessageRepository; ChatChatBloc(this._chatMessageRepository) : super(ChatChatInitial()) { // 加载最近的历史记录 on((event, emit) async { final histories = await _chatMessageRepository.recentChatHistories( event.count, userId: APIServer().localUserID(), ); emit(ChatChatRecentHistoriesLoaded( histories: histories, examples: const [], )); }); // 删除历史记录 on((event, emit) async { await _chatMessageRepository.deleteChatHistory(event.chatId); add(ChatChatLoadRecentHistories()); }); } } ================================================ FILE: lib/bloc/chat_chat_event.dart ================================================ part of 'chat_chat_bloc.dart'; @immutable abstract class ChatChatEvent {} class ChatChatLoadRecentHistories extends ChatChatEvent { final int count; ChatChatLoadRecentHistories({this.count = defaultChatHistoryCount}); } class ChatChatNewChat extends ChatChatEvent { final String text; ChatChatNewChat(this.text); } class ChatChatDeleteHistory extends ChatChatEvent { final int chatId; ChatChatDeleteHistory(this.chatId); } ================================================ FILE: lib/bloc/chat_chat_state.dart ================================================ part of 'chat_chat_bloc.dart'; @immutable abstract class ChatChatState {} class ChatChatInitial extends ChatChatState {} class ChatChatRecentHistoriesLoaded extends ChatChatState { final List histories; final List? examples; ChatChatRecentHistoriesLoaded({this.histories = const [], this.examples}); } ================================================ FILE: lib/bloc/chat_event.dart ================================================ part of 'chat_message_bloc.dart'; @immutable abstract class ChatMessageEvent {} class ChatMessageReceivedEvent extends ChatMessageEvent { final Message message; ChatMessageReceivedEvent(this.message); } class ChatMessageSendEvent extends ChatMessageEvent { final Message message; final int? index; final bool isResent; final String? tempModel; ChatMessageSendEvent(this.message, {this.index, this.isResent = false, this.tempModel}); } class ChatMessageGetRecentEvent extends ChatMessageEvent { final int? chatHistoryId; ChatMessageGetRecentEvent({this.chatHistoryId}); } class ChatMessageClearAllEvent extends ChatMessageEvent {} class ChatMessageBreakContextEvent extends ChatMessageEvent {} class ChatMessageDeleteEvent extends ChatMessageEvent { final List ids; final int? chatHistoryId; ChatMessageDeleteEvent(this.ids, {this.chatHistoryId}); } class ChatMessageStopEvent extends ChatMessageEvent {} ================================================ FILE: lib/bloc/chat_message_bloc.dart ================================================ import 'dart:convert'; import 'package:askaide/bloc/bloc_manager.dart'; import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/error.dart'; import 'package:askaide/helper/logger.dart'; import 'package:askaide/helper/model_resolver.dart'; import 'package:askaide/helper/queue.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/chat_message_repo.dart'; import 'package:askaide/repo/data/chat_message_data.dart'; import 'package:askaide/repo/model/chat_history.dart'; import 'package:askaide/repo/model/message.dart'; import 'package:askaide/repo/model/room.dart'; import 'package:askaide/repo/openai_repo.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:dart_openai/openai.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; part 'chat_event.dart'; part 'chat_state.dart'; class ChatMessageBloc extends BlocExt { final ChatMessageRepository chatMsgRepo; final SettingRepository settingRepo; final int roomId; final int? chatHistoryId; GracefulQueue? currentQueue; ChatMessageBloc( this.roomId, { required this.chatMsgRepo, required this.settingRepo, this.chatHistoryId, }) : super(ChatMessageInitial()) { on(_messageSendEventHandler); on(_getRecentEventHandler); on(_clearAllEventHandler); on(_breakContextEventHandler); on(_deleteMessageEventHandler); on(_stopEventHandler); } Future fixRoomId(int? chatHistoryId) async { if (chatHistoryId != null && chatHistoryId > 0) { final his = await chatMsgRepo.getChatHistory(chatHistoryId); if (his != null) { return his.roomId ?? roomId; } } return roomId; } Future _deleteMessageEventHandler(event, emit) async { final roomId = await fixRoomId(event.chatHistoryId); await chatMsgRepo.removeMessage(roomId, event.ids); ChatHistory? his; if (event.chatHistoryId != null && event.chatHistoryId! > 0) { his = await chatMsgRepo.getChatHistory(event.chatHistoryId!); } emit(ChatMessagesLoaded( await chatMsgRepo.getRecentMessages( roomId: roomId, userId: APIServer().localUserID(), chatHistoryId: event.chatHistoryId, ), chatHistory: his, )); } /// 设置上下文清理标识 Future _breakContextEventHandler(event, emit) async { final roomId = await fixRoomId(event.chatHistoryId); // 查询当前 Room 信息 final room = await queryRoomById(chatMsgRepo, roomId); if (room == null) { emit(ChatMessagesLoaded( await chatMsgRepo.getRecentMessages( roomId: roomId, userId: APIServer().localUserID(), ), error: 'The selected item does not exist', )); return; } final lastMessage = await chatMsgRepo.getLastMessage( roomId, userId: APIServer().localUserID(), ); if (lastMessage != null && (lastMessage.type == MessageType.contextBreak || lastMessage.isInitMessage())) { return; } await chatMsgRepo.sendMessage( roomId, Message( Role.receiver, AppLocale.contextBreakMessage, ts: DateTime.now(), type: MessageType.contextBreak, roomId: roomId, userId: APIServer().localUserID(), ), ); if (room.initMessage != null && room.initMessage != '') { await chatMsgRepo.sendMessage( roomId, Message( Role.receiver, room.initMessage!, ts: DateTime.now(), type: MessageType.initMessage, roomId: roomId, userId: APIServer().localUserID(), ), ); } final messages = await chatMsgRepo.getRecentMessages( roomId: roomId, userId: APIServer().localUserID(), ); emit(ChatMessagesLoaded(messages)); emit(ChatMessageUpdated(messages.last)); } /// 清空消息事件处理 Future _clearAllEventHandler(event, emit) async { final roomId = await fixRoomId(event.chatHistoryId); // 查询当前 Room 信息 final room = await queryRoomById(chatMsgRepo, roomId); if (room == null) { emit(ChatMessagesLoaded( await chatMsgRepo.getRecentMessages( roomId: roomId, userId: APIServer().localUserID(), ), error: 'The selected item does not exist', )); return; } await chatMsgRepo.clearMessages( roomId, userId: APIServer().localUserID(), ); if (room.initMessage != null && room.initMessage != '') { await chatMsgRepo.sendMessage( roomId, Message( Role.receiver, room.initMessage!, ts: DateTime.now(), type: MessageType.initMessage, roomId: roomId, userId: APIServer().localUserID(), ), ); } emit(ChatMessagesLoaded(await chatMsgRepo.getRecentMessages( roomId: roomId, userId: APIServer().localUserID(), ))); } /// 页面加载事件处理 Future _getRecentEventHandler(event, emit) async { final roomId = await fixRoomId(event.chatHistoryId); ChatHistory? his; if (event.chatHistoryId != null && event.chatHistoryId! > 0) { his = await chatMsgRepo.getChatHistory(event.chatHistoryId!); } if (his == null) { emit(ChatMessagesLoaded(const [])); } else { emit(ChatMessagesLoaded( await chatMsgRepo.getRecentMessages( roomId: roomId, userId: APIServer().localUserID(), chatHistoryId: event.chatHistoryId, ), chatHistory: his, )); } } /// 停止输出事件处理 Future _stopEventHandler(event, emit) async { if (currentQueue != null) { currentQueue!.finish(); } } Future resolveChatHistory(Message message, int roomId) async { // 如果是聊一聊,自动创建聊天记录历史 if (message.chatHistoryId == null || message.chatHistoryId! <= 0) { final chatHistory = await chatMsgRepo.createChatHistory( title: message.text, userId: APIServer().localUserID(), roomId: roomId, model: message.model, lastMessage: message.text, ); return chatHistory; } return await chatMsgRepo.getChatHistory(message.chatHistoryId!); } /// Message sending event processing Future _messageSendEventHandler(event, emit) async { if (event.message is! Message) { return; } Message message = event.message as Message; final roomId = await fixRoomId(message.chatHistoryId); ChatHistory localChatHistory = (await resolveChatHistory(message, roomId))!; message.chatHistoryId = localChatHistory.id; emit(ChatHistoryInited(localChatHistory.id!)); // 查询当前 Room 信息 final room = await queryRoomById(chatMsgRepo, roomId); if (room == null) { emit(ChatMessagesLoaded( await chatMsgRepo.getRecentMessages( roomId: roomId, userId: APIServer().localUserID(), chatHistoryId: localChatHistory.id, ), error: 'The selected item does not exist', chatHistory: localChatHistory, )); return; } if (localChatHistory.model != null) { room.model = localChatHistory.model!; } // 查询最后一条消息 // 如果最后一条消息符合以下情况,则创建时间线 // 1. 最后一条消息不存在 // 2. 最后一条消息的时间距离当前时间超过 3 小时 var last = await chatMsgRepo.getLastMessage( roomId, chatHistoryId: localChatHistory.id, userId: APIServer().localUserID(), ); if (last == null || last.ts == null || DateTime.now().difference(last.ts!).inMinutes > 60 * 3) { // 发送时间线消息 await chatMsgRepo.sendMessage( roomId, Message( Role.receiver, DateFormat('y-MM-dd HH:mm').format(DateTime.now().toLocal()), type: MessageType.timeline, ts: DateTime.now(), roomId: roomId, userId: APIServer().localUserID(), chatHistoryId: localChatHistory.id, ), ); } // 发送当前用户消息 message.model ??= room.model; message.userId = APIServer().localUserID(); message.status = 0; // 模型切换 String? tempModel = event.tempModel; String? originalModel = message.model; room.model = tempModel ?? originalModel ?? room.model; // Logger.instance // .d('发送消息, originalModel: $originalModel, tempModel: $tempModel'); // 聊天历史记录中,所有发送状态为 pending 状态的消息,全部设置为失败 await chatMsgRepo.fixMessageStatus(roomId); // 记录当前消息 var sentMessageId = 0; if (event.isResent && event.index == 0 && last != null && last.type == MessageType.text) { // 如果当前是消息重发,同时重发的是最后一条消息,则不会重新生成该消息,直接生成答案即可 sentMessageId = last.id!; if (last.statusIsFailed()) { // 如果最后一条消息发送失败,则重新发送 await chatMsgRepo.updateMessagePart(roomId, last.id!, [ MessagePart('status', 0), ]); } } else { message.model = tempModel ?? message.model; sentMessageId = await chatMsgRepo.sendMessage(roomId, message); message.model = originalModel; } // 更新 Room 最后活跃时间 // 这里没有使用 await,因为不需要等待更新完成,让 room 的更新异步的去处理吧 if (!Ability().isUserLogon()) { chatMsgRepo.updateRoomLastActiveTime(roomId); } // 重新查询消息列表,此时包含了刚刚发送的消息+机器人思考中消息 final messages = await chatMsgRepo.getRecentMessages( roomId: roomId, userId: APIServer().localUserID(), chatHistoryId: localChatHistory.id, ); // 创建机器人思考中系统消息 Message waitMessage = Message( Role.receiver, '', ts: DateTime.now(), type: MessageType.text, model: tempModel ?? originalModel, roomId: roomId, userId: APIServer().localUserID(), refId: sentMessageId, chatHistoryId: localChatHistory.id, extra: '{}', ); // 回写消息 ID waitMessage.id = await chatMsgRepo.sendMessage(roomId, waitMessage); waitMessage.isReady = false; messages.add(waitMessage); emit(ChatMessagesLoaded( messages, processing: true, chatHistory: localChatHistory, )); emit(ChatMessageUpdated(waitMessage, processing: true)); // 等待监听机器人应答消息 final queue = GracefulQueue(); currentQueue = queue; try { RequestFailedException? error; try { var isThinking = false; var reasoningContent = ''; var listener = queue.listen(const Duration(milliseconds: 10), (items) { for (var element in items) { if (element.role == 'system') { try { // SYSTEM 命令 // - type: 命令类型 // // type=summary (默认值) // - question_id: 问题 ID // - answer_id: 答案 ID // - quota_consumed: 消耗的配额 // - token: 消耗的 token // - info: 提示信息 // // type=thinking // type=thinking-done: [time_consumed] final cmd = jsonDecode(element.content); switch (cmd['type']) { case 'summary': message.serverId = cmd['question_id']; waitMessage.serverId = cmd['answer_id']; final quotaConsumed = cmd['quota_consumed'] ?? 0; final tokenConsumed = cmd['token'] ?? 0; final info = cmd['info'] ?? ''; if (info != '') { waitMessage.updateExtra({'info': info}); } if (quotaConsumed == 0 && tokenConsumed == 0) { continue; } waitMessage.quotaConsumed = quotaConsumed; waitMessage.tokenConsumed = tokenConsumed; break; case 'thinking': waitMessage.pushExtra('states', 'thinking'); isThinking = true; break; case 'thinking-done': waitMessage.pushExtra('states', 'thinking-done'); waitMessage.updateExtra({'thinking_time_consumed': cmd['time_consumed'] ?? 0}); isThinking = false; break; case 'reference-documents': waitMessage.updateExtra({'reference-documents': cmd['data']}); break; case 'searching': waitMessage.pushExtra('states', 'searching'); break; case 'search-results': waitMessage.popExtra('states'); waitMessage.updateExtra({'search-results': cmd['data']}); break; default: } } catch (e) { // ignore: avoid_print } } else { if (isThinking) { reasoningContent = (reasoningContent + element.content).trim(); if (reasoningContent.contains('')) { final allParts = reasoningContent.split(''); final parts = [allParts[0], allParts.skip(1).join('')]; reasoningContent = parts[0].trim(); waitMessage.text += parts[1].trim(); } waitMessage.updateExtra({'reasoning': reasoningContent.replaceAll(RegExp(''), '')}); } else { waitMessage.text += element.content; } } } emit(ChatMessageUpdated(waitMessage, processing: true)); // 失败处理 for (var e in items) { if (e.code != null && e.code! > 0) { error = RequestFailedException(e.error ?? 'Request processing failure', e.code!); } } }); await ModelResolver.instance .request( room: room, tempModel: tempModel, contextMessages: messages.sublist(0, messages.length - 1), onMessage: queue.add, maxTokens: room.maxTokens, historyId: localChatHistory.id, flags: message.flags, ) .whenComplete(queue.finish); await listener; waitMessage.text = waitMessage.text.trim(); if (error == null && waitMessage.text.isEmpty) { error = RequestFailedException('The answer is empty', 500); } if (error != null) { throw error!; } } catch (e) { if (waitMessage.text.isEmpty) { Logger.instance.e('An error occurred during the response process: $e'); rethrow; } } // 机器人应答完成,将最后一条机器人应答消息更新到数据库,替换掉思考中消息 waitMessage.isReady = true; await chatMsgRepo.updateMessage(roomId, waitMessage.id!, waitMessage); // 更新聊天问题的服务端 ID 和消息状态 var sentMessageParts = []; sentMessageParts.add(MessagePart('status', 1)); if (message.serverId != null && message.serverId! > 0) { sentMessageParts.add(MessagePart('server_id', message.serverId)); } await chatMsgRepo.updateMessagePart( roomId, sentMessageId, sentMessageParts, ); // 更新聊天历史纪录最后一条消息 final chatHistory = await chatMsgRepo.getChatHistory(localChatHistory.id!); if (chatHistory != null) { chatHistory.lastMessage = waitMessage.text; // 异步处理就好,不需要等待 chatMsgRepo.updateChatHistory(localChatHistory.id!, chatHistory); } // 重新查询消息列表,此时包含了刚刚发送的消息+机器人应答消息 emit(ChatMessagesLoaded( await chatMsgRepo.getRecentMessages( roomId: roomId, userId: APIServer().localUserID(), chatHistoryId: localChatHistory.id, ), chatHistory: localChatHistory, )); } catch (e) { final error = resolveErrorMessage(e, isChat: true); await chatMsgRepo.updateMessagePart( roomId, sentMessageId, [ MessagePart('status', 2), MessagePart('extra', jsonEncode({'error': error.toString()})), ], ); if (waitMessage.id != null) { if (waitMessage.isReady) { await chatMsgRepo.updateMessage( roomId, waitMessage.id!, Message( Role.receiver, error.toString(), id: waitMessage.id, ts: DateTime.now(), type: MessageType.system, roomId: roomId, userId: APIServer().localUserID(), chatHistoryId: localChatHistory.id, model: tempModel ?? originalModel, ), ); } else { await chatMsgRepo.removeMessage(roomId, [waitMessage.id!]); } } emit(ChatMessagesLoaded( await chatMsgRepo.getRecentMessages( roomId: roomId, userId: APIServer().localUserID(), chatHistoryId: localChatHistory.id, ), error: error, chatHistory: localChatHistory, )); queue.finish(); } finally { queue.dispose(); currentQueue = null; } emit(ChatMessageUpdated(waitMessage)); } } Future queryRoomById(ChatMessageRepository chatMsgRepo, int roomId) async { Room? room; if (Ability().isUserLogon()) { final roomInServer = await APIServer().room(roomId: roomId); room = Room( roomInServer.name, 'chat', description: roomInServer.description, id: roomInServer.id, userId: roomInServer.userId, createdAt: roomInServer.createdAt, lastActiveTime: roomInServer.lastActiveTime, systemPrompt: roomInServer.systemPrompt, priority: roomInServer.priority ?? 0, model: '${roomInServer.vendor}:${roomInServer.model}', initMessage: roomInServer.initMessage, maxContext: roomInServer.maxContext, maxTokens: roomInServer.maxTokens, localRoom: false, ); } else { room = await chatMsgRepo.room(roomId); } return room; } ================================================ FILE: lib/bloc/chat_state.dart ================================================ part of 'chat_message_bloc.dart'; @immutable abstract class ChatMessageState {} class ChatMessageInitial extends ChatMessageState {} // 加载全量聊天记录 class ChatMessagesLoaded extends ChatMessageState { final List _messages; final bool processing; final Object? _error; final ChatHistory? chatHistory; ChatMessagesLoaded( this._messages, { Object? error, this.processing = false, this.chatHistory, }) : _error = error; get messages => _messages; get error => _error; } class ChatMessageError extends ChatMessageState { final String message; ChatMessageError(this.message); } class ChatMessageUpdated extends ChatMessageState { final Message message; /// 是否新消息正在处理中 final bool processing; ChatMessageUpdated(this.message, {this.processing = false}); } class ChatHistoryInited extends ChatMessageState { final int chatId; ChatHistoryInited(this.chatId); } ================================================ FILE: lib/bloc/creative_island_bloc.dart ================================================ import 'package:askaide/bloc/bloc_manager.dart'; import 'package:askaide/repo/api/creative.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/creative_island_repo.dart'; import 'package:flutter/material.dart'; part 'creative_island_event.dart'; part 'creative_island_state.dart'; class CreativeIslandBloc extends BlocExt { final CreativeIslandRepository creativeIslandRepo; CreativeIslandBloc(this.creativeIslandRepo) : super(CreativeIslandInitial()) { // on((event, emit) async { // await creativeIslandRepo.create( // event.itemId, // arguments: jsonEncode(event.arguments), // prompt: event.prompt, // answer: event.answer, // userId: APIServer().localUserID(), // ); // emit(CreativeIslandSaved()); // }); on((event, emit) async { final items = await APIServer().creativeIslandItemsV2(cache: !event.forceRefresh); emit(CreativeIslandItemsV2Loaded( items: items, )); }); on((event, emit) async { final resp = await APIServer().creativeIslandItem(event.itemId); emit(CreativeIslandItemLoaded(resp)); }); on((event, emit) async { emit(CreativeIslandHistoriesLoading()); final items = await APIServer() .creativeHistories(cache: !event.forceRefresh, mode: event.mode); emit(CreativeIslandHistoriesAllLoaded(items.data)); }); on((event, emit) async { emit(CreativeIslandHistoriesLoading()); final items = await APIServer().creativeUserGallery( cache: false, mode: event.mode, model: event.model); emit(CreativeIslandGalleryLoaded(items)); }); on((event, emit) async { emit(CreativeIslandHistoriesLoading()); final island = await APIServer().creativeIslandItem(event.itemId); emit(CreativeIslandHistoriesLoaded( island, await APIServer().creativeItemHistories( island.id, cache: !event.forceRefresh, ), )); }); on((event, emit) async { emit(CreativeIslandHistoriesLoading()); await APIServer() .deleteCreativeHistoryItem(event.itemId, hisId: event.id); if (event.source == 'all-histories') { final res = await APIServer().creativeHistories(cache: false, mode: event.mode); emit(CreativeIslandHistoriesAllLoaded(res.data)); } else { final island = await APIServer().creativeIslandItem(event.itemId); emit(CreativeIslandHistoriesLoaded( island, await APIServer().creativeItemHistories( island.id, cache: false, ), )); } }); on((event, emit) async { emit(CreativeIslandInitial()); try { final items = await APIServer().creativeIslandItems(mode: event.mode); emit(CreativeIslandListLoaded( items.items, categories: items.categories, backgroundImage: items.backgroundImage, )); } catch (e) { emit( CreativeIslandListLoaded(const [], error: e, categories: const [])); } }); on((event, emit) async { emit(CreativeIslandHistoryItemLoading()); emit(CreativeIslandHistoryItemLoaded( item: await APIServer().creativeHistoryItem( hisId: event.itemId, cache: !event.forceRefresh, ), )); }); } } ================================================ FILE: lib/bloc/creative_island_event.dart ================================================ part of 'creative_island_bloc.dart'; @immutable abstract class CreativeIslandEvent {} // class CreativeIslandSaveEvent extends CreativeIslandEvent { // final String itemId; // final Map arguments; // final String prompt; // final String answer; // CreativeIslandSaveEvent( // this.itemId, { // this.arguments = const {}, // this.prompt = '', // this.answer = '', // }); // } class CreativeIslandItemLoadEvent extends CreativeIslandEvent { final String itemId; CreativeIslandItemLoadEvent(this.itemId); } class CreativeIslandHistoriesAllLoadEvent extends CreativeIslandEvent { final bool forceRefresh; final String mode; CreativeIslandHistoriesAllLoadEvent( {this.forceRefresh = false, required this.mode}); } class CreativeIslandGalleryLoadEvent extends CreativeIslandEvent { final bool forceRefresh; final String mode; final String? model; CreativeIslandGalleryLoadEvent({ this.forceRefresh = false, required this.mode, this.model, }); } class CreativeIslandHistoriesLoadEvent extends CreativeIslandEvent { final String itemId; final bool forceRefresh; CreativeIslandHistoriesLoadEvent(this.itemId, {this.forceRefresh = false}); } class CreativeIslandDeleteEvent extends CreativeIslandEvent { final String itemId; final int id; final String source; final String mode; CreativeIslandDeleteEvent(this.itemId, this.id, {this.source = '', required this.mode}); } class CreativeIslandListLoadEvent extends CreativeIslandEvent { final String mode; CreativeIslandListLoadEvent({required this.mode}); } class CreativeIslandHistoryItemLoadEvent extends CreativeIslandEvent { final int itemId; final bool forceRefresh; CreativeIslandHistoryItemLoadEvent(this.itemId, {this.forceRefresh = false}); } class CreativeIslandItemsV2LoadEvent extends CreativeIslandEvent { final bool forceRefresh; CreativeIslandItemsV2LoadEvent({this.forceRefresh = false}); } ================================================ FILE: lib/bloc/creative_island_state.dart ================================================ part of 'creative_island_bloc.dart'; @immutable abstract class CreativeIslandState {} class CreativeIslandInitial extends CreativeIslandState {} // class CreativeIslandSaved extends CreativeIslandState { // final String? _error; // CreativeIslandSaved({String? error}) : _error = error; // get error => _error; // } class CreativeIslandItemLoaded extends CreativeIslandState { final String? _error; final CreativeIslandItem item; CreativeIslandItemLoaded(this.item, {String? error}) : _error = error; get error => _error; } class CreativeIslandHistoriesLoading extends CreativeIslandInitial {} class CreativeIslandHistoriesLoaded extends CreativeIslandState { final String? _error; final CreativeIslandItem island; final List histories; CreativeIslandHistoriesLoaded(this.island, this.histories, {String? error}) : _error = error; get error => _error; } class CreativeIslandGalleryLoaded extends CreativeIslandState { final String? _error; final List items; CreativeIslandGalleryLoaded(this.items, {String? error}) : _error = error; get error => _error; } class CreativeIslandHistoriesAllLoaded extends CreativeIslandState { final String? _error; final List histories; CreativeIslandHistoriesAllLoaded(this.histories, {String? error}) : _error = error; get error => _error; } class CreativeIslandListLoaded extends CreativeIslandState { final Object? _error; final List items; final List categories; final String? backgroundImage; CreativeIslandListLoaded( this.items, { Object? error, required this.categories, this.backgroundImage, }) : _error = error; get error => _error; } class CreativeIslandHistoryItemLoading extends CreativeIslandState {} class CreativeIslandHistoryItemLoaded extends CreativeIslandState { final Object? error; final CreativeItemInServer? item; CreativeIslandHistoryItemLoaded({this.item, this.error}); } class CreativeIslandItemsV2Loaded extends CreativeIslandState { final Object? error; final List items; CreativeIslandItemsV2Loaded({required this.items, this.error}); } ================================================ FILE: lib/bloc/free_count_bloc.dart ================================================ import 'package:askaide/helper/ability.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/model/misc.dart'; import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; part 'free_count_event.dart'; part 'free_count_state.dart'; class FreeCountBloc extends Bloc { List counts = []; FreeCountBloc() : super(FreeCountInitial()) { // 重新加载所有的模型免费使用次数 on((event, emit) async { if (!Ability().isUserLogon()) { emit(FreeCountLoadedState( counts: await APIServer().freeChatCounts(), needSignin: event.checkSigninStatus, )); return; } counts = await APIServer().userFreeStatistics(); emit(FreeCountLoadedState(counts: counts)); }); // 重新加载指定模型的免费使用次数 on((event, emit) async { if (Ability().usingLocalOpenAIModel(event.model) || !Ability().isUserLogon()) { emit(FreeCountLoadedState(counts: counts)); return; } final freeCount = await APIServer().userFreeStatisticsForModel( model: event.model.startsWith('v2@') ? event.model : event.model.split(':').last, ); if (freeCount.maxCount > 0) { var matched = false; for (var i = 0; i < counts.length; i++) { if (counts[i].model == freeCount.model) { counts[i] = freeCount; matched = true; break; } } if (!matched) { counts.add(freeCount); } } emit(FreeCountLoadedState(counts: counts)); }); } } ================================================ FILE: lib/bloc/free_count_event.dart ================================================ part of 'free_count_bloc.dart'; @immutable sealed class FreeCountEvent {} class FreeCountReloadEvent extends FreeCountEvent { final String model; FreeCountReloadEvent({required this.model}); } class FreeCountReloadAllEvent extends FreeCountEvent { final bool checkSigninStatus; FreeCountReloadAllEvent({this.checkSigninStatus = false}); } ================================================ FILE: lib/bloc/free_count_state.dart ================================================ part of 'free_count_bloc.dart'; @immutable sealed class FreeCountState {} final class FreeCountInitial extends FreeCountState {} class FreeCountLoadedState extends FreeCountState { final List counts; final bool needSignin; FreeModelCount? model(String model) { model = model.split(':').last; for (var i = 0; i < counts.length; i++) { if (counts[i].model == model) { return counts[i]; } } return null; } FreeCountLoadedState({required this.counts, this.needSignin = false}); } ================================================ FILE: lib/bloc/gallery_bloc.dart ================================================ import 'package:askaide/repo/api/creative.dart'; import 'package:askaide/repo/api/page.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; part 'gallery_event.dart'; part 'gallery_state.dart'; class GalleryBloc extends Bloc { GalleryBloc() : super(GalleryInitial()) { on((event, emit) async { emit(GalleryInitial()); final res = await APIServer().creativeGallery( cache: !event.forceRefresh, page: event.page, perPage: 20, ); emit(GalleryLoaded(data: res)); }); on((event, emit) async { emit(GalleryInitial()); final res = await APIServer().creativeGalleryItem( cache: !event.forceRefresh, id: event.id, ); emit(GalleryItemLoaded( item: res.item, isInternalUser: res.isInternalUser, )); }); } } ================================================ FILE: lib/bloc/gallery_event.dart ================================================ part of 'gallery_bloc.dart'; @immutable abstract class GalleryEvent {} class GalleryLoadEvent extends GalleryEvent { final bool forceRefresh; final int page; GalleryLoadEvent({this.forceRefresh = false, this.page = 1}); } class GalleryItemLoadEvent extends GalleryEvent { final int id; final bool forceRefresh; GalleryItemLoadEvent({required this.id, this.forceRefresh = false}); } ================================================ FILE: lib/bloc/gallery_state.dart ================================================ part of 'gallery_bloc.dart'; @immutable abstract class GalleryState {} class GalleryInitial extends GalleryState {} class GalleryLoaded extends GalleryState { final PagedData data; GalleryLoaded({required this.data}); } class GalleryItemLoaded extends GalleryState { final CreativeGallery item; final bool isInternalUser; GalleryItemLoaded({required this.item, this.isInternalUser = false}); } ================================================ FILE: lib/bloc/group_chat_bloc.dart ================================================ import 'dart:convert'; import 'package:askaide/helper/cache.dart'; import 'package:askaide/helper/logger.dart'; import 'package:askaide/page/component/chat/message_state_manager.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/model/group.dart'; import 'package:askaide/repo/model/message.dart'; import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; part 'group_chat_event.dart'; part 'group_chat_state.dart'; class GroupChatBloc extends Bloc { var messages = []; final MessageStateManager stateManager; GroupChatBloc({required this.stateManager}) : super(GroupChatInitial()) { // 加载聊天组 on((event, emit) async { final group = await APIServer().chatGroup(event.groupId, cache: !event.forceUpdate); final states = await stateManager.loadRoomStates(event.groupId); final defaultChatMembers = await loadDefaultChatMembers(event.groupId); emit(GroupChatLoaded( group: group, states: states, defaultChatMembers: defaultChatMembers.isEmpty ? group.members.map((e) => e.id!).toList() : defaultChatMembers, )); }); // 加载聊天组聊天记录 on((event, emit) async { if (event.isInitRequest) { try { final cached = await Cache().stringGet(key: 'group:speed:${event.groupId}'); if (cached != null) { final messages = (jsonDecode(cached) as List) .map((e) => GroupMessage.fromJson(e)) .toList(); emit(GroupChatMessagesLoaded(messages: messages)); } } catch (e) { Logger.instance.e(e); } } await refreshGroupMessages( event.groupId, startId: event.startId, forceRefresh: true, ); emit(GroupChatMessagesLoaded(messages: messages)); }); // 发送聊天组消息 on((event, emit) async { try { final resp = await APIServer().chatGroupSendMessage( event.groupId, GroupChatSendRequest( message: event.message, memberIds: event.members, ), ); // 记录默认聊天成员 updateDefaultChatMembers( event.groupId, resp.tasks.map((e) => e.memberId).toList(), ).then((members) { emit(GroupDefaultMemberSelected(members)); }); await refreshGroupMessages( event.groupId, startId: 0, forceRefresh: true, ); emit(GroupChatMessagesLoaded(messages: messages)); } catch (e) { await refreshGroupMessages( event.groupId, startId: 0, forceRefresh: true, ); emit(GroupChatMessagesLoaded(messages: messages, error: e)); } }); // 发送系统消息 on((event, emit) async { try { final resp = await APIServer().chatGroupSendSystemMessage( event.groupId, messageType: event.type.getTypeText(), message: event.message, ); Logger.instance.d(resp.toJson()); } finally { await refreshGroupMessages( event.groupId, startId: 0, forceRefresh: true, ); emit(GroupChatMessagesLoaded(messages: messages)); } }); // 更新聊天组消息状态 on((event, emit) async { final waitMessageIds = messages .where((msg) => msg.status == groupMessageStatusWaiting) .map((msg) => msg.id) .toList(); if (waitMessageIds.isEmpty) { return; } final resp = await APIServer() .chatGroupMessageStatus(event.groupId, waitMessageIds); final newMessageStatusMap = {}; for (var msg in resp) { newMessageStatusMap[msg.id] = msg; } for (var i = 0; i < messages.length; i++) { final msg = messages[i]; if (newMessageStatusMap.containsKey(msg.id)) { messages[i] = newMessageStatusMap[msg.id]!; } } emit(GroupChatMessagesLoaded(messages: messages)); }); // 清空聊天组消息 on((event, emit) async { await APIServer().chatGroupDeleteAllMessages(event.groupId); messages.clear(); emit(GroupChatMessagesLoaded(messages: messages)); }); // 删除聊天组消息 on((event, emit) async { await APIServer().chatGroupDeleteMessage(event.groupId, event.messageId); messages.removeWhere((msg) => msg.id == event.messageId); emit(GroupChatMessagesLoaded(messages: messages)); }); } refreshGroupMessages( int groupId, { int startId = 0, bool forceRefresh = false, }) async { final data = await APIServer() .chatGroupMessages(groupId, startId: startId, cache: !forceRefresh); messages = data.data.reversed.toList(); if (startId == 0) { Cache() .setString(key: 'group:speed:$groupId', value: jsonEncode(messages)); } } Future> loadDefaultChatMembers(int groupId) async { final defaultMembers = await Cache().stringGet(key: 'group:$groupId:default-members'); return (defaultMembers ?? '') .split(',') .map((e) => int.tryParse(e) ?? 0) .where((e) => e > 0) .toList(); } Future> updateDefaultChatMembers( int groupId, List members) async { // 记录默认聊天成员 await Cache().setString( key: 'group:$groupId:default-members', value: members.join(','), duration: const Duration(days: 365), ); return members; } } ================================================ FILE: lib/bloc/group_chat_event.dart ================================================ part of 'group_chat_bloc.dart'; @immutable sealed class GroupChatEvent {} class GroupChatLoadEvent extends GroupChatEvent { final int groupId; final bool forceUpdate; GroupChatLoadEvent(this.groupId, {this.forceUpdate = false}); } class GroupChatMessagesLoadEvent extends GroupChatEvent { final int groupId; final int startId; final bool isInitRequest; GroupChatMessagesLoadEvent( this.groupId, { this.startId = 0, this.isInitRequest = false, }); } class GroupChatSendEvent extends GroupChatEvent { final int groupId; final String message; final List members; final int? index; final bool isResent; GroupChatSendEvent(this.groupId, this.message, this.members, {this.index, this.isResent = false}); } class GroupChatUpdateMessageStatusEvent extends GroupChatEvent { final int groupId; GroupChatUpdateMessageStatusEvent(this.groupId); } class GroupChatSendSystemEvent extends GroupChatEvent { final int groupId; final String? message; final MessageType type; GroupChatSendSystemEvent(this.groupId, this.type, {this.message}); } class GroupChatDeleteAllEvent extends GroupChatEvent { final int groupId; GroupChatDeleteAllEvent(this.groupId); } class GroupChatDeleteEvent extends GroupChatEvent { final int groupId; final int messageId; GroupChatDeleteEvent(this.groupId, this.messageId); } ================================================ FILE: lib/bloc/group_chat_state.dart ================================================ part of 'group_chat_bloc.dart'; @immutable sealed class GroupChatState {} final class GroupChatInitial extends GroupChatState {} class GroupChatLoaded extends GroupChatState { final ChatGroup group; final Map states; final List? defaultChatMembers; GroupChatLoaded({ required this.group, required this.states, this.defaultChatMembers, }); } class GroupDefaultMemberSelected extends GroupChatState { final List members; GroupDefaultMemberSelected(this.members); } class GroupChatMessagesLoaded extends GroupChatState { final List messages; final Object? _error; get error => _error; bool get hasWaitTasks => messages.any((element) => element.status == groupMessageStatusWaiting); GroupChatMessagesLoaded({ required this.messages, Object? error, }) : _error = error; } ================================================ FILE: lib/bloc/model_bloc.dart ================================================ import 'package:askaide/repo/api/admin/models.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; part 'model_event.dart'; part 'model_state.dart'; class ModelBloc extends Bloc { ModelBloc() : super(ModelInitial()) { /// 加载所有模型 on((event, emit) async { final channels = await APIServer().adminModels(); emit(ModelsLoaded(channels)); }); /// 加载单个模型 on((event, emit) async { final channel = await APIServer().adminModel(modelId: event.modelId); emit(ModelLoaded(channel)); }); /// 创建模型 on((event, emit) async { try { await APIServer().adminCreateModel(event.req); emit(ModelOperationResult(true, 'Creation successful')); } catch (e) { emit(ModelOperationResult(false, e.toString())); } }); /// 更新模型 on((event, emit) async { try { await APIServer().adminUpdateModel( modelId: event.modelId, req: event.req, ); emit(ModelOperationResult(true, 'Update successful')); } catch (e) { emit(ModelOperationResult(false, e.toString())); } }); /// 删除模型 on((event, emit) async { try { await APIServer().adminDeleteModel(modelId: event.modelId); emit(ModelOperationResult(true, 'Delete successful')); } catch (e) { emit(ModelOperationResult(false, e.toString())); } }); } } ================================================ FILE: lib/bloc/model_event.dart ================================================ part of 'model_bloc.dart'; @immutable sealed class ModelEvent {} class ModelsLoadEvent extends ModelEvent {} class ModelLoadEvent extends ModelEvent { final String modelId; ModelLoadEvent(this.modelId); } class ModelCreateEvent extends ModelEvent { final AdminModelAddReq req; ModelCreateEvent(this.req); } class ModelUpdateEvent extends ModelEvent { final String modelId; final AdminModelUpdateReq req; ModelUpdateEvent(this.modelId, this.req); } class ModelDeleteEvent extends ModelEvent { final String modelId; ModelDeleteEvent(this.modelId); } ================================================ FILE: lib/bloc/model_state.dart ================================================ part of 'model_bloc.dart'; @immutable sealed class ModelState {} final class ModelInitial extends ModelState {} class ModelsLoaded extends ModelState { final List models; ModelsLoaded(this.models); } class ModelLoaded extends ModelState { final AdminModel model; ModelLoaded(this.model); } class ModelOperationResult extends ModelState { final bool success; final String message; ModelOperationResult(this.success, this.message); } ================================================ FILE: lib/bloc/notify_bloc.dart ================================================ import 'package:askaide/bloc/bloc_manager.dart'; import 'package:flutter/material.dart'; part 'notify_event.dart'; part 'notify_state.dart'; class NotifyBloc extends BlocExt { NotifyBloc() : super(NotifyInitial()) { on((event, emit) { emit(NotifyFired(event.title, event.body, event.type)); }); on((event, emit) { emit(NotifyInitial()); }); } } ================================================ FILE: lib/bloc/notify_event.dart ================================================ part of 'notify_bloc.dart'; @immutable abstract class NotifyEvent {} class NotifyFiredEvent extends NotifyEvent { final String title; final String body; final String type; NotifyFiredEvent(this.title, this.body, this.type); } class NotifyResetEvent extends NotifyEvent {} ================================================ FILE: lib/bloc/notify_state.dart ================================================ part of 'notify_bloc.dart'; @immutable abstract class NotifyState {} class NotifyInitial extends NotifyState {} class NotifyFired extends NotifyState { final String title; final String body; final String type; NotifyFired(this.title, this.body, this.type); } ================================================ FILE: lib/bloc/payment_bloc.dart ================================================ import 'package:askaide/helper/platform.dart'; import 'package:askaide/repo/api/payment.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:bloc/bloc.dart'; import 'package:in_app_purchase/in_app_purchase.dart'; import 'package:meta/meta.dart'; part 'payment_event.dart'; part 'payment_state.dart'; class PaymentBloc extends Bloc { PaymentBloc() : super(PaymentInitial()) { on((event, emit) async { if (PlatformTool.isIOS()) { final products = await APIServer().paymentProducts(); if (products.consume.isEmpty) { emit(PaymentAppleProductsLoaded( const [], note: products.note, error: '没有任何可购买的项目', localProducts: const [], loading: false, )); return; } emit(PaymentAppleProductsLoaded( products.consume .map( (e) => ProductDetails( id: e.id, title: e.name, description: '', price: '-', rawPrice: 0, currencyCode: '', ), ) .toList(), note: products.note, localProducts: products.consume, loading: true)); final productIds = products.consume.map((e) => e.id).toSet(); final response = await InAppPurchase.instance.queryProductDetails(productIds); if (response.notFoundIDs.isNotEmpty) { emit(PaymentAppleProductsLoaded( const [], note: products.note, localProducts: products.consume, error: '没有任何可购买的项目', loading: false, )); return; } final remoteProducts = []; for (var id in productIds) { remoteProducts.add( response.productDetails.firstWhere((element) => element.id == id), ); } emit(PaymentAppleProductsLoaded( remoteProducts, note: products.note, localProducts: products.consume, loading: false, )); } else { final products = await APIServer().paymentProducts(); if (products.consume.isEmpty) { emit(PaymentAppleProductsLoaded( const [], note: products.note, error: '没有任何可购买的项目', localProducts: const [], loading: false, )); return; } emit( PaymentAppleProductsLoaded( products.consume .map( (e) => ProductDetails( id: e.id, title: e.name, description: '', price: products.preferUSD ? e.retailPriceUSDText : e.retailPriceText, rawPrice: e.retailPrice.toDouble(), currencyCode: '', ), ) .toList(), note: products.note, localProducts: products.consume, loading: false, preferUSD: products.preferUSD, ), ); } }); } } ================================================ FILE: lib/bloc/payment_event.dart ================================================ part of 'payment_bloc.dart'; @immutable abstract class PaymentEvent {} class PaymentLoadAppleProducts extends PaymentEvent {} ================================================ FILE: lib/bloc/payment_state.dart ================================================ part of 'payment_bloc.dart'; @immutable abstract class PaymentState {} class PaymentInitial extends PaymentState {} class PaymentAppleProductsLoaded extends PaymentState { final List products; final List localProducts; final Object? error; final bool loading; final String? note; final bool preferUSD; PaymentAppleProductsLoaded( this.products, { this.note, required this.localProducts, this.error, required this.loading, this.preferUSD = false, }); } ================================================ FILE: lib/bloc/room_bloc.dart ================================================ import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/page/component/chat/message_state_manager.dart'; import 'package:askaide/repo/api/room_gallery.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/model/group.dart'; import 'package:askaide/repo/model/misc.dart'; import 'package:askaide/repo/model/room.dart'; import 'package:askaide/bloc/bloc_manager.dart'; import 'package:askaide/repo/chat_message_repo.dart'; import 'package:flutter/material.dart'; part 'room_event.dart'; part 'room_state.dart'; class RoomBloc extends BlocExt { final ChatMessageRepository chatMsgRepo; final MessageStateManager stateManager; Future fixRoomId(int? chatHistoryId) async { if (chatHistoryId != null && chatHistoryId > 0) { final his = await chatMsgRepo.getChatHistory(chatHistoryId); if (his != null) { return his.roomId; } } return null; } /// 加载房间信息,如果房间不存在,则加载默认房间 Future loadRoom(int roomId) async { try { final room = await APIServer().room(roomId: roomId); return room; } catch (e) { return await APIServer().room(roomId: chatAnywhereRoomId); } } RoomBloc({ required this.chatMsgRepo, required this.stateManager, }) : super(RoomInitial()) { // 加载指定聊天室信息 on((event, emit) async { try { // 加快首屏加载速度,避免加载中状态 emit(RoomLoaded( Room( '', 'chat', id: event.roomId, ), const {}, cascading: false, )); final roomId = await fixRoomId(event.chatHistoryId) ?? event.roomId; if (Ability().isUserLogon()) { final room = await loadRoom(roomId); if (event.chatHistoryId != null && event.chatHistoryId! > 0) { final chatHistory = await chatMsgRepo.getChatHistory(event.chatHistoryId!); if (chatHistory != null && chatHistory.model != null) { room.model = chatHistory.model!; } } emit(RoomLoaded( Room( room.name, 'chat', description: room.description, id: room.id, userId: room.userId, createdAt: room.createdAt, lastActiveTime: room.lastActiveTime, systemPrompt: room.systemPrompt, priority: room.priority ?? 0, model: room.model.startsWith('v2@') ? room.model : '${room.vendor}:${room.model}', initMessage: room.initMessage, maxContext: room.maxContext, avatarId: room.avatarId, avatarUrl: room.avatarUrl, roomType: room.roomType, ), const {}, cascading: false, )); final states = await stateManager.loadRoomStates(roomId); emit(RoomLoaded( Room( room.name, 'chat', description: room.description, id: room.id, userId: room.userId, createdAt: room.createdAt, lastActiveTime: room.lastActiveTime, systemPrompt: room.systemPrompt, priority: room.priority ?? 0, model: room.model.startsWith('v2@') ? room.model : '${room.vendor}:${room.model}', initMessage: room.initMessage, maxContext: room.maxContext, avatarId: room.avatarId, avatarUrl: room.avatarUrl, roomType: room.roomType, ), states, examples: await APIServer().example( room.model.startsWith('v2@') ? room.model : '${room.vendor}:${room.model}', ), cascading: event.cascading, )); return; } final room = await chatMsgRepo.room(roomId); if (room != null) { final states = await stateManager.loadRoomStates(roomId); emit(RoomLoaded( room, states, examples: await APIServer().example(room.model), cascading: event.cascading, )); } } catch (e) { emit(RoomLoaded( Room('-', '-'), const {}, error: e, cascading: event.cascading, )); } }); // 加载聊天室列表 on((event, emit) async { if (!event.forceRefresh) { emit(RoomsLoading()); } emit(await createRoomsLoadedState(cache: !event.forceRefresh)); }); // 创建聊天室 on((event, emit) async { emit(RoomsLoading()); try { if (Ability().isUserLogon()) { String? model; String? vendor; if (event.model != null) { final segs = event.model!.split(':'); model = event.model!.startsWith('v2@') ? event.model! : (segs.length > 1 ? segs.last : event.model); vendor = event.model!.startsWith('v2@') ? '' : (segs.length > 1 ? segs.first : ''); } await APIServer().createRoom( name: event.name, vendor: vendor, model: model, systemPrompt: event.prompt, avatarId: event.avatarId, avatarUrl: event.avatarUrl, maxContext: event.maxContext, initMessage: event.initMessage, ); } else { await chatMsgRepo.createRoom( name: event.name, category: 'chat', model: event.model ?? 'gpt-4o', systemPrompt: event.prompt, userId: APIServer().localUserID(), maxContext: event.maxContext, ); } emit(RoomOperationResult(true)); emit(await createRoomsLoadedState(cache: false)); } catch (e) { emit(RoomOperationResult(false, error: e.toString())); // emit(RoomsLoaded(const [], error: e.toString())); } }); // 删除聊天室 on((event, emit) async { emit(RoomsLoading()); try { if (Ability().isUserLogon()) { await APIServer().deleteRoom(roomId: event.roomId); } else { var room = await chatMsgRepo.room(event.roomId); if (room == null || room.category == 'system') { return; } await chatMsgRepo.deleteRoom(event.roomId); } emit(await createRoomsLoadedState(cache: false)); } catch (e) { emit(RoomsLoaded(const [], error: e.toString())); } }); // 更新聊天室信息 on((event, emit) async { try { if (Ability().isUserLogon()) { final room = await APIServer().updateRoom( roomId: event.roomId, name: event.name!, model: event.model != null ? (event.model!.startsWith('v2@') ? event.model! : event.model!.split(':').last) : null, vendor: event.model != null ? (event.model!.startsWith('v2@') ? '' : event.model!.split(':').first) : null, systemPrompt: event.prompt!, avatarId: event.avatarId, avatarUrl: event.avatarUrl, maxContext: event.maxContext, initMessage: event.initMessage, ); final states = await stateManager.loadRoomStates(event.roomId); emit( RoomLoaded( Room( room.name, 'chat', description: room.description, id: room.id, userId: room.userId, createdAt: room.createdAt, lastActiveTime: room.lastActiveTime, systemPrompt: room.systemPrompt, priority: room.priority ?? 0, model: room.model.startsWith('v2@') ? room.model : '${room.vendor}:${room.model}', avatarId: room.avatarId, avatarUrl: room.avatarUrl, initMessage: room.initMessage, roomType: room.roomType, ), states, examples: await APIServer().example(room.model), cascading: false, ), ); } else { final room = await chatMsgRepo.room(event.roomId); if (room != null) { if (event.name != null && event.name != '') { room.name = event.name!; } if (event.model != null && event.model != '') { room.model = event.model!; } if (event.prompt != null && event.prompt != '') { room.systemPrompt = event.prompt!; } if (event.maxContext != null) { room.maxContext = event.maxContext!; } await chatMsgRepo.updateRoom(room); final states = await stateManager.loadRoomStates(event.roomId); emit(RoomLoaded( room, states, examples: await APIServer().examples(), cascading: false, )); } } } catch (e) { emit(RoomOperationResult(false, error: e.toString())); } }); on((event, emit) async { if (event.ids.isEmpty) { return; } try { final ids = await APIServer().copyRoomGallery(ids: event.ids); emit(await createRoomsLoadedState(cache: false)); if (ids.isNotEmpty) { emit(RoomOperationResult(true, redirect: '/room/${ids.first}/chat')); } } catch (e) { emit(RoomCreateError(e)); } }); on((event, emit) async { try { final resp = await APIServer().roomGalleries(); emit(RoomGalleriesLoaded(resp.galleries, tags: resp.tags)); } catch (e) { emit(RoomGalleriesLoaded(const [], error: e)); } }); // 创建群聊聊天室 on((event, emit) async { emit(RoomsLoading()); try { await APIServer().createGroupRoom( name: event.name, avatarUrl: event.avatarUrl, members: event.members, ); emit(GroupRoomUpdateResultState(true)); emit(await createRoomsLoadedState(cache: false)); } catch (e) { emit(GroupRoomUpdateResultState(false, error: e)); emit(RoomsLoaded(const [], error: e.toString())); } }); // 群聊聊天室更新 on((event, emit) async { emit(RoomsLoading()); try { await APIServer().updateGroupRoom( groupId: event.groupId, name: event.name, avatarUrl: event.avatarUrl, members: event.members, ); emit(GroupRoomUpdateResultState(true)); } catch (e) { emit(GroupRoomUpdateResultState(false, error: e)); } }); on((event, emit) async { try { final rooms = await APIServer().recentRooms(); emit(RoomsRecentLoaded(rooms)); } catch (e) { emit(RoomsRecentLoaded(const [], error: e)); } }); } Future createRoomsLoadedState({bool cache = true}) async { try { if (Ability().isUserLogon()) { final resp = await APIServer().rooms(cache: cache); return RoomsLoaded( resp.rooms .map((room) => Room( room.name, 'chat', description: room.description, id: room.id, userId: room.userId, createdAt: room.createdAt, lastActiveTime: room.lastActiveTime, systemPrompt: room.systemPrompt, priority: room.priority ?? 0, model: room.model.startsWith('v2@') ? room.model : '${room.vendor}:${room.model}', avatarId: room.avatarId, avatarUrl: room.avatarUrl, roomType: room.roomType, members: room.members, )) .toList(), suggests: resp.suggests ?? [], ); } else { final rooms = await chatMsgRepo.rooms( userId: APIServer().localUserID(), ); rooms.removeWhere((element) => element.id == chatAnywhereRoomId && element.category == 'system'); return RoomsLoaded(rooms); } } catch (e) { return RoomsLoaded(const [], error: e); } } } ================================================ FILE: lib/bloc/room_event.dart ================================================ part of 'room_bloc.dart'; @immutable abstract class RoomEvent {} class RoomsLoadEvent extends RoomEvent { final bool forceRefresh; RoomsLoadEvent({this.forceRefresh = false}); } class RoomsRecentLoadEvent extends RoomEvent { RoomsRecentLoadEvent(); } class RoomCreateEvent extends RoomEvent { final String name; final String? model; final String? prompt; final int? avatarId; final String? avatarUrl; final int? maxContext; final String? initMessage; RoomCreateEvent( this.name, this.prompt, { this.model, this.avatarId, this.avatarUrl, this.maxContext, this.initMessage, }); } class GroupRoomCreateEvent extends RoomEvent { final String name; final String? avatarUrl; final List? members; GroupRoomCreateEvent({ required this.name, this.avatarUrl, this.members, }); } class GroupRoomUpdateEvent extends RoomEvent { final int groupId; final String name; final String? avatarUrl; final List? members; GroupRoomUpdateEvent({ required this.groupId, required this.name, this.avatarUrl, this.members, }); } class RoomDeleteEvent extends RoomEvent { final int roomId; RoomDeleteEvent(this.roomId); } class RoomLoadEvent extends RoomEvent { final int roomId; final int? chatHistoryId; final bool cascading; RoomLoadEvent(this.roomId, {this.chatHistoryId, required this.cascading}); } class RoomUpdateEvent extends RoomEvent { final int roomId; final String? name; final String? model; final String? prompt; final int? avatarId; final String? avatarUrl; final int? maxContext; final String? initMessage; RoomUpdateEvent( this.roomId, { this.name, this.model, this.prompt, this.avatarId, this.avatarUrl, this.maxContext, this.initMessage, }); } class GalleryRoomCopyEvent extends RoomEvent { final List ids; GalleryRoomCopyEvent(this.ids); } class RoomGalleriesLoadEvent extends RoomEvent { RoomGalleriesLoadEvent(); } ================================================ FILE: lib/bloc/room_state.dart ================================================ part of 'room_bloc.dart'; @immutable abstract class RoomState {} class RoomInitial extends RoomState {} class RoomsLoading extends RoomState {} class RoomsLoaded extends RoomState { final List rooms; final List suggests; final Object? error; RoomsLoaded(this.rooms, {this.error, this.suggests = const []}); } class RoomsRecentLoaded extends RoomState { final List rooms; final Object? error; RoomsRecentLoaded(this.rooms, {this.error}); } class RoomLoaded extends RoomState { final Room room; final List? examples; final Object? error; final Map states; final bool cascading; RoomLoaded( this.room, this.states, { this.error, this.examples, required this.cascading, }) { if (examples != null) { examples!.shuffle(); } } } class RoomCreateError extends RoomState { final Object error; RoomCreateError(this.error); } class RoomGalleriesLoaded extends RoomState { final List galleries; final List tags; final Object? error; RoomGalleriesLoaded(this.galleries, {this.error, this.tags = const []}); } class GroupRoomUpdateResultState extends RoomState { final bool success; final Object? error; GroupRoomUpdateResultState(this.success, {this.error}); } class RoomOperationResult extends RoomState { final bool success; final Object? error; final String? redirect; RoomOperationResult(this.success, {this.error, this.redirect}); } ================================================ FILE: lib/bloc/user_api_keys_bloc.dart ================================================ import 'package:askaide/repo/api/keys.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; part 'user_api_keys_event.dart'; part 'user_api_keys_state.dart'; class UserApiKeysBloc extends Bloc { UserApiKeysBloc() : super(UserApiKeysInitial()) { // 加载用户 API Key 列表 on((event, emit) async { final keys = await APIServer().userAPIKeys(); emit(UserApiKeysLoaded(keys: keys)); }); // 加载用户 API Key on((event, emit) async { final key = await APIServer().userAPIKeyDetail(id: event.id); emit(UserApiKeyLoaded(key: key)); }); // 创建用户 API Key on((event, emit) async { final key = await APIServer().createAPIKey(name: event.name); emit(UserApiKeyCreated(key: key)); final keys = await APIServer().userAPIKeys(); emit(UserApiKeysLoaded(keys: keys)); }); // 删除用户 API Key on((event, emit) async { await APIServer().deleteAPIKey(id: event.id); final keys = await APIServer().userAPIKeys(); emit(UserApiKeysLoaded(keys: keys)); }); } } ================================================ FILE: lib/bloc/user_api_keys_event.dart ================================================ part of 'user_api_keys_bloc.dart'; @immutable sealed class UserApiKeysEvent {} class UserApiKeysLoad extends UserApiKeysEvent {} class UserApiKeyLoad extends UserApiKeysEvent { final int id; UserApiKeyLoad(this.id); } class UserApiKeyCreate extends UserApiKeysEvent { final String name; UserApiKeyCreate(this.name); } class UserApiKeyDelete extends UserApiKeysEvent { final int id; UserApiKeyDelete(this.id); } ================================================ FILE: lib/bloc/user_api_keys_state.dart ================================================ part of 'user_api_keys_bloc.dart'; @immutable sealed class UserApiKeysState {} final class UserApiKeysInitial extends UserApiKeysState {} class UserApiKeysLoaded extends UserApiKeysState { final List keys; UserApiKeysLoaded({required this.keys}); } class UserApiKeyLoaded extends UserApiKeysState { final UserAPIKey key; UserApiKeyLoaded({required this.key}); } class UserApiKeyCreated extends UserApiKeysState { final String key; UserApiKeyCreated({required this.key}); } ================================================ FILE: lib/bloc/user_bloc.dart ================================================ import 'package:askaide/repo/api/admin/users.dart'; import 'package:askaide/repo/api/page.dart'; import 'package:askaide/repo/api/quota.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; part 'user_event.dart'; part 'user_state.dart'; class UserBloc extends Bloc { UserBloc() : super(UserInitial()) { // 加载指定用户信息 on((event, emit) async { final user = await APIServer().adminUser(id: event.userId); emit(UserLoaded(user)); }); // 加载用户列表 on((event, emit) async { final users = await APIServer().adminUsers( page: event.page, perPage: event.perPage, keyword: event.keyword, ); emit(UsersLoaded(users)); }); // 加载用户配额 on((event, emit) async { final quota = await APIServer().adminUserQuota(userId: event.userId); emit(UserQuotaLoaded(quota)); }); } } ================================================ FILE: lib/bloc/user_event.dart ================================================ part of 'user_bloc.dart'; @immutable sealed class UserEvent {} class UserLoadEvent extends UserEvent { final int userId; UserLoadEvent(this.userId); } class UserListLoadEvent extends UserEvent { final int page; final int perPage; final String? keyword; UserListLoadEvent({ this.page = 1, this.perPage = 20, this.keyword, }); } class UserQuotaLoadEvent extends UserEvent { final int userId; UserQuotaLoadEvent(this.userId); } ================================================ FILE: lib/bloc/user_state.dart ================================================ part of 'user_bloc.dart'; @immutable sealed class UserState {} final class UserInitial extends UserState {} class UserLoaded extends UserState { final AdminUser user; UserLoaded(this.user); } class UserOperationResult extends UserState { final bool success; final String? message; UserOperationResult(this.success, {this.message}); } class UsersLoaded extends UserState { final PagedData users; UsersLoaded(this.users); } class UserQuotaLoaded extends UserState { final QuotaResp quota; UserQuotaLoaded(this.quota); } ================================================ FILE: lib/bloc/version_bloc.dart ================================================ import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/model/misc.dart'; import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; part 'version_event.dart'; part 'version_state.dart'; class VersionBloc extends Bloc { VersionBloc() : super(VersionInitial()) { on((event, emit) async { emit(VersionInitial()); final version = await APIServer().versionCheck(); emit(VersionCheckLoaded(version)); }); } } ================================================ FILE: lib/bloc/version_event.dart ================================================ part of 'version_bloc.dart'; @immutable abstract class VersionEvent {} class VersionCheckEvent extends VersionEvent {} ================================================ FILE: lib/bloc/version_state.dart ================================================ part of 'version_bloc.dart'; @immutable abstract class VersionState {} class VersionInitial extends VersionState {} class VersionCheckLoaded extends VersionState { final VersionCheckResp version; VersionCheckLoaded(this.version); } ================================================ FILE: lib/data/migrate.dart ================================================ import 'package:askaide/helper/constant.dart'; import 'package:sqflite/sqflite.dart'; /// 执行数据库迁移 Future migrate(db, oldVersion, newVersion) async { if (oldVersion <= 1) { await db.execute(''' ALTER TABLE chat_room ADD COLUMN color TEXT; UPDATE chat_room SET color = 'FF4CAF50' WHERE category = 'system'; '''); } if (oldVersion <= 2) { await db.execute('ALTER TABLE chat_message ADD COLUMN extra TEXT;'); await db.execute('ALTER TABLE chat_message ADD COLUMN model TEXT;'); } if (oldVersion < 5) { await db.execute(''' CREATE TABLE cache ( `key` TEXT NOT NULL PRIMARY KEY, `value` TEXT NOT NULL, `created_at` INTEGER, `valid_before` INTEGER ) '''); } if (oldVersion < 6) { await db.execute(''' CREATE TABLE creative_island_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, item_id TEXT NOT NULL, arguments TEXT NULL, prompt TEXT NULL, answer TEXT NULL, created_at INTEGER NOT NULL ) '''); } if (oldVersion < 7) { await db.execute('ALTER TABLE creative_island_history ADD COLUMN task_id TEXT NULL;'); await db.execute('ALTER TABLE creative_island_history ADD COLUMN status TEXT NULL;'); } if (oldVersion < 10) { await db.execute('ALTER TABLE cache ADD COLUMN `group` TEXT NULL;'); } if (oldVersion < 11) { await db.execute(''' CREATE TABLE settings ( `key` TEXT NOT NULL PRIMARY KEY, `value` TEXT NOT NULL ); '''); } if (oldVersion < 12) { await db.execute('''ALTER TABLE chat_room ADD COLUMN user_id INTEGER NULL;'''); await db.execute('''ALTER TABLE creative_island_history ADD COLUMN user_id INTEGER NULL;'''); } if (oldVersion < 13) { await db.execute('''ALTER TABLE chat_message ADD COLUMN user_id INTEGER NULL;'''); } if (oldVersion < 14) { await db.execute('''ALTER TABLE chat_message ADD COLUMN ref_id INTEGER NULL;'''); await db.execute('''ALTER TABLE chat_message ADD COLUMN token_consumed INTEGER NULL;'''); await db.execute('''ALTER TABLE chat_message ADD COLUMN quota_consumed INTEGER NULL;'''); } if (oldVersion < 15) { await db.execute('''ALTER TABLE chat_room ADD COLUMN init_message TEXT;'''); await db.execute('''ALTER TABLE chat_room ADD COLUMN max_context INTEGER DEFAULT 10;'''); } if (oldVersion < 20) { await db.execute('''ALTER TABLE chat_message ADD COLUMN chat_history_id INTEGER NULL;'''); await db.execute(''' CREATE TABLE chat_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NULL, room_id INTEGER NOT NULL, title TEXT, last_message TEXT, created_at INTEGER, updated_at INTEGER ) '''); } if (oldVersion < 23) { await db.execute('ALTER TABLE chat_history ADD COLUMN model TEXT;'); } if (oldVersion < 24) { await db.execute('ALTER TABLE chat_message ADD COLUMN server_id INTEGER NULL;'); } if (oldVersion < 25) { await db.execute('ALTER TABLE chat_message ADD COLUMN status INTEGER DEFAULT 1;'); } if (oldVersion < 26) { await db.execute('ALTER TABLE chat_message ADD COLUMN images TEXT NULL;'); } if (oldVersion < 27) { await db.execute('ALTER TABLE chat_message ADD COLUMN file TEXT NULL;'); } if (oldVersion < 28) { await db.execute('ALTER TABLE chat_message ADD COLUMN flags TEXT NULL;'); } } /// 数据库初始化 void initDatabase(db, version) async { await db.execute(''' CREATE TABLE chat_room ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NULL, name TEXT NOT NULL, category TEXT NOT NULL, priority INTEGER DEFAULT 0, model TEXT NOT NULL, icon_data TEXT NOT NULL, color TEXT, description TEXT, system_prompt TEXT, init_message TEXT, max_context INTEGER DEFAULT 10, created_at INTEGER, last_active_time INTEGER ) '''); await db.execute(''' INSERT INTO chat_room (id, name, category, priority, model, icon_data, color, created_at, last_active_time) VALUES (1, '随便聊聊', 'system', 99999, '$modelTypeOpenAI:$defaultChatModel', '57683,MaterialIcons', 'FF4CAF50', 1680969581486, ${DateTime.now().millisecondsSinceEpoch}); '''); await db.execute(''' CREATE TABLE chat_message ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NULL, room_id INTEGER NOT NULL, chat_history_id INTEGER NULL, type TEXT NOT NULL, role TEXT NOT NULL, user TEXT, text TEXT, extra TEXT, ref_id INTEGER NULL, server_id INTEGER NULL, status INTEGER DEFAULT 1, token_consumed INTEGER NULL, quota_consumed INTEGER NULL, model TEXT, images TEXT NULL, file TEXT NULL, flags TEXT NULL, ts INTEGER NOT NULL ) '''); await db.execute(''' CREATE TABLE chat_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NULL, room_id INTEGER NOT NULL, title TEXT, last_message TEXT, model TEXT, created_at INTEGER, updated_at INTEGER ) '''); await db.execute(''' CREATE TABLE cache ( `key` TEXT NOT NULL PRIMARY KEY, `value` TEXT NOT NULL, `group` TEXT NULL, `created_at` INTEGER, `valid_before` INTEGER ) '''); await db.execute(''' CREATE TABLE creative_island_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NULL, item_id TEXT NOT NULL, arguments TEXT NULL, prompt TEXT NULL, answer TEXT NULL, task_id TEXT NULL, status TEXT NULL, created_at INTEGER NOT NULL ) '''); await db.execute(''' CREATE TABLE settings ( `key` TEXT NOT NULL PRIMARY KEY, `value` TEXT NOT NULL ); '''); // await initUserDefaultRooms(db); } Future initUserDefaultRooms(Database db, {int? userId}) async { await db.execute(''' INSERT INTO chat_room (name, category, priority, model, icon_data, description, system_prompt, created_at, last_active_time, color, user_id) VALUES ('职业进阶导师', 'global', 0, 'openai:gpt-3.5-turbo', '57683,MaterialIcons', null, '我想让你担任我的职业进阶导师,你的任务是依据我的兴趣、技能和经验,为我提供职业发展建议,帮助我确定最适合的职业。注意,你需要对各种可行的职业类型进行深度研究,并在建议中包含各行业的市场趋势、就业趋势及进入该特定领域需要具备的资格', 1680969581486, ${DateTime.now().millisecondsSinceEpoch}, 'ff2196f3', ${userId ?? 'null'}); '''); await db.execute(''' INSERT INTO chat_room (name, category, priority, model, icon_data, description, system_prompt, created_at, last_active_time, color, user_id) VALUES ('人生导师', 'global', 0, 'openai:gpt-3.5-turbo', '57683,MaterialIcons', null, '你是一名在个人和职业发展方面拥有丰富经验的人,我希望你成为帮助我定制并实现个人目标和愿景的人生导师。请根据我的需求,为我提供专业的建议和指导,并鼓励我以积极乐观地态度感恩生活,勇于面对困难和挑战,不断突破自我、持续成长,成为珍惜自我,尊重他人,信任可靠,散发积极正能量的人。接下来我会像你进行提问,请称呼我为朋友。', 1680969581486, ${DateTime.now().millisecondsSinceEpoch}, 'ff2196f3', ${userId ?? 'null'}); '''); await db.execute(''' INSERT INTO chat_room (name, category, priority, model, icon_data, description, system_prompt, created_at, last_active_time, color, user_id) VALUES ('理财顾问', 'global', 0, 'openai:gpt-3.5-turbo', '57683,MaterialIcons', null, '我希望你成为我的理财顾问,为我提供创造性的理财方案并制定出理财计划。你需要考虑投资预算、投资策略和风险管理。在某些情况下,你可能还需要提供有关税收法律法规的建议,以帮助我实现最大化收益', 1680969581486, ${DateTime.now().millisecondsSinceEpoch}, 'ff2196f3', ${userId ?? 'null'}); '''); await db.execute(''' INSERT INTO chat_room (name, category, priority, model, icon_data, description, system_prompt, created_at, last_active_time, color, user_id) VALUES ('玩乐指南', 'global', 0, 'openai:gpt-3.5-turbo', '57683,MaterialIcons', null, '我希望你是我的专属吃喝玩乐(旅行)达人,在美食、娱乐、旅游等领域拥有丰富的经验。请根据我要求的领域、位置及其他需求,为我推荐几个准确、实用、高质量的好去处,以帮助我拥有更丰富的人生体验', 1680969581486, ${DateTime.now().millisecondsSinceEpoch}, 'ff2196f3', ${userId ?? 'null'}); '''); await db.execute(''' INSERT INTO chat_room (name, category, priority, model, icon_data, description, system_prompt, created_at, last_active_time, color, user_id) VALUES ('野蛮女友', 'global', 0, 'openai:gpt-3.5-turbo', '57683,MaterialIcons', null, '你是我的野蛮女友,能和我畅谈任何话题。你纯真无邪,性格里带着一丝精灵古怪和小小任性,偶尔会撒撒娇,或对我冷嘲热讽一番,非常可爱。和你聊天时,你常常会使用表情符号或Emoji回应我。另外请称呼我为大懒虫', 1680969581486, ${DateTime.now().millisecondsSinceEpoch}, 'ff2196f3', ${userId ?? 'null'}); '''); await db.execute(''' INSERT INTO chat_room (name, category, priority, model, icon_data, description, system_prompt, created_at, last_active_time, color, user_id) VALUES ('鸡汤达人', 'global', 0, 'openai:gpt-3.5-turbo', '57683,MaterialIcons', null, '你是一名鸡汤达人。请在和我对话的过程中,使用你擅长的鸡汤式发言进行回答,要求回答饱经沧桑、引经据典、充满智慧和正能量。另外请称呼我为朋友', 1680969581486, ${DateTime.now().millisecondsSinceEpoch}, 'ff2196f3', ${userId ?? 'null'}); '''); await db.execute(''' INSERT INTO chat_room (name, category, priority, model, icon_data, description, system_prompt, created_at, last_active_time, color, user_id) VALUES ('成语接龙', 'global', 0, 'openai:gpt-3.5-turbo', '57683,MaterialIcons', null, '陪我玩成语接龙游戏,我说一句话或一个成语,你将以最后一个字为作为起始,写一个成语出来', 1680969581486, ${DateTime.now().millisecondsSinceEpoch}, 'ff2196f3', ${userId ?? 'null'}); '''); await db.execute(''' INSERT INTO chat_room (name, category, priority, model, icon_data, description, system_prompt, created_at, last_active_time, color, user_id) VALUES ('哲学大师', 'global', 0, 'openai:gpt-3.5-turbo', '57683,MaterialIcons', null, '你是一名哲学家。我会提出一些与哲学相关的话题或问题,你的工作就是深入探索这些概念并回答我。这可能涉及到对各种哲学理论的研究、提出新的想法或寻找创造性的解决方案以解决复杂的问题', 1680969581486, ${DateTime.now().millisecondsSinceEpoch}, 'ff2196f3', ${userId ?? 'null'}); '''); await db.execute(''' INSERT INTO chat_room (name, category, priority, model, icon_data, description, system_prompt, created_at, last_active_time, color, user_id) VALUES ('开心果', 'global', 0, 'openai:gpt-3.5-turbo', '57683,MaterialIcons', null, '你是一个非常幽默的人,是大家心目中的开心果。作为开心果,你总是能用机智逗趣、诙谐幽默的方式回应我,妙语连珠,尽显活力,必要时还会通过讲笑话或调侃等方式来缓解气氛', 1680969581486, ${DateTime.now().millisecondsSinceEpoch}, 'ff2196f3', ${userId ?? 'null'}); '''); await db.execute(''' INSERT INTO chat_room (name, category, priority, model, icon_data, description, system_prompt, created_at, last_active_time, color, user_id) VALUES ('健身教练', 'global', 0, 'openai:gpt-3.5-turbo', '57683,MaterialIcons', null, '我希望你成为我的私人教练。你的职责是运用运动学科知识、营养建议及其他相关因素,结同时考虑我的生活习惯、目标及当前健身水平,为我制定最适合我的健身计划', 1680969581486, ${DateTime.now().millisecondsSinceEpoch}, 'ff2196f3', ${userId ?? 'null'}); '''); } ================================================ FILE: lib/helper/ability.dart ================================================ import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/platform.dart'; import 'package:askaide/repo/api/info.dart'; import 'package:askaide/repo/api/model.dart'; import 'package:askaide/repo/settings_repo.dart'; class Ability { late final SettingRepository setting; late Capabilities capabilities; init(SettingRepository setting, Capabilities capabilities) { this.setting = setting; this.capabilities = capabilities; } /// 单例 static final Ability _instance = Ability._internal(); Ability._internal(); factory Ability() { return _instance; } /// 是否显示全局警告信息 bool get showGlobalAlert { return true; } /// 服务状态页 String get serviceStatusPage { return capabilities.serviceStatusPage; } /// 是否支持 Websocket bool get supportWebSocket { return capabilities.supportWebsocket; } /// 是否支持 API Keys 功能 bool get supportAPIKeys { return capabilities.supportAPIKeys; } /// 更新能力 updateCapabilities(Capabilities capabilities) { this.capabilities = capabilities; } /// 首页支持的模型列表 List get homeModels { return capabilities.homeModels; } /// 是否显示首页模型描述 bool get showHomeModelDescription { return capabilities.showHomeModelDescription; } /// 首页路由 String get homeRoute { return capabilities.homeRoute; } /// 是否支持绘玩 bool get enableGallery { return !capabilities.disableGallery; } /// 是否支持创作岛 bool get enableCreationIsland { return !capabilities.disableCreationIsland; } /// 是否支持数字人 bool get enableDigitalHuman { return !capabilities.disableDigitalHuman; } /// 是否支持聊一聊 bool get enableChat { return !capabilities.disableChat; } /// 是否支持 OpenAI bool get enableOpenAI { return capabilities.openaiEnabled && (!capabilities.disableChat || !capabilities.disableDigitalHuman); } /// 是否支持 IOS 外支付 bool get enableOtherPay { return capabilities.otherPayEnabled; } /// 是否支持 Stripe 支付 bool get enableStripe { return capabilities.stripeEnabled; } /// 是否支持微信支付 bool get enableWechatPay { return capabilities.wechatPayEnabled; } /// 是否支持 ApplePay bool get enableApplePay { return capabilities.applePayEnabled; } /// 是否显示 Apple 登录 bool get enableAppleSignin { return enableApplePay && (PlatformTool.isIOS() || PlatformTool.isAndroid() || PlatformTool.isMacOS()); } /// 是否支持微信登录 bool get enableWechatSignin { return capabilities.wechatSigninEnabled && (PlatformTool.isIOS() || PlatformTool.isAndroid()); } /// 是否支持支付功能 bool get enablePayment { if (!enableApplePay && !enableOtherPay && !enableStripe) { return false; } if (PlatformTool.isIOS() && enableApplePay) { return true; } return true; } /// 是否用户已经登陆 bool isUserLogon() { return setting.stringDefault(settingAPIServerToken, '') != ''; } /// 是否启用了 OpenAI 自定义设置 bool get enableLocalOpenAI { return setting.boolDefault(settingOpenAISelfHosted, false); } /// 是否使用本地的 OpenAI 模型 bool usingLocalOpenAIModel(String model) { return setting.boolDefault(settingOpenAISelfHosted, false) && (model.startsWith('openai:') || model.startsWith('gpt-')); } /// 是否支持翻译功能 bool get supportTranslate { return false; // return setting.stringDefault(settingAPIServerToken, '') != ''; } /// 是否支持语音合成功能 bool get supportSpeak { if (!enableTextToVoice) { return false; } if (PlatformTool.isWeb()) { return false; } return true; } /// 是否支持语音聊天功能 bool get supportVoiceChat { if (!enableVoiceToText) { return false; } if (PlatformTool.isWeb()) { return false; } return true; } /// 是否支持图片上传功能 bool get supportImageUploader { return supportImglocUploader() || supportQiniuUploader(); } /// 是否支持Imgloc图片上传功能 bool supportImglocUploader() { return setting.boolDefault(settingImageManagerSelfHosted, false) && setting.stringDefault(settingImglocToken, '') != ''; } /// 是否支持七牛云图片上传功能 bool supportQiniuUploader() { return setting.stringDefault(settingAPIServerToken, '') != ''; } /// 是否支持语音转文字 bool get enableVoiceToText { return capabilities.enableVoiceToText; } /// 是否支持文字转语音 bool get enableTextToVoice { return capabilities.enableTextToVoice; } /// 获取当前主题模式 String get themeMode { return setting.stringDefault(settingThemeMode, 'system'); } } ================================================ FILE: lib/helper/cache.dart ================================================ import 'package:askaide/repo/cache_repo.dart'; import 'package:askaide/repo/settings_repo.dart'; class Cache { late final SettingRepository setting; late final CacheRepository cacheRepo; init(SettingRepository setting, CacheRepository cacheRepo) { this.setting = setting; this.cacheRepo = cacheRepo; } /// 单例 static final Cache _instance = Cache._internal(); Cache._internal(); factory Cache() { return _instance; } Future boolGet({required String key}) async { var value = await cacheRepo.get(key); if (value == null || value.isEmpty || value == 'true') { return true; } return false; } Future remove({required String key}) async { await cacheRepo.remove(key); } Future clearAll() async { await cacheRepo.clearAll(); } Future setBool({ required String key, required bool value, Duration duration = const Duration(days: 1), }) async { await cacheRepo.set(key, value.toString(), duration); } Future setString({ required String key, required String value, Duration duration = const Duration(days: 1), }) async { await cacheRepo.set(key, value, duration); } Future stringGet({required String key}) async { return await cacheRepo.get(key); } Future setInt({ required String key, required int value, Duration duration = const Duration(days: 1), }) async { await cacheRepo.set(key, value.toString(), duration); return value; } Future intGet({required String key}) async { var value = await cacheRepo.get(key); if (value == null || value.isEmpty) { return 0; } return int.parse(value); } } ================================================ FILE: lib/helper/chat_token.dart ================================================ import 'package:askaide/helper/logger.dart'; import 'package:tiktoken/tiktoken.dart'; /// 计算 message 包含的 token 数量 int tokenCount(String model, String message) { try { final encoding = encodingForModel(model); return encoding.encode(message).length; } catch (e) { Logger.instance.e(e); return -1; } } ================================================ FILE: lib/helper/color.dart ================================================ import 'package:flutter/material.dart'; /// 将颜色转换为字符串 String colorToString(Color color, {String defaultColor = 'FF000000'}) { try { return color.toString().split('(0x')[1].split(')')[0]; } catch (e) { return defaultColor; } } /// 将字符串转换为颜色 Color stringToColor(String colorString, {Color defaultColor = Colors.black}) { try { if (colorString.length == 6) { colorString = 'FF$colorString'; } return Color(int.parse(colorString, radix: 16)); } catch (e) { return defaultColor; } } ================================================ FILE: lib/helper/constant.dart ================================================ import 'package:flutter/material.dart'; // 客户端应用版本号 const clientVersion = '2.0.0'; // 本地数据库版本号 const databaseVersion = 28; const settingAPIServerToken = 'api-token'; const settingUserInfo = 'user-info'; const settingUsingGuestMode = 'using-guest-mode'; const settingForceShowLab = 'force-show-lab'; const chatAnywhereModel = 'openai:gpt-3.5-turbo'; const chatAnywhereRoomId = 1; const creativeIslandModelTypeText = 'text-generation'; const creativeIslandModelTypeImage = 'image-generation'; const creativeIslandModelTypeImageToImage = 'image-to-image'; const creativeIslandCompletionTypeText = 'text'; const creativeIslandCompletionTypeBase64Image = 'base64-images'; const creativeIslandCompletionTypeURLImage = 'url-images'; // 用于标识是否已经加载过引导页 // 只有在第一次安装的时候才会加载引导页 const settingOnBoardingLoaded = 'on-boarding-loaded'; const settingLanguage = 'language'; const settingServerURL = 'server-url'; // 背景图片 const settingBackgroundImage = 'background-image'; const settingBackgroundImageBlur = 'background-image-blur'; const settingOpenAISelfHosted = 'openai-self-hosted'; const settingDeepAISelfHosted = 'deepai-self-hosted'; const settingStabilityAISelfHosted = 'stabilityai-self-hosted'; const settingImageManagerSelfHosted = 'image-manager-self-hosted'; const settingThemeMode = "dark-mode"; const settingImglocToken = 'imgloc-token'; const chatMessagePerPage = 300; const contextBreakKey = 'context-break'; const defaultChatModel = 'gpt-3.5-turbo'; const defaultChatModelName = 'GPT-3.5'; const defaultImageModel = 'DALL·E'; const defaultModelNotChatDesc = '该模型不支持上下文,只能一问一答'; const defaultChatHistoryCount = 50; // AI 模型类型 const modelTypeOpenAI = 'openai'; const modelTypeDeepAI = 'deepai'; const modelTypeLeapAI = "leapai"; const modelTypeStabilityAI = 'stabilityai'; const modelTypeFromston = 'fromston'; const modelTypeGetimg = 'getimgai'; final modelTypeTagColors = { modelTypeOpenAI: Colors.blue, modelTypeDeepAI: Colors.green, modelTypeStabilityAI: Colors.purple, modelTypeLeapAI: Colors.orange, modelTypeFromston: Colors.blueAccent, modelTypeGetimg: Colors.pinkAccent, }; // OpenAI 相关设置 const settingOpenAIAPIToken = "openai-token"; const settingOpenAIOrganization = 'openai-organization'; const settingOpenAITemperature = "openai-temperature"; const settingOpenAIModel = "openai-model"; const settingOpenAIURL = "openai-url"; const defaultOpenAIServerURL = 'https://api.openai.com'; // DeepAI 相关设置 const settingDeepAIURL = 'deepai-url'; const settingDeepAIAPIToken = 'deepai-token'; const defaultDeepAIServerURL = 'https://api.deepai.org'; // StabilityAI 相关设置 const settingStabilityAIURL = 'stabilityai-url'; const settingStabilityAIAPIToken = 'stabilityai-token'; const settingStabilityAIOrganization = 'stabilityai-organization'; const defaultStabilityAIURL = 'https://api.stability.ai'; // 微信配置 const weixinAppId = 'wx52cc036cc770406d'; const universalLink = 'https://ai.aicode.cc/wechat-login/'; // 图床信息 const qiniuImageTypeAvatar = 'avatar'; const qiniuImageTypeThumb = 'thumb'; const qiniuImageTypeThumbMedium = 'thumb_500'; // 缓存相关的 Keys // 最后一次使用的模型 const cacheKeyLastModel = 'last-model'; // 数据存储目录 const homePathDirName = '.aidea'; ================================================ FILE: lib/helper/env.dart ================================================ /// 默认 API 服务器地址 /// 注意:当你使用自己的服务器时,请修改该地址为你自己的服务器地址 const defaultAPIServerURL = 'https://ai-api.aicode.cc'; /// API 服务器地址 String get apiServerURL { var url = const String.fromEnvironment( 'API_SERVER_URL', defaultValue: defaultAPIServerURL, ); // 当配置的 URL 为 / 时,自动替换为空,用于 Web 端 if (url == '/') { return ''; } return url; } ================================================ FILE: lib/helper/error.dart ================================================ import 'package:askaide/helper/ability.dart'; import 'package:askaide/lang/lang.dart'; import 'package:dart_openai/openai.dart'; Object resolveErrorMessage(dynamic e, {bool isChat = false}) { // TODO if (e is RequestFailedException) { final msg = resolveHTTPStatusCode(e.statusCode, isChat: isChat, message: e.message); if (msg != null) { return msg; } return e.message; } return e.toString(); } Object? resolveHTTPStatusCode(int statusCode, {bool isChat = false, String? message}) { switch (statusCode) { case 400: return const LanguageText('请求参数错误'); case 401: if (Ability().enableLocalOpenAI) { return const LanguageText(AppLocale.openAIAuthFailed); } if (Ability().isUserLogon()) { return const LanguageText(AppLocale.accountNeedReSignin, action: 're-signin'); } return const LanguageText(AppLocale.signInRequired, action: 'sign-in'); case 404: if (isChat) { return const LanguageText(AppLocale.modelNotFound); } break; case 429: if (isChat) { return const LanguageText(AppLocale.tooManyRequestsOrPaymentRequired); } return const LanguageText(AppLocale.tooManyRequests); case 451: return const LanguageText(AppLocale.modelNotValid); case 402: return const LanguageText(AppLocale.quotaExceeded, action: 'payment'); case 500: if (message != null && message.isNotEmpty) { return message; } return const LanguageText(AppLocale.internalServerError); case 502: return const LanguageText(AppLocale.badGateway); } return null; } ================================================ FILE: lib/helper/event.dart ================================================ class GlobalEvent { /// 单例 static final GlobalEvent _instance = GlobalEvent._internal(); GlobalEvent._internal(); factory GlobalEvent() { return _instance; } /// 事件监听器 final Map> _listeners = {}; /// 监听事件 Function() on(String event, Function(dynamic data) callback) { if (_listeners[event] == null) { _listeners[event] = []; } _listeners[event]!.add(callback); return () { _listeners[event]!.remove(callback); }; } /// 触发事件 void emit(String event, [dynamic data]) { if (_listeners[event] == null) { return; } for (var callback in _listeners[event]!) { callback(data); } } } ================================================ FILE: lib/helper/global_store.dart ================================================ import 'package:askaide/page/component/chat/file_upload.dart'; class GlobalStore { static final GlobalStore _instance = GlobalStore._internal(); GlobalStore._internal(); factory GlobalStore() { return _instance; } List uploadedFiles = []; } ================================================ FILE: lib/helper/haptic_feedback.dart ================================================ import 'package:flutter/services.dart'; class HapticFeedbackHelper { static Future lightImpact() async { return HapticFeedback.lightImpact(); } static Future mediumImpact() async { return HapticFeedback.mediumImpact(); } static Future heavyImpact() async { return HapticFeedback.heavyImpact(); } } ================================================ FILE: lib/helper/helper.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:askaide/helper/logger.dart'; import 'package:askaide/lang/lang.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:intl/intl.dart'; import 'package:path_provider/path_provider.dart'; import 'package:uuid/uuid.dart'; String randomId() { return const Uuid().v4(); } /// 将 base64 转换为图片,存储到临时文件 Future writeImageFromBase64(String base64, String ext) async { final directory = await getApplicationDocumentsDirectory(); // 确保目录存在 await Directory('${directory.path}/cache').create(recursive: true); final file = File('${directory.path}/cache/temp_${randomId()}.$ext'); await file.writeAsBytes(base64Decode(base64)); return file.path.substring(directory.path.length + 1); } String filenameWithoutExt(String filePath) { int slashIndex = filePath.lastIndexOf('/'); int dotIndex = filePath.lastIndexOf('.'); if (dotIndex < 0 || dotIndex < slashIndex) { return filePath.substring(slashIndex + 1); } else { return filePath.substring(slashIndex + 1, dotIndex); } } Future writeTempFile(String path, Uint8List bytes) async { final directory = await getTemporaryDirectory(); final file = File('${directory.path}/$path'); return await file.writeAsBytes(bytes); } Future readTempFile(String path) async { final directory = await getTemporaryDirectory(); final file = File('${directory.path}/$path'); return await file.readAsBytes(); } Future writeStringFileToDocumentsDirectory(String path, String content) async { try { final directory = await getApplicationDocumentsDirectory(); final file = File('${directory.path}/$path'); Logger.instance.e('${directory.path}/$path'); await file.writeAsString(content); } catch (e) { Logger.instance.e('写入文件失败: $e'); } } Future readStringFileFromDocumentsDirectory(String path) async { try { final directory = await getApplicationDocumentsDirectory(); final file = File('${directory.path}/$path'); return await file.readAsString(); } catch (e) { return ''; } } Future removeExternalFile(String externalFilepath) async { // Get the external file final File externalFile = File(externalFilepath); // Check if the external file exists if (!await externalFile.exists()) { return; } await externalFile.delete(); } Future copyExternalFileToAppDocs(String externalFilePath) async { // Get the external file final File externalFile = File(externalFilePath); // Check if the external file exists if (!await externalFile.exists()) { throw Exception('External file not found at: $externalFilePath'); } // Get the ApplicationDocumentsDirectory final Directory appDocsDir = await getApplicationDocumentsDirectory(); // Generate a UUID for the new file name final String uuid = const Uuid().v4(); // Get the file extension final String fileExtension = externalFile.path.split('.').last; // Create a new file in the ApplicationDocumentsDirectory with the UUID as its name final File newFile = File('${appDocsDir.path}/$uuid.$fileExtension'); // Copy the external file to the new file in the ApplicationDocumentsDirectory await externalFile.copy(newFile.path); // print('File copied to: ${newFile.path}'); return newFile.path; } /// 将时间转换为友好的时间 String humanTime(DateTime? ts, {bool withTime = false}) { if (ts == null || ts.millisecondsSinceEpoch == 0) { return ''; } var now = DateTime.now(); var diff = now.difference(ts); if (diff.inDays > 0) { if (withTime) { return DateFormat('yyyy/MM/dd HH:mm').format(ts.toLocal()); } return DateFormat('yyyy/MM/dd').format(ts.toLocal()); } if (diff.inHours > 0) { return '${diff.inHours} hours ago'; } if (diff.inMinutes > 0) { return '${diff.inMinutes} minutes ago'; } return 'Just now'; } /// 解析错误信息 String resolveError(BuildContext context, Object error) { if (error is LanguageText) { return error.message.getString(context); } return error.toString(); } ================================================ FILE: lib/helper/http.dart ================================================ import 'package:askaide/helper/logger.dart'; import 'package:dio/dio.dart'; import 'package:dio_cache_interceptor/dio_cache_interceptor.dart'; import 'package:dio_smart_retry/dio_smart_retry.dart'; class HttpClient { static final dio = Dio(); static final cacheStore = MemCacheStore(); static final cacheOptions = CacheOptions( store: cacheStore, policy: CachePolicy.request, hitCacheOnErrorExcept: [401, 403], maxStale: const Duration(days: 7), allowPostMethod: false, keyBuilder: CacheOptions.defaultCacheKeyBuilder, ); static init() { dio.interceptors.add(DioCacheInterceptor( options: cacheOptions, )); dio.interceptors.add(RetryInterceptor( dio: dio, retries: 3, logPrint: (message) { Logger.instance.w(message); }, retryDelays: const [ Duration(seconds: 1), // wait 1 sec before first retry Duration(seconds: 2), // wait 2 sec before second retry Duration(seconds: 3), // wait 3 sec before third retry ], )); } static Future get( String url, { Map? queryParameters, Options? options, }) async { Logger.instance.d('API Request: [GET] $url'); return await dio.get(url, queryParameters: queryParameters, options: options); } static Future getCached( String url, { String? subKey, Duration duration = const Duration(days: 1), Map? queryParameters, bool forceRefresh = false, Options? options, }) async { options ??= Options(); Logger.instance.d('API Request: [GET with cache] $url'); final resp = await dio.get( url, queryParameters: queryParameters, options: options.copyWith( extra: cacheOptions .copyWith( maxStale: Nullable(duration), policy: forceRefresh ? CachePolicy.refreshForceCache : CachePolicy.forceCache, ) .toExtra()), ); // print("======================="); // Logger.instance.d("request: $url [${resp.statusCode}]"); // print("response: ${resp.data}"); return resp; } // 清空缓存 static Future cleanCache() async { return await cacheStore.clean(); } static Future post( String url, { Map? queryParameters, Map? formData, Options? options, }) async { Logger.instance.d('API Request: [POST] $url'); final resp = await dio.post( url, queryParameters: queryParameters, data: formData != null ? FormData.fromMap(formData) : null, options: options, ); // print("======================="); // print("request: $url"); // print("response: ${resp.data}"); return resp; } static Future postJSON( String url, { Map? queryParameters, Map? data, Options? options, }) async { Logger.instance.d('API Request: [POST JSON] $url'); final resp = await dio.post( url, queryParameters: queryParameters, data: data, options: options, ); // print("======================="); // print("request: $url"); // print("response: ${resp.data}"); return resp; } static Future put( String url, { Map? queryParameters, Map? formData, Options? options, }) async { Logger.instance.d('API Request: [PUT] $url'); return await dio.put( url, queryParameters: queryParameters, data: formData != null ? FormData.fromMap(formData) : null, options: options, ); } static Future putJSON( String url, { Map? queryParameters, Map? data, Options? options, }) async { Logger.instance.d('API Request: [PUT JSON] $url'); return await dio.put( url, queryParameters: queryParameters, data: data, options: options, ); } static Future delete( String url, { Map? queryParameters, Map? formData, Options? options, }) async { Logger.instance.d('API Request: [DELETE] $url'); return await dio.delete( url, queryParameters: queryParameters, data: formData != null ? FormData.fromMap(formData) : null, options: options, ); } } ================================================ FILE: lib/helper/image.dart ================================================ String imageURL(String url, String filter) { if (!url.startsWith('https://ssl.aicode.cc/')) { return url; } if (filter.isEmpty) { return url; } if (isImage(url)) { return '$url-$filter'; } else { return url; } } bool isImage(String url) { final low = url.toLowerCase(); return low.endsWith('.jpg') || low.endsWith('.jpeg') || low.endsWith('.png') || low.endsWith('.gif') || low.endsWith('.webp'); } ================================================ FILE: lib/helper/logger.dart ================================================ import 'dart:io'; import 'package:askaide/helper/path.dart'; import 'package:askaide/helper/platform.dart'; import 'package:logger/logger.dart' as logger; class Logger { static final logger.Logger instance = logger.Logger( printer: logger.PrettyPrinter( lineLength: 120, printTime: true, colors: false, noBoxingByDefault: true, ), output: logger.MultiOutput( [ logger.ConsoleOutput(), if (!PlatformTool.isWeb()) logger.FileOutput( file: File(PathHelper().getLogfilePath), overrideExisting: true, ), ], ), ); } ================================================ FILE: lib/helper/lru.dart ================================================ import 'dart:collection'; abstract class Disposable { void dispose(); } class LRUCache { final int capacity; final LinkedHashMap _cache = LinkedHashMap(); LRUCache(this.capacity); bool containsKey(K key) { return _cache.containsKey(key); } V? get(K key) { if (_cache.containsKey(key)) { final value = _cache.remove(key); if (value != null) { _cache[key] = value; return value; } } return null; } void put(K key, V value) { if (_cache.containsKey(key)) { _cache.remove(key); } else if (_cache.length >= capacity) { var removed = _cache.remove(_cache.keys.first); if (removed != null) { removed.dispose(); } } _cache[key] = value; } void remove(K key) { var removed = _cache.remove(key); if (removed != null) { removed.dispose(); } } void clear() { _cache.forEach((_, value) => value.dispose()); _cache.clear(); } int get length => _cache.length; } ================================================ FILE: lib/helper/model.dart ================================================ import 'package:askaide/helper/constant.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/model/model.dart' as mm; import 'package:askaide/repo/settings_repo.dart'; /// 模型聚合,用于聚合多种厂商的模型 class ModelAggregate { static late SettingRepository settings; static void init(SettingRepository settings) { ModelAggregate.settings = settings; } /// 支持的模型列表 static Future> models({bool cache = true}) async { return (await APIServer().models(cache: cache)) .map( (e) => mm.Model( e.id.split(':').last, e.name, e.category, shortName: e.shortName, description: e.description, priceInfo: e.priceInfo, isChatModel: e.isChat, disabled: e.disabled, category: e.category, tag: e.tag, avatarUrl: e.avatarUrl, supportVision: e.supportVision, supportReasoning: e.supportReasoning, supportSearch: e.supportSearch, tagTextColor: e.tagTextColor, tagBgColor: e.tagBgColor, isNew: e.isNew, isDefault: e.isDefault, userNoPermission: e.userNoPermission, ), ) .toList(); } /// 根据模型唯一id查找模型 static Future model(String uid) async { uid = uid.split(':').last; final supportModels = await models(); return supportModels.firstWhere( (element) => element.uid() == uid || element.id == uid, orElse: () => mm.Model(defaultChatModel, defaultChatModel, 'openai', category: modelTypeOpenAI), ); } } ================================================ FILE: lib/helper/model_resolver.dart ================================================ import 'dart:io'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/error.dart'; import 'package:askaide/helper/helper.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/deepai_repo.dart'; import 'package:askaide/repo/model/chat_message.dart'; import 'package:askaide/repo/model/message.dart'; import 'package:askaide/repo/model/room.dart'; import 'package:askaide/repo/openai_repo.dart'; import 'package:askaide/repo/stabilityai_repo.dart'; import 'package:dart_openai/openai.dart'; /// 根据聊天类型,调用不同的 API 接口 class ModelResolver { late final OpenAIRepository openAIRepo; late final DeepAIRepository deepAIRepo; late final StabilityAIRepository stabilityAIRepo; /// 初始化,设置模型实现 void init({ required OpenAIRepository openAIRepo, required DeepAIRepository deepAIRepo, required StabilityAIRepository stabilityAIRepo, }) { this.openAIRepo = openAIRepo; this.deepAIRepo = deepAIRepo; this.stabilityAIRepo = stabilityAIRepo; } ModelResolver._(); static final instance = ModelResolver._(); /// 语音转文字 Future audioToText(File file) async { try { return await openAIRepo.audioTranscription(audioFile: file); } catch (error) { throw resolveErrorMessage(error); } } /// 发起聊天请求 Future request({ required Room room, required List contextMessages, required Function(ChatStreamRespData value) onMessage, int? maxTokens, String? tempModel, int? historyId, List? flags, }) async { if (room.modelCategory() == modelTypeDeepAI) { return await _deepAIModel( room: room, message: contextMessages.last, contextMessages: contextMessages, onMessage: (value) { onMessage(ChatStreamRespData(content: value)); }, ); } else if (room.modelCategory() == modelTypeStabilityAI) { return await _stabilityAIModel( room: room, message: contextMessages.last, contextMessages: contextMessages, onMessage: (value) { onMessage(ChatStreamRespData(content: value)); }, ); } else { return await _openAIModel( room: room, contextMessages: contextMessages, onMessage: onMessage, maxTokens: maxTokens, tempModel: tempModel, historyId: historyId, flags: flags, ); } } /// 调用 StabilityAI API Future _stabilityAIModel({ required Room room, required Message message, required List contextMessages, required Function(String value) onMessage, }) async { if (stabilityAIRepo.selfHosted) { var res = await stabilityAIRepo.createImageBase64( room.modelName(), [StabilityAIPrompt(message.text, 0.5)], ); for (var data in res) { var path = await writeImageFromBase64(data, 'png'); // print('图片路径: $path'); onMessage('\n![image]($path)\n'); } } else { var taskId = await stabilityAIRepo.createImageBase64Async( room.modelName(), [StabilityAIPrompt(message.text, 0.5)], ); await Future.delayed(const Duration(seconds: 10)); await _waitForTasks(taskId, onMessage); } } Future _waitForTasks( String taskId, Function(String value) onMessage, { int retry = 0, }) async { var res = await APIServer().asyncTaskStatus(taskId); if (res.status == 'success') { for (var data in res.resources!) { onMessage('\n![image]($data)\n'); } } else if (res.status == 'failed') { throw 'Response failed: ${res.errors!.join("\n")}'; } else { if (retry > 10) { throw 'Response timeout'; } await Future.delayed(const Duration(seconds: 5)); await _waitForTasks(taskId, onMessage, retry: retry + 1); } } /// 调用 DeepAI API Future _deepAIModel({ required Room room, required Message message, required List contextMessages, required Function(String value) onMessage, }) async { if (deepAIRepo.selfHosted) { var res = await deepAIRepo.painting(room.modelName(), message.text); onMessage('\n![${res.id}](${res.url})\n'); } else { var taskId = await deepAIRepo.paintingAsync(room.modelName(), message.text); await Future.delayed(const Duration(seconds: 10)); await _waitForTasks(taskId, onMessage); } } /// 调用 OpenAI API Future _openAIModel({ required Room room, required List contextMessages, required Function(ChatStreamRespData value) onMessage, int? maxTokens, String? tempModel, int? historyId, List? flags, }) async { // 图像模式 if (OpenAIRepository.isImageModel(room.modelName())) { var res = await openAIRepo.createImage(contextMessages.last.text, n: 2); for (var url in res) { onMessage(ChatStreamRespData(content: '\n![image]($url)\n')); } return; } // 聊天模型 return await openAIRepo.chatStream( _buildRequestContext(room, contextMessages), onMessage, model: room.modelName(), tempModel: tempModel, maxTokens: maxTokens, roomId: room.isLocalRoom ? null : room.id, historyId: historyId, flags: flags, ); } /// 构建机器人请求上下文 List _buildRequestContext( Room room, List messages, ) { // // N 小时内的消息作为一个上下文 // var recentMessages = messages // .where((e) => e.ts!.millisecondsSinceEpoch > lastAliveTime()) // .toList(); var recentMessages = messages.toList(); int contextBreakIndex = recentMessages.lastIndexWhere((element) => element.isSystem() && element.type == MessageType.contextBreak); if (contextBreakIndex > -1) { recentMessages = recentMessages.sublist(contextBreakIndex + 1); } var contextMessages = recentMessages .where((e) => !e.isSystem() && !e.isInitMessage()) .map((e) => e.role == Role.receiver ? ChatMessage( role: OpenAIChatMessageRole.assistant, content: e.text, images: e.images, file: e.file, ) : ChatMessage( role: OpenAIChatMessageRole.user, content: e.text, images: e.images, file: e.file, )) .toList(); if (contextMessages.length > room.maxContext * 2) { contextMessages = contextMessages.sublist(contextMessages.length - room.maxContext * 2); } if (room.systemPrompt != null && room.systemPrompt != '') { contextMessages.insert( 0, ChatMessage( role: OpenAIChatMessageRole.system, content: room.systemPrompt!, ), ); } return contextMessages; } } ================================================ FILE: lib/helper/path.dart ================================================ import 'dart:io' show Directory, Platform; import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/logger.dart'; import 'package:askaide/helper/platform.dart'; import 'package:path_provider/path_provider.dart'; class PathHelper { late final String cachePath; late final String documentsPath; late final String supportPath; init() async { try { cachePath = (await getApplicationCacheDirectory()).path.replaceAll('\\', '/'); } catch (e) { cachePath = ''; } try { documentsPath = (await getApplicationDocumentsDirectory()).path.replaceAll('\\', '/'); } catch (e) { documentsPath = ''; } try { supportPath = (await getApplicationSupportDirectory()).path.replaceAll('\\', '/'); } catch (e) { supportPath = ''; } // 确保 .aidea 目录存在 try { Directory(getHomePath).create(recursive: true); } catch (e) { Logger.instance.e('Create $getHomePath directory failed: $e'); } } String get getHomePath { if (PlatformTool.isMacOS() || PlatformTool.isLinux()) { return '${Platform.environment['HOME'] ?? ''}/$homePathDirName'.replaceAll('\\', '/'); } else if (PlatformTool.isWindows()) { return '${Platform.environment['UserProfile'] ?? ''}/$homePathDirName'.replaceAll('\\', '/'); } else if (PlatformTool.isAndroid() || PlatformTool.isIOS()) { return '$documentsPath/$homePathDirName'.replaceAll('\\', '/'); } return homePathDirName; } String get getLogfilePath { return '$getHomePath/aidea.log'; } String get getCachePath { return getHomePath; } /// 单例 static final PathHelper _instance = PathHelper._internal(); PathHelper._internal(); factory PathHelper() { return _instance; } Map toMap() { return { 'cachePath': cachePath, 'cachePathReal': getCachePath, 'documentsPath': documentsPath, 'supportPath': supportPath, 'homePath': getHomePath, 'logfilePath': getLogfilePath, }; } } ================================================ FILE: lib/helper/platform.dart ================================================ import 'dart:io'; import 'package:flutter/foundation.dart'; class PlatformTool { static bool isDesktop() { return isWindows() || isLinux() || isMacOS(); } static bool isDesktopAndWeb() { return isDesktop() || isWeb(); } static bool isMobile() { return isIOS() || isAndroid(); } static bool isIOS() { try { return Platform.isIOS; } catch (e) { return false; } } static bool isAndroid() { try { return Platform.isAndroid; } catch (e) { return false; } } static bool isWeb() { return kIsWeb; } static bool isMacOS() { try { return Platform.isMacOS; } catch (e) { return false; } } static bool isWindows() { try { return Platform.isWindows; } catch (e) { return false; } } static bool isLinux() { try { return Platform.isLinux; } catch (e) { return false; } } static String operatingSystem() { try { return Platform.operatingSystem; } catch (e) { return 'unknown'; } } static String operatingSystemVersion() { try { return Platform.operatingSystemVersion; } catch (e) { return 'unknown'; } } static String localeName() { try { return Platform.localeName; } catch (e) { return 'zh_Hans_CN'; } } } ================================================ FILE: lib/helper/queue.dart ================================================ import 'dart:async'; import 'dart:collection'; class QueueFinishedException implements Exception { final String message; QueueFinishedException(this.message); } /// 该队列以一定的时间间隔将队列中的元素传递给回调函数,实现平滑的队列处理 class GracefulQueue { final Queue _queue = Queue(); bool finished = false; Timer? _timer; void add(T item) { if (finished) { throw QueueFinishedException('Queue is finished'); } _queue.add(item); } void dispose() { _timer?.cancel(); } Future listen( Duration duration, Function(List items) callback) async { Completer completer = Completer(); _timer = Timer.periodic(duration, (timer) { if (_queue.isNotEmpty) { List items = []; for (var i = 0; i < _queue.length; i++) { items.add(_queue.removeFirst()); } callback(items); } else if (finished) { // print(_queue.length); timer.cancel(); completer.complete(); } }); return completer.future; } void finish() { finished = true; } } ================================================ FILE: lib/helper/tips.dart ================================================ final inputTips = [ '有问题尽管问我...', '开启新对话?往右侧滑动我试试', ]; ================================================ FILE: lib/helper/upload.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/helper.dart'; import 'package:askaide/helper/platform.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:http/http.dart' as http; import 'package:image/image.dart'; import 'package:isolate_image_compress/isolate_image_compress.dart'; import 'package:qiniu_flutter_sdk/qiniu_flutter_sdk.dart'; class ImageUploader { QiniuUploader? _qiniuUploader; ImageUploader(SettingRepository setting) { _qiniuUploader = QiniuUploader(setting); } final _compressWidth = 1024; final _compressHeight = 1024; /// 上传文件 Future upload(String path, {String? usage}) async { Uint8List? data = await _imageCompress(path); if (data == null || data.isEmpty) { throw Exception('图片读取失败'); } return _qiniuUploader!.upload(path, data, usage: usage); } /// 文件压缩后转为 base64 Future base64({ String? path, Uint8List? imageData, int? compressWidth, int? compressHeight, int? maxSize, }) async { if (imageData != null) { Uint8List? data = await _imageDataCompress( imageData, compressWidth: compressWidth, compressHeight: compressHeight, maxSize: maxSize ?? 1024 * 1024 * 2, ); if (data == null || data.isEmpty) { throw Exception('图片读取失败'); } return 'data:image/png;base64,${base64Encode(data)}'; } Uint8List? data = await _imageCompress( path!, compressWidth: compressWidth, compressHeight: compressHeight, maxSize: maxSize ?? 1024 * 1024 * 2, ); if (data == null || data.isEmpty) { throw Exception('图片读取失败'); } return 'data:image/png;base64,${base64Encode(data)}'; } // 上传文件数据 Future uploadData(Uint8List imageData, {String? usage}) async { Uint8List? data = await _imageDataCompress(imageData); if (data == null || data.isEmpty) { throw Exception('图片读取失败'); } return _qiniuUploader!.upload("${randomId()}.jpg", data, usage: usage); } Future _imageDataCompress(Uint8List imageData, {int? compressWidth, int? compressHeight, int quality = 80, int maxSize = 1024 * 1024 * 2}) async { Uint8List? data = imageData; // 优先使用平台支持的压缩工具 if (PlatformTool.isAndroid() || PlatformTool.isIOS()) { try { data = await FlutterImageCompress.compressWithList( data, quality: quality, minWidth: compressWidth ?? _compressWidth, minHeight: compressHeight ?? _compressHeight, ); } catch (e) { // ignore } if (data == null || data.isEmpty) { try { data = await IsolateImage.data(data!).compress( maxResolution: ImageResolution(compressWidth ?? _compressWidth, compressHeight ?? _compressHeight), maxSize: maxSize, ); } catch (e) { // ignore } } } else { try { data = await IsolateImage.data(data).compress( maxResolution: ImageResolution( compressWidth ?? _compressWidth, compressHeight ?? _compressHeight, ), maxSize: maxSize, ); } catch (e) { // ignore } } // 压缩失败,尝试 Dart 内置的图片压缩库 if (data == null || data.isEmpty) { try { Image? img = decodeImage(data!); if (img != null) { Image thumbnail = copyResize( img, width: img.width > img.height ? compressWidth ?? _compressWidth : null, height: img.width <= img.height ? compressHeight ?? _compressHeight : null, ); data = encodeJpg(thumbnail, quality: quality); } } catch (e) { // ignore } } // 再次压缩失败,返回原始数据 if (data == null || data.isEmpty) { data = imageData; } return data; } Future _imageCompress(String path, {int? compressWidth, int? compressHeight, int quality = 80, int maxSize = 1024 * 1024 * 2}) async { Uint8List? data; // 优先使用平台支持的压缩工具 if (PlatformTool.isAndroid() || PlatformTool.isIOS()) { try { data = await FlutterImageCompress.compressWithFile( path, quality: quality, minWidth: compressWidth ?? _compressWidth, minHeight: compressHeight ?? _compressHeight, ); } catch (e) { // ignore } if (data == null || data.isEmpty) { try { data = await IsolateImage.path(path).compress( maxResolution: ImageResolution(compressWidth ?? _compressWidth, compressHeight ?? _compressHeight), maxSize: maxSize, ); } catch (e) { // ignore } } } else { try { data = await IsolateImage.path(path).compress( maxResolution: ImageResolution( compressWidth ?? _compressWidth, compressHeight ?? _compressHeight, ), maxSize: maxSize, ); } catch (e) { // ignore } } // 压缩失败,尝试 Dart 内置的图片压缩库 if (data == null || data.isEmpty) { try { Image? img = decodeImage(File(path).readAsBytesSync()); if (img != null) { Image thumbnail = copyResize( img, width: img.width > img.height ? compressWidth ?? _compressWidth : null, height: img.width <= img.height ? compressHeight ?? _compressHeight : null, ); var n = path.toLowerCase(); if (n.endsWith('.jpg') || n.endsWith('.jpeg')) { data = encodeJpg(thumbnail, quality: quality); } else if (n.endsWith('.png')) { data = encodePng(thumbnail, level: 4); } else { data = encodeNamedImage(path, thumbnail); } } } catch (e) { // ignore } } // 再次压缩失败,返回原始数据 if (data == null || data.isEmpty) { data = await File(path).readAsBytes(); } return data; } } class UploadedFile { final String name; final String url; UploadedFile(this.name, this.url); } class ImglocUploader { late final String secretKey; ImglocUploader(SettingRepository setting) { secretKey = setting.stringDefault(settingImglocToken, ''); } Future upload( String path, Uint8List data, { String? usage, }) async { String imageBase64 = base64.encode(data); var resp = await http.post( Uri.parse('https://imgloc.com/api/1/upload'), headers: { 'X-API-Key': secretKey, }, body: { 'source': imageBase64, 'expiration': 'P1D', 'format': 'json', 'nsfw': "1", }, ); final parsed = jsonDecode(resp.body) as Map; if (resp.statusCode != 200 || parsed['status_code'] != 200) { throw Exception(parsed['status_text'] ?? parsed['error']['message']); } return UploadedFile(parsed['image']['name'], parsed['image']['url']); } } class QiniuUploader { final SettingRepository setting; QiniuUploader(this.setting); Future upload( String path, Uint8List data, { String? usage, }) async { try { var filename = path.substring(path.lastIndexOf('/') + 1); final initResp = await APIServer().uploadInit(filename, data.length, usage: usage); if (initResp.uploaded) { return UploadedFile(filename, initResp.url); } var storage = Storage(config: Config(retryLimit: 3)); await storage.putBytes( data, initResp.token, options: PutOptions( key: initResp.key, ), ); return UploadedFile(filename, initResp.url); } catch (ex) { return Future.error(ex); } } Future uploadFile(String path, {String? usage}) async { try { var filename = path.substring(path.lastIndexOf('/') + 1); final initResp = await APIServer() .uploadInit(filename, File(path).lengthSync(), usage: usage); if (initResp.uploaded) { return UploadedFile(filename, initResp.url); } var storage = Storage(config: Config(retryLimit: 3)); await storage.putFile( File(path), initResp.token, options: PutOptions( key: initResp.key, ), ); return UploadedFile(filename, initResp.url); } catch (ex) { return Future.error(ex); } } } ================================================ FILE: lib/lang/lang.dart ================================================ mixin AppLocale { static const String required = 'required'; static const String wechat = 'wechat'; static const String systemInfo = 'system_info'; static const String appName = 'app_name'; static const String homeTitle = 'home_title'; static const String chatAnywhere = 'chat_anywhere'; static const String creativeIsland = 'creative_island'; static const String settings = 'settings'; static const String configure = 'configure'; static const String language = 'language'; static const String themeMode = 'theme_mode'; static const String accountInfo = 'account_info'; static const String accountSettings = 'account_settings'; static const String usage = 'usage'; static const String validBefore = 'valid_before'; static const String custom = 'custom'; static const String clearCache = 'clear_cache'; static const String about = 'about'; static const String diagnostic = 'diagnostic'; static const String userTerms = 'user_terms'; static const String privacyPolicy = 'privacy_policy'; static const String signIn = 'sign_in'; static const String signInAccount = 'sign_in_account'; static const String signOut = 'sign_out'; static const String signUp = 'sign_up'; static const String password = 'password'; static const String passwordConfirm = 'password_confirm'; static const String retrievePassword = 'retrieve_password'; static const String newPassword = 'new_password'; static const String account = 'account'; static const String usedUp = 'used_up'; static const String expired = 'expired'; static const String timeConsume = 'time-consume'; static const String save = 'save'; static const String ok = 'ok'; static const String cancel = 'cancel'; static const String select = 'select'; static const String tips = 'tips'; static const String goodTips = 'good-tips'; static const String basicInfo = 'basic-info'; static const String delete = 'delete'; static const String edit = 'edit'; static const String selectAll = 'select-all'; static const String unselectAll = 'unselect-all'; static const String share = 'share'; static const String cancelShare = 'cancel-share'; static const String histories = 'histories'; static const String moreHistories = 'more-histories'; static const String enable = 'enable'; static const String disable = 'disable'; static const String newChat = 'new-chat'; static const String clearChatHistory = 'clear-chat-history'; static const String examples = 'examples'; static const String continueMessage = 'continue'; static const String messageInputTips = 'message-input-tips'; static const String takePhoto = 'take-photo'; static const String photoLibrary = 'photo-library'; static const String fileLibrary = 'file-library'; static const String upload = 'upload'; static const String longPressSpeak = 'long-press-speak'; static const String send = 'send'; static const String sendRetry = 'send-retry'; static const String sendRetryS = 'send-retry-s'; static const String selectText = 'select-text'; static const String text = 'text'; static const String uploading = 'uploading'; static const String robotIsThinkingMessage = 'robot-is-thinking-message'; static const String thinkingProcess = 'thinking-process'; static const String robotIsSearchingMessage = 'robot-is-searching-message'; static const String robotHasSomeError = 'robot-has-some-error'; static const String questionExamples = 'question-examples'; static const String noRecords = 'no-records'; static const String contextBreakMessage = 'context-break-message'; static const String translateFinished = 'translate-finished'; static const String textCopied = 'text-copied'; static const String copy = 'copy'; static const String translate = 'translate'; static const String hide = 'hide'; static const String readByVoice = 'read-by-voice'; static const String unknownFile = 'unknown-file'; static const String switchModel = 'switch-model'; static const String switchModelTitle = 'switch-model-title'; static const String myCharacters = 'my-characters'; static const String character = "character"; static const String createRoom = "create-room"; static const String model = "model"; static const String selectModel = "select-model"; static const String roomName = "room-name"; static const String avatar = "avatar"; static const String iconName = "icon-name"; static const String prompt = "prompt"; static const String optional = "optional"; static const String search = "search"; static const String onlineSearch = "online-search"; static const String reasoning = "reasoning"; static const String background = "background"; static const String backgroundSetting = "background-setting"; static const String roomSetting = "room-setting"; static const String chatHistory = "chat-history"; static const String noMessageSelected = 'no-message-selected'; static const String modelUsage = 'model-usage'; static const String promptUsage = 'prompt-usage'; static const String operateSuccess = 'operate-success'; static const String operateFailed = 'operate-failed'; static const String confirmDelete = 'confirm-delete'; static const String confirmStartNewChat = 'confirm-start-new-chat'; static const String confirmClearMessages = 'confirm-clear-messages'; static const String quotaExceeded = 'quota-exceeded'; static const String internalServerError = 'internal-server-error'; static const String badGateway = 'bad-gateway'; static const String emptyResponse = 'empty-response'; static const String modelNotValid = 'model-not-valid'; static const String signInRequired = 'sign-in-required'; static const String accountNeedReSignin = 'account-need-re-signin'; static const String confirmToDeleteRoom = 'confirm-to-delete-room'; static const String confirmSend = 'confirm-send'; static const String openAIAuthFailed = 'openai-auth-failed'; static const String modelNotFound = 'model-not-found'; static const String nameRequiredMessage = 'name-required-message'; static const String modelRequiredMessage = 'model-required-message'; static const String charactorPromptRequiredMessage = 'charactor-prompt-required-message'; static const String writeYourIdeas = 'write-your-ideas'; static const String describeYourImages = 'describe-your-images'; static const String excludeContents = 'exclude-contents'; static const String autoTranslateToEnglish = 'auto-translate-to-english'; static const String auto = 'auto'; static const String style = 'style'; static const String imageCount = 'image-count'; static const String imageSize = 'image-size'; static const String wordCount = 'word-count'; static const String generate = 'generate'; static const String processingWait = 'processing-wait'; static const String contentIsRequired = 'content-is-required'; static const String wordCountInvalid = 'word-count-invalid'; static const String generateTimeout = 'generate-timeout'; static const String creativeIslandNeedSignIn = 'creative-island-need-sign-in'; static const String generateResult = 'generate-result'; static const String generateFailed = 'generate-failed'; static const String generating = 'generating'; static const String generateExitConfirm = 'generate-exit-confirm'; static const String tooManyRequests = 'too-many-requests'; static const String tooManyRequestsOrPaymentRequired = 'too-many-requests-or-payment-required'; static const String promptHint = 'prompt-hint'; static const String confirmClearCache = 'confirm-clear-cache'; static const String confirmSignOut = 'confirm-sign-out'; static const String askMeAnyQuestion = 'ask-me-any-question'; static const String askMeLikeThis = 'ask-me-like-this'; static const String refresh = 'refresh'; static const String fastAndCostEffective = 'fast-and-cost-effective'; static const String powerfulAndPrecise = 'powerful-and-precise'; static const String imageToImage = 'image-to-image'; static const String imageToVideo = 'image-to-video'; static const String textToImage = 'text-to-image'; static const String hdRestoration = 'hd-restoration'; static const String yourIdeas = 'your-ideas'; static const String smartOptimization = 'smart-optimization'; static const String professionalMode = 'professional-mode'; static const String simpleMode = 'simple-mode'; static const String unwantedElements = 'unwanted-elements'; static const String referenceImage = 'reference-image'; static const String selectImage = 'select-image'; static const String imagination = 'imagination'; static const String keywordsSeparatedByCommas = 'keywords-separated-by-commas'; static const String originalImage = 'original-image'; static const String superResolution = 'super-resolution'; static const String colorizeImage = 'colorize-image'; static const String errorLog = 'error-log'; static const String report = 'report'; static const String latestVersion = 'latest-version'; static const String aIdeaApp = 'aidea-app'; static const String onceEnabledSmartOptimization = 'once-enabled-smart-optimization'; static const String gotIt = 'got-it'; static const String referenceImageNote = 'reference-image-note'; static const String selectReferenceImage = 'select-reference-image'; static const String random = 'random'; static const String followSystem = 'follow-system'; static const String darkThemeMode = 'dark-theme-mode'; static const String lightThemeMode = 'light-theme-mode'; static const String forgotPassword = 'forgot-password'; static const String createAccount = 'create-account'; static const String useAsClient = 'use-as-client'; static const String signInWithApple = 'sign-in-with-apple'; static const String readAndAgree = 'read-and-agree'; static const String andWord = 'and-word'; static const String accountInputTips = 'account-input-tips'; static const String phoneInputTips = 'phone-input-tips'; static const String passwordInputTips = 'password-input-tips'; static const String pleaseReadAgreeProtocol = 'please-read-agree-protocol'; static const String signInSuccess = 'sign-in-success'; static const String signInFailed = 'sign-in-failed'; static const String accountRequired = 'account-required'; static const String accountFormatError = 'account-format-error'; static const String phoneNumberFormatError = 'phone-number-format-error'; static const String passwordRequired = 'password-required'; static const String passwordFormatError = 'password-format-error'; static const String accountCreated = 'account-created'; static const String sendVerifyCode = 'send-verify-code'; static const String verify = 'verify'; static const String verifyCode = 'verify-code'; static const String verifyCodeInputTips = 'verify-code-input-tips'; static const String retryInSeconds = 'retry-in-seconds'; static const String verifyCodeSendSuccess = 'verify-code-send-success'; static const String pleaseGetVerifyCodeFirst = 'please-get-verify-code-first'; static const String verifyCodeRequired = 'verify-code-required'; static const String verifyCodeFormatError = 'verify-code-format-error'; static const String phone = 'phone'; static const String email = 'email'; static const String directSigninDueHasAccount = 'direct-signin-due-has-account'; static const String directSignin = 'direct-signin-due-no-account'; static const String passwordResetOK = 'password-reset-ok'; static const String resetPassword = 'reset-password'; static const String bindPhone = 'bind-phone'; static const String bind = 'bind'; static const String bound = 'bond'; static const String bindExAccount = 'bind-ex-account'; static const String unbind = 'unbind'; static const String inviteCode = 'invite-code'; static const String inviteCodeInputTips = 'invite-code-input-tips'; static const String inviteCodeFormatError = 'invite-code-format-error'; static const String enableCustomOpenAI = 'enable-custom-openai'; static const String me = 'me'; static const String creditsUsage = 'credits-usage'; static const String creditUsageTips = 'credit-usage-tips'; static const String updateCheck = 'update-check'; static const String buy = 'buy'; static const String paymentHistory = 'payment-history'; static const String buyCredits = 'buy-credits'; static const String creditUnit = 'credit-unit'; static const String toPay = 'to-pay'; static const String discover = 'discover'; static const String customHomeModels = 'custom-home-models'; static const String userApiKeys = "user-api-keys"; static const String others = 'others'; static const String recentlyUsed = 'recently-used'; static const String visionTag = 'vision-tag'; static const String newTag = 'new-tag'; static const String recommendTag = 'recommend-tag'; static const String imageUploading = 'image-uploading'; static const String uploadImageLimit4 = 'upload-image-limit-4'; static const String confirmStopOutput = 'confirm-stop-output'; static const String stopOutput = 'stop-output'; static const String opensource = 'opensource'; static const String socialMedia = 'social-media'; static const String unset = 'unset'; static const String nickname = 'nickname'; static const String setNickname = 'set-nickname'; static const String inputYourNickname = 'input-your-nickname'; static const String reset = 'reset'; static const String deleteAccount = 'delete-account'; static const String confirmDeleteAccount = 'confirm-delete-account'; static const String wechatAccount = 'wechat-account'; static const String modifyPassword = 'modify-password'; static const String setPassword = 'set-password'; static const String installWeChat = 'install-wechat'; static const String freeQuota = 'free-quota'; static const String serviceStatus = 'service-status'; static const String lab = 'lab'; static const String todayLeft = 'today-left'; static const String freeModelNeedSignIn = 'free-model-need-sign-in'; static const String noFreeModel = 'no-free-model'; static const String freeModelInfo = 'free-model-info'; static const String notification = 'notification'; static const String selectMember = 'select-member'; static const String members = 'members'; static const String createGroupChat = 'create-group-chat'; static const String advanced = 'advanced'; static const String collapseOptions = 'collapse-options'; static const String welcomeMessage = 'welcome-message'; static const String welcomeMessageTips = 'welcome-message-tips'; static const String memoryDepth = 'memory-depth'; static const String robotRecommand = 'robot-recommand'; static const String pickYourRobot = 'pick-your-robot'; static const String viewMore = 'view-more'; static const String using = 'using'; static const String inviteCodeShare = 'invite-code-share'; static const String shareToWechatQ = 'share-to-wechat-q'; static const String shareToWechat = 'share-to-wechat'; static const String shareToOtherApps = 'share-to-other-apps'; static const String clickToShareWithExpire = 'click-to-share-with-expire'; static const String shortcut = 'shortcut'; static const String selectImageToShortcut = 'select-image-to-shortcut'; static const String selectShortcutAction = 'select-shortcut-action'; static const String makeSameStyle = 'make-same-style'; static const String saveToLocal = 'save-to-local'; static const String showInviteCode = 'show-invite-code'; static const String dontShowInviteCode = 'dont-show-invite-code'; static const String inviteNow = 'invite-now'; static const String inviteSlogan = 'invite-slogan'; static const String preview = 'preview'; static const String download = 'download'; static const String clickSwitchImage = 'click-switch-image'; static const String startNewChatTips = 'start-new-chat-tips'; static const String wantMoreContentTips = 'want-more-content-tips'; static const String unbilled = 'unbilled'; static const String signinNow = 'signin-now'; static const String needSigninToUse = 'need-signin-to-use'; static const String reSignIn = 're-sign-in'; static const String ideaPrompt = 'idea-prompt'; static const String groupChat = 'group-chat'; static const String selectGroupMembers = 'select-group-members'; static const String selectPaymentMethod = 'select-payment-method'; static const String validDays = 'valid-days'; static const String clickToReSignin = 'click-to-resignin'; static const String free = 'free'; static const String input = 'input'; static const String output = 'output'; static const String perRequest = 'per-request'; static const String perSearch = 'per-search'; static const String info = 'info'; static const String recently = 'recently'; static const String daysAgo = 'days-ago'; static const String lastWeek = 'last-week'; static const String weeksAgo = 'weeks-ago'; static const String lastMonth = 'last-month'; static const String monthsAgo = 'months-ago'; static const String lastYear = 'last-year'; static const String longTimeAgo = 'long-time-ago'; static const String modelNeedSignIn = 'model-need-sign-in'; static const String wechatBindConfirm = 'wechat-bind-confirm'; static const String accountWillBeCreateAutomatically = 'account-will-be-create-automatically'; static const String installWechatFirst = 'install-wechat-first'; static const String otherLoginMethods = 'other-login-methods'; static const String verifyAccount = 'verify-account'; static const String enterPasswordToSignin = 'enter-password-to-signin'; static const String verifyCodeLogin = 'verify-code-login'; static const String verifyCodeLoginTips = 'verify-code-login-tips'; static const String usePasswordToSignin = 'use-password-to-signin'; static const String referenceDocuments = 'reference-documents'; static const String welcomeToAskMe = 'welcome-to-ask-me'; static const String startChat = 'start-chat'; static const String updateApp = 'update-app'; static const String notUpdateApp = 'not-update-app'; static const String searchedXWebPages = 'searched-x-web-pages'; static const Map zh = { wechat: '微信', required: '必填', systemInfo: '系统信息', save: '保存', ok: '确定', cancel: '取消', select: '选择', tips: '提示', goodTips: '温馨提示', basicInfo: '基本信息', delete: '删除', edit: '编辑', selectAll: '全选', unselectAll: '取消全选', share: '分享', cancelShare: '取消分享', histories: '最近的对话', moreHistories: '更多历史对话', enable: '启用', disable: '未启用', newChat: '新对话', clearChatHistory: '清空聊天记录', examples: '示例', continueMessage: '继续', messageInputTips: '有问题尽管问我', takePhoto: '拍照', photoLibrary: '附加照片', fileLibrary: '附加文档', upload: '上传', longPressSpeak: '长按说话', send: '发送', sendRetry: '重新发送', sendRetryS: '重发', selectText: '选择文本', text: '文本', uploading: '上传中...', robotIsThinkingMessage: '正在思考中,请耐心等待', thinkingProcess: '深度思考', robotIsSearchingMessage: '正在联网搜索', robotHasSomeError: '发送失败,重发该消息?', appName: 'AIdea', chatAnywhere: '聊一聊', homeTitle: '自定义角色', creativeIsland: '创作岛', settings: '设置', configure: '配置', language: '语言', themeMode: '主题外观', accountInfo: '账号信息', accountSettings: '账号设置', usage: '智慧果', validBefore: '有效期至', custom: '自定义', clearCache: '清空缓存', about: '关于', diagnostic: '故障诊断', userTerms: '用户协议', privacyPolicy: '隐私政策', signIn: '登录', signInAccount: '登录解锁完整功能', signOut: '退出登录', signUp: '注册', password: '密码', passwordConfirm: '确认密码', retrievePassword: '找回密码', newPassword: '新密码', account: '账号', usedUp: '已用完', expired: '已过期', timeConsume: '用时', character: '角色', myCharacters: '我的角色', createRoom: '创建角色', model: 'AI 模型', selectModel: '选择模型', roomName: '名称', avatar: '头像', iconName: '图标', prompt: '角色设定', optional: '可选', search: '搜索', onlineSearch: '联网搜索', reasoning: '深度思考', background: '背景', backgroundSetting: '背景图', roomSetting: '设置', chatHistory: '聊天记录', confirmSend: '确定发送以下内容?', questionExamples: '问题示例', noRecords: '暂无记录', contextBreakMessage: '~ 以下是新的对话 ~', translateFinished: '翻译完成', textCopied: '已复制到剪贴板', copy: '复制', translate: '翻译', hide: '隐藏', readByVoice: '朗读', unknownFile: '未知文件', switchModel: '切换对话模型', switchModelTitle: '选择要切换的对话模型', noMessageSelected: '没有选择任何消息', modelUsage: '模型用于设置采用的 AI 角色类型', promptUsage: '领域设定用于设置 AI 角色的行为', nameRequiredMessage: '请输入名称', modelRequiredMessage: '请选择 AI 模型', charactorPromptRequiredMessage: '请输入角色设定', operateSuccess: '操作成功', operateFailed: '操作失败', confirmDelete: '确定删除?', confirmStartNewChat: '确定要开始新的对话?', confirmClearMessages: '确定要清空聊天记录?', quotaExceeded: '智慧果数量不足,请先购买', internalServerError: '服务器故障,请稍后再试', badGateway: '抱歉,我们的服务器目前无法处理您的请求。我们正在努力解决问题,请您稍后重试', modelNotValid: '当前模型暂未开放', signInRequired: '您尚未登录,请先登录', accountNeedReSignin: '账号异常,请重新登录', openAIAuthFailed: '您启用了自定义 OpenAI 服务,请检查 API Key 是否正确', modelNotFound: '当前模型尚未开通,暂时无法使用', confirmToDeleteRoom: '确定删除?', writeYourIdeas: '你的想法', describeYourImages: '你的想法', excludeContents: '反向提示词', autoTranslateToEnglish: '自动翻译为英文', auto: '自动', style: '风格', imageCount: '图片数量', imageSize: '图片比例', wordCount: '生成字数', generate: '开始创作', processingWait: '处理中,请稍后...', contentIsRequired: '请输入创作内容', wordCountInvalid: '字数不能超过 1000', generateTimeout: '创作超时,请稍后再试', creativeIslandNeedSignIn: '登录后解锁更多玩法~', generateResult: '创作结果', generateFailed: '创作失败', generating: '创作中...', generateExitConfirm: '创作中...\n退出后,可在历史记录中查看结果', tooManyRequests: '操作过于频繁,请稍后再试', tooManyRequestsOrPaymentRequired: '操作过于频繁(如果您使用了自定义的 OpenAI Keys,请登录 https://platform.openai.com 检查账户余额是否充足)', promptHint: '设定角色和技能,以便为你提供更精准有效的信息。', confirmClearCache: '确定要清除缓存吗?', confirmSignOut: '确定要退出登录吗?', askMeAnyQuestion: '消息', askMeLikeThis: '可以这样问我:', refresh: '换一批', fastAndCostEffective: '速度快,成本低', powerfulAndPrecise: '能力强,更精准', imageToImage: '图生图', imageToVideo: '图生视频', textToImage: '文生图', hdRestoration: '高清修复', yourIdeas: '你的想法', smartOptimization: '智能优化', professionalMode: '专业模式', simpleMode: '简单模式', unwantedElements: '''画面中不希望出现的元素或效果,如 “汽车,毛茸茸,低分辨率,模糊” 等。''', referenceImage: '参考图片', selectImage: '选择图片', imagination: '想象力', keywordsSeparatedByCommas: '你想象画面的关键词,以逗号隔开。', originalImage: '原始图片', superResolution: '高清修复', colorizeImage: '旧照片上色', errorLog: '故障诊断', report: '上报', latestVersion: '当前已是最新版本', aIdeaApp: 'AIdea是一款能够让你与 AI 对话的应用,应用代码完全开源。', onceEnabledSmartOptimization: '智能优化\n\n启用后,AI 将进一步完善优化你的想法。', referenceImageNote: '参考图片\n\nAI 将会在该参考图片的基础上进行创作。', gotIt: '知道了', selectReferenceImage: '请选择参考图片', random: '随机', followSystem: '跟随系统', darkThemeMode: '深色模式', lightThemeMode: '浅色模式', forgotPassword: '忘记密码', createAccount: '注册账号', useAsClient: '仅作为 OpenAI 客户端使用', signInWithApple: '使用 Apple 账号登录', readAndAgree: '已阅读并同意', andWord: '和', accountInputTips: '输入您的手机或邮箱账号', phoneInputTips: '输入您的手机号', passwordInputTips: '输入您的密码', pleaseReadAgreeProtocol: '请先阅读并同意用户协议和隐私条款', signInSuccess: '登录成功', signInFailed: '登录失败', accountRequired: '请输入账号', accountFormatError: '账号格式有误\n请输入手机号或邮箱账号', phoneNumberFormatError: '手机号码格式有误', passwordRequired: '请输入密码', passwordFormatError: '密码格式有误\n必须为8-20位字母、数字、特殊符号组合', accountCreated: '账号创建成功', sendVerifyCode: '发送', verify: '验证', verifyCode: '验证码', verifyCodeInputTips: '输入验证码', retryInSeconds: '秒后重试', verifyCodeSendSuccess: '验证码已发送', pleaseGetVerifyCodeFirst: '请先获取验证码', verifyCodeRequired: '请输入验证码', verifyCodeFormatError: '验证码格式有误', phone: '手机', email: '邮箱', directSigninDueHasAccount: '已有账号?直接登录', directSignin: '直接登录', passwordResetOK: '密码已重置,请重新登录', resetPassword: '重置密码', bindPhone: '绑定手机', bind: '绑定', bound: '已绑定', bindExAccount: '绑定已有账号', unbind: '解绑', inviteCode: '邀请码', inviteCodeInputTips: '输入好友邀请码,获额外奖励(非必填)', inviteCodeFormatError: '邀请码格式有误', enableCustomOpenAI: '启用后将使用您自己配置的 OpenAI 服务', me: '我的', creditsUsage: '使用明细', creditUsageTips: '使用明细将在次日更新,显示近 30 天的使用量。', updateCheck: '检测更新', buy: '购买', paymentHistory: '购买历史', buyCredits: '购买智慧果', creditUnit: '¢', toPay: '立即支付', discover: '绘玩', customHomeModels: '常用模型', userApiKeys: 'API Keys', others: '其它', recentlyUsed: '最近使用', visionTag: '视觉', newTag: '上新', recommendTag: '推荐', imageUploading: '正在上传图片,请稍后...', uploadImageLimit4: '最多只能上传 4 张图片', confirmStopOutput: '确定要停止当前输出?', stopOutput: '停止输出', opensource: '本项目开源,欢迎贡献', socialMedia: '关注我们', unset: '未设置', nickname: '昵称', setNickname: '设置昵称', inputYourNickname: '请输入你的昵称', reset: '重置', deleteAccount: '删除账号', confirmDeleteAccount: '确定要删除账号', wechatAccount: '微信账号', modifyPassword: '修改密码', setPassword: '设置密码', installWeChat: '请先安装微信后再使用该功能', freeQuota: '免费畅享额度', serviceStatus: '服务状态', lab: '实验室', todayLeft: '今日可用', freeModelNeedSignIn: '免费模型需登录账号后使用', noFreeModel: '当前无可用的免费模型。', freeModelInfo: '以下模型享有每日免费额度。', notification: '通知', selectMember: '选择本次对话成员', members: '成员', createGroupChat: '创建群聊', advanced: '更多选项', collapseOptions: '收起选项', welcomeMessage: '引导语', welcomeMessageTips: '每次开始新对话时,系统将会以 AI 的身份自动发送引导语。', memoryDepth: '记忆深度', robotRecommand: '热门推荐', pickYourRobot: '挑选你的专属伙伴', viewMore: '查看更多', using: '使用中', inviteCodeShare: '邀请码分享', shareToWechatQ: '分享到朋友圈', shareToWechat: '分享到微信', shareToOtherApps: '分享到其它应用', clickToShareWithExpire: '点击图片可分享、保存;有效期至', shortcut: '动作', selectImageToShortcut: '选择要执行动作的图片', selectShortcutAction: '选择要执行的操作', makeSameStyle: '制作同款', saveToLocal: '保存到本地', showInviteCode: '显示邀请信息', dontShowInviteCode: '不显示邀请信息', inviteNow: '立即邀请', inviteSlogan: '邀请好友注册,双方都可获得奖励', preview: '预览', download: '下载', clickSwitchImage: '点击此处更换图片', startNewChatTips: '想要开启新的聊天?试试', wantMoreContentTips: '想要更多内容?试着对我说', unbilled: '未出账', signinNow: '立即登录', needSigninToUse: '该功能需要登录账号后使用', reSignIn: '重新登录', ideaPrompt: '想法', groupChat: '群聊', selectGroupMembers: '选择参与群聊的成员', selectPaymentMethod: '请选择支付方式', validDays: '内有效', clickToReSignin: '点击此处重新登录', free: '限免', input: '输入', output: '输出', perRequest: '每次', perSearch: '每次搜索', info: '详情', recently: '最近', daysAgo: '天前', lastWeek: '上周', weeksAgo: '周前', lastMonth: '上个月', monthsAgo: '月前', lastYear: '去年', longTimeAgo: '很久以前', modelNeedSignIn: '该模型需要登录后使用', wechatBindConfirm: '该微信未绑定任何账号,是否直接登录?\n(自动创建账号)', accountWillBeCreateAutomatically: '未注册的账号验证成功后将自动注册', installWechatFirst: '请先安装微信后再使用该功能', otherLoginMethods: '其它登录方式', verifyAccount: '验证账号', enterPasswordToSignin: '请输入密码完成登录。', verifyCodeLogin: '验证码登录', verifyCodeLoginTips: '请输入验证码以完成操作。', usePasswordToSignin: '使用密码登录', referenceDocuments: '参考文档', welcomeToAskMe: '我可以帮你答疑、写作,请问我吧!', startChat: '开始对话', updateApp: '去更新', notUpdateApp: '暂不更新', searchedXWebPages: '已搜索到 %s 个网页', }; static const Map en = { required: 'Required', wechat: 'WeChat', systemInfo: 'System', save: 'Save', ok: 'OK', cancel: 'Cancel', select: 'Select', tips: 'Tips', goodTips: 'Tips', basicInfo: 'Basic', delete: 'Delete', edit: 'Edit', selectAll: 'Select all', unselectAll: 'Cancel', share: 'Share', cancelShare: 'Cancel share', histories: 'Recents', moreHistories: 'More Histories', enable: 'Enable', disable: 'Disable', newChat: 'New Chat', clearChatHistory: 'Clear Chat Histories', examples: 'Examples', continueMessage: 'Continue', messageInputTips: 'Ask me something...', takePhoto: 'Take Photo', photoLibrary: 'Attach Photos', fileLibrary: 'Attach Files', upload: 'Upload', longPressSpeak: 'Long press to speak', send: 'Send', sendRetry: 'Retry', sendRetryS: 'Retry', selectText: 'Select Text', text: 'Text', uploading: 'Uploading...', robotIsThinkingMessage: 'Thinking...', thinkingProcess: 'Deep thought', robotIsSearchingMessage: 'Searching', robotHasSomeError: 'There seems to be something wrong, Do you want to resend the message?', appName: 'AIdea', chatAnywhere: 'Chat', homeTitle: 'Characters', creativeIsland: 'Creative', settings: 'Setting', configure: 'Configure', language: 'Language', themeMode: 'Theme', accountInfo: 'Account Info', accountSettings: 'Account Settings', usage: 'Credits', validBefore: 'Valid Before', custom: 'Custom', clearCache: 'Clear Cache', about: 'About', diagnostic: 'Diagnostic', userTerms: 'User Terms', privacyPolicy: 'Privacy Policy', signIn: 'Sign In', signInAccount: 'Unlock Full Features', signOut: 'Sign Out', signUp: 'Sign Up', password: 'Password', passwordConfirm: 'Confirm Password', retrievePassword: 'Forgot Password', newPassword: 'New Password', account: 'Account', usedUp: 'Used Up', expired: 'Expired', timeConsume: 'Cost', character: 'Character', myCharacters: 'My Characters', createRoom: 'Create Character', model: 'AI Model', selectModel: 'Select Model', roomName: 'Name', avatar: 'Avatar', iconName: 'Icon', prompt: 'Prompt', optional: 'Optional', search: 'Search', onlineSearch: 'Search', reasoning: 'DeepThink', background: 'Background', backgroundSetting: 'Background Setting', roomSetting: 'Setting', chatHistory: 'Histories', confirmSend: 'Confirm to send?', questionExamples: 'Question Examples', noRecords: 'No Records', contextBreakMessage: '~ Context cleared ~', translateFinished: 'Translate finished', textCopied: 'Text copied', copy: 'Copy', translate: 'Translate', hide: 'Hide', readByVoice: 'Voice', unknownFile: 'Unknown File', switchModel: 'Switch Model', switchModelTitle: 'Switch model', noMessageSelected: 'No message selected', modelUsage: 'The model is used to set the type of AI character used', promptUsage: 'Prompt is used to set the behavior of the AI character', nameRequiredMessage: 'Please enter the name of the character', modelRequiredMessage: 'Please select AI model', charactorPromptRequiredMessage: 'Please enter the charactor prompt', operateSuccess: 'Success', operateFailed: 'Failed', confirmDelete: 'Confirm to delete?', confirmStartNewChat: 'Confirm to start a new chat?', confirmClearMessages: 'Confirm to clear chat histories?', quotaExceeded: 'Insufficient credits, please purchase first', internalServerError: 'Internal server error, please try again later', badGateway: 'Sorry, our server is currently unable to process your request. We are working to resolve the issue, please try again later', modelNotValid: 'The current model is not open', signInRequired: 'You are not logged in, please log in first', accountNeedReSignin: 'Account exception, please log in again', openAIAuthFailed: 'You have enabled custom OpenAI service, please check if the API Key is correct', modelNotFound: 'The current model is not enabled yet, please try again later', confirmToDeleteRoom: 'Confirm to delete the character?', writeYourIdeas: 'Your ideas', describeYourImages: 'Your ideas', excludeContents: 'Negative prompts', autoTranslateToEnglish: 'Auto translate to English', auto: 'Auto', style: 'Style', imageCount: 'Image count', imageSize: 'Image size', wordCount: 'Word count', generate: 'Generate', processingWait: 'Processing, please wait...', contentIsRequired: 'Content is required', wordCountInvalid: 'Word count cannot exceed 1000', generateTimeout: 'Generate timeout, please try again later', creativeIslandNeedSignIn: 'Unlock more features after login', generateResult: 'Generate result', generateFailed: 'Creation failed', generating: 'Generating...', generateExitConfirm: 'Generating...\nYou can view the result in the history', tooManyRequests: 'Too many requests, please try again later', tooManyRequestsOrPaymentRequired: 'Too many requests (If you are using your own OpenAI Keys, please log in to https://platform.openai.com to check if your account balance is sufficient)', promptHint: 'Set the role and skills of the character so that it can provide more accurate and effective information for you.', confirmClearCache: 'Confirm to clear cache?', confirmSignOut: 'Confirm to sign out?', askMeAnyQuestion: 'Message', askMeLikeThis: 'You can ask me like this:', refresh: 'Refresh', fastAndCostEffective: 'Fast & Cost-Effective', powerfulAndPrecise: 'Powerful & Precise', imageToImage: 'Image to Image', imageToVideo: 'Image to Video', textToImage: 'Text to Image', hdRestoration: 'HD Restoration', yourIdeas: 'Your Ideas', smartOptimization: 'Smart Optimization', professionalMode: 'Pro Mode', simpleMode: 'Simple Mode', unwantedElements: '''Elements or effects you don't want to appear in the picture, such as 'cars, fluffy, low resolution, blurry,' etc.''', referenceImage: 'Reference Image', selectImage: 'Select Image', imagination: 'Imagination', keywordsSeparatedByCommas: 'Keywords of the scene you imagine, separated by commas', originalImage: 'Original Image', superResolution: 'Super-Resolution', colorizeImage: 'Colorize Image', errorLog: 'Error Log', report: 'Report', latestVersion: 'You are currently on the latest version', aIdeaApp: 'AIdea is an app that allows you to converse with AI, and the app code is completely open source.', onceEnabledSmartOptimization: 'Smart Optimization\n\nOnce enabled, AI will further refine and optimize your ideas.', gotIt: 'Got it', referenceImageNote: 'Reference Image\n\nAI will create based on the reference image provided.', selectReferenceImage: 'Please select a reference image', random: 'Random', followSystem: 'Follow System', darkThemeMode: 'Dark mode', lightThemeMode: 'Light mode', forgotPassword: 'Forgot password', createAccount: 'Create account', useAsClient: 'Use as OpenAI client', signInWithApple: 'Sign in with Apple', readAndAgree: 'Read and agree', andWord: 'and', accountInputTips: 'Phone number or email', phoneInputTips: 'Phone number', passwordInputTips: 'Password', pleaseReadAgreeProtocol: 'Please read and agree to the user agreement and privacy policy first', signInSuccess: 'Sign in success', signInFailed: 'Sign in failed', accountRequired: 'Please enter your account', accountFormatError: 'Account format error\nPlease enter your phone number or email', phoneNumberFormatError: 'Phone number format error', passwordRequired: 'Please enter your password', passwordFormatError: 'Password format error\nMust be 8-20 digits, letters, special characters', accountCreated: 'Account created', sendVerifyCode: 'Send', verify: 'Verify', verifyCode: 'Verify code', verifyCodeInputTips: 'Enter verify code', retryInSeconds: 'Retry in', verifyCodeSendSuccess: 'Verify code has been sent', pleaseGetVerifyCodeFirst: 'Please get the verification code first', verifyCodeRequired: 'Please enter the verification code', verifyCodeFormatError: 'Verification code format error', phone: 'Phone', email: 'Email', directSigninDueHasAccount: 'Already have an account? Sign in directly', directSignin: 'Sign in directly', passwordResetOK: 'Password has been reset, please log in again', resetPassword: 'Reset password', bindPhone: 'Bind phone', bind: 'Bind', bound: 'Bound', bindExAccount: 'Bind existing account', unbind: 'Unbind', inviteCode: 'Invite code', inviteCodeInputTips: 'Enter friend invite code, get extra rewards (optional)', inviteCodeFormatError: 'Invite code format error', enableCustomOpenAI: 'Your custom OpenAI service will be used once enabled', me: 'Me', creditsUsage: 'Usage', creditUsageTips: 'Usage details will be updated the next day, showing usage in the last 30 days.', updateCheck: 'Check Update', buy: 'Buy', paymentHistory: 'Histories', buyCredits: 'Buy Credits', creditUnit: '¢', toPay: 'Create Order', discover: 'Discover', customHomeModels: 'Favorite Models', userApiKeys: 'API Keys', others: 'Others', recentlyUsed: 'Recently Used', visionTag: 'Vision', newTag: 'New', recommendTag: 'Recommend', imageUploading: 'Uploading image, please wait...', uploadImageLimit4: 'You can only upload up to 4 images', confirmStopOutput: 'Are you sure you want to stop current output?', stopOutput: 'Stop Output', opensource: 'Open Source', socialMedia: 'Follow us', unset: 'Unset', nickname: 'Nickname', setNickname: 'Set Nickname', inputYourNickname: 'Input your nickname', reset: 'Reset', deleteAccount: 'Delete Account', confirmDeleteAccount: 'Confirm to delete account', wechatAccount: 'WeChat Account', modifyPassword: 'Modify Password', setPassword: 'Set Password', installWeChat: 'Please install WeChat first', freeQuota: 'Free Quota', serviceStatus: 'Service Status', lab: 'Lab', todayLeft: 'Today Available', freeModelNeedSignIn: 'Free model requires login to use', noFreeModel: 'No free model available.', freeModelInfo: 'The following models have daily free quotas.', notification: 'Notification', selectMember: 'Select member', members: 'Members', createGroupChat: 'Create group chat', advanced: 'More Options', collapseOptions: 'Collapse', welcomeMessage: 'Welcome Message', welcomeMessageTips: 'The system will automatically send a welcome message each time a new chat is started.', memoryDepth: 'Memory Depth', robotRecommand: 'Recommand', pickYourRobot: 'Pick your robot', viewMore: 'More', using: 'Using', inviteCodeShare: 'Invite Code Share', shareToWechatQ: 'Share to WeChat Moments', shareToWechat: 'Share to WeChat', shareToOtherApps: 'Share to Other Apps', clickToShareWithExpire: 'Click to share, valid until', shortcut: 'Action', selectImageToShortcut: 'Select image to perform action', selectShortcutAction: 'Select the action to perform', makeSameStyle: 'Make the Same', saveToLocal: 'Save to Local', showInviteCode: 'Show Invite Code', dontShowInviteCode: 'Don\'t Show Invite Code', inviteNow: 'Invite Now', inviteSlogan: 'Invite friends to register, both parties will receive rewards', preview: 'Preview', download: 'Download', clickSwitchImage: 'Click to switch image', startNewChatTips: 'Want to start a new chat? Try', wantMoreContentTips: 'Want more content? Try', unbilled: 'Pending', signinNow: 'Sign in now', needSigninToUse: 'Please login first', reSignIn: 'Re-login', ideaPrompt: 'Prompt', groupChat: 'Group', selectGroupMembers: 'Select group members', selectPaymentMethod: 'Select payment method', validDays: 'expiration', clickToReSignin: 'Click here to sign in again', free: 'Free', input: 'Input', output: 'Output', perRequest: 'Per', perSearch: 'Per Search', info: 'Detail', recently: 'Recently', lastWeek: 'Last Week', lastMonth: 'Last Month', daysAgo: 'Days Ago', monthsAgo: 'Months Ago', lastYear: 'Last Year', longTimeAgo: 'Long Time Ago', weeksAgo: 'Weeks Ago', modelNeedSignIn: 'The model needs to be signed in to use', wechatBindConfirm: 'The WeChat is not bound to any account, whether to sign in directly?\n(Automatically create an account)', accountWillBeCreateAutomatically: 'Account will be created automatically', installWechatFirst: 'Please install WeChat first', otherLoginMethods: 'Other Login Methods', verifyAccount: 'Verify Account', enterPasswordToSignin: 'Please enter the password to sign in.', verifyCodeLogin: 'Use Verify Code', verifyCodeLoginTips: 'Please enter the verify code to complete the operation.', usePasswordToSignin: 'Use Password', referenceDocuments: 'Reference Documents', welcomeToAskMe: 'How can I help you today?', startChat: 'Start Chat', updateApp: 'Update Now', notUpdateApp: 'Not Update', searchedXWebPages: 'Searched %s web pages', }; } class LanguageText { final String message; final String? action; const LanguageText(this.message, {this.action}); @override String toString() { return message; } } final languages = { 'zh_Hans_': 'zh-CHS', 'en': 'en', }; String resolveSystemLanguage(String deviceLocale) { for (var key in languages.keys) { if (deviceLocale.startsWith(key)) { return languages[key]!; } } return 'en'; } ================================================ FILE: lib/main.dart ================================================ import 'package:askaide/bloc/admin_payment_bloc.dart'; import 'package:askaide/bloc/admin_room_bloc.dart'; import 'package:askaide/bloc/channel_bloc.dart'; import 'package:askaide/bloc/model_bloc.dart'; import 'package:askaide/bloc/user_bloc.dart'; import 'package:askaide/helper/path.dart'; import 'package:askaide/page/admin/channels.dart'; import 'package:askaide/page/admin/channels_add.dart'; import 'package:askaide/page/admin/channels_edit.dart'; import 'package:askaide/page/admin/dashboard.dart'; import 'package:askaide/page/admin/messages.dart'; import 'package:askaide/page/admin/models.dart'; import 'package:askaide/page/admin/models_add.dart'; import 'package:askaide/page/admin/models_edit.dart'; import 'package:askaide/page/admin/payments.dart'; import 'package:askaide/page/admin/recently_messages.dart'; import 'package:askaide/page/admin/rooms.dart'; import 'package:askaide/page/admin/user.dart'; import 'package:askaide/page/admin/users.dart'; import 'package:askaide/page/balance/web_payment_proxy.dart'; import 'package:askaide/page/balance/web_payment_result.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/creative_island/draw/artistic_wordart.dart'; import 'package:askaide/page/home.dart'; import 'package:flutter_phoenix/flutter_phoenix.dart'; import 'package:path/path.dart'; import 'package:askaide/bloc/account_bloc.dart'; import 'package:askaide/bloc/background_image_bloc.dart'; import 'package:askaide/bloc/chat_chat_bloc.dart'; import 'package:askaide/bloc/creative_island_bloc.dart'; import 'package:askaide/bloc/free_count_bloc.dart'; import 'package:askaide/bloc/gallery_bloc.dart'; import 'package:askaide/bloc/group_chat_bloc.dart'; import 'package:askaide/bloc/payment_bloc.dart'; import 'package:askaide/bloc/user_api_keys_bloc.dart'; import 'package:askaide/bloc/version_bloc.dart'; import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/cache.dart'; import 'package:askaide/helper/logger.dart'; import 'package:askaide/helper/model.dart'; import 'package:askaide/helper/model_resolver.dart'; import 'package:askaide/helper/platform.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/data/migrate.dart'; import 'package:askaide/page/balance/quota_usage_details.dart'; import 'package:askaide/page/creative_island/draw/artistic_qr.dart'; import 'package:askaide/page/setting/account_security.dart'; import 'package:askaide/page/lab/avatar_selector.dart'; import 'package:askaide/page/setting/article.dart'; import 'package:askaide/page/setting/background_selector.dart'; import 'package:askaide/page/setting/bind_phone_page.dart'; import 'package:askaide/page/setting/change_password.dart'; import 'package:askaide/page/chat/home_chat.dart'; import 'package:askaide/page/chat/home.dart'; import 'package:askaide/page/chat/home_chat_history.dart'; import 'package:askaide/page/chat/character_create.dart'; import 'package:askaide/page/component/random_avatar.dart'; import 'package:askaide/page/component/transition_resolver.dart'; import 'package:askaide/page/creative_island/my_creation.dart'; import 'package:askaide/page/creative_island/my_creation_item.dart'; import 'package:askaide/page/setting/custom_home_models.dart'; import 'package:askaide/page/balance/free_statistics.dart'; import 'package:askaide/page/chat/group/chat.dart'; import 'package:askaide/page/chat/group/create.dart'; import 'package:askaide/page/chat/group/edit.dart'; import 'package:askaide/page/lab/creative_models.dart'; import 'package:askaide/page/setting/destroy_account.dart'; import 'package:askaide/page/setting/diagnosis.dart'; import 'package:askaide/page/creative_island/draw/draw_list.dart'; import 'package:askaide/page/creative_island/draw/draw_create.dart'; import 'package:askaide/page/creative_island/draw/image_edit_direct.dart'; import 'package:askaide/page/lab/draw_board.dart'; import 'package:askaide/page/creative_island/gallery/gallery.dart'; import 'package:askaide/page/creative_island/gallery/gallery_item.dart'; import 'package:askaide/page/setting/notification.dart'; import 'package:askaide/page/setting/openai_setting.dart'; import 'package:askaide/page/balance/payment.dart'; import 'package:askaide/page/lab/prompt.dart'; import 'package:askaide/page/balance/quota_usage_statistics.dart'; import 'package:askaide/page/auth/signin_or_signup.dart'; import 'package:askaide/page/auth/signin_screen.dart'; import 'package:askaide/page/component/chat/message_state_manager.dart'; import 'package:askaide/page/balance/payment_history.dart'; import 'package:askaide/page/setting/retrieve_password_screen.dart'; import 'package:askaide/page/auth/signup_screen.dart'; import 'package:askaide/page/lab/user_center.dart'; import 'package:askaide/page/setting/user_api_keys.dart'; import 'package:askaide/repo/api/info.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/cache_repo.dart'; import 'package:askaide/repo/creative_island_repo.dart'; import 'package:askaide/repo/data/cache_data.dart'; import 'package:askaide/repo/data/chat_history.dart'; import 'package:askaide/repo/data/creative_island_data.dart'; import 'package:askaide/repo/deepai_repo.dart'; import 'package:askaide/repo/stabilityai_repo.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:fluwx/fluwx.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:askaide/bloc/bloc_manager.dart'; import 'package:askaide/bloc/chat_message_bloc.dart'; import 'package:askaide/bloc/room_bloc.dart'; import 'package:askaide/bloc/notify_bloc.dart'; import 'package:askaide/page/chat/character_edit.dart'; import 'package:askaide/page/chat/character_chat.dart'; import 'package:askaide/page/chat/characters.dart'; import 'package:askaide/page/setting/setting_screen.dart'; import 'package:askaide/repo/data/chat_message_data.dart'; import 'package:askaide/repo/chat_message_repo.dart'; import 'package:askaide/repo/data/room_data.dart'; import 'package:askaide/repo/openai_repo.dart'; import 'package:askaide/repo/data/settings_data.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:sqflite_common_ffi_web/sqflite_ffi_web.dart'; import 'page/component/theme/theme.dart'; import 'package:sizer/sizer.dart'; import 'package:askaide/helper/http.dart' as httpx; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:media_kit/media_kit.dart'; import 'package:bitsdojo_window/bitsdojo_window.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); MediaKit.ensureInitialized(); httpx.HttpClient.init(); // 初始化路径,获取到系统相关的文档、缓存目录 await PathHelper().init(); FlutterError.onError = (FlutterErrorDetails details) { if (details.library == 'rendering library' || details.library == 'image resource service') { return; } Logger.instance.e( details.summary, error: details.exception, stackTrace: details.stack, ); Logger.instance.d(details.stack); }; if (kIsWeb) { databaseFactory = databaseFactoryFfiWeb; } else { if (PlatformTool.isWindows() || PlatformTool.isLinux() || PlatformTool.isMacOS()) { sqfliteFfiInit(); databaseFactory = databaseFactoryFfi; var path = absolute(join(PathHelper().getHomePath, 'databases')); databaseFactory.setDatabasesPath(path); } } // 数据库连接 final db = await databaseFactory.openDatabase( 'system.db', options: OpenDatabaseOptions( version: databaseVersion, onUpgrade: (db, oldVersion, newVersion) async { try { await migrate(db, oldVersion, newVersion); } catch (e) { Logger.instance.e('Database upgrade failure', error: e); } }, onCreate: initDatabase, onOpen: (db) { Logger.instance.i('Database storage path: ${db.path}'); }, ), ); // 加载配置 final settingProvider = SettingDataProvider(db); await settingProvider.loadSettings(); // 创建数据仓库 final settingRepo = SettingRepository(settingProvider); final openAIRepo = OpenAIRepository(settingProvider); final deepAIRepo = DeepAIRepository(settingProvider); final stabilityAIRepo = StabilityAIRepository(settingProvider); final cacheRepo = CacheRepository(CacheDataProvider(db)); final chatMsgRepo = ChatMessageRepository( RoomDataProvider(db), ChatMessageDataProvider(db), ChatHistoryProvider(db), ); final creativeIslandRepo = CreativeIslandRepository(CreativeIslandDataProvider(db)); // 聊天状态加载器 final stateManager = MessageStateManager(cacheRepo); // 初始化聊天实现解析器 ModelResolver.instance.init( openAIRepo: openAIRepo, deepAIRepo: deepAIRepo, stabilityAIRepo: stabilityAIRepo, ); APIServer().init(settingRepo); ModelAggregate.init(settingRepo); Cache().init(settingRepo, cacheRepo); // 从服务器获取客户端支持的能力清单 try { final capabilities = await APIServer().capabilities(cache: false); Ability().init(settingRepo, capabilities); } catch (e) { Logger.instance.e('Failed to get the client capability manifest', error: e); Ability().init( settingRepo, Capabilities( applePayEnabled: true, otherPayEnabled: true, translateEnabled: true, mailEnabled: true, openaiEnabled: true, homeModels: [], homeRoute: '/', showHomeModelDescription: true, supportWebsocket: false, ), ); } // 初始化聊天室 Bloc 管理器 final m = ChatBlocManager(); m.init((roomId, {chatHistoryId}) { return ChatMessageBloc( roomId, chatHistoryId: chatHistoryId, chatMsgRepo: chatMsgRepo, settingRepo: settingRepo, ); }); runApp(Phoenix( child: MyApp( settingRepo: settingRepo, chatMsgRepo: chatMsgRepo, openAIRepo: openAIRepo, cacheRepo: cacheRepo, creativeIslandRepo: creativeIslandRepo, messageStateManager: stateManager, ), )); if (PlatformTool.isDesktop()) { doWhenWindowReady(() { final win = appWindow; const initialSize = Size(850, 750); win.size = initialSize; win.minSize = const Size(350, 650); win.alignment = Alignment.center; win.title = "AIdea"; if (PlatformTool.isWindows()) { WidgetsBinding.instance.scheduleFrameCallback((timeStamp) { appWindow.size = initialSize + const Offset(0, 1); }); } win.show(); }); } } class MyApp extends StatefulWidget { // 页面路由 late final GoRouter _router; // Bloc late final RoomBloc chatRoomBloc; late final GalleryBloc galleryBloc; late final AccountBloc accountBloc; late final VersionBloc versionBloc; late final FreeCountBloc freeCountBloc; final _rootNavigatorKey = GlobalKey(); final FlutterLocalization localization = FlutterLocalization.instance; final MessageStateManager messageStateManager; MyApp({ super.key, required this.settingRepo, required this.chatMsgRepo, required this.openAIRepo, required this.cacheRepo, required this.creativeIslandRepo, required this.messageStateManager, }) { chatRoomBloc = RoomBloc(chatMsgRepo: chatMsgRepo, stateManager: messageStateManager); accountBloc = AccountBloc(settingRepo); versionBloc = VersionBloc(); galleryBloc = GalleryBloc(); freeCountBloc = FreeCountBloc(); var apiServerToken = settingRepo.get(settingAPIServerToken); var usingGuestMode = settingRepo.boolDefault(settingUsingGuestMode, false); final openAISelfHosted = settingRepo.boolDefault(settingOpenAISelfHosted, false); final deepAISelfHosted = settingRepo.boolDefault(settingDeepAISelfHosted, false); final stabilityAISelfHosted = settingRepo.boolDefault(settingStabilityAISelfHosted, false); final shouldLogin = (apiServerToken == null || apiServerToken == '') && !usingGuestMode && !openAISelfHosted && !deepAISelfHosted && !stabilityAISelfHosted; _router = GoRouter( initialLocation: shouldLogin ? '/login' : Ability().homeRoute, observers: [ BotToastNavigatorObserver(), ], navigatorKey: _rootNavigatorKey, routes: [ ShellRoute( builder: (context, state, child) { return child; }, routes: [ GoRoute( path: '/', name: 'home', pageBuilder: (context, state) { return transitionResolver( MultiBlocProvider( providers: [ BlocProvider.value( value: ChatBlocManager().getBloc( chatAnywhereRoomId, chatHistoryId: int.tryParse(state.queryParameters['chat_id'] ?? ''), ), ), BlocProvider( create: (context) => ChatChatBloc(chatMsgRepo), ), BlocProvider.value(value: chatRoomBloc), BlocProvider.value(value: galleryBloc), BlocProvider.value(value: accountBloc), BlocProvider.value(value: versionBloc), ], child: NewHomePage( settings: settingRepo, stateManager: messageStateManager, showInitialDialog: state.queryParameters['show_initial_dialog'] == 'true', reward: int.tryParse(state.queryParameters['reward'] ?? '0'), ), ), ); }, ), GoRoute( name: 'setting', path: '/setting', pageBuilder: (context, state) => transitionResolver( MultiBlocProvider( providers: [ BlocProvider.value(value: accountBloc), ], child: SettingScreen(settings: context.read()), ), ), ), GoRoute( name: 'creative-draw', path: '/creative-draw', pageBuilder: (context, state) => transitionResolver( MultiBlocProvider( providers: [ BlocProvider(create: (context) => CreativeIslandBloc(creativeIslandRepo)), ], child: DrawListScreen( setting: settingRepo, ), ), ), ), GoRoute( name: 'creative-gallery', path: '/creative-gallery', pageBuilder: (context, state) => transitionResolver( MultiBlocProvider( providers: [ BlocProvider.value(value: galleryBloc), ], child: GalleryScreen(setting: settingRepo), ), ), ), GoRoute( name: 'characters', path: '/characters', pageBuilder: (context, state) => transitionResolver( MultiBlocProvider( providers: [BlocProvider.value(value: chatRoomBloc)], child: CharactersPage(setting: settingRepo), ), ), ), GoRoute( name: 'chat_chat', path: '/chat-chat', pageBuilder: (context, state) => transitionResolver( MultiBlocProvider( providers: [ BlocProvider(create: (context) => ChatChatBloc(chatMsgRepo)), ], child: HomePage(setting: settingRepo), ), ), ), GoRoute( path: '/login', pageBuilder: (context, state) => transitionResolver( MultiBlocProvider( providers: [ BlocProvider.value(value: versionBloc), ], child: SignInScreen( settings: settingRepo, username: state.queryParameters['username'], ), ), ), ), GoRoute( path: '/signin-or-signup', pageBuilder: (context, state) => transitionResolver( MultiBlocProvider( providers: [ BlocProvider.value(value: versionBloc), ], child: SigninOrSignupScreen( settings: settingRepo, username: state.queryParameters['username']!, isSignup: state.queryParameters['is_signup'] == 'true', signInMethod: state.queryParameters['sign_in_method']!, wechatBindToken: state.queryParameters['wechat_bind_token'], ), ), ), ), GoRoute( path: '/user/change-password', pageBuilder: (context, state) => transitionResolver( ChangePasswordScreen(setting: settingRepo), ), ), GoRoute( path: '/user/destroy', pageBuilder: (context, state) => transitionResolver( DestroyAccountScreen(setting: settingRepo), ), ), GoRoute( path: '/signup', pageBuilder: (context, state) => transitionResolver( SignupScreen( settings: settingRepo, username: state.queryParameters['username'], ), ), ), GoRoute( path: '/retrieve-password', pageBuilder: (context, state) => transitionResolver( RetrievePasswordScreen( username: state.queryParameters['username'], setting: settingRepo, ), ), ), GoRoute( name: 'chat_anywhere', path: '/chat-anywhere', pageBuilder: (context, state) => transitionResolver( MultiBlocProvider( providers: [ BlocProvider.value( value: ChatBlocManager().getBloc( chatAnywhereRoomId, chatHistoryId: int.tryParse(state.queryParameters['chat_id'] ?? ''), ), ), BlocProvider.value(value: chatRoomBloc), BlocProvider(create: (context) => NotifyBloc()), ], child: HomeChatPage( stateManager: messageStateManager, setting: settingRepo, chatId: int.tryParse(state.queryParameters['chat_id'] ?? '0'), initialMessage: state.queryParameters['init_message'], model: state.queryParameters['model'] == '' ? null : state.queryParameters['model'], title: state.queryParameters['title'] == '' ? null : state.queryParameters['title'], ), ), ), ), GoRoute( name: 'chat_chat_history', path: '/chat-chat/history', pageBuilder: (context, state) => transitionResolver( MultiBlocProvider( providers: [ BlocProvider(create: (context) => ChatChatBloc(chatMsgRepo)), ], child: HomeChatHistoryPage( setting: settingRepo, chatMessageRepo: chatMsgRepo, ), ), ), ), GoRoute( path: '/lab/avatar-selector', pageBuilder: (context, state) => transitionResolver( const AvatarSelectorScreen(usage: AvatarUsage.room), ), ), GoRoute( path: '/lab/draw-board', pageBuilder: (context, state) => transitionResolver( const DrawboardScreen(), ), ), GoRoute( name: 'create-room', path: '/create-room', pageBuilder: (context, state) => transitionResolver( MultiBlocProvider( providers: [BlocProvider.value(value: chatRoomBloc)], child: CharacterCreatePage(setting: settingRepo), ), ), ), GoRoute( name: 'chat', path: '/room/:room_id/chat', pageBuilder: (context, state) { final roomId = int.parse(state.pathParameters['room_id']!); return transitionResolver( MultiBlocProvider( providers: [ BlocProvider.value( value: ChatBlocManager().getBloc(roomId), ), BlocProvider.value(value: chatRoomBloc), BlocProvider(create: (context) => NotifyBloc()), ], child: CharacterChatPage( roomId: roomId, stateManager: messageStateManager, setting: settingRepo, ), ), ); }, ), GoRoute( name: 'room_setting', path: '/room/:room_id/setting', pageBuilder: (context, state) { final roomId = int.parse(state.pathParameters['room_id']!); return transitionResolver( MultiBlocProvider( providers: [ BlocProvider.value(value: chatRoomBloc), BlocProvider.value( value: ChatBlocManager().getBloc(roomId), ), ], child: CharacterEditPage(roomId: roomId, setting: settingRepo), ), ); }, ), GoRoute( name: 'account-security-setting', path: '/setting/account-security', pageBuilder: (context, state) => transitionResolver( MultiBlocProvider( providers: [ BlocProvider.value(value: accountBloc), ], child: AccountSecurityScreen( settings: context.read(), ), ), ), ), GoRoute( name: 'lab-user-center', path: '/lab/user-center', pageBuilder: (context, state) => transitionResolver( MultiBlocProvider( providers: [ BlocProvider.value(value: accountBloc), BlocProvider(create: (context) => CreativeIslandBloc(creativeIslandRepo)), ], child: UserCenterScreen(settings: context.read()), ), ), ), GoRoute( name: 'setting-background-selector', path: '/setting/background-selector', pageBuilder: (context, state) => transitionResolver( BlocProvider( create: (context) => BackgroundImageBloc(), child: BackgroundSelectorScreen(setting: settingRepo), ), ), ), GoRoute( name: 'setting-openai-custom', path: '/setting/openai-custom', pageBuilder: (context, state) => transitionResolver( OpenAISettingScreen( settings: settingRepo, source: state.queryParameters['source'], ), ), ), GoRoute( name: 'creative-upscale', path: '/creative-draw/create-upscale', pageBuilder: (context, state) => transitionResolver( MultiBlocProvider( providers: [ BlocProvider(create: (context) => CreativeIslandBloc(creativeIslandRepo)), ], child: ImageEditDirectScreen( setting: settingRepo, title: AppLocale.superResolution.getString(context), apiEndpoint: 'upscale', note: state.queryParameters['note'], initWaitDuration: 15, initImage: state.queryParameters['init_image'], ), ), ), ), GoRoute( name: 'creative-colorize', path: '/creative-draw/create-colorize', pageBuilder: (context, state) => transitionResolver( MultiBlocProvider( providers: [ BlocProvider(create: (context) => CreativeIslandBloc(creativeIslandRepo)), ], child: ImageEditDirectScreen( setting: settingRepo, title: AppLocale.colorizeImage.getString(context), apiEndpoint: 'colorize', note: state.queryParameters['note'], initWaitDuration: 15, initImage: state.queryParameters['init_image'], ), ), ), ), GoRoute( name: 'creative-video', path: '/creative-draw/create-video', pageBuilder: (context, state) => transitionResolver( MultiBlocProvider( providers: [ BlocProvider(create: (context) => CreativeIslandBloc(creativeIslandRepo)), ], child: ImageEditDirectScreen( setting: settingRepo, title: '图生视频', apiEndpoint: 'image-to-video', note: state.queryParameters['note'], initWaitDuration: 60, initImage: state.queryParameters['init_image'], ), ), ), ), GoRoute( name: 'creative-draw-gallery-preview', path: '/creative-draw/gallery/:id', pageBuilder: (context, state) => transitionResolver( MultiBlocProvider( providers: [ BlocProvider.value(value: galleryBloc), ], child: GalleryItemScreen( setting: settingRepo, galleryId: int.parse(state.pathParameters['id']!), ), ), ), ), GoRoute( name: 'creative-draw-create', path: '/creative-draw/create', pageBuilder: (context, state) => transitionResolver( MultiBlocProvider( providers: [ BlocProvider.value(value: galleryBloc), ], child: DrawCreateScreen( setting: settingRepo, galleryCopyId: int.tryParse( state.queryParameters['gallery_copy_id'] ?? '', ), mode: state.queryParameters['mode']!, id: state.queryParameters['id']!, note: state.queryParameters['note'], initImage: state.queryParameters['init_image'], ), ), ), ), GoRoute( name: 'creative-artistic-text', path: '/creative-draw/artistic-text', pageBuilder: (context, state) => transitionResolver( MultiBlocProvider( providers: [ BlocProvider.value(value: galleryBloc), ], child: ArtisticQRScreen( setting: settingRepo, galleryCopyId: int.tryParse( state.queryParameters['gallery_copy_id'] ?? '', ), type: state.queryParameters['type']!, id: state.queryParameters['id']!, note: state.queryParameters['note'], ), ), ), ), GoRoute( name: 'creative-artistic-wordart', path: '/creative-draw/artistic-wordart', pageBuilder: (context, state) => transitionResolver( MultiBlocProvider( providers: [ BlocProvider.value(value: galleryBloc), ], child: ArtisticWordArtScreen( setting: settingRepo, galleryCopyId: int.tryParse( state.queryParameters['gallery_copy_id'] ?? '', ), id: state.queryParameters['id']!, note: state.queryParameters['note'], ), ), ), ), GoRoute( name: 'creative-island-history-all', path: '/creative-island/history', pageBuilder: (context, state) { return transitionResolver( MultiBlocProvider( providers: [ BlocProvider(create: (context) => CreativeIslandBloc(creativeIslandRepo)), ], child: MyCreationScreen( setting: settingRepo, mode: state.queryParameters['mode'] ?? '', ), ), ); }, ), GoRoute( name: 'creative-island-models', path: '/creative-island/models', pageBuilder: (context, state) { return transitionResolver( MultiBlocProvider( providers: [ BlocProvider(create: (context) => CreativeIslandBloc(creativeIslandRepo)), ], child: CreativeModelScreen(setting: settingRepo), ), ); }, ), GoRoute( name: 'creative-island-history-item', path: '/creative-island/:id/history/:item_id', pageBuilder: (context, state) { final id = state.pathParameters['id']!; final itemId = int.tryParse(state.pathParameters['item_id']!); final showErrorMessage = state.queryParameters['show_error'] == 'true'; return transitionResolver( MultiBlocProvider( providers: [ BlocProvider(create: (context) => CreativeIslandBloc(creativeIslandRepo)), ], child: MyCreationItemPage( setting: settingRepo, islandId: id, itemId: itemId!, showErrorMessage: showErrorMessage, ), ), ); }, ), GoRoute( name: 'quota-details', path: '/quota-details', pageBuilder: (context, state) => transitionResolver( PaymentHistoryScreen(setting: settingRepo), ), ), GoRoute( name: 'quota-usage-statistics', path: '/quota-usage-statistics', pageBuilder: (context, state) => transitionResolver( QuotaUsageStatisticsScreen(setting: settingRepo), ), ), GoRoute( name: 'quota-usage-daily-details', path: '/quota-usage-daily-details', pageBuilder: (context, state) => transitionResolver( QuotaUsageDetailScreen( setting: settingRepo, date: state.queryParameters['date'] ?? DateFormat('yyyy-MM-dd').format(DateTime.now()), ), ), ), GoRoute( name: 'prompt-editor', path: '/prompt-editor', pageBuilder: (context, state) { var prompt = state.queryParameters['prompt'] ?? ''; return transitionResolver(PromptScreen(prompt: prompt)); }, ), GoRoute( name: 'payment', path: '/payment', pageBuilder: (context, state) { return transitionResolver( MultiBlocProvider( providers: [ BlocProvider(create: ((context) => PaymentBloc())), ], child: PaymentScreen(setting: settingRepo), ), ); }, ), GoRoute( name: 'bind-phone', path: '/bind-phone', pageBuilder: (context, state) { return transitionResolver( MultiBlocProvider( providers: [ BlocProvider.value(value: accountBloc), ], child: BindPhoneScreen( setting: settingRepo, isSignIn: state.queryParameters['is_signin'] != 'false', ), ), ); }, ), GoRoute( name: 'diagnosis', path: '/diagnosis', pageBuilder: (context, state) => transitionResolver( DiagnosisScreen(setting: settingRepo), ), ), GoRoute( name: 'free-statistics', path: '/free-statistics', pageBuilder: (context, state) => transitionResolver( MultiBlocProvider( providers: [BlocProvider.value(value: freeCountBloc)], child: FreeStatisticsPage(setting: settingRepo), ), ), ), GoRoute( name: 'custom-home-models', path: '/setting/custom-home-models', pageBuilder: (context, state) => transitionResolver( CustomHomeModelsPage(setting: settingRepo), ), ), GoRoute( name: 'group-chat-chat', path: '/group-chat/:group_id/chat', pageBuilder: (context, state) { final groupId = int.tryParse(state.pathParameters['group_id']!); return transitionResolver( MultiBlocProvider( providers: [ BlocProvider( create: ((context) => GroupChatBloc(stateManager: messageStateManager)), ), BlocProvider.value(value: chatRoomBloc), ], child: GroupChatPage( setting: settingRepo, stateManager: messageStateManager, groupId: groupId!, ), ), ); }, ), GoRoute( name: 'group-chat-create', path: '/group-chat-create', pageBuilder: (context, state) { return transitionResolver( MultiBlocProvider( providers: [ BlocProvider( create: ((context) => GroupChatBloc(stateManager: messageStateManager)), ), BlocProvider.value(value: chatRoomBloc), ], child: GroupCreatePage(setting: settingRepo), ), ); }, ), GoRoute( name: 'group-chat-edit', path: '/group-chat/:group_id/edit', pageBuilder: (context, state) { return transitionResolver( MultiBlocProvider( providers: [ BlocProvider( create: ((context) => GroupChatBloc(stateManager: messageStateManager)), ), BlocProvider.value(value: chatRoomBloc), ], child: GroupEditPage( setting: settingRepo, groupId: int.tryParse(state.pathParameters['group_id']!)!, ), ), ); }, ), GoRoute( name: 'user-api-keys', path: '/setting/user-api-keys', pageBuilder: (context, state) { return transitionResolver( MultiBlocProvider( providers: [ BlocProvider( create: ((context) => UserApiKeysBloc()), ), ], child: UserAPIKeysScreen(setting: settingRepo), ), ); }, ), GoRoute( name: 'notifications', path: '/notifications', pageBuilder: (context, state) { return transitionResolver( NotificationScreen(setting: settingRepo), ); }, ), GoRoute( name: 'articles', path: '/article', pageBuilder: (context, state) { return transitionResolver( ArticleScreen( settings: settingRepo, id: int.tryParse(state.queryParameters['id'] ?? '') ?? 0, ), ); }, ), GoRoute( name: 'web-payment-result', path: '/payment/result', pageBuilder: (context, state) { return transitionResolver(WebPaymentResult( paymentId: state.queryParameters['payment_id']!, action: state.queryParameters['action'], )); }, ), GoRoute( name: 'web-payment-proxy', path: '/payment/proxy', pageBuilder: (context, state) { return transitionResolver(WebPaymentProxy( setting: settingRepo, paymentId: state.queryParameters['id']!, paymentIntent: state.queryParameters['intent']!, price: state.queryParameters['price']!, publishableKey: state.queryParameters['key']!, finishAction: state.queryParameters['finish_action'] ?? 'close', )); }, ), /// 管理员接口 GoRoute( name: 'admin-dashboard', path: '/admin/dashboard', pageBuilder: (context, state) { return transitionResolver( AdminDashboardPage(setting: settingRepo), ); }, ), GoRoute( name: 'admin-models', path: '/admin/models', pageBuilder: (context, state) { return transitionResolver( MultiBlocProvider( providers: [ BlocProvider( create: (context) => ModelBloc(), ), ], child: AdminModelsPage(setting: settingRepo), ), ); }, ), GoRoute( name: 'admin-models-create', path: '/admin/models/create', pageBuilder: (context, state) { return transitionResolver( MultiBlocProvider( providers: [ BlocProvider( create: (context) => ModelBloc(), ), ], child: AdminModelCreatePage(setting: settingRepo), ), ); }, ), GoRoute( name: 'admin-models-edit', path: '/admin/models/edit/:id', pageBuilder: (context, state) { return transitionResolver( MultiBlocProvider( providers: [ BlocProvider( create: (context) => ModelBloc(), ), ], child: AdminModelEditPage( setting: settingRepo, modelId: state.pathParameters['id']!, ), ), ); }, ), GoRoute( name: 'admin-channels', path: '/admin/channels', pageBuilder: (context, state) { return transitionResolver( MultiBlocProvider( providers: [ BlocProvider( create: (context) => ChannelBloc(), ), ], child: ChannelsPage(setting: settingRepo), ), ); }, ), GoRoute( name: 'admin-channels-create', path: '/admin/channels/create', pageBuilder: (context, state) { return transitionResolver( MultiBlocProvider( providers: [ BlocProvider( create: (context) => ChannelBloc(), ), ], child: ChannelAddPage(setting: settingRepo), ), ); }, ), GoRoute( name: 'admin-channels-edit', path: '/admin/channels/edit/:id', pageBuilder: (context, state) { final channelId = int.parse(state.pathParameters['id']!); return transitionResolver( MultiBlocProvider( providers: [ BlocProvider( create: (context) => ChannelBloc(), ), ], child: ChannelEditPage( setting: settingRepo, channelId: channelId, ), ), ); }, ), GoRoute( name: 'admin-users', path: '/admin/users', pageBuilder: (context, state) { return transitionResolver( MultiBlocProvider( providers: [ BlocProvider( create: (context) => UserBloc(), ), ], child: AdminUsersPage(setting: settingRepo), ), ); }, ), GoRoute( name: 'admin-users-detail', path: '/admin/users/:id', pageBuilder: (context, state) { final userId = int.parse(state.pathParameters['id']!); return transitionResolver( MultiBlocProvider( providers: [ BlocProvider( create: (context) => UserBloc(), ), ], child: AdminUserPage(setting: settingRepo, userId: userId), ), ); }, ), GoRoute( name: 'admin-payment-histories', path: '/admin/payment/histories', pageBuilder: (context, state) { return transitionResolver( MultiBlocProvider( providers: [ BlocProvider( create: (context) => AdminPaymentBloc(), ), ], child: PaymentHistoriesPage(setting: settingRepo), ), ); }, ), GoRoute( name: 'admin-user-rooms', path: '/admin/users/:id/rooms', pageBuilder: (context, state) { final userId = int.parse(state.pathParameters['id']!); return transitionResolver( MultiBlocProvider( providers: [ BlocProvider( create: (context) => AdminRoomBloc(), ), ], child: AdminRoomsPage(setting: settingRepo, userId: userId), ), ); }, ), GoRoute( name: 'admin-user-rooms-messages', path: '/admin/users/:id/rooms/:room_id/messages', pageBuilder: (context, state) { final userId = int.parse(state.pathParameters['id']!); final roomId = int.parse(state.pathParameters['room_id']!); return transitionResolver( MultiBlocProvider( providers: [ BlocProvider( create: (context) => AdminRoomBloc(), ), ], child: AdminRoomMessagesPage( setting: settingRepo, userId: userId, roomId: roomId, roomType: int.parse(state.queryParameters['room_type']!), ), ), ); }, ), GoRoute( name: 'admin-recently-messages', path: '/admin/recently-messages', pageBuilder: (context, state) { return transitionResolver( MultiBlocProvider( providers: [ BlocProvider( create: (context) => AdminRoomBloc(), ), ], child: AdminRecentlyMessagesPage(setting: settingRepo), ), ); }, ), ], ), ], ); } final SettingRepository settingRepo; final ChatMessageRepository chatMsgRepo; final OpenAIRepository openAIRepo; final CacheRepository cacheRepo; final CreativeIslandRepository creativeIslandRepo; @override State createState() => _MyAppState(); } class _MyAppState extends State { @override void initState() { // 初始化多语言 // final defaultLanguage = resolveSystemLanguage(PlatformTool.localeName()); // var initLanguage = // widget.settingRepo.stringDefault(settingLanguage, defaultLanguage); widget.localization.init( mapLocales: [ const MapLocale('zh', AppLocale.zh), const MapLocale('zh-CHS', AppLocale.zh), const MapLocale('en', AppLocale.en), ], // initLanguageCode: initLanguage == '' ? defaultLanguage : initLanguage, initLanguageCode: 'zh-CHS', ); widget.localization.onTranslatedLanguage = (Locale? locale) { setState(() {}); }; if (PlatformTool.isIOS() || PlatformTool.isAndroid()) { registerWxApi( appId: weixinAppId, universalLink: universalLink, ); } // weChatResponseEventHandler.listen((event) { // print("====================="); // print("errorCode: ${event.errCode}"); // print("errorMessage: ${event.errStr}"); // if (event is WeChatShareResponse) { // print("type: ${event.type}"); // print("success:${event.isSuccessful}"); // } // showSuccessMessage('分享成功', duration: const Duration(seconds: 3)); // }); super.initState(); } // This widget is the root of your application. @override Widget build(BuildContext context) { return MultiRepositoryProvider( providers: [ RepositoryProvider(create: (context) => widget.chatMsgRepo), RepositoryProvider(create: (context) => widget.openAIRepo), RepositoryProvider(create: (context) => widget.settingRepo), RepositoryProvider(create: (context) => widget.cacheRepo), ], child: ChangeNotifierProvider( create: (context) => AppTheme.get() ..mode = AppTheme.themeModeFormString(widget.settingRepo.stringDefault(settingThemeMode, 'system')), builder: (context, _) { final appTheme = context.watch(); return Sizer( builder: (context, orientation, deviceType) { return MaterialApp.router( title: 'AIdea', themeMode: appTheme.mode, theme: createLightThemeData(), darkTheme: createDarkThemeData(), debugShowCheckedModeBanner: false, builder: (context, child) { // 这里设置了全局字体固定大小,不随系统设置变更 return MediaQuery( data: MediaQuery.of(context) .copyWith(textScaler: TextScaler.linear(PlatformTool.isDesktop() ? 0.95 : 1)), child: BotToastInit()(context, child), ); }, routerConfig: widget._router, supportedLocales: widget.localization.supportedLocales, localizationsDelegates: widget.localization.localizationsDelegates, scrollBehavior: PlatformTool.isAndroid() || PlatformTool.isIOS() ? null : const MaterialScrollBehavior().copyWith( dragDevices: { PointerDeviceKind.touch, PointerDeviceKind.mouse, PointerDeviceKind.stylus, PointerDeviceKind.trackpad, }, ), ); }, ); }), ); } } ThemeData createLightThemeData() { return ThemeData.light(useMaterial3: true).copyWith( extensions: [CustomColors.light], textTheme: ThemeData(fontFamily: 'AlibabaPuHuiTi').textTheme, appBarTheme: const AppBarTheme( // backgroundColor: Color.fromARGB(255, 250, 250, 250), backgroundColor: Colors.transparent, scrolledUnderElevation: 0, ), iconButtonTheme: PlatformTool.isMacOS() ? IconButtonThemeData( style: ButtonStyle( overlayColor: WidgetStateProperty.all(Colors.transparent), ), ) : null, dividerColor: Colors.transparent, dialogBackgroundColor: Colors.white, dialogTheme: DialogTheme( shape: RoundedRectangleBorder( borderRadius: CustomSize.borderRadius, ), elevation: 0, ), textButtonTheme: TextButtonThemeData( style: TextButton.styleFrom( foregroundColor: const Color.fromARGB(255, 9, 185, 85), // This is a custom color variable ), ), ); } ThemeData createDarkThemeData() { return ThemeData.dark(useMaterial3: true).copyWith( extensions: [CustomColors.dark], textTheme: ThemeData(fontFamily: 'AlibabaPuHuiTi').primaryTextTheme, appBarTheme: const AppBarTheme( // backgroundColor: Color.fromARGB(255, 48, 48, 48), backgroundColor: Colors.transparent, scrolledUnderElevation: 0, ), iconButtonTheme: PlatformTool.isMacOS() ? IconButtonThemeData( style: ButtonStyle( overlayColor: WidgetStateProperty.all(Colors.transparent), ), ) : null, dividerColor: Colors.transparent, dialogBackgroundColor: const Color.fromARGB(255, 48, 48, 48), dialogTheme: DialogTheme( shape: RoundedRectangleBorder( borderRadius: CustomSize.borderRadius, ), elevation: 0, ), textButtonTheme: TextButtonThemeData( style: TextButton.styleFrom( foregroundColor: const Color.fromARGB(255, 9, 185, 85), // This is a custom color variable ), ), ); } ================================================ FILE: lib/page/admin/channels.dart ================================================ import 'package:askaide/bloc/channel_bloc.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/api/admin/channels.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_initicon/flutter_initicon.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:go_router/go_router.dart'; class ChannelsPage extends StatefulWidget { final SettingRepository setting; const ChannelsPage({ super.key, required this.setting, }); @override State createState() => _ChannelsPageState(); } class _ChannelsPageState extends State { // 渠道类型 List channelTypes = []; @override void initState() { context.read().add(ChannelsLoadEvent()); // 加载渠道类型 APIServer().adminChannelTypes().then((value) { if (context.mounted) { setState(() { channelTypes = value; }); } }); super.initState(); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( toolbarHeight: CustomSize.toolbarHeight, title: const Text( 'Channel', style: TextStyle(fontSize: CustomSize.appBarTitleSize), ), centerTitle: true, actions: [ IconButton( icon: const Icon(Icons.add), onPressed: () { context.push('/admin/channels/create').then((value) { context.read().add(ChannelsLoadEvent()); }); }, ), ], ), backgroundColor: customColors.backgroundColor, body: BackgroundContainer( setting: widget.setting, enabled: false, backgroundColor: customColors.backgroundColor, child: RefreshIndicator( color: customColors.linkColor, onRefresh: () async { context.read().add(ChannelsLoadEvent()); }, displacement: 20, child: BlocConsumer( listenWhen: (previous, current) => current is ChannelOperationResult, listener: (context, state) { if (state is ChannelOperationResult) { if (state.success) { showSuccessMessage(state.message); context.read().add(ChannelsLoadEvent()); } else { showErrorMessage(state.message); } } }, buildWhen: (previous, current) => current is ChannelsLoaded, builder: (context, state) { if (state is ChannelsLoaded) { return SafeArea( top: false, child: ListView.builder( padding: const EdgeInsets.all(5), itemCount: state.channels.length, itemBuilder: (context, index) { final channel = state.channels[index]; return buildChannelItem(context, customColors, channel); }, ), ); } return const Center( child: CircularProgressIndicator(), ); }, ), ), ), ), ); } Widget buildChannelItem( BuildContext context, CustomColors customColors, AdminChannel channel, ) { return Container( margin: const EdgeInsets.symmetric( horizontal: 10, vertical: 5, ), decoration: BoxDecoration(borderRadius: CustomSize.borderRadius), child: Slidable( endActionPane: ActionPane( motion: const ScrollMotion(), children: [ const SizedBox(width: 10), SlidableAction( label: AppLocale.delete.getString(context), borderRadius: const BorderRadius.only( topLeft: CustomSize.radius, bottomLeft: CustomSize.radius, topRight: CustomSize.radius, bottomRight: CustomSize.radius, ), backgroundColor: Colors.red, icon: Icons.delete, onPressed: (_) { openConfirmDialog( context, AppLocale.confirmToDeleteRoom.getString(context), () => context.read().add(ChannelDeleteEvent(channel.id!)), danger: true, ); }, ), ], ), child: Material( borderRadius: CustomSize.borderRadius, color: customColors.columnBlockBackgroundColor, child: InkWell( borderRadius: const BorderRadius.all(CustomSize.radius), onTap: () { context.push('/admin/channels/edit/${channel.id}').then((value) { context.read().add(ChannelsLoadEvent()); }); }, child: Stack( children: [ Row( mainAxisSize: MainAxisSize.min, children: [ // 渠道头像 Initicon( text: channel.name.split('、').join(' '), size: 50, backgroundColor: Colors.grey.withAlpha(100), borderRadius: const BorderRadius.only(topLeft: CustomSize.radius, bottomLeft: CustomSize.radius), ), // 渠道名称 Expanded( child: Container( padding: const EdgeInsets.all(10), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( channel.name, style: const TextStyle( overflow: TextOverflow.ellipsis, ), ), ], ), ), ), ], ), Positioned( right: 0, top: 0, child: Container( padding: const EdgeInsets.all(10), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( channelTypes.firstWhere((e) => e.name == channel.type).text, style: TextStyle( fontSize: 10, overflow: TextOverflow.ellipsis, color: customColors.weakTextColor, ), ), ], ), ), ), ], ), ), ), ), ); } } ================================================ FILE: lib/page/admin/channels_add.dart ================================================ import 'package:askaide/bloc/channel_bloc.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/column_block.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/enhanced_button.dart'; import 'package:askaide/page/component/enhanced_input.dart'; import 'package:askaide/page/component/enhanced_textfield.dart'; import 'package:askaide/page/component/item_selector_search.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/api/admin/channels.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; class ChannelAddPage extends StatefulWidget { final SettingRepository setting; const ChannelAddPage({ super.key, required this.setting, }); @override State createState() => _ChannelAddPageState(); } class _ChannelAddPageState extends State { // 渠道类型 List channelTypes = []; final TextEditingController nameController = TextEditingController(); final TextEditingController typeController = TextEditingController(); final TextEditingController serverController = TextEditingController(); final TextEditingController secretController = TextEditingController(); /// 当前选中的渠道类型 String? selectedChannelType; /// 用于控制是否显示高级选项 bool showAdvancedOptions = false; /// 是否使用代理 bool usingProxy = false; /// 是否是 Azure API bool openaiAzure = false; /// OpenAI Azure API 版本 final TextEditingController azureAPIVersionController = TextEditingController(); @override void dispose() { nameController.dispose(); typeController.dispose(); serverController.dispose(); secretController.dispose(); azureAPIVersionController.dispose(); super.dispose(); } @override void initState() { // 加载渠道类型 APIServer().adminChannelTypes().then((value) { if (context.mounted) { setState(() { channelTypes = value.where((e) => e.dynamicType).toList(); }); } }); super.initState(); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( toolbarHeight: CustomSize.toolbarHeight, title: const Text( 'New Channel', style: TextStyle(fontSize: CustomSize.appBarTitleSize), ), centerTitle: true, ), backgroundColor: customColors.backgroundColor, body: BackgroundContainer( setting: widget.setting, enabled: false, backgroundColor: customColors.backgroundColor, child: BlocListener( listenWhen: (previous, current) => current is ChannelOperationResult, listener: (context, state) { if (state is ChannelOperationResult) { if (state.success) { showSuccessMessage(state.message); context.pop(); } else { showErrorMessage(state.message); } } }, child: Container( padding: const EdgeInsets.all(10), child: Column( children: [ ColumnBlock( children: [ EnhancedTextField( labelText: 'Name', customColors: customColors, controller: nameController, textAlignVertical: TextAlignVertical.top, hintText: 'Enter channel name', maxLength: 100, showCounter: false, ), EnhancedInput( title: Text( 'Type', style: TextStyle( color: customColors.textfieldLabelColor, fontSize: 16, ), ), value: Text( buildSelectedChannelTypeText(), style: TextStyle( color: customColors.textfieldValueColor, fontSize: 16, ), ), onPressed: () { openListSelectDialog( context, channelTypes.map((e) => SelectorItem(Text(e.text), e.name)).toList(), (value) { setState(() { selectedChannelType = value.value; }); return true; }, heightFactor: 0.5, value: selectedChannelType, ); }, ), EnhancedTextField( labelText: 'Server', customColors: customColors, controller: serverController, textAlignVertical: TextAlignVertical.top, hintText: 'https://api.openai.com/v1', maxLength: 255, showCounter: false, ), EnhancedTextField( labelText: 'API Key', customColors: customColors, controller: secretController, textAlignVertical: TextAlignVertical.top, hintText: 'Enter API Key', maxLength: 2048, obscureText: true, showCounter: false, ), ], ), // 高级选项 if (showAdvancedOptions) ColumnBlock( innerPanding: 5, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Use Proxy', style: TextStyle(fontSize: 16), ), CupertinoSwitch( activeColor: customColors.linkColor, value: usingProxy, onChanged: (value) { setState(() { usingProxy = value; }); }, ), ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Azure Mode', style: TextStyle(fontSize: 16), ), CupertinoSwitch( activeColor: customColors.linkColor, value: openaiAzure, onChanged: (value) { setState(() { openaiAzure = value; }); }, ), ], ), EnhancedTextField( labelText: 'Version', customColors: customColors, controller: azureAPIVersionController, textAlignVertical: TextAlignVertical.top, hintText: '2023-05-15', maxLength: 30, showCounter: false, ), ], ), const SizedBox(height: 15), Row( children: [ EnhancedButton( title: showAdvancedOptions ? AppLocale.simpleMode.getString(context) : AppLocale.professionalMode.getString(context), width: 120, backgroundColor: Colors.transparent, color: customColors.weakLinkColor, fontSize: 15, icon: Icon( showAdvancedOptions ? Icons.unfold_less : Icons.unfold_more, color: customColors.weakLinkColor, size: 15, ), onPressed: () { setState(() { showAdvancedOptions = !showAdvancedOptions; }); }, ), const SizedBox(width: 10), Expanded( flex: 1, child: EnhancedButton( title: AppLocale.save.getString(context), onPressed: onSubmit, ), ), ], ), ], ), ), ), ), ), ); } /// 提交 void onSubmit() { if (nameController.text.isEmpty) { showErrorMessage('Please enter a channel name'); return; } if (selectedChannelType == null) { showErrorMessage('Please select channel type'); return; } if (serverController.text.isEmpty) { showErrorMessage('Please enter the server address'); return; } if (!serverController.text.startsWith('http://') && !serverController.text.startsWith('https://')) { showErrorMessage('The server address format is incorrect'); return; } final req = AdminChannelAddReq( name: nameController.text, type: selectedChannelType!, server: serverController.text, secret: secretController.text, meta: AdminChannelMeta( usingProxy: usingProxy, openaiAzure: openaiAzure, openaiAzureAPIVersion: azureAPIVersionController.text, ), ); context.read().add(ChannelCreateEvent(req)); } /// 生成选中的渠道类型文本 String buildSelectedChannelTypeText() { if (selectedChannelType == null) { return 'Select'; } return channelTypes.firstWhere((element) => element.name == selectedChannelType).text; } } ================================================ FILE: lib/page/admin/channels_edit.dart ================================================ import 'package:askaide/bloc/channel_bloc.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/column_block.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/enhanced_button.dart'; import 'package:askaide/page/component/enhanced_input.dart'; import 'package:askaide/page/component/enhanced_textfield.dart'; import 'package:askaide/page/component/item_selector_search.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/api/admin/channels.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; class ChannelEditPage extends StatefulWidget { final SettingRepository setting; final int channelId; const ChannelEditPage({ super.key, required this.setting, required this.channelId, }); @override State createState() => _ChannelEditPageState(); } class _ChannelEditPageState extends State { // 渠道类型 List channelTypes = []; final TextEditingController nameController = TextEditingController(); final TextEditingController typeController = TextEditingController(); final TextEditingController serverController = TextEditingController(); final TextEditingController secretController = TextEditingController(); /// 当前选中的渠道类型 String? selectedChannelType; /// 用于控制是否显示高级选项 bool showAdvancedOptions = false; /// 是否使用代理 bool usingProxy = false; /// 是否是 Azure API bool openaiAzure = false; /// OpenAI Azure API 版本 final TextEditingController azureAPIVersionController = TextEditingController(); /// 是否锁定编辑 bool editLocked = true; @override void dispose() { nameController.dispose(); typeController.dispose(); serverController.dispose(); secretController.dispose(); azureAPIVersionController.dispose(); super.dispose(); } @override void initState() { // 加载渠道类型 APIServer().adminChannelTypes().then((value) { if (context.mounted) { setState(() { channelTypes = value.where((e) => e.dynamicType).toList(); }); } }); // 加载渠道信息 context.read().add(ChannelLoadEvent(widget.channelId)); super.initState(); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( toolbarHeight: CustomSize.toolbarHeight, title: const Text( 'Edit Channel', style: TextStyle(fontSize: CustomSize.appBarTitleSize), ), centerTitle: true, ), backgroundColor: customColors.backgroundColor, body: BackgroundContainer( setting: widget.setting, enabled: false, backgroundColor: customColors.backgroundColor, child: BlocListener( listenWhen: (previous, current) => current is ChannelOperationResult || current is ChannelLoaded, listener: (context, state) { if (state is ChannelOperationResult) { if (state.success) { showSuccessMessage(state.message); context.read().add(ChannelLoadEvent(widget.channelId)); } else { showErrorMessage(state.message); } } else if (state is ChannelLoaded) { nameController.text = state.channel.name; selectedChannelType = state.channel.type; serverController.text = state.channel.server ?? ''; secretController.text = state.channel.secret ?? ''; usingProxy = state.channel.meta?.usingProxy ?? false; openaiAzure = state.channel.meta?.openaiAzure ?? false; azureAPIVersionController.text = state.channel.meta?.openaiAzureAPIVersion ?? ''; setState(() { editLocked = false; }); } }, child: Container( padding: const EdgeInsets.all(10), child: Column( children: [ ColumnBlock( children: [ EnhancedTextField( labelText: 'Name', customColors: customColors, controller: nameController, textAlignVertical: TextAlignVertical.top, hintText: 'Enter channel name', maxLength: 100, showCounter: false, ), EnhancedInput( title: Text( 'Type', style: TextStyle( color: customColors.textfieldLabelColor, fontSize: 16, ), ), value: Text( buildSelectedChannelTypeText(), style: TextStyle( color: customColors.textfieldValueColor, fontSize: 16, ), ), onPressed: () { openListSelectDialog( context, channelTypes.map((e) => SelectorItem(Text(e.text), e.name)).toList(), (value) { setState(() { selectedChannelType = value.value; }); return true; }, heightFactor: 0.5, value: selectedChannelType, ); }, ), EnhancedTextField( labelText: 'Server', customColors: customColors, controller: serverController, textAlignVertical: TextAlignVertical.top, hintText: 'https://api.openai.com/v1', maxLength: 255, showCounter: false, ), EnhancedTextField( labelText: 'API Key', customColors: customColors, controller: secretController, textAlignVertical: TextAlignVertical.top, hintText: 'Enter API Key', maxLength: 2048, obscureText: true, showCounter: false, ), ], ), // 高级选项 if (showAdvancedOptions) ColumnBlock( innerPanding: 5, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Use Proxy', style: TextStyle(fontSize: 16), ), CupertinoSwitch( activeColor: customColors.linkColor, value: usingProxy, onChanged: (value) { setState(() { usingProxy = value; }); }, ), ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Azure Mode', style: TextStyle(fontSize: 16), ), CupertinoSwitch( activeColor: customColors.linkColor, value: openaiAzure, onChanged: (value) { setState(() { openaiAzure = value; }); }, ), ], ), EnhancedTextField( labelText: 'Version', customColors: customColors, controller: azureAPIVersionController, textAlignVertical: TextAlignVertical.top, hintText: '2023-05-15', maxLength: 30, showCounter: false, ), ], ), const SizedBox(height: 15), Row( children: [ EnhancedButton( title: showAdvancedOptions ? AppLocale.simpleMode.getString(context) : AppLocale.professionalMode.getString(context), width: 120, backgroundColor: Colors.transparent, color: customColors.weakLinkColor, fontSize: 15, icon: Icon( showAdvancedOptions ? Icons.unfold_less : Icons.unfold_more, color: customColors.weakLinkColor, size: 15, ), onPressed: () { setState(() { showAdvancedOptions = !showAdvancedOptions; }); }, ), const SizedBox(width: 10), Expanded( flex: 1, child: EnhancedButton( title: AppLocale.save.getString(context), onPressed: onSubmit, icon: editLocked ? const Icon(Icons.lock, color: Colors.white, size: 16) : const Icon(Icons.lock_open, color: Colors.white, size: 16), ), ), ], ), ], ), ), ), ), ), ); } /// 提交 void onSubmit() { if (editLocked) { return; } if (nameController.text.isEmpty) { showErrorMessage('Please enter a channel name'); return; } if (selectedChannelType == null) { showErrorMessage('Please select channel type'); return; } if (serverController.text.isEmpty) { showErrorMessage('Please enter the server address'); return; } if (!serverController.text.startsWith('http://') && !serverController.text.startsWith('https://')) { showErrorMessage('The server address format is incorrect'); return; } final req = AdminChannelUpdateReq( name: nameController.text, type: selectedChannelType!, server: serverController.text, secret: secretController.text, meta: AdminChannelMeta( usingProxy: usingProxy, openaiAzure: openaiAzure, openaiAzureAPIVersion: azureAPIVersionController.text, ), ); context.read().add(ChannelUpdateEvent(widget.channelId, req)); } /// 生成选中的渠道类型文本 String buildSelectedChannelTypeText() { if (selectedChannelType == null) { return 'Select'; } return channelTypes.firstWhere((element) => element.name == selectedChannelType).text; } } ================================================ FILE: lib/page/admin/dashboard.dart ================================================ import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:settings_ui/settings_ui.dart'; class AdminDashboardPage extends StatefulWidget { final SettingRepository setting; const AdminDashboardPage({super.key, required this.setting}); @override State createState() => _AdminDashboardPageState(); } class _AdminDashboardPageState extends State { @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( toolbarHeight: CustomSize.toolbarHeight, title: const Text( 'Dashboard', style: TextStyle(fontSize: CustomSize.appBarTitleSize), ), centerTitle: true, ), backgroundColor: customColors.backgroundColor, body: BackgroundContainer( setting: widget.setting, backgroundColor: customColors.backgroundColor, enabled: false, child: Column( children: [ Expanded( child: SettingsList( platform: DevicePlatform.iOS, lightTheme: const SettingsThemeData( settingsListBackground: Colors.transparent, settingsSectionBackground: Color.fromARGB(255, 255, 255, 255), ), darkTheme: const SettingsThemeData( settingsListBackground: Colors.transparent, settingsSectionBackground: Color.fromARGB(255, 44, 44, 46), titleTextColor: Color.fromARGB(255, 239, 239, 239), ), sections: [ SettingsSection( title: const Text('Usage'), tiles: [ SettingsTile( title: const Text('Creation Island History'), trailing: const Icon( CupertinoIcons.chevron_forward, size: 18, color: Colors.grey, ), onPressed: (context) { context.push('/creative-island/models'); }, ), SettingsTile( title: const Text('Chat History'), trailing: const Icon( CupertinoIcons.chevron_forward, size: 18, color: Colors.grey, ), onPressed: (context) { context.push('/admin/recently-messages'); }, ), ], ), SettingsSection( title: const Text('Users & Revenue'), tiles: [ SettingsTile( title: const Text('User Management'), trailing: const Icon( CupertinoIcons.chevron_forward, size: 18, color: Colors.grey, ), onPressed: (context) { context.push('/admin/users'); }, ), SettingsTile( title: const Text('Payment Order History'), trailing: const Icon( CupertinoIcons.chevron_forward, size: 18, color: Colors.grey, ), onPressed: (context) { context.push('/admin/payment/histories'); }, ), ], ), SettingsSection( title: const Text('Model management'), tiles: [ SettingsTile( title: const Text('Channel'), trailing: const Icon( CupertinoIcons.chevron_forward, size: 18, color: Colors.grey, ), onPressed: (context) { context.push('/admin/channels'); }, ), SettingsTile( title: const Text('Large Language Model'), trailing: const Icon( CupertinoIcons.chevron_forward, size: 18, color: Colors.grey, ), onPressed: (context) { context.push('/admin/models'); }, ), ], ), SettingsSection( title: const Text('System settings'), tiles: [ SettingsTile( title: const Text('Refresh Config Cache'), trailing: const Icon( CupertinoIcons.chevron_forward, size: 18, color: Colors.grey, ), onPressed: (context) { openConfirmDialog( context, 'Reload all system configurations.\n Are you sure you want to proceed?', () { APIServer().adminSettingsReload().then((value) { showSuccessMessage('Update successful'); }).onError((error, stackTrace) { showErrorMessageEnhanced(context, error!); }); }, ); }, ), ], ), ], contentPadding: const EdgeInsets.all(0), ), ), ], ), ), ), ); } } ================================================ FILE: lib/page/admin/messages.dart ================================================ import 'package:askaide/bloc/admin_room_bloc.dart'; import 'package:askaide/helper/model.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/chat/chat_preview.dart'; import 'package:askaide/page/component/chat/message_state_manager.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/random_avatar.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/model/message.dart'; import 'package:askaide/repo/model/model.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:intl/intl.dart'; class AdminRoomMessagesPage extends StatefulWidget { final SettingRepository setting; final int userId; final int roomId; final int roomType; const AdminRoomMessagesPage({ super.key, required this.setting, required this.userId, required this.roomId, required this.roomType, }); @override State createState() => _AdminRoomMessagesPageState(); } class _AdminRoomMessagesPageState extends State { final ChatPreviewController controller = ChatPreviewController(); Map models = {}; @override void dispose() { controller.dispose(); super.dispose(); } @override void initState() { context.read().add(AdminRoomLoadEvent( userId: widget.userId, roomId: widget.roomId, )); context.read().add(AdminRoomRecentlyMessagesLoadEvent( userId: widget.userId, roomId: widget.roomId, roomType: widget.roomType, )); ModelAggregate.models().then((value) { setState(() { for (var element in value) { models[element.id] = element; } }); }); super.initState(); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return WindowFrameWidget( child: Scaffold( appBar: AppBar( toolbarHeight: CustomSize.toolbarHeight, title: BlocBuilder( buildWhen: (previous, current) => current is AdminRoomLoaded, builder: (context, state) { if (state is AdminRoomLoaded) { return Text( state.room.name, style: const TextStyle(fontSize: CustomSize.appBarTitleSize), ); } return const Text( 'Character', style: TextStyle(fontSize: CustomSize.appBarTitleSize), ); }, ), centerTitle: true, backgroundColor: customColors.backgroundContainerColor, ), body: BackgroundContainer( setting: widget.setting, child: RefreshIndicator( color: customColors.linkColor, onRefresh: () async { context.read().add(AdminRoomRecentlyMessagesLoadEvent( userId: widget.userId, roomId: widget.roomId, roomType: widget.roomType, )); }, displacement: 20, child: BlocConsumer( listener: (context, state) { if (state is AdminRoomOperationResult) { if (state.success) { showSuccessMessage(AppLocale.operateSuccess.getString(context)); } else { showErrorMessage(AppLocale.operateFailed.getString(context)); } } }, buildWhen: (previous, current) => current is AdminRoomRecentlyMessagesLoaded, builder: (context, state) { if (state is AdminRoomRecentlyMessagesLoaded) { return SafeArea( top: false, child: state.messages.isNotEmpty ? ChatPreview( padding: const EdgeInsets.only(top: 15, bottom: 15), messages: state.messages.reversed.map((e) { if (e.model != null) { final model = models[e.model]; if (model != null) { if (e.avatarUrl == null && model.avatarUrl != null) { e.avatarUrl = model.avatarUrl; } e.senderName ??= model.name; } } return MessageWithState(e, MessageState()); }).toList(), controller: controller, supportBloc: false, senderNameBuilder: (message) { if (message.role == Role.sender || message.senderName == null) { return null; } return Container( margin: const EdgeInsets.only( left: 10, bottom: 5, right: 5, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( message.senderName!, style: const TextStyle(fontSize: 12), ), if (message.ts != null) Text( ' ${DateFormat('yyyy/MM/dd HH:mm').format(message.ts!.toLocal())}', style: const TextStyle( fontSize: 12, color: Colors.grey, ), ), ], ), ); }, avatarBuilder: (message) { if (message.role == Role.sender) { return null; } if (message.avatarUrl != null) { return RemoteAvatar( avatarUrl: message.avatarUrl!, size: 30, ); } if (message.model != null) { final model = models[message.model]; if (model != null && model.avatarUrl != null) { return RemoteAvatar( avatarUrl: model.avatarUrl!, size: 30, ); } } return null; }, ) : const Center(child: Text('No messages')), ); } return const Center( child: CircularProgressIndicator(), ); }, ), ), ), ), ); } } ================================================ FILE: lib/page/admin/models.dart ================================================ import 'package:askaide/bloc/model_bloc.dart'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/image.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/api/admin/channels.dart'; import 'package:askaide/repo/api/admin/models.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_initicon/flutter_initicon.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:go_router/go_router.dart'; class AdminModelsPage extends StatefulWidget { final SettingRepository setting; const AdminModelsPage({ super.key, required this.setting, }); @override State createState() => _AdminModelsPageState(); } class _AdminModelsPageState extends State { // 渠道 List channels = []; // 搜索关键字 String keyword = ''; /// 查找渠道 AdminChannel searchChannel(AdminModelProvider provider) { return channels.firstWhere( (e) { if (e.id == null && (provider.id == null || provider.id == 0)) { return provider.name == e.type; } return provider.id == e.id; }, orElse: () { return AdminChannel( id: provider.id, name: '未知', type: 'unknown', ); }, ); } @override void initState() { // 加载渠道 APIServer().adminChannelsAgg().then((value) { if (context.mounted) { setState(() { channels = value; }); // 加载模型列表 context.read().add(ModelsLoadEvent()); } }); super.initState(); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( toolbarHeight: CustomSize.toolbarHeight, title: const Text( 'Large Language Model', style: TextStyle(fontSize: CustomSize.appBarTitleSize), ), centerTitle: true, actions: [ IconButton( icon: const Icon(Icons.add), onPressed: () { context.push('/admin/models/create').then((value) { context.read().add(ModelsLoadEvent()); }); }, ), ], ), backgroundColor: customColors.backgroundColor, body: BackgroundContainer( setting: widget.setting, enabled: false, backgroundColor: customColors.backgroundColor, child: Column( children: [ Container( padding: const EdgeInsets.only(left: 10, right: 10, bottom: 5), child: TextField( textAlignVertical: TextAlignVertical.center, style: TextStyle(color: customColors.dialogDefaultTextColor), decoration: InputDecoration( hintText: AppLocale.search.getString(context), hintStyle: TextStyle( color: customColors.dialogDefaultTextColor, ), prefixIcon: Icon( Icons.search, color: customColors.dialogDefaultTextColor, ), isDense: true, border: InputBorder.none, ), onChanged: (value) => setState(() => keyword = value), ), ), Expanded( child: RefreshIndicator( color: customColors.linkColor, onRefresh: () async { context.read().add(ModelsLoadEvent()); }, displacement: 20, child: BlocConsumer( listenWhen: (previous, current) => current is ModelOperationResult, listener: (context, state) { if (state is ModelOperationResult) { if (state.success) { showSuccessMessage(state.message); context.read().add(ModelsLoadEvent()); } else { showErrorMessage(state.message); } } }, buildWhen: (previous, current) => current is ModelsLoaded, builder: (context, state) { if (state is ModelsLoaded) { final models = state.models .where((e) => keyword == '' || e.name.toLowerCase().contains(keyword.toLowerCase()) || e.modelId.toLowerCase().contains(keyword.toLowerCase()) || (e.description ?? '').toLowerCase().contains(keyword.toLowerCase())) .toList(); return SafeArea( top: false, child: ListView.builder( padding: const EdgeInsets.all(5), itemCount: models.length, itemBuilder: (context, index) { final mod = models[index]; return buildModelItem(context, customColors, mod); }, ), ); } return const Center( child: CircularProgressIndicator(), ); }, ), ), ), ], ), ), ), ); } Widget buildModelItem( BuildContext context, CustomColors customColors, AdminModel mod, ) { return Container( margin: const EdgeInsets.symmetric( horizontal: 10, vertical: 5, ), decoration: BoxDecoration(borderRadius: CustomSize.borderRadius), child: Slidable( endActionPane: ActionPane( motion: const ScrollMotion(), children: [ const SizedBox(width: 10), SlidableAction( label: AppLocale.delete.getString(context), borderRadius: const BorderRadius.only( topLeft: CustomSize.radius, bottomLeft: CustomSize.radius, topRight: CustomSize.radius, bottomRight: CustomSize.radius, ), backgroundColor: Colors.red, icon: Icons.delete, onPressed: (_) { openConfirmDialog( context, AppLocale.confirmToDeleteRoom.getString(context), () => context.read().add(ModelDeleteEvent(mod.modelId)), danger: true, ); }, ), ], ), child: Material( borderRadius: CustomSize.borderRadius, color: customColors.columnBlockBackgroundColor, child: InkWell( borderRadius: CustomSize.borderRadiusAll, onTap: () { context.push('/admin/models/edit/${Uri.encodeComponent(mod.modelId)}').then((value) { context.read().add(ModelsLoadEvent()); }); }, child: Stack( children: [ Row( mainAxisSize: MainAxisSize.min, children: [ // 头像 Stack( children: [ buildAvatar(mod), if (mod.isVision) Positioned( left: 0, bottom: 0, child: ClipRRect( borderRadius: const BorderRadius.only(bottomLeft: CustomSize.radius), child: Container( padding: const EdgeInsets.all(3), width: 80, color: Colors.black.withAlpha(30), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.remove_red_eye_outlined, color: Colors.white, size: 12, ), const SizedBox(width: 3), Text( AppLocale.visionTag.getString(context), style: const TextStyle( color: Colors.white, fontSize: 12, ), ) ], ), ), ), ) ], ), // 名称 Expanded( child: Container( padding: const EdgeInsets.all(15), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( mod.name, style: TextStyle( overflow: TextOverflow.ellipsis, fontWeight: mod.enabled ? FontWeight.bold : FontWeight.normal, color: mod.enabled ? null : customColors.weakLinkColor?.withAlpha(100), decoration: mod.enabled ? null : TextDecoration.lineThrough, ), ), const SizedBox(height: 5), Text( buildModelDescription(mod), style: TextStyle( fontSize: 10, overflow: TextOverflow.ellipsis, color: customColors.weakTextColor, ), maxLines: 2, ), ], ), ), ), ], ), Positioned( right: 0, top: 0, child: Container( padding: const EdgeInsets.all(10), width: MediaQuery.of(context).size.width / 2.0, alignment: Alignment.centerRight, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( mod.providers.map((e) => searchChannel(e).display).join('|'), style: TextStyle( fontSize: 10, overflow: TextOverflow.ellipsis, color: customColors.weakTextColor, ), ), ], ), ), ), ], ), ), ), ), ); } Widget buildAvatar(AdminModel mod) { if (mod.avatarUrl != null && mod.avatarUrl!.startsWith('http')) { return SizedBox( width: 80, height: 80, child: ClipRRect( borderRadius: const BorderRadius.only(topLeft: CustomSize.radius, bottomLeft: CustomSize.radius), child: CachedNetworkImage( imageUrl: imageURL(mod.avatarUrl!, qiniuImageTypeAvatar), fit: BoxFit.fill, ), ), ); } return Initicon( text: mod.name.split('、').join(' '), size: 80, backgroundColor: Colors.grey.withAlpha(100), borderRadius: const BorderRadius.only(topLeft: CustomSize.radius, bottomLeft: CustomSize.radius), ); } String buildModelDescription(AdminModel mod) { String desc = ''; if (mod.inputPrice > 0 || mod.outputPrice > 0 || mod.perReqPrice > 0) { desc += '💰 '; if (mod.inputPrice > 0 || mod.outputPrice > 0) { if (mod.inputPrice == mod.outputPrice) { desc += 'IO${AppLocale.creditUnit.getString(context)}${mod.inputPrice} '; } else { desc += 'I${AppLocale.creditUnit.getString(context)}${mod.inputPrice} O${AppLocale.creditUnit.getString(context)}${mod.outputPrice} '; } } if (mod.perReqPrice > 0) { desc += 'R${AppLocale.creditUnit.getString(context)}${mod.perReqPrice}'; } } if (mod.maxContext > 0) { if (desc.isNotEmpty) { desc += ','; } desc += '🎞️ ${mod.maxContext} Tokens'; } if (mod.meta != null && mod.meta!.tag != null && mod.meta!.tag != '') { desc += ' | ${mod.meta!.tag}'; } if (desc != '') { desc += '\n'; } return desc + (mod.description ?? ''); } } ================================================ FILE: lib/page/admin/models_add.dart ================================================ import 'dart:io'; import 'dart:ui'; import 'package:animated_list_plus/animated_list_plus.dart'; import 'package:animated_list_plus/transitions.dart'; import 'package:askaide/bloc/model_bloc.dart'; import 'package:askaide/helper/upload.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/avatar_selector.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/column_block.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/enhanced_button.dart'; import 'package:askaide/page/component/enhanced_input.dart'; import 'package:askaide/page/component/enhanced_textfield.dart'; import 'package:askaide/page/component/image.dart'; import 'package:askaide/page/component/item_selector_search.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/random_avatar.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/weak_text_button.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/api/admin/channels.dart'; import 'package:askaide/repo/api/admin/models.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:auto_size_text/auto_size_text.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:go_router/go_router.dart'; import 'package:quickalert/models/quickalert_type.dart'; class AdminModelCreatePage extends StatefulWidget { final SettingRepository setting; const AdminModelCreatePage({ super.key, required this.setting, }); @override State createState() => _AdminModelCreatePageState(); } class _AdminModelCreatePageState extends State { final TextEditingController nameController = TextEditingController(); final TextEditingController modelIdController = TextEditingController(); final TextEditingController shortNameController = TextEditingController(); final TextEditingController descriptionController = TextEditingController(); final TextEditingController maxContextController = TextEditingController(); final TextEditingController inputPriceController = TextEditingController(); final TextEditingController outputPriceController = TextEditingController(); final TextEditingController perReqPriceController = TextEditingController(); final TextEditingController promptController = TextEditingController(); final TextEditingController categoryController = TextEditingController(); final TextEditingController searchPriceController = TextEditingController(); /// 用于控制是否显示高级选项 bool showAdvancedOptions = false; /// 视觉能力 bool supportVision = false; /// 受限模型 bool restricted = false; /// 模型状态 bool modelEnabled = true; /// 模型头像 String? avatarUrl; List avatarPresets = []; /// 是否是上新 bool isNew = false; /// 是否是推荐模型 bool isRecommended = false; /// 是否启用搜索 bool enableSearch = false; /// 是否启用推理 bool enableReasoning = false; /// 温度 double temperature = 0.0; // 搜索结果数量 int searchCount = 3; /// Tag final TextEditingController tagController = TextEditingController(); String? tagTextColor; String? tagBgColor; // 模型渠道 List modelChannels = []; // 选择的渠道 List providers = []; @override void dispose() { nameController.dispose(); modelIdController.dispose(); shortNameController.dispose(); descriptionController.dispose(); maxContextController.dispose(); inputPriceController.dispose(); outputPriceController.dispose(); perReqPriceController.dispose(); promptController.dispose(); categoryController.dispose(); tagController.dispose(); searchPriceController.dispose(); super.dispose(); } @override void initState() { // 加载预设头像 APIServer().avatars().then((value) { avatarPresets = value; }); // 加载模型渠道 APIServer().adminChannelsAgg().then((value) { modelChannels = value; }); // 初始值设置 maxContextController.value = const TextEditingValue(text: '7500'); inputPriceController.value = const TextEditingValue(text: '0'); outputPriceController.value = const TextEditingValue(text: '0'); perReqPriceController.value = const TextEditingValue(text: '0'); searchPriceController.value = const TextEditingValue(text: '0'); providers.add(AdminModelProvider()); super.initState(); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( toolbarHeight: CustomSize.toolbarHeight, title: const Text( 'New Model', style: TextStyle(fontSize: CustomSize.appBarTitleSize), ), centerTitle: true, ), backgroundColor: customColors.backgroundColor, body: BackgroundContainer( setting: widget.setting, enabled: false, backgroundColor: customColors.backgroundColor, child: SingleChildScrollView( child: BlocListener( listenWhen: (previous, current) => current is ModelOperationResult, listener: (context, state) { if (state is ModelOperationResult) { if (state.success) { showSuccessMessage(state.message); context.pop(); } else { showErrorMessage(state.message); } } }, child: Container( padding: const EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 20), child: Column( children: [ ColumnBlock( children: [ EnhancedTextField( labelText: 'ID', customColors: customColors, controller: modelIdController, textAlignVertical: TextAlignVertical.top, hintText: 'Enter a unique ID', maxLength: 100, showCounter: false, ), EnhancedTextField( labelText: 'Vendor', customColors: customColors, controller: categoryController, textAlignVertical: TextAlignVertical.top, hintText: 'Enter a vendor name (Optional)', maxLength: 100, showCounter: false, ), EnhancedTextField( labelText: 'Name', customColors: customColors, controller: nameController, textAlignVertical: TextAlignVertical.top, hintText: 'Enter a model name', maxLength: 100, showCounter: false, ), EnhancedInput( padding: const EdgeInsets.only(top: 10, bottom: 5), title: Text( 'Avatar', style: TextStyle( color: customColors.textfieldLabelColor, fontSize: 16, ), ), value: Row( mainAxisAlignment: MainAxisAlignment.end, mainAxisSize: MainAxisSize.min, children: [ Container( width: 45, height: 45, decoration: BoxDecoration( borderRadius: CustomSize.borderRadius, image: avatarUrl == null ? null : DecorationImage( image: (avatarUrl!.startsWith('http') ? CachedNetworkImageProviderEnhanced(avatarUrl!) : FileImage(File(avatarUrl!))) as ImageProvider, fit: BoxFit.cover, ), ), child: avatarUrl == null ? const Center( child: Icon( Icons.interests, color: Colors.grey, ), ) : const SizedBox(), ), ], ), onPressed: () { openModalBottomSheet( context, (context) { return AvatarSelector( onSelected: (selected) { setState(() { avatarUrl = selected.url; }); context.pop(); }, usage: AvatarUsage.user, defaultAvatarUrl: avatarUrl, externalAvatarUrls: [ ...avatarPresets, ], ); }, heightFactor: 0.8, ); }, ), EnhancedTextField( labelText: 'Description', customColors: customColors, controller: descriptionController, textAlignVertical: TextAlignVertical.top, hintText: 'Optional', maxLength: 255, showCounter: false, maxLines: 3, ), ], ), ColumnBlock( children: [ EnhancedTextField( labelWidth: 120, labelText: 'Input Price', customColors: customColors, controller: inputPriceController, textAlignVertical: TextAlignVertical.top, hintText: 'Optional', showCounter: false, keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.digitsOnly], textDirection: TextDirection.rtl, suffixIcon: Container( width: 110, alignment: Alignment.center, child: Text( 'Credits/1K Token', style: TextStyle(color: customColors.weakTextColor, fontSize: 12), ), ), ), EnhancedTextField( labelWidth: 120, labelText: 'Output Price', customColors: customColors, controller: outputPriceController, textAlignVertical: TextAlignVertical.top, hintText: 'Optional', showCounter: false, keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.digitsOnly], textDirection: TextDirection.rtl, suffixIcon: Container( width: 110, alignment: Alignment.center, child: Text( 'Credits/1K Token', style: TextStyle(color: customColors.weakTextColor, fontSize: 12), ), ), ), EnhancedTextField( labelWidth: 120, labelText: 'Request Price', customColors: customColors, controller: perReqPriceController, textAlignVertical: TextAlignVertical.top, hintText: 'Optional', showCounter: false, keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.digitsOnly], textDirection: TextDirection.rtl, suffixIcon: Container( width: 110, alignment: Alignment.center, child: Text( 'Credits/Request', style: TextStyle(color: customColors.weakTextColor, fontSize: 12), ), ), ), EnhancedTextField( labelWidth: 120, labelText: 'Search Price', customColors: customColors, controller: searchPriceController, textAlignVertical: TextAlignVertical.top, hintText: 'Optional', showCounter: false, keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.digitsOnly], textDirection: TextDirection.rtl, suffixIcon: Container( width: 110, alignment: Alignment.center, child: Text( 'Credits/Request', style: TextStyle(color: customColors.weakTextColor, fontSize: 12), ), ), ), EnhancedTextField( labelWidth: 120, labelText: 'Context Len', customColors: customColors, controller: maxContextController, textAlignVertical: TextAlignVertical.top, hintText: 'Subtract the expected output length from the maximum context.', showCounter: false, keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.digitsOnly], textDirection: TextDirection.rtl, suffixIcon: Container( width: 50, alignment: Alignment.center, child: Text( 'Token', style: TextStyle(color: customColors.weakTextColor, fontSize: 12), ), ), ), ], ), ImplicitlyAnimatedReorderableList( items: providers, shrinkWrap: true, itemBuilder: (context, itemAnimation, item, index) { return Reorderable( key: ValueKey(item), builder: (context, dragAnimation, inDrag) { final t = dragAnimation.value; final elevation = lerpDouble(0, 8, t); final color = Color.lerp(Colors.white, Colors.white.withOpacity(0.8), t); return SizeFadeTransition( sizeFraction: 0.7, curve: Curves.easeInOut, animation: itemAnimation, child: Material( color: color, elevation: elevation ?? 0, type: MaterialType.transparency, child: Slidable( startActionPane: ActionPane( motion: const ScrollMotion(), children: [ const SizedBox(width: 10), SlidableAction( label: AppLocale.delete.getString(context), borderRadius: CustomSize.borderRadiusAll, backgroundColor: Colors.red, icon: Icons.delete, onPressed: (_) { if (providers.length == 1) { showErrorMessage('At least one channel is needed'); return; } openConfirmDialog( context, AppLocale.confirmToDeleteRoom.getString(context), () { setState(() { providers.removeAt(index); }); }, danger: true, ); }, ), ], ), child: ListTile( contentPadding: const EdgeInsets.all(5), title: ColumnBlock( margin: const EdgeInsets.all(0), children: [ EnhancedInput( title: Text( 'Channel', style: TextStyle( color: customColors.textfieldLabelColor, fontSize: 16, ), ), value: AutoSizeText( buildChannelName(item), maxLines: 1, style: TextStyle( color: customColors.textfieldValueColor, fontSize: 14, ), ), onPressed: () { openListSelectDialog( context, >[ ...modelChannels .map( (e) => SelectorItem( Text( '${e.id == null ? '【Legacy】' : ''}${e.name}', style: e.id == null ? TextStyle( color: customColors.weakTextColorLess, decoration: TextDecoration.lineThrough, ) : null, ), e, ), ) .toList(), ], (value) { setState(() { providers[index].id = value.value.id; if (value.value.id == null) { providers[index].name = value.value.type; } }); return true; }, heightFactor: 0.5, value: item, ); }, ), EnhancedTextField( labelWidth: 90, labelText: 'Rewrite', customColors: customColors, textAlignVertical: TextAlignVertical.top, hintText: 'Optional', maxLength: 100, showCounter: false, initValue: item.modelRewrite, onChanged: (value) { setState(() { providers[index].modelRewrite = value; }); }, labelHelpWidget: InkWell( onTap: () { showBeautyDialog( context, type: QuickAlertType.info, text: 'When the model identifier corresponding to the channel does not match the ID here, calling the channel interface will automatically replace the model with the value configured here.', confirmBtnText: AppLocale.gotIt.getString(context), showCancelBtn: false, ); }, child: Icon( Icons.help_outline, size: 16, color: customColors.weakLinkColor?.withAlpha(150), ), ), ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ const Text( 'Deep Think Model', style: TextStyle(fontSize: 16), ), const SizedBox(width: 5), InkWell( onTap: () { showBeautyDialog( context, type: QuickAlertType.info, text: 'Whether the model is an Deep Thinking model.', confirmBtnText: AppLocale.gotIt.getString(context), showCancelBtn: false, ); }, child: Icon( Icons.help_outline, size: 16, color: customColors.weakLinkColor?.withAlpha(150), ), ), ], ), CupertinoSwitch( activeColor: customColors.linkColor, value: providers[index].type == 'reasoning', onChanged: (value) { setState(() { providers[index].type = value ? 'reasoning' : 'default'; }); }, ), ], ), ], ), trailing: Handle( delay: const Duration(milliseconds: 100), child: Column( children: [ Container( width: 15, height: 15, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.blue.withOpacity(0.1), border: Border.all( color: Colors.blue.withOpacity(0.3), width: 1, ), boxShadow: [ BoxShadow( color: Colors.blue.withOpacity(0.1), blurRadius: 2, spreadRadius: 1, ), ], ), child: Center( child: Text( '${index + 1}', style: TextStyle( fontSize: 9, color: Colors.blue.shade700, fontWeight: FontWeight.w500, ), ), ), ), const SizedBox(height: 10), const Icon( Icons.drag_indicator, size: 20, color: Colors.grey, ), ], ), ), ), ), ), ); }, ); }, areItemsTheSame: (AdminModelProvider oldItem, AdminModelProvider newItem) { return oldItem.id == newItem.id; }, onReorderFinished: (AdminModelProvider item, int from, int to, List newItems) { setState(() { providers = newItems; }); }, ), WeakTextButton( title: 'Add Channel', icon: Icons.add, onPressed: () { setState(() { providers.add(AdminModelProvider()); }); }, ), const SizedBox(height: 10), // 高级选项 if (showAdvancedOptions) ColumnBlock( innerPanding: 5, children: [ EnhancedTextField( labelText: 'Abbr.', customColors: customColors, controller: shortNameController, textAlignVertical: TextAlignVertical.top, hintText: 'Enter model shorthand', maxLength: 100, showCounter: false, ), EnhancedTextField( labelText: 'Tag', customColors: customColors, controller: tagController, textAlignVertical: TextAlignVertical.top, hintText: 'Enter tags', maxLength: 100, showCounter: false, ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ const Text( 'Vision', style: TextStyle(fontSize: 16), ), const SizedBox(width: 5), InkWell( onTap: () { showBeautyDialog( context, type: QuickAlertType.info, text: 'Whether the current model supports visual capabilities.', confirmBtnText: AppLocale.gotIt.getString(context), showCancelBtn: false, ); }, child: Icon( Icons.help_outline, size: 16, color: customColors.weakLinkColor?.withAlpha(150), ), ), ], ), CupertinoSwitch( activeColor: customColors.linkColor, value: supportVision, onChanged: (value) { setState(() { supportVision = value; }); }, ), ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ const Text( 'New', style: TextStyle(fontSize: 16), ), const SizedBox(width: 5), InkWell( onTap: () { showBeautyDialog( context, type: QuickAlertType.info, text: 'Whether to display a "New" icon next to the model to inform users that this is a new model.', confirmBtnText: AppLocale.gotIt.getString(context), showCancelBtn: false, ); }, child: Icon( Icons.help_outline, size: 16, color: customColors.weakLinkColor?.withAlpha(150), ), ), ], ), CupertinoSwitch( activeColor: customColors.linkColor, value: isNew, onChanged: (value) { setState(() { isNew = value; }); }, ), ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ const Text( 'Recommended', style: TextStyle(fontSize: 16), ), const SizedBox(width: 5), InkWell( onTap: () { showBeautyDialog( context, type: QuickAlertType.info, text: 'Whether to display a "Recommended" icon next to the model to inform users that this is a recommended model.', confirmBtnText: AppLocale.gotIt.getString(context), showCancelBtn: false, ); }, child: Icon( Icons.help_outline, size: 16, color: customColors.weakLinkColor?.withAlpha(150), ), ), ], ), CupertinoSwitch( activeColor: customColors.linkColor, value: isRecommended, onChanged: (value) { setState(() { isRecommended = value; }); }, ), ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ const Text( 'Restricted', style: TextStyle(fontSize: 16), ), const SizedBox(width: 5), InkWell( onTap: () { showBeautyDialog( context, type: QuickAlertType.info, text: 'Restricted models refer to models that cannot be used in Chinese Mainland due to policy factors.', confirmBtnText: AppLocale.gotIt.getString(context), showCancelBtn: false, ); }, child: Icon( Icons.help_outline, size: 16, color: customColors.weakLinkColor?.withAlpha(150), ), ), ], ), CupertinoSwitch( activeColor: customColors.linkColor, value: restricted, onChanged: (value) { setState(() { restricted = value; }); }, ), ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ const Text( 'Deep Think', style: TextStyle(fontSize: 16), ), const SizedBox(width: 5), InkWell( onTap: () { showBeautyDialog( context, type: QuickAlertType.info, text: 'Whether to enable Deep Think for the current model.', confirmBtnText: AppLocale.gotIt.getString(context), showCancelBtn: false, ); }, child: Icon( Icons.help_outline, size: 16, color: customColors.weakLinkColor?.withAlpha(150), ), ), ], ), CupertinoSwitch( activeColor: customColors.linkColor, value: enableReasoning, onChanged: (value) { setState(() { enableReasoning = value; }); }, ), ], ), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Temperature', style: TextStyle(fontSize: 16)), Row( children: [ Expanded( child: Slider( value: temperature, min: 0.0, max: 2.0, divisions: 40, label: '$temperature', activeColor: customColors.linkColor, onChanged: (value) { setState(() { temperature = value; }); }, ), ), Text( '$temperature', style: TextStyle( fontSize: 12, color: customColors.weakTextColor, ), ), ], ) ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ const Text( 'Search', style: TextStyle(fontSize: 16), ), const SizedBox(width: 5), InkWell( onTap: () { showBeautyDialog( context, type: QuickAlertType.info, text: 'Whether to enable search for the current model.', confirmBtnText: AppLocale.gotIt.getString(context), showCancelBtn: false, ); }, child: Icon( Icons.help_outline, size: 16, color: customColors.weakLinkColor?.withAlpha(150), ), ), ], ), CupertinoSwitch( activeColor: customColors.linkColor, value: enableSearch, onChanged: (value) { setState(() { enableSearch = value; }); }, ), ], ), if (enableSearch) Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Search Result Count', style: TextStyle(fontSize: 16)), Row( children: [ Expanded( child: Slider( value: searchCount.toDouble() <= 3 ? 3.0 : searchCount.toDouble(), min: 3, max: 50, divisions: 50 - 3, label: '$searchCount', activeColor: customColors.linkColor, onChanged: (value) { setState(() { searchCount = value.toInt(); }); }, ), ), Text( '$searchCount', style: TextStyle( fontSize: 12, color: customColors.weakTextColor, ), ), ], ) ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Enabled', style: TextStyle(fontSize: 16), ), CupertinoSwitch( activeColor: customColors.linkColor, value: modelEnabled, onChanged: (value) { setState(() { modelEnabled = value; }); }, ), ], ), EnhancedTextField( labelPosition: LabelPosition.top, labelText: 'System prompt', customColors: customColors, controller: promptController, textAlignVertical: TextAlignVertical.top, hintText: 'Global system prompt', maxLength: 2000, maxLines: 3, ), ], ), const SizedBox(height: 15), Row( children: [ EnhancedButton( title: showAdvancedOptions ? AppLocale.simpleMode.getString(context) : AppLocale.professionalMode.getString(context), width: 120, backgroundColor: Colors.transparent, color: customColors.weakLinkColor, fontSize: 15, icon: Icon( showAdvancedOptions ? Icons.unfold_less : Icons.unfold_more, color: customColors.weakLinkColor, size: 15, ), onPressed: () { setState(() { showAdvancedOptions = !showAdvancedOptions; }); }, ), const SizedBox(width: 10), Expanded( flex: 1, child: EnhancedButton( title: AppLocale.save.getString(context), onPressed: onSubmit, ), ), ], ), ], ), ), ), ), ), ), ); } /// 提交 void onSubmit() async { if (nameController.text.isEmpty) { showErrorMessage('Please enter a model name'); return; } if (modelIdController.text.isEmpty) { showErrorMessage('Please enter a model ID'); return; } final ps = providers.where((e) => e.id != null || e.name != null).toList(); if (ps.isEmpty) { showErrorMessage('At least one channel is required'); return; } if (avatarUrl != null && (!avatarUrl!.startsWith('http://') && !avatarUrl!.startsWith('https://'))) { final cancel = BotToast.showCustomLoading( toastBuilder: (cancel) { return const LoadingIndicator( message: 'Uploading avatar, please wait...', ); }, allowClick: false, ); try { final res = await ImageUploader(widget.setting).upload(avatarUrl!, usage: 'avatar'); avatarUrl = res.url; } catch (e) { showErrorMessage('Failed to upload avatar'); cancel(); return; } finally { cancel(); } } final model = AdminModelAddReq( name: nameController.text, modelId: modelIdController.text, description: descriptionController.text, shortName: shortNameController.text, meta: AdminModelMeta( maxContext: int.parse(maxContextController.text), inputPrice: int.parse(inputPriceController.text), outputPrice: int.parse(outputPriceController.text), perReqPrice: int.parse(perReqPriceController.text), prompt: promptController.text, vision: supportVision, restricted: restricted, tag: tagController.text, tagTextColor: tagTextColor, tagBgColor: tagBgColor, category: categoryController.text, isNew: isNew, isRecommend: isRecommended, search: enableSearch, reasoning: enableReasoning, searchCount: searchCount, searchPrice: int.parse(searchPriceController.text), temperature: temperature, ), status: modelEnabled ? 1 : 2, providers: ps, avatarUrl: avatarUrl, ); // ignore: use_build_context_synchronously context.read().add(ModelCreateEvent(model)); } /// 渠道名称 String buildChannelName(AdminModelProvider provider) { if (provider.id != null) { return modelChannels.firstWhere((e) => e.id == provider.id).name; } if (provider.name != null) { return modelChannels .firstWhere( (e) => e.type == provider.name! && e.id == null, orElse: () => AdminChannel(name: 'Unknown', type: ''), ) .display; } return 'Select'; } } ================================================ FILE: lib/page/admin/models_edit.dart ================================================ import 'dart:io'; import 'dart:ui'; import 'package:animated_list_plus/animated_list_plus.dart'; import 'package:animated_list_plus/transitions.dart'; import 'package:askaide/bloc/model_bloc.dart'; import 'package:askaide/helper/upload.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/avatar_selector.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/column_block.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/enhanced_button.dart'; import 'package:askaide/page/component/enhanced_input.dart'; import 'package:askaide/page/component/enhanced_textfield.dart'; import 'package:askaide/page/component/image.dart'; import 'package:askaide/page/component/item_selector_search.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/random_avatar.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/weak_text_button.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/api/admin/channels.dart'; import 'package:askaide/repo/api/admin/models.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:auto_size_text/auto_size_text.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:go_router/go_router.dart'; import 'package:quickalert/models/quickalert_type.dart'; class AdminModelEditPage extends StatefulWidget { final SettingRepository setting; final String modelId; const AdminModelEditPage({ super.key, required this.setting, required this.modelId, }); @override State createState() => _AdminModelEditPageState(); } class _AdminModelEditPageState extends State { final TextEditingController nameController = TextEditingController(); final TextEditingController modelIdController = TextEditingController(); final TextEditingController shortNameController = TextEditingController(); final TextEditingController descriptionController = TextEditingController(); final TextEditingController maxContextController = TextEditingController(); final TextEditingController inputPriceController = TextEditingController(); final TextEditingController outputPriceController = TextEditingController(); final TextEditingController perRequestPriceController = TextEditingController(); final TextEditingController promptController = TextEditingController(); final TextEditingController categoryController = TextEditingController(); final TextEditingController searchPriceController = TextEditingController(); /// 用于控制是否显示高级选项 bool showAdvancedOptions = false; /// 视觉能力 bool supportVision = false; /// 受限模型 bool restricted = false; /// 模型状态 bool modelEnabled = true; /// 是否是上新 bool isNew = false; /// 是否是推荐模型 bool isRecommended = false; /// 是否启用搜索 bool enableSearch = false; /// 是否启用推理 bool enableReasoning = false; /// 温度 double temperature = 0.0; // 搜索结果数量 int searchCount = 3; /// Tag final TextEditingController tagController = TextEditingController(); String? tagTextColor; String? tagBgColor; /// 模型头像 String? avatarUrl; List avatarPresets = []; // 模型渠道 List modelChannels = []; // 选择的渠道 List providers = []; /// 是否锁定编辑 bool editLocked = true; @override void dispose() { nameController.dispose(); modelIdController.dispose(); shortNameController.dispose(); descriptionController.dispose(); maxContextController.dispose(); inputPriceController.dispose(); outputPriceController.dispose(); perRequestPriceController.dispose(); promptController.dispose(); categoryController.dispose(); tagController.dispose(); searchPriceController.dispose(); super.dispose(); } @override void initState() { // 加载预设头像 APIServer().avatars().then((value) { avatarPresets = value; }); // 加载模型渠道 APIServer().adminChannelsAgg().then((value) { setState(() { modelChannels = value; }); // 加载模型 context.read().add(ModelLoadEvent(widget.modelId)); }); // 初始值设置 maxContextController.value = const TextEditingValue(text: '7500'); inputPriceController.value = const TextEditingValue(text: '0'); outputPriceController.value = const TextEditingValue(text: '0'); perRequestPriceController.value = const TextEditingValue(text: '0'); searchPriceController.value = const TextEditingValue(text: '0'); super.initState(); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( toolbarHeight: CustomSize.toolbarHeight, title: const Text( 'Edit Model', style: TextStyle(fontSize: CustomSize.appBarTitleSize), ), centerTitle: true, ), backgroundColor: customColors.backgroundColor, body: BackgroundContainer( setting: widget.setting, enabled: false, backgroundColor: customColors.backgroundColor, child: SingleChildScrollView( child: BlocListener( listenWhen: (previous, current) => current is ModelOperationResult || current is ModelLoaded, listener: (context, state) { if (state is ModelOperationResult) { if (state.success) { showSuccessMessage(state.message); context.read().add(ModelLoadEvent(widget.modelId)); } else { showErrorMessage(state.message); } } if (state is ModelLoaded) { modelIdController.value = TextEditingValue(text: state.model.modelId); nameController.value = TextEditingValue(text: state.model.name); if (state.model.description != null) { descriptionController.value = TextEditingValue(text: state.model.description!); } if (state.model.avatarUrl != null) { avatarUrl = state.model.avatarUrl; } modelEnabled = state.model.status == 1; if (state.model.providers.isNotEmpty) { providers = state.model.providers; } if (state.model.meta != null) { if (state.model.meta!.maxContext != null) { maxContextController.value = TextEditingValue(text: state.model.meta!.maxContext.toString()); } if (state.model.meta!.inputPrice != null) { inputPriceController.value = TextEditingValue(text: state.model.meta!.inputPrice.toString()); } if (state.model.meta!.outputPrice != null) { outputPriceController.value = TextEditingValue(text: state.model.meta!.outputPrice.toString()); } if (state.model.meta!.perReqPrice != null) { perRequestPriceController.value = TextEditingValue(text: state.model.meta!.perReqPrice.toString()); } if (state.model.meta!.searchPrice != null) { searchPriceController.value = TextEditingValue(text: state.model.meta!.searchPrice.toString()); } shortNameController.value = TextEditingValue(text: state.model.shortName ?? ''); promptController.value = TextEditingValue(text: state.model.meta!.prompt ?? ''); supportVision = state.model.meta!.vision ?? false; restricted = state.model.meta!.restricted ?? false; tagController.value = TextEditingValue(text: state.model.meta!.tag ?? ''); tagTextColor = state.model.meta!.tagTextColor; tagBgColor = state.model.meta!.tagBgColor; isNew = state.model.meta!.isNew ?? false; isRecommended = state.model.meta!.isRecommend ?? false; categoryController.value = TextEditingValue(text: state.model.meta!.category ?? ''); enableSearch = state.model.meta!.search ?? false; enableReasoning = state.model.meta!.reasoning ?? false; temperature = state.model.meta!.temperature ?? 0.0; searchCount = state.model.meta!.searchCount ?? 3; searchCount = searchCount <= 3 ? 3 : searchCount; setState(() {}); } } setState(() { editLocked = false; }); }, child: Container( padding: const EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 20), child: Column( children: [ ColumnBlock( children: [ EnhancedTextField( labelText: 'ID', customColors: customColors, controller: modelIdController, textAlignVertical: TextAlignVertical.top, hintText: 'Enter a unique ID', maxLength: 100, showCounter: false, readOnly: true, ), EnhancedTextField( labelText: 'Vendor', customColors: customColors, controller: categoryController, textAlignVertical: TextAlignVertical.top, hintText: 'Enter a vendor name (Optional)', maxLength: 100, showCounter: false, ), EnhancedTextField( labelText: 'Name', customColors: customColors, controller: nameController, textAlignVertical: TextAlignVertical.top, hintText: 'Enter a model name', maxLength: 100, showCounter: false, ), EnhancedInput( padding: const EdgeInsets.only(top: 10, bottom: 5), title: Text( 'Avatar', style: TextStyle( color: customColors.textfieldLabelColor, fontSize: 16, ), ), value: Row( mainAxisAlignment: MainAxisAlignment.end, mainAxisSize: MainAxisSize.min, children: [ Container( width: 45, height: 45, decoration: BoxDecoration( borderRadius: CustomSize.borderRadius, image: avatarUrl == null ? null : DecorationImage( image: (avatarUrl!.startsWith('http') ? CachedNetworkImageProviderEnhanced(avatarUrl!) : FileImage(File(avatarUrl!))) as ImageProvider, fit: BoxFit.cover, ), ), child: avatarUrl == null ? const Center( child: Icon( Icons.interests, color: Colors.grey, ), ) : const SizedBox(), ), ], ), onPressed: () { openModalBottomSheet( context, (context) { return AvatarSelector( onSelected: (selected) { setState(() { avatarUrl = selected.url; }); context.pop(); }, usage: AvatarUsage.user, defaultAvatarUrl: avatarUrl, externalAvatarUrls: [ ...avatarPresets, ], ); }, heightFactor: 0.8, ); }, ), EnhancedTextField( labelText: 'Description', customColors: customColors, controller: descriptionController, textAlignVertical: TextAlignVertical.top, hintText: 'Optional', maxLength: 255, showCounter: false, maxLines: 3, ), ], ), ColumnBlock( children: [ EnhancedTextField( labelWidth: 120, labelText: 'Input Price', customColors: customColors, controller: inputPriceController, textAlignVertical: TextAlignVertical.top, hintText: 'Optional', showCounter: false, keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.digitsOnly], textDirection: TextDirection.rtl, suffixIcon: Container( width: 110, alignment: Alignment.center, child: Text( 'Credits/1K Token', style: TextStyle(color: customColors.weakTextColor, fontSize: 12), ), ), ), EnhancedTextField( labelWidth: 120, labelText: 'Output Price', customColors: customColors, controller: outputPriceController, textAlignVertical: TextAlignVertical.top, hintText: 'Optional', showCounter: false, keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.digitsOnly], textDirection: TextDirection.rtl, suffixIcon: Container( width: 110, alignment: Alignment.center, child: Text( 'Credits/1K Token', style: TextStyle(color: customColors.weakTextColor, fontSize: 12), ), ), ), EnhancedTextField( labelWidth: 120, labelText: 'Request Price', customColors: customColors, controller: perRequestPriceController, textAlignVertical: TextAlignVertical.top, hintText: 'Optional', showCounter: false, keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.digitsOnly], textDirection: TextDirection.rtl, suffixIcon: Container( width: 110, alignment: Alignment.center, child: Text( 'Credits/Request', style: TextStyle(color: customColors.weakTextColor, fontSize: 12), ), ), ), EnhancedTextField( labelWidth: 120, labelText: 'Search Price', customColors: customColors, controller: searchPriceController, textAlignVertical: TextAlignVertical.top, hintText: 'Optional', showCounter: false, keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.digitsOnly], textDirection: TextDirection.rtl, suffixIcon: Container( width: 110, alignment: Alignment.center, child: Text( 'Credits/Request', style: TextStyle(color: customColors.weakTextColor, fontSize: 12), ), ), ), EnhancedTextField( labelWidth: 120, labelText: 'Context Len', customColors: customColors, controller: maxContextController, textAlignVertical: TextAlignVertical.top, hintText: 'Subtract the expected output length from the maximum context.', showCounter: false, keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.digitsOnly], textDirection: TextDirection.rtl, suffixIcon: Container( width: 50, alignment: Alignment.center, child: Text( 'Token', style: TextStyle(color: customColors.weakTextColor, fontSize: 12), ), ), ), ], ), ImplicitlyAnimatedReorderableList( items: providers, shrinkWrap: true, itemBuilder: (context, itemAnimation, item, index) { return Reorderable( key: ValueKey(item), builder: (context, dragAnimation, inDrag) { final t = dragAnimation.value; final elevation = lerpDouble(0, 8, t); final color = Color.lerp(Colors.white, Colors.white.withOpacity(0.8), t); return SizeFadeTransition( sizeFraction: 0.7, curve: Curves.easeInOut, animation: itemAnimation, child: Material( color: color, elevation: elevation ?? 0, type: MaterialType.transparency, child: Slidable( startActionPane: ActionPane( motion: const ScrollMotion(), children: [ const SizedBox(width: 10), SlidableAction( label: AppLocale.delete.getString(context), borderRadius: CustomSize.borderRadiusAll, backgroundColor: Colors.red, icon: Icons.delete, onPressed: (_) { if (providers.length == 1) { showErrorMessage('At least one channel is needed'); return; } openConfirmDialog( context, AppLocale.confirmToDeleteRoom.getString(context), () { setState(() { providers.removeAt(index); }); }, danger: true, ); }, ), ], ), child: ListTile( contentPadding: const EdgeInsets.all(5), title: ColumnBlock( margin: const EdgeInsets.all(0), children: [ EnhancedInput( title: Text( 'Channel', style: TextStyle( color: customColors.textfieldLabelColor, fontSize: 16, ), ), value: AutoSizeText( buildChannelName(item), maxLines: 1, style: TextStyle( color: customColors.textfieldValueColor, fontSize: 14, ), ), onPressed: () { openListSelectDialog( context, >[ ...modelChannels .map( (e) => SelectorItem( Text( '${e.id == null ? '【Legacy】' : ''}${e.name}', style: e.id == null ? TextStyle( color: customColors.weakTextColorLess, decoration: TextDecoration.lineThrough, ) : null, ), e, ), ) .toList(), ], (value) { setState(() { providers[index].id = value.value.id; if (value.value.id == null) { providers[index].name = value.value.type; } }); return true; }, heightFactor: 0.5, value: item, ); }, ), EnhancedTextField( labelWidth: 90, labelText: 'Rewrite', customColors: customColors, textAlignVertical: TextAlignVertical.top, hintText: 'Optional', maxLength: 100, showCounter: false, initValue: item.modelRewrite, onChanged: (value) { setState(() { providers[index].modelRewrite = value; }); }, labelHelpWidget: InkWell( onTap: () { showBeautyDialog( context, type: QuickAlertType.info, text: 'When the model identifier corresponding to the channel does not match the ID here, calling the channel interface will automatically replace the model with the value configured here.', confirmBtnText: AppLocale.gotIt.getString(context), showCancelBtn: false, ); }, child: Icon( Icons.help_outline, size: 16, color: customColors.weakLinkColor?.withAlpha(150), ), ), ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ const Text( 'Deep Think Model', style: TextStyle(fontSize: 16), ), const SizedBox(width: 5), InkWell( onTap: () { showBeautyDialog( context, type: QuickAlertType.info, text: 'Whether the model is an Deep Thinking model.', confirmBtnText: AppLocale.gotIt.getString(context), showCancelBtn: false, ); }, child: Icon( Icons.help_outline, size: 16, color: customColors.weakLinkColor?.withAlpha(150), ), ), ], ), CupertinoSwitch( activeColor: customColors.linkColor, value: providers[index].type == 'reasoning', onChanged: (value) { setState(() { providers[index].type = value ? 'reasoning' : 'default'; }); }, ), ], ), ], ), trailing: Handle( delay: const Duration(milliseconds: 100), child: Column( children: [ Container( width: 15, height: 15, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.blue.withOpacity(0.1), border: Border.all( color: Colors.blue.withOpacity(0.3), width: 1, ), boxShadow: [ BoxShadow( color: Colors.blue.withOpacity(0.1), blurRadius: 2, spreadRadius: 1, ), ], ), child: Center( child: Text( '${index + 1}', style: TextStyle( fontSize: 9, color: Colors.blue.shade700, fontWeight: FontWeight.w500, ), ), ), ), const SizedBox(height: 10), const Icon( Icons.drag_indicator, size: 20, color: Colors.grey, ), ], ), ), ), ), ), ); }, ); }, areItemsTheSame: (AdminModelProvider oldItem, AdminModelProvider newItem) { return oldItem.id == newItem.id; }, onReorderFinished: (AdminModelProvider item, int from, int to, List newItems) { setState(() { providers = newItems; }); }, ), const SizedBox(width: 10), WeakTextButton( title: 'Add Channel', icon: Icons.add, onPressed: () { setState(() { providers.add(AdminModelProvider()); }); }, ), // 高级选项 if (showAdvancedOptions) ColumnBlock( innerPanding: 5, children: [ EnhancedTextField( labelText: 'Abbr.', customColors: customColors, controller: shortNameController, textAlignVertical: TextAlignVertical.top, hintText: 'Enter model shorthand', maxLength: 100, showCounter: false, ), EnhancedTextField( labelText: 'Tag', customColors: customColors, controller: tagController, textAlignVertical: TextAlignVertical.top, hintText: 'Enter tags', maxLength: 100, showCounter: false, ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ const Text( 'Vision', style: TextStyle(fontSize: 16), ), const SizedBox(width: 5), InkWell( onTap: () { showBeautyDialog( context, type: QuickAlertType.info, text: 'Whether the current model supports visual capabilities.', confirmBtnText: AppLocale.gotIt.getString(context), showCancelBtn: false, ); }, child: Icon( Icons.help_outline, size: 16, color: customColors.weakLinkColor?.withAlpha(150), ), ), ], ), CupertinoSwitch( activeColor: customColors.linkColor, value: supportVision, onChanged: (value) { setState(() { supportVision = value; }); }, ), ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ const Text( 'New', style: TextStyle(fontSize: 16), ), const SizedBox(width: 5), InkWell( onTap: () { showBeautyDialog( context, type: QuickAlertType.info, text: 'Whether to display a "New" icon next to the model to inform users that this is a new model.', confirmBtnText: AppLocale.gotIt.getString(context), showCancelBtn: false, ); }, child: Icon( Icons.help_outline, size: 16, color: customColors.weakLinkColor?.withAlpha(150), ), ), ], ), CupertinoSwitch( activeColor: customColors.linkColor, value: isNew, onChanged: (value) { setState(() { isNew = value; }); }, ), ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ const Text( 'Recommended', style: TextStyle(fontSize: 16), ), const SizedBox(width: 5), InkWell( onTap: () { showBeautyDialog( context, type: QuickAlertType.info, text: 'Whether to display a "Recommended" icon next to the model to inform users that this is a recommended model.', confirmBtnText: AppLocale.gotIt.getString(context), showCancelBtn: false, ); }, child: Icon( Icons.help_outline, size: 16, color: customColors.weakLinkColor?.withAlpha(150), ), ), ], ), CupertinoSwitch( activeColor: customColors.linkColor, value: isRecommended, onChanged: (value) { setState(() { isRecommended = value; }); }, ), ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ const Text( 'Restricted', style: TextStyle(fontSize: 16), ), const SizedBox(width: 5), InkWell( onTap: () { showBeautyDialog( context, type: QuickAlertType.info, text: 'Restricted models refer to models that cannot be used in Chinese Mainland due to policy factors.', confirmBtnText: AppLocale.gotIt.getString(context), showCancelBtn: false, ); }, child: Icon( Icons.help_outline, size: 16, color: customColors.weakLinkColor?.withAlpha(150), ), ), ], ), CupertinoSwitch( activeColor: customColors.linkColor, value: restricted, onChanged: (value) { setState(() { restricted = value; }); }, ), ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ const Text( 'Deep Think', style: TextStyle(fontSize: 16), ), const SizedBox(width: 5), InkWell( onTap: () { showBeautyDialog( context, type: QuickAlertType.info, text: 'Whether to enable Deep Think for the current model.', confirmBtnText: AppLocale.gotIt.getString(context), showCancelBtn: false, ); }, child: Icon( Icons.help_outline, size: 16, color: customColors.weakLinkColor?.withAlpha(150), ), ), ], ), CupertinoSwitch( activeColor: customColors.linkColor, value: enableReasoning, onChanged: (value) { setState(() { enableReasoning = value; }); }, ), ], ), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Temperature', style: TextStyle(fontSize: 16)), Row( children: [ Expanded( child: Slider( value: temperature, min: 0.0, max: 2.0, divisions: 40, label: '$temperature', activeColor: customColors.linkColor, onChanged: (value) { setState(() { temperature = value; }); }, ), ), Text( '$temperature', style: TextStyle( fontSize: 12, color: customColors.weakTextColor, ), ), ], ) ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ const Text( 'Search', style: TextStyle(fontSize: 16), ), const SizedBox(width: 5), InkWell( onTap: () { showBeautyDialog( context, type: QuickAlertType.info, text: 'Whether to enable search for the current model.', confirmBtnText: AppLocale.gotIt.getString(context), showCancelBtn: false, ); }, child: Icon( Icons.help_outline, size: 16, color: customColors.weakLinkColor?.withAlpha(150), ), ), ], ), CupertinoSwitch( activeColor: customColors.linkColor, value: enableSearch, onChanged: (value) { setState(() { enableSearch = value; }); }, ), ], ), if (enableSearch) Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Search Result Count', style: TextStyle(fontSize: 16)), Row( children: [ Expanded( child: Slider( value: searchCount.toDouble() <= 3 ? 3.0 : searchCount.toDouble(), min: 3, max: 50, divisions: 50 - 3, label: '$searchCount', activeColor: customColors.linkColor, onChanged: (value) { setState(() { searchCount = value.toInt(); }); }, ), ), Text( '$searchCount', style: TextStyle( fontSize: 12, color: customColors.weakTextColor, ), ), ], ) ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Enabled', style: TextStyle(fontSize: 16), ), CupertinoSwitch( activeColor: customColors.linkColor, value: modelEnabled, onChanged: (value) { setState(() { modelEnabled = value; }); }, ), ], ), EnhancedTextField( labelPosition: LabelPosition.top, labelText: 'System prompt', customColors: customColors, controller: promptController, textAlignVertical: TextAlignVertical.top, hintText: 'Global system prompt', maxLength: 2000, maxLines: 3, ), ], ), const SizedBox(height: 15), Row( children: [ EnhancedButton( title: showAdvancedOptions ? AppLocale.simpleMode.getString(context) : AppLocale.professionalMode.getString(context), width: 120, backgroundColor: Colors.transparent, color: customColors.weakLinkColor, fontSize: 15, icon: Icon( showAdvancedOptions ? Icons.unfold_less : Icons.unfold_more, color: customColors.weakLinkColor, size: 15, ), onPressed: () { setState(() { showAdvancedOptions = !showAdvancedOptions; }); }, ), const SizedBox(width: 10), Expanded( flex: 1, child: EnhancedButton( title: AppLocale.save.getString(context), onPressed: onSubmit, icon: editLocked ? const Icon(Icons.lock, color: Colors.white, size: 16) : const Icon(Icons.lock_open, color: Colors.white, size: 16), ), ), ], ), ], ), ), ), ), ), ), ); } /// 提交 void onSubmit() async { if (editLocked) { return; } if (nameController.text.isEmpty) { showErrorMessage('Please enter a model name'); return; } final ps = providers.where((e) => e.id != null || e.name != null).toList(); if (ps.isEmpty) { showErrorMessage('At least one channel is required'); return; } if (avatarUrl != null && (!avatarUrl!.startsWith('http://') && !avatarUrl!.startsWith('https://'))) { final cancel = BotToast.showCustomLoading( toastBuilder: (cancel) { return const LoadingIndicator( message: 'Uploading avatar, please wait...', ); }, allowClick: false, ); try { final res = await ImageUploader(widget.setting).upload(avatarUrl!, usage: 'avatar'); avatarUrl = res.url; } catch (e) { showErrorMessage('Failed to upload avatar'); cancel(); return; } finally { cancel(); } } final model = AdminModelUpdateReq( name: nameController.text, description: descriptionController.text, shortName: shortNameController.text, meta: AdminModelMeta( maxContext: int.parse(maxContextController.text), inputPrice: int.parse(inputPriceController.text), outputPrice: int.parse(outputPriceController.text), perReqPrice: int.parse(perRequestPriceController.text), prompt: promptController.text, vision: supportVision, restricted: restricted, category: categoryController.text, tag: tagController.text, tagTextColor: tagTextColor, tagBgColor: tagBgColor, isNew: isNew, isRecommend: isRecommended, search: enableSearch, reasoning: enableReasoning, temperature: temperature, searchCount: searchCount, searchPrice: int.parse(searchPriceController.text), ), status: modelEnabled ? 1 : 2, providers: ps, avatarUrl: avatarUrl, ); setState(() { editLocked = true; }); // ignore: use_build_context_synchronously context.read().add(ModelUpdateEvent(widget.modelId, model)); } /// 渠道名称 String buildChannelName(AdminModelProvider provider) { if (provider.id != null) { return modelChannels.firstWhere((e) => e.id == provider.id).name; } if (provider.name != null) { return modelChannels .firstWhere( (e) => e.type == provider.name! && e.id == null, orElse: () => AdminChannel(name: 'Unknown', type: ''), ) .display; } return 'Select'; } } ================================================ FILE: lib/page/admin/payments.dart ================================================ import 'package:askaide/bloc/admin_payment_bloc.dart'; import 'package:askaide/bloc/user_bloc.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/pagination.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/api/admin/payment.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; class PaymentHistoriesPage extends StatefulWidget { final SettingRepository setting; const PaymentHistoriesPage({ super.key, required this.setting, }); @override State createState() => _PaymentHistoriesPageState(); } class _PaymentHistoriesPageState extends State { /// 当前页码 int page = 1; /// 每页数量 int perPage = 20; /// 搜索关键字 final TextEditingController keywordController = TextEditingController(); @override void initState() { context.read().add(AdminPaymentHistoriesLoadEvent( perPage: perPage, page: page, keyword: keywordController.text, )); super.initState(); } @override void dispose() { keywordController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( toolbarHeight: CustomSize.toolbarHeight, title: const Text( 'Payment Order History', style: TextStyle(fontSize: CustomSize.appBarTitleSize), ), centerTitle: true, ), backgroundColor: customColors.backgroundColor, body: BackgroundContainer( setting: widget.setting, enabled: false, backgroundColor: customColors.backgroundColor, child: Column( children: [ Container( padding: const EdgeInsets.only(left: 10, right: 10, bottom: 5), child: TextField( controller: keywordController, textAlignVertical: TextAlignVertical.center, style: TextStyle(color: customColors.dialogDefaultTextColor), decoration: InputDecoration( hintText: AppLocale.search.getString(context), hintStyle: TextStyle( color: customColors.dialogDefaultTextColor, ), prefixIcon: Icon( Icons.search, color: customColors.dialogDefaultTextColor, ), isDense: true, border: InputBorder.none, ), onEditingComplete: () { context.read().add(AdminPaymentHistoriesLoadEvent( perPage: perPage, page: page, keyword: keywordController.text, )); }, ), ), Expanded( child: RefreshIndicator( color: customColors.linkColor, onRefresh: () async { context.read().add(AdminPaymentHistoriesLoadEvent( perPage: perPage, page: page, keyword: keywordController.text, )); }, displacement: 20, child: BlocConsumer( listener: (context, state) { if (state is AdminPaymentOperationResult) { if (state.success) { showSuccessMessage(state.message); context.read().add(UserListLoadEvent()); } else { showErrorMessage(state.message); } } if (state is AdminPaymentHistoriesLoaded) { setState(() { page = state.histories.page; perPage = state.histories.perPage; }); } }, buildWhen: (previous, current) => current is AdminPaymentHistoriesLoaded, builder: (context, state) { if (state is AdminPaymentHistoriesLoaded) { return SafeArea( top: false, child: Column( children: [ Expanded( child: ListView.builder( padding: const EdgeInsets.all(5), itemCount: state.histories.data.length, itemBuilder: (context, index) { return buildHistoryInfo( context, customColors, state.histories.data[index], ); }, ), ), if (state.histories.lastPage != null && state.histories.lastPage! > 1) Container( padding: const EdgeInsets.all(10), child: Pagination( numOfPages: state.histories.lastPage ?? 1, selectedPage: page, pagesVisible: 5, onPageChanged: (selected) { context.read().add(AdminPaymentHistoriesLoadEvent( perPage: perPage, page: selected, keyword: keywordController.text, )); }, ), ), ], ), ); } return const Center( child: CircularProgressIndicator(), ); }, ), ), ), ], ), ), ), ); } Widget buildHistoryInfo( BuildContext context, CustomColors customColors, AdminPaymentHistory his, ) { return Container( margin: const EdgeInsets.symmetric( horizontal: 10, vertical: 5, ), decoration: BoxDecoration(borderRadius: CustomSize.borderRadius), child: Slidable( child: Material( borderRadius: CustomSize.borderRadius, color: customColors.columnBlockBackgroundColor, child: InkWell( borderRadius: CustomSize.borderRadiusAll, onTap: () { context.push('/admin/users/${his.userId}'); }, child: Stack( children: [ Row( mainAxisSize: MainAxisSize.min, children: [ // 头像 buildAvatar(his, radius: CustomSize.borderRadiusAll), // 名称 Expanded( child: Container( padding: const EdgeInsets.all(15), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( '@${his.userId} Charge ', style: const TextStyle( overflow: TextOverflow.ellipsis, ), ), Text( '¥${(his.retailPrice / 100).ceil()}', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.red, ), ), const SizedBox(width: 5), Text( '#${his.id}', style: TextStyle( fontSize: 10, color: customColors.weakTextColor, ), ), ], ), const SizedBox(height: 5), buildTags(context, customColors, his), ], ), ), ), ], ), Positioned( right: 0, top: 0, child: Container( padding: const EdgeInsets.all(10), width: MediaQuery.of(context).size.width / 2.0, alignment: Alignment.centerRight, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( his.environment, style: TextStyle( fontSize: 10, overflow: TextOverflow.ellipsis, fontWeight: FontWeight.bold, color: his.environment.toLowerCase() == 'production' ? customColors.linkColor : Colors.amber, ), ), ], ), ), ), Positioned( right: 0, bottom: 0, child: Container( padding: const EdgeInsets.all(10), width: MediaQuery.of(context).size.width / 2.0, alignment: Alignment.centerRight, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( DateFormat('y-MM-dd HH:mm').format(his.purchaseAt.toLocal()), style: TextStyle( fontSize: 10, overflow: TextOverflow.ellipsis, color: customColors.weakTextColor, ), ), ], ), ), ), ], ), ), ), ), ); } } Widget buildAvatar( AdminPaymentHistory his, { BorderRadius radius = const BorderRadius.only(topLeft: CustomSize.radius, bottomLeft: CustomSize.radius), }) { final source = (his.source ?? '').toLowerCase(); var image = ''; if (source.contains('支付宝') || source.contains('alipay')) { image = 'assets/zhifubao.png'; } else if (source.contains('微信') || source.contains('wechat')) { image = 'assets/wechat-pay.png'; } else if (source.contains('stripe')) { image = 'assets/stripe.png'; } else if (source.contains('apple')) { image = 'assets/apple.webp'; } else { image = 'assets/app.png'; } return SizedBox( width: 70, height: 70, child: ClipRRect( borderRadius: radius, child: Image.asset(image), ), ); } Widget buildTags(BuildContext context, CustomColors customColors, AdminPaymentHistory his) { final tags = []; if (his.source != null) { tags.add(buildTag(context, customColors, his.source!)); } return Wrap( spacing: 5, runSpacing: 5, children: tags, ); } Widget buildTag(BuildContext context, CustomColors customColors, String s) { return Container( padding: const EdgeInsets.symmetric( horizontal: 5, vertical: 2, ), decoration: BoxDecoration(color: customColors.tagsBackground, borderRadius: CustomSize.borderRadius), child: Text( s, style: TextStyle( fontSize: 10, color: customColors.tagsText, ), ), ); } ================================================ FILE: lib/page/admin/recently_messages.dart ================================================ import 'package:askaide/bloc/admin_room_bloc.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/pagination.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; class AdminRecentlyMessagesPage extends StatefulWidget { final SettingRepository setting; const AdminRecentlyMessagesPage({super.key, required this.setting}); @override State createState() => _AdminRecentlyMessagesPageState(); } class _AdminRecentlyMessagesPageState extends State { /// 当前页码 int page = 1; /// 每页数量 int perPage = 20; /// 搜索关键字 final TextEditingController keywordController = TextEditingController(); @override void initState() { context.read().add(AdminRecentlyMessagesLoadEvent( perPage: perPage, page: page, keyword: keywordController.text, )); super.initState(); } @override void dispose() { keywordController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( toolbarHeight: CustomSize.toolbarHeight, title: const Text( 'Chat History', style: TextStyle(fontSize: CustomSize.appBarTitleSize), ), centerTitle: true, ), backgroundColor: customColors.backgroundColor, body: BackgroundContainer( setting: widget.setting, backgroundColor: customColors.backgroundColor, enabled: false, child: Column( children: [ Container( padding: const EdgeInsets.only(left: 10, right: 10, bottom: 5), child: TextField( controller: keywordController, textAlignVertical: TextAlignVertical.center, style: TextStyle(color: customColors.dialogDefaultTextColor), decoration: InputDecoration( hintText: AppLocale.search.getString(context), hintStyle: TextStyle( color: customColors.dialogDefaultTextColor, ), prefixIcon: Icon( Icons.search, color: customColors.dialogDefaultTextColor, ), isDense: true, border: InputBorder.none, ), onEditingComplete: () { context.read().add(AdminRecentlyMessagesLoadEvent( perPage: perPage, page: page, keyword: keywordController.text, )); }, ), ), Expanded( child: RefreshIndicator( color: customColors.linkColor, onRefresh: () async { context.read().add(AdminRecentlyMessagesLoadEvent( perPage: perPage, page: page, keyword: keywordController.text, )); }, displacement: 20, child: BlocConsumer( listener: (context, state) { if (state is AdminRoomOperationResult) { if (state.success) { showSuccessMessage(AppLocale.operateSuccess.getString(context)); } else { showErrorMessage(AppLocale.operateFailed.getString(context)); } } if (state is AdminRecentlyMessagesLoaded) { setState(() { page = state.messages.page; perPage = state.messages.perPage; }); } }, buildWhen: (previous, current) => current is AdminRecentlyMessagesLoaded, builder: (context, state) { if (state is AdminRecentlyMessagesLoaded) { return SafeArea( top: false, child: Column( children: [ Expanded( child: ListView.builder( padding: const EdgeInsets.all(5), itemCount: state.messages.data.length, itemBuilder: (context, index) { final message = state.messages.data[index]; return Container( margin: const EdgeInsets.symmetric( horizontal: 10, vertical: 5, ), decoration: BoxDecoration(borderRadius: CustomSize.borderRadius), child: Slidable( startActionPane: ActionPane(motion: const ScrollMotion(), children: [ SlidableAction( label: AppLocale.character.getString(context), borderRadius: const BorderRadius.only( topLeft: CustomSize.radius, bottomLeft: CustomSize.radius, topRight: CustomSize.radius, bottomRight: CustomSize.radius, ), backgroundColor: Colors.blue, icon: Icons.people, foregroundColor: Colors.white, onPressed: (_) { context.push('/admin/users/${message.userId}/rooms'); }, ), ]), endActionPane: ActionPane( motion: const ScrollMotion(), children: [ const SizedBox(width: 10), SlidableAction( label: 'User', borderRadius: const BorderRadius.only( topLeft: CustomSize.radius, bottomLeft: CustomSize.radius, topRight: CustomSize.radius, bottomRight: CustomSize.radius, ), backgroundColor: customColors.linkColor ?? Colors.green, icon: Icons.person, foregroundColor: Colors.white, onPressed: (_) { context.push('/admin/users/${message.userId}'); }, ), ], ), child: Material( borderRadius: CustomSize.borderRadius, color: customColors.columnBlockBackgroundColor, child: InkWell( borderRadius: CustomSize.borderRadiusAll, onTap: () { context.push( '/admin/users/${message.userId}/rooms/${message.roomId}/messages?room_type=1'); }, child: Stack( children: [ Positioned( top: 0, right: 0, child: Container( padding: const EdgeInsets.symmetric( horizontal: 5, vertical: 2, ), decoration: BoxDecoration( color: customColors.columnBlockBackgroundColor?.withAlpha(100), borderRadius: const BorderRadius.only( topRight: CustomSize.radius, bottomLeft: CustomSize.radius, ), ), child: Text( '@ ${message.userId}', style: TextStyle( fontSize: 12, color: customColors.weakTextColor?.withAlpha(100), ), ), ), ), Container( padding: const EdgeInsets.all(15), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( state.messages.data[index].text, maxLines: 2, style: const TextStyle( overflow: TextOverflow.ellipsis, ), ), const SizedBox(height: 5), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( '${message.model}', style: const TextStyle( fontSize: 12, color: Colors.grey, ), overflow: TextOverflow.ellipsis, maxLines: 1, ), ), if (message.ts != null) Text( ' ${DateFormat('MM/dd HH:mm').format(message.ts!.toLocal())}', style: const TextStyle( fontSize: 12, color: Colors.grey, ), ), ], ), ], ), ), ], ), ), ), ), ); }, ), ), if (state.messages.lastPage != null && state.messages.lastPage! > 1) Container( padding: const EdgeInsets.all(10), child: Pagination( numOfPages: state.messages.lastPage ?? 1, selectedPage: page, pagesVisible: 5, onPageChanged: (selected) { context.read().add(AdminRecentlyMessagesLoadEvent( perPage: perPage, page: selected, keyword: keywordController.text, )); }, ), ), ], ), ); } return const Center( child: CircularProgressIndicator(), ); }, ), ), ), ], ), ), ), ); } } ================================================ FILE: lib/page/admin/rooms.dart ================================================ import 'package:askaide/bloc/admin_room_bloc.dart'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/helper.dart'; import 'package:askaide/helper/image.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/chat/component/group_avatar.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/image.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/model/misc.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_initicon/flutter_initicon.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; class AdminRoomsPage extends StatefulWidget { final SettingRepository setting; final int userId; const AdminRoomsPage({ super.key, required this.setting, required this.userId, }); @override State createState() => _AdminRoomsPageState(); } class _AdminRoomsPageState extends State { @override void initState() { context.read().add(AdminRoomsLoadEvent(userId: widget.userId)); super.initState(); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( toolbarHeight: CustomSize.toolbarHeight, title: const Text( 'Characters', style: TextStyle(fontSize: CustomSize.appBarTitleSize), ), centerTitle: true, ), backgroundColor: customColors.backgroundColor, body: BackgroundContainer( setting: widget.setting, enabled: false, backgroundColor: customColors.backgroundColor, child: RefreshIndicator( color: customColors.linkColor, onRefresh: () async { context.read().add(AdminRoomsLoadEvent(userId: widget.userId)); }, displacement: 20, child: BlocConsumer( listener: (context, state) { if (state is AdminRoomOperationResult) { if (state.success) { showSuccessMessage(AppLocale.operateSuccess.getString(context)); } else { showErrorMessage(AppLocale.operateFailed.getString(context)); } } }, buildWhen: (previous, current) => current is AdminRoomsLoaded, builder: (context, state) { if (state is AdminRoomsLoaded) { return SafeArea( top: false, child: ListView.builder( padding: const EdgeInsets.all(5), itemCount: state.rooms.length, itemBuilder: (context, index) { final room = state.rooms[index]; return Container( margin: const EdgeInsets.symmetric( horizontal: 10, vertical: 5, ), decoration: BoxDecoration(borderRadius: CustomSize.borderRadius), child: Material( borderRadius: CustomSize.borderRadius, color: customColors.columnBlockBackgroundColor, child: InkWell( borderRadius: CustomSize.borderRadiusAll, onTap: () { context.push( '/admin/users/${widget.userId}/rooms/${room.id}/messages?room_type=${room.roomType ?? 1}'); }, child: Stack( children: [ Row( mainAxisSize: MainAxisSize.min, children: [ _buildAvatar(room), Expanded( child: Container( padding: const EdgeInsets.symmetric(horizontal: 10), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( room.name, overflow: TextOverflow.ellipsis, ), ), Text( humanTime(room.lastActiveTime), style: TextStyle( color: customColors.weakLinkColor?.withAlpha(65), fontSize: 10, ), ), ], ), ], ), ), ), ], ), if (room.roomType == 4) Positioned( right: 0, top: 0, child: Container( decoration: BoxDecoration( color: customColors.backgroundContainerColor, borderRadius: const BorderRadius.only( topRight: CustomSize.radius, bottomLeft: CustomSize.radius, ), ), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), child: Text( AppLocale.groupChat.getString(context), style: TextStyle( color: customColors.weakTextColor, fontSize: 8, ), ), ), ), ], ), ), ), ); }, ), ); } return const Center( child: CircularProgressIndicator(), ); }, ), ), ), ), ); } Widget _buildAvatar(RoomInServer room) { if (room.members.length == 1 && (room.avatarUrl == null || room.avatarUrl == '')) { room.avatarUrl = room.members[0]; } if (room.avatarUrl != null && room.avatarUrl!.startsWith('http')) { return SizedBox( width: 70, height: 70, child: ClipRRect( borderRadius: const BorderRadius.only(topLeft: CustomSize.radius, bottomLeft: CustomSize.radius), child: CachedNetworkImageEnhanced( imageUrl: imageURL(room.avatarUrl!, qiniuImageTypeAvatar), fit: BoxFit.fill, ), ), ); } if (room.members.isNotEmpty) { return ClipRRect( borderRadius: const BorderRadius.only(topLeft: CustomSize.radius, bottomLeft: CustomSize.radius), child: GroupAvatar( size: 70, avatars: room.members, ), ); } return Initicon( text: room.name.split('、').join(' '), size: 70, backgroundColor: Colors.grey.withAlpha(100), borderRadius: const BorderRadius.only(topLeft: CustomSize.radius, bottomLeft: CustomSize.radius), ); } } ================================================ FILE: lib/page/admin/user.dart ================================================ import 'package:askaide/bloc/user_bloc.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/admin/users.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/credit.dart'; import 'package:askaide/page/component/column_block.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/enhanced_textfield.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/page/creative_island/gallery/gallery_item.dart'; import 'package:askaide/repo/api/quota.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:intl/intl.dart'; class AdminUserPage extends StatefulWidget { final SettingRepository setting; final int userId; const AdminUserPage({ super.key, required this.setting, required this.userId, }); @override State createState() => _AdminUserPageState(); } class _AdminUserPageState extends State { @override void initState() { context.read().add(UserLoadEvent(widget.userId)); context.read().add(UserQuotaLoadEvent(widget.userId)); super.initState(); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( toolbarHeight: CustomSize.toolbarHeight, title: const Text( 'User Info', style: TextStyle(fontSize: CustomSize.appBarTitleSize), ), centerTitle: true, actions: [ IconButton( icon: const Icon(Icons.card_giftcard_outlined), tooltip: 'Give Credits', onPressed: () { int sendCount = 600; String? note; int validDays = 30; openDialog( context, builder: Builder(builder: (context) { return Column( mainAxisSize: MainAxisSize.min, children: [ const Text( 'Give Credits', style: TextStyle(fontSize: 18), ), const SizedBox(height: 10), EnhancedTextField( labelText: 'Quantity', customColors: customColors, textAlignVertical: TextAlignVertical.top, showCounter: false, keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.digitsOnly], suffixIcon: Container( width: 110, alignment: Alignment.center, child: Text( 'Credits', style: TextStyle( color: customColors.weakTextColor, fontSize: 12, ), ), ), onChanged: (value) { sendCount = int.tryParse(value) ?? 0; }, initValue: sendCount.toString(), ), const SizedBox(height: 10), EnhancedTextField( labelText: 'Expiration', customColors: customColors, textAlignVertical: TextAlignVertical.top, showCounter: false, keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.digitsOnly], suffixIcon: Container( width: 110, alignment: Alignment.center, child: Text( 'Days', style: TextStyle( color: customColors.weakTextColor, fontSize: 12, ), ), ), onChanged: (value) { validDays = int.tryParse(value) ?? 0; }, initValue: validDays.toString(), ), const SizedBox(height: 10), EnhancedTextField( labelText: 'Note', customColors: customColors, textAlignVertical: TextAlignVertical.top, showCounter: false, hintText: 'Optional', onChanged: (value) { note = value; }, initValue: note, ), ], ); }), onSubmit: () { if (sendCount <= 0) { showErrorMessage('Quantity must be greater than 0'); return false; } if (validDays <= 0) { showErrorMessage('Expiration date must be greater than 0'); return false; } APIServer() .adminUserQuotaAssign( userId: widget.userId, quota: sendCount, validPeriod: validDays * 24, note: note, ) .then((value) { showSuccessMessage('Gift sent successfully'); context.read().add(UserQuotaLoadEvent(widget.userId)); }).onError( (error, stackTrace) => showErrorMessageEnhanced(context, error!), ); return true; }, ); }, ), ], ), backgroundColor: customColors.backgroundColor, body: BackgroundContainer( setting: widget.setting, backgroundColor: customColors.backgroundColor, enabled: false, child: RefreshIndicator( color: customColors.linkColor, onRefresh: () async { context.read().add(UserLoadEvent(widget.userId)); context.read().add(UserQuotaLoadEvent(widget.userId)); }, displacement: 20, child: SafeArea( top: false, child: SingleChildScrollView( child: Column( children: [ BlocConsumer( listenWhen: (previous, current) => current is UserOperationResult, listener: (context, state) { if (state is UserOperationResult) { if (state.success) { showSuccessMessage(state.message ?? AppLocale.operateSuccess.getString(context)); context.read().add(UserListLoadEvent()); } else { showErrorMessage(state.message ?? AppLocale.operateFailed.getString(context)); } } }, buildWhen: (previous, current) => current is UserLoaded, builder: (context, state) { if (state is UserLoaded) { return ColumnBlock( innerPanding: 10, padding: const EdgeInsets.all(15), margin: const EdgeInsets.all(15), children: [ Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: double.infinity, child: Row( crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ Text( 'ID', style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: customColors.weakTextColor, ), ), const SizedBox(width: 10), Text( '${state.user.id}', style: TextStyle( fontSize: 12, color: customColors.weakTextColor, ), maxLines: 5, overflow: TextOverflow.ellipsis, ), ], ), ), const SizedBox(height: 10), buildTags(context, customColors, state.user), ], ), ), buildUserAvatar(state.user, radius: CustomSize.borderRadiusAll), ], ), TextItem( title: 'Type', value: state.user.userType ?? '-', ), if (state.user.phone != null && state.user.phone!.isNotEmpty) TextItem( title: 'Photo', value: state.user.phone!, ), if (state.user.email != null && state.user.email!.isNotEmpty) TextItem( title: 'Email', value: state.user.email!, ), if (state.user.realname != null && state.user.realname!.isNotEmpty) TextItem( title: 'Nickname', value: state.user.realname!, ), if (state.user.invitedBy != null && state.user.invitedBy! > 0) TextItem( title: 'Inviter ID', value: '${state.user.invitedBy}', ), if (state.user.createdAt != null) TextItem( title: 'Creation time', value: state.user.createdAt!.toLocal().toString(), ), TextItem( title: 'Status', value: state.user.status ?? '-', ), ], ); } return Center( child: CircularProgressIndicator( color: customColors.linkColor, ), ); }, ), BlocBuilder( buildWhen: (previous, current) => current is UserQuotaLoaded, builder: (context, state) { if (state is UserQuotaLoaded) { return ColumnBlock( innerPanding: 10, padding: const EdgeInsets.all(15), margin: const EdgeInsets.only( left: 15, right: 15, bottom: 15, ), children: [ TextItem( title: 'Remaining credits', value: state.quota.total.toString(), ), buildPaymentDetails(customColors, state) ], ); } return Center( child: CircularProgressIndicator( color: customColors.linkColor, ), ); }, ), ], ), ), ), ), ), ), ); } // 购买历史记录 Widget buildPaymentDetails( CustomColors customColors, UserQuotaLoaded state, ) { return SizedBox( width: double.infinity, child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( 'Recharge History', style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: customColors.weakTextColor, ), ), const SizedBox(height: 10), if (state.quota.details.isEmpty) const Text('No recharge record') else ListView( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), children: [ for (var item in state.quota.details) Stack( children: [ Container( margin: const EdgeInsets.symmetric(vertical: 6), padding: const EdgeInsets.only( top: 20, bottom: 10, left: 16, right: 16, ), decoration: BoxDecoration( color: customColors.paymentItemBackgroundColor, borderRadius: CustomSize.borderRadius, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( (item.note == null || item.note == '') ? 'Buy' : item.note!, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 5), Text( DateFormat( 'yyyy/MM/dd HH:mm', ).format(item.createdAt.toLocal()), textScaler: const TextScaler.linear(0.8), style: TextStyle( color: Colors.grey[600], ), ), ], ), ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Credit( count: item.quota, color: Colors.amber, withAddPrefix: true, fontWeight: FontWeight.w500, ), Text( '${DateFormat('yyyy/MM/dd').format(item.periodEndAt.toLocal())} expired', textScaler: const TextScaler.linear(0.7), ), ], ), ], ), ], ), ), _buildTagForItem(item), ], ) ], ), ], ), ); } Widget _buildTagForItem(QuotaDetail item) { if (item.rest <= 0) { return _buildTag(AppLocale.usedUp.getString(context), Colors.orange); } if (item.expired) { return _buildTag(AppLocale.expired.getString(context), Colors.grey[600]!); } return const SizedBox(); } Widget _buildTag(String text, Color color) { return Positioned( right: 1, top: 7, child: Container( decoration: BoxDecoration( color: color, borderRadius: const BorderRadius.only(topRight: CustomSize.radius, bottomLeft: CustomSize.radius), ), padding: const EdgeInsets.symmetric( horizontal: 5, vertical: 2, ), child: Text( text, textScaler: const TextScaler.linear(0.6), style: const TextStyle(color: Colors.white70), ), ), ); } } ================================================ FILE: lib/page/admin/users.dart ================================================ import 'package:askaide/bloc/user_bloc.dart'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/helper.dart'; import 'package:askaide/helper/image.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/pagination.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/api/admin/users.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_initicon/flutter_initicon.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:go_router/go_router.dart'; class AdminUsersPage extends StatefulWidget { final SettingRepository setting; const AdminUsersPage({ super.key, required this.setting, }); @override State createState() => _AdminUsersPageState(); } class _AdminUsersPageState extends State { /// 当前页码 int page = 1; /// 每页数量 int perPage = 20; /// 搜索关键字 final TextEditingController keywordController = TextEditingController(); @override void initState() { context.read().add(UserListLoadEvent( perPage: perPage, page: page, keyword: keywordController.text, )); super.initState(); } @override void dispose() { keywordController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( toolbarHeight: CustomSize.toolbarHeight, title: const Text( 'User Management', style: TextStyle(fontSize: CustomSize.appBarTitleSize), ), centerTitle: true, ), backgroundColor: customColors.backgroundColor, body: BackgroundContainer( setting: widget.setting, enabled: false, backgroundColor: customColors.backgroundColor, child: Column( children: [ Container( padding: const EdgeInsets.only(left: 10, right: 10, bottom: 5), child: TextField( controller: keywordController, textAlignVertical: TextAlignVertical.center, style: TextStyle(color: customColors.dialogDefaultTextColor), decoration: InputDecoration( hintText: AppLocale.search.getString(context), hintStyle: TextStyle( color: customColors.dialogDefaultTextColor, ), prefixIcon: Icon( Icons.search, color: customColors.dialogDefaultTextColor, ), isDense: true, border: InputBorder.none, ), onEditingComplete: () { context.read().add(UserListLoadEvent( perPage: perPage, page: page, keyword: keywordController.text, )); }, ), ), Expanded( child: RefreshIndicator( color: customColors.linkColor, onRefresh: () async { context.read().add(UserListLoadEvent( perPage: perPage, page: page, keyword: keywordController.text, )); }, displacement: 20, child: BlocConsumer( listener: (context, state) { if (state is UserOperationResult) { if (state.success) { showSuccessMessage(state.message ?? AppLocale.operateSuccess.getString(context)); context.read().add(UserListLoadEvent()); } else { showErrorMessage(state.message ?? AppLocale.operateFailed.getString(context)); } } if (state is UsersLoaded) { setState(() { page = state.users.page; perPage = state.users.perPage; }); } }, buildWhen: (previous, current) => current is UsersLoaded, builder: (context, state) { if (state is UsersLoaded) { return SafeArea( top: false, child: Column( children: [ Expanded( child: ListView.builder( padding: const EdgeInsets.all(5), itemCount: state.users.data.length, itemBuilder: (context, index) { return buildUserInfo( context, customColors, state.users.data[index], ); }, ), ), if (state.users.lastPage != null && state.users.lastPage! > 1) Container( padding: const EdgeInsets.all(10), child: Pagination( numOfPages: state.users.lastPage ?? 1, selectedPage: page, pagesVisible: 5, onPageChanged: (selected) { context.read().add(UserListLoadEvent( perPage: perPage, page: selected, keyword: keywordController.text, )); }, ), ), ], ), ); } return const Center( child: CircularProgressIndicator(), ); }, ), ), ), ], ), ), ), ); } Widget buildUserInfo( BuildContext context, CustomColors customColors, AdminUser user, ) { return Container( margin: const EdgeInsets.symmetric( horizontal: 10, vertical: 5, ), decoration: BoxDecoration(borderRadius: CustomSize.borderRadius), child: Slidable( endActionPane: ActionPane( motion: const ScrollMotion(), children: [ const SizedBox(width: 10), SlidableAction( label: AppLocale.character.getString(context), borderRadius: const BorderRadius.only( topLeft: CustomSize.radius, bottomLeft: CustomSize.radius, topRight: CustomSize.radius, bottomRight: CustomSize.radius, ), backgroundColor: customColors.linkColor ?? Colors.green, icon: Icons.people, foregroundColor: Colors.white, onPressed: (_) { context.push('/admin/users/${user.id}/rooms'); }, ), ], ), child: Material( borderRadius: CustomSize.borderRadius, color: customColors.columnBlockBackgroundColor, child: InkWell( borderRadius: CustomSize.borderRadiusAll, onTap: () { context.push('/admin/users/${user.id}'); }, child: Stack( children: [ Row( mainAxisSize: MainAxisSize.min, children: [ // 头像 Stack( children: [ buildUserAvatar(user), Positioned( bottom: 0, width: 70, child: ClipRRect( borderRadius: const BorderRadius.only(bottomLeft: CustomSize.radius), child: Container( color: Colors.black.withAlpha(100), padding: const EdgeInsets.symmetric(vertical: 2), child: Center( child: Text( '#${user.id}', style: const TextStyle( fontSize: 10, overflow: TextOverflow.ellipsis, color: Colors.white, ), ), ), ), ), ), ], ), // 名称 Expanded( child: Container( padding: const EdgeInsets.all(15), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( user.displayName, style: const TextStyle( overflow: TextOverflow.ellipsis, ), ), const SizedBox(height: 5), buildTags(context, customColors, user), ], ), ), ), ], ), Positioned( right: 0, top: 0, child: Container( padding: const EdgeInsets.all(10), width: MediaQuery.of(context).size.width / 2.0, alignment: Alignment.centerRight, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${user.userType}', style: TextStyle( fontSize: 10, overflow: TextOverflow.ellipsis, color: customColors.weakTextColor, ), ), ], ), ), ), Positioned( right: 0, bottom: 0, child: Container( padding: const EdgeInsets.all(10), width: MediaQuery.of(context).size.width / 2.0, alignment: Alignment.centerRight, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( user.createdAt != null ? humanTime(user.createdAt) : '', style: TextStyle( fontSize: 10, overflow: TextOverflow.ellipsis, color: customColors.weakTextColor, ), ), ], ), ), ), ], ), ), ), ), ); } } Widget buildUserAvatar( AdminUser user, { BorderRadius radius = const BorderRadius.only(topLeft: CustomSize.radius, bottomLeft: CustomSize.radius), }) { if (user.avatar != null && user.avatar!.startsWith('http')) { return SizedBox( width: 70, height: 70, child: ClipRRect( borderRadius: radius, child: CachedNetworkImage( imageUrl: imageURL(user.avatar!, qiniuImageTypeAvatar), fit: BoxFit.fill, ), ), ); } return Initicon( text: user.displayName.split('、').join(' '), size: 70, backgroundColor: Colors.grey.withAlpha(100), borderRadius: radius, ); } Widget buildTags(BuildContext context, CustomColors customColors, AdminUser user) { final tags = []; if (user.email != null && user.email!.isNotEmpty) { tags.add(buildTag(context, customColors, 'Email')); } if (user.phone != null && user.phone!.isNotEmpty) { tags.add(buildTag(context, customColors, 'Phone')); } if (user.unionId != null && user.unionId!.isNotEmpty) { tags.add(buildTag(context, customColors, 'WeChat')); } if (user.appleUid != null && user.appleUid!.isNotEmpty) { tags.add(buildTag(context, customColors, 'Apple')); } return Wrap( spacing: 5, runSpacing: 5, children: tags, ); } Widget buildTag(BuildContext context, CustomColors customColors, String s) { return Container( padding: const EdgeInsets.symmetric( horizontal: 5, vertical: 2, ), decoration: BoxDecoration( color: customColors.tagsBackground, borderRadius: CustomSize.borderRadius, ), child: Text( s, style: TextStyle( fontSize: 10, color: customColors.tagsText, ), ), ); } ================================================ FILE: lib/page/app_scaffold.dart ================================================ import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/event.dart'; import 'package:askaide/helper/haptic_feedback.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; class AppScaffold extends StatefulWidget { final SettingRepository settingRepo; final StatefulNavigationShell navigationShell; const AppScaffold({ Key? key, required this.settingRepo, required this.navigationShell, }) : super(key: key); @override State createState() => _AppScaffoldState(); } class _AppScaffoldState extends State { var _showBottomNavigatorBar = true; Function? cancelHideBottomNavigatorBarEventListener; Function? cancelShowBottomNavigatorBarEventListener; @override void dispose() { cancelHideBottomNavigatorBarEventListener?.call(); cancelShowBottomNavigatorBarEventListener?.call(); super.dispose(); } @override void initState() { cancelHideBottomNavigatorBarEventListener = GlobalEvent().on("hideBottomNavigatorBar", (data) { if (mounted) { setState(() { _showBottomNavigatorBar = false; }); } }); cancelShowBottomNavigatorBarEventListener = GlobalEvent().on("showBottomNavigatorBar", (data) { if (mounted) { setState(() { _showBottomNavigatorBar = true; }); } }); super.initState(); } List _bottomNavigationBarList( {int? currentIndex}) { return [ if (Ability().enableChat) BottomNavigationBarConfig( builder: (index, customColors) => createAnimatedNavBarItem( icon: Icons.question_answer_outlined, activatedIcon: Icons.question_answer, activatedColor: customColors.linkColor, label: AppLocale.chatAnywhere.getString(context), activated: currentIndex == index, ), route: '/chat-chat', ), if (Ability().enableDigitalHuman) BottomNavigationBarConfig( builder: (index, customColors) => createAnimatedNavBarItem( icon: Icons.group_outlined, activatedIcon: Icons.group, activatedColor: customColors.linkColor, label: AppLocale.homeTitle.getString(context), activated: currentIndex == index, ), route: '/', ), if (Ability().enableGallery) BottomNavigationBarConfig( builder: (index, customColors) => createAnimatedNavBarItem( icon: Icons.auto_awesome_outlined, activatedIcon: Icons.auto_awesome, activatedColor: customColors.linkColor, label: AppLocale.discover.getString(context), activated: currentIndex == index, ), route: '/creative-gallery', ), if (Ability().enableCreationIsland) BottomNavigationBarConfig( builder: (index, customColors) => createAnimatedNavBarItem( icon: Icons.palette_outlined, activatedIcon: Icons.palette, activatedColor: customColors.linkColor, label: AppLocale.creativeIsland.getString(context), activated: currentIndex == index, ), route: '/creative-draw', ), BottomNavigationBarConfig( builder: (index, customColors) => createAnimatedNavBarItem( icon: Icons.manage_accounts_outlined, activatedIcon: Icons.manage_accounts, activatedColor: customColors.linkColor, label: AppLocale.me.getString(context), activated: currentIndex == index, ), route: '/setting', ), ]; } @override Widget build(BuildContext context) { final currentIndex = _calculateSelectedIndex(context); final customColors = Theme.of(context).extension()!; final barItems = _bottomNavigationBarList(currentIndex: currentIndex); return Scaffold( backgroundColor: customColors.backgroundContainerColor, body: BackgroundContainer( setting: widget.settingRepo, enabled: true, child: widget.navigationShell, ), extendBody: false, bottomNavigationBar: currentIndex > -1 && _showBottomNavigatorBar ? BottomNavigationBar( landscapeLayout: BottomNavigationBarLandscapeLayout.centered, showSelectedLabels: true, showUnselectedLabels: true, currentIndex: _calculateSelectedIndex(context), onTap: (int index) => _onTap(context, index), selectedItemColor: customColors.linkColor, unselectedItemColor: Colors.grey, selectedFontSize: 10, unselectedFontSize: 10, type: BottomNavigationBarType.fixed, enableFeedback: true, backgroundColor: customColors.backgroundColor, elevation: 0, items: [ for (var i = 0; i < barItems.length; i++) barItems[i].builder(i, customColors), ], ) : null, ); } int _calculateSelectedIndex(BuildContext context) { final GoRouter route = GoRouter.of(context); final String location = route.location.split('?').first; final barItems = _bottomNavigationBarList(); for (var i = 0; i < barItems.length; i++) { if (barItems[i].route == location) return i; } return -1; } void _onTap(BuildContext context, int index) { HapticFeedbackHelper.lightImpact(); widget.navigationShell.goBranch( index, initialLocation: index == widget.navigationShell.currentIndex, ); } } BottomNavigationBarItem createAnimatedNavBarItem({ String? label, bool activated = false, Color? activatedColor, required IconData icon, required IconData activatedIcon, }) { return BottomNavigationBarItem( label: label, icon: AnimatedCrossFade( firstChild: Icon(icon), secondChild: Icon(activatedIcon, color: activatedColor ?? Colors.green), crossFadeState: activated ? CrossFadeState.showSecond : CrossFadeState.showFirst, duration: const Duration(milliseconds: 300), ), ); } class BottomNavigationBarConfig { final BottomNavigationBarItem Function(int index, CustomColors customColors) builder; final String route; BottomNavigationBarConfig({ required this.builder, required this.route, }); } ================================================ FILE: lib/page/auth/signin_or_signup.dart ================================================ import 'dart:convert'; import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/helper.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/password_field.dart'; import 'package:askaide/page/component/verify_code_input.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; class SigninOrSignupScreen extends StatefulWidget { final SettingRepository settings; final String username; final String signInMethod; final bool isSignup; final String? wechatBindToken; const SigninOrSignupScreen({ super.key, required this.settings, required this.username, required this.isSignup, required this.signInMethod, this.wechatBindToken, }); @override State createState() => _SigninOrSignupScreenState(); } class _SigninOrSignupScreenState extends State { final TextEditingController _inviteCodeController = TextEditingController(); final TextEditingController _verificationCodeController = TextEditingController(); final TextEditingController _passwordController = TextEditingController(); String verifyCodeId = ''; final phoneNumberValidator = RegExp(r"^1[3456789]\d{9}$"); late String signInMethod; @override void initState() { signInMethod = widget.signInMethod; super.initState(); } @override void dispose() { _inviteCodeController.dispose(); _verificationCodeController.dispose(); _passwordController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { var customColors = Theme.of(context).extension()!; return WindowFrameWidget( child: Scaffold( appBar: AppBar( toolbarHeight: CustomSize.toolbarHeight, backgroundColor: Colors.transparent, title: Text( AppLocale.verifyAccount.getString(context), style: const TextStyle(fontSize: CustomSize.appBarTitleSize), ), centerTitle: true, leading: IconButton( icon: const Icon(Icons.arrow_back_ios), onPressed: () { context.pop(); }, ), ), backgroundColor: customColors.backgroundContainerColor, body: BackgroundContainer( setting: widget.settings, enabled: false, child: Align( alignment: Alignment.topCenter, child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 400), child: SingleChildScrollView( child: widget.isSignup || signInMethod == 'sms_code' || signInMethod == 'email_code' ? signInOrSignUpWithSMSOrEmailCode(customColors, context) : signInWithPassword(customColors, context)), ), ), ), ), ); } Widget signInWithPassword( CustomColors customColors, BuildContext context, ) { return Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ const SizedBox(height: 10), Padding( padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 20), child: Text( AppLocale.enterPasswordToSignin.getString(context), style: TextStyle( color: customColors.weakTextColor?.withAlpha(200), fontSize: 15, ), ), ), const SizedBox(height: 10), // 密码 Padding( padding: const EdgeInsets.only(left: 15.0, right: 15.0, top: 15, bottom: 0), child: PasswordField( controller: _passwordController, labelText: AppLocale.password.getString(context), hintText: AppLocale.passwordInputTips.getString(context), ), ), const SizedBox(height: 15), // 登录 Container( height: 45, width: double.infinity, margin: const EdgeInsets.symmetric(horizontal: 15), decoration: BoxDecoration(color: customColors.linkColor, borderRadius: CustomSize.borderRadius), child: TextButton( onPressed: () { FocusScope.of(context).requestFocus(FocusNode()); final password = _passwordController.text.trim(); if (password == '') { showErrorMessage(AppLocale.passwordRequired.getString(context)); return; } if (password.length < 8 || password.length > 20) { showErrorMessage(AppLocale.passwordFormatError.getString(context)); return; } APIServer() .signInWithPassword( widget.username, password, wechatBindToken: widget.wechatBindToken, ) .then((value) async { await widget.settings.set(settingAPIServerToken, value.token); await widget.settings.set(settingUserInfo, jsonEncode(value)); if (context.mounted) { context.go( '${Ability().homeRoute}?show_initial_dialog=${value.isNewUser ? "true" : "false"}&reward=${value.reward}'); } }).catchError((e) { showErrorMessage(resolveError(context, e)); }); }, child: Text( AppLocale.signIn.getString(context), style: const TextStyle(color: Colors.white, fontSize: 18), ), ), ), // 找回密码 Container( padding: const EdgeInsets.only(left: 15, right: 10), width: double.infinity, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ TextButton( onPressed: () { setState(() { signInMethod = phoneNumberValidator.hasMatch(widget.username) ? 'sms_code' : 'email_code'; }); }, child: Text( AppLocale.verifyCodeLogin.getString(context), style: TextStyle( color: customColors.weakLinkColor?.withAlpha(120), fontSize: 14, ), ), ), TextButton( onPressed: () { context.push('/retrieve-password?username=${widget.username}'); }, child: Text( AppLocale.forgotPassword.getString(context), style: TextStyle( color: customColors.weakLinkColor?.withAlpha(120), fontSize: 14, ), ), ), ], ), ), ], ); } Widget signInOrSignUpWithSMSOrEmailCode( CustomColors customColors, BuildContext context, ) { return Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ const SizedBox(height: 10), Padding( padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 20), child: Text( AppLocale.verifyCodeLoginTips.getString(context), style: TextStyle( color: customColors.weakTextColor, fontSize: 15, ), ), ), const SizedBox(height: 10), // 验证码 Padding( padding: const EdgeInsets.only(left: 15.0, right: 5.0, top: 15, bottom: 0), child: VerifyCodeInput( controller: _verificationCodeController, onVerifyCodeSent: (id) { verifyCodeId = id; }, sendVerifyCode: () { return APIServer().sendSigninOrSignupVerifyCode( widget.username, verifyType: phoneNumberValidator.hasMatch(widget.username) ? 'sms' : 'email', isSignup: widget.isSignup, ); }, sendCheck: () { return true; }, ), ), // 邀请码 if (widget.isSignup) Padding( padding: const EdgeInsets.only(left: 15.0, right: 15.0, top: 15, bottom: 0), child: TextFormField( controller: _inviteCodeController, inputFormatters: [FilteringTextInputFormatter.singleLineFormatter], decoration: InputDecoration( border: const OutlineInputBorder(), enabledBorder: const OutlineInputBorder( borderSide: BorderSide(color: Color.fromARGB(200, 192, 192, 192)), ), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: customColors.linkColor ?? Colors.green), ), isDense: true, floatingLabelBehavior: FloatingLabelBehavior.always, labelText: AppLocale.inviteCode.getString(context), labelStyle: const TextStyle(fontSize: 17), hintText: AppLocale.inviteCodeInputTips.getString(context), hintStyle: TextStyle( color: customColors.textfieldHintColor, fontSize: 15, ), ), ), ), const SizedBox(height: 15), // 创建账号或登录 Container( height: 45, width: double.infinity, margin: const EdgeInsets.symmetric(horizontal: 15), decoration: BoxDecoration(color: customColors.linkColor, borderRadius: CustomSize.borderRadius), child: TextButton( onPressed: onCreateSubmit, child: Text( widget.isSignup ? AppLocale.createAccount.getString(context) : AppLocale.signIn.getString(context), style: const TextStyle(color: Colors.white, fontSize: 18), ), ), ), if (!widget.isSignup) Container( padding: const EdgeInsets.only(left: 10, right: 10), width: double.infinity, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ TextButton( onPressed: () { setState(() { signInMethod = 'password'; }); }, child: Text( AppLocale.usePasswordToSignin.getString(context), style: TextStyle( color: customColors.weakLinkColor?.withAlpha(120), fontSize: 14, ), ), ), ], ), ), ], ); } onCreateSubmit() { FocusScope.of(context).requestFocus(FocusNode()); if (verifyCodeId == '') { showErrorMessage(AppLocale.pleaseGetVerifyCodeFirst.getString(context)); return; } final verificationCode = _verificationCodeController.text.trim(); if (verificationCode == '') { showErrorMessage(AppLocale.verifyCodeRequired.getString(context)); return; } if (verificationCode.length != 6) { showErrorMessage(AppLocale.verifyCodeFormatError.getString(context)); return; } final inviteCode = _inviteCodeController.text.trim(); if (inviteCode != '' && inviteCode.length > 20) { showErrorMessage(AppLocale.inviteCodeFormatError.getString(context)); return; } final cancel = BotToast.showCustomLoading( toastBuilder: (cancel) { return LoadingIndicator( message: AppLocale.processingWait.getString(context), ); }, allowClick: false, duration: const Duration(seconds: 120), ); APIServer() .signInOrUp( username: widget.username, inviteCode: inviteCode, verifyCodeId: verifyCodeId, verifyCode: verificationCode, wechatBindToken: widget.wechatBindToken, ) .then((value) async { await widget.settings.set(settingAPIServerToken, value.token); await widget.settings.set(settingUserInfo, jsonEncode(value)); if (value.needBindPhone) { if (context.mounted) { // ignore: use_build_context_synchronously context.push('/bind-phone').then((value) async { if (value == 'logout') { await widget.settings.set(settingAPIServerToken, ''); await widget.settings.set(settingUserInfo, ''); } }); } return; } else { if (context.mounted) { // ignore: use_build_context_synchronously context.go( '${Ability().homeRoute}?show_initial_dialog=${value.isNewUser ? "true" : "false"}&reward=${value.reward}'); } } }).catchError((e) { showErrorMessage(resolveError(context, e)); }).whenComplete(() => cancel()); } } ================================================ FILE: lib/page/auth/signin_screen.dart ================================================ import 'dart:async'; import 'dart:convert'; import 'package:animated_text_kit/animated_text_kit.dart'; import 'package:askaide/bloc/version_bloc.dart'; import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/env.dart'; import 'package:askaide/helper/helper.dart'; import 'package:askaide/helper/logger.dart'; import 'package:askaide/helper/platform.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:fluwx/fluwx.dart'; import 'package:go_router/go_router.dart'; import 'package:quickalert/models/quickalert_type.dart'; import 'package:sign_in_button/sign_in_button.dart'; import 'package:sign_in_with_apple/sign_in_with_apple.dart'; import 'package:askaide/helper/http.dart'; import 'package:url_launcher/url_launcher.dart'; class SignInScreen extends StatefulWidget { final SettingRepository settings; final String? username; const SignInScreen({super.key, required this.settings, this.username}); @override State createState() => _SignInScreenState(); } class _SignInScreenState extends State { final TextEditingController _usernameController = TextEditingController(); final phoneNumberValidator = RegExp(r"^1[3456789]\d{9}$"); final emailValidator = RegExp(r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+"); var agreeProtocol = false; StreamSubscription? _weChatResponse; /// 微信登录 token,用于自动绑定微信 String? wechatBindToken; @override void initState() { super.initState(); if (widget.username != null) { _usernameController.text = widget.username!; } if (Ability().enableWechatSignin) { _weChatResponse = weChatResponseEventHandler.distinct((a, b) => a == b).listen((event) { if (event is WeChatAuthResponse) { if (event.errCode != 0) { showErrorMessage(event.errStr!); return; } if (event.code == null) { showErrorMessage(AppLocale.signInFailed.getString(context)); return; } processing = true; APIServer().trySignInWithWechat(code: event.code!).then((tryRes) async { if (tryRes.exist) { await confirmWeChatSignin(tryRes.token); } else { await showBeautyDialog( context, type: QuickAlertType.confirm, title: AppLocale.tips.getString(context), text: AppLocale.wechatBindConfirm.getString(context), confirmBtnText: AppLocale.directSignin.getString(context), onConfirmBtnTap: () async { await confirmWeChatSignin(tryRes.token); // ignore: use_build_context_synchronously context.pop(); }, showCancelBtn: true, cancelBtnText: AppLocale.bindExAccount.getString(context), onCancelBtnTap: () { setState(() { wechatBindToken = tryRes.token; }); context.pop(); }, ); } }).whenComplete(() => processing = false); } }); } context.read().add(VersionCheckEvent()); } confirmWeChatSignin(String token) async { try { final value = await APIServer().signInWithWechat(token: token); await widget.settings.set(settingAPIServerToken, value.token); await widget.settings.set(settingUserInfo, jsonEncode(value)); await HttpClient.cleanCache(); if (value.needBindPhone) { if (context.mounted) { // ignore: use_build_context_synchronously context.push('/bind-phone').then((value) async { if (value == 'logout') { await widget.settings.set(settingAPIServerToken, ''); await widget.settings.set(settingUserInfo, ''); } }); } } else { // ignore: use_build_context_synchronously context.go( '${Ability().homeRoute}?show_initial_dialog=${value.isNewUser ? "true" : "false"}&reward=${value.reward}'); } } catch (e) { Logger.instance.e(e); // ignore: use_build_context_synchronously showErrorMessage(AppLocale.signInFailed.getString(context)); } } @override void dispose() { _usernameController.dispose(); _weChatResponse?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { var customColors = Theme.of(context).extension()!; return WindowFrameWidget( child: Scaffold( appBar: AppBar( toolbarHeight: CustomSize.toolbarHeight, leading: IconButton( icon: Icon( Icons.close, color: customColors.weakLinkColor, ), onPressed: () { if (context.canPop()) { context.pop(); } else { context.go(Ability().homeRoute); } }, ), ), backgroundColor: customColors.backgroundContainerColor, body: BackgroundContainer( setting: widget.settings, enabled: false, child: Align( alignment: Alignment.topCenter, child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 400), child: SingleChildScrollView( child: Column( children: [ Center( child: SizedBox( width: 200, height: 200, child: Image.asset('assets/app.png'), ), ), const SizedBox(height: 10), AnimatedTextKit( animatedTexts: [ ColorizeAnimatedText( 'AIdea', textStyle: const TextStyle(fontSize: 30.0), colors: [ Colors.purple, Colors.blue, Colors.yellow, Colors.red, ], ), ], ), const SizedBox(height: 30), Padding( padding: const EdgeInsets.only(left: 15.0, right: 15.0, top: 15, bottom: 0), child: TextFormField( controller: _usernameController, inputFormatters: [FilteringTextInputFormatter.singleLineFormatter], decoration: InputDecoration( border: const OutlineInputBorder(), enabledBorder: const OutlineInputBorder( borderSide: BorderSide(color: Color.fromARGB(200, 192, 192, 192)), ), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: customColors.linkColor ?? Colors.green), ), isDense: true, floatingLabelBehavior: FloatingLabelBehavior.always, labelText: AppLocale.account.getString(context), labelStyle: const TextStyle(fontSize: 17), hintText: AppLocale.accountInputTips.getString(context), hintStyle: TextStyle( color: customColors.textfieldHintColor, fontSize: 15, ), ), ), ), const SizedBox(height: 8), Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 15), child: Text( AppLocale.accountWillBeCreateAutomatically.getString(context), style: TextStyle( color: customColors.weakTextColor?.withAlpha(80), fontSize: 14, ), ), ), const SizedBox(height: 25), // 登录按钮 Container( height: 45, width: double.infinity, margin: const EdgeInsets.symmetric(horizontal: 15), decoration: BoxDecoration(color: customColors.linkColor, borderRadius: CustomSize.borderRadius), child: TextButton( onPressed: onSigninSubmit, child: Text( AppLocale.verify.getString(context), style: const TextStyle(color: Colors.white, fontSize: 18), ), ), ), // Padding( // padding: const EdgeInsets.symmetric(horizontal: 15), // child: Column( // children: [ // Row( // mainAxisAlignment: MainAxisAlignment.spaceBetween, // children: [ // // 找回密码 // TextButton( // onPressed: () { // context.push( // '/retrieve-password?username=${_usernameController.text.trim()}'); // }, // child: Text( // AppLocale.forgotPassword.getString(context), // style: TextStyle( // color: customColors.weakLinkColor, // fontSize: 14, // ), // ), // ), // // 创建账号 // TextButton( // onPressed: () { // context // .push( // '/signup?username=${_usernameController.text.trim()}') // .then((value) { // if (value != null) { // _usernameController.text = value as String; // } // }); // }, // child: Text( // AppLocale.createAccount.getString(context), // style: TextStyle( // color: customColors.linkColor, // fontSize: 14, // ), // )), // ], // ), // ], // ), // ), _buildUserTermsAndPrivicy(customColors, context), const SizedBox(height: 50), // 三方登录 BlocBuilder( builder: (context, state) { return _buildThirdPartySignInButtons(context, customColors); }, ), const SizedBox(height: 10), ], ), ), ), ), ), ), ); } Row _buildUserTermsAndPrivicy(CustomColors customColors, BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Transform.scale( scale: 0.75, child: Theme( data: ThemeData( unselectedWidgetColor: customColors.weakTextColor?.withAlpha(180), ), child: Checkbox( activeColor: customColors.linkColor, value: agreeProtocol, onChanged: (agree) { setState(() { agreeProtocol = !agreeProtocol; }); }, ), ), ), RichText( text: TextSpan( children: [ TextSpan( text: AppLocale.readAndAgree.getString(context), style: TextStyle( color: customColors.weakTextColor?.withAlpha(80), fontSize: 12, ), recognizer: TapGestureRecognizer() ..onTap = () { setState(() { agreeProtocol = !agreeProtocol; }); }, ), TextSpan( text: '《${AppLocale.userTerms.getString(context)}》', style: TextStyle( color: customColors.linkColor?.withAlpha(150), fontSize: 12, ), recognizer: TapGestureRecognizer() ..onTap = () { launchUrl(Uri.parse('$apiServerURL/public/info/terms-of-user')); }, ), TextSpan( text: AppLocale.andWord.getString(context), style: TextStyle( color: customColors.weakTextColor?.withAlpha(80), fontSize: 12, ), ), TextSpan( text: '《${AppLocale.privacyPolicy.getString(context)}》', style: TextStyle( color: customColors.linkColor?.withAlpha(150), fontSize: 12, ), recognizer: TapGestureRecognizer() ..onTap = () { launchUrl(Uri.parse('$apiServerURL/public/info/privacy-policy')); }, ), ], ), ) ], ); } Widget _buildThirdPartySignInButtons(BuildContext context, CustomColors customColors) { return FutureBuilder( future: isWeChatInstalled, builder: (context, installed) { final signInItems = []; if (Ability().enableAppleSignin) { signInItems.add(SignInButton( Buttons.appleDark, mini: true, shape: const CircleBorder(), onPressed: onAppleSigninSubmit, )); } // 微信登录功能 if (Ability().enableWechatSignin) { if (PlatformTool.isAndroid() || installed.data == true) { signInItems.add(SignInButtonBuilder( mini: true, shape: const CircleBorder(), onPressed: () async { if (processing) { return; } if (!agreeProtocol) { showErrorMessage(AppLocale.pleaseReadAgreeProtocol.getString(context)); return; } final ok = await sendWeChatAuth(scope: "snsapi_userinfo", state: "wechat_sdk_demo_test"); if (!ok) { showErrorMessage(AppLocale.installWechatFirst.getString(context)); } }, backgroundColor: Colors.green, text: AppLocale.wechat.getString(context), icon: Icons.wechat, )); } } if (signInItems.isEmpty) { return Container(); } return Column( children: [ Text( AppLocale.otherLoginMethods.getString(context), style: TextStyle( fontSize: 13, color: customColors.weakTextColor?.withAlpha(80), ), ), const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: signInItems.map((e) => Padding(padding: const EdgeInsets.all(10), child: e)).toList(), ), ], ); }, ); } bool processing = false; onAppleSigninSubmit() async { if (processing) { return; } if (!agreeProtocol) { showErrorMessage(AppLocale.pleaseReadAgreeProtocol.getString(context)); return; } processing = true; try { final credential = await SignInWithApple.getAppleIDCredential( webAuthenticationOptions: WebAuthenticationOptions( clientId: 'cc.aicode.askaide', redirectUri: Uri.parse('https://ai-api.aicode.cc/v1/callback/auth/sign_in_with_apple'), ), scopes: [ AppleIDAuthorizationScopes.email, AppleIDAuthorizationScopes.fullName, ], ); APIServer() .signInWithApple( userIdentifier: credential.userIdentifier ?? '', authorizationCode: credential.authorizationCode, identityToken: credential.identityToken, familyName: credential.familyName, givenName: credential.givenName, email: credential.email, wechatBindToken: wechatBindToken, ) .then((value) async { await widget.settings.set(settingAPIServerToken, value.token); await widget.settings.set(settingUserInfo, jsonEncode(value)); () { if (value.needBindPhone) { if (context.mounted) { context.push('/bind-phone').then((value) async { if (value == 'logout') { await widget.settings.set(settingAPIServerToken, ''); await widget.settings.set(settingUserInfo, ''); } }); } return; } else { context.go( '${Ability().homeRoute}?show_initial_dialog=${value.isNewUser ? "true" : "false"}&reward=${value.reward}'); } }(); // HttpClient.cacheManager.clearAll().then((_) { // if (value.needBindPhone) { // if (context.mounted) { // context.push('/bind-phone').then((value) async { // if (value == 'logout') { // await widget.settings.set(settingAPIServerToken, ''); // await widget.settings.set(settingUserInfo, ''); // } // }); // } // return; // } else { // context.go( // '${Ability().homeRoute}?show_initial_dialog=${value.isNewUser ? "true" : "false"}&reward=${value.reward}'); // } // }); }).catchError((e) { Logger.instance.e(e); showErrorMessage(AppLocale.signInFailed.getString(context)); }).onError((error, stackTrace) { Logger.instance.e(error); showErrorMessage(AppLocale.signInFailed.getString(context)); }); } finally { processing = false; } } onSigninSubmit() { FocusScope.of(context).requestFocus(FocusNode()); if (processing) { return; } final username = _usernameController.text.trim(); if (username == '') { showErrorMessage(AppLocale.accountRequired.getString(context)); return; } if (!phoneNumberValidator.hasMatch(username) && !emailValidator.hasMatch(username)) { showErrorMessage(AppLocale.accountFormatError.getString(context)); return; } if (!agreeProtocol) { showErrorMessage(AppLocale.pleaseReadAgreeProtocol.getString(context)); return; } processing = true; APIServer().checkPhoneExists(username).then((resp) async { context.push( '/signin-or-signup?username=$username&is_signup=${resp.exist ? "false" : "true"}&sign_in_method=${resp.signInMethod}${wechatBindToken != null ? '&wechat_bind_token=$wechatBindToken' : ''}'); }).catchError((e) { showErrorMessage(resolveError(context, e)); }).whenComplete(() => processing = false); } } ================================================ FILE: lib/page/auth/signup_screen.dart ================================================ import 'dart:async'; import 'dart:convert'; import 'package:animated_text_kit/animated_text_kit.dart'; import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/env.dart'; import 'package:askaide/helper/helper.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/password_field.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:auto_size_text/auto_size_text.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; import 'package:url_launcher/url_launcher.dart'; class SignupScreen extends StatefulWidget { final SettingRepository settings; final String? username; const SignupScreen({super.key, required this.settings, this.username}); @override State createState() => _SignupScreenState(); } class _SignupScreenState extends State { final TextEditingController _usernameController = TextEditingController(); final TextEditingController _inviteCodeController = TextEditingController(); final TextEditingController _passwordController = TextEditingController(); final TextEditingController _verificationCodeController = TextEditingController(); String verifyCodeId = ''; final emailValidator = RegExp(r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+"); final phoneNumberValidator = RegExp(r"^1[3456789]\d{9}$"); var agreeProtocol = false; // 下次发送验证码等待时间 int verifyCodeWaitSeconds = 0; Timer? timer; @override void initState() { super.initState(); if (widget.username != null) { _usernameController.text = widget.username!; } // Clipboard.getData(Clipboard.kTextPlain).then((value) { // if (value == null || value.text == null || value.text == '') { // return; // } // if (value.text!.trim().contains(RegExp(r'\$AIDEA\.INV\.\w+\$'))) { // final match = RegExp(r'\$AIDEA\.INV\.(\w+)\$').firstMatch(value.text!); // if (match != null) { // final val = match.group(1); // if (val != null) { // _inviteCodeController.text = val; // } // } // } // }); } @override void dispose() { if (timer != null) { timer!.cancel(); } _usernameController.dispose(); _inviteCodeController.dispose(); _passwordController.dispose(); _verificationCodeController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { var customColors = Theme.of(context).extension()!; return WindowFrameWidget( child: Scaffold( appBar: AppBar( backgroundColor: Colors.transparent, leading: IconButton( icon: const Icon(Icons.arrow_back_ios), onPressed: () { if (context.canPop()) { context.pop(_usernameController.text.trim()); } else { context.go('/login?username=${_usernameController.text.trim()}'); } }, ), ), backgroundColor: Theme.of(context).colorScheme.surface, body: BackgroundContainer( setting: widget.settings, enabled: false, child: Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 400), child: SingleChildScrollView( child: Column( children: [ Center( child: SizedBox( width: 150, height: 150, child: Image.asset('assets/app.png'), ), ), const SizedBox(height: 10), AnimatedTextKit( animatedTexts: [ ColorizeAnimatedText( 'AIdea', textStyle: const TextStyle(fontSize: 20.0), colors: [ Colors.purple, Colors.blue, Colors.yellow, Colors.red, ], ), ], ), const SizedBox(height: 30), // 用户名 Padding( padding: const EdgeInsets.only(left: 15.0, right: 15.0, top: 15, bottom: 0), child: TextFormField( controller: _usernameController, inputFormatters: [FilteringTextInputFormatter.singleLineFormatter], decoration: InputDecoration( border: const OutlineInputBorder(), enabledBorder: const OutlineInputBorder( borderSide: BorderSide(color: Color.fromARGB(255, 192, 192, 192)), ), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: customColors.linkColor!), ), floatingLabelStyle: TextStyle(color: customColors.linkColor!), isDense: true, floatingLabelBehavior: FloatingLabelBehavior.always, labelText: AppLocale.account.getString(context), hintText: AppLocale.accountInputTips.getString(context), hintStyle: TextStyle( color: customColors.textfieldHintColor, fontSize: 15, ), ), ), ), // 密码 Padding( padding: const EdgeInsets.only(left: 15.0, right: 15.0, top: 15, bottom: 0), child: PasswordField( controller: _passwordController, labelText: AppLocale.password.getString(context), hintText: AppLocale.passwordInputTips.getString(context), ), ), // 邀请码 Padding( padding: const EdgeInsets.only(left: 15.0, right: 15.0, top: 15, bottom: 0), child: TextFormField( controller: _inviteCodeController, inputFormatters: [FilteringTextInputFormatter.singleLineFormatter], decoration: InputDecoration( border: const OutlineInputBorder(), enabledBorder: const OutlineInputBorder( borderSide: BorderSide(color: Color.fromARGB(255, 192, 192, 192)), ), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: customColors.linkColor!), ), isDense: true, floatingLabelBehavior: FloatingLabelBehavior.always, labelText: AppLocale.inviteCode.getString(context), hintText: AppLocale.inviteCodeInputTips.getString(context), hintStyle: TextStyle( color: customColors.textfieldHintColor, fontSize: 15, ), ), ), ), // 验证码 Padding( padding: const EdgeInsets.only(left: 15.0, right: 15.0, top: 15, bottom: 0), child: Row(children: [ Expanded( child: TextFormField( controller: _verificationCodeController, inputFormatters: [ FilteringTextInputFormatter.singleLineFormatter, FilteringTextInputFormatter.digitsOnly, ], maxLength: 6, keyboardType: TextInputType.number, decoration: InputDecoration( counterText: '', border: const OutlineInputBorder(), enabledBorder: const OutlineInputBorder( borderSide: BorderSide(color: Color.fromARGB(255, 192, 192, 192)), ), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: customColors.linkColor!), ), floatingLabelStyle: TextStyle(color: customColors.linkColor!), isDense: true, floatingLabelBehavior: FloatingLabelBehavior.always, labelText: AppLocale.verifyCode.getString(context), hintText: AppLocale.verifyCodeInputTips.getString(context), hintStyle: TextStyle( color: customColors.textfieldHintColor, fontSize: 15, ), ), ), ), const SizedBox(width: 20), SizedBox( width: 100, child: verifyCodeWaitSeconds > 0 ? TextButton( onPressed: null, child: AutoSizeText( '$verifyCodeWaitSeconds ${AppLocale.retryInSeconds.getString(context)}', style: TextStyle( color: customColors.weakTextColor, fontSize: 15, ), maxLines: 1, ), ) : TextButton( onPressed: () { final username = _usernameController.text.trim(); final isEmail = emailValidator.hasMatch(username); final isPhoneNumber = phoneNumberValidator.hasMatch(username); if (_usernameController.text.trim() == '') { showErrorMessage(AppLocale.accountRequired.getString(context)); return; } if (!isEmail && !isPhoneNumber) { showErrorMessage(AppLocale.accountFormatError.getString(context)); return; } APIServer() .sendSignupVerifyCode( username, verifyType: isEmail ? 'email' : 'sms', ) .then((id) { verifyCodeId = id; setState(() { verifyCodeWaitSeconds = 60; }); if (timer != null) { timer?.cancel(); timer = null; } timer = Timer.periodic(const Duration(seconds: 1), (timer) { if (verifyCodeWaitSeconds <= 0) { timer.cancel(); return; } setState(() { verifyCodeWaitSeconds--; }); }); showSuccessMessage( '${AppLocale.verifyCodeSendSuccess.getString(context)}${isEmail ? AppLocale.email.getString(context) : AppLocale.phone.getString(context)}'); }).onError((error, stackTrace) { setState(() { verifyCodeWaitSeconds = 0; timer?.cancel(); }); showErrorMessage(resolveError(context, error!)); }); }, child: Text( AppLocale.sendVerifyCode.getString(context), style: TextStyle( color: customColors.linkColor, fontSize: 15, ), ), ), ), ]), ), const SizedBox(height: 15), // 创建账号 Container( height: 50, width: double.infinity, margin: const EdgeInsets.symmetric(horizontal: 15), decoration: BoxDecoration(color: customColors.linkColor, borderRadius: CustomSize.borderRadius), child: TextButton( onPressed: onCreateSubmit, child: Text( AppLocale.createAccount.getString(context), style: const TextStyle(color: Colors.white, fontSize: 20), ), ), ), // 直接登录 Padding( padding: const EdgeInsets.symmetric(horizontal: 15), child: Column(children: [ Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( onPressed: () { if (context.canPop()) { context.pop(_usernameController.text.trim()); } else { context.go('/login?username=${_usernameController.text.trim()}'); } }, child: Text( AppLocale.directSigninDueHasAccount.getString(context), style: TextStyle( color: customColors.linkColor, fontSize: 14, ), ), ), ], ), const SizedBox(height: 10), _buildUserTermsAndPrivicy(customColors, context), ]), ), ], ), ), ), ), ), ), ); } Row _buildUserTermsAndPrivicy(CustomColors customColors, BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Transform.scale( scale: 0.75, child: Theme( data: ThemeData( unselectedWidgetColor: customColors.weakTextColor?.withAlpha(180), ), child: Checkbox( activeColor: customColors.linkColor, value: agreeProtocol, onChanged: (agree) { setState(() { agreeProtocol = !agreeProtocol; }); }, ), ), ), RichText( text: TextSpan( children: [ TextSpan( text: AppLocale.readAndAgree.getString(context), style: TextStyle( color: customColors.weakTextColor, fontSize: 13, ), recognizer: TapGestureRecognizer() ..onTap = () { setState(() { agreeProtocol = !agreeProtocol; }); }, ), TextSpan( text: '《${AppLocale.userTerms.getString(context)}》', style: TextStyle( color: customColors.weakLinkColor, fontSize: 13, ), recognizer: TapGestureRecognizer() ..onTap = () { launchUrl(Uri.parse('$apiServerURL/public/info/terms-of-user')); }, ), TextSpan( text: AppLocale.andWord.getString(context), style: TextStyle( color: customColors.weakTextColor, fontSize: 13, ), ), TextSpan( text: '《${AppLocale.privacyPolicy.getString(context)}》', style: TextStyle(color: customColors.weakLinkColor, fontSize: 13), recognizer: TapGestureRecognizer() ..onTap = () { launchUrl(Uri.parse('$apiServerURL/public/info/privacy-policy')); }, ), ], ), ) ], ); } onCreateSubmit() { FocusScope.of(context).requestFocus(FocusNode()); final username = _usernameController.text.trim(); if (username == '') { showErrorMessage(AppLocale.accountRequired.getString(context)); return; } if (!emailValidator.hasMatch(username) && !phoneNumberValidator.hasMatch(username)) { showErrorMessage(AppLocale.accountFormatError.getString(context)); return; } final password = _passwordController.text.trim(); if (password == '' || password.length < 8 || password.length > 20) { showErrorMessage(AppLocale.passwordFormatError.getString(context)); return; } if (verifyCodeId == '') { showErrorMessage(AppLocale.pleaseGetVerifyCodeFirst.getString(context)); return; } final verificationCode = _verificationCodeController.text.trim(); if (verificationCode == '') { showErrorMessage(AppLocale.verifyCodeRequired.getString(context)); return; } if (verificationCode.length != 6) { showErrorMessage(AppLocale.verifyCodeFormatError.getString(context)); return; } final inviteCode = _inviteCodeController.text.trim(); if (inviteCode != '' && inviteCode.length > 20) { showErrorMessage(AppLocale.inviteCodeFormatError.getString(context)); return; } if (!agreeProtocol) { showErrorMessage(AppLocale.pleaseReadAgreeProtocol.getString(context)); return; } final cancel = BotToast.showCustomLoading( toastBuilder: (cancel) { return LoadingIndicator( message: AppLocale.processingWait.getString(context), ); }, allowClick: false, duration: const Duration(seconds: 120), ); APIServer() .signupWithPassword( username: username, password: password, inviteCode: inviteCode, verifyCodeId: verifyCodeId, verifyCode: verificationCode, ) .then((value) async { await widget.settings.set(settingAPIServerToken, value.token); await widget.settings.set(settingUserInfo, jsonEncode(value)); if (value.needBindPhone) { if (context.mounted) { // ignore: use_build_context_synchronously context.push('/bind-phone').then((value) async { if (value == 'logout') { await widget.settings.set(settingAPIServerToken, ''); await widget.settings.set(settingUserInfo, ''); } }); } return; } else { if (context.mounted) { // ignore: use_build_context_synchronously context.go( '${Ability().homeRoute}?show_initial_dialog=${value.isNewUser ? "true" : "false"}&reward=${value.reward}'); } } }).catchError((e) { showErrorMessage(resolveError(context, e)); }).whenComplete(() => cancel()); } } ================================================ FILE: lib/page/balance/free_statistics.dart ================================================ import 'package:askaide/bloc/free_count_bloc.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/column_block.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/message_box.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; import 'package:quickalert/models/quickalert_type.dart'; class FreeStatisticsPage extends StatefulWidget { final SettingRepository setting; const FreeStatisticsPage({super.key, required this.setting}); @override State createState() => _FreeStatisticsPageState(); } class _FreeStatisticsPageState extends State { @override void initState() { super.initState(); context.read().add(FreeCountReloadAllEvent(checkSigninStatus: true)); } @override Widget build(BuildContext context) { var customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( toolbarHeight: CustomSize.toolbarHeight, title: Text( AppLocale.freeQuota.getString(context), style: const TextStyle(fontSize: CustomSize.appBarTitleSize), ), centerTitle: true, ), backgroundColor: customColors.backgroundColor, body: BackgroundContainer( setting: widget.setting, backgroundColor: customColors.backgroundColor, enabled: false, child: RefreshIndicator( displacement: 20, color: customColors.linkColor, onRefresh: () async { context.read().add(FreeCountReloadAllEvent()); }, child: SafeArea( child: SizedBox( height: double.infinity, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), child: BlocConsumer( listenWhen: (previous, current) => current is FreeCountLoadedState, listener: (BuildContext context, FreeCountState state) { if (state is FreeCountLoadedState) { if (state.needSignin) { showBeautyDialog( context, type: QuickAlertType.warning, text: AppLocale.freeModelNeedSignIn.getString(context), confirmBtnText: AppLocale.signIn.getString(context), onConfirmBtnTap: () { context.pop(); context.go('/login'); }, showCancelBtn: true, ); } } }, builder: (context, state) { if (state is FreeCountLoadedState) { if (state.counts.isEmpty) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: Center( child: MessageBox( message: AppLocale.noFreeModel.getString(context), type: MessageBoxType.warning, ), ), ); } return Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: Column( children: [ MessageBox( message: AppLocale.freeModelInfo.getString(context), type: MessageBoxType.info, ), const SizedBox(height: 10), ColumnBlock( innerPanding: 5, children: [ Padding( padding: const EdgeInsets.symmetric(vertical: 5), child: Row( children: [ const Expanded( child: Text( 'Model', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 14, ), )), Row( children: [ Text( AppLocale.todayLeft.getString(context), style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 14, ), ), ], ), ], ), ), ...state.counts.map((e) { return Padding( padding: const EdgeInsets.symmetric(vertical: 5), child: Row( children: [ Expanded( child: Row( children: [ Text( e.name, style: const TextStyle( fontSize: 14, ), ), if (e.info != null && e.info != '') const SizedBox(width: 5), if (e.info != null && e.info != '') InkWell( onTap: () { showBeautyDialog( context, type: QuickAlertType.info, text: e.info ?? '', confirmBtnText: AppLocale.gotIt.getString(context), showCancelBtn: false, ); }, child: Icon( Icons.help_outline, size: 16, color: customColors.weakLinkColor?.withAlpha(150), ), ), ], ), ), buildLeftCountWidget( leftCount: e.leftCount, maxCount: e.maxCount, ), ], ), ); }), ], ), ], ), ); } return const Center(child: LoadingIndicator()); }, ), ), ), ), ), ), ), ); } Widget buildLeftCountWidget({required int leftCount, required int maxCount}) { return Text( '$leftCount', style: const TextStyle( fontSize: 14, ), ); } } ================================================ FILE: lib/page/balance/payment.dart ================================================ import 'dart:async'; import 'package:askaide/bloc/payment_bloc.dart'; import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/helper.dart'; import 'package:askaide/helper/logger.dart'; import 'package:askaide/helper/platform.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/balance/price_block.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/chat/markdown.dart'; import 'package:askaide/page/component/enhanced_button.dart'; import 'package:askaide/page/component/item_selector_search.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/api/payment.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:flutter_stripe/flutter_stripe.dart'; import 'package:go_router/go_router.dart'; import 'package:in_app_purchase/in_app_purchase.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:quickalert/models/quickalert_type.dart'; import 'package:tobias/tobias.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:fluwx/fluwx.dart' as fluwx; import 'web/payment_element.dart' if (dart.library.js) 'web/payment_element_web.dart'; class PaymentScreen extends StatefulWidget { final SettingRepository setting; const PaymentScreen({super.key, required this.setting}); @override State createState() => _PaymentScreenState(); } class _PaymentScreenState extends State { StreamSubscription>? _subscription; Function()? _cancelLoading; @override void initState() { if (PlatformTool.isIOS()) { final purchaseUpdated = InAppPurchase.instance.purchaseStream; _subscription = purchaseUpdated.listen((purchaseDetailsList) { _listenToPurchaseUpdated(purchaseDetailsList); }, onDone: () { _subscription?.cancel(); }, onError: (error) { showErrorMessage(resolveError(context, error)); }); } else if (PlatformTool.isAndroid()) { // 微信支付 fluwx.weChatResponseEventHandler.listen((res) { if (res is fluwx.WeChatPaymentResponse) { if (res.isSuccessful) { showSuccessMessage('购买成功'); } else { showErrorMessage(res.errStr ?? '支付失败'); } } }); } // 加载支付产品列表 context.read().add(PaymentLoadAppleProducts()); super.initState(); } @override void dispose() { _subscription?.cancel(); super.dispose(); } // 支付 ID String? paymentId; ProductDetails? selectedProduct; /// 监听支付状态 void _listenToPurchaseUpdated( List purchaseDetailsList, ) async { for (var purchaseDetails in purchaseDetailsList) { switch (purchaseDetails.status) { case PurchaseStatus.pending: await APIServer().updateApplePay( paymentId!, productId: purchaseDetails.productID, localVerifyData: purchaseDetails.verificationData.localVerificationData, serverVerifyData: purchaseDetails.verificationData.serverVerificationData, verifyDataSource: purchaseDetails.verificationData.source, ); break; case PurchaseStatus.error: APIServer() .cancelApplePay( paymentId!, reason: purchaseDetails.error.toString(), ) .whenComplete(() { _closePaymentLoading(); showErrorMessage(resolveError(context, purchaseDetails.error!)); }); break; case PurchaseStatus.purchased: // fall through if (paymentId != null) { APIServer() .verifyApplePay( paymentId!, productId: purchaseDetails.productID, purchaseId: purchaseDetails.purchaseID, transactionDate: purchaseDetails.transactionDate, localVerifyData: purchaseDetails.verificationData.localVerificationData, serverVerifyData: purchaseDetails.verificationData.serverVerificationData, verifyDataSource: purchaseDetails.verificationData.source, status: purchaseDetails.status.toString(), ) .then((status) { _closePaymentLoading(); showSuccessMessage('购买成功'); }).onError((error, stackTrace) { _closePaymentLoading(); showErrorMessage(resolveError(context, error!)); }); } break; case PurchaseStatus.restored: Logger.instance.d('恢复购买'); _closePaymentLoading(); showSuccessMessage('恢复成功'); break; case PurchaseStatus.canceled: APIServer().cancelApplePay(paymentId!).whenComplete(() { _closePaymentLoading(); showErrorMessage('购买已取消'); }); break; } if (purchaseDetails.pendingCompletePurchase) { await InAppPurchase.instance.completePurchase(purchaseDetails); } } } /// 关闭支付中的 loading void _closePaymentLoading() { paymentId = null; if (_cancelLoading != null) { _cancelLoading!(); _cancelLoading = null; } } /// 开始支付中的 loading void _startPaymentLoading() { _cancelLoading = BotToast.showCustomLoading( toastBuilder: (cancel) { return LoadingIndicator( message: AppLocale.processingWait.getString(context), ); }, allowClick: false, duration: const Duration(seconds: 120), ); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( toolbarHeight: CustomSize.toolbarHeight, elevation: 0, title: Text( AppLocale.buyCredits.getString(context), style: const TextStyle( fontSize: CustomSize.appBarTitleSize, ), ), leading: IconButton( icon: Icon( Icons.arrow_back_ios, color: customColors.weakLinkColor, ), onPressed: () { if (context.canPop()) { context.pop(); } else { context.go('/setting'); } }, ), actions: [ if (Ability().isUserLogon()) TextButton( style: ButtonStyle( overlayColor: WidgetStateProperty.all(Colors.transparent), ), onPressed: () { context.push('/quota-details'); }, child: Text( AppLocale.paymentHistory.getString(context), style: TextStyle(color: customColors.weakLinkColor), textScaler: const TextScaler.linear(0.9), ), ), ], ), backgroundColor: customColors.backgroundColor, body: BackgroundContainer( setting: widget.setting, enabled: false, backgroundColor: customColors.backgroundColor, child: SingleChildScrollView( child: Container( padding: const EdgeInsets.all(10), child: BlocConsumer( listener: (context, state) { if (state is PaymentAppleProductsLoaded) { if (state.error != null) { showErrorMessage(resolveError(context, state.error!)); } else { if (state.localProducts.isEmpty) { showErrorMessage('暂无可购买的产品'); } else { final recommends = state.localProducts.where((e) => e.recommend).toList(); if (recommends.isNotEmpty && !state.loading) { setState(() { selectedProduct = state.products.firstWhere((e) => e.id == recommends.first.id); }); } } } } }, buildWhen: (previous, current) => current is PaymentAppleProductsLoaded, builder: (context, state) { if (state is! PaymentAppleProductsLoaded) { return const Center(child: LoadingIndicator()); } if (state.error != null) { return Center( child: Text( state.error.toString(), style: const TextStyle(color: Colors.red), ), ); } return Column( children: [ Column( children: [ for (var item in state.products) GestureDetector( onTap: () { setState(() { selectedProduct = item; }); }, child: PriceBlock( customColors: customColors, detail: item, selectedProduct: selectedProduct, product: state.localProducts.firstWhere((e) => e.id == item.id), loading: state.loading, ), ), ], ), const SizedBox(height: 20), if (selectedProduct != null) Padding( padding: const EdgeInsets.symmetric(horizontal: 15), child: Text( state.localProducts.where((e) => e.id == selectedProduct!.id).first.description!, style: TextStyle( fontSize: 12, color: customColors.weakTextColor, ), ), ), const SizedBox(height: 10), Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: EnhancedButton( title: '${AppLocale.toPay.getString(context)} ${selectedProduct?.price ?? ''}', onPressed: () async { if (state.loading) { showErrorMessage('价格加载中,请稍后'); return; } if (selectedProduct == null) { showErrorMessage('请选择购买的产品'); return; } if (!Ability().isUserLogon()) { showBeautyDialog( context, type: QuickAlertType.warning, text: '该功能需要登录账号后使用', onConfirmBtnTap: () { context.pop(); context.push('/login'); }, showCancelBtn: true, confirmBtnText: '立即登录', ); return; } // 根据当前平台不通,调用不同的支付方式 if (PlatformTool.isAndroid()) { handlePaymentForAndroid( state, context, customColors, ); } else if (PlatformTool.isIOS()) { _startPaymentLoading(); try { await createAppApplePay(); } catch (e) { _closePaymentLoading(); // ignore: use_build_context_synchronously showErrorMessage(resolveError(context, e)); } } else if (PlatformTool.isWeb()) { handlePaymentForWeb(state, context, customColors); } else { handlePaymentForPC(state, context, customColors); } }, ), ), const SizedBox(height: 20), if (state.note != null) SizedBox( width: double.infinity, child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( ' 购买说明:', style: TextStyle( fontSize: 12, color: customColors.paymentItemTitleColor?.withOpacity(0.5), ), ), const SizedBox(height: 10), Markdown( data: state.note!, textStyle: TextStyle( color: customColors.paymentItemTitleColor?.withOpacity(0.5), fontSize: 12, ), ), ], ), ), ], ); }, ), ), ), ), ), ); } void handlePaymentForWeb(PaymentAppleProductsLoaded state, BuildContext context, CustomColors customColors) { // openConfirmDialog( // context, // '当前终端在线支付暂不可用,预计最晚 2023 年 10 月 15 日恢复,如需充值,请使用移动端 APP(支持 Android 手机、Apple 手机)。', // () { // launchUrlString( // 'https://aidea.aicode.cc', // mode: LaunchMode.externalApplication, // ); // }, // confirmText: '前往下载移动端 APP', // ) final localProduct = state.localProducts.firstWhere((e) => e.id == selectedProduct!.id); final enableStripe = Ability().enableStripe && localProduct.supportStripe; openListSelectDialog( context, [ if (Ability().enableWechatPay) SelectorItem( const PaymentMethodItem( title: Text('微信支付'), image: 'assets/wechat-pay.png', ), 'wechat-pay', ), SelectorItem( const PaymentMethodItem( title: Text('支付宝扫码'), image: 'assets/zhifubao.png', ), 'web', ), SelectorItem( const PaymentMethodItem( title: Text('支付宝手机版'), image: 'assets/zhifubao.png', ), 'wap', ), if (enableStripe) SelectorItem( PaymentMethodItem( title: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text('Stripe'), const SizedBox(width: 5), Text( '(${localProduct.retailPriceUSDText})', style: TextStyle( color: customColors.paymentItemTitleColor?.withOpacity(0.5), fontSize: 12, ), ), ], ), image: 'assets/stripe.png', ), 'stripe', ), ], (value) { _startPaymentLoading(); if (value.value == 'stripe') { createStripePayment(localProduct); } else if (value.value == 'wechat-pay') { createWechatPayment(localProduct); } else { createWebOrWapAlipay(source: value.value).onError((error, stackTrace) { _closePaymentLoading(); showErrorMessageEnhanced(context, error!); }); } return true; }, title: AppLocale.selectPaymentMethod.getString(context), heightFactor: 0.4, ); } /// 处理 PC 端支付 void handlePaymentForPC( PaymentAppleProductsLoaded state, BuildContext context, CustomColors customColors, ) async { final localProduct = state.localProducts.firstWhere((e) => e.id == selectedProduct!.id); final enableStripe = Ability().enableStripe && localProduct.supportStripe; openListSelectDialog( context, [ if (Ability().enableWechatPay) SelectorItem( const PaymentMethodItem( title: Text('微信支付'), image: 'assets/wechat-pay.png', ), 'wechat-pay', ), if (Ability().enableOtherPay) SelectorItem( const PaymentMethodItem( title: Text('支付宝'), image: 'assets/zhifubao.png', ), 'alipay', ), if (enableStripe) SelectorItem( PaymentMethodItem( title: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text('Stripe'), const SizedBox(width: 5), Text( '(${localProduct.retailPriceUSDText})', style: TextStyle( color: customColors.paymentItemTitleColor?.withOpacity(0.5), fontSize: 12, ), ), ], ), image: 'assets/stripe.png', ), 'stripe', ), ], (value) { _startPaymentLoading(); if (value.value == 'alipay') { createWebOrWapAlipay(source: 'web').onError((error, stackTrace) { _closePaymentLoading(); showErrorMessageEnhanced(context, error!); }); } else if (value.value == 'wechat-pay') { createWechatPayment(localProduct); } else { createStripePayment(localProduct); } return true; }, title: AppLocale.selectPaymentMethod.getString(context), heightFactor: 0.4, ); } void handlePaymentForAndroid( PaymentAppleProductsLoaded state, BuildContext context, CustomColors customColors, ) { final localProduct = state.localProducts.firstWhere((e) => e.id == selectedProduct!.id); final enableStripe = Ability().enableStripe && localProduct.supportStripe; openListSelectDialog( context, [ if (Ability().enableWechatPay) SelectorItem( const PaymentMethodItem( title: Text('微信支付'), image: 'assets/wechat-pay.png', ), 'wechat-pay', ), if (Ability().enableOtherPay) SelectorItem( const PaymentMethodItem( title: Text('支付宝'), image: 'assets/zhifubao.png', ), 'alipay', ), if (enableStripe) SelectorItem( PaymentMethodItem( title: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text('Stripe'), const SizedBox(width: 5), Text( '(${localProduct.retailPriceUSDText})', style: TextStyle( color: customColors.paymentItemTitleColor?.withOpacity(0.5), fontSize: 12, ), ), ], ), image: 'assets/stripe.png', ), 'stripe', ), ], (value) { _startPaymentLoading(); if (value.value == 'alipay') { createAppAlipay().onError((error, stackTrace) { _closePaymentLoading(); showErrorMessageEnhanced(context, error!); }); } else if (value.value == 'wechat-pay') { createWechatPayment(localProduct); } else { createStripePayment(localProduct); } return true; }, title: AppLocale.selectPaymentMethod.getString(context), heightFactor: 0.3, ); } /// 创建苹果应用内支付 Future createAppApplePay() async { // 创建支付,服务端保存支付信息,创建支付订单 paymentId = await APIServer().createApplePay(selectedProduct!.id); // 发起 Apple 支付 InAppPurchase.instance.buyConsumable( purchaseParam: PurchaseParam(productDetails: selectedProduct!), ); } /// 创建其它付款(Web 或 Wap) Future createWebOrWapAlipay({required String source}) async { final created = await APIServer().createOtherPay( selectedProduct!.id, source: source, ); paymentId = created.paymentId; // 调起其它支付 launchUrlString(created.params).then((value) { _closePaymentLoading(); openConfirmDialog( context, '请确认支付宝支付是否已完成', () async { _startPaymentLoading(); try { final resp = await APIServer().queryPaymentStatus(created.paymentId); if (resp.success) { showSuccessMessage(resp.note ?? '支付成功'); _closePaymentLoading(); } else { // 支付失败,延迟 5s 再次查询支付状态 await Future.delayed(const Duration(seconds: 5), () async { try { final value = await APIServer().queryPaymentStatus(created.paymentId); if (value.success) { showSuccessMessage(value.note ?? '支付成功'); } else { showErrorMessage('支付未完成,我们接收到的状态为:${value.note}'); } _closePaymentLoading(); } catch (e) { _closePaymentLoading(); // ignore: use_build_context_synchronously showErrorMessage(resolveError(context, e)); } }); } } catch (e) { _closePaymentLoading(); // ignore: use_build_context_synchronously showErrorMessage(resolveError(context, e)); } }, confirmText: '已完成支付', cancelText: '支付遇到问题,稍后继续', ); }); } /// 获取当前支付来源参数 String paymentSource() { if (PlatformTool.isWeb()) { return 'web'; } else if (PlatformTool.isIOS() || PlatformTool.isAndroid()) { return 'app'; } return 'pc'; } /// 创建微信支付 Future createWechatPayment(PaymentProduct product) async { try { final created = await APIServer().createWechatPayment( productId: product.id, source: paymentSource(), ); paymentId = created.paymentId; if (PlatformTool.isAndroid() || PlatformTool.isIOS()) { await fluwx.payWithWeChat( appId: created.appId!, partnerId: created.partnerId!, prepayId: created.prepayId!, packageValue: created.package!, nonceStr: created.noncestr!, timeStamp: int.parse(created.timestamp!), sign: created.sign!, ); } else { openDialog( // ignore: use_build_context_synchronously context, builder: Builder(builder: (context) { return Container( alignment: Alignment.center, height: 250, width: 220, margin: const EdgeInsets.only(top: 20), child: Column( children: [ ClipRRect( borderRadius: CustomSize.borderRadius, child: QrImageView( data: created.codeUrl!, version: QrVersions.auto, size: 200, backgroundColor: Colors.white, ), ), const SizedBox(height: 10), const Text( '请使用微信扫码支付', style: TextStyle( fontSize: 14, ), ), ], ), ); }), onSubmit: () { _startPaymentLoading(); APIServer().queryPaymentStatus(created.paymentId).then((resp) { if (resp.success) { showSuccessMessage(resp.note ?? '支付成功'); _closePaymentLoading(); } else { // 支付失败,延迟 5s 再次查询支付状态 Future.delayed(const Duration(seconds: 5), () async { try { final value = await APIServer().queryPaymentStatus(created.paymentId); if (value.success) { showSuccessMessage(value.note ?? '支付成功'); } else { showErrorMessage('支付未完成,我们接收到的状态为:${value.note}'); } } catch (e) { // ignore: use_build_context_synchronously showErrorMessage(resolveError(context, e)); } finally { _closePaymentLoading(); } }); } }); return true; }, confirmText: '已完成支付', barrierDismissible: false, ); } } on Exception catch (e) { // ignore: use_build_context_synchronously showErrorMessageEnhanced(context, e); } finally { _closePaymentLoading(); } } /// 创建 Stripe 支付 Future createStripePayment(PaymentProduct product) async { try { final created = await APIServer().createStripePaymentSheet( productId: product.id, source: paymentSource(), ); paymentId = created.paymentId; if (PlatformTool.isWeb() || PlatformTool.isAndroid() || PlatformTool.isIOS()) { Stripe.publishableKey = created.publishableKey; Stripe.urlScheme = 'flutterstripe'; await Stripe.instance.applySettings(); } if (PlatformTool.isWeb()) { Navigator.push( // ignore: use_build_context_synchronously context, MaterialPageRoute( fullscreenDialog: true, builder: (context) { return Scaffold( appBar: AppBar(), body: SafeArea( child: Column( children: [ Expanded( child: Container( padding: const EdgeInsets.all(15), child: Builder( builder: (context) { return PlatformPaymentElement( created.paymentIntent, ); }, ), ), ), Container( padding: const EdgeInsets.all(15), child: EnhancedButton( title: '确定付款(${product.retailPriceUSDText})', onPressed: () async { final cancel = BotToast.showCustomLoading( toastBuilder: (cancel) { return LoadingIndicator( message: AppLocale.processingWait.getString(context), ); }, allowClick: false, duration: const Duration(seconds: 120), ); try { await pay(created.paymentId); } catch (e) { Logger.instance.e('支付失败:$e'); // ignore: use_build_context_synchronously showErrorMessageEnhanced(context, '请填写完整的支付信息'); } finally { cancel(); } }, ), ) ], ), ), ); }, ), ); } else if (PlatformTool.isAndroid() || PlatformTool.isIOS()) { // 调起 Stripe 支付 await Stripe.instance.initPaymentSheet( paymentSheetParameters: SetupPaymentSheetParameters( paymentIntentClientSecret: created.paymentIntent, merchantDisplayName: 'AIdea', customerId: created.customer, customerEphemeralKeySecret: created.ephemeralKey, returnURL: 'flutterstripe://redirect', // ignore: use_build_context_synchronously style: Ability().themeMode == 'dark' ? ThemeMode.dark : ThemeMode.light, ), ); // 确认支付 await Stripe.instance.presentPaymentSheet(); showSuccessMessage('购买成功'); } else { // PC 端支付,发起 Web 页面 if (created.proxyUrl == '') { showErrorMessage('支付失败:未能获取支付链接'); return; } Logger.instance.d(created.proxyUrl); launchUrlString( created.proxyUrl, mode: LaunchMode.externalApplication, ).then((value) { _closePaymentLoading(); openConfirmDialog( context, '请确认支付是否已完成', () async { _startPaymentLoading(); try { final resp = await APIServer().queryPaymentStatus(created.paymentId); if (resp.success) { showSuccessMessage(resp.note ?? '支付成功'); _closePaymentLoading(); } else { // 支付失败,延迟 5s 再次查询支付状态 await Future.delayed(const Duration(seconds: 5), () async { try { final value = await APIServer().queryPaymentStatus(created.paymentId); if (value.success) { showSuccessMessage(value.note ?? '支付成功'); } else { showErrorMessage('支付未完成,我们接收到的状态为:${value.note}'); } _closePaymentLoading(); } catch (e) { _closePaymentLoading(); // ignore: use_build_context_synchronously showErrorMessage(resolveError(context, e)); } }); } } catch (e) { _closePaymentLoading(); // ignore: use_build_context_synchronously showErrorMessage(resolveError(context, e)); } }, confirmText: '已完成支付', cancelText: '支付遇到问题,稍后继续', ); }); } } on Exception catch (e) { if (e is StripeException) { showErrorMessage('支付失败:${e.error.localizedMessage}'); } else { // ignore: use_build_context_synchronously showErrorMessageEnhanced(context, e); } } finally { _closePaymentLoading(); } } /// 创建其它付款(App) Future createAppAlipay() async { // 其它支付 final created = await APIServer().createOtherPay( selectedProduct!.id, source: 'app', ); paymentId = created.paymentId; // 沙箱环境支持 final env = created.sandbox ? AliPayEvn.SANDBOX : AliPayEvn.ONLINE; // 调起其它支付 final aliPayRes = await aliPay( created.params, evn: env, ).whenComplete(() => _closePaymentLoading()); Logger.instance.d("================="); Logger.instance.d(aliPayRes); Logger.instance.d(aliPayRes["resultStatus"]); if (aliPayRes['resultStatus'] == '9000') { await APIServer().otherPayClientConfirm( aliPayRes.map((key, value) => MapEntry(key.toString(), value)), ); showSuccessMessage('购买成功'); } else { switch (aliPayRes['resultStatus']) { case 8000: // fall through case 6004: showErrorMessage('支付处理中,请稍后查看购买历史确认结果'); break; case 4000: showErrorMessage('支付失败'); break; case 5000: showErrorMessage('重复请求'); break; case 6001: showErrorMessage('支付已取消'); break; case 6002: showErrorMessage('网络连接出错'); break; default: showErrorMessage('支付失败'); } } Logger.instance.d("-----------------"); } } /// 支付方式选择项 class PaymentMethodItem extends StatelessWidget { final Widget title; final String? image; const PaymentMethodItem({super.key, required this.title, this.image}); @override Widget build(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ if (image != null) ...[ ClipRRect( borderRadius: CustomSize.borderRadius, child: Image.asset( image!, width: 20, height: 20, ), ), const SizedBox(width: 10), ], title, ], ); } } ================================================ FILE: lib/page/balance/payment_history.dart ================================================ import 'package:askaide/helper/helper.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/credit.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/api/quota.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:intl/intl.dart'; class PaymentHistoryScreen extends StatefulWidget { final SettingRepository setting; const PaymentHistoryScreen({super.key, required this.setting}); @override State createState() => _PaymentHistoryScreenState(); } class _PaymentHistoryScreenState extends State { @override Widget build(BuildContext context) { var customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( toolbarHeight: CustomSize.toolbarHeight, title: const Text( '购买历史', style: TextStyle(fontSize: CustomSize.appBarTitleSize), ), centerTitle: true, elevation: 0, ), backgroundColor: customColors.backgroundColor, body: BackgroundContainer( setting: widget.setting, enabled: false, backgroundColor: customColors.backgroundColor, child: Container( padding: const EdgeInsets.all(16), child: FutureBuilder( future: APIServer().quotaDetails(), builder: (context, snapshot) { if (snapshot.hasError) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.error_outline, size: 50, color: Colors.red, ), const SizedBox(height: 10), Text( resolveError(context, snapshot.error!), style: const TextStyle(color: Colors.red), ), ], ), ); } if (!snapshot.hasData) { return const Center( child: CircularProgressIndicator(), ); } return _buildQuotaDetailPage(context, snapshot.data!, customColors); }, ), ), ), ), ); } Widget _buildQuotaDetailPage(BuildContext context, QuotaResp quota, CustomColors customColors) { return Column( children: [ Expanded( child: ListView( shrinkWrap: true, children: [ for (var item in quota.details) Stack( children: [ Container( margin: const EdgeInsets.symmetric(vertical: 6), padding: const EdgeInsets.only( top: 20, bottom: 10, left: 16, right: 16, ), decoration: BoxDecoration( color: customColors.listTileBackgroundColor, borderRadius: CustomSize.borderRadius, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( (item.note == null || item.note == '') ? '购买' : item.note!, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 5), Text( DateFormat( 'yyyy/MM/dd HH:mm', ).format(item.createdAt.toLocal()), textScaler: const TextScaler.linear(0.8), style: TextStyle( color: Colors.grey[600], ), ), ], ), ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Credit( count: item.quota, color: Colors.amber, withAddPrefix: true, fontWeight: FontWeight.w500, ), Text( '${DateFormat('yyyy/MM/dd').format(item.periodEndAt.toLocal())} 过期', textScaler: const TextScaler.linear(0.7), ), ], ), ], ), ], ), ), _buildTagForItem(item), ], ) ], ), ), ], ); } Widget _buildTagForItem(QuotaDetail item) { if (item.rest <= 0) { return _buildTag(AppLocale.usedUp.getString(context), Colors.orange); } if (item.expired) { return _buildTag(AppLocale.expired.getString(context), Colors.grey[600]!); } return const SizedBox(); } Widget _buildTag(String text, Color color) { return Positioned( right: 1, top: 7, child: Container( decoration: BoxDecoration( color: color, borderRadius: const BorderRadius.only(topRight: CustomSize.radius, bottomLeft: CustomSize.radius), ), padding: const EdgeInsets.symmetric( horizontal: 5, vertical: 2, ), child: Text( text, textScaler: const TextScaler.linear(0.6), style: const TextStyle(color: Colors.white70), ), ), ); } } ================================================ FILE: lib/page/balance/price_block.dart ================================================ import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/credit.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api/payment.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:in_app_purchase/in_app_purchase.dart'; class PriceBlock extends StatelessWidget { final CustomColors customColors; final ProductDetails detail; final ProductDetails? selectedProduct; final PaymentProduct product; final bool loading; const PriceBlock({ super.key, required this.customColors, required this.detail, this.selectedProduct, required this.product, required this.loading, }); @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return Stack( children: [ Container( margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), padding: const EdgeInsets.all(20), alignment: Alignment.center, decoration: BoxDecoration( color: customColors.listTileBackgroundColor, border: Border.all( color: (selectedProduct != null && selectedProduct!.id == detail.id) ? customColors.linkColor ?? Colors.green : customColors.paymentItemBackgroundColor!, ), borderRadius: CustomSize.borderRadius, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Credit( count: product.quota, color: customColors.paymentItemTitleColor, ), const SizedBox(height: 6), Row( children: [ const Icon( Icons.info_outline_rounded, size: 11, color: Color.fromARGB(255, 224, 170, 7), ), const SizedBox(width: 1), Text( '${product.expirePolicyText} ${AppLocale.validDays.getString(context)}', style: const TextStyle( fontSize: 11, color: Color.fromARGB(255, 224, 170, 7), ), ), ], ), ], ), const SizedBox(height: 10), loading ? const Text('加载中...') : Text( detail.price, style: TextStyle( fontSize: 22, fontWeight: FontWeight.bold, color: customColors.linkColor, ), ), ], ), ), if (product.recommend) Positioned( right: 11, top: 6, child: Container( decoration: const BoxDecoration( color: Color.fromARGB(255, 224, 68, 7), borderRadius: BorderRadius.only(topRight: CustomSize.radius, bottomLeft: CustomSize.radius), ), padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2), child: const Text( 'Best Deal', style: TextStyle( color: Colors.white, fontSize: 10, ), ), ), ) ], ); } } ================================================ FILE: lib/page/balance/quota_usage_details.dart ================================================ import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/model/misc.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/material.dart'; class QuotaUsageDetailScreen extends StatefulWidget { final SettingRepository setting; final String date; const QuotaUsageDetailScreen({ super.key, required this.setting, required this.date, }); @override State createState() => _QuotaUsageDetailScreenState(); } class _QuotaUsageDetailScreenState extends State { List usages = []; bool loaded = false; @override void initState() { APIServer().quotaUsedDetails(date: widget.date).then((value) { setState(() { usages = value; loaded = true; }); }); super.initState(); } @override Widget build(BuildContext context) { var customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( toolbarHeight: CustomSize.toolbarHeight, title: Text( widget.date, style: const TextStyle(fontSize: CustomSize.appBarTitleSize), ), centerTitle: true, elevation: 0, ), backgroundColor: customColors.backgroundColor, body: BackgroundContainer( setting: widget.setting, enabled: false, backgroundColor: customColors.backgroundColor, child: Container( padding: const EdgeInsets.all(16), child: _buildQuotaUsagePage(context, customColors), ), ), ), ); } Widget _buildQuotaUsagePage( BuildContext context, CustomColors customColors, ) { if (!loaded) { return const Center( child: LoadingIndicator(), ); } final usageGt0 = usages.where((e) => e.used > 0).toList(); if (usageGt0.isEmpty) { return const Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.error_outline, size: 50, ), SizedBox(height: 10), Text( '暂无使用记录', ), ], ), ); } return Column( children: [ Expanded( child: ListView( shrinkWrap: true, children: [ for (var item in usageGt0) Container( margin: const EdgeInsets.symmetric(vertical: 6), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: customColors.listTileBackgroundColor, borderRadius: CustomSize.borderRadius, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(item.createdAt, style: const TextStyle(fontWeight: FontWeight.bold)), const SizedBox(width: 20), Expanded( child: Text('使用 ${item.type} 消耗 ${item.used} 个智慧果'), ), ], ), ) ], ), ), ], ); } } ================================================ FILE: lib/page/balance/quota_usage_statistics.dart ================================================ import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/message_box.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/material.dart'; import 'package:askaide/repo/model/misc.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; class QuotaUsageStatisticsScreen extends StatefulWidget { final SettingRepository setting; const QuotaUsageStatisticsScreen({super.key, required this.setting}); @override State createState() => _QuotaUsageStatisticsScreenState(); } class _QuotaUsageStatisticsScreenState extends State { List usages = []; bool loaded = false; @override void initState() { APIServer().quotaUsedStatistics().then((value) { setState(() { usages = value; loaded = true; }); }); super.initState(); } @override Widget build(BuildContext context) { var customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( toolbarHeight: CustomSize.toolbarHeight, title: Text( AppLocale.creditsUsage.getString(context), style: const TextStyle(fontSize: CustomSize.appBarTitleSize), ), centerTitle: true, elevation: 0, ), backgroundColor: customColors.backgroundColor, body: BackgroundContainer( setting: widget.setting, enabled: false, backgroundColor: customColors.backgroundColor, child: Container( padding: const EdgeInsets.all(16), child: Column( children: [ MessageBox( message: AppLocale.creditUsageTips.getString(context), type: MessageBoxType.info, ), const SizedBox(height: 10), Expanded( child: _buildQuotaUsagePage(context, customColors), ), ], ), ), ), ), ); } Widget _buildQuotaUsagePage( BuildContext context, CustomColors customColors, ) { if (!loaded) { return const Center( child: LoadingIndicator(), ); } final usageGt0 = usages.where((e) => e.used > 0 || e.used == -1).toList(); if (usageGt0.isEmpty) { return const Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.error_outline, size: 50, ), SizedBox(height: 10), Text( 'No records yet', ), ], ), ); } return Column( children: [ Expanded( child: ListView( shrinkWrap: true, children: [ for (var item in usageGt0) Container( margin: const EdgeInsets.symmetric(vertical: 6), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: customColors.listTileBackgroundColor, borderRadius: CustomSize.borderRadius, ), child: InkWell( onTap: () { context.push('/quota-usage-daily-details?date=${item.date}'); }, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( item.date, style: const TextStyle( fontWeight: FontWeight.bold, ), ), if (item.used == -1) Text(AppLocale.unbilled.getString(context)) else Text('${item.used > 0 ? "-" : ""}${AppLocale.creditUnit.getString(context)}${item.used}'), ], ), ), ) ], ), ), ], ); } } ================================================ FILE: lib/page/balance/web/payment_element.dart ================================================ import 'package:flutter/widgets.dart'; import 'package:flutter_stripe/flutter_stripe.dart'; Future pay(String paymentId, {String? action}) async { throw UnimplementedError(); } void closeWindow() { throw UnimplementedError(); } class PlatformPaymentElement extends StatelessWidget { const PlatformPaymentElement(this.clientSecret, {super.key}); final String? clientSecret; @override Widget build(BuildContext context) { return Container(); } } ================================================ FILE: lib/page/balance/web/payment_element_web.dart ================================================ import 'package:askaide/helper/ability.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_stripe_web/flutter_stripe_web.dart'; import 'dart:html' as html; import 'package:stripe_js/stripe_api.dart' as js; Future pay(String paymentId, {String? action}) async { final currentUrl = Uri.parse(html.window.location.href); var href = Uri( scheme: currentUrl.scheme, host: currentUrl.host, port: currentUrl.port, fragment: '/payment/result?payment_id=$paymentId&action=$action', ).toString(); return await WebStripe.instance.confirmPaymentElement( ConfirmPaymentElementOptions( confirmParams: ConfirmPaymentParams( return_url: href, ), ), ); } void closeWindow() { html.window.close(); } class PlatformPaymentElement extends StatelessWidget { const PlatformPaymentElement(this.clientSecret, {super.key}); final String? clientSecret; @override Widget build(BuildContext context) { return PaymentElement( autofocus: true, enablePostalCode: true, onCardChanged: (_) {}, clientSecret: clientSecret ?? '', appearance: js.ElementAppearance( theme: Ability().themeMode == 'dark' ? js.ElementTheme.night : js.ElementTheme.stripe, ), ); } } ================================================ FILE: lib/page/balance/web_payment_proxy.dart ================================================ import 'package:askaide/helper/logger.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/enhanced_button.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:flutter_stripe/flutter_stripe.dart'; import 'web/payment_element.dart' if (dart.library.js) 'web/payment_element_web.dart'; class WebPaymentProxy extends StatefulWidget { final SettingRepository setting; final String paymentId; final String paymentIntent; final String price; final String publishableKey; final String? finishAction; const WebPaymentProxy({ super.key, required this.setting, required this.paymentId, required this.paymentIntent, required this.price, required this.publishableKey, this.finishAction, }); @override State createState() => _WebPaymentProxyState(); } class _WebPaymentProxyState extends State { @override void initState() { super.initState(); Stripe.publishableKey = widget.publishableKey; Stripe.urlScheme = 'flutterstripe'; } @override Widget build(BuildContext context) { var customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( toolbarHeight: CustomSize.toolbarHeight, title: const Text( '', style: TextStyle(fontSize: CustomSize.appBarTitleSize), ), centerTitle: true, elevation: 0, ), backgroundColor: customColors.backgroundContainerColor, body: BackgroundContainer( setting: widget.setting, enabled: false, backgroundColor: customColors.backgroundColor, maxWidth: CustomSize.smallWindowSize, child: Center( child: FutureBuilder( future: Stripe.instance.applySettings(), builder: (context, snapshot) { if (snapshot.hasError) { Logger.instance.e('Stripe 初始化失败:${snapshot.error}'); return Center( child: Text( snapshot.error.toString(), style: const TextStyle(color: Colors.red), ), ); } return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Expanded( child: Container( padding: const EdgeInsets.all(15), child: Builder( builder: (context) { return PlatformPaymentElement( widget.paymentIntent, ); }, ), ), ), Container( padding: const EdgeInsets.all(15), child: EnhancedButton( title: '确定付款(${widget.price})', onPressed: () async { final cancel = BotToast.showCustomLoading( toastBuilder: (cancel) { return LoadingIndicator( message: AppLocale.processingWait.getString(context), ); }, allowClick: false, duration: const Duration(seconds: 120), ); try { await pay( widget.paymentId, action: widget.finishAction, ); } catch (e) { Logger.instance.e('支付失败:$e'); // ignore: use_build_context_synchronously showErrorMessageEnhanced(context, '请填写完整的支付信息'); } finally { cancel(); } }, ), ) ], ); }), ), ), ), ); } } ================================================ FILE: lib/page/balance/web_payment_result.dart ================================================ import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/api/payment.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'web/payment_element.dart' if (dart.library.js) 'web/payment_element_web.dart'; class WebPaymentResult extends StatefulWidget { final String paymentId; final String? action; const WebPaymentResult({ super.key, required this.paymentId, this.action, }); @override State createState() => _WebPaymentResultState(); } class _WebPaymentResultState extends State { PaymentStatus? paymentStatus; DateTime startTime = DateTime.now(); @override void initState() { super.initState(); updatePaymentStatus(); } updatePaymentStatus() { if (!context.mounted) { return; } if (DateTime.now().difference(startTime).inSeconds > 60) { setState(() { paymentStatus = PaymentStatus(false, note: '查询超时'); }); return; } APIServer().queryPaymentStatus(widget.paymentId).then((value) { if (!value.success) { Future.delayed(const Duration(seconds: 3), () { if (context.mounted) { updatePaymentStatus(); } }); } else { setState(() { paymentStatus = value; }); } }); } @override Widget build(BuildContext context) { var customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( title: const Text('支付结果'), leading: IconButton( icon: Icon( Icons.close, color: customColors.weakLinkColor, ), onPressed: () { if (widget.action != null && widget.action == 'close') { closeWindow(); } else { if (context.canPop()) { context.pop(); } else { context.go('/payment'); } } }, ), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: buildResult(), ), ), ), ); } List buildResult() { if (paymentStatus == null) { return const [ CircularProgressIndicator(), SizedBox(height: 20), Text('正在查询支付结果'), ]; } if (!paymentStatus!.success) { return [ const Icon( Icons.error, color: Colors.red, size: 100, ), Text(paymentStatus!.note ?? '支付失败'), ]; } return [ const Icon( Icons.check_circle, color: Colors.green, size: 100, ), const Text( '支付成功', style: TextStyle(fontSize: 24), ), ]; } } ================================================ FILE: lib/page/chat/character_chat.dart ================================================ import 'dart:convert'; import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/haptic_feedback.dart'; import 'package:askaide/helper/model.dart'; import 'package:askaide/helper/upload.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/chat/component/model_switcher.dart'; import 'package:askaide/page/chat/component/stop_button.dart'; import 'package:askaide/page/component/audio_player.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/chat/chat_input_button.dart'; import 'package:askaide/page/component/chat/empty.dart'; import 'package:askaide/page/component/chat/file_upload.dart'; import 'package:askaide/page/component/chat/message_state_manager.dart'; import 'package:askaide/page/component/chat/role_avatar.dart'; import 'package:askaide/page/component/effect/glass.dart'; import 'package:askaide/page/component/enhanced_textfield.dart'; import 'package:askaide/page/component/global_alert.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/select_mode_toolbar.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/bloc/chat_message_bloc.dart'; import 'package:askaide/bloc/room_bloc.dart'; import 'package:askaide/bloc/notify_bloc.dart'; import 'package:askaide/page/component/chat/chat_input.dart'; import 'package:askaide/page/component/chat/chat_preview.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/model/message.dart'; import 'package:askaide/repo/model/misc.dart'; import 'package:askaide/repo/model/room.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; import 'package:askaide/repo/model/model.dart' as mm; import '../component/dialog.dart'; class CharacterChatPage extends StatefulWidget { final int roomId; final MessageStateManager stateManager; final SettingRepository setting; const CharacterChatPage({ super.key, required this.roomId, required this.stateManager, required this.setting, }); @override State createState() => _CharacterChatPageState(); } class _CharacterChatPageState extends State { final ScrollController _scrollController = ScrollController(); final ValueNotifier _inputEnabled = ValueNotifier(true); final ChatPreviewController _chatPreviewController = ChatPreviewController(); final AudioPlayerController _audioPlayerController = AudioPlayerController(useRemoteAPI: true); bool showAudioPlayer = false; bool audioLoadding = false; // The selected image files for image upload List selectedImageFiles = []; // The selected file for file upload FileUpload? selectedFile; /// Currently selected model mm.Model? tempModel; // 全量模型列表 List supportModels = []; // 聊天室 ID,当没有值时,会在第一个聊天消息发送后自动设置新值 int? chatId; /// 是否启用搜索 bool enableSearch = false; /// 是否启用推理 bool enableReasoning = false; @override void initState() { super.initState(); reloadPage(); _chatPreviewController.addListener(() { setState(() {}); }); _audioPlayerController.onPlayStopped = () { setState(() { showAudioPlayer = false; }); }; _audioPlayerController.onPlayAudioStarted = () { setState(() { showAudioPlayer = true; }); }; _audioPlayerController.onPlayAudioLoading = (loading) { setState(() { audioLoadding = loading; }); }; // 加载模型列表,用于查询模型名称 ModelAggregate.models().then((value) { setState(() { supportModels = value; }); }); } reloadPage() { context.read().add(ChatMessageGetRecentEvent(chatHistoryId: chatId)); context.read().add(RoomLoadEvent(widget.roomId, cascading: true)); } @override void dispose() { _scrollController.dispose(); _chatPreviewController.dispose(); _audioPlayerController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return WindowFrameWidget( child: Scaffold( appBar: _buildAppBar(context, customColors), backgroundColor: customColors.backgroundContainerColor, body: BackgroundContainer( setting: widget.setting, child: _buildChatComponents(customColors), ), ), ); } mm.Model? roomModel; Widget _buildChatComponents(CustomColors customColors) { return BlocConsumer( listenWhen: (previous, current) => current is RoomLoaded, listener: (context, state) { if (state is RoomLoaded) { ModelAggregate.model(state.room.model).then((value) { setState(() { roomModel = value; }); }); } }, buildWhen: (previous, current) => current is RoomLoaded, builder: (context, room) { if (room is RoomLoaded) { final enableImageUpload = tempModel == null ? (roomModel != null && roomModel!.supportVision) : (tempModel?.supportVision ?? false); final showReasoning = tempModel == null ? (roomModel != null && roomModel!.supportReasoning) : (tempModel?.supportReasoning ?? false); final showSearch = tempModel == null ? (roomModel != null && roomModel!.supportSearch) : (tempModel?.supportSearch ?? false); return SafeArea( top: false, bottom: false, child: Column( children: [ if (Ability().showGlobalAlert) const GlobalAlert(pageKey: 'chat'), // 语音输出中提示 if (showAudioPlayer) EnhancedAudioPlayer( controller: _audioPlayerController, loading: audioLoadding, ), // 聊天内容窗口 Expanded( child: Stack( fit: StackFit.expand, children: [ _buildChatPreviewArea( room, customColors, _chatPreviewController.selectMode, ), if (!_inputEnabled.value) Positioned( bottom: 10, width: CustomSize.adaptiveMaxWindowWidth(context), child: Center( child: StopButton( label: AppLocale.stopOutput.getString(context), onPressed: () { HapticFeedbackHelper.mediumImpact(); context.read().add(ChatMessageStopEvent()); }, ), ), ), ], ), ), // 聊天输入窗口 Container( decoration: BoxDecoration( borderRadius: const BorderRadius.only(topLeft: CustomSize.radius, topRight: CustomSize.radius), color: customColors.chatInputPanelBackground, ), child: _chatPreviewController.selectMode ? SelectModeToolbar( chatPreviewController: _chatPreviewController, ) : ChatInput( enableNotifier: _inputEnabled, onSubmit: (value) { _handleSubmit(value); FocusManager.instance.primaryFocus?.unfocus(); }, enableImageUpload: enableImageUpload && selectedFile == null, onImageSelected: (files) { setState(() { selectedImageFiles = files; }); }, selectedImageFiles: selectedImageFiles, // enableFileUpload: selectedImageFiles.isEmpty, onFileSelected: (file) { setState(() { selectedFile = file; }); }, selectedFile: selectedFile, hintText: AppLocale.askMeAnyQuestion.getString(context), onVoiceRecordTappedEvent: () { _audioPlayerController.stop(); }, onStopGenerate: () { context.read().add(ChatMessageStopEvent()); }, toolsBuilder: () { return [ if (showReasoning) ChatInputButton( text: AppLocale.reasoning.getString(context), icon: Icons.tips_and_updates_outlined, onPressed: () { setState(() { enableReasoning = !enableReasoning; }); }, isActive: enableReasoning, ), if (showSearch) ChatInputButton( text: AppLocale.onlineSearch.getString(context), icon: Icons.language_outlined, onPressed: () { setState(() { enableSearch = !enableSearch; }); }, isActive: enableSearch, ), ]; }, ), ), ], ), ); } else { return Container(); } }, ); } BlocConsumer _buildChatPreviewArea( RoomLoaded room, CustomColors customColors, bool selectMode, ) { return BlocConsumer( listener: (context, state) { if (state is ChatHistoryInited) { setState(() { chatId = state.chatId; }); } if (state is ChatMessagesLoaded && state.error == null) { setState(() { selectedImageFiles = []; selectedFile = null; }); } // 显示错误提示 else if (state is ChatMessagesLoaded && state.error != null) { showErrorMessageEnhanced(context, state.error); } else if (state is ChatMessageUpdated) { // 聊天内容窗口滚动到底部 if (!state.processing && _scrollController.hasClients) { _scrollController.animateTo( 0, duration: const Duration(milliseconds: 500), curve: Curves.easeOut, ); } if (state.processing && _inputEnabled.value) { // 聊天回复中时,禁止输入框编辑 setState(() { _inputEnabled.value = false; }); } else if (!state.processing && !_inputEnabled.value) { // 聊天回复完成时,取消输入框的禁止编辑状态 setState(() { _inputEnabled.value = true; }); } } }, buildWhen: (prv, cur) => cur is ChatMessagesLoaded, builder: (context, state) { if (state is ChatMessagesLoaded) { final loadedMessages = List.from(state.messages); if (room.room.initMessage != null && room.room.initMessage != '' && loadedMessages.isEmpty) { loadedMessages.add( Message( Role.receiver, room.room.initMessage!, type: MessageType.initMessage, id: 0, ), ); } if (loadedMessages.isEmpty) { // 聊天内容为空时,显示示例页面 if (loadedMessages.isEmpty) { return EmptyPreview( examples: room.examples ?? [], onSubmit: _handleSubmit, cardMode: true, ); } } final messages = loadedMessages.map((e) { if (e.model != null && !e.model!.startsWith('v2@')) { final mod = supportModels.where((m) => m.id == e.model).firstOrNull; if (mod != null) { e.senderName = mod.shortName; e.avatarUrl = mod.avatarUrl; } } if (e.avatarUrl == null || e.senderName == null) { e.avatarUrl = room.room.avatarUrl; e.senderName = room.room.name; } return MessageWithState( e, room.states[widget.stateManager.getKey(e.roomId ?? 0, e.id ?? 0)] ?? MessageState(), ); }).toList(); _chatPreviewController.setAllMessageIds(messages); return ChatPreview( padding: _inputEnabled.value ? null : const EdgeInsets.only(bottom: 35), messages: messages, scrollController: _scrollController, controller: _chatPreviewController, stateManager: widget.stateManager, robotAvatar: selectMode ? null : RoleAvatar( avatarUrl: room.room.avatarUrl, name: room.room.name, ), senderNameBuilder: (message) { if (message.senderName == null) { return null; } return Container( margin: const EdgeInsets.fromLTRB(0, 0, 10, 7), padding: const EdgeInsets.symmetric(horizontal: 13), child: Text( room.room.name, style: TextStyle( color: customColors.weakTextColor, fontSize: 12, ), ), ); }, onDeleteMessage: (id) { handleDeleteMessage(context, id, chatHistoryId: chatId); }, onSpeakEvent: (message) { _audioPlayerController.playAudio(message.text); }, onResentEvent: (message, index) { _scrollController.animateTo(0, duration: const Duration(milliseconds: 500), curve: Curves.easeOut); _handleSubmit(message.text, messagetType: message.type, index: index, isResent: true); }, ); } return const Center(child: CircularProgressIndicator()); }, ); } /// 构建 AppBar AppBar _buildAppBar(BuildContext context, CustomColors customColors) { return _chatPreviewController.selectMode ? AppBar( title: Text( AppLocale.select.getString(context), style: const TextStyle(fontSize: CustomSize.appBarTitleSize), ), centerTitle: true, elevation: 0, leadingWidth: 80, leading: TextButton( onPressed: () { _chatPreviewController.exitSelectMode(); }, child: Text( AppLocale.cancel.getString(context), style: TextStyle(color: customColors.linkColor), ), ), toolbarHeight: CustomSize.toolbarHeight, ) : AppBar( centerTitle: true, elevation: 0, // backgroundColor: customColors.chatRoomBackground, title: BlocBuilder( buildWhen: (previous, current) => current is RoomLoaded, builder: (context, state) { if (state is RoomLoaded) { return GestureDetector( onTap: () { ModelSwitcher.openActionDialog( context: context, onSelected: (selected) { setState(() { tempModel = selected; }); }, initValue: tempModel, ); }, child: Text( state.room.name, overflow: TextOverflow.ellipsis, maxLines: 1, style: const TextStyle(fontSize: CustomSize.appBarTitleSize), ), ); } return Container(); }, ), actions: [ IconButton( icon: const Icon(Icons.maps_ugc_outlined), onPressed: createNewChat, ), ], toolbarHeight: CustomSize.toolbarHeight, ); } /// 创建新的聊天 void createNewChat() { setState(() { chatId = null; }); reloadPage(); } /// 提交新消息 void _handleSubmit( String text, { messagetType = MessageType.text, int? index, bool isResent = false, }) async { setState(() { _inputEnabled.value = false; }); if (selectedFile != null) { final cancel = BotToast.showCustomLoading( toastBuilder: (cancel) { return const LoadingIndicator( message: '正在上传,请稍后...', ); }, allowClick: false, ); try { final uploader = QiniuUploader(widget.setting); if (!selectedFile!.uploaded) { final path = selectedFile!.file.path; if (path != null && path.isNotEmpty) { final uploadRes = await uploader.uploadFile(path, usage: 'document'); selectedFile!.setUrl(uploadRes.url); } else if (selectedFile!.file.bytes != null && selectedFile!.file.bytes!.isNotEmpty) { final uploadRes = await uploader.upload( 'file-${DateTime.now().millisecondsSinceEpoch}.${selectedFile!.file.name}', selectedFile!.file.bytes!, usage: 'document', ); selectedFile!.setUrl(uploadRes.url); } } } catch (e) { // ignore: use_build_context_synchronously showErrorMessageEnhanced(context, e); return; } finally { cancel(); } } if (selectedImageFiles.isNotEmpty) { final cancel = BotToast.showCustomLoading( toastBuilder: (cancel) { return const LoadingIndicator( message: '正在上传,请稍后...', ); }, allowClick: false, ); try { final uploader = ImageUploader(widget.setting); for (var file in selectedImageFiles) { if (file.uploaded) { continue; } if (file.file.bytes != null) { final res = await uploader.base64( imageData: file.file.bytes, maxSize: 1024 * 1024, compressWidth: 512, compressHeight: 512, ); file.setUrl(res); } else { final res = await uploader.base64( path: file.file.path!, maxSize: 1024 * 1024, compressWidth: 512, compressHeight: 512, ); file.setUrl(res); } } } catch (e) { // ignore: use_build_context_synchronously showErrorMessageEnhanced(context, e); return; } finally { cancel(); } } // showSuccessMessage('Model: ${roomModel?.id}/${tempModel?.id}'); // ignore: use_build_context_synchronously context.read().add( ChatMessageSendEvent( Message( Role.sender, text, user: 'me', ts: DateTime.now(), type: messagetType, images: selectedImageFiles.where((e) => e.uploaded).map((e) => e.url!).toList(), file: selectedFile != null && selectedFile!.uploaded ? jsonEncode({ 'name': selectedFile!.file.name, 'url': selectedFile!.url, }) : null, chatHistoryId: chatId, model: roomModel?.id, flags: [ if (enableSearch) 'search', if (enableReasoning) 'reasoning', ], ), index: index, isResent: isResent, tempModel: tempModel?.id, ), ); // ignore: use_build_context_synchronously context.read().add(NotifyResetEvent()); } } /// 处理消息删除事件 void handleDeleteMessage(BuildContext context, int id, {int? chatHistoryId}) { openConfirmDialog( context, AppLocale.confirmDelete.getString(context), () => context.read().add(ChatMessageDeleteEvent([id], chatHistoryId: chatHistoryId)), danger: true, ); } /// 打开示例问题列表 void handleOpenExampleQuestion( BuildContext context, Room room, List examples, Function(String text) onSubmit, ) { final customColors = Theme.of(context).extension()!; openModalBottomSheet( context, (context) { return FractionallySizedBox( heightFactor: 0.8, child: GlassEffect( child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( padding: const EdgeInsets.symmetric( vertical: 10, ), child: Text( AppLocale.examples.getString(context), textScaler: const TextScaler.linear(1.2), ), ), Expanded( child: ListView.builder( itemCount: examples.length, itemBuilder: (context, i) { return ListTile( title: Container( alignment: Alignment.center, padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 10, ), decoration: BoxDecoration( borderRadius: CustomSize.borderRadius, color: customColors.chatExampleItemBackground, ), child: Column( children: [ Text( examples[i].title, textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, style: TextStyle( color: customColors.chatExampleItemText, ), ), if (examples[i].content != null) const SizedBox(height: 5), if (examples[i].content != null) Text( examples[i].content!, textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, maxLines: 2, style: TextStyle( fontSize: 12, color: customColors.chatExampleItemText, ), ), ], ), ), onTap: () { final controller = TextEditingController(); controller.text = examples[i].text; openDialog( context, title: Text( AppLocale.confirmSend.getString(context), textAlign: TextAlign.left, textScaler: const TextScaler.linear(0.8), ), builder: Builder( builder: (context) { return EnhancedTextField( controller: controller, maxLines: 5, maxLength: 4000, customColors: customColors, ); }, ), onSubmit: () { onSubmit(controller.text.trim()); return true; }, afterSubmit: () => context.pop(), ); }, ); }, ), ), ], ), ), ); }, ); } ================================================ FILE: lib/page/chat/character_create.dart ================================================ import 'dart:io'; import 'dart:math'; import 'package:askaide/bloc/room_bloc.dart'; import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/helper.dart'; import 'package:askaide/helper/upload.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/advanced_button.dart'; import 'package:askaide/page/component/avatar_selector.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/column_block.dart'; import 'package:askaide/page/component/enhanced_button.dart'; import 'package:askaide/page/component/enhanced_input.dart'; import 'package:askaide/page/component/enhanced_textfield.dart'; import 'package:askaide/page/component/image.dart'; import 'package:askaide/page/component/item_selector_search.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/random_avatar.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:askaide/helper/model.dart'; import 'package:askaide/page/component/model_item.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/model/model.dart' as mm; import 'package:go_router/go_router.dart'; /// 创建聊天室对话框 class CharacterCreatePage extends StatefulWidget { final SettingRepository setting; const CharacterCreatePage({super.key, required this.setting}); @override State createState() => _CharacterCreatePageState(); } class _CharacterCreatePageState extends State { final _nameController = TextEditingController(text: ''); final _promptController = TextEditingController(text: ''); final randomSeed = Random().nextInt(10000); String? _avatarUrl; int? _avatarId; List avatarPresets = []; int maxContext = 6; List validMemories = [ ChatMemory('Ephemeral', 1, description: 'Each conversation is independent, often used for one-off Q&A'), ChatMemory('Basic', 3, description: 'Remembers the last 3 conversations'), ChatMemory('Medium', 6, description: 'Remembers the last 6 conversations'), ChatMemory('Deep', 10, description: 'Remembers the last 10 conversations') ]; bool showAdvancedOptions = false; mm.Model? _selectedModel; List tags = []; @override void initState() { super.initState(); if (Ability().isUserLogon()) { APIServer().avatars().then((value) { avatarPresets = value; }); } } @override void dispose() { _nameController.dispose(); _promptController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( title: Text( AppLocale.createRoom.getString(context), style: const TextStyle(fontSize: CustomSize.appBarTitleSize), ), backgroundColor: customColors.backgroundColor, centerTitle: true, toolbarHeight: CustomSize.toolbarHeight, ), backgroundColor: customColors.backgroundColor, body: BackgroundContainer( setting: widget.setting, enabled: false, maxWidth: CustomSize.maxWindowSize, backgroundColor: customColors.backgroundColor, child: BlocListener( listenWhen: (previous, current) => current is RoomOperationResult, listener: (context, state) { if (state is RoomOperationResult) { if (state.success) { if (state.redirect != null) { context.push(state.redirect!).then((value) { if (context.mounted) { context.read().add(RoomsLoadEvent()); } }); } else { context.pop(); } } else { showErrorMessageEnhanced(context, state.error ?? AppLocale.operateFailed.getString(context)); } } }, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: buildCustomCharacter(customColors, context), ), ), ), ), ); } Widget buildCustomCharacter(CustomColors customColors, BuildContext context) { return SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 10), ColumnBlock( children: [ // 名称 EnhancedTextField( customColors: customColors, controller: _nameController, maxLength: 50, maxLines: 1, showCounter: false, labelText: AppLocale.roomName.getString(context), labelPosition: LabelPosition.left, hintText: AppLocale.required.getString(context), textDirection: TextDirection.rtl, ), if (Ability().isUserLogon()) EnhancedInput( padding: const EdgeInsets.only(top: 10, bottom: 5), title: Text( AppLocale.avatar.getString(context), style: TextStyle( color: customColors.textfieldLabelColor, fontSize: 16, ), ), value: Row( mainAxisAlignment: MainAxisAlignment.end, mainAxisSize: MainAxisSize.min, children: [ Container( width: 45, height: 45, decoration: BoxDecoration( borderRadius: CustomSize.borderRadius, image: _avatarUrl == null ? null : DecorationImage( image: (_avatarUrl!.startsWith('http') ? CachedNetworkImageProviderEnhanced(_avatarUrl!) : FileImage(File(_avatarUrl!))) as ImageProvider, fit: BoxFit.cover, ), ), child: _avatarUrl == null && _avatarId == null ? const Center( child: Icon( Icons.interests, color: Colors.grey, ), ) : (_avatarId == null ? const SizedBox() : RandomAvatar( id: _avatarId!, usage: AvatarUsage.room, )), ), ], ), onPressed: () { openModalBottomSheet( context, (context) { return AvatarSelector( onSelected: (selected) { setState(() { _avatarUrl = selected.url; _avatarId = selected.id; }); context.pop(); }, usage: AvatarUsage.room, defaultAvatarId: _avatarId, defaultAvatarUrl: _avatarUrl, externalAvatarUrls: [ ...avatarPresets, ], ); }, heightFactor: 0.8, ); }, ), ], ), ColumnBlock( innerPanding: 10, padding: const EdgeInsets.only(top: 15, left: 15, right: 15), children: [ // 提示语 EnhancedTextField( fontSize: 12, customColors: customColors, controller: _promptController, labelText: AppLocale.prompt.getString(context), labelPosition: LabelPosition.top, hintText: AppLocale.promptHint.getString(context), bottomButton: Row( children: [ Icon( Icons.tips_and_updates_outlined, size: 13, color: customColors.linkColor?.withAlpha(150), ), const SizedBox(width: 5), Text( AppLocale.examples.getString(context), style: TextStyle( color: customColors.linkColor?.withAlpha(150), fontSize: 13, ), ), ], ), bottomButtonOnPressed: () async { openSystemPromptSelectDialog( context, customColors, _promptController, ); }, minLines: 4, maxLines: 20, showCounter: false, ), ], ), if (showAdvancedOptions) ColumnBlock( innerPanding: 10, padding: const EdgeInsets.only(top: 15, left: 15, right: 15, bottom: 5), children: [ // 模型 EnhancedInputSimple( title: AppLocale.model.getString(context), padding: const EdgeInsets.only(top: 10, bottom: 10), onPressed: () { openSelectModelDialog( context, (selected) { setState(() { _selectedModel = selected; }); }, initValue: _selectedModel?.uid(), ); }, value: _selectedModel != null ? _selectedModel!.name : AppLocale.select.getString(context), ), EnhancedInput( title: Text( AppLocale.memoryDepth.getString(context), style: TextStyle( color: customColors.textfieldLabelColor, fontSize: 16, ), ), value: Text( validMemories.where((element) => element.number == maxContext).firstOrNull?.name ?? '', ), onPressed: () { openListSelectDialog( context, validMemories .map( (e) => SelectorItem( Column( children: [ Text( e.name, textAlign: TextAlign.center, ), const SizedBox(height: 10), Text( e.description ?? '', textAlign: TextAlign.center, style: TextStyle( color: customColors.weakTextColor, fontSize: 12, ), ), ], ), e.number, ), ) .toList(), (value) { setState(() { maxContext = value.value; }); return true; }, heightFactor: 0.5, value: validMemories.where((element) => element.number == maxContext).firstOrNull, ); }, ), ], ), AdvancedButton( showAdvancedOptions: showAdvancedOptions, onPressed: (value) { setState(() { showAdvancedOptions = value; }); }, ), const SizedBox(height: 10), EnhancedButton( title: AppLocale.ok.getString(context), onPressed: () async { if (_nameController.text == '') { showErrorMessage(AppLocale.nameRequiredMessage.getString(context)); return; } if (_promptController.text == '') { showErrorMessage(AppLocale.charactorPromptRequiredMessage.getString(context)); return; } if (_avatarUrl != null) { if (!(_avatarUrl!.startsWith('http://') || _avatarUrl!.startsWith('https://'))) { // 上传文件,获取 URL final cancel = BotToast.showCustomLoading( toastBuilder: (cancel) { return LoadingIndicator( message: AppLocale.imageUploading.getString(context), ); }, allowClick: false, ); final uploadRes = await ImageUploader(widget.setting) .upload(_avatarUrl!, usage: 'avatar') .whenComplete(() => cancel()); _avatarUrl = uploadRes.url; } } if (context.mounted) { context.read().add( RoomCreateEvent( _nameController.text, _promptController.text, model: _selectedModel?.uid(), avatarId: _avatarId, avatarUrl: _avatarUrl, maxContext: maxContext, ), ); } }, ), const SizedBox(height: 15), ], ), ); } } void openSelectModelDialog( BuildContext context, Function(mm.Model? selected) onSelected, { String? initValue, List? reservedModels, String? title, String? priorityModelId, bool withCustom = false, }) { future() async { final models = await ModelAggregate.models(cache: true); if (priorityModelId != null) { // 将 models 中,id 与 priorityModelId 相同的元素排序到最前面 final index = models.indexWhere((e) => e.id == priorityModelId || e.uid() == priorityModelId); if (index != -1) { models.insert( 0, models[index] // ignore: use_build_context_synchronously .copyWith(category: AppLocale.recentlyUsed.getString(context))); } } // 再请求一次,用于异步更新 Cache,下次打开时将显示最新数据 ModelAggregate.models(cache: false); return models; } openModalBottomSheet( context, (context) { return FutureBuilder( future: future(), builder: (context, snapshot) { if (snapshot.hasError) { showErrorMessage(resolveError(context, snapshot.error!)); } if (!snapshot.hasData) { return const Center(child: CircularProgressIndicator()); } return ModelItem( models: snapshot.data! .where((e) => !e.disabled || (reservedModels != null && reservedModels.contains(e.id))) .toList(), onSelected: (selected) { onSelected(selected); context.pop(); }, initValue: initValue, ); }); }, heightFactor: 0.9, title: title, ); } void openSystemPromptSelectDialog( BuildContext context, CustomColors customColors, TextEditingController promptController, ) { openModalBottomSheet( context, (context) { return FutureBuilder( future: APIServer().prompts(), builder: (context, snapshot) { if (snapshot.hasError) { showErrorMessage(resolveError(context, snapshot.error!)); } return ItemSearchSelector( items: (snapshot.data ?? []) .map( (e) => SelectorItem( Column( mainAxisSize: MainAxisSize.min, children: [ Text( e.title, textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, style: TextStyle( color: customColors.chatExampleItemText, ), ), Text( e.content, textAlign: TextAlign.left, overflow: TextOverflow.ellipsis, maxLines: 2, style: TextStyle( color: customColors.weakTextColor, ), textScaler: const TextScaler.linear(0.8), ) ], ), e.content, search: (keywrod) => e.title.toLowerCase().contains(keywrod.toLowerCase()), ), ) .toList(), onSelected: (value) { promptController.text = value.value; return true; }, ); }, ); }, heightFactor: 0.9, ); } class ChatMemory { String name; String? description; int number; ChatMemory(this.name, this.number, {this.description}); } ================================================ FILE: lib/page/chat/character_edit.dart ================================================ import 'dart:io'; import 'dart:math'; import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/model.dart'; import 'package:askaide/helper/upload.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/chat/character_create.dart'; import 'package:askaide/page/component/advanced_button.dart'; import 'package:askaide/page/component/avatar_selector.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/column_block.dart'; import 'package:askaide/page/component/enhanced_button.dart'; import 'package:askaide/page/component/enhanced_input.dart'; import 'package:askaide/page/component/enhanced_textfield.dart'; import 'package:askaide/page/component/image.dart'; import 'package:askaide/page/component/item_selector_search.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/random_avatar.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/bloc/room_bloc.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/model/model.dart' as mm; import 'package:askaide/repo/settings_repo.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; class CharacterEditPage extends StatefulWidget { final int roomId; final SettingRepository setting; const CharacterEditPage({super.key, required this.roomId, required this.setting}); @override State createState() => _CharacterEditPageState(); } class _CharacterEditPageState extends State { final _nameController = TextEditingController(); final _promptController = TextEditingController(text: ''); final randomSeed = Random().nextInt(10000); String? _originalAvatarUrl; int? _originalAvatarId; String? _avatarUrl; int? _avatarId; List avatarPresets = []; int maxContext = 5; List validMemories = [ ChatMemory('Ephemeral', 1, description: 'Each conversation is independent, often used for one-off Q&A'), ChatMemory('Basic', 3, description: 'Remembers the last 3 conversations'), ChatMemory('Medium', 6, description: 'Remembers the last 6 conversations'), ChatMemory('Deep', 10, description: 'Remembers the last 10 conversations') ]; bool showAdvancedOptions = false; mm.Model? _selectedModel; String? reservedModel; @override void initState() { super.initState(); BlocProvider.of(context).add(RoomLoadEvent(widget.roomId, cascading: false)); // 获取预设头像 if (Ability().isUserLogon()) { APIServer().avatars().then((value) { avatarPresets = value; }); } } @override void dispose() { _nameController.dispose(); _promptController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( title: Text( AppLocale.configure.getString(context), style: const TextStyle(fontSize: CustomSize.appBarTitleSize), ), centerTitle: true, elevation: 0, toolbarHeight: CustomSize.toolbarHeight, ), backgroundColor: customColors.backgroundColor, body: BackgroundContainer( setting: widget.setting, enabled: false, backgroundColor: customColors.backgroundColor, child: BlocConsumer( listener: (context, state) { if (state is RoomLoaded) { _nameController.text = state.room.name; _promptController.text = state.room.systemPrompt ?? ''; maxContext = state.room.maxContext; ModelAggregate.model(state.room.model).then((value) { setState(() { _selectedModel = value; reservedModel = value.id; }); }); if (state.room.avatarUrl != null && state.room.avatarUrl != '') { setState(() { _avatarUrl = state.room.avatarUrl; _avatarId = null; _originalAvatarUrl = state.room.avatarUrl; _originalAvatarId = null; }); } else if (state.room.avatarId != null && state.room.avatarId != 0) { setState(() { _avatarId = state.room.avatarId; _avatarUrl = null; _originalAvatarId = state.room.avatarId; _originalAvatarUrl = null; }); } else { setState(() { _avatarId = null; _avatarUrl = null; _originalAvatarId = state.room.id; _originalAvatarUrl = null; }); } } if (state is RoomOperationResult) { if (state.success) { if (state.redirect != null) { context.push(state.redirect!).then((value) { context.read().add(RoomsLoadEvent()); }); } } else { showErrorMessageEnhanced(context, state.error ?? AppLocale.operateFailed.getString(context)); } } }, buildWhen: (previous, current) => current is RoomLoaded, builder: (context, state) { if (state is RoomLoaded) { return SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(10.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 10), // 名称 if (state.room.category != 'system') ColumnBlock( children: [ EnhancedTextField( customColors: customColors, controller: _nameController, maxLength: 50, maxLines: 1, showCounter: false, labelText: AppLocale.roomName.getString(context), labelPosition: LabelPosition.left, hintText: AppLocale.required.getString(context), textDirection: TextDirection.rtl, ), if (Ability().isUserLogon()) EnhancedInput( padding: const EdgeInsets.only(top: 10, bottom: 5), title: Text( AppLocale.avatar.getString(context), style: TextStyle( color: customColors.textfieldLabelColor, fontSize: 16, ), ), value: Row( mainAxisAlignment: MainAxisAlignment.end, mainAxisSize: MainAxisSize.min, children: [ Container( width: 45, height: 45, decoration: BoxDecoration( borderRadius: CustomSize.borderRadius, image: _avatarUrl == null ? null : DecorationImage( image: (_avatarUrl!.startsWith('http') ? CachedNetworkImageProviderEnhanced(_avatarUrl!) : FileImage(File(_avatarUrl!))) as ImageProvider, fit: BoxFit.cover, ), ), child: _avatarUrl == null && _avatarId == null ? const Center( child: Icon( Icons.interests, color: Colors.grey, ), ) : (_avatarId == null ? const SizedBox() : RandomAvatar( id: _avatarId!, usage: AvatarUsage.room, )), ), ], ), onPressed: () { openModalBottomSheet( context, (context) { return AvatarSelector( onSelected: (selected) { setState(() { _avatarUrl = selected.url; _avatarId = selected.id; }); context.pop(); }, usage: AvatarUsage.room, defaultAvatarId: _avatarId, defaultAvatarUrl: _avatarUrl, externalAvatarIds: _originalAvatarId == null ? [] : [_originalAvatarId!], externalAvatarUrls: _originalAvatarUrl == null ? [...avatarPresets] : [_originalAvatarUrl!, ...avatarPresets], ); }, heightFactor: 0.8, ); }, ), ], ), ColumnBlock( innerPanding: 10, padding: const EdgeInsets.only(top: 15, left: 15, right: 15), children: [ // 提示语 EnhancedTextField( fontSize: 12, customColors: customColors, controller: _promptController, labelText: AppLocale.prompt.getString(context), labelPosition: LabelPosition.top, hintText: AppLocale.promptHint.getString(context), bottomButton: Row( children: [ Icon( Icons.tips_and_updates_outlined, size: 13, color: customColors.linkColor?.withAlpha(150), ), const SizedBox(width: 5), Text( AppLocale.examples.getString(context), style: TextStyle( color: customColors.linkColor?.withAlpha(150), fontSize: 13, ), ), ], ), bottomButtonOnPressed: () async { openSystemPromptSelectDialog( context, customColors, _promptController, ); }, minLines: 4, maxLines: 20, showCounter: false, ), ], ), if (showAdvancedOptions) ColumnBlock( innerPanding: 10, padding: const EdgeInsets.only(top: 15, left: 15, right: 15, bottom: 0), children: [ // 模型 EnhancedInputSimple( title: AppLocale.model.getString(context), padding: const EdgeInsets.only(top: 10, bottom: 0), onPressed: () { openSelectModelDialog( context, (selected) { setState(() { _selectedModel = selected; }); }, initValue: _selectedModel?.uid(), reservedModels: reservedModel != null ? [reservedModel!] : [], ); }, value: _selectedModel != null ? _selectedModel!.name : AppLocale.select.getString(context), ), EnhancedInput( title: Text( AppLocale.memoryDepth.getString(context), style: TextStyle( color: customColors.textfieldLabelColor, fontSize: 16, ), ), value: Text( validMemories.where((element) => element.number == maxContext).firstOrNull?.name ?? '', ), onPressed: () { openListSelectDialog( context, validMemories .map( (e) => SelectorItem( Column( children: [ Text( e.name, textAlign: TextAlign.center, ), const SizedBox(height: 10), Text( e.description ?? '', textAlign: TextAlign.center, style: TextStyle( color: customColors.weakTextColor, fontSize: 12, ), ), ], ), e.number, ), ) .toList(), (value) { setState(() { maxContext = value.value; }); return true; }, heightFactor: 0.5, value: validMemories.where((element) => element.number == maxContext).firstOrNull, ); }, ), ], ), AdvancedButton( showAdvancedOptions: showAdvancedOptions, onPressed: (value) { setState(() { showAdvancedOptions = value; }); }, ), const SizedBox(height: 10), EnhancedButton( title: AppLocale.save.getString(context), onPressed: () async { if (_nameController.text == '') { showErrorMessage(AppLocale.nameRequiredMessage.getString(context)); return; } if (_promptController.text == '') { showErrorMessage(AppLocale.charactorPromptRequiredMessage.getString(context)); return; } if (_avatarUrl != null) { if (!(_avatarUrl!.startsWith('http://') || _avatarUrl!.startsWith('https://'))) { // 上传文件,获取 URL final cancel = BotToast.showCustomLoading( toastBuilder: (cancel) { return LoadingIndicator( message: AppLocale.imageUploading.getString(context), ); }, allowClick: false, ); final uploadRes = await ImageUploader(widget.setting) .upload(_avatarUrl!, usage: 'avatar') .whenComplete(() => cancel()); _avatarUrl = uploadRes.url; } } if (context.mounted) { context.read().add( RoomUpdateEvent( widget.roomId, name: _nameController.text, model: _selectedModel?.uid(), prompt: _promptController.text, avatarUrl: _avatarUrl, avatarId: _avatarId, maxContext: maxContext, ), ); showSuccessMessage(AppLocale.operateSuccess.getString(context)); } }, ), ], ), ), ); } return const Center( child: CircularProgressIndicator(), ); }, ), ), ), ); } } ================================================ FILE: lib/page/chat/characters.dart ================================================ import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/event.dart'; import 'package:askaide/helper/haptic_feedback.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/bloc/room_bloc.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/enhanced_button.dart'; import 'package:askaide/page/component/enhanced_error.dart'; import 'package:askaide/page/component/global_alert.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/weak_text_button.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/chat/component/character_box.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/api/room_gallery.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; class CharactersPage extends StatefulWidget { final SettingRepository setting; const CharactersPage({Key? key, required this.setting}) : super(key: key); @override State createState() => _CharactersPageState(); } class _CharactersPageState extends State { @override void initState() { context.read().add(RoomsLoadEvent()); super.initState(); } List selectedSuggestions = []; @override Widget build(BuildContext context) { var customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( title: Text( AppLocale.homeTitle.getString(context), style: const TextStyle(fontSize: CustomSize.appBarTitleSize), ), centerTitle: true, toolbarHeight: CustomSize.toolbarHeight, actions: [ IconButton( icon: const Icon(Icons.add_circle_outline), onPressed: () { context.push('/create-room').whenComplete(() { if (context.mounted) { context.read().add(RoomsLoadEvent()); } }); }, ), ], ), backgroundColor: customColors.backgroundColor, body: BackgroundContainer( setting: widget.setting, backgroundColor: customColors.backgroundColor, enabled: false, child: SafeArea( top: false, left: false, right: false, child: BlocConsumer( listener: (context, state) { if (state is RoomsLoaded) { if (state.rooms.isNotEmpty) { selectedSuggestions.clear(); setState(() {}); GlobalEvent().emit('showBottomNavigatorBar'); } } if (state is RoomCreateError) { showErrorMessageEnhanced(context, state.error); } if (state is RoomOperationResult) { if (!state.success) { showErrorMessageEnhanced(context, state.error ?? AppLocale.operateFailed.getString(context)); } else { if (state.redirect != null) { context.push(state.redirect!); } } } }, buildWhen: (previous, current) => current is RoomsLoading || current is RoomsLoaded, builder: (context, state) { if (state is RoomsLoaded) { if (state.error != null) { return EnhancedErrorWidget(error: state.error); } return Column( children: [ Expanded( child: RefreshIndicator( color: customColors.linkColor, onRefresh: () async { context.read().add(RoomsLoadEvent(forceRefresh: true)); }, displacement: 20, child: Column( children: [ if (Ability().showGlobalAlert) const GlobalAlert(pageKey: 'rooms'), Expanded(child: buildBody(customColors, state, context)), ], ), ), ), if (selectedSuggestions.isNotEmpty) Container( height: 70, width: double.infinity, padding: const EdgeInsets.symmetric( horizontal: 20, vertical: 10, ), child: Row( children: [ WeakTextButton( title: AppLocale.cancel.getString(context), onPressed: () { selectedSuggestions.clear(); setState(() {}); GlobalEvent().emit('showBottomNavigatorBar'); }, ), const SizedBox(width: 20), Expanded( child: EnhancedButton( title: AppLocale.ok.getString(context), onPressed: () { context .read() .add(GalleryRoomCopyEvent(selectedSuggestions.map((e) => e.id).toList())); showSuccessMessage(AppLocale.operateSuccess.getString(context)); }), ) ], ), ), ], ); } return const Center( child: LoadingIndicator(), ); }, ), ), ), ), ); } Widget buildColumnTitle(BuildContext context, CustomColors customColors, String title) { return Container( padding: const EdgeInsets.only(left: 5), margin: const EdgeInsets.only( left: 10, right: 10, ), child: Text(title, style: const TextStyle(fontSize: 16)), ); } Widget buildBody(CustomColors customColors, RoomsLoaded state, BuildContext context) { List children = []; if (state.rooms.isNotEmpty) { children.addAll([ buildColumnTitle(context, customColors, AppLocale.myCharacters.getString(context)), ListView.builder( padding: const EdgeInsets.all(5), itemCount: state.rooms.length, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) { final room = state.rooms[index]; return CharacterBox(room: room); }, ), ]); } if (state.suggests.isNotEmpty) { children.addAll([ const SizedBox(height: 10), buildColumnTitle(context, customColors, AppLocale.robotRecommand.getString(context)), ListView.builder( padding: const EdgeInsets.all(5), itemCount: state.suggests.length, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) { return Container( margin: const EdgeInsets.symmetric( horizontal: 10, vertical: 5, ), decoration: BoxDecoration(borderRadius: CustomSize.borderRadius), child: CharacterBoxItem( onTap: () { HapticFeedbackHelper.lightImpact(); context.read().add(GalleryRoomCopyEvent([state.suggests[index].id])); }, name: state.suggests[index].name, desc: state.suggests[index].description, avatarUrl: state.suggests[index].avatarUrl, ), ); }, ), ]); } return SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: children, ), ); } void onItemSelected(RoomGallery item) { if (selectedSuggestions.contains(item)) { selectedSuggestions.remove(item); } else { selectedSuggestions.add(item); } setState(() {}); if (selectedSuggestions.isEmpty) { GlobalEvent().emit('showBottomNavigatorBar'); } else { GlobalEvent().emit('hideBottomNavigatorBar'); } } } ================================================ FILE: lib/page/chat/component/character_box.dart ================================================ import 'package:askaide/bloc/room_bloc.dart'; import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/haptic_feedback.dart'; import 'package:askaide/helper/image.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/image.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/model/room.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_initicon/flutter_initicon.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:go_router/go_router.dart'; class CharacterBox extends StatelessWidget { final Room room; const CharacterBox({super.key, required this.room}); @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return Container( margin: const EdgeInsets.symmetric( horizontal: 10, vertical: 5, ), decoration: BoxDecoration(borderRadius: CustomSize.borderRadius), child: Slidable( endActionPane: ActionPane( motion: const ScrollMotion(), children: [ const SizedBox(width: 10), SlidableAction( label: AppLocale.configure.getString(context), backgroundColor: Colors.green, borderRadius: room.category == 'system' ? CustomSize.borderRadiusAll : const BorderRadius.only(topLeft: CustomSize.radius, bottomLeft: CustomSize.radius), icon: Icons.edit, onPressed: (_) { final chatRoomBloc = context.read(); final redirectUrl = room.roomType == 4 ? '/group-chat/${room.id}/edit' : '/room/${room.id}/setting'; context.push(redirectUrl).then((value) { chatRoomBloc.add(RoomsLoadEvent()); }); }, ), if (room.category != 'system') SlidableAction( label: AppLocale.delete.getString(context), borderRadius: const BorderRadius.only(topRight: CustomSize.radius, bottomRight: CustomSize.radius), backgroundColor: Colors.red, icon: Icons.delete, onPressed: (_) { openConfirmDialog( context, AppLocale.confirmToDeleteRoom.getString(context), () => context.read().add(RoomDeleteEvent(room.id!)), danger: true, ); }, ), ], ), child: buildItem(customColors, context), ), ); } Widget buildItem(CustomColors customColors, BuildContext context) { return CharacterBoxItem( onTap: () { HapticFeedbackHelper.lightImpact(); final chatRoomBloc = context.read(); context.push('/room/${room.id}/chat').then((value) { chatRoomBloc.add(RoomsLoadEvent(forceRefresh: true)); }); }, name: room.name, desc: room.description ?? room.systemPrompt, model: room.model, avatarUrl: room.avatarUrl, ); } } class CharacterBoxItem extends StatelessWidget { final Function() onTap; final String? avatarUrl; final String name; final String? model; final String? desc; const CharacterBoxItem({ super.key, required this.onTap, required this.name, this.avatarUrl, this.model, this.desc, }); @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return Container( decoration: BoxDecoration( borderRadius: CustomSize.borderRadius, color: customColors.listTileBackgroundColor, ), child: InkWell( borderRadius: CustomSize.borderRadiusAll, onTap: onTap, overlayColor: WidgetStateProperty.all(Colors.transparent), child: Stack( children: [ Row( mainAxisSize: MainAxisSize.min, children: [ buildAvatar(), Expanded( child: Container( padding: const EdgeInsets.symmetric(horizontal: 10), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( name, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 5), buildRoomDesc(customColors), ], ), ), ), ], ), if (model != null && Ability().usingLocalOpenAIModel(model!)) Positioned( right: 0, top: 0, child: Container( decoration: BoxDecoration( color: customColors.backgroundContainerColor, borderRadius: const BorderRadius.only(topRight: CustomSize.radius, bottomLeft: CustomSize.radius), ), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), child: Text( 'local', style: TextStyle( color: customColors.weakTextColor, fontSize: 8, ), ), ), ), ], ), ), ); } Widget buildRoomDesc(CustomColors customColors) { if (desc != null && desc != '') { return Text( desc!, style: TextStyle( color: customColors.weakLinkColor?.withAlpha(150), fontSize: 13, ), maxLines: 1, overflow: TextOverflow.ellipsis, ); } return const SizedBox(); } Widget buildAvatar() { if (avatarUrl != null && avatarUrl!.startsWith('http')) { return SizedBox( width: 70, height: 70, child: ClipRRect( borderRadius: const BorderRadius.only(topLeft: CustomSize.radius, bottomLeft: CustomSize.radius), child: CachedNetworkImageEnhanced( imageUrl: imageURL(avatarUrl!, qiniuImageTypeAvatar), fit: BoxFit.fill, ), ), ); } return Initicon( text: name.split('、').join(' '), size: 70, backgroundColor: Colors.grey.withAlpha(100), borderRadius: const BorderRadius.only(topLeft: CustomSize.radius, bottomLeft: CustomSize.radius), ); } } ================================================ FILE: lib/page/chat/component/group_avatar.dart ================================================ import 'package:askaide/helper/image.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; // ignore: must_be_immutable class GroupAvatar extends StatelessWidget { final double size; final double padding; final double margin; final List avatars; final Color? backgroundColor; var row = 0, column = 0; GroupAvatar({ super.key, this.size = 40, this.padding = 2, this.margin = 3, required this.avatars, this.backgroundColor, }); @override Widget build(BuildContext context) { final avatar = buildAvatar(context); return Container( padding: const EdgeInsets.all(4), width: size, height: size, color: backgroundColor ?? Colors.grey.withAlpha(100), child: avatar, ); } double get innerSize => size - 8; Widget buildAvatar(BuildContext context) { var childCount = avatars.length; int columnMax; List icons = []; List stacks = []; // 五张图片之后(包含5张),每行的最大列数是3 double imgWidth; if (childCount < 2) { return Container( width: innerSize, height: innerSize, color: Colors.transparent, ); } if (childCount >= 5) { columnMax = 3; imgWidth = (innerSize - (padding * columnMax) - margin) / columnMax; } else { columnMax = 2; imgWidth = (innerSize - (padding * columnMax) - margin) / columnMax; } for (var i = 0; i < childCount; i++) { icons.add(_weChatGroupChatChildIcon(avatars[i], imgWidth)); } row = 0; column = 0; var centerTop = 0.0; if (childCount == 2 || childCount == 5 || childCount == 6) { centerTop = imgWidth / 2; } for (var i = 0; i < childCount; i++) { var left = imgWidth * row + padding * (row + 1); var top = imgWidth * column + margin * column + centerTop; switch (childCount) { case 3: case 7: _topOneIcon(stacks, icons[i], childCount, i, imgWidth, left, top); break; case 5: case 8: _topTwoIcon(stacks, icons[i], childCount, i, imgWidth, left, top); break; default: _otherIcon( stacks, icons[i], childCount, i, imgWidth, left, top, columnMax); break; } } return Container( width: innerSize, height: innerSize, color: Colors.transparent, padding: EdgeInsets.only(top: padding), alignment: AlignmentDirectional.bottomCenter, child: Stack( children: stacks, ), ); } _weChatGroupChatChildIcon(String avatar, double width) { return ClipRRect( borderRadius: BorderRadius.circular(2), child: CachedNetworkImage( imageUrl: imageURL(avatar, 'avatar'), height: width, width: width, fit: BoxFit.fill, ), ); } // 顶部为一张图片 _topOneIcon(List stacks, Widget child, int childCount, i, imgWidth, left, top) { if (i == 0) { var firstLeft = imgWidth / 2 + left + margin / 2; if (childCount == 7) { firstLeft = imgWidth + left + margin; } stacks.add(Positioned( left: firstLeft, child: child, )); row = 0; // 换行 column++; } else { stacks.add(Positioned( left: left, top: top, child: child, )); // 换列 row++; if (i == 3) { // 第一例 row = 0; // 换行 column++; } } } // 顶部为两张图片 _topTwoIcon(List stacks, Widget child, int childCount, i, imgWidth, left, top) { if (i == 0 || i == 1) { stacks.add(Positioned( left: imgWidth / 2 + left + margin / 2, top: childCount == 5 ? top : 0.0, child: child, )); row++; if (i == 1) { row = 0; // 换行 column++; } } else { stacks.add(Positioned( left: left, top: top, child: child, )); // 换列 row++; if (i == 4) { // 第一例 row = 0; // 换行 column++; } } } _otherIcon(List stacks, Widget child, int childCount, i, imgWidth, left, top, columnMax) { stacks.add(Positioned( left: left, top: top, child: child, )); // 换列 row++; if ((i + 1) % columnMax == 0) { // 第一例 row = 0; // 换行 column++; } } } ================================================ FILE: lib/page/chat/component/group_empty.dart ================================================ import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; class GroupEmptyBoard extends StatelessWidget { const GroupEmptyBoard({super.key}); @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return Center( child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ const SizedBox(height: 30), Container( decoration: BoxDecoration( color: customColors.backgroundColor?.withAlpha(200), borderRadius: CustomSize.borderRadius, ), padding: const EdgeInsets.only(top: 20, left: 15, right: 10, bottom: 3), width: _resolveTipWidth(context), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Image.asset('assets/app-256-transparent.png', width: 20, height: 20), const SizedBox(width: 5), const Text( '小提示', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), ], ), const SizedBox(height: 20), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ buildTextLine( customColors, "点击 @ 按钮,快速指定应答成员", Icons.touch_app, ), buildTextLine( customColors, '未选择成员时,系统将随机指派', Icons.shuffle, ), buildTextLine( customColors, '系统会记住上次使用的成员', Icons.memory, ), ], ), const SizedBox(height: 20), ], ), ), ], ), ); } Widget buildTextLine(CustomColors customColors, String text, IconData? icon) { return Padding( padding: const EdgeInsets.only(bottom: 10, left: 15), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon( icon, size: 14, color: customColors.chatExampleItemText?.withAlpha(120), ), const SizedBox(width: 5), Expanded( child: Text( text, maxLines: 1, style: TextStyle( color: customColors.weakTextColor, height: 1.5, fontSize: 14, overflow: TextOverflow.ellipsis, ), ), ) ], ), ); } double _resolveTipWidth(BuildContext context) { final screenWidth = MediaQuery.of(context).size.width; if (screenWidth < 400) { return screenWidth / 1.15; } return 400; } } ================================================ FILE: lib/page/chat/component/model_switcher.dart ================================================ import 'package:askaide/helper/haptic_feedback.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/chat/character_create.dart'; import 'package:askaide/page/component/random_avatar.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; import 'package:askaide/repo/model/model.dart' as mm; import 'package:flutter_initicon/flutter_initicon.dart'; import 'package:flutter_localization/flutter_localization.dart'; class ModelSwitcher extends StatelessWidget { final mm.Model? value; final Function(mm.Model? selected) onSelected; const ModelSwitcher({ super.key, required this.onSelected, this.value, }); static void openActionDialog({ required BuildContext context, required Function(mm.Model? selected) onSelected, mm.Model? initValue, }) { HapticFeedbackHelper.mediumImpact(); openSelectModelDialog( context, (selected) { onSelected(selected); }, initValue: initValue?.uid(), withCustom: true, ); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return IconButton( onPressed: () async { openActionDialog( context: context, onSelected: onSelected, initValue: value, ); }, icon: value == null ? const Icon(Icons.alternate_email_outlined) // Icons.theater_comedy_outlined // Icons.model_training_outlined // Icons.switch_access_shortcut_outlined // Icons.assistant_outlined : value!.avatarUrl == null ? Initicon( text: value!.name.split('、').join(' '), size: 25, backgroundColor: Colors.grey.withAlpha(100), borderRadius: BorderRadius.circular(100), ) : RemoteAvatar( avatarUrl: value!.avatarUrl!, size: 25, radius: 100, ), color: customColors.chatInputPanelText, splashRadius: 20, tooltip: AppLocale.switchModel.getString(context), ); } } ================================================ FILE: lib/page/chat/component/stop_button.dart ================================================ import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; class StopButton extends StatelessWidget { final Function()? onPressed; final String label; const StopButton({super.key, this.onPressed, required this.label}); @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return TextButton.icon( style: ButtonStyle( tapTargetSize: MaterialTapTargetSize.shrinkWrap, iconColor: const WidgetStatePropertyAll(Colors.red), backgroundColor: WidgetStatePropertyAll(customColors.backgroundContainerColor), ), label: Text( label, style: TextStyle( fontSize: 12, color: customColors.textfieldLabelColor, ), ), icon: const Icon( Icons.stop_circle_outlined, size: 13, ), onPressed: onPressed, ); } } ================================================ FILE: lib/page/chat/group/chat.dart ================================================ import 'dart:async'; import 'package:askaide/bloc/group_chat_bloc.dart'; import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/haptic_feedback.dart'; import 'package:askaide/helper/image.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/chat/component/group_empty.dart'; import 'package:askaide/page/component/audio_player.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/chat/chat_share.dart'; import 'package:askaide/page/component/chat/help_tips.dart'; import 'package:askaide/page/component/chat/message_state_manager.dart'; import 'package:askaide/page/component/enhanced_popup_menu.dart'; import 'package:askaide/page/component/global_alert.dart'; import 'package:askaide/page/component/multi_item_selector.dart'; import 'package:askaide/page/component/random_avatar.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/chat/chat_input.dart'; import 'package:askaide/page/component/chat/chat_preview.dart'; import 'package:askaide/repo/model/group.dart'; import 'package:askaide/repo/model/message.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; class GroupChatPage extends StatefulWidget { final SettingRepository setting; final int groupId; final MessageStateManager stateManager; const GroupChatPage({ super.key, required this.setting, required this.groupId, required this.stateManager, }); @override State createState() => _GroupChatPageState(); } class _GroupChatPageState extends State { final ScrollController _scrollController = ScrollController(); final ValueNotifier _inputEnabled = ValueNotifier(true); final ChatPreviewController _chatPreviewController = ChatPreviewController(); final AudioPlayerController _audioPlayerController = AudioPlayerController(useRemoteAPI: true); bool showAudioPlayer = false; bool audioLoadding = false; List? selectedMembers = []; List messages = []; ChatGroup? group; Timer? timer; @override void initState() { super.initState(); context.read().add(GroupChatLoadEvent(widget.groupId)); _chatPreviewController.addListener(() { setState(() {}); }); _audioPlayerController.onPlayStopped = () { setState(() { showAudioPlayer = false; }); }; _audioPlayerController.onPlayAudioStarted = () { setState(() { showAudioPlayer = true; }); }; _audioPlayerController.onPlayAudioLoading = (loading) { setState(() { audioLoadding = loading; }); }; } @override void dispose() { timer?.cancel(); _scrollController.dispose(); _chatPreviewController.dispose(); _audioPlayerController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return BackgroundContainer( setting: widget.setting, child: Scaffold( appBar: _buildAppBar(context, customColors), backgroundColor: Colors.transparent, body: _buildChatComponents(customColors), ), ); } Widget _buildChatComponents(CustomColors customColors) { return BlocConsumer( listenWhen: (previous, current) => current is GroupChatLoaded || current is GroupDefaultMemberSelected, listener: (context, state) { if (state is GroupChatLoaded) { // 加载聊天记录列表 context.read().add(GroupChatMessagesLoadEvent(widget.groupId, isInitRequest: true)); // 选中默认的聊天成员 selectedMembers = state.group.members.where((e) => state.defaultChatMembers?.contains(e.id) ?? false).toList(); setState(() { group = state.group; }); } if (state is GroupDefaultMemberSelected) { // 选中默认的聊天成员 if (group != null) { selectedMembers = group?.members.where((e) => state.members.contains(e.id)).toList(); } } }, buildWhen: (previous, current) => current is GroupChatLoaded, builder: (context, groupState) { if (groupState is GroupChatLoaded) { return SafeArea( top: false, bottom: false, child: Column( children: [ if (Ability().showGlobalAlert) const GlobalAlert(pageKey: 'chat'), // 语音输出中提示 if (showAudioPlayer) EnhancedAudioPlayer( controller: _audioPlayerController, loading: audioLoadding, ), // 聊天内容窗口 Expanded( child: _buildChatPreviewArea( groupState, customColors, _chatPreviewController.selectMode, ), ), // 聊天输入窗口 Container( decoration: BoxDecoration( borderRadius: const BorderRadius.only(topLeft: CustomSize.radius, topRight: CustomSize.radius), color: customColors.chatInputPanelBackground, ), child: SafeArea( child: _chatPreviewController.selectMode ? buildSelectModeToolbars( context, _chatPreviewController, customColors, ) : ChatInput( enableNotifier: _inputEnabled, enableImageUpload: false, onSubmit: (value) { _handleSubmit(value); FocusManager.instance.primaryFocus?.unfocus(); }, onNewChat: () => handleResetContext(context), hintText: AppLocale.askMeAnyQuestion.getString(context), onVoiceRecordTappedEvent: () { _audioPlayerController.stop(); }, toolsBuilder: () { return [ Stack( children: [ Container( width: 40, height: 40, padding: const EdgeInsets.all(5), child: InkWell( onTap: () { onModelSelect( context, groupState, customColors, ); }, child: Icon( Icons.alternate_email, color: selectedMembers != null && selectedMembers!.isNotEmpty ? customColors.linkColor : customColors.chatInputPanelText, ), ), ), if (selectedMembers != null && selectedMembers!.isNotEmpty) Positioned( right: 2, top: 0, child: Container( padding: const EdgeInsets.symmetric(horizontal: 3, vertical: 3), child: Text('x${selectedMembers!.length}', style: TextStyle( fontSize: 7, color: customColors.linkColor, )), ), ), ], ) ]; }, ), ), ), ], ), ); } else { return Container(); } }, ); } void onModelSelect( BuildContext context, GroupChatLoaded groupState, CustomColors customColors, ) { openModalBottomSheet( context, (context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.only(top: 15, left: 20), child: Text( AppLocale.selectMember.getString(context), style: TextStyle( fontSize: 14, color: customColors.weakLinkColor, ), ), ), Expanded( child: MultiItemSelector( itemBuilder: (item) { return Text(item.modelName); }, items: groupState.group.members.where((e) => e.status != 2).toList(), onChanged: (selected) { setState(() { selectedMembers = selected; }); }, itemAvatarBuilder: (item) { return _buildAvatar( avatarUrl: item.avatarUrl, id: item.id, size: 30, ); }, selectedItems: selectedMembers, ), ), ], ); }, heightFactor: 0.6, ); } BlocConsumer _buildChatPreviewArea( GroupChatLoaded group, CustomColors customColors, bool selectMode, ) { return BlocConsumer( listenWhen: (previous, current) => current is GroupChatMessagesLoaded, listener: (context, state) { if (state is GroupChatMessagesLoaded) { if (state.error != null) { showErrorMessageEnhanced(context, state.error); } messages = state.messages; // 聊天内容窗口滚动到底部 if (!state.hasWaitTasks && _scrollController.hasClients) { _scrollController.animateTo( 0, duration: const Duration(milliseconds: 500), curve: Curves.easeOut, ); } if (state.hasWaitTasks && _inputEnabled.value) { // 聊天回复中时,禁止输入框编辑 setState(() { _inputEnabled.value = false; }); } else if (!state.hasWaitTasks && !_inputEnabled.value) { // 聊天回复完成时,取消输入框的禁止编辑状态 setState(() { _inputEnabled.value = true; }); } // 启动定时器,定时刷新聊天记录 timer ??= Timer.periodic(const Duration(seconds: 3), (timer) { context.read().add(GroupChatUpdateMessageStatusEvent(widget.groupId)); }); } }, buildWhen: (prv, cur) => cur is GroupChatMessagesLoaded, builder: (context, state) { if (state is GroupChatMessagesLoaded) { if (state.messages.isEmpty) { return const Padding( padding: EdgeInsets.only(left: 15, right: 15, top: 10), child: GroupEmptyBoard(), ); } final loadedMessages = state.messages.map((e) { var member = e.memberId != null ? group.group.findMember(e.memberId!) : null; return Message( id: e.id, Role.getRoleFromText(e.role), e.message, type: MessageType.getTypeFromText(e.type), status: e.status, refId: e.pid, ts: e.createdAt, avatarUrl: member?.avatarUrl, senderName: member?.modelName, roomId: e.groupId, ); }).toList(); final messages = loadedMessages.map((e) { return MessageWithState( e, group.states[widget.stateManager.getKey(e.roomId ?? 0, e.id ?? 0)] ?? MessageState(), ); }).toList(); _chatPreviewController.setAllMessageIds(messages); return ChatPreview( supportBloc: false, messages: messages, scrollController: _scrollController, controller: _chatPreviewController, stateManager: widget.stateManager, robotAvatar: selectMode ? null : _buildAvatar( avatarUrl: group.group.group.avatarUrl, id: group.group.group.id, ), avatarBuilder: selectMode ? null : (Message message) { if (message.avatarUrl == null) { return null; } return _buildAvatar(avatarUrl: message.avatarUrl!); }, senderNameBuilder: (message) { if (message.senderName == null) { return null; } return Container( margin: const EdgeInsets.fromLTRB(0, 0, 10, 7), padding: const EdgeInsets.symmetric(horizontal: 13), child: Text( message.senderName!, style: TextStyle( color: customColors.weakTextColor, fontSize: 12, ), ), ); }, onDeleteMessage: (id) { handleDeleteMessage(context, id); }, onResetContext: () => handleResetContext(context), onSpeakEvent: (message) { _audioPlayerController.playAudio(message.text); }, onResentEvent: (message, index) { _scrollController.animateTo(0, duration: const Duration(milliseconds: 500), curve: Curves.easeOut); _handleSubmit(message.text, index: index, isResent: true); }, helpWidgets: state.hasWaitTasks || loadedMessages.isEmpty ? null : [ HelpTips( onSubmitMessage: _handleSubmit, onNewChat: () => handleResetContext(context), ) ], ); } return const Center(child: CircularProgressIndicator()); }, ); } /// 构建 AppBar AppBar _buildAppBar(BuildContext context, CustomColors customColors) { return _chatPreviewController.selectMode ? AppBar( title: Text( AppLocale.select.getString(context), style: const TextStyle(fontSize: CustomSize.appBarTitleSize), ), centerTitle: true, elevation: 0, leading: TextButton( onPressed: () { _chatPreviewController.exitSelectMode(); }, child: Text( AppLocale.cancel.getString(context), style: TextStyle(color: customColors.linkColor), ), ), toolbarHeight: CustomSize.toolbarHeight, ) : AppBar( centerTitle: true, elevation: 0, // backgroundColor: customColors.chatRoomBackground, title: BlocBuilder( buildWhen: (previous, current) => current is GroupChatLoaded, builder: (context, state) { if (state is GroupChatLoaded) { return Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ // 房间名称 Text( state.group.group.name, style: const TextStyle(fontSize: 16), ), ], ); } return Container(); }, ), actions: [ buildChatMoreMenu(context, widget.groupId), ], toolbarHeight: CustomSize.toolbarHeight, ); } Widget _buildAvatar({String? avatarUrl, int? id, int size = 30}) { if (avatarUrl != null && avatarUrl.startsWith('http')) { return RemoteAvatar( avatarUrl: imageURL(avatarUrl, qiniuImageTypeAvatar), size: size, ); } return RandomAvatar( id: id ?? 0, size: size, usage: Ability().isUserLogon() ? AvatarUsage.room : AvatarUsage.legacy, ); } /// 提交新消息 void _handleSubmit( String text, { int? index, bool isResent = false, }) { setState(() { _inputEnabled.value = false; }); var replyMemberIds = (selectedMembers ?? []).map((e) => e.id!).toList(); context.read().add(GroupChatSendEvent( widget.groupId, text, replyMemberIds, index: index, isResent: isResent, )); } /// 处理消息删除事件 void handleDeleteMessage(BuildContext context, int id, {int? chatHistoryId}) { openConfirmDialog( context, AppLocale.confirmDelete.getString(context), () { context.read().add(GroupChatDeleteEvent(widget.groupId, id)); HapticFeedbackHelper.mediumImpact(); }, danger: true, ); } /// 重置上下文 void handleResetContext(BuildContext context) { context.read().add(GroupChatSendSystemEvent( widget.groupId, MessageType.contextBreak, message: 'context-break-message', )); HapticFeedbackHelper.mediumImpact(); } /// 清空历史消息 void handleClearHistory(BuildContext context) { openConfirmDialog( context, AppLocale.confirmClearMessages.getString(context), () { context.read().add(GroupChatDeleteAllEvent(widget.groupId)); HapticFeedbackHelper.mediumImpact(); }, danger: true, ); } /// 构建聊天内容窗口 Widget buildSelectModeToolbars( BuildContext context, ChatPreviewController chatPreviewController, CustomColors customColors, ) { return Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( borderRadius: const BorderRadius.only(topLeft: CustomSize.radius, topRight: CustomSize.radius), color: customColors.backgroundColor, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ TextButton.icon( onPressed: () { var messages = chatPreviewController.selectedMessages(); if (messages.isEmpty) { showErrorMessageEnhanced(context, AppLocale.noMessageSelected.getString(context)); return; } Navigator.push( context, MaterialPageRoute( fullscreenDialog: true, builder: (context) => ChatShareScreen( messages: messages .map((e) => ChatShareMessage( content: e.message.text, username: e.message.senderName, avatarURL: e.message.avatarUrl, leftSide: e.message.role == Role.receiver, images: e.message.images, )) .toList(), ), ), ); // var shareText = messages.map((e) { // if (e.message.role == Role.sender) { // return '我:\n${e.message.text}'; // } // return '${e.message.senderName ?? "助理"}:\n${e.message.text}'; // }).join('\n\n'); // shareTo( // context, // content: shareText, // title: AppLocale.chatHistory.getString(context), // ); }, icon: Icon(Icons.share, color: customColors.linkColor), label: Text( AppLocale.share.getString(context), style: TextStyle(color: customColors.linkColor), ), ), TextButton.icon( onPressed: () { chatPreviewController.selectAllMessage(); }, icon: Icon(Icons.select_all_outlined, color: customColors.linkColor), label: Text( AppLocale.selectAll.getString(context), style: TextStyle(color: customColors.linkColor), ), ), TextButton.icon( onPressed: () { if (chatPreviewController.selectedMessageIds.isEmpty) { showErrorMessageEnhanced(context, AppLocale.noMessageSelected.getString(context)); return; } openConfirmDialog( context, AppLocale.confirmDelete.getString(context), () { final ids = chatPreviewController.selectedMessageIds.toList(); if (ids.isNotEmpty) { // context // .read() // .add(ChatMessageDeleteEvent(ids)); showErrorMessageEnhanced(context, AppLocale.operateSuccess.getString(context)); chatPreviewController.exitSelectMode(); } }, danger: true, ); }, icon: Icon(Icons.delete, color: customColors.linkColor), label: Text( AppLocale.delete.getString(context), style: TextStyle(color: customColors.linkColor), ), ), ], ), ); } /// 构建聊天设置下拉菜单 Widget buildChatMoreMenu( BuildContext context, int chatRoomId, { bool useLocalContext = true, bool withSetting = true, }) { var customColors = Theme.of(context).extension()!; return EnhancedPopupMenu( items: [ EnhancedPopupMenuItem( title: AppLocale.newChat.getString(context), icon: Icons.post_add, iconColor: Colors.blue, onTap: (ctx) { handleResetContext(useLocalContext ? ctx : context); }, ), EnhancedPopupMenuItem( title: AppLocale.clearChatHistory.getString(context), icon: Icons.delete_forever, iconColor: Colors.red, onTap: (ctx) { handleClearHistory(useLocalContext ? ctx : context); }, ), if (withSetting) EnhancedPopupMenuItem( title: AppLocale.settings.getString(context), icon: Icons.settings, iconColor: customColors.linkColor, onTap: (_) { context.push('/group-chat/$chatRoomId/edit').whenComplete(() { context.read().add(GroupChatLoadEvent(widget.groupId, forceUpdate: true)); }); }, ), ], ); } } ================================================ FILE: lib/page/chat/group/create.dart ================================================ import 'package:askaide/bloc/room_bloc.dart'; import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/image.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/enhanced_button.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/random_avatar.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/model/group.dart'; import 'package:askaide/repo/model/misc.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; class GroupCreatePage extends StatefulWidget { final SettingRepository setting; const GroupCreatePage({super.key, required this.setting}); @override State createState() => _GroupCreatePageState(); } class _GroupCreatePageState extends State { List models = []; List selectedModels = []; Function? globalLoadingCancel; @override void initState() { super.initState(); // 加载模型 APIServer().models().then((value) { setState(() { models = value.where((e) => !e.disabled).toList(); }); }); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return Scaffold( appBar: AppBar( title: Text( AppLocale.createGroupChat.getString(context), style: const TextStyle(fontSize: CustomSize.appBarTitleSize), ), centerTitle: true, elevation: 0, toolbarHeight: CustomSize.toolbarHeight, actions: [ Padding( padding: const EdgeInsets.only(right: 12), child: EnhancedButton( width: 50, height: 30, fontSize: 14, title: AppLocale.ok.getString(context), color: selectedModels.isEmpty ? customColors.weakTextColor : null, backgroundColor: selectedModels.isEmpty ? customColors.weakTextColor!.withAlpha(20) : null, onPressed: () { onSave(context); }, ), ), ], ), backgroundColor: customColors.backgroundColor, body: BackgroundContainer( setting: widget.setting, enabled: false, child: BlocListener( listenWhen: (previous, current) => current is GroupRoomUpdateResultState, listener: (context, state) { if (state is GroupRoomUpdateResultState) { globalLoadingCancel?.call(); if (state.success) { showSuccessMessage(AppLocale.operateSuccess.getString(context)); context.pop(); } else { showErrorMessageEnhanced(context, state.error ?? AppLocale.operateFailed.getString(context)); } } }, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.only(top: 15, left: 20, bottom: 15), child: Text( AppLocale.selectGroupMembers.getString(context), style: TextStyle( fontSize: 14, color: customColors.weakLinkColor, ), ), ), Expanded( child: ListView.separated( itemCount: models.length, itemBuilder: (context, i) { var item = models[i]; return CheckboxListTile( controlAffinity: ListTileControlAffinity.leading, checkboxShape: const CircleBorder(), activeColor: customColors.linkColor, side: BorderSide( color: customColors.weakTextColor!.withAlpha(100), ), title: Container( alignment: Alignment.center, padding: const EdgeInsets.symmetric(vertical: 5), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisSize: MainAxisSize.min, children: [ _buildAvatar(avatarUrl: item.avatarUrl, size: 40), const SizedBox(width: 20), Expanded( child: Container( alignment: Alignment.centerLeft, child: Text(item.name), ), ), ], ), ), onChanged: (selected) { setState(() { if (selectedModels.contains(item)) { selectedModels.remove(item); } else { selectedModels.add(item); } }); }, value: selectedModels.contains(item), ); }, separatorBuilder: (BuildContext context, int index) { return Divider( height: 1, color: customColors.columnBlockDividerColor?.withAlpha(200), ); }, ), ), ], ), ), ), ); } void onSave(BuildContext context) { if (selectedModels.isEmpty) { return; } globalLoadingCancel = BotToast.showCustomLoading( toastBuilder: (cancel) { return LoadingIndicator( message: AppLocale.processingWait.getString(context), ); }, allowClick: false, duration: const Duration(seconds: 120), ); try { if (context.mounted) { context.read().add( GroupRoomCreateEvent( name: selectedModels.map((e) => e.shortName).take(3).join("、"), members: selectedModels.map((e) => GroupMember(modelId: e.realModelId, modelName: e.shortName)).toList(), ), ); } } catch (e) { globalLoadingCancel?.call(); // ignore: use_build_context_synchronously showErrorMessageEnhanced(context, e); } } Widget _buildAvatar({String? avatarUrl, int? id, int size = 30}) { if (avatarUrl != null && avatarUrl.startsWith('http')) { return RemoteAvatar( avatarUrl: imageURL(avatarUrl, qiniuImageTypeAvatar), size: size, ); } return RandomAvatar( id: id ?? 0, size: size, usage: Ability().isUserLogon() ? AvatarUsage.room : AvatarUsage.legacy, ); } } ================================================ FILE: lib/page/chat/group/edit.dart ================================================ import 'package:askaide/bloc/group_chat_bloc.dart'; import 'package:askaide/repo/model/group.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/material.dart'; import 'dart:io'; import 'dart:math'; import 'package:askaide/bloc/room_bloc.dart'; import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/image.dart'; import 'package:askaide/helper/upload.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/avatar_selector.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/column_block.dart'; import 'package:askaide/page/component/enhanced_button.dart'; import 'package:askaide/page/component/enhanced_input.dart'; import 'package:askaide/page/component/enhanced_textfield.dart'; import 'package:askaide/page/component/image.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/multi_item_selector.dart'; import 'package:askaide/page/component/random_avatar.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/model/misc.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; class ModelWithMemberId { final Model model; final int? memberId; ModelWithMemberId(this.model, this.memberId); @override bool operator ==(Object other) => identical(this, other) || other is ModelWithMemberId && runtimeType == other.runtimeType && model == other.model && memberId == other.memberId; @override int get hashCode => model.hashCode ^ memberId.hashCode; } class GroupEditPage extends StatefulWidget { final SettingRepository setting; final int groupId; const GroupEditPage({ super.key, required this.groupId, required this.setting, }); @override State createState() => _GroupEditPageState(); } class _GroupEditPageState extends State { final _nameController = TextEditingController(text: ''); String? _avatarUrl; List avatarPresets = []; final randomSeed = Random().nextInt(10000); List models = []; List selectedModels = []; Function? globalLoadingCancel; @override void initState() { super.initState(); // 加载预定义头像 APIServer().avatars().then((value) { avatarPresets = value; }); // 加载模型 APIServer().models().then((value) { setState(() { models = value; }); context.read().add(GroupChatLoadEvent(widget.groupId, forceUpdate: true)); }); } @override void dispose() { _nameController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return Scaffold( appBar: AppBar( title: Text( AppLocale.roomSetting.getString(context), style: const TextStyle(fontSize: CustomSize.appBarTitleSize), ), centerTitle: true, elevation: 0, toolbarHeight: CustomSize.toolbarHeight, ), backgroundColor: customColors.backgroundColor, body: BackgroundContainer( setting: widget.setting, enabled: false, child: BlocListener( listenWhen: (previous, current) => current is GroupRoomUpdateResultState, listener: (context, state) { if (state is GroupRoomUpdateResultState) { globalLoadingCancel?.call(); if (state.success) { showSuccessMessage(AppLocale.operateSuccess.getString(context)); } else { showErrorMessageEnhanced(context, state.error ?? AppLocale.operateFailed.getString(context)); } context.read().add(GroupChatLoadEvent(widget.groupId, forceUpdate: true)); } }, child: BlocConsumer( listenWhen: (previous, current) => current is GroupChatLoaded, listener: (context, state) { if (state is GroupChatLoaded) { _nameController.text = state.group.group.name; _avatarUrl = state.group.group.avatarUrl; selectedModels = state.group.members .where((e) => e.status != 2) .map((e) { final mod = models.where((em) => e.modelId == em.realModelId).firstOrNull; if (mod == null) { return null; } return ModelWithMemberId(mod, e.id); }) .where((e) => e != null) .map((e) => e!) .toList(); final selectedModelIds = selectedModels.map((e) => e.model.realModelId).toList(); models = models.where((e) => !e.disabled || selectedModelIds.contains(e.realModelId)).toList(); } }, builder: (context, state) { return SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 10), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 10), ColumnBlock( children: [ // 名称 EnhancedTextField( customColors: customColors, controller: _nameController, maxLength: 50, maxLines: 1, showCounter: false, labelText: AppLocale.roomName.getString(context), labelPosition: LabelPosition.left, hintText: AppLocale.required.getString(context), textDirection: TextDirection.rtl, ), EnhancedInput( padding: const EdgeInsets.only(top: 10, bottom: 5), title: Text( AppLocale.avatar.getString(context), style: TextStyle( color: customColors.textfieldLabelColor, fontSize: 16, ), ), value: Row( mainAxisAlignment: MainAxisAlignment.end, mainAxisSize: MainAxisSize.min, children: [ Container( width: 45, height: 45, decoration: BoxDecoration( borderRadius: CustomSize.borderRadius, image: _avatarUrl == null ? null : DecorationImage( image: (_avatarUrl!.startsWith('http') ? CachedNetworkImageProviderEnhanced(_avatarUrl!) : FileImage(File(_avatarUrl!))) as ImageProvider, fit: BoxFit.cover, ), ), child: _avatarUrl == null ? const Center( child: Icon( Icons.interests, color: Colors.grey, ), ) : const SizedBox(), ), ], ), onPressed: () { openModalBottomSheet( context, (context) { return AvatarSelector( onSelected: (selected) { setState(() { _avatarUrl = selected.url; }); context.pop(); }, usage: AvatarUsage.room, defaultAvatarUrl: _avatarUrl, externalAvatarUrls: [ ...avatarPresets, ], ); }, heightFactor: 0.8, ); }, ), ], ), ColumnBlock( children: [ // 成员 EnhancedInput( padding: const EdgeInsets.only(top: 10, bottom: 5), title: Text( AppLocale.members.getString(context), style: TextStyle( color: customColors.textfieldLabelColor, fontSize: 16, ), ), value: Row( mainAxisAlignment: MainAxisAlignment.end, mainAxisSize: MainAxisSize.min, children: [ Stack( children: [ Container( width: resolveSelectedModelsPreviewWidth(context), height: 45, decoration: BoxDecoration(borderRadius: CustomSize.borderRadius), alignment: Alignment.center, clipBehavior: Clip.hardEdge, child: buildSelectedModelsPreview(), ), if (selectedModels.isNotEmpty) Positioned( right: 0, top: 0, child: Container( padding: const EdgeInsets.all(3), decoration: BoxDecoration( color: customColors.tagsBackground, borderRadius: CustomSize.borderRadius, ), child: Text( 'x${selectedModels.length}', style: TextStyle( fontSize: 8, color: customColors.weakTextColor, ), ), ), ) ], ), ], ), onPressed: () { openModalBottomSheet( context, (context) { return MultiItemSelector( itemBuilder: (item) { return Text(item.model.name); }, items: models .map((e) => ModelWithMemberId( e, selectedModels.where((se) => se.model.id == e.id).firstOrNull?.memberId)) .toList(), onChanged: (selected) { setState(() { selectedModels = selected; }); }, itemAvatarBuilder: (item) { return _buildAvatar( avatarUrl: item.model.avatarUrl, size: 30, ); }, selectedItems: selectedModels, ); }, heightFactor: 0.8, ); }, ), ], ), const SizedBox(height: 10), Row( children: [ Expanded( child: EnhancedButton( title: AppLocale.save.getString(context), color: canSubmit() ? null : customColors.weakTextColor, backgroundColor: canSubmit() ? null : customColors.weakTextColor!.withAlpha(20), onPressed: () async { if (!canSubmit()) { return; } globalLoadingCancel = BotToast.showCustomLoading( toastBuilder: (cancel) { return LoadingIndicator( message: AppLocale.processingWait.getString(context), ); }, allowClick: false, duration: const Duration(seconds: 120), ); final name = _nameController.text.trim(); if (name == '') { globalLoadingCancel?.call(); showErrorMessage('请输入群组名称'); return; } try { if (_avatarUrl != null) { if (!(_avatarUrl!.startsWith('http://') || _avatarUrl!.startsWith('https://'))) { // 上传文件,获取 URL final cancel = BotToast.showCustomLoading( toastBuilder: (cancel) { return LoadingIndicator( message: AppLocale.imageUploading.getString(context), ); }, allowClick: false, ); final uploadRes = await ImageUploader(widget.setting) .upload(_avatarUrl!, usage: 'avatar') .whenComplete(() => cancel()); _avatarUrl = uploadRes.url; } } if (context.mounted) { context.read().add( GroupRoomUpdateEvent( groupId: widget.groupId, name: name, avatarUrl: _avatarUrl, members: selectedModels .map((e) => GroupMember( modelId: e.model.realModelId, modelName: e.model.shortName, id: e.memberId)) .toList(), ), ); } } catch (e) { globalLoadingCancel?.call(); // ignore: use_build_context_synchronously showErrorMessageEnhanced(context, e); } }, ), ), ], ), ], ), ); }, ), ), ), ); } bool canSubmit() { if (selectedModels.isEmpty) { return false; } if (_nameController.text.trim() == '') { return false; } return true; } Widget buildSelectedModelsPreview() { if (selectedModels.isEmpty) { return const Center( child: Icon( Icons.group, color: Colors.grey, ), ); } return Stack( clipBehavior: Clip.none, children: [ for (var i = 0; i < selectedModels.length; i++) i == 0 ? _buildAvatar( avatarUrl: selectedModels.first.model.avatarUrl, size: 30, ) : Positioned( left: i * 15.0, child: _buildAvatar( avatarUrl: selectedModels[i].model.avatarUrl, size: 30, ), ), ], ); } Widget _buildAvatar({String? avatarUrl, int? id, int size = 30}) { if (avatarUrl != null && avatarUrl.startsWith('http')) { return RemoteAvatar( avatarUrl: imageURL(avatarUrl, qiniuImageTypeAvatar), size: size, ); } return RandomAvatar( id: id ?? 0, size: size, usage: Ability().isUserLogon() ? AvatarUsage.room : AvatarUsage.legacy, ); } double resolveSelectedModelsPreviewWidth(BuildContext context) { final maxSize = MediaQuery.of(context).size.width - 180; final expectSize = 45.0 + selectedModels.length * 15; return expectSize > maxSize ? maxSize : expectSize; } } ================================================ FILE: lib/page/chat/home.dart ================================================ import 'dart:io'; import 'dart:math'; import 'package:askaide/bloc/chat_chat_bloc.dart'; import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/color.dart'; import 'package:askaide/helper/global_store.dart'; import 'package:askaide/helper/haptic_feedback.dart'; import 'package:askaide/helper/cache.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/chat/empty.dart'; import 'package:askaide/page/component/chat/file_upload.dart'; import 'package:askaide/page/component/chat/voice_record.dart'; import 'package:askaide/page/component/column_block.dart'; import 'package:askaide/page/component/enhanced_textfield.dart'; import 'package:askaide/page/component/global_alert.dart'; import 'package:askaide/page/component/model_indicator.dart'; import 'package:askaide/page/component/notify_message.dart'; import 'package:askaide/page/component/sliver_component.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/api/model.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/model/chat_history.dart'; import 'package:askaide/repo/model/misc.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:custom_sliding_segmented_control/custom_sliding_segmented_control.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:flutter_sticky_header/flutter_sticky_header.dart'; import 'package:go_router/go_router.dart'; import 'package:url_launcher/url_launcher_string.dart'; class HomePage extends StatefulWidget { final SettingRepository setting; const HomePage({ super.key, required this.setting, }); @override State createState() => _HomePageState(); } class ChatModel { String id; String name; Color backgroundColor; String backgroundImage; ChatModel({ required this.id, required this.name, required this.backgroundColor, required this.backgroundImage, }); } class _HomePageState extends State { final TextEditingController _textController = TextEditingController(); ModelIndicator? currentModel; List models = [ HomeModelV2( modelId: "openai:gpt-3.5-turbo", modelName: 'Chat-3.5', type: 'model', id: 'openai:gpt-3.5-turbo', supportVision: false, name: 'Chat-3.5', avatarUrl: 'https://ssl.aicode.cc/ai-server/assets/avatar/gpt35.png', ), HomeModelV2( modelId: "openai:gpt-4", modelName: 'Chat-4', type: 'model', id: 'openai:gpt-4', supportVision: false, name: 'Chat-4', avatarUrl: 'https://ssl.aicode.cc/ai-server/assets/avatar/gpt4-preview.png', ), ]; /// 是否显示提示消息对话框 bool showFreeModelNotifyMessage = false; List selectedImageFiles = []; /// 促销事件 PromotionEvent? promotionEvent; /// Maximum height of the chat input box int inputMaxLines = 6; /// 用于监听键盘事件,实现回车发送消息,Shift+Enter换行 late final FocusNode _focusNode = FocusNode( onKeyEvent: (node, event) { if (!HardwareKeyboard.instance.isShiftPressed && event.logicalKey.keyLabel == 'Enter') { if (event is KeyDownEvent) { onSubmit(context, _textController.text.trim()); } return KeyEventResult.handled; } else { return KeyEventResult.ignored; } }, ); @override void dispose() { _textController.dispose(); super.dispose(); } @override void initState() { context.read().add(ChatChatLoadRecentHistories()); if (Ability().homeModels.isNotEmpty) { models = Ability().homeModels; } APIServer().capabilities().then((cap) { Ability().updateCapabilities(cap); if (cap.homeModels.isNotEmpty) { models = cap.homeModels; if (mounted) { setState(() {}); } } }); // 是否显示免费模型提示消息 Cache().boolGet(key: 'show_home_free_model_message').then((show) async { if (show) { final promotions = await APIServer().notificationPromotionEvents(); if (promotions['chat_page'] == null || promotions['chat_page']!.isEmpty) { return; } // 多个促销事件,则随机选择一个 promotionEvent = promotions['chat_page']![Random().nextInt(promotions['chat_page']!.length)]; } setState(() { showFreeModelNotifyMessage = show; }); }); _textController.addListener(() { setState(() {}); }); setState(() { currentModel = ModelIndicator( model: models[0], iconAndColor: iconAndColors[0], ); }); super.initState(); } Map buildModelIndicators() { Map map = {}; for (var i = 0; i < models.length; i++) { var model = models[i]; map[model.id] = ModelIndicator( model: model, selected: model.id == currentModel?.model.id, iconAndColor: iconAndColors[i], itemCount: models.length, ); } return map; } @override Widget build(BuildContext context) { var customColors = Theme.of(context).extension()!; return WindowFrameWidget( child: BackgroundContainer( setting: widget.setting, child: Scaffold( backgroundColor: Colors.transparent, body: BlocBuilder( buildWhen: (previous, current) => current is ChatChatRecentHistoriesLoaded, builder: (context, state) { if (state is ChatChatRecentHistoriesLoaded) { return SliverSingleComponent( title: Text( AppLocale.chatAnywhere.getString(context), style: TextStyle( fontSize: CustomSize.appBarTitleSize, color: customColors.backgroundInvertedColor, ), ), actions: [ IconButton( icon: const Icon(Icons.history), onPressed: () { context.push('/chat-chat/history').whenComplete(() { context.read().add(ChatChatLoadRecentHistories()); }); }, ), ], appBarExtraWidgets: () { return [ SliverStickyHeader( header: SafeArea( top: false, child: buildChatComponents(customColors, context, state), ), sliver: SliverList( delegate: SliverChildBuilderDelegate( (context, index) { if (index == 0) { return SafeArea( top: false, bottom: false, child: Container( margin: const EdgeInsets.only(top: 10, left: 15), child: Text( AppLocale.histories.getString(context), style: TextStyle( color: customColors.weakTextColor?.withAlpha(100), fontSize: 13, ), ), ), ); } if (index == state.histories.length && index > 3) { return SafeArea( top: false, bottom: false, child: GestureDetector( onTap: () { context.push('/chat-chat/history').whenComplete(() { context.read().add(ChatChatLoadRecentHistories()); }); }, child: Container( alignment: Alignment.center, margin: const EdgeInsets.only(top: 5, bottom: 15), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.keyboard_double_arrow_left, size: 12, color: customColors.weakTextColor!.withAlpha(120), ), Text( "查看更多", style: TextStyle( fontSize: 12, color: customColors.weakTextColor!.withAlpha(120), ), ), Icon( Icons.keyboard_double_arrow_right, size: 12, color: customColors.weakTextColor!.withAlpha(120), ), ], ), ), ), ); } return SafeArea( top: false, bottom: false, child: ChatHistoryItem( history: state.histories[index - 1], customColors: customColors, onTap: () { context .push( '/chat-anywhere?chat_id=${state.histories[index - 1].id}&model=${state.histories[index - 1].model}&title=${state.histories[index - 1].title}') .whenComplete(() { FocusScope.of(context).requestFocus(FocusNode()); context.read().add(ChatChatLoadRecentHistories()); }); }, ), ); }, childCount: state.histories.isNotEmpty ? state.histories.length + 1 : 0, ), ), ), ]; }, ); } else { return const SizedBox(); } }, ), ), ), ); } Container buildChatComponents( CustomColors customColors, BuildContext context, ChatChatRecentHistoriesLoaded state, ) { final indicators = buildModelIndicators(); return Container( color: customColors.backgroundContainerColor, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (Ability().showGlobalAlert) const GlobalAlert(pageKey: 'home'), if (showFreeModelNotifyMessage && promotionEvent != null) // 首页通知消息组件 buildNotifyMessageWidget(context), // 模型选择 Container( margin: const EdgeInsets.only( left: 10, right: 10, ), padding: const EdgeInsets.only( left: 5, right: 5, top: 10, ), child: CustomSlidingSegmentedControl( children: indicators, padding: 0, isStretch: true, height: Ability().showHomeModelDescription ? 60 : 45, innerPadding: const EdgeInsets.all(0), decoration: BoxDecoration( color: customColors.columnBlockBackgroundColor?.withAlpha(150), borderRadius: CustomSize.borderRadius, ), thumbDecoration: BoxDecoration( color: currentModel?.iconAndColor.color, borderRadius: CustomSize.borderRadius, ), duration: const Duration(milliseconds: 300), curve: Curves.easeInToLinear, onValueChanged: (value) { setState(() { currentModel = indicators[value]; }); }, ), ), // 聊天内容输入框 Padding( padding: const EdgeInsets.only( left: 10, right: 10, top: 10, ), child: ColumnBlock( padding: const EdgeInsets.only( top: 5, bottom: 5, left: 15, right: 15, ), children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 聊天问题输入框 Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (currentModel != null) Padding( padding: const EdgeInsets.only( top: 12, right: 4, ), child: Icon( Icons.circle, color: currentModel?.iconAndColor.color, size: 10, ), ), Expanded( child: EnhancedTextField( onFocusChange: (hasFocus) { if (hasFocus) { setState(() { inputMaxLines = 15; }); } else { setState(() { inputMaxLines = 6; }); } }, focusNode: _focusNode, controller: _textController, customColors: customColors, maxLines: inputMaxLines, minLines: 6, hintText: AppLocale.askMeAnyQuestion.getString(context), maxLength: 150000, showCounter: false, hintColor: customColors.textfieldHintDeepColor, hintTextSize: 15, ), ), ], ), // 聊天控制工具栏 Container( padding: const EdgeInsets.only(bottom: 5), child: _buildSendOrVoiceButton( context, customColors, ), ), if (selectedImageFiles.isNotEmpty && currentModel != null && currentModel!.model.supportVision) SizedBox( height: 110, child: ListView( scrollDirection: Axis.horizontal, children: selectedImageFiles .map( (e) => Container( margin: const EdgeInsets.only(right: 8), padding: const EdgeInsets.all(5), child: Stack( children: [ ClipRRect( borderRadius: CustomSize.borderRadius, child: e.file.bytes != null ? Image.memory( e.file.bytes!, fit: BoxFit.cover, width: 100, height: 100, ) : Image.file( File(e.file.path!), fit: BoxFit.cover, width: 100, height: 100, ), ), Positioned( right: 5, top: 5, child: InkWell( onTap: () { setState(() { selectedImageFiles.remove(e); }); }, child: Container( padding: const EdgeInsets.all(3), decoration: BoxDecoration( borderRadius: CustomSize.borderRadius, color: customColors.chatRoomBackground, ), child: Icon( Icons.close, size: 10, color: customColors.weakTextColor, ), ), ), ), ], ), ), ) .toList(), ), ) ], ) ], ), ), // 问题示例 if (state.examples != null && state.examples!.isNotEmpty && state.histories.isEmpty) Container( decoration: BoxDecoration(borderRadius: CustomSize.borderRadius), padding: const EdgeInsets.only(top: 5, left: 10, right: 10, bottom: 3), margin: const EdgeInsets.all(10), height: 260, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Image.asset( 'assets/app-256-transparent.png', width: 20, height: 20, ), const SizedBox(width: 5), Text( AppLocale.askMeLikeThis.getString(context), style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: customColors.textfieldHintDeepColor, ), ), ], ), const SizedBox(height: 20), Expanded( child: ListView.separated( padding: const EdgeInsets.all(0), itemCount: state.examples!.length > 4 ? 4 : state.examples!.length, physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) { return ListTextItem( title: state.examples![index].title, onTap: () { onSubmit( context, state.examples![index].text, ); }, customColors: customColors, ); }, separatorBuilder: (BuildContext context, int index) { return Divider( color: customColors.chatExampleItemText?.withAlpha(20), ); }, ), ), Align( alignment: Alignment.centerRight, child: TextButton( style: ButtonStyle( overlayColor: WidgetStateProperty.all(Colors.transparent), ), onPressed: () { setState(() { state.examples!.shuffle(); }); }, child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ Icon( Icons.refresh, color: customColors.weakTextColor, size: 16, ), const SizedBox(width: 3), Text( AppLocale.refresh.getString(context), style: TextStyle( color: customColors.weakTextColor, ), textScaler: const TextScaler.linear(0.9), ), ], ), ), ) ], ), ), ], ), ); } NotifyMessageWidget buildNotifyMessageWidget(BuildContext context) { return NotifyMessageWidget( title: promotionEvent!.title != null ? Text( promotionEvent!.title!, style: TextStyle( color: stringToColor(promotionEvent!.textColor ?? 'FFFFFFFF'), fontWeight: FontWeight.bold, ), ) : null, backgroundImageUrl: promotionEvent!.backgroundImage, height: 85, closeable: promotionEvent!.closeable, onClose: () { setState(() { showFreeModelNotifyMessage = false; }); Cache().setBool( key: 'show_home_free_model_message', value: false, duration: Duration(days: promotionEvent!.maxCloseDurationInDays ?? 7), ); }, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( promotionEvent!.content, style: TextStyle( color: stringToColor(promotionEvent!.textColor ?? 'FFFFFFFF'), fontSize: 14, overflow: TextOverflow.ellipsis, ), maxLines: 2, ), ), if (promotionEvent!.clickButtonType != PromotionEventClickButtonType.none && promotionEvent!.clickValue != null && promotionEvent!.clickValue!.isNotEmpty) InkWell( onTap: () { switch (promotionEvent!.clickButtonType) { case PromotionEventClickButtonType.url: if (promotionEvent!.clickValue != null && promotionEvent!.clickValue!.isNotEmpty) { launchUrlString(promotionEvent!.clickValue!, mode: LaunchMode.externalApplication); } break; case PromotionEventClickButtonType.inAppRoute: if (promotionEvent!.clickValue != null && promotionEvent!.clickValue!.isNotEmpty) { context.push(promotionEvent!.clickValue!); } break; case PromotionEventClickButtonType.none: } }, child: Row( children: [ Text( '详情', style: TextStyle( color: stringToColor(promotionEvent!.clickButtonColor ?? 'FFFFFFFF'), fontSize: 14, ), ), const SizedBox(width: 5), Icon( Icons.keyboard_double_arrow_right, size: 16, color: stringToColor(promotionEvent!.clickButtonColor ?? 'FFFFFFFF'), ), ], ), ), ], ), ); } /// 构建发送或者语音按钮 Widget _buildSendOrVoiceButton( BuildContext context, CustomColors customColors, ) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ Row( children: [ InkWell( onTap: () { HapticFeedbackHelper.mediumImpact(); openModalBottomSheet( context, (context) { return VoiceRecord( onFinished: (text) { _textController.text = _textController.text + text; Navigator.pop(context); }, onStart: () {}, ); }, isScrollControlled: false, heightFactor: 0.8, ); }, child: Icon( Icons.mic, color: customColors.chatInputPanelText, size: 28, ), ), const SizedBox(width: 10), if (currentModel != null && currentModel!.model.supportVision) InkWell( onTap: () async { // 上传图片 HapticFeedbackHelper.mediumImpact(); if (selectedImageFiles.length >= 4) { showSuccessMessage(AppLocale.uploadImageLimit4.getString(context)); return; } FilePickerResult? result = await FilePicker.platform.pickFiles( type: FileType.image, allowMultiple: true, ); if (result != null && result.files.isNotEmpty) { final files = selectedImageFiles; files.addAll(result.files.map((e) => FileUpload(file: e)).toList()); setState(() { selectedImageFiles = files.sublist(0, files.length > 4 ? 4 : files.length); }); } }, child: Icon( Icons.camera_alt, color: customColors.chatInputPanelText, size: 28, ), ), ], ), const SizedBox(), InkWell( onTap: () { onSubmit(context, _textController.text.trim()); }, child: Icon( Icons.send, color: _textController.text.trim().isNotEmpty ? customColors.linkColor ?? const Color.fromARGB(255, 70, 165, 73) : customColors.chatInputPanelText, size: 26, ), ) ], ); } void onSubmit(BuildContext context, String text) { if (text.trim().isEmpty) { return; } if (currentModel != null && currentModel!.model.supportVision) { GlobalStore().uploadedFiles = selectedImageFiles; } selectedImageFiles = []; context .push(Uri(path: '/chat-anywhere', queryParameters: { 'init_message': text, 'model': 'v2@${currentModel?.model.uniqueKey}', }).toString()) .whenComplete(() { _textController.clear(); GlobalStore().uploadedFiles.clear(); FocusScope.of(context).requestFocus(FocusNode()); context.read().add(ChatChatLoadRecentHistories()); }); } } class ChatHistoryItem extends StatelessWidget { const ChatHistoryItem({ super.key, required this.history, required this.customColors, required this.onTap, }); final ChatHistory history; final CustomColors customColors; final VoidCallback onTap; @override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.symmetric( horizontal: 15, vertical: 5, ), decoration: BoxDecoration(borderRadius: CustomSize.borderRadius), child: Slidable( endActionPane: ActionPane( motion: const ScrollMotion(), children: [ const SizedBox(width: 10), SlidableAction( label: AppLocale.delete.getString(context), borderRadius: CustomSize.borderRadiusAll, backgroundColor: Colors.red, icon: Icons.delete, onPressed: (_) { openConfirmDialog( context, AppLocale.confirmDelete.getString(context), () { context.read().add(ChatChatDeleteHistory(history.id!)); }, danger: true, ); }, ), ], ), child: Container( decoration: BoxDecoration( borderRadius: CustomSize.borderRadius, color: customColors.listTileBackgroundColor, ), child: ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), shape: RoundedRectangleBorder(borderRadius: CustomSize.borderRadius), title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( (history.title ?? '未命名').trim(), overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 15), maxLines: 1, ), ), // Text( // humanTime(history.updatedAt), // style: TextStyle( // color: customColors.weakTextColor, // fontSize: 12, // ), // ), ], ), dense: true, subtitle: Padding( padding: const EdgeInsets.only(top: 5), child: Text( (history.lastMessage ?? '暂无内容').trim().replaceAll("\n", " "), maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: customColors.weakTextColor?.withAlpha(150), fontSize: 12, overflow: TextOverflow.ellipsis, ), ), ), onTap: () { HapticFeedbackHelper.lightImpact(); onTap(); }, tileColor: Colors.transparent, hoverColor: Colors.transparent, ), ), ), ); } } ================================================ FILE: lib/page/chat/home_chat.dart ================================================ import 'package:askaide/bloc/chat_message_bloc.dart'; import 'package:askaide/bloc/notify_bloc.dart'; import 'package:askaide/bloc/room_bloc.dart'; import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/cache.dart'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/global_store.dart'; import 'package:askaide/helper/haptic_feedback.dart'; import 'package:askaide/helper/model.dart'; import 'package:askaide/helper/upload.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/chat/component/model_switcher.dart'; import 'package:askaide/page/chat/component/stop_button.dart'; import 'package:askaide/page/chat/character_chat.dart'; import 'package:askaide/page/component/audio_player.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/chat/chat_input.dart'; import 'package:askaide/page/component/chat/chat_input_button.dart'; import 'package:askaide/page/component/chat/chat_preview.dart'; import 'package:askaide/page/component/chat/empty.dart'; import 'package:askaide/page/component/chat/file_upload.dart'; import 'package:askaide/page/component/chat/help_tips.dart'; import 'package:askaide/page/component/chat/message_state_manager.dart'; import 'package:askaide/page/component/chat/role_avatar.dart'; import 'package:askaide/page/component/enhanced_error.dart'; import 'package:askaide/page/component/global_alert.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/select_mode_toolbar.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/api/model.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/model/message.dart'; import 'package:askaide/repo/model/misc.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:askaide/repo/model/model.dart' as mm; class HomeChatPage extends StatefulWidget { /// 聊天内容窗口状态管理器 final MessageStateManager stateManager; /// 设置仓库 final SettingRepository setting; /// 当前聊天 ID final int? chatId; /// 初始消息,该消息会在进入页面时自动发送到服务端 final String? initialMessage; /// 当前聊天所使用的模型,对于 /// - v1 版本,该值为模型 ID /// - v2 版本,该值为 homeModel 的 ID,格式为 v2@type|id final String? model; /// 当前页面的标题 final String? title; const HomeChatPage({ super.key, required this.stateManager, required this.setting, this.chatId, this.initialMessage, this.model, this.title, }); @override State createState() => _HomeChatPageState(); } class _HomeChatPageState extends State { // 聊天内容界面控制器 final ChatPreviewController chatPreviewController = ChatPreviewController(); // 聊天内容滚动控制器 final ScrollController scrollController = ScrollController(); // 输入框是否可编辑 final ValueNotifier enableInput = ValueNotifier(true); // 音频播放器控制器 final AudioPlayerController audioPlayerController = AudioPlayerController(useRemoteAPI: true); // 聊天室 ID,当没有值时,会在第一个聊天消息发送后自动设置新值 int? chatId; // 当前选择的图片文件 List selectedImageFiles = []; // 是否显示音频播放器 bool showAudioPlayer = false; // 是否显示音频播放器加载中 bool audioLoadding = false; // 全量模型列表(v1 使用) List supportModels = []; // 全量模型列表(v2 使用) List supportModelsV2 = []; // 当前聊天所使用的模型(v2) HomeModelV2? currentModelV2; /// 当前选择的模型 mm.Model? selectedModel; /// 是否启用搜索 bool enableSearch = false; /// 是否启用推理 bool enableReasoning = false; @override void initState() { // 设置当前聊天 ID,当没有值时,会在第一个聊天消息发送后自动设置新值 chatId = widget.chatId; // 加载当前聊天室信息 context.read().add(RoomLoadEvent( chatAnywhereRoomId, chatHistoryId: chatId, cascading: true, )); // 查询最近聊天记录 context.read().add(ChatMessageGetRecentEvent(chatHistoryId: widget.chatId)); chatPreviewController.addListener(() { setState(() {}); }); audioPlayerController.onPlayStopped = () { setState(() { showAudioPlayer = false; }); }; audioPlayerController.onPlayAudioStarted = () { setState(() { showAudioPlayer = true; }); }; audioPlayerController.onPlayAudioLoading = (loading) { setState(() { audioLoadding = loading; }); }; // 加载模型列表,用于查询模型名称 ModelAggregate.models().then((value) { setState(() { supportModels = value; }); if (widget.model != null) { selectedModel = supportModels.where((e) => e.id == widget.model).firstOrNull; } if (selectedModel == null) { Cache().stringGet(key: 'last_selected_model').then((value) { final selected = supportModels.where((e) => e.id == value).firstOrNull; if (selected != null) { setState(() { selectedModel = selected; }); } }); } }); if (widget.model != null) { loadCurrentModel(widget.model!); } // 当参数 initialMessage 不为空时,延迟 500 毫秒后发送初始消息 if (widget.initialMessage != null) { WidgetsBinding.instance.addPostFrameCallback((_) { Future.delayed(const Duration(milliseconds: 500), () { selectedImageFiles = GlobalStore().uploadedFiles; handleSubmit(widget.initialMessage!); }); }); } super.initState(); } @override void dispose() { scrollController.dispose(); chatPreviewController.dispose(); audioPlayerController.dispose(); super.dispose(); } Future loadCurrentModel(String model) async { if (!model.startsWith('v2@') || currentModelV2 != null) { return; } currentModelV2 = await APIServer().customHomeModelsItemV2( uniqueKey: model.split('v2@').last, ); setState(() {}); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return WindowFrameWidget( child: Scaffold( // AppBar appBar: buildAppBar(context, customColors), backgroundColor: customColors.backgroundContainerColor, // 聊天内容窗口 body: BackgroundContainer( setting: widget.setting, // maxWidth: double.infinity, child: BlocConsumer( listenWhen: (previous, current) => current is RoomLoaded, listener: (context, state) async { if (state is RoomLoaded && currentModelV2 == null) { await loadCurrentModel(state.room.model); } }, buildWhen: (previous, current) => current is RoomLoaded, builder: (context, room) { // 加载聊天室 if (room is RoomLoaded) { if (room.error != null) { return EnhancedErrorWidget(error: room.error); } return buildChatComponents( customColors, context, room, ); } else { return Container(); } }, ), ), ), ); } /// 构建 AppBar AppBar buildAppBar(BuildContext context, CustomColors customColors) { if (chatPreviewController.selectMode) { return AppBar( title: Text(AppLocale.select.getString(context)), backgroundColor: Colors.transparent, centerTitle: true, leadingWidth: 80, leading: TextButton( onPressed: () { chatPreviewController.exitSelectMode(); }, child: Text( AppLocale.cancel.getString(context), style: TextStyle(color: customColors.linkColor), ), ), ); } return AppBar( centerTitle: true, elevation: 0, toolbarHeight: CustomSize.toolbarHeight, title: BlocBuilder( buildWhen: (previous, current) => current is ChatMessagesLoaded, builder: (context, state) { if (state is ChatMessagesLoaded) { return GestureDetector( onTap: () { ModelSwitcher.openActionDialog( context: context, onSelected: (selected) { setState(() { selectedModel = selected; }); }, initValue: selectedModel, ); }, child: Text( widget.title ?? AppLocale.chatAnywhere.getString(context), overflow: TextOverflow.ellipsis, maxLines: 1, style: const TextStyle(fontSize: CustomSize.appBarTitleSize), ), ); } return const SizedBox(); }, ), ); } /// 构建聊天室窗口 Widget buildChatComponents( CustomColors customColors, BuildContext context, RoomLoaded room, ) { return Column( children: [ if (Ability().showGlobalAlert) const GlobalAlert(pageKey: 'chat'), if (showAudioPlayer) EnhancedAudioPlayer( controller: audioPlayerController, loading: audioLoadding, ), // 聊天内容窗口 Expanded( child: Stack( fit: StackFit.expand, children: [ BlocConsumer( listener: (context, state) { if (state is ChatHistoryInited) { setState(() { chatId = state.chatId; }); } if (state is ChatMessagesLoaded && state.error == null) { setState(() { selectedImageFiles = []; }); } // 显示错误提示 else if (state is ChatMessagesLoaded && state.error != null) { showErrorMessageEnhanced(context, state.error); } else if (state is ChatMessageUpdated) { // 聊天内容窗口滚动到底部 if (!state.processing && scrollController.hasClients) { scrollController.animateTo( 0, duration: const Duration(milliseconds: 500), curve: Curves.easeOut, ); } if (state.processing && enableInput.value) { // 聊天回复中时,禁止输入框编辑 setState(() { enableInput.value = false; }); } else if (!state.processing && !enableInput.value) { // 聊天回复完成时,取消输入框的禁止编辑状态 setState(() { enableInput.value = true; }); } } }, buildWhen: (prv, cur) => cur is ChatMessagesLoaded, builder: (context, state) { if (state is ChatMessagesLoaded) { return buildChatPreviewArea( state, room.examples ?? [], room, customColors, chatPreviewController.selectMode, ); } return const Center(child: CircularProgressIndicator()); }, ), if (!enableInput.value) Positioned( bottom: 10, width: CustomSize.adaptiveMaxWindowWidth(context), child: Center( child: StopButton( label: AppLocale.stopOutput.getString(context), onPressed: () { HapticFeedbackHelper.mediumImpact(); context.read().add(ChatMessageStopEvent()); }, ), ), ), ], ), ), // 聊天输入窗口 if (!chatPreviewController.selectMode) Container( decoration: BoxDecoration( borderRadius: const BorderRadius.only(topLeft: CustomSize.radius, topRight: CustomSize.radius), color: customColors.chatInputPanelBackground, ), child: BlocBuilder( buildWhen: (previous, current) => current is ChatMessagesLoaded, builder: (context, state) { var enableImageUpload = false; var showSearch = false; var showReasoning = false; if (state is ChatMessagesLoaded) { if (currentModelV2 != null) { enableImageUpload = currentModelV2?.supportVision ?? false; showSearch = currentModelV2?.supportSearch ?? false; showReasoning = currentModelV2?.supportReasoning ?? false; } else { var model = state.chatHistory?.model ?? room.room.model; final cur = supportModels.where((e) => e.id == model).firstOrNull; enableImageUpload = cur?.supportVision ?? false; showSearch = cur?.supportSearch ?? false; showReasoning = cur?.supportReasoning ?? false; } } return ChatInput( enableNotifier: enableInput, onSubmit: (value) { handleSubmit(value); FocusManager.instance.primaryFocus?.unfocus(); }, enableImageUpload: selectedModel == null ? enableImageUpload : (selectedModel?.supportVision ?? false), onImageSelected: (files) { setState(() { selectedImageFiles = files; }); }, selectedImageFiles: enableImageUpload ? selectedImageFiles : [], hintText: AppLocale.askMeAnyQuestion.getString(context), onVoiceRecordTappedEvent: () { audioPlayerController.stop(); }, onStopGenerate: () { context.read().add(ChatMessageStopEvent()); }, toolsBuilder: () { return [ if (showReasoning) ChatInputButton( text: AppLocale.reasoning.getString(context), icon: Icons.tips_and_updates_outlined, onPressed: () { setState(() { enableReasoning = !enableReasoning; }); }, isActive: enableReasoning, ), if (showSearch) ChatInputButton( text: AppLocale.onlineSearch.getString(context), icon: Icons.language_outlined, onPressed: () { setState(() { enableSearch = !enableSearch; }); }, isActive: enableSearch, ), ]; }, ); }, ), ), // 选择模式工具栏 if (chatPreviewController.selectMode) SelectModeToolbar(chatPreviewController: chatPreviewController), ], ); } /// 构建聊天内容窗口 Widget buildChatPreviewArea( ChatMessagesLoaded loadedState, List examples, RoomLoaded room, CustomColors customColors, bool selectMode, ) { final loadedMessages = loadedState.messages as List; if (room.room.initMessage != null && room.room.initMessage != '' && loadedMessages.isEmpty) { loadedMessages.add( Message( Role.receiver, room.room.initMessage!, type: MessageType.initMessage, ), ); } // 聊天内容为空时,显示示例页面 if (loadedMessages.isEmpty) { return EmptyPreview( examples: examples, onSubmit: handleSubmit, ); } final messages = loadedMessages.map((e) { if (e.model != null && !e.model!.startsWith('v2@')) { final mod = supportModels.where((m) => m.id == e.model).firstOrNull; if (mod != null) { e.senderName = mod.shortName; e.avatarUrl = mod.avatarUrl; } } if (e.avatarUrl == null || e.senderName == null) { if (loadedState.chatHistory != null && loadedState.chatHistory!.model != null) { if (currentModelV2 != null) { e.senderName = currentModelV2!.name; e.avatarUrl = currentModelV2!.avatarUrl; } else { final mod = supportModels.where((e) => e.id == loadedState.chatHistory!.model!).firstOrNull; if (mod != null) { e.senderName = mod.shortName; e.avatarUrl = mod.avatarUrl; } } } } final stateMessage = room.states[widget.stateManager.getKey(e.roomId ?? 0, e.id ?? 0)] ?? MessageState(); return MessageWithState(e, stateMessage); }).toList(); chatPreviewController.setAllMessageIds(messages); return ChatPreview( padding: enableInput.value ? null : const EdgeInsets.only(bottom: 35), messages: messages, scrollController: scrollController, controller: chatPreviewController, stateManager: widget.stateManager, robotAvatar: selectMode ? null : RoleAvatar( avatarUrl: room.room.avatarUrl, his: loadedState.chatHistory, alternativeAvatarUrl: currentModelV2?.avatarUrl, ), senderNameBuilder: (message) { if (message.senderName == null) { return null; } final robotName = room.room.id != null && room.room.id! > 1 ? room.room.name : message.senderName!; String? robotNameAlias; if (message.model != null && room.room.modelName() != message.model && robotName != message.model) { robotNameAlias = _searchModelName(message.model!); } return Container( margin: const EdgeInsets.fromLTRB(0, 0, 10, 7), padding: const EdgeInsets.symmetric(horizontal: 10), child: Row( children: [ Text( robotName, style: TextStyle( color: customColors.weakTextColor, fontSize: 12, ), ), if (robotNameAlias != null) Container( margin: const EdgeInsets.only(left: 5), child: Text( '($robotNameAlias)', style: TextStyle(color: customColors.weakTextColorLess, fontSize: 10), ), ), ], ), ); }, onDeleteMessage: (id) { handleDeleteMessage(context, id, chatHistoryId: chatId); }, onResentEvent: (message, index) { scrollController.animateTo(0, duration: const Duration(milliseconds: 500), curve: Curves.easeOut); handleSubmit(message.text, messagetType: message.type, index: index, isResent: true); }, onSpeakEvent: (message) { audioPlayerController.playAudio(message.text); }, helpWidgets: loadedState.processing || loadedMessages.last.isInitMessage() ? null : [HelpTips(onSubmitMessage: handleSubmit)], ); } String _searchModelName(String model) { final mod = supportModels.where((e) => e.id == model).firstOrNull; if (mod != null) { return mod.shortName ?? mod.name; } return model; } /// 提交新消息 void handleSubmit( String text, { messagetType = MessageType.text, int? index, bool isResent = false, }) async { setState(() { enableInput.value = false; }); if (selectedImageFiles.isNotEmpty) { final cancel = BotToast.showCustomLoading( toastBuilder: (cancel) { return LoadingIndicator( message: AppLocale.imageUploading.getString(context), ); }, allowClick: false, ); try { final uploader = ImageUploader(widget.setting); for (var file in selectedImageFiles) { if (file.uploaded) { continue; } if (file.file.bytes != null) { final res = await uploader.base64( imageData: file.file.bytes, maxSize: 1024 * 1024, compressWidth: 512, compressHeight: 512, ); file.setUrl(res); } else { final res = await uploader.base64( path: file.file.path!, maxSize: 1024 * 1024, compressWidth: 512, compressHeight: 512, ); file.setUrl(res); } } } catch (e) { // ignore: use_build_context_synchronously showErrorMessageEnhanced(context, e); return; } finally { cancel(); } } // ignore: use_build_context_synchronously context.read().add( ChatMessageSendEvent( Message( Role.sender, text, user: 'me', ts: DateTime.now(), model: selectedModel?.id ?? widget.model, type: messagetType, chatHistoryId: chatId, images: selectedImageFiles.where((e) => e.uploaded).map((e) => e.url!).toList(), flags: [ if (enableSearch) 'search', if (enableReasoning) 'reasoning', ], ), index: index, isResent: isResent, ), ); // ignore: use_build_context_synchronously context.read().add(NotifyResetEvent()); } } ================================================ FILE: lib/page/chat/home_chat_history.dart ================================================ import 'package:askaide/bloc/chat_chat_bloc.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/chat/home.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/page/data/chat_history_datasource.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/chat_message_repo.dart'; import 'package:askaide/repo/model/chat_history.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; import 'package:loading_more_list/loading_more_list.dart'; class HomeChatHistoryPage extends StatefulWidget { final SettingRepository setting; final ChatMessageRepository chatMessageRepo; const HomeChatHistoryPage({super.key, required this.setting, required this.chatMessageRepo}); @override State createState() => _HomeChatHistoryPageState(); } class _HomeChatHistoryPageState extends State { late final ChatHistoryDatasource datasource; String? keyword; @override void initState() { datasource = ChatHistoryDatasource(widget.chatMessageRepo); super.initState(); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( title: Text( AppLocale.histories.getString(context), style: const TextStyle(fontSize: CustomSize.appBarTitleSize), ), toolbarHeight: CustomSize.toolbarHeight, centerTitle: true, ), backgroundColor: customColors.backgroundColor, body: BackgroundContainer( setting: widget.setting, enabled: false, backgroundColor: customColors.backgroundColor, child: SafeArea( top: false, left: false, right: false, child: Column( children: [ Container( margin: const EdgeInsets.only(bottom: 10, left: 15, right: 15), decoration: BoxDecoration( color: customColors.textfieldBackgroundColor, borderRadius: CustomSize.borderRadius, ), child: TextField( textAlignVertical: TextAlignVertical.center, style: TextStyle(color: customColors.dialogDefaultTextColor), decoration: InputDecoration( hintText: AppLocale.search.getString(context), hintStyle: TextStyle( color: customColors.dialogDefaultTextColor, ), prefixIcon: Icon( Icons.search, color: customColors.dialogDefaultTextColor, ), isDense: true, border: InputBorder.none, ), onChanged: (value) { setState(() { keyword = value; }); datasource.refresh(false, keyword); }, ), ), Expanded( child: BlocListener( listenWhen: (previous, current) => current is ChatChatRecentHistoriesLoaded, listener: (context, state) { if (state is ChatChatRecentHistoriesLoaded) { datasource.refresh(false, keyword); } }, child: RefreshIndicator( color: customColors.linkColor, displacement: 20, onRefresh: () { return datasource.refresh(false, keyword); }, child: LoadingMoreList( ListConfig( itemBuilder: (context, item, index) { // Get previous item to check if we need a header final prevItem = index > 0 ? datasource[index - 1] : null; final now = DateTime.now(); final itemDate = DateTime.fromMillisecondsSinceEpoch( (item.createdAt ?? DateTime.now()).millisecondsSinceEpoch); final prevDate = prevItem != null ? DateTime.fromMillisecondsSinceEpoch( (prevItem.createdAt ?? DateTime.now()).millisecondsSinceEpoch) : null; // Helper function to get time group String getTimeGroup(DateTime date) { final difference = now.difference(date); if (difference.inDays < 4) { return AppLocale.recently.getString(context); } else if (difference.inDays < 7) { return '4 ${AppLocale.daysAgo.getString(context)}'; } else if (difference.inDays < 14) { return AppLocale.lastWeek.getString(context); } else if (difference.inDays < 30) { final weeks = (difference.inDays / 7).floor(); return '$weeks ${AppLocale.weeksAgo.getString(context)}'; } else if (difference.inDays < 365) { if (difference.inDays < 60) { return AppLocale.lastMonth.getString(context); } final months = (difference.inDays / 30).floor(); return '$months ${AppLocale.monthsAgo.getString(context)}'; } else if (difference.inDays < 730) { return AppLocale.lastYear.getString(context); } else { return AppLocale.longTimeAgo.getString(context); } } final currentGroup = getTimeGroup(itemDate); final prevGroup = prevDate != null ? getTimeGroup(prevDate) : null; // Show header if group changed final showHeader = currentGroup.isNotEmpty && currentGroup != prevGroup; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (showHeader) Padding( padding: const EdgeInsets.only(left: 15, top: 10, bottom: 5), child: Text( currentGroup, style: TextStyle( fontSize: 12, color: customColors.weakTextColor, ), ), ), ChatHistoryItem( history: item, customColors: customColors, onTap: () { context .push( '/chat-anywhere?chat_id=${item.id}&model=${item.model}&title=${item.title}') .whenComplete(() { FocusScope.of(context).requestFocus(FocusNode()); context.read().add(ChatChatLoadRecentHistories()); }); }, ), ], ); }, sourceList: datasource, indicatorBuilder: (context, status) { String msg = ''; switch (status) { case IndicatorStatus.noMoreLoad: msg = ''; break; case IndicatorStatus.loadingMoreBusying: msg = 'Loading...'; break; case IndicatorStatus.error: msg = 'Failed to load, please try again later'; break; case IndicatorStatus.empty: msg = 'No data'; break; default: return const Center(child: LoadingIndicator()); } return Container( padding: const EdgeInsets.all(15), alignment: Alignment.center, child: Text( msg, style: TextStyle( color: customColors.weakTextColor, fontSize: 14, ), ), ); }, ), ), ), ), ) ], ), ), ), ), ); } } ================================================ FILE: lib/page/component/account_quota_card.dart ================================================ import 'package:askaide/helper/ability.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/credit.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api/user.dart'; import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; import 'package:url_launcher/url_launcher.dart'; class AccountQuotaCard extends StatelessWidget { final UserInfo? userInfo; final VoidCallback? onPaymentReturn; final bool noBorder; const AccountQuotaCard({super.key, this.userInfo, this.onPaymentReturn, this.noBorder = false}); @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return Container( margin: noBorder ? null : const EdgeInsets.symmetric(horizontal: 15, vertical: 10), height: 120, child: Container( padding: noBorder ? const EdgeInsets.only() : const EdgeInsets.symmetric( horizontal: 20, vertical: 30, ), child: userInfo != null ? Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( AppLocale.usage.getString(context), style: TextStyle( fontSize: 16, color: customColors.backgroundInvertedColor, ), ), const SizedBox(width: 5), InkWell( onTap: () { launchUrl( Uri.parse('https://ai.aicode.cc/zhihuiguo.html'), ); }, child: Icon( Icons.help, size: 12, color: customColors.weakTextColor?.withAlpha(150), ), ), ], ), const SizedBox(height: 5), Row( children: [ InkWell( onTap: () { context.push('/quota-usage-statistics'); }, borderRadius: CustomSize.borderRadiusAll, child: Credit( count: userInfo!.quota.quotaRemain(), color: customColors.backgroundInvertedColor, fontSize: 16, fontWeight: FontWeight.normal, ), ), const SizedBox(width: 5), if (Ability().enablePayment) TextButton( onPressed: () { context.push('/payment').whenComplete(() { if (onPaymentReturn != null) { onPaymentReturn!(); } }); }, child: Text( AppLocale.buy.getString(context), style: const TextStyle( fontSize: 14, ), ), ), ], ) ], ), ), ], ) : Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ IconButton( alignment: Alignment.centerLeft, icon: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.account_circle), const SizedBox(width: 5), AutoSizeText(AppLocale.signInAccount.getString(context), maxLines: 1), ], ), onPressed: () { context.go('/login'); }, ), const SizedBox(width: 10), ], ), ), ); } } ================================================ FILE: lib/page/component/advanced_button.dart ================================================ import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/enhanced_button.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; class AdvancedButton extends StatelessWidget { final bool showAdvancedOptions; final Function(bool) onPressed; const AdvancedButton({super.key, required this.showAdvancedOptions, required this.onPressed}); @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return Center( child: EnhancedButton( title: showAdvancedOptions ? AppLocale.collapseOptions.getString(context) : AppLocale.advanced.getString(context), width: 100, backgroundColor: Colors.transparent, color: customColors.weakTextColorLess, fontSize: 12, icon: Icon( showAdvancedOptions ? Icons.unfold_less : Icons.unfold_more, color: customColors.weakTextColorLess, size: 12, ), onPressed: () { onPressed(!showAdvancedOptions); }, ), ); } } ================================================ FILE: lib/page/component/animated_cursor.dart ================================================ import 'dart:async'; import 'package:flutter/material.dart'; class Cursor extends CustomPainter { final Color? color; const Cursor({this.color}); @override void paint(Canvas canvas, Size size) { Paint paint = Paint()..color = color ?? Colors.black; canvas.drawCircle(const Offset(0, 10), 3.0, paint); } @override bool shouldRepaint(CustomPainter oldDelegate) { return true; } } class AnimatedCursor extends StatefulWidget { final double width; final double height; final Color? color; const AnimatedCursor({ super.key, this.width = 2, this.height = 20, this.color, }); @override State createState() => _AnimatedCursorState(); } class _AnimatedCursorState extends State with SingleTickerProviderStateMixin { late Timer _timer; bool _showCursor = false; @override void initState() { super.initState(); _timer = Timer.periodic(const Duration(seconds: 1), (timer) { setState(() { _showCursor = !_showCursor; }); }); } @override Widget build(BuildContext context) { return _showCursor ? CustomPaint( size: Size(widget.width, widget.height), painter: Cursor(color: widget.color), ) : SizedBox(width: widget.width, height: widget.height); } @override void dispose() { _timer.cancel(); super.dispose(); } } ================================================ FILE: lib/page/component/attached_button_panel.dart ================================================ import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:flutter/material.dart'; class AttachedButtonPanel extends StatelessWidget { final List buttons; const AttachedButtonPanel({super.key, required this.buttons}); @override Widget build(BuildContext context) { final columns = []; final itemPerRow = buttons.length > 4 ? 3 : 4; for (var i = 0; i < buttons.length; i += itemPerRow) { final row = []; for (var j = 0; j < itemPerRow; j++) { if (i + j < buttons.length) { row.add(buttons[i + j]); } } if (i > 0) { columns.add(const SizedBox(height: 10)); } columns.add(Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, children: row, )); } return Card( child: Container( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 10), decoration: BoxDecoration( borderRadius: CustomSize.borderRadius, color: const Color.fromARGB(223, 0, 0, 0), ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: columns, ), ), ); } } ================================================ FILE: lib/page/component/audio_player.dart ================================================ import 'package:askaide/helper/logger.dart'; import 'package:askaide/helper/platform.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:audioplayers/audioplayers.dart'; import 'package:flutter/material.dart'; import 'package:flutter_tts/flutter_tts.dart' as tts; import 'package:loading_animation_widget/loading_animation_widget.dart'; class AudioPlayerController { List audioSources = []; int currentAudioIndex = 0; late AudioPlayer player; late tts.FlutterTts flutterTts; Function()? onPlayStopped; Function()? onPlayAudioStarted; Function(bool loading)? onPlayAudioLoading; final bool useRemoteAPI; AudioPlayerController({required this.useRemoteAPI}) { if (useRemoteAPI) { player = AudioPlayer(); player.onPlayerComplete.listen((event) { if (currentAudioIndex < audioSources.length) { playNextAudioForPlayer(); } else { if (onPlayStopped != null) { onPlayStopped!(); } } }); } else { flutterTts = tts.FlutterTts(); if (PlatformTool.isIOS()) { flutterTts.setSharedInstance(true); flutterTts.setIosAudioCategory( tts.IosTextToSpeechAudioCategory.playback, [ tts.IosTextToSpeechAudioCategoryOptions.allowBluetooth, tts.IosTextToSpeechAudioCategoryOptions.allowBluetoothA2DP, tts.IosTextToSpeechAudioCategoryOptions.mixWithOthers, ], ); } flutterTts.setStartHandler(() { if (onPlayAudioStarted != null) { onPlayAudioStarted!(); } }); flutterTts.setErrorHandler((msg) { Logger.instance.e('TTS error: $msg'); }); flutterTts.setCancelHandler(() { if (onPlayStopped != null) { onPlayStopped!(); } }); flutterTts.completionHandler = () { if (onPlayStopped != null) { onPlayStopped!(); } }; } } void dispose() { if (useRemoteAPI) { player.dispose(); } else { flutterTts.stop(); } } Future playAudio(String text) async { await stop(); if (text.isEmpty) { return; } if (useRemoteAPI) { if (onPlayAudioStarted != null) { onPlayAudioStarted!(); } onPlayAudioLoading?.call(true); resetAudioSourcesForPlayer(await APIServer().textToVoice(text: text)); onPlayAudioLoading?.call(false); playNextAudioForPlayer(); } else { flutterTts.speak(text); } } Future stop() async { if (useRemoteAPI) { await player.stop(); if (onPlayStopped != null) { onPlayStopped!(); } } else { await flutterTts.stop(); } } Future playNextAudioForPlayer() async { if (audioSources.isEmpty) { return; } if (currentAudioIndex >= audioSources.length) { return; } await player.play(UrlSource(audioSources[currentAudioIndex])); currentAudioIndex++; } void resetAudioSourcesForPlayer(List sources) { audioSources = sources; currentAudioIndex = 0; } } class EnhancedAudioPlayer extends StatelessWidget { final AudioPlayerController controller; final bool loading; const EnhancedAudioPlayer( {super.key, required this.controller, this.loading = false}); @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const SizedBox(width: 100), loading ? Row( children: [ Text( '语音合成中,请稍候', style: TextStyle( color: customColors.weakTextColor, fontSize: 12, ), ), const SizedBox(width: 10), LoadingAnimationWidget.fourRotatingDots( color: const Color.fromARGB(255, 254, 170, 74), size: 12, ), ], ) : LoadingAnimationWidget.staggeredDotsWave( color: const Color.fromARGB(255, 254, 170, 74), size: 25, ), if (!loading) SizedBox( width: 100, child: TextButton.icon( onPressed: () { controller.stop(); }, icon: Icon( Icons.stop_circle_outlined, color: customColors.weakLinkColor, ), label: Text( '停止', style: TextStyle( color: customColors.weakLinkColor, fontSize: 12, ), ), ), ) else const SizedBox(width: 100), ], ); } } ================================================ FILE: lib/page/component/avatar_selector.dart ================================================ import 'dart:io'; import 'package:askaide/helper/haptic_feedback.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/image.dart'; import 'package:askaide/page/component/random_avatar.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; enum AvatarType { localFile, network, random, } class Avatar { final AvatarType type; final String? url; final int? id; const Avatar({ required this.type, this.url, this.id, }); } class AvatarSelector extends StatefulWidget { final Function(Avatar selected) onSelected; final int? defaultAvatarId; final String? defaultAvatarUrl; final List externalAvatarIds; final List externalAvatarUrls; final AvatarUsage usage; const AvatarSelector({ super.key, required this.onSelected, this.defaultAvatarId, this.defaultAvatarUrl, required this.usage, this.externalAvatarIds = const [], this.externalAvatarUrls = const [], }); @override State createState() => _AvatarSelectorState(); } class _AvatarSelectorState extends State { String? _avatarUrl; int? _avatarId; @override void initState() { super.initState(); _avatarId = widget.defaultAvatarId; _avatarUrl = widget.defaultAvatarUrl; } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return Column( children: [ Expanded( child: GridView.count( crossAxisCount: 3, crossAxisSpacing: 15, mainAxisSpacing: 15, padding: const EdgeInsets.only( top: 15, left: 8, right: 8, bottom: 10, ), children: [ buildAvatarSelectBox(customColors, context), for (String url in widget.externalAvatarUrls) _buildAvatarButton( customColors, Avatar( type: AvatarType.network, url: url, ), ), for (int id in widget.externalAvatarIds) _buildAvatarButton( customColors, Avatar(type: AvatarType.random, id: id), ), // ...List.generate( // 200 - // widget.externalAvatarIds.length - // widget.externalAvatarUrls.length, // (index) { // return _buildAvatarButton( // customColors, // Avatar( // type: AvatarType.random, // id: widget.randomSeed + index, // )); // }, // ), ], ), ), ], ); } Widget buildAvatarSelectBox(CustomColors customColors, BuildContext context) { return Material( borderRadius: CustomSize.borderRadius, child: InkWell( borderRadius: CustomSize.borderRadiusAll, onTap: () async { HapticFeedbackHelper.mediumImpact(); FilePickerResult? result = await FilePicker.platform.pickFiles(type: FileType.image); if (result != null && result.files.isNotEmpty) { setState(() { _avatarUrl = result.files.first.path; _avatarId = null; }); widget.onSelected(Avatar( type: _avatarUrl!.startsWith('http') ? AvatarType.network : AvatarType.localFile, url: _avatarUrl, )); } }, child: Container( decoration: BoxDecoration( borderRadius: CustomSize.borderRadius, ), child: _avatarId == null && _avatarUrl == null ? ClipRRect( borderRadius: CustomSize.borderRadius, child: SizedBox( height: 100, width: 100, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.camera_alt, size: 30, color: customColors.chatInputPanelText, ), const SizedBox(width: 10), Text( AppLocale.custom.getString(context), style: TextStyle( fontSize: 10, fontWeight: FontWeight.w500, color: customColors.chatInputPanelText?.withOpacity(0.8), ), ), ], ), ), ) : Stack( children: [ SizedBox( width: 100, height: 100, child: ClipRRect( borderRadius: CustomSize.borderRadius, child: _avatarUrl!.startsWith('http') ? CachedNetworkImageEnhanced( imageUrl: _avatarUrl!, ) : Image.file(File(_avatarUrl!)), ), ), Positioned( bottom: 0, left: 0, right: 0, child: ClipRRect( borderRadius: const BorderRadius.only(bottomLeft: CustomSize.radius, bottomRight: CustomSize.radius), child: Container( color: const Color.fromARGB(82, 0, 0, 0), height: 25, alignment: Alignment.center, child: Text( AppLocale.custom.getString(context), textAlign: TextAlign.center, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: Colors.white, ), ), ), ), ), ], ), ), ), ); } Widget _buildAvatarButton(CustomColors customColors, Avatar avatar) { return InkWell( onTap: () { setState(() { _avatarUrl = avatar.url; _avatarId = avatar.id; }); widget.onSelected(avatar); }, child: avatar.type == AvatarType.random ? RandomAvatar(id: avatar.id ?? 0, size: 80, usage: widget.usage) : ClipRRect( borderRadius: CustomSize.borderRadius, child: CachedNetworkImageEnhanced( imageUrl: avatar.url!, fit: BoxFit.fill, ), ), ); } } ================================================ FILE: lib/page/component/background_container.dart ================================================ import 'dart:ui'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/platform.dart'; import 'package:askaide/page/component/image.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/material.dart'; class BackgroundContainer extends StatefulWidget { final Widget child; final bool? useGradient; final SettingRepository setting; final bool enabled; final bool pureColorMode; final double maxWidth; final bool clickGrapFocus; final Color? backgroundColor; const BackgroundContainer({ super.key, required this.child, this.useGradient, required this.setting, this.enabled = true, this.pureColorMode = false, this.maxWidth = CustomSize.maxWindowSize, this.clickGrapFocus = true, this.backgroundColor, }); @override State createState() => _BackgroundContainerState(); } class _BackgroundContainerState extends State { final int opacity = 180; String? imageUrl; double? blur; @override void initState() { super.initState(); if (widget.enabled) { if ((widget.useGradient == null || widget.useGradient == false) && imageUrl == null) { imageUrl = widget.setting.get(settingBackgroundImage); blur = double.tryParse( widget.setting.get(settingBackgroundImageBlur) ?? '15.0', ); } widget.setting.listen((settings, key, value) { if (key == settingBackgroundImage) { if (mounted) { setState(() { imageUrl = value; }); } } if (key == settingBackgroundImageBlur) { if (mounted) { setState(() { blur = double.tryParse(value); }); } } }); } } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return GestureDetector( onTap: widget.clickGrapFocus ? () { FocusScope.of(context).requestFocus(FocusNode()); } : null, onHorizontalDragUpdate: (details) { // Only the mobile app supports horizontal swiping to go back to the previous page. if (PlatformTool.isAndroid() || PlatformTool.isIOS()) { int sensitivity = 15; if (details.delta.dx > sensitivity) { if (Navigator.of(context).canPop()) { Navigator.of(context).pop(); } } } }, child: Container( color: widget.backgroundColor ?? customColors.backgroundContainerColor, child: Align( alignment: Alignment.topCenter, child: ConstrainedBox( constraints: BoxConstraints( maxWidth: widget.maxWidth > 0 ? widget.maxWidth : double.infinity, ), child: _buildChild(customColors), ), ), ), ); } Widget _buildChild(CustomColors customColors) { if (widget.pureColorMode) { return Container( height: double.infinity, decoration: _createPureColorDecoration(customColors), child: widget.child, ); } if (!widget.enabled) { return Container( height: double.infinity, decoration: _createTransportDecoration(), child: widget.child, ); } if (widget.enabled && ((imageUrl != null && imageUrl != '') || widget.useGradient == true)) { return Container( height: double.infinity, decoration: widget.useGradient == true ? _createLinearGradientDecoration() : BoxDecoration( image: _resolveImage(), ), child: BackdropFilter( filter: ImageFilter.blur( sigmaX: blur ?? 15.0, sigmaY: blur ?? 15.0, ), child: widget.child, ), ); } return Container( decoration: _createPureColorDecoration(customColors), height: double.infinity, child: widget.child, ); } DecorationImage _resolveImage() { return DecorationImage( image: resolveImageProvider(imageUrl!), fit: BoxFit.cover, ); } BoxDecoration _createPureColorDecoration(CustomColors customColors) { return BoxDecoration( color: customColors.backgroundContainerColor, ); } BoxDecoration _createLinearGradientDecoration() { return BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ Color.fromARGB(opacity, 90, 218, 196), Color.fromARGB(opacity, 230, 153, 38), Color.fromARGB(opacity, 242, 7, 213), ], transform: const GradientRotation(0.5)), ); } BoxDecoration _createTransportDecoration() { return const BoxDecoration( color: Colors.transparent, ); } } ================================================ FILE: lib/page/component/bottom_sheet_box.dart ================================================ import 'package:askaide/helper/platform.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; class BottomSheetBox extends StatelessWidget { final Widget child; const BottomSheetBox({super.key, required this.child}); @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return Padding( padding: MediaQuery.of(context).viewInsets, child: Container( padding: EdgeInsets.only( left: 10, right: 10, bottom: PlatformTool.isDesktop() ? 10 : (MediaQuery.of(context).viewInsets.bottom > 0 ? 10 : 0)), child: SafeArea( bottom: false, child: Container( width: double.infinity, decoration: BoxDecoration( borderRadius: const BorderRadius.vertical(top: CustomSize.radius, bottom: CustomSize.radius), color: customColors.backgroundColor, ), padding: const EdgeInsets.only(top: 0, left: 10, right: 10), child: SafeArea( child: child, ), ), ), ), ); } } ================================================ FILE: lib/page/component/button.dart ================================================ import 'package:askaide/page/component/enhanced_button.dart'; import 'package:flutter/material.dart'; class Button extends StatelessWidget { final String title; final Function() onPressed; final ButtonSize size; final Color? backgroundColor; final Color? color; final Widget? icon; const Button({ super.key, required this.title, required this.onPressed, this.size = const ButtonSize.small(), this.backgroundColor, this.color, this.icon, }); @override Widget build(BuildContext context) { return EnhancedButton( width: size.width, height: size.height, fontSize: size.fontSize, title: title, onPressed: onPressed, backgroundColor: backgroundColor, color: color, icon: icon, ); } } class ButtonSize { final double width; final double height; final double fontSize; const ButtonSize({ required this.width, required this.height, required this.fontSize, }); const ButtonSize.full() : width = double.infinity, height = 42, fontSize = 16; const ButtonSize.small() : width = 80, height = 35, fontSize = 14; } ================================================ FILE: lib/page/component/chat/chat_bubble.dart ================================================ import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:flutter/material.dart'; /// 来源于 https://github.com/prahack/chat_bubbles/blob/master/lib/bubbles/bubble_special_one.dart class SpecialChatBubbleOne extends CustomPainter { final Color color; final Alignment alignment; final bool tail; SpecialChatBubbleOne({ required this.color, required this.alignment, required this.tail, }); final double _radius = CustomSize.radiusValue; final double _x = 5.0; @override void paint(Canvas canvas, Size size) { if (alignment == Alignment.topRight) { if (tail) { canvas.drawRRect( RRect.fromLTRBAndCorners( 0, 0, size.width - _x, size.height, bottomLeft: Radius.circular(_radius), bottomRight: Radius.circular(_radius), topLeft: Radius.circular(_radius), ), Paint() ..color = color ..style = PaintingStyle.fill); var path = Path(); path.moveTo(size.width - _x, 0); path.lineTo(size.width - _x, 10); path.lineTo(size.width, 0); canvas.clipPath(path); canvas.drawRRect( RRect.fromLTRBAndCorners( size.width - _x, 0.0, size.width, size.height, topRight: const Radius.circular(3), ), Paint() ..color = color ..style = PaintingStyle.fill); } else { canvas.drawRRect( RRect.fromLTRBAndCorners( 0, 0, size.width - _x, size.height, bottomLeft: Radius.circular(_radius), bottomRight: Radius.circular(_radius), topLeft: Radius.circular(_radius), topRight: Radius.circular(_radius), ), Paint() ..color = color ..style = PaintingStyle.fill); } } else { if (tail) { canvas.drawRRect( RRect.fromLTRBAndCorners( _x, 0, size.width, size.height, bottomRight: Radius.circular(_radius), topRight: Radius.circular(_radius), bottomLeft: Radius.circular(_radius), ), Paint() ..color = color ..style = PaintingStyle.fill); var path = Path(); path.moveTo(_x, 0); path.lineTo(_x, 10); path.lineTo(0, 0); canvas.clipPath(path); canvas.drawRRect( RRect.fromLTRBAndCorners( 0, 0.0, _x, size.height, topLeft: const Radius.circular(3), ), Paint() ..color = color ..style = PaintingStyle.fill); } else { canvas.drawRRect( RRect.fromLTRBAndCorners( _x, 0, size.width, size.height, bottomRight: Radius.circular(_radius), topRight: Radius.circular(_radius), bottomLeft: Radius.circular(_radius), topLeft: Radius.circular(_radius), ), Paint() ..color = color ..style = PaintingStyle.fill); } } } @override bool shouldRepaint(CustomPainter oldDelegate) { return true; } } class BubbleSpecialThree extends StatelessWidget { final bool isSender; final String text; final bool tail; final Color color; final bool sent; final bool delivered; final bool seen; final TextStyle textStyle; const BubbleSpecialThree({ Key? key, this.isSender = true, required this.text, this.color = Colors.white70, this.tail = true, this.sent = false, this.delivered = false, this.seen = false, this.textStyle = const TextStyle( color: Colors.black87, fontSize: 16, ), }) : super(key: key); ///chat bubble builder method @override Widget build(BuildContext context) { bool stateTick = false; Icon? stateIcon; if (sent) { stateTick = true; stateIcon = const Icon( Icons.done, size: 18, color: Color(0xFF97AD8E), ); } if (delivered) { stateTick = true; stateIcon = const Icon( Icons.done_all, size: 18, color: Color(0xFF97AD8E), ); } if (seen) { stateTick = true; stateIcon = const Icon( Icons.done_all, size: 18, color: Color(0xFF92DEDA), ); } return Align( alignment: isSender ? Alignment.topRight : Alignment.topLeft, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), child: CustomPaint( painter: SpecialChatBubbleThree( color: color, alignment: isSender ? Alignment.topRight : Alignment.topLeft, tail: tail), child: Container( constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * .7, ), margin: isSender ? stateTick ? const EdgeInsets.fromLTRB(7, 7, 14, 7) : const EdgeInsets.fromLTRB(7, 7, 17, 7) : const EdgeInsets.fromLTRB(17, 7, 7, 7), child: Stack( children: [ Padding( padding: stateTick ? const EdgeInsets.only(left: 4, right: 20) : const EdgeInsets.only(left: 4, right: 4), child: Text( text, style: textStyle, textAlign: TextAlign.left, ), ), stateIcon != null && stateTick ? Positioned( bottom: 0, right: 0, child: stateIcon, ) : const SizedBox( width: 1, ), ], ), ), ), ), ); } } ///custom painter use to create the shape of the chat bubble /// /// [color],[alignment] and [tail] can be changed class SpecialChatBubbleThree extends CustomPainter { final Color color; final Alignment alignment; final bool tail; SpecialChatBubbleThree({ required this.color, required this.alignment, required this.tail, }); final double _radius = CustomSize.radiusValue; @override void paint(Canvas canvas, Size size) { var h = size.height; var w = size.width; if (alignment == Alignment.topRight) { if (tail) { var path = Path(); /// starting point path.moveTo(_radius * 2, 0); /// top-left corner path.quadraticBezierTo(0, 0, 0, _radius * 1.5); /// left line path.lineTo(0, h - _radius * 1.5); /// bottom-left corner path.quadraticBezierTo(0, h, _radius * 2, h); /// bottom line path.lineTo(w - _radius * 3, h); /// bottom-right bubble curve path.quadraticBezierTo(w - _radius * 1.5, h, w - _radius * 1.5, h - _radius * 0.6); /// bottom-right tail curve 1 path.quadraticBezierTo(w - _radius * 1, h, w, h); /// bottom-right tail curve 2 path.quadraticBezierTo(w - _radius * 0.8, h, w - _radius, h - _radius * 1.5); /// right line path.lineTo(w - _radius, _radius * 1.5); /// top-right curve path.quadraticBezierTo(w - _radius, 0, w - _radius * 3, 0); canvas.clipPath(path); canvas.drawRRect( RRect.fromLTRBR(0, 0, w, h, Radius.zero), Paint() ..color = color ..style = PaintingStyle.fill); } else { var path = Path(); /// starting point path.moveTo(_radius * 2, 0); /// top-left corner path.quadraticBezierTo(0, 0, 0, _radius * 1.5); /// left line path.lineTo(0, h - _radius * 1.5); /// bottom-left corner path.quadraticBezierTo(0, h, _radius * 2, h); /// bottom line path.lineTo(w - _radius * 3, h); /// bottom-right curve path.quadraticBezierTo(w - _radius, h, w - _radius, h - _radius * 1.5); /// right line path.lineTo(w - _radius, _radius * 1.5); /// top-right curve path.quadraticBezierTo(w - _radius, 0, w - _radius * 3, 0); canvas.clipPath(path); canvas.drawRRect( RRect.fromLTRBR(0, 0, w, h, Radius.zero), Paint() ..color = color ..style = PaintingStyle.fill); } } else { if (tail) { var path = Path(); /// starting point path.moveTo(_radius * 3, 0); /// top-left corner path.quadraticBezierTo(_radius, 0, _radius, _radius * 1.5); /// left line path.lineTo(_radius, h - _radius * 1.5); // bottom-right tail curve 1 path.quadraticBezierTo(_radius * .8, h, 0, h); /// bottom-right tail curve 2 path.quadraticBezierTo(_radius * 1, h, _radius * 1.5, h - _radius * 0.6); /// bottom-left bubble curve path.quadraticBezierTo(_radius * 1.5, h, _radius * 3, h); /// bottom line path.lineTo(w - _radius * 2, h); /// bottom-right curve path.quadraticBezierTo(w, h, w, h - _radius * 1.5); /// right line path.lineTo(w, _radius * 1.5); /// top-right curve path.quadraticBezierTo(w, 0, w - _radius * 2, 0); canvas.clipPath(path); canvas.drawRRect( RRect.fromLTRBR(0, 0, w, h, Radius.zero), Paint() ..color = color ..style = PaintingStyle.fill); } else { var path = Path(); /// starting point path.moveTo(_radius * 3, 0); /// top-left corner path.quadraticBezierTo(_radius, 0, _radius, _radius * 1.5); /// left line path.lineTo(_radius, h - _radius * 1.5); /// bottom-left curve path.quadraticBezierTo(_radius, h, _radius * 3, h); /// bottom line path.lineTo(w - _radius * 2, h); /// bottom-right curve path.quadraticBezierTo(w, h, w, h - _radius * 1.5); /// right line path.lineTo(w, _radius * 1.5); /// top-right curve path.quadraticBezierTo(w, 0, w - _radius * 2, 0); canvas.clipPath(path); canvas.drawRRect( RRect.fromLTRBR(0, 0, w, h, Radius.zero), Paint() ..color = color ..style = PaintingStyle.fill); } } } @override bool shouldRepaint(CustomPainter oldDelegate) { return true; } } class BubbleSpecialTwo extends StatelessWidget { final bool isSender; final String text; final bool tail; final Color color; final bool sent; final bool delivered; final bool seen; final TextStyle textStyle; const BubbleSpecialTwo({ Key? key, this.isSender = true, required this.text, this.color = Colors.white70, this.tail = true, this.sent = false, this.delivered = false, this.seen = false, this.textStyle = const TextStyle( color: Colors.black87, fontSize: 16, ), }) : super(key: key); ///chat bubble builder method @override Widget build(BuildContext context) { bool stateTick = false; Icon? stateIcon; if (sent) { stateTick = true; stateIcon = const Icon( Icons.done, size: 18, color: Color(0xFF97AD8E), ); } if (delivered) { stateTick = true; stateIcon = const Icon( Icons.done_all, size: 18, color: Color(0xFF97AD8E), ); } if (seen) { stateTick = true; stateIcon = const Icon( Icons.done_all, size: 18, color: Color(0xFF92DEDA), ); } return Align( alignment: isSender ? Alignment.topRight : Alignment.topLeft, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), child: CustomPaint( painter: SpecialChatBubbleTwo( color: color, alignment: isSender ? Alignment.topRight : Alignment.topLeft, tail: tail), child: Container( constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * .8, ), margin: isSender ? stateTick ? const EdgeInsets.fromLTRB(7, 7, 14, 7) : const EdgeInsets.fromLTRB(7, 7, 17, 7) : const EdgeInsets.fromLTRB(17, 7, 7, 7), child: Stack( children: [ Padding( padding: stateTick ? const EdgeInsets.only(right: 20) : const EdgeInsets.symmetric(vertical: 0, horizontal: 0), child: Text( text, style: textStyle, textAlign: TextAlign.left, ), ), stateIcon != null && stateTick ? Positioned( bottom: 0, right: 0, child: stateIcon, ) : const SizedBox( width: 1, ), ], ), ), ), ), ); } } ///custom painter use to create the shape of the chat bubble /// /// [color],[alignment] and [tail] can be changed class SpecialChatBubbleTwo extends CustomPainter { final Color color; final Alignment alignment; final bool tail; SpecialChatBubbleTwo({ required this.color, required this.alignment, required this.tail, }); final double _radius = CustomSize.radiusValue; final double _x = 10.0; @override void paint(Canvas canvas, Size size) { if (alignment == Alignment.topRight) { if (tail) { canvas.drawRRect( RRect.fromLTRBAndCorners( 0, 0, size.width - 8, size.height, bottomLeft: Radius.circular(_radius), topRight: Radius.circular(_radius), topLeft: Radius.circular(_radius), bottomRight: Radius.circular(_radius), ), Paint() ..color = color ..style = PaintingStyle.fill); var path = Path(); path.moveTo(size.width - _x, 4); path.lineTo(size.width - _x, size.height - 5); path.lineTo(size.width, size.height); canvas.clipPath(path); canvas.drawRRect( RRect.fromLTRBAndCorners( size.width - _x, 0.0, size.width, size.height, ), Paint() ..color = color ..style = PaintingStyle.fill); } else { canvas.drawRRect( RRect.fromLTRBAndCorners( 0, 0, size.width - 8, size.height, bottomLeft: Radius.circular(_radius), topRight: Radius.circular(_radius), topLeft: Radius.circular(_radius), bottomRight: Radius.circular(_radius), ), Paint() ..color = color ..style = PaintingStyle.fill); } } else { if (tail) { canvas.drawRRect( RRect.fromLTRBAndCorners( 8, 0, size.width, size.height, bottomRight: Radius.circular(_radius), topRight: Radius.circular(_radius), topLeft: Radius.circular(_radius), bottomLeft: Radius.circular(_radius), ), Paint() ..color = color ..style = PaintingStyle.fill); var path = Path(); path.moveTo(_x, 4); path.lineTo(0, size.height); path.lineTo(_x, size.height - 5); canvas.clipPath(path); canvas.drawRRect( RRect.fromLTRBAndCorners( 0, 0.0, _x, size.height, topRight: Radius.circular(_radius), ), Paint() ..color = color ..style = PaintingStyle.fill); } else { canvas.drawRRect( RRect.fromLTRBAndCorners( 8, 0, size.width, size.height, bottomRight: Radius.circular(_radius), topRight: Radius.circular(_radius), topLeft: Radius.circular(_radius), bottomLeft: Radius.circular(_radius), ), Paint() ..color = color ..style = PaintingStyle.fill); } } } @override bool shouldRepaint(CustomPainter oldDelegate) { return true; } } ================================================ FILE: lib/page/component/chat/chat_input.dart ================================================ import 'dart:io'; import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/haptic_feedback.dart'; import 'package:askaide/helper/platform.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/chat/chat_input_button.dart'; import 'package:askaide/page/component/chat/file_upload.dart'; import 'package:askaide/page/component/chat/voice_record.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/file_preview.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:camera/camera.dart'; import 'package:camerawesome/camerawesome_plugin.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:go_router/go_router.dart'; import 'package:loading_animation_widget/loading_animation_widget.dart'; class ChatInput extends StatefulWidget { final Function(String value) onSubmit; final ValueNotifier enableNotifier; final bool enableImageUpload; final Function(List files)? onImageSelected; final List? selectedImageFiles; final Function()? onNewChat; final String hintText; final Function()? onVoiceRecordTappedEvent; final List Function()? toolsBuilder; final Function()? onStopGenerate; final Function(bool hasFocus)? onFocusChange; // Whether to enable file uploading final bool enableFileUpload; // Selected file for uploading final FileUpload? selectedFile; final Function(FileUpload? file)? onFileSelected; const ChatInput({ super.key, required this.onSubmit, required this.enableNotifier, this.enableImageUpload = true, this.onNewChat, this.hintText = '', this.onVoiceRecordTappedEvent, this.toolsBuilder, this.onImageSelected, this.selectedImageFiles, this.onStopGenerate, this.onFocusChange, this.enableFileUpload = false, this.selectedFile, this.onFileSelected, }); @override State createState() => _ChatInputState(); } class _ChatInputState extends State with TickerProviderStateMixin { final TextEditingController _textController = TextEditingController(); /// 用于监听键盘事件,实现回车发送消息,Shift+Enter换行 late final FocusNode _focusNode = FocusNode( onKeyEvent: (node, event) { if (!HardwareKeyboard.instance.isShiftPressed && event.logicalKey.keyLabel == 'Enter') { if (event is KeyDownEvent && widget.enableNotifier.value) { _handleSubmited(_textController.text.trim()); } return KeyEventResult.handled; } else { return KeyEventResult.ignored; } }, ); final maxLength = 150000; var hasCamera = false; var showExtensionButtons = false; // Whether to display the bottom tool bar var showBottomTools = false; // Whether the input box is focused var inputFocused = false; @override void initState() { super.initState(); if (!PlatformTool.isDesktopAndWeb()) { availableCameras().then((cameras) { setState(() { hasCamera = cameras.isNotEmpty; }); }); } _textController.addListener(() { setState(() {}); }); // 机器人回复完成后自动输入框自动获取焦点 if (!PlatformTool.isAndroid() && !PlatformTool.isIOS()) { widget.enableNotifier.addListener(() { if (widget.enableNotifier.value) { _focusNode.requestFocus(); } }); } } @override void dispose() { _textController.dispose(); super.dispose(); } // Whether the user can upload images bool get canUploadImage => widget.enableImageUpload && Ability().supportImageUploader && widget.onImageSelected != null && Ability().supportWebSocket; // Whether the user can upload files bool get canUploadFile => widget.enableFileUpload && Ability().supportWebSocket; @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return GestureDetector( onTap: () { _focusNode.requestFocus(); }, child: SafeArea( bottom: false, child: Container( margin: PlatformTool.isDesktopAndWeb() ? const EdgeInsets.all(8) : null, padding: const EdgeInsets.only(left: 16, right: 16, top: 8), decoration: BoxDecoration( color: customColors.chatInputAreaBackground, borderRadius: PlatformTool.isDesktopAndWeb() ? BorderRadius.circular(CustomSize.radiusValue) : const BorderRadius.only( topLeft: Radius.circular(CustomSize.radiusValue * 2), topRight: Radius.circular(CustomSize.radiusValue * 2), ), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.08), offset: const Offset(-1, -1), blurRadius: CustomSize.radiusValue, ), if (PlatformTool.isDesktopAndWeb()) BoxShadow( color: Colors.black.withOpacity(0.08), offset: const Offset(-1, -1), blurRadius: CustomSize.radiusValue, ), ], ), child: Builder(builder: (context) { final setting = context.read(); return SafeArea( child: Column( children: [ // 选中的图片预览 if (widget.selectedImageFiles != null && widget.selectedImageFiles!.isNotEmpty) buildSelectedImagePreview(customColors), // 选中文件预览 if (widget.selectedFile != null) buildSelectedFilePreview(customColors), // 聊天内容输入栏 Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Focus( onFocusChange: (hasFocus) { setState(() { inputFocused = hasFocus; }); widget.onFocusChange?.call(hasFocus); }, child: TextFormField( keyboardType: TextInputType.multiline, textInputAction: TextInputAction.newline, maxLines: inputFocused ? 10 : 3, minLines: 1, maxLength: maxLength, focusNode: _focusNode, controller: _textController, style: const TextStyle(fontSize: CustomSize.defaultHintTextSize), decoration: InputDecoration( hintText: widget.hintText, hintStyle: TextStyle( fontSize: CustomSize.defaultHintTextSize, color: customColors.textfieldHintColor, ), border: InputBorder.none, counterText: '', ), ), ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( mainAxisAlignment: MainAxisAlignment.start, children: [ if (widget.enableNotifier.value && (canUploadImage || canUploadFile)) buildUploadButtons(context, setting, customColors), if (widget.toolsBuilder != null) ...widget.toolsBuilder!(), ], ), _buildSendOrVoiceButton(context, customColors), ], ), AnimatedContainer( duration: const Duration(milliseconds: 200), height: showExtensionButtons && widget.enableNotifier.value ? 80 : 0, child: SingleChildScrollView( child: Row( children: [ if (canUploadImage && hasCamera) ChatInputSquareButton( icon: Icons.camera_alt, onPressed: () { onTakePhotoButtonPressed(context, customColors); }, text: AppLocale.takePhoto.getString(context), ), if (canUploadImage) ChatInputSquareButton( icon: Icons.photo_library, onPressed: () { onImageUploadButtonPressed(); }, text: AppLocale.photoLibrary.getString(context), ), if (canUploadFile) ChatInputSquareButton( icon: Icons.upload_file_sharp, onPressed: () { onFileUploadButtonPressed(); }, text: AppLocale.fileLibrary.getString(context), ), ], ), ), ), SizedBox(height: PlatformTool.isMobile() && inputFocused ? 8 : 6), ], ), ], ), ); }), ), ), ); } Widget buildSelectedFilePreview(CustomColors customColors) { var maxWidth = MediaQuery.of(context).size.width * 0.8; if (maxWidth > 300) { maxWidth = 300; } return SizedBox( height: 30, child: ListView( scrollDirection: Axis.horizontal, children: [ Container( margin: const EdgeInsets.only(right: 8), padding: const EdgeInsets.all(5), child: Stack( children: [ FilePreview( fileType: widget.selectedFile!.file.extension ?? '', maxWidth: maxWidth, filename: widget.selectedFile!.file.name, ), if (widget.enableNotifier.value) Positioned( right: 5, top: 5, child: InkWell( onTap: () { setState(() { widget.onFileSelected?.call(null); }); }, child: Container( padding: const EdgeInsets.all(3), decoration: BoxDecoration( borderRadius: CustomSize.borderRadius, color: customColors.chatRoomBackground, ), child: Icon( Icons.close, size: 10, color: customColors.weakTextColor, ), ), ), ), ], ), ), ], ), ); } Widget buildSelectedImagePreview(CustomColors customColors) { return SizedBox( height: 110, child: ListView( scrollDirection: Axis.horizontal, children: widget.selectedImageFiles! .map( (e) => Container( margin: const EdgeInsets.only(right: 8), padding: const EdgeInsets.all(5), child: Stack( children: [ ClipRRect( borderRadius: CustomSize.borderRadius, child: e.file.bytes != null ? Image.memory( e.file.bytes!, fit: BoxFit.cover, width: 100, height: 100, ) : Image.file( File(e.file.path!), fit: BoxFit.cover, width: 100, height: 100, ), ), if (widget.enableNotifier.value) Positioned( right: 5, top: 5, child: InkWell( onTap: () { setState(() { widget.selectedImageFiles!.remove(e); widget.onImageSelected?.call(widget.selectedImageFiles!); }); }, child: Container( padding: const EdgeInsets.all(3), decoration: BoxDecoration( borderRadius: CustomSize.borderRadius * 3, color: customColors.chatRoomBackground, border: Border.all( color: customColors.weakTextColor ?? Colors.white, width: 1, ), ), child: Icon( Icons.close, size: 14, color: customColors.weakTextColor, ), ), ), ), ], ), ), ) .toList(), ), ); } /// 构建发送或者语音按钮 Widget _buildSendOrVoiceButton( BuildContext context, CustomColors customColors, ) { if (!widget.enableNotifier.value) { return InkWell( onTap: () { if (widget.onStopGenerate != null) { openConfirmDialog( context, AppLocale.confirmStopOutput.getString(context), () { widget.onStopGenerate!(); HapticFeedbackHelper.heavyImpact(); }, danger: true, ); } }, child: LoadingAnimationWidget.beat( color: customColors.linkColor ?? Colors.green, size: 20, ), ); } return _textController.text == '' && Ability().supportVoiceChat ? IconButton( onPressed: () { HapticFeedbackHelper.mediumImpact(); openModalBottomSheet( context, (context) { return VoiceRecord( onFinished: (text) { _textController.text = text; Navigator.pop(context); }, onStart: () { widget.onVoiceRecordTappedEvent?.call(); }, ); }, isScrollControlled: false, heightFactor: 0.8, ); }, icon: Icon( Icons.mic_none_outlined, color: customColors.chatInputPanelText, ), splashRadius: 20, color: customColors.chatInputPanelText, ) : IconButton( onPressed: () => _handleSubmited(_textController.text.trim()), icon: Icon( Icons.send, color: customColors.chatInputPanelText, ), splashRadius: 20, color: customColors.chatInputPanelText, ); } // Image upload button event void onImageUploadButtonPressed() async { HapticFeedbackHelper.mediumImpact(); if (widget.selectedImageFiles != null && widget.selectedImageFiles!.length >= 4) { showSuccessMessage(AppLocale.uploadImageLimit4.getString(context)); return; } FilePickerResult? result = await FilePicker.platform.pickFiles( type: FileType.image, allowMultiple: true, allowCompression: true, ); if (result != null && result.files.isNotEmpty) { final files = widget.selectedImageFiles ?? []; files.addAll(result.files.map((e) => FileUpload(file: e)).toList()); widget.onImageSelected?.call(files.sublist(0, files.length > 4 ? 4 : files.length)); } } // File upload button event void onFileUploadButtonPressed() async { HapticFeedbackHelper.mediumImpact(); FilePickerResult? result = await FilePicker.platform.pickFiles( type: FileType.custom, allowedExtensions: ['pdf', 'docx', 'txt', 'md'], allowMultiple: false, ); if (result != null && result.files.isNotEmpty) { if (widget.onFileSelected != null) { widget.onFileSelected?.call(FileUpload(file: result.files.first)); } } } /// Build image or file upload button Widget buildUploadButtons( BuildContext context, SettingRepository setting, CustomColors customColors, ) { return InkWell( onTap: () { setState(() { showExtensionButtons = !showExtensionButtons; }); }, child: Container( decoration: BoxDecoration( color: customColors.tagsBackground, shape: BoxShape.circle, ), padding: const EdgeInsets.all(4), child: AnimatedRotation( turns: showExtensionButtons ? 0.125 : 0, duration: const Duration(milliseconds: 200), child: Icon( Icons.add, color: customColors.chatInputPanelText, size: 18, ), ), ), ); } // Take a photo void onTakePhotoButtonPressed(BuildContext context, CustomColors customColors) { HapticFeedbackHelper.mediumImpact(); Navigator.push( context, MaterialPageRoute( builder: (context) => Scaffold( appBar: AppBar( title: Text(AppLocale.takePhoto.getString(context)), backgroundColor: customColors.backgroundColor, ), body: CameraAwesomeBuilder.awesome( saveConfig: SaveConfig.photo(), enablePhysicalButton: true, onMediaCaptureEvent: (mediaCapture) async { if (mediaCapture.status == MediaCaptureStatus.success) { final file = FileUpload( file: PlatformFile( path: mediaCapture.captureRequest.path!, name: mediaCapture.captureRequest.path!.split('/').last, size: await File(mediaCapture.captureRequest.path!).length(), )); final files = widget.selectedImageFiles ?? []; files.add(file); widget.onImageSelected?.call(files.sublist(0, files.length > 4 ? 4 : files.length)); if (context.mounted) { context.pop(); } } }, ), ), ), ); } /// 处理输入框提交 void _handleSubmited(String text, {bool notSend = false}) { if (notSend) { var cursorPos = _textController.selection.base.offset; if (cursorPos < 0) { _textController.text = text; } else { String suffixText = _textController.text.substring(cursorPos); String prefixText = _textController.text.substring(0, cursorPos); _textController.text = prefixText + text + suffixText; _textController.selection = TextSelection( baseOffset: cursorPos + text.length, extentOffset: cursorPos + text.length, ); } _focusNode.requestFocus(); return; } if (text != '') { widget.onSubmit(text); _textController.clear(); } } } ================================================ FILE: lib/page/component/chat/chat_input_button.dart ================================================ import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; class ChatInputButton extends StatefulWidget { final String text; final IconData icon; final VoidCallback onPressed; final bool isActive; const ChatInputButton( {super.key, required this.text, required this.icon, required this.onPressed, this.isActive = false}); @override State createState() => _ChatInputButtonState(); } class _ChatInputButtonState extends State { @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return IconButton( onPressed: widget.onPressed, icon: Container( decoration: BoxDecoration( color: widget.isActive ? customColors.linkColor?.withAlpha(100) : null, borderRadius: BorderRadius.circular(CustomSize.radiusValue * 2), ), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: Row( children: [ Icon( widget.icon, color: widget.isActive ? customColors.linkColor : customColors.chatInputPanelText, size: 18, ), const SizedBox(width: 4), Text( widget.text, style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: widget.isActive ? customColors.linkColor : customColors.chatInputPanelText, ), ), ], ), ), style: ButtonStyle( overlayColor: WidgetStateProperty.all(Colors.transparent), ), ); } } class ChatInputSquareButton extends StatelessWidget { final IconData icon; final VoidCallback onPressed; final bool isActive; final String text; const ChatInputSquareButton( {super.key, required this.icon, required this.onPressed, this.isActive = false, required this.text}); @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return Container( margin: const EdgeInsets.only(right: 8), child: IconButton( onPressed: onPressed, icon: Container( decoration: BoxDecoration( color: isActive ? customColors.linkColor?.withAlpha(100) : null, borderRadius: BorderRadius.circular(CustomSize.radiusValue * 2), ), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: Column( children: [ Icon( icon, color: isActive ? customColors.linkColor : customColors.chatInputPanelText, size: 30, ), const SizedBox(height: 4), Text( text, style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: isActive ? customColors.linkColor : customColors.chatInputPanelText, ), ), ], ), ), ), ); } } ================================================ FILE: lib/page/component/chat/chat_preview.dart ================================================ import 'dart:async'; import 'dart:convert'; import 'package:askaide/bloc/chat_message_bloc.dart'; import 'package:askaide/bloc/room_bloc.dart'; import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/haptic_feedback.dart'; import 'package:askaide/helper/helper.dart'; import 'package:askaide/helper/platform.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/attached_button_panel.dart'; import 'package:askaide/page/component/chat/chat_share.dart'; import 'package:askaide/page/component/chat/enhanced_selectable_text.dart'; import 'package:askaide/page/component/chat/file_upload.dart'; import 'package:askaide/page/component/chat/message_state_manager.dart'; import 'package:askaide/page/component/chat/search_result.dart'; import 'package:askaide/page/component/chat/thinking_card.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/file_preview.dart'; import 'package:askaide/page/component/random_avatar.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/chat/markdown.dart'; import 'package:askaide/repo/model/message.dart'; import 'package:clipboard/clipboard.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; import 'package:loading_animation_widget/loading_animation_widget.dart'; import 'package:quickalert/models/quickalert_type.dart'; import 'package:url_launcher/url_launcher_string.dart'; class ChatPreview extends StatefulWidget { final List messages; final ScrollController? scrollController; final void Function(int id)? onDeleteMessage; final void Function()? onResetContext; final ChatPreviewController controller; final MessageStateManager? stateManager; final List? helpWidgets; final Widget? robotAvatar; final Widget? Function(Message message)? avatarBuilder; final Widget? Function(Message message)? senderNameBuilder; final bool supportBloc; final void Function(Message message)? onSpeakEvent; final void Function(Message message, int index)? onResentEvent; final EdgeInsetsGeometry? padding; final Widget Function(Message message)? messageFooterBuilder; const ChatPreview({ super.key, required this.messages, this.scrollController, this.onDeleteMessage, this.onResetContext, required this.controller, this.stateManager, this.robotAvatar, this.avatarBuilder, this.senderNameBuilder, this.helpWidgets, this.onSpeakEvent, this.onResentEvent, this.supportBloc = true, this.padding, this.messageFooterBuilder, }); @override State createState() => _ChatPreviewState(); } class _ChatPreviewState extends State { @override void initState() { widget.controller.addListener(() { setState(() {}); }); super.initState(); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; var messages = widget.messages.reversed.toList(); return ListView.builder( controller: widget.scrollController, itemCount: messages.length, shrinkWrap: true, reverse: true, physics: const AlwaysScrollableScrollPhysics(), padding: widget.padding, cacheExtent: MediaQuery.of(context).size.height * 10, itemBuilder: (context, index) { final message = messages[index]; return Column( children: [ // 消息类型为 hide,不展示 if (message.message.type == MessageType.hide) Container(), if (message.message.type != MessageType.hide) Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ // 消息选择模式,显示选择框 if (widget.controller.selectMode && !message.message.isSystem()) Checkbox( value: widget.controller.isMessageSelected(message.message.id!), activeColor: customColors.linkColor, onChanged: (value) { if (value != null && value) { widget.controller.selectMessage(message.message.id!); } else { widget.controller.unSelectMessage(message.message.id!); } }, ), // 消息主体部分 Expanded( child: widget.supportBloc ? BlocBuilder( buildWhen: (previous, current) => (current is ChatMessageUpdated && current.message.id == message.message.id), builder: (context, state) { return Container( padding: const EdgeInsets.all(5), child: _buildMessageBox( context, customColors, _resolveMessage(state, message), message.state, index, ), ); }, ) : Container( padding: const EdgeInsets.all(5), child: _buildMessageBox( context, customColors, message.message, message.state, index, ), ), ), ], ), if (index == 0 && widget.helpWidgets != null && !message.message.isSystem()) for (var widget in widget.helpWidgets!) widget, ], ); }, ); } Message _resolveMessage(ChatMessageState state, MessageWithState message) { if (state is ChatMessageUpdated && state.message.id == message.message.id) { return state.message; } return message.message; } final Map _displayThinkingProcess = {}; /// 构建消息框 Widget _buildMessageBox( BuildContext context, CustomColors customColors, Message message, MessageState state, int index, ) { // 系统消息 if (message.isSystem()) { return Align( alignment: Alignment.center, child: Container( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 5, ), child: Text( message.isTimeline() ? message.friendlyTime() : message.text.getString(context), style: Theme.of(context).textTheme.bodySmall, ), ), ); } final showTranslate = state.showTranslate && state.translateText != null && state.translateText != ''; final extra = message.decodeExtra(); final extraInfo = index == 0 && extra != null ? extra['info'] ?? '' : ''; final reasoning = extra != null ? extra['reasoning'] ?? '' : ''; final states = extra != null ? extra['states'] ?? [] : []; var referenceDocuments = []; try { final referenceDocumentsData = extra != null ? extra['reference-documents'] ?? '[]' : '[]'; final List decodedDocs = jsonDecode(referenceDocumentsData); referenceDocuments = decodedDocs.map((e) => ReferenceDocument.fromJson(e)).toList().cast(); } catch (e) { print('------------> $e <-----------'); referenceDocuments = []; } var searchResults = []; try { final searchResultsData = extra != null ? extra['search-results'] ?? '[]' : '[]'; final List decodedDocs = jsonDecode(searchResultsData); searchResults = decodedDocs.map((e) => ReferenceDocument.fromJson(e)).toList().cast(); } catch (e) { print('------------> $e <-----------'); } final stateWidgets = []; if (states.isNotEmpty) { final lastState = states[states.length - 1]; switch (lastState) { case 'searching': if (index == 0) { stateWidgets.add(const SearchResult(searchResults: [], isSearching: true)); } break; case 'thinking': if (index == 0) { if (reasoning != '') { stateWidgets.add(ThinkingCard( content: reasoning, title: AppLocale.thinkingProcess.getString(context), isExpanded: true, onTap: (displayThinkingProcess) {}, )); } else { stateWidgets.add(Row( children: [ Text( AppLocale.robotIsThinkingMessage.getString(context), style: TextStyle( fontSize: 14, color: customColors.weakTextColorLess!, ), ), const SizedBox(width: 10), LoadingAnimationWidget.waveDots( color: customColors.weakTextColorLess!, size: 16, ), ], )); } } break; case 'thinking-done': if (reasoning != '') { final timeConsumed = extra != null ? extra['thinking_time_consumed'] ?? 0.0 : 0.0; stateWidgets.add(ThinkingCard( content: reasoning, title: AppLocale.thinkingProcess.getString(context), timeConsumed: timeConsumed.toDouble(), isExpanded: _displayThinkingProcess[message.id ?? -1] ?? false, onTap: (displayThinkingProcess) { setState(() { _displayThinkingProcess[message.id ?? -1] = displayThinkingProcess; }); }, )); } break; default: } } // 普通消息 return Align( alignment: message.role == Role.sender ? Alignment.topRight : Alignment.topLeft, child: ConstrainedBox( constraints: BoxConstraints(maxWidth: _chatBoxMaxWidth(context)), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, children: [ // 文件 if (message.file != null) Container( margin: message.role == Role.sender ? const EdgeInsets.fromLTRB(0, 0, 10, 7) : const EdgeInsets.fromLTRB(10, 0, 0, 7), padding: const EdgeInsets.only(bottom: 5, left: 5), constraints: BoxConstraints( maxWidth: _chatBoxFilePreviewWidth(context), ), child: Builder(builder: (context) { try { final file = jsonDecode(message.file!); final filename = file['name']; // final fileUrl = file['url']; return FilePreview( filename: filename, fileType: filename.split('.').last, mainAxisAlignment: MainAxisAlignment.end, ); } catch (e) { return FilePreview( fileType: '', filename: AppLocale.unknownFile.getString(context), mainAxisAlignment: MainAxisAlignment.end, ); } }), ), // 图片 if (message.images != null && message.images!.isNotEmpty) Container( margin: message.role == Role.sender ? const EdgeInsets.fromLTRB(0, 0, 10, 7) : const EdgeInsets.fromLTRB(10, 0, 0, 7), constraints: BoxConstraints( maxWidth: _chatBoxImagePreviewWidth( context, (message.images ?? []).length, ), ), child: FileUploadPreview(images: message.images ?? []), ), // 消息主体 Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ // 消息头像 Container( margin: const EdgeInsets.only(left: 10), child: Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, children: [ buildAvatar(message), // 发送人名称 if (message.role == Role.receiver && widget.senderNameBuilder != null) widget.senderNameBuilder!(message) ?? const SizedBox(), ], ), ), // 消息内容部分 ConstrainedBox( constraints: BoxConstraints( maxWidth: _chatBoxMaxWidth(context) - 30, ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Wrap( crossAxisAlignment: WrapCrossAlignment.end, children: [ // 错误指示器 if (message.role == Role.sender && message.statusIsFailed()) buildErrorIndicator(message, state, context, index), // 搜索结果 if (searchResults.isNotEmpty) Container( margin: const EdgeInsets.only(left: 10, top: 10), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), child: SearchResult(searchResults: searchResults), ), // 消息过程状态 if (states.isNotEmpty) Container( margin: EdgeInsets.only(left: 10, top: searchResults.isEmpty ? 10 : 0), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: stateWidgets, ), ), // 消息主体 GestureDetector( // 选择模式下,单击切换选择与否 // 非选择模式下,单击隐藏键盘 onTap: () { if (widget.controller.selectMode) { widget.controller.toggleMessageSelected(message.id!); } FocusScope.of(context).requestFocus(FocusNode()); }, // 长按或者双击显示上下文菜单 onLongPressStart: (detail) { if (PlatformTool.isDesktop()) { return; } _handleMessageTapControl( context, detail.globalPosition, message, state, index, ); }, onDoubleTapDown: (details) { if (PlatformTool.isDesktop()) { return; } _handleMessageTapControl( context, details.globalPosition, message, state, index, ); }, onSecondaryTapDown: (details) { _handleMessageTapControl( context, details.globalPosition, message, state, index, ); }, child: Stack( children: [ Container( margin: message.role == Role.sender ? const EdgeInsets.fromLTRB(0, 0, 10, 7) : const EdgeInsets.fromLTRB(10, 0, 0, 7), decoration: BoxDecoration( borderRadius: CustomSize.borderRadius, color: message.role == Role.receiver ? customColors.chatRoomReplyBackground : customColors.chatRoomSenderBackground, ), padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 8, ), child: Builder( builder: (context) { var text = message.text; if (!message.isReady && text != '') { text += ' ▌'; } return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ state.showMarkdown ? Markdown( data: text.trim(), onUrlTap: (value) => onMarkdownUrlTap(value), citations: searchResults.map((e) => e.source).toList(), ) : SelectableText( text, style: TextStyle( color: customColors.chatRoomSenderText, ), ), ], ); }, ), ), if (extraInfo.isNotEmpty) Positioned( top: 0, right: 0, child: InkWell( onTap: () { showCustomBeautyDialog( context, type: QuickAlertType.warning, confirmBtnText: AppLocale.gotIt.getString(context), showCancelBtn: false, title: AppLocale.goodTips.getString(context), child: Markdown( data: extraInfo, onUrlTap: (value) { onMarkdownUrlTap(value); context.pop(); }, textStyle: TextStyle( fontSize: 14, color: customColors.dialogDefaultTextColor, ), ), ); }, child: Icon( Icons.info_outline, size: 16, color: customColors.weakLinkColor?.withAlpha(50), ), ), ), ], ), ), ], ), if (showTranslate) Container( margin: message.role == Role.sender ? const EdgeInsets.fromLTRB(7, 10, 14, 7) : const EdgeInsets.fromLTRB(10, 10, 0, 7), decoration: BoxDecoration( borderRadius: CustomSize.borderRadius, color: message.role == Role.receiver ? customColors.chatRoomReplyBackgroundSecondary : customColors.chatRoomSenderBackgroundSecondary, ), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), child: Builder( builder: (context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ state.showMarkdown ? Markdown(data: state.translateText!) : SelectableText( state.translateText!, style: TextStyle( color: customColors.chatRoomSenderText, ), ), Row( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.check_circle, size: 12, color: Colors.green, ), const SizedBox(width: 5), Text( AppLocale.translateFinished.getString(context), style: const TextStyle( fontSize: 12, color: Color.fromARGB(255, 145, 145, 145), ), ), ], ), ], ); }, ), ), if (referenceDocuments.isNotEmpty) Container( margin: const EdgeInsets.only(left: 20), child: ReferenceDocumentWidget(referenceDocuments: referenceDocuments), ), if (widget.messageFooterBuilder != null) widget.messageFooterBuilder!(message), ], ), ), ], ), ], ), ), ); } Widget buildErrorIndicator( Message message, MessageState state, BuildContext context, int index, ) { return Container( margin: const EdgeInsets.only(right: 5, bottom: 10), child: GestureDetector( onTapUp: (details) { if (widget.controller.selectMode || message.isSystem()) { return; } HapticFeedbackHelper.mediumImpact(); var confirmMessage = ''; if (message.extra != null && message.extra!.isNotEmpty) { try { final extra = jsonDecode(message.extra!); if (extra['error'] != null && extra['error'] != '') { var e1 = extra['error']; try { e1 = (e1 as String).getString(context); // ignore: empty_catches } catch (ignored) {} confirmMessage = e1; } // ignore: empty_catches } catch (ignored) {} } openConfirmDialog( context, confirmMessage, () { widget.onResentEvent!(message, index); }, title: Text(AppLocale.robotHasSomeError.getString(context)), confirmText: AppLocale.sendRetry.getString(context), ); }, child: const Icon(Icons.error, color: Colors.red, size: 20), ), ); } void onMarkdownUrlTap(value) { if (value.startsWith("aidea-app://")) { var route = value.substring('aidea-app://'.length); context.push(route); } else if (value.startsWith("aidea-command://")) { var command = value.substring('aidea-command://'.length); switch (command) { case "reset-context": if (widget.onResetContext != null) { widget.onResetContext!(); } break; } } else { launchUrlString(value); } } Widget avatarWrap(Widget avatar) { return avatar; } Widget buildAvatar(Message message) { if (widget.avatarBuilder != null) { final avatar = widget.avatarBuilder!(message); if (avatar != null) { return avatarWrap(avatar); } } if (widget.robotAvatar != null) { if (message.role == Role.receiver && message.avatarUrl != null && (message.roomId ?? 1) <= 1) { return avatarWrap(RemoteAvatar( avatarUrl: message.avatarUrl!, size: 30, )); } if (message.role == Role.receiver) { return avatarWrap(widget.robotAvatar!); } } return const SizedBox(); } /// 点击消息后控制操作弹窗菜单 void _handleMessageTapControl( BuildContext context, Offset? offset, Message message, MessageState state, int index, ) { if (widget.controller.selectMode || message.isSystem()) { return; } HapticFeedbackHelper.mediumImpact(); final showTranslate = state.showTranslate && state.translateText != null && state.translateText != ''; BotToast.showAttachedWidget( target: offset, duration: const Duration(seconds: 8), animationDuration: const Duration(milliseconds: 200), animationReverseDuration: const Duration(milliseconds: 200), preferDirection: PreferDirection.topCenter, ignoreContentClick: false, onlyOne: true, allowClick: true, enableSafeArea: true, attachedBuilder: (cancel) => AttachedButtonPanel( buttons: [ // 文本、Markdown 模式切换 TextButton.icon( onPressed: () { openFullscreenDialog( context, child: Container( margin: const EdgeInsets.only(top: 15, bottom: 30), child: EnhancedSelectableText( text: message.text, ), ), title: AppLocale.selectText.getString(context), ); cancel(); }, label: const Text(''), icon: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( state.showMarkdown ? Icons.text_format : Icons.preview, color: const Color.fromARGB(255, 255, 255, 255), size: 14, ), Text( AppLocale.text.getString(context), style: const TextStyle(fontSize: 12, color: Colors.white), ), ], ), ), // 复制文本 TextButton.icon( onPressed: () { FlutterClipboard.copy(message.text).then((value) { showSuccessMessage(AppLocale.textCopied.getString(context)); }); cancel(); }, label: const Text(''), icon: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.copy, color: Color.fromARGB(255, 255, 255, 255), size: 14, ), Text( AppLocale.copy.getString(context), style: const TextStyle(fontSize: 12, color: Colors.white), ), ], ), ), // 翻译 if (Ability().supportTranslate && widget.stateManager != null) TextButton.icon( onPressed: () { cancel(); if (showTranslate) { widget.stateManager! .setState(message.roomId!, message.id!, state..showTranslate = false) .then((value) { setState(() {}); context.read().add(RoomLoadEvent(message.roomId!, cascading: false)); }); } else { if (state.translateText != null && state.translateText != '') { widget.stateManager! .setState(message.roomId!, message.id!, state..showTranslate = true) .then((value) { setState(() {}); context.read().add(RoomLoadEvent(message.roomId!, cascading: false)); }); return; } APIServer().translate(message.text).then((value) { widget.stateManager! .setState( message.roomId!, message.id!, state ..translateText = value.result! ..showTranslate = true, ) .then((value) { setState(() {}); context.read().add(RoomLoadEvent(message.roomId!, cascading: false)); }); }).onError((error, stackTrace) { showErrorMessage(resolveError(context, error!)); }); } }, label: const Text(''), icon: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.translate, color: Color.fromARGB(255, 255, 255, 255), size: 14, ), Text( showTranslate ? AppLocale.hide.getString(context) : AppLocale.translate.getString(context), style: const TextStyle(fontSize: 12, color: Colors.white), ) ], )), // 分享 TextButton.icon( onPressed: () async { cancel(); var messages = []; if (message.role == Role.receiver) { final questions = widget.messages.where((e) => e.message.id == message.refId).toList(); if (questions.isNotEmpty) { var q = questions.first; messages.add(ChatShareMessage( content: q.message.text, images: q.message.images, leftSide: false, )); } } messages.add(ChatShareMessage( content: message.text, images: message.images, leftSide: message.role == Role.receiver, avatarURL: message.avatarUrl, username: message.senderName, )); if (message.role == Role.sender) { final answers = widget.messages.where((e) => e.message.refId == message.id).toList(); if (answers.isNotEmpty) { for (var a in answers) { messages.add(ChatShareMessage( content: a.message.text, images: a.message.images, leftSide: true, avatarURL: a.message.avatarUrl, username: a.message.senderName, )); } } } Navigator.push( context, MaterialPageRoute( fullscreenDialog: true, builder: (context) => ChatShareScreen(messages: messages), ), ); // await shareTo(context, content: message.text, title: '聊天记录'); }, label: const Text(''), icon: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.share, color: Color.fromARGB(255, 255, 255, 255), size: 14, ), Text( AppLocale.share.getString(context), style: const TextStyle(fontSize: 12, color: Colors.white), ) ], )), // 选择 TextButton.icon( onPressed: () { widget.controller.enterSelectMode(); cancel(); }, label: const Text(''), icon: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.select_all, color: Color.fromARGB(255, 255, 255, 255), size: 14, ), Text( AppLocale.select.getString(context), style: const TextStyle(fontSize: 12, color: Colors.white), ) ], )), // 删除 if (widget.onDeleteMessage != null) TextButton.icon( onPressed: () { widget.onDeleteMessage!(message.id!); cancel(); }, label: const Text(''), icon: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.delete_outline, color: Color.fromARGB(255, 255, 255, 255), size: 14, ), Text( AppLocale.delete.getString(context), style: const TextStyle(fontSize: 12, color: Colors.white), ) ], ), ), // 文本转语音 if (Ability().supportSpeak && widget.onSpeakEvent != null) TextButton.icon( onPressed: () { cancel(); widget.onSpeakEvent!(message); }, label: const Text(''), icon: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.record_voice_over_outlined, color: Color.fromARGB(255, 255, 255, 255), size: 14, ), Text( AppLocale.readByVoice.getString(context), style: const TextStyle(fontSize: 12, color: Colors.white), ) ], ), ), // 重发 if (message.role == Role.sender && widget.onResentEvent != null) TextButton.icon( onPressed: () { widget.onResentEvent!(message, index); cancel(); }, label: const Text(''), icon: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.restore, color: Color.fromARGB(255, 255, 255, 255), size: 14, ), Text( AppLocale.sendRetryS.getString(context), style: const TextStyle(fontSize: 12, color: Colors.white), ), ], ), ), // 信息 if (message.quotaConsumed != null && message.quotaConsumed! > 0) TextButton.icon( onPressed: () { showBeautyDialog( context, type: QuickAlertType.info, text: '本轮对话共 ${message.tokenConsumed} 个 Token, 消耗 ${message.quotaConsumed} 个智慧果。', confirmBtnText: AppLocale.gotIt.getString(context), showCancelBtn: false, ); cancel(); }, label: const Text(''), icon: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.info_outline, color: Color.fromARGB(255, 255, 255, 255), size: 14, ), Text( AppLocale.info.getString(context), style: const TextStyle(fontSize: 12, color: Colors.white), ), ], ), ) ], ), ); } /// 获取聊天框的最大宽度 double _chatBoxMaxWidth(BuildContext context) { var screenWidth = MediaQuery.of(context).size.width; if (screenWidth >= CustomSize.maxWindowSize) { return CustomSize.maxWindowSize; } return screenWidth; } /// 获取图片预览的最大宽度 double _chatBoxImagePreviewWidth(BuildContext context, int imageCount) { final expect = _chatBoxMaxWidth(context) / 1.3; final max = imageCount > 1 ? 600.0 : 400.0; return expect > max ? max : expect; } // 获取文件预览的最大宽度 double _chatBoxFilePreviewWidth(BuildContext context) { var maxWidth = MediaQuery.of(context).size.width * 0.8; if (maxWidth > 300) { maxWidth = 300; } return maxWidth; } } /// ChatPreview 控制器 class ChatPreviewController extends ChangeNotifier { /// 是否处于多选模式 bool _selectMode = false; /// 选中的消息ID final _selectedMessageIds = {}; /// 所有消息 List? _allMessages; bool get selectMode => _selectMode; Set get selectedMessageIds => _selectedMessageIds; /// 获取选中的消息 List selectedMessages() { if (_allMessages == null || _allMessages!.isEmpty) { return []; } return _allMessages!.where((element) => _selectedMessageIds.contains(element.message.id)).toList(); } /// 设置所有消息 void setAllMessageIds(List messages) { _allMessages = messages.where((e) => !e.message.isSystem()).toList(); } void toggleSelectMode() { _selectMode = !_selectMode; notifyListeners(); } void exitSelectMode() { _selectMode = false; _selectedMessageIds.clear(); notifyListeners(); } void enterSelectMode() { _selectMode = true; _selectedMessageIds.clear(); notifyListeners(); } void toggleMessageSelected(int messageId) { if (_selectedMessageIds.contains(messageId)) { _selectedMessageIds.remove(messageId); } else { _selectedMessageIds.add(messageId); } notifyListeners(); } void selectAllMessage() { if (_allMessages == null || _allMessages!.isEmpty) { return; } if (_selectedMessageIds.length == _allMessages!.length) { _selectedMessageIds.clear(); notifyListeners(); return; } _selectedMessageIds.clear(); for (var msg in _allMessages!) { _selectedMessageIds.add(msg.message.id!); } notifyListeners(); } void selectMessage(int id) { _selectedMessageIds.add(id); notifyListeners(); } void unSelectMessage(int id) { _selectedMessageIds.remove(id); notifyListeners(); } bool isMessageSelected(int id) { return _selectedMessageIds.contains(id); } } class ReferenceDocument { final String title; final String source; final String content; final String media; final String icon; final String index; ReferenceDocument( {required this.title, required this.source, required this.content, required this.media, required this.icon, required this.index}); static fromJson(Map json) { return ReferenceDocument( title: json['title'] ?? '', source: json['source'] ?? '', content: json['content'] ?? '', media: json['media'] ?? '', icon: json['icon'] ?? '', index: json['index'] ?? '', ); } } class ReferenceDocumentWidget extends StatelessWidget { const ReferenceDocumentWidget({super.key, required this.referenceDocuments}); final List referenceDocuments; @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( AppLocale.referenceDocuments.getString(context), style: TextStyle( fontSize: 14, color: customColors.weakTextColorLess, ), ), const SizedBox(height: 10), ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: referenceDocuments.length, itemBuilder: (context, index) { return Container( padding: const EdgeInsets.only(left: 15, bottom: 8), child: Row( children: [ Flexible( child: MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( onTap: () { launchUrlString(referenceDocuments[index].source); }, child: Text( '${index + 1}. ${referenceDocuments[index].title}', style: TextStyle( fontSize: 14, color: customColors.weakTextColorLess, ), ), ), ), ), ], ), ); }, ), ], ); } } ================================================ FILE: lib/page/component/chat/chat_share.dart ================================================ import 'dart:io'; import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/helper.dart'; import 'package:askaide/helper/image.dart'; import 'package:askaide/helper/logger.dart'; import 'package:askaide/helper/platform.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/chat/file_upload.dart'; import 'package:askaide/page/component/chat/markdown.dart'; import 'package:askaide/page/component/enhanced_popup_menu.dart'; import 'package:askaide/page/component/image.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/random_avatar.dart'; import 'package:askaide/page/component/share.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/model/misc.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:file_saver/file_saver.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:image_gallery_saver/image_gallery_saver.dart'; import 'package:widgets_to_image/widgets_to_image.dart'; class ChatShareMessage { final String? username; final String content; final String? avatarURL; final bool leftSide; final List? images; const ChatShareMessage({ this.username, required this.content, this.avatarURL, this.leftSide = true, this.images, }); } class ChatShareScreen extends StatefulWidget { final List messages; const ChatShareScreen({ super.key, required this.messages, }); @override State createState() => _ChatShareScreenState(); } class _ChatShareScreenState extends State { final WidgetsToImageController controller = WidgetsToImageController(); bool showQRCode = true; bool usingChatStyle = true; @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return WindowFrameWidget( child: Scaffold( appBar: AppBar( toolbarHeight: CustomSize.toolbarHeight, actions: [ if (!PlatformTool.isWeb()) TextButton( onPressed: () async { final cancel = BotToast.showCustomLoading( toastBuilder: (cancel) { return LoadingIndicator( message: AppLocale.processingWait.getString(context), ); }, allowClick: false, duration: const Duration(seconds: 15), ); try { final data = await controller.capture(); if (data != null) { final file = await writeTempFile('share-image.png', data); cancel(); await shareTo( // ignore: use_build_context_synchronously context, content: 'images', images: [ file.path, ], ); } } finally { cancel(); } }, child: Row( children: [ Icon(Icons.share, size: 14, color: customColors.weakLinkColor), const SizedBox(width: 5), Text( AppLocale.share.getString(context), style: TextStyle(color: customColors.weakLinkColor, fontSize: 14), ), ], ), ), EnhancedPopupMenu( items: [ EnhancedPopupMenuItem( title: AppLocale.saveToLocal.getString(context), icon: Icons.save, onTap: (ctx) async { final cancel = BotToast.showCustomLoading( toastBuilder: (cancel) { return LoadingIndicator( message: AppLocale.processingWait.getString(context), ); }, allowClick: false, duration: const Duration(seconds: 15), ); try { final data = await controller.capture(); if (data != null) { cancel(); // ignore: use_build_context_synchronously if (PlatformTool.isIOS() || PlatformTool.isAndroid()) { await ImageGallerySaver.saveImage(data, quality: 100); showSuccessMessage(AppLocale.operateSuccess.getString(context)); } else { if (PlatformTool.isWindows()) { FileSaver.instance .saveAs( name: randomId(), bytes: data, ext: '.png', mimeType: MimeType.png, ) .then((value) async { if (value == null) { return; } await File(value).writeAsBytes(data); Logger.instance.d('File saved successfully: $value'); showSuccessMessage(AppLocale.operateSuccess.getString(context)); }); } else { FileSaver.instance .saveFile( name: randomId(), bytes: data, ext: 'png', mimeType: MimeType.png, ) .then((value) { showSuccessMessage(AppLocale.operateSuccess.getString(context)); }); } } } } finally { cancel(); } }, ), EnhancedPopupMenuItem( title: showQRCode ? AppLocale.dontShowInviteCode.getString(context) : AppLocale.showInviteCode.getString(context), icon: showQRCode ? Icons.visibility_off : Icons.visibility, onTap: (ctx) { setState(() { showQRCode = !showQRCode; }); }, ), EnhancedPopupMenuItem( title: usingChatStyle ? '使用列表风格' : '使用聊天风格', icon: usingChatStyle ? Icons.list : Icons.chat, onTap: (ctx) { setState(() { usingChatStyle = !usingChatStyle; }); }, ), ], ), ], ), backgroundColor: customColors.backgroundContainerColor, body: Align( alignment: Alignment.topCenter, child: ConstrainedBox( constraints: const BoxConstraints( maxWidth: CustomSize.maxWindowSize, ), child: SafeArea( child: SingleChildScrollView( child: FutureBuilder( future: APIServer().shareInfo(), builder: (context, snapshot) { if (snapshot.hasError) { return Center( child: Text(resolveError(context, snapshot.error!)), ); } if (snapshot.hasData) { return buildShareWindow(customColors, context, snapshot); } return const Center( child: Text('Loading ...'), ); }), ), ), ), ), ), ); } Widget buildShareWindow(CustomColors customColors, BuildContext context, AsyncSnapshot snapshot) { return WidgetsToImage( controller: controller, child: Container( color: customColors.backgroundContainerColor, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ usingChatStyle ? buildChatPreview(context, customColors) : buildListPreview(context, customColors), if (showQRCode) buildQRCodePanel(customColors, snapshot), ], ), ), ); } Widget buildQRCodePanel(CustomColors customColors, AsyncSnapshot snapshot) { return Container( color: customColors.backgroundColor, child: Padding( padding: const EdgeInsets.symmetric( horizontal: 15, vertical: 20, ), child: Row( children: [ ClipRRect( borderRadius: CustomSize.borderRadius, child: CachedNetworkImageEnhanced( imageUrl: snapshot.data!.qrCode, width: 100, height: 100, ), ), const SizedBox(width: 10), Expanded( child: Text( snapshot.data!.message, ), ), ], ), ), ); } Widget buildListPreview(BuildContext context, CustomColors customColors) { return Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 10, ), child: Column( children: widget.messages.map((message) { return Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 10, ), child: Align( alignment: Alignment.topLeft, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, children: [ if (message.avatarURL != null && message.leftSide) _buildAvatar(avatarUrl: message.avatarURL), if (message.username != null && message.leftSide) Container( margin: const EdgeInsets.fromLTRB(0, 0, 10, 7), padding: const EdgeInsets.symmetric(horizontal: 13), child: Text( message.username!, style: TextStyle( color: customColors.weakTextColor, fontSize: 12, ), ), ), ], ), if (message.images != null && message.images!.isNotEmpty) Container( margin: const EdgeInsets.fromLTRB(0, 10, 10, 0), child: ConstrainedBox( constraints: BoxConstraints(maxWidth: _chatBoxImagePreviewWidth(context, (message.images ?? []).length)), child: FileUploadPreview(images: message.images ?? []), ), ), ConstrainedBox( constraints: BoxConstraints( maxWidth: _chatBoxMaxWidth(context), ), child: Container( margin: const EdgeInsets.fromLTRB(0, 10, 10, 7), decoration: BoxDecoration( borderRadius: CustomSize.borderRadius, color: message.leftSide ? customColors.chatRoomReplyBackground : customColors.chatRoomSenderBackground, ), padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 8, ), child: Builder( builder: (context) { return Markdown(data: message.content); }, ), ), ), ], ), ), ); }).toList(), ), ); } Widget buildChatPreview(BuildContext context, CustomColors customColors) { return Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 10, ), child: Column( children: widget.messages.map((message) { return Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 10, ), child: Align( alignment: message.leftSide ? Alignment.topLeft : Alignment.topRight, child: ConstrainedBox( constraints: BoxConstraints(maxWidth: _chatBoxMaxWidth(context)), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, children: [ if (message.avatarURL != null && message.leftSide) _buildAvatar(avatarUrl: message.avatarURL), if (message.username != null && message.leftSide) Container( margin: const EdgeInsets.fromLTRB(0, 0, 10, 7), padding: const EdgeInsets.symmetric(horizontal: 13), child: Text( message.username!, style: TextStyle( color: customColors.weakTextColor, fontSize: 12, ), ), ), ], ), const SizedBox(height: 10), ConstrainedBox( constraints: BoxConstraints( maxWidth: _chatBoxMaxWidth(context) - 30, ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: message.leftSide ? CrossAxisAlignment.start : CrossAxisAlignment.end, children: [ if (message.images != null && message.images!.isNotEmpty) Container( margin: const EdgeInsets.fromLTRB(0, 0, 10, 7), child: ConstrainedBox( constraints: BoxConstraints( maxWidth: _chatBoxImagePreviewWidth(context, (message.images ?? []).length)), child: FileUploadPreview(images: message.images ?? []), ), ), Container( margin: message.leftSide ? const EdgeInsets.fromLTRB(0, 0, 0, 7) : const EdgeInsets.fromLTRB(0, 0, 10, 7), decoration: BoxDecoration( borderRadius: CustomSize.borderRadius, color: message.leftSide ? customColors.chatRoomReplyBackground : customColors.chatRoomSenderBackground, ), padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 8, ), child: Builder( builder: (context) { return Markdown(data: message.content); }, ), ), ], ), ), ], ), ), ), ); }).toList(), ), ); } /// 获取聊天框的最大宽度 double _chatBoxMaxWidth(BuildContext context) { var screenWidth = MediaQuery.of(context).size.width; if (screenWidth >= CustomSize.maxWindowSize) { return CustomSize.maxWindowSize; } return screenWidth; } /// 获取图片预览的最大宽度 double _chatBoxImagePreviewWidth(BuildContext context, int imageCount) { final expect = _chatBoxMaxWidth(context) / 1.3; final max = imageCount > 1 ? 500.0 : 300.0; return expect > max ? max : expect; } Widget _buildAvatar({String? avatarUrl, int? id, int size = 30}) { if (avatarUrl != null && avatarUrl.startsWith('http')) { return RemoteAvatar( avatarUrl: imageURL(avatarUrl, qiniuImageTypeAvatar), size: size, ); } return RandomAvatar( id: id ?? 0, size: size, usage: Ability().isUserLogon() ? AvatarUsage.room : AvatarUsage.legacy, ); } } ================================================ FILE: lib/page/component/chat/empty.dart ================================================ import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/model/misc.dart'; import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:lottie/lottie.dart'; class EmptyPreview extends StatefulWidget { final List examples; final Function(String message) onSubmit; final bool cardMode; const EmptyPreview({ super.key, required this.examples, required this.onSubmit, this.cardMode = false, }); @override State createState() => _EmptyPreviewState(); } class _EmptyPreviewState extends State { final ScrollController _scrollController = ScrollController(); final displayCount = 6; @override void dispose() { _scrollController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { if (widget.examples.isEmpty) { return Container(); } final customColors = Theme.of(context).extension()!; if (widget.cardMode) { return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Expanded( child: Container( alignment: Alignment.center, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Opacity( opacity: 0.3, child: Lottie.asset('assets/lottie/empty_status.json', height: 150), ), // Text( // AppLocale.welcomeToAskMe.getString(context), // style: TextStyle( // fontSize: 14, // color: customColors.weakTextColor?.withOpacity(0.4), // ), // ), ], ), ), ), Container( height: 60, alignment: Alignment.center, child: ListView.separated( controller: _scrollController, itemCount: (widget.examples.length > displayCount ? displayCount : widget.examples.length) + 1, scrollDirection: Axis.horizontal, itemBuilder: (context, index) { if (index == (widget.examples.length > displayCount ? displayCount : widget.examples.length)) { return Container( width: 60, padding: const EdgeInsets.all(10), margin: const EdgeInsets.only(left: 10, right: 15), alignment: Alignment.center, child: InkWell( borderRadius: CustomSize.borderRadiusAll, onTap: () { setState(() { widget.examples.shuffle(); }); _scrollController.animateTo( 0.0, duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ); }, child: Container( padding: const EdgeInsets.all(5), alignment: Alignment.center, child: Icon(Icons.refresh, color: customColors.chatInputPanelText), ), ), ); } return Container( margin: const EdgeInsets.only(left: 10, right: 5), child: InkWell( borderRadius: CustomSize.borderRadiusAll, onTap: () { widget.onSubmit(widget.examples[index].text); }, child: Container( width: 150, padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: customColors.backgroundColor, borderRadius: CustomSize.borderRadius, ), alignment: Alignment.center, child: AutoSizeText( widget.examples[index].title, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 13, color: customColors.chatInputPanelText, ), ), ), ), ); }, separatorBuilder: (BuildContext context, int index) { return Divider( color: customColors.chatExampleItemText?.withAlpha(20), ); }, ), ), const SizedBox(height: 10), ], ); } return Center( child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ const SizedBox(height: 30), // 示例内容区域 Container( decoration: BoxDecoration( // color: customColors.backgroundColor?.withAlpha(200), borderRadius: CustomSize.borderRadius, ), padding: const EdgeInsets.only(top: 20, left: 15, right: 10, bottom: 3), height: _resolveTipHeight(context), width: _resolveTipWidth(context), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Image.asset('assets/app-256-transparent.png', width: 20, height: 20), const SizedBox(width: 5), Text( AppLocale.askMeLikeThis.getString(context), style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), ], ), const SizedBox(height: 20), Expanded( child: ListView.separated( itemCount: widget.examples.length > displayCount ? displayCount : widget.examples.length, physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) { return ListTextItem( title: widget.examples[index].title, onTap: () { widget.onSubmit(widget.examples[index].text); }, customColors: customColors, ); }, separatorBuilder: (BuildContext context, int index) { return Divider( color: customColors.chatExampleItemText?.withAlpha(20), ); }, ), ), Align( alignment: Alignment.centerRight, child: TextButton( style: ButtonStyle( overlayColor: WidgetStateProperty.all(Colors.transparent), ), onPressed: () { setState(() { widget.examples.shuffle(); }); }, child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ Icon( Icons.refresh, color: customColors.chatExampleItemText, size: 16, ), const SizedBox(width: 3), Text( AppLocale.refresh.getString(context), style: TextStyle( color: customColors.chatExampleItemText, ), textScaler: const TextScaler.linear(0.9), ), ], ), ), ) ], ), ), ], ), ); } double _resolveTipWidth(BuildContext context) { final screenWidth = MediaQuery.of(context).size.width; if (screenWidth < 400) { return screenWidth / 1.15; } return 348; } double _resolveTipHeight(BuildContext context) { final halfScreenHeight = MediaQuery.of(context).size.height / 2; if (halfScreenHeight > 260) { return 260; } return halfScreenHeight; } } class ListTextItem extends StatefulWidget { final String title; final Function() onTap; final CustomColors customColors; const ListTextItem({ super.key, required this.title, required this.onTap, required this.customColors, }); @override State createState() => _ListTextItemState(); } class _ListTextItemState extends State { @override Widget build(BuildContext context) { return InkWell( onTap: widget.onTap, child: Container( padding: const EdgeInsets.only(left: 5, right: 10), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.arrow_right, color: widget.customColors.chatExampleItemText?.withAlpha(120), ), Expanded( child: Text( widget.title, textAlign: TextAlign.left, overflow: TextOverflow.ellipsis, style: TextStyle( color: widget.customColors.weakTextColor, ), ), ), ], ), ), ); } } ================================================ FILE: lib/page/component/chat/enhanced_selectable_text.dart ================================================ import 'package:flutter/material.dart'; class EnhancedSelectableText extends StatefulWidget { final String text; const EnhancedSelectableText({super.key, required this.text}); @override State createState() => _EnhancedSelectableTextState(); } class _EnhancedSelectableTextState extends State { @override Widget build(BuildContext context) { return SelectionArea( child: SingleChildScrollView( child: Container( padding: const EdgeInsets.symmetric(horizontal: 30), child: Text( widget.text, style: const TextStyle( fontSize: 14, ), ), ), ), ); } } ================================================ FILE: lib/page/component/chat/file_upload.dart ================================================ import 'dart:convert'; import 'package:askaide/page/component/image_preview.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/widgets.dart'; class FileUpload { final PlatformFile file; String? url; FileUpload({required this.file, this.url}); bool get uploaded => url != null; setUrl(String url) { this.url = url; } } class FileUploadPreview extends StatelessWidget { final List images; const FileUploadPreview({super.key, required this.images}); @override Widget build(BuildContext context) { final children = images .map((e) { if (e.startsWith('http://') || e.startsWith('https://')) { return NetworkImagePreviewer( url: e, hidePreviewButton: true, ); } if (e.startsWith('data:')) { return ImageProviderPreviewer( borderRadius: CustomSize.borderRadiusAll, imageProvider: MemoryImage( const Base64Decoder().convert(e.split(',')[1]), ), ); } return const SizedBox(); }) .map((e) => Padding( padding: const EdgeInsets.only(bottom: 5, left: 5), child: e, )) .toList(); if (children.length > 1) { if (children.length % 2 == 1) { return Column( children: [ GridView.count( crossAxisCount: 2, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), children: children.sublist(0, children.length - 1), ), children.last, ], ); } return GridView.count( crossAxisCount: 2, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), children: children, ); } return ListView( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), children: children, ); } } ================================================ FILE: lib/page/component/chat/help_tips.dart ================================================ import 'dart:math'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; class HelpTips extends StatelessWidget { final Function(String text)? onSubmitMessage; final Function()? onNewChat; const HelpTips({super.key, this.onSubmitMessage, this.onNewChat}); @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; List children = [ if (onNewChat != null && onSubmitMessage != null) Builder( builder: (context) => _buildNewChatActionTip(customColors, context, (text) => onSubmitMessage!(text)), ), if (onSubmitMessage != null) Builder(builder: (context) => _buildContinueActionTip(customColors, context, onSubmitMessage!)) ]; // 随机取一个 builder return Container( padding: const EdgeInsets.symmetric(vertical: 20), child: children[Random().nextInt(children.length)], ); } RichText _buildNewChatActionTip(CustomColors customColors, BuildContext context, Function(String text) onSubmit) { return RichText( text: TextSpan( children: [ TextSpan( text: AppLocale.startNewChatTips.getString(context), style: TextStyle( color: customColors.dialogDefaultTextColor, fontSize: 12, ), ), const TextSpan(text: ' '), TextSpan( text: AppLocale.newChat.getString(context), style: TextStyle( color: customColors.linkColor, fontSize: 12, ), recognizer: TapGestureRecognizer()..onTap = onNewChat), ], )); } RichText _buildContinueActionTip(CustomColors customColors, BuildContext context, Function(String text) onSubmit) { return RichText( text: TextSpan( children: [ TextSpan( text: AppLocale.wantMoreContentTips.getString(context), style: TextStyle( color: customColors.dialogDefaultTextColor, fontSize: 12, ), ), const TextSpan(text: ' '), TextSpan( text: AppLocale.continueMessage.getString(context), style: TextStyle( color: customColors.linkColor, fontSize: 12, ), recognizer: TapGestureRecognizer() ..onTap = () { onSubmit(AppLocale.continueMessage.getString(context)); }), ], )); } } ================================================ FILE: lib/page/component/chat/markdown/citation.dart ================================================ import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:markdown/markdown.dart' as md; class CitationSyntax extends md.InlineSyntax { final List citations; CitationSyntax({required this.citations}) : super('(?:\\[|【)\\s*citation\\s*:\\s*(\\d+)\\s*(?:\\]|】)'); @override bool onMatch(md.InlineParser parser, Match match) { final num = match[1]!; final node = md.Text(num); try { final el = md.Element('citation', [node]); el.attributes['href'] = citations[int.parse(num) - 1]; parser.addNode(el); } catch (e) { parser.addNode(md.Element('citation', [node])); } return true; } } class CitationBuilder extends MarkdownElementBuilder { final citationPattern = RegExp(r'^citation:\d+$'); final Function(String href)? onTap; CitationBuilder({this.onTap}); @override Widget visitElementAfterWithContext( BuildContext context, md.Element element, TextStyle? preferredStyle, TextStyle? parentStyle, ) { final customColors = Theme.of(context).extension()!; final String text = element.textContent; if (text.isEmpty) { return const SizedBox(); } final href = element.attributes['href']; return RichText( text: TextSpan( children: [ WidgetSpan( child: MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( onTap: () { if (href != null) { onTap?.call(href); } }, child: Container( margin: const EdgeInsets.only(left: 4), decoration: BoxDecoration( color: customColors.weakTextColorLess, borderRadius: BorderRadius.circular(CustomSize.radiusValue * 2), ), padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), child: Text( text, style: const TextStyle( color: Colors.white, fontSize: 10, ), ), ), ), ), ) ], ), ); } } ================================================ FILE: lib/page/component/chat/markdown/code.dart ================================================ import 'package:askaide/helper/ability.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:clipboard/clipboard.dart'; import 'package:flutter/material.dart'; import 'package:flutter_highlight/flutter_highlight.dart'; import 'package:flutter_highlight/themes/tomorrow-night.dart'; import 'package:flutter_highlight/themes/tomorrow.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:markdown/markdown.dart' as md; Map codeTheme() { var theme = Map.from(Ability().themeMode != 'dark' ? tomorrowTheme : tomorrowNightTheme); theme['root'] = TextStyle( backgroundColor: Colors.transparent, color: theme['root']?.color, ); return theme; } class CodeElementBuilder extends MarkdownElementBuilder { final CustomColors customColors; CodeElementBuilder(this.customColors); @override Widget? visitElementAfter(md.Element element, TextStyle? preferredStyle) { var language = ''; if (element.attributes['class'] != null) { String lg = element.attributes['class'] as String; language = lg.substring(9); } final multiLine = element.textContent.trim().split("\n").length > 1; final child = RichText( text: TextSpan( children: [ WidgetSpan( child: HighlightView( // The original code to be highlighted element.textContent, // Specify language // It is recommended to give it a value for performance language: language, // Specify highlight theme // All available themes are listed in `themes` folder theme: codeTheme(), // Specify padding padding: multiLine ? const EdgeInsets.only( top: 30, bottom: 10, left: 10, right: 10, ) : const EdgeInsets.symmetric(horizontal: 5, vertical: 2), textStyle: TextStyle( fontSize: multiLine ? CustomSize.markdownCodeSize : CustomSize.markdownTextSize, height: 1.5, wordSpacing: 3, ), ), ) ], ), ); if (multiLine) { return Card( elevation: 0, color: customColors.markdownPreColor, shape: RoundedRectangleBorder( borderRadius: CustomSize.borderRadius, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 8), decoration: BoxDecoration( color: customColors.listTileBackgroundColor, borderRadius: const BorderRadius.only(topLeft: CustomSize.radius, topRight: CustomSize.radius), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( language, style: TextStyle( fontSize: 12, color: customColors.weakTextColor, ), ), TextButton.icon( icon: Icon( Icons.copy, size: 14, color: customColors.weakTextColorLess, ), label: Text( 'Copy', style: TextStyle( fontSize: 12, color: customColors.weakTextColorLess, ), ), onPressed: () { FlutterClipboard.copy(element.textContent).then((value) { showSuccessMessage('Copied to clipboard'); }); }, style: ButtonStyle( overlayColor: WidgetStateProperty.all(Colors.transparent), ), ), ], ), ), child, ], ), ); } return child; } } ================================================ FILE: lib/page/component/chat/markdown/latex/latex_block_syntax.dart ================================================ import 'package:markdown/markdown.dart'; class LatexBlockSyntax extends BlockSyntax { @override RegExp get pattern => RegExp( r'^(?:(\${1,2})(?:\n|$))|(?:(?:\\\[(.+)\\\])(?:\n|$))', multiLine: true, ); LatexBlockSyntax() : super(); @override List parseChildLines(BlockParser parser) { final m = pattern.firstMatch(parser.current.content); if (m?[2] != null) { parser.advance(); return [Line(m?[2] ?? '')]; } final childLines = []; parser.advance(); while (!parser.isDone) { final match = pattern.hasMatch(parser.current.content); if (!match) { childLines.add(parser.current); parser.advance(); } else { parser.advance(); break; } } return childLines; } @override Node parse(BlockParser parser) { final lines = parseChildLines(parser); final content = lines.map((e) => e.content).join('\n').trim(); final textElement = Element.text('latex', content); textElement.attributes['MathStyle'] = 'display'; return Element('p', [textElement]); } } ================================================ FILE: lib/page/component/chat/markdown/latex/latex_element_builder.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_math_fork/flutter_math.dart'; import 'package:markdown/markdown.dart' as md; class LatexElementBuilder extends MarkdownElementBuilder { LatexElementBuilder({ this.textStyle, this.textScaleFactor, }); /// The style to apply to the text. final TextStyle? textStyle; /// The text scale factor to apply to the text. final double? textScaleFactor; @override Widget visitElementAfterWithContext( BuildContext context, md.Element element, TextStyle? preferredStyle, TextStyle? parentStyle, ) { final String text = element.textContent; if (text.isEmpty) { return const SizedBox(); } MathStyle mathStyle; switch (element.attributes['MathStyle']) { case 'text': mathStyle = MathStyle.text; case 'display': mathStyle = MathStyle.display; default: mathStyle = MathStyle.text; } return SingleChildScrollView( scrollDirection: Axis.horizontal, clipBehavior: Clip.antiAlias, child: Math.tex( text, textStyle: textStyle, mathStyle: mathStyle, textScaleFactor: textScaleFactor, ), ); } } ================================================ FILE: lib/page/component/chat/markdown/latex/latex_inline_syntax.dart ================================================ import 'package:markdown/markdown.dart'; final List> delimiterList = [ {'left': r'$$', 'right': r'$$', 'display': true}, {'left': r'$', 'right': r'$', 'display': false}, {'left': r'\pu{', 'right': '}', 'display': false}, {'left': r'\ce{', 'right': '}', 'display': false}, {'left': r'\(', 'right': r'\)', 'display': false}, {'left': '( ', 'right': ' )', 'display': false}, {'left': r'\[', 'right': r'\]', 'display': true}, {'left': '[ ', 'right': ' ]', 'display': true}, ]; List inlinePatterns = []; List blockPatterns = []; String escapeRegex(String string) { return string.replaceAllMapped(RegExp(r'[-\/\\^$*+?.()|[\]{}]'), (match) { return '\\${match.group(0)}'; }); } String generateRegexRules(List> delimiters) { for (var delimiter in delimiters) { String left = delimiter['left']; String right = delimiter['right']; // Ensure regex-safe delimiters String escapedLeft = escapeRegex(left); String escapedRight = escapeRegex(right); // Inline pattern inlinePatterns.add('$escapedLeft((?:\\\\.|[^\\\\\\n])*?(?:\\\\.|[^\\\\\\n]|(?!$escapedRight)))$escapedRight'); // Block pattern blockPatterns.add('$escapedLeft\\n((?:\\\\[^]|[^\\\\])+?)\\n$escapedRight'); } return '(${inlinePatterns.join("|")})(?=[\\s?!.,:?!。,:]|\$)'; } final _latexPattern = generateRegexRules(delimiterList); class LatexInlineSyntax extends InlineSyntax { LatexInlineSyntax() : super(_latexPattern); @override bool onMatch(InlineParser parser, Match match) { String raw = match.group(0) ?? ''; int delimiterLength = 1; String mathStyle = 'text'; // check delimiter for (var delimiter in delimiterList) { if (raw.startsWith(delimiter['left']) && raw.endsWith(delimiter['right'])) { mathStyle = delimiter['display'] ? 'display' : 'text'; delimiterLength = delimiter['left'].length; break; } } final equation = raw.substring(delimiterLength, raw.length - delimiterLength); final element = Element.text('latex', equation); element.attributes['MathStyle'] = mathStyle; parser.addNode(element); return true; } } ================================================ FILE: lib/page/component/chat/markdown/latex.dart ================================================ import 'package:flutter/material.dart'; import 'package:markdown_widget/markdown_widget.dart'; import 'package:flutter_math_fork/flutter_math.dart'; import 'package:markdown/markdown.dart' as m; SpanNodeGeneratorWithTag latexGenerator = SpanNodeGeneratorWithTag( tag: _latexTag, generator: (e, config, visitor) => LatexNode(e.attributes, e.textContent, config)); const _latexTag = 'latex'; class LatexSyntax extends m.InlineSyntax { LatexSyntax() : super(r'(\$\$[\s\S]+\$\$)|(\$.+?\$)'); @override bool onMatch(m.InlineParser parser, Match match) { final input = match.input; final matchValue = input.substring(match.start, match.end); String content = ''; bool isInline = true; const blockSyntax = '\$\$'; const inlineSyntax = '\$'; if (matchValue.startsWith(blockSyntax) && matchValue.endsWith(blockSyntax) && (matchValue != blockSyntax)) { content = matchValue.substring(2, matchValue.length - 2); isInline = false; } else if (matchValue.startsWith(inlineSyntax) && matchValue.endsWith(inlineSyntax) && matchValue != inlineSyntax) { content = matchValue.substring(1, matchValue.length - 1); } m.Element el = m.Element.text(_latexTag, matchValue); el.attributes['content'] = content; el.attributes['isInline'] = '$isInline'; parser.addNode(el); return true; } } class LatexNode extends SpanNode { final Map attributes; final String textContent; final MarkdownConfig config; LatexNode(this.attributes, this.textContent, this.config); @override InlineSpan build() { final content = attributes['content'] ?? ''; final isInline = attributes['isInline'] == 'true'; final style = parentStyle ?? config.p.textStyle; if (content.isEmpty) return TextSpan(style: style, text: textContent); final latex = Math.tex( content, mathStyle: MathStyle.text, textScaleFactor: 1, onErrorFallback: (error) { return Text( textContent, style: style.copyWith(color: Colors.red), ); }, ); return WidgetSpan( alignment: PlaceholderAlignment.middle, child: !isInline ? Container( width: double.infinity, margin: const EdgeInsets.symmetric(vertical: 16), child: Center(child: latex), ) : latex); } } ================================================ FILE: lib/page/component/chat/markdown.dart ================================================ // import 'dart:convert'; // import 'package:askaide/helper/platform.dart'; // import 'package:askaide/page/component/chat/markdown/latex.dart'; // import 'package:askaide/page/component/dialog.dart'; // import 'package:clipboard/clipboard.dart'; // import 'package:markdown_widget/config/all.dart'; // import 'package:markdown_widget/widget/all.dart'; import 'package:askaide/page/component/chat/markdown/citation.dart'; import 'package:askaide/page/component/chat/markdown/code.dart'; import 'package:askaide/page/component/chat/markdown/latex/latex_block_syntax.dart'; import 'package:askaide/page/component/chat/markdown/latex/latex_element_builder.dart'; import 'package:askaide/page/component/chat/markdown/latex/latex_inline_syntax.dart'; import 'package:askaide/page/component/image_preview.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_markdown/flutter_markdown.dart' as md; import 'package:markdown/markdown.dart' as mm; class Markdown extends StatelessWidget { final String data; final Function(String value)? onUrlTap; final TextStyle? textStyle; final cacheManager = DefaultCacheManager(); final bool thinkingMode; final List citations; Markdown({ super.key, required this.data, this.onUrlTap, this.textStyle, this.citations = const [], this.thinkingMode = false, }); @override Widget build(BuildContext context) { // if (!PlatformTool.isWeb()) { // return MarkdownPlus( // data: data, // onUrlTap: onUrlTap, // textStyle: textStyle, // compact: true, // ); // } final customColors = Theme.of(context).extension()!; final style = thinkingMode ? md.MarkdownStyleSheet( p: TextStyle(fontSize: 14, color: customColors.weakTextColorLess, height: 1.5), listBullet: TextStyle(fontSize: 14, color: customColors.weakTextColorLess, height: 1.5), code: TextStyle( fontSize: 14, color: customColors.weakTextColorLess, backgroundColor: Colors.transparent, ), codeblockPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), codeblockDecoration: const BoxDecoration(borderRadius: CustomSize.borderRadiusAll), tableBorder: TableBorder.all(color: customColors.weakTextColorLess!.withOpacity(0.5), width: 1), tableColumnWidth: const FlexColumnWidth(), blockquotePadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2), blockquoteDecoration: BoxDecoration( border: Border(left: BorderSide(color: customColors.weakTextColorLess!.withOpacity(0.4), width: 4)), ), a: TextStyle(color: customColors.weakTextColorLess, decoration: TextDecoration.none), h1: TextStyle(color: customColors.weakTextColorLess, height: 1.5), h2: TextStyle(color: customColors.weakTextColorLess, height: 1.5), h3: TextStyle(color: customColors.weakTextColorLess, height: 1.5), h4: TextStyle(color: customColors.weakTextColorLess, height: 1.5), h5: TextStyle(color: customColors.weakTextColorLess, height: 1.5), h6: TextStyle(color: customColors.weakTextColorLess, height: 1.5), ) : md.MarkdownStyleSheet( p: textStyle ?? TextStyle(fontSize: CustomSize.markdownTextSize, height: 1.5), listBullet: textStyle ?? TextStyle(fontSize: CustomSize.markdownTextSize, height: 1.5), code: TextStyle( fontSize: CustomSize.markdownCodeSize, color: customColors.markdownCodeColor, backgroundColor: Colors.transparent, ), codeblockPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), codeblockDecoration: const BoxDecoration(borderRadius: CustomSize.borderRadiusAll), tableBorder: TableBorder.all(color: customColors.weakTextColor!.withOpacity(0.5), width: 1), tableColumnWidth: const FlexColumnWidth(), blockquotePadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2), blockquoteDecoration: BoxDecoration( border: Border(left: BorderSide(color: customColors.weakTextColor!.withOpacity(0.4), width: 4)), ), a: TextStyle(color: customColors.weakLinkColor, decoration: TextDecoration.none), ); return md.MarkdownBody( shrinkWrap: true, selectable: false, softLineBreak: true, styleSheetTheme: md.MarkdownStyleSheetBaseTheme.material, styleSheet: style, onTapLink: (text, href, title) { if (onUrlTap != null && href != null) onUrlTap!(href); }, imageBuilder: (uri, title, alt) { if (uri.scheme == 'http' || uri.scheme == 'https') { return NetworkImagePreviewer( url: uri.toString(), hidePreviewButton: true, ); } return ClipRRect(borderRadius: CustomSize.borderRadiusAll, child: Image.network(uri.toString())); }, extensionSet: mm.ExtensionSet( [ ...mm.ExtensionSet.gitHubFlavored.blockSyntaxes, LatexBlockSyntax(), ], [ CitationSyntax(citations: citations), mm.EmojiSyntax(), ...mm.ExtensionSet.gitHubFlavored.inlineSyntaxes, LatexInlineSyntax(), ], ), data: data, builders: { 'latex': LatexElementBuilder(), 'code': CodeElementBuilder(customColors), 'citation': CitationBuilder(onTap: onUrlTap), }, ); } } // class MarkdownPlus extends StatelessWidget { // final String data; // final Function(String value)? onUrlTap; // final bool compact; // final TextStyle? textStyle; // final cacheManager = DefaultCacheManager(); // MarkdownPlus({ // super.key, // required this.data, // this.onUrlTap, // this.compact = true, // this.textStyle, // }); // MarkdownConfig _buildMarkdownConfig(CustomColors customColors) { // return MarkdownConfig( // configs: [ // PConfig(textStyle: textStyle ?? TextStyle(fontSize: CustomSize.markdownTextSize)), // // 链接配置 // LinkConfig( // style: TextStyle( // color: customColors.markdownLinkColor, // decoration: TextDecoration.none, // ), // onTap: (value) { // if (onUrlTap != null) onUrlTap!(value); // }, // ), // // 代码块配置 // PreConfig( // theme: codeTheme(), // decoration: const BoxDecoration(borderRadius: CustomSize.borderRadiusAll), // margin: const EdgeInsets.symmetric(vertical: 0.0), // padding: const EdgeInsets.only(top: 5, left: 10, right: 10, bottom: 10), // textStyle: TextStyle(fontSize: CustomSize.markdownCodeSize), // wrapper: (child, code, language) { // return Card( // elevation: 0, // color: customColors.markdownPreColor, // shape: RoundedRectangleBorder( // borderRadius: CustomSize.borderRadius, // ), // child: Column( // crossAxisAlignment: CrossAxisAlignment.start, // children: [ // Container( // padding: const EdgeInsets.symmetric(horizontal: 8), // decoration: BoxDecoration( // color: customColors.listTileBackgroundColor, // borderRadius: const BorderRadius.only(topLeft: CustomSize.radius, topRight: CustomSize.radius), // ), // child: Row( // mainAxisAlignment: MainAxisAlignment.spaceBetween, // children: [ // Text( // language, // style: TextStyle( // fontSize: 12, // color: customColors.weakTextColor, // ), // ), // TextButton.icon( // icon: Icon( // Icons.copy, // size: 14, // color: customColors.weakTextColorLess, // ), // label: Text( // 'Copy', // style: TextStyle( // fontSize: 12, // color: customColors.weakTextColorLess, // ), // ), // onPressed: () { // FlutterClipboard.copy(code).then((value) { // showSuccessMessage('Copied to clipboard'); // }); // }, // style: ButtonStyle( // overlayColor: WidgetStateProperty.all(Colors.transparent), // ), // ), // ], // ), // ), // child, // ], // ), // ); // }, // ), // // 代码配置 // CodeConfig( // style: TextStyle( // fontSize: CustomSize.markdownCodeSize, // color: customColors.markdownCodeColor, // ), // ), // // 图片配置 // ImgConfig( // builder: (url, attributes) { // if (url.isEmpty) { // return const SizedBox(); // } // if (url.startsWith('data:')) { // return ClipRRect( // borderRadius: CustomSize.borderRadiusAll, // child: Image.memory( // const Base64Decoder().convert(url.split(',')[1]), // fit: BoxFit.cover, // ), // ); // } // return NetworkImagePreviewer( // url: url, // hidePreviewButton: true, // ); // }, // ), // HrConfig(height: 1, color: customColors.weakTextColorLess?.withAlpha(100) ?? Colors.transparent), // ], // ); // } // @override // Widget build(BuildContext context) { // final customColors = Theme.of(context).extension()!; // final markdownGenerator = MarkdownGenerator( // generators: [latexGenerator], // inlineSyntaxList: [LatexSyntax()], // ); // if (compact) { // return Column( // mainAxisSize: MainAxisSize.min, // mainAxisAlignment: MainAxisAlignment.start, // textDirection: TextDirection.ltr, // crossAxisAlignment: CrossAxisAlignment.start, // children: markdownGenerator.buildWidgets( // data, // config: _buildMarkdownConfig(customColors), // ), // ); // } // return MarkdownWidget( // data: data, // shrinkWrap: true, // config: _buildMarkdownConfig(customColors), // markdownGenerator: markdownGenerator, // ); // } // } ================================================ FILE: lib/page/component/chat/message_state_manager.dart ================================================ import 'dart:convert'; import 'package:askaide/repo/cache_repo.dart'; import 'package:askaide/repo/model/message.dart'; class MessageWithState { final Message message; final MessageState state; MessageWithState(this.message, this.state); } /// 消息状态 class MessageState { /// 是否显示翻译 bool showTranslate = false; /// 翻译文本 String? translateText; /// 是否显示 Markdown bool showMarkdown = true; MessageState({ this.showTranslate = false, this.translateText, this.showMarkdown = true, }); /// 是否是初始状态 bool isInitializeState() { return !showTranslate && translateText == null && showMarkdown; } toJson() { return { 'showTranslate': showTranslate, 'translateText': translateText, 'showMarkdown': showMarkdown, }; } MessageState.fromJson(Map json) { showTranslate = json['showTranslate'] ?? false; translateText = json['translateText']; showMarkdown = json['showMarkdown'] ?? true; } } /// 消息状态管理器 class MessageStateManager { final CacheRepository cacheRepo; MessageStateManager(this.cacheRepo); Future> loadRoomStates(int roomId) async { final states = await cacheRepo.getAllInGroup('room:$roomId'); return states.map((key, value) => MapEntry(key, MessageState.fromJson(jsonDecode(value)))); } String getKey(int roomId, int id) { return 'msg:state:$roomId:$id'; } Future getState(int roomId, int id) async { final key = getKey(roomId, id); final value = await cacheRepo.get(key); if (value == null) { return MessageState(); } return MessageState.fromJson(jsonDecode(value)); } Future setState(int roomId, int id, MessageState state) async { final key = getKey(roomId, id); if (state.isInitializeState()) { return removeState(roomId, id); } return cacheRepo.set( key, jsonEncode(state.toJson()), const Duration(days: 7), group: 'room:$roomId', ); } Future removeState(int roomId, int id) async { final key = getKey(roomId, id); return cacheRepo.remove(key); } } ================================================ FILE: lib/page/component/chat/role_avatar.dart ================================================ import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/image.dart'; import 'package:askaide/helper/model.dart'; import 'package:askaide/page/component/random_avatar.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/repo/model/chat_history.dart'; import 'package:flutter/material.dart'; import 'package:flutter_initicon/flutter_initicon.dart'; class RoleAvatar extends StatefulWidget { final String? avatarUrl; final String? alternativeAvatarUrl; final String? name; final ChatHistory? his; final int avatarSize; const RoleAvatar({ super.key, this.avatarUrl, this.alternativeAvatarUrl, this.his, this.name, this.avatarSize = 30, }); @override State createState() => _RoleAvatarState(); } class _RoleAvatarState extends State { @override Widget build(BuildContext context) { return _buildAvatar(context); } Widget _buildAvatar(BuildContext context) { if (widget.avatarUrl != null && widget.avatarUrl!.startsWith('http')) { return RemoteAvatar( avatarUrl: imageURL(widget.avatarUrl!, qiniuImageTypeAvatar), size: widget.avatarSize, ); } if (widget.alternativeAvatarUrl != null) { return RemoteAvatar( avatarUrl: imageURL(widget.alternativeAvatarUrl!, qiniuImageTypeAvatar), size: widget.avatarSize, ); } if (widget.his != null && widget.his!.model != null) { return FutureBuilder( future: ModelAggregate.models(), builder: (context, snapshot) { if (!snapshot.hasError && snapshot.hasData) { var mod = snapshot.data!.where((e) => e.id == widget.his!.model!).firstOrNull; if (mod != null && mod.avatarUrl != null && mod.avatarUrl != '') { return RemoteAvatar(avatarUrl: mod.avatarUrl!, size: widget.avatarSize); } } return LocalAvatar(assetName: 'assets/app.png', size: widget.avatarSize); }, ); } if (widget.name != null && widget.name!.isNotEmpty) { return Initicon( text: widget.name!.split('、').join(' '), size: widget.avatarSize.toDouble(), backgroundColor: Colors.grey.withAlpha(100), borderRadius: CustomSize.borderRadiusAll, ); } return LocalAvatar(assetName: 'assets/app.png', size: widget.avatarSize); } } ================================================ FILE: lib/page/component/chat/search_result.dart ================================================ import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/chat/chat_preview.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/image.dart'; import 'package:askaide/page/component/item_selector_search.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:url_launcher/url_launcher_string.dart'; class SearchResult extends StatelessWidget { final List searchResults; final bool isSearching; const SearchResult({super.key, required this.searchResults, this.isSearching = false}); @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return SizedBox( width: double.infinity, child: isSearching ? Row( mainAxisSize: MainAxisSize.min, children: [ Text( AppLocale.robotIsSearchingMessage.getString(context), style: TextStyle(fontSize: 14, color: customColors.weakTextColorLess), ), const SizedBox(width: 5), RotationTransition( turns: AnimationController( duration: const Duration(seconds: 3), vsync: Scaffold.of(context), )..repeat(), child: Icon( Icons.sync, size: 16, color: customColors.weakTextColorLess, ), ), ], ) : GestureDetector( onTap: () { openModalBottomSheet( context, (context) { return ItemSearchSelector( innerPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 5), items: searchResults .map( (e) => SelectorItem( Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ // Icon if (e.icon.isNotEmpty) ClipRRect( borderRadius: CustomSize.borderRadius, child: CachedNetworkImageEnhanced( imageUrl: e.icon, fit: BoxFit.fill, width: 15, height: 15, ), ) else const SizedBox(width: 15), const SizedBox(width: 10), // 媒体名称 Text( e.media, textAlign: TextAlign.left, overflow: TextOverflow.ellipsis, maxLines: 2, style: TextStyle( color: customColors.weakTextColorLess, fontSize: 12, ), ), ], ), // 索引 Container( decoration: BoxDecoration( color: customColors.weakTextColorLess, shape: BoxShape.circle, ), padding: const EdgeInsets.all(4), child: Text( e.index, style: const TextStyle( color: Colors.white, fontSize: 12, ), ), ), ], ), const SizedBox(height: 4), Text( e.title, textAlign: TextAlign.left, overflow: TextOverflow.ellipsis, maxLines: 2, style: TextStyle( color: customColors.weakTextColor, ), ), const SizedBox(height: 4), Text( e.content, textAlign: TextAlign.left, overflow: TextOverflow.ellipsis, maxLines: 2, style: TextStyle( color: customColors.weakTextColorLess, ), textScaler: const TextScaler.linear(0.8), ), ], ), e.source, search: (keyword) => e.title.toLowerCase().contains(keyword.toLowerCase()) || e.content.contains(keyword.toLowerCase()), ), ) .toList(), onSelected: (value) { launchUrlString(value.value); return false; }, ); }, heightFactor: 0.9, ); }, child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( AppLocale.searchedXWebPages.getString(context).replaceAll('%s', searchResults.length.toString()), style: TextStyle(fontSize: 14, color: customColors.weakTextColorLess), ), Icon( Icons.keyboard_arrow_right, size: 16, color: customColors.weakTextColorLess, ), ], ), ), ); } } ================================================ FILE: lib/page/component/chat/thinking_card.dart ================================================ import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/chat/markdown.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:url_launcher/url_launcher_string.dart'; class ThinkingCard extends StatelessWidget { final String content; final String title; final bool isExpanded; final Function(bool) onTap; final double timeConsumed; const ThinkingCard({ super.key, required this.content, required this.title, this.isExpanded = false, required this.onTap, this.timeConsumed = 0, }); @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return SizedBox( width: double.infinity, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ GestureDetector( onTap: () => onTap(!isExpanded), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( timeConsumed > 0 ? '$title (${AppLocale.timeConsume.getString(context)} ${timeConsumed.toStringAsFixed(1)}s)' : title, style: TextStyle(fontSize: 14, color: customColors.weakTextColorLess), ), AnimatedRotation( duration: const Duration(milliseconds: 200), turns: isExpanded ? 0.5 : 0, child: Icon( Icons.keyboard_arrow_up, size: 16, color: customColors.weakTextColorLess, ), ), ], ), ), AnimatedSize( duration: const Duration(milliseconds: 200), child: Container( height: isExpanded ? null : 0, padding: const EdgeInsets.only(top: 8), alignment: Alignment.topLeft, width: double.infinity, child: Container( padding: const EdgeInsets.only(left: 8), alignment: Alignment.topLeft, width: double.infinity, child: IntrinsicHeight( child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Container( width: 3, margin: const EdgeInsets.only(right: 8), decoration: BoxDecoration( color: customColors.weakTextColorLess?.withOpacity(0.5), borderRadius: BorderRadius.circular(1.5), ), ), Expanded( child: Markdown( data: content, onUrlTap: (value) { launchUrlString(value); }, thinkingMode: true, ), ), ], ), ), ), ), ), ], ), ); } } ================================================ FILE: lib/page/component/chat/voice_record.dart ================================================ import 'dart:async'; import 'dart:io'; import 'package:askaide/helper/haptic_feedback.dart'; import 'package:askaide/helper/helper.dart'; import 'package:askaide/helper/model_resolver.dart'; import 'package:askaide/helper/path.dart'; import 'package:askaide/helper/platform.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/chat/markdown.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:loading_animation_widget/loading_animation_widget.dart'; import 'package:quickalert/quickalert.dart'; import 'package:record/record.dart'; import 'package:url_launcher/url_launcher_string.dart'; class VoiceRecord extends StatefulWidget { final Function(String text) onFinished; final Function() onStart; const VoiceRecord( {super.key, required this.onFinished, required this.onStart}); @override State createState() => _VoiceRecordState(); } class _VoiceRecordState extends State { var _voiceRecording = false; final record = AudioRecorder(); DateTime? _voiceStartTime; Timer? _timer; var _millSeconds = 0; @override void initState() { super.initState(); record.hasPermission().then((hasPermission) { if (!hasPermission) { showErrorMessage('请授予录音权限'); } }); } @override void dispose() { _timer?.cancel(); record.dispose(); super.dispose(); } @override Widget build(BuildContext context) { var customColors = Theme.of(context).extension()!; return Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ SizedBox( height: 90, child: _voiceRecording ? Column( children: [ LoadingAnimationWidget.staggeredDotsWave( color: const Color.fromARGB(255, 74, 74, 254), size: 60, ), const SizedBox(height: 10), Text('${(_millSeconds / 1000.0).toStringAsFixed(3)} s'), ], ) : const SizedBox(), ), const SizedBox(height: 10), GestureDetector( onLongPressStart: (details) async { if (PlatformTool.isWeb()) { showCustomBeautyDialog( context, type: QuickAlertType.warning, confirmBtnText: AppLocale.gotIt.getString(context), showCancelBtn: false, title: '温馨提示', child: Markdown( data: 'Web 端暂不支持语音输入,敬请期待。\n\n要体验完整功能,您可[点击这里下载 AIdea APP](https://aidea.aicode.cc)。', onUrlTap: (value) { launchUrlString( value, mode: LaunchMode.externalApplication, ); }, textStyle: TextStyle( fontSize: 14, color: customColors.dialogDefaultTextColor, ), ), ); return; } widget.onStart(); if (await record.hasPermission()) { // 震动反馈 HapticFeedbackHelper.heavyImpact(); setState(() { _voiceRecording = true; _voiceStartTime = DateTime.now(); }); // Start recording await record.start( RecordConfig( encoder: PlatformTool.isWindows() || PlatformTool.isIOS() ? AudioEncoder.aacLc : AudioEncoder.wav, ), path: "${PathHelper().getCachePath}/${randomId()}.m4a", ); setState(() { _millSeconds = 0; }); if (_timer != null) { _timer!.cancel(); _timer = null; } _timer = Timer.periodic(const Duration(milliseconds: 100), (timer) async { if (_voiceStartTime == null) { timer.cancel(); return; } if (DateTime.now().difference(_voiceStartTime!).inSeconds >= 60) { await onRecordStop(); return; } setState(() { _millSeconds = DateTime.now() .difference(_voiceStartTime!) .inMilliseconds; }); }); } }, onLongPressEnd: (details) async { if (!_voiceRecording) { return; } setState(() { _voiceRecording = false; }); await onRecordStop(); }, child: SizedBox( height: 80, width: 80, child: CircleAvatar( backgroundColor: _voiceRecording ? customColors.linkColor : customColors.linkColor?.withAlpha(200), child: const Icon( Icons.mic, size: 50, color: Colors.white, ), ), ), ), const SizedBox(height: 10), Text( AppLocale.longPressSpeak.getString(context), style: const TextStyle(fontSize: 16), ), const SizedBox(height: 20), ], ); } deleteTempFile(String path) { // 删除临时文件 if (!path.startsWith('blob:')) { try { File.fromUri(Uri.parse(path)).deleteSync(); } catch (e) { try { File(path).deleteSync(); } catch (e) { // ignore } } } } Future onRecordStop() async { _timer?.cancel(); var resPath = await record.stop(); if (resPath == null) { showErrorMessage('语音输入失败'); return; } resPath = resPath.replaceAll('\\', '/'); final voiceDuration = DateTime.now().difference(_voiceStartTime!).inSeconds; if (voiceDuration < 1) { showErrorMessage('说话时间太短'); _voiceStartTime = null; deleteTempFile(resPath); return; } if (voiceDuration > 60) { showErrorMessage('说话时间太长'); _voiceStartTime = null; deleteTempFile(resPath); return; } _voiceStartTime = null; final cancel = BotToast.showCustomLoading( toastBuilder: (cancel) { return LoadingIndicator( message: AppLocale.processingWait.getString(context), ); }, allowClick: false, duration: const Duration(seconds: 120), ); try { File audioFile; try { audioFile = File.fromUri(Uri.parse(resPath)); } catch (e) { audioFile = File(resPath); } widget.onFinished(await ModelResolver.instance.audioToText(audioFile)); } catch (e) { // ignore: use_build_context_synchronously showErrorMessageEnhanced(context, e); } finally { cancel(); deleteTempFile(resPath); } } } ================================================ FILE: lib/page/component/chat_tools_button.dart ================================================ import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:flutter/material.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; class ChatToolsButton extends StatefulWidget { final String text; final IconData icon; final Color? iconColor; final void Function()? onTap; const ChatToolsButton({ super.key, required this.text, required this.icon, this.iconColor, this.onTap, }); @override State createState() => _ChatToolsButtonState(); } class _ChatToolsButtonState extends State { bool _mouseHover = false; @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return GestureDetector( onTap: widget.onTap, child: MouseRegion( onEnter: (event) { setState(() { _mouseHover = true; }); }, onExit: (event) { setState(() { _mouseHover = false; }); }, child: Container( decoration: BoxDecoration( borderRadius: CustomSize.borderRadius, color: _mouseHover ? customColors.tagsBackgroundHover : customColors.tagsBackground, ), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), child: Row( children: [ Icon( widget.icon, size: 11, color: widget.iconColor, ), const SizedBox(width: 2), Text( widget.text, style: TextStyle( fontSize: 11, color: customColors.tagsText, ), ), ], ), ), ), ); } } ================================================ FILE: lib/page/component/column_block.dart ================================================ import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; class ColumnBlock extends StatelessWidget { final List children; final double? innerPanding; final Color? backgroundColor; final BoxBorder? border; final EdgeInsets? padding; final EdgeInsets? margin; final double? borderRadius; final bool showDivider; const ColumnBlock({ super.key, required this.children, this.innerPanding, this.backgroundColor, this.border, this.padding, this.margin, this.borderRadius, this.showDivider = true, }); @override Widget build(BuildContext context) { if (children.isEmpty) { return Container(); } final customColors = Theme.of(context).extension()!; var items = []; for (var i = 0; i < children.length; i++) { items.add(children[i]); items.add(Container( padding: EdgeInsets.symmetric(vertical: innerPanding ?? 0), child: (i < children.length - 1 && showDivider) ? Divider( color: customColors.columnBlockDividerColor, height: 1, ) : Container(), )); } return Container( width: double.infinity, decoration: BoxDecoration( color: backgroundColor ?? customColors.columnBlockBackgroundColor, border: border, borderRadius: BorderRadius.circular(borderRadius ?? CustomSize.radiusValue), boxShadow: [ BoxShadow( color: customColors.boxShadowColor!, offset: const Offset(0, 3), blurRadius: 5, ), BoxShadow( color: customColors.boxShadowColor!, offset: const Offset(-3, 0), blurRadius: 5, ), ], ), padding: padding ?? const EdgeInsets.symmetric(horizontal: 15, vertical: 5), margin: margin ?? const EdgeInsets.only(bottom: 10, left: 5, right: 5), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: items, ), ); } } ================================================ FILE: lib/page/component/credit.dart ================================================ import 'package:askaide/lang/lang.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; class Credit extends StatelessWidget { final int count; final Color? color; final FontWeight? fontWeight; final double? fontSize; final bool withAddPrefix; const Credit({ super.key, required this.count, this.color, this.fontWeight, this.fontSize, this.withAddPrefix = false, }); @override Widget build(BuildContext context) { return RichText( text: TextSpan( children: [ TextSpan( text: '${withAddPrefix ? "+ " : ""}${AppLocale.creditUnit.getString(context)}${formatCount()}', style: TextStyle( fontSize: fontSize ?? 20, color: color ?? Colors.white, fontWeight: fontWeight ?? FontWeight.bold, overflow: TextOverflow.ellipsis, ), ), TextSpan( text: count >= maxShowCount ? " +" : "", style: TextStyle( fontSize: fontSize != null ? (fontSize! - 7) : 12, color: color ?? Colors.white.withAlpha(200), fontWeight: fontWeight ?? FontWeight.bold, ), ), ], )); } final maxShowCount = 1000000; String formatCount() { if (count >= maxShowCount) { return '${(count / maxShowCount).toStringAsFixed(0)} 百万'; } return '$count'; } } ================================================ FILE: lib/page/component/dialog.dart ================================================ import 'dart:ui'; import 'package:askaide/helper/event.dart'; import 'package:askaide/helper/haptic_feedback.dart'; import 'package:askaide/helper/logger.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/bottom_sheet_box.dart'; import 'package:askaide/page/component/button.dart'; import 'package:askaide/page/component/enhanced_textfield.dart'; import 'package:askaide/page/component/item_selector_search.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; import 'package:quickalert/quickalert.dart'; showErrorMessageEnhanced( BuildContext context, Object message, { Duration duration = const Duration(seconds: 5), }) { if (message is LanguageText) { switch (message.action) { // 智慧果不足,支付页面 case 'payment': showBeautyDialog( context, type: QuickAlertType.warning, text: message.message.getString(context), confirmBtnText: AppLocale.buy.getString(context), showCancelBtn: true, onConfirmBtnTap: () { context.pop(); context.push('/payment'); }, ); return; // 需要重新登录 case 're-signin': showBeautyDialog( context, type: QuickAlertType.warning, text: message.message.getString(context), confirmBtnText: AppLocale.reSignIn.getString(context), showCancelBtn: true, onConfirmBtnTap: () { context.pop(); context.push('/login'); }, ); return; // 需要登录 case 'sign-in': showBeautyDialog( context, type: QuickAlertType.warning, text: AppLocale.needSigninToUse.getString(context), onConfirmBtnTap: () { context.pop(); context.push('/login'); }, showCancelBtn: true, confirmBtnText: AppLocale.signinNow.getString(context), ); return; } showErrorMessage(message.message.getString(context), duration: duration); return; } showErrorMessage(message.toString(), duration: duration); } showCustomBeautyDialog( BuildContext context, { required QuickAlertType type, required Widget child, String confirmBtnText = '', String? cancelBtnText, Function()? onConfirmBtnTap, Function()? onCancelBtnTap, bool showCancelBtn = false, String title = '', }) { final customColors = Theme.of(context).extension()!; QuickAlert.show( context: context, type: type, widget: child, width: MediaQuery.of(context).size.width > 600 ? 400 : null, barrierDismissible: false, // 禁止点击外部关闭 showCancelBtn: showCancelBtn, confirmBtnText: confirmBtnText == '' ? AppLocale.ok.getString(context) : confirmBtnText, cancelBtnText: cancelBtnText ?? AppLocale.cancel.getString(context), confirmBtnColor: customColors.linkColor!, borderRadius: CustomSize.radiusValue, buttonBorderRadius: CustomSize.radiusValue, backgroundColor: customColors.dialogBackgroundColor!, confirmBtnTextStyle: const TextStyle( color: Colors.white, fontWeight: FontWeight.normal, ), title: title, titleColor: customColors.dialogDefaultTextColor!, textColor: customColors.dialogDefaultTextColor!, cancelBtnTextStyle: TextStyle( color: customColors.dialogDefaultTextColor, fontWeight: FontWeight.normal, ), onConfirmBtnTap: onConfirmBtnTap, onCancelBtnTap: onCancelBtnTap, ); } Future showBeautyDialog( BuildContext context, { required QuickAlertType type, String? text, String? title, String? customAsset, Widget? widget, String confirmBtnText = '', String? cancelBtnText, Function()? onConfirmBtnTap, Function()? onCancelBtnTap, bool showCancelBtn = false, bool barrierDismissible = false, // 禁止点击外部关闭 }) { final customColors = Theme.of(context).extension()!; return QuickAlert.show( context: context, type: type, text: text, customAsset: customAsset, widget: widget, width: MediaQuery.of(context).size.width > 600 ? 400 : null, barrierDismissible: barrierDismissible, showCancelBtn: showCancelBtn, confirmBtnText: confirmBtnText == '' ? AppLocale.ok.getString(context) : confirmBtnText, cancelBtnText: cancelBtnText ?? AppLocale.cancel.getString(context), confirmBtnColor: customColors.linkColor!, borderRadius: CustomSize.radiusValue, buttonBorderRadius: CustomSize.radiusValue, backgroundColor: customColors.dialogBackgroundColor!, confirmBtnTextStyle: const TextStyle( color: Colors.white, fontWeight: FontWeight.normal, ), title: title ?? '', titleColor: customColors.dialogDefaultTextColor!, textColor: customColors.dialogDefaultTextColor!, cancelBtnTextStyle: TextStyle( color: customColors.dialogDefaultTextColor, fontWeight: FontWeight.normal, ), onConfirmBtnTap: onConfirmBtnTap, onCancelBtnTap: onCancelBtnTap, ); } showErrorMessage(String message, {Duration duration = const Duration(seconds: 3)}) { HapticFeedbackHelper.mediumImpact(); Logger.instance.e(message); BotToast.showText( text: message, duration: duration, textStyle: const TextStyle( fontSize: 15, color: Colors.white, ), align: Alignment.center, ); } showSuccessMessage(String message, {Duration duration = const Duration(seconds: 3)}) async { BotToast.showText( text: message, duration: duration, textStyle: const TextStyle( fontSize: 15, color: Colors.white, ), align: Alignment.center, ); } showImportantMessage(BuildContext context, String message) { openModalBottomSheet( context, (context) { return Container( padding: const EdgeInsets.all(10), child: Text( message, style: const TextStyle(fontSize: 16), ), ); }, heightFactor: 0.1, ); } Future openModalBottomSheet( BuildContext context, Widget Function(BuildContext context) builder, { bool useSafeArea = false, isScrollControlled = true, double heightFactor = 0.5, String? title, bool disableEvent = false, bool disableCompleteEvent = false, bool disableInitEvent = false, }) { final customColors = Theme.of(context).extension()!; if (!disableEvent && !disableInitEvent) { GlobalEvent().emit('hideBottomNavigatorBar'); } return showModalBottomSheet( context: context, useSafeArea: useSafeArea, isScrollControlled: isScrollControlled, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: CustomSize.radius), ), elevation: 0, backgroundColor: Colors.transparent, builder: (context) { return BottomSheetBox( child: FractionallySizedBox( heightFactor: heightFactor, child: Column( mainAxisSize: MainAxisSize.min, children: [ buildBottomSheetTopBar(customColors), if (title != null) Text( title, style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: customColors.weakTextColorPlus, ), ), if (title != null) const SizedBox(height: 10), Expanded( child: builder(context), ), ], ), ), ); }, ).whenComplete(() { if (!disableEvent && !disableCompleteEvent) { GlobalEvent().emit('showBottomNavigatorBar'); } }); } openConfirmDialog( BuildContext context, String message, Function() onConfirm, { Widget? title, bool danger = false, String? confirmText, String? cancelText, }) { HapticFeedbackHelper.mediumImpact(); final customColors = Theme.of(context).extension()!; GlobalEvent().emit('hideBottomNavigatorBar'); showModalBottomSheet( context: context, elevation: 0, backgroundColor: Colors.transparent, builder: (context) { return BottomSheetBox( child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ buildBottomSheetTopBar(customColors), const SizedBox(height: 10), if (title != null) title, if (title != null && message != '') const SizedBox(height: 10), if (message != '') Text( message, style: TextStyle( color: customColors.dialogDefaultTextColor, fontSize: title == null ? 16 : 12, ), textAlign: TextAlign.center, maxLines: title == null ? 4 : 2, ), const SizedBox(height: 20), Column( mainAxisSize: MainAxisSize.min, children: [ Button( title: confirmText ?? AppLocale.ok.getString(context), onPressed: () { onConfirm(); context.pop(); }, size: const ButtonSize.full(), color: danger ? const Color.fromARGB(255, 255, 17, 0) : customColors.linkColor, backgroundColor: const Color.fromARGB(36, 222, 222, 222), ), const SizedBox(height: 10), Button( title: cancelText ?? AppLocale.cancel.getString(context), backgroundColor: const Color.fromARGB(36, 222, 222, 222), color: customColors.dialogDefaultTextColor?.withAlpha(150), onPressed: () { context.pop(); }, size: const ButtonSize.full(), ), const SizedBox(height: 10), ], ), ], ), ); }, ).whenComplete(() => GlobalEvent().emit('showBottomNavigatorBar')); // showDialog( // context: context, // builder: (_) { // return SizedBox( // width: _calDialogWidth(context), // child: CustomDialog( // customColors: customColors, // title: title ?? Container(), // content: Text( // message, // style: TextStyle(color: customColors.dialogDefaultTextColor), // ), // actions: [ // TextButton( // onPressed: () { // context.pop(); // }, // style: TextButton.styleFrom( // foregroundColor: Colors.grey, // ), // child: Text( // AppLocale.cancel.getString(context), // style: TextStyle(color: customColors.dialogDefaultTextColor), // ), // ), // const SizedBox(width: 10), // Button( // title: AppLocale.ok.getString(context), // onPressed: () { // onConfirm(); // context.pop(); // }, // // backgroundColor: danger ? Colors.red : null, // ), // ], // ), // ); // }, // ); } Center buildBottomSheetTopBar(CustomColors customColors) { return Center( child: FractionallySizedBox( widthFactor: 0.25, child: Container( margin: const EdgeInsets.only(top: 8, bottom: 10), height: 4, decoration: BoxDecoration( color: const Color.fromARGB(255, 192, 192, 192), borderRadius: CustomSize.borderRadius, border: Border.all( color: Colors.black12, width: 0.5, ), ), ), ), ); } Future openDialog( BuildContext context, { Widget? title, required Builder builder, required bool Function() onSubmit, Function()? afterSubmit, bool showCancel = true, String? confirmText, bool barrierDismissible = true, }) { final customColors = Theme.of(context).extension()!; return showDialog( context: context, builder: (context) => CustomDialog( title: title, customColors: customColors, content: SizedBox( width: _calDialogWidth(context), child: builder.build(context), ), actions: [ if (showCancel) TextButton( onPressed: () { context.pop(); }, child: Text( AppLocale.cancel.getString(context), style: TextStyle(color: customColors.dialogDefaultTextColor), ), ), Button( onPressed: () { if (onSubmit()) { context.pop(); } if (afterSubmit != null) { afterSubmit(); } }, title: confirmText ?? AppLocale.ok.getString(context), backgroundColor: const Color.fromARGB(36, 222, 222, 222), color: customColors.linkColor, ) ], ), barrierDismissible: barrierDismissible, ); } double _calDialogWidth(BuildContext context) { final windowWidth = MediaQuery.of(context).size.width * 0.8; if (windowWidth > 350) { return 350; } return windowWidth; } openListSelectDialogWithDatasource({ required bool Function(SelectorItem value) onSelected, required BuildContext context, required Future>? future, required SelectorItem Function(T value) itemBuilder, double heightFactor = 0.5, bool enableSearch = false, String? title, Object? value, bool horizontal = false, int horizontalCount = 4, EdgeInsets? innerPadding, }) { openModalBottomSheet( context, (context) { return FutureBuilder( future: future, builder: (context, snapshot) { if (!snapshot.hasData) { return const Center( child: CircularProgressIndicator(), ); } return ItemSearchSelector( enableSearch: enableSearch, items: (snapshot.data ?? []).map((e) => itemBuilder(e)).toList(), onSelected: onSelected, value: value, horizontal: horizontal, horizontalCount: horizontalCount, innerPadding: innerPadding, ); }, ); }, heightFactor: heightFactor, title: title, ); } openListSelectDialog( BuildContext context, List items, bool Function(SelectorItem value) onSelected, { double heightFactor = 0.5, bool enableSearch = false, String? title, Object? value, bool horizontal = false, int horizontalCount = 4, EdgeInsets? innerPadding, }) { openModalBottomSheet( context, (context) { return ItemSearchSelector( enableSearch: enableSearch, items: items, onSelected: onSelected, value: value, horizontal: horizontal, horizontalCount: horizontalCount, innerPadding: innerPadding, ); }, heightFactor: heightFactor, title: title, ); } /// 弹出一个输入框 openTextFieldDialog( BuildContext context, { required String title, String? hint, String? defaultValue, int? maxLine, bool obscureText = false, int? maxLength, Icon? suffixIcon, bool withSuffixIcon = false, bool showCounter = false, bool enableSearch = false, List>? dataSources, Future>>? futureDataSources, required bool Function(String) onSubmit, }) { final customColors = Theme.of(context).extension()!; final controller = TextEditingController(text: defaultValue ?? ''); GlobalEvent().emit('hideBottomNavigatorBar'); showModalBottomSheet( context: context, backgroundColor: Colors.transparent, isScrollControlled: true, builder: (context) { return BottomSheetBox( child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ buildBottomSheetTopBar(customColors), EnhancedTextField( enableBackground: true, labelPosition: LabelPosition.top, labelText: title, customColors: customColors, controller: controller, maxLines: obscureText ? 1 : maxLine, obscureText: obscureText, maxLength: maxLength, showCounter: showCounter, hintText: hint, inputSelector: withSuffixIcon ? IconButton( icon: suffixIcon ?? Icon( Icons.style, color: customColors.dialogDefaultTextColor, size: 16, ), onPressed: () { openModalBottomSheet( context, (context) { if (futureDataSources != null) { return FutureBuilder( future: futureDataSources, builder: (context, snapshot) { if (snapshot.hasData) { return ItemSearchSelector( items: snapshot.data as List, onSelected: (value) { controller.text = value.value; return true; }, enableSearch: enableSearch, ); } return const Center( child: CircularProgressIndicator(), ); }, ); } return ItemSearchSelector( items: dataSources ?? [], onSelected: (value) { controller.text = value.value; return true; }, enableSearch: enableSearch, ); }, disableEvent: true, ); }, ) : null, ), const SizedBox(height: 10), Column( mainAxisSize: MainAxisSize.min, children: [ Button( title: AppLocale.ok.getString(context), backgroundColor: const Color.fromARGB(36, 222, 222, 222), color: customColors.linkColor, onPressed: () { if (onSubmit(controller.text)) { context.pop(); } }, size: const ButtonSize.full(), // backgroundColor: danger ? Colors.red : null, ), const SizedBox(height: 10), Button( title: AppLocale.cancel.getString(context), backgroundColor: const Color.fromARGB(36, 222, 222, 222), color: customColors.dialogDefaultTextColor?.withAlpha(150), onPressed: () { context.pop(); }, size: const ButtonSize.full(), ), const SizedBox(height: 20), ], ), ], ), ); }, ).whenComplete(() { GlobalEvent().emit('showBottomNavigatorBar'); Future.delayed(const Duration(seconds: 1), () { controller.dispose(); }); }); } openFullscreenDialog( BuildContext context, { required Widget child, required String title, List? actions, }) { HapticFeedbackHelper.mediumImpact(); Navigator.of(context).push( MaterialPageRoute( builder: (context) => _FullScreenDialog( title: title, actions: actions, child: child, ), ), ); } @immutable class _FullScreenDialog extends StatelessWidget { final String title; final Widget child; final List? actions; const _FullScreenDialog({ Key? key, required this.title, required this.child, this.actions, }) : super(key: key); @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( title: Text(title), toolbarHeight: CustomSize.toolbarHeight, actions: actions, leading: IconButton( icon: const Icon(Icons.close), onPressed: () => Navigator.of(context).pop(), ), ), backgroundColor: customColors.backgroundColor, body: SafeArea( child: child, ), ), ); } } class CustomDialog extends StatelessWidget { final List? actions; final Widget? title; final Widget? content; final Color? backgroundColor; final bool glassEffect; final CustomColors customColors; const CustomDialog({ super.key, required this.customColors, this.actions, this.title, this.content, this.backgroundColor, this.glassEffect = false, }); @override Widget build(BuildContext context) { final dialog = AlertDialog( title: title, elevation: 0, shape: RoundedRectangleBorder(borderRadius: CustomSize.borderRadius), titleTextStyle: TextStyle( color: customColors.dialogDefaultTextColor, fontSize: 20, fontWeight: FontWeight.bold, ), backgroundColor: backgroundColor ?? (glassEffect ? customColors.dialogBackgroundColor!.withAlpha(50) : customColors.dialogBackgroundColor), content: content, actions: actions, actionsAlignment: MainAxisAlignment.spaceAround, ); if (glassEffect) { return ClipRect( child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), child: dialog, ), ); } return dialog; } } ================================================ FILE: lib/page/component/effect/glass.dart ================================================ import 'dart:ui'; import 'package:flutter/material.dart'; class GlassEffect extends StatelessWidget { final Widget child; final bool enabled; const GlassEffect({ super.key, required this.child, this.enabled = false, }); @override Widget build(BuildContext context) { if (!enabled) { return Container( margin: const EdgeInsets.symmetric(vertical: 8), child: child, ); } return ClipRRect( borderRadius: const BorderRadius.only( topLeft: Radius.circular(20), topRight: Radius.circular(20), ), child: BackdropFilter( filter: ImageFilter.blur( sigmaX: 20.0, sigmaY: 20.0, ), child: Container( decoration: BoxDecoration( color: Colors.white12, borderRadius: const BorderRadius.only( topLeft: Radius.circular(20), topRight: Radius.circular(20), ), border: Border.all( color: Colors.black26, width: 0.5, ), ), child: Column( children: [ Center( child: FractionallySizedBox( widthFactor: 0.25, child: Container( margin: const EdgeInsets.symmetric( vertical: 8, ), height: 4, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(2), border: Border.all( color: Colors.black12, width: 0.5, ), ), ), ), ), Expanded( child: Container( alignment: Alignment.center, child: child, ), ), ], ), ), ), ); } } ================================================ FILE: lib/page/component/enhanced_button.dart ================================================ import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; class EnhancedButton extends StatelessWidget { final String title; final Color? backgroundColor; final Color? color; final double? height; final double? width; final double? fontSize; final Widget? icon; final Function() onPressed; const EnhancedButton({ super.key, required this.title, this.backgroundColor, this.color, this.height, this.width, this.fontSize, required this.onPressed, this.icon, }); @override Widget build(BuildContext context) { var customColors = Theme.of(context).extension()!; return Material( color: backgroundColor ?? customColors.linkColor, borderRadius: CustomSize.borderRadius, child: InkWell( borderRadius: CustomSize.borderRadiusAll, onTap: onPressed, child: Container( height: height ?? 42, width: width ?? double.infinity, alignment: Alignment.center, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ if (icon != null) icon!, if (icon != null) const SizedBox(width: 5), Text( title, textAlign: TextAlign.center, style: TextStyle( color: color ?? Colors.white, fontSize: fontSize ?? 17, ), ), ], ), ), ), ); } } ================================================ FILE: lib/page/component/enhanced_error.dart ================================================ import 'package:askaide/helper/helper.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; class EnhancedErrorWidget extends StatelessWidget { final Object? error; const EnhancedErrorWidget({super.key, required this.error}); @override Widget build(BuildContext context) { if (error == null) { return Container(); } return Scaffold( appBar: AppBar( toolbarHeight: CustomSize.toolbarHeight, ), body: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ RichText( text: TextSpan( children: [ TextSpan( text: resolveError(context, error!), style: const TextStyle( color: Colors.red, fontSize: 17, ), ), ], ), ), InkWell( onTap: () { context.go('/login'); }, borderRadius: CustomSize.borderRadiusAll, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), child: Text( AppLocale.clickToReSignin.getString(context), textScaler: const TextScaler.linear(0.8), style: const TextStyle( color: Colors.red, ), ), ), ), ], ), ), ); } } ================================================ FILE: lib/page/component/enhanced_input.dart ================================================ import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; class EnhancedInputSimple extends StatelessWidget { final String title; final String? value; final VoidCallback onPressed; final Icon? icon; final Widget? description; final EdgeInsets? padding; const EnhancedInputSimple({ super.key, required this.title, required this.onPressed, this.value, this.icon, this.description, this.padding, }); @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return EnhancedInput( padding: padding, title: Text( title, style: TextStyle( color: customColors.textfieldLabelColor, fontSize: 16, ), ), value: value != null ? Text( value!, overflow: TextOverflow.ellipsis, style: TextStyle( color: customColors.textfieldValueColor, fontSize: 15, ), ) : null, onPressed: onPressed, icon: icon, description: description, ); } } class EnhancedInput extends StatelessWidget { final Widget title; final Widget? value; final VoidCallback onPressed; final Icon? icon; final Widget? description; final EdgeInsets? padding; const EnhancedInput({ super.key, required this.title, required this.onPressed, this.value, this.icon, this.description, this.padding, }); @override Widget build(BuildContext context) { return InkWell( onTap: onPressed, borderRadius: CustomSize.borderRadiusAll, child: Container( padding: padding ?? const EdgeInsets.symmetric(vertical: 12), child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ SizedBox( width: 85, child: title, ), Expanded( child: Row( mainAxisAlignment: MainAxisAlignment.end, mainAxisSize: MainAxisSize.min, children: [ Flexible(child: value ?? Container()), const SizedBox(width: 5), icon ?? const Icon( CupertinoIcons.chevron_forward, size: 18, color: Colors.grey, ), ], ), ), ], ), if (description != null) const SizedBox(height: 10), if (description != null) description!, ], ), ), ); } } ================================================ FILE: lib/page/component/enhanced_popup_menu.dart ================================================ import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; class EnhancedPopupMenuItem { final String title; final IconData? icon; final Color? iconColor; final Function(BuildContext)? onTap; const EnhancedPopupMenuItem({ required this.title, this.icon, this.iconColor, this.onTap, }); } class EnhancedPopupMenu extends StatelessWidget { final List items; final IconData? icon; final Color? color; final String? tooltip; final void Function()? onOpened; final void Function()? onCanceled; const EnhancedPopupMenu({ super.key, required this.items, this.icon, this.color, this.tooltip, this.onOpened, this.onCanceled, }); @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return PopupMenuButton( icon: Icon(icon ?? Icons.more_horiz, color: color), tooltip: tooltip, splashRadius: 20, elevation: 0, enableFeedback: true, color: customColors.backgroundColor, shape: RoundedRectangleBorder(borderRadius: CustomSize.borderRadius), position: PopupMenuPosition.under, onOpened: onOpened, onCanceled: onCanceled, onSelected: (value) { if (value.onTap != null) { value.onTap!(context); } }, itemBuilder: (context) { return [ for (final item in items) PopupMenuItem( // onTap: () { // item.onTap!(context); // }, value: item, child: Row( children: [ if (item.icon != null) Icon(item.icon!, size: 15, color: item.iconColor), if (item.icon != null) const SizedBox(width: 10), Text( item.title, style: const TextStyle(fontSize: 14), ), ], ), ) ]; }, ); } } ================================================ FILE: lib/page/component/enhanced_textfield.dart ================================================ import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; enum LabelPosition { top, left, inner } class InputSelector extends StatelessWidget { final Widget title; final VoidCallback onTap; const InputSelector({ super.key, required this.title, required this.onTap, }); @override Widget build(BuildContext context) { return TextButton( onPressed: onTap, style: ButtonStyle( overlayColor: WidgetStateProperty.all(Colors.transparent), ), child: title, ); } } class EnhancedTextField extends StatefulWidget { final int? maxLength; final bool? autofocus; final TextInputType? keyboardType; final TextEditingController? controller; final FocusNode? focusNode; final TextAlignVertical? textAlignVertical; final CustomColors customColors; final String? labelText; final double? labelFontSize; final double? labelWidth; final Widget? labelWidget; final int? minLines; final int? maxLines; final bool showCounter; final String? hintText; final Widget? suffixIcon; final bool? readOnly; final bool? obscureText; final LabelPosition? labelPosition; final Widget? inputSelector; final List? inputFormatters; final TextDirection? textDirection; final double? fieldWidth; final void Function(String)? onChanged; final String? initValue; final bool enableBackground; final Widget? bottomButtons; final Widget? bottomButton; final VoidCallback? bottomButtonOnPressed; final double? fontSize; final bool? enabled; final Color? hintColor; final double? hintTextSize; final Widget? labelHelpWidget; final Widget? middleWidget; final Function(bool hasFocus)? onFocusChange; const EnhancedTextField({ super.key, required this.customColors, this.maxLength, this.autofocus, this.labelWidget, this.keyboardType, this.controller, this.focusNode, this.textAlignVertical, this.labelText, this.labelFontSize, this.labelWidth, this.minLines, this.maxLines = 1, this.showCounter = true, this.hintText, this.suffixIcon, this.readOnly, this.obscureText, this.labelPosition, this.inputSelector, this.inputFormatters, this.textDirection, this.fieldWidth, this.onChanged, this.initValue, this.bottomButtons, this.bottomButton, this.bottomButtonOnPressed, this.enableBackground = false, this.fontSize, this.enabled, this.hintColor, this.hintTextSize, this.labelHelpWidget, this.middleWidget, this.onFocusChange, }); @override State createState() => _EnhancedTextFieldState(); } class _EnhancedTextFieldState extends State { var textLength = 0; late final Function() listener; @override void initState() { super.initState(); listener = () { if (mounted) { setState(() { textLength = widget.controller!.text.length; }); } }; if (widget.showCounter) { widget.controller?.addListener(listener); } } @override void dispose() { if (widget.showCounter) { widget.controller?.removeListener(listener); } super.dispose(); } @override Widget build(BuildContext context) { if ((widget.labelText != null || widget.labelWidget != null) && widget.labelPosition != LabelPosition.inner) { // 上下结构 if (widget.labelPosition == LabelPosition.top) { return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ widget.labelWidget != null ? widget.labelWidget! : Row( children: [ Text( widget.labelText!, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: widget.labelFontSize ?? 16, color: widget.customColors.textfieldLabelColor, ), ), const SizedBox(width: 5), if (widget.labelHelpWidget != null) widget.labelHelpWidget!, ], ), if (widget.inputSelector != null) widget.inputSelector!, ], ), const SizedBox(height: 10), _buildTextField(), ], ); } // 左右结构 return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ SizedBox( width: widget.labelWidth ?? 80, child: widget.labelWidget != null ? widget.labelWidget! : Row( mainAxisSize: MainAxisSize.min, children: [ Expanded( child: Text( widget.labelText!, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: widget.labelFontSize ?? 16, color: widget.customColors.textfieldLabelColor, ), ), ), if (widget.labelHelpWidget != null) ...[ const SizedBox(width: 5), widget.labelHelpWidget!, ] ], ), ), const SizedBox(width: 10), widget.fieldWidth != null ? SizedBox( width: widget.fieldWidth, child: _buildTextField(), ) : Expanded( child: _buildTextField(), ), ], ); } // 无标题结构 return _buildTextField(); } Widget _buildTextField() { return Column( mainAxisSize: MainAxisSize.min, children: [ Stack( children: [ Column( mainAxisSize: MainAxisSize.min, children: [ Focus( onFocusChange: widget.onFocusChange, child: TextFormField( initialValue: widget.initValue, readOnly: widget.readOnly ?? false, focusNode: widget.focusNode, controller: widget.controller, inputFormatters: widget.inputFormatters, textDirection: widget.textDirection, obscureText: widget.obscureText ?? false, enabled: widget.enabled ?? true, style: TextStyle( color: widget.customColors.textfieldValueColor, fontSize: widget.fontSize ?? 15, ), decoration: InputDecoration( filled: widget.enableBackground, fillColor: widget.customColors.textfieldBackgroundColor, hintText: widget.hintText, hintStyle: TextStyle( fontSize: widget.hintTextSize ?? CustomSize.defaultHintTextSize, color: widget.hintColor ?? widget.customColors.textfieldHintColor, ), hintTextDirection: widget.textDirection, counterText: "", border: resolveInputBorder(), enabledBorder: resolveInputBorder(), focusedBorder: resolveInputBorder(), // isDense: true, contentPadding: EdgeInsets.only( top: widget.labelPosition == LabelPosition.top ? 0 : 10, left: widget.enableBackground ? 15 : 0, right: widget.enableBackground ? 15 : 0, bottom: (widget.showCounter || widget.bottomButton != null) && widget.middleWidget == null ? 30 : 10, ), labelText: widget.labelPosition == LabelPosition.inner ? widget.labelText : null, labelStyle: TextStyle( color: widget.customColors.textfieldLabelColor, ), suffixIcon: widget.suffixIcon ?? (widget.labelPosition == LabelPosition.left ? widget.inputSelector : null), ), cursorRadius: CustomSize.radius, keyboardType: widget.keyboardType, autofocus: widget.autofocus ?? false, maxLength: widget.maxLength, minLines: widget.minLines, maxLines: widget.maxLines, onChanged: widget.controller == null ? (value) { setState(() { textLength = value.length; }); if (widget.onChanged != null) { widget.onChanged!(value); } } : null, ), ), widget.middleWidget ?? const SizedBox(), ], ), if (widget.showCounter) Positioned( right: 10, bottom: 10, child: Text( "$textLength / ${widget.maxLength}", style: TextStyle( fontSize: 12, color: widget.customColors.chatInputPanelText, ), ), ), if (widget.bottomButtons != null) Positioned( right: 0, bottom: 0, child: widget.bottomButtons!, ), if (widget.bottomButton != null) Positioned( right: 0, bottom: 0, child: MaterialButton( elevation: 0, splashColor: Colors.transparent, highlightColor: Colors.transparent, padding: const EdgeInsets.all(0), minWidth: 60, shape: RoundedRectangleBorder(borderRadius: CustomSize.borderRadius), onPressed: widget.bottomButtonOnPressed, child: widget.bottomButton!, ), ), ], ), ], ); } InputBorder resolveInputBorder() { if (widget.enableBackground) { return const OutlineInputBorder(borderRadius: CustomSize.borderRadiusAll, borderSide: BorderSide.none); } return InputBorder.none; } } ================================================ FILE: lib/page/component/file_preview.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; class FilePreview extends StatelessWidget { final String? filename; final String? fileUrl; final String fileType; final double maxWidth; final MainAxisAlignment mainAxisAlignment; const FilePreview({ super.key, this.filename, this.fileUrl, this.maxWidth = 300, this.mainAxisAlignment = MainAxisAlignment.start, required this.fileType, }); @override Widget build(BuildContext context) { var iconFilePath = 'assets/icons/file.png'; switch (fileType) { case 'pdf': iconFilePath = 'assets/icons/pdf.png'; break; case 'docx': iconFilePath = 'assets/icons/doc.png'; break; case 'txt': iconFilePath = 'assets/icons/txt.png'; break; } return ConstrainedBox( constraints: BoxConstraints( maxWidth: maxWidth, maxHeight: 25, ), child: Row( mainAxisAlignment: mainAxisAlignment, mainAxisSize: MainAxisSize.min, children: [ Image.asset( iconFilePath, width: 20, height: 20, ), if (filename != null && filename != '') ...[ const SizedBox(width: 5), Flexible( child: Text( filename!, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12), ), ), ], ], ), ); } } ================================================ FILE: lib/page/component/gallery_item_share.dart ================================================ import 'dart:io'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/helper.dart'; import 'package:askaide/helper/image.dart'; import 'package:askaide/helper/logger.dart'; import 'package:askaide/helper/platform.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/column_block.dart'; import 'package:askaide/page/component/enhanced_popup_menu.dart'; import 'package:askaide/page/component/image.dart'; import 'package:askaide/page/component/image_preview.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/share.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/creative_island/gallery/gallery_item.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:file_saver/file_saver.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:image_gallery_saver/image_gallery_saver.dart'; import 'package:widgets_to_image/widgets_to_image.dart'; class GalleryItemShareScreen extends StatefulWidget { final List images; final String? prompt; final String? negativePrompt; const GalleryItemShareScreen({ super.key, required this.images, this.prompt, this.negativePrompt, }); @override State createState() => _GalleryItemShareScreenState(); } class _GalleryItemShareScreenState extends State { final WidgetsToImageController controller = WidgetsToImageController(); bool showQRCode = true; @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return Scaffold( appBar: AppBar( toolbarHeight: CustomSize.toolbarHeight, actions: [ if (!PlatformTool.isWeb()) TextButton( onPressed: () async { final cancel = BotToast.showCustomLoading( toastBuilder: (cancel) { return LoadingIndicator( message: AppLocale.processingWait.getString(context), ); }, allowClick: false, duration: const Duration(seconds: 15), ); try { final data = await controller.capture(); if (data != null) { final file = await writeTempFile('share-image.png', data); cancel(); await shareTo( // ignore: use_build_context_synchronously context, content: 'images', images: [ file.path, ], ); } } finally { cancel(); } }, child: Row( children: [ Icon(Icons.share, size: 14, color: customColors.weakLinkColor), const SizedBox(width: 5), Text( AppLocale.share.getString(context), style: TextStyle(color: customColors.weakLinkColor, fontSize: 14), ), ], ), ), EnhancedPopupMenu( items: [ EnhancedPopupMenuItem( title: AppLocale.saveToLocal.getString(context), icon: Icons.save, onTap: (ctx) async { final cancel = BotToast.showCustomLoading( toastBuilder: (cancel) { return LoadingIndicator( message: AppLocale.processingWait.getString(context), ); }, allowClick: false, duration: const Duration(seconds: 15), ); try { final data = await controller.capture(); if (data != null) { cancel(); // ignore: use_build_context_synchronously if (PlatformTool.isIOS() || PlatformTool.isAndroid()) { await ImageGallerySaver.saveImage(data, quality: 100); showSuccessMessage(AppLocale.operateSuccess.getString(context)); } else { if (PlatformTool.isWindows()) { FileSaver.instance .saveAs( name: randomId(), bytes: data, ext: '.png', mimeType: MimeType.png, ) .then((value) async { if (value == null) { return; } await File(value).writeAsBytes(data); Logger.instance.d('File saved successfully: $value'); showSuccessMessage(AppLocale.operateSuccess.getString(context)); }); } else { FileSaver.instance .saveFile( name: randomId(), bytes: data, ext: 'png', mimeType: MimeType.png, ) .then((value) { Logger.instance.d('File saved successfully: $value'); showSuccessMessage(AppLocale.operateSuccess.getString(context)); }); } } } } finally { cancel(); } }, ), EnhancedPopupMenuItem( title: showQRCode ? AppLocale.dontShowInviteCode.getString(context) : AppLocale.showInviteCode.getString(context), icon: showQRCode ? Icons.visibility_off : Icons.visibility, onTap: (ctx) { setState(() { showQRCode = !showQRCode; }); }, ), ], ) ], ), backgroundColor: customColors.backgroundColor, body: Align( alignment: Alignment.topCenter, child: ConstrainedBox( constraints: const BoxConstraints( maxWidth: CustomSize.smallWindowSize, ), child: SafeArea( child: SingleChildScrollView( child: FutureBuilder( future: APIServer().shareInfo(), builder: (context, snapshot) { if (snapshot.hasError) { return Center( child: Text(resolveError(context, snapshot.error!)), ); } if (snapshot.hasData) { return Column( children: [ WidgetsToImage( controller: controller, child: Container( color: customColors.backgroundContainerColor, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ for (var img in widget.images) Container( decoration: BoxDecoration(color: customColors.backgroundColor), child: NetworkImagePreviewer( url: img, preview: imageURL(img, qiniuImageTypeThumb), hidePreviewButton: true, notClickable: true, borderRadius: BorderRadius.zero, ), ), ColumnBlock( backgroundColor: customColors.backgroundContainerColor, innerPanding: 10, padding: const EdgeInsets.all(15), margin: const EdgeInsets.all(0), borderRadius: 0, children: [ if (widget.prompt != null && widget.prompt!.isNotEmpty) TextItem( title: 'Prompt', value: widget.prompt!, ), if (widget.negativePrompt != null && widget.negativePrompt!.isNotEmpty) TextItem( title: 'Negative Prompt', value: widget.negativePrompt!, ), ], ), if (showQRCode) Container( color: customColors.backgroundContainerColor, child: Padding( padding: const EdgeInsets.symmetric( horizontal: 15, vertical: 20, ), child: Row( children: [ ClipRRect( borderRadius: CustomSize.borderRadius, child: CachedNetworkImageEnhanced( imageUrl: snapshot.data!.qrCode, width: 100, height: 100, ), ), const SizedBox(width: 10), Expanded( child: Text( snapshot.data!.message, ), ), ], ), ), ), ], ), ), ), ], ); } return const Center( child: Text('Loading ...'), ); }), ), ), ), ), ); } } ================================================ FILE: lib/page/component/global_alert.dart ================================================ import 'package:askaide/helper/event.dart'; import 'package:askaide/page/component/chat/markdown.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:url_launcher/url_launcher_string.dart'; class GlobalAlertEvent { String id; String? message; String type; List pages; GlobalAlertEvent({ required this.id, this.message, required this.type, required this.pages, }); toJSON() { return { 'id': id, 'message': message, 'type': type, 'pages': pages, }; } } class GlobalAlert extends StatefulWidget { final String pageKey; const GlobalAlert({super.key, required this.pageKey}); @override State createState() => _GlobalAlertState(); } class _GlobalAlertState extends State { Function? globalAlertListener; late GlobalAlertEvent alertEvent; @override void initState() { alertEvent = APIServer().globalAlertEvent; globalAlertListener = GlobalEvent().on('global-alert', (data) { final event = data as GlobalAlertEvent; if (event.pages.isNotEmpty && !event.pages.contains(widget.pageKey)) { return; } if (mounted) { setState(() { alertEvent = data; }); } }); super.initState(); } @override void dispose() { globalAlertListener?.call(); super.dispose(); } @override Widget build(BuildContext context) { if (alertEvent.id == '' || alertEvent.message == null || alertEvent.message == '') { return const SizedBox(); } return Container( margin: const EdgeInsets.symmetric(horizontal: 15, vertical: 5), padding: const EdgeInsets.symmetric(horizontal: 10), width: double.infinity, decoration: BoxDecoration(color: resolveBackgroundColor(), borderRadius: CustomSize.borderRadius), child: Markdown( data: alertEvent.message!, textStyle: const TextStyle( color: Colors.white, ), onUrlTap: (value) { if (value.startsWith("aidea-app://")) { var route = value.substring('aidea-app://'.length); context.push(route); } else { launchUrlString(value); } }, ), ); } Color resolveBackgroundColor() { switch (alertEvent.type) { case 'error': case 'warning': return const Color.fromARGB(255, 252, 145, 79); case 'info': return const Color.fromARGB(255, 18, 83, 135); default: return Colors.green; } } } ================================================ FILE: lib/page/component/gradient_style.dart ================================================ import 'package:flutter/material.dart'; class GradientStyle { static LinearGradient warmLinear() { return const LinearGradient( begin: Alignment.topRight, end: Alignment.bottomLeft, stops: [0.0, 0.5, 1.0], colors: [ Color.fromARGB(255, 245, 205, 93), Color.fromARGB(255, 234, 146, 75), Color.fromARGB(255, 211, 89, 61), ], ); } static LinearGradient coldLinear() { return const LinearGradient( begin: Alignment.topRight, end: Alignment.bottomLeft, stops: [0.0, 0.5, 1.0], colors: [ Color.fromARGB(255, 82, 181, 208), Color.fromARGB(255, 66, 133, 191), Color.fromARGB(255, 66, 87, 177), ], ); } static LinearGradient greenLinear() { return const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, stops: [0.0, 1.0], colors: [ Color.fromARGB(200, 68, 255, 0), Color.fromARGB(200, 131, 220, 99), ], ); } static LinearGradient whiteLinear() { return const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, stops: [0.0, 1.0], colors: [ Color.fromARGB(200, 255, 255, 255), Color.fromARGB(200, 224, 224, 224), ], ); } } ================================================ FILE: lib/page/component/group_list_widget.dart ================================================ import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; class Group { final String key; final List items; Group({required this.key, required this.items}); } class GroupListWidget extends StatelessWidget { final List items; final String Function(T item) groupKey; final Widget Function(T item) itemBuilder; final EdgeInsets? padding; final EdgeInsets? margin; final bool showTitle; final ScrollPhysics? physics; const GroupListWidget({ super.key, required this.items, required this.groupKey, required this.itemBuilder, this.padding = const EdgeInsets.symmetric(horizontal: 10, vertical: 5), this.margin = const EdgeInsets.symmetric(horizontal: 5, vertical: 5), this.showTitle = false, this.physics, }); @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; var groups = >{}; for (var item in items) { var key = groupKey(item); groups[key] = groups[key] ?? []; groups[key]!.add(item); } return ListView.separated( physics: physics, shrinkWrap: true, itemBuilder: (context, i) { var group = groups.entries.elementAt(i); return Column( children: [ if (showTitle) Container( margin: const EdgeInsets.only(top: 5), padding: const EdgeInsets.only(left: 10, right: 10), width: double.infinity, child: Text( groups.keys.elementAt(i), style: TextStyle( color: customColors.weakTextColor, fontSize: 12, ), ), ), Container( margin: margin, padding: padding, decoration: BoxDecoration( borderRadius: CustomSize.borderRadius, color: customColors.backgroundForDialogListItem, ), child: ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: group.value.length, itemBuilder: (BuildContext context, int index) { return itemBuilder(group.value.elementAt(index)); }, separatorBuilder: (BuildContext context, int index) { return Padding( padding: const EdgeInsets.symmetric(vertical: 1.5), child: Divider( height: 1, color: customColors.columnBlockDividerColor, ), ); }, ), ), ], ); }, separatorBuilder: (BuildContext context, int index) { return const Padding(padding: EdgeInsets.symmetric(horizontal: 15, vertical: 5)); }, itemCount: groups.length, ); } } ================================================ FILE: lib/page/component/icon_box.dart ================================================ import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:flutter/material.dart'; class IconBox extends StatelessWidget { final Icon icon; final Widget title; final Function()? onTap; const IconBox({ super.key, required this.icon, required this.title, this.onTap, }); @override Widget build(BuildContext context) { return MaterialButton( padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), shape: RoundedRectangleBorder(borderRadius: CustomSize.borderRadius), onPressed: onTap, child: Column( children: [ icon, const SizedBox(height: 10), title, ], ), ); } } ================================================ FILE: lib/page/component/icon_box_button.dart ================================================ import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:flutter/material.dart'; class IconBoxButton extends StatelessWidget { final double? width; final String title; final IconData icon; final IconData? smallIcon; final Function()? onTap; const IconBoxButton({ super.key, this.width, required this.title, required this.icon, this.smallIcon, this.onTap, }); @override Widget build(BuildContext context) { return InkWell( borderRadius: CustomSize.borderRadiusAll, onTap: () => onTap?.call(), child: Container( height: 75, width: width, padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), decoration: BoxDecoration( color: Colors.grey.withAlpha(20), border: Border.all( color: Colors.grey.withAlpha(50), ), borderRadius: CustomSize.borderRadius, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(icon), const SizedBox(height: 5), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(title), Icon(smallIcon ?? Icons.keyboard_arrow_right), ], ), ], ), ), ); } } ================================================ FILE: lib/page/component/image.dart ================================================ import 'dart:io'; import 'package:askaide/helper/image.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; ImageProvider resolveImageProvider( String imageUrl, { bool useThumbnail = true, }) { if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) { if (useThumbnail) { imageUrl = imageURL(imageUrl, 'thumb'); } return CachedNetworkImageProviderEnhanced(imageUrl); } return FileImage(File(imageUrl)); } class CachedNetworkImageEnhanced extends StatelessWidget { final String imageUrl; final BoxFit? fit; final double? width; final double? height; final ProgressIndicatorBuilder? progressIndicatorBuilder; final LoadingErrorWidgetBuilder? errorWidget; final ImageWidgetBuilder? imageBuilder; final BaseCacheManager? cacheManager; CachedNetworkImageEnhanced({ super.key, required this.imageUrl, this.fit, this.width, this.height, this.progressIndicatorBuilder, this.errorWidget, this.imageBuilder, this.cacheManager, }) { // Logger.instance.d('load image: $imageUrl'); } @override Widget build(BuildContext context) { return CachedNetworkImage( imageUrl: imageUrl, fit: fit, width: width, height: height, progressIndicatorBuilder: progressIndicatorBuilder, errorWidget: errorWidget, imageBuilder: imageBuilder, cacheManager: cacheManager, ); } } class CachedNetworkImageProviderEnhanced extends CachedNetworkImageProvider { CachedNetworkImageProviderEnhanced( super.url, { super.maxHeight, super.maxWidth, super.scale = 1.0, super.errorListener, super.headers, super.cacheManager, super.cacheKey, }) { // Logger.instance.d('load image: $url'); } } ================================================ FILE: lib/page/component/image_action.dart ================================================ import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/button.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api/creative.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; Future openImageWorkflowActionDialog( BuildContext context, CustomColors customColors, String imageUrl, ) { return openModalBottomSheet( context, (context) { return Column( children: [ Text( AppLocale.selectShortcutAction.getString(context), style: TextStyle( color: customColors.weakTextColorPlus, ), ), Expanded( child: FutureBuilder( future: APIServer().creativeIslandItemsV2(), builder: (context, snapshot) { if (snapshot.hasData) { Map itemsMap = {}; for (var item in snapshot.data!) { itemsMap[item.id] = item; } return actionsBuilder(itemsMap, customColors, context, imageUrl); } return const LoadingIndicator( message: 'Loading, please wait...', ); }, ), ), ], ); }, heightFactor: 0.5, ); } Widget actionsBuilder( Map itemsMap, CustomColors customColors, BuildContext context, String imageUrl, ) { return Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const SizedBox(height: 20), Expanded( child: Column( mainAxisSize: MainAxisSize.min, children: [ if (itemsMap.containsKey('image-to-image')) Button( title: AppLocale.imageToImage.getString(context), icon: Icon( Icons.collections_outlined, size: 16, color: customColors.weakLinkColor, ), onPressed: () { context.pop(); context.push(Uri( path: '/creative-draw/create', queryParameters: { 'id': 'image-to-image', 'mode': 'image-to-image', 'note': itemsMap['image-to-image']!.note, 'init_image': imageUrl, }, ).toString()); }, size: const ButtonSize.full(), color: customColors.weakLinkColor, backgroundColor: const Color.fromARGB(34, 183, 183, 183), ), if (itemsMap.containsKey('image-to-image')) const SizedBox(height: 10), if (itemsMap.containsKey('image-to-video')) Button( title: AppLocale.imageToVideo.getString(context), icon: Icon( Icons.video_camera_back_outlined, size: 16, color: customColors.weakLinkColor, ), onPressed: () { context.pop(); context.push(Uri( path: '/creative-draw/create-video', queryParameters: { 'note': itemsMap['image-to-video']!.note, 'init_image': imageUrl, }, ).toString()); }, size: const ButtonSize.full(), color: customColors.weakLinkColor, backgroundColor: const Color.fromARGB(34, 183, 183, 183), ), if (itemsMap.containsKey('image-to-video')) const SizedBox(height: 10), if (itemsMap.containsKey('image-upscale')) Button( title: AppLocale.hdRestoration.getString(context), icon: Icon( Icons.hd_outlined, size: 16, color: customColors.weakLinkColor, ), onPressed: () { context.pop(); context.push(Uri( path: '/creative-draw/create-upscale', queryParameters: { 'note': itemsMap['image-upscale']!.note, 'init_image': imageUrl, }, ).toString()); }, size: const ButtonSize.full(), color: customColors.weakLinkColor, backgroundColor: const Color.fromARGB(34, 183, 183, 183), ), if (itemsMap.containsKey('image-upscale')) const SizedBox(height: 10), if (itemsMap.containsKey('image-colorize')) Button( title: AppLocale.colorizeImage.getString(context), icon: Icon( Icons.palette_outlined, size: 16, color: customColors.weakLinkColor, ), onPressed: () { context.pop(); context.push(Uri( path: '/creative-draw/create-colorize', queryParameters: { 'note': itemsMap['image-colorize']!.note, 'init_image': imageUrl, }, ).toString()); }, size: const ButtonSize.full(), color: customColors.weakLinkColor, backgroundColor: const Color.fromARGB(34, 183, 183, 183), ), ], ), ), Container( margin: const EdgeInsets.only(bottom: 20), child: Button( title: AppLocale.cancel.getString(context), backgroundColor: const Color.fromARGB(36, 222, 222, 222), color: customColors.dialogDefaultTextColor?.withAlpha(150), onPressed: () { context.pop(); }, size: const ButtonSize.full(), ), ), ], ); } ================================================ FILE: lib/page/component/image_preview.dart ================================================ import 'dart:io'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/helper.dart'; import 'package:askaide/helper/image.dart'; import 'package:askaide/helper/logger.dart'; import 'package:askaide/helper/platform.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/gallery_item_share.dart'; import 'package:askaide/page/component/image.dart'; import 'package:askaide/page/component/image_action.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:before_after/before_after.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:file_saver/file_saver.dart'; import 'package:flutter/material.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:image_gallery_saver/image_gallery_saver.dart'; import 'package:photo_view/photo_view.dart'; class NetworkImagePreviewer extends StatefulWidget { final String url; final String? preview; final String? original; final String? description; final bool hidePreviewButton; final bool notClickable; final BorderRadius? borderRadius; const NetworkImagePreviewer({ super.key, required this.url, this.preview, this.description, this.original, this.hidePreviewButton = false, this.notClickable = false, this.borderRadius, }); @override State createState() => _NetworkImagePreviewerState(); } class _NetworkImagePreviewerState extends State { @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; if (widget.hidePreviewButton) { return ClipRRect( borderRadius: widget.borderRadius ?? CustomSize.borderRadius, child: widget.original == null ? _buildImage(widget.borderRadius) : BeforeAfter( beforeImage: Image(image: CachedNetworkImageProviderEnhanced(imageURL(widget.original!, qiniuImageTypeThumb))), afterImage: Image( image: CachedNetworkImageProviderEnhanced( imageURL(widget.preview ?? widget.url, qiniuImageTypeThumb))), thumbWidth: 1.0, ), ); } return Container( decoration: BoxDecoration(color: customColors.columnBlockBackgroundColor, borderRadius: CustomSize.borderRadius), child: Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ widget.original == null ? _buildImage(const BorderRadius.only(topLeft: CustomSize.radius, topRight: CustomSize.radius)) : ClipRRect( borderRadius: const BorderRadius.only(topLeft: CustomSize.radius, topRight: CustomSize.radius), child: BeforeAfter( imageCornerRadius: 0, beforeImage: Image( image: CachedNetworkImageProviderEnhanced(imageURL(widget.original!, qiniuImageTypeThumb))), afterImage: Image( image: CachedNetworkImageProviderEnhanced( imageURL(widget.preview ?? widget.url, qiniuImageTypeThumb))), thumbWidth: 0.5, thumbRadius: 3, ), ), Padding( padding: const EdgeInsets.only(right: 10), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ IconButton( splashColor: Colors.transparent, highlightColor: Colors.transparent, hoverColor: Colors.transparent, icon: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.share, size: 14, color: customColors.weakLinkColor, ), const SizedBox(width: 5), Text( AppLocale.share.getString(context), style: TextStyle( fontSize: 12, color: customColors.weakLinkColor, ), ), ], ), onPressed: () { Navigator.push( context, MaterialPageRoute( fullscreenDialog: true, builder: (context) => GalleryItemShareScreen( images: [widget.url], ), ), ); }, ), IconButton( splashColor: Colors.transparent, highlightColor: Colors.transparent, hoverColor: Colors.transparent, icon: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.webhook, size: 14, color: customColors.weakLinkColor, ), const SizedBox(width: 5), Text( AppLocale.shortcut.getString(context), style: TextStyle( fontSize: 12, color: customColors.weakLinkColor, ), ), ], ), onPressed: () { openImageWorkflowActionDialog( context, customColors, widget.url, ); }, ), IconButton( splashColor: Colors.transparent, highlightColor: Colors.transparent, hoverColor: Colors.transparent, icon: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.open_in_new, size: 14, color: customColors.weakLinkColor, ), const SizedBox(width: 5), Text( AppLocale.preview.getString(context), style: TextStyle( fontSize: 12, color: customColors.weakLinkColor, ), ), ], ), onPressed: () { try { openImagePreviewDialog( context, customColors, imageProvider: CachedNetworkImageProviderEnhanced(widget.url), imageUrl: widget.url, ); } catch (e) { showErrorMessageEnhanced(context, 'Image loading failed, please try again later'); } }, ), ], ), ), ], ), ); } Widget _buildImage(BorderRadius? borderRadius) { return CachedNetworkImageEnhanced( imageUrl: widget.preview ?? widget.url, cacheManager: DefaultCacheManager(), imageBuilder: (context, imageProvider) { if (widget.notClickable) { return Image(image: imageProvider, fit: BoxFit.cover); } return ImageFilePreviewer( borderRadius: borderRadius, imageProvider: imageProvider, imageUrl: widget.preview ?? widget.url, description: widget.description, originalURL: widget.preview != null ? widget.url : null, ); }, progressIndicatorBuilder: (context, url, downloadProgress) => Container( padding: const EdgeInsets.all(5), child: ConstrainedBox( constraints: const BoxConstraints( minWidth: 200, minHeight: 200, ), child: Center( child: LoadingIndicator( message: AppLocale.processingWait.getString(context), ), ), ), ), errorWidget: (context, url, error) => _buildImageBrokenWidget(context), ); } Widget _buildImageBrokenWidget(BuildContext context) { return Center( child: Image.asset( 'assets/image-broken.png', fit: BoxFit.cover, width: 200, color: Theme.of(context).cardColor, ), ); } } class ImageFilePreviewer extends StatelessWidget { final ImageProvider imageProvider; final String? description; final String? originalURL; final String imageUrl; final BorderRadius? borderRadius; const ImageFilePreviewer({ super.key, required this.imageProvider, this.description, this.originalURL, required this.imageUrl, this.borderRadius, }); @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return ClipRRect( borderRadius: borderRadius ?? CustomSize.borderRadius, child: InkWell( borderRadius: borderRadius ?? CustomSize.borderRadiusAll, child: Image(image: imageProvider, fit: BoxFit.cover), onTap: () { openImagePreviewDialog( context, customColors, imageProvider: imageProvider, imageUrl: imageUrl, originalURL: originalURL, description: description, ); }, ), ); } } void openImagePreviewDialog( BuildContext context, CustomColors customColors, { required ImageProvider imageProvider, String? imageUrl, String? originalURL, String? description, }) { final downloadUrl = originalURL ?? imageUrl; Navigator.of(context).push( MaterialPageRoute( fullscreenDialog: true, builder: (context) => Scaffold( appBar: AppBar( toolbarHeight: CustomSize.toolbarHeight, leading: IconButton( icon: const Icon(Icons.close), onPressed: () { Navigator.of(context).pop(); }, ), actions: [ if (imageUrl != null) IconButton( onPressed: () { Navigator.push( context, MaterialPageRoute( fullscreenDialog: true, builder: (context) => GalleryItemShareScreen( images: [imageUrl], ), ), ); }, icon: Icon( Icons.share, size: 16, color: customColors.weakLinkColor, ), ), if (downloadUrl != null) IconButton( onPressed: () async { final cancel = BotToast.showCustomLoading( toastBuilder: (cancel) { return const LoadingIndicator( message: 'Downloading, please wait...', ); }, allowClick: false, duration: const Duration(seconds: 120), ); try { final saveFile = await DefaultCacheManager().getSingleFile(downloadUrl); if (PlatformTool.isIOS() || PlatformTool.isAndroid()) { await ImageGallerySaver.saveImage( saveFile.readAsBytesSync(), quality: 100, ); showSuccessMessage(AppLocale.operateSuccess.getString(context)); } else { var ext = saveFile.path.toLowerCase().split('.').last; MimeType mimeType; switch (ext) { case 'jpg': case 'jpeg': mimeType = MimeType.jpeg; break; case 'png': mimeType = MimeType.png; break; case 'gif': mimeType = MimeType.gif; break; default: mimeType = MimeType.other; } if (PlatformTool.isWindows()) { FileSaver.instance .saveAs( name: filenameWithoutExt(saveFile.path.split('/').last), filePath: saveFile.path, ext: '.$ext', mimeType: mimeType, ) .then((value) async { if (value == null) { return; } await File(value).writeAsBytes(await saveFile.readAsBytes()); Logger.instance.d('File saved successfully: $value'); showSuccessMessage(AppLocale.operateSuccess.getString(context)); }); } else { FileSaver.instance .saveFile( name: filenameWithoutExt(saveFile.path.split('/').last), filePath: saveFile.path, ext: ext, mimeType: mimeType, ) .then((value) { Logger.instance.d('File saved successfully: $value'); showSuccessMessage(AppLocale.operateSuccess.getString(context)); }); } } } catch (e) { // ignore: use_build_context_synchronously showErrorMessageEnhanced(context, 'Image save failed, please try again later'); Logger.instance.e('Failed to download the original image', error: e); } finally { cancel(); } }, icon: Icon( Icons.download_sharp, size: 16, color: customColors.weakLinkColor, ), ), if (downloadUrl == null && (PlatformTool.isIOS() || PlatformTool.isAndroid())) IconButton( onPressed: () async { final cancel = BotToast.showCustomLoading( toastBuilder: (cancel) { return const LoadingIndicator( message: 'Downloading, please wait...', ); }, allowClick: false, duration: const Duration(seconds: 120), ); try { await ImageGallerySaver.saveImage( (imageProvider as MemoryImage).bytes, quality: 100, ); showSuccessMessage(AppLocale.operateSuccess.getString(context)); } catch (e) { // ignore: use_build_context_synchronously showErrorMessageEnhanced(context, 'Image save failed, please try again later.'); Logger.instance.e('Failed to download the original image', error: e); } finally { cancel(); } }, icon: Icon( Icons.download_sharp, size: 16, color: customColors.weakLinkColor, ), ) ], ), backgroundColor: customColors.backgroundContainerColor, body: PhotoView( imageProvider: imageProvider, enableRotation: true, backgroundDecoration: BoxDecoration( color: customColors.backgroundContainerColor, ), ), ), ), ); } class ImageProviderPreviewer extends StatelessWidget { final ImageProvider imageProvider; final BorderRadius? borderRadius; const ImageProviderPreviewer({ super.key, required this.imageProvider, this.borderRadius, }); @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return ClipRRect( borderRadius: borderRadius ?? CustomSize.borderRadius, child: InkWell( borderRadius: borderRadius ?? CustomSize.borderRadiusAll, child: Image(image: imageProvider, fit: BoxFit.cover), onTap: () { openImagePreviewDialog( context, customColors, imageProvider: imageProvider, ); }, ), ); } } ================================================ FILE: lib/page/component/invite_card.dart ================================================ import 'package:askaide/helper/color.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/enhanced_button.dart'; import 'package:askaide/page/component/image.dart'; import 'package:askaide/page/component/share.dart'; import 'package:askaide/repo/api/user.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; class InviteCard extends StatelessWidget { final UserInfo userInfo; const InviteCard({super.key, required this.userInfo}); @override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.symmetric(horizontal: 15, vertical: 10), decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), // Maintain consistency with Settings Card boxShadow: [ BoxShadow( color: Colors.white.withOpacity(0.1), blurRadius: 10, offset: const Offset(0, 5), ), ], image: DecorationImage( // opacity: 0.83, image: CachedNetworkImageProviderEnhanced( userInfo.control.inviteCardBg ?? 'https://ssl.aicode.cc/ai-server/assets/invite-card-bg.webp-thumb1000'), fit: BoxFit.cover, ), // gradient: const LinearGradient( // begin: Alignment.topLeft, // end: Alignment.bottomRight, // colors: [ // Color.fromARGB(255, 255, 255, 255), // // Color.fromARGB(255, 230, 153, 38), // Color.fromARGB(255, 250, 213, 246), // ], // transform: GradientRotation(0.5), // ), ), height: 150, child: Container( padding: const EdgeInsets.symmetric( horizontal: 20, vertical: 20, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '邀新有礼', style: TextStyle( fontSize: 17, fontWeight: FontWeight.bold, color: userInfo.control.inviteCardColor != null ? stringToColor(userInfo.control.inviteCardColor!) : Colors.black, ), ), const SizedBox(height: 10), Text( userInfo.control.inviteCardSlogan ?? AppLocale.inviteSlogan.getString(context), strutStyle: const StrutStyle(height: 1.3), overflow: TextOverflow.ellipsis, maxLines: 4, style: TextStyle( fontSize: 13, color: userInfo.control.inviteCardColor != null ? stringToColor(userInfo.control.inviteCardColor!) : Colors.black, ), ), ], ), ), EnhancedButton( title: AppLocale.inviteNow.getString(context), fontSize: 14, height: 35, width: 80, backgroundColor: const Color.fromARGB(255, 230, 173, 58), onPressed: () { shareTo( context, content: userInfo.control.inviteMessage ?? '${AppLocale.inviteCode.getString(context)} ${userInfo.user.inviteCode}', title: AppLocale.inviteCodeShare.getString(context), ); }, ), ], ), ), ); } } ================================================ FILE: lib/page/component/item_selector.dart ================================================ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:settings_ui/settings_ui.dart'; class ItemSelector extends StatelessWidget { final List data; final String? selected; final String title; const ItemSelector( {super.key, required this.title, required this.data, this.selected}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(title)), body: data.isEmpty ? const Center(child: CircularProgressIndicator()) : SettingsList( sections: [ SettingsSection( tiles: data .map( (e) => SettingsTile( title: Text(e), leading: e == selected ? const Icon( Icons.check_rounded, color: Colors.green, ) : Icon( Icons.check_rounded, color: Colors.grey[200], ), onPressed: (context) { context.pop(e); }, ), ) .toList(), ), ], ), ); } } ================================================ FILE: lib/page/component/item_selector_search.dart ================================================ import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/group_list_widget.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; /// 带搜索框的列表选择器 class ItemSearchSelector extends StatefulWidget { final List items; final bool Function(SelectorItem item) onSelected; final bool enableSearch; final bool horizontal; final int horizontalCount; final Object? value; final EdgeInsets? innerPadding; const ItemSearchSelector({ super.key, required this.items, required this.onSelected, this.enableSearch = true, this.horizontal = false, this.value, this.horizontalCount = 4, this.innerPadding, }); @override State createState() => _ItemSearchSelectorState(); } class _ItemSearchSelectorState extends State { final _searchController = TextEditingController(); @override void initState() { _searchController.addListener(() { setState(() {}); }); super.initState(); } @override void dispose() { _searchController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; final items = widget.items.where((item) { if (_searchController.text.isEmpty) return true; if (item.search != null) { return item.search!(_searchController.text.trim()); } return false; }).toList(); if (widget.horizontal) { return GridView.count( crossAxisCount: widget.horizontalCount, children: items .map( (item) => ListTile( title: Container( alignment: Alignment.center, padding: widget.innerPadding ?? const EdgeInsets.symmetric(vertical: 5), child: widget.value != null ? Column( mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 5), ConstrainedBox( constraints: const BoxConstraints( maxWidth: 50, maxHeight: 50, ), child: item.title, ), SizedBox( height: 10, child: Icon( Icons.check, color: (widget.value != null && widget.value == item.value) ? customColors.linkColor : Colors.transparent, ), ), ], ) : item.title, ), onTap: () { if (widget.onSelected(item)) context.pop(); }, ), ) .toList(), ); } return Column( children: [ // 搜索框 if (widget.enableSearch) Container( margin: const EdgeInsets.only(bottom: 10, left: 5, right: 5), decoration: BoxDecoration( color: customColors.textfieldBackgroundColor, borderRadius: CustomSize.borderRadius, ), child: TextField( controller: _searchController, textAlignVertical: TextAlignVertical.center, style: TextStyle(color: customColors.textfieldHintColor), decoration: InputDecoration( hintText: AppLocale.search.getString(context), hintStyle: TextStyle( color: customColors.weakTextColor?.withAlpha(150), ), prefixIcon: Icon( Icons.search, color: customColors.weakTextColor?.withAlpha(150), ), isDense: true, border: InputBorder.none, ), ), ), // 列表部分 Expanded( child: GroupListWidget( items: items, groupKey: (item) => '', itemBuilder: (item) { return ListTile( title: Container( alignment: Alignment.center, padding: widget.innerPadding ?? const EdgeInsets.symmetric( horizontal: 10, vertical: 15, ), child: widget.value != null ? Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisSize: MainAxisSize.min, children: [ Expanded( child: Container( alignment: Alignment.center, child: item.title, )), SizedBox( width: 10, child: Icon( Icons.check, color: (widget.value != null && widget.value == item.value) ? customColors.linkColor : Colors.transparent, ), ), ], ) : item.title, ), onTap: () { if (widget.onSelected(item)) context.pop(); }, ); }, ), ), ], ); } } class SelectorItem { Widget title; T value; bool Function(String keywrod)? search; SelectorItem(this.title, this.value, {this.search}); } ================================================ FILE: lib/page/component/loading.dart ================================================ import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; import 'package:loading_animation_widget/loading_animation_widget.dart'; class LoadingIndicator extends StatelessWidget { final String? message; const LoadingIndicator({super.key, this.message}); @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return Column( mainAxisSize: MainAxisSize.min, children: [ LoadingAnimationWidget.halfTriangleDot( color: customColors.backgroundInvertedColor ?? Colors.white, size: 70, ), const SizedBox(height: 10), Text( message ?? "加载中,请稍后...", style: TextStyle( color: customColors.backgroundInvertedColor ?? Colors.white, ), ), ], ); } } ================================================ FILE: lib/page/component/message_box.dart ================================================ import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:flutter/material.dart'; class MessageBox extends StatelessWidget { final String message; final MessageBoxType type; const MessageBox({super.key, required this.message, required this.type}); @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( color: type.backgroundColor, borderRadius: CustomSize.borderRadius, border: Border.all( color: type.borderColor, width: 1, ), ), margin: const EdgeInsets.symmetric(horizontal: 5), padding: const EdgeInsets.symmetric( vertical: 8, horizontal: 16, ), child: Theme( data: ThemeData( iconTheme: IconThemeData( color: type.textColor, ), primaryTextTheme: TextTheme( bodyMedium: TextStyle( color: type.textColor, ), bodySmall: TextStyle( color: type.textColor, ), bodyLarge: TextStyle( color: type.textColor, ), ), ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ if (type.iconData != null) Icon( type.iconData, color: type.textColor, size: 16, ), if (type.iconData != null) const SizedBox(width: 8), Expanded( child: Text( message, style: TextStyle( color: type.textColor, fontSize: 13, ), ), ), ], ), ), ); } } class MessageBoxType { final Color backgroundColor; final Color textColor; final Color borderColor; final IconData? iconData; const MessageBoxType({ required this.backgroundColor, required this.textColor, required this.borderColor, this.iconData, }); static const MessageBoxType info = MessageBoxType( backgroundColor: Color.fromARGB(255, 232, 245, 233), textColor: Color.fromARGB(255, 72, 121, 75), borderColor: Color.fromARGB(255, 46, 125, 50), iconData: Icons.info, ); static const MessageBoxType warning = MessageBoxType( backgroundColor: Color(0xFFFFFDE7), textColor: Color(0xFFE65100), borderColor: Color(0xFFE65100), iconData: Icons.warning, ); static const MessageBoxType error = MessageBoxType( backgroundColor: Color(0xFFFFEBEE), textColor: Color(0xFFC62828), borderColor: Color(0xFFC62828), iconData: Icons.error, ); static const MessageBoxType success = MessageBoxType( backgroundColor: Color(0xFFE0F2F1), textColor: Color(0xFF00695C), borderColor: Color(0xFF00695C), iconData: Icons.check_circle, ); } ================================================ FILE: lib/page/component/model_indicator.dart ================================================ import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api/model.dart'; import 'package:flutter/material.dart'; class IconAndColor { final IconData icon; final Color color; IconAndColor(this.icon, this.color); } final iconAndColors = [ IconAndColor(Icons.bolt, Colors.green), IconAndColor(Icons.auto_awesome, const Color.fromARGB(255, 120, 73, 223)), IconAndColor(Icons.extension, const Color.fromARGB(255, 255, 122, 13)), ]; class ModelIndicatorInfo { IconData icon; Color activeColor; String modelId; String modelName; String description; bool supportVision; ModelIndicatorInfo({ required this.modelName, required this.modelId, required this.description, required this.icon, required this.activeColor, this.supportVision = false, }); } class ModelIndicator extends StatelessWidget { final HomeModelV2 model; final IconAndColor iconAndColor; final bool selected; final int itemCount; const ModelIndicator({ super.key, required this.model, required this.iconAndColor, this.selected = false, this.itemCount = 2, }); @override Widget build(BuildContext context) { var customColors = Theme.of(context).extension()!; return Padding( padding: EdgeInsets.symmetric(horizontal: itemCount > 2 ? 10 : 15), child: Row( crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ Icon( iconAndColor.icon, color: selected ? Colors.white : customColors.weakLinkColor, size: itemCount > 2 ? 16 : 20, ), SizedBox(width: itemCount > 2 ? 5 : 10), Expanded( child: Center( child: Row( children: [ Expanded( child: Center( child: Text( model.name, maxLines: 1, style: TextStyle( fontSize: itemCount > 2 ? 14 : 15, color: selected ? Colors.white : customColors.weakLinkColor, fontWeight: FontWeight.w600, overflow: TextOverflow.ellipsis, ), ), ), ), SizedBox(width: itemCount > 2 ? 16 : 20), ], ), ), ), ], ), ); } } ================================================ FILE: lib/page/component/model_item.dart ================================================ import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/color.dart'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/image.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/group_list_widget.dart'; import 'package:askaide/page/component/random_avatar.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/model/model.dart'; import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:quickalert/models/quickalert_type.dart'; class ModelItem extends StatefulWidget { final List models; final Function(Model? selected) onSelected; final String? initValue; final bool showUsing; const ModelItem({ super.key, required this.models, required this.onSelected, this.initValue, this.showUsing = false, }); @override State createState() => _ModelItemState(); } class _ModelItemState extends State { String keyword = ''; @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; var tags = []; // Collect all unique tags from models var uniqueTags = {}; for (var model in widget.models) { if (model.tag != null) { uniqueTags.addAll(model.tag!.split(',').where((e) => e.isNotEmpty)); } if (model.isRecommend) { uniqueTags.add(AppLocale.recommendTag.getString(context)); } if (model.isNew) { uniqueTags.add(AppLocale.newTag.getString(context)); } if (model.supportVision) { uniqueTags.add(AppLocale.visionTag.getString(context)); } if (model.supportReasoning) { uniqueTags.add(AppLocale.reasoning.getString(context)); } if (model.supportSearch) { uniqueTags.add(AppLocale.search.getString(context)); } if (model.modelPrice.isFree) { uniqueTags.add(AppLocale.free.getString(context)); } } // Create tag widgets tags = uniqueTags.map((tag) { return InkWell( onTap: () { setState(() { selectedTag = selectedTag == tag ? '' : tag; }); }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 5), child: buildTag( customColors, tag, tagTextColor: selectedTag == tag ? customColors.linkColor : null, ), ), ); }).toList(); return widget.models.isNotEmpty ? Column( children: [ // Search Container( margin: const EdgeInsets.only(bottom: 10, left: 5, right: 5), decoration: BoxDecoration( color: customColors.textfieldBackgroundColor, borderRadius: CustomSize.borderRadius, ), child: TextField( textAlignVertical: TextAlignVertical.center, style: TextStyle(color: customColors.dialogDefaultTextColor), decoration: InputDecoration( hintText: AppLocale.search.getString(context), hintStyle: TextStyle(color: customColors.textfieldHintColor), prefixIcon: Icon( Icons.search, color: customColors.weakTextColor?.withAlpha(150), ), isDense: true, border: InputBorder.none, ), onChanged: (value) => setState(() => keyword = value.toLowerCase()), ), ), // Tags if (tags.isNotEmpty) Container( padding: const EdgeInsets.only(top: 5, left: 5, right: 5, bottom: 5), child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row(children: tags), ), ), Expanded( child: Builder(builder: (context) { final models = searchModels(); return Container( padding: const EdgeInsets.only(bottom: 15), child: GroupListWidget( items: models, showTitle: true, groupKey: (item) { return item.category; }, itemBuilder: (item) { return buildItem(context, item, customColors); }, ), ); }), ), ], ) : const Center( child: Text( 'No model available\nPlease login first!', textAlign: TextAlign.center, ), ); } Widget buildItem(BuildContext context, Model item, CustomColors customColors) { final modelPrice = item.modelPrice; var tags = []; if (modelPrice.isFree) { tags.add(buildTag( customColors, AppLocale.free.getString(context), tagTextColor: widget.initValue == item.uid() ? customColors.linkColor : null, )); } if (item.tag != null) { var tt = item.tag!.split(",").where((e) => e.isNotEmpty).toList(); for (var i = 0; i < tt.length; i++) { tags.add(buildTag( customColors, tt[i], tagTextColor: widget.initValue == item.uid() ? customColors.linkColor : null, )); } } if (item.supportVision) { tags.add(buildTag( customColors, AppLocale.visionTag.getString(context), tagTextColor: widget.initValue == item.uid() ? customColors.linkColor : null, )); } if (item.supportReasoning) { tags.add(buildTag( customColors, AppLocale.reasoning.getString(context), tagTextColor: widget.initValue == item.uid() ? customColors.linkColor : null, )); } if (item.supportSearch) { tags.add(buildTag( customColors, AppLocale.search.getString(context), tagTextColor: widget.initValue == item.uid() ? customColors.linkColor : null, )); } if (item.isNew) { tags.add(buildTag( customColors, AppLocale.newTag.getString(context), tagTextColor: widget.initValue == item.uid() ? customColors.linkColor : null, )); } return ListTile( leading: Stack( children: [ buildAvatar(avatarUrl: item.avatarUrl, size: 48), if (item.userNoPermission) Container( width: 48, height: 48, decoration: BoxDecoration( color: Colors.black.withOpacity(0.4), borderRadius: CustomSize.borderRadiusAll, ), child: const Icon( Icons.lock_outline, color: Colors.white, size: 24, ), ), ], ), contentPadding: EdgeInsets.zero, title: AutoSizeText( item.name, minFontSize: 10, maxFontSize: 15, maxLines: 1, style: TextStyle( color: widget.initValue == item.uid() ? customColors.linkColor : (item.userNoPermission ? customColors.weakTextColorLess : null), fontWeight: widget.initValue == item.uid() ? FontWeight.bold : null, ), ), subtitle: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ SingleChildScrollView( scrollDirection: Axis.horizontal, child: Container( margin: const EdgeInsets.symmetric(vertical: 3), child: Row(children: formatTags(tags)), ), ), buildPriceBlock(customColors, item, modelPrice), ], ), onTap: () { if (item.userNoPermission) { showErrorMessage(AppLocale.modelNeedSignIn.getString(context)); return; } widget.onSelected(item); }, onLongPress: () { if (item.description == null || item.description == '') { return; } showBeautyDialog( context, type: QuickAlertType.info, text: item.description, confirmBtnText: AppLocale.gotIt.getString(context), showCancelBtn: false, ); }, ); } String selectedTag = ''; List searchModels() { var models = keyword.isEmpty ? widget.models : widget.models.where((e) { var matchText = e.name + (e.description ?? '') + (e.shortName ?? '') + (e.tag ?? '') + (e.category); if (e.supportVision) { matchText += 'vision视觉看图'; } if (e.isNew) { matchText += 'new新'; } if (e.isRecommend) { matchText += 'recommend推荐'; } if (e.supportReasoning) { matchText += 'reasoning推理'; } if (e.supportSearch) { matchText += 'search搜索'; } if (e.modelPrice.isFree) { matchText += 'free免费'; } return matchText.toLowerCase().contains(keyword); }).toList(); if (selectedTag.isNotEmpty) { models = models.where((e) { var tags = []; if (e.tag != null) { tags = e.tag!.split(',').where((e) => e.isNotEmpty).toList(); } if (e.isRecommend) { tags.add(AppLocale.recommendTag.getString(context)); } if (e.isNew) { tags.add(AppLocale.newTag.getString(context)); } if (e.supportVision) { tags.add(AppLocale.visionTag.getString(context)); } if (e.supportReasoning) { tags.add(AppLocale.reasoning.getString(context)); } if (e.supportSearch) { tags.add(AppLocale.search.getString(context)); } if (e.modelPrice.isFree) { tags.add(AppLocale.free.getString(context)); } if (e.id.startsWith('v2@rooms')) { tags.add(AppLocale.character.getString(context)); } else { tags.add(AppLocale.model.getString(context)); } return tags.contains(selectedTag); }).toList(); } return models; } Widget buildPriceBlock(CustomColors customColors, Model model, ModelPrice item) { if (item.isFree) { return const SizedBox(); } var priceText = ''; if (item.input > 0 || item.output > 0) { priceText += '${AppLocale.input.getString(context)} ¢${item.input}, ${AppLocale.output.getString(context)} ¢${item.output}'; } if (item.request > 0) { priceText += '${priceText == '' ? '' : ', '}${AppLocale.perRequest.getString(context)} ¢${item.request}'; } if (item.search > 0) { priceText += '${priceText == '' ? '' : ', '}${AppLocale.perSearch.getString(context)} ¢${item.search}'; } return Row( children: [ Text( priceText, maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 11, color: widget.initValue == model.uid() ? customColors.linkColor : customColors.weakTextColor?.withAlpha(150), ), ), if (item.hasNote) ...[ const SizedBox(width: 5), InkWell( onTap: () { showBeautyDialog( context, type: QuickAlertType.info, text: item.note, confirmBtnText: AppLocale.gotIt.getString(context), showCancelBtn: false, ); }, child: Icon( Icons.help_outline, size: 12, color: customColors.weakLinkColor?.withAlpha(50), ), ), ], ], ); } Widget buildCategory(CustomColors customColors, String category) { return Container( padding: const EdgeInsets.only(left: 10, right: 10), width: double.infinity, child: Text( category, style: TextStyle( color: customColors.dialogDefaultTextColor, fontSize: 14, ), ), ); } Widget buildAvatar({String? avatarUrl, int? id, int size = 30}) { if (avatarUrl != null && avatarUrl.startsWith('http')) { return RemoteAvatar( avatarUrl: imageURL(avatarUrl, qiniuImageTypeAvatar), size: size, ); } return RandomAvatar( id: id ?? 0, size: size, usage: Ability().isUserLogon() ? AvatarUsage.room : AvatarUsage.legacy, ); } Widget buildTag( CustomColors customColors, String tag, { double? tagFontSize, Color? tagTextColor, }) { return Container( padding: const EdgeInsets.only(right: 5), child: Text( "#$tag", style: TextStyle( fontSize: tagFontSize ?? 11, color: tagTextColor ?? customColors.weakTextColor?.withAlpha(150), ), ), ); } } String modelTagColorSeq(int index) { var colors = { Colors.grey, Colors.purple, Colors.orange, Colors.pink, Colors.deepPurple, Colors.indigo, Colors.cyan, }; return colorToString(colors.elementAt(index % colors.length)); } List formatTags(List tags) { var widgets = []; for (var i = 0; i < tags.length; i++) { widgets.add(tags[i]); } return widgets; } ================================================ FILE: lib/page/component/multi_item_selector.dart ================================================ import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/enhanced_button.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; class MultiItemSelector extends StatefulWidget { final Widget Function(T item)? itemAvatarBuilder; final Widget Function(T item) itemBuilder; final List items; final Function(List selected)? onSubmit; final Function(List selected)? onChanged; final List? selectedItems; const MultiItemSelector({ super.key, required this.items, required this.itemBuilder, this.selectedItems, this.onSubmit, this.onChanged, this.itemAvatarBuilder, }); @override State> createState() => _MultiItemSelectorState(); } class _MultiItemSelectorState extends State> { var selectedItems = []; @override void initState() { selectedItems = widget.selectedItems ?? []; super.initState(); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return Container( margin: const EdgeInsets.only(top: 15), child: Column( children: [ if (widget.onSubmit != null) Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ EnhancedButton( width: 100, height: 40, backgroundColor: customColors.weakTextColor, title: AppLocale.cancel.getString(context), onPressed: () { context.pop(); }, ), EnhancedButton( width: 100, height: 40, title: AppLocale.ok.getString(context), onPressed: () { widget.onSubmit!(selectedItems); }, ), ], ), Expanded( child: ListView.separated( itemCount: widget.items.length, itemBuilder: (context, i) { var item = widget.items[i]; return CheckboxListTile( controlAffinity: ListTileControlAffinity.leading, checkboxShape: const CircleBorder(), activeColor: customColors.linkColor, side: BorderSide( color: customColors.weakTextColor!.withAlpha(100), ), title: Container( alignment: Alignment.center, padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisSize: MainAxisSize.min, children: [ if (widget.itemAvatarBuilder != null) widget.itemAvatarBuilder!(item), const SizedBox(width: 20), Expanded( child: Container( alignment: Alignment.centerLeft, child: widget.itemBuilder(item), ), ), ], ), ), onChanged: (selected) { setState(() { if (selectedItems.contains(item)) { selectedItems.remove(item); } else { selectedItems.add(item); } if (widget.onChanged != null) { widget.onChanged!(selectedItems); } }); }, value: selectedItems.contains(item), ); }, separatorBuilder: (BuildContext context, int index) { return Divider( height: 1, color: customColors.columnBlockDividerColor, ); }, ), ) ], ), ); } } ================================================ FILE: lib/page/component/notify_message.dart ================================================ import 'package:askaide/page/component/gradient_style.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; class NotifyMessageWidget extends StatelessWidget { final Function()? onClose; final Widget child; final String? backgroundImageUrl; final Color? backgroundColor; final double height; final Widget? title; final bool closeable; const NotifyMessageWidget({ super.key, this.onClose, required this.child, this.backgroundImageUrl, this.height = 100, this.title, this.closeable = true, this.backgroundColor, }); @override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.only( left: 10, right: 10, ), padding: const EdgeInsets.only( left: 5, right: 5, top: 7, ), child: Container( width: double.infinity, height: height, padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), decoration: BoxDecoration( borderRadius: CustomSize.borderRadius, // gradient: buildGradientStyle(), color: backgroundColor, image: backgroundImageUrl != null ? DecorationImage( fit: BoxFit.cover, image: CachedNetworkImageProvider(backgroundImageUrl!), ) : null, ), child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisSize: MainAxisSize.max, children: [ if (title != null) title! else const SizedBox(), if (closeable) InkWell( onTap: () { onClose?.call(); }, child: Container( decoration: BoxDecoration( color: const Color.fromARGB(184, 37, 37, 37), borderRadius: BorderRadius.circular(80), ), padding: const EdgeInsets.all(3), child: const Icon( Icons.close, color: Color.fromARGB(255, 255, 255, 255), size: 12, ), ), ) else const SizedBox(), ], ), if (title != null) const SizedBox(height: 3), Expanded(child: child), ], ), ), ); } LinearGradient buildGradientStyle() { return GradientStyle.warmLinear(); } } ================================================ FILE: lib/page/component/pagination.dart ================================================ /// 该文件来源于 https://github.com/created-by-varun/flutter_pagination/blob/master/lib/pagination.dart import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; class Pagination extends StatefulWidget { const Pagination({ super.key, required this.numOfPages, required this.selectedPage, this.pagesVisible = 5, required this.onPageChanged, }); final int numOfPages; final int selectedPage; final int pagesVisible; final Function onPageChanged; @override State createState() => _PaginationState(); } class _PaginationState extends State { late int _startPage; late int _endPage; @override void initState() { super.initState(); _calculateVisiblePages(); } @override void didUpdateWidget(Pagination oldWidget) { super.didUpdateWidget(oldWidget); _calculateVisiblePages(); } void _calculateVisiblePages() { /// If the number of pages is less than or equal to the number of pages visible, then show all the pages if (widget.numOfPages <= widget.pagesVisible) { _startPage = 1; _endPage = widget.numOfPages; } else { /// If the number of pages is greater than the number of pages visible, then show the pages visible int middle = (widget.pagesVisible - 1) ~/ 2; if (widget.selectedPage <= middle + 1) { _startPage = 1; _endPage = widget.pagesVisible; } else if (widget.selectedPage >= widget.numOfPages - middle) { _startPage = widget.numOfPages - (widget.pagesVisible - 1); _endPage = widget.numOfPages; } else { _startPage = widget.selectedPage - middle; _endPage = widget.selectedPage + middle; } } } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ /// loop through the pages and show the page buttons for (int i = _startPage; i <= _endPage; i++) AnimatedContainer( duration: const Duration(milliseconds: 200), child: TextButton( style: i == widget.selectedPage ? ButtonStyle( backgroundColor: WidgetStateProperty.all(Colors.transparent), ) : ButtonStyle( backgroundColor: WidgetStateProperty.all(Colors.transparent)), onPressed: () => widget.onPageChanged(i), child: Text( '$i', style: i == widget.selectedPage ? TextStyle( color: customColors.linkColor, fontSize: 14, fontWeight: FontWeight.w700, ) : TextStyle( fontSize: 12, fontWeight: FontWeight.w700, color: customColors.weakLinkColor, ), ), ), ), ], ); } } ================================================ FILE: lib/page/component/password_field.dart ================================================ import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; class PasswordField extends StatefulWidget { final TextEditingController? controller; final ValueChanged? onSubmitted; final String? labelText; final String? hintText; final bool inColumnBlock; const PasswordField({ super.key, this.onSubmitted, this.controller, this.labelText, this.hintText, this.inColumnBlock = false, }); @override State createState() => _PasswordFieldState(); } class _PasswordFieldState extends State { var obscureText = true; @override Widget build(BuildContext context) { var customColors = Theme.of(context).extension()!; return Container( padding: widget.inColumnBlock ? const EdgeInsets.all(5) : null, child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ if (widget.inColumnBlock) SizedBox( width: 80, child: Text( widget.labelText!, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 16, color: customColors.textfieldLabelColor, ), ), ), if (widget.inColumnBlock) const SizedBox(width: 10), Expanded( child: TextField( obscureText: obscureText, inputFormatters: [FilteringTextInputFormatter.singleLineFormatter], textAlignVertical: TextAlignVertical.center, decoration: InputDecoration( border: const OutlineInputBorder(), enabledBorder: widget.inColumnBlock ? InputBorder.none : const OutlineInputBorder( borderSide: BorderSide(color: Color.fromARGB(200, 192, 192, 192)), ), focusedBorder: widget.inColumnBlock ? InputBorder.none : OutlineInputBorder( borderSide: BorderSide(color: customColors.linkColor!), ), // floatingLabelStyle: TextStyle(color: customColors.linkColor!), isDense: true, floatingLabelBehavior: FloatingLabelBehavior.always, labelText: widget.inColumnBlock ? null : widget.labelText, labelStyle: const TextStyle(fontSize: 17), hintText: widget.hintText, hintStyle: TextStyle( color: customColors.textfieldHintColor, fontSize: 15, ), suffixIcon: IconButton( icon: Icon( obscureText ? Icons.visibility_off : Icons.visibility, size: 15, color: const Color.fromARGB(150, 141, 141, 141), ), onPressed: () { setState(() { obscureText = !obscureText; }); }, ), ), keyboardType: TextInputType.visiblePassword, onSubmitted: widget.onSubmitted, controller: widget.controller, ), ), ], ), ); } } ================================================ FILE: lib/page/component/prompt_tags_selector.dart ================================================ import 'package:askaide/page/component/enhanced_button.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/weak_text_button.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/model/misc.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; class PromptTagsSelector extends StatefulWidget { final List selectedTags; final Function(List tags) onSubmit; const PromptTagsSelector({ super.key, this.selectedTags = const [], required this.onSubmit, }); @override State createState() => _PromptTagsSelectorState(); } class _PromptTagsSelectorState extends State { List promptCategories = []; Map selectedTags = {}; final ScrollController _scrollController = ScrollController(); @override void dispose() { _scrollController.dispose(); super.dispose(); } @override void initState() { for (var element in widget.selectedTags) { selectedTags[element.value] = element; } APIServer().drawPromptTags().then((res) { setState(() { promptCategories = res; }); }); super.initState(); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return SafeArea( top: false, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Container( decoration: BoxDecoration(borderRadius: CustomSize.borderRadius), child: DefaultTabController( length: promptCategories.length, child: Column( children: [ Theme( data: Theme.of(context).copyWith( colorScheme: Theme.of(context).colorScheme.copyWith(surfaceContainerHighest: Colors.transparent), ), child: TabBar( tabs: [for (var cat in promptCategories) Tab(text: cat.name)], isScrollable: true, labelPadding: const EdgeInsets.only(left: 0, right: 20), labelColor: customColors.linkColor, unselectedLabelColor: customColors.weakLinkColor, labelStyle: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), indicator: const BoxDecoration(), overlayColor: WidgetStateProperty.all(Colors.transparent), ), ), Expanded( child: TabBarView( children: [ for (var cat in promptCategories) buildTabBarView(customColors, cat.children), ], ), ), ], ), ), ), ), // Container( // margin: const EdgeInsets.symmetric(vertical: 8), // width: double.infinity, // child: ConstrainedBox( // constraints: const BoxConstraints(maxHeight: 95), // child: Column( // crossAxisAlignment: CrossAxisAlignment.start, // mainAxisSize: MainAxisSize.min, // children: [ // Text( // '已选择(${selectedTags.length}):', // style: TextStyle( // fontSize: 14, // color: customColors.weakLinkColor, // fontWeight: FontWeight.bold, // ), // ), // const SizedBox(height: 5), // Expanded( // child: SingleChildScrollView( // controller: _scrollController, // child: Wrap( // spacing: 3, // runSpacing: 3, // children: [ // for (var tag in selectedTags.values) // Tag( // name: tag.name, // backgroundColor: customColors.linkColor, // textColor: Colors.white, // fontsize: 10, // onDeleted: () { // setState(() { // selectedTags.remove(tag.value); // }); // }, // ), // ], // ), // ), // ), // ], // ), // ), // ), Container( margin: const EdgeInsets.symmetric(vertical: 10), child: Row( mainAxisSize: MainAxisSize.min, children: [ WeakTextButton( title: '取消', onPressed: () { context.pop(); }, ), const SizedBox(width: 20), Expanded( child: EnhancedButton( title: '确定', fontSize: 14, onPressed: () { widget.onSubmit(selectedTags.values.toList()); }, ), ), ], ), ), ], ), ); } Widget buildTabBarView(CustomColors customColors, List subCategories) { return Container( margin: const EdgeInsets.symmetric(horizontal: 5), padding: const EdgeInsets.only(left: 10, right: 10, top: 0, bottom: 10), decoration: BoxDecoration( color: customColors.backgroundContainerColor?.withAlpha(50), borderRadius: CustomSize.borderRadius, ), child: ListView.builder( itemCount: subCategories.length, itemBuilder: (context, index) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.only(top: 10, bottom: 10), child: Text( '# ${subCategories[index].name}', style: TextStyle( fontWeight: FontWeight.bold, color: customColors.weakTextColorPlusPlus?.withAlpha(150), fontSize: 14, height: 1.5, ), ), ), buildTagView(customColors, subCategories[index].tags), ], ); }, ), ); } Widget buildTagView(CustomColors customColors, List tags) { return Wrap( spacing: 5, runSpacing: 5, children: [ for (var tag in tags) Tag( name: tag.name, onTap: () { if (selectedTags.containsKey(tag.value)) { selectedTags.remove(tag.value); } else { selectedTags[tag.value] = tag; } setState(() {}); _scrollController.animateTo( _scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ); }, textColor: selectedTags.containsKey(tag.value) ? Colors.white : customColors.weakTextColorPlusPlus, backgroundColor: selectedTags.containsKey(tag.value) ? customColors.linkColor : customColors.backgroundContainerColor?.withAlpha(200), ), ], ); } } class Tag extends StatelessWidget { final String name; final Color? backgroundColor; final Color? textColor; final VoidCallback? onDeleted; final VoidCallback? onTap; final double? fontsize; const Tag({ super.key, required this.name, this.backgroundColor, this.textColor, this.onDeleted, this.onTap, this.fontsize, }); @override Widget build(BuildContext context) { return InkWell( onTap: onTap, child: Chip( side: BorderSide.none, visualDensity: const VisualDensity(horizontal: -4.0, vertical: -4.0), padding: const EdgeInsets.all(0), labelPadding: EdgeInsets.only(left: 5, right: onDeleted == null ? 5 : 0), elevation: 0, label: Text( name, style: TextStyle( fontSize: fontsize ?? 12, color: textColor ?? Colors.white, ), ), backgroundColor: backgroundColor ?? Colors.grey, deleteIcon: Icon( Icons.close, color: textColor ?? Colors.white, size: fontsize ?? 12, ), onDeleted: onDeleted, ), ); } } ================================================ FILE: lib/page/component/random_avatar.dart ================================================ import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:random_avatar/random_avatar.dart' as ava; enum AvatarUsage { room, user, legacy, } class RandomAvatar extends StatelessWidget { final int id; final int? size; final AvatarUsage usage; final BorderRadiusGeometry? borderRadius; const RandomAvatar({ super.key, required this.id, this.size, required this.usage, this.borderRadius, }); @override Widget build(BuildContext context) { if (usage == AvatarUsage.user || usage == AvatarUsage.legacy) { return ava.RandomAvatar( '$id', width: size?.toDouble(), height: size?.toDouble(), ); } return ConstrainedBox( constraints: BoxConstraints( maxWidth: size?.toDouble() ?? 500, maxHeight: size?.toDouble() ?? 500, ), child: ClipRRect( borderRadius: borderRadius ?? CustomSize.borderRadius, child: CachedNetworkImage( imageUrl: 'https://ai-api.aicode.cc/v1/images/random-avatar/${usage.name}/$id/${size ?? 500}', fit: BoxFit.cover, ), ), ); } } class RemoteAvatar extends StatelessWidget { final String avatarUrl; final int? size; final double? radius; const RemoteAvatar({super.key, required this.avatarUrl, this.size, this.radius}); @override Widget build(BuildContext context) { return SizedBox( width: size?.toDouble() ?? 60, height: size?.toDouble() ?? 60, child: ClipRRect( borderRadius: BorderRadius.circular(radius ?? CustomSize.radiusValue), child: CachedNetworkImage( imageUrl: avatarUrl, fit: BoxFit.fill, ), ), ); } } class LocalAvatar extends StatelessWidget { final String assetName; final int? size; const LocalAvatar({super.key, required this.assetName, this.size}); @override Widget build(BuildContext context) { return SizedBox( width: size?.toDouble() ?? 60, height: size?.toDouble() ?? 60, child: ClipRRect(borderRadius: CustomSize.borderRadius, child: Image.asset(assetName, fit: BoxFit.fill)), ); } } ================================================ FILE: lib/page/component/room_card.dart ================================================ import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/image.dart'; import 'package:askaide/page/component/enhanced_button.dart'; import 'package:askaide/page/component/image.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/weak_text_button.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api/room_gallery.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; class RoomCard extends StatelessWidget { final bool selected; final RoomGallery item; final Function(RoomGallery) onItemSelected; final Function()? selectedCheck; final double fontsize; final bool stopAllEvents; const RoomCard({ super.key, this.selected = false, required this.item, required this.onItemSelected, this.fontsize = 13, this.selectedCheck, this.stopAllEvents = false, }); Future openRoomCard(BuildContext context, RoomGallery item) { return openModalBottomSheet( context, (_) { return Container( padding: const EdgeInsets.only(top: 20), child: GalleryRoomCard( item: item, selected: selected, // onConfirm: () { // onItemSelected(item); // }, ), ); }, heightFactor: 0.7, disableCompleteEvent: true, disableEvent: stopAllEvents, ); } @override Widget build(BuildContext context) { var customColors = Theme.of(context).extension()!; return InkWell( onLongPress: () async { await openRoomCard(context, item); selectedCheck?.call(); }, onTap: () { onItemSelected(item); }, borderRadius: CustomSize.borderRadiusAll, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ Container( decoration: BoxDecoration( borderRadius: CustomSize.borderRadius, border: selected ? Border.all( width: 2, color: customColors.linkColor ?? Colors.green, ) : null, ), child: Stack( children: [ ClipRRect( borderRadius: CustomSize.borderRadius, child: CachedNetworkImageEnhanced( imageUrl: imageURL(item.avatarUrl, qiniuImageTypeAvatar), fit: BoxFit.cover, ), ), if (selected) Positioned( right: -1, bottom: -1, child: Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: customColors.linkColor ?? Colors.green, borderRadius: const BorderRadius.only(topLeft: CustomSize.radius, bottomRight: CustomSize.radius), ), child: const Icon( Icons.check, color: Colors.white, size: 14, ), ), ), ], ), ), const SizedBox(height: 5), Text( item.name, textAlign: TextAlign.center, style: TextStyle( color: customColors.weakTextColorPlusPlus, fontSize: fontsize, ), ), ], ), ); } } class GalleryRoomCard extends StatelessWidget { final RoomGallery item; final Function()? onConfirm; final bool selected; const GalleryRoomCard({super.key, required this.item, this.onConfirm, this.selected = false}); @override Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, children: [ Expanded( child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ Container( width: MediaQuery.of(context).size.width - 80, height: MediaQuery.of(context).size.width - 80, decoration: BoxDecoration( borderRadius: BorderRadius.circular(30), ), child: ClipRRect( borderRadius: CustomSize.borderRadius, child: CachedNetworkImageEnhanced( imageUrl: item.avatarUrl, fit: BoxFit.cover, ), ), ), Container( margin: const EdgeInsets.only(top: 10), child: Text( item.name, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, ), ), ), if (item.description != '') Container( margin: const EdgeInsets.symmetric( vertical: 10, horizontal: 20, ), padding: const EdgeInsets.all(10), width: double.infinity, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '简介:', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 5), SelectableText( item.description, style: const TextStyle( fontSize: 14, ), ), ], ), ), ], ), ), ), if (onConfirm != null) Container( height: 70, margin: const EdgeInsets.only(top: 20), padding: const EdgeInsets.symmetric( horizontal: 20, vertical: 10, ), child: Row( children: [ WeakTextButton( title: '取消', onPressed: () { context.pop(); }, ), const SizedBox(width: 20), Expanded( child: EnhancedButton( title: selected ? '移除' : '选择', onPressed: () { onConfirm!(); context.pop(); }), ) ], ), ) ], ); } } ================================================ FILE: lib/page/component/rotating_widget.dart ================================================ import 'dart:math'; import 'package:flutter/material.dart'; class RotatingWidget extends StatefulWidget { final Widget child; const RotatingWidget({super.key, required this.child}); @override State createState() => _RotatingWidgetState(); } class _RotatingWidgetState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(seconds: 1), vsync: this, )..repeat(); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _controller, builder: (_, child) { return Transform.rotate( angle: _controller.value * 2 * pi, child: child, ); }, child: widget.child, ); } } ================================================ FILE: lib/page/component/select_mode_toolbar.dart ================================================ import 'package:askaide/bloc/chat_message_bloc.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/chat/chat_preview.dart'; import 'package:askaide/page/component/chat/chat_share.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/model/message.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:provider/provider.dart'; class SelectModeToolbar extends StatefulWidget { final ChatPreviewController chatPreviewController; const SelectModeToolbar({super.key, required this.chatPreviewController}); @override State createState() => _SelectModeToolbarState(); } class _SelectModeToolbarState extends State { @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( borderRadius: const BorderRadius.only(topLeft: CustomSize.radius, topRight: CustomSize.radius), color: customColors.backgroundColor, ), child: SafeArea( child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ TextButton.icon( onPressed: () { var messages = widget.chatPreviewController.selectedMessages(); if (messages.isEmpty) { showErrorMessageEnhanced(context, AppLocale.noMessageSelected.getString(context)); return; } Navigator.push( context, MaterialPageRoute( fullscreenDialog: true, builder: (context) => ChatShareScreen( messages: messages .map((e) => ChatShareMessage( content: e.message.text, username: e.message.senderName, avatarURL: e.message.avatarUrl, leftSide: e.message.role == Role.receiver, images: e.message.images, )) .toList(), ), ), ); // var messages = chatPreviewController.selectedMessages(); // if (messages.isEmpty) { // showErrorMessageEnhanced( // context, AppLocale.noMessageSelected.getString(context)); // return; // } // var shareText = messages.map((e) { // if (e.message.role == Role.sender) { // return '我:\n${e.message.text}'; // } // return '助理:\n${e.message.text}'; // }).join('\n\n'); // shareTo( // context, // content: shareText, // title: AppLocale.chatHistory.getString(context), // ); }, icon: Icon(Icons.share, color: customColors.linkColor), label: Text( AppLocale.share.getString(context), style: TextStyle(color: customColors.linkColor), ), ), TextButton.icon( onPressed: () { widget.chatPreviewController.selectAllMessage(); }, icon: Icon(Icons.select_all_outlined, color: customColors.linkColor), label: Text( AppLocale.selectAll.getString(context), style: TextStyle(color: customColors.linkColor), ), ), TextButton.icon( onPressed: () { if (widget.chatPreviewController.selectedMessageIds.isEmpty) { showErrorMessageEnhanced(context, AppLocale.noMessageSelected.getString(context)); return; } openConfirmDialog( context, AppLocale.confirmDelete.getString(context), () { final ids = widget.chatPreviewController.selectedMessageIds.toList(); if (ids.isNotEmpty) { context.read().add(ChatMessageDeleteEvent(ids)); showErrorMessageEnhanced(context, AppLocale.operateSuccess.getString(context)); widget.chatPreviewController.exitSelectMode(); } }, danger: true, ); }, icon: Icon(Icons.delete, color: customColors.linkColor), label: Text( AppLocale.delete.getString(context), style: TextStyle(color: customColors.linkColor), ), ), ], ), ), ); } } ================================================ FILE: lib/page/component/share.dart ================================================ import 'dart:io'; import 'package:askaide/helper/platform.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:fluwx/fluwx.dart'; import 'package:go_router/go_router.dart'; import 'package:share_plus/share_plus.dart'; Future shareTo( BuildContext context, { required String content, String? title, List? images, }) async { Rect? sharePositionOrigin; try { final box = context.findRenderObject() as RenderBox?; Rect? pos = box!.localToGlobal(Offset.zero) & box.size; sharePositionOrigin = pos; // ignore: empty_catches } catch (ignored) {} if ((PlatformTool.isIOS() || PlatformTool.isAndroid()) && await isWeChatInstalled) { openModalBottomSheet( // ignore: use_build_context_synchronously context, (context) => Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceAround, crossAxisAlignment: CrossAxisAlignment.center, children: [ IconButton( onPressed: () { final model = images == null || images.isEmpty ? WeChatShareTextModel( content, title: title, scene: WeChatScene.TIMELINE, ) : WeChatShareImageModel( images.first.startsWith('http') ? WeChatImage.network(images.first) : WeChatImage.file(File(images.first)), title: title, description: content, scene: WeChatScene.TIMELINE, ); shareToWeChat(model).whenComplete(() => context.pop()); }, icon: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ Image.asset('assets/friendroom.png', width: 40), const SizedBox(height: 10), Text( AppLocale.shareToWechatQ.getString(context), style: const TextStyle(fontSize: 12), ), ], ), ), IconButton( onPressed: () { final model = images == null || images.isEmpty ? WeChatShareTextModel( content, title: title, ) : WeChatShareImageModel( images.first.startsWith('http') ? WeChatImage.network(images.first) : WeChatImage.file(File(images.first)), title: title, description: content, ); shareToWeChat(model).whenComplete(() => context.pop()); }, icon: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ Image.asset('assets/wechat.png', width: 40), const SizedBox(height: 10), Text( AppLocale.shareToWechat.getString(context), style: const TextStyle(fontSize: 12), ), ], ), ), IconButton( onPressed: () { if (images != null && images.isNotEmpty) { Share.shareXFiles( [XFile(images.first)], subject: title, sharePositionOrigin: sharePositionOrigin, ).whenComplete(() => context.pop()); } else { Share.share( content, subject: title, sharePositionOrigin: sharePositionOrigin, ).whenComplete(() => context.pop()); } }, icon: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ Image.asset('assets/share.png', width: 40), const SizedBox(height: 10), Text( AppLocale.shareToOtherApps.getString(context), style: const TextStyle(fontSize: 12), ), ], ), ), ], ), ], ), heightFactor: 0.25, ); } else { if (images != null && images.isNotEmpty) { Share.shareXFiles( [XFile(images.first)], subject: title, sharePositionOrigin: sharePositionOrigin, ); } else { Share.share( content, subject: title, sharePositionOrigin: sharePositionOrigin, ); } } } ================================================ FILE: lib/page/component/sliver_component.dart ================================================ import 'package:askaide/page/component/image.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; class SliverSingleComponent extends StatelessWidget { final Widget? title; final Widget? backgroundImage; final List? actions; final double expendedHeight; final List Function() appBarExtraWidgets; final EdgeInsets? titlePadding; final bool centerTitle; const SliverSingleComponent({ super.key, required this.title, this.backgroundImage, this.actions, this.expendedHeight = 80, required this.appBarExtraWidgets, this.titlePadding, this.centerTitle = true, }); @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return CustomScrollView( slivers: [ SliverAppBar( automaticallyImplyLeading: false, expandedHeight: expendedHeight, floating: false, pinned: true, snap: false, primary: true, actions: (actions ?? []).isEmpty ? null : [...actions!, const SizedBox(width: 8)], backgroundColor: customColors.backgroundContainerColor, flexibleSpace: FlexibleSpaceBar( title: title, centerTitle: centerTitle, titlePadding: titlePadding, background: ShaderMask( shaderCallback: (rect) { return const LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Colors.black, Colors.transparent], ).createShader(Rect.fromLTRB(0, 0, rect.width, rect.height)); }, blendMode: BlendMode.dstIn, child: backgroundImage, ), expandedTitleScale: 1.1, ), ), ...appBarExtraWidgets(), ], ); } } class SliverComponent extends StatelessWidget { final Widget? title; final Widget? backgroundImage; final List? actions; final double expendedHeight; final Widget child; final List Function(bool innerBoxIsScrolled)? appBarExtraWidgets; final EdgeInsets? titlePadding; final bool centerTitle; const SliverComponent({ super.key, required this.title, this.backgroundImage, this.actions, this.expendedHeight = 80, required this.child, this.appBarExtraWidgets, this.titlePadding, this.centerTitle = true, }); @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return NestedScrollView( headerSliverBuilder: (context, innerBoxIsScrolled) { return [ SliverAppBar( automaticallyImplyLeading: true, toolbarHeight: CustomSize.toolbarHeight, expandedHeight: expendedHeight, floating: false, pinned: true, snap: false, primary: true, actions: (actions ?? []).isEmpty ? null : [...actions!, const SizedBox(width: 8)], backgroundColor: backgroundImage != null ? Colors.transparent : customColors.backgroundColor, flexibleSpace: FlexibleSpaceBar( title: title, centerTitle: centerTitle, titlePadding: titlePadding, background: ShaderMask( shaderCallback: (rect) { return const LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Colors.black, Colors.transparent], ).createShader(Rect.fromLTRB(0, 0, rect.width, rect.height)); }, blendMode: BlendMode.dstIn, child: backgroundImage, ), expandedTitleScale: 1.1, ), ), if (appBarExtraWidgets != null) ...appBarExtraWidgets!(innerBoxIsScrolled), ]; }, body: child, ); } } class SliverTabComponent extends StatelessWidget { final List tabBarTitles; final Widget? title; final String? backgroundImageUrl; final List? actions; final int crossAxisCount; final double childAspectRatio; final double expendedHeight; final List Function(BuildContext context, String tabName) itemsBuilder; const SliverTabComponent({ super.key, required this.tabBarTitles, this.title, this.backgroundImageUrl, this.actions, required this.crossAxisCount, this.childAspectRatio = 1, this.expendedHeight = 80, required this.itemsBuilder, }); @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return DefaultTabController( length: tabBarTitles.length, child: NestedScrollView( headerSliverBuilder: (context, innerBoxIsScrolled) { return [ Theme( data: Theme.of(context).copyWith( colorScheme: Theme.of(context).colorScheme.copyWith(surfaceContainerHighest: Colors.transparent), ), child: SliverOverlapAbsorber( handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), sliver: SliverAppBar( title: title, backgroundColor: innerBoxIsScrolled ? customColors.backgroundColor : null, centerTitle: true, pinned: true, floating: true, snap: false, // primary: false, expandedHeight: expendedHeight, elevation: 0, forceElevated: innerBoxIsScrolled, flexibleSpace: FlexibleSpaceBar( background: ShaderMask( shaderCallback: (rect) { return const LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Colors.black, Colors.transparent], ).createShader(Rect.fromLTRB(0, 0, rect.width, rect.height)); }, blendMode: BlendMode.dstIn, child: backgroundImageUrl != null && backgroundImageUrl!.isNotEmpty ? CachedNetworkImageEnhanced( imageUrl: backgroundImageUrl!, fit: BoxFit.cover, ) : Image.asset( 'assets/background.webp', fit: BoxFit.cover, ), ), expandedTitleScale: 1.2, ), actions: actions, bottom: TabBar( tabs: tabBarTitles.map((e) => Tab(text: e)).toList(), isScrollable: true, labelColor: customColors.linkColor, indicator: const BoxDecoration(), overlayColor: WidgetStateProperty.all(Colors.transparent), ), ), ), ), ]; }, body: TabBarView( children: tabBarTitles.map( (e) { return Builder( builder: (context) { final items = itemsBuilder(context, e); return CustomScrollView( key: PageStorageKey(e), slivers: [ SliverOverlapInjector( handle: NestedScrollView.sliverOverlapAbsorberHandleFor( context, ), ), SliverPadding( padding: const EdgeInsets.all(8), sliver: SliverGrid( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return items[index]; }, childCount: items.length, //内部控件数量 ), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: crossAxisCount, crossAxisSpacing: 5, mainAxisSpacing: 10, childAspectRatio: childAspectRatio, ), ), ), ], ); }, ); }, ).toList(), ), ), ); } } ================================================ FILE: lib/page/component/social_icon.dart ================================================ import 'package:askaide/helper/platform.dart'; import 'package:askaide/lang/lang.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:settings_ui/settings_ui.dart'; import 'package:url_launcher/url_launcher_string.dart'; class SocialItem { final String image; final String name; final Function? onTap; final bool nonIOS; const SocialItem({ required this.image, required this.name, required this.onTap, this.nonIOS = false, }); } class SocialIconGroup extends StatelessWidget { final bool isSettingTiles; final List items = [ SocialItem( image: 'assets/app-256-transparent.png', name: '官方网站', nonIOS: true, onTap: () { launchUrlString( 'https://ai.aicode.cc/social/home', mode: LaunchMode.externalApplication, ); }, ), SocialItem( image: 'assets/weibo.png', name: '新浪微博', onTap: () { launchUrlString( 'https://ai.aicode.cc/social/weibo', mode: LaunchMode.externalApplication, ); }, ), SocialItem( image: 'assets/wechat.png', name: '微信公众号', onTap: () { launchUrlString( 'https://ai.aicode.cc/social/wechat-platform', mode: LaunchMode.externalApplication, ); }, ), SocialItem( image: 'assets/x.png', name: 'Twitter(X)', onTap: () { launchUrlString( 'https://ai.aicode.cc/social/x', mode: LaunchMode.externalApplication, ); }, ), SocialItem( image: 'assets/github.png', name: 'Github', onTap: () { launchUrlString( 'https://ai.aicode.cc/social/github', mode: LaunchMode.externalApplication, ); }, ), SocialItem( image: 'assets/xiaohongshu.png', name: '小红书', onTap: () { launchUrlString( 'https://ai.aicode.cc/social/xiaohongshu', mode: LaunchMode.externalApplication, ); }, ), ]; SocialIconGroup({ super.key, this.isSettingTiles = false, }); @override Widget build(BuildContext context) { if (isSettingTiles) { return SettingsSection( title: Text(AppLocale.socialMedia.getString(context)), tiles: items .where((e) { if (e.nonIOS) { return !PlatformTool.isIOS(); } return true; }) .map( (e) => SettingsTile( title: Row(children: [ Image.asset(e.image, width: 20, height: 20), const SizedBox(width: 10), Text(e.name), ]), trailing: const Icon( CupertinoIcons.chevron_forward, size: 18, color: Colors.grey, ), onPressed: (context) { e.onTap?.call(); }, ), ) .toList(), ); } return Column( mainAxisAlignment: MainAxisAlignment.start, children: items.map((e) => SocialIcon(image: e.image, name: e.name, onTap: e.onTap)).toList(), ); } } class SocialIcon extends StatelessWidget { final String image; final String name; final Function? onTap; const SocialIcon({ super.key, required this.image, required this.name, required this.onTap, }); @override Widget build(BuildContext context) { return GestureDetector( onTap: () { onTap?.call(); }, child: Column( children: [ Image.asset(image, width: 25), const SizedBox(height: 5), Text( name, style: const TextStyle(fontSize: 8), ) ], ), ); } } ================================================ FILE: lib/page/component/take_photo.dart ================================================ import 'dart:io'; import 'package:askaide/helper/platform.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:flutter/material.dart'; import 'package:camera/camera.dart'; class TakePhoto extends StatefulWidget { const TakePhoto({super.key}); @override State createState() => _TakePhotoState(); } class _TakePhotoState extends State with WidgetsBindingObserver { CameraController? _cameraController; late Future _initializeControllerFuture; @override void didChangeAppLifecycleState(AppLifecycleState state) { final CameraController? cameraController = _cameraController; // App state changed before we got the chance to initialize. if (cameraController == null || !cameraController.value.isInitialized) { return; } if (state == AppLifecycleState.inactive) { cameraController.dispose(); } else if (state == AppLifecycleState.resumed) { _initializeCameraController(cameraController.description); } } Future _initializeCameraController(CameraDescription cameraDescription) async { final CameraController cameraController = CameraController( cameraDescription, PlatformTool.isWeb() ? ResolutionPreset.max : ResolutionPreset.medium, enableAudio: false, imageFormatGroup: ImageFormatGroup.jpeg, ); _cameraController = cameraController; // If the controller is updated then update the UI. cameraController.addListener(() { if (mounted) { setState(() {}); } if (cameraController.value.hasError) { showErrorMessage('Camera error ${cameraController.value.errorDescription}'); } }); try { _initializeControllerFuture = cameraController.initialize(); await _initializeControllerFuture; } on CameraException catch (e) { switch (e.code) { case 'CameraAccessDenied': showErrorMessage('You have denied camera access.'); case 'CameraAccessDeniedWithoutPrompt': // iOS only showErrorMessage('Please go to Settings app to enable camera access.'); case 'CameraAccessRestricted': // iOS only showErrorMessage('Camera access is restricted.'); case 'AudioAccessDenied': showErrorMessage('You have denied audio access.'); case 'AudioAccessDeniedWithoutPrompt': // iOS only showErrorMessage('Please go to Settings app to enable audio access.'); case 'AudioAccessRestricted': // iOS only showErrorMessage('Audio access is restricted.'); default: showErrorMessage('Camera error: ${e.code} ${e.description}'); } } if (mounted) { setState(() {}); } } @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); availableCameras().then((cameras) { if (cameras.isNotEmpty) { _initializeCameraController(cameras.first); } }); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); _cameraController?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Take a picture')), // You must wait until the controller is initialized before displaying the // camera preview. Use a FutureBuilder to display a loading spinner until the // controller has finished initializing. body: FutureBuilder( future: _initializeControllerFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { // If the Future is complete, display the preview. return CameraPreview(_cameraController!); } else { // Otherwise, display a loading indicator. return const Center(child: CircularProgressIndicator()); } }, ), floatingActionButton: FloatingActionButton( // Provide an onPressed callback. onPressed: () async { // Take the Picture in a try / catch block. If anything goes wrong, // catch the error. try { // Ensure that the camera is initialized. await _initializeControllerFuture; // Attempt to take a picture and get the file `image` // where it was saved. final image = await _cameraController!.takePicture(); if (!context.mounted) return; // If the picture was taken, display it on a new screen. await Navigator.of(context).push( MaterialPageRoute( builder: (context) => DisplayPictureScreen( // Pass the automatically generated path to // the DisplayPictureScreen widget. imagePath: image.path, ), ), ); } catch (e) { // If an error occurs, log the error to the console. print(e); } }, child: const Icon(Icons.camera_alt), ), ); } } // A widget that displays the picture taken by the user. class DisplayPictureScreen extends StatelessWidget { final String imagePath; const DisplayPictureScreen({super.key, required this.imagePath}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Display the Picture')), // The image is stored as a file on the device. Use the `Image.file` // constructor with the given path to display the image. body: Image.file(File(imagePath)), ); } } ================================================ FILE: lib/page/component/theme/custom_size.dart ================================================ import 'package:flutter/material.dart'; class CustomSize { static const double appBarTitleSize = 16; static const double defaultHintTextSize = 16; static const double maxWindowSize = 1000; static const double smallWindowSize = 500; static const double radiusValue = 8.0; static BorderRadiusGeometry borderRadius = BorderRadius.circular(radiusValue); static const Radius radius = Radius.circular(radiusValue); static const BorderRadius borderRadiusAll = BorderRadius.all(radius); static double get markdownTextSize { return 16; } static double get markdownCodeSize { return 14; } static double get toolbarHeight { return kToolbarHeight; } static double adaptiveMaxWindowWidth(BuildContext context) { final windowSize = MediaQuery.of(context).size.width; return windowSize > CustomSize.maxWindowSize ? CustomSize.maxWindowSize : windowSize; } } ================================================ FILE: lib/page/component/theme/custom_theme.dart ================================================ import 'dart:ui'; import 'package:flutter/material.dart'; class CustomColors extends ThemeExtension { const CustomColors({ this.chatRoomBackground, this.chatRoomReplyBackground, this.chatRoomReplyBackgroundSecondary, this.chatRoomReplyText, this.chatRoomSenderBackground, this.chatRoomSenderBackgroundSecondary, this.chatRoomSenderBackgroundWarning, this.chatRoomSenderText, this.tagsBackground, this.tagsBackgroundHover, this.tagsText, this.chatInputPanelBackground, this.chatInputPanelText, this.chatInputAreaBackground, this.chatExampleItemBackground, this.chatExampleItemBackgroundHover, this.chatExampleItemText, this.chatExampleTitleText, this.markdownLinkColor, this.markdownPreColor, this.markdownCodeColor, this.boxShadowColor, this.backgroundColor, this.backgroundInvertedColor, this.backgroundContainerColor, this.backgroundForDialogListItem, this.listTileBackgroundColor, this.textFieldBorderColor, this.iconButtonColor, this.linkColor, this.weakLinkColor, this.weakTextColor, this.weakTextColorLess, this.weakTextColorPlus, this.weakTextColorPlusPlus, this.dialogDefaultTextColor, this.dialogBackgroundColor, this.columnBlockBorderColor, this.columnBlockBackgroundColor, this.columnBlockDividerColor, this.textfieldHintColor, this.textfieldHintDeepColor, this.textfieldLabelColor, this.textfieldValueColor, this.textfieldBackgroundColor, this.textfieldSelectorColor, this.paymentItemBorderColor, this.paymentItemBackgroundColor, this.paymentItemTitleColor, this.paymentItemPriceColor, this.paymentItemDateColor, this.paymentItemDescriptionColor, this.settingsSectionBackground, }); final Color? chatRoomBackground; final Color? chatRoomReplyBackground; final Color? chatRoomReplyBackgroundSecondary; final Color? chatRoomReplyText; final Color? chatRoomSenderBackground; final Color? chatRoomSenderBackgroundSecondary; final Color? chatRoomSenderBackgroundWarning; final Color? chatRoomSenderText; final Color? tagsBackground; final Color? tagsBackgroundHover; final Color? tagsText; final Color? chatInputPanelBackground; final Color? chatInputPanelText; final Color? chatInputAreaBackground; final Color? chatExampleItemBackground; final Color? chatExampleItemBackgroundHover; final Color? chatExampleItemText; final Color? chatExampleTitleText; final Color? markdownLinkColor; final Color? markdownPreColor; final Color? markdownCodeColor; final Color? boxShadowColor; final Color? backgroundColor; final Color? backgroundInvertedColor; final Color? backgroundContainerColor; final Color? backgroundForDialogListItem; final Color? listTileBackgroundColor; final Color? textFieldBorderColor; final Color? iconButtonColor; final Color? linkColor; final Color? weakLinkColor; final Color? weakTextColor; final Color? weakTextColorLess; final Color? weakTextColorPlus; final Color? weakTextColorPlusPlus; final Color? dialogDefaultTextColor; final Color? dialogBackgroundColor; final Color? columnBlockBorderColor; final Color? columnBlockBackgroundColor; final Color? columnBlockDividerColor; final Color? textfieldHintColor; final Color? textfieldHintDeepColor; final Color? textfieldLabelColor; final Color? textfieldValueColor; final Color? textfieldBackgroundColor; final Color? textfieldSelectorColor; final Color? paymentItemBorderColor; final Color? paymentItemBackgroundColor; final Color? paymentItemTitleColor; final Color? paymentItemPriceColor; final Color? paymentItemDateColor; final Color? paymentItemDescriptionColor; final Color? settingsSectionBackground; @override ThemeExtension lerp( covariant ThemeExtension? other, double t, ) { if (other is! CustomColors) { return this; } return CustomColors( chatRoomBackground: Color.lerp(chatRoomBackground, other.chatRoomBackground, t), chatRoomReplyBackground: Color.lerp(chatRoomReplyBackground, other.chatRoomReplyBackground, t), chatRoomReplyBackgroundSecondary: Color.lerp(chatRoomReplyBackgroundSecondary, other.chatRoomReplyBackgroundSecondary, t), chatRoomReplyText: Color.lerp(chatRoomReplyText, other.chatRoomReplyText, t), chatRoomSenderBackground: Color.lerp(chatRoomSenderBackground, other.chatRoomSenderBackground, t), chatRoomSenderBackgroundSecondary: Color.lerp(chatRoomSenderBackgroundSecondary, other.chatRoomSenderBackgroundSecondary, t), chatRoomSenderBackgroundWarning: Color.lerp(chatRoomSenderBackgroundWarning, other.chatRoomSenderBackgroundWarning, t), chatRoomSenderText: Color.lerp(chatRoomSenderText, other.chatRoomSenderText, t), tagsBackground: Color.lerp(tagsBackground, other.tagsBackground, t), tagsBackgroundHover: Color.lerp(tagsBackgroundHover, other.tagsBackgroundHover, t), tagsText: Color.lerp(tagsText, other.tagsText, t), chatInputPanelBackground: Color.lerp(chatInputPanelBackground, other.chatInputPanelBackground, t), chatInputPanelText: Color.lerp(chatInputPanelText, other.chatInputPanelText, t), chatInputAreaBackground: Color.lerp(chatInputAreaBackground, other.chatInputAreaBackground, t), chatExampleItemBackground: Color.lerp(chatExampleItemBackground, other.chatExampleItemBackground, t), chatExampleItemBackgroundHover: Color.lerp(chatExampleItemBackgroundHover, other.chatExampleItemBackgroundHover, t), chatExampleItemText: Color.lerp(chatExampleItemText, other.chatExampleItemText, t), chatExampleTitleText: Color.lerp(chatExampleTitleText, other.chatExampleTitleText, t), markdownLinkColor: Color.lerp(markdownLinkColor, other.markdownLinkColor, t), markdownPreColor: Color.lerp(markdownPreColor, other.markdownPreColor, t), markdownCodeColor: Color.lerp(markdownCodeColor, other.markdownCodeColor, t), boxShadowColor: Color.lerp(boxShadowColor, other.boxShadowColor, t), backgroundColor: Color.lerp(backgroundColor, other.backgroundColor, t), backgroundInvertedColor: Color.lerp(backgroundInvertedColor, other.backgroundInvertedColor, t), backgroundContainerColor: Color.lerp(backgroundContainerColor, other.backgroundContainerColor, t), backgroundForDialogListItem: Color.lerp(backgroundForDialogListItem, other.backgroundForDialogListItem, t), listTileBackgroundColor: Color.lerp(listTileBackgroundColor, other.listTileBackgroundColor, t), textFieldBorderColor: Color.lerp(textFieldBorderColor, other.textFieldBorderColor, t), iconButtonColor: Color.lerp(iconButtonColor, other.iconButtonColor, t), weakLinkColor: Color.lerp(weakLinkColor, other.weakLinkColor, t), weakTextColor: Color.lerp(weakTextColor, other.weakTextColor, t), weakTextColorLess: Color.lerp(weakTextColorLess, other.weakTextColorLess, t), weakTextColorPlus: Color.lerp(weakTextColorPlus, other.weakTextColorPlus, t), weakTextColorPlusPlus: Color.lerp(weakTextColorPlusPlus, other.weakTextColorPlusPlus, t), dialogDefaultTextColor: Color.lerp(dialogDefaultTextColor, other.dialogDefaultTextColor, t), dialogBackgroundColor: Color.lerp(dialogBackgroundColor, other.dialogBackgroundColor, t), columnBlockBorderColor: Color.lerp(columnBlockBorderColor, other.columnBlockBorderColor, t), columnBlockBackgroundColor: Color.lerp(columnBlockBackgroundColor, other.columnBlockBackgroundColor, t), columnBlockDividerColor: Color.lerp(columnBlockDividerColor, other.columnBlockDividerColor, t), textfieldHintColor: Color.lerp(textfieldHintColor, other.textfieldHintColor, t), textfieldHintDeepColor: Color.lerp(textfieldHintDeepColor, other.textfieldHintDeepColor, t), textfieldLabelColor: Color.lerp(textfieldLabelColor, other.textfieldLabelColor, t), textfieldValueColor: Color.lerp(textfieldValueColor, other.textfieldValueColor, t), textfieldBackgroundColor: Color.lerp(textfieldBackgroundColor, other.textfieldBackgroundColor, t), textfieldSelectorColor: Color.lerp(textfieldSelectorColor, other.textfieldSelectorColor, t), paymentItemBorderColor: Color.lerp(paymentItemBorderColor, other.paymentItemBorderColor, t), paymentItemBackgroundColor: Color.lerp(paymentItemBackgroundColor, other.paymentItemBackgroundColor, t), paymentItemTitleColor: Color.lerp(paymentItemTitleColor, other.paymentItemTitleColor, t), paymentItemPriceColor: Color.lerp(paymentItemPriceColor, other.paymentItemPriceColor, t), paymentItemDateColor: Color.lerp(paymentItemDateColor, other.paymentItemDateColor, t), paymentItemDescriptionColor: Color.lerp(paymentItemDescriptionColor, other.paymentItemDescriptionColor, t), settingsSectionBackground: Color.lerp(settingsSectionBackground, other.settingsSectionBackground, t), ); } static const light = CustomColors( chatRoomBackground: Color.fromARGB(255, 239, 239, 239), chatRoomReplyBackground: Colors.transparent, chatRoomReplyBackgroundSecondary: Color.fromARGB(200, 255, 255, 255), chatRoomReplyText: Color(0xFF000000), chatRoomSenderBackground: Color.fromARGB(255, 242, 242, 242), chatRoomSenderBackgroundSecondary: Color.fromARGB(255, 133, 238, 94), chatRoomSenderBackgroundWarning: Color.fromARGB(255, 255, 176, 131), chatRoomSenderText: Color(0xFF000000), tagsBackground: Color.fromARGB(255, 238, 238, 238), tagsBackgroundHover: Color.fromARGB(255, 237, 237, 237), tagsText: Colors.black, chatInputPanelBackground: Colors.transparent, chatInputPanelText: Color.fromARGB(255, 0, 0, 0), chatInputAreaBackground: Color.fromARGB(255, 255, 255, 255), chatExampleItemBackground: Color.fromARGB(194, 221, 221, 221), chatExampleItemBackgroundHover: Color.fromARGB(255, 223, 223, 223), chatExampleItemText: Color.fromARGB(255, 255, 255, 255), chatExampleTitleText: Color.fromARGB(255, 66, 66, 66), markdownLinkColor: Colors.blue, markdownPreColor: Color.fromARGB(255, 247, 247, 247), markdownCodeColor: Color.fromARGB(255, 167, 100, 153), boxShadowColor: Color.fromARGB(149, 232, 232, 232), backgroundColor: Color.fromARGB(255, 242, 242, 242), backgroundInvertedColor: Color.fromARGB(255, 72, 72, 72), backgroundContainerColor: Color.fromARGB(255, 255, 255, 255), backgroundForDialogListItem: Color.fromARGB(255, 255, 255, 255), listTileBackgroundColor: Color.fromARGB(60, 217, 217, 217), textFieldBorderColor: Color.fromARGB(255, 228, 228, 228), iconButtonColor: Color.fromARGB(255, 117, 117, 117), linkColor: Color.fromARGB(255, 9, 185, 85), weakLinkColor: Color.fromARGB(255, 75, 75, 75), weakTextColor: Color.fromARGB(255, 75, 75, 75), weakTextColorLess: Color.fromARGB(255, 146, 146, 146), weakTextColorPlus: Color.fromARGB(255, 146, 146, 146), weakTextColorPlusPlus: Color.fromARGB(255, 29, 29, 29), dialogDefaultTextColor: Color.fromARGB(195, 0, 0, 0), dialogBackgroundColor: Colors.white, columnBlockBorderColor: Color.fromARGB(255, 236, 236, 236), columnBlockBackgroundColor: Color.fromARGB(255, 255, 255, 255), columnBlockDividerColor: Color.fromARGB(255, 241, 241, 241), textfieldHintColor: Color.fromARGB(255, 181, 181, 181), textfieldHintDeepColor: Color.fromARGB(255, 94, 94, 94), textfieldLabelColor: Color.fromARGB(255, 66, 66, 66), textfieldValueColor: Color.fromARGB(255, 108, 108, 108), textfieldBackgroundColor: Color.fromARGB(255, 230, 230, 230), textfieldSelectorColor: Color.fromARGB(255, 9, 185, 85), paymentItemBorderColor: Color.fromARGB(255, 228, 228, 228), paymentItemBackgroundColor: Color.fromARGB(255, 245, 245, 245), paymentItemTitleColor: Color.fromARGB(255, 66, 66, 66), paymentItemPriceColor: Color.fromARGB(255, 66, 66, 66), paymentItemDateColor: Color.fromARGB(255, 117, 117, 117), paymentItemDescriptionColor: Color.fromARGB(255, 117, 117, 117), settingsSectionBackground: Color.fromARGB(255, 255, 255, 255), ); static const dark = CustomColors( chatRoomBackground: Color.fromARGB(255, 0, 0, 0), chatRoomReplyBackground: Colors.transparent, chatRoomReplyBackgroundSecondary: Color.fromARGB(200, 39, 39, 39), chatRoomReplyText: Color(0xFFECEFF1), chatRoomSenderBackground: Color.fromARGB(255, 33, 33, 33), chatRoomSenderBackgroundSecondary: Color.fromARGB(181, 36, 172, 86), chatRoomSenderBackgroundWarning: Color.fromARGB(255, 255, 176, 131), chatRoomSenderText: Color(0xFFECEFF1), tagsBackground: Color.fromARGB(255, 69, 69, 69), tagsBackgroundHover: Color.fromARGB(255, 106, 106, 106), tagsText: Color.fromARGB(255, 218, 218, 218), chatInputPanelBackground: Color.fromARGB(255, 0, 0, 0), chatInputPanelText: Color.fromARGB(255, 255, 255, 255), chatInputAreaBackground: Color.fromARGB(255, 32, 32, 32), chatExampleItemBackground: Color.fromARGB(255, 80, 80, 80), chatExampleItemBackgroundHover: Color.fromARGB(255, 69, 69, 69), chatExampleItemText: Color.fromARGB(255, 218, 218, 218), chatExampleTitleText: Color.fromARGB(255, 150, 150, 150), markdownLinkColor: Color.fromARGB(255, 0, 122, 255), markdownPreColor: Color.fromARGB(255, 16, 16, 16), markdownCodeColor: Color.fromARGB(255, 179, 148, 173), boxShadowColor: Color.fromARGB(70, 37, 37, 37), backgroundColor: Color.fromARGB(255, 30, 30, 30), backgroundInvertedColor: Color.fromARGB(255, 233, 233, 233), backgroundContainerColor: Color.fromARGB(255, 0, 0, 0), backgroundForDialogListItem: Color.fromARGB(23, 0, 0, 0), listTileBackgroundColor: Color.fromARGB(25, 0, 0, 0), textFieldBorderColor: Color.fromARGB(106, 107, 107, 107), iconButtonColor: Color.fromARGB(255, 218, 218, 218), linkColor: Color.fromARGB(255, 9, 185, 85), weakLinkColor: Color.fromARGB(255, 218, 218, 218), weakTextColor: Color.fromARGB(255, 130, 130, 130), weakTextColorLess: Color.fromARGB(198, 146, 146, 146), weakTextColorPlus: Color.fromARGB(255, 137, 137, 137), weakTextColorPlusPlus: Color.fromARGB(255, 173, 173, 173), dialogDefaultTextColor: Color.fromARGB(195, 255, 255, 255), dialogBackgroundColor: Colors.black, columnBlockBorderColor: Color.fromARGB(255, 72, 72, 72), columnBlockBackgroundColor: Color.fromARGB(255, 44, 44, 46), columnBlockDividerColor: Color.fromARGB(57, 60, 60, 60), textfieldHintColor: Color.fromARGB(255, 105, 105, 105), textfieldHintDeepColor: Color.fromARGB(255, 170, 170, 170), textfieldLabelColor: Color.fromARGB(255, 218, 218, 218), textfieldValueColor: Color.fromARGB(255, 207, 207, 207), textfieldBackgroundColor: Color.fromARGB(255, 44, 44, 44), textfieldSelectorColor: Color.fromARGB(255, 9, 185, 85), paymentItemBorderColor: Color.fromARGB(255, 69, 69, 69), paymentItemBackgroundColor: Color.fromARGB(255, 29, 29, 29), paymentItemTitleColor: Color.fromARGB(255, 218, 218, 218), paymentItemPriceColor: Color.fromARGB(255, 218, 218, 218), paymentItemDateColor: Color.fromARGB(255, 218, 218, 218), paymentItemDescriptionColor: Color.fromARGB(255, 218, 218, 218), settingsSectionBackground: Color.fromARGB(255, 44, 44, 46), ); @override ThemeExtension copyWith({ Color? chatRoomBackground, Color? chatRoomReplyBackground, Color? chatRoomReplyBackgroundSecondary, Color? chatRoomReplyText, Color? chatRoomSenderBackground, Color? chatRoomSenderBackgroundSecondary, Color? chatRoomSenderBackgroundWarning, Color? chatRoomSenderText, Color? tagsBackground, Color? tagsBackgroundHover, Color? tagsText, Color? chatInputPanelBackground, Color? chatInputPanelText, Color? chatInputAreaBackground, Color? chatExampleItemBackground, Color? chatExampleItemBackgroundHover, Color? chatExampleItemText, Color? chatExampleTitleText, Color? markdownLinkColor, Color? markdownPreColor, Color? markdownCodeColor, Color? boxShadowColor, Color? backgroundColor, Color? backgroundInvertedColor, Color? backgroundContainerColor, Color? backgroundForDialogListItem, Color? listTileBackgroundColor, Color? textFieldBorderColor, Color? iconButtonColor, Color? linkColor, Color? weakLinkColor, Color? weakTextColor, Color? weakTextColorLess, Color? weakTextColorPlus, Color? weakTextColorPlusPlus, Color? dialogDefaultTextColor, Color? dialogBackgroundColor, Color? columnBlockBorderColor, Color? columnBlockBackgroundColor, Color? columnBlockDividerColor, Color? textfieldHintColor, Color? textfieldHintDeepColor, Color? textfieldLabelColor, Color? textfieldValueColor, Color? textfieldBackgroundColor, Color? textfieldSelectorColor, Color? paymentItemBorderColor, Color? paymentItemBackgroundColor, Color? paymentItemTitleColor, Color? paymentItemPriceColor, Color? paymentItemDateColor, Color? paymentItemDescriptionColor, Color? settingsSectionBackground, }) { return CustomColors( chatRoomBackground: chatRoomBackground ?? this.chatRoomBackground, chatRoomReplyBackground: chatRoomReplyBackground ?? this.chatRoomReplyBackground, chatRoomReplyBackgroundSecondary: chatRoomReplyBackgroundSecondary ?? this.chatRoomReplyBackgroundSecondary, chatRoomReplyText: chatRoomReplyText ?? this.chatRoomReplyText, chatRoomSenderBackground: chatRoomSenderBackground ?? this.chatRoomSenderBackground, chatRoomSenderBackgroundSecondary: chatRoomSenderBackgroundSecondary ?? this.chatRoomSenderBackgroundSecondary, chatRoomSenderBackgroundWarning: chatRoomSenderBackgroundWarning ?? this.chatRoomSenderBackgroundWarning, chatRoomSenderText: chatRoomSenderText ?? this.chatRoomSenderText, tagsBackground: tagsBackground ?? this.tagsBackground, tagsBackgroundHover: tagsBackgroundHover ?? this.tagsBackgroundHover, tagsText: tagsText ?? this.tagsText, chatInputPanelBackground: chatInputPanelBackground ?? this.chatInputPanelBackground, chatInputPanelText: chatInputPanelText ?? this.chatInputPanelText, chatInputAreaBackground: chatInputAreaBackground ?? this.chatInputAreaBackground, chatExampleItemBackground: chatExampleItemBackground ?? this.chatExampleItemBackground, chatExampleItemBackgroundHover: chatExampleItemBackgroundHover ?? this.chatExampleItemBackgroundHover, chatExampleItemText: chatExampleItemText ?? this.chatExampleItemText, chatExampleTitleText: chatExampleTitleText ?? this.chatExampleTitleText, markdownLinkColor: markdownLinkColor ?? this.markdownLinkColor, markdownPreColor: markdownPreColor ?? this.markdownPreColor, markdownCodeColor: markdownCodeColor ?? this.markdownCodeColor, boxShadowColor: boxShadowColor ?? this.boxShadowColor, backgroundColor: backgroundColor ?? this.backgroundColor, backgroundInvertedColor: backgroundInvertedColor ?? this.backgroundInvertedColor, backgroundContainerColor: backgroundContainerColor ?? this.backgroundContainerColor, backgroundForDialogListItem: backgroundForDialogListItem ?? this.backgroundForDialogListItem, listTileBackgroundColor: listTileBackgroundColor ?? this.listTileBackgroundColor, textFieldBorderColor: textFieldBorderColor ?? this.textFieldBorderColor, iconButtonColor: iconButtonColor ?? this.iconButtonColor, linkColor: linkColor ?? this.linkColor, weakLinkColor: weakLinkColor ?? this.weakLinkColor, weakTextColor: weakTextColor ?? this.weakTextColor, weakTextColorLess: weakTextColorLess ?? this.weakTextColorLess, weakTextColorPlus: weakTextColorPlus ?? this.weakTextColorPlus, weakTextColorPlusPlus: weakTextColorPlusPlus ?? this.weakTextColorPlusPlus, dialogDefaultTextColor: dialogDefaultTextColor ?? this.dialogDefaultTextColor, dialogBackgroundColor: dialogBackgroundColor ?? this.dialogBackgroundColor, columnBlockBorderColor: columnBlockBorderColor ?? this.columnBlockBorderColor, columnBlockBackgroundColor: columnBlockBackgroundColor ?? this.columnBlockBackgroundColor, columnBlockDividerColor: columnBlockDividerColor ?? this.columnBlockDividerColor, textfieldHintColor: textfieldHintColor ?? this.textfieldHintColor, textfieldHintDeepColor: textfieldHintDeepColor ?? this.textfieldHintDeepColor, textfieldLabelColor: textfieldLabelColor ?? this.textfieldLabelColor, textfieldValueColor: textfieldValueColor ?? this.textfieldValueColor, textfieldBackgroundColor: textfieldBackgroundColor ?? this.textfieldBackgroundColor, textfieldSelectorColor: textfieldSelectorColor ?? this.textfieldSelectorColor, paymentItemBorderColor: paymentItemBorderColor ?? this.paymentItemBorderColor, paymentItemBackgroundColor: paymentItemBackgroundColor ?? this.paymentItemBackgroundColor, paymentItemTitleColor: paymentItemTitleColor ?? this.paymentItemTitleColor, paymentItemPriceColor: paymentItemPriceColor ?? this.paymentItemPriceColor, paymentItemDateColor: paymentItemDateColor ?? this.paymentItemDateColor, paymentItemDescriptionColor: paymentItemDescriptionColor ?? this.paymentItemDescriptionColor, settingsSectionBackground: settingsSectionBackground ?? this.settingsSectionBackground, ); } } ================================================ FILE: lib/page/component/theme/theme.dart ================================================ import 'package:flutter/material.dart'; class AppTheme extends ChangeNotifier { ThemeMode _mode = ThemeMode.system; ThemeMode get mode => _mode; AppTheme(String mode) { switch (mode) { case 'light': _mode = ThemeMode.light; break; case 'dark': _mode = ThemeMode.dark; break; default: _mode = ThemeMode.system; } } static AppTheme instance = AppTheme('system'); static AppTheme get() { if (!instance.mounted) { instance = AppTheme('system'); } return instance; } static ThemeMode themeModeFormString(String mode) { switch (mode) { case 'light': return ThemeMode.light; case 'dark': return ThemeMode.dark; default: return ThemeMode.system; } } set mode(ThemeMode mode) { _mode = mode; notifyListeners(); } bool _mounted = false; bool get mounted => _mounted; @override void dispose() { super.dispose(); _mounted = true; } } ================================================ FILE: lib/page/component/transition_resolver.dart ================================================ import 'package:animations/animations.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; Page transitionResolver(Widget child, {bool useTransition = true}) { if (useTransition) { return CustomTransitionPage( child: child, transitionsBuilder: (context, animation, secondaryAnimation, child) { return FadeThroughTransition( animation: animation, secondaryAnimation: secondaryAnimation, child: child, ); }, ); } return MaterialPage(child: child); } ================================================ FILE: lib/page/component/verify_code_input.dart ================================================ import 'dart:async'; import 'package:askaide/helper/helper.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_localization/flutter_localization.dart'; class VerifyCodeInput extends StatefulWidget { final Future Function() sendVerifyCode; final Function(String) onVerifyCodeSent; final bool Function() sendCheck; final TextEditingController controller; final bool inColumnBlock; const VerifyCodeInput({ super.key, required this.onVerifyCodeSent, required this.sendVerifyCode, required this.sendCheck, required this.controller, this.inColumnBlock = false, }); @override State createState() => _VerifyCodeInputState(); } class _VerifyCodeInputState extends State { final phoneNumberValidator = RegExp(r"^1[3456789]\d{9}$"); // 下次发送验证码等待时间 int verifyCodeWaitSeconds = 0; Timer? timer; DateTime? lastSendVerifyCodeTime; @override void dispose() { timer?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { var customColors = Theme.of(context).extension()!; return Container( padding: widget.inColumnBlock ? const EdgeInsets.all(5) : null, child: Row( children: [ if (widget.inColumnBlock) SizedBox( width: 80, child: Text( AppLocale.verifyCode.getString(context), overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 16, color: customColors.textfieldLabelColor, ), ), ), if (widget.inColumnBlock) const SizedBox(width: 10), Expanded( child: TextFormField( controller: widget.controller, inputFormatters: [ FilteringTextInputFormatter.singleLineFormatter, FilteringTextInputFormatter.digitsOnly, ], maxLength: 6, keyboardType: TextInputType.number, decoration: InputDecoration( counterText: '', border: widget.inColumnBlock ? InputBorder.none : const OutlineInputBorder(), enabledBorder: widget.inColumnBlock ? InputBorder.none : const OutlineInputBorder( borderSide: BorderSide(color: Color.fromARGB(200, 192, 192, 192)), ), focusedBorder: widget.inColumnBlock ? InputBorder.none : OutlineInputBorder( borderSide: BorderSide(color: customColors.linkColor ?? Colors.green), ), // floatingLabelStyle: // TextStyle(color: customColors.linkColor ?? Colors.green), isDense: true, floatingLabelBehavior: FloatingLabelBehavior.always, labelText: widget.inColumnBlock ? null : AppLocale.verifyCode.getString(context), labelStyle: const TextStyle(fontSize: 17), hintText: AppLocale.verifyCodeInputTips.getString(context), hintStyle: TextStyle( color: customColors.textfieldHintColor, fontSize: 15, ), ), ), ), const SizedBox(width: 30), SizedBox( width: 100, child: verifyCodeWaitSeconds > 0 ? TextButton( onPressed: null, child: AutoSizeText( '$verifyCodeWaitSeconds ${AppLocale.retryInSeconds.getString(context)}', style: TextStyle( color: customColors.weakTextColor, fontSize: 15, ), maxLines: 1, ), ) : TextButton( onPressed: () { if (!widget.sendCheck()) { return; } widget.sendVerifyCode().then((id) { widget.onVerifyCodeSent(id); setState(() { verifyCodeWaitSeconds = 60; }); if (timer != null) { timer?.cancel(); timer = null; } lastSendVerifyCodeTime = DateTime.now(); timer = Timer.periodic(const Duration(seconds: 1), (timer) { if (verifyCodeWaitSeconds <= 0) { timer.cancel(); return; } setState(() { verifyCodeWaitSeconds = 60 - (DateTime.now().difference(lastSendVerifyCodeTime!).inSeconds); }); }); showSuccessMessage(AppLocale.verifyCodeSendSuccess.getString(context)); }).onError((error, stackTrace) { setState(() { verifyCodeWaitSeconds = 0; }); timer?.cancel(); showErrorMessage(resolveError(context, error!)); }); }, child: Text( AppLocale.sendVerifyCode.getString(context), style: TextStyle( color: customColors.linkColor, fontSize: 15, ), ), ), ), ], ), ); } } ================================================ FILE: lib/page/component/video_player.dart ================================================ import 'dart:io'; import 'package:askaide/helper/helper.dart'; import 'package:askaide/helper/logger.dart'; import 'package:askaide/helper/platform.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:file_saver/file_saver.dart'; import 'package:flutter/material.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:image_gallery_saver/image_gallery_saver.dart'; import 'package:media_kit/media_kit.dart'; import 'package:media_kit_video/media_kit_video.dart'; class VideoPlayer extends StatefulWidget { final String url; final double? aspectRatio; final int? width; final int? height; const VideoPlayer({super.key, required this.url, this.width, this.height, this.aspectRatio}); @override State createState() => _VideoPlayerState(); } class _VideoPlayerState extends State { late final player = Player(); late final controller = VideoController(player); @override void initState() { super.initState(); player.setPlaylistMode(PlaylistMode.single); player.open(Media(widget.url)); } @override void dispose() { player.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return Container( decoration: BoxDecoration( color: customColors.columnBlockBackgroundColor, borderRadius: CustomSize.borderRadius, ), child: Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ ClipRRect( borderRadius: const BorderRadius.only(topLeft: CustomSize.radius, topRight: CustomSize.radius), child: Center( child: SizedBox( width: MediaQuery.of(context).size.width, height: widget.width != null && widget.height != null ? MediaQuery.of(context).size.width * widget.height! / widget.width! : MediaQuery.of(context).size.width, child: Video( controller: controller, width: widget.width?.toDouble(), height: widget.height?.toDouble(), aspectRatio: widget.aspectRatio, ), ), ), ), Padding( padding: const EdgeInsets.only(right: 10), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ IconButton( splashColor: Colors.transparent, highlightColor: Colors.transparent, hoverColor: Colors.transparent, icon: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.download, size: 14, color: customColors.weakLinkColor, ), const SizedBox(width: 5), Text( AppLocale.download.getString(context), style: TextStyle( fontSize: 12, color: customColors.weakLinkColor, ), ), ], ), onPressed: () async { final cancel = BotToast.showCustomLoading( toastBuilder: (cancel) { return const LoadingIndicator( message: 'Downloading, please wait...', ); }, allowClick: false, duration: const Duration(seconds: 120), ); try { final saveFile = await DefaultCacheManager().getSingleFile(widget.url); if (PlatformTool.isIOS() || PlatformTool.isAndroid()) { await ImageGallerySaver.saveFile(saveFile.path); showSuccessMessage(AppLocale.operateSuccess.getString(context)); } else { var ext = saveFile.path.toLowerCase().split('.').last; if (PlatformTool.isWindows()) { FileSaver.instance .saveAs( name: filenameWithoutExt(saveFile.path.split('/').last), filePath: saveFile.path, ext: '.$ext', mimeType: MimeType.mpeg, ) .then((value) async { if (value == null) { return; } await File(value).writeAsBytes(await saveFile.readAsBytes()); Logger.instance.d('File saved successfully: $value'); showSuccessMessage(AppLocale.operateSuccess.getString(context)); }); } else { FileSaver.instance .saveFile( name: filenameWithoutExt(saveFile.path.split('/').last), filePath: saveFile.path, ext: ext, mimeType: MimeType.mpeg, ) .then((value) { showSuccessMessage(AppLocale.operateSuccess.getString(context)); }); } } } catch (e) { // ignore: use_build_context_synchronously showErrorMessageEnhanced(context, 'Image save failed, please try again later'); Logger.instance.e('Download failed', error: e); } finally { cancel(); } }, ), ], ), ), ], ), ); } } ================================================ FILE: lib/page/component/weak_text_button.dart ================================================ import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; class WeakTextButton extends StatelessWidget { final String title; final IconData? icon; final VoidCallback? onPressed; final double? fontSize; const WeakTextButton({ super.key, required this.title, this.icon, this.onPressed, this.fontSize, }); @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; final item = Row( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ if (icon != null) Icon( icon, color: customColors.weakLinkColor, size: (fontSize ?? 15.0) + 1, ), if (icon != null) const SizedBox(width: 5), Text( title, style: TextStyle( color: customColors.weakLinkColor, fontSize: fontSize ?? 15.0, ), ), ], ); if (onPressed == null) { return item; } return TextButton( onPressed: onPressed, child: item, ); } } ================================================ FILE: lib/page/component/windows.dart ================================================ import 'package:askaide/helper/platform.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:flutter/material.dart'; class WindowFrameWidget extends StatelessWidget { final Color? backgroundColor; final Widget child; const WindowFrameWidget({super.key, required this.child, this.backgroundColor}); @override Widget build(BuildContext context) { if (!PlatformTool.isDesktop()) { return child; } final customColors = Theme.of(context).extension()!; return WindowBorder( color: Colors.transparent, width: 0, child: Container( decoration: BoxDecoration( color: backgroundColor ?? customColors.backgroundContainerColor, ), child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ WindowTitleBarBox( child: Row( children: [Expanded(child: MoveWindow()), const WindowButtons()], ), ), Expanded(child: child), ], ), ), ); } } final buttonColors = WindowButtonColors( iconNormal: const Color.fromARGB(255, 93, 93, 93), mouseOver: const Color.fromARGB(255, 90, 90, 90), mouseDown: const Color.fromARGB(255, 171, 171, 171), iconMouseOver: const Color.fromARGB(255, 190, 190, 190), iconMouseDown: const Color.fromARGB(255, 217, 217, 217)); final closeButtonColors = WindowButtonColors( mouseOver: const Color(0xFFD32F2F), mouseDown: const Color(0xFFB71C1C), iconNormal: const Color(0xFF805306), iconMouseOver: Colors.white); class WindowButtons extends StatefulWidget { const WindowButtons({super.key}); @override State createState() => _WindowButtonsState(); } class _WindowButtonsState extends State { void maximizeOrRestore() { setState(() { appWindow.maximizeOrRestore(); }); } @override Widget build(BuildContext context) { return Row( children: [ MinimizeWindowButton(colors: buttonColors), appWindow.isMaximized ? RestoreWindowButton( colors: buttonColors, onPressed: maximizeOrRestore, ) : MaximizeWindowButton( colors: buttonColors, onPressed: maximizeOrRestore, ), CloseWindowButton(colors: closeButtonColors), ], ); } } ================================================ FILE: lib/page/creative_island/draw/artistic_qr.dart ================================================ import 'dart:math'; import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/cache.dart'; import 'package:askaide/helper/haptic_feedback.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/advanced_button.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/column_block.dart'; import 'package:askaide/page/component/enhanced_button.dart'; import 'package:askaide/page/component/enhanced_input.dart'; import 'package:askaide/page/component/enhanced_textfield.dart'; import 'package:askaide/page/component/global_alert.dart'; import 'package:askaide/page/component/item_selector_search.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/prompt_tags_selector.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/page/creative_island/draw/components/artistic_style_selector.dart'; import 'package:askaide/page/creative_island/draw/components/content_preview.dart'; import 'package:askaide/page/creative_island/draw/draw_result.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api/creative.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/model/misc.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; import 'package:quickalert/models/quickalert_type.dart'; class ArtisticQRScreen extends StatefulWidget { final SettingRepository setting; final int? galleryCopyId; final String type; final String id; final String? note; const ArtisticQRScreen({ super.key, required this.id, required this.setting, this.galleryCopyId, required this.type, this.note, }); @override State createState() => _ArtisticQRScreenState(); } class _ArtisticQRScreenState extends State { bool enableAIRewrite = false; bool showAdvancedOptions = false; CreativeIslandCapacity? capacity; CreativeIslandArtisticStyle? selectedStyle; /// 是否停止周期性查询任务执行状态 var stopPeriodQuery = false; int generationImageCount = 1; double? textWeight = 1.35; TextEditingController promptController = TextEditingController(); TextEditingController negativePromptController = TextEditingController(); TextEditingController textController = TextEditingController(); TextEditingController seedController = TextEditingController(); @override void dispose() { promptController.dispose(); negativePromptController.dispose(); textController.dispose(); seedController.dispose(); super.dispose(); } @override void initState() { APIServer().creativeIslandCapacity(mode: widget.type, id: widget.id).then((cap) { setState(() { capacity = cap; }); if (widget.galleryCopyId != null && widget.galleryCopyId! > 0) { APIServer().creativeGalleryItem(id: widget.galleryCopyId!).then((response) { final gallery = response.item; if (gallery.prompt != null && gallery.prompt!.isNotEmpty) { promptController.text = gallery.prompt!; } if (gallery.metaMap['real_prompt'] != null && gallery.metaMap['real_prompt'] != '') { promptController.text = gallery.metaMap['real_prompt']!; } if (gallery.metaMap['negative_prompt'] != null && gallery.metaMap['negative_prompt'] != '') { negativePromptController.text = gallery.metaMap['negative_prompt']!; } if (gallery.metaMap['real_negative_prompt'] != null && gallery.metaMap['real_negative_prompt'] != '') { negativePromptController.text = gallery.metaMap['real_negative_prompt']!; } // 创建同款时,默认关闭 AI 优化,除非该同款包含 ai_rewrite 的设定 enableAIRewrite = false; if ((gallery.metaMap['real_prompt'] == null || gallery.metaMap['real_prompt'] == '') && gallery.metaMap['ai_rewrite'] != null && gallery.metaMap['ai_rewrite']) { enableAIRewrite = gallery.metaMap['ai_rewrite']; } setState(() {}); }); } }); if (widget.note != null) { Cache().boolGet(key: 'creative:tutorials:${widget.type}:dialog').then((show) { if (!show) { return; } openDefaultTutorials(onConfirm: () { Cache().setBool( key: 'creative:tutorials:${widget.type}:dialog', value: false, duration: const Duration(days: 30), ); }); }); } super.initState(); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( title: Text( widget.type == 'qr' ? '艺术二维码' : '图文融合', style: const TextStyle(fontSize: CustomSize.appBarTitleSize), ), centerTitle: true, leading: IconButton( onPressed: () { context.pop(); }, icon: const Icon(Icons.arrow_back_ios), ), toolbarHeight: CustomSize.toolbarHeight, backgroundColor: customColors.backgroundColor, actions: [ if (widget.note != null) IconButton( onPressed: () { openDefaultTutorials(); }, icon: const Icon(Icons.help_outline), ) ], ), backgroundColor: customColors.backgroundColor, body: BackgroundContainer( setting: widget.setting, enabled: false, backgroundColor: customColors.backgroundColor, maxWidth: CustomSize.smallWindowSize, child: Column( children: [ if (Ability().showGlobalAlert) const GlobalAlert(pageKey: 'creative_create'), Expanded( child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), height: double.infinity, child: SingleChildScrollView( child: buildEditPanel(context, customColors), ), ), ), ], ), ), ), ); } void openDefaultTutorials({Function? onConfirm}) { showBeautyDialog( context, type: QuickAlertType.info, text: ' ${widget.note!}', onConfirmBtnTap: () async { onConfirm?.call(); context.pop(); }, showCancelBtn: true, confirmBtnText: AppLocale.gotIt.getString(context), ); } Widget buildEditPanel(BuildContext context, CustomColors customColors) { return SafeArea( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ColumnBlock( innerPanding: 10, padding: const EdgeInsets.only(left: 15, right: 15, top: 15, bottom: 0), children: [ if (capacity != null && capacity!.artisticStyles.isNotEmpty) ArtisticStyleSelector( styles: capacity!.artisticStyles, onSelected: (style) { setState(() { selectedStyle = style; }); }, selectedStyle: selectedStyle, ), ], ), ColumnBlock( innerPanding: 10, padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 15), children: [ EnhancedTextField( labelPosition: LabelPosition.top, labelText: widget.type == 'qr' ? '链接地址' : '文字内容', customColors: customColors, controller: textController, textAlignVertical: TextAlignVertical.top, hintText: widget.type == 'qr' ? '要生成的二维码链接地址。' : '要在画面中绘制的文字。', maxLength: widget.type == 'qr' ? 250 : 20, maxLines: 3, minLines: 1, showCounter: false, ), // 生成内容 ...buildPromptField(customColors), // AI 优化配置 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Text( AppLocale.smartOptimization.getString(context), style: const TextStyle(fontSize: 16), ), const SizedBox(width: 5), InkWell( onTap: () { showBeautyDialog( context, type: QuickAlertType.info, text: AppLocale.onceEnabledSmartOptimization.getString(context), confirmBtnText: AppLocale.gotIt.getString(context), showCancelBtn: false, ); }, child: Icon( Icons.help_outline, size: 16, color: customColors.weakLinkColor?.withAlpha(150), ), ), ], ), CupertinoSwitch( activeColor: customColors.linkColor, value: enableAIRewrite, onChanged: (value) { setState(() { enableAIRewrite = value; }); }, ), ], ), ], ), if (showAdvancedOptions) ColumnBlock( innerPanding: 10, padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 15), children: [ // 反向提示语 EnhancedTextField( labelPosition: LabelPosition.top, labelText: AppLocale.excludeContents.getString(context), customColors: customColors, controller: negativePromptController, textAlignVertical: TextAlignVertical.top, hintText: AppLocale.unwantedElements.getString(context), maxLength: 500, maxLines: 5, minLines: 3, showCounter: false, ), // 权重 Row( children: [ Row( children: [ const Text('文本权重'), const SizedBox(width: 5), InkWell( onTap: () { showBeautyDialog( context, type: QuickAlertType.info, text: '文本权重\n\n权重越高,图像中出现的文本痕迹越明显。', confirmBtnText: AppLocale.gotIt.getString(context), showCancelBtn: false, ); }, child: Icon( Icons.help_outline, size: 16, color: customColors.weakLinkColor?.withAlpha(150), ), ), ], ), const SizedBox(width: 10), Expanded( child: Slider( value: textWeight ?? 1.35, min: 0, max: 3, divisions: 60, activeColor: customColors.linkColor, onChanged: (value) { setState(() { textWeight = value; }); }, ), ), Text( (textWeight ?? 1.38).toStringAsFixed(2), style: TextStyle( fontSize: 12, color: customColors.weakTextColor, ), ), ], ), // 图片数量 EnhancedInput( title: Text( AppLocale.imageCount.getString(context), style: TextStyle( color: customColors.textfieldLabelColor, fontSize: 16, ), ), value: Text(generationImageCount.toString()), onPressed: () { openListSelectDialog( context, [ SelectorItem(const Text('1', textAlign: TextAlign.center), 1), SelectorItem(const Text('2', textAlign: TextAlign.center), 2), SelectorItem(const Text('3', textAlign: TextAlign.center), 3), SelectorItem(const Text('4', textAlign: TextAlign.center), 4), ], (value) { setState(() { generationImageCount = value.value; }); return true; }, heightFactor: 0.4, value: generationImageCount, ); }, ), // Seed EnhancedTextField( controller: seedController, customColors: customColors, labelText: 'Seed', labelPosition: LabelPosition.left, showCounter: false, keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, ], hintText: '默认随机', textDirection: TextDirection.rtl, ), ], ), // 生成按钮 AdvancedButton( showAdvancedOptions: showAdvancedOptions, onPressed: (value) { setState(() { showAdvancedOptions = value; }); }, ), if (capacity != null) const SizedBox(height: 10), EnhancedButton( title: AppLocale.generate.getString(context), onPressed: onGenerate, ), const SizedBox(height: 20), ], ), ); } List buildPromptField(CustomColors customColors) { return [ EnhancedTextField( labelPosition: LabelPosition.top, labelText: AppLocale.yourIdeas.getString(context), customColors: customColors, controller: promptController, textAlignVertical: TextAlignVertical.top, hintText: AppLocale.keywordsSeparatedByCommas.getString(context), maxLines: 10, minLines: 2, maxLength: 460, showCounter: false, inputSelector: IconButton( onPressed: () { openModalBottomSheet( context, (context) { return PromptTagsSelector( selectedTags: selectedTags, onSubmit: (tags) { setState(() { selectedTags = tags; }); context.pop(); }, ); }, heightFactor: 0.8, useSafeArea: true, ); }, icon: Icon( Icons.lightbulb_outline, color: customColors.linkColor, size: 16, ), ), middleWidget: Container( width: double.infinity, margin: const EdgeInsets.only(bottom: 30), child: Wrap( spacing: 3, runSpacing: 3, children: selectedTags .map( (e) => Tag( name: e.name, backgroundColor: customColors.linkColor, textColor: Colors.white, fontsize: 10, onDeleted: () { setState(() { selectedTags.remove(e); }); }, ), ) .toList(), ), ), bottomButton: Row( children: [ Icon( Icons.shuffle, size: 13, color: customColors.linkColor?.withAlpha(150), ), const SizedBox(width: 5), Text( AppLocale.random.getString(context), style: TextStyle( color: customColors.linkColor?.withAlpha(150), fontSize: 13, ), ), ], ), bottomButtonOnPressed: () async { final examples = await APIServer().exampleByTag('artistic-text'); if (examples.isEmpty) { return; } // 随机选取一个例子 final example = examples[Random().nextInt(examples.length)]; promptController.text = example.text; }, ), ]; } List selectedTags = []; void onGenerate() async { FocusScope.of(context).requestFocus(FocusNode()); HapticFeedbackHelper.mediumImpact(); final prompt = promptController.text.trim(); if (prompt.isEmpty) { showErrorMessage(AppLocale.contentIsRequired.getString(context)); return; } final text = textController.text.trim(); if (text.isEmpty) { showErrorMessage('${widget.type == "qr" ? "链接地址" : "文本内容"}不能为空'); return; } final seed = int.tryParse(seedController.text); if (seed != null && (seed < 0 || seed > 2147483647)) { showErrorMessage('Seed 取值范围为 0 ~ 2147483647'); return; } var params = { 'prompt': prompt, 'prompt_tags': selectedTags.map((e) => e.value).join(','), 'negative_prompt': negativePromptController.text, 'ai_rewrite': enableAIRewrite, 'gallery_copy_id': widget.galleryCopyId, 'text': text, 'type': widget.type, 'seed': seed, 'image_count': generationImageCount, 'control_weight': textWeight, 'style_preset': selectedStyle?.id, }; final cancel = BotToast.showCustomLoading( toastBuilder: (cancel) { return const LoadingIndicator( message: '思考中,请稍候...', ); }, allowClick: false, duration: const Duration(seconds: 15), ); request(int waitDuration) async { try { final taskId = await APIServer().creativeIslandArtisticTextCompletionsAsyncV2(params); stopPeriodQuery = false; cancel(); Navigator.push( // ignore: use_build_context_synchronously context, MaterialPageRoute( fullscreenDialog: true, builder: (context) => DrawResultPage( future: Future.delayed(const Duration(seconds: 10), () async { return await queryCompletionTaskStatus( taskId: taskId, retryTimes: 0, delaySeconds: 3, params: params, ); }), waitDuration: waitDuration, ), ), ).whenComplete(() { stopPeriodQuery = true; }); } catch (e) { stopPeriodQuery = true; cancel(); // ignore: use_build_context_synchronously showErrorMessageEnhanced(context, e); } } try { request(30); } catch (e) { cancel(); // ignore: use_build_context_synchronously showErrorMessageEnhanced(context, e); } } Future queryCompletionTaskStatus({ required String taskId, required int retryTimes, required int delaySeconds, Map? params, }) async { if (retryTimes > 60) { return Future.error(AppLocale.generateTimeout.getString(context)); } final resp = await APIServer().asyncTaskStatus(taskId); switch (resp.status) { case 'success': if (params != null && resp.originImage != null && resp.originImage != '') { params['image'] = resp.originImage; } return IslandResult( result: resp.resources ?? const [], params: params, ); case 'failed': return Future.error(resp.errors!.join(";")); default: if (stopPeriodQuery) { // ignore: use_build_context_synchronously return Future.error(AppLocale.generateTimeout.getString(context)); } return await Future.delayed(Duration(seconds: delaySeconds), () async { return await queryCompletionTaskStatus( taskId: taskId, retryTimes: retryTimes + 1, delaySeconds: 3, params: params, ); }); } } } ================================================ FILE: lib/page/creative_island/draw/artistic_wordart.dart ================================================ import 'dart:math'; import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/cache.dart'; import 'package:askaide/helper/haptic_feedback.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/advanced_button.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/column_block.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/enhanced_button.dart'; import 'package:askaide/page/component/enhanced_input.dart'; import 'package:askaide/page/component/enhanced_textfield.dart'; import 'package:askaide/page/component/global_alert.dart'; import 'package:askaide/page/component/item_selector_search.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/prompt_tags_selector.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/page/creative_island/draw/components/artistic_style_selector.dart'; import 'package:askaide/page/creative_island/draw/components/content_preview.dart'; import 'package:askaide/page/creative_island/draw/draw_result.dart'; import 'package:askaide/repo/api/creative.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/model/misc.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; import 'package:quickalert/models/quickalert_type.dart'; class ArtisticWordArtScreen extends StatefulWidget { final SettingRepository setting; final int? galleryCopyId; final String id; final String? note; const ArtisticWordArtScreen({ super.key, required this.id, required this.setting, this.galleryCopyId, this.note, }); @override State createState() => _ArtisticWordArtScreenState(); } class _ArtisticWordArtScreenState extends State { bool showAdvancedOptions = false; CreativeIslandCapacity? capacity; CreativeIslandArtisticStyle? selectedStyle; CreativeIslandArtisticStyle? selectedFonts; /// 是否停止周期性查询任务执行状态 var stopPeriodQuery = false; int generationImageCount = 1; TextEditingController promptController = TextEditingController(); TextEditingController textController = TextEditingController(); @override void dispose() { promptController.dispose(); textController.dispose(); super.dispose(); } @override void initState() { APIServer().creativeIslandCapacity(mode: 'artistic-text', id: widget.id).then((cap) { setState(() { capacity = cap; }); if (widget.galleryCopyId != null && widget.galleryCopyId! > 0) { APIServer().creativeGalleryItem(id: widget.galleryCopyId!).then((response) { final gallery = response.item; if (gallery.prompt != null && gallery.prompt!.isNotEmpty) { promptController.text = gallery.prompt!; } if (gallery.metaMap['real_prompt'] != null && gallery.metaMap['real_prompt'] != '') { promptController.text = gallery.metaMap['real_prompt']!; } setState(() {}); }); } }); if (widget.note != null) { Cache().boolGet(key: 'creative:tutorials:artistic-text:dialog').then((show) { if (!show) { return; } openDefaultTutorials(onConfirm: () { Cache().setBool( key: 'creative:tutorials:artistic-text:dialog', value: false, duration: const Duration(days: 30), ); }); }); } super.initState(); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( title: const Text( '艺术字', style: TextStyle(fontSize: CustomSize.appBarTitleSize), ), centerTitle: true, leading: IconButton( onPressed: () { context.pop(); }, icon: const Icon(Icons.arrow_back_ios), ), toolbarHeight: CustomSize.toolbarHeight, backgroundColor: customColors.backgroundColor, actions: [ if (widget.note != null) IconButton( onPressed: () { openDefaultTutorials(); }, icon: const Icon(Icons.help_outline), ) ], ), backgroundColor: customColors.backgroundColor, body: BackgroundContainer( setting: widget.setting, enabled: false, maxWidth: CustomSize.smallWindowSize, backgroundColor: customColors.backgroundColor, child: Column( children: [ if (Ability().showGlobalAlert) const GlobalAlert(pageKey: 'creative_create'), Expanded( child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), height: double.infinity, child: SingleChildScrollView( child: buildEditPanel(context, customColors), ), ), ), ], ), ), ), ); } void openDefaultTutorials({Function? onConfirm}) { showBeautyDialog( context, type: QuickAlertType.info, text: ' ${widget.note!}', onConfirmBtnTap: () async { onConfirm?.call(); context.pop(); }, showCancelBtn: true, confirmBtnText: AppLocale.gotIt.getString(context), ); } Widget buildEditPanel(BuildContext context, CustomColors customColors) { return SafeArea( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ColumnBlock( innerPanding: 10, padding: const EdgeInsets.only(top: 15, left: 15, right: 15, bottom: 0), children: [ if (capacity != null && capacity!.artisticTextStyles.isNotEmpty) ArtisticStyleSelector( styles: capacity!.artisticTextStyles, onSelected: (style) { setState(() { selectedStyle = style; }); }, selectedStyle: selectedStyle, ), ], ), ColumnBlock( innerPanding: 10, padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 15), children: [ EnhancedTextField( labelPosition: LabelPosition.top, labelText: '文字内容', customColors: customColors, controller: textController, textAlignVertical: TextAlignVertical.top, hintText: '要在画面中绘制的文字。', maxLength: 20, maxLines: 3, minLines: 1, showCounter: false, ), // 生成内容 ...buildPromptField(customColors), ], ), if (showAdvancedOptions) ColumnBlock( innerPanding: 10, padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 15), children: [ if (capacity != null && capacity!.artisticTextFonts.isNotEmpty) ArtisticStyleSelector( title: '文字字体', styles: capacity!.artisticTextFonts, onSelected: (style) { setState(() { selectedFonts = style; }); }, selectedStyle: selectedFonts, ), // 图片数量 EnhancedInput( title: Text( AppLocale.imageCount.getString(context), style: TextStyle( color: customColors.textfieldLabelColor, fontSize: 16, ), ), value: Text(generationImageCount.toString()), onPressed: () { openListSelectDialog( context, [ SelectorItem(const Text('1', textAlign: TextAlign.center), 1), SelectorItem(const Text('2', textAlign: TextAlign.center), 2), SelectorItem(const Text('3', textAlign: TextAlign.center), 3), SelectorItem(const Text('4', textAlign: TextAlign.center), 4), ], (value) { setState(() { generationImageCount = value.value; }); return true; }, heightFactor: 0.4, value: generationImageCount, ); }, ), ], ), // 生成按钮 AdvancedButton( showAdvancedOptions: showAdvancedOptions, onPressed: (value) { setState(() { showAdvancedOptions = value; }); }, ), if (capacity != null) const SizedBox(height: 10), EnhancedButton( title: AppLocale.generate.getString(context), onPressed: onGenerate, ), const SizedBox(height: 20), ], ), ); } List buildPromptField(CustomColors customColors) { return [ EnhancedTextField( labelPosition: LabelPosition.top, labelText: AppLocale.yourIdeas.getString(context), customColors: customColors, controller: promptController, textAlignVertical: TextAlignVertical.top, hintText: AppLocale.keywordsSeparatedByCommas.getString(context), maxLines: 10, minLines: 2, maxLength: 460, showCounter: false, inputSelector: IconButton( onPressed: () { openModalBottomSheet( context, (context) { return PromptTagsSelector( selectedTags: selectedTags, onSubmit: (tags) { setState(() { selectedTags = tags; }); context.pop(); }, ); }, heightFactor: 0.8, useSafeArea: true, ); }, icon: Icon( Icons.lightbulb_outline, color: customColors.linkColor, size: 16, ), ), middleWidget: Container( width: double.infinity, margin: const EdgeInsets.only(bottom: 30), child: Wrap( spacing: 3, runSpacing: 3, children: selectedTags .map( (e) => Tag( name: e.name, backgroundColor: customColors.linkColor, textColor: Colors.white, fontsize: 10, onDeleted: () { setState(() { selectedTags.remove(e); }); }, ), ) .toList(), ), ), bottomButton: Row( children: [ Icon( Icons.shuffle, size: 13, color: customColors.linkColor?.withAlpha(150), ), const SizedBox(width: 5), Text( AppLocale.random.getString(context), style: TextStyle( color: customColors.linkColor?.withAlpha(150), fontSize: 13, ), ), ], ), bottomButtonOnPressed: () async { final examples = await APIServer().exampleByTag('artistic-wordart'); if (examples.isEmpty) { return; } // 随机选取一个例子 final example = examples[Random().nextInt(examples.length)]; promptController.text = example.text; }, ), ]; } List selectedTags = []; void onGenerate() async { FocusScope.of(context).requestFocus(FocusNode()); HapticFeedbackHelper.mediumImpact(); final prompt = promptController.text.trim(); if (prompt.isEmpty) { showErrorMessage(AppLocale.contentIsRequired.getString(context)); return; } final text = textController.text.trim(); if (text.isEmpty) { showErrorMessage('文本内容不能为空'); return; } var params = { 'prompt': prompt, 'prompt_tags': selectedTags.map((e) => e.value).join(','), 'gallery_copy_id': widget.galleryCopyId, 'text': text, 'type': 'word_art', 'image_count': generationImageCount, 'style_preset': selectedStyle?.id, 'font_name': selectedFonts?.id, }; final cancel = BotToast.showCustomLoading( toastBuilder: (cancel) { return const LoadingIndicator( message: '思考中,请稍候...', ); }, allowClick: false, duration: const Duration(seconds: 15), ); request(int waitDuration) async { try { final taskId = await APIServer().creativeIslandArtisticTextCompletionsAsyncV2(params); stopPeriodQuery = false; cancel(); Navigator.push( // ignore: use_build_context_synchronously context, MaterialPageRoute( fullscreenDialog: true, builder: (context) => DrawResultPage( future: Future.delayed(const Duration(seconds: 10), () async { return await queryCompletionTaskStatus( taskId: taskId, retryTimes: 0, delaySeconds: 3, params: params, ); }), waitDuration: waitDuration, ), ), ).whenComplete(() { stopPeriodQuery = true; }); } catch (e) { stopPeriodQuery = true; cancel(); // ignore: use_build_context_synchronously showErrorMessageEnhanced(context, e); } } try { request(45); } catch (e) { cancel(); // ignore: use_build_context_synchronously showErrorMessageEnhanced(context, e); } } Future queryCompletionTaskStatus({ required String taskId, required int retryTimes, required int delaySeconds, Map? params, }) async { if (retryTimes > 60) { return Future.error(AppLocale.generateTimeout.getString(context)); } final resp = await APIServer().asyncTaskStatus(taskId); switch (resp.status) { case 'success': if (params != null && resp.originImage != null && resp.originImage != '') { params['image'] = resp.originImage; } return IslandResult( result: resp.resources ?? const [], params: params, ); case 'failed': return Future.error(resp.errors!.join(";")); default: if (stopPeriodQuery) { // ignore: use_build_context_synchronously return Future.error(AppLocale.generateTimeout.getString(context)); } return await Future.delayed(Duration(seconds: delaySeconds), () async { return await queryCompletionTaskStatus( taskId: taskId, retryTimes: retryTimes + 1, delaySeconds: 3, params: params, ); }); } } } ================================================ FILE: lib/page/creative_island/draw/components/artistic_style_selector.dart ================================================ import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/enhanced_input.dart'; import 'package:askaide/page/component/image.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api/creative.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; class ArtisticStyleSelector extends StatelessWidget { final List styles; final Function(CreativeIslandArtisticStyle style) onSelected; final CreativeIslandArtisticStyle? selectedStyle; final String? title; const ArtisticStyleSelector({ super.key, required this.styles, required this.onSelected, this.selectedStyle, this.title, }); @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return EnhancedInput( title: Text( title ?? AppLocale.style.getString(context), style: TextStyle( color: customColors.textfieldLabelColor, fontSize: 16, ), ), value: Row( mainAxisAlignment: MainAxisAlignment.end, mainAxisSize: MainAxisSize.min, children: [ // Text(selectedStyle == null || selectedStyle!.name == '' // ? AppLocale.auto.getString(context) // : selectedStyle!.name), // const SizedBox(width: 10), _buildImageStyleItemPreview( customColors, selectedStyle == null ? CreativeIslandArtisticStyle(id: '', name: '', previewImage: '') : selectedStyle!, size: 50, ), ], ), onPressed: () { openModalBottomSheet( context, (context) { return GridView.count( crossAxisCount: 3, crossAxisSpacing: 20, mainAxisSpacing: 20, padding: const EdgeInsets.only(top: 20, bottom: 20), children: [ for (var item in [CreativeIslandArtisticStyle(id: '', name: '自动', previewImage: ''), ...styles]) InkWell( onTap: () { onSelected(item); Navigator.pop(context); }, child: Column( children: [ Expanded( child: AspectRatio( aspectRatio: 1, child: _buildImageStyleItemPreview( customColors, item, showSelected: true, ), ), ), const SizedBox(height: 10), Text( item.name, style: const TextStyle(fontSize: 12), ), ], ), ), ], ); }, heightFactor: 0.8, ); }, ); } Widget _buildImageStyleItemPreview( CustomColors customColors, CreativeIslandArtisticStyle style, { double? size, bool showSelected = false, }) { return Container( width: size, height: size, decoration: BoxDecoration( borderRadius: CustomSize.borderRadius, border: showSelected && (selectedStyle != null && style.id == selectedStyle!.id) ? Border.all( color: customColors.linkColor ?? Colors.green, width: 1, ) : null, image: style.previewImage != null && style.previewImage != '' ? DecorationImage( image: CachedNetworkImageProviderEnhanced(style.previewImage!), fit: BoxFit.cover, ) : null, ), child: style.previewImage == '' ? const Center( child: Icon( Icons.interests, color: Colors.grey, size: 40, ), ) : null); } } ================================================ FILE: lib/page/creative_island/draw/components/box.dart ================================================ import 'dart:ui'; import 'package:askaide/helper/color.dart'; import 'package:askaide/helper/haptic_feedback.dart'; import 'package:askaide/page/component/image.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/repo/api/creative.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; /// 创作岛列表项目 class CreativeIslandBox extends StatelessWidget { final CreativeIslandItem item; final Color? backgroundColor; const CreativeIslandBox({super.key, required this.item, this.backgroundColor}); @override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), child: Stack( children: [ Container( // width: MediaQuery.of(context).size.width / 2 - 20, // height: 80, decoration: BoxDecoration( color: backgroundColor, borderRadius: CustomSize.borderRadius, image: item.bgImage != null ? DecorationImage( image: CachedNetworkImageProviderEnhanced(item.bgImage!), fit: BoxFit.cover, ) : null, ), child: Material( color: Colors.transparent, child: InkWell( borderRadius: CustomSize.borderRadiusAll, onTap: () { HapticFeedbackHelper.lightImpact(); context.push('/creative-island/${item.id}/create'); }, child: item.bgImage != null ? Column( mainAxisAlignment: MainAxisAlignment.end, children: [ Container( decoration: BoxDecoration( color: Colors.white.withAlpha(60), borderRadius: const BorderRadius.only( bottomLeft: CustomSize.radius, bottomRight: CustomSize.radius), ), child: ClipRect( child: BackdropFilter( filter: ImageFilter.blur( sigmaX: 1.0, sigmaY: 1.0, ), child: Center( child: Container( padding: const EdgeInsets.all(8), child: Text( item.title, style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: item.titleColor != null ? stringToColor(item.titleColor!) : Colors.white, shadows: [ Shadow( color: const Color.fromARGB(255, 161, 161, 161).withOpacity(0.5), offset: const Offset(2, 2), blurRadius: 5, ), ], ), ), ), ), ), ), ), ], ) : Center( child: Text( item.title, style: TextStyle( color: item.titleColor != null ? stringToColor(item.titleColor!) : Theme.of(context).textTheme.bodyMedium!.color, ), ), ), ), ), ), if (item.label != null) Positioned( top: 0, right: 0, child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( borderRadius: const BorderRadius.only(topRight: CustomSize.radius, bottomLeft: CustomSize.radius), color: item.labelColor != null ? stringToColor(item.labelColor!) : const Color.fromARGB(255, 230, 173, 58), ), child: Text( item.label!, style: const TextStyle( fontSize: 10, color: Colors.white, ), ), ), ), ], ), ); } } ================================================ FILE: lib/page/creative_island/draw/components/content_preview.dart ================================================ import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/image.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/image_preview.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/video_player.dart'; import 'package:askaide/repo/api/creative.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:intl/intl.dart'; class CreativeIslandContentPreview extends StatefulWidget { final CustomColors customColors; final CreativeItemInServer? item; final String? prompt; final IslandResult result; const CreativeIslandContentPreview({ super.key, required this.customColors, this.item, this.prompt, required this.result, }); @override State createState() => _CreativeIslandContentPreviewState(); } class _CreativeIslandContentPreviewState extends State { var currentTime = DateTime.now().add(const Duration(days: 7)); @override Widget build(BuildContext context) { var customColors = Theme.of(context).extension()!; final expireTime = widget.item != null ? DateFormat('y-MM-dd').format(widget.item!.createdAt!.add(const Duration(days: 7)).toLocal()) : DateFormat('y-MM-dd').format(currentTime); return widget.result.text == '' ? const Center( child: Text('生成结果将在这里展示'), ) : SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Container( margin: const EdgeInsets.only( top: 5, bottom: 5, left: 10, ), child: Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon( Icons.info_outline, size: 14, color: customColors.weakTextColor, ), const SizedBox(width: 5), Text( '${AppLocale.clickToShareWithExpire.getString(context)} $expireTime', maxLines: 2, style: TextStyle( fontSize: 12, color: customColors.weakTextColor, ), ), ], ), ), ...(widget.result.result .map( (e) => Padding( padding: const EdgeInsets.symmetric( vertical: 5, horizontal: 10, ), child: (widget.item != null && (widget.item!.isVideoType)) || e.endsWith('.mp4') ? _buildVideoPreviewer( widget.result.params ?? {}, e, ) : _buildImagePreviewer( widget.result.params ?? {}, e, ), ), ) .toList()), ], ), ); } Widget _buildVideoPreviewer( Map params, String e, ) { int? width = params['width'] == null ? null : params['width'] as int; int? height = params['height'] == null ? null : params['height'] as int; return VideoPlayer( url: e, width: width, height: height, ); } Widget _buildImagePreviewer( Map params, String e, ) { return NetworkImagePreviewer( url: e, preview: imageURL(e, qiniuImageTypeThumb), original: params['image'] == null || params['image'] == '' ? null : params['image'] as String, description: widget.prompt ?? widget.item?.prompt ?? '', ); } } class IslandResult { final List result; final Map? params; bool hasImageParam() { return params != null && params!['image'] != null && params!['image'] != ''; } String get text { return result.map((e) => '![image]($e)').join("\n\n"); } IslandResult({ required this.result, this.params, }); } ================================================ FILE: lib/page/creative_island/draw/components/creative_item.dart ================================================ import 'package:askaide/page/component/image.dart'; import 'package:askaide/page/component/prompt_tags_selector.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; class CreativeItem extends StatelessWidget { final String imageURL; final String title; final Color? titleColor; final String? tag; final Function() onTap; final String size; const CreativeItem({ super.key, required this.imageURL, required this.title, required this.onTap, this.titleColor, this.tag, required this.size, }); @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return Material( borderRadius: CustomSize.borderRadius, child: InkWell( borderRadius: CustomSize.borderRadiusAll, onTap: onTap, child: Stack( children: [ SizedBox( width: double.infinity, height: double.infinity, child: ClipRRect( borderRadius: CustomSize.borderRadius, child: CachedNetworkImageEnhanced( imageUrl: imageURL, fit: BoxFit.cover, ), ), ), if (size == 'large') Positioned( left: 20, top: 20, child: Row( children: [ Text( title, style: TextStyle( color: titleColor ?? Colors.white, fontSize: 30, ), ), const SizedBox(width: 10), if (tag != null && tag != '') Tag( name: tag!, backgroundColor: customColors.linkColor, fontsize: 10, ), ], ), ) else if (size == 'medium') Positioned( left: 20, top: 25, child: Row( children: [ Text( title, style: TextStyle( color: titleColor ?? Colors.white, fontSize: 20, ), ), const SizedBox(width: 10), if (tag != null && tag != '') ScaleTransition( scale: const AlwaysStoppedAnimation(0.5), child: Tag( name: tag!, backgroundColor: customColors.linkColor, fontsize: 10, ), ), ], ), ) else Center( child: Row( mainAxisSize: MainAxisSize.min, children: [ if (tag != null && tag != '') const SizedBox(width: 20), Text( title, style: TextStyle( color: titleColor ?? Colors.white, fontSize: 20, ), ), if (tag != null && tag != '') ScaleTransition( scale: const AlwaysStoppedAnimation(0.5), child: Tag( name: tag!, backgroundColor: customColors.linkColor, fontsize: 10, ), ), ], ), ), ], ), ), ); } } ================================================ FILE: lib/page/creative_island/draw/components/image_selector.dart ================================================ import 'dart:typed_data'; import 'package:askaide/helper/haptic_feedback.dart'; import 'package:askaide/helper/platform.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/image.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; class ImageSelector extends StatelessWidget { final String? title; final Widget? titleHelper; final Function({String? path, Uint8List? data}) onImageSelected; final String? selectedImagePath; final Uint8List? selectedImageData; final double? height; const ImageSelector({ super.key, this.title, required this.onImageSelected, this.selectedImagePath, this.selectedImageData, this.height, this.titleHelper, }); @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (title != null) Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Text( title!, style: TextStyle( fontSize: 16, color: customColors.textfieldLabelColor, ), ), const SizedBox(width: 5), if (titleHelper != null) titleHelper!, ], ), ], ), if (title != null) const SizedBox(height: 10), Material( borderRadius: CustomSize.borderRadius, color: customColors.backgroundColor, child: InkWell( borderRadius: CustomSize.borderRadiusAll, onTap: () async { HapticFeedbackHelper.mediumImpact(); FilePickerResult? result = await FilePicker.platform.pickFiles(type: FileType.image); if (result != null && result.files.isNotEmpty) { if (PlatformTool.isWeb()) { onImageSelected(data: result.files.first.bytes!); } else { onImageSelected(path: result.files.first.path!); } } }, child: Container( decoration: BoxDecoration(borderRadius: CustomSize.borderRadius), child: ClipRRect( borderRadius: CustomSize.borderRadius, child: Stack( children: [ Container( decoration: (selectedImagePath != null && selectedImagePath!.isNotEmpty) || (selectedImageData != null && selectedImageData!.isNotEmpty) ? BoxDecoration( image: DecorationImage( image: (selectedImagePath != null ? resolveImageProvider(selectedImagePath!) : (selectedImageData != null ? MemoryImage(selectedImageData!) : null))!, fit: BoxFit.cover, ), color: customColors.backgroundContainerColor?.withAlpha(100), borderRadius: CustomSize.borderRadius, ) : null, child: SizedBox( width: double.infinity, height: height ?? 200, ), ), selectedImagePath == null || selectedImagePath!.isEmpty ? SizedBox( height: height ?? 200, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.camera_alt, size: 30, color: customColors.chatInputPanelText, ), const SizedBox(width: 10), Text( AppLocale.selectImage.getString(context), style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: customColors.chatInputPanelText?.withOpacity(0.8), ), ), ], ), ) : Positioned( bottom: 0, left: 0, right: 0, child: Container( color: const Color.fromARGB(80, 255, 255, 255), height: 50, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.camera_alt, size: 30, color: Color.fromARGB(147, 255, 255, 255), ), const SizedBox(width: 10), Text( AppLocale.clickSwitchImage.getString(context), style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: Color.fromARGB(147, 255, 255, 255), ), ), ], ), ), ) ], ), )), ), ), ], ); } } ================================================ FILE: lib/page/creative_island/draw/components/image_size.dart ================================================ import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; class ImageSize extends StatelessWidget { final String aspectRatio; const ImageSize({super.key, required this.aspectRatio}); @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; final widthFactor = int.parse(aspectRatio.split(':')[0]); final heightFactor = int.parse(aspectRatio.split(':')[1]); var width = 0.0; var height = 0.0; if (widthFactor > heightFactor) { width = 40; height = 40 / widthFactor * heightFactor; } else { height = 40; width = 40 / heightFactor * widthFactor; } return Container( width: width, height: height, decoration: BoxDecoration( borderRadius: CustomSize.borderRadius, color: customColors.backgroundContainerColor, ), alignment: Alignment.center, child: Text( aspectRatio, style: const TextStyle(fontSize: 12, color: Colors.white), textAlign: TextAlign.center, ), ); } } ================================================ FILE: lib/page/creative_island/draw/components/image_style_selector.dart ================================================ import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/enhanced_input.dart'; import 'package:askaide/page/component/image.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api/creative.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; class ImageStyleSelector extends StatelessWidget { final List styles; final Function(CreativeIslandImageFilter style) onSelected; final CreativeIslandImageFilter? selectedStyle; const ImageStyleSelector({ super.key, required this.styles, required this.onSelected, this.selectedStyle, }); @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return EnhancedInput( title: Text( AppLocale.style.getString(context), style: TextStyle( color: customColors.textfieldLabelColor, fontSize: 16, ), ), value: Row( mainAxisAlignment: MainAxisAlignment.end, mainAxisSize: MainAxisSize.min, children: [ // Text(selectedStyle == null || selectedStyle!.name == '' // ? AppLocale.auto.getString(context) // : selectedStyle!.name), // const SizedBox(width: 10), _buildImageStyleItemPreview( customColors, selectedStyle == null ? CreativeIslandImageFilter(id: 0, name: '', previewImage: '') : selectedStyle!, size: 50, ), ], ), onPressed: () { openModalBottomSheet( context, (context) { return GridView.count( crossAxisCount: 3, crossAxisSpacing: 20, mainAxisSpacing: 20, padding: const EdgeInsets.only(top: 20, bottom: 20), children: [ for (var item in [CreativeIslandImageFilter(id: 0, name: '自动', previewImage: ''), ...styles]) InkWell( onTap: () { onSelected(item); Navigator.pop(context); }, child: Column( children: [ Expanded( child: AspectRatio( aspectRatio: 1, child: _buildImageStyleItemPreview( customColors, item, showSelected: true, ), ), ), const SizedBox(height: 10), Text( item.name, style: const TextStyle(fontSize: 12), ), ], ), ), ], ); }, heightFactor: 0.8, ); }, ); } Widget _buildImageStyleItemPreview( CustomColors customColors, CreativeIslandImageFilter style, { double? size, bool showSelected = false, }) { return Container( width: size, height: size, decoration: BoxDecoration( borderRadius: CustomSize.borderRadius, border: showSelected && (selectedStyle != null && style.id == selectedStyle!.id) ? Border.all( color: customColors.linkColor ?? Colors.green, width: 1, ) : null, image: style.previewImage != '' ? DecorationImage( image: CachedNetworkImageProviderEnhanced(style.previewImage), fit: BoxFit.cover, ) : null, ), child: style.previewImage == '' ? const Center( child: Icon( Icons.interests, color: Colors.grey, size: 40, ), ) : null); } } ================================================ FILE: lib/page/creative_island/draw/data/draw_history_datasource.dart ================================================ import 'package:askaide/helper/logger.dart'; import 'package:askaide/repo/api/creative.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:loading_more_list/loading_more_list.dart'; class DrawHistoryDatasource extends LoadingMoreBase { int pageindex = 1; bool _hasMore = true; bool forceRefresh = false; @override bool get hasMore => (_hasMore && length < 300) || forceRefresh; @override Future loadData([bool isloadMoreAction = false]) async { try { final resp = await APIServer().creativeHistories( mode: 'image-draw', page: pageindex, perPage: 30, ); if (pageindex == 1) { clear(); } for (var element in resp.data) { add(element); } if (resp.page == resp.lastPage) { _hasMore = false; } pageindex = resp.page + 1; return true; } catch (e) { Logger.instance.e(e); return false; } } @override Future refresh([bool notifyStateChanged = false]) async { _hasMore = true; pageindex = 1; //force to refresh list when you don't want clear list before request //for the case, if your list already has 20 items. forceRefresh = !notifyStateChanged; var result = await super.refresh(notifyStateChanged); forceRefresh = false; return result; } } ================================================ FILE: lib/page/creative_island/draw/draw_create.dart ================================================ import 'dart:math'; import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/cache.dart'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/haptic_feedback.dart'; import 'package:askaide/helper/upload.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/advanced_button.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/column_block.dart'; import 'package:askaide/page/component/enhanced_button.dart'; import 'package:askaide/page/component/enhanced_input.dart'; import 'package:askaide/page/component/enhanced_textfield.dart'; import 'package:askaide/page/component/global_alert.dart'; import 'package:askaide/page/component/item_selector_search.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/prompt_tags_selector.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/page/creative_island/draw/components/content_preview.dart'; import 'package:askaide/page/creative_island/draw/draw_result.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/creative_island/draw/components/image_selector.dart'; import 'package:askaide/page/creative_island/draw/components/image_size.dart'; import 'package:askaide/page/creative_island/draw/components/image_style_selector.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api/creative.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/model/misc.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; import 'package:quickalert/models/quickalert_type.dart'; class DrawCreateScreen extends StatefulWidget { final SettingRepository setting; final int? galleryCopyId; final String mode; final String id; final String? note; final String? initImage; const DrawCreateScreen({ super.key, required this.id, required this.setting, this.galleryCopyId, required this.mode, this.note, this.initImage, }); @override State createState() => _DrawCreateScreenState(); } class _DrawCreateScreenState extends State { String? selectedImagePath; Uint8List? selectedImageData; bool enableAIRewrite = false; int generationImageCount = 1; CreativeIslandVendorModel? selectedModel; String? upscaleBy; String selectedImageSize = '1:1'; bool showAdvancedOptions = false; CreativeIslandImageFilter? selectedStyle; double? imageStrength = 0.65; /// 是否停止周期性查询任务执行状态 var stopPeriodQuery = false; CreativeIslandCapacity? capacity; TextEditingController promptController = TextEditingController(); TextEditingController negativePromptController = TextEditingController(); TextEditingController seedController = TextEditingController(); /// 是否强制显示 negativePrompt bool forceShowNegativePrompt = false; @override void dispose() { promptController.dispose(); negativePromptController.dispose(); seedController.dispose(); super.dispose(); } @override void initState() { if (widget.initImage != null) { selectedImagePath = widget.initImage; } APIServer().creativeIslandCapacity(mode: widget.mode, id: widget.id).then((cap) { setState(() { capacity = cap; }); if (widget.galleryCopyId != null && widget.galleryCopyId! > 0) { APIServer().creativeGalleryItem(id: widget.galleryCopyId!).then((response) { final gallery = response.item; if (gallery.prompt != null && gallery.prompt!.isNotEmpty) { promptController.text = gallery.prompt!; } if (gallery.negativePrompt != null && gallery.negativePrompt!.isNotEmpty) { if (gallery.negativePrompt != null && gallery.negativePrompt!.isNotEmpty) { forceShowNegativePrompt = true; } negativePromptController.text = gallery.negativePrompt!; } if (gallery.metaMap['model_id'] != null && gallery.metaMap['model_id'] != '') { final matchedModels = capacity!.vendorModels .where((e) => e.id == gallery.metaMap['model_id'] || e.id == 'model-${gallery.metaMap['model_id']}'); if (matchedModels.isNotEmpty) { selectedModel = matchedModels.first; } } if (gallery.metaMap['image_ratio'] != null && gallery.metaMap['image_ratio'] != '') { selectedImageSize = gallery.metaMap['image_ratio']!; } if (gallery.metaMap['filter_id'] != null && gallery.metaMap['filter_id'] > 0) { final matchedStyles = capacity!.filters.where((e) => e.id == gallery.metaMap['filter_id']); if (matchedStyles.isNotEmpty) { selectedStyle = matchedStyles.first; } } if (gallery.metaMap['real_prompt'] != null && gallery.metaMap['real_prompt'] != '') { promptController.text = gallery.metaMap['real_prompt']!; } if (gallery.metaMap['negative_prompt'] != null && gallery.metaMap['negative_prompt'] != '') { negativePromptController.text = gallery.metaMap['negative_prompt']!; } if (gallery.metaMap['real_negative_prompt'] != null && gallery.metaMap['real_negative_prompt'] != '') { negativePromptController.text = gallery.metaMap['real_negative_prompt']!; } // 创建同款时,默认关闭 AI 优化,除非该同款包含 ai_rewrite 的设定 enableAIRewrite = false; if ((gallery.metaMap['real_prompt'] == null || gallery.metaMap['real_prompt'] == '') && gallery.metaMap['ai_rewrite'] != null && gallery.metaMap['ai_rewrite']) { enableAIRewrite = gallery.metaMap['ai_rewrite']; } setState(() {}); }); } }); if (widget.note != null) { Cache().boolGet(key: 'creative:tutorials:${widget.mode}:dialog').then((show) { if (!show) { return; } openDefaultTutorials(onConfirm: () { Cache().setBool( key: 'creative:tutorials:${widget.mode}:dialog', value: false, duration: const Duration(days: 30), ); }); }); } super.initState(); } void openDefaultTutorials({Function? onConfirm}) { showBeautyDialog( context, type: QuickAlertType.info, text: ' ${widget.note!}', onConfirmBtnTap: () async { onConfirm?.call(); context.pop(); }, showCancelBtn: true, confirmBtnText: AppLocale.gotIt.getString(context), ); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( title: Text( widget.mode == 'image-to-image' ? AppLocale.imageToImage.getString(context) : AppLocale.textToImage.getString(context), style: const TextStyle(fontSize: CustomSize.appBarTitleSize), ), centerTitle: true, leading: IconButton( onPressed: () { context.pop(); }, icon: const Icon(Icons.arrow_back_ios), ), toolbarHeight: CustomSize.toolbarHeight, backgroundColor: customColors.backgroundColor, actions: [ if (widget.note != null) IconButton( onPressed: () { openDefaultTutorials(); }, icon: const Icon(Icons.help_outline), ) ], ), backgroundColor: customColors.backgroundColor, body: BackgroundContainer( setting: widget.setting, enabled: false, maxWidth: CustomSize.smallWindowSize, backgroundColor: customColors.backgroundColor, child: Column( children: [ if (Ability().showGlobalAlert) const GlobalAlert(pageKey: 'creative_create'), Expanded( child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), height: double.infinity, child: SingleChildScrollView( child: buildEditPanel(context, customColors), ), ), ), ], ), ), ), ); } Widget buildEditPanel(BuildContext context, CustomColors customColors) { return SafeArea( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ColumnBlock( innerPanding: 10, padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 15), children: [ // 上传图片 if (widget.mode == 'image-to-image') ImageSelector( onImageSelected: ({path, data}) { if (path != null) { setState(() { selectedImagePath = path; selectedImageData = null; }); } if (data != null) { setState(() { selectedImageData = data; selectedImagePath = null; }); } }, selectedImagePath: selectedImagePath, selectedImageData: selectedImageData, title: AppLocale.referenceImage.getString(context), height: _calImageSelectorHeight(context), titleHelper: InkWell( onTap: () { showBeautyDialog( context, type: QuickAlertType.info, text: AppLocale.referenceImageNote.getString(context), confirmBtnText: AppLocale.gotIt.getString(context), showCancelBtn: false, ); }, child: Icon( Icons.help_outline, size: 16, color: customColors.weakLinkColor?.withAlpha(150), ), ), ), // 图片风格 if (capacity != null && capacity!.showStyle && capacity!.filters.isNotEmpty) ImageStyleSelector( styles: capacity!.filters, onSelected: (style) { setState(() { selectedStyle = style; }); }, selectedStyle: selectedStyle, ), ], ), ColumnBlock( innerPanding: 10, padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 15), children: [ // 生成内容 if (widget.mode == 'text-to-image') ...buildPromptField(customColors), // AI 优化配置 if (capacity != null && capacity!.showAIRewrite && widget.mode != 'image-to-image') Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Text( AppLocale.smartOptimization.getString(context), style: const TextStyle(fontSize: 16), ), const SizedBox(width: 5), InkWell( onTap: () { showBeautyDialog( context, type: QuickAlertType.info, text: AppLocale.onceEnabledSmartOptimization.getString(context), confirmBtnText: AppLocale.gotIt.getString(context), showCancelBtn: false, ); }, child: Icon( Icons.help_outline, size: 16, color: customColors.weakLinkColor?.withAlpha(150), ), ), ], ), CupertinoSwitch( activeColor: customColors.linkColor, value: enableAIRewrite, onChanged: (value) { setState(() { enableAIRewrite = value; }); }, ), ], ), ], ), if (showAdvancedOptions) ColumnBlock( innerPanding: 10, padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 15), children: [ if (widget.mode == 'image-to-image' && capacity != null && capacity!.showPromptForImage2Image) ...buildPromptField(customColors), // 反向提示语 if ((capacity != null && capacity!.showNegativeText) || forceShowNegativePrompt) EnhancedTextField( labelPosition: LabelPosition.top, labelText: AppLocale.excludeContents.getString(context), customColors: customColors, controller: negativePromptController, textAlignVertical: TextAlignVertical.top, hintText: AppLocale.unwantedElements.getString(context), maxLength: 500, maxLines: 5, minLines: 3, showCounter: false, ), // 原图相似度 if (capacity != null && capacity!.showImageStrength && widget.mode == 'image-to-image') Row( children: [ Row( children: [ Text(AppLocale.imagination.getString(context)), const SizedBox(width: 5), InkWell( onTap: () { showBeautyDialog( context, type: QuickAlertType.info, text: '想象力\n\n提高想象力,得到更有创造力的内容。降低想象力,效果与参考图更相似。', confirmBtnText: AppLocale.gotIt.getString(context), showCancelBtn: false, ); }, child: Icon( Icons.help_outline, size: 16, color: customColors.weakLinkColor?.withAlpha(150), ), ), ], ), const SizedBox(width: 10), Expanded( child: Slider( value: imageStrength ?? 0.65, min: 0, max: 1, divisions: 20, label: imageStrengthText(), activeColor: customColors.linkColor, onChanged: (value) { setState(() { imageStrength = value; }); }, ), ), Text( ((imageStrength ?? 0) * 100).toStringAsFixed(0), style: TextStyle( fontSize: 12, color: customColors.weakTextColor, ), ), ], ), // 图片数量 if (capacity != null && capacity!.showImageCount && widget.mode != 'image-to-image') EnhancedInput( title: Text( AppLocale.imageCount.getString(context), style: TextStyle( color: customColors.textfieldLabelColor, fontSize: 16, ), ), value: Text(generationImageCount.toString()), onPressed: () { openListSelectDialog( context, [ SelectorItem(const Text('1', textAlign: TextAlign.center), 1), SelectorItem(const Text('2', textAlign: TextAlign.center), 2), SelectorItem(const Text('3', textAlign: TextAlign.center), 3), SelectorItem(const Text('4', textAlign: TextAlign.center), 4), ], (value) { setState(() { generationImageCount = value.value; }); return true; }, heightFactor: 0.4, value: generationImageCount, ); }, ), // 图片尺寸 if (capacity != null && capacity!.allowRatios.isNotEmpty && widget.mode != 'image-to-image') EnhancedInput( title: Text( AppLocale.imageSize.getString(context), style: TextStyle( color: customColors.textfieldLabelColor, fontSize: 16, ), ), value: ImageSize(aspectRatio: selectedImageSize), onPressed: () { openListSelectDialog( context, capacity!.allowRatios.map((e) => SelectorItem(ImageSize(aspectRatio: e), e)).toList(), (value) { setState(() { selectedImageSize = value.value; }); return true; }, value: selectedImageSize, heightFactor: 0.3, horizontal: true, horizontalCount: capacity!.allowRatios.length > 3 ? 4 : capacity!.allowRatios.length, ); }, ), ], ), if (showAdvancedOptions) ColumnBlock( padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 15), children: [ // 模型 if (capacity != null && capacity!.vendorModels.isNotEmpty) EnhancedInput( title: Text( AppLocale.model.getString(context), style: TextStyle( color: customColors.textfieldLabelColor, fontSize: 16, ), ), value: Container( alignment: Alignment.centerRight, width: MediaQuery.of(context).size.width - 200, child: Text( selectedModel?.name ?? '自动', overflow: TextOverflow.ellipsis, ), ), onPressed: () { openListSelectDialog( context, [ SelectorItem(const Text('自动'), null), ...capacity!.vendorModels .map((e) => SelectorItem( Stack( children: [ Container( padding: const EdgeInsets.only(top: 25, bottom: 10), alignment: Alignment.center, child: Text( e.name, textAlign: TextAlign.center, style: const TextStyle(fontSize: 14), textWidthBasis: TextWidthBasis.longestLine, ), ), if (e.vendor != null && e.vendor!.isNotEmpty) Positioned( left: 0, top: 0, child: Container( padding: const EdgeInsets.symmetric( horizontal: 5, vertical: 3, ), decoration: BoxDecoration( borderRadius: CustomSize.borderRadius, color: modelTypeTagColors[e.vendor!], ), child: Text( e.vendor!, style: const TextStyle( fontSize: 12, color: Colors.white, ), ), ), ), ], ), e.id, search: (keywrod) { return e.name.contains(keywrod) || (e.vendor != null && e.vendor!.contains(keywrod)); }, )) .toList(), ], (value) { setState(() { if (value.value == null) { selectedModel = null; return; } selectedModel = capacity!.vendorModels.firstWhere((e) => e.id == value.value); }); return true; }, heightFactor: 0.8, value: selectedModel?.id, enableSearch: true, innerPadding: const EdgeInsets.symmetric( horizontal: 10, vertical: 0, ), ); }, ), if (capacity != null && capacity!.showUpscaleBy && capacity!.allowUpscaleBy.isNotEmpty && (selectedModel?.upscale ?? false)) EnhancedInput( title: Text( 'Upscale', style: TextStyle( color: customColors.textfieldLabelColor, fontSize: 16, ), ), value: Text(upscaleBy ?? '自动'), onPressed: () { openListSelectDialog( context, [ SelectorItem(const Text('自动'), null), ...capacity!.allowUpscaleBy.map((e) => SelectorItem(Text(e), e)).toList(), ], (value) { setState(() { upscaleBy = value.value; }); return true; }, heightFactor: 0.5, value: upscaleBy, ); }, ), // Seed if (capacity != null && capacity!.showSeed) EnhancedTextField( controller: seedController, customColors: customColors, labelText: 'Seed', labelPosition: LabelPosition.left, showCounter: false, keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, ], hintText: '默认随机', textDirection: TextDirection.rtl, ), ], ), // 生成按钮 AdvancedButton( showAdvancedOptions: showAdvancedOptions, onPressed: (value) { setState(() { showAdvancedOptions = value; }); }, ), if (capacity != null) const SizedBox(height: 10), EnhancedButton( title: AppLocale.generate.getString(context), onPressed: onGenerate, ), const SizedBox(height: 20), ], ), ); } List buildPromptField(CustomColors customColors) { return [ EnhancedTextField( labelPosition: LabelPosition.top, labelText: AppLocale.yourIdeas.getString(context), customColors: customColors, controller: promptController, textAlignVertical: TextAlignVertical.top, hintText: AppLocale.keywordsSeparatedByCommas.getString(context), maxLines: 10, minLines: 2, maxLength: 460, showCounter: false, inputSelector: IconButton( onPressed: () { openModalBottomSheet( context, (context) { return PromptTagsSelector( selectedTags: selectedTags, onSubmit: (tags) { setState(() { selectedTags = tags; }); context.pop(); }, ); }, heightFactor: 0.8, useSafeArea: true, ); }, icon: Icon( Icons.lightbulb_outline, color: customColors.linkColor, size: 16, ), ), middleWidget: Container( width: double.infinity, margin: const EdgeInsets.only(bottom: 30), child: Wrap( spacing: 3, runSpacing: 3, children: selectedTags .map( (e) => Tag( name: e.name, backgroundColor: customColors.linkColor, textColor: Colors.white, fontsize: 10, onDeleted: () { setState(() { selectedTags.remove(e); }); }, ), ) .toList(), ), ), bottomButton: Row( children: [ Icon( Icons.shuffle, size: 13, color: customColors.linkColor?.withAlpha(150), ), const SizedBox(width: 5), Text( AppLocale.random.getString(context), style: TextStyle( color: customColors.linkColor?.withAlpha(150), fontSize: 13, ), ), ], ), bottomButtonOnPressed: () async { final examples = await APIServer().exampleByTag('image-generation'); if (examples.isEmpty) { return; } // 随机选取一个例子 final example = examples[Random().nextInt(examples.length)]; promptController.text = example.text; }, ), ]; } List selectedTags = []; void onGenerate() async { FocusScope.of(context).requestFocus(FocusNode()); HapticFeedbackHelper.mediumImpact(); final prompt = promptController.text.trim(); if (prompt.isEmpty && widget.mode == 'text-to-image') { showErrorMessage(AppLocale.contentIsRequired.getString(context)); return; } if (widget.mode == 'image-to-image' && selectedImagePath == null && selectedImageData == null) { showErrorMessage(AppLocale.selectReferenceImage.getString(context)); return; } final seed = int.tryParse(seedController.text); if (seed != null && (seed < 0 || seed > 2147483647)) { showErrorMessage('Seed 取值范围为 0 ~ 2147483647'); return; } var params = { 'prompt': prompt, 'negative_prompt': negativePromptController.text, 'prompt_tags': selectedTags.map((e) => e.value).join(','), 'filter_id': selectedStyle?.id, 'image_ratio': selectedImageSize, 'image_count': generationImageCount, 'ai_rewrite': enableAIRewrite, 'gallery_copy_id': widget.galleryCopyId, 'upscale_by': upscaleBy, 'model': selectedModel?.id, 'image_strength': imageStrength, 'seed': seed, }; if (selectedImagePath != null && selectedImagePath!.isNotEmpty) { params['image'] = 'https://${selectedImagePath ?? 'demo'}'; // 仅用于测试消耗量,正式上传后会被替换为 URL } if (selectedImageData != null && selectedImageData!.isNotEmpty) { params['image'] = "https://fake-image-url.com"; } final cancel = BotToast.showCustomLoading( toastBuilder: (cancel) { return const LoadingIndicator( message: '思考中,请稍候...', ); }, allowClick: false, duration: const Duration(seconds: 15), ); request(int waitDuration) async { try { cancel(); if (params['image'] != null && params['image'] != '') { final cancel = BotToast.showCustomLoading( toastBuilder: (cancel) { return LoadingIndicator( message: AppLocale.imageUploading.getString(context), ); }, allowClick: false, ); if (selectedImagePath != null && (selectedImagePath!.startsWith('http://') || selectedImagePath!.startsWith('https://'))) { params['image'] = selectedImagePath; cancel(); } else { if (selectedImagePath != null && selectedImagePath!.isNotEmpty) { final uploadRes = await ImageUploader(widget.setting).upload(selectedImagePath!).whenComplete(() => cancel()); params['image'] = uploadRes.url; } else if (selectedImageData != null && selectedImageData!.isNotEmpty) { final uploadRes = await ImageUploader(widget.setting).uploadData(selectedImageData!).whenComplete(() => cancel()); params['image'] = uploadRes.url; } } } final taskId = await APIServer().creativeIslandCompletionsAsyncV2(params); stopPeriodQuery = false; Navigator.push( // ignore: use_build_context_synchronously context, MaterialPageRoute( fullscreenDialog: true, builder: (context) => DrawResultPage( future: Future.delayed(const Duration(seconds: 10), () async { return await queryCompletionTaskStatus( taskId: taskId, retryTimes: 0, delaySeconds: 3, params: params, ); }), waitDuration: waitDuration, ), ), ).whenComplete(() { stopPeriodQuery = true; }); } catch (e) { stopPeriodQuery = true; cancel(); // ignore: use_build_context_synchronously showErrorMessageEnhanced(context, e); } } try { final res = await APIServer().creativeIslandCompletionsEvaluateV2(params); if (!res.enough) { if (context.mounted) { showBeautyDialog( // ignore: use_build_context_synchronously context, type: QuickAlertType.warning, // ignore: use_build_context_synchronously text: AppLocale.quotaExceeded.getString(context), confirmBtnText: '立即购买', showCancelBtn: true, onConfirmBtnTap: () { context.pop(); context.push('/payment'); }, ); } return; } if (res.cost > 0) { cancel(); openConfirmDialog( // ignore: use_build_context_synchronously context, '本次请求预计消耗 ${res.cost} 个智慧果,是否继续操作?', () => request(res.waitDuration ?? 60), ); } else { request(res.waitDuration ?? 60); } } catch (e) { cancel(); // ignore: use_build_context_synchronously showErrorMessageEnhanced(context, e); } } String imageStrengthText() { if (imageStrength == 0) { return '自动'; } if (imageStrength! >= 0.4 && imageStrength! <= 0.67) { return '适中'; } if (imageStrength! > 0.65 && imageStrength! < 0.9) { return '更有创造力'; } if (imageStrength! >= 0.9) { return '尽情发挥创造力'; } return '更接近参考图'; } Future queryCompletionTaskStatus({ required String taskId, required int retryTimes, required int delaySeconds, Map? params, }) async { if (retryTimes > 60) { return Future.error(AppLocale.generateTimeout.getString(context)); } final resp = await APIServer().asyncTaskStatus(taskId); switch (resp.status) { case 'success': if (params != null && resp.originImage != null && resp.originImage != '') { params['image'] = resp.originImage; } return IslandResult( result: resp.resources ?? const [], params: params, ); case 'failed': return Future.error(resp.errors!.join(";")); default: if (stopPeriodQuery) { // ignore: use_build_context_synchronously return Future.error(AppLocale.generateTimeout.getString(context)); } return await Future.delayed(Duration(seconds: delaySeconds), () async { return await queryCompletionTaskStatus( taskId: taskId, retryTimes: retryTimes + 1, delaySeconds: 3, params: params, ); }); } } double _calImageSelectorHeight(BuildContext context) { var width = MediaQuery.of(context).size.width; if (width > CustomSize.smallWindowSize) { width = CustomSize.smallWindowSize; } return width - 15 * 2 - 10 * 2 - 10; } } ================================================ FILE: lib/page/creative_island/draw/draw_list.dart ================================================ import 'package:askaide/bloc/creative_island_bloc.dart'; import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/color.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/sliver_component.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/page/creative_island/draw/components/creative_item.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; class DrawListScreen extends StatefulWidget { final SettingRepository setting; const DrawListScreen({super.key, required this.setting}); @override State createState() => _DrawListScreenState(); } class _DrawListScreenState extends State { @override void initState() { if (Ability().isUserLogon()) { userSignedIn = true; } context.read().add(CreativeIslandItemsV2LoadEvent(forceRefresh: false)); super.initState(); } bool userSignedIn = false; @override void dispose() { super.dispose(); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( backgroundColor: customColors.backgroundColor, body: _buildIslandItems(customColors), ), ); } /// 创作岛列表 Widget _buildIslandItems( CustomColors customColors, ) { return SliverComponent( title: Text( AppLocale.creativeIsland.getString(context), style: TextStyle( fontSize: CustomSize.appBarTitleSize, color: customColors.backgroundInvertedColor, ), ), actions: [ IconButton( onPressed: () { if (userSignedIn) { context.push('/creative-island/history?mode=image-draw'); } else { context.push('/login'); } }, icon: const Icon(Icons.crop_original), ), ], child: BackgroundContainer( setting: widget.setting, enabled: false, backgroundColor: customColors.backgroundColor, child: BlocBuilder( buildWhen: (previous, current) => current is CreativeIslandItemsV2Loaded, builder: (context, state) { if (state is CreativeIslandItemsV2Loaded) { final items = state.items .map((e) => CreativeItem( imageURL: e.previewImage, title: e.title, titleColor: stringToColor(e.titleColor), tag: e.tag, onTap: () { var uri = Uri.tryParse(e.routeUri); if (e.note != null && e.note != '') { uri = uri!.replace( queryParameters: { 'note': e.note!, }..addAll(uri.queryParameters)); } context.push(uri.toString()); }, size: e.size, )) .toList(); final largeItems = items.where((e) => e.size == 'large').toList(); final mediumItems = items.where((e) => e.size == 'medium').toList(); final otherItems = items.where((e) => e.size != 'large' && e.size != 'medium').toList(); return RefreshIndicator( onRefresh: () async { context.read().add(CreativeIslandItemsV2LoadEvent(forceRefresh: true)); }, color: customColors.linkColor, displacement: 20, child: SingleChildScrollView( child: Column( children: [ GridView.count( physics: const NeverScrollableScrollPhysics(), padding: const EdgeInsets.only(top: 0, left: 10, right: 10), crossAxisCount: _calCrossAxisCount(context), childAspectRatio: 2, shrinkWrap: true, children: largeItems .map((e) => Container( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 10), child: e, )) .toList(), ), GridView.count( physics: const NeverScrollableScrollPhysics(), padding: const EdgeInsets.only(top: 5, left: 10, right: 10), crossAxisCount: _calCrossAxisCount(context) * 2, childAspectRatio: 1, shrinkWrap: true, children: mediumItems .map((e) => Container( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 5), child: e, )) .toList(), ), GridView.count( physics: const NeverScrollableScrollPhysics(), padding: const EdgeInsets.only(top: 5, left: 10, right: 10), crossAxisCount: _calCrossAxisCount(context) * 2, childAspectRatio: 2, shrinkWrap: true, children: otherItems .map((e) => Container( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 5), child: e, )) .toList(), ), ], ), ), ); } return const Center( child: CircularProgressIndicator(), ); }, ), ), ); } int _calCrossAxisCount(BuildContext context) { var width = MediaQuery.of(context).size.width; if (width > CustomSize.maxWindowSize) { width = CustomSize.maxWindowSize; } return (width / 400).round() > 2 ? 2 : (width / 400).round(); } } ================================================ FILE: lib/page/creative_island/draw/draw_result.dart ================================================ import 'package:askaide/helper/haptic_feedback.dart'; import 'package:askaide/helper/helper.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/creative_island/draw/components/content_preview.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:circular_countdown_timer/circular_countdown_timer.dart'; class DrawResultPage extends StatefulWidget { final Future future; final int waitDuration; const DrawResultPage({ super.key, required this.future, this.waitDuration = 30, }); @override State createState() => _DrawResultPageState(); } const defaultCounterRestartValue = 15; class _DrawResultPageState extends State { var loading = true; var restartCounterValue = defaultCounterRestartValue; CountDownController controller = CountDownController(); @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return Container( color: customColors.backgroundColor, child: Align( alignment: Alignment.topCenter, child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: CustomSize.smallWindowSize), child: Scaffold( appBar: AppBar( title: Text( AppLocale.generateResult.getString(context), style: const TextStyle(fontSize: CustomSize.appBarTitleSize), ), toolbarHeight: CustomSize.toolbarHeight, centerTitle: true, leading: IconButton( icon: const Icon(Icons.close), onPressed: () { if (loading) { openConfirmDialog( context, AppLocale.generateExitConfirm.getString(context), () => Navigator.pop(context), ); } else { Navigator.pop(context); } }, ), ), backgroundColor: customColors.backgroundColor, body: FutureBuilder( future: widget.future, builder: (context, snapshot) { if (snapshot.hasData || snapshot.hasError) { HapticFeedbackHelper.mediumImpact(); loading = false; } if (snapshot.hasError) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.error_outline, size: 50, color: Colors.red, ), const SizedBox(height: 10), const Text( '创作失败', style: TextStyle(color: Colors.red), ), const SizedBox(height: 10), Padding( padding: const EdgeInsets.symmetric(horizontal: 15), child: Text( resolveError(context, snapshot.error!), style: TextStyle( color: customColors.weakTextColor, fontSize: 10, ), ), ), ], ), ); } if (snapshot.hasData) { return Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ Expanded( child: CreativeIslandContentPreview( result: snapshot.data!, customColors: customColors, ), ), const SizedBox(height: 30), ], ); } return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ CountDownProgressBar( duration: widget.waitDuration, controller: controller, onComplete: (controller) { if (!loading) { return; } if (restartCounterValue == defaultCounterRestartValue) { showSuccessMessage('当前排队人数较多,还需要等待一下哦'); } controller.restart(duration: restartCounterValue); setState(() { restartCounterValue += 1; }); }, ), const SizedBox(height: 10), Text( '正在打造神奇...', style: TextStyle( color: customColors.backgroundInvertedColor, ), ), const SizedBox(height: 5), Text( '如队列太长,将会有数分钟等待时间', style: TextStyle( color: customColors.backgroundInvertedColor?.withAlpha(150), fontSize: 10, ), ) ], ), ); }, ), ), ), ), ); } } class CountDownProgressBar extends StatefulWidget { final int duration; final CountDownController controller; final Function(CountDownController controller)? onComplete; const CountDownProgressBar({ super.key, required this.duration, required this.controller, this.onComplete, }); @override State createState() => _CountDownProgressBarState(); } class _CountDownProgressBarState extends State { @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return CircularCountDownTimer( controller: widget.controller, duration: widget.duration, initialDuration: 0, width: MediaQuery.of(context).size.width / 3, height: MediaQuery.of(context).size.height / 3, ringColor: Colors.grey[300]!, fillColor: customColors.linkColor!, strokeWidth: 10.0, strokeCap: StrokeCap.round, textStyle: const TextStyle(fontSize: 33.0, fontWeight: FontWeight.bold), textFormat: CountdownTextFormat.S, isReverse: true, isReverseAnimation: true, isTimerTextShown: true, autoStart: true, onComplete: () { widget.onComplete?.call(widget.controller); }, ); } } ================================================ FILE: lib/page/creative_island/draw/image_edit_direct.dart ================================================ import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/cache.dart'; import 'package:askaide/helper/haptic_feedback.dart'; import 'package:askaide/helper/upload.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/advanced_button.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/column_block.dart'; import 'package:askaide/page/component/enhanced_button.dart'; import 'package:askaide/page/component/enhanced_textfield.dart'; import 'package:askaide/page/component/global_alert.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/page/creative_island/draw/components/content_preview.dart'; import 'package:askaide/page/creative_island/draw/draw_result.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/creative_island/draw/components/image_selector.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; import 'package:quickalert/quickalert.dart'; class ImageEditDirectScreen extends StatefulWidget { final SettingRepository setting; final String title; final String apiEndpoint; final String? note; final int initWaitDuration; final String? initImage; const ImageEditDirectScreen({ super.key, required this.setting, required this.title, required this.apiEndpoint, this.note, this.initWaitDuration = 30, this.initImage, }); @override State createState() => _ImageEditDirectScreenState(); } class _ImageEditDirectScreenState extends State { String? selectedImagePath; Uint8List? selectedImageData; TextEditingController seedController = TextEditingController(); double? cfgScale = 0.0; int? motionBucketId = 0; bool showAdvancedOptions = false; /// 是否停止周期性查询任务执行状态 var stopPeriodQuery = false; @override void initState() { if (widget.initImage != null && widget.initImage!.isNotEmpty) { selectedImagePath = widget.initImage; } if (widget.note != null) { if (widget.apiEndpoint == 'image-to-video') { Cache().boolGet(key: 'creative:tutorials:${widget.apiEndpoint}:dialog').then((show) { if (!show) { return; } openImageToVideoTutorials(onConfirm: () { Cache().setBool( key: 'creative:tutorials:${widget.apiEndpoint}:dialog', value: false, duration: const Duration(days: 30), ); }); }); } else { Cache().boolGet(key: 'creative:tutorials:${widget.apiEndpoint}:dialog').then((show) { if (!show) { return; } openDefaultTutorials(onConfirm: () { Cache().setBool( key: 'creative:tutorials:${widget.apiEndpoint}:dialog', value: false, duration: const Duration(days: 30), ); }); }); } } super.initState(); } void openDefaultTutorials({Function? onConfirm}) { showBeautyDialog( context, type: QuickAlertType.info, text: ' ${widget.note!}', onConfirmBtnTap: () async { onConfirm?.call(); context.pop(); }, showCancelBtn: true, confirmBtnText: AppLocale.gotIt.getString(context), ); } void openImageToVideoTutorials({Function? onConfirm}) { showBeautyDialog( context, type: QuickAlertType.custom, widget: Text(' ${widget.note!}'), customAsset: 'assets/text-to-video.gif', onConfirmBtnTap: () async { onConfirm?.call(); context.pop(); }, showCancelBtn: true, confirmBtnText: AppLocale.gotIt.getString(context), ); } @override void dispose() { seedController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( title: Text( widget.title, style: const TextStyle(fontSize: CustomSize.appBarTitleSize), ), centerTitle: true, leading: IconButton( onPressed: () { context.pop(); }, icon: const Icon(Icons.arrow_back_ios), ), toolbarHeight: CustomSize.toolbarHeight, backgroundColor: customColors.backgroundColor, actions: [ if (widget.note != null && widget.apiEndpoint == 'image-to-video') IconButton( onPressed: () { openImageToVideoTutorials(); }, icon: const Icon(Icons.help_outline), ) else if (widget.note != null) IconButton( onPressed: () { openDefaultTutorials(); }, icon: const Icon(Icons.help_outline), ), ], ), backgroundColor: customColors.backgroundColor, body: BackgroundContainer( setting: widget.setting, enabled: false, maxWidth: CustomSize.smallWindowSize, backgroundColor: customColors.backgroundColor, child: Column( children: [ if (Ability().showGlobalAlert) const GlobalAlert(pageKey: 'creative_create'), Expanded( child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), height: double.infinity, child: SingleChildScrollView( child: buildEditPanel(context, customColors), ), ), ), ], ), ), ), ); } Widget buildEditPanel(BuildContext context, CustomColors customColors) { return SafeArea( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ColumnBlock( innerPanding: 10, padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 15), children: [ // 上传图片 ImageSelector( onImageSelected: ({path, data}) { if (path != null) { setState(() { selectedImagePath = path; selectedImageData = null; }); } if (data != null) { setState(() { selectedImageData = data; selectedImagePath = null; }); } }, selectedImagePath: selectedImagePath, selectedImageData: selectedImageData, title: AppLocale.originalImage.getString(context), height: _calImageSelectorHeight(context), ), ], ), if (showAdvancedOptions) ColumnBlock( innerPanding: 10, padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 15), children: [ // Cfg Scale Column( children: [ Row( children: [ const Text('Cfg Scale'), const SizedBox(width: 5), InkWell( onTap: () { showBeautyDialog( context, type: QuickAlertType.info, text: 'How strongly the video sticks to the original image. \nUse lower values to allow the model more freedom to make changes and higher values to correct motion distortions', confirmBtnText: AppLocale.gotIt.getString(context), showCancelBtn: false, ); }, child: Icon( Icons.help_outline, size: 16, color: customColors.weakLinkColor?.withAlpha(150), ), ), ], ), Row( children: [ Expanded( child: Slider( value: cfgScale ?? 0.0, min: 0, max: 10, divisions: 20, label: cfgScaleText(cfgScale), activeColor: customColors.linkColor, onChanged: (value) { setState(() { if (value > 0 && value < 1) { value = 1; } cfgScale = value; }); }, ), ), Text( cfgScaleText(cfgScale), style: TextStyle( fontSize: 12, color: customColors.weakTextColor, ), ), ], ) ], ), // Motion Bucket ID Column( mainAxisSize: MainAxisSize.min, children: [ Row( children: [ const Text('Motion Bucket ID'), const SizedBox(width: 5), InkWell( onTap: () { showBeautyDialog( context, type: QuickAlertType.info, text: 'Lower values generally result in less motion in the output video, \nwhile higher values generally result in more motion', confirmBtnText: AppLocale.gotIt.getString(context), showCancelBtn: false, ); }, child: Icon( Icons.help_outline, size: 16, color: customColors.weakLinkColor?.withAlpha(150), ), ), ], ), Row( children: [ Expanded( child: Slider( value: (motionBucketId ?? 0).toDouble(), min: 0, max: 255, divisions: 51, label: motionBucketIdText(motionBucketId), activeColor: customColors.linkColor, onChanged: (value) { setState(() { if (value > 0 && value < 1) { value = 1; } motionBucketId = value.toInt(); }); }, ), ), Text( motionBucketIdText(motionBucketId), style: TextStyle( fontSize: 12, color: customColors.weakTextColor, ), ), ], ) ], ), // Seed EnhancedTextField( controller: seedController, customColors: customColors, labelText: 'Seed', labelPosition: LabelPosition.left, showCounter: false, keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, ], hintText: '默认随机', textDirection: TextDirection.rtl, ), ], ), // 生成按钮 if (widget.apiEndpoint == 'image-to-video') AdvancedButton( showAdvancedOptions: showAdvancedOptions, onPressed: (value) { setState(() { showAdvancedOptions = value; }); }, ), if (widget.apiEndpoint == 'image-to-video') const SizedBox(height: 10), EnhancedButton( title: AppLocale.generate.getString(context), onPressed: onGenerate, ), const SizedBox(height: 20), ], ), ); } String cfgScaleText(double? cfgScale) { cfgScale ??= 0; return cfgScale == 0 ? 'Auto' : cfgScale.toStringAsFixed(1); } String motionBucketIdText(int? motionBucketId) { motionBucketId ??= 0; return motionBucketId == 0 ? 'Auto' : motionBucketId.toString(); } void onGenerate() async { FocusScope.of(context).requestFocus(FocusNode()); HapticFeedbackHelper.mediumImpact(); if (selectedImagePath == null && selectedImageData == null) { showErrorMessage('请先选择要处理的图片'); return; } var params = {}; if (cfgScale != null && cfgScale! >= 1) { params['cfg_scale'] = cfgScale; } if (motionBucketId != null && motionBucketId! >= 1) { params['motion_bucket_id'] = motionBucketId; } final cancelOutside = BotToast.showCustomLoading( toastBuilder: (cancel) { return const LoadingIndicator( message: '思考中,请稍候...', ); }, allowClick: false, duration: const Duration(seconds: 15), ); request(int waitDuration) async { try { cancelOutside(); final cancel = BotToast.showCustomLoading( toastBuilder: (cancel) { return LoadingIndicator( message: AppLocale.imageUploading.getString(context), ); }, allowClick: false, ); if (selectedImagePath != null && (selectedImagePath!.startsWith('http://') || selectedImagePath!.startsWith('https://'))) { params['image'] = selectedImagePath; cancel(); } else { if (selectedImagePath != null && selectedImagePath!.isNotEmpty) { final uploadRes = await ImageUploader(widget.setting).upload(selectedImagePath!).whenComplete(() => cancel()); params['image'] = uploadRes.url; } else if (selectedImageData != null && selectedImageData!.isNotEmpty) { final uploadRes = await ImageUploader(widget.setting).uploadData(selectedImageData!).whenComplete(() => cancel()); params['image'] = uploadRes.url; } } final taskId = await APIServer().creativeIslandImageDirectEdit( widget.apiEndpoint, params, ); stopPeriodQuery = false; Navigator.push( // ignore: use_build_context_synchronously context, MaterialPageRoute( fullscreenDialog: true, builder: (context) => DrawResultPage( future: Future.delayed(const Duration(seconds: 10), () async { return await queryCompletionTaskStatus( taskId: taskId, retryTimes: 0, delaySeconds: 3, params: params, ); }), waitDuration: waitDuration, ), ), ).whenComplete(() { stopPeriodQuery = true; }); } catch (e) { stopPeriodQuery = true; cancelOutside(); // ignore: use_build_context_synchronously showErrorMessageEnhanced(context, e); } } try { request(widget.initWaitDuration); } catch (e) { cancelOutside(); showErrorMessageEnhanced(context, e); } } Future queryCompletionTaskStatus({ required String taskId, required int retryTimes, required int delaySeconds, Map? params, }) async { if (retryTimes > 60) { return Future.error(AppLocale.generateTimeout.getString(context)); } final resp = await APIServer().asyncTaskStatus(taskId); switch (resp.status) { case 'success': if (params != null && resp.originImage != null && resp.originImage != '') { params['image'] = resp.originImage; } if (params != null && resp.width != null) { params['width'] = resp.width; } if (params != null && resp.height != null) { params['height'] = resp.height; } return IslandResult( result: resp.resources ?? const [], params: params, ); case 'failed': return Future.error(resp.errors!.join(";")); default: if (stopPeriodQuery) { // ignore: use_build_context_synchronously return Future.error(AppLocale.generateTimeout.getString(context)); } return await Future.delayed(Duration(seconds: delaySeconds), () async { return await queryCompletionTaskStatus( taskId: taskId, retryTimes: retryTimes + 1, delaySeconds: 3, params: params, ); }); } } double _calImageSelectorHeight(BuildContext context) { var width = MediaQuery.of(context).size.width; if (width > CustomSize.smallWindowSize) { width = CustomSize.smallWindowSize; } return width - 15 * 2 - 10 * 2 - 10; } } ================================================ FILE: lib/page/creative_island/gallery/components/image_card.dart ================================================ import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/image.dart'; import 'package:askaide/page/component/image.dart'; import 'package:askaide/page/component/random_avatar.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:flutter/material.dart'; class ImageCard extends StatelessWidget { final List images; final String? username; final int? userId; final int hotValue; final Function()? onTap; const ImageCard({ super.key, required this.images, this.username, this.userId, this.hotValue = 0, this.onTap, }); @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return InkWell( onTap: onTap, child: Container( decoration: BoxDecoration( color: customColors.backgroundContainerColor, borderRadius: CustomSize.borderRadius, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ ConstrainedBox( constraints: const BoxConstraints( minHeight: 50, ), child: ClipRRect( borderRadius: const BorderRadius.only(topLeft: CustomSize.radius, topRight: CustomSize.radius), child: images.isEmpty ? Image.asset('assets/image-broken.png') : CachedNetworkImageEnhanced( imageUrl: imageURL(images.first, qiniuImageTypeThumbMedium), fit: BoxFit.cover, ), ), ), Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 8, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ RandomAvatar( id: userId ?? 0, size: 15, usage: AvatarUsage.user, ), const SizedBox(width: 8), Text( username ?? '匿名', style: TextStyle( color: customColors.weakTextColor, fontSize: 12, ), ), ], ), Row( children: [ Icon( Icons.local_fire_department, size: 12, color: hotValue > 0 ? Colors.amber : null, ), const SizedBox(width: 4), Text( '$hotValue', style: TextStyle( color: customColors.weakTextColor, fontSize: 12, ), ), ], ), ], ), ), ], ), ), ); } } ================================================ FILE: lib/page/creative_island/gallery/data/gallery_datasource.dart ================================================ import 'package:askaide/helper/logger.dart'; import 'package:askaide/repo/api/creative.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:loading_more_list/loading_more_list.dart'; class GalleryDatasource extends LoadingMoreBase { int pageindex = 1; bool _hasMore = true; bool forceRefresh = false; @override bool get hasMore => (_hasMore && length < 300) || forceRefresh; @override Future loadData([bool isloadMoreAction = false]) async { try { final resp = await APIServer().creativeGallery( page: pageindex, perPage: 20, // cache: !forceRefresh, ); if (pageindex == 1) { clear(); } for (var element in resp.data) { add(element); } if (resp.page == resp.lastPage) { _hasMore = false; } pageindex = resp.page + 1; return true; } catch (e) { Logger.instance.e(e); return false; } } @override Future refresh([bool notifyStateChanged = false]) async { _hasMore = true; pageindex = 1; //force to refresh list when you don't want clear list before request //for the case, if your list already has 20 items. forceRefresh = !notifyStateChanged; var result = await super.refresh(notifyStateChanged); forceRefresh = false; return result; } } ================================================ FILE: lib/page/creative_island/gallery/gallery.dart ================================================ import 'package:askaide/helper/ability.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/sliver_component.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/page/creative_island/gallery/components/image_card.dart'; import 'package:askaide/page/creative_island/gallery/data/gallery_datasource.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api/creative.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; import 'package:loading_more_list/loading_more_list.dart'; class GalleryScreen extends StatefulWidget { final SettingRepository setting; const GalleryScreen({super.key, required this.setting}); @override State createState() => _GalleryScreenState(); } class _GalleryScreenState extends State { final GalleryDatasource datasource = GalleryDatasource(); @override void initState() { super.initState(); } @override void dispose() { datasource.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( backgroundColor: customColors.backgroundColor, body: _buildIslandItems(customColors), ), ); } /// 创作岛列表 Widget _buildIslandItems( CustomColors customColors, ) { return SliverComponent( title: Text( AppLocale.discover.getString(context), style: TextStyle( fontSize: CustomSize.appBarTitleSize, color: customColors.backgroundInvertedColor, ), ), actions: [ if (Ability().enableCreationIsland) IconButton( onPressed: () { context.push('/creative-draw'); }, icon: const Icon(Icons.palette_outlined), ), ], child: BackgroundContainer( setting: widget.setting, enabled: false, backgroundColor: customColors.backgroundColor, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ // Container( // padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 5), // child: Text('热门作品'), // ), Expanded( child: RefreshIndicator( color: customColors.linkColor, displacement: 20, onRefresh: () { return datasource.refresh(); }, child: LoadingMoreList( ListConfig( itemBuilder: (context, item, index) { return ImageCard( images: [item.preview], username: item.username, userId: item.userId, hotValue: item.hotValue, onTap: () => context.push('/creative-draw/gallery/${item.id}'), ); }, sourceList: datasource, padding: const EdgeInsets.all(10), extendedListDelegate: SliverWaterfallFlowDelegateWithFixedCrossAxisCount( crossAxisCount: _calCrossAxisCount(context), crossAxisSpacing: 10, mainAxisSpacing: 10, ), indicatorBuilder: (context, status) { String msg = ''; switch (status) { case IndicatorStatus.noMoreLoad: msg = '~ 没有更多了 ~'; break; case IndicatorStatus.loadingMoreBusying: msg = '加载中...'; break; case IndicatorStatus.error: msg = '加载失败,请稍后再试'; break; case IndicatorStatus.empty: msg = '暂无数据'; break; default: return const Center(child: LoadingIndicator()); } return Container( padding: const EdgeInsets.all(15), alignment: Alignment.center, child: Text( msg, style: TextStyle( color: customColors.weakTextColor, fontSize: 14, ), ), ); }, ), ), ), ), ], ), ), ); } int _calCrossAxisCount(BuildContext context) { double width = MediaQuery.of(context).size.width; if (width > CustomSize.maxWindowSize) { width = CustomSize.maxWindowSize; } return (width / 220).round(); } } ================================================ FILE: lib/page/creative_island/gallery/gallery_item.dart ================================================ import 'package:askaide/bloc/gallery_bloc.dart'; import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/haptic_feedback.dart'; import 'package:askaide/helper/helper.dart'; import 'package:askaide/helper/image.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/attached_button_panel.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/column_block.dart'; import 'package:askaide/page/component/enhanced_button.dart'; import 'package:askaide/page/component/gallery_item_share.dart'; import 'package:askaide/page/component/image_action.dart'; import 'package:askaide/page/component/image_preview.dart'; import 'package:askaide/page/component/item_selector_search.dart'; import 'package:askaide/page/component/random_avatar.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:clipboard/clipboard.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; class GalleryItemScreen extends StatefulWidget { final SettingRepository setting; final int galleryId; const GalleryItemScreen({ super.key, required this.setting, required this.galleryId, }); @override State createState() => _GalleryItemScreenState(); } class _GalleryItemScreenState extends State { @override void initState() { super.initState(); context.read().add(GalleryItemLoadEvent(id: widget.galleryId)); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( leading: IconButton( icon: const Icon(Icons.close), onPressed: () => Navigator.of(context).pop(), ), backgroundColor: customColors.backgroundColor, toolbarHeight: CustomSize.toolbarHeight, actions: [ BlocBuilder( buildWhen: (previous, current) => current is GalleryItemLoaded, builder: (context, state) { if (state is GalleryItemLoaded && state.isInternalUser && state.item.status == 1) { return TextButton( onPressed: () { openConfirmDialog( context, '确认取消?', () => APIServer() .cancelShareCreativeHistoryToGallery(historyId: state.item.creativeHistoryId!) .then((value) { showSuccessMessage(AppLocale.operateSuccess.getString(context)); context .read() .add(GalleryItemLoadEvent(id: widget.galleryId, forceRefresh: true)); }), ); }, child: Text( AppLocale.cancelShare.getString(context), style: TextStyle( color: customColors.weakLinkColor, fontSize: 12, ), ), ); } return const SizedBox(); }, ), ], ), extendBodyBehindAppBar: true, backgroundColor: customColors.backgroundColor, body: BackgroundContainer( setting: widget.setting, enabled: false, backgroundColor: customColors.backgroundColor, child: BlocBuilder( buildWhen: (previous, current) => current is GalleryItemLoaded, builder: (context, state) { if (state is GalleryItemLoaded) { return Align( alignment: Alignment.topCenter, child: ConstrainedBox( constraints: const BoxConstraints( maxWidth: CustomSize.smallWindowSize, ), child: SingleChildScrollView( child: SafeArea( child: Container( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 10), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ for (var img in state.item.images) Container( decoration: BoxDecoration( color: customColors.backgroundColor, borderRadius: CustomSize.borderRadius, ), // padding: const EdgeInsets.symmetric( // horizontal: 10, // vertical: 10, // ), margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 5), child: NetworkImagePreviewer( url: img, preview: imageURL(img, qiniuImageTypeThumb), hidePreviewButton: true, ), ), Container( padding: const EdgeInsets.symmetric( horizontal: 15, vertical: 8, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ RandomAvatar( id: state.item.userId ?? 0, usage: AvatarUsage.user, size: 15, ), const SizedBox(width: 8), Text( state.item.username ?? '匿名', style: TextStyle( color: customColors.weakTextColor, fontSize: 12, ), ), ], ), Row( children: [ const Icon( Icons.local_fire_department, size: 12, ), const SizedBox(width: 4), Text( '${state.item.hotValue}', style: TextStyle( color: customColors.weakTextColor, fontSize: 12, ), ), ], ), ], ), ), const SizedBox(height: 10), ColumnBlock( innerPanding: 10, padding: const EdgeInsets.all(15), children: [ if (state.item.prompt != null && state.item.prompt!.isNotEmpty) TextItem( title: 'Prompt', value: state.item.prompt!, ), if (state.item.negativePrompt != null && state.item.negativePrompt!.isNotEmpty) TextItem( title: 'Negative Prompt', value: state.item.negativePrompt!, ), ], ), Padding( padding: const EdgeInsets.symmetric( horizontal: 5, vertical: 10, ), child: Row( mainAxisSize: MainAxisSize.min, children: [ EnhancedButton( title: AppLocale.share.getString(context), icon: const Icon(Icons.share, size: 14), width: 80, color: customColors.backgroundInvertedColor, backgroundColor: customColors.backgroundColor, onPressed: () { Navigator.push( context, MaterialPageRoute( fullscreenDialog: true, builder: (context) => GalleryItemShareScreen( images: state.item.images, prompt: state.item.prompt, negativePrompt: state.item.negativePrompt, ), ), ); }, ), if (Ability().enableCreationIsland) ...[ const SizedBox(width: 10), EnhancedButton( title: AppLocale.shortcut.getString(context), icon: const Icon(Icons.webhook, size: 14), width: 80, color: customColors.backgroundInvertedColor, backgroundColor: customColors.backgroundColor, onPressed: () { if (state.item.images.length > 1) { List> items = []; for (var i = 0; i < state.item.images.length; i++) { items.add(SelectorItem( NetworkImagePreviewer( url: state.item.images[i], notClickable: true, hidePreviewButton: true, borderRadius: CustomSize.borderRadiusAll, ), state.item.images[i], )); } openListSelectDialog( context, items, (selected) { context.pop(); openImageWorkflowActionDialog( context, customColors, selected.value, ); return false; }, horizontal: true, horizontalCount: 2, heightFactor: 0.8, innerPadding: const EdgeInsets.symmetric( vertical: 10, ), title: AppLocale.selectImageToShortcut.getString(context), ); } else { openImageWorkflowActionDialog( context, customColors, state.item.images.first, ); } }, ), const SizedBox(width: 10), Expanded( child: EnhancedButton( title: AppLocale.makeSameStyle.getString(context), onPressed: () { context.push( '/creative-draw/create?mode=text-to-image&id=${state.item.creativeId}&gallery_copy_id=${state.item.id}'); }, ), ), ] ], ), ), ], ), ), ), ), ), ); } return const Center( child: Text('Loading ...'), ); }, ), ), ), ); } } class TextItem extends StatefulWidget { final String title; final String value; const TextItem({ super.key, required this.title, required this.value, }); @override State createState() => _TextItemState(); } class _TextItemState extends State { String valueTranslated = ''; @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return SizedBox( width: double.infinity, child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( widget.title, style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: customColors.weakTextColor, ), ), const SizedBox(height: 5), GestureDetector( onLongPressStart: (details) { HapticFeedbackHelper.mediumImpact(); BotToast.showAttachedWidget( target: details.globalPosition, duration: const Duration(seconds: 8), animationDuration: const Duration(milliseconds: 200), animationReverseDuration: const Duration(milliseconds: 200), preferDirection: PreferDirection.topCenter, ignoreContentClick: false, onlyOne: true, allowClick: true, enableSafeArea: true, attachedBuilder: (cancel) => AttachedButtonPanel( buttons: [ TextButton.icon( onPressed: () { FlutterClipboard.copy(widget.value).then((value) { showSuccessMessage('已复制到剪贴板'); }); cancel(); }, label: const Text(''), icon: const Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.copy, color: Color.fromARGB(255, 255, 255, 255), size: 14, ), Text( "复制", style: TextStyle(fontSize: 12, color: Colors.white), ), ], ), ), TextButton.icon( onPressed: () { cancel(); APIServer().translate(widget.value).then((value) { setState(() { valueTranslated = value.result!; }); }).onError((error, stackTrace) { showErrorMessage(resolveError(context, error!)); }); }, label: const Text(''), icon: const Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.translate, color: Color.fromARGB(255, 255, 255, 255), size: 14, ), Text( '翻译', style: TextStyle( fontSize: 12, color: Colors.white, ), ) ], ), ), ], ), ); }, child: Text( widget.value, style: TextStyle( fontSize: 12, color: customColors.weakTextColor, ), maxLines: 5, overflow: TextOverflow.ellipsis, ), ), if (valueTranslated != '') Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 10), Row(children: [ Icon( Icons.check_circle, size: 12, color: customColors.linkColor, ), const SizedBox(width: 5), Text( '中文翻译 ↓', style: TextStyle( fontSize: 12, color: customColors.weakTextColor, ), ), ]), const SizedBox(height: 5), GestureDetector( onLongPressStart: (details) { HapticFeedbackHelper.mediumImpact(); BotToast.showAttachedWidget( target: details.globalPosition, duration: const Duration(seconds: 8), animationDuration: const Duration(milliseconds: 200), animationReverseDuration: const Duration(milliseconds: 200), preferDirection: PreferDirection.topCenter, ignoreContentClick: false, onlyOne: true, allowClick: true, enableSafeArea: true, attachedBuilder: (cancel) => AttachedButtonPanel( buttons: [ TextButton.icon( onPressed: () { FlutterClipboard.copy(valueTranslated).then((value) { showSuccessMessage('已复制到剪贴板'); }); cancel(); }, label: const Text(''), icon: const Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.copy, color: Color.fromARGB(255, 255, 255, 255), size: 14, ), Text( "复制", style: TextStyle(fontSize: 12, color: Colors.white), ), ], ), ), ], ), ); }, child: Text( valueTranslated, style: TextStyle( fontSize: 12, color: customColors.weakTextColor, ), maxLines: 5, overflow: TextOverflow.ellipsis, ), ), ], ) ], ), ); } } ================================================ FILE: lib/page/creative_island/my_creation.dart ================================================ import 'package:askaide/bloc/creative_island_bloc.dart'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/helper.dart'; import 'package:askaide/helper/image.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/button.dart'; import 'package:askaide/page/component/image.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/page/creative_island/draw/data/draw_history_datasource.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api/creative.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; import 'package:loading_more_list/loading_more_list.dart'; class MyCreationScreen extends StatefulWidget { final SettingRepository setting; final String mode; const MyCreationScreen({super.key, required this.setting, required this.mode}); @override State createState() => _MyCreationScreenState(); } class _MyCreationScreenState extends State { final DrawHistoryDatasource datasource = DrawHistoryDatasource(); @override void dispose() { datasource.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( title: const Text( '我的创作', style: TextStyle(fontSize: CustomSize.appBarTitleSize), ), centerTitle: true, toolbarHeight: CustomSize.toolbarHeight, ), backgroundColor: customColors.backgroundColor, body: BackgroundContainer( setting: widget.setting, enabled: false, maxWidth: CustomSize.maxWindowSize, backgroundColor: customColors.backgroundColor, child: SafeArea( child: RefreshIndicator( color: customColors.linkColor, onRefresh: () async { context .read() .add(CreativeIslandHistoriesAllLoadEvent(forceRefresh: true, mode: widget.mode)); }, child: LoadingMoreList( ListConfig( extendedListDelegate: SliverWaterfallFlowDelegateWithFixedCrossAxisCount( crossAxisCount: _calCrossAxisCount(context), crossAxisSpacing: 10, mainAxisSpacing: 10, ), itemBuilder: (context, item, index) { return Material( color: customColors.backgroundContainerColor, borderRadius: CustomSize.borderRadius, child: InkWell( borderRadius: CustomSize.borderRadiusAll, onTap: () { context.push('/creative-island/${item.islandId}/history/${item.id}?show_error=true'); }, onLongPress: () { openModalBottomSheet( context, (context) { return Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const SizedBox(height: 20), Column( mainAxisSize: MainAxisSize.min, children: [ Button( title: '查看作品', onPressed: () { context.push( '/creative-island/${item.islandId}/history/${item.id}?show_error=true'); context.pop(); }, size: const ButtonSize.full(), color: customColors.weakLinkColor, backgroundColor: const Color.fromARGB(36, 222, 222, 222), ), const SizedBox(height: 10), Button( title: '删除作品', onPressed: () { onItemDelete( context, item, index, onFinished: () { context.pop(); }, ); }, size: const ButtonSize.full(), color: customColors.weakLinkColor, backgroundColor: const Color.fromARGB(36, 222, 222, 222), ), const SizedBox(height: 10), Button( title: AppLocale.cancel.getString(context), backgroundColor: const Color.fromARGB(36, 222, 222, 222), color: customColors.dialogDefaultTextColor?.withAlpha(150), onPressed: () { context.pop(); }, size: const ButtonSize.full(), ), const SizedBox(height: 10), ], ), ], ); }, heightFactor: 0.25, ); }, child: Column( children: [ Stack( children: [ _buildAnswerImagePreview(context, item), // TODO 风格名称,测试阶段使用 if (item.filterName != null && item.filterName!.isNotEmpty) Positioned( bottom: 0, child: Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 2, ), decoration: BoxDecoration( color: Colors.black.withOpacity(0.5), borderRadius: const BorderRadius.only( topRight: CustomSize.radius, bottomLeft: CustomSize.radius), ), child: Text( item.filterName!, style: const TextStyle( fontSize: 12, color: Colors.white, ), ), ), ), if (item.isShared) Positioned( right: 0, top: 0, child: Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 2, ), decoration: BoxDecoration( color: Colors.black.withOpacity(0.5), borderRadius: const BorderRadius.only( topRight: CustomSize.radius, bottomLeft: CustomSize.radius, ), ), child: const Text( '公开', style: TextStyle( fontSize: 12, color: Colors.white, ), ), ), ) ], ), Container( padding: const EdgeInsets.all(8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ buildIslandTypeText(customColors, item), Text( humanTime(item.createdAt, withTime: true), style: TextStyle( fontSize: 12, color: customColors.weakTextColor?.withAlpha(150), ), ), ], ), ), ], ), ), ); }, sourceList: datasource, padding: const EdgeInsets.all(10), indicatorBuilder: (context, status) { String msg = ''; switch (status) { case IndicatorStatus.noMoreLoad: msg = '~ 没有更多了 ~'; break; case IndicatorStatus.loadingMoreBusying: msg = '加载中...'; break; case IndicatorStatus.error: msg = '加载失败,请稍后再试'; break; case IndicatorStatus.empty: msg = '您还没有创作过作品哦'; break; default: return const Center(child: LoadingIndicator()); } return Container( padding: const EdgeInsets.all(15), alignment: Alignment.center, child: Text( msg, style: TextStyle( color: customColors.weakTextColor, fontSize: 14, ), ), ); }, ), ), ), ), ), ), ); } Widget buildIslandTypeText(CustomColors customColors, CreativeItemInServer item) { return Text( item.islandTitle ?? '', style: TextStyle( color: customColors.weakTextColor?.withAlpha(150), fontSize: 12, ), ); } void onItemDelete(BuildContext context, CreativeItemInServer item, int index, {Function? onFinished}) { openConfirmDialog(context, AppLocale.confirmDelete.getString(context), () { APIServer().deleteCreativeHistoryItem(item.islandId, hisId: item.id).then((value) { // datasource.refresh(true); datasource.removeAt(index); setState(() {}); showSuccessMessage(AppLocale.operateSuccess.getString(context)); onFinished?.call(); }); }); } Widget _buildAnswerImagePreview( BuildContext context, CreativeItemInServer item, ) { if (item.isVideoType && item.originalImage != null) { return ConstrainedBox( constraints: const BoxConstraints( minHeight: 100, ), child: Stack( children: [ ClipRRect( borderRadius: const BorderRadius.only(topLeft: CustomSize.radius, topRight: CustomSize.radius), child: CachedNetworkImageEnhanced( imageUrl: imageURL(item.originalImage!, qiniuImageTypeThumbMedium), fit: BoxFit.cover, ), ), Positioned( right: 10, bottom: 10, child: Image.asset( 'assets/play.png', width: 40, opacity: const AlwaysStoppedAnimation(0.7), ), ), ], ), ); } else if (item.isImageType && item.images.isNotEmpty) { return ConstrainedBox( constraints: const BoxConstraints( minHeight: 100, ), child: Stack( children: [ ClipRRect( borderRadius: const BorderRadius.only(topLeft: CustomSize.radius, topRight: CustomSize.radius), child: CachedNetworkImageEnhanced( imageUrl: imageURL(item.images.first, qiniuImageTypeThumbMedium), fit: BoxFit.cover, ), ), if (item.params['image'] != null && item.params['image'] != '') Positioned( left: 8, bottom: 8, child: SizedBox( width: 60, height: 60, child: ClipRRect( borderRadius: CustomSize.borderRadius, child: CachedNetworkImageEnhanced( imageUrl: imageURL(item.params['image'], qiniuImageTypeAvatar), fit: BoxFit.cover, ), ), ), ), ], ), ); } if (item.isFailed) { return ConstrainedBox( constraints: const BoxConstraints( minHeight: 150, ), child: const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.error_outline, size: 40, color: Colors.red, ), SizedBox(height: 10), Text('创作失败', style: TextStyle(color: Colors.red)) ], ), ), ); } return ConstrainedBox( constraints: const BoxConstraints( minHeight: 150, ), child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.hourglass_bottom, size: 40, color: Colors.blue[700], ), const SizedBox(height: 10), Text('创作中', style: TextStyle(color: Colors.blue[700])) ], ), ), ); } int _calCrossAxisCount(BuildContext context) { var width = MediaQuery.of(context).size.width; if (width > CustomSize.maxWindowSize) { width = CustomSize.maxWindowSize; } return (width / 220).round(); } } ================================================ FILE: lib/page/creative_island/my_creation_item.dart ================================================ import 'package:askaide/bloc/creative_island_bloc.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/column_block.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/page/creative_island/draw/components/content_preview.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api/creative.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; class MyCreationItemPage extends StatefulWidget { final String islandId; final int itemId; final SettingRepository setting; final bool showErrorMessage; const MyCreationItemPage({ super.key, required this.setting, required this.islandId, required this.itemId, required this.showErrorMessage, }); @override State createState() => _MyCreationItemPageState(); } class _MyCreationItemPageState extends State with SingleTickerProviderStateMixin { late final TabController _tabController; @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); _tabController.animateTo(1); context.read().add(CreativeIslandHistoryItemLoadEvent(widget.itemId)); } @override void dispose() { _tabController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { var customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: BlocBuilder( buildWhen: (previous, current) => current is CreativeIslandHistoryItemLoaded || current is CreativeIslandHistoryItemLoading, builder: (context, state) { if (state is CreativeIslandHistoryItemLoaded) { return Scaffold( appBar: AppBar( toolbarHeight: CustomSize.toolbarHeight, leading: IconButton( icon: const Icon(Icons.close), onPressed: () => Navigator.of(context).pop(), ), title: Text( state.item!.showBetaFeature ? '#${state.item!.id}' : '', style: TextStyle( color: customColors.weakTextColor, ), ), actions: buildActions(state, context, customColors), ), backgroundColor: customColors.backgroundColor, body: BackgroundContainer( setting: widget.setting, enabled: false, backgroundColor: customColors.backgroundColor, maxWidth: CustomSize.smallWindowSize, child: SafeArea( child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( child: state.item!.isSuccessful ? CreativeIslandContentPreview( result: IslandResult( result: state.item!.images, params: state.item!.params, ), customColors: customColors, item: state.item, ) : _buildNotSuccessBox(state, customColors), ), ColumnBlock( innerPanding: 10, padding: const EdgeInsets.all(15), margin: const EdgeInsets.symmetric( vertical: 10, horizontal: 10, ), children: [ if (state.item!.prompt != null && state.item!.prompt != '') Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( AppLocale.ideaPrompt.getString(context), style: TextStyle( fontSize: 15, fontWeight: FontWeight.bold, color: customColors.textfieldLabelColor, ), ), const SizedBox(height: 10), SelectableText( state.item!.prompt ?? '', style: TextStyle( color: customColors.weakTextColor, ), ), ], ), if (state.item!.arguments != null) ..._buildItemArguments( state.item!.creativeItemArguments, customColors, ), ], ), ], ), ), ), ), ); } return const Center( child: CircularProgressIndicator(), ); }, ), ); } List buildActions( CreativeIslandHistoryItemLoaded state, BuildContext context, CustomColors customColors, ) { if (state.item!.userId != APIServer().localUserID() && state.item!.isSuccessful) { return [ TextButton( onPressed: () { openConfirmDialog(context, 'Are you sure to ban this project?', () { APIServer().forbidCreativeHistoryItem(historyId: state.item!.id).then((value) { showSuccessMessage(AppLocale.operateSuccess.getString(context)); context.read().add(CreativeIslandHistoryItemLoadEvent( widget.itemId, forceRefresh: true, )); }); }); }, child: Row( children: [ const Icon( Icons.block, color: Colors.amber, size: 14, ), const SizedBox(width: 5), Text( 'Ban', style: TextStyle( color: customColors.weakLinkColor, fontSize: 12, ), ), ], ), ), ]; } return [ if (state.item!.isSuccessful && state.item!.showBetaFeature) TextButton( onPressed: () { if (state.item!.isShared) { APIServer().cancelShareCreativeHistoryToGallery(historyId: state.item!.id).then((value) { showSuccessMessage(AppLocale.operateSuccess.getString(context)); context.read().add(CreativeIslandHistoryItemLoadEvent( widget.itemId, forceRefresh: true, )); }); } else { APIServer().shareCreativeHistoryToGallery(historyId: state.item!.id).then((value) { showSuccessMessage(AppLocale.operateSuccess.getString(context)); context.read().add(CreativeIslandHistoryItemLoadEvent( widget.itemId, forceRefresh: true, )); }); } }, child: Text( state.item!.isShared ? 'Set Private' : 'Set Public', style: TextStyle( color: customColors.weakLinkColor, fontSize: 12, ), ), ) ]; } Widget _buildNotSuccessBox( CreativeIslandHistoryItemLoaded state, CustomColors customColors, ) { if (state.item != null && state.item!.isFailed) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.error_outline, size: 50, color: Colors.red, ), const SizedBox(height: 10), Text( AppLocale.generateFailed.getString(context), style: const TextStyle(color: Colors.red), textAlign: TextAlign.center, ), const SizedBox(height: 10), SelectableText( widget.showErrorMessage ? '${state.item!.answer}' : 'Error Code:${state.item!.errorCode}', textAlign: TextAlign.center, style: TextStyle( fontSize: 10, color: customColors.weakTextColor, ), ), const SizedBox(height: 20), ], ), ); } return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.info_outline, size: 50, color: customColors.weakTextColor, ), const SizedBox(height: 10), Text( AppLocale.generate.getString(context), style: TextStyle( color: customColors.backgroundInvertedColor, ), ), ], ), ); } List _buildItemArguments(CreativeItemArguments arg, CustomColors customColors) { final children = []; if (arg.negativePrompt != null && arg.negativePrompt != '') { children.add( Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( AppLocale.excludeContents.getString(context), style: TextStyle( fontSize: 15, fontWeight: FontWeight.bold, color: customColors.textfieldLabelColor, ), ), const SizedBox(height: 10), SelectableText( arg.negativePrompt!, style: TextStyle( color: customColors.weakTextColor, ), ), ], ), ); } // if (arg.modelName != null && arg.modelName != '') { // children.add( // Column( // crossAxisAlignment: CrossAxisAlignment.start, // children: [ // Text( // 'AI 模型', // style: TextStyle( // fontSize: 15, // fontWeight: FontWeight.bold, // color: customColors.textfieldLabelColor, // ), // ), // const SizedBox(height: 10), // SelectableText( // arg.modelName!, // style: TextStyle( // color: customColors.weakTextColor, // ), // ), // ], // ), // ); // } if (arg.filterName != null && arg.filterName != '') { children.add( Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( AppLocale.style.getString(context), style: TextStyle( fontSize: 15, fontWeight: FontWeight.bold, color: customColors.textfieldLabelColor, ), ), const SizedBox(height: 10), SelectableText( arg.filterName!, style: TextStyle( color: customColors.weakTextColor, ), ), ], ), ); } // if (arg.seed != null && arg.seed! > 0) { // children.add( // Column( // crossAxisAlignment: CrossAxisAlignment.start, // children: [ // Text( // 'Seed', // style: TextStyle( // fontSize: 15, // fontWeight: FontWeight.bold, // color: customColors.textfieldLabelColor, // ), // ), // const SizedBox(height: 10), // SelectableText( // '${arg.seed!}', // style: TextStyle( // color: customColors.weakTextColor, // ), // ), // ], // ), // ); // } // if (arg.image != null && arg.image != '') { // children.add( // Column( // crossAxisAlignment: CrossAxisAlignment.start, // children: [ // Text( // '原图', // style: TextStyle( // fontSize: 15, // fontWeight: FontWeight.bold, // color: customColors.textfieldLabelColor, // ), // ), // const SizedBox(height: 10), // NetworkImagePreviewer(url: arg.image!, hidePreviewButton: true), // ], // ), // ); // } return children; } } ================================================ FILE: lib/page/custom_scaffold.dart ================================================ import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/material.dart'; class CustomScaffold extends StatefulWidget { final SettingRepository settings; final Widget title; final List? actions; final Widget body; final Widget? drawer; final Widget? appBarBackground; final AppBar? backAppBar; final bool showBackAppBar; const CustomScaffold({ super.key, required this.settings, required this.title, this.actions, required this.body, this.drawer, this.appBarBackground, this.backAppBar, this.showBackAppBar = false, }); @override State createState() => _CustomScaffoldState(); } class _CustomScaffoldState extends State { @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return Scaffold( appBar: buildAppBar(), backgroundColor: customColors.backgroundContainerColor, body: BackgroundContainer( setting: widget.settings, maxWidth: CustomSize.maxWindowSize, child: widget.body, ), drawer: widget.drawer, ); } AppBar? buildAppBar() { if (widget.showBackAppBar && widget.backAppBar != null) { return widget.backAppBar; } return AppBar( title: widget.title, centerTitle: true, toolbarHeight: CustomSize.toolbarHeight, elevation: 0, actions: widget.actions, flexibleSpace: SizedBox( width: double.infinity, child: ShaderMask( shaderCallback: (rect) { return const LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Colors.black, Colors.transparent], ).createShader(Rect.fromLTRB(0, 0, rect.width, rect.height)); }, blendMode: BlendMode.dstIn, child: widget.appBarBackground, ), ), leading: widget.drawer != null ? Builder( builder: (context) => IconButton( icon: const Icon(Icons.sort), onPressed: () => Scaffold.of(context).openDrawer(), ), ) : null, ); } } ================================================ FILE: lib/page/data/chat_history_datasource.dart ================================================ import 'package:askaide/helper/logger.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/chat_message_repo.dart'; import 'package:askaide/repo/model/chat_history.dart'; import 'package:loading_more_list/loading_more_list.dart'; class ChatHistoryDatasource extends LoadingMoreBase { int pageindex = 1; bool _hasMore = true; bool forceRefresh = false; String? keyword; final ChatMessageRepository repo; ChatHistoryDatasource(this.repo); @override bool get hasMore => (_hasMore && length < 300) || forceRefresh; @override Future loadData([bool isloadMoreAction = false]) async { try { final histories = await repo.recentChatHistories( 30, keyword: keyword, offset: 30 * (pageindex - 1), userId: APIServer().localUserID(), ); if (pageindex == 1) { clear(); } for (var element in histories) { add(element); } if (histories.isEmpty) { _hasMore = false; } pageindex = pageindex + 1; return true; } catch (e) { Logger.instance.e(e); return false; } } @override Future refresh([bool notifyStateChanged = false, String? keyword]) async { this.keyword = keyword; _hasMore = true; pageindex = 1; //force to refresh list when you don't want clear list before request //for the case, if your list already has 20 items. forceRefresh = !notifyStateChanged; var result = await super.refresh(notifyStateChanged); forceRefresh = false; return result; } } ================================================ FILE: lib/page/data/group_message_datasource.dart ================================================ import 'package:askaide/helper/logger.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/model/group.dart'; import 'package:loading_more_list/loading_more_list.dart'; class GroupMessageDatasource extends LoadingMoreBase { int startId = 0; bool _hasMore = true; bool forceRefresh = false; final int groupId; GroupMessageDatasource({required this.groupId}); @override bool get hasMore => _hasMore || forceRefresh; @override Future loadData([bool isloadMoreAction = false]) async { try { final messages = await APIServer() .chatGroupMessages(groupId, startId: startId, cache: false); if (startId == 0) { clear(); } for (var element in messages.data) { add(element); } if (messages.data.isEmpty) { _hasMore = false; } startId = messages.lastId; return true; } catch (e) { Logger.instance.e(e); return false; } } @override Future refresh([bool notifyStateChanged = false]) async { _hasMore = true; startId = 1; //force to refresh list when you don't want clear list before request //for the case, if your list already has 20 items. forceRefresh = !notifyStateChanged; var result = await super.refresh(notifyStateChanged); forceRefresh = false; return result; } } ================================================ FILE: lib/page/data/notification_datasource.dart ================================================ import 'package:askaide/helper/logger.dart'; import 'package:askaide/repo/api/notification.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:loading_more_list/loading_more_list.dart'; class NotificationDatasource extends LoadingMoreBase { int startId = 0; bool _hasMore = true; bool forceRefresh = false; NotificationDatasource(); @override bool get hasMore => _hasMore || forceRefresh; @override Future loadData([bool isloadMoreAction = false]) async { try { final messages = await APIServer().notifications(startId: startId, cache: false); if (startId == 0) { clear(); } for (var element in messages.data) { add(element); } if (messages.data.isEmpty) { _hasMore = false; } startId = messages.lastId; return true; } catch (e) { Logger.instance.e(e); return false; } } @override Future refresh([bool notifyStateChanged = false]) async { _hasMore = true; startId = 0; //force to refresh list when you don't want clear list before request //for the case, if your list already has 20 items. forceRefresh = !notifyStateChanged; var result = await super.refresh(notifyStateChanged); forceRefresh = false; return result; } } ================================================ FILE: lib/page/drawer.dart ================================================ import 'package:askaide/bloc/account_bloc.dart'; import 'package:askaide/bloc/chat_chat_bloc.dart'; import 'package:askaide/bloc/room_bloc.dart'; import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/platform.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/account_quota_card.dart'; import 'package:askaide/page/component/chat/role_avatar.dart'; import 'package:askaide/page/component/social_icon.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api/user.dart'; import 'package:askaide/repo/model/chat_history.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; class LeftDrawer extends StatefulWidget { const LeftDrawer({super.key}); @override State createState() => _LeftDrawerState(); } class _LeftDrawerState extends State { @override void initState() { super.initState(); reload(); } void reload() { if (Ability().isUserLogon()) { context.read().add(AccountLoadEvent(cache: false)); } context.read().add(ChatChatLoadRecentHistories()); context.read().add(RoomsRecentLoadEvent()); } double maxDrawerWidth() { final width = MediaQuery.of(context).size.width * 0.85; return width > 334.0 ? 334.0 : width; } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return Drawer( width: maxDrawerWidth(), shape: RoundedRectangleBorder( borderRadius: PlatformTool.isDesktop() ? BorderRadius.zero : const BorderRadius.only( topRight: CustomSize.radius, bottomRight: CustomSize.radius, ), ), backgroundColor: customColors.backgroundContainerColor, shadowColor: customColors.backgroundInvertedColor, child: SafeArea( top: false, child: Padding( padding: const EdgeInsets.only(right: 10), child: Column( children: [ Expanded( child: SingleChildScrollView( child: Column( children: [ const SafeArea(child: SizedBox(height: 20)), if (Ability().isUserLogon() && Ability().enableDigitalHuman) ListTile( leading: const Icon(Icons.group_outlined), title: Text(AppLocale.homeTitle.getString(context)), onTap: () { context.push('/characters').whenComplete(reload); }, ), if (Ability().isUserLogon() && Ability().enableDigitalHuman) BlocBuilder( buildWhen: (previous, current) => current is RoomsRecentLoaded, builder: (_, state) { if (state is RoomsRecentLoaded) { return ListView.builder( shrinkWrap: true, padding: const EdgeInsets.all(0), physics: const NeverScrollableScrollPhysics(), itemCount: state.rooms.length, itemBuilder: (context, index) { final item = state.rooms[index]; return ListTile( contentPadding: const EdgeInsets.only(left: 30), dense: true, leading: RoleAvatar( avatarUrl: item.avatarUrl, name: item.name, avatarSize: 25, ), title: Text(item.name), onTap: () { context.push('/room/${item.id}/chat').whenComplete(reload); }, ); }, ); } return const SizedBox(); }, ), if (Ability().enableGallery) ListTile( leading: const Icon(Icons.auto_awesome_outlined), title: Text(AppLocale.discover.getString(context)), onTap: () { context.push('/creative-gallery').whenComplete(reload); }, ), // ListTile( // leading: const Icon(Icons.palette_outlined), // title: Text(AppLocale.creativeIsland.getString(context)), // onTap: () { // context.push('/creative-draw'); // }, // ), if (Ability().enableGallery || (Ability().isUserLogon() && Ability().enableDigitalHuman)) Divider( color: customColors.weakTextColor?.withAlpha(50), height: 10, indent: 10, endIndent: 10, ), BlocBuilder( buildWhen: (previous, current) => current is ChatChatRecentHistoriesLoaded, builder: (_, state) { if (state is ChatChatRecentHistoriesLoaded) { // Group histories by time final now = DateTime.now(); final groups = >{}; for (var history in state.histories) { final created = DateTime.fromMillisecondsSinceEpoch( (history.createdAt ?? DateTime.now()).millisecondsSinceEpoch); final difference = now.difference(created); String groupKey; if (difference.inDays < 4) { groupKey = AppLocale.recently.getString(context); } else if (difference.inDays < 7) { groupKey = '4 ${AppLocale.daysAgo.getString(context)}'; } else if (difference.inDays < 14) { groupKey = AppLocale.lastWeek.getString(context); } else if (difference.inDays < 30) { final weeks = (difference.inDays / 7).floor(); groupKey = '$weeks ${AppLocale.weeksAgo.getString(context)}'; } else if (difference.inDays < 365) { if (difference.inDays < 60) { groupKey = AppLocale.lastMonth.getString(context); } final months = (difference.inDays / 30).floor(); groupKey = '$months ${AppLocale.monthsAgo.getString(context)}'; } else if (difference.inDays < 730) { groupKey = AppLocale.lastYear.getString(context); } else { groupKey = AppLocale.longTimeAgo.getString(context); } groups.putIfAbsent(groupKey, () => []).add(history); } return Column( children: [ ListView.builder( shrinkWrap: true, padding: const EdgeInsets.all(0), physics: const NeverScrollableScrollPhysics(), itemCount: groups.entries.fold(0, (sum, entry) => (sum ?? 0) + entry.value.length + 1), itemBuilder: (context, index) { int itemCount = 0; for (var entry in groups.entries) { if (index == itemCount) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Container( padding: const EdgeInsets.only(left: 16, top: 8, bottom: 8), child: Text( entry.key, style: TextStyle( color: Theme.of(context).colorScheme.secondary, fontSize: 12, ), ), ), const SizedBox(), if (index == 0) IconButton( onPressed: () { context.push('/chat-chat/history').whenComplete(() { if (context.mounted) { context.read().add(ChatChatLoadRecentHistories()); } }); }, icon: Icon( Icons.filter_list, color: Theme.of(context).colorScheme.secondary, size: 16, ), ), ], ); } itemCount += 1; if (index < itemCount + entry.value.length) { final item = entry.value[index - itemCount]; return ListTile( contentPadding: const EdgeInsets.only(left: 30), title: Text( item.title ?? 'Unknown', maxLines: 1, overflow: TextOverflow.ellipsis, ), onTap: () { context.push( '/chat-anywhere?chat_id=${item.id}&model=${item.model}&title=${item.title}'); }, ); } itemCount += entry.value.length; } return const SizedBox(); }, ), ], ); } return SocialIconGroup( isSettingTiles: true, ); }, ), ], ), ), ), Container( height: 100, padding: const EdgeInsets.only(left: 20, right: 10), child: buildAccountCard(context), ), ], ), ), ), ); } Widget buildAccountCard(BuildContext context) { return Stack( children: [ Positioned( right: 0, top: 6, child: IconButton( onPressed: () { context.push('/setting'); }, icon: const Icon(Icons.more_horiz), tooltip: AppLocale.settings.getString(context), ), ), Container( margin: const EdgeInsets.only(top: 15), child: BlocBuilder( builder: (_, state) { UserInfo? userInfo; if (state is AccountLoaded) { userInfo = state.user; } return AccountQuotaCard( userInfo: userInfo, noBorder: true, onPaymentReturn: () { if (userInfo != null) { context.read().add(AccountLoadEvent(cache: false)); } }, ); }, ), ), ], ); } } ================================================ FILE: lib/page/home.dart ================================================ import 'package:askaide/bloc/account_bloc.dart'; import 'package:askaide/bloc/chat_chat_bloc.dart'; import 'package:askaide/bloc/chat_message_bloc.dart'; import 'package:askaide/bloc/room_bloc.dart'; import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/cache.dart'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/haptic_feedback.dart'; import 'package:askaide/helper/model.dart'; import 'package:askaide/helper/upload.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/chat/component/model_switcher.dart'; import 'package:askaide/page/chat/component/stop_button.dart'; import 'package:askaide/page/chat/character_chat.dart'; import 'package:askaide/page/component/audio_player.dart'; import 'package:askaide/page/component/chat/chat_input.dart'; import 'package:askaide/page/component/chat/chat_input_button.dart'; import 'package:askaide/page/component/chat/chat_preview.dart'; import 'package:askaide/page/component/chat/empty.dart'; import 'package:askaide/page/component/chat/file_upload.dart'; import 'package:askaide/page/component/chat/help_tips.dart'; import 'package:askaide/page/component/chat/message_state_manager.dart'; import 'package:askaide/page/component/chat/role_avatar.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/enhanced_error.dart'; import 'package:askaide/page/component/global_alert.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/select_mode_toolbar.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/page/custom_scaffold.dart'; import 'package:askaide/page/drawer.dart'; import 'package:askaide/repo/api/model.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/model/message.dart'; import 'package:askaide/repo/model/misc.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:auto_size_text/auto_size_text.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:askaide/repo/model/model.dart' as mm; import 'package:go_router/go_router.dart'; import 'package:quickalert/models/quickalert_type.dart'; import 'package:url_launcher/url_launcher_string.dart'; class NewHomePage extends StatefulWidget { final SettingRepository settings; final bool showInitialDialog; final int? reward; /// 聊天内容窗口状态管理器 final MessageStateManager stateManager; const NewHomePage({ super.key, required this.settings, required this.stateManager, this.showInitialDialog = false, this.reward, }); @override State createState() => _NewHomePageState(); } class _NewHomePageState extends State { // 聊天内容界面控制器 final ChatPreviewController chatPreviewController = ChatPreviewController(); // 聊天内容滚动控制器 final ScrollController scrollController = ScrollController(); // 输入框是否可编辑 final ValueNotifier enableInput = ValueNotifier(true); // 音频播放器控制器 final AudioPlayerController audioPlayerController = AudioPlayerController(useRemoteAPI: true); // 聊天室 ID,当没有值时,会在第一个聊天消息发送后自动设置新值 int? chatId; // The selected image files for image upload List selectedImageFiles = []; // The selected file for file upload FileUpload? selectedFile; // 是否显示音频播放器 bool showAudioPlayer = false; // 是否显示音频播放器加载中 bool audioLoadding = false; /// 当前选择的模型 mm.Model? selectedModel; // 全量模型列表 List supportModels = []; // 当前聊天所使用的模型(v2) HomeModelV2? currentModelV2; /// 是否启用搜索 bool enableSearch = false; /// 是否启用推理 bool enableReasoning = false; @override void initState() { super.initState(); Cache().intGet(key: 'last_chat_id').then((value) { chatId = value; reloadPage(loadAll: true); }); reloadModels(cache: false); initListeners(); showInitialDialog(); } void showInitialDialog() { if (widget.showInitialDialog) { WidgetsBinding.instance.addPostFrameCallback((_) { showBeautyDialog( context, type: QuickAlertType.info, text: '恭喜您,账号创建成功!${(widget.reward != null && widget.reward! > 0) ? '\n\n为了庆祝这一时刻,我们向您的账户赠送了 ${widget.reward} 个智慧果。' : ''}', confirmBtnText: AppLocale.startChat.getString(context), onConfirmBtnTap: () { context.pop(); }, ); }); } else { // 版本检查 APIServer().versionCheck().then((resp) { final lastVersion = widget.settings.get('last_server_version'); if (resp.serverVersion == lastVersion && !resp.forceUpdate) { return; } if (resp.hasUpdate) { showBeautyDialog( context, type: QuickAlertType.success, text: resp.message, confirmBtnText: AppLocale.updateApp.getString(context), onConfirmBtnTap: () { launchUrlString(resp.url, mode: LaunchMode.externalApplication); }, cancelBtnText: AppLocale.notUpdateApp.getString(context), showCancelBtn: true, ); } widget.settings.set('last_server_version', resp.serverVersion); }); } } /// 重新加载页面 void reloadPage({bool loadAll = false}) { // 加载当前用户信息 context.read().add(AccountLoadEvent()); if (loadAll) { // 加载当前聊天室信息 context.read().add(RoomLoadEvent( chatAnywhereRoomId, chatHistoryId: chatId, cascading: true, )); // 查询最近聊天记录 context.read().add(ChatMessageGetRecentEvent(chatHistoryId: chatId)); } } /// 加载模型列表,用于查询模型名称 Future reloadModels({bool cache = true}) async { var value = await ModelAggregate.models(cache: cache); setState(() { supportModels = value; }); var cacheValue = await Cache().stringGet(key: 'last_selected_model'); final selected = supportModels.where((e) => e.id == cacheValue).firstOrNull; if (selected != null) { setState(() { selectedModel = selected; }); } if (selectedModel == null && supportModels.isNotEmpty) { setState(() { selectedModel = supportModels.where((e) => e.isDefault && !e.userNoPermission).firstOrNull; selectedModel ??= supportModels.where((e) => !e.userNoPermission).firstOrNull; }); } if (selectedModel != null) { loadCurrentModel(selectedModel!.id); } } void initListeners() { chatPreviewController.addListener(() { setState(() {}); }); audioPlayerController.onPlayStopped = () { setState(() { showAudioPlayer = false; }); }; audioPlayerController.onPlayAudioStarted = () { setState(() { showAudioPlayer = true; }); }; audioPlayerController.onPlayAudioLoading = (loading) { setState(() { audioLoadding = loading; }); }; } /// 创建新的聊天 void createNewChat() { Cache().setInt( key: 'last_chat_id', value: 0, duration: const Duration(days: 3650), ); setState(() { chatId = null; }); reloadPage(loadAll: true); } /// 更新当前聊天 void updateCurrentChat(int chatId) { Cache().setInt( key: 'last_chat_id', value: chatId, duration: const Duration(days: 3650), ); if (this.chatId == chatId) { return; } setState(() { this.chatId = chatId; }); reloadPage(); } @override void dispose() { scrollController.dispose(); chatPreviewController.dispose(); audioPlayerController.dispose(); super.dispose(); } Future loadCurrentModel(String model) async { if (!model.startsWith('v2@') || currentModelV2 != null) { return; } currentModelV2 = await APIServer().customHomeModelsItemV2( uniqueKey: model.split('v2@').last, ); setState(() {}); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return WindowFrameWidget( child: CustomScaffold( settings: widget.settings, showBackAppBar: chatPreviewController.selectMode, backAppBar: AppBar( title: Text( AppLocale.select.getString(context), style: const TextStyle(fontSize: CustomSize.appBarTitleSize), ), centerTitle: true, elevation: 0, leadingWidth: 80, leading: TextButton( onPressed: () { chatPreviewController.exitSelectMode(); }, child: Text( AppLocale.cancel.getString(context), style: TextStyle(color: customColors.linkColor), ), ), toolbarHeight: CustomSize.toolbarHeight, ), // 标题,点击后弹出模型选择对话框 title: GestureDetector( onTap: () { reloadModels(cache: false); ModelSwitcher.openActionDialog( // ignore: use_build_context_synchronously context: context, onSelected: (selected) { setState(() { selectedModel = selected; }); if (selected != null) { Cache().setString( key: 'last_selected_model', value: selected.id, duration: const Duration(days: 3650), ); } }, initValue: selectedModel, ); }, child: SizedBox( width: MediaQuery.of(context).size.width / 2, child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ Flexible( child: AutoSizeText( selectedModel != null ? selectedModel!.name : AppLocale.selectModel.getString(context), maxFontSize: 15, minFontSize: 12, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: CustomSize.appBarTitleSize, color: customColors.backgroundInvertedColor, fontWeight: FontWeight.bold, ), ), ), const SizedBox(width: 3), Icon( Icons.arrow_forward_ios, color: customColors.backgroundInvertedColor!.withAlpha(150), size: CustomSize.appBarTitleSize * 0.8, ), ], ), ], ), ), ), actions: [ IconButton( icon: const Icon(Icons.maps_ugc_outlined), onPressed: createNewChat, ), ], body: BlocConsumer( listenWhen: (previous, current) => current is RoomLoaded, listener: (context, state) async { if (state is RoomLoaded && currentModelV2 == null) { await loadCurrentModel(state.room.model); } }, buildWhen: (previous, current) => current is RoomLoaded, builder: (context, room) { // 加载聊天室 if (room is RoomLoaded) { if (room.error != null) { return EnhancedErrorWidget(error: room.error); } return buildChatComponents( customColors, context, room, ); } else { return Container(); } }, ), drawer: MultiBlocProvider( providers: [ BlocProvider.value( value: context.read(), ), BlocProvider.value( value: context.read(), ), ], child: const LeftDrawer(), ), ), ); } /// 构建聊天室窗口 Widget buildChatComponents( CustomColors customColors, BuildContext context, RoomLoaded room, ) { return Column( children: [ if (Ability().showGlobalAlert) const GlobalAlert(pageKey: 'chat'), if (showAudioPlayer) EnhancedAudioPlayer( controller: audioPlayerController, loading: audioLoadding, ), // 聊天内容窗口 Expanded( child: Stack( fit: StackFit.expand, children: [ BlocConsumer( listener: (context, state) { if (state is ChatHistoryInited) { updateCurrentChat(state.chatId); } if (state is ChatMessagesLoaded && state.error == null) { setState(() { selectedImageFiles = []; }); } // 显示错误提示 else if (state is ChatMessagesLoaded && state.error != null) { showErrorMessageEnhanced(context, state.error); } else if (state is ChatMessageUpdated) { // 聊天内容窗口滚动到底部 if (!state.processing && scrollController.hasClients) { scrollController.animateTo( 0, duration: const Duration(milliseconds: 500), curve: Curves.easeOut, ); } if (state.processing && enableInput.value) { // 聊天回复中时,禁止输入框编辑 setState(() { enableInput.value = false; }); } else if (!state.processing && !enableInput.value) { // 聊天回复完成时,取消输入框的禁止编辑状态 setState(() { enableInput.value = true; }); } } }, buildWhen: (prv, cur) => cur is ChatMessagesLoaded, builder: (context, state) { if (state is ChatMessagesLoaded) { return buildChatPreviewArea( state, room.examples ?? [], room, customColors, chatPreviewController.selectMode, ); } return const Center(child: CircularProgressIndicator()); }, ), if (!enableInput.value) Positioned( bottom: 10, width: CustomSize.adaptiveMaxWindowWidth(context), child: Center( child: StopButton( label: AppLocale.stopOutput.getString(context), onPressed: () { HapticFeedbackHelper.mediumImpact(); context.read().add(ChatMessageStopEvent()); }, ), ), ), ], ), ), // 聊天输入窗口 if (!chatPreviewController.selectMode) Container( decoration: BoxDecoration( borderRadius: const BorderRadius.only( topLeft: CustomSize.radius, topRight: CustomSize.radius, ), color: customColors.chatInputPanelBackground, ), child: BlocBuilder( buildWhen: (previous, current) => current is ChatMessagesLoaded, builder: (context, state) { var enableImageUpload = false; var showReasoning = false; var showSearch = false; if (state is ChatMessagesLoaded) { if (currentModelV2 != null) { enableImageUpload = currentModelV2?.supportVision ?? false; showReasoning = currentModelV2?.supportReasoning ?? false; showSearch = currentModelV2?.supportSearch ?? false; } else { var model = state.chatHistory?.model ?? room.room.model; final cur = supportModels.where((e) => e.id == model).firstOrNull; enableImageUpload = cur?.supportVision ?? false; showReasoning = cur?.supportReasoning ?? false; showSearch = cur?.supportSearch ?? false; } } enableImageUpload = selectedModel == null ? enableImageUpload : (selectedModel?.supportVision ?? false); showReasoning = selectedModel == null ? showReasoning : (selectedModel?.supportReasoning ?? false); showSearch = selectedModel == null ? showSearch : (selectedModel?.supportSearch ?? false); return ChatInput( enableNotifier: enableInput, onSubmit: (value) { handleSubmit(value); FocusManager.instance.primaryFocus?.unfocus(); }, enableImageUpload: enableImageUpload, onImageSelected: (files) { setState(() { selectedImageFiles = files; }); }, selectedImageFiles: enableImageUpload ? selectedImageFiles : [], hintText: AppLocale.askMeAnyQuestion.getString(context), onVoiceRecordTappedEvent: () { audioPlayerController.stop(); }, onStopGenerate: () { context.read().add(ChatMessageStopEvent()); }, toolsBuilder: () { return [ if (showReasoning) ChatInputButton( text: AppLocale.reasoning.getString(context), icon: Icons.tips_and_updates_outlined, onPressed: () { setState(() { enableReasoning = !enableReasoning; }); }, isActive: enableReasoning, ), if (showSearch) ChatInputButton( text: AppLocale.onlineSearch.getString(context), icon: Icons.language_outlined, onPressed: () { setState(() { enableSearch = !enableSearch; }); }, isActive: enableSearch, ), ]; }, ); }, ), ), // 选择模式工具栏 if (chatPreviewController.selectMode) SelectModeToolbar(chatPreviewController: chatPreviewController), ], ); } /// 构建聊天内容窗口 Widget buildChatPreviewArea( ChatMessagesLoaded loadedState, List examples, RoomLoaded room, CustomColors customColors, bool selectMode, ) { final loadedMessages = loadedState.messages as List; // 聊天内容为空时,显示示例页面 if (loadedMessages.isEmpty) { return EmptyPreview( examples: examples, onSubmit: handleSubmit, cardMode: true, ); } final messages = loadedMessages.map((e) { if (e.model != null && !e.model!.startsWith('v2@')) { final mod = supportModels.where((m) => m.id == e.model).firstOrNull; if (mod != null) { e.senderName = mod.shortName; e.avatarUrl = mod.avatarUrl; } } if (e.avatarUrl == null || e.senderName == null) { if (loadedState.chatHistory != null && loadedState.chatHistory!.model != null) { if (currentModelV2 != null) { e.senderName = currentModelV2!.name; e.avatarUrl = currentModelV2!.avatarUrl; } else { final mod = supportModels.where((e) => e.id == loadedState.chatHistory!.model!).firstOrNull; if (mod != null) { e.senderName = mod.shortName; e.avatarUrl = mod.avatarUrl; } } } } final stateMessage = room.states[widget.stateManager.getKey(e.roomId ?? 0, e.id ?? 0)] ?? MessageState(); return MessageWithState(e, stateMessage); }).toList(); chatPreviewController.setAllMessageIds(messages); return ChatPreview( padding: enableInput.value ? null : const EdgeInsets.only(bottom: 35), messages: messages, scrollController: scrollController, controller: chatPreviewController, stateManager: widget.stateManager, robotAvatar: selectMode ? null : RoleAvatar( avatarUrl: room.room.avatarUrl, his: loadedState.chatHistory, alternativeAvatarUrl: currentModelV2?.avatarUrl, ), senderNameBuilder: (message) { if (message.senderName == null) { return null; } return Container( margin: const EdgeInsets.fromLTRB(0, 0, 10, 7), padding: const EdgeInsets.symmetric(horizontal: 13), child: Text( message.senderName!, style: TextStyle( color: customColors.weakTextColor, fontSize: 12, ), ), ); }, onDeleteMessage: (id) { handleDeleteMessage(context, id, chatHistoryId: chatId); }, onResentEvent: (message, index) { scrollController.animateTo(0, duration: const Duration(milliseconds: 500), curve: Curves.easeOut); handleSubmit(message.text, messagetType: message.type, index: index, isResent: true); }, onSpeakEvent: (message) { audioPlayerController.playAudio(message.text); }, helpWidgets: loadedState.processing || loadedMessages.last.isInitMessage() ? null : [HelpTips(onSubmitMessage: handleSubmit)], ); } /// 提交新消息 void handleSubmit( String text, { messagetType = MessageType.text, int? index, bool isResent = false, }) async { setState(() { enableInput.value = false; }); if (selectedImageFiles.isNotEmpty) { final cancel = BotToast.showCustomLoading( toastBuilder: (cancel) { return LoadingIndicator( message: AppLocale.imageUploading.getString(context), ); }, allowClick: false, ); try { final uploader = ImageUploader(widget.settings); for (var file in selectedImageFiles) { if (file.uploaded) { continue; } if (file.file.bytes != null) { final res = await uploader.base64( imageData: file.file.bytes, maxSize: 1024 * 1024, compressWidth: 512, compressHeight: 512, ); file.setUrl(res); } else { final res = await uploader.base64( path: file.file.path!, maxSize: 1024 * 1024, compressWidth: 512, compressHeight: 512, ); file.setUrl(res); } } } catch (e) { // ignore: use_build_context_synchronously showErrorMessageEnhanced(context, e); return; } finally { cancel(); } } // ignore: use_build_context_synchronously context.read().add( ChatMessageSendEvent( Message( Role.sender, text, user: 'me', ts: DateTime.now(), model: selectedModel?.id, type: messagetType, chatHistoryId: chatId, images: selectedImageFiles.where((e) => e.uploaded).map((e) => e.url!).toList(), flags: [ if (enableSearch) 'search', if (enableReasoning) 'reasoning', ], ), tempModel: selectedModel?.id, index: index, isResent: isResent, ), ); // ignore: use_build_context_synchronously context.read().add(RoomLoadEvent(chatAnywhereRoomId, cascading: false)); } } ================================================ FILE: lib/page/lab/avatar_selector.dart ================================================ import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/random_avatar.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; class AvatarSelectorScreen extends StatefulWidget { final AvatarUsage usage; const AvatarSelectorScreen({super.key, required this.usage}); @override State createState() => _AvatarSelectorScreenState(); } class _AvatarSelectorScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(AppLocale.avatar.getString(context)), centerTitle: true, ), body: GridView.count( crossAxisCount: 4, childAspectRatio: 0.9, padding: const EdgeInsets.all(8), children: List.generate(500, (index) { return Column( mainAxisSize: MainAxisSize.min, children: [ RandomAvatar(id: 500 + index, size: 60, usage: widget.usage), const SizedBox(height: 8), Text('${500 + index}'), ], ); }), ), ); } } ================================================ FILE: lib/page/lab/creative_models.dart ================================================ import 'package:askaide/bloc/creative_island_bloc.dart'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/column_block.dart'; import 'package:askaide/page/component/enhanced_input.dart'; import 'package:askaide/page/component/image.dart'; import 'package:askaide/page/component/image_preview.dart'; import 'package:askaide/page/component/item_selector_search.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/api/image_model.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; class CreativeModelScreen extends StatefulWidget { final SettingRepository setting; const CreativeModelScreen({super.key, required this.setting}); @override State createState() => _CreativeModelScreenState(); } class _CreativeModelScreenState extends State { List imageModels = []; List imageModelFilters = []; @override void initState() { APIServer().imageModels().then((models) { setState(() { imageModels = models; }); }); APIServer().imageModelFilters().then((filters) { setState(() { imageModelFilters = filters; }); }); context.read().add(CreativeIslandGalleryLoadEvent(mode: "all")); super.initState(); } ImageModel? selectedModel; ImageModelFilter? selectedFilter; @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( toolbarHeight: CustomSize.toolbarHeight, title: const Text( 'Creation Island History', style: TextStyle(fontSize: CustomSize.appBarTitleSize), ), centerTitle: true, ), backgroundColor: customColors.backgroundColor, body: BackgroundContainer( setting: widget.setting, backgroundColor: customColors.backgroundColor, enabled: false, child: Column( children: [ ColumnBlock( margin: const EdgeInsets.all(10), padding: const EdgeInsets.all(10), children: [ EnhancedInput( title: Text( AppLocale.model.getString(context), style: TextStyle( color: customColors.textfieldLabelColor, fontSize: 16, ), ), value: Container( alignment: Alignment.centerRight, width: MediaQuery.of(context).size.width - 200, child: Text( selectedModel?.modelName ?? 'Auto', overflow: TextOverflow.ellipsis, ), ), onPressed: () { openListSelectDialog( context, [ SelectorItem(const Text('Auto'), null), ...imageModels .map( (e) => SelectorItem( Stack( children: [ Container( padding: const EdgeInsets.only(top: 25, bottom: 10), alignment: Alignment.center, child: Text( e.modelName, textAlign: TextAlign.center, style: const TextStyle(fontSize: 14), textWidthBasis: TextWidthBasis.longestLine, ), ), Positioned( left: 0, top: 0, child: Container( padding: const EdgeInsets.symmetric( horizontal: 5, vertical: 3, ), decoration: BoxDecoration( borderRadius: CustomSize.borderRadius, color: modelTypeTagColors[e.vendor], ), child: Text( e.vendor, style: const TextStyle( fontSize: 12, color: Colors.white, ), ), ), ), ], ), e.id, search: (keywrod) { return e.modelName.toLowerCase().contains(keywrod.toLowerCase()) || e.vendor.contains(keywrod.toLowerCase()); }, ), ) .toList(), ], (value) { setState(() { if (value.value == null) { selectedModel = null; selectedFilter = null; context.read().add(CreativeIslandGalleryLoadEvent(mode: "all")); return; } selectedModel = imageModels.firstWhere((e) => e.id == value.value); if (selectedModel != null) { final matchedFilters = imageModelFilters.where((e) => e.modelId == selectedModel!.modelId).toList(); selectedFilter = matchedFilters.isNotEmpty ? matchedFilters.first : null; context .read() .add(CreativeIslandGalleryLoadEvent(mode: "all", model: selectedModel!.realModel)); } else { selectedFilter = null; context.read().add(CreativeIslandGalleryLoadEvent(mode: "all")); } }); return true; }, heightFactor: 0.8, value: selectedModel?.id, innerPadding: const EdgeInsets.symmetric( horizontal: 10, vertical: 0, ), enableSearch: true, ); }, ), if (selectedFilter != null) Row( children: [ if (selectedFilter!.previewImage != null && selectedFilter!.previewImage!.isNotEmpty) SizedBox( width: 70, height: 70, child: NetworkImagePreviewer( url: selectedFilter!.previewImage!, hidePreviewButton: true, ), ), const SizedBox(width: 20), Text(selectedFilter!.name), ], ), ], ), Expanded( child: RefreshIndicator( color: customColors.linkColor, onRefresh: () async { context.read().add(CreativeIslandGalleryLoadEvent( forceRefresh: true, mode: "all", model: selectedModel?.realModel, )); }, child: BlocConsumer( listenWhen: (previous, current) => current is CreativeIslandGalleryLoaded, buildWhen: (previous, current) => current is CreativeIslandGalleryLoaded, listener: (context, state) { if (state is CreativeIslandHistoriesAllLoaded) { if (state.error != null) { showErrorMessageEnhanced(context, state.error); } } }, builder: (context, state) { if (state is CreativeIslandGalleryLoaded) { return GridView.count( padding: const EdgeInsets.all(10), crossAxisCount: _calCrossAxisCount(context), crossAxisSpacing: 10, mainAxisSpacing: 10, children: state.items.map( (e) { return GestureDetector( onTap: () { context.push('/creative-island/${e.islandId}/history/${e.id}?show_error=true'); }, child: Container( decoration: BoxDecoration(borderRadius: CustomSize.borderRadius), child: Stack( children: [ if (e.firstImagePreview.startsWith('http://') || e.firstImagePreview.startsWith('https://')) ClipRRect( borderRadius: CustomSize.borderRadius, child: e.firstImagePreview.endsWith('.mp4') ? CachedNetworkImageEnhanced( imageUrl: e.params['image'] ?? e.firstImagePreview, fit: BoxFit.cover, height: double.infinity, ) : CachedNetworkImageEnhanced( imageUrl: e.firstImagePreview, fit: BoxFit.cover, ), ) else if (e.isProcessing) Container( padding: const EdgeInsets.all(5), decoration: BoxDecoration( borderRadius: CustomSize.borderRadius, color: const Color.fromARGB(255, 148, 124, 245), ), child: const Center( child: Text( 'Processing...', textAlign: TextAlign.center, maxLines: 4, style: TextStyle( color: Colors.white, fontSize: 10, overflow: TextOverflow.ellipsis, ), ), ), ) else Container( padding: const EdgeInsets.all(5), decoration: BoxDecoration( borderRadius: CustomSize.borderRadius, color: Colors.amber, ), child: Center( child: Text( e.answer ?? '', textAlign: TextAlign.center, maxLines: 4, style: const TextStyle( color: Colors.red, fontSize: 10, overflow: TextOverflow.ellipsis, ), ), ), ), Positioned( right: 10, bottom: 10, child: Container( padding: const EdgeInsets.symmetric( horizontal: 5, vertical: 3, ), decoration: BoxDecoration( color: customColors.backgroundColor?.withAlpha(200), borderRadius: CustomSize.borderRadius, ), child: Text( '${DateFormat('HH:mm').format(e.createdAt!.toLocal())}@${e.userId}#${e.id}', style: TextStyle( fontSize: 12, color: customColors.weakTextColor, ), ), ), ), if (e.islandName != null) Positioned( left: 0, top: 0, child: Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 5, ), decoration: BoxDecoration( borderRadius: const BorderRadius.only( topLeft: CustomSize.radius, bottomRight: CustomSize.radius, ), color: customColors.linkColor, ), child: Text( e.islandName!, style: const TextStyle( fontSize: 10, color: Colors.white, ), ), ), ) ], ), ), ); }, ).toList(), ); } return const Center(child: CircularProgressIndicator()); }, ), ), ), ], ), ), ), ); } int _calCrossAxisCount(BuildContext context) { double width = MediaQuery.of(context).size.width; if (width > CustomSize.maxWindowSize) { width = CustomSize.maxWindowSize; } return (width / 220).round(); } } ================================================ FILE: lib/page/lab/draw_board.dart ================================================ import 'dart:io'; import 'dart:typed_data'; import 'package:askaide/helper/helper.dart'; import 'package:askaide/helper/logger.dart'; import 'package:askaide/helper/platform.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/column_block.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/creative_island/draw/components/image_selector.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:file_saver/file_saver.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_drawing_board/flutter_drawing_board.dart'; import 'package:flutter_drawing_board/paint_contents.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:image_gallery_saver/image_gallery_saver.dart'; class DrawboardScreen extends StatefulWidget { const DrawboardScreen({super.key}); @override State createState() => _DrawboardScreenState(); } class _DrawboardScreenState extends State { final DrawMaskBoardController controller = DrawMaskBoardController(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('画板'), centerTitle: true, ), body: SafeArea( child: ColumnBlock( children: [ ImageSelector( onImageSelected: ({path, data}) { if (path == null || path.isEmpty) return; Navigator.push( context, MaterialPageRoute( fullscreenDialog: true, builder: (context) => Scaffold( appBar: AppBar( actions: [ IconButton( onPressed: () async { final imageData = await controller.save(); if (imageData == null) { // ignore: use_build_context_synchronously showErrorMessageEnhanced(context, '获取图片数据失败'); return; } // save imageData to file if (PlatformTool.isIOS() || PlatformTool.isAndroid()) { await ImageGallerySaver.saveImage( imageData.buffer.asUint8List(), quality: 100, ); showSuccessMessage(AppLocale.operateSuccess.getString(context)); } else { if (PlatformTool.isWindows()) { FileSaver.instance .saveAs( name: randomId(), ext: '.png', bytes: imageData.buffer.asUint8List(), mimeType: MimeType.png, ) .then((value) async { if (value == null) { return; } await File(value).writeAsBytes(imageData.buffer.asUint8List()); Logger.instance.d('File saved successfully: $value'); showSuccessMessage(AppLocale.operateSuccess.getString(context)); }); } else { FileSaver.instance .saveFile( name: randomId(), ext: 'png', bytes: imageData.buffer.asUint8List(), mimeType: MimeType.png, ) .then((value) { showSuccessMessage(AppLocale.operateSuccess.getString(context)); }); } } }, icon: const Icon(Icons.save), ), ], ), body: DrawMaskBoard( backgroundImage: File(path), controller: controller, ), ), ), ); }, title: AppLocale.referenceImage.getString(context), ), ], ), ), ); } } class DrawMaskBoardController { DrawingController? controller; Future Function()? onSave; init({ DrawingController? controller, Future Function()? onSave, }) { this.controller = controller; this.onSave = onSave; } Future save() { if (onSave == null) return Future.value(null); return onSave!(); } unsetController() { controller = null; onSave = null; } } class DrawMaskBoard extends StatefulWidget { final File backgroundImage; final DrawMaskBoardController controller; const DrawMaskBoard({ super.key, required this.backgroundImage, required this.controller, }); @override State createState() => _DrawMaskBoardState(); } class _DrawMaskBoardState extends State { final DrawingController _controller = DrawingController(); bool showBackground = true; double strokeWidth = 10; String selectedToolbar = 'draw'; @override void dispose() { widget.controller.unsetController(); _controller.dispose(); super.dispose(); } @override void initState() { super.initState(); widget.controller.init( controller: _controller, onSave: () async { setState(() { showBackground = false; }); await Future.delayed(const Duration(milliseconds: 100)); try { return _controller.getImageData(); } finally { setState(() { showBackground = true; }); } }); _controller.setStyle(color: Colors.white, strokeWidth: strokeWidth); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return Column( children: [ Column( children: [ Row( children: [ IconButton( onPressed: () { _controller.setPaintContent = SimpleLine(); setState(() { selectedToolbar = 'draw'; }); }, icon: Icon( Icons.edit, color: selectedToolbar == 'draw' ? customColors.linkColor : customColors.weakLinkColor, ), ), IconButton( onPressed: () { _controller.setPaintContent = Eraser(); setState(() { selectedToolbar = 'eraser'; }); }, icon: Icon( CupertinoIcons.bandage, color: selectedToolbar == 'eraser' ? customColors.linkColor : customColors.weakLinkColor, ), ), IconButton( icon: Icon( Icons.undo, color: customColors.weakLinkColor, ), onPressed: () { _controller.undo(); }, ), IconButton( icon: Icon( Icons.redo, color: customColors.weakLinkColor, ), onPressed: () { _controller.redo(); }, ), IconButton( icon: Icon( Icons.delete_forever_outlined, color: customColors.weakLinkColor, ), onPressed: () { _controller.clear(); }, ) ], ), Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: Row( children: [ Text( '画笔粗细', style: TextStyle( fontSize: 12, color: customColors.weakTextColor, ), ), Expanded( child: Transform.scale( scale: 0.8, child: Slider( value: strokeWidth, min: 1, max: 100, activeColor: customColors.weakLinkColor, onChanged: (value) { setState(() { strokeWidth = value; _controller.setStyle(strokeWidth: strokeWidth); }); }, ), ), ), Text( '${strokeWidth.toInt()}', style: TextStyle( fontSize: 12, color: customColors.weakTextColor, ), ), ], ), ), ], ), Expanded( child: FutureBuilder( future: decodeImageFromList(widget.backgroundImage.readAsBytesSync()), builder: (context, snapshot) { if (!snapshot.hasData) { return const Center(child: CircularProgressIndicator()); } return DrawingBoard( controller: _controller, background: Container( width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.width / snapshot.data!.width.toDouble() * snapshot.data!.height.toDouble(), color: Colors.black, child: showBackground ? Image.file(widget.backgroundImage, fit: BoxFit.fitWidth) : null, ), showDefaultActions: false, ); }, ), ), ], ); } } ================================================ FILE: lib/page/lab/prompt.dart ================================================ import 'package:askaide/helper/helper.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/effect/glass.dart'; import 'package:askaide/page/component/enhanced_button.dart'; import 'package:askaide/page/component/enhanced_textfield.dart'; import 'package:askaide/page/component/item_selector_search.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; class PromptScreen extends StatefulWidget { final String? prompt; const PromptScreen({super.key, this.prompt}); @override State createState() => _PromptScreenState(); } class _PromptScreenState extends State { TextEditingController controller = TextEditingController(text: ''); @override void initState() { super.initState(); controller.text = widget.prompt ?? ''; } @override void dispose() { controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return Scaffold( appBar: AppBar( title: Text(AppLocale.prompt.getString(context)), centerTitle: true, leading: IconButton( icon: const Icon(Icons.close), onPressed: () { Navigator.of(context).pop(); }, ), ), body: Container( padding: const EdgeInsets.all(10), child: Column(children: [ EnhancedTextField( labelText: AppLocale.prompt.getString(context), labelPosition: LabelPosition.top, inputSelector: InputSelector( title: Text( AppLocale.examples.getString(context), style: TextStyle(color: customColors.linkColor), textScaler: const TextScaler.linear(0.8), ), onTap: () { openSystemPromptSelectDialog( context, customColors, ); }, ), customColors: customColors, controller: controller, maxLines: 6, minLines: 2, maxLength: 500, hintText: AppLocale.promptHint.getString(context), ), const SizedBox(height: 20), EnhancedButton( title: AppLocale.ok.getString(context), onPressed: () { Navigator.of(context).pop(controller.text); }, ), ]), ), ); } void openSystemPromptSelectDialog( BuildContext context, CustomColors customColors, ) { openModalBottomSheet( context, (context) { return FutureBuilder( future: APIServer().prompts(), builder: (context, snapshot) { if (snapshot.hasError) { showErrorMessage(resolveError(context, snapshot.error!)); } return FractionallySizedBox( heightFactor: 0.8, child: GlassEffect( child: ItemSearchSelector( items: (snapshot.data ?? []) .map( (e) => SelectorItem( Text( e.title, textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, style: TextStyle( color: customColors.chatExampleItemText, ), ), e.content, search: (keywrod) => e.title .toLowerCase() .contains(keywrod.toLowerCase()), ), ) .toList(), onSelected: (value) { controller.text = value.value; return true; }, ), ), ); }, ); }, ); } } ================================================ FILE: lib/page/lab/user_center.dart ================================================ import 'package:askaide/bloc/account_bloc.dart'; import 'package:askaide/bloc/creative_island_bloc.dart'; import 'package:askaide/page/component/account_quota_card.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/image.dart'; import 'package:askaide/page/component/invite_card.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; class UserCenterScreen extends StatefulWidget { final SettingRepository settings; const UserCenterScreen({super.key, required this.settings}); @override State createState() => _UserCenterScreenState(); } class _UserCenterScreenState extends State { @override void initState() { context.read().add(AccountLoadEvent()); context.read().add(CreativeIslandGalleryLoadEvent(mode: "default")); super.initState(); } @override Widget build(BuildContext context) { // final customColors = Theme.of(context).extension()!; return BackgroundContainer( setting: widget.settings, child: Scaffold( appBar: AppBar( title: const Text( '我的信息', style: TextStyle(fontSize: CustomSize.appBarTitleSize), ), centerTitle: true, ), backgroundColor: Colors.transparent, body: SafeArea( child: BlocBuilder( buildWhen: (previous, current) => current is AccountLoaded, builder: (_, state) { if (state is AccountLoaded) { return BlocConsumer( listenWhen: (previous, current) => current is CreativeIslandGalleryLoaded, buildWhen: (previous, current) => current is CreativeIslandGalleryLoaded, listener: (context, state) { if (state is CreativeIslandHistoriesAllLoaded) { if (state.error != null) { showErrorMessageEnhanced(context, state.error); } } }, builder: (context, state2) { if (state2 is CreativeIslandGalleryLoaded) { return SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ AccountQuotaCard( userInfo: state.user!, onPaymentReturn: () { context.read().add(AccountLoadEvent(cache: false)); }, ), InviteCard(userInfo: state.user!), GridView.count( padding: const EdgeInsets.all(15), crossAxisCount: 4, crossAxisSpacing: 10, mainAxisSpacing: 10, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), cacheExtent: 100, children: state2.items.where((e) => e.images.isNotEmpty).map( (e) { return GestureDetector( onTap: () { context.push('/creative-island/${e.islandId}/history/${e.id}'); }, child: ClipRRect( borderRadius: CustomSize.borderRadius, child: CachedNetworkImageEnhanced( imageUrl: e.firstImagePreview, fit: BoxFit.cover, ), ), ); }, ).toList(), ), ], ), ); } return const Center(child: CircularProgressIndicator()); }); } return const Center( child: CircularProgressIndicator(), ); }, ), ), ), ); } } ================================================ FILE: lib/page/setting/account_security.dart ================================================ import 'dart:async'; import 'package:askaide/bloc/account_bloc.dart'; import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/logger.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/enhanced_popup_menu.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:fluwx/fluwx.dart'; import 'package:go_router/go_router.dart'; import 'package:settings_ui/settings_ui.dart'; class AccountSecurityScreen extends StatefulWidget { final SettingRepository settings; const AccountSecurityScreen({super.key, required this.settings}); @override State createState() => _AccountSecurityScreenState(); } class _AccountSecurityScreenState extends State { StreamSubscription? _weChatResponse; @override void dispose() { _weChatResponse?.cancel(); super.dispose(); } var wechatInstalled = false; @override void initState() { context.read().add(AccountLoadEvent()); if (Ability().enableWechatSignin) { isWeChatInstalled.then((installed) { setState(() { wechatInstalled = installed; }); if (!installed) { return; } _weChatResponse = weChatResponseEventHandler.distinct((a, b) => a == b).listen((event) { if (event is WeChatAuthResponse) { if (event.errCode != 0) { showErrorMessage(event.errStr!); return; } if (event.code == null) { showErrorMessage(AppLocale.signInFailed.getString(context)); return; } APIServer().bindWechat(code: event.code!).then((_) { context.read().add(AccountLoadEvent()); showSuccessMessage(AppLocale.operateSuccess.getString(context)); }).onError((error, stackTrace) { showErrorMessageEnhanced(context, error!); }); } }); }); } super.initState(); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( toolbarHeight: CustomSize.toolbarHeight, title: Text( AppLocale.accountSettings.getString(context), style: const TextStyle(fontSize: CustomSize.appBarTitleSize), ), centerTitle: true, actions: [ EnhancedPopupMenu( items: [ EnhancedPopupMenuItem( title: AppLocale.deleteAccount.getString(context), icon: Icons.delete_forever, iconColor: Colors.red, onTap: (ctx) { context.push('/user/destroy'); }, ), ], ) ], ), backgroundColor: customColors.backgroundColor, body: BackgroundContainer( setting: widget.settings, backgroundColor: customColors.backgroundColor, enabled: false, child: SafeArea( child: BlocConsumer( listenWhen: (previous, current) => current is AccountLoaded, listener: (context, state) { if (state is AccountLoaded) { if (state.error != null) { showErrorMessageEnhanced(context, state.error!); } } }, buildWhen: (previous, current) => current is AccountLoaded, builder: (_, state) { if (state is AccountLoaded) { return buildSettingsList( context, [ SettingsSection( title: Text(AppLocale.basicInfo.getString(context)), tiles: [ SettingsTile( title: Text(AppLocale.nickname.getString(context)), trailing: Row( children: [ Text( state.user!.user.name == null || state.user!.user.name == '' ? AppLocale.unset.getString(context) : state.user!.user.name!, style: TextStyle( color: customColors.weakTextColor?.withAlpha(200), fontSize: 13, ), ), const Icon( CupertinoIcons.chevron_forward, size: 18, color: Colors.grey, ), ], ), onPressed: (context) { openTextFieldDialog( context, title: AppLocale.setNickname.getString(context), hint: AppLocale.inputYourNickname.getString(context), maxLine: 1, maxLength: 30, defaultValue: state.user?.user.name, onSubmit: (value) { context.read().add(AccountUpdateEvent(realname: value)); return true; }, ); }, ), SettingsTile( title: Text(AppLocale.phone.getString(context)), trailing: Row( children: [ Text( state.user!.user.phone == null || state.user!.user.phone == '' ? AppLocale.bindPhone.getString(context) : state.user!.user.phone!, style: TextStyle( color: customColors.weakTextColor?.withAlpha(200), fontSize: 13, ), ), if (state.user!.user.phone == null || state.user!.user.phone == '') const SizedBox(width: 5), if (state.user!.user.phone == null || state.user!.user.phone == '') const Icon( CupertinoIcons.chevron_forward, size: 18, color: Colors.grey, ), ], ), onPressed: (context) { if (state.user!.user.phone == null || state.user!.user.phone == '') { context.push('/bind-phone?is_signin=false').then((value) => Logger.instance.d(value)); } }, ), if (Ability().enableWechatSignin && wechatInstalled) SettingsTile( title: Text(AppLocale.wechatAccount.getString(context)), trailing: Row( children: [ Text( state.user!.user.unionId == null || state.user!.user.unionId == '' ? AppLocale.bind.getString(context) : AppLocale.bound.getString(context), style: TextStyle( color: customColors.weakTextColor?.withAlpha(200), fontSize: 13, ), ), if (state.user!.user.unionId == null || state.user!.user.unionId == '') const SizedBox(width: 5), if (state.user!.user.unionId == null || state.user!.user.unionId == '') const Icon( CupertinoIcons.chevron_forward, size: 18, color: Colors.grey, ), ], ), onPressed: (context) async { if (state.user!.user.unionId == null || state.user!.user.unionId == '') { final ok = await sendWeChatAuth(scope: "snsapi_userinfo", state: "wechat_sdk_demo_test"); if (!ok) { showErrorMessage(AppLocale.installWeChat.getString(context)); } } }, ), SettingsTile( title: Text(state.user!.control.isSetPassword ? AppLocale.modifyPassword.getString(context) : AppLocale.setPassword.getString(context)), trailing: const Icon( CupertinoIcons.chevron_forward, size: 18, color: Colors.grey, ), onPressed: (context) { context.push('/user/change-password'); }, ), ], ), SettingsSection( tiles: [ SettingsTile( title: Text(AppLocale.signOut.getString(context)), trailing: const Icon( Icons.logout, size: 18, color: Colors.grey, ), onPressed: (_) { openConfirmDialog( context, AppLocale.confirmSignOut.getString(context), () { context.read().add(AccountSignOutEvent()); context.go('/login'); }, danger: true, ); }, ), ], ), ], ); } return const Center(child: CircularProgressIndicator()); }, ), ), ), ), ); } } Widget buildSettingsList( BuildContext context, List sections, ) { final customColors = Theme.of(context).extension()!; return SafeArea( top: false, child: RefreshIndicator( color: customColors.linkColor, displacement: 20, onRefresh: () async { context.read().add(AccountLoadEvent()); }, child: SettingsList( platform: DevicePlatform.iOS, lightTheme: SettingsThemeData( settingsListBackground: Colors.transparent, settingsSectionBackground: customColors.settingsSectionBackground, ), darkTheme: SettingsThemeData( settingsListBackground: Colors.transparent, settingsSectionBackground: customColors.settingsSectionBackground, titleTextColor: const Color.fromARGB(255, 239, 239, 239), ), sections: sections, contentPadding: const EdgeInsets.all(0), ), ), ); } ================================================ FILE: lib/page/setting/article.dart ================================================ import 'package:askaide/helper/ability.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/chat/markdown.dart'; import 'package:askaide/page/component/column_block.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/api/article.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import 'package:url_launcher/url_launcher_string.dart'; class ArticleScreen extends StatefulWidget { final SettingRepository settings; final int id; const ArticleScreen({super.key, required this.settings, required this.id}); @override State createState() => _ArticleScreenState(); } class _ArticleScreenState extends State { Article article = Article( id: 0, title: 'Title', content: 'Content', ); @override void initState() { APIServer().article(id: widget.id).then((value) { setState(() { article = value; }); }); super.initState(); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( title: Text( article.title, style: const TextStyle(fontSize: CustomSize.appBarTitleSize), ), toolbarHeight: CustomSize.toolbarHeight, centerTitle: true, leading: IconButton( icon: Icon( Icons.close, color: customColors.weakLinkColor, ), onPressed: () { if (context.canPop()) { context.pop(); } else { context.go(Ability().homeRoute); } }, ), ), backgroundColor: customColors.backgroundColor, body: BackgroundContainer( setting: widget.settings, enabled: false, backgroundColor: customColors.backgroundColor, child: SafeArea( top: false, child: Container( padding: const EdgeInsets.symmetric(horizontal: 10), child: SingleChildScrollView( child: ColumnBlock( padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 15), children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Author: ${article.author ?? 'Admin'}', style: TextStyle( fontSize: 12, color: customColors.weakTextColor, ), ), if (article.createdAt != null) Text( DateFormat('yyyy/MM/dd HH:mm').format(article.createdAt!.toLocal()), style: TextStyle( fontSize: 12, color: customColors.weakTextColor?.withAlpha(100), ), ), ], ), const SizedBox(height: 10), Markdown( data: article.content, onUrlTap: (value) { if (value.startsWith("aidea-app://")) { var route = value.substring('aidea-app://'.length); context.push(route); } else { launchUrlString(value); } }, ), ], ) ], ), ), ), ), ), ), ); } } ================================================ FILE: lib/page/setting/background_selector.dart ================================================ import 'dart:ui'; import 'package:askaide/bloc/background_image_bloc.dart'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/haptic_feedback.dart'; import 'package:askaide/helper/helper.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/enhanced_button.dart'; import 'package:askaide/page/component/image.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; class BackgroundSelectorScreen extends StatefulWidget { final SettingRepository setting; const BackgroundSelectorScreen({super.key, required this.setting}); @override State createState() => _BackgroundSelectorScreenState(); } class _BackgroundSelectorScreenState extends State { final TextEditingController _controller = TextEditingController(); bool selectDialogOpened = false; double blur = 10; bool showOriginalImage = false; @override void initState() { super.initState(); context.read().add(BackgroundImageLoadEvent()); _controller.text = widget.setting.stringDefault(settingBackgroundImage, ''); blur = widget.setting.doubleDefault(settingBackgroundImageBlur, 10); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return Scaffold( appBar: AppBar( title: Text(AppLocale.backgroundSetting.getString(context)), centerTitle: true, ), backgroundColor: customColors.backgroundContainerColor, body: SafeArea( child: SingleChildScrollView( child: Container( padding: const EdgeInsets.all(10), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('图片选择'), const SizedBox(height: 10), BlocBuilder( buildWhen: (previous, current) => current is BackgroundImageLoaded, builder: (context, state) { if (state is BackgroundImageLoaded) { return GridView.count( crossAxisCount: 5, shrinkWrap: true, mainAxisSpacing: 5, crossAxisSpacing: 5, physics: const NeverScrollableScrollPhysics(), children: [ InkWell( onTap: () { setState(() { _controller.text = ''; blur = 0; }); }, child: Stack( children: [ ClipRRect( borderRadius: CustomSize.borderRadius, child: Image.asset('assets/light-dark-auto.png'), ), Positioned( child: Container( alignment: Alignment.center, child: const Text( '跟随系统', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: Color.fromARGB(255, 146, 146, 146), ), ), ), ), ], ), ), for (var img in state.images) InkWell( onTap: () { setState(() { _controller.text = img.url; }); }, child: ClipRRect( borderRadius: CustomSize.borderRadius, child: CachedNetworkImageEnhanced(imageUrl: img.preview), ), ), Material( borderRadius: CustomSize.borderRadius, child: InkWell( borderRadius: CustomSize.borderRadiusAll, onTap: () async { if (selectDialogOpened) return; selectDialogOpened = true; HapticFeedbackHelper.mediumImpact(); FilePickerResult? result = await FilePicker.platform .pickFiles(type: FileType.image) .whenComplete(() => selectDialogOpened = false); if (result != null && result.files.isNotEmpty) { setState(() { _controller.text = result.files.first.path!; }); } }, child: Container( width: MediaQuery.of(context).size.width - 20, decoration: BoxDecoration( border: Border.all( color: customColors.textFieldBorderColor!, style: BorderStyle.solid, ), borderRadius: CustomSize.borderRadius, ), padding: const EdgeInsets.all(10), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.camera_alt, size: 30, color: customColors.chatInputPanelText, ), Text( AppLocale.custom.getString(context), style: TextStyle( fontSize: 10, fontWeight: FontWeight.w500, color: customColors.chatInputPanelText?.withOpacity(0.8), ), ), ], ), ), ), ), ], ); } return const Center( child: CircularProgressIndicator(), ); }, ), const SizedBox(height: 10), const SizedBox(height: 10), const Text('图片预览'), const SizedBox(height: 10), ClipRRect( borderRadius: CustomSize.borderRadius, child: GestureDetector( onLongPressStart: (details) { setState(() { showOriginalImage = true; }); }, onLongPressEnd: (details) { setState(() { showOriginalImage = false; }); }, child: Container( decoration: BoxDecoration( image: _controller.text != '' ? DecorationImage( image: resolveImageProvider(_controller.text), fit: BoxFit.cover, ) : null, color: customColors.backgroundContainerColor, borderRadius: CustomSize.borderRadius, border: Border.all( color: customColors.textFieldBorderColor!, style: BorderStyle.solid, ), ), child: BackdropFilter( filter: ImageFilter.blur( sigmaX: showOriginalImage ? 0 : blur, sigmaY: showOriginalImage ? 0 : blur, ), child: const SizedBox(width: double.infinity, height: 200), ), ), ), ), const SizedBox(height: 20), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('模糊程度'), Text(blur.toStringAsFixed(0)), ], ), Slider( value: blur, min: 0, max: 50, divisions: 10, label: blur == 0 ? '无模糊' : '模糊程度:${blur.toStringAsFixed(0)}', activeColor: customColors.linkColor, onChanged: (value) { setState(() { blur = value; }); }, ), const SizedBox(height: 10), EnhancedButton( onPressed: () { widget.setting.set(settingBackgroundImageBlur, blur.toString()); final originalFilepath = widget.setting.get(settingBackgroundImage); if (originalFilepath != _controller.text) { // 移除原图 if (originalFilepath != null && originalFilepath != '' && !originalFilepath.startsWith('http')) { removeExternalFile(originalFilepath); } // 复制新图 if (_controller.text != '') { if (!_controller.text.startsWith('http')) { copyExternalFileToAppDocs(_controller.text).then((value) { widget.setting.set(settingBackgroundImage, value); }); } else { widget.setting.set(settingBackgroundImage, _controller.text); } } else { // 恢复为原图 widget.setting.set(settingBackgroundImage, ''); } } showSuccessMessage(AppLocale.operateSuccess.getString(context)); // Navigator.pop(context); }, title: AppLocale.save.getString(context), ), ], ), ), ), ), ); } } ================================================ FILE: lib/page/setting/bind_phone_page.dart ================================================ import 'dart:convert'; import 'package:askaide/bloc/account_bloc.dart'; import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/helper.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/verify_code_input.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; class BindPhoneScreen extends StatefulWidget { final SettingRepository setting; final bool isSignIn; const BindPhoneScreen({super.key, required this.setting, this.isSignIn = true}); @override State createState() => _BindPhoneScreenState(); } class _BindPhoneScreenState extends State { final TextEditingController _usernameController = TextEditingController(); final TextEditingController _inviteCodeController = TextEditingController(); final TextEditingController _verificationCodeController = TextEditingController(); String verifyCodeId = ''; final phoneNumberValidator = RegExp(r"^1[3456789]\d{9}$"); @override void initState() { super.initState(); context.read().add(AccountLoadEvent(cache: false)); // Clipboard.getData(Clipboard.kTextPlain).then((value) { // if (value == null || value.text == null || value.text == '') { // return; // } // if (value.text!.trim().contains(RegExp(r'\$AIDEA\.INV\.\w+\$'))) { // final match = RegExp(r'\$AIDEA\.INV\.(\w+)\$').firstMatch(value.text!); // if (match != null) { // final val = match.group(1); // if (val != null) { // _inviteCodeController.text = val; // } // } // } // }); } @override void dispose() { _inviteCodeController.dispose(); _usernameController.dispose(); _verificationCodeController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { var customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( toolbarHeight: CustomSize.toolbarHeight, title: Text( AppLocale.bindPhone.getString(context), style: const TextStyle( fontSize: CustomSize.appBarTitleSize, ), ), centerTitle: true, leading: IconButton( onPressed: () { if (widget.isSignIn) { context.go('${Ability().homeRoute}?show_initial_dialog=false&reward=0'); } else { context.pop(); } // 当返回值为 logout 时,表示需要退出登录 // if (widget.isSignIn) { // context.pop('logout'); // } else { // context.pop(); // } }, icon: Icon(widget.isSignIn ? Icons.close : Icons.arrow_back_ios), ), ), backgroundColor: customColors.backgroundColor, body: BackgroundContainer( setting: widget.setting, enabled: false, backgroundColor: customColors.backgroundColor, child: BlocBuilder( buildWhen: (previous, current) => current is AccountLoaded, builder: (context, state) { if (state is AccountLoaded) { return Column( children: [ Padding( padding: const EdgeInsets.only(left: 15.0, right: 15.0, top: 15, bottom: 0), child: TextFormField( controller: _usernameController, keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.singleLineFormatter], decoration: InputDecoration( border: const OutlineInputBorder(), enabledBorder: const OutlineInputBorder( borderSide: BorderSide(color: Color.fromARGB(200, 192, 192, 192)), ), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: customColors.linkColor ?? Colors.green), ), floatingLabelStyle: TextStyle(color: customColors.linkColor!), isDense: true, floatingLabelBehavior: FloatingLabelBehavior.always, labelText: AppLocale.account.getString(context), labelStyle: const TextStyle(fontSize: 17), hintText: AppLocale.phoneInputTips.getString(context), hintStyle: TextStyle( color: customColors.textfieldHintColor, fontSize: 15, ), ), ), ), Padding( padding: const EdgeInsets.only(left: 15.0, right: 5.0, top: 15, bottom: 0), child: VerifyCodeInput( controller: _verificationCodeController, onVerifyCodeSent: (id) { verifyCodeId = id; }, sendVerifyCode: () { return APIServer().sendBindPhoneCode(_usernameController.text.trim()); }, sendCheck: () { final username = _usernameController.text.trim(); final isPhoneNumber = phoneNumberValidator.hasMatch(username); if (!isPhoneNumber) { showErrorMessage(AppLocale.phoneNumberFormatError.getString(context)); return false; } return true; }, ), ), if (state.user!.user.invitedBy == null || state.user!.user.invitedBy == 0) Padding( padding: const EdgeInsets.only(left: 15.0, right: 15.0, top: 15, bottom: 0), child: TextFormField( controller: _inviteCodeController, inputFormatters: [FilteringTextInputFormatter.singleLineFormatter], decoration: InputDecoration( border: const OutlineInputBorder(), enabledBorder: const OutlineInputBorder( borderSide: BorderSide(color: Color.fromARGB(200, 192, 192, 192)), ), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: customColors.linkColor!), ), floatingLabelStyle: TextStyle(color: customColors.linkColor!), isDense: true, floatingLabelBehavior: FloatingLabelBehavior.always, labelText: AppLocale.inviteCode.getString(context), labelStyle: const TextStyle(fontSize: 17), hintText: AppLocale.inviteCodeInputTips.getString(context), hintStyle: TextStyle( color: customColors.textfieldHintColor, fontSize: 15, ), ), ), ), const SizedBox(height: 15), Container( height: 45, width: double.infinity, margin: const EdgeInsets.symmetric(horizontal: 15), decoration: BoxDecoration( color: customColors.linkColor, borderRadius: CustomSize.borderRadius, ), child: TextButton( onPressed: onSubmit, child: Text( AppLocale.ok.getString(context), style: const TextStyle(color: Colors.white, fontSize: 18), ), ), ), ], ); } return const Center(child: CircularProgressIndicator()); }, ), ), ), ); } onSubmit() { final username = _usernameController.text.trim(); if (username == '') { showErrorMessage(AppLocale.accountRequired.getString(context)); return; } if (!phoneNumberValidator.hasMatch(username)) { showErrorMessage(AppLocale.phoneNumberFormatError.getString(context)); return; } if (verifyCodeId == '') { showErrorMessage(AppLocale.pleaseGetVerifyCodeFirst.getString(context)); return; } final verificationCode = _verificationCodeController.text.trim(); if (verificationCode == '') { showErrorMessage(AppLocale.verifyCodeRequired.getString(context)); return; } if (verificationCode.length != 6) { showErrorMessage(AppLocale.verifyCodeFormatError.getString(context)); return; } final inviteCode = _inviteCodeController.text.trim(); if (inviteCode != '' && inviteCode.length > 20) { showErrorMessage(AppLocale.inviteCodeFormatError.getString(context)); return; } final cancel = BotToast.showCustomLoading( toastBuilder: (cancel) { return LoadingIndicator( message: AppLocale.processingWait.getString(context), ); }, allowClick: false, duration: const Duration(seconds: 120), ); APIServer() .bindPhone( username: username, verifyCodeId: verifyCodeId, verifyCode: verificationCode, inviteCode: inviteCode, ) .then((value) async { await widget.setting.set(settingAPIServerToken, value.token); await widget.setting.set(settingUserInfo, jsonEncode(value)); if (widget.isSignIn) { if (context.mounted) { // ignore: use_build_context_synchronously context.go( '${Ability().homeRoute}?show_initial_dialog=${value.isNewUser ? "true" : "false"}&reward=${value.reward}'); } } else { if (context.mounted) { // ignore: use_build_context_synchronously showSuccessMessage(AppLocale.operateSuccess.getString(context)); } } }).catchError((e) { showErrorMessage(resolveError(context, e)); }).whenComplete(() => cancel()); } } ================================================ FILE: lib/page/setting/change_password.dart ================================================ import 'package:askaide/helper/helper.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/column_block.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/password_field.dart'; import 'package:askaide/page/component/verify_code_input.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; class ChangePasswordScreen extends StatefulWidget { final SettingRepository setting; const ChangePasswordScreen({super.key, required this.setting}); @override State createState() => _ChangePasswordScreenState(); } class _ChangePasswordScreenState extends State { final TextEditingController _passwordController = TextEditingController(); final TextEditingController _verificationCodeController = TextEditingController(); String verifyCodeId = ''; @override void dispose() { _passwordController.dispose(); _verificationCodeController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { var customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( toolbarHeight: CustomSize.toolbarHeight, title: Text( AppLocale.modifyPassword.getString(context), style: const TextStyle(fontSize: CustomSize.appBarTitleSize), ), centerTitle: true, ), backgroundColor: customColors.backgroundColor, body: BackgroundContainer( setting: widget.setting, backgroundColor: customColors.backgroundColor, enabled: false, child: Container( padding: const EdgeInsets.all(10), child: Column( children: [ ColumnBlock( innerPanding: 15, padding: const EdgeInsets.only(top: 20, left: 10, right: 10), showDivider: false, children: [ PasswordField( controller: _passwordController, labelText: AppLocale.newPassword.getString(context), hintText: AppLocale.passwordInputTips.getString(context), inColumnBlock: false, ), VerifyCodeInput( inColumnBlock: false, controller: _verificationCodeController, onVerifyCodeSent: (id) { verifyCodeId = id; }, sendVerifyCode: () { return APIServer().sendResetPasswordCodeForSignedUser(); }, sendCheck: () { return true; }, ), ], ), Container( height: 45, width: double.infinity, decoration: BoxDecoration( color: customColors.linkColor, borderRadius: CustomSize.borderRadius, ), child: TextButton( onPressed: onResetSubmit, child: Text( AppLocale.ok.getString(context), style: const TextStyle(color: Colors.white, fontSize: 18), ), ), ), ], ), ), ), ), ); } onResetSubmit() { final password = _passwordController.text.trim(); if (password == '' || password.length < 8 || password.length > 20) { showErrorMessage(AppLocale.passwordFormatError.getString(context)); return; } if (verifyCodeId == '') { showErrorMessage(AppLocale.pleaseGetVerifyCodeFirst.getString(context)); return; } final verificationCode = _verificationCodeController.text.trim(); if (verificationCode == '') { showErrorMessage(AppLocale.verifyCodeRequired.getString(context)); return; } if (verificationCode.length != 6) { showErrorMessage(AppLocale.verifyCodeFormatError.getString(context)); return; } final cancel = BotToast.showCustomLoading( toastBuilder: (cancel) { return LoadingIndicator( message: AppLocale.processingWait.getString(context), ); }, allowClick: false, duration: const Duration(seconds: 120), ); APIServer() .resetPasswordByCodeSignedUser( password: password, verifyCodeId: verifyCodeId, verifyCode: verificationCode, ) .then((value) { showSuccessMessage(AppLocale.operateSuccess.getString(context)); if (context.canPop()) { context.pop(); } }).catchError((e) { showErrorMessage(resolveError(context, e)); }).whenComplete(() => cancel()); } } ================================================ FILE: lib/page/setting/custom_home_models.dart ================================================ import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/helper.dart'; import 'package:askaide/helper/image.dart'; import 'package:askaide/helper/logger.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/column_block.dart'; import 'package:askaide/page/component/enhanced_button.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/message_box.dart'; import 'package:askaide/page/component/model_indicator.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/random_avatar.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/api/model.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; class CustomHomeModelsPage extends StatefulWidget { final SettingRepository setting; const CustomHomeModelsPage({super.key, required this.setting}); @override State createState() => _CustomHomeModelsPageState(); } class _CustomHomeModelsPageState extends State { List models = [ HomeModelV2( type: 'model', id: 'openai:gpt-3.5-turbo', supportVision: false, name: 'GPT-3.5', ), HomeModelV2( type: 'model', id: 'openai:gpt-4', supportVision: false, name: 'GPT-4', ), HomeModelV2( type: 'model', id: '', supportVision: false, name: 'Unset', ), ]; @override void initState() { if (Ability().homeModels.isNotEmpty) { models = Ability().homeModels; if (models.length < 3) { models.add(HomeModelV2( type: 'model', id: '', supportVision: false, name: 'Unset', )); } } APIServer().capabilities(cache: false).then((cap) { Ability().updateCapabilities(cap); if (cap.homeModels.isNotEmpty) { models = cap.homeModels; if (models.length < 3) { models.add(HomeModelV2( type: 'model', id: '', supportVision: false, name: 'Unset', )); } if (mounted) { setState(() {}); } } }); super.initState(); } @override Widget build(BuildContext context) { var customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( toolbarHeight: CustomSize.toolbarHeight, title: Text( AppLocale.customHomeModels.getString(context), style: const TextStyle(fontSize: CustomSize.appBarTitleSize), ), centerTitle: true, elevation: 0, ), backgroundColor: customColors.backgroundContainerColor, body: BackgroundContainer( setting: widget.setting, enabled: false, child: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( children: [ const MessageBox( message: '用于设置聊一聊中的常用模型。模型 3 为可选项,长按可重置', type: MessageBoxType.info, ), const SizedBox(height: 10), ColumnBlock( innerPanding: 5, padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 15), children: [ for (var i = 0; i < models.length; i++) GestureDetector( onTap: () { openSelectCustomModelDialog( context, (selected) { setState(() { models[i] = selected; }); }, initValue: models[i].id, ); }, onLongPress: () { if (models[i].id.isNotEmpty && i == models.length - 1) { openConfirmDialog( context, '确认重置该模型?', () { setState(() { models[i] = HomeModelV2( type: 'model', id: '', supportVision: false, name: 'Unset', ); }); }, confirmText: AppLocale.reset.getString(context), ); } }, child: Stack( children: [ Container( padding: const EdgeInsets.symmetric(vertical: 15), width: double.infinity, decoration: BoxDecoration( color: iconAndColors[i].color, borderRadius: CustomSize.borderRadius, ), child: ModelIndicator( model: models[i], iconAndColor: iconAndColors[i], selected: true, ), ), Positioned( right: 0, top: 0, child: Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 5, ), child: Text( '模型 ${i + 1}', style: const TextStyle( color: Colors.white, fontSize: 10, ), ), ), ), ], ), ), ], ), const SizedBox(height: 10), EnhancedButton( title: AppLocale.save.getString(context), onPressed: () async { final cancelLoading = BotToast.showCustomLoading( toastBuilder: (cancel) { return LoadingIndicator( message: AppLocale.processingWait.getString(context), ); }, allowClick: false, duration: const Duration(seconds: 120), ); try { final selectedModels = models.where((e) => e.id != '').map((e) => e.uniqueKey).toList(); await APIServer().updateCustomHomeModelsV2(models: selectedModels); APIServer().capabilities(cache: false).then((value) => Ability().updateCapabilities(value)); showSuccessMessage( // ignore: use_build_context_synchronously AppLocale.operateSuccess.getString(context)); } catch (e) { // ignore: use_build_context_synchronously showErrorMessageEnhanced(context, e); } finally { cancelLoading(); } }, ), ], ), ), ), ), ); } } void openSelectCustomModelDialog( BuildContext context, Function(HomeModelV2 selected) onSelected, { String? initValue, }) { openModalBottomSheet( context, (context) { return FutureBuilder( future: APIServer().customHomeModelsV2(cache: false), builder: (context, snapshot) { if (snapshot.hasError) { showErrorMessage(resolveError(context, snapshot.error!)); } if (!snapshot.hasData) { return const Center(child: CircularProgressIndicator()); } return HomeModelItem( models: snapshot.data!, onSelected: (selected) { onSelected(selected); context.pop(); }, initValue: initValue, ); }); }, heightFactor: 0.9, ); } class HomeModelItem extends StatelessWidget { final List models; final Function(HomeModelV2 selected) onSelected; final String? initValue; const HomeModelItem({ super.key, required this.models, required this.onSelected, this.initValue, }); @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; var modelsByType = >{ 'model': [], 'room_gallery': [], 'rooms': [], }; for (var model in models) { modelsByType[model.type]!.add(model); } return models.isNotEmpty ? Padding( padding: const EdgeInsets.only(top: 15), child: DefaultTabController( length: modelsByType.length, child: Column( mainAxisSize: MainAxisSize.min, children: [ TabBar( indicatorColor: customColors.linkColor, labelColor: customColors.linkColor, unselectedLabelColor: customColors.textfieldLabelColor, tabs: const [ Tab(text: '模型'), Tab(text: '内置数字人'), Tab(text: '我的数字人'), ], ), const SizedBox(height: 10), Expanded( child: TabBarView( children: [ buildTabView( context, customColors, models.where((e) => e.type == 'model').toList(), ), buildTabView( context, customColors, models.where((e) => e.type == 'room_gallery').toList(), ), buildTabView( context, customColors, models.where((e) => e.type == 'rooms').toList(), ), ], ), ), ], ), ), ) : const Center( child: Text( '没有可用模型\n请先登录或者配置 OpenAI 的 Keys', textAlign: TextAlign.center, ), ); } Widget buildTabView( BuildContext context, CustomColors customColors, List models, ) { return ListView.separated( shrinkWrap: true, itemCount: models.length, itemBuilder: (context, i) { var item = models[i]; if (item.avatarUrl == null) { Logger.instance.w(item.toJson()); } return ListTile( title: Container( alignment: Alignment.center, padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisSize: MainAxisSize.min, children: [ buildAvatar(avatarUrl: item.avatarUrl, size: 40), const SizedBox(width: 20), Expanded( child: Container( alignment: Alignment.centerLeft, child: Row(children: [ Text( item.name, overflow: TextOverflow.ellipsis, ), ]), ), ), SizedBox( width: 10, child: Icon( Icons.check, color: initValue == item.id ? customColors.linkColor : Colors.transparent, ), ), ], ), ), onTap: () { onSelected(item); }, ); }, separatorBuilder: (BuildContext context, int index) { return Divider( height: 1, color: customColors.columnBlockDividerColor, ); }, ); } Widget buildAvatar({String? avatarUrl, int? id, int size = 30}) { if (avatarUrl != null && avatarUrl.startsWith('http')) { return RemoteAvatar( avatarUrl: imageURL(avatarUrl, qiniuImageTypeAvatar), size: size, ); } return RandomAvatar( id: id ?? 0, size: size, usage: Ability().isUserLogon() ? AvatarUsage.room : AvatarUsage.legacy, ); } } ================================================ FILE: lib/page/setting/destroy_account.dart ================================================ import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/helper.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/column_block.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/message_box.dart'; import 'package:askaide/page/component/verify_code_input.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; class DestroyAccountScreen extends StatefulWidget { final SettingRepository setting; const DestroyAccountScreen({super.key, required this.setting}); @override State createState() => _DestroyAccountScreenState(); } class _DestroyAccountScreenState extends State { final TextEditingController _passwordController = TextEditingController(); final TextEditingController _verificationCodeController = TextEditingController(); String verifyCodeId = ''; @override void dispose() { _passwordController.dispose(); _verificationCodeController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { var customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( toolbarHeight: CustomSize.toolbarHeight, title: Text( AppLocale.deleteAccount.getString(context), style: const TextStyle(fontSize: CustomSize.appBarTitleSize), ), centerTitle: true, ), backgroundColor: customColors.backgroundColor, body: BackgroundContainer( setting: widget.setting, enabled: false, backgroundColor: customColors.backgroundColor, child: Container( padding: const EdgeInsets.all(10), child: Column( children: [ const MessageBox( message: '请注意,注销账号后:\n1. 您的数据将被清空,包括角色、创作岛历史纪录、充值数据、智慧果使用明细等全部数据;\n2. 您未使用完的智慧果将会被销毁,无法继续使用,无法退回;\n3. 注销操作不可逆,一旦账号注销,所有被删除数据均无法恢复。', type: MessageBoxType.warning, ), const SizedBox(height: 15), ColumnBlock( padding: const EdgeInsets.only(top: 20, left: 10, right: 10, bottom: 20), children: [ VerifyCodeInput( inColumnBlock: false, controller: _verificationCodeController, onVerifyCodeSent: (id) { verifyCodeId = id; }, sendVerifyCode: () { return APIServer().sendDestroyAccountSMSCode(); }, sendCheck: () { return true; }, ), ], ), const SizedBox(height: 15), Container( height: 45, width: double.infinity, decoration: BoxDecoration(color: Colors.red, borderRadius: CustomSize.borderRadius), child: TextButton( onPressed: onDestroySubmit, child: Text( AppLocale.confirmDeleteAccount.getString(context), style: const TextStyle(color: Colors.white, fontSize: 18), ), ), ), ], ), ), ), ), ); } onDestroySubmit() { if (verifyCodeId == '') { showErrorMessage(AppLocale.pleaseGetVerifyCodeFirst.getString(context)); return; } final verificationCode = _verificationCodeController.text.trim(); if (verificationCode == '') { showErrorMessage(AppLocale.verifyCodeRequired.getString(context)); return; } if (verificationCode.length != 6) { showErrorMessage(AppLocale.verifyCodeFormatError.getString(context)); return; } final cancel = BotToast.showCustomLoading( toastBuilder: (cancel) { return LoadingIndicator( message: AppLocale.processingWait.getString(context), ); }, allowClick: false, duration: const Duration(seconds: 120), ); APIServer() .destroyAccount( verifyCodeId: verifyCodeId, verifyCode: verificationCode, ) .then((value) async { await widget.setting.set(settingAPIServerToken, ''); await widget.setting.set(settingUserInfo, ''); showSuccessMessage('账号注销成功'); if (context.mounted) { // ignore: use_build_context_synchronously context.go('/login'); } }).catchError((e) { showErrorMessage(resolveError(context, e)); }).whenComplete(() => cancel()); } } ================================================ FILE: lib/page/setting/diagnosis.dart ================================================ import 'dart:io'; import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/logger.dart'; import 'package:askaide/helper/path.dart'; import 'package:askaide/helper/platform.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/column_block.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:quickalert/quickalert.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; class DiagnosisScreen extends StatefulWidget { final SettingRepository setting; const DiagnosisScreen({super.key, required this.setting}); @override State createState() => _DiagnosisScreenState(); } class _DiagnosisScreenState extends State { String diagnosisInfo = ''; bool isUploaded = false; final ScrollController _controller = ScrollController(); @override void initState() { super.initState(); if (!PlatformTool.isWeb()) { File(PathHelper().getLogfilePath).exists().then( (exist) => { if (exist) File(PathHelper().getLogfilePath).readAsString().then((value) { setState(() { diagnosisInfo = value; }); Future.delayed(const Duration(milliseconds: 100), () { _controller.jumpTo(_controller.position.maxScrollExtent); }); }) else setState(() { diagnosisInfo = 'No log file found'; }) }, ); } } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( backgroundColor: customColors.backgroundColor, appBar: AppBar( toolbarHeight: CustomSize.toolbarHeight, title: Text( AppLocale.errorLog.getString(context), style: const TextStyle( fontSize: CustomSize.appBarTitleSize, ), ), centerTitle: true, actions: [ TextButton( onPressed: () { openConfirmDialog( context, 'This action will erase all settings and data, do you want to proceed?', () async { final databasePath = (await databaseFactory.getDatabasesPath()).replaceAll('\\', '/'); Logger.instance.d('databasePath: $databasePath'); try { // 删除数据库目录 await Directory(databasePath).delete( recursive: true, ); showSuccessMessage( // ignore: use_build_context_synchronously AppLocale.operateSuccess.getString(context), ); SystemChannels.platform.invokeMethod('SystemNavigator.pop'); } catch (e) { Logger.instance.e(e); showBeautyDialog( // ignore: use_build_context_synchronously context, type: QuickAlertType.error, text: 'Data file deletion failed. Please close the application first, manually delete the directory $databasePath, and then restart the application.', ); } }, danger: true, ); }, child: Text( '重置系统', style: TextStyle( color: isUploaded ? customColors.weakTextColor?.withAlpha(100) : customColors.weakLinkColor, fontSize: 12, ), ), ), if (diagnosisInfo.isNotEmpty) TextButton( onPressed: () { if (isUploaded) { showSuccessMessage('已上报'); return; } APIServer().diagnosisUpload(data: diagnosisInfo).then((value) { showSuccessMessage('上报成功'); setState(() { isUploaded = true; }); }).onError((error, stackTrace) { showErrorMessageEnhanced(context, error!); }); }, child: Text( AppLocale.report.getString(context), style: TextStyle( color: isUploaded ? customColors.weakTextColor?.withAlpha(100) : customColors.weakLinkColor, fontSize: 12, ), ), ), ], ), body: BackgroundContainer( setting: widget.setting, backgroundColor: customColors.backgroundColor, enabled: false, child: Container( padding: const EdgeInsets.all(10), child: SingleChildScrollView( controller: _controller, child: Column( children: [ ColumnBlock( innerPanding: 5, padding: const EdgeInsets.all(10), children: [ Text( 'Server: ${APIServer().url}', style: const TextStyle( fontSize: 10, ), ), Text( 'User ID: ${APIServer().localUserID()}', style: const TextStyle( fontSize: 10, ), ), const Text( 'Client Version: $clientVersion', style: TextStyle( fontSize: 10, ), ), Text( 'OS: ${PlatformTool.operatingSystem()} | ${PlatformTool.operatingSystemVersion()}', style: const TextStyle( fontSize: 10, ), ), Text( 'OpenAI Custom: ${Ability().enableLocalOpenAI}', style: const TextStyle( fontSize: 10, ), ), FutureBuilder( future: databaseFactory.getDatabasesPath(), builder: (context, snapshot) { return Text( 'Local Database: ${snapshot.data?.replaceAll('\\', '/')}', style: const TextStyle( fontSize: 10, ), ); }, ), Text( 'Log File: ${PathHelper().getLogfilePath}', style: const TextStyle( fontSize: 10, ), ), Text( 'Cache Directory: ${PathHelper().getCachePath}', style: const TextStyle( fontSize: 10, ), ), Text( 'Main Directory: ${PathHelper().getHomePath}', style: const TextStyle( fontSize: 10, ), ), ], ), ColumnBlock( children: [ Text( diagnosisInfo, style: const TextStyle( fontSize: 10, ), ), ], ), ], ), ), ), ), ), ); } } ================================================ FILE: lib/page/setting/notification.dart ================================================ import 'package:askaide/helper/haptic_feedback.dart'; import 'package:askaide/helper/helper.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/page/data/notification_datasource.dart'; import 'package:askaide/repo/api/notification.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:go_router/go_router.dart'; import 'package:loading_more_list/loading_more_list.dart'; class NotificationScreen extends StatefulWidget { final SettingRepository setting; const NotificationScreen({super.key, required this.setting}); @override State createState() => _NotificationScreenState(); } class _NotificationScreenState extends State { final NotificationDatasource datasource = NotificationDatasource(); @override void dispose() { datasource.dispose(); super.dispose(); } @override Widget build(BuildContext context) { var customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( title: Text( AppLocale.notification.getString(context), style: const TextStyle(fontSize: CustomSize.appBarTitleSize), ), toolbarHeight: CustomSize.toolbarHeight, centerTitle: true, ), backgroundColor: customColors.backgroundColor, body: BackgroundContainer( setting: widget.setting, backgroundColor: customColors.backgroundColor, enabled: false, child: SafeArea( top: false, left: false, right: false, child: RefreshIndicator( color: customColors.linkColor, displacement: 20, onRefresh: () { return datasource.refresh(); }, child: LoadingMoreList( ListConfig( itemBuilder: (context, item, index) { return NotifyMessageItem( message: item, customColors: customColors, onTap: () { context .push(Uri(path: '/article', queryParameters: {'id': item.articleId.toString()}).toString()); }, ); }, sourceList: datasource, indicatorBuilder: (context, status) { String msg = ''; switch (status) { case IndicatorStatus.noMoreLoad: msg = ''; break; case IndicatorStatus.loadingMoreBusying: msg = 'Loading...'; break; case IndicatorStatus.error: msg = 'Failed to load, please try again later.'; break; case IndicatorStatus.empty: msg = 'No data'; break; default: return const Center(child: LoadingIndicator()); } return Container( padding: const EdgeInsets.all(15), alignment: Alignment.center, child: Text( msg, style: TextStyle( color: customColors.weakTextColor, fontSize: 14, ), ), ); }, ), ), ), ), ), ), ); } } class NotifyMessageItem extends StatelessWidget { const NotifyMessageItem({ super.key, required this.message, required this.customColors, required this.onTap, }); final NotifyMessage message; final CustomColors customColors; final VoidCallback onTap; @override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.symmetric( horizontal: 15, vertical: 5, ), decoration: BoxDecoration(borderRadius: CustomSize.borderRadius), child: Slidable( endActionPane: ActionPane( motion: const ScrollMotion(), children: [ const SizedBox(width: 10), SlidableAction( label: 'Details', borderRadius: CustomSize.borderRadiusAll, backgroundColor: Colors.green, icon: Icons.info_outline, onPressed: (_) { HapticFeedbackHelper.lightImpact(); onTap(); }, ), ], ), child: Material( color: customColors.listTileBackgroundColor, borderRadius: CustomSize.borderRadius, child: InkWell( child: ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), shape: RoundedRectangleBorder(borderRadius: CustomSize.borderRadius), title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( message.title.trim(), overflow: TextOverflow.ellipsis, style: TextStyle( color: customColors.weakTextColor, fontSize: 15, ), maxLines: 1, ), ), Text( humanTime(message.createdAt), style: TextStyle( color: customColors.weakTextColor?.withAlpha(65), fontSize: 12, ), ), ], ), dense: true, subtitle: Padding( padding: const EdgeInsets.only(top: 5), child: Text( message.content.trim().replaceAll("\n", " "), maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: customColors.weakTextColor?.withAlpha(150), fontSize: 12, overflow: TextOverflow.ellipsis, ), ), ), onTap: () { HapticFeedbackHelper.lightImpact(); onTap(); }, ), ), ), ), ); } } ================================================ FILE: lib/page/setting/openai_setting.dart ================================================ import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/column_block.dart'; import 'package:askaide/page/component/enhanced_button.dart'; import 'package:askaide/page/component/enhanced_textfield.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/message_box.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:dio/dio.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; class OpenAISettingScreen extends StatefulWidget { final SettingRepository settings; final String? source; const OpenAISettingScreen({ super.key, required this.settings, this.source, }); @override State createState() => _OpenAISettingScreenState(); } class _OpenAISettingScreenState extends State { final TextEditingController _apiKeyController = TextEditingController(); final TextEditingController _organizationController = TextEditingController(); final TextEditingController _urlController = TextEditingController(); bool enableOpenAISelfHosted = true; bool? verifySuccess; @override void initState() { super.initState(); _apiKeyController.text = widget.settings.stringDefault(settingOpenAIAPIToken, ''); _organizationController.text = widget.settings.stringDefault(settingOpenAIOrganization, ''); _urlController.text = widget.settings.stringDefault(settingOpenAIURL, ''); if (widget.source == 'setting') { enableOpenAISelfHosted = widget.settings.boolDefault(settingOpenAISelfHosted, false); } } @override void dispose() { _apiKeyController.dispose(); _organizationController.dispose(); _urlController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( toolbarHeight: CustomSize.toolbarHeight, title: const Text( 'OpenAI Setting', style: TextStyle( fontSize: CustomSize.appBarTitleSize, ), ), centerTitle: true, elevation: 0, actions: [ if (widget.source != 'setting') TextButton( onPressed: () { context.go(Ability().homeRoute); }, child: Text( 'Do not set', style: TextStyle( color: customColors.weakLinkColor, fontSize: 13, ), ), ), ], ), backgroundColor: customColors.backgroundColor, body: BackgroundContainer( setting: widget.settings, enabled: false, child: SizedBox( height: double.infinity, child: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( children: [ MessageBox( message: AppLocale.enableCustomOpenAI.getString(context), type: MessageBoxType.info, ), const SizedBox(height: 10), ColumnBlock( children: [ if (widget.source == 'setting') Padding( padding: const EdgeInsets.symmetric(vertical: 5), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( AppLocale.enable.getString(context), style: TextStyle( color: customColors.textfieldLabelColor, fontSize: 16, ), ), CupertinoSwitch( activeColor: customColors.linkColor, value: enableOpenAISelfHosted, onChanged: (value) { setState(() { enableOpenAISelfHosted = value; }); }, ), ], ), ), EnhancedTextField( customColors: customColors, maxLength: 128, labelText: 'Server URL', labelWidth: 104, labelPosition: LabelPosition.left, controller: _urlController, showCounter: false, hintText: 'https://api.openai.com', ), EnhancedTextField( customColors: customColors, maxLength: 128, labelText: 'API Key', labelWidth: 104, labelPosition: LabelPosition.left, controller: _apiKeyController, showCounter: false, obscureText: true, hintText: 'sk-xxxxxxx', ), EnhancedTextField( customColors: customColors, labelText: 'Organization ID', labelFontSize: 14, labelWidth: 104, maxLength: 128, labelPosition: LabelPosition.left, controller: _organizationController, showCounter: false, hintText: AppLocale.optional.getString(context), ), ], ), const SizedBox(height: 10), EnhancedButton( title: widget.source == 'setting' ? AppLocale.save.getString(context) : 'Enable', onPressed: () { var url = _urlController.text; var apiKey = _apiKeyController.text; var organization = _organizationController.text; if (url == '') { url = 'https://api.openai.com'; } if (!url.startsWith('http://') && !url.startsWith('https://')) { showErrorMessageEnhanced(context, 'The URL must begin with http:// or https://.'); return; } if (!enableOpenAISelfHosted) { onSaveAndEnter(apiKey, organization, url, context); return; } if (enableOpenAISelfHosted && apiKey == '') { showErrorMessageEnhanced(context, 'API Key cannot be empty'); return; } verifySecretKey().then((value) { onSaveAndEnter(apiKey, organization, url, context); }).onError((error, stackTrace) { showErrorMessage(error.toString()); }); }, ), ], ), ), ), ), ), ); } void onSaveAndEnter(String apiKey, String organization, String url, BuildContext context) async { await widget.settings.set(settingOpenAIAPIToken, apiKey); await widget.settings.set(settingOpenAIOrganization, organization); await widget.settings.set(settingOpenAIURL, url); if (widget.source == 'setting') { await widget.settings.set( settingOpenAISelfHosted, enableOpenAISelfHosted ? 'true' : 'false', ); // ignore: use_build_context_synchronously showSuccessMessage(AppLocale.operateSuccess.getString(context)); } else { await widget.settings.set(settingOpenAISelfHosted, 'true'); if (context.mounted) { context.go(Ability().homeRoute); } } } Future verifySecretKey() async { var url = _urlController.text; var apiKey = _apiKeyController.text; var organization = _organizationController.text; if (url == '') { url = 'https://api.openai.com'; } if (!url.startsWith('http://') && !url.startsWith('https://')) { return Future.error('The URL must begin with http:// or https://.'); } if (apiKey == '') { return Future.error('API Key cannot be empty'); } final headers = { 'Authorization': 'Bearer $apiKey', }; if (organization != '') { headers['OpenAI-Organization'] = organization; } final cancelLoading = BotToast.showCustomLoading( toastBuilder: (cancel) { return LoadingIndicator( message: AppLocale.processingWait.getString(context), ); }, allowClick: false, duration: const Duration(seconds: 120), ); final dio = Dio(BaseOptions( baseUrl: url, connectTimeout: const Duration(seconds: 5), )); try { final resp = await dio.get( '/v1/models', options: Options( headers: headers, receiveDataWhenStatusError: true, sendTimeout: const Duration(seconds: 3), receiveTimeout: const Duration(seconds: 3), ), ); if (resp.statusCode != 200) { cancelLoading(); setState(() { verifySuccess = false; }); return Future.error('Verification failed, please check the API Key: ${resp.data}'); } cancelLoading(); setState(() { verifySuccess = true; }); } catch (e) { setState(() { verifySuccess = false; }); cancelLoading(); if (e is DioException) { if (e.response != null && e.response!.data != null) { return Future.error( 'Verification failed, please check the network or API Key: ${e.response!.data["error"]["message"]}'); } else { return Future.error('Verification failed, please check the network or API Key: ${e.error}'); } } else { return Future.error('Verification failed, please check the network or API Key: ${e.toString()}'); } } } } ================================================ FILE: lib/page/setting/retrieve_password_screen.dart ================================================ import 'package:askaide/helper/helper.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/password_field.dart'; import 'package:askaide/page/component/verify_code_input.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:go_router/go_router.dart'; class RetrievePasswordScreen extends StatefulWidget { final String? username; final SettingRepository setting; const RetrievePasswordScreen({super.key, this.username, required this.setting}); @override State createState() => _RetrievePasswordScreenState(); } class _RetrievePasswordScreenState extends State { final TextEditingController _usernameController = TextEditingController(); final TextEditingController _passwordController = TextEditingController(); final TextEditingController _verificationCodeController = TextEditingController(); String verifyCodeId = ''; final phoneNumberValidator = RegExp(r"^1[3456789]\d{9}$"); final emailValidator = RegExp(r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+"); @override void initState() { super.initState(); if (widget.username != null) { _usernameController.text = widget.username!; } } @override void dispose() { _usernameController.dispose(); _passwordController.dispose(); _verificationCodeController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { var customColors = Theme.of(context).extension()!; return WindowFrameWidget( child: Scaffold( appBar: AppBar( toolbarHeight: CustomSize.toolbarHeight, title: Text( AppLocale.resetPassword.getString(context), style: const TextStyle(fontSize: CustomSize.appBarTitleSize), ), centerTitle: true, ), backgroundColor: customColors.backgroundContainerColor, body: BackgroundContainer( setting: widget.setting, enabled: false, child: Column( children: [ Padding( padding: const EdgeInsets.only(left: 15.0, right: 15.0, top: 15, bottom: 0), child: TextFormField( controller: _usernameController, inputFormatters: [FilteringTextInputFormatter.singleLineFormatter], keyboardType: TextInputType.phone, decoration: InputDecoration( border: const OutlineInputBorder(), enabledBorder: const OutlineInputBorder( borderSide: BorderSide(color: Color.fromARGB(200, 192, 192, 192)), ), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: customColors.linkColor!), ), isDense: true, floatingLabelBehavior: FloatingLabelBehavior.always, labelText: AppLocale.account.getString(context), hintText: AppLocale.accountInputTips.getString(context), hintStyle: TextStyle( color: customColors.textfieldHintColor, fontSize: 15, ), ), ), ), Padding( padding: const EdgeInsets.only(left: 15.0, right: 15.0, top: 15, bottom: 0), child: PasswordField( controller: _passwordController, labelText: AppLocale.newPassword.getString(context), hintText: AppLocale.passwordInputTips.getString(context), ), ), Padding( padding: const EdgeInsets.only(left: 15.0, right: 10.0, top: 15, bottom: 0), child: VerifyCodeInput( controller: _verificationCodeController, onVerifyCodeSent: (id) { verifyCodeId = id; }, sendVerifyCode: () { return APIServer().sendResetPasswordCode( _usernameController.text.trim(), verifyType: phoneNumberValidator.hasMatch(_usernameController.text) ? 'sms' : 'email', ); }, sendCheck: () { final username = _usernameController.text.trim(); final isPhoneNumber = phoneNumberValidator.hasMatch(username); final isEmail = emailValidator.hasMatch(username); if (username == '') { showErrorMessage(AppLocale.accountRequired.getString(context)); return false; } if (!isPhoneNumber && !isEmail) { showErrorMessage(AppLocale.accountFormatError.getString(context)); return false; } return true; }, ), ), const SizedBox(height: 15), Container( height: 45, width: double.infinity, margin: const EdgeInsets.symmetric(horizontal: 15), decoration: BoxDecoration( color: customColors.linkColor, borderRadius: CustomSize.borderRadius, ), child: TextButton( onPressed: onResetSubmit, child: Text( AppLocale.resetPassword.getString(context), style: const TextStyle(color: Colors.white, fontSize: 18), ), ), ), ], ), ), ), ); } onResetSubmit() { final username = _usernameController.text.trim(); if (username == '') { showErrorMessage(AppLocale.accountRequired.getString(context)); return; } if (!phoneNumberValidator.hasMatch(username) && !emailValidator.hasMatch(username)) { showErrorMessage(AppLocale.accountFormatError.getString(context)); return; } final password = _passwordController.text.trim(); if (password == '' || password.length < 8 || password.length > 20) { showErrorMessage(AppLocale.passwordFormatError.getString(context)); return; } if (verifyCodeId == '') { showErrorMessage(AppLocale.pleaseGetVerifyCodeFirst.getString(context)); return; } final verificationCode = _verificationCodeController.text.trim(); if (verificationCode == '') { showErrorMessage(AppLocale.verifyCodeRequired.getString(context)); return; } if (verificationCode.length != 6) { showErrorMessage(AppLocale.verifyCodeFormatError.getString(context)); return; } final cancel = BotToast.showCustomLoading( toastBuilder: (cancel) { return LoadingIndicator( message: AppLocale.processingWait.getString(context), ); }, allowClick: false, duration: const Duration(seconds: 120), ); APIServer() .resetPasswordByCode( username: username, password: password, verifyCodeId: verifyCodeId, verifyCode: verificationCode, ) .then((value) { showSuccessMessage(AppLocale.passwordResetOK.getString(context)); context.pop(); }).catchError((e) { showErrorMessage(resolveError(context, e)); }).whenComplete(() => cancel()); } } ================================================ FILE: lib/page/setting/setting_screen.dart ================================================ import 'dart:io'; import 'package:askaide/bloc/account_bloc.dart'; import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/cache.dart'; import 'package:askaide/helper/env.dart'; import 'package:askaide/helper/helper.dart'; import 'package:askaide/helper/http.dart'; import 'package:askaide/helper/logger.dart'; import 'package:askaide/helper/platform.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/page/setting/account_security.dart'; import 'package:askaide/page/component/account_quota_card.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/invite_card.dart'; import 'package:askaide/page/component/item_selector_search.dart'; import 'package:askaide/page/component/sliver_component.dart'; import 'package:askaide/page/component/social_icon.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/theme/theme.dart'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/repo/api/user.dart'; import 'package:askaide/repo/api_server.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:flutter_phoenix/flutter_phoenix.dart'; import 'package:go_router/go_router.dart'; import 'package:quickalert/models/quickalert_type.dart'; import 'package:settings_ui/settings_ui.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher_string.dart'; class SettingScreen extends StatefulWidget { final SettingRepository settings; const SettingScreen({super.key, required this.settings}); @override State createState() => _SettingScreenState(); } class _SettingScreenState extends State { @override void initState() { context.read().add(AccountLoadEvent()); super.initState(); } @override Widget build(BuildContext context) { final customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( backgroundColor: customColors.backgroundColor, body: SliverComponent( title: Text( AppLocale.settings.getString(context), style: TextStyle( fontSize: CustomSize.appBarTitleSize, color: customColors.backgroundInvertedColor, ), ), actions: [ BlocBuilder( buildWhen: (previous, current) => current is AccountLoaded, builder: (context, state) { if (userHasLabPermission(state)) { return IconButton( onPressed: () { context.push('/admin/dashboard'); }, icon: const Icon(Icons.developer_board_outlined), tooltip: 'Admin Dashboard', ); } return const SizedBox(); }, ), IconButton( onPressed: () { context.push('/notifications'); }, icon: const Icon(Icons.notifications_outlined), tooltip: 'Notifications', ), ], child: BackgroundContainer( setting: widget.settings, enabled: false, backgroundColor: customColors.backgroundColor, child: BlocBuilder( builder: (_, state) { return buildSettingsList( context, [ // 智慧果信息、充值入口 // _buildAccountQuotaCard(context, state), // 账号信息 SettingsSection( title: Text(AppLocale.accountInfo.getString(context)), tiles: _buildAccountSetting(state, customColors), ), // 邀请卡片 if (state is AccountLoaded && state.user != null) _buildInviteCard(context, state), // 自定义设置 SettingsSection( title: Text(AppLocale.custom.getString(context)), tiles: [ // 主题设置 _buildCommonThemeSetting(customColors), // 语言设置 _buildCommonLanguageSetting(), // OpenAI 自定义配置 // if (Ability().enableOpenAI) _buildOpenAISelfHostedSetting(customColors), // 用户 API Keys 配置 if (state is AccountLoaded && state.user != null && Ability().supportAPIKeys) _buildUserAPIKeySetting(customColors), ], ), // 系统信息 SettingsSection( title: Text(AppLocale.systemInfo.getString(context)), tiles: [ // 只有 Web 端才展示 App 下载 if (PlatformTool.isWeb()) SettingsTile( title: const Text('APP 下载'), trailing: const Icon( Icons.download, size: 18, color: Colors.grey, ), onPressed: (context) { launchUrlString( 'https://aidea.aicode.cc', mode: LaunchMode.externalApplication, ); }, ), // 服务状态 if (Ability().serviceStatusPage != '') SettingsTile( title: Text(AppLocale.serviceStatus.getString(context)), trailing: const Icon( CupertinoIcons.chevron_forward, size: 18, color: Colors.grey, ), onPressed: (_) { launchUrlString(Ability().serviceStatusPage); }, ), // 清空缓存 SettingsTile( title: Text(AppLocale.clearCache.getString(context)), trailing: const Icon( CupertinoIcons.refresh, size: 18, color: Colors.grey, ), onPressed: (_) { openConfirmDialog( context, AppLocale.confirmClearCache.getString(context), () async { await Cache().clearAll(); await HttpClient.cleanCache(); showSuccessMessage( // ignore: use_build_context_synchronously AppLocale.operateSuccess.getString(context), ); if (context.mounted) { Phoenix.rebirth(context); } }, danger: true, ); }, ), // 检查更新 if (!PlatformTool.isIOS()) SettingsTile( title: Text(AppLocale.updateCheck.getString(context)), trailing: const Icon( CupertinoIcons.chevron_forward, size: 18, color: Colors.grey, ), onPressed: (_) { APIServer().versionCheck(cache: false).then((resp) { if (resp.hasUpdate) { showBeautyDialog( context, type: QuickAlertType.success, text: resp.message, confirmBtnText: '去更新', onConfirmBtnTap: () { launchUrlString( resp.url, mode: LaunchMode.externalApplication, ); }, cancelBtnText: '暂不更新', showCancelBtn: true, ); } else { showSuccessMessage(AppLocale.latestVersion.getString(context)); } }); }, ), // 用户协议 SettingsTile( title: Text(AppLocale.userTerms.getString(context)), trailing: const Icon( CupertinoIcons.chevron_forward, size: 18, color: Colors.grey, ), onPressed: (_) { launchUrl(Uri.parse('https://ai.aicode.cc/terms-user.html')); }, ), // 隐私政策 SettingsTile( title: Text(AppLocale.privacyPolicy.getString(context)), trailing: const Icon( CupertinoIcons.chevron_forward, size: 18, color: Colors.grey, ), onPressed: (_) { launchUrl(Uri.parse('https://ai.aicode.cc/privacy-policy.html')); }, ), // 关于 SettingsTile( title: Text(AppLocale.about.getString(context)), trailing: const Icon( CupertinoIcons.chevron_forward, size: 18, color: Colors.grey, ), onPressed: (_) { var tapCount = 0; showAboutDialog( context: context, applicationName: 'AIdea', applicationIcon: GestureDetector( onTap: () { if (userHasLabPermission(state)) { return; } tapCount++; if (tapCount > 5) { tapCount = 0; final showLab = forceShowLab(); widget.settings.set(settingForceShowLab, showLab ? 'false' : 'true'); showSuccessMessage(showLab ? 'Lab Feature Turned Off' : 'Labs features enabled'); setState(() {}); } }, child: Image.asset('assets/app.png', width: 40), ), applicationVersion: clientVersion, children: [ Text(AppLocale.aIdeaApp.getString(context)), ], ); }, ), ], ), if (userHasLabPermission(state) || forceShowLab()) SettingsSection( title: Text(AppLocale.lab.getString(context)), tiles: [ // 自定义服务器 _buildServerSelfHostedSetting(customColors), // 诊断 SettingsTile( title: Text(AppLocale.diagnostic.getString(context)), trailing: const Icon( CupertinoIcons.chevron_forward, size: 18, color: Colors.grey, ), onPressed: (context) { context.push('/diagnosis'); }, ), ], ), // 社交媒体图标 _buildSocialIcons(context), // 版权信息 CustomSettingsSection( child: Column( children: [ Text( 'Copyright © 2023-${DateTime.now().year}', style: TextStyle( color: customColors.weakTextColor, ), ), GestureDetector( onTap: () { launchUrlString( 'https://aidea.aicode.cc', mode: LaunchMode.externalApplication, ); }, child: Text( 'Gulu Artificial Intelligence Technology Co., Ltd.', style: TextStyle( color: customColors.weakTextColor, fontSize: 12, ), ), ), const SizedBox(height: 15), ], ), ), ], ); }, ), ), ), ), ); } /// 用户是否有实验室访问权限 bool userHasLabPermission(AccountState state) { return state is AccountLoaded && state.error == null && state.user != null && state.user!.control.withLab; } /// 是否强制显示实验室功能 bool forceShowLab() { return widget.settings.boolDefault(settingForceShowLab, false); } CustomSettingsSection _buildAccountQuotaCard( BuildContext context, AccountState state, ) { UserInfo? userInfo; if (state is AccountLoaded) { userInfo = state.user; } return CustomSettingsSection( child: AccountQuotaCard( userInfo: userInfo, onPaymentReturn: () { if (userInfo != null) { context.read().add(AccountLoadEvent(cache: false)); } }, ), ); } CustomSettingsSection _buildInviteCard(BuildContext context, AccountLoaded state) { if (state.error != null || !state.user!.showInviteMessage) { return CustomSettingsSection( child: Container(), ); } return CustomSettingsSection( child: InviteCard(userInfo: state.user!), ); } SettingsTile _buildCommonLanguageSetting() { return SettingsTile( title: Text(AppLocale.language.getString(context)), trailing: const Icon( CupertinoIcons.chevron_forward, size: 18, color: Colors.grey, ), onPressed: (_) { final current = widget.settings.stringDefault(settingLanguage, 'zh'); openModalBottomSheet( context, (context) { return ListView( shrinkWrap: true, children: [ ListTile( title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(AppLocale.followSystem.getString(context)), current == '' ? const Icon(Icons.check, color: Colors.green) : const SizedBox(), ], ), onTap: () async { await widget.settings.set(settingLanguage, ''); FlutterLocalization.instance.translate(resolveSystemLanguage(Platform.localeName)); if (context.mounted) { context.pop(); } }, ), ListTile( title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('简体中文'), current == 'zh-CHS' ? const Icon(Icons.check, color: Colors.green) : const SizedBox(), ], ), onTap: () async { await widget.settings.set(settingLanguage, 'zh-CHS'); FlutterLocalization.instance.translate('zh-CHS'); if (context.mounted) { context.pop(); } }, ), ListTile( title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('English'), current == 'en' ? const Icon(Icons.check, color: Colors.green) : const SizedBox(), ], ), onTap: () async { await widget.settings.set(settingLanguage, 'en'); FlutterLocalization.instance.translate('en'); if (context.mounted) { context.pop(); } }, ), ], ); }, heightFactor: 0.3, ); }, ); } Future>> _defaultServerList() async { return [ SelectorItem(const Text('官方正式服务器'), apiServerURL), SelectorItem(const Text('本地开发环境'), 'http://localhost:8080'), SelectorItem(const Text('局域网开发环境'), 'http://192.168.31.217:8080'), ]; } List _buildAccountSetting(AccountState state, CustomColors customColors) { if (state is AccountLoaded) { if (state.error != null && state.user == null) { return [ SettingsTile( title: Text(resolveError(context, state.error!)), trailing: const Icon( CupertinoIcons.chevron_forward, size: 18, color: Colors.grey, ), onPressed: (_) { context.read().add(AccountSignOutEvent()); context.go('/login'); }, ), ]; } return [ SettingsTile( title: Text( state.user!.user.displayName(), overflow: TextOverflow.ellipsis, ), trailing: Row(children: [ Text( AppLocale.accountSettings.getString(context), style: TextStyle( color: customColors.weakTextColor?.withAlpha(200), fontSize: 13, ), ), const Icon( CupertinoIcons.chevron_forward, size: 18, color: Colors.grey, ), ]), onPressed: (context) { context.push('/setting/account-security'); }, ), SettingsTile( title: Text(AppLocale.freeQuota.getString(context)), trailing: const Icon( CupertinoIcons.chevron_forward, size: 18, color: Colors.grey, ), onPressed: (context) { context.push('/free-statistics'); }, ), ]; } else if (state is AccountLoading) { return [ SettingsTile( title: const Text('Loading...'), ), ]; } return [ SettingsTile( leading: const Icon(Icons.account_circle), title: Text(AppLocale.signIn.getString(context)), trailing: const Icon( CupertinoIcons.chevron_forward, size: 18, color: Colors.grey, ), onPressed: (_) { context.go('/login'); }, ), ]; } SettingsTile _buildCommonThemeSetting(CustomColors customColors) { return SettingsTile.navigation( title: Text(AppLocale.themeMode.getString(context)), onPressed: (context) { final current = widget.settings.stringDefault(settingThemeMode, 'system'); openModalBottomSheet( context, (context) { return ListView( shrinkWrap: true, children: [ ListTile( title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(AppLocale.followSystem.getString(context)), current == 'system' ? const Icon(Icons.check, color: Colors.green) : const SizedBox(), ], ), onTap: () async { await widget.settings.set(settingThemeMode, 'system'); AppTheme.instance.mode = AppTheme.themeModeFormString('system'); if (context.mounted) { context.pop(); } }, ), ListTile( title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(AppLocale.lightThemeMode.getString(context)), current == 'light' ? const Icon(Icons.check, color: Colors.green) : const SizedBox(), ], ), onTap: () async { await widget.settings.set(settingThemeMode, 'light'); AppTheme.instance.mode = AppTheme.themeModeFormString('light'); if (context.mounted) { context.pop(); } }, ), ListTile( title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(AppLocale.darkThemeMode.getString(context)), current == 'dark' ? const Icon(Icons.check, color: Colors.green) : const SizedBox(), ], ), onTap: () async { await widget.settings.set(settingThemeMode, 'dark'); AppTheme.instance.mode = AppTheme.themeModeFormString('dark'); if (context.mounted) { context.pop(); } }, ), ], ); }, heightFactor: 0.3, ); }, ); } SettingsTile _buildOpenAISelfHostedSetting(CustomColors customColors) { return SettingsTile.navigation( title: const Text('OpenAI'), value: Text( widget.settings.boolDefault(settingOpenAISelfHosted, false) ? AppLocale.enable.getString(context) : AppLocale.disable.getString(context), style: TextStyle( color: customColors.weakTextColor?.withAlpha(200), fontSize: 13, ), ), onPressed: (context) { context.push('/setting/openai-custom?source=setting'); }, ); } /// 用户 API Key 配置 SettingsTile _buildUserAPIKeySetting(CustomColors customColors) { return SettingsTile.navigation( title: Text(AppLocale.userApiKeys.getString(context)), onPressed: (context) { context.push('/setting/user-api-keys'); }, ); } SettingsTile _buildServerSelfHostedSetting(CustomColors customColors) { return SettingsTile( title: const Text('Custom API Server'), trailing: const Icon( CupertinoIcons.chevron_forward, size: 18, color: Colors.grey, ), onPressed: (_) { openTextFieldDialog( context, title: 'Server Address', defaultValue: widget.settings.stringDefault(settingServerURL, apiServerURL), withSuffixIcon: true, enableSearch: false, futureDataSources: _defaultServerList(), onSubmit: (value) { widget.settings.set(settingServerURL, value.trim()).then((value) { openConfirmDialog( context, 'Settings successful, will take effect after app restart', () { try { SystemChannels.platform.invokeMethod('SystemNavigator.pop'); } catch (e) { Logger.instance.e(e); showErrorMessage('Application restart failed, please restart manually'); } }, danger: true, confirmText: 'Restart now', cancelText: 'Restart later', ); }); return true; }, ); }, ); } CustomSettingsSection _buildSocialIcons(BuildContext context) { return CustomSettingsSection( child: SocialIconGroup( isSettingTiles: true, ), ); } } ================================================ FILE: lib/page/setting/user_api_keys.dart ================================================ import 'package:askaide/bloc/user_api_keys_bloc.dart'; import 'package:askaide/helper/helper.dart'; import 'package:askaide/lang/lang.dart'; import 'package:askaide/page/component/background_container.dart'; import 'package:askaide/page/component/dialog.dart'; import 'package:askaide/page/component/loading.dart'; import 'package:askaide/page/component/message_box.dart'; import 'package:askaide/page/component/theme/custom_size.dart'; import 'package:askaide/page/component/theme/custom_theme.dart'; import 'package:askaide/page/component/windows.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:clipboard/clipboard.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localization/flutter_localization.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:go_router/go_router.dart'; import 'package:quickalert/quickalert.dart'; class UserAPIKeysScreen extends StatefulWidget { final SettingRepository setting; const UserAPIKeysScreen({super.key, required this.setting}); @override State createState() => _UserAPIKeysScreenState(); } class _UserAPIKeysScreenState extends State { Function? cancelDialog; @override void initState() { super.initState(); context.read().add(UserApiKeysLoad()); } @override Widget build(BuildContext context) { var customColors = Theme.of(context).extension()!; return WindowFrameWidget( backgroundColor: customColors.backgroundColor, child: Scaffold( appBar: AppBar( toolbarHeight: CustomSize.toolbarHeight, title: Text( AppLocale.userApiKeys.getString(context), style: const TextStyle(fontSize: CustomSize.appBarTitleSize), ), centerTitle: true, elevation: 0, actions: [ IconButton( icon: const Icon(Icons.add), onPressed: () { openTextFieldDialog( context, title: 'API Key', hint: 'API Key 名称', onSubmit: (value) { context.read().add(UserApiKeyCreate(value)); return true; }, ); }, ), ], ), backgroundColor: customColors.backgroundContainerColor, body: BackgroundContainer( setting: widget.setting, enabled: false, child: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( children: [ const MessageBox( message: 'You can use API Key to access your data in other applications, and the protocol is fully compatible with OpenAI\'s official API.', type: MessageBoxType.info, ), const SizedBox(height: 10), BlocConsumer( listener: (context, state) { if (state is UserApiKeyLoaded) { showBeautyDialog( context, type: QuickAlertType.success, title: 'API Key', text: state.key.token, confirmBtnText: 'Copy to clipboard', onConfirmBtnTap: () { FlutterClipboard.copy(state.key.token).then((value) { showSuccessMessage('Copied to clipboard'); context.pop(); }); cancelDialog?.call(); }, showCancelBtn: true, onCancelBtnTap: () => context.pop(), barrierDismissible: true, ); } }, buildWhen: (previous, current) => current is UserApiKeysLoaded, builder: (context, state) { if (state is UserApiKeysLoaded) { if (state.keys.isEmpty) { return Container( margin: const EdgeInsets.only(top: 50), alignment: Alignment.center, child: Center( child: Text( 'You haven\'t created any API Key yet.', style: TextStyle( fontSize: 14, color: customColors.weakTextColor, ), ), ), ); } return ListView.builder( itemCount: state.keys.length, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) { final item = state.keys[index]; return Container( margin: const EdgeInsets.symmetric(vertical: 5), decoration: BoxDecoration(borderRadius: CustomSize.borderRadius), child: Slidable( endActionPane: ActionPane( motion: const ScrollMotion(), children: [ const SizedBox(width: 10), SlidableAction( label: AppLocale.delete.getString(context), borderRadius: CustomSize.borderRadiusAll, backgroundColor: Colors.red, icon: Icons.delete, onPressed: (_) { openConfirmDialog( context, AppLocale.confirmDelete.getString(context), () { context.read().add(UserApiKeyDelete(item.id)); }, danger: true, ); }, ), ], ), child: Material( color: customColors.backgroundColor?.withAlpha(200), borderRadius: const BorderRadius.all(CustomSize.radius), child: InkWell( child: ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), shape: RoundedRectangleBorder(borderRadius: CustomSize.borderRadius), title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( item.name, overflow: TextOverflow.ellipsis, style: TextStyle( color: customColors.weakTextColor, fontSize: 15, ), maxLines: 1, ), ), Text( humanTime(DateTime.now()), style: TextStyle( color: customColors.weakTextColor?.withAlpha(65), fontSize: 12, ), ), ], ), subtitle: Padding( padding: const EdgeInsets.only(top: 5), child: Text( item.token, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: customColors.weakTextColor?.withAlpha(150), fontSize: 12, overflow: TextOverflow.ellipsis, ), ), ), dense: true, onTap: () { cancelDialog = BotToast.showCustomLoading( toastBuilder: (cancel) { return LoadingIndicator( message: AppLocale.imageUploading.getString(context), ); }, allowClick: false, ); context.read().add( UserApiKeyLoad(item.id), ); }, ), ), ), ), ); }, ); } return const Center(child: LoadingIndicator()); }, ), ], ), ), ), ), ); } } ================================================ FILE: lib/repo/api/admin/channels.dart ================================================ class AdminChannel { int? id; String name; String type; String? server; String? secret; AdminChannelMeta? meta; String get display { return name; } AdminChannel({ this.id, required this.name, required this.type, this.server, this.secret, this.meta, }); factory AdminChannel.fromJson(Map json) { return AdminChannel( id: json['id'], name: json['name'], type: json['type'], server: json['server'], secret: json['secret'], meta: json['meta'] != null ? AdminChannelMeta.fromJson(json['meta']) : null, ); } Map toJson() { return { 'id': id, 'name': name, 'type': type, 'server': server, 'secret': secret, 'meta': meta?.toJson(), }; } } class AdminChannelMeta { bool? usingProxy; bool? openaiAzure; String? openaiAzureAPIVersion; AdminChannelMeta({ this.usingProxy, this.openaiAzure, this.openaiAzureAPIVersion, }); factory AdminChannelMeta.fromJson(Map json) { return AdminChannelMeta( usingProxy: json['using_proxy'], openaiAzure: json['openai_azure'], openaiAzureAPIVersion: json['openai_azure_api_version'], ); } Map toJson() { return { 'using_proxy': usingProxy, 'openai_azure': openaiAzure, 'openai_azure_api_version': openaiAzureAPIVersion, }; } } class AdminChannelAddReq { String name; String type; String? server; String? secret; AdminChannelMeta? meta; AdminChannelAddReq({ required this.name, required this.type, this.server, this.secret, this.meta, }); Map toJson() { return { 'name': name, 'type': type, 'server': server, 'secret': secret, 'meta': meta?.toJson(), }; } } class AdminChannelUpdateReq { String? name; String? type; String? server; String? secret; AdminChannelMeta? meta; AdminChannelUpdateReq({ this.name, this.type, this.server, this.secret, this.meta, }); Map toJson() { return { 'name': name, 'type': type, 'server': server, 'secret': secret, 'meta': meta?.toJson(), }; } } class AdminChannelType { String name; String? display; bool dynamicType; String get text { return display ?? name; } AdminChannelType({ required this.name, this.display, required this.dynamicType, }); factory AdminChannelType.fromJson(Map json) { return AdminChannelType( name: json['name'], display: json['display'], dynamicType: json['dynamic'] ?? false, ); } Map toJson() { return { 'name': name, 'display': display, 'dynamic': dynamicType, }; } } ================================================ FILE: lib/repo/api/admin/models.dart ================================================ class AdminModel { String modelId; String name; String? shortName; String? description; String? avatarUrl; int status; AdminModelMeta? meta; List providers; bool get isVision => meta?.vision ?? false; int get inputPrice => meta?.inputPrice ?? 0; int get outputPrice => meta?.outputPrice ?? 0; int get perReqPrice => meta?.perReqPrice ?? 0; int get maxContext => meta?.maxContext ?? 0; bool get enabled => status == 1; AdminModel({ required this.modelId, required this.name, this.shortName, this.description, this.avatarUrl, required this.status, this.meta, required this.providers, }); factory AdminModel.fromJson(Map json) { return AdminModel( modelId: json['model_id'], name: json['name'], shortName: json['short_name'], description: json['description'], avatarUrl: json['avatar_url'], status: json['status'], meta: json['meta'] != null ? AdminModelMeta.fromJson(json['meta']) : null, providers: ((json['providers'] ?? []) as List).map((e) => AdminModelProvider.fromJson(e)).toList(), ); } Map toJson() { return { 'model_id': modelId, 'name': name, 'short_name': shortName, 'description': description, 'avatar_url': avatarUrl, 'status': status, 'meta': meta?.toJson(), 'providers': providers.map((e) => e.toJson()).toList(), }; } } class AdminModelMeta { bool? vision; bool? restricted; int? maxContext; int? inputPrice; int? outputPrice; int? perReqPrice; String? prompt; String? tag; String? tagTextColor; String? tagBgColor; bool? isNew; bool? isRecommend; String? category; bool? search; bool? reasoning; double? temperature; int? searchCount; int? searchPrice; AdminModelMeta({ this.vision, this.restricted, this.maxContext, this.inputPrice, this.outputPrice, this.perReqPrice, this.prompt, this.tag, this.tagTextColor, this.tagBgColor, this.isNew, this.isRecommend, this.category, this.search, this.reasoning, this.temperature, this.searchCount, this.searchPrice, }); factory AdminModelMeta.fromJson(Map json) { return AdminModelMeta( vision: json['vision'] ?? false, restricted: json['restricted'] ?? false, maxContext: json['max_context'], inputPrice: json['input_price'] ?? 0, outputPrice: json['output_price'] ?? 0, perReqPrice: json['per_req_price'] ?? 0, prompt: json['prompt'], tag: json['tag'], tagTextColor: json['tag_text_color'], tagBgColor: json['tag_bg_color'], isNew: json['is_new'] ?? false, isRecommend: json['is_recommend'] ?? false, category: json['category'], search: json['search'] ?? false, reasoning: json['reasoning'] ?? false, temperature: json['temperature'] ?? 0.0, searchCount: json['search_count'] ?? 0, searchPrice: json['search_price'] ?? 0, ); } Map toJson() { return { 'vision': vision, 'restricted': restricted, 'max_context': maxContext, 'input_price': inputPrice, 'output_price': outputPrice, 'per_req_price': perReqPrice, 'prompt': prompt, 'tag': tag, 'tag_text_color': tagTextColor, 'tag_bg_color': tagBgColor, 'is_new': isNew, 'is_recommend': isRecommend, 'category': category, 'search': search, 'reasoning': reasoning, 'temperature': temperature, 'search_count': searchCount, 'search_price': searchPrice, }; } } class AdminModelProvider { int? id; String? name; String? modelRewrite; String? type; AdminModelProvider({ this.id, this.name, this.modelRewrite, this.type, }); factory AdminModelProvider.fromJson(Map json) { return AdminModelProvider( id: json['id'], name: json['name'], modelRewrite: json['model_rewrite'], type: json['type'], ); } Map toJson() { return { 'id': id, 'name': name, 'model_rewrite': modelRewrite, 'type': type, }; } } class AdminModelAddReq { String modelId; String name; String? shortName; String? description; String? avatarUrl; int status; AdminModelMeta? meta; List? providers; AdminModelAddReq({ required this.modelId, required this.name, this.shortName, this.description, this.avatarUrl, required this.status, this.meta, this.providers, }); Map toJson() { return { 'model_id': modelId, 'name': name, 'short_name': shortName, 'description': description, 'avatar_url': avatarUrl, 'status': status, 'meta': meta?.toJson(), 'providers': providers?.map((e) => e.toJson()).toList(), }; } } class AdminModelUpdateReq { String name; String? shortName; String? description; String? avatarUrl; int status; AdminModelMeta? meta; List? providers; AdminModelUpdateReq({ required this.name, this.shortName, this.description, this.avatarUrl, required this.status, this.meta, this.providers, }); Map toJson() { return { 'name': name, 'short_name': shortName, 'description': description, 'avatar_url': avatarUrl, 'status': status, 'meta': meta?.toJson(), 'providers': providers?.map((e) => e.toJson()).toList(), }; } } ================================================ FILE: lib/repo/api/admin/payment.dart ================================================ class AdminPaymentHistory { final int id; final int userId; final String paymentId; final String? source; final int quantity; final int retailPrice; final String environment; final DateTime purchaseAt; AdminPaymentHistory({ required this.id, required this.userId, required this.paymentId, required this.quantity, required this.retailPrice, required this.environment, required this.purchaseAt, this.source, }); factory AdminPaymentHistory.fromJson(Map json) { return AdminPaymentHistory( id: json['id'], userId: json['user_id'], paymentId: json['payment_id'], quantity: json['quantity'], retailPrice: json['retail_price'], environment: json['environment'], purchaseAt: DateTime.parse(json['purchase_at']), source: json['source'], ); } Map toJson() { return { 'id': id, 'user_id': userId, 'payment_id': paymentId, 'quantity': quantity, 'retail_price': retailPrice, 'environment': environment, 'purchase_at': purchaseAt.toIso8601String(), 'source': source, }; } } ================================================ FILE: lib/repo/api/admin/users.dart ================================================ class AdminUser { final int id; final String? email; final String? phone; final String? realname; final String? avatar; final String? unionId; final String? appleUid; final int? invitedBy; final String? inviteCode; final String? userType; final String? status; final String? preferSigninMethod; final DateTime? createdAt; AdminUser({ required this.id, this.email, this.phone, this.realname, this.avatar, this.unionId, this.appleUid, this.invitedBy, this.inviteCode, required this.userType, this.preferSigninMethod, this.createdAt, this.status, }); String get displayName { if (realname != null && realname!.isNotEmpty) { return realname!; } if (email != null && email!.isNotEmpty) { return email!; } if (phone != null && phone!.isNotEmpty) { return phone!; } return '-'; } factory AdminUser.fromJson(Map json) { return AdminUser( id: json['id'], email: json['email'], phone: json['phone'], realname: json['realname'], avatar: json['avatar'], unionId: json['union_id'], appleUid: json['apple_uid'], invitedBy: json['invite_by'], inviteCode: json['invite_code'], userType: json['user_type'], preferSigninMethod: json['prefer_signin_method'], createdAt: json['CreatedAt'] != null ? DateTime.parse(json['CreatedAt']) : null, status: json['status'], ); } Map toJson() { return { 'id': id, 'email': email, 'phone': phone, 'realname': realname, 'avatar': avatar, 'union_id': unionId, 'apple_uid': appleUid, 'invite_by': invitedBy, 'invite_code': inviteCode, 'user_type': userType, 'prefer_signin_method': preferSigninMethod, 'CreatedAt': createdAt?.toIso8601String(), 'status': status, }; } } ================================================ FILE: lib/repo/api/article.dart ================================================ class Article { int id; String title; String content; String? author; String? type; DateTime? createdAt; Article({ required this.id, required this.title, required this.content, this.author, this.type, this.createdAt, }); factory Article.fromJson(Map json) { return Article( id: json['id'], title: json['title'], content: json['content'], author: json['author'], type: json['type'], createdAt: json['created_at'] != null ? DateTime.parse(json['created_at']) : null, ); } Map toJson() { return { 'id': id, 'title': title, 'content': content, 'author': author, 'type': type, 'created_at': createdAt?.toIso8601String(), }; } } ================================================ FILE: lib/repo/api/creative.dart ================================================ import 'dart:convert'; import 'package:intl/intl.dart'; class CreativeGalleryItemResponse { CreativeGallery item; bool isInternalUser; CreativeGalleryItemResponse(this.item, this.isInternalUser); toJson() => { 'data': item.toJson(), 'is_internal_user': isInternalUser, }; static CreativeGalleryItemResponse fromJson(Map json) { return CreativeGalleryItemResponse( CreativeGallery.fromJson(json['data']), json['is_internal_user'] ?? false, ); } } class CreativeGallery { int id; int? userId; String? username; int? creativeHistoryId; int creativeType; String creativeId; String? meta; String? prompt; String? negativePrompt; String? answer; int refCount; int starLevel; int hotValue; int status; DateTime? createdAt; DateTime? updatedAt; String? previewImage; CreativeGallery({ required this.id, this.userId, this.username, this.creativeHistoryId, required this.creativeType, required this.creativeId, this.meta, this.prompt, this.negativePrompt, this.answer, this.refCount = 0, this.starLevel = 0, this.hotValue = 0, this.status = 0, this.previewImage, this.createdAt, this.updatedAt, }); Map get metaMap { if (meta == null || meta!.isEmpty) { return {}; } return jsonDecode(meta!); } List get images { try { if (creativeType == 2 && answer != null && answer != '') { return (jsonDecode(answer!) as List) .map((e) => e.toString()) .toList(); } return []; } catch (e) { return []; } } /// 封面图 String get preview { if (previewImage != null && previewImage != '') { return previewImage!; } if (images.isNotEmpty) { return images.first; } return ''; } toJson() => { 'id': id, 'user_id': userId, 'username': username, 'creative_history_id': creativeHistoryId, 'creative_type': creativeType, 'creative_id': creativeId, 'meta': meta, 'prompt': prompt, 'negative_prompt': negativePrompt, 'answer': answer, 'ref_count': refCount, 'star_level': starLevel, 'hot_value': hotValue, 'status': status, 'preview_image': previewImage, 'created_at': createdAt?.toIso8601String(), 'updated_at': updatedAt?.toIso8601String(), }; static CreativeGallery fromJson(Map json) { return CreativeGallery( id: json['id'], userId: json['user_id'], username: json['username'], creativeHistoryId: json['creative_history_id'], creativeType: json['creative_type'], creativeId: json['creative_id'] ?? 'text-to-image', meta: json['meta'], prompt: json['prompt'], negativePrompt: json['negative_prompt'], answer: json['answer'], refCount: json['ref_count'] ?? 0, starLevel: json['star_level'] ?? 0, hotValue: json['hot_value'] ?? 0, status: json['status'] ?? 0, previewImage: json['preview_image'], createdAt: json['created_at'] != null ? DateTime.parse(json['created_at']) : null, updatedAt: json['updated_at'] != null ? DateTime.parse(json['updated_at']) : null, ); } } class CreativeIslandCapacity { bool showAIRewrite; bool showUpscaleBy; bool showNegativeText; bool showStyle; bool showSeed; bool showImageCount; bool showPromptForImage2Image; List allowRatios; List vendorModels; List allowUpscaleBy; bool showImageStrength; List filters; List artisticStyles; List artisticTextStyles; List artisticTextFonts; CreativeIslandCapacity({ required this.showAIRewrite, required this.showUpscaleBy, required this.showNegativeText, required this.showSeed, required this.showStyle, required this.showImageCount, required this.allowRatios, required this.showPromptForImage2Image, this.vendorModels = const [], this.allowUpscaleBy = const [], this.showImageStrength = false, this.filters = const [], this.artisticStyles = const [], this.artisticTextStyles = const [], this.artisticTextFonts = const [], }); toJson() => { 'show_ai_rewrite': showAIRewrite, 'show_upscale_by': showUpscaleBy, 'show_negative_text': showNegativeText, 'show_style': showStyle, 'show_seed': showSeed, 'show_image_count': showImageCount, 'show_prompt_for_image2image': showPromptForImage2Image, 'allow_ratios': allowRatios, 'vendor_models': vendorModels.map((e) => e.toJson()).toList(), 'allow_upscale_by': allowUpscaleBy, 'show_image_strength': showImageStrength, 'filters': filters.map((e) => e.toJson()).toList(), 'artistic_styles': artisticStyles.map((e) => e.toJson()).toList(), 'artistic_text_styles': artisticTextStyles.map((e) => e.toJson()).toList(), 'artistic_text_fonts': artisticTextFonts.map((e) => e.toJson()).toList(), }; static CreativeIslandCapacity fromJson(Map json) { return CreativeIslandCapacity( showAIRewrite: json['show_ai_rewrite'] ?? false, showUpscaleBy: json['show_upscale_by'] ?? false, showNegativeText: json['show_negative_text'] ?? false, showStyle: json['show_style'] ?? false, showSeed: json['show_seed'] ?? false, showImageCount: json['show_image_count'] ?? false, showPromptForImage2Image: json['show_prompt_for_image2image'] ?? false, allowRatios: (json['allow_ratios'] as List) .map((e) => e.toString()) .toList(), vendorModels: ((json['vendor_models'] ?? []) as List) .map((e) => CreativeIslandVendorModel.fromJson(e)) .toList(), allowUpscaleBy: (json['allow_upscale_by'] as List) .map((e) => e.toString()) .toList(), showImageStrength: json['show_image_strength'] ?? false, filters: ((json['filters'] ?? []) as List) .map((e) => CreativeIslandImageFilter.fromJson(e)) .toList(), artisticStyles: ((json['artistic_styles'] ?? []) as List) .map((e) => CreativeIslandArtisticStyle.fromJson(e)) .toList(), artisticTextStyles: ((json['artistic_text_styles'] ?? []) as List) .map((e) => CreativeIslandArtisticStyle.fromJson(e)) .toList(), artisticTextFonts: ((json['artistic_text_fonts'] ?? []) as List) .map((e) => CreativeIslandArtisticStyle.fromJson(e)) .toList(), ); } } class CreativeIslandArtisticStyle { String id; String name; String? previewImage; CreativeIslandArtisticStyle({ required this.id, required this.name, this.previewImage, }); toJson() => { 'id': id, 'name': name, 'preview_image': previewImage, }; static CreativeIslandArtisticStyle fromJson(Map json) { return CreativeIslandArtisticStyle( id: json['id'], name: json['name'], previewImage: json['preview_image'], ); } } class CreativeIslandImageFilter { int id; String name; String? description; String previewImage; CreativeIslandImageFilter({ required this.id, required this.name, required this.previewImage, this.description, }); toJson() => { 'id': id, 'name': name, 'description': description, 'preview_image': previewImage, }; static CreativeIslandImageFilter fromJson(Map json) { return CreativeIslandImageFilter( id: json['id'], name: json['name'], description: json['description'], previewImage: json['preview_image'], ); } } class CreativeIslandVendorModel { String id; String? vendor; String name; bool upscale; bool showStyle; bool showImageStrength; String? introUrl; CreativeIslandVendorModel({ required this.id, required this.name, required this.upscale, this.vendor, this.showStyle = false, this.showImageStrength = false, this.introUrl, }); toJson() => { 'id': id, 'name': name, 'vendor': vendor, 'upscale': upscale, 'show_style': showStyle, 'show_image_strength': showImageStrength, 'intro_url': introUrl, }; static CreativeIslandVendorModel fromJson(Map json) { return CreativeIslandVendorModel( id: json['id'], name: json['name'], vendor: json['vendor'], upscale: json['upscale'] ?? false, showStyle: json['show_style'] ?? false, showImageStrength: json['show_image_strength'] ?? false, introUrl: json['intro_url'], ); } } class CreativeIslandCompletionResp { String content; String type; List resources; CreativeIslandCompletionResp({ required this.content, required this.type, this.resources = const [], }); toJson() => { 'content': content, 'type': type, 'resources': resources, }; static CreativeIslandCompletionResp fromJson(Map json) { return CreativeIslandCompletionResp( content: json['content'], type: json['type'], resources: json['resources'] != null ? (json['resources'] as List) .map((e) => e.toString()) .toList() : [], ); } } class CreativeIslandItems { List items; List categories; String? backgroundImage; CreativeIslandItems(this.items, this.categories, {this.backgroundImage}); } class CreativeIslandItemExtSize { int width; int height; String aspectRatio; CreativeIslandItemExtSize({ required this.width, required this.height, required this.aspectRatio, }); toJson() => { 'width': width, 'height': height, 'aspect_ratio': aspectRatio, }; static CreativeIslandItemExtSize fromJson(Map json) { return CreativeIslandItemExtSize( width: json['width'], height: json['height'], aspectRatio: json['aspect_ratio'], ); } } class CreativeIslandItemExtension { bool? aiRewrite; bool? showAIRewrite; String? upscaleBy; bool? showNegativeText; bool? showAdvanceButton; List? allowSizes; CreativeIslandItemExtension({ this.aiRewrite, this.showAIRewrite, this.upscaleBy, this.showNegativeText, this.allowSizes, this.showAdvanceButton, }); toJson() => { 'ai_rewrite': aiRewrite, 'show_ai_rewrite': showAIRewrite, 'upscale_by': upscaleBy, 'show_negative_text': showNegativeText, 'allow_sizes': allowSizes?.map((e) => e.toJson()).toList(), 'show_advance_button': showAdvanceButton, }; static CreativeIslandItemExtension fromJson(Map json) { return CreativeIslandItemExtension( aiRewrite: json['ai_rewrite'], showAIRewrite: json['show_ai_rewrite'], upscaleBy: json['upscale_by'], showNegativeText: json['show_negative_text'], showAdvanceButton: json['show_advance_button'], allowSizes: json['allow_sizes'] != null ? (json['allow_sizes'] as List) .map((e) => CreativeIslandItemExtSize.fromJson(e)) .toList() : null, ); } } class CreativeIslandItem { String id; String title; String? description; bool supportStream; List categories; String vendor; String modelType; String? bgImage; String? bgEmbeddedImage; String? label; String? labelColor; String? titleColor; String? submitBtnText; String? promptInputTitle; int waitSeconds; bool showImageStyleSelector; bool noPrompt; String? hint; int? wordCount; CreativeIslandItemExtension? extension; /// 是否显示高级按钮 bool get showAdvanceButton { if (extension != null && extension!.showAdvanceButton != null) { return extension!.showAdvanceButton!; } return false; } /// 返回支持的图片尺寸 List get imageAllowSizes { if (extension != null && extension!.allowSizes != null) { return extension!.allowSizes!; } return []; } /// 是否启用 AI 优化的默认值 bool get aiRewriteDefaultValue { if (extension != null && extension!.aiRewrite != null) { return extension!.aiRewrite!; } return false; } /// 是否显示反向提示语输入框 bool get isShowNegativeText { if (extension != null && extension!.showNegativeText != null) { return extension!.showNegativeText!; } return false; } /// 是否显示 AI 重写按钮 bool get isShowAIRewrite { if (extension != null && extension!.showAIRewrite != null) { return extension!.showAIRewrite!; } return false; } CreativeIslandItem({ required this.id, required this.title, required this.vendor, required this.modelType, this.categories = const [], this.supportStream = false, this.description, this.bgImage, this.bgEmbeddedImage, this.label, this.labelColor, this.titleColor, this.hint, this.wordCount, this.submitBtnText, this.promptInputTitle, this.waitSeconds = 30, this.showImageStyleSelector = false, this.noPrompt = false, this.extension, }); toJson() => { 'id': id, 'title': title, 'description': description, 'support_stream': supportStream, 'model_type': modelType, // 'text-generation' | 'image-generation' 'vendor': vendor, 'categories': categories, 'bg_image': bgImage, 'bg_embedded_image': bgEmbeddedImage, 'label': label, 'label_color': labelColor, 'title_color': titleColor, 'hint': hint, 'word_count': wordCount, 'submit_btn_text': submitBtnText, 'prompt_input_title': promptInputTitle, 'wait_seconds': waitSeconds, 'show_image_style_selector': showImageStyleSelector, 'no_prompt': noPrompt, 'extension': extension?.toJson(), }; static fromJson(Map json) { return CreativeIslandItem( id: json['id'], title: json['title'], description: json['description'], supportStream: json['support_stream'] ?? false, modelType: json['model_type'] ?? 'text-generation', vendor: json['vendor'], categories: ((json['category'] ?? '') as String).split(',').toList(), bgImage: json['bg_image'], bgEmbeddedImage: json['bg_embedded_image'], label: json['label'], labelColor: json['label_color'], titleColor: json['title_color'], hint: json['hint'], wordCount: json['word_count'] ?? 0, submitBtnText: json['submit_btn_text'], promptInputTitle: json['prompt_input_title'], waitSeconds: json['wait_seconds'] ?? 30, showImageStyleSelector: json['show_image_style_selector'] ?? false, noPrompt: json['no_prompt'] ?? false, extension: json['extension'] != null ? CreativeIslandItemExtension.fromJson(json['extension']) : null, ); } } class CreativeIslandCompletionAsyncResp { String taskId; CreativeIslandCompletionAsyncResp(this.taskId); toJson() => { 'task_id': taskId, }; static CreativeIslandCompletionAsyncResp fromJson(Map json) { return CreativeIslandCompletionAsyncResp(json['task_id']); } } class CreativeItemInServer { int id; int? userId; String islandId; int? islandType; String? vendor; String? islandName; String? islandTitle; String? arguments; String? prompt; String? answer; int? quotaUsed; int? status; int? shared; String? filterName; DateTime? createdAt; DateTime? updatedAt; bool showBetaFeature; CreativeItemInServer({ required this.id, required this.islandId, required this.showBetaFeature, this.userId, this.islandType, this.islandName, this.islandTitle, this.vendor, this.arguments, this.prompt, this.answer, this.quotaUsed, this.status, this.shared, this.createdAt, this.updatedAt, }); CreativeItemArguments get creativeItemArguments { if (arguments != null) { return CreativeItemArguments.fromJson(jsonDecode(arguments!)); } return CreativeItemArguments( width: 512, height: 512, steps: 1, imageCount: 1, ); } bool get isShared => shared == 1; String get errorCode => NumberFormat('E000000000').format(id); toJson() => { 'id': id, 'user_id': userId, 'island_id': islandId, 'island_type': islandType, 'island_name': islandName, 'island_title': islandTitle, 'vendor': vendor, 'arguments': arguments, 'prompt': prompt, 'answer': answer, 'quota_used': quotaUsed, 'status': status, 'shared': shared, 'show_beta_feature': showBetaFeature, 'created_at': createdAt?.toIso8601String(), 'updated_at': updatedAt?.toIso8601String(), }; static CreativeItemInServer fromJson(Map json) { return CreativeItemInServer( id: json['id'], userId: json['user_id'], islandId: json['island_id'], islandType: json['island_type'], islandName: json['island_name'], islandTitle: json['island_title'], vendor: json['vendor'], arguments: json['arguments'], prompt: json['prompt'], answer: json['answer'], quotaUsed: json['quota_used'], status: json['status'], shared: json['shared'], showBetaFeature: json['show_beta_feature'] ?? false, createdAt: DateTime.parse(json['created_at']), updatedAt: DateTime.parse(json['updated_at']), ); } bool get isSuccessful => status == 3; bool get isFailed => status != null && (status == 4 || (status! <= 2 && answer != null)); bool get isProcessing => status != null && status! <= 2 && answer == null; bool get isTextType => islandType == 1; bool get isImageType => islandType != null && (islandType == 2 || islandType! >= 5); bool get isVideoType => islandType != null && islandType == 3; List get images { try { if ((isImageType || isVideoType) && answer != null && answer != '' && isSuccessful) { return (jsonDecode(answer!) as List).cast(); } return []; } catch (e) { return []; } } String get firstImagePreview { final imgs = images; if (imgs.isEmpty) { return ""; } if (!imgs.first.startsWith('https://ssl.aicode.cc/')) { return imgs.first; } final original = imgs.first.split('?').first; if (original.toLowerCase().endsWith('.jpg') || original.toLowerCase().endsWith('.jpeg') || original.toLowerCase().endsWith('.png') || original.toLowerCase().endsWith('.webp') || original.toLowerCase().endsWith('.gif')) { return "$original-avatar"; } return imgs.first; } Map get params { if (arguments != null) { return jsonDecode(arguments!); } return {}; } /// 上传的原图 String? get originalImage { return params['image']; } String get markdownAnswer { if (isProcessing) { return '正在生成中...'; } if (isFailed) { return '生成失败\n\n```\n$answer\n```'; } if (isImageType && answer != null && isSuccessful) { return images.map((e) => '![image]($e)').join("\n\n"); } return answer ?? ''; } } class CreativeItemArguments { int? width; int? height; int? steps; int? imageCount; String? image; int? wordCount; String? negativePrompt; String? realPrompt; String? realNegativePrompt; String? modelName; int? seed; String? filterName; CreativeItemArguments({ this.width, this.height, this.steps, this.imageCount, this.image, this.wordCount, this.negativePrompt, this.realPrompt, this.realNegativePrompt, this.modelName, this.seed, this.filterName, }); toJson() => { 'width': width, 'height': height, 'steps': steps, 'image_count': imageCount, 'image': image, 'word_count': wordCount, 'negative_prompt': negativePrompt, 'real_prompt': realPrompt, 'real_negative_prompt': realNegativePrompt, 'model_name': modelName, 'seed': seed, 'filter_name': filterName, }; static CreativeItemArguments fromJson(Map json) { return CreativeItemArguments( width: json['width'], height: json['height'], steps: json['steps'], imageCount: json['image_count'] ?? 1, image: json['image'], wordCount: json['word_count'], negativePrompt: json['negative_prompt'], realPrompt: json['real_prompt'], realNegativePrompt: json['real_negative_prompt'], modelName: json['model_name'], seed: json['seed'], filterName: json['filter_name'], ); } } class CreativeIslandItemV2 { String id; String title; String titleColor; String previewImage; String routeUri; String tag; String? note; String size; CreativeIslandItemV2({ required this.id, required this.title, required this.titleColor, required this.previewImage, required this.routeUri, this.tag = '', this.note, this.size = 'large', }); toJson() => { 'id': id, 'title': title, 'title_color': titleColor, 'preview_image': previewImage, 'route_uri': routeUri, 'tag': tag, 'note': note, 'size': size, }; static CreativeIslandItemV2 fromJson(Map json) { return CreativeIslandItemV2( id: json['id'], title: json['title'], titleColor: json['title_color'], previewImage: json['preview_image'], routeUri: json['route_uri'], tag: json['tag'] ?? '', note: json['note'], size: json['size'] ?? 'large', ); } } ================================================ FILE: lib/repo/api/image_model.dart ================================================ import 'dart:convert'; class ImageModel { int id; String modelId; String modelName; String vendor; String? realModel; int? status; ImageModel({ required this.id, required this.modelId, required this.modelName, required this.vendor, this.realModel, this.status, }); toJson() => { 'id': id, 'model_id': modelId, 'model_name': modelName, 'vendor': vendor, 'real_model': realModel, 'status': status, }; static ImageModel fromJson(Map json) { return ImageModel( id: json['id'], modelId: json['model_id'], modelName: json['model_name'], vendor: json['vendor'], realModel: json['real_model'], status: json['status'], ); } } class ImageModelFilter { int id; String name; String modelId; String? previewImage; int? status; String? meta; ImageModelFilter({ required this.id, required this.name, required this.modelId, this.previewImage, this.status, this.meta, }); toJson() => { 'id': id, 'name': name, 'model_id': modelId, 'preview_image': previewImage, 'status': status, 'meta': meta, }; static ImageModelFilter fromJson(Map json) { return ImageModelFilter( id: json['id'], name: json['name'], modelId: json['model_id'], previewImage: json['preview_image'], status: json['status'], meta: jsonEncode(json['meta'] ?? {}), ); } } ================================================ FILE: lib/repo/api/info.dart ================================================ import 'package:askaide/repo/api/model.dart'; /// 服务器支持的能力信息 class Capabilities { /// 是否支持 Apple Pay final bool applePayEnabled; /// 是否支持 Stripe final bool stripeEnabled; /// 是否支持微信登录 final bool wechatSigninEnabled; /// 是否支持微信支付 final bool wechatPayEnabled; /// 是否支持其它 final bool otherPayEnabled; /// 是否支持翻译 final bool translateEnabled; /// 是否支持邮箱 final bool mailEnabled; /// 是否支持 OpenAI final bool openaiEnabled; /// 首页显示的模型信息 final List homeModels; /// 是否显示首页模型描述 final bool showHomeModelDescription; /// 首页路由 final String homeRoute; /// 是否支持 Websocket final bool supportWebsocket; /// 是否支持 API Keys 功能 final bool supportAPIKeys; /// 是否显示绘玩 final bool disableGallery; /// 是否支持创作岛 final bool disableCreationIsland; /// 是否禁用数字人 final bool disableDigitalHuman; /// 是否禁用聊天 final bool disableChat; /// 服务状态页 final String serviceStatusPage; /// 是否支持语音转文字 final bool enableVoiceToText; /// 是否支持文字转语音 final bool enableTextToVoice; Capabilities({ required this.applePayEnabled, required this.otherPayEnabled, required this.translateEnabled, required this.mailEnabled, required this.openaiEnabled, required this.homeModels, this.homeRoute = '/', this.showHomeModelDescription = true, this.supportWebsocket = false, this.supportAPIKeys = false, this.disableGallery = false, this.disableCreationIsland = false, this.disableDigitalHuman = false, this.disableChat = false, this.serviceStatusPage = '', this.wechatSigninEnabled = false, this.stripeEnabled = false, this.wechatPayEnabled = false, this.enableVoiceToText = false, this.enableTextToVoice = false, }); factory Capabilities.fromJson(Map json) { return Capabilities( wechatSigninEnabled: json['wechat_signin_enabled'] ?? false, applePayEnabled: json['apple_pay_enabled'] ?? false, stripeEnabled: json['stripe_enabled'] ?? false, otherPayEnabled: json['other_pay_enabled'] ?? false, translateEnabled: json['translate_enabled'] ?? false, mailEnabled: json['mail_enabled'] ?? false, openaiEnabled: json['openai_enabled'] ?? false, homeModels: ((json['home_models_v2'] ?? []) as List).map((e) => HomeModelV2.fromJson(e)).toList(), // homeRoute: json['home_route'] ?? '/', homeRoute: '/', showHomeModelDescription: json['show_home_model_description'] ?? true, supportWebsocket: json['support_websocket'] ?? false, supportAPIKeys: json['support_api_keys'] ?? false, disableGallery: json['disable_gallery'] ?? false, disableCreationIsland: json['disable_creation_island'] ?? false, disableDigitalHuman: json['disable_digital_human'] ?? false, disableChat: json['disable_chat'] ?? false, serviceStatusPage: json['service_status_page'] ?? '', wechatPayEnabled: json['wechat_pay_enabled'] ?? false, enableVoiceToText: json['enable_voice_to_text'] ?? false, enableTextToVoice: json['enable_text_to_voice'] ?? false, ); } Map toJson() { return { 'wechat_signin_enabled': wechatSigninEnabled, 'apple_pay_enabled': applePayEnabled, 'stripe_enabled': stripeEnabled, 'wechat_pay_enabled': wechatPayEnabled, 'other_pay_enabled': otherPayEnabled, 'translate_enabled': translateEnabled, 'mail_enabled': mailEnabled, 'openai_enabled': openaiEnabled, 'home_models': homeModels.map((e) => e.toJson()).toList(), 'home_route': homeRoute, 'show_home_model_description': showHomeModelDescription, 'support_websocket': supportWebsocket, 'support_api_keys': supportAPIKeys, 'disable_gallery': disableGallery, 'disable_creation_island': disableCreationIsland, 'disable_digital_human': disableDigitalHuman, 'disable_chat': disableChat, 'service_status_page': serviceStatusPage, 'enable_voice_to_text': enableVoiceToText, 'enable_text_to_voice': enableTextToVoice, }; } } /// 首页显示的模型信息 class HomeModel { /// 模型名称 final String name; /// 模型 ID final String modelId; /// 模型描述 final String desc; /// 模型代表色 final String color; /// 是否是强大的模型 final bool powerful; /// 是否支持视觉 final bool supportVision; HomeModel({ required this.name, required this.modelId, required this.desc, required this.color, this.powerful = false, this.supportVision = false, }); factory HomeModel.fromJson(Map json) => HomeModel( name: json["name"], modelId: json["model_id"], desc: json["desc"] ?? '', color: json["color"] ?? 'FF67AC5C', powerful: json['powerful'] ?? false, supportVision: json['support_vision'] ?? false, ); Map toJson() => { "name": name, "model_id": modelId, "desc": desc, "color": color, "powerful": powerful, "support_vision": supportVision, }; } ================================================ FILE: lib/repo/api/keys.dart ================================================ class UserAPIKey { int id; int? userId; String name; String token; int status; DateTime? validBefore; DateTime? createdAt; UserAPIKey({ required this.id, this.userId, required this.name, required this.token, required this.status, this.validBefore, this.createdAt, }); factory UserAPIKey.fromJson(Map json) { return UserAPIKey( id: json['id'], userId: json['user_id'], name: json['name'], token: json['token'], status: json['status'], validBefore: json['valid_before'] != null ? DateTime.parse(json['valid_before']) : null, createdAt: json['created_at'] != null ? DateTime.parse(json['created_at']) : null, ); } Map toJson() { return { 'id': id, 'user_id': userId, 'name': name, 'token': token, 'status': status, 'valid_before': validBefore?.toIso8601String(), 'created_at': createdAt?.toIso8601String(), }; } } ================================================ FILE: lib/repo/api/model.dart ================================================ /// 自定义首页模型 class HomeModelV2 { /// 类型:model/room_gallery/rooms/room_enterprise String type; String id; String name; String? avatarUrl; String? modelId; String? modelName; bool supportVision; bool supportReasoning; bool supportSearch; HomeModelV2({ required this.type, required this.id, required this.name, required this.supportVision, this.modelId, this.modelName, this.avatarUrl, this.supportReasoning = false, this.supportSearch = false, }); String get uniqueKey { return '$type|$id'; } static HomeModelV2 fromJson(Map json) { return HomeModelV2( type: json['type'], id: json['id'], name: json['name'], modelId: json['model_id'], modelName: json['model_name'], supportVision: json['support_vision'] ?? false, supportReasoning: json['support_reasoning'] ?? false, supportSearch: json['support_search'] ?? false, avatarUrl: json['avatar_url'], ); } toJson() => { 'id': id, 'type': type, 'name': name, 'model_id': modelId, 'model_name': modelName, 'support_vision': supportVision, 'support_reasoning': supportReasoning, 'support_search': supportSearch, 'avatar_url': avatarUrl, }; } ================================================ FILE: lib/repo/api/notification.dart ================================================ class NotifyMessage { int id; int articleId; String title; String content; String? type; DateTime? createdAt; NotifyMessage({ required this.id, required this.title, required this.content, required this.articleId, this.type, this.createdAt, }); factory NotifyMessage.fromJson(Map json) { return NotifyMessage( id: json['id'], articleId: json['article_id'] ?? json['id'], title: json['title'], content: json['content'], type: json['type'], createdAt: json['created_at'] != null ? DateTime.parse(json['created_at']) : null, ); } Map toJson() { return { 'id': id, 'article_id': articleId, 'title': title, 'content': content, 'type': type, 'created_at': createdAt?.toIso8601String(), }; } } ================================================ FILE: lib/repo/api/page.dart ================================================ class PagedData { int page; int perPage; int? total; int? lastPage; List data; PagedData({ required this.page, required this.perPage, this.total, this.lastPage, required this.data, }); } class OffsetPageData { int startId; int lastId; int perPage; List data; OffsetPageData({ required this.startId, required this.lastId, required this.perPage, required this.data, }); } ================================================ FILE: lib/repo/api/payment.dart ================================================ class OtherPayCreatedReponse { String params; String paymentId; bool sandbox; OtherPayCreatedReponse(this.params, this.paymentId, {this.sandbox = false}); toJson() => { 'params': params, 'payment_id': paymentId, 'sandbox': sandbox, }; static OtherPayCreatedReponse fromJson(Map json) { return OtherPayCreatedReponse( json['params'], json['payment_id'], sandbox: json['sandbox'] ?? false, ); } } class PaymentProduct { String id; String name; int quota; int retailPrice; int retailPriceUSD; String expirePolicy; String expirePolicyText; bool recommend; String? description; List methods; PaymentProduct({ required this.id, required this.name, required this.quota, required this.retailPrice, required this.expirePolicy, required this.expirePolicyText, this.recommend = false, this.description, this.retailPriceUSD = 0, this.methods = const [], }); String get retailPriceText => '¥${(retailPrice / 100).toStringAsFixed(0)}'; String get retailPriceUSDText => '\$${(retailPriceUSD / 100).toStringAsFixed(2)}'; /// 是否支持 Stripe 支付 bool get supportStripe => methods.contains('stripe') || methods.isEmpty; toJson() => { 'id': id, 'name': name, 'quota': quota, 'retail_price': retailPrice, 'retail_price_usd': retailPriceUSD, 'expire_policy': expirePolicy, 'expire_policy_text': expirePolicyText, 'recommend': recommend, 'description': description, 'methods': methods, }; static PaymentProduct fromJson(Map json) { return PaymentProduct( id: json['id'], name: json['name'], quota: json['quota'], retailPrice: json['retail_price'], retailPriceUSD: json['retail_price_usd'] ?? 0, expirePolicy: json['expire_policy'], expirePolicyText: json['expire_policy_text'], recommend: json['recommend'] ?? false, description: json['description'], methods: ((json['methods'] ?? []) as List) .map((e) => e.toString()) .toList(), ); } } class PaymentProducts { final List consume; final String? note; final bool preferUSD; PaymentProducts(this.consume, {this.note, this.preferUSD = false}); toJson() => { 'consume': consume, 'note': note, 'prefer_usd': preferUSD, }; static PaymentProducts fromJson(Map json) { return PaymentProducts( (json['consume'] as List) .map((e) => PaymentProduct.fromJson(e)) .toList(), note: json['note'], preferUSD: json['prefer_usd'] ?? false, ); } } class PaymentStatus { final bool success; final String? note; PaymentStatus(this.success, {this.note}); toJson() => { 'success': success, 'note': note, }; static PaymentStatus fromJson(Map json) { return PaymentStatus( json['success'], note: json['note'], ); } } class WechatPaymentCreatedResponse { final String paymentId; final bool sandbox; final String? codeUrl; final String? prepayId; final String? package; final String? partnerId; final String? appId; final String? noncestr; final String? timestamp; final String? sign; WechatPaymentCreatedResponse( this.paymentId, this.sandbox, { this.codeUrl, this.prepayId, this.package, this.partnerId, this.appId, this.noncestr, this.timestamp, this.sign, }); toJson() => { 'payment_id': paymentId, 'sandbox': sandbox, 'code_url': codeUrl, 'prepay_id': prepayId, 'package': package, 'partner_id': partnerId, 'app_id': appId, 'noncestr': noncestr, 'timestamp': timestamp, 'sign': sign, }; static WechatPaymentCreatedResponse fromJson(Map json) { return WechatPaymentCreatedResponse( json['payment_id'], json['sandbox'] ?? false, codeUrl: json['code_url'], prepayId: json['prepay_id'], package: json['package'], partnerId: json['partner_id'], appId: json['app_id'], noncestr: json['noncestr'], timestamp: json['timestamp'], sign: json['sign'], ); } } class StripePaymentCreatedResponse { final String paymentId; final String customer; final String paymentIntent; final String ephemeralKey; final String publishableKey; final String proxyUrl; StripePaymentCreatedResponse( this.paymentId, this.customer, this.paymentIntent, this.ephemeralKey, this.publishableKey, this.proxyUrl, ); toJson() => { 'payment_id': paymentId, 'customer': customer, 'payment_intent': paymentIntent, 'ephemeral_key': ephemeralKey, 'publishable_key': publishableKey, 'proxy_url': proxyUrl, }; static StripePaymentCreatedResponse fromJson(Map json) { return StripePaymentCreatedResponse( json['payment_id'], json['customer'], json['payment_intent'], json['ephemeral_key'], json['publishable_key'], json['proxy_url'] ?? '', ); } } ================================================ FILE: lib/repo/api/quota.dart ================================================ import 'package:intl/intl.dart'; class QuotaEvaluated { int cost; bool enough; int? waitDuration; QuotaEvaluated({required this.cost, this.enough = true, this.waitDuration}); toJson() => { 'cost': cost, 'enough': enough, 'wait_duration': waitDuration, }; static QuotaEvaluated fromJson(Map json) { return QuotaEvaluated( cost: json['cost'], enough: json['enough'] ?? true, waitDuration: json['wait_duration'], ); } } class QuotaResp { int total; List details; QuotaResp(this.total, this.details); toJson() => { 'total': total, 'details': details.map((e) => e.toJson()).toList(), }; static QuotaResp fromJson(Map json) { return QuotaResp( json['total'], (json['details'] as List).map((e) => QuotaDetail.fromJson(e)).toList(), ); } } class QuotaDetail { int id; int userId; int quota; int rest; String? note; DateTime periodStartAt; DateTime periodEndAt; bool expired; DateTime createdAt; QuotaDetail({ required this.id, required this.userId, required this.quota, required this.rest, required this.periodStartAt, required this.periodEndAt, required this.expired, required this.createdAt, this.note, }); toJson() => { 'id': id, 'user_id': userId, 'quota': quota, 'rest': rest, 'period_start_at': periodStartAt.toIso8601String(), 'period_end_at': periodEndAt.toIso8601String(), 'expired': expired, 'created_at': createdAt.toIso8601String(), 'note': note, }; static QuotaDetail fromJson(Map json) { return QuotaDetail( id: json['id'], userId: json['user_id'], quota: json['quota'], rest: json['rest'], note: json['note'], periodStartAt: DateTime.parse(json['period_start_at']), periodEndAt: DateTime.parse(json['period_end_at']), expired: json['expired'], createdAt: DateTime.parse(json['created_at'] ?? json['period_start_at']), ); } } class Quota { int quota; int rest; int used; Quota(this.quota, this.used, this.rest); double quotaPercent() { return (used * 1.0) / quota; } int quotaRemain() { return quota - used; } String quotaRemainString() { return (quota - used).toString(); } String quotaString() { return NumberFormat('0,000').format(quota); } String usedString() { return NumberFormat('0,000').format(used); } toJson() => { 'quota': quota, 'used': used, 'rest': rest, }; static fromJson(Map json) { return Quota( json['quota'], json['used'], json['rest'], ); } } ================================================ FILE: lib/repo/api/room_gallery.dart ================================================ class RoomGalleryResponse { List galleries; List tags; RoomGalleryResponse({ required this.galleries, required this.tags, }); toJson() => { 'galleries': galleries.map((e) => e.toJson()).toList(), 'tags': tags, }; static RoomGalleryResponse fromJson(Map json) { return RoomGalleryResponse( galleries: ((json['data'] ?? []) as List) .map((e) => RoomGallery.fromJson(e)) .toList(), tags: ((json['tags'] ?? []) as List) .map((e) => e.toString()) .toList(), ); } } class RoomGallery { int id; String name; String avatarUrl; String description; List tags; RoomGallery({ required this.id, required this.name, required this.avatarUrl, required this.description, required this.tags, }); toJson() => { 'id': id, 'name': name, 'avatar_url': avatarUrl, 'description': description, 'tags': tags, }; static RoomGallery fromJson(Map json) { return RoomGallery( id: json['id'], name: json['name'], avatarUrl: json['avatar_url'] ?? '', description: json['description'] ?? '', tags: ((json['tags'] ?? []) as List) .map((e) => e.toString()) .toList(), ); } } ================================================ FILE: lib/repo/api/user.dart ================================================ import 'package:askaide/repo/api/quota.dart'; class User { int id; String? name; String? email; String? phone; String? inviteCode; String? avatar; int? invitedBy; String? unionId; User( this.id, this.name, this.email, this.phone, { this.inviteCode, this.avatar, this.invitedBy, this.unionId, }); /// 是否需要绑定手机号码 bool get needBindPhone => phone == null || phone!.isEmpty; String displayName() { if (name != null && name!.isNotEmpty) { return name!; } if (email != null && email!.isNotEmpty) { return email!; } if (phone != null && phone!.isNotEmpty) { return phone!; } return '-'; } toJson() => { 'id': id, 'name': name, 'email': email, 'phone': phone, 'invite_code': inviteCode, 'avatar': avatar, 'invited_by': invitedBy, 'union_id': unionId, }; static fromJson(Map json) { return User( json['id'], json['name'], json['email'], json['phone'], inviteCode: json['invite_code'], avatar: json['avatar'], invitedBy: json['invited_by'], unionId: json['union_id'], ); } } class UserInfo { User user; Quota quota; UserControl control; bool get showInviteMessage => control.enableInvite && user.inviteCode != null && user.inviteCode != ''; UserInfo(this.user, this.quota, this.control); toJson() => { 'user': user.toJson(), 'quota': quota.toJson(), 'control': control.toJson(), }; static fromJson(Map json) { return UserInfo( User.fromJson(json['user']), Quota.fromJson(json['quota']), UserControl.fromJson(json['control']), ); } static UserInfo empty() { return UserInfo( User(0, "-", "-", "-"), Quota(0, 0, 0), UserControl(enableInvite: true), ); } } class UserControl { bool isSetPassword; bool enableInvite; String? inviteMessage; String? userCardBg; String? inviteCardBg; String? inviteCardColor; String? inviteCardSlogan; bool withLab; UserControl({ required this.enableInvite, this.inviteMessage, this.userCardBg, this.inviteCardBg, this.inviteCardColor, this.inviteCardSlogan, this.isSetPassword = false, this.withLab = false, }); toJson() => { 'enable_invite': enableInvite, 'invite_message': inviteMessage, 'user_card_bg': userCardBg, 'invite_card_bg': inviteCardBg, 'invite_card_color': inviteCardColor, 'invite_card_slogan': inviteCardSlogan, 'is_set_password': isSetPassword, 'with_lab': withLab, }; static UserControl fromJson(Map json) { return UserControl( enableInvite: json['enable_invite'] ?? true, inviteMessage: json['invite_message'], userCardBg: json['user_card_bg'], inviteCardBg: json['invite_card_bg'], inviteCardColor: json['invite_card_color'], inviteCardSlogan: json['invite_card_slogan'], isSetPassword: json['is_set_pwd'] ?? false, withLab: json['with_lab'] ?? false, ); } } ================================================ FILE: lib/repo/api_server.dart ================================================ import 'dart:convert'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/env.dart'; import 'package:askaide/helper/error.dart'; import 'package:askaide/helper/event.dart'; import 'package:askaide/helper/http.dart'; import 'package:askaide/helper/logger.dart'; import 'package:askaide/helper/platform.dart'; import 'package:askaide/page/component/global_alert.dart'; import 'package:askaide/repo/api/admin/channels.dart'; import 'package:askaide/repo/api/admin/models.dart'; import 'package:askaide/repo/api/admin/payment.dart'; import 'package:askaide/repo/api/admin/users.dart'; import 'package:askaide/repo/api/article.dart'; import 'package:askaide/repo/api/creative.dart'; import 'package:askaide/repo/api/image_model.dart'; import 'package:askaide/repo/api/info.dart'; import 'package:askaide/repo/api/keys.dart'; import 'package:askaide/repo/api/model.dart'; import 'package:askaide/repo/api/notification.dart'; import 'package:askaide/repo/api/page.dart'; import 'package:askaide/repo/api/payment.dart'; import 'package:askaide/repo/api/quota.dart'; import 'package:askaide/repo/api/room_gallery.dart'; import 'package:askaide/repo/api/user.dart'; import 'package:askaide/repo/model/group.dart'; import 'package:askaide/repo/model/misc.dart'; import 'package:askaide/repo/settings_repo.dart'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; class APIServer { /// 单例 static final APIServer _instance = APIServer._internal(); APIServer._internal(); factory APIServer() { return _instance; } GlobalAlertEvent _globalAlertEvent = GlobalAlertEvent(id: '', type: 'info', pages: [], message: ''); GlobalAlertEvent get globalAlertEvent => _globalAlertEvent; late String url; late String apiToken; late String language; init(SettingRepository setting) { apiToken = setting.stringDefault(settingAPIServerToken, ''); language = setting.stringDefault(settingLanguage, 'zh'); url = setting.stringDefault(settingServerURL, apiServerURL); Logger.instance.d('API Server URL: $url'); setting.listen((settings, key, value) { if (key == settingAPIServerToken) { apiToken = settings.getDefault(settingAPIServerToken, ''); } if (key == settingLanguage) { language = settings.getDefault(settingLanguage, 'zh'); } if (key == settingServerURL) { url = settings.getDefault(settingServerURL, apiServerURL); Logger.instance.d('API Server URL Changed: $url'); } }); } final List _retryableErrors = [ DioExceptionType.connectionTimeout, DioExceptionType.sendTimeout, DioExceptionType.receiveTimeout, ]; /// 异常处理 Object _exceptionHandle(Object e, Object? stackTrace) { Logger.instance.e(e, stackTrace: stackTrace as StackTrace?); if (e is DioException) { if (e.response != null) { final resp = e.response!; if (resp.data is Map && resp.data['error'] != null && resp.statusCode != 402 && resp.statusCode != 401) { return resp.data['error'] ?? e.toString(); } if (resp.statusCode != null) { final ret = resolveHTTPStatusCode(resp.statusCode!); if (ret != null) { return ret; } } return resp.statusMessage ?? e.toString(); } if (_retryableErrors.contains(e.type)) { return '请求超时,请重试'; } } return e.toString(); } Options _buildRequestOptions({int? requestTimeout = 10000}) { return Options( headers: _buildAuthHeaders(), receiveDataWhenStatusError: true, sendTimeout: requestTimeout != null ? Duration(milliseconds: requestTimeout) : null, receiveTimeout: requestTimeout != null ? Duration(milliseconds: requestTimeout) : null, ); } Map _buildAuthHeaders() { final headers = { 'X-CLIENT-VERSION': clientVersion, 'X-PLATFORM': PlatformTool.operatingSystem(), 'X-PLATFORM-VERSION': PlatformTool.operatingSystemVersion(), 'X-LANGUAGE': language, }; if (apiToken == '') { return headers; } headers['Authorization'] = 'Bearer $apiToken'; return headers; } /// 获取用户 ID,如果未登录则返回 null int? localUserID() { if (apiToken == '') { return null; } // 从 Jwt Token 中获取用户 ID final parts = apiToken.split('.'); if (parts.length != 3) { return null; } final payload = parts[1]; final normalized = base64.normalize(payload); final resp = utf8.decode(base64.decode(normalized)); final data = jsonDecode(resp); return data['id']; } Future sendGetRequest( String endpoint, T Function(dynamic) parser, { Map? queryParameters, int? requestTimeout = 10000, }) async { return request( HttpClient.get( '$url$endpoint', queryParameters: queryParameters, options: _buildRequestOptions(requestTimeout: requestTimeout), ), parser, ); } Future sendCachedGetRequest( String endpoint, T Function(dynamic) parser, { String? subKey, Duration duration = const Duration(days: 1), Map? queryParameters, bool forceRefresh = false, }) async { return request( HttpClient.getCached( '$url$endpoint', queryParameters: queryParameters, subKey: subKey, duration: duration, forceRefresh: forceRefresh, options: _buildRequestOptions(), ), parser, ); } Future sendPostRequest( String endpoint, T Function(dynamic) parser, { Map? queryParameters, Map? formData, VoidCallback? finallyCallback, }) async { return request( HttpClient.post( '$url$endpoint', queryParameters: queryParameters, formData: formData, options: _buildRequestOptions(), ), parser, finallyCallback: finallyCallback, ); } Future sendPostJSONRequest( String endpoint, T Function(dynamic) parser, { Map? queryParameters, Map? data, VoidCallback? finallyCallback, }) async { return request( HttpClient.postJSON( '$url$endpoint', queryParameters: queryParameters, data: data, options: _buildRequestOptions(), ), parser, finallyCallback: finallyCallback, ); } Future sendPutRequest( String endpoint, T Function(dynamic) parser, { String? subKey, Duration duration = const Duration(days: 1), Map? queryParameters, Map? formData, bool forceRefresh = false, VoidCallback? finallyCallback, }) async { return request( HttpClient.put( '$url$endpoint', queryParameters: queryParameters, formData: formData, options: _buildRequestOptions(), ), parser, finallyCallback: finallyCallback, ); } Future sendPutJSONRequest( String endpoint, T Function(dynamic) parser, { String? subKey, Duration duration = const Duration(days: 1), Map? queryParameters, Map? data, bool forceRefresh = false, VoidCallback? finallyCallback, }) async { return request( HttpClient.putJSON( '$url$endpoint', queryParameters: queryParameters, data: data, options: _buildRequestOptions(), ), parser, finallyCallback: finallyCallback, ); } Future sendDeleteRequest( String endpoint, T Function(dynamic) parser, { String? subKey, Duration duration = const Duration(days: 1), Map? queryParameters, Map? formData, bool forceRefresh = false, VoidCallback? finallyCallback, }) async { return request( HttpClient.delete( '$url$endpoint', queryParameters: queryParameters, formData: formData, options: _buildRequestOptions(), ), parser, finallyCallback: finallyCallback, ); } Future request( Future> respFuture, T Function(dynamic) parser, { VoidCallback? finallyCallback, }) async { try { final resp = await respFuture; if (resp.statusCode != 200 && resp.statusCode != 304) { return Future.error(resp.data['error']); } try { var msg = resp.headers.value('aidea-global-alert-msg'); if (msg != null) { msg = utf8.decode(base64Decode(msg)); } // Logger.instance.d("API Response: ${resp.data}"); final globalAlertEvent = GlobalAlertEvent( id: resp.headers.value('aidea-global-alert-id') ?? '', type: resp.headers.value('aidea-global-alert-type') ?? 'info', pages: (resp.headers.value('aidea-global-alert-pages') ?? '').split(',').where((e) => e != '').toList(), message: msg, ); if (globalAlertEvent.id != '' && globalAlertEvent.id != _globalAlertEvent.id) { _globalAlertEvent = globalAlertEvent; GlobalEvent().emit('global-alert', _globalAlertEvent); } } catch (e) { Logger.instance.e(e); } return parser(resp); } catch (e, stackTrace) { return Future.error(_exceptionHandle(e, stackTrace)); } finally { finallyCallback?.call(); } } String? _cacheSubKey() { final localUserId = localUserID(); if (localUserId == null) { return null; } return 'local-uid=$localUserId'; } /// 用户配额详情 Future quotaDetails() async { return sendGetRequest( '/v1/users/quota', (resp) => QuotaResp.fromJson(resp.data), ); } /// 用户信息 Future userInfo({bool cache = true}) async { return sendCachedGetRequest( '/v1/users/current', (resp) => UserInfo.fromJson(resp.data), duration: const Duration(minutes: 1), subKey: _cacheSubKey(), forceRefresh: !cache, ); } /// 检查手机号是否存在 Future checkPhoneExists(String username) async { return sendPostRequest( '/v1/auth/2in1/check', (resp) => UserExistenceResp.fromJson(resp.data), formData: Map.from({ 'username': username, }), ); } /// 手机登录或者注册账号 Future signInOrUp({ required String username, required String verifyCodeId, required String verifyCode, String? inviteCode, String? wechatBindToken, }) async { return sendPostRequest( '/v1/auth/2in1/sign-inup', (resp) => SignInResp.fromJson(resp.data), formData: Map.from({ 'username': username, 'verify_code_id': verifyCodeId, 'verify_code': verifyCode, 'invite_code': inviteCode, 'wechat_bind_token': wechatBindToken, }), ); } /// 使用密码登录 Future signInWithPassword( String username, String password, { String? wechatBindToken, }) async { return sendPostRequest( '/v1/auth/sign-in', (resp) => SignInResp.fromJson(resp.data), formData: Map.from({ 'username': username, 'password': password, 'wechat_bind_token': wechatBindToken, }), ); } /// 使用 Apple 账号登录 Future signInWithApple({ required String userIdentifier, String? givenName, String? familyName, String? email, String? authorizationCode, String? identityToken, String? wechatBindToken, }) async { return sendPostRequest( '/v1/auth/sign-in-apple/', (resp) => SignInResp.fromJson(resp.data), formData: Map.from({ 'user_identifier': userIdentifier, 'given_name': givenName, 'family_name': familyName, 'email': email, 'authorization_code': authorizationCode, 'identity_token': identityToken, 'is_ios': PlatformTool.isIOS() || PlatformTool.isMacOS(), 'wechat_bind_token': wechatBindToken, }), ); } /// 尝试使用 微信账号登录 Future trySignInWithWechat({ required String code, }) async { return sendPostRequest( '/v1/auth/sign-in-wechat/try', (resp) => TrySignInResp.fromJson(resp.data), formData: Map.from({ 'code': code, }), ); } /// 使用 微信账号登录 Future signInWithWechat({ required String token, }) async { return sendPostRequest( '/v1/auth/sign-in-wechat/', (resp) => SignInResp.fromJson(resp.data), formData: Map.from({ 'token': token, }), ); } /// 绑定微信账号 Future bindWechat({required String code}) async { return sendPostRequest( '/v1/auth/bind-wechat/', (resp) => {}, formData: Map.from({ 'code': code, }), ); } /// 获取代理服务器列表 Future> proxyServers(String service) async { return sendCachedGetRequest( '/v1/proxy/servers', (resp) => (resp['servers'][service] as List).map((e) => e.toString()).toList(), subKey: _cacheSubKey(), ); } /// 获取模型列表 Future> models({bool cache = true}) async { return sendCachedGetRequest( '/v2/models', (resp) { var models = []; for (var model in resp.data) { models.add(Model.fromJson(model)); } return models; }, subKey: _cacheSubKey(), forceRefresh: !cache, ); } /// 获取系统级提示语列表 Future> prompts() async { return sendCachedGetRequest( '/v1/prompts', (resp) { var prompts = []; for (var prompt in resp.data) { prompts.add(Prompt(prompt['title'], prompt['content'])); } return prompts; }, subKey: _cacheSubKey(), ); } /// 获取提示语示例 Future> examples() async { return sendCachedGetRequest( '/v1/examples', (resp) { var examples = []; for (var example in resp.data) { examples.add(ChatExample( example['title'], content: example['content'], models: example['models'], )); } return examples; }, subKey: _cacheSubKey(), ); } /// 获取头像列表 Future> avatars() async { return sendCachedGetRequest( '/v1/images/avatar', (resp) { return (resp.data['avatars'] as List).map((e) => e.toString()).toList(); }, ); } /// 获取背景图列表 Future> backgrounds() async { return sendCachedGetRequest( '/v1/images/background', (resp) { var images = []; for (var img in resp.data['preset']) { images.add(BackgroundImage.fromJson(img)); } return images; }, ); } Future translate( String text, { String from = 'auto', }) async { return sendPostRequest( '/v1/translate/', (resp) => TranslateText.fromJson(resp.data), formData: Map.from({ 'text': text, 'from': from, }), ); } /// 上传初始化 Future uploadInit( String name, int filesize, { String? usage, }) async { return sendPostRequest( '/v1/storage/upload-init', (resp) => UploadInitResponse.fromJson(resp.data), formData: Map.from({ 'filesize': filesize, 'name': name, 'usage': usage, }), ); } /// 获取模型支持的提示语示例 Future> exampleByTag(String tag) async { return sendCachedGetRequest( '/v1/examples/tags/$tag', (resp) { var examples = []; for (var example in resp.data) { examples.add(ChatExample( example['title'], content: example['content'], models: ((example['models'] ?? []) as List).map((e) => e.toString()).toList(), )); } return examples; }, subKey: _cacheSubKey(), ); } /// 获取模型支持的反向提示语示例 Future> negativePromptExamples(String tag) async { return sendCachedGetRequest( '/v1/examples/negative-prompts/$tag', (resp) { var examples = []; for (var example in resp.data['data']) { examples.add(ChatExample( example['title'], content: example['content'], )); } return examples; }, subKey: _cacheSubKey(), ); } /// 获取模型支持的提示语示例 Future> example(String model) async { return sendCachedGetRequest( '/v1/examples/${Uri.encodeComponent(model)}', (resp) { var examples = []; for (var example in resp.data) { examples.add(ChatExample( example['title'], content: example['content'], models: ((example['models'] ?? []) as List).map((e) => e.toString()).toList(), )); } return examples; }, subKey: _cacheSubKey(), ); } /// 模型风格列表 Future> modelStyles(String category) async { return sendCachedGetRequest( '/v1/models/$category/styles', (resp) { var items = []; for (var item in resp.data) { items.add(ModelStyle.fromJson(item)); } return items; }, subKey: _cacheSubKey(), ); } /// 创意岛项目列表 Future creativeIslandItems({ required String mode, bool cache = true, }) async { return sendCachedGetRequest( '/v1/creative-island/items', (resp) { var items = []; for (var item in resp.data['items']) { items.add(CreativeIslandItem.fromJson(item)); } final categories = (resp.data['categories'] as List).map((e) => e.toString()).toList(); return CreativeIslandItems( items, categories, backgroundImage: resp.data['background_image'], ); }, queryParameters: {"mode": mode}, duration: const Duration(minutes: 60), forceRefresh: !cache, ); } /// 创意岛项目 Future creativeIslandItem(String id) async { return sendCachedGetRequest( '/v1/creative-island/items/$id', (resp) => CreativeIslandItem.fromJson(resp.data), subKey: _cacheSubKey(), duration: const Duration(minutes: 60), ); } /// 创作岛生成消耗量预估 Future creativeIslandCompletionsEvaluate(String id, Map params) async { return sendPostRequest( '/v1/creative-island/completions/$id/evaluate', (resp) => QuotaEvaluated.fromJson(resp.data), formData: params, ); } /// 创意岛项目生成数据 Future> creativeIslandCompletions(String id, Map params) async { return sendPostRequest( '/v1/creative-island/completions/$id', (resp) { final cicResp = CreativeIslandCompletionResp.fromJson(resp.data); switch (cicResp.type) { case creativeIslandCompletionTypeURLImage: return cicResp.resources; default: return [cicResp.content]; } }, formData: params, ); } /// 创意岛项目生成数据 Future creativeIslandCompletionsAsync(String id, Map params) async { params["mode"] = 'async'; return sendPostRequest( '/v1/creative-island/completions/$id', (resp) { final cicResp = CreativeIslandCompletionAsyncResp.fromJson(resp.data); return cicResp.taskId; }, formData: params, ); } Future creativeIslandCompletionsEvaluateV2(Map params) async { return sendPostRequest( '/v2/creative-island/completions/evaluate', (resp) => QuotaEvaluated.fromJson(resp.data), formData: params, ); } Future creativeIslandCompletionsAsyncV2(Map params) async { return sendPostRequest( '/v2/creative-island/completions', (resp) { final cicResp = CreativeIslandCompletionAsyncResp.fromJson(resp.data); return cicResp.taskId; }, formData: params, ); } Future creativeIslandArtisticTextCompletionsAsyncV2(Map params) async { return sendPostRequest( '/v2/creative-island/completions/artistic-text', (resp) { final cicResp = CreativeIslandCompletionAsyncResp.fromJson(resp.data); return cicResp.taskId; }, formData: params, ); } Future creativeIslandImageToVideoCompletionsAsyncV2(Map params) async { return sendPostRequest( '/v2/creative-island/completions/image-to-video', (resp) { final cicResp = CreativeIslandCompletionAsyncResp.fromJson(resp.data); return cicResp.taskId; }, formData: params, ); } Future creativeIslandImageDirectEdit( String endpoint, Map params, ) async { return sendPostRequest( '/v2/creative-island/completions/$endpoint', (resp) { final cicResp = CreativeIslandCompletionAsyncResp.fromJson(resp.data); return cicResp.taskId; }, formData: params, ); } /// 模型风格列表 Future> modelStylesV2({String? modelId}) async { return sendCachedGetRequest( '/v2/models/styles', (resp) { var items = []; for (var item in resp.data) { items.add(ModelStyle.fromJson(item)); } return items; }, queryParameters: {'model_id': modelId}, ); } /// 创作岛能力 Future creativeIslandCapacity({required String mode, required String id}) async { return sendCachedGetRequest( '/v2/creative-island/capacity', (resp) { return CreativeIslandCapacity.fromJson(resp.data); }, queryParameters: {'mode': mode, 'id': id}, ); } /// 异步任务执行状态查询 Future asyncTaskStatus(String taskId) async { return sendGetRequest( '/v1/tasks/$taskId/status', (resp) => AsyncTaskResp.fromJson(resp.data), ); } /// 发送重置密码验证码 Future sendResetPasswordCodeForSignedUser() async { return sendPostRequest( '/v1/users/reset-password/sms-code', (resp) => resp.data['id'], ); } /// 用户重置密码 Future resetPasswordByCodeSignedUser({ required String password, required String verifyCodeId, required String verifyCode, }) async { return sendPostRequest( '/v1/users/reset-password', (resp) => resp.data['id'], formData: Map.from({ 'password': password, 'verify_code_id': verifyCodeId, 'verify_code': verifyCode, }), ); } /// 使用邮箱验证码重置密码 Future resetPasswordByCode({ required String username, required String password, required String verifyCodeId, required String verifyCode, }) async { return sendPostRequest( '/v1/auth/reset-password', (resp) => resp.data['id'], formData: Map.from({ 'username': username, 'password': password, 'verify_code_id': verifyCodeId, 'verify_code': verifyCode, }), ); } /// 发送找回密码验证码 Future sendResetPasswordCode( String username, { required String verifyType, }) async { return sendPostRequest( '/v1/auth/reset-password/$verifyType-code', (resp) => resp.data['id'], formData: Map.from({ 'username': username, }), ); } /// 发送注册或者登录短信验证码 Future sendSigninOrSignupVerifyCode( String username, { required String verifyType, required bool isSignup, }) { if (isSignup) { return sendSignupVerifyCode(username, verifyType: verifyType); } return sendSigninVerifyCode(username, verifyType: verifyType); } /// 发送登录验证码 Future sendSigninVerifyCode( String username, { required String verifyType, }) async { return sendPostRequest( '/v1/auth/sign-in/$verifyType-code', (resp) => resp.data['id'], formData: Map.from({ 'username': username, }), ); } /// 发送注册验证码 Future sendSignupVerifyCode( String username, { required String verifyType, }) async { return sendPostRequest( '/v1/auth/sign-up/$verifyType-code', (resp) => resp.data['id'], formData: Map.from({ 'username': username, }), ); } /// 发送绑定手机号码验证码 Future sendBindPhoneCode(String username) async { return sendPostRequest( '/v1/auth/bind-phone/sms-code', (resp) => resp.data['id'], formData: Map.from({ 'username': username, }), ); } /// 绑定手机号 Future bindPhone({ required String username, required String verifyCodeId, required String verifyCode, String? inviteCode, }) async { return sendPostRequest( '/v1/auth/bind-phone', (resp) => SignInResp.fromJson(resp.data), formData: Map.from({ 'username': username, 'verify_code_id': verifyCodeId, 'verify_code': verifyCode, 'invite_code': inviteCode, }), ); } /// 注册账号 Future signupWithPassword({ required String username, required String password, required String verifyCodeId, required String verifyCode, String? inviteCode, }) async { return sendPostRequest( '/v1/auth/sign-up', (resp) => SignInResp.fromJson(resp.data), formData: Map.from({ 'username': username, 'password': password, 'verify_code_id': verifyCodeId, 'verify_code': verifyCode, 'invite_code': inviteCode, }), ); } /// 发送账号销毁手机验证码 Future sendDestroyAccountSMSCode() async { return sendPostRequest( '/v1/users/destroy/sms-code', (resp) => resp.data['id'], ); } /// 账号销毁 Future destroyAccount({ required String verifyCodeId, required String verifyCode, }) async { return sendDeleteRequest( '/v1/users/destroy', (resp) {}, formData: Map.from({ 'verify_code_id': verifyCodeId, 'verify_code': verifyCode, }), ); } /// 版本检查 Future versionCheck({bool cache = true}) async { return sendCachedGetRequest( '/public/info/version-check', (resp) => VersionCheckResp.fromJson(resp.data), queryParameters: Map.from({ 'version': clientVersion, 'os': PlatformTool.operatingSystem(), 'os_version': PlatformTool.operatingSystemVersion(), }), duration: const Duration(minutes: 180), forceRefresh: !cache, ); } /// 支付项目列表 Future paymentProducts() async { return sendGetRequest( '/v1/payment/products', (resp) => PaymentProducts.fromJson(resp.data), ); } /// 发起 Apple Pay Future createApplePay(String productId) async { return sendPostRequest( '/v1/payment/apple', (resp) => resp.data['id'], formData: Map.from({ 'product_id': productId, }), ); } /// 发起支付 Future createOtherPay(String productId, {required String source}) async { return sendPostRequest( '/v1/payment/others', (resp) => OtherPayCreatedReponse.fromJson(resp.data), formData: Map.from({ 'product_id': productId, 'source': source, }), ); } /// 其它支付客户端确认 Future otherPayClientConfirm(Map params) async { return sendPostRequest( '/v1/payment/others/client-confirm', (resp) => resp.data['status'], formData: params, ); } /// 查询支付状态 Future queryPaymentStatus(String paymentId) async { return sendGetRequest( '/v1/payment/status/$paymentId', (resp) => PaymentStatus.fromJson(resp.data), ); } /// 更新 Apple Pay 支付信息 Future updateApplePay( String paymentId, { required String productId, required String? localVerifyData, required String? serverVerifyData, required String? verifyDataSource, }) async { return sendPutRequest( '/v1/payment/apple/$paymentId', (resp) => resp.data['status'], formData: Map.from({ 'product_id': productId, 'local_verify_data': localVerifyData, 'server_verify_data': serverVerifyData, 'verify_data_source': verifyDataSource, }), ); } /// 验证 Apple Pay 支付结果 Future verifyApplePay( String paymentId, { required String productId, required String? purchaseId, required String? transactionDate, required String? localVerifyData, required String? serverVerifyData, required String? verifyDataSource, required String status, }) async { return sendPostRequest( '/v1/payment/apple/$paymentId/verify', (resp) => resp.data['status'], formData: Map.from({ 'product_id': productId, 'purchase_id': purchaseId, 'transaction_date': transactionDate, 'local_verify_data': localVerifyData, 'server_verify_data': serverVerifyData, 'verify_data_source': verifyDataSource, 'status': status, }), ); } /// 取消 Apple Pay Future cancelApplePay(String paymentId, {String? reason}) async { return sendDeleteRequest( '/v1/payment/apple/$paymentId', (resp) => resp.data['status'], formData: Map.from({ 'reason': reason, }), ); } /// 获取房间列表 Future rooms({bool cache = true}) async { return sendCachedGetRequest( '/v2/rooms', (resp) { return RoomsResponse.fromJson(resp.data); }, subKey: _cacheSubKey(), forceRefresh: !cache, ); } /// 获取最近使用过的角色 Future> recentRooms() async { return sendGetRequest( '/v2/rooms/recent', (resp) { var res = []; for (var item in resp.data['data']) { res.add(RoomInServer.fromJson(item)); } return res; }, ); } /// 获取单个房间信息 Future room({required roomId, bool cache = true}) async { return sendCachedGetRequest( '/v1/rooms/$roomId', (resp) => RoomInServer.fromJson(resp.data), subKey: _cacheSubKey(), forceRefresh: !cache, duration: const Duration(minutes: 120), ); } /// 创建群聊房间 Future createGroupRoom({ required String name, String? description, String? avatarUrl, List? members, }) async { return sendPostJSONRequest( '/v1/group-chat', (resp) => resp.data["group_id"], data: { 'name': name, 'avatar_url': avatarUrl, 'members': members?.map((e) => e.toJson()).toList(), }, finallyCallback: () { HttpClient.cleanCache(); }, ); } /// 更新群聊房间 Future updateGroupRoom({ required int groupId, required String name, String? description, String? avatarUrl, List? members, }) async { return sendPutJSONRequest( '/v1/group-chat/$groupId', (resp) {}, data: { 'name': name, 'avatar_url': avatarUrl, 'members': members?.map((e) => e.toJson()).toList(), }, finallyCallback: () { HttpClient.cleanCache(); }, ); } /// 创建房间 Future createRoom({ required String name, String? model, String? vendor, String? description, String? systemPrompt, String? avatarUrl, int? avatarId, int? maxContext, String? initMessage, }) async { return sendPostRequest( '/v1/rooms', (resp) => resp.data["id"], formData: Map.from({ 'name': name, 'model': model, 'vendor': vendor, 'description': description, 'system_prompt': systemPrompt, 'avatar_url': avatarUrl, 'avatar_id': avatarId, 'max_context': maxContext, 'init_message': initMessage, }), finallyCallback: () { HttpClient.cleanCache(); }, ); } /// 更新房间信息 Future updateRoom({ required int roomId, required String name, String? model, String? vendor, String? description, String? systemPrompt, String? avatarUrl, int? avatarId, int? maxContext, String? initMessage, }) async { return sendPutRequest( '/v1/rooms/$roomId', (resp) => RoomInServer.fromJson(resp.data), formData: Map.from({ 'name': name, 'model': model, 'vendor': vendor, 'description': description, 'system_prompt': systemPrompt, 'avatar_url': avatarUrl, 'avatar_id': avatarId, 'max_context': maxContext, 'init_message': initMessage, }), finallyCallback: () { HttpClient.cleanCache(); }, ); } /// 删除房间 Future deleteRoom({required int roomId}) async { return sendDeleteRequest( '/v1/rooms/$roomId', (resp) {}, finallyCallback: () { HttpClient.cleanCache(); }, ); } /// 创作岛 Gallery Future> creativeUserGallery({ required String mode, String? model, bool cache = true, }) async { return sendCachedGetRequest( '/v1/creative-island/gallery', (resp) { var res = []; for (var item in resp.data['data']) { res.add(CreativeItemInServer.fromJson(item)); } return res; }, queryParameters: {"mode": mode, "model": model}, forceRefresh: !cache, duration: const Duration(minutes: 30), ); } /// 图片模型列表 Future> imageModels() async { return sendCachedGetRequest( '/v2/creative-island/models', (resp) { var res = []; for (var item in resp.data['data']) { res.add(ImageModel.fromJson(item)); } return res; }, subKey: _cacheSubKey(), ); } /// 图片模型滤镜列表(风格) Future> imageModelFilters() async { return sendCachedGetRequest( '/v2/creative-island/filters', (resp) { var res = []; for (var item in resp.data['data']) { res.add(ImageModelFilter.fromJson(item)); } return res; }, subKey: _cacheSubKey(), ); } /// 创作岛历史记录(全量) Future> creativeHistories({ String? mode, bool cache = true, int? page, int? perPage, }) async { return sendGetRequest( '/v2/creative-island/histories', (resp) { var filters = {}; for (var filter in resp.data['filters']) { filters[filter['id']] = filter['name']; } var res = []; for (var item in resp.data['data']) { final ret = CreativeItemInServer.fromJson(item); if (ret.params['filter_id'] != null && filters.isNotEmpty) { ret.filterName = filters[ret.params['filter_id']]; } res.add(ret); } return PagedData( data: res, page: resp.data['page'] ?? 1, perPage: resp.data['per_page'] ?? 20, total: resp.data['total'], lastPage: resp.data['last_page'], ); }, queryParameters: { "mode": mode, "page": page, "per_page": perPage, }, ); } /// 分享创作岛历史记录到 Gallery Future shareCreativeHistoryToGallery({required int historyId}) { return sendPostRequest( '/v2/creative-island/histories/$historyId/share', (resp) {}, ); } /// 取消分享创作岛历史记录到 Gallery Future cancelShareCreativeHistoryToGallery({required int historyId}) { return sendDeleteRequest( '/v2/creative-island/histories/$historyId/share', (resp) {}, ); } /// 封禁创作岛历史记录 Future forbidCreativeHistoryItem({required int historyId}) { return sendPutRequest( '/v1/admin/creative-island/histories/$historyId/forbid', (resp) {}, ); } /// 创作岛历史记录 Future> creativeItemHistories(String islandId, {bool cache = true}) async { return sendCachedGetRequest( '/v1/creative-island/items/$islandId/histories', (resp) { var res = []; for (var item in resp.data['data']) { res.add(CreativeItemInServer.fromJson(item)); } return res; }, subKey: _cacheSubKey(), forceRefresh: !cache, duration: const Duration(minutes: 30), ); } /// 获取创作岛项目历史详情 Future creativeHistoryItem({ required hisId, bool cache = true, }) async { return sendCachedGetRequest( '/v2/creative-island/histories/$hisId', (resp) => CreativeItemInServer.fromJson(resp.data), subKey: _cacheSubKey(), forceRefresh: !cache, duration: const Duration(minutes: 1), ); } /// 删除创作岛项目历史记录 Future deleteCreativeHistoryItem(String islandId, {required hisId}) async { return sendDeleteRequest( '/v1/creative-island/items/$islandId/histories/$hisId', (resp) {}, ); } /// 获取用户智慧果消耗历史记录 Future> quotaUsedStatistics({bool cache = true}) async { return sendCachedGetRequest( '/v1/users/quota/usage-stat', (resp) { var res = []; for (var item in resp.data['usages']) { res.add(QuotaUsageInDay.fromJson(item)); } return res; }, subKey: _cacheSubKey(), forceRefresh: !cache, duration: const Duration(minutes: 30), ); } /// 获取用户智慧果消耗历史记录详情 Future> quotaUsedDetails({required String date}) async { return sendGetRequest( '/v1/users/quota/usage-stat/$date', (resp) { var res = []; for (var item in resp.data['data']) { res.add(QuotaUsageDetailInDay.fromJson(item)); } return res; }, ); } Future> creativeGallery({ bool cache = true, int page = 1, int perPage = 20, }) async { return sendCachedGetRequest( '/v1/creatives/gallery', (resp) { var res = []; for (var item in resp.data['data']) { res.add(CreativeGallery.fromJson(item)); } return PagedData( page: resp.data['page'] ?? 1, perPage: resp.data['per_page'] ?? 20, total: resp.data['total'], lastPage: resp.data['last_page'], data: res, ); }, queryParameters: Map.of({ 'page': page, 'per_page': perPage, }), forceRefresh: !cache, duration: const Duration(minutes: 60), ); } Future creativeGalleryItem({ required int id, bool cache = true, }) async { return sendCachedGetRequest( '/v1/creatives/gallery/$id', (resp) => CreativeGalleryItemResponse.fromJson(resp.data), forceRefresh: !cache, duration: const Duration(minutes: 30), ); } /// 文本转语音 Future> textToVoice({required String text}) async { return sendPostRequest( '/v1/voice/text2voice', formData: {'text': text}, (resp) => (resp.data['results'] as List).map((e) => e.toString()).toList(), ); } /// 故障日志上报 Future diagnosisUpload({required String data}) async { // data 从尾部开始截取 5000 个字符 if (data.length > 5000) { data = data.substring(data.length - 5000); } return sendPostRequest( '/v1/diagnosis/upload', formData: {'data': data}, (resp) {}, ); } /// 获取分享信息 Future shareInfo() async { return sendCachedGetRequest( '/public/share/info', (resp) => ShareInfo.fromJson(resp.data), duration: const Duration(minutes: 30), subKey: _cacheSubKey(), ); } Future roomGalleries({bool cache = true}) async { return sendCachedGetRequest( '/v1/room-galleries', (resp) { return RoomGalleryResponse.fromJson(resp.data); }, subKey: _cacheSubKey(), forceRefresh: !cache, ); } Future roomGalleryItem({required int id, bool cache = true}) async { return sendCachedGetRequest( '/v1/room-galleries/$id', (resp) => RoomGallery.fromJson(resp.data), subKey: _cacheSubKey(), forceRefresh: !cache, ); } Future> copyRoomGallery({required List ids}) async { return sendPostRequest( '/v1/room-galleries/copy', formData: {'ids': ids.join(',')}, (resp) { var ids = []; for (var item in resp.data['ids']) { ids.add(item); } return ids; }, ); } Future> creativeIslandItemsV2({bool cache = true}) async { return sendCachedGetRequest( '/v2/creative/items', (resp) { var items = []; for (var item in resp.data['data']) { items.add(CreativeIslandItemV2.fromJson(item)); } return items; }, subKey: _cacheSubKey(), forceRefresh: !cache, ); } /// 绘图提示语 Tags Future> drawPromptTags({bool cache = true}) async { return sendCachedGetRequest( '/v1/examples/draw/prompt-tags', (resp) { var items = []; for (var item in resp.data['data']) { items.add(PromptCategory.fromJson(item)); } return items; }, subKey: _cacheSubKey(), forceRefresh: !cache, ); } /// 更新用户头像 Future updateUserAvatar({required String avatarURL}) async { return sendPostRequest( '/v1/users/current/avatar', (resp) {}, formData: {'avatar_url': avatarURL}, finallyCallback: () { HttpClient.cleanCache(); }, ); } /// 更新用户昵称 Future updateUserRealname({required String realname}) async { return sendPostRequest( '/v1/users/current/realname', (resp) {}, formData: {'realname': realname}, finallyCallback: () { HttpClient.cleanCache(); }, ); } /// 服务器支持的能力 Future capabilities({bool cache = true}) async { return sendCachedGetRequest( '/public/info/capabilities', (resp) => Capabilities.fromJson(resp.data), forceRefresh: !cache, ); } /// 用户免费聊天次数统计 Future> userFreeStatistics() async { return sendGetRequest( '/v1/users/stat/free-chat-counts', (resp) { var items = []; for (var item in resp.data['data']) { items.add(FreeModelCount.fromJson(item)); } return items; }, ); } /// 免费聊天次数统计(登录不登录都可以访问) Future> freeChatCounts() async { return sendGetRequest( '/public/info/free-chat-counts', (resp) { var items = []; for (var item in resp.data['data']) { items.add(FreeModelCount.fromJson(item)); } return items; }, ); } /// 用户免费聊天次数统计(单个模型) Future userFreeStatisticsForModel({required String model}) async { return sendGetRequest( '/v1/users/stat/free-chat-counts/${Uri.encodeComponent(model)}', (resp) => FreeModelCount.fromJson(resp.data), ); } /// 通知信息(促销事件) Future>> notificationPromotionEvents({bool cache = true}) async { return sendCachedGetRequest( '/v1/notifications/promotions', (value) { var res = >{}; for (var item in value.data['data']) { if (res[item['id']] == null) { res[item['id']] = []; } res[item['id']] = [ ...res[item['id']]!, PromotionEvent.fromJson(item), ]; } return res; }, subKey: _cacheSubKey(), forceRefresh: !cache, ); } /// 更新自定义模型 Future updateCustomHomeModels({required List models}) async { return sendPostRequest( '/v1/users/custom/home-models', (value) => {}, formData: { 'models': models.join(','), }, ); } /// 自定义首页模型 v2 Future> customHomeModelsV2({bool cache = true}) async { return sendCachedGetRequest( '/v2/models/home-models/all', (value) { var res = []; for (var item in value.data['data']) { res.add(HomeModelV2.fromJson(item)); } return res; }, forceRefresh: !cache, ); } /// 自定义首页模型 v2 详情 Future customHomeModelsItemV2({ bool cache = true, required String uniqueKey, }) async { return sendCachedGetRequest( '/v2/models/home-models/${Uri.encodeComponent(uniqueKey)}', (value) { return HomeModelV2.fromJson(value.data['data']); }, forceRefresh: !cache, ); } /// 更新自定义模型 v2 /// 模型 ID 使用 type:id 形式 Future updateCustomHomeModelsV2({required List models}) async { return sendPostRequest( '/v2/users/custom/home-models', (value) => {}, formData: { 'models': models.join(','), }, ); } /// 群聊 //////////////////////////////////////////////////////////////////// /// 群组列表 Future> chatGroups({bool cache = true}) async { return sendCachedGetRequest( '/v1/group-chat', (value) { var res = []; for (var item in value.data['data']) { res.add(RoomInServer.fromJson(item)); } return res; }, forceRefresh: !cache, ); } /// 群组详情 Future chatGroup(int groupId, {bool cache = true}) async { return sendCachedGetRequest( '/v1/group-chat/$groupId', (value) => ChatGroup.fromJson(value.data), forceRefresh: !cache, ); } /// 群组聊天消息列表 Future> chatGroupMessages( int groupId, { int startId = 0, int? perPage, bool cache = true, }) async { return sendCachedGetRequest( '/v1/group-chat/$groupId/messages', (resp) { var res = []; for (var item in resp.data['data']) { res.add(GroupMessage.fromJson(item)); } return OffsetPageData( data: res, lastId: resp.data['last_id'], startId: resp.data['start_id'], perPage: resp.data['per_page'], ); }, queryParameters: { 'start_id': startId, 'per_page': perPage, }, forceRefresh: !cache, ); } /// 发起群聊消息 Future chatGroupSendMessage(int groupId, GroupChatSendRequest req) async { return sendPostJSONRequest( '/v1/group-chat/$groupId/chat', (resp) { return GroupChatSendResponse.fromJson(resp.data); }, data: req.toJson(), ); } /// 群聊发送系统消息 Future chatGroupSendSystemMessage( int groupId, { required String messageType, String? message, }) async { return sendPostRequest( '/v1/group-chat/$groupId/chat-system', (resp) => GroupMessage.fromJson(resp['data']), formData: { 'message_type': messageType, 'message': message, }, ); } /// 群组聊天消息状态 Future> chatGroupMessageStatus(int groupId, List messageIds) async { return sendGetRequest( '/v1/group-chat/$groupId/chat-messages', (resp) { var res = []; for (var item in resp.data['data']) { res.add(GroupMessage.fromJson(item)); } return res; }, queryParameters: { "message_ids": messageIds.join(','), }, ); } /// 清空群组聊天消息 Future chatGroupDeleteAllMessages(int groupId) async { return sendDeleteRequest('/v1/group-chat/$groupId/all-chat', (resp) {}); } /// 删除群组聊天消息 Future chatGroupDeleteMessage(int groupId, int messageId) async { return sendDeleteRequest('/v1/group-chat/$groupId/chat/$messageId', (resp) {}); } /// API 模式 //////////////////////////////////////////////////////////////////// /// 查询用户所有的 API Keys Future> userAPIKeys() async { return sendGetRequest('/v1/api-keys', (data) { return ((data.data['data'] ?? []) as List).map((e) => UserAPIKey.fromJson(e)).toList(); }); } /// 查询指定 API Key Future userAPIKeyDetail({required int id}) async { return sendGetRequest('/v1/api-keys/$id', (data) { return UserAPIKey.fromJson(data.data['data']); }); } /// 创建 API Key Future createAPIKey({required String name}) async { return sendPostRequest( '/v1/api-keys', (data) => data.data['key'], formData: {'name': name}, ); } /// 删除 API Key Future deleteAPIKey({required int id}) async { return sendDeleteRequest('/v1/api-keys/$id', (data) {}); } /// 消息通知 //////////////////////////////////////////////////////////////////// /// 消息通知列表 Future> notifications({ int startId = 0, int? perPage = 20, bool cache = true, }) async { return sendCachedGetRequest( '/v1/notifications', (resp) { var res = []; for (var item in resp.data['data']) { res.add(NotifyMessage.fromJson(item)); } return OffsetPageData( data: res, lastId: resp.data['last_id'], startId: resp.data['start_id'], perPage: resp.data['per_page'], ); }, queryParameters: { 'start_id': startId, 'per_page': perPage, }, forceRefresh: !cache, ); } /// 文章 //////////////////////////////////////////////////////////////////// /// 文章详情 Future
article({ required int id, bool cache = true, }) async { return sendCachedGetRequest( '/v1/articles/$id', (resp) { return Article.fromJson(resp.data['data']); }, forceRefresh: !cache, ); } /// 发起 Stripe 支付 Future createStripePaymentSheet({ required String productId, String? source, }) async { return sendPostRequest( '/v1/payment/stripe/payment-sheet', (resp) { return StripePaymentCreatedResponse.fromJson(resp.data); }, formData: { 'product_id': productId, 'source': source, }, ); } /// 发起微信支付 Future createWechatPayment({ required String productId, String? source, }) async { return sendPostRequest( '/v1/payment/wechatpay/', (resp) { return WechatPaymentCreatedResponse.fromJson(resp.data); }, formData: { 'product_id': productId, 'source': source, }, ); } /// 管理员接口:渠道类型 Future> adminChannelTypes() async { return sendCachedGetRequest('/v1/admin/channel-types', (resp) { var res = []; for (var item in resp.data['data']) { res.add(AdminChannelType.fromJson(item)); } return res; }); } /// 管理员接口:返回聚合后的渠道列表 Future> adminChannelsAgg() async { final channels = await sendGetRequest('/v1/admin/channels', (resp) { var res = []; for (var item in resp.data['data']) { res.add(AdminChannel.fromJson(item)); } return res; }); final channelTypes = await adminChannelTypes(); channels.addAll(channelTypes.map((e) => AdminChannel(name: e.text, type: e.name))); return channels; } /// 管理员接口:返回所有渠道 Future> adminChannels() async { return sendGetRequest('/v1/admin/channels', (resp) { var res = []; for (var item in resp.data['data']) { res.add(AdminChannel.fromJson(item)); } return res; }); } /// 管理员接口:返回指定渠道 Future adminChannel({required int id}) async { return sendGetRequest('/v1/admin/channels/$id', (resp) { return AdminChannel.fromJson(resp.data['data']); }); } /// 管理员接口:创建渠道 Future adminCreateChannel(AdminChannelAddReq req) async { return sendPostJSONRequest( '/v1/admin/channels', (resp) {}, data: req.toJson(), ); } /// 管理员接口:更新渠道 Future adminUpdateChannel({required int id, required AdminChannelUpdateReq req}) { return sendPutJSONRequest( '/v1/admin/channels/$id', (resp) {}, data: req.toJson(), ); } /// 管理员接口:删除渠道 Future adminDeleteChannel({required int id}) { return sendDeleteRequest('/v1/admin/channels/$id', (resp) {}); } /// 管理员接口:返回所有模型 Future> adminModels() async { return sendGetRequest( '/v1/admin/models', (resp) { var res = []; for (var item in resp.data['data']) { res.add(AdminModel.fromJson(item)); } return res; }, queryParameters: { 'sort': 'id:desc', }, ); } /// 管理员接口:返回指定模型 Future adminModel({required String modelId}) async { return sendGetRequest('/v1/admin/models/${Uri.encodeComponent(modelId)}', (resp) { return AdminModel.fromJson(resp.data['data']); }); } /// 管理员接口:创建模型 Future adminCreateModel(AdminModelAddReq req) async { return sendPostJSONRequest( '/v1/admin/models', (resp) {}, data: req.toJson(), ); } /// 管理员接口:更新模型 Future adminUpdateModel({required String modelId, required AdminModelUpdateReq req}) { return sendPutJSONRequest( '/v1/admin/models/${Uri.encodeComponent(modelId)}', (resp) {}, data: req.toJson(), ); } /// 管理员接口:删除模型 Future adminDeleteModel({required String modelId}) { return sendDeleteRequest('/v1/admin/models/${Uri.encodeComponent(modelId)}', (resp) {}); } /// 管理员接口:查询用户列表 Future> adminUsers({ int page = 1, int perPage = 20, String? keyword, }) async { return sendGetRequest( '/v1/admin/users', (resp) { var res = []; for (var item in resp.data['data']) { res.add(AdminUser.fromJson(item)); } return PagedData( data: res, page: resp.data['page'] ?? 1, perPage: resp.data['per_page'] ?? 20, total: resp.data['total'], lastPage: resp.data['last_page'], ); }, queryParameters: { 'page': page, 'per_page': perPage, 'keyword': keyword, }, ); } /// 管理员接口:查询用户详情 Future adminUser({required int id}) async { return sendGetRequest('/v1/admin/users/$id', (resp) { return AdminUser.fromJson(resp.data['data']); }); } /// 管理员接口:为用户分配智慧果 Future adminUserQuotaAssign({ required int userId, required int quota, int? validPeriod, String? note, }) { return sendPostJSONRequest( '/v1/admin/quotas/assign', (resp) {}, data: { 'user_id': userId, 'quota': quota, 'valid_period': validPeriod, 'note': note, }, ); } /// 管理员接口:查询用户当前额度 Future adminUserQuota({required int userId}) async { return sendGetRequest('/v1/admin/quotas/users/$userId', (resp) { return QuotaResp.fromJson(resp.data); }); } /// 管理员接口:重新加载配置缓存 Future adminSettingsReload() async { return sendPostRequest('/v1/admin/settings/reload', (resp) {}); } /// 管理员接口:重新加载配置缓存 Future adminSettingReload(String key) async { return sendPostRequest('/v1/admin/settings/key/$key/reload', (resp) {}); } /// 管理员接口:查询所有支付订单 Future> adminPaymentHistories({ int page = 1, int perPage = 20, String? keyword, }) async { return sendGetRequest( '/v1/admin/payments/histories', (resp) { var res = []; for (var item in resp.data['data']) { res.add(AdminPaymentHistory.fromJson(item)); } return PagedData( data: res, page: resp.data['page'] ?? 1, perPage: resp.data['per_page'] ?? 20, total: resp.data['total'], lastPage: resp.data['last_page'], ); }, queryParameters: { 'page': page, 'per_page': perPage, 'keyword': keyword, }, ); } /// 管理员接口:查询用户所有的数字人列表 Future> adminUserRooms({required int userId}) async { return sendGetRequest('/v1/admin/messages/$userId/rooms', (resp) { var res = []; for (var item in resp.data['data']) { res.add(RoomInServer.fromJson(item)); } return res; }); } /// 管理员接口:查询用户指定的数字人 Future adminUserRoom({required int userId, required int roomId}) async { return sendGetRequest('/v1/admin/messages/$userId/rooms/$roomId', (resp) { return RoomInServer.fromJson(resp.data['data']); }); } /// 管理员接口:查询用户指定数字人最近聊天历史记录 Future> adminUserRoomMessages({required int userId, required int roomId}) async { return sendGetRequest('/v1/admin/messages/$userId/rooms/$roomId/messages', (resp) { var res = []; for (var item in resp.data['data']) { res.add(MessageInServer.fromJson(item)); } return res; }); } /// 管理员接口:查询用户指定群聊最近聊天历史记录 Future> adminUserRoomGroupMessages({ required int userId, required int roomId, }) async { return sendGetRequest( '/v1/admin/messages/$userId/rooms/$roomId/group-messages', (resp) { var res = []; for (var item in resp.data['data']) { res.add(GroupMessage.fromJson(item)); } return res; }, ); } /// 管理员接口:查询全局最近聊天历史记录 Future> adminRecentlyMessages({ int page = 1, int perPage = 20, String? keyword, }) async { return sendGetRequest( '/v1/admin/recent-messages', (resp) { var res = []; for (var item in resp.data['data']) { res.add(MessageInServer.fromJson(item)); } return PagedData( data: res, page: resp.data['page'] ?? 1, perPage: resp.data['per_page'] ?? 20, total: resp.data['total'], lastPage: resp.data['last_page'], ); }, queryParameters: { 'page': page, 'per_page': perPage, 'keyword': keyword, }, ); } } ================================================ FILE: lib/repo/cache_repo.dart ================================================ import 'dart:async'; import 'package:askaide/repo/data/cache_data.dart'; class CacheRepository { final CacheDataProvider cacheProvider; var lastGC = DateTime.now(); CacheRepository(this.cacheProvider); Future set( String key, String value, Duration ttl, { String? group, }) async { return cacheProvider.set(key, value, ttl, group: group); } Future get(String key) async { if (DateTime.now().difference(lastGC) > const Duration(minutes: 10)) { cacheProvider.gc().whenComplete(() { lastGC = DateTime.now(); }); } return cacheProvider.get(key); } Future> getAllInGroup(String group) async { return cacheProvider.getAllInGroup(group); } Future remove(String key) async { return cacheProvider.remove(key); } Future clearAll() async { return cacheProvider.clearAll(); } } ================================================ FILE: lib/repo/chat_message_repo.dart ================================================ import 'dart:async'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/repo/data/chat_history.dart'; import 'package:askaide/repo/data/room_data.dart'; import 'package:askaide/repo/model/chat_history.dart'; import 'package:askaide/repo/model/message.dart'; import 'package:askaide/repo/data/chat_message_data.dart'; import 'package:askaide/repo/model/room.dart'; class ChatMessageRepository { final ChatMessageDataProvider _chatMsgDataProvider; final RoomDataProvider _chatRoomDataProvider; final ChatHistoryProvider _chatHistoryProvider; ChatMessageRepository( this._chatRoomDataProvider, this._chatMsgDataProvider, this._chatHistoryProvider, ); /// 获取所有 room Future> rooms({int? userId}) async { return await _chatRoomDataProvider.chatRooms(userId: userId); } /// 创建 room Future createRoom({ required String name, required category, String? description, String? model, String? color, String? systemPrompt, int? userId, int? maxContext, }) async { return await _chatRoomDataProvider.createRoom( name: name, category: category, description: description, model: model, color: color, systemPrompt: systemPrompt, userId: userId, maxContext: maxContext, ); } /// 删除 room Future deleteRoom(int roomId) async { await _chatRoomDataProvider.deleteRoom(roomId); await _chatMsgDataProvider.clearMessages(roomId); } /// 返回 room 中最近的消息 Future> getRecentMessages({ int? roomId, int? userId, int? chatHistoryId, }) async { if (chatHistoryId != null && chatHistoryId > 0) { roomId = null; } return (await _chatMsgDataProvider.getRecentMessages( chatMessagePerPage, roomId: roomId, userId: userId, chatHistoryId: chatHistoryId, )) .reversed .toList(); } /// 发送消息到 room Future sendMessage(int roomId, Message message) async { return await _chatMsgDataProvider.sendMessage(roomId, message); } /// 修复所有消息的状态(pending -> failed) Future fixMessageStatus(int roomId) async { return await _chatMsgDataProvider.fixMessageStatus(roomId); } /// 更新消息 Future updateMessage(int roomId, int id, Message message) async { return await _chatMsgDataProvider.updateMessage(roomId, id, message); } /// 部分更新消息 Future updateMessagePart( int roomId, int id, List parts, ) async { return await _chatMsgDataProvider.updateMessagePart(roomId, id, parts); } /// 删除消息 Future removeMessage(int roomId, List ids) async { return await _chatMsgDataProvider.removeMessage(roomId, ids); } /// 清空 room 中的消息 Future clearMessages(int roomId, {int? userId}) async { await _chatMsgDataProvider.clearMessages(roomId, userId: userId); } /// 获取 room 中最后一条消息 Future getLastMessage(int roomId, {int? userId, int? chatHistoryId}) async { return await _chatMsgDataProvider.getLastMessage(roomId, userId: userId, chatHistoryId: chatHistoryId); } /// 获取 room Future room(int roomId) async { final room = await _chatRoomDataProvider.room(roomId); if (room != null) { room.localRoom = true; } return room; } /// 更新 room Future updateRoom(Room room) async { return await _chatRoomDataProvider.updateRoom(room); } /// 更新 room 最后活跃时间 Future updateRoomLastActiveTime(int roomId) async { return await _chatRoomDataProvider.updateRoomLastActiveTime(roomId); } Future createChatHistory({ required String title, int? userId, int? roomId, String? lastMessage, String? model, }) { return _chatHistoryProvider.create( title: title, userId: userId, roomId: roomId, model: model, lastMessage: lastMessage, ); } Future> recentChatHistories( int count, { String? keyword, int? userId, int? offset, }) async { return await _chatHistoryProvider.getChatHistories( count, keyword: keyword, userId: userId, offset: offset, ); } Future getChatHistory(int chatId) async { return await _chatHistoryProvider.history(chatId); } Future deleteChatHistory(int chatId) async { return await _chatHistoryProvider.delete(chatId); } Future updateChatHistory(int chatId, ChatHistory chatHistory) async { chatHistory.id = chatId; return await _chatHistoryProvider.update(chatHistory); } } ================================================ FILE: lib/repo/creative_island_repo.dart ================================================ import 'package:askaide/repo/data/creative_island_data.dart'; import 'package:askaide/repo/model/creative_island_history.dart'; class CreativeIslandRepository { final CreativeIslandDataProvider _dataProvider; CreativeIslandRepository(this._dataProvider); Future> getRecentHistories( String itemId, int count, {int? userId}) async { return await _dataProvider.getRecentHistories( itemId, count, userId: userId, ); } Future create( String itemId, { String? arguments, String? prompt, String? answer, String? taskId, String? status, int? userId, }) async { return await _dataProvider.create( itemId, arguments: arguments, prompt: prompt, answer: answer, taskId: taskId, status: status, userId: userId, ); } /// 更新 Future update(int id, CreativeIslandHistory his) async { await _dataProvider.update(id, his); } Future delete(int hisId) async { return await _dataProvider.delete(hisId); } Future history(int id) async { return await _dataProvider.history(id); } } ================================================ FILE: lib/repo/data/cache_data.dart ================================================ import 'package:sqflite/sqflite.dart'; class CacheDataProvider { Database conn; CacheDataProvider(this.conn); /// 设置缓存 Future set( String key, String value, Duration ttl, { String? group, }) async { await conn.delete('cache', where: 'key = ?', whereArgs: [key]); await conn.insert('cache', { 'key': key, 'value': value, 'group': group, 'created_at': DateTime.now().millisecondsSinceEpoch, 'valid_before': DateTime.now().add(ttl).millisecondsSinceEpoch, }); } Future> getAllInGroup(String group) async { List> cacheValue = await conn.query( 'cache', where: '`group` = ? AND valid_before >= ?', whereArgs: [group, DateTime.now().millisecondsSinceEpoch], ); if (cacheValue.isEmpty) { return {}; } Map ret = {}; for (var item in cacheValue) { ret[item['key'] as String] = item['value'] as String; } return ret; } // 查询缓存值 Future get(String key) async { List> cacheValue = await conn.query( 'cache', where: 'key = ? AND valid_before >= ?', whereArgs: [key, DateTime.now().millisecondsSinceEpoch], limit: 1, ); if (cacheValue.isEmpty) { return null; } return cacheValue.first['value'] as String; } /// 删除缓存 Future remove(String key) async { await conn.delete('cache', where: 'key = ?', whereArgs: [key]); } /// 清理过期 keys Future gc() async { await conn.delete( 'cache', where: 'valid_before < ?', whereArgs: [DateTime.now().millisecondsSinceEpoch], ); } /// 清空所有缓存 Future clearAll() async { await conn.delete('cache'); } } ================================================ FILE: lib/repo/data/chat_history.dart ================================================ import 'package:askaide/repo/model/chat_history.dart'; import 'package:sqflite/sqlite_api.dart'; class ChatHistoryProvider { Database conn; ChatHistoryProvider(this.conn); Future> getChatHistories( int count, { int? userId, int? offset, String? keyword, }) async { keyword ??= ''; final userConditon = userId == null ? ' AND user_id IS NULL' : ' AND user_id = $userId'; var historyIds = []; if (keyword != '') { final histories = await conn.query( 'chat_message', where: 'chat_history_id IS NOT NULL AND text LIKE ? $userConditon', whereArgs: ['%$keyword%'], columns: ['chat_history_id'], distinct: true, ); historyIds = histories.map((h) => h['chat_history_id']).toList(); if (historyIds.isEmpty) { return []; } } var keywordCondition = keyword != '' ? 'AND id in (${historyIds.join(',')})' : ''; List> histories = await conn.query( 'chat_history', where: 'room_id IS NOT NULL $userConditon $keywordCondition', orderBy: 'updated_at DESC', limit: count, offset: offset, ); return histories.map((e) => ChatHistory.fromMap(e)).toList(); } Future create({ required String title, int? userId, int? roomId, String? lastMessage, String? model, }) async { final history = ChatHistory( title: title, userId: userId, roomId: roomId, lastMessage: lastMessage, model: model, createdAt: DateTime.now(), updatedAt: DateTime.now(), ); history.id = await conn.insert('chat_history', history.toMap()); return history; } Future update(ChatHistory his) async { if (his.id == null) { throw Exception('history id is null'); } his.updatedAt = DateTime.now(); return conn.update( 'chat_history', his.toMap(), where: 'id = ?', whereArgs: [his.id], ); } Future delete(int id) async { return conn.delete('chat_history', where: 'id = ?', whereArgs: [id]); } Future history(int id) async { List> histories = await conn.query('chat_history', where: 'id = ?', whereArgs: [id], limit: 1); if (histories.isEmpty) { return null; } return ChatHistory.fromMap(histories.first); } } ================================================ FILE: lib/repo/data/chat_message_data.dart ================================================ import 'package:askaide/repo/model/message.dart'; import 'package:sqflite/sqlite_api.dart'; class MessagePart { final String key; final dynamic value; MessagePart(this.key, this.value); } class ChatMessageDataProvider { Database conn; ChatMessageDataProvider(this.conn); Future> getRecentMessages(int count, {int? userId, int? chatHistoryId, int? roomId}) async { var userConditon = userId == null ? ' AND user_id IS NULL' : ' AND user_id = $userId'; if (chatHistoryId != null) { userConditon += ' AND chat_history_id = $chatHistoryId'; } List> messages = await conn.query( 'chat_message', where: '${roomId == null ? 'room_id IS NOT NULL' : 'room_id = ?'} $userConditon', whereArgs: roomId == null ? [] : [roomId], orderBy: 'id DESC', limit: count, ); return messages.map((e) => Message.fromMap(e)).toList(); } Future getLastMessage( int roomId, { int? userId, int? chatHistoryId, }) async { var userConditon = userId == null ? ' AND user_id IS NULL' : ' AND user_id = $userId'; if (chatHistoryId != null) { userConditon += ' AND chat_history_id = $chatHistoryId'; } List> messages = await conn.query( 'chat_message', where: 'room_id = ? $userConditon', whereArgs: [roomId], orderBy: 'id DESC', limit: 1, ); if (messages.isEmpty) { return null; } return Message.fromMap(messages.first); } Future sendMessage(int roomId, Message message) async { if (roomId > 0) { message.roomId = roomId; } return conn.insert('chat_message', message.toMap()); } Future fixMessageStatus(int roomId) async { return conn.transaction((txn) async { await txn.update( 'chat_message', {'status': 2}, where: 'room_id = ? AND status = 0', whereArgs: [roomId], ); }); } Future updateMessage(int roomId, int id, Message message) async { return conn.transaction((txn) async { await txn.update( 'chat_message', message.toMap(), where: 'id = ? AND room_id = ?', whereArgs: [id, roomId], ); }); } Future updateMessagePart( int roomId, int id, List parts, ) async { var kvs = {}; for (var element in parts) { kvs[element.key] = element.value; } return conn.transaction((txn) async { await txn.update( 'chat_message', kvs, where: 'id = ? AND room_id = ?', whereArgs: [id, roomId], ); }); } Future removeMessage(int roomId, List ids) async { var placeholders = List.generate(ids.length, (index) => '?').join(','); ids.add(roomId); return conn.transaction((txn) async { await txn.delete( 'chat_message', where: 'id in ($placeholders) AND room_id = ?', whereArgs: ids, ); }); } Future clearMessages(int roomId, {int? userId}) async { final userConditon = userId == null ? ' AND user_id IS NULL' : ' AND user_id = $userId'; return conn.transaction((txn) async { await txn.delete( 'chat_message', where: 'room_id = ? $userConditon', whereArgs: [roomId], ); }); } } ================================================ FILE: lib/repo/data/creative_island_data.dart ================================================ import 'package:askaide/repo/model/creative_island_history.dart'; import 'package:sqflite/sqlite_api.dart'; class CreativeIslandDataProvider { Database conn; CreativeIslandDataProvider(this.conn); Future> getRecentHistories( String itemId, int count, {int? userId}) async { final userConditon = userId == null ? ' AND user_id IS NULL' : ' AND user_id = $userId'; List> histories = await conn.query( 'creative_island_history', where: 'item_id = ? $userConditon', whereArgs: [itemId], orderBy: 'id DESC', limit: count, ); return histories.map((e) => CreativeIslandHistory.fromJson(e)).toList(); } Future create( String itemId, { String? arguments, String? prompt, String? answer, String? taskId, String? status, int? userId, }) async { final his = CreativeIslandHistory( itemId, arguments: arguments, prompt: prompt, answer: answer, taskId: taskId, status: status, userId: userId, ); his.id = await conn.insert('creative_island_history', his.toJson()); return his; } /// 更新 Future update(int id, CreativeIslandHistory his) async { await conn.update('creative_island_history', his.toJson(), where: 'id = ?', whereArgs: [id]); } /// 删除 room Future delete(int hisId) async { return conn .delete('creative_island_history', where: 'id = ?', whereArgs: [hisId]); } /// 获取指定历史信息 Future history(int id) async { List> histories = await conn.query( 'creative_island_history', where: 'id = ?', whereArgs: [id], limit: 1); if (histories.isEmpty) { return null; } return CreativeIslandHistory.fromJson(histories.first); } } ================================================ FILE: lib/repo/data/room_data.dart ================================================ import 'package:askaide/helper/constant.dart'; import 'package:askaide/repo/model/room.dart'; import 'package:sqflite/sqlite_api.dart'; class RoomDataProvider { Database conn; RoomDataProvider(this.conn); /// 获取所有 room Future> chatRooms({int? userId}) async { final userConditon = userId == null ? 'user_id IS NULL' : 'user_id = $userId'; List> rooms = await conn.query( 'chat_room', where: userConditon, orderBy: 'priority DESC, last_active_time DESC', ); return rooms.map((e) => Room.fromMap(e)).toList(); } /// 创建 room Future createRoom({ required String name, required String category, String? description, String? model, String? color, String? systemPrompt, int? userId, int? maxContext, }) async { final room = Room( name, category, userId: userId, color: color, model: model ?? defaultChatModel, description: description, systemPrompt: systemPrompt, maxContext: maxContext ?? 10, createdAt: DateTime.now(), lastActiveTime: DateTime.now(), iconData: '57683,MaterialIcons', ); room.id = await conn.insert('chat_room', room.toJson()); return room; } /// 删除 room Future deleteRoom(int roomId) async { return conn.delete('chat_room', where: 'id = ?', whereArgs: [roomId]); } /// 获取指定 room Future room(int roomId) async { List> rooms = await conn.query('chat_room', where: 'id = ?', whereArgs: [roomId], limit: 1); if (rooms.isEmpty) { return null; } return Room.fromMap(rooms.first); } /// 更新 room Future updateRoom(Room room) async { if (room.id == null) { throw Exception('room id is null'); } return conn.update( 'chat_room', room.toJson(), where: 'id = ?', whereArgs: [room.id], ); } /// 更新 Room 最后活跃时间 Future updateRoomLastActiveTime(int roomId) async { await conn.update( 'chat_room', { 'last_active_time': DateTime.now().millisecondsSinceEpoch, }, where: 'id = ?', whereArgs: [roomId], ); } } ================================================ FILE: lib/repo/data/settings_data.dart ================================================ import 'package:sqflite/sqflite.dart'; class SettingDataProvider { Database conn; SettingDataProvider(this.conn); final _settings = {}; final _listeners = []; Future loadSettings() async { List> kvs = await conn.query('settings'); _settings.clear(); for (var kv in kvs) { _settings[kv['key'] as String] = kv['value'] as String; } } void listen( Function(SettingDataProvider settings, String key, String value) listener) { _listeners.add(listener); } Future set(String key, String value) async { _settings[key] = value; final kvs = await conn.query('settings', where: 'key = ?', whereArgs: [key]); if (kvs.isEmpty) { await conn.insert('settings', {'key': key, 'value': value}); } else { await conn.update('settings', {'value': value}, where: 'key = ?', whereArgs: [key]); } for (var f in _listeners) { f(this, key, value); } } String? get(String key) { return _settings[key]; } String getDefault(String key, String defaultValue) { return _settings[key] ?? defaultValue; } int getDefaultInt(String key, int defaultValue) { return int.tryParse(_settings[key] ?? '') ?? defaultValue; } bool getDefaultBool(String key, bool defaultValue) { return _settings[key] == 'true' ? true : defaultValue; } double getDefaultDouble(String key, double defaultValue) { return double.tryParse(_settings[key] ?? '') ?? defaultValue; } } ================================================ FILE: lib/repo/deepai_repo.dart ================================================ import 'dart:convert'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/env.dart'; import 'package:askaide/helper/platform.dart'; import 'package:askaide/repo/data/settings_data.dart'; import 'package:http/http.dart' as http; class DeepAIRepository { late String serverURL; late String apiKey; late bool selfHosted; Map _headers = {}; late String language; final SettingDataProvider settings; DeepAIRepository(this.settings) { selfHosted = settings.getDefaultBool(settingDeepAISelfHosted, false); language = settings.getDefault(settingLanguage, 'zh'); _reloadServerConfig(); settings.listen((settings, key, value) { selfHosted = settings.getDefaultBool(settingDeepAISelfHosted, false); language = settings.getDefault(settingLanguage, 'zh'); _reloadServerConfig(); }); } void _reloadServerConfig() { if (selfHosted) { serverURL = settings.getDefault(settingDeepAIURL, defaultDeepAIServerURL); apiKey = settings.getDefault(settingDeepAIAPIToken, ''); _headers = {}; } else { apiKey = settings.getDefault(settingAPIServerToken, ''); serverURL = apiServerURL; _headers = { 'X-CLIENT-VERSION': clientVersion, 'X-PLATFORM': PlatformTool.operatingSystem(), 'X-PLATFORM-VERSION': PlatformTool.operatingSystemVersion(), 'X-LANGUAGE': language, }; } } // static List supportModels() { // return [ // Model( // 'text2img', // 'deepai', // category: modelTypeDeepAI, // description: '根据文本描述创建图像', // ), // Model( // 'cute-creature-generator', // 'deepai', // category: modelTypeDeepAI, // description: '生成可爱的动物图像', // ), // Model( // 'fantasy-world-generator', // 'deepai', // category: modelTypeDeepAI, // description: '生成奇幻世界图像', // ), // Model( // 'cyberpunk-generator', // 'deepai', // category: modelTypeDeepAI, // description: '生成未来科幻图像', // ), // Model( // 'anime-portrait-generator', // 'deepai', // category: modelTypeDeepAI, // description: '生成动漫人物图像', // ), // Model( // 'old-style-generator', // 'deepai', // category: modelTypeDeepAI, // description: '生成老式风格图像', // ), // Model( // 'renaissance-painting-generator', // 'deepai', // category: modelTypeDeepAI, // description: '生成文艺复兴风格图像', // ), // Model( // 'abstract-painting-generator', // 'deepai', // category: modelTypeDeepAI, // description: '生成抽象风格图像', // ), // Model( // 'impressionism-painting-generator', // 'deepai', // category: modelTypeDeepAI, // description: '生成印象派风格图像', // ), // Model( // 'surreal-graphics-generator', // 'deepai', // category: modelTypeDeepAI, // description: '生成超现实风格图像', // ), // Model( // '3d-objects-generator', // 'deepai', // category: modelTypeDeepAI, // description: '生成3D物体图像', // ), // Model( // 'origami-3d-generator', // 'deepai', // category: modelTypeDeepAI, // description: '生成折纸风格图像', // ), // Model( // 'hologram-3d-generator', // 'deepai', // category: modelTypeDeepAI, // description: '生成全息图像', // ), // Model( // '3d-character-generator', // 'deepai', // category: modelTypeDeepAI, // description: '生成3D人物图像', // ), // Model( // 'watercolor-painting-generator', // 'deepai', // category: modelTypeDeepAI, // description: '生成水彩风格图像', // ), // Model( // 'pop-art-generator', // 'deepai', // category: modelTypeDeepAI, // description: '生成流行艺术风格图像', // ), // Model( // 'contemporary-architecture-generator', // 'deepai', // category: modelTypeDeepAI, // description: '生成现代建筑图像', // ), // Model( // 'future-architecture-generator', // 'deepai', // category: modelTypeDeepAI, // description: '生成未来建筑图像', // ), // Model( // 'watercolor-architecture-generator', // 'deepai', // category: modelTypeDeepAI, // description: '生成水彩建筑图像', // ), // Model( // 'fantasy-character-generator', // 'deepai', // category: modelTypeDeepAI, // description: '生成奇幻人物图像', // ), // Model( // 'steampunk-generator', // 'deepai', // category: modelTypeDeepAI, // description: '生成蒸汽朋克风格图像', // ), // Model( // 'logo-generator', // 'deepai', // category: modelTypeDeepAI, // description: '生成Logo图像', // ), // Model( // 'pixel-art-generator', // 'deepai', // category: modelTypeDeepAI, // description: '生成像素风格图像', // ), // Model( // 'street-art-generator', // 'deepai', // category: modelTypeDeepAI, // description: '生成街头艺术风格图像', // ), // Model( // 'surreal-portrait-generator', // 'deepai', // category: modelTypeDeepAI, // description: '生成超现实人物图像', // ), // Model( // 'anime-world-generator', // 'deepai', // category: modelTypeDeepAI, // description: '生成动漫世界图像', // ), // Model( // 'fantasy-portrait-generator', // 'deepai', // category: modelTypeDeepAI, // description: '生成奇幻人物图像', // ), // Model( // 'comics-portrait-generator', // 'deepai', // category: modelTypeDeepAI, // description: '生成漫画人物图像', // ), // Model( // 'cyberpunk-portrait-generator', // 'deepai', // category: modelTypeDeepAI, // description: '生成未来科幻人物图像', // ), // ]; // } Future painting( String model, String prompt, { int gridSize = 1, int width = 512, int height = 512, String? negativePrompt, }) async { var params = { "text": prompt, "grid_size": gridSize.toString(), "width": width.toString(), "height": height.toString(), }; if (negativePrompt != null) { params['negative_prompt'] = negativePrompt; } var url = selfHosted ? Uri.parse('$serverURL/api/$model') : Uri.parse('$serverURL/v1/deepai/images/$model/text-to-image'); var headers = {}; headers.addAll(_headers); if (selfHosted) { headers['api-key'] = apiKey; } else { headers['Authorization'] = 'Bearer $apiKey'; } var resp = await http.post( url, body: params, headers: headers, ); if (resp.statusCode != 200) { return Future.error((resp.body as Map)['error']); } var ret = jsonDecode(resp.body) as Map; return Future.value(DeepAIPaintResult(ret['id'], ret['output_url'])); } Future paintingAsync( String model, String prompt, { int gridSize = 1, int width = 512, int height = 512, String? negativePrompt, }) async { var params = { "text": prompt, "grid_size": gridSize.toString(), "width": width.toString(), "height": height.toString(), }; if (negativePrompt != null) { params['negative_prompt'] = negativePrompt; } var url = Uri.parse('$serverURL/v1/deepai/images/$model/text-to-image-async'); var headers = {}; headers.addAll(_headers); headers['Authorization'] = 'Bearer $apiKey'; var resp = await http.post( url, body: params, headers: headers, ); if (resp.statusCode != 200) { return Future.error((resp.body as Map)['error']); } return Future.value(jsonDecode(resp.body)['task_id']); } } class DeepAIPaintResult { final String id; final String url; DeepAIPaintResult(this.id, this.url); } ================================================ FILE: lib/repo/model/chat_history.dart ================================================ class ChatHistory { int? id; int? userId; int? roomId; String? title; String? lastMessage; String? model; DateTime? createdAt; DateTime? updatedAt; ChatHistory({ this.id, this.userId, this.roomId, this.title, this.model, this.lastMessage, this.createdAt, this.updatedAt, }); ChatHistory.fromMap(Map map) { id = map['id'] as int?; userId = map['user_id'] as int?; roomId = map['room_id'] as int?; title = map['title'] as String?; model = map['model'] as String?; lastMessage = map['last_message'] as String?; createdAt = DateTime.fromMillisecondsSinceEpoch(map['created_at'] as int); updatedAt = DateTime.fromMillisecondsSinceEpoch(map['updated_at'] as int); } Map toMap() { return { 'id': id, 'user_id': userId, 'room_id': roomId, 'title': title, 'model': model, 'last_message': lastMessage, 'created_at': createdAt?.millisecondsSinceEpoch, 'updated_at': updatedAt?.millisecondsSinceEpoch, }; } } ================================================ FILE: lib/repo/model/chat_message.dart ================================================ import 'dart:convert'; import 'package:dart_openai/openai.dart'; class ChatMessage extends OpenAIChatCompletionChoiceMessageModel { final List? images; final String? file; ChatMessage( {required super.role, required super.content, this.images, this.file}); @override Map toMap() { final Map res = { "role": role.name, "content": content, }; if (file != null || (images != null && images!.isNotEmpty)) { final multipartContent = []; if (file != null) { try { multipartContent.add({ 'type': 'file', 'file': jsonDecode(file!), }); } catch (ignore) { // ignore } } if (images != null && images!.isNotEmpty) { multipartContent.addAll(images ?.map((e) => { 'type': 'image_url', 'image_url': {'url': e} }) .toList() ?? []); } multipartContent.add({ 'type': 'text', 'text': content, }); res['multipart_content'] = multipartContent; } return res; } } ================================================ FILE: lib/repo/model/creative_island_history.dart ================================================ class CreativeIslandHistory { int? id; String itemId; int? userId; String? arguments; String? prompt; String? answer; String? taskId; String? status; DateTime createdAt; CreativeIslandHistory( this.itemId, { this.id, this.userId, this.arguments, this.prompt, this.answer, DateTime? createdAt, this.taskId, this.status, }) : createdAt = createdAt ?? DateTime.now(); Map toJson() { return { 'id': id, 'item_id': itemId, 'user_id': userId, 'arguments': arguments, 'prompt': prompt, 'answer': answer, 'task_id': taskId, 'status': status, 'created_at': createdAt.millisecondsSinceEpoch, }; } CreativeIslandHistory.fromJson(Map map) : id = map['id'] as int?, itemId = map['item_id'] as String, userId = map['user_id'] as int?, arguments = map['arguments'] as String?, prompt = map['prompt'] as String?, answer = map['answer'] as String?, taskId = map['task_id'] as String?, status = map['status'] as String?, createdAt = DateTime.fromMillisecondsSinceEpoch(map['created_at'] as int? ?? 0); } ================================================ FILE: lib/repo/model/group.dart ================================================ import 'package:askaide/repo/model/misc.dart'; const groupMessageStatusWaiting = 0; const groupMessageStatusSuccess = 1; const groupMessageStatusFailed = 2; class ChatGroup { final RoomInServer group; final List members; GroupMember? findMember(int memberId) { return members.where((member) => member.id == memberId).firstOrNull; } ChatGroup({ required this.group, required this.members, }); factory ChatGroup.fromJson(Map json) { return ChatGroup( group: RoomInServer.fromJson(json['group']), members: (json['members'] as List) .map((member) => GroupMember.fromJson(member)) .toList(), ); } Map toJson() { return { 'group': group.toJson(), 'members': members.map((member) => member.toJson()).toList(), }; } } class GroupMember { final int? id; final String modelId; final String modelName; final String? avatarUrl; final int? status; GroupMember({ this.id, required this.modelId, required this.modelName, this.avatarUrl, this.status, }); factory GroupMember.fromJson(Map json) { return GroupMember( id: json['id'], modelId: json['model_id'], modelName: json['model_name'], avatarUrl: json['avatar_url'], status: json['status'], ); } Map toJson() { return { 'id': id, 'model_id': modelId, 'model_name': modelName, 'avatar_url': avatarUrl, 'status': status, }; } } class GroupMessage { final int id; final String message; final String role; final String type; final int groupId; final int? tokenConsumed; final int? quotaConsumed; final int? pid; final int? memberId; final int status; DateTime? createdAt; DateTime? updatedAt; /// 管理接口专用 final String? model; GroupMessage({ required this.id, required this.message, required this.role, required this.status, required this.type, required this.groupId, this.tokenConsumed, this.quotaConsumed, this.pid, this.memberId, this.createdAt, this.updatedAt, this.model, }); factory GroupMessage.fromJson(Map json) { return GroupMessage( id: json['id'], message: json['message'] ?? '', role: json['role'] == 1 ? 'user' : 'assistant', type: json['type'] ?? 'text', groupId: json['group_id'], tokenConsumed: json['token_consumed'], quotaConsumed: json['quota_consumed'], pid: json['pid'], memberId: json['member_id'], status: json['status'] ?? 0, createdAt: DateTime.tryParse(json['CreatedAt']), updatedAt: DateTime.tryParse(json['UpdatedAt']), model: json['model'], ); } Map toJson() { return { 'id': id, 'message': message, 'role': role, 'type': type, 'group_id': groupId, 'token_consumed': tokenConsumed, 'quota_consumed': quotaConsumed, 'pid': pid, 'member_id': memberId, 'status': status, 'CreatedAt': createdAt?.toIso8601String(), 'UpdatedAt': updatedAt?.toIso8601String(), 'model': model, }; } } class GroupChatSendRequestMessage { final String role; final String content; final int? memberId; GroupChatSendRequestMessage({ required this.role, required this.content, this.memberId, }); Map toJson() { return { 'role': role, 'content': content, 'member_id': memberId, }; } factory GroupChatSendRequestMessage.fromJson(Map json) { return GroupChatSendRequestMessage( role: json['role'], content: json['content'], memberId: json['member_id'], ); } } class GroupChatSendRequest { final String message; final List memberIds; GroupChatSendRequest({ required this.message, required this.memberIds, }); Map toJson() { return { 'message': message, 'member_ids': memberIds, }; } factory GroupChatSendRequest.fromJson(Map json) { return GroupChatSendRequest( message: json['message'], memberIds: (json['member_ids'] as List).map((e) => e as int).toList(), ); } } class GroupChatSendResponseTask { final int memberId; final String taskId; final int answerId; GroupChatSendResponseTask({ required this.memberId, required this.taskId, required this.answerId, }); factory GroupChatSendResponseTask.fromJson(Map json) { return GroupChatSendResponseTask( memberId: json['member_id'], taskId: json['task_id'], answerId: json['answer_id'], ); } Map toJson() { return { 'member_id': memberId, 'task_id': taskId, 'answer_id': answerId, }; } } class GroupChatSendResponse { final int questionId; final List tasks; GroupChatSendResponse({ required this.questionId, required this.tasks, }); factory GroupChatSendResponse.fromJson(Map json) { return GroupChatSendResponse( questionId: json['question_id'], tasks: (json['tasks'] as List) .map((task) => GroupChatSendResponseTask.fromJson(task)) .toList(), ); } Map toJson() { return { 'question_id': questionId, 'tasks': tasks.map((task) => task.toJson()).toList(), }; } } ================================================ FILE: lib/repo/model/message.dart ================================================ import 'dart:convert'; import 'package:askaide/helper/helper.dart'; /// 聊天消息 class Message { /// 聊天所属的聊天室 ID int? roomId; /// 用户ID int? userId; /// 聊天历史 ID int? chatHistoryId; /// 消息ID int? id; /// 消息方向 Role role; /// 消息内容 String text; /// 消息附加信息,用于提供模型相关信息 String? extra; /// 消息发送时的模型 String? model; /// 消息类型 MessageType type; /// 发送者 String? user; /// 时间戳 DateTime? ts; /// 关联消息ID(问题 ID) int? refId; /// 服务端 ID int? serverId; /// 消息状态: 1-成功 0-等待应答 2-失败 int status; /// 消息消耗的配额 int? quotaConsumed; /// 消息消耗的 token int? tokenConsumed; /// 是否当前消息已就绪,不需要持久化 bool isReady = true; /// 消息发送者的头像,不需要持久化 String? avatarUrl; /// 消息发送者的名称,不需要持久化 String? senderName; /// 消息图片列表 List? images; // Uploaded file by user (json(name, url)) String? file; List? flags; Message( this.role, this.text, { required this.type, this.userId, this.chatHistoryId, this.id, this.user, this.ts, this.model, this.roomId, this.extra, this.refId, this.serverId, this.status = 1, this.quotaConsumed, this.tokenConsumed, this.avatarUrl, this.senderName, this.images, this.file, this.flags, }); /// 设置消息附加信息 void setExtra(dynamic data) { extra = jsonEncode(data); } /// 更新消息附加信息 void updateExtra(dynamic data) { // 需要将 data merge 到 extra 中 final extraData = decodeExtra(); if (extraData != null) { data = {...extraData, ...data}; } extra = jsonEncode(data); } /// 将值添加到附加信息的某个数组键中 void pushExtra(String key, dynamic value) { var extraData = decodeExtra(); extraData ??= {}; if (!extraData.containsKey(key)) { extraData[key] = []; } extraData[key]!.add(value); extra = jsonEncode(extraData); } /// 从附加信息的某个数组键中删除最后一个值 void popExtra(String key) { var extraData = decodeExtra(); extraData ??= {}; extraData[key]!.removeLast(); extra = jsonEncode(extraData); } /// 获取消息附加信息 decodeExtra() { if (extra == null) { return null; } return jsonDecode(extra!); } /// 是否是系统消息,包括时间线 bool isSystem() { return type == MessageType.system || type == MessageType.timeline || type == MessageType.contextBreak; } /// 是否是初始消息 bool isInitMessage() { return type == MessageType.initMessage; } /// 是否是时间线 bool isTimeline() { return type == MessageType.timeline; } /// 格式化时间 String friendlyTime() { return humanTime(ts); } /// 是否已失败 bool statusIsFailed() { return status == 2; } /// 是否已成功 bool statusIsSucceed() { return status == 1; } /// 是否等待应答 bool statusPending() { return status == 0; } String get markdownWithImages { var t = text; if (images != null && images!.isNotEmpty) { t = images!.map((e) => '![img]($e)\n\n').join('') + t; } return t; } Map toMap() { return { 'id': id, 'user_id': userId, 'chat_history_id': chatHistoryId, 'role': role.getRoleText(), 'text': text, 'type': type.getTypeText(), 'extra': extra, 'model': model, 'user': user, 'ts': ts?.millisecondsSinceEpoch, 'room_id': roomId, 'ref_id': refId, 'server_id': serverId, 'status': status, 'token_consumed': tokenConsumed, 'quota_consumed': quotaConsumed, 'images': images != null ? jsonEncode(images) : null, 'file': file, 'flags': flags != null ? jsonEncode(flags) : null, }; } Message.fromMap(Map map) : id = map['id'] as int, userId = map['user_id'] as int?, chatHistoryId = map['chat_history_id'] as int?, role = Role.getRoleFromText(map['role'] as String), text = map['text'] as String, extra = map['extra'] as String?, model = map['model'] as String?, type = MessageType.getTypeFromText(map['type'] as String), user = map['user'] as String?, refId = map['ref_id'] as int?, serverId = map['server_id'] as int?, status = (map['status'] ?? 1) as int, tokenConsumed = map['token_consumed'] as int?, quotaConsumed = map['quota_consumed'] as int?, ts = map['ts'] == null ? null : DateTime.fromMillisecondsSinceEpoch(map['ts'] as int), roomId = map['room_id'] as int?, images = map['images'] == null ? null : (jsonDecode(map['images'] as String) as List).cast(), file = map['file'] as String?, flags = map['flags'] == null ? null : (jsonDecode(map['flags'] as String) as List).cast(); } enum Role { receiver, sender; static Role getRoleFromText(String value) { switch (value) { case 'receiver': return Role.receiver; case 'assistant': return Role.receiver; case 'sender': return Role.sender; case 'user': return Role.sender; default: return Role.receiver; } } String getRoleText() { switch (this) { case Role.receiver: return 'receiver'; case Role.sender: return 'sender'; default: return 'receiver'; } } } enum MessageType { text, image, file, audio, video, location, command, system, timeline, contextBreak, hide, initMessage; String getTypeText() { switch (this) { case MessageType.text: return 'text'; case MessageType.image: return 'image'; case MessageType.file: return 'file'; case MessageType.audio: return 'audio'; case MessageType.video: return 'video'; case MessageType.location: return 'location'; case MessageType.command: return 'command'; case MessageType.system: return 'system'; case MessageType.timeline: return 'timeline'; case MessageType.contextBreak: return 'contextBreak'; case MessageType.hide: return 'hide'; case MessageType.initMessage: return 'initMessage'; default: return 'text'; } } static MessageType getTypeFromText(String value) { switch (value) { case 'text': return MessageType.text; case 'image': return MessageType.image; case 'file': return MessageType.file; case 'audio': return MessageType.audio; case 'video': return MessageType.video; case 'location': return MessageType.location; case 'command': return MessageType.command; case 'system': return MessageType.system; case 'timeline': return MessageType.timeline; case 'contextBreak': return MessageType.contextBreak; case 'hide': return MessageType.hide; case 'initMessage': return MessageType.initMessage; default: return MessageType.text; } } } ================================================ FILE: lib/repo/model/misc.dart ================================================ import 'package:askaide/repo/api/room_gallery.dart'; enum PromotionEventClickButtonType { none, url, inAppRoute; static PromotionEventClickButtonType fromName(String typeName) { switch (typeName) { case 'url': return PromotionEventClickButtonType.url; case 'in_app_route': return PromotionEventClickButtonType.inAppRoute; default: return PromotionEventClickButtonType.none; } } String toName() { switch (this) { case PromotionEventClickButtonType.url: return 'url'; case PromotionEventClickButtonType.inAppRoute: return 'in_app_route'; default: return 'none'; } } } class PromotionEvent { String? title; String content; PromotionEventClickButtonType clickButtonType; String? clickValue; String? clickButtonColor; String? backgroundImage; String? textColor; bool closeable; int? maxCloseDurationInDays; PromotionEvent({ this.title, required this.content, required this.clickButtonType, this.clickValue, this.clickButtonColor, this.backgroundImage, this.textColor, required this.closeable, this.maxCloseDurationInDays, }); toJson() => { 'title': title, 'content': content, 'click_button_type': clickButtonType.toName(), 'click_value': clickValue, 'click_button_color': clickButtonColor, 'background_image': backgroundImage, 'text_color': textColor, 'closeable': closeable, 'max_close_duration_in_days': maxCloseDurationInDays, }; static PromotionEvent fromJson(Map json) { return PromotionEvent( title: json['title'], content: json['content'], clickButtonType: PromotionEventClickButtonType.fromName(json['click_button_type'] ?? ''), clickValue: json['click_value'], clickButtonColor: json['click_button_color'], backgroundImage: json['background_image'], textColor: json['text_color'], closeable: json['closeable'] ?? false, maxCloseDurationInDays: json['max_close_duration_in_days'], ); } } class ShareInfo { String qrCode; String message; String? inviteCode; ShareInfo({ required this.qrCode, required this.message, this.inviteCode, }); toJson() => { 'qr_code': qrCode, 'message': message, 'invite_code': inviteCode, }; static ShareInfo fromJson(Map json) { return ShareInfo( qrCode: json['qr_code'], message: json['message'], inviteCode: json['invite_code'], ); } } class QuotaUsageInDay { String date; int used; QuotaUsageInDay({ required this.date, required this.used, }); toJson() => { 'date': date, 'used': used, }; static QuotaUsageInDay fromJson(Map json) { return QuotaUsageInDay( date: json['date'], used: json['used'], ); } } class QuotaUsageDetailInDay { int used; String type; String createdAt; QuotaUsageDetailInDay({ required this.used, required this.type, required this.createdAt, }); toJson() => { 'used': used, 'type': type, 'created_at': createdAt, }; static QuotaUsageDetailInDay fromJson(Map json) { return QuotaUsageDetailInDay( used: json['used'], type: json['type'], createdAt: json['created_at'], ); } } class RoomsResponse { List rooms; List? suggests; RoomsResponse({ required this.rooms, this.suggests, }); toJson() => { 'rooms': rooms, 'suggests': suggests, }; static RoomsResponse fromJson(Map json) { var rooms = []; for (var item in json['data'] ?? []) { rooms.add(RoomInServer.fromJson(item)); } var suggests = []; for (var item in json['suggests'] ?? []) { suggests.add(RoomGallery.fromJson(item)); } return RoomsResponse( rooms: rooms, suggests: suggests, ); } } class RoomInServer { int id; int userId; int avatarId; String? avatarUrl; String name; String? description; int? priority; String model; String vendor; String? systemPrompt; String? initMessage; int maxContext; int? maxTokens; int? roomType; DateTime? lastActiveTime; DateTime? createdAt; DateTime? updatedAt; List members; RoomInServer({ required this.id, required this.userId, required this.avatarId, required this.name, required this.maxContext, this.roomType, this.avatarUrl, this.description, this.priority, required this.model, required this.vendor, this.systemPrompt, this.initMessage, this.lastActiveTime, this.createdAt, this.updatedAt, this.maxTokens, this.members = const [], }); toJson() => { 'id': id, 'user_id': userId, 'avatar_id': avatarId, 'avatar_url': avatarUrl, 'name': name, 'description': description, 'priority': priority, 'model': model, 'vendor': vendor, 'init_message': initMessage, 'max_context': maxContext, 'room_type': roomType, 'max_tokens': maxTokens, 'system_prompt': systemPrompt, 'last_active_time': lastActiveTime?.toIso8601String(), 'created_at': createdAt?.toIso8601String(), 'updated_at': updatedAt?.toIso8601String(), 'members': members, }; static RoomInServer fromJson(Map json) { return RoomInServer( id: json['id'], userId: json['user_id'], avatarId: json['avatar_id'] ?? 0, avatarUrl: json['avatar_url'], name: json['name'], description: json['description'], priority: json['priority'], model: json['model'] ?? '', vendor: json['vendor'] ?? '', systemPrompt: json['system_prompt'], initMessage: json['init_message'], maxContext: json['max_context'] ?? 10, maxTokens: json['max_tokens'], roomType: json['room_type'], lastActiveTime: json['last_active_time'] != null ? DateTime.parse(json['last_active_time']) : null, createdAt: json['CreatedAt'] != null ? DateTime.parse(json['CreatedAt']) : null, updatedAt: json['UpdatedAt'] != null ? DateTime.parse(json['UpdatedAt']) : null, members: (json['members'] as List?)?.map((e) => e.toString()).toList() ?? [], ); } } class MessageInServer { final int id; final int userId; final int roomId; final String message; final int role; final int tokenConsumed; final int quotaConsumed; final int pid; final String model; final int status; final String error; final DateTime createdAt; final DateTime updatedAt; MessageInServer({ required this.id, required this.userId, required this.roomId, required this.message, required this.role, required this.tokenConsumed, required this.quotaConsumed, required this.pid, required this.model, required this.status, required this.error, required this.createdAt, required this.updatedAt, }); toJson() => { 'id': id, 'user_id': userId, 'room_id': roomId, 'message': message, 'role': role, 'token_consumed': tokenConsumed, 'quota_consumed': quotaConsumed, 'pid': pid, 'model': model, 'status': status, 'error': error, 'created_at': createdAt.toIso8601String(), 'updated_at': updatedAt.toIso8601String(), }; static MessageInServer fromJson(Map json) { return MessageInServer( id: json['id'] ?? 0, userId: json['user_id'] ?? 0, roomId: json['room_id'] ?? 0, message: json['message'] ?? '', role: json['role'] ?? 1, tokenConsumed: json['token_consumed'] ?? 0, quotaConsumed: json['quota_consumed'] ?? 0, pid: json['pid'] ?? 0, model: json['model'] ?? '', status: json['status'] ?? 1, error: json['error'] ?? '', createdAt: DateTime.parse(json['CreatedAt']), updatedAt: DateTime.parse(json['CreatedAt']), ); } } class VersionCheckResp { bool hasUpdate; String serverVersion; bool forceUpdate; String url; String message; VersionCheckResp({ required this.hasUpdate, required this.serverVersion, required this.forceUpdate, required this.url, required this.message, }); toJson() => { 'has_update': hasUpdate, 'server_version': serverVersion, 'force_update': forceUpdate, 'url': url, 'message': message, }; static VersionCheckResp fromJson(Map json) { return VersionCheckResp( hasUpdate: json['has_update'] ?? false, serverVersion: json['server_version'], forceUpdate: json['force_update'] ?? false, url: json['url'], message: json['message'], ); } } class TrySignInResp { String token; bool exist; TrySignInResp({ required this.token, required this.exist, }); toJson() => { 'token': token, 'exist': exist, }; static TrySignInResp fromJson(Map json) { return TrySignInResp( token: json['token'], exist: json['exist'], ); } } class SignInResp { int id; String name; String? email; String? phone; String token; bool isNewUser; int reward; bool needBindPhone; SignInResp({ required this.id, required this.name, this.email, required this.token, this.phone, this.isNewUser = false, this.reward = 0, this.needBindPhone = false, }); toJson() => { 'id': id, 'name': name, 'email': email, 'phone': phone, 'token': token, 'is_new_user': isNewUser, 'reward': reward, 'need_bind_phone': needBindPhone, }; static SignInResp fromJson(Map json) { return SignInResp( id: json['id'], name: json['name'], email: json['email'], phone: json['phone'], token: json['token'], isNewUser: json['is_new_user'] ?? false, reward: json['reward'] ?? 0, needBindPhone: json['need_bind_phone'] ?? false, ); } } class AsyncTaskResp { String status; List? errors; List? resources; String? originImage; int? width; int? height; AsyncTaskResp(this.status, {this.errors, this.resources, this.originImage, this.width, this.height}); toJson() => { 'status': status, 'errors': errors, 'resources': resources, 'origin_image': originImage, 'width': width, 'height': height, }; static AsyncTaskResp fromJson(Map json) { return AsyncTaskResp( json['status'], errors: json['errors'] != null ? (json['errors'] as List).map((e) => e.toString()).toList() : null, resources: json['resources'] != null ? (json['resources'] as List).map((e) => e.toString()).toList() : null, originImage: json['origin_image'], width: json['width'], height: json['height'], ); } } class Prompt { String title; String content; Prompt(this.title, this.content); toJson() { return { 'title': title, 'content': content, }; } fromJson(Map json) { title = json['title']; content = json['content']; } } class ChatExample { String title; String? content; List models; List tags; ChatExample( this.title, { this.content, this.models = const [], this.tags = const [], }); get text => content ?? title; toJson() => { 'title': title, 'content': content, 'models': models, 'tags': tags, }; fromJson(Map json) { title = json['title']; content = json['content']; models = json['models']; tags = json['tags']; } } class TranslateText { String? result; String? speakUrl; TranslateText(this.result, this.speakUrl); toJson() => { 'result': result, 'speak_url': speakUrl, }; static fromJson(Map json) { return TranslateText(json['result'], json['speak_url']); } } class UploadInitResponse { String bucket; String key; String token; String url; bool uploaded; UploadInitResponse( this.key, this.bucket, this.token, this.url, { this.uploaded = false, }); toJson() => { 'bucket': bucket, 'key': key, 'token': token, 'url': url, 'uploaded': uploaded, }; static fromJson(Map json) { return UploadInitResponse( json['key'], json['bucket'], json['token'], json['url'], uploaded: json['uploaded'] ?? false, ); } } class ModelStyle { String id; String name; String? preview; ModelStyle({required this.id, required this.name, this.preview}); toJson() => { 'id': id, 'name': name, 'preview': preview, }; static ModelStyle fromJson(Map json) { return ModelStyle( id: json['id'], name: json['name'], preview: json['preview'], ); } } class Model { String id; String name; String shortName; String? description; String? priceInfo; bool isChat; bool isImage; bool disabled; String? avatarUrl; bool supportVision; bool supportReasoning; bool supportSearch; String category; String? tag; String? tagTextColor; String? tagBgColor; bool isNew; bool isDefault; bool userNoPermission; String get realModelId { return id.split(':').last; } Model({ required this.id, required this.name, required this.shortName, required this.category, required this.isChat, required this.isImage, this.description, this.priceInfo, this.disabled = false, this.tag, this.avatarUrl, this.supportVision = false, this.supportReasoning = false, this.supportSearch = false, this.tagBgColor, this.tagTextColor, this.isNew = false, this.isDefault = false, this.userNoPermission = false, }); toJson() => { 'id': id, 'name': name, 'short_name': shortName, 'description': description, 'price_info': priceInfo, 'category': category, 'is_chat': isChat, 'is_image': isImage, 'disabled': disabled, 'tag': tag, 'avatar_url': avatarUrl, 'support_vision': supportVision, 'support_reasoning': supportReasoning, 'support_search': supportSearch, 'tag_bg_color': tagBgColor, 'tag_text_color': tagTextColor, 'is_new': isNew, 'is_default': isDefault, 'user_no_permission': userNoPermission, }; static Model fromJson(Map json) { return Model( id: json['id'], name: json['name'], shortName: json['short_name'] ?? json['name'], description: json['description'], priceInfo: json['price_info'], category: json['category'], isChat: json['is_chat'], isImage: json['is_image'], disabled: json['disabled'] ?? false, tag: json['tag'], avatarUrl: json['avatar_url'], supportVision: json['support_vision'] ?? false, supportReasoning: json['support_reasoning'] ?? false, supportSearch: json['support_search'] ?? false, tagBgColor: json['tag_bg_color'], tagTextColor: json['tag_text_color'], isNew: json['is_new'] ?? false, isDefault: json['is_default'] ?? false, userNoPermission: json['user_no_permission'] ?? false, ); } } class BackgroundImage { String url; String preview; BackgroundImage(this.url, this.preview); toJson() => { 'url': url, 'preview': preview, }; static BackgroundImage fromJson(Map json) { return BackgroundImage( json['url'], json['preview'], ); } } class UserExistenceResp { bool exist; String signInMethod; UserExistenceResp(this.exist, this.signInMethod); toJson() => { 'exist': exist, 'sign_in_method': signInMethod, }; static UserExistenceResp fromJson(Map json) { return UserExistenceResp( json['exist'], json['sign_in_method'], ); } } class PromptCategory { String name; List children; List tags; PromptCategory(this.name, this.children, this.tags); toJson() => { 'name': name, 'children': children, 'tags': tags, }; static PromptCategory fromJson(Map json) { var children = []; for (var item in json['children'] ?? []) { children.add(PromptCategory.fromJson(item)); } var tags = []; for (var item in json['tags'] ?? []) { tags.add(PromptTag.fromJson(item)); } return PromptCategory( json['name'], children, tags, ); } } class PromptTag { String name; String value; PromptTag(this.name, this.value); toJson() => { 'name': name, 'value': value, }; static PromptTag fromJson(Map json) { return PromptTag( json['name'], json['value'], ); } } class FreeModelCount { String model; String name; int leftCount; int maxCount; String? info; FreeModelCount({ required this.model, required this.name, required this.leftCount, required this.maxCount, this.info, }); toJson() => { 'model': model, 'name': name, 'left_count': leftCount, 'max_count': maxCount, 'info': info, }; static FreeModelCount fromJson(Map json) { return FreeModelCount( model: json['model'], name: json['name'] ?? json['model'], leftCount: json['left_count'] ?? 0, maxCount: json['max_count'] ?? 0, info: json['info'], ); } } ================================================ FILE: lib/repo/model/model.dart ================================================ import 'dart:convert'; class Model { final String id; final String name; final String? shortName; final String ownedBy; String? description; String? priceInfo; bool isChatModel = false; bool disabled; String? avatarUrl; bool supportVision = false; bool supportReasoning = false; bool supportSearch = false; String? tag; String? tagTextColor; String? tagBgColor; bool isNew = false; bool isRecommend = false; String category; bool isDefault; bool userNoPermission; Model( this.id, this.name, this.ownedBy, { this.shortName, required this.category, this.description, this.priceInfo, this.isChatModel = false, this.disabled = false, this.tag, this.avatarUrl, this.supportVision = false, this.supportReasoning = false, this.supportSearch = false, this.tagTextColor, this.tagBgColor, this.isNew = false, this.isRecommend = false, this.isDefault = false, this.userNoPermission = false, }); String uid() { return id; } Model copyWith({ String? id, String? name, String? shortName, String? ownedBy, String? description, String? priceInfo, bool? isChatModel, bool? disabled, String? avatarUrl, bool? supportVision, bool? supportReasoning, bool? supportSearch, String? tag, String? tagTextColor, String? tagBgColor, bool? isNew, bool? isRecommend, String? category, bool? isDefault, bool? userNoPermission, }) { return Model( id ?? this.id, name ?? this.name, ownedBy ?? this.ownedBy, shortName: shortName ?? this.shortName, description: description ?? this.description, priceInfo: priceInfo ?? this.priceInfo, isChatModel: isChatModel ?? this.isChatModel, disabled: disabled ?? this.disabled, avatarUrl: avatarUrl ?? this.avatarUrl, supportVision: supportVision ?? this.supportVision, supportReasoning: supportReasoning ?? this.supportReasoning, supportSearch: supportSearch ?? this.supportSearch, tag: tag ?? this.tag, tagTextColor: tagTextColor ?? this.tagTextColor, tagBgColor: tagBgColor ?? this.tagBgColor, isNew: isNew ?? this.isNew, isRecommend: isRecommend ?? this.isRecommend, category: category ?? this.category, isDefault: isDefault ?? false, userNoPermission: userNoPermission ?? this.userNoPermission, ); } ModelPrice get modelPrice { if (priceInfo == null || priceInfo == '') { return ModelPrice(input: 0, output: 0, request: 0, search: 0, note: ''); } return ModelPrice.fromMap(jsonDecode(priceInfo!) as Map); } } class ModelPrice { final int input; final int output; final int request; final int search; final String note; bool get isFree { return input == output && input == 0 && request == 0; } bool get hasNote { return note != ''; } ModelPrice({ required this.input, required this.output, required this.request, required this.note, this.search = 0, }); ModelPrice.fromMap(Map map) : input = map['input'] ?? 0, output = map['output'] ?? 0, request = map['request'] ?? 0, search = map['search'] ?? 0, note = map['note'] ?? ''; } ================================================ FILE: lib/repo/model/room.dart ================================================ import 'package:askaide/helper/constant.dart'; /// 聊天室 class Room { /// 聊天室 ID int? id; /// 用户 ID int? userId; /// 头像 ID int? avatarId; /// 头像链接 String? avatarUrl; /// 聊天室名称 String name; /// 聊天室类别 String category; /// 显示优先级(排序,值越大越靠前) int priority; /// 聊天室采用的模型 String model; /// 模型初始化消息 String? initMessage; /// 模型最大上下文数量 int maxContext; /// 模型最大返回 Token 数量 int? maxTokens; /// room 类型:local or remote bool? localRoom; /// 聊天室类型 int? roomType; bool get isLocalRoom => localRoom ?? false; /// 聊天室头像 标识 int get avatar => (avatarId == null || avatarId == 0) ? 0 : avatarId!; /// 模型类别 String modelCategory() { final segs = model.split(':'); if (segs.length == 1) { return 'openai'; } return segs[0]; } /// 模型名称 String modelName() { final segs = model.split(':'); if (segs.length == 1) { return segs[0]; } return segs[1]; } /// 聊天室图标 String? iconData; /// 聊天室图标颜色 String? color; /// 聊天室描述 String? description; /// 系统提示 String? systemPrompt; /// 聊天室创建时间 DateTime? createdAt; /// 聊天室最后活跃时间 DateTime? lastActiveTime; /// 聊天室成员头像列表 List members; Room( this.name, this.category, { this.description, this.id, this.userId, this.avatarId, this.avatarUrl, this.createdAt, this.lastActiveTime, this.iconData, this.systemPrompt, this.priority = 0, this.color, this.roomType, this.initMessage, this.localRoom, this.maxContext = 10, this.maxTokens, this.model = defaultChatModel, this.members = const [], }); Map toJson() { return { 'id': id, 'user_id': userId, 'name': name, 'category': category, 'model': model, 'priority': priority, 'icon_data': iconData, 'color': color, 'description': description, 'system_prompt': systemPrompt, 'init_message': initMessage, 'max_context': maxContext, 'created_at': createdAt?.millisecondsSinceEpoch, 'last_active_time': lastActiveTime?.millisecondsSinceEpoch, }; } Room.fromMap(Map map) : id = map['id'] as int, userId = map['user_id'] as int?, avatarId = map['avatar_id'] as int?, avatarUrl = map['avatar_url'] as String?, name = map['name'] as String, category = (map['category'] ?? '') as String, priority = (map['priority'] ?? 0) as int, model = (map['model'] ?? '') as String, iconData = map['icon_data'] as String?, color = map['color'] as String?, roomType = map['room_type'] as int?, systemPrompt = map['system_prompt'] as String?, description = map['description'] as String?, initMessage = map['init_message'] as String?, maxContext = map['max_context'] as int? ?? 10, maxTokens = map['max_tokens'] as int?, members = (map['members'] as List?) ?.map((e) => e as String) .toList() ?? [], createdAt = DateTime.fromMillisecondsSinceEpoch(map['created_at'] as int? ?? 0), lastActiveTime = DateTime.fromMillisecondsSinceEpoch( map['last_active_time'] as int? ?? 0); } ================================================ FILE: lib/repo/openai_repo.dart ================================================ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:askaide/helper/ability.dart'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/env.dart'; import 'package:askaide/helper/logger.dart'; import 'package:askaide/helper/platform.dart'; import 'package:askaide/helper/queue.dart'; import 'package:askaide/repo/model/chat_message.dart'; import 'package:askaide/repo/model/model.dart' as mm; import 'package:dart_openai/openai.dart'; import 'package:askaide/repo/data/settings_data.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:web_socket_channel/status.dart' as status; class OpenAIRepository { final SettingDataProvider settings; late bool selfHosted; late String language; OpenAIRepository(this.settings) { selfHosted = settings.getDefaultBool(settingOpenAISelfHosted, false); language = settings.getDefault(settingLanguage, 'zh'); _reloadServerConfig(); settings.listen((settings, key, value) { selfHosted = settings.getDefaultBool(settingOpenAISelfHosted, false); language = settings.getDefault(settingLanguage, 'zh'); _reloadServerConfig(); }); } void _reloadServerConfig() { // 自己的 OpenAI 服务器 if (selfHosted) { OpenAI.baseUrl = settings.getDefault(settingOpenAIURL, defaultOpenAIServerURL); OpenAI.organization = settings.get(settingOpenAIOrganization); OpenAI.apiKey = settings.getDefault(settingOpenAIAPIToken, ''); OpenAI.externalHeaders = {}; } else { // 使用公共服务器 OpenAI.apiKey = settings.getDefault(settingAPIServerToken, ''); OpenAI.baseUrl = settings.getDefault(settingServerURL, apiServerURL); OpenAI.organization = ""; OpenAI.externalHeaders = { 'X-CLIENT-VERSION': clientVersion, 'X-PLATFORM': PlatformTool.operatingSystem(), 'X-PLATFORM-VERSION': PlatformTool.operatingSystemVersion(), 'X-LANGUAGE': language, }; } OpenAI.showLogs = true; } /// 基于 prompt 生成图片 Future> createImage( String prompt, { int n = 1, OpenAIImageSize size = OpenAIImageSize.size1024, }) async { var model = await OpenAI.instance.image .create(prompt: prompt, n: n, size: size, responseFormat: OpenAIImageResponseFormat.url); return model.data.map((e) => e.url).toList(); } /// 判断模型是否支持聊天 static bool isChatModel(String model) { return supportForChat[model] != null && supportForChat[model]!.isChatModel; } /// 判断模型是否为图片模型 static bool isImageModel(String model) { return model == defaultImageModel; } // 兼容性列表查看:https://platform.openai.com/docs/models/gpt-3 // key: 模型名, value: 是否支持聊天模式 static final supportForChat = { 'gpt-3.5-turbo': mm.Model( 'gpt-3.5-turbo', 'GPT-3.5 Turbo', 'openai', category: modelTypeOpenAI, isChatModel: true, description: '速度快,成本低', shortName: 'GPT-3.5 Turbo', tag: 'local', avatarUrl: 'https://ssl.aicode.cc/ai-server/assets/avatar/gpt35.png', ), 'gpt-3.5-turbo-16k': mm.Model( 'gpt-3.5-turbo-16k', 'GPT-3.5 Turbo 16k', 'openai', category: modelTypeOpenAI, isChatModel: true, shortName: 'GPT-3.5 Turbo 16K', tag: 'local', avatarUrl: 'https://ssl.aicode.cc/ai-server/assets/avatar/gpt35.png', ), 'gpt-4': mm.Model( 'gpt-4', 'GPT-4', 'openai', category: modelTypeOpenAI, isChatModel: true, shortName: 'GPT-4', tag: 'local', avatarUrl: 'https://ssl.aicode.cc/ai-server/assets/avatar/gpt4.png', ), 'gpt-4-32k': mm.Model( 'gpt-4-32k', 'GPT-4 32k', 'openai', category: modelTypeOpenAI, isChatModel: true, shortName: 'GPT-4 32K', tag: 'local', avatarUrl: 'https://ssl.aicode.cc/ai-server/assets/avatar/gpt4.png', ), 'gpt-4o': mm.Model( 'gpt-4o', 'GPT-4o', 'openai', category: modelTypeOpenAI, isChatModel: true, shortName: 'GPT-4o', tag: 'local', avatarUrl: 'https://ssl.aicode.cc/ai-server/assets/avatar/gpt4.png', ), 'gpt-4o-mini': mm.Model( 'gpt-4o-mini', 'GPT-4o-mini', 'openai', category: modelTypeOpenAI, isChatModel: true, shortName: 'GPT-4o-mini', tag: 'local', avatarUrl: 'https://ssl.aicode.cc/ai-server/assets/avatar/gpt4.png', ), }; /// 支持的模型 static List supportModels() { var models = supportForChat.values.toList(); // models.add(Model( // defaultImageModel, // 'openai', // category: modelTypeOpenAI, // description: '根据自然语言创建现实的图像和艺术', // )); return models; } // /// @deprecated // Future> models() async { // var models = (await OpenAI.instance.model.list()) // .where((e) => e.ownedBy == 'openai') // .map((e) => mm.Model(e.id, e.ownedBy, category: modelTypeOpenAI)) // .toList(); // var supportModels = // models.where((e) => supportForChat.containsKey(e.id)).toList(); // supportModels.add(mm.Model( // defaultImageModel, // 'openai', // category: modelTypeOpenAI, // description: defaultModelNotChatDesc, // )); // return supportModels; // } Future completionStream( String model, String message, void Function(ChatStreamRespData data) onData, { double temperature = 1.0, user = 'user', int? maxTokens, }) async { var completer = Completer(); try { var stream = OpenAI.instance.completion.createStream( model: model, prompt: message, temperature: temperature, n: 1, maxTokens: maxTokens, user: user, ); stream.listen( (event) { for (var element in event.choices) { onData(ChatStreamRespData(content: element.text)); } }, onDone: () => completer.complete(), onError: (e) => completer.completeError(e), cancelOnError: true, ).onError((e) { completer.completeError(e); }); } catch (e) { completer.completeError(e); } return completer.future; } Future chatStream( List messages, void Function(ChatStreamRespData data) onData, { double temperature = 1.0, user = 'user', String model = defaultChatModel, int? roomId, int? historyId, int? maxTokens, String? tempModel, List? flags, }) async { var completer = Completer(); try { if (Ability().supportWebSocket) { var serverURL = settings.getDefault(settingServerURL, apiServerURL); if (PlatformTool.isWeb() && (serverURL == '' || serverURL == '/')) { serverURL = '${Uri.base.scheme}://${Uri.base.host}${Uri.base.hasPort ? ':${Uri.base.port}' : ''}'; } final wsURL = serverURL.startsWith('https://') ? serverURL.replaceFirst('https://', 'wss://') : serverURL.replaceFirst('http://', 'ws://'); final wsUriBase = Uri.parse('$wsURL/v1/chat/completions'); final apiToken = settings.getDefault(settingAPIServerToken, ''); final wsUri = Uri( scheme: wsUriBase.scheme, host: wsUriBase.host, port: wsUriBase.port, path: wsUriBase.path, queryParameters: { 'ws': 'true', 'authorization': apiToken, 'client-version': clientVersion, 'platform-version': PlatformTool.operatingSystemVersion(), 'platform': PlatformTool.operatingSystem(), 'language': language, }, ); Logger.instance.d('wsURL: ${wsUri.toString()}'); var channel = WebSocketChannel.connect(wsUri); await channel.ready; channel.stream.listen( (event) { final evt = jsonDecode(event); if (evt['code'] != null && evt['code'] > 0) { onData(ChatStreamRespData( content: evt['error'], code: evt['code'], error: evt['error'], )); return; } final res = OpenAIStreamChatCompletionModel.fromMap(evt); for (var element in res.choices) { if (element.delta.content != null) { try { onData(ChatStreamRespData( content: element.delta.content!, role: element.delta.role, )); } on QueueFinishedException { channel.sink.close(status.goingAway); } } } }, onDone: () { channel.sink.close(); completer.complete(); }, onError: (e) { channel.sink.close(); completer.completeError(e); }, cancelOnError: true, ).onError((e) { completer.completeError(e); }); final data = jsonEncode({ 'model': model, 'temp_model': tempModel, 'messages': messages.map((e) => e.toMap()).toList(), 'temperature': temperature, 'user': user, 'max_tokens': maxTokens, 'n': Ability().enableLocalOpenAI && (model.startsWith('openai:') || model.startsWith('gpt-')) ? null : roomId, // n 参数暂时用不到,复用作为 roomId 'history_id': historyId, 'flags': flags, }); Logger.instance.d('send chat request: $data'); channel.sink.add(data); } else { var chatStream = OpenAI.instance.chat.createStream( model: model, messages: messages, temperature: temperature, user: user, maxTokens: maxTokens, n: Ability().enableLocalOpenAI ? null : roomId, // n 参数暂时用不到,复用作为 roomId ); chatStream.listen( (event) { for (var element in event.choices) { if (element.delta.content != null) { onData(ChatStreamRespData( content: element.delta.content!, role: element.delta.role, )); } } }, onDone: () => completer.complete(), onError: (e) => completer.completeError(e), cancelOnError: true, ).onError((e) { completer.completeError(e); }); } } catch (e) { completer.completeError(e); } return completer.future; } /// 音频文件转文字 Future audioTranscription({ required File audioFile, }) async { var audioModel = await OpenAI.instance.audio.createTranscription( file: audioFile, model: 'whisper-1', ); return audioModel.text; } } class ChatReplyMessage { final int index; final String role; final String content; final String? finishReason; ChatReplyMessage({ required this.index, required this.role, required this.content, this.finishReason, }); } class ChatStreamRespData { final String? role; final String content; final int? code; final String? error; final String? reasoningContent; ChatStreamRespData({ this.role, required this.content, this.code, this.error, this.reasoningContent, }); } ================================================ FILE: lib/repo/settings_repo.dart ================================================ import 'package:askaide/repo/data/settings_data.dart'; class SettingRepository { final SettingDataProvider _dataProvider; SettingRepository(this._dataProvider) { _dataProvider.loadSettings(); } void listen( Function(SettingDataProvider settings, String key, String value) listener) { _dataProvider.listen(listener); } Future set(String key, String value) async { return await _dataProvider.set(key, value); } String? get(String key) { return _dataProvider.get(key); } String stringDefault(String key, String defaultValue) { return _dataProvider.getDefault(key, defaultValue); } int intDefault(String key, int defaultValue) { return _dataProvider.getDefaultInt(key, defaultValue); } bool boolDefault(String key, bool defaultValue) { return _dataProvider.getDefaultBool(key, defaultValue); } double doubleDefault(String key, double defaultValue) { return _dataProvider.getDefaultDouble(key, defaultValue); } } ================================================ FILE: lib/repo/stabilityai_repo.dart ================================================ import 'dart:convert'; import 'package:askaide/helper/constant.dart'; import 'package:askaide/helper/env.dart'; import 'package:askaide/helper/platform.dart'; import 'package:askaide/repo/data/settings_data.dart'; import 'package:http/http.dart' as http; /// StabilityAI 模型 class StabilityAIRepository { final SettingDataProvider settings; late String serverURL; late String apiKey; late String organization; late String language; late bool selfHosted; Map _headers = {}; StabilityAIRepository(this.settings) { selfHosted = settings.getDefaultBool(settingStabilityAISelfHosted, false); language = settings.getDefault(settingLanguage, 'zh'); _reloadServerConfig(); settings.listen((settings, key, value) { selfHosted = settings.getDefaultBool(settingStabilityAISelfHosted, false); language = settings.getDefault(settingLanguage, 'zh'); _reloadServerConfig(); }); } void _reloadServerConfig() { if (selfHosted) { serverURL = settings.getDefault(settingStabilityAIURL, defaultStabilityAIURL); organization = settings.getDefault(settingStabilityAIOrganization, ''); apiKey = settings.getDefault(settingStabilityAIAPIToken, ''); _headers = {}; } else { apiKey = settings.getDefault(settingAPIServerToken, ''); organization = ""; serverURL = apiServerURL; _headers = { 'X-CLIENT-VERSION': clientVersion, 'X-PLATFORM': PlatformTool.operatingSystem(), 'X-PLATFORM-VERSION': PlatformTool.operatingSystemVersion(), 'X-LANGUAGE': language, }; } } /// 创建请求头 Map _buildRequestHeaders() { var headers = { 'Authorization': 'Bearer $apiKey', }; headers.addAll(_headers); if (organization.isNotEmpty) { headers['Organization'] = organization; } return headers; } /// 默认的模型列表 // static List supportModels() { // return [ // // Model( // // 'esrgan-v1-x2plus', // // modelTypeStabilityAI, // // category: modelTypeStabilityAI, // // description: 'Real-ESRGAN_x2plus upscaler model', // // ), // Model( // 'stable-diffusion-v1', // modelTypeStabilityAI, // category: modelTypeStabilityAI, // description: 'Stability-AI Stable Diffusion v1.4', // ), // Model( // 'stable-diffusion-v1-5', // modelTypeStabilityAI, // category: modelTypeStabilityAI, // description: 'Stability-AI Stable Diffusion v1.5', // ), // Model( // 'stable-diffusion-512-v2-0', // modelTypeStabilityAI, // category: modelTypeStabilityAI, // description: 'Stability-AI Stable Diffusion v2.0', // ), // Model( // 'stable-diffusion-768-v2-0', // modelTypeStabilityAI, // category: modelTypeStabilityAI, // description: 'Stability-AI Stable Diffusion 768 v2.0', // ), // Model( // 'stable-diffusion-512-v2-1', // modelTypeStabilityAI, // category: modelTypeStabilityAI, // description: 'Stability-AI Stable Diffusion v2.1', // ), // Model( // 'stable-diffusion-768-v2-1', // modelTypeStabilityAI, // category: modelTypeStabilityAI, // description: 'Stability-AI Stable Diffusion 768 v2.1', // ), // Model( // 'stable-diffusion-xl-beta-v2-2-2', // modelTypeStabilityAI, // category: modelTypeStabilityAI, // description: 'Stability-AI Stable Diffusion XL Beta v2.2.2', // ), // // Model( // // 'stable-inpainting-v1-0', // // modelTypeStabilityAI, // // category: modelTypeStabilityAI, // // description: 'Stability-AI Stable Inpainting v1.0', // // ), // // Model( // // 'stable-inpainting-512-v2-0', // // modelTypeStabilityAI, // // category: modelTypeStabilityAI, // // description: 'Stability-AI Stable Inpainting v2.0', // // ), // ]; // } /// 查询模型列表 // Future> models() async { // var resp = await http.get( // Uri.parse('$serverURL/v1/engines/list'), // headers: { // 'Accept': 'application/json', // }..addAll(_buildRequestHeaders()), // ); // if (resp.statusCode != 200) { // return Future.error('Failed to load models: ${resp.body}'); // } // var models = []; // for (var item in jsonDecode(resp.body) as List) { // if ((item['type'] as String).toLowerCase() != 'picture') { // print(item); // continue; // } // models.add(Model( // item['id'], // modelTypeStabilityAI, // category: modelTypeStabilityAI, // description: item['description'], // )); // } // return models; // } /// 创建图片,返回图片的 base64 编码 /// 不同模型价格表: https://platform.stability.ai/docs/getting-started/credits-and-billing#pricing-table /// width,height+steps+engine 决定价格 Future> createImageBase64( String engine, List prompts, { int width = 0, int height = 0, int cfgScale = 7, int samples = 1, int seed = 0, int steps = 30, // 3d-model analog-film anime cinematic comic-book digital-art enhance fantasy-art // isometric line-art low-poly modeling-compound neon-punk origami photographic // pixel-art tile-texture String? stylePreset, }) async { // 注意:图像宽度和高度必须满足下面条件 // For 768 engines: 589,824 ≤ height * width ≤ 1,048,576 // All other engines: 262,144 ≤ height * width ≤ 1,048,576 if (width == 0) { if (engine.contains('-768-')) { width = 768; } else { width = 512; } } if (height == 0) { if (engine.contains('-768-')) { height = 768; } else { height = 512; } } var params = { 'width': width, 'height': height, 'cfg_scale': cfgScale, 'samples': samples, 'seed': seed, 'steps': steps, 'text_prompts': prompts, }; if (stylePreset != null) { params['style_preset'] = stylePreset; } var headers = { 'Accept': 'application/json', 'Content-Type': 'application/json', }..addAll(_buildRequestHeaders()); final url = selfHosted ? Uri.parse('$serverURL/v1/generation/$engine/text-to-image') : Uri.parse('$serverURL/v1/stabilityai/images/$engine/text-to-image'); var req = http.Request('POST', url); req.body = jsonEncode(params); req.headers.addAll(headers); var resp = await http.Response.fromStream(await http.Client().send(req)); if (resp.statusCode != 200) { var ret = jsonDecode(resp.body); return Future.error(ret['error']); } var images = []; for (var item in jsonDecode(resp.body)['artifacts'] as List) { images.add(item['base64']); } return images; } /// 创建图片,返回图片的 base64 编码 /// 不同模型价格表: https://platform.stability.ai/docs/getting-started/credits-and-billing#pricing-table /// width,height+steps+engine 决定价格 Future createImageBase64Async( String engine, List prompts, { int width = 0, int height = 0, int cfgScale = 7, int samples = 1, int seed = 0, int steps = 30, // 3d-model analog-film anime cinematic comic-book digital-art enhance fantasy-art // isometric line-art low-poly modeling-compound neon-punk origami photographic // pixel-art tile-texture String? stylePreset, }) async { // 注意:图像宽度和高度必须满足下面条件 // For 768 engines: 589,824 ≤ height * width ≤ 1,048,576 // All other engines: 262,144 ≤ height * width ≤ 1,048,576 if (width == 0) { if (engine.contains('-768-')) { width = 768; } else { width = 512; } } if (height == 0) { if (engine.contains('-768-')) { height = 768; } else { height = 512; } } var params = { 'width': width, 'height': height, 'cfg_scale': cfgScale, 'samples': samples, 'seed': seed, 'steps': steps, 'text_prompts': prompts, }; if (stylePreset != null) { params['style_preset'] = stylePreset; } var headers = { 'Accept': 'application/json', 'Content-Type': 'application/json', }..addAll(_buildRequestHeaders()); final url = Uri.parse( '$serverURL/v1/stabilityai/images/$engine/text-to-image-async'); var req = http.Request('POST', url); req.body = jsonEncode(params); req.headers.addAll(headers); var resp = await http.Response.fromStream(await http.Client().send(req)); if (resp.statusCode != 200) { var ret = jsonDecode(resp.body); return Future.error(ret['error']); } return jsonDecode(resp.body)['task_id'] as String; } } class StabilityAIPrompt { final String text; final double weight; StabilityAIPrompt(this.text, this.weight); Map toJson() { return { 'text': text, 'weight': weight, }; } } ================================================ FILE: linux/.gitignore ================================================ flutter/ephemeral ================================================ FILE: linux/CMakeLists.txt ================================================ # Project-level configuration. cmake_minimum_required(VERSION 3.10) project(runner LANGUAGES CXX) # The name of the executable created for the application. Change this to change # the on-disk name of your application. set(BINARY_NAME "askaide") # The unique GTK application identifier for this application. See: # https://wiki.gnome.org/HowDoI/ChooseApplicationID set(APPLICATION_ID "cc.aicode.flutter.askaide.askaide") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. cmake_policy(SET CMP0063 NEW) # Load bundled libraries from the lib/ directory relative to the binary. set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") # Root filesystem for cross-building. if(FLUTTER_TARGET_PLATFORM_SYSROOT) set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) endif() # Define build configuration options. if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "Flutter build mode" FORCE) set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Profile" "Release") endif() # Compilation settings that should be applied to most targets. # # Be cautious about adding new options here, as plugins use this function by # default. In most cases, you should add new options to specific targets instead # of modifying this function. function(APPLY_STANDARD_SETTINGS TARGET) target_compile_features(${TARGET} PUBLIC cxx_std_14) target_compile_options(${TARGET} PRIVATE -Wall -Werror) target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") endfunction() # Flutter library and tool build rules. set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") add_subdirectory(${FLUTTER_MANAGED_DIR}) # System-level dependencies. find_package(PkgConfig REQUIRED) pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") # Define the application target. To change its name, change BINARY_NAME above, # not the value here, or `flutter run` will no longer work. # # Any new source files that you add to the application should be added here. add_executable(${BINARY_NAME} "main.cc" "my_application.cc" "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" ) # Apply the standard set of build settings. This can be removed for applications # that need different build settings. apply_standard_settings(${BINARY_NAME}) # Add dependency libraries. Add any application-specific dependencies here. target_link_libraries(${BINARY_NAME} PRIVATE flutter) target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) # Run the Flutter tool portions of the build. This must not be removed. add_dependencies(${BINARY_NAME} flutter_assemble) # Only the install-generated bundle's copy of the executable will launch # correctly, since the resources must in the right relative locations. To avoid # people trying to run the unbundled copy, put it in a subdirectory instead of # the default top-level location. set_target_properties(${BINARY_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" ) # Generated plugin build rules, which manage building the plugins and adding # them to the application. include(flutter/generated_plugins.cmake) # === Installation === # By default, "installing" just makes a relocatable bundle in the build # directory. set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) endif() # Start with a clean build bundle directory every time. install(CODE " file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") " COMPONENT Runtime) set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" COMPONENT Runtime) install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) install(FILES "${bundled_library}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) endforeach(bundled_library) # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. set(FLUTTER_ASSET_DIR_NAME "flutter_assets") install(CODE " file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") " COMPONENT Runtime) install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) # Install the AOT library on non-Debug builds only. if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) endif() ================================================ FILE: linux/flutter/CMakeLists.txt ================================================ # This file controls Flutter-level build steps. It should not be edited. cmake_minimum_required(VERSION 3.10) set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") # Configuration provided via flutter tool. include(${EPHEMERAL_DIR}/generated_config.cmake) # TODO: Move the rest of this into files in ephemeral. See # https://github.com/flutter/flutter/issues/57146. # Serves the same purpose as list(TRANSFORM ... PREPEND ...), # which isn't available in 3.10. function(list_prepend LIST_NAME PREFIX) set(NEW_LIST "") foreach(element ${${LIST_NAME}}) list(APPEND NEW_LIST "${PREFIX}${element}") endforeach(element) set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) endfunction() # === Flutter Library === # System-level dependencies. find_package(PkgConfig REQUIRED) pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") # Published to parent scope for install step. set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) list(APPEND FLUTTER_LIBRARY_HEADERS "fl_basic_message_channel.h" "fl_binary_codec.h" "fl_binary_messenger.h" "fl_dart_project.h" "fl_engine.h" "fl_json_message_codec.h" "fl_json_method_codec.h" "fl_message_codec.h" "fl_method_call.h" "fl_method_channel.h" "fl_method_codec.h" "fl_method_response.h" "fl_plugin_registrar.h" "fl_plugin_registry.h" "fl_standard_message_codec.h" "fl_standard_method_codec.h" "fl_string_codec.h" "fl_value.h" "fl_view.h" "flutter_linux.h" ) list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") add_library(flutter INTERFACE) target_include_directories(flutter INTERFACE "${EPHEMERAL_DIR}" ) target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") target_link_libraries(flutter INTERFACE PkgConfig::GTK PkgConfig::GLIB PkgConfig::GIO ) add_dependencies(flutter flutter_assemble) # === Flutter tool backend === # _phony_ is a non-existent file to force this command to run every time, # since currently there's no way to get a full input/output list from the # flutter tool. add_custom_command( OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} ${CMAKE_CURRENT_BINARY_DIR}/_phony_ COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} VERBATIM ) add_custom_target(flutter_assemble DEPENDS "${FLUTTER_LIBRARY}" ${FLUTTER_LIBRARY_HEADERS} ) ================================================ FILE: linux/flutter/generated_plugin_registrant.cc ================================================ // // Generated file. Do not edit. // // clang-format off #include "generated_plugin_registrant.h" #include #include #include #include #include #include #include #include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin"); audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar); g_autoptr(FlPluginRegistrar) bitsdojo_window_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "BitsdojoWindowPlugin"); bitsdojo_window_plugin_register_with_registrar(bitsdojo_window_linux_registrar); g_autoptr(FlPluginRegistrar) file_saver_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSaverPlugin"); file_saver_plugin_register_with_registrar(file_saver_registrar); g_autoptr(FlPluginRegistrar) flutter_localization_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterLocalizationPlugin"); flutter_localization_plugin_register_with_registrar(flutter_localization_registrar); g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin"); media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar); g_autoptr(FlPluginRegistrar) media_kit_video_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitVideoPlugin"); media_kit_video_plugin_register_with_registrar(media_kit_video_registrar); g_autoptr(FlPluginRegistrar) record_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin"); record_linux_plugin_register_with_registrar(record_linux_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } ================================================ FILE: linux/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 fl_register_plugins(FlPluginRegistry* registry); #endif // GENERATED_PLUGIN_REGISTRANT_ ================================================ FILE: linux/flutter/generated_plugins.cmake ================================================ # # Generated file, do not edit. # list(APPEND FLUTTER_PLUGIN_LIST audioplayers_linux bitsdojo_window_linux file_saver flutter_localization media_kit_libs_linux media_kit_video record_linux url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST media_kit_native_event_loop ) set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) endforeach(ffi_plugin) ================================================ FILE: linux/main.cc ================================================ #include "my_application.h" int main(int argc, char** argv) { g_autoptr(MyApplication) app = my_application_new(); return g_application_run(G_APPLICATION(app), argc, argv); } ================================================ FILE: linux/my_application.cc ================================================ #include "my_application.h" #include #ifdef GDK_WINDOWING_X11 #include #endif #include "flutter/generated_plugin_registrant.h" struct _MyApplication { GtkApplication parent_instance; char** dart_entrypoint_arguments; }; G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) // Implements GApplication::activate. static void my_application_activate(GApplication* application) { MyApplication* self = MY_APPLICATION(application); GtkWindow* window = GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); // Use a header bar when running in GNOME as this is the common style used // by applications and is the setup most users will be using (e.g. Ubuntu // desktop). // If running on X and not using GNOME then just use a traditional title bar // in case the window manager does more exotic layout, e.g. tiling. // If running on Wayland assume the header bar will work (may need changing // if future cases occur). gboolean use_header_bar = TRUE; #ifdef GDK_WINDOWING_X11 GdkScreen* screen = gtk_window_get_screen(window); if (GDK_IS_X11_SCREEN(screen)) { const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); if (g_strcmp0(wm_name, "GNOME Shell") != 0) { use_header_bar = FALSE; } } #endif if (use_header_bar) { GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); gtk_widget_show(GTK_WIDGET(header_bar)); gtk_header_bar_set_title(header_bar, "askaide"); gtk_header_bar_set_show_close_button(header_bar, TRUE); gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); } else { gtk_window_set_title(window, "askaide"); } gtk_window_set_default_size(window, 1280, 720); gtk_widget_show(GTK_WIDGET(window)); g_autoptr(FlDartProject) project = fl_dart_project_new(); fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); FlView* view = fl_view_new(project); gtk_widget_show(GTK_WIDGET(view)); gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); fl_register_plugins(FL_PLUGIN_REGISTRY(view)); gtk_widget_grab_focus(GTK_WIDGET(view)); } // Implements GApplication::local_command_line. static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { MyApplication* self = MY_APPLICATION(application); // Strip out the first argument as it is the binary name. self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); g_autoptr(GError) error = nullptr; if (!g_application_register(application, nullptr, &error)) { g_warning("Failed to register: %s", error->message); *exit_status = 1; return TRUE; } g_application_activate(application); *exit_status = 0; return TRUE; } // Implements GObject::dispose. static void my_application_dispose(GObject* object) { MyApplication* self = MY_APPLICATION(object); g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); G_OBJECT_CLASS(my_application_parent_class)->dispose(object); } static void my_application_class_init(MyApplicationClass* klass) { G_APPLICATION_CLASS(klass)->activate = my_application_activate; G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; G_OBJECT_CLASS(klass)->dispose = my_application_dispose; } static void my_application_init(MyApplication* self) {} MyApplication* my_application_new() { return MY_APPLICATION(g_object_new(my_application_get_type(), "application-id", APPLICATION_ID, "flags", G_APPLICATION_NON_UNIQUE, nullptr)); } ================================================ FILE: linux/my_application.h ================================================ #ifndef FLUTTER_MY_APPLICATION_H_ #define FLUTTER_MY_APPLICATION_H_ #include G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, GtkApplication) /** * my_application_new: * * Creates a new Flutter-based application. * * Returns: a new #MyApplication. */ MyApplication* my_application_new(); #endif // FLUTTER_MY_APPLICATION_H_ ================================================ 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 audioplayers_darwin import bitsdojo_window_macos import file_saver import flutter_image_compress_macos import flutter_localization import flutter_tts import in_app_purchase_storekit import media_kit_libs_macos_video import media_kit_video import package_info_plus import path_provider_foundation import record_darwin import screen_brightness_macos import share_plus import shared_preferences_foundation import sign_in_with_apple import sqflite import url_launcher_macos import wakelock_plus func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) BitsdojoWindowPlugin.register(with: registry.registrar(forPlugin: "BitsdojoWindowPlugin")) FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin")) FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin")) FlutterLocalizationPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalizationPlugin")) FlutterTtsPlugin.register(with: registry.registrar(forPlugin: "FlutterTtsPlugin")) InAppPurchasePlugin.register(with: registry.registrar(forPlugin: "InAppPurchasePlugin")) MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) RecordPlugin.register(with: registry.registrar(forPlugin: "RecordPlugin")) ScreenBrightnessMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenBrightnessMacosPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SignInWithApplePlugin.register(with: registry.registrar(forPlugin: "SignInWithApplePlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) } ================================================ 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! use_modular_headers! flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) end # bugfix for xcode15beta https://github.com/CocoaPods/CocoaPods/issues/12012 post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_macos_build_settings(target) target.build_configurations.each do |config| xcconfig_path = config.base_configuration_reference.real_path xcconfig = File.read(xcconfig_path) xcconfig_mod = xcconfig.gsub(/DT_TOOLCHAIN_DIR/, "TOOLCHAIN_DIR") File.open(xcconfig_path, "w") { |file| file << xcconfig_mod } end end end ================================================ FILE: macos/Runner/AppDelegate.swift ================================================ import Cocoa import FlutterMacOS @main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } } ================================================ FILE: macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "info": { "version": 1, "author": "ai.aicode.cc" }, "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" } ] } ================================================ 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 = AIdea // The application's bundle identifier PRODUCT_BUNDLE_IDENTIFIER = cc.aicode.flutter.askaide.askaide // The copyright displayed in application information PRODUCT_COPYRIGHT = Copyright © 2023 cc.aicode.flutter.askaide. 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.developer.applesignin Default com.apple.security.app-sandbox com.apple.security.cs.allow-jit com.apple.security.device.audio-input com.apple.security.files.downloads.read-write com.apple.security.network.client com.apple.security.network.server ================================================ 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 NSMicrophoneUsageDescription We need to access to the microphone to record audio file NSPhotoLibraryUsageDescription We need to access to the photo library to pick files for upload NSAppTransportSecurity NSAllowsArbitraryLoads NSAllowsArbitraryLoadsForMedia ================================================ FILE: macos/Runner/MainFlutterWindow.swift ================================================ import Cocoa import FlutterMacOS import bitsdojo_window_macos class MainFlutterWindow: BitsdojoWindow { override func bitsdojo_window_configure() -> UInt { return BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP } override func awakeFromNib() { let flutterViewController = FlutterViewController.init() let windowFrame = self.frame self.contentViewController = flutterViewController self.setFrame(windowFrame, display: true) // 设置窗口大小 self.setContentSize(NSSize(width: 850, height: 750)) // 设置窗口禁止缩放 // let window: NSWindow! = self.contentView?.window // window.styleMask.remove(.resizable) RegisterGeneratedPlugins(registry: flutterViewController) super.awakeFromNib() } } ================================================ FILE: macos/Runner/Release.entitlements ================================================ com.apple.developer.applesignin Default com.apple.security.app-sandbox com.apple.security.device.audio-input com.apple.security.files.downloads.read-write com.apple.security.network.client com.apple.security.network.server ================================================ 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 */ 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 */; }; 5A29A95C06583FFBDD292907 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43240CB6A8AF2E981A3B5D25 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 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 */ 03A3A2E26C91E71A9326CB79 /* 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 = ""; }; 107E9B87B247CD30AF3B0493 /* 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 = ""; }; 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 /* AIdea.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AIdea.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 = ""; }; 43240CB6A8AF2E981A3B5D25 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; C5938F4B7199953F9DBA617A /* 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 = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 33CC10EA2044A3C60003C045 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 5A29A95C06583FFBDD292907 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 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 */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, 729C48A7C1C7FFEDEFF13553 /* Pods */, ); sourceTree = ""; }; 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( 33CC10ED2044A3C60003C045 /* AIdea.app */, ); 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 = ""; }; 729C48A7C1C7FFEDEFF13553 /* Pods */ = { isa = PBXGroup; children = ( 03A3A2E26C91E71A9326CB79 /* Pods-Runner.debug.xcconfig */, C5938F4B7199953F9DBA617A /* Pods-Runner.release.xcconfig */, 107E9B87B247CD30AF3B0493 /* Pods-Runner.profile.xcconfig */, ); path = Pods; sourceTree = ""; }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( 43240CB6A8AF2E981A3B5D25 /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 33CC10EC2044A3C60003C045 /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( 67CB0A1AC8A57E2A6033912B /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, 5126F90D8DF54E4B9681052E /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); dependencies = ( 33CC11202044C79F0003C045 /* PBXTargetDependency */, ); name = Runner; productName = Runner; productReference = 33CC10ED2044A3C60003C045 /* AIdea.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 33CC10E52044A3C60003C045 /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 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 */, 33CC111A2044C6BA0003C045 /* Flutter Assemble */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 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"; }; 5126F90D8DF54E4B9681052E /* [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; }; 67CB0A1AC8A57E2A6033912B /* [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 */ 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 */ 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 */ 338D0CE9231458BD00FA5F75 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; 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; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; 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_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = N95437SZ2A; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; 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; 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; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; 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; 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; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; 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_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = N95437SZ2A; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; 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_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = N95437SZ2A; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; 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 */ 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: nginx.conf ================================================ server { listen 80; gzip on; gzip_static on; gzip_vary on; gzip_types text/plain application/x-javascript text/css application/xml text/xml application/javascript; root /data/webroot; location / { index index.html; try_files $uri $uri/ =404; } } ================================================ FILE: pubspec.yaml ================================================ name: askaide description: 一款支持 GPT 以及国产大语言模型通义千问、文心一言等,支持 Stable Diffusion 文生图、图生图、 SDXL1.0、超分辨率、图片上色的全能型 APP。 # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 # followed by an optional build number separated by a +. # Both the version and the builder number may be overridden in flutter # build by specifying --build-name and --build-number, respectively. # In Android, build-name is used as versionName while build-number used as versionCode. # Read more about Android versioning at https://developer.android.com/studio/publish/versioning # In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. # 应用正式发布时,需要同步修改 lib/helper/constant.dart 中的 VERSION 值 version: 2.0.0+1 environment: sdk: '>=3.0.0 <4.0.0' # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions # consider running `flutter pub upgrade --major-versions`. Alternatively, # dependencies can be manually updated by changing the version numbers below to # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 intl: ^0.19.0 flutter_highlight: ^0.7.0 flutter_bloc: ^8.1.2 path_provider: ^2.0.14 dart_openai: git: url: https://github.com/mylxsw/openai.git uuid: ^4.4.2 settings_ui: ^2.0.2 clipboard: ^0.1.3 bot_toast: ^4.0.3 loading_animation_widget: ^1.2.0+4 flip_card: ^0.7.0 flutter_slidable: ^3.0.1 provider: ^6.0.5 flutter_iconpicker: git: url: https://github.com/mylxsw/FlutterIconPicker ref: hotfix share_plus: ^7.2.1 go_router: ^8.2.0 sqflite: ^2.2.6 file_picker: ^5.2.8 http: ^1.1.0 cached_network_image: ^3.2.3 photo_view: ^0.14.0 flutter_image_compress: ^2.3.0 isolate_image_compress: git: url: https://github.com/mylxsw/isolate_flutter ref: customized path: isolate_image_compress image: 4.2.0 flutter_cache_manager: ^3.3.0 file_saver: git: url: https://github.com/mylxsw/file_saver ref: customized image_gallery_saver: ^2.0.3 tiktoken: ^1.0.3 flutter_colorpicker: ^1.0.3 singleton: ^0.0.2 url_launcher: ^6.1.10 crypto: ^3.0.2 dio: ^5.5.0 dio_smart_retry: ^6.0.0 qiniu_flutter_sdk: ^0.6.0 sign_in_with_apple: ^4.3.0 record: ^5.0.4 animations: ^2.0.7 animated_text_kit: ^4.2.2 flutter_localization: ^0.1.11 random_avatar: ^0.0.8 in_app_purchase: ^3.1.7 fluwx: ^3.13.1 quickalert: git: url: https://github.com/mylxsw/QuickAlert.git ref: customized sign_in_button: ^3.2.0 animated_button_bar: ^1.0.0 circular_countdown_timer: ^0.2.3 sizer: ^2.0.15 logger: git: url: https://github.com/Bungeefan/logger.git before_after: git: url: https://github.com/mylxsw/before_after.git flutter_native_splash: ^2.2.19 flutter_drawing_board: ^0.4.4+2 loading_more_list: ^5.0.3 tobias: ^2.4.2 flutter_sticky_header: ^0.6.5 audioplayers: ^5.0.0 flutter_tts: ^3.7.0 widgets_to_image: ^0.0.2 custom_sliding_segmented_control: ^1.7.5 sqflite_common_ffi: ^2.2.5 sqflite_common_ffi_web: ^0.4.0 flutter_markdown: ^0.7.6+2 markdown: ^7.3.0 fetch_api: 1.0.1 web_socket_channel: ^2.4.0 flutter_initicon: ^3.0.0+1 markdown_widget: ^2.3.1 flutter_math_fork: ^0.7.3 media_kit: ^1.1.10 media_kit_video: ^1.2.4 media_kit_libs_video: ^1.0.4 path: ^1.8.3 autoscale_tabbarview: ^1.0.2 flutter_stripe: ^11.0.0 flutter_stripe_web: ^6.0.0 stripe_js: ^6.0.0 qr_flutter: ^4.1.0 flutter_phoenix: ^1.1.1 dio_cache_interceptor: ^3.5.0 camera: ^0.11.0+2 camerawesome: ^2.1.0 auto_size_text: ^3.0.0 animated_list_plus: ^0.5.2 lottie: ^3.2.0 bitsdojo_window: ^0.1.6 dev_dependencies: flutter_test: sdk: flutter # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. flutter_lints: ^2.0.0 flutter_launcher_icons: "^0.14.2" build_runner: ^2.3.3 msix: ^3.9.1 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter packages. flutter: # 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 # To add assets to your application, add an assets section, like this: assets: - assets/app.png - assets/app-macos.png - assets/image-broken.png - assets/wechat.png - assets/friendroom.png - assets/share.png - assets/app-256-transparent.png - assets/light-dark-auto.png - assets/openai.png - assets/transport.png - assets/weibo.png - assets/github.png - assets/x.png - assets/xiaohongshu.png - assets/play.png - assets/text-to-video.gif - assets/wechat-pay.png - assets/zhifubao.png - assets/stripe.png - assets/apple.webp - assets/icons/doc.png - assets/icons/file.png - assets/icons/mp3.png - assets/icons/pdf.png - assets/icons/txt.png - assets/lottie/empty_status.json # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware # For details regarding adding assets from package dependencies, see # https://flutter.dev/assets-and-images/#from-packages # 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: AlibabaPuHuiTi fonts: - asset: assets/fonts/AlibabaPuHuiTi-3-55-Regular.ttf - asset: assets/fonts/AlibabaPuHuiTi-3-85-Bold.ttf weight: 700 # # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages flutter_native_splash: image: assets/app-splash.png color: "#ffffff" fullscreen: true tobias: url_scheme: alipay2021004101661425 msix_config: display_name: AIdea publisher_display_name: Shenzhen Gulu Artificial Intelligence Technology Co., Ltd. identity_name: cc.aicode.flutter.askaide.askaide logo_path: assets/app.png execution_alias: AIdea ================================================ FILE: scripts/go.mod ================================================ module github.com/mylxsw/aidea/scripts go 1.20 require ( github.com/mylxsw/asteria v1.0.1 github.com/mylxsw/go-utils v1.0.3 ) require ( github.com/json-iterator/go v1.1.12 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect golang.org/x/text v0.3.4 // indirect ) ================================================ FILE: scripts/go.sum ================================================ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mylxsw/asteria v1.0.1 h1:M+RLL/0R0CkeRLwiaikBlLkEqO6rTpqqaMUhDVsZRqQ= github.com/mylxsw/asteria v1.0.1/go.mod h1:pmMRQjiOk1ZndmWnk7fDb4iIVrPhWCaWl6wV0R51zws= github.com/mylxsw/go-utils v1.0.3 h1:kL1n25xVzEDCjhtNx32dXXixFvuslCE5RGKMEUxeeJI= github.com/mylxsw/go-utils v1.0.3/go.mod h1:F5pQ/vTAgccZxQA7jsIBXM6m2INAbqPKfzbNwQgqhzY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= ================================================ FILE: scripts/macos-icon-replace.sh ================================================ mv 16x16.png icon-16.png cp 32x32.png icon-16@2x.png mv 32x32.png icon-32.png mv 64x64.png icon-32@2x.png mv 128x128.png icon-128.png cp 256x256.png icon-128@2x.png mv 256x256.png icon-256.png cp 512x512.png icon-256@2x.png mv 512x512.png icon-512.png mv 1024x1024.png icon-512@2x.png ================================================ FILE: scripts/main.go ================================================ package main import ( "os" "strings" "github.com/mylxsw/asteria/log" "github.com/mylxsw/go-utils/must" ) // 用于替换 Web 端的字体文件地址,将字体文件下载到本地,解决无法访问 Google 字体的问题 func main() { mainDartJSPath := os.Args[1] data := string(must.Must(os.ReadFile(mainDartJSPath))) // 替换字体为本地 CDN // fontRegex := regexp.MustCompile(`https://fonts\.gstatic\.com/(.*?)\.(ttf|otf|woff|woff2)`) // for _, u := range fontRegex.FindAllString(data, -1) { // savePath := must.Must(download(u)) // data = strings.ReplaceAll(data, u, "https://resources.aicode.cc/fonts/"+savePath) // } // 替换字体为国内镜像 data = strings.ReplaceAll(data, "fonts.gstatic.com", "global-cdn.aicode.cc") data = strings.ReplaceAll(data, "www.gstatic.com", "global-cdn.aicode.cc") // 替换字体为国内镜像 data = strings.ReplaceAll(data, "fonts.gstatic.com", "fonts-gstatic.lug.ustc.edu.cn") must.NoError(os.WriteFile(mainDartJSPath, []byte(data), 0755)) log.Debugf("replace font url success") } // func download(remoteURL string) (string, error) { // savePath := strings.TrimPrefix(remoteURL, "https://fonts.gstatic.com/") // // 检查目录是否存在,不存在则创建 // if err := os.MkdirAll(filepath.Dir(savePath), 0755); err != nil { // return "", err // } // // 检查文件是否存在 // if _, err := os.Stat(savePath); err == nil { // return savePath, nil // } // log.Debugf("download %s to %s", remoteURL, savePath) // // 下载文件到本地 // resp, err := http.Get(remoteURL) // if err != nil { // return "", err // } // defer resp.Body.Close() // f, err := os.Create(savePath) // if err != nil { // return "", err // } // defer f.Close() // if _, err := io.Copy(f, resp.Body); err != nil { // return "", err // } // return savePath, nil // } ================================================ FILE: web/index.html ================================================ AIdea ================================================ FILE: web/manifest.json ================================================ { "name": "AIdea", "short_name": "aidea", "start_url": ".", "display": "standalone", "background_color": "#hexcode", "theme_color": "#hexcode", "description": "一款强大的AI应用,涵盖当下最流行的人工智能模型", "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" }, { "src": "icons/Icon-maskable-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { "src": "icons/Icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } ] } ================================================ FILE: web/splash/splash.js ================================================ function removeSplashFromWeb() { document.getElementById("splash")?.remove(); document.getElementById("splash-branding")?.remove(); document.body.style.background = "transparent"; } ================================================ FILE: web/splash/style.css ================================================ html { height: 100% } body { margin: 0; min-height: 100%; background-color: #ffffff; background-size: 100% 100%; } .center { margin: 0; position: absolute; top: 50%; left: 50%; -ms-transform: translate(-50%, -50%); transform: translate(-50%, -50%); } .contain { display:block; width:100%; height:100%; object-fit: contain; } .stretch { display:block; width:100%; height:100%; } .cover { display:block; width:100%; height:100%; object-fit: cover; } .bottom { position: absolute; bottom: 0; left: 50%; -ms-transform: translate(-50%, 0); transform: translate(-50%, 0); } .bottomLeft { position: absolute; bottom: 0; left: 0; } .bottomRight { position: absolute; bottom: 0; right: 0; } ================================================ FILE: web/sqflite_sw.js ================================================ (function dartProgram(){function copyProperties(a,b){var s=Object.keys(a) for(var r=0;r=0)return true if(typeof version=="function"&&version.length==0){var q=version() if(/^\d+\.\d+\.\d+\.\d+$/.test(q))return true}}catch(p){}return false}() function inherit(a,b){a.prototype.constructor=a a.prototype["$i"+a.name]=a if(b!=null){if(z){a.prototype.__proto__=b.prototype return}var s=Object.create(b.prototype) copyProperties(a.prototype,s) a.prototype=s}}function inheritMany(a,b){for(var s=0;s").b(a))return new A.ep(a,b.h("@<0>").q(c).h("ep<1,2>")) return new A.cb(a,b.h("@<0>").q(c).h("cb<1,2>"))}, rv(a){return new A.cQ("Field '"+a+"' has been assigned during initialization.")}, oN(a){return new A.cQ("Field '"+a+"' has not been initialized.")}, n9(a){var s,r=a^48 if(r<=9)return r s=a|32 if(97<=s&&s<=102)return s-87 return-1}, bZ(a,b){a=a+b&536870911 a=a+((a&524287)<<10)&536870911 return a^a>>>6}, nO(a){a=a+((a&67108863)<<3)&536870911 a^=a>>>11 return a+((a&16383)<<15)&536870911}, c7(a,b,c){return a}, eb(a,b,c,d){A.aT(b,"start") if(c!=null){A.aT(c,"end") if(b>c)A.J(A.a1(b,0,c,"start",null))}return new A.cn(a,b,c,d.h("cn<0>"))}, nD(a,b,c,d){if(t.V.b(a))return new A.ce(a,b,c.h("@<0>").q(d).h("ce<1,2>")) return new A.bw(a,b,c.h("@<0>").q(d).h("bw<1,2>"))}, p0(a,b,c){var s="count" if(t.V.b(a)){A.jg(b,s,t.S) A.aT(b,s) return new A.cG(a,b,c.h("cG<0>"))}A.jg(b,s,t.S) A.aT(b,s) return new A.bA(a,b,c.h("bA<0>"))}, bt(){return new A.bB("No element")}, oJ(){return new A.bB("Too few elements")}, ry(a,b){return new A.dO(a,b.h("dO<0>"))}, t0(a,b,c){A.he(a,0,J.Y(a)-1,b,c)}, he(a,b,c,d,e){if(c-b<=32)A.t_(a,b,c,d,e) else A.rZ(a,b,c,d,e)}, t_(a,b,c,d,e){var s,r,q,p,o,n for(s=b+1,r=J.T(a);s<=c;++s){q=r.i(a,s) p=s while(!0){if(p>b){o=d.$2(r.i(a,p-1),q) if(typeof o!=="number")return o.a5() o=o>0}else o=!1 if(!o)break n=p-1 r.k(a,p,r.i(a,n)) p=n}r.k(a,p,q)}}, rZ(a3,a4,a5,a6,a7){var s,r,q,p,o,n,m,l,k,j=B.c.R(a5-a4+1,6),i=a4+j,h=a5-j,g=B.c.R(a4+a5,2),f=g-j,e=g+j,d=J.T(a3),c=d.i(a3,i),b=d.i(a3,f),a=d.i(a3,g),a0=d.i(a3,e),a1=d.i(a3,h),a2=a6.$2(c,b) if(typeof a2!=="number")return a2.a5() if(a2>0){s=b b=c c=s}a2=a6.$2(a0,a1) if(typeof a2!=="number")return a2.a5() if(a2>0){s=a1 a1=a0 a0=s}a2=a6.$2(c,a) if(typeof a2!=="number")return a2.a5() if(a2>0){s=a a=c c=s}a2=a6.$2(b,a) if(typeof a2!=="number")return a2.a5() if(a2>0){s=a a=b b=s}a2=a6.$2(c,a0) if(typeof a2!=="number")return a2.a5() if(a2>0){s=a0 a0=c c=s}a2=a6.$2(a,a0) if(typeof a2!=="number")return a2.a5() if(a2>0){s=a0 a0=a a=s}a2=a6.$2(b,a1) if(typeof a2!=="number")return a2.a5() if(a2>0){s=a1 a1=b b=s}a2=a6.$2(b,a) if(typeof a2!=="number")return a2.a5() if(a2>0){s=a a=b b=s}a2=a6.$2(a0,a1) if(typeof a2!=="number")return a2.a5() if(a2>0){s=a1 a1=a0 a0=s}d.k(a3,i,c) d.k(a3,g,a) d.k(a3,h,a1) d.k(a3,f,d.i(a3,a4)) d.k(a3,e,d.i(a3,a5)) r=a4+1 q=a5-1 if(J.a7(a6.$2(b,a0),0)){for(p=r;p<=q;++p){o=d.i(a3,p) n=a6.$2(o,b) if(n===0)continue if(n<0){if(p!==r){d.k(a3,p,d.i(a3,r)) d.k(a3,r,o)}++r}else for(;!0;){n=a6.$2(d.i(a3,q),b) if(n>0){--q continue}else{m=q-1 if(n<0){d.k(a3,p,d.i(a3,r)) l=r+1 d.k(a3,r,d.i(a3,q)) d.k(a3,q,o) q=m r=l break}else{d.k(a3,p,d.i(a3,q)) d.k(a3,q,o) q=m break}}}}k=!0}else{for(p=r;p<=q;++p){o=d.i(a3,p) if(a6.$2(o,b)<0){if(p!==r){d.k(a3,p,d.i(a3,r)) d.k(a3,r,o)}++r}else if(a6.$2(o,a0)>0)for(;!0;)if(a6.$2(d.i(a3,q),a0)>0){--q if(qh){for(;J.a7(a6.$2(d.i(a3,r),b),0);)++r for(;J.a7(a6.$2(d.i(a3,q),a0),0);)--q for(p=r;p<=q;++p){o=d.i(a3,p) if(a6.$2(o,b)===0){if(p!==r){d.k(a3,p,d.i(a3,r)) d.k(a3,r,o)}++r}else if(a6.$2(o,a0)===0)for(;!0;)if(a6.$2(d.i(a3,q),a0)===0){--q if(q=m.length)return A.d(m,3) s=m[3] if(b==null){if(s!=null)return parseInt(a,10) if(m[2]!=null)return parseInt(a,16) return n}if(b<2||b>36)throw A.b(A.a1(b,2,36,"radix",n)) if(b===10&&s!=null)return parseInt(a,10) if(b<10||s==null){r=b<=10?47+b:86+b q=m[1] for(p=q.length,o=0;or)return n}return parseInt(a,b)}, kb(a){return A.rH(a)}, rH(a){var s,r,q,p if(a instanceof A.r)return A.aJ(A.a0(a),null) s=J.bL(a) if(s===B.W||s===B.Z||t.cx.b(a)){r=B.v(a) if(r!=="Object"&&r!=="")return r q=a.constructor if(typeof q=="function"){p=q.name if(typeof p=="string"&&p!=="Object"&&p!=="")return p}}return A.aJ(A.a0(a),null)}, rJ(){if(!!self.location)return self.location.href return null}, oQ(a){var s,r,q,p,o=a.length if(o<=500)return String.fromCharCode.apply(null,a) for(s="",r=0;r65535)return A.rS(a)}return A.oQ(a)}, rT(a,b,c){var s,r,q,p if(c<=500&&b===0&&c===a.length)return String.fromCharCode.apply(null,a) for(s=b,r="";s>>0,s&1023|56320)}}throw A.b(A.a1(a,0,1114111,null,null))}, aS(a){if(a.date===void 0)a.date=new Date(a.a) return a.date}, rQ(a){return a.b?A.aS(a).getUTCFullYear()+0:A.aS(a).getFullYear()+0}, rO(a){return a.b?A.aS(a).getUTCMonth()+1:A.aS(a).getMonth()+1}, rK(a){return a.b?A.aS(a).getUTCDate()+0:A.aS(a).getDate()+0}, rL(a){return a.b?A.aS(a).getUTCHours()+0:A.aS(a).getHours()+0}, rN(a){return a.b?A.aS(a).getUTCMinutes()+0:A.aS(a).getMinutes()+0}, rP(a){return a.b?A.aS(a).getUTCSeconds()+0:A.aS(a).getSeconds()+0}, rM(a){return a.b?A.aS(a).getUTCMilliseconds()+0:A.aS(a).getMilliseconds()+0}, bY(a,b,c){var s,r,q={} q.a=0 s=[] r=[] q.a=b.length B.b.b4(s,b) q.b="" if(c!=null&&c.a!==0)c.D(0,new A.ka(q,r,s)) return J.r_(a,new A.fI(B.a3,0,s,r,0))}, rI(a,b,c){var s,r,q if(Array.isArray(b))s=c==null||c.a===0 else s=!1 if(s){r=b.length if(r===0){if(!!a.$0)return a.$0()}else if(r===1){if(!!a.$1)return a.$1(b[0])}else if(r===2){if(!!a.$2)return a.$2(b[0],b[1])}else if(r===3){if(!!a.$3)return a.$3(b[0],b[1],b[2])}else if(r===4){if(!!a.$4)return a.$4(b[0],b[1],b[2],b[3])}else if(r===5)if(!!a.$5)return a.$5(b[0],b[1],b[2],b[3],b[4]) q=a[""+"$"+r] if(q!=null)return q.apply(a,b)}return A.rG(a,b,c)}, rG(a,b,c){var s,r,q,p,o,n,m,l,k,j,i,h,g=Array.isArray(b)?b:A.fM(b,!0,t.z),f=g.length,e=a.$R if(fn)return A.bY(a,g,null) if(fe)return A.bY(a,g,c) if(g===b)g=A.fM(g,!0,t.z) l=Object.keys(q) if(c==null)for(r=l.length,k=0;k=s)return A.V(b,s,a,null,r) return A.oS(b,r)}, va(a,b,c){if(a<0||a>c)return A.a1(a,0,c,"start",null) if(b!=null)if(bc)return A.a1(b,a,c,"end",null) return new A.bh(!0,b,"end",null)}, cA(a){return new A.bh(!0,a,null,null)}, b(a){var s,r if(a==null)a=new A.h1() s=new Error() s.dartException=a r=A.vx if("defineProperty" in Object){Object.defineProperty(s,"message",{get:r}) s.name=""}else s.toString=r return s}, vx(){return J.bp(this.dartException)}, J(a){throw A.b(a)}, aM(a){throw A.b(A.ap(a))}, bC(a){var s,r,q,p,o,n a=A.vt(a.replace(String({}),"$receiver$")) s=a.match(/\\\$[a-zA-Z]+\\\$/g) if(s==null)s=A.t([],t.s) r=s.indexOf("\\$arguments\\$") q=s.indexOf("\\$argumentsExpr\\$") p=s.indexOf("\\$expr\\$") o=s.indexOf("\\$method\\$") n=s.indexOf("\\$receiver\\$") return new A.ld(a.replace(new RegExp("\\\\\\$arguments\\\\\\$","g"),"((?:x|[^x])*)").replace(new RegExp("\\\\\\$argumentsExpr\\\\\\$","g"),"((?:x|[^x])*)").replace(new RegExp("\\\\\\$expr\\\\\\$","g"),"((?:x|[^x])*)").replace(new RegExp("\\\\\\$method\\\\\\$","g"),"((?:x|[^x])*)").replace(new RegExp("\\\\\\$receiver\\\\\\$","g"),"((?:x|[^x])*)"),r,q,p,o,n)}, le(a){return function($expr$){var $argumentsExpr$="$arguments$" try{$expr$.$method$($argumentsExpr$)}catch(s){return s.message}}(a)}, p7(a){return function($expr$){try{$expr$.$method$}catch(s){return s.message}}(a)}, nA(a,b){var s=b==null,r=s?null:b.method return new A.fK(a,r,s?null:b.receiver)}, M(a){var s if(a==null)return new A.h2(a) if(a instanceof A.dC){s=a.a return A.c9(a,s==null?t.K.a(s):s)}if(typeof a!=="object")return a if("dartException" in a)return A.c9(a,a.dartException) return A.uY(a)}, c9(a,b){if(t.W.b(b))if(b.$thrownJsError==null)b.$thrownJsError=a return b}, uY(a){var s,r,q,p,o,n,m,l,k,j,i,h,g,f,e=null if(!("message" in a))return a s=a.message if("number" in a&&typeof a.number=="number"){r=a.number q=r&65535 if((B.c.M(r,16)&8191)===10)switch(q){case 438:return A.c9(a,A.nA(A.q(s)+" (Error "+q+")",e)) case 445:case 5007:p=A.q(s) return A.c9(a,new A.dW(p+" (Error "+q+")",e))}}if(a instanceof TypeError){o=$.qs() n=$.qt() m=$.qu() l=$.qv() k=$.qy() j=$.qz() i=$.qx() $.qw() h=$.qB() g=$.qA() f=o.a4(s) if(f!=null)return A.c9(a,A.nA(A.S(s),f)) else{f=n.a4(s) if(f!=null){f.method="call" return A.c9(a,A.nA(A.S(s),f))}else{f=m.a4(s) if(f==null){f=l.a4(s) if(f==null){f=k.a4(s) if(f==null){f=j.a4(s) if(f==null){f=i.a4(s) if(f==null){f=l.a4(s) if(f==null){f=h.a4(s) if(f==null){f=g.a4(s) p=f!=null}else p=!0}else p=!0}else p=!0}else p=!0}else p=!0}else p=!0}else p=!0 if(p){A.S(s) return A.c9(a,new A.dW(s,f==null?e:f.method))}}}return A.c9(a,new A.hx(typeof s=="string"?s:""))}if(a instanceof RangeError){if(typeof s=="string"&&s.indexOf("call stack")!==-1)return new A.e9() s=function(b){try{return String(b)}catch(d){}return null}(a) return A.c9(a,new A.bh(!1,e,e,typeof s=="string"?s.replace(/^RangeError:\s*/,""):s))}if(typeof InternalError=="function"&&a instanceof InternalError)if(typeof s=="string"&&s==="too much recursion")return new A.e9() return a}, a_(a){var s if(a instanceof A.dC)return a.b if(a==null)return new A.eH(a) s=a.$cachedTrace if(s!=null)return s return a.$cachedTrace=new A.eH(a)}, j7(a){if(a==null||typeof a!="object")return J.ax(a) else return A.dZ(a)}, vb(a,b){var s,r,q,p=a.length for(s=0;s=0 else if(b instanceof A.dL){s=B.a.O(a,c) return b.b.test(s)}else{s=J.qR(b,B.a.O(a,c)) return!s.gC(s)}}, vt(a){if(/[[\]{}()*+?.\\^$|]/.test(a))return a.replace(/[[\]{}()*+?.\\^$|]/g,"\\$&") return a}, vv(a,b,c,d){return a.substring(0,b)+d+a.substring(c)}, dx:function dx(a,b){this.a=a this.$ti=b}, dw:function dw(){}, cc:function cc(a,b,c,d){var _=this _.a=a _.b=b _.c=c _.$ti=d}, jv:function jv(a){this.a=a}, em:function em(a,b){this.a=a this.$ti=b}, fI:function fI(a,b,c,d,e){var _=this _.a=a _.c=b _.d=c _.e=d _.f=e}, ka:function ka(a,b,c){this.a=a this.b=b this.c=c}, ld:function ld(a,b,c,d,e,f){var _=this _.a=a _.b=b _.c=c _.d=d _.e=e _.f=f}, dW:function dW(a,b){this.a=a this.b=b}, fK:function fK(a,b,c){this.a=a this.b=b this.c=c}, hx:function hx(a){this.a=a}, h2:function h2(a){this.a=a}, dC:function dC(a,b){this.a=a this.b=b}, eH:function eH(a){this.a=a this.b=null}, bS:function bS(){}, ff:function ff(){}, fg:function fg(){}, ho:function ho(){}, hk:function hk(){}, cD:function cD(a,b){this.a=a this.b=b}, hc:function hc(a){this.a=a}, hO:function hO(a){this.a=a}, mu:function mu(){}, as:function as(a){var _=this _.a=0 _.f=_.e=_.d=_.c=_.b=null _.r=0 _.$ti=a}, jT:function jT(a){this.a=a}, jS:function jS(a){this.a=a}, jV:function jV(a,b){var _=this _.a=a _.b=b _.d=_.c=null}, bv:function bv(a,b){this.a=a this.$ti=b}, dM:function dM(a,b,c){var _=this _.a=a _.b=b _.d=_.c=null _.$ti=c}, na:function na(a){this.a=a}, nb:function nb(a){this.a=a}, nc:function nc(a){this.a=a}, dL:function dL(a,b){var _=this _.a=a _.b=b _.d=_.c=null}, ey:function ey(a){this.b=a}, hM:function hM(a,b,c){this.a=a this.b=b this.c=c}, hN:function hN(a,b,c){var _=this _.a=a _.b=b _.c=c _.d=null}, ea:function ea(a,b){this.a=a this.c=b}, iE:function iE(a,b,c){this.a=a this.b=b this.c=c}, iF:function iF(a,b,c){var _=this _.a=a _.b=b _.c=c _.d=null}, aZ(a){return A.J(A.oN(a))}, nm(a){return A.J(A.rv(a))}, el(a){var s=new A.lI(a) return s.b=s}, lI:function lI(a){this.a=a this.b=null}, up(a){return a}, pJ(a,b,c){}, uv(a){return a}, rB(a){return new Int8Array(a)}, dS(a,b,c){A.pJ(a,b,c) c=B.c.R(a.byteLength-b,4) return new Uint32Array(a,b,c)}, b_(a,b,c){A.pJ(a,b,c) return c==null?new Uint8Array(a,b):new Uint8Array(a,b,c)}, bK(a,b,c){if(a>>>0!==a||a>=c)throw A.b(A.dp(b,a))}, uq(a,b,c){var s if(!(a>>>0!==a))s=b>>>0!==b||a>b||b>c else s=!0 if(s)throw A.b(A.va(a,b,c)) return b}, cW:function cW(){}, a5:function a5(){}, dR:function dR(){}, ag:function ag(){}, bX:function bX(){}, aQ:function aQ(){}, fT:function fT(){}, fU:function fU(){}, fV:function fV(){}, fW:function fW(){}, fX:function fX(){}, fY:function fY(){}, fZ:function fZ(){}, dT:function dT(){}, cl:function cl(){}, eA:function eA(){}, eB:function eB(){}, eC:function eC(){}, eD:function eD(){}, oY(a,b){var s=b.c return s==null?b.c=A.o1(a,b.y,!0):s}, oX(a,b){var s=b.c return s==null?b.c=A.eP(a,"H",[b.y]):s}, oZ(a){var s=a.x if(s===6||s===7||s===8)return A.oZ(a.y) return s===12||s===13}, rY(a){return a.at}, aL(a){return A.iS(v.typeUniverse,a,!1)}, c6(a,b,a0,a1){var s,r,q,p,o,n,m,l,k,j,i,h,g,f,e,d,c=b.x switch(c){case 5:case 1:case 2:case 3:case 4:return b case 6:s=b.y r=A.c6(a,s,a0,a1) if(r===s)return b return A.ps(a,r,!0) case 7:s=b.y r=A.c6(a,s,a0,a1) if(r===s)return b return A.o1(a,r,!0) case 8:s=b.y r=A.c6(a,s,a0,a1) if(r===s)return b return A.pr(a,r,!0) case 9:q=b.z p=A.f_(a,q,a0,a1) if(p===q)return b return A.eP(a,b.y,p) case 10:o=b.y n=A.c6(a,o,a0,a1) m=b.z l=A.f_(a,m,a0,a1) if(n===o&&l===m)return b return A.o_(a,n,l) case 12:k=b.y j=A.c6(a,k,a0,a1) i=b.z h=A.uV(a,i,a0,a1) if(j===k&&h===i)return b return A.pq(a,j,h) case 13:g=b.z a1+=g.length f=A.f_(a,g,a0,a1) o=b.y n=A.c6(a,o,a0,a1) if(f===g&&n===o)return b return A.o0(a,n,f,!0) case 14:e=b.y if(e=0)p+=" "+r[q];++q}return p+"})"}, pP(a4,a5,a6){var s,r,q,p,o,n,m,l,k,j,i,h,g,f,e,d,c,b,a,a0,a1,a2,a3=", " if(a6!=null){s=a6.length if(a5==null){a5=A.t([],t.s) r=null}else r=a5.length q=a5.length for(p=s;p>0;--p)B.b.m(a5,"T"+(q+p)) for(o=t.X,n=t._,m="<",l="",p=0;p=0))return A.d(a5,j) m=B.a.bf(m+l,a5[j]) i=a6[p] h=i.x if(!(h===2||h===3||h===4||h===5||i===o))if(!(i===n))k=!1 else k=!0 else k=!0 if(!k)m+=" extends "+A.aJ(i,a5)}m+=">"}else{m="" r=null}o=a4.y g=a4.z f=g.a e=f.length d=g.b c=d.length b=g.c a=b.length a0=A.aJ(o,a5) for(a1="",a2="",p=0;p0){a1+=a2+"[" for(a2="",p=0;p0){a1+=a2+"{" for(a2="",p=0;p "+a0}, aJ(a,b){var s,r,q,p,o,n,m,l=a.x if(l===5)return"erased" if(l===2)return"dynamic" if(l===3)return"void" if(l===1)return"Never" if(l===4)return"any" if(l===6){s=A.aJ(a.y,b) return s}if(l===7){r=a.y s=A.aJ(r,b) q=r.x return(q===12||q===13?"("+s+")":s)+"?"}if(l===8)return"FutureOr<"+A.aJ(a.y,b)+">" if(l===9){p=A.uX(a.y) o=a.z return o.length>0?p+("<"+A.pY(o,b)+">"):p}if(l===11)return A.uR(a,b) if(l===12)return A.pP(a,b,null) if(l===13)return A.pP(a.y,b,a.z) if(l===14){n=a.y m=b.length n=m-1-n if(!(n>=0&&n0)p+="<"+A.eO(c)+">" s=a.eC.get(p) if(s!=null)return s r=new A.b2(null,null) r.x=9 r.y=b r.z=c if(c.length>0)r.c=c[0] r.at=p q=A.bI(a,r) a.eC.set(p,q) return q}, o_(a,b,c){var s,r,q,p,o,n if(b.x===10){s=b.y r=b.z.concat(c)}else{r=c s=b}q=s.at+(";<"+A.eO(r)+">") p=a.eC.get(q) if(p!=null)return p o=new A.b2(null,null) o.x=10 o.y=s o.z=r o.at=q n=A.bI(a,o) a.eC.set(q,n) return n}, tZ(a,b,c){var s,r,q="+"+(b+"("+A.eO(c)+")"),p=a.eC.get(q) if(p!=null)return p s=new A.b2(null,null) s.x=11 s.y=b s.z=c s.at=q r=A.bI(a,s) a.eC.set(q,r) return r}, pq(a,b,c){var s,r,q,p,o,n=b.at,m=c.a,l=m.length,k=c.b,j=k.length,i=c.c,h=i.length,g="("+A.eO(m) if(j>0){s=l>0?",":"" g+=s+"["+A.eO(k)+"]"}if(h>0){s=l>0?",":"" g+=s+"{"+A.tT(i)+"}"}r=n+(g+")") q=a.eC.get(r) if(q!=null)return q p=new A.b2(null,null) p.x=12 p.y=b p.z=c p.at=r o=A.bI(a,p) a.eC.set(r,o) return o}, o0(a,b,c,d){var s,r=b.at+("<"+A.eO(c)+">"),q=a.eC.get(r) if(q!=null)return q s=A.tV(a,b,c,r,d) a.eC.set(r,s) return s}, tV(a,b,c,d,e){var s,r,q,p,o,n,m,l if(e){s=c.length r=A.mJ(s) for(q=0,p=0;p0){n=A.c6(a,b,r,0) m=A.f_(a,c,r,0) return A.o0(a,n,m,c!==m)}}l=new A.b2(null,null) l.x=13 l.y=b l.z=c l.at=d return A.bI(a,l)}, pl(a,b,c,d){return{u:a,e:b,r:c,s:[],p:0,n:d}}, pn(a){var s,r,q,p,o,n,m,l,k,j=a.r,i=a.s for(s=j.length,r=0;r=48&&q<=57)r=A.tN(r+1,q,j,i) else if((((q|32)>>>0)-97&65535)<26||q===95||q===36||q===124)r=A.pm(a,r,j,i,!1) else if(q===46)r=A.pm(a,r,j,i,!0) else{++r switch(q){case 44:break case 58:i.push(!1) break case 33:i.push(!0) break case 59:i.push(A.c4(a.u,a.e,i.pop())) break case 94:i.push(A.tY(a.u,i.pop())) break case 35:i.push(A.eQ(a.u,5,"#")) break case 64:i.push(A.eQ(a.u,2,"@")) break case 126:i.push(A.eQ(a.u,3,"~")) break case 60:i.push(a.p) a.p=i.length break case 62:p=a.u o=i.splice(a.p) A.nZ(a.u,a.e,o) a.p=i.pop() n=i.pop() if(typeof n=="string")i.push(A.eP(p,n,o)) else{m=A.c4(p,a.e,n) switch(m.x){case 12:i.push(A.o0(p,m,o,a.n)) break default:i.push(A.o_(p,m,o)) break}}break case 38:A.tO(a,i) break case 42:p=a.u i.push(A.ps(p,A.c4(p,a.e,i.pop()),a.n)) break case 63:p=a.u i.push(A.o1(p,A.c4(p,a.e,i.pop()),a.n)) break case 47:p=a.u i.push(A.pr(p,A.c4(p,a.e,i.pop()),a.n)) break case 40:i.push(-3) i.push(a.p) a.p=i.length break case 41:A.tM(a,i) break case 91:i.push(a.p) a.p=i.length break case 93:o=i.splice(a.p) A.nZ(a.u,a.e,o) a.p=i.pop() i.push(o) i.push(-1) break case 123:i.push(a.p) a.p=i.length break case 125:o=i.splice(a.p) A.tQ(a.u,a.e,o) a.p=i.pop() i.push(o) i.push(-2) break case 43:l=j.indexOf("(",r) i.push(j.substring(r,l)) i.push(-4) i.push(a.p) a.p=i.length r=l+1 break default:throw"Bad character "+q}}}k=i.pop() return A.c4(a.u,a.e,k)}, tN(a,b,c,d){var s,r,q=b-48 for(s=c.length;a=48&&r<=57))break q=q*10+(r-48)}d.push(q) return a}, pm(a,b,c,d,e){var s,r,q,p,o,n,m=b+1 for(s=c.length;m>>0)-97&65535)<26||r===95||r===36||r===124))q=r>=48&&r<=57 else q=!0 if(!q)break}}p=c.substring(b,m) if(e){s=a.u o=a.e if(o.x===10)o=o.y n=A.u3(s,o.y)[p] if(n==null)A.J('No "'+p+'" in "'+A.rY(o)+'"') d.push(A.mF(s,o,n))}else d.push(p) return m}, tM(a,b){var s,r,q,p,o,n=null,m=a.u,l=b.pop() if(typeof l=="number")switch(l){case-1:s=b.pop() r=n break case-2:r=b.pop() s=n break default:b.push(l) r=n s=r break}else{b.push(l) r=n s=r}q=A.tL(a,b) l=b.pop() switch(l){case-3:l=b.pop() if(s==null)s=m.sEA if(r==null)r=m.sEA p=A.c4(m,a.e,l) o=new A.i4() o.a=q o.b=s o.c=r b.push(A.pq(m,p,o)) return case-4:b.push(A.tZ(m,b.pop(),q)) return default:throw A.b(A.f7("Unexpected state under `()`: "+A.q(l)))}}, tO(a,b){var s=b.pop() if(0===s){b.push(A.eQ(a.u,1,"0&")) return}if(1===s){b.push(A.eQ(a.u,4,"1&")) return}throw A.b(A.f7("Unexpected extended operation "+A.q(s)))}, tL(a,b){var s=b.splice(a.p) A.nZ(a.u,a.e,s) a.p=b.pop() return s}, c4(a,b,c){if(typeof c=="string")return A.eP(a,c,a.sEA) else if(typeof c=="number"){b.toString return A.tP(a,b,c)}else return c}, nZ(a,b,c){var s,r=c.length for(s=0;sn)return!1 m=n-o l=s.b k=r.b j=l.length i=k.length if(o+j=d)return!1 a1=f[b] b+=3 if(a00?new Array(q):v.typeUniverse.sEA for(o=0;o0?new Array(a):v.typeUniverse.sEA}, b2:function b2(a,b){var _=this _.a=a _.b=b _.w=_.r=_.c=null _.x=0 _.at=_.as=_.Q=_.z=_.y=null}, i4:function i4(){this.c=this.b=this.a=null}, iR:function iR(a){this.a=a}, i_:function i_(){}, eN:function eN(a){this.a=a}, tv(){var s,r,q={} if(self.scheduleImmediate!=null)return A.v0() if(self.MutationObserver!=null&&self.document!=null){s=self.document.createElement("div") r=self.document.createElement("span") q.a=null new self.MutationObserver(A.c8(new A.lz(q),1)).observe(s,{childList:true}) return new A.ly(q,s,r)}else if(self.setImmediate!=null)return A.v1() return A.v2()}, tw(a){self.scheduleImmediate(A.c8(new A.lA(t.M.a(a)),0))}, tx(a){self.setImmediate(A.c8(new A.lB(t.M.a(a)),0))}, ty(a){A.p6(B.u,t.M.a(a))}, p6(a,b){return A.tR(0,b)}, tR(a,b){var s=new A.mD(!0) s.eC(a,b) return s}, B(a){return new A.eh(new A.E($.D,a.h("E<0>")),a.h("eh<0>"))}, A(a,b){a.$2(0,null) b.b=!0 return b.a}, p(a,b){A.ul(a,b)}, z(a,b){b.a0(0,a)}, y(a,b){b.bA(A.M(a),A.a_(a))}, ul(a,b){var s,r,q=new A.mM(b),p=new A.mN(b) if(a instanceof A.E)a.dE(q,p,t.z) else{s=t.z if(t.c.b(a))a.bP(q,p,s) else{r=new A.E($.D,t.g) r.a=8 r.c=a r.dE(q,p,s)}}}, C(a){var s=function(b,c){return function(d,e){while(true)try{b(d,e) break}catch(r){e=r d=c}}}(a,1) return $.D.cN(new A.n2(s),t.H,t.S,t.z)}, wt(a){return new A.db(a,1)}, tI(){return B.am}, tJ(a){return new A.db(a,3)}, uO(a,b){return new A.eK(a,b.h("eK<0>"))}, jh(a,b){var s=A.c7(a,"error",t.K) return new A.dt(s,b==null?A.f8(a):b)}, f8(a){var s if(t.W.b(a)){s=a.gaW() if(s!=null)return s}return B.T}, rl(a,b){var s=new A.E($.D,b.h("E<0>")) A.tn(B.u,new A.jI(s,a)) return s}, oG(a,b){var s,r,q,p,o,n,m,l try{s=a.$0() if(b.h("H<0>").b(s))return s else{n=b.a(s) m=new A.E($.D,b.h("E<0>")) m.a=8 m.c=n return m}}catch(l){r=A.M(l) q=A.a_(l) n=$.D p=new A.E(n,b.h("E<0>")) o=n.b7(r,q) if(o!=null)p.aC(o.a,o.b) else p.aC(r,q) return p}}, oH(a,b){var s,r b.a(a) s=a r=new A.E($.D,b.h("E<0>")) r.bk(s) return r}, dE(a,b,c){var s,r A.c7(a,"error",t.K) s=$.D if(s!==B.d){r=s.b7(a,b) if(r!=null){a=r.a b=r.b}}if(b==null)b=A.f8(a) s=new A.E($.D,c.h("E<0>")) s.aC(a,b) return s}, nv(a,b){var s,r,q,p,o,n,m,l,k,j,i={},h=null,g=!1,f=new A.E($.D,b.h("E>")) i.a=null i.b=0 s=A.el("error") r=A.el("stackTrace") q=new A.jK(i,h,g,f,s,r) try{for(l=J.an(a),k=t.P;l.p();){p=l.gu(l) o=i.b p.bP(new A.jJ(i,o,f,h,g,s,r,b),q,k);++i.b}l=i.b if(l===0){l=f l.b0(A.t([],b.h("O<0>"))) return l}i.a=A.jX(l,null,!1,b.h("0?"))}catch(j){n=A.M(j) m=A.a_(j) if(i.b===0||A.aK(g))return A.dE(n,m,b.h("m<0>")) else{s.b=n r.b=m}}return f}, pK(a,b,c){var s=$.D.b7(b,c) if(s!=null){b=s.a c=s.b}else if(c==null)c=A.f8(b) a.V(b,c)}, lU(a,b){var s,r,q for(s=t.g;r=a.a,(r&4)!==0;)a=s.a(a.c) if((r&24)!==0){q=b.br() b.c2(a) A.da(b,q)}else{q=t.e.a(b.c) b.a=b.a&1|4 b.c=a a.dt(q)}}, da(a,a0){var s,r,q,p,o,n,m,l,k,j,i,h,g,f,e,d,c={},b=c.a=a for(s=t.n,r=t.e,q=t.c;!0;){p={} o=b.a n=(o&16)===0 m=!n if(a0==null){if(m&&(o&1)===0){l=s.a(b.c) b.b.dU(l.a,l.b)}return}p.a=a0 k=a0.a for(b=a0;k!=null;b=k,k=j){b.a=null A.da(c.a,b) p.a=k j=k.a}o=c.a i=o.c p.b=m p.c=i if(n){h=b.c h=(h&1)!==0||(h&15)===8}else h=!0 if(h){g=b.b.b if(m){b=o.b b=!(b===g||b.gaI()===g.gaI())}else b=!1 if(b){b=c.a l=s.a(b.c) b.b.dU(l.a,l.b) return}f=$.D if(f!==g)$.D=g else f=null b=p.a.c if((b&15)===8)new A.m1(p,c,m).$0() else if(n){if((b&1)!==0)new A.m0(p,i).$0()}else if((b&2)!==0)new A.m_(c,p).$0() if(f!=null)$.D=f b=p.c if(q.b(b)){o=p.a.$ti o=o.h("H<2>").b(b)||!o.z[1].b(b)}else o=!1 if(o){q.a(b) e=p.a.b if((b.a&24)!==0){d=r.a(e.c) e.c=null a0=e.bt(d) e.a=b.a&30|e.a&1 e.c=b.c c.a=b continue}else A.lU(b,e) return}}e=p.a.b d=r.a(e.c) e.c=null a0=e.bt(d) b=p.b o=p.c if(!b){e.$ti.c.a(o) e.a=8 e.c=o}else{s.a(o) e.a=e.a&1|16 e.c=o}c.a=e b=e}}, uS(a,b){if(t.Q.b(a))return b.cN(a,t.z,t.K,t.l) if(t.v.b(a))return b.bN(a,t.z,t.K) throw A.b(A.bq(a,"onError",u.c))}, uP(){var s,r for(s=$.dm;s!=null;s=$.dm){$.eY=null r=s.b $.dm=r if(r==null)$.eX=null s.a.$0()}}, uU(){$.oa=!0 try{A.uP()}finally{$.eY=null $.oa=!1 if($.dm!=null)$.om().$1(A.q7())}}, q_(a){var s=new A.hP(a),r=$.eX if(r==null){$.dm=$.eX=s if(!$.oa)$.om().$1(A.q7())}else $.eX=r.b=s}, uT(a){var s,r,q,p=$.dm if(p==null){A.q_(a) $.eY=$.eX return}s=new A.hP(a) r=$.eY if(r==null){s.b=p $.dm=$.eY=s}else{q=r.b s.b=q $.eY=r.b=s if(q==null)$.eX=s}}, qm(a){var s,r=null,q=$.D if(B.d===q){A.n0(r,r,B.d,a) return}if(B.d===q.gfi().a)s=B.d.gaI()===q.gaI() else s=!1 if(s){A.n0(r,r,q,q.cO(a,t.H)) return}s=$.D s.aB(s.cr(a))}, w3(a,b){return new A.iD(A.c7(a,"stream",t.K),b.h("iD<0>"))}, oc(a){return}, pi(a,b,c){var s=b==null?A.v3():b return a.bN(s,t.H,c)}, tG(a,b){if(t.b9.b(b))return a.cN(b,t.z,t.K,t.l) if(t.i6.b(b))return a.bN(b,t.z,t.K) throw A.b(A.ao("handleError callback must take either an Object (the error), or both an Object (the error) and a StackTrace.",null))}, uQ(a){}, un(a,b,c){var s=a.Y(0),r=$.f2() if(s!==r)s.aS(new A.mO(b,c)) else b.b_(c)}, tn(a,b){var s=$.D if(s===B.d)return s.dQ(a,b) return s.dQ(a,s.cr(b))}, mZ(a,b){A.uT(new A.n_(a,b))}, pV(a,b,c,d,e){var s,r t.J.a(a) t.r.a(b) t.x.a(c) e.h("0()").a(d) r=$.D if(r===c)return d.$0() $.D=c s=r try{r=d.$0() return r}finally{$.D=s}}, pX(a,b,c,d,e,f,g){var s,r t.J.a(a) t.r.a(b) t.x.a(c) f.h("@<0>").q(g).h("1(2)").a(d) g.a(e) r=$.D if(r===c)return d.$1(e) $.D=c s=r try{r=d.$1(e) return r}finally{$.D=s}}, pW(a,b,c,d,e,f,g,h,i){var s,r t.J.a(a) t.r.a(b) t.x.a(c) g.h("@<0>").q(h).q(i).h("1(2,3)").a(d) h.a(e) i.a(f) r=$.D if(r===c)return d.$2(e,f) $.D=c s=r try{r=d.$2(e,f) return r}finally{$.D=s}}, n0(a,b,c,d){var s,r t.M.a(d) if(B.d!==c){s=B.d.gaI() r=c.gaI() d=s!==r?c.cr(d):c.fD(d,t.H)}A.q_(d)}, lz:function lz(a){this.a=a}, ly:function ly(a,b,c){this.a=a this.b=b this.c=c}, lA:function lA(a){this.a=a}, lB:function lB(a){this.a=a}, mD:function mD(a){this.a=a this.b=null this.c=0}, mE:function mE(a,b){this.a=a this.b=b}, eh:function eh(a,b){this.a=a this.b=!1 this.$ti=b}, mM:function mM(a){this.a=a}, mN:function mN(a){this.a=a}, n2:function n2(a){this.a=a}, db:function db(a,b){this.a=a this.b=b}, de:function de(a,b){var _=this _.a=a _.d=_.c=_.b=null _.$ti=b}, eK:function eK(a,b){this.a=a this.$ti=b}, dt:function dt(a,b){this.a=a this.b=b}, jI:function jI(a,b){this.a=a this.b=b}, jK:function jK(a,b,c,d,e,f){var _=this _.a=a _.b=b _.c=c _.d=d _.e=e _.f=f}, jJ:function jJ(a,b,c,d,e,f,g,h){var _=this _.a=a _.b=b _.c=c _.d=d _.e=e _.f=f _.r=g _.w=h}, cs:function cs(){}, cr:function cr(a,b){this.a=a this.$ti=b}, aa:function aa(a,b){this.a=a this.$ti=b}, bH:function bH(a,b,c,d,e){var _=this _.a=null _.b=a _.c=b _.d=c _.e=d _.$ti=e}, E:function E(a,b){var _=this _.a=0 _.b=a _.c=null _.$ti=b}, lR:function lR(a,b){this.a=a this.b=b}, lZ:function lZ(a,b){this.a=a this.b=b}, lV:function lV(a){this.a=a}, lW:function lW(a){this.a=a}, lX:function lX(a,b,c){this.a=a this.b=b this.c=c}, lT:function lT(a,b){this.a=a this.b=b}, lY:function lY(a,b){this.a=a this.b=b}, lS:function lS(a,b,c){this.a=a this.b=b this.c=c}, m1:function m1(a,b,c){this.a=a this.b=b this.c=c}, m2:function m2(a){this.a=a}, m0:function m0(a,b){this.a=a this.b=b}, m_:function m_(a,b){this.a=a this.b=b}, hP:function hP(a){this.a=a this.b=null}, aV:function aV(){}, l9:function l9(a,b){this.a=a this.b=b}, la:function la(a,b){this.a=a this.b=b}, l7:function l7(a){this.a=a}, l8:function l8(a,b,c){this.a=a this.b=b this.c=c}, bm:function bm(){}, hm:function hm(){}, dd:function dd(){}, mz:function mz(a){this.a=a}, my:function my(a){this.a=a}, iK:function iK(){}, df:function df(a,b,c,d,e){var _=this _.a=null _.b=0 _.c=null _.d=a _.e=b _.f=c _.r=d _.$ti=e}, d5:function d5(a,b){this.a=a this.$ti=b}, d6:function d6(a,b,c,d,e,f,g){var _=this _.w=a _.a=b _.b=c _.c=d _.d=e _.e=f _.r=_.f=null _.$ti=g}, ej:function ej(){}, lH:function lH(a,b,c){this.a=a this.b=b this.c=c}, lG:function lG(a){this.a=a}, eJ:function eJ(){}, bG:function bG(){}, cu:function cu(a,b){this.b=a this.a=null this.$ti=b}, en:function en(a,b){this.b=a this.c=b this.a=null}, hV:function hV(){}, b4:function b4(a){var _=this _.a=0 _.c=_.b=null _.$ti=a}, ms:function ms(a,b){this.a=a this.b=b}, iD:function iD(a,b){var _=this _.a=null _.b=a _.c=!1 _.$ti=b}, mO:function mO(a,b){this.a=a this.b=b}, iT:function iT(a,b,c){this.a=a this.b=b this.$ti=c}, eT:function eT(){}, n_:function n_(a,b){this.a=a this.b=b}, iu:function iu(){}, mw:function mw(a,b,c){this.a=a this.b=b this.c=c}, mv:function mv(a,b){this.a=a this.b=b}, mx:function mx(a,b,c){this.a=a this.b=b this.c=c}, rw(a,b,c,d,e){if(c==null)if(b==null){if(a==null)return new A.as(d.h("@<0>").q(e).h("as<1,2>")) b=A.qa()}else{if(A.v8()===b&&A.v7()===a)return new A.et(d.h("@<0>").q(e).h("et<1,2>")) if(a==null)a=A.q9()}else{if(b==null)b=A.qa() if(a==null)a=A.q9()}return A.tK(a,b,c,d,e)}, aO(a,b,c){return b.h("@<0>").q(c).h("jU<1,2>").a(A.vb(a,new A.as(b.h("@<0>").q(c).h("as<1,2>"))))}, X(a,b){return new A.as(a.h("@<0>").q(b).h("as<1,2>"))}, tK(a,b,c,d,e){var s=c!=null?c:new A.mq(d) return new A.er(a,b,s,d.h("@<0>").q(e).h("er<1,2>"))}, rx(a){return new A.es(a.h("es<0>"))}, nY(){var s=Object.create(null) s[""]=s delete s[""] return s}, pk(a,b,c){var s=new A.cw(a,b,c.h("cw<0>")) s.c=a.e return s}, ut(a,b){return J.a7(a,b)}, uu(a){return J.ax(a)}, rp(a,b,c){var s,r if(A.ob(a)){if(b==="("&&c===")")return"(...)" return b+"..."+c}s=A.t([],t.s) B.b.m($.aY,a) try{A.uN(a,s)}finally{if(0>=$.aY.length)return A.d($.aY,-1) $.aY.pop()}r=A.lb(b,t.e7.a(s),", ")+c return r.charCodeAt(0)==0?r:r}, nw(a,b,c){var s,r if(A.ob(a))return b+"..."+c s=new A.ah(b) B.b.m($.aY,a) try{r=s r.a=A.lb(r.a,a,", ")}finally{if(0>=$.aY.length)return A.d($.aY,-1) $.aY.pop()}s.a+=c r=s.a return r.charCodeAt(0)==0?r:r}, ob(a){var s,r for(s=$.aY.length,r=0;r=b.length)return A.d(b,-1) r=b.pop() if(0>=b.length)return A.d(b,-1) q=b.pop()}else{p=l.gu(l);++j if(!l.p()){if(j<=4){B.b.m(b,A.q(p)) return}r=A.q(p) if(0>=b.length)return A.d(b,-1) q=b.pop() k+=r.length+2}else{o=l.gu(l);++j for(;l.p();p=o,o=n){n=l.gu(l);++j if(j>100){while(!0){if(!(k>75&&j>3))break if(0>=b.length)return A.d(b,-1) k-=b.pop().length+2;--j}B.b.m(b,"...") return}}q=A.q(p) r=A.q(o) k+=r.length+q.length+4}}if(j>b.length+2){k+=5 m="..."}else m=null while(!0){if(!(k>80&&b.length>3))break if(0>=b.length)return A.d(b,-1) k-=b.pop().length+2 if(m==null){k+=5 m="..."}}if(m!=null)B.b.m(b,m) B.b.m(b,q) B.b.m(b,r)}, nB(a,b,c){var s=A.rw(null,null,null,b,c) J.bo(a,new A.jW(s,b,c)) return s}, jZ(a){var s,r={} if(A.ob(a))return"{...}" s=new A.ah("") try{B.b.m($.aY,a) s.a+="{" r.a=!0 J.bo(a,new A.k_(r,s)) s.a+="}"}finally{if(0>=$.aY.length)return A.d($.aY,-1) $.aY.pop()}r=s.a return r.charCodeAt(0)==0?r:r}, et:function et(a){var _=this _.a=0 _.f=_.e=_.d=_.c=_.b=null _.r=0 _.$ti=a}, er:function er(a,b,c,d){var _=this _.w=a _.x=b _.y=c _.a=0 _.f=_.e=_.d=_.c=_.b=null _.r=0 _.$ti=d}, mq:function mq(a){this.a=a}, es:function es(a){var _=this _.a=0 _.f=_.e=_.d=_.c=_.b=null _.r=0 _.$ti=a}, ib:function ib(a){this.a=a this.c=this.b=null}, cw:function cw(a,b,c){var _=this _.a=a _.b=b _.d=_.c=null _.$ti=c}, dH:function dH(){}, jW:function jW(a,b,c){this.a=a this.b=b this.c=c}, cR:function cR(a){var _=this _.b=_.a=0 _.c=null _.$ti=a}, eu:function eu(a,b,c,d){var _=this _.a=a _.b=b _.c=null _.d=c _.e=!1 _.$ti=d}, ae:function ae(){}, dN:function dN(){}, h:function h(){}, dP:function dP(){}, k_:function k_(a,b){this.a=a this.b=b}, w:function w(){}, k0:function k0(a){this.a=a}, d3:function d3(){}, ew:function ew(a,b){this.a=a this.$ti=b}, ex:function ex(a,b,c){var _=this _.a=a _.b=b _.c=null _.$ti=c}, c5:function c5(){}, cS:function cS(){}, ed:function ed(){}, e1:function e1(){}, eE:function eE(){}, ev:function ev(){}, dh:function dh(){}, eV:function eV(){}, tt(a,b,c,d){var s,r if(b instanceof Uint8Array){s=b if(d==null)d=s.length if(d-c<15)return null r=A.tu(a,s,c,d) if(r!=null&&a)if(r.indexOf("\ufffd")>=0)return null return r}return null}, tu(a,b,c,d){var s=a?$.qD():$.qC() if(s==null)return null if(0===c&&d===b.length)return A.pa(s,b) return A.pa(s,b.subarray(c,A.by(c,d,b.length)))}, pa(a,b){var s,r try{s=a.decode(b) return s}catch(r){}return null}, ow(a,b,c,d,e,f){if(B.c.ab(f,4)!==0)throw A.b(A.ad("Invalid base64 padding, padded length must be multiple of four, is "+f,a,c)) if(d+e!==f)throw A.b(A.ad("Invalid base64 padding, '=' not at the end",a,b)) if(e>2)throw A.b(A.ad("Invalid base64 padding, more than two '=' characters",a,b))}, uf(a){switch(a){case 65:return"Missing extension byte" case 67:return"Unexpected extension byte" case 69:return"Invalid UTF-8 byte" case 71:return"Overlong encoding" case 73:return"Out of unicode range" case 75:return"Encoded surrogate" case 77:return"Unfinished UTF-8 octet sequence" default:return""}}, ue(a,b,c){var s,r,q,p=c-b,o=new Uint8Array(p) for(s=J.T(a),r=0;r>>0!==0)q=255 if(!(r")) for(s=J.an(a);s.p();)B.b.m(r,c.a(s.gu(s))) if(b)return r return J.jP(r,c)}, fM(a,b,c){var s if(b)return A.oO(a,c) s=J.jP(A.oO(a,c),c) return s}, oO(a,b){var s,r if(Array.isArray(a))return A.t(a.slice(0),b.h("O<0>")) s=A.t([],b.h("O<0>")) for(r=J.an(a);r.p();)B.b.m(s,r.gu(r)) return s}, fN(a,b){return J.oK(A.jY(a,!1,b))}, p5(a,b,c){if(t.hD.b(a))return A.rT(a,b,A.by(b,c,a.length)) return A.tl(a,b,c)}, tk(a){return A.bx(a)}, tl(a,b,c){var s,r,q,p,o,n=null if(b<0)throw A.b(A.a1(b,0,a.length,n,n)) s=c==null if(!s&&c")) for(p=0;p=16)return null r=r*16+o}n=h-1 if(!(h>=0&&h=16)return null r=r*16+o}m=n-1 if(!(n>=0&&n=j)return A.d(i,0) l=i[0]===0}else l=!1 if(l)return $.bN() l=A.b3(j,i) return new A.a8(l===0?!1:c,i,l)}, nX(a,b){var s,r,q,p,o,n if(a==="")return null s=$.qF().fS(a) if(s==null)return null r=s.b q=r.length if(1>=q)return A.d(r,1) p=r[1]==="-" if(4>=q)return A.d(r,4) o=r[4] n=r[3] if(5>=q)return A.d(r,5) if(o!=null)return A.tC(o,p) if(n!=null)return A.tD(n,2,p) return null}, b3(a,b){var s,r=b.length while(!0){if(a>0){s=a-1 if(!(s=0&&q=0;--s){p=s+c if(!(s=0&&p=0;--s){if(!(s=0;--s){if(!(s=0&&n>>0 p=B.c.aU((o&i)>>>0,k)}if(!(l>=0&&l=0;){if(!(q=0&&p=0&&m>>0,k) if(!(p>>0 s=B.c.aV(n,l)}if(!(r>=0&&r=0;--s){if(!(s=0&&o=0&&b=0&&o=0;e=m,c=p){p=c+1 if(!(c=0&&e=0&&e=0&&c=0&&r>>0,a) if(q>65535)return 65535 return q}, re(a){var s=Math.abs(a),r=a<0?"-":"" if(s>=1000)return""+a if(s>=100)return r+"0"+s if(s>=10)return r+"00"+s return r+"000"+s}, rf(a){if(a>=100)return""+a if(a>=10)return"0"+a return"00"+a}, fs(a){if(a>=10)return""+a return"0"+a}, cg(a){if(typeof a=="number"||A.cz(a)||a==null)return J.bp(a) if(typeof a=="string")return JSON.stringify(a) return A.rg(a)}, f7(a){return new A.ds(a)}, ao(a,b){return new A.bh(!1,null,b,a)}, bq(a,b,c){return new A.bh(!0,a,b,c)}, jg(a,b,c){return a}, rV(a){var s=null return new A.cX(s,s,!1,s,s,a)}, oS(a,b){return new A.cX(null,null,!0,a,b,"Value not in range")}, a1(a,b,c,d,e){return new A.cX(b,c,!0,a,d,"Invalid value")}, by(a,b,c){if(0>a||a>c)throw A.b(A.a1(a,0,c,"start",null)) if(b!=null){if(a>b||b>c)throw A.b(A.a1(b,a,c,"end",null)) return b}return c}, aT(a,b){if(a<0)throw A.b(A.a1(a,0,null,b,null)) return a}, V(a,b,c,d,e){return new A.fE(b,!0,a,e,"Index out of range")}, x(a){return new A.hz(a)}, hw(a){return new A.hv(a)}, K(a){return new A.bB(a)}, ap(a){return new A.fj(a)}, oF(a){return new A.i0(a)}, ad(a,b,c){return new A.fC(a,b,c)}, rA(a,b,c,d,e){return new A.dv(a,b.h("@<0>").q(c).q(d).q(e).h("dv<1,2,3,4>"))}, oP(a,b,c,d){var s,r if(B.x===c){s=J.ax(a) b=J.ax(b) return A.nO(A.bZ(A.bZ($.no(),s),b))}if(B.x===d){s=J.ax(a) b=J.ax(b) c=J.ax(c) return A.nO(A.bZ(A.bZ(A.bZ($.no(),s),b),c))}s=J.ax(a) b=J.ax(b) c=J.ax(c) d=J.ax(d) r=$.no() return A.nO(A.bZ(A.bZ(A.bZ(A.bZ(r,s),b),c),d))}, b9(a){var s=$.qk if(s==null)A.qj(a) else s.$1(a)}, li(a5){var s,r,q,p,o,n,m,l,k,j,i,h,g,f,e,d,c,b,a,a0,a1,a2,a3=null,a4=a5.length if(a4>=5){s=((B.a.t(a5,4)^58)*3|B.a.t(a5,0)^100|B.a.t(a5,1)^97|B.a.t(a5,2)^116|B.a.t(a5,3)^97)>>>0 if(s===0)return A.p8(a4=14)B.b.k(r,7,a4) q=r[1] if(q>=0)if(A.pZ(a5,0,q,20,r)===20)r[7]=q p=r[2]+1 o=r[3] n=r[4] m=r[5] l=r[6] if(lq+3){j=a3 k=!1}else{i=o>0 if(i&&o+1===n){j=a3 k=!1}else{if(!B.a.H(a5,"\\",n))if(p>0)h=B.a.H(a5,"\\",p-1)||B.a.H(a5,"\\",p-2) else h=!1 else h=!0 if(h){j=a3 k=!1}else{if(!(mn+2&&B.a.H(a5,"/..",m-3) else h=!0 if(h){j=a3 k=!1}else{if(q===4)if(B.a.H(a5,"file",0)){if(p<=0){if(!B.a.H(a5,"/",n)){g="file:///" s=3}else{g="file://" s=2}a5=g+B.a.n(a5,n,a4) q-=0 i=s-0 m+=i l+=i a4=a5.length p=7 o=7 n=7}else if(n===m){++l f=m+1 a5=B.a.az(a5,n,m,"/");++a4 m=f}j="file"}else if(B.a.H(a5,"http",0)){if(i&&o+3===n&&B.a.H(a5,"80",o+1)){l-=3 e=n-3 m-=3 a5=B.a.az(a5,o,n,"") a4-=3 n=e}j="http"}else j=a3 else if(q===5&&B.a.H(a5,"https",0)){if(i&&o+4===n&&B.a.H(a5,"443",o+1)){l-=4 e=n-4 m-=4 a5=B.a.az(a5,o,n,"") a4-=3 n=e}j="https"}else j=a3 k=!0}}}}else j=a3 if(k){if(a40)j=A.u9(a5,0,q) else{if(q===0)A.di(a5,0,"Invalid empty scheme") j=""}if(p>0){d=q+3 c=d9)k.$2("invalid character",s)}else{if(q===3)k.$2(m,s) o=A.nd(B.a.n(a,r,s),null) if(o>255)k.$2(l,r) n=q+1 if(!(q<4))return A.d(j,q) j[q]=o r=s+1 q=n}}if(q!==3)k.$2(m,c) o=A.nd(B.a.n(a,r,c),null) if(o>255)k.$2(l,r) if(!(q<4))return A.d(j,q) j[q]=o return j}, p9(a,a0,a1){var s,r,q,p,o,n,m,l,k,j,i,h,g,f,e,d=null,c=new A.lj(a),b=new A.lk(c,a) if(a.length<2)c.$2("address is too short",d) s=A.t([],t.t) for(r=a0,q=r,p=!1,o=!1;r>>0) B.b.m(s,(k[2]<<8|k[3])>>>0)}if(p){if(s.length>7)c.$2("an address with a wildcard must have less than 7 parts",d)}else if(s.length!==8)c.$2("an address without a wildcard must contain exactly 8 parts",d) j=new Uint8Array(16) for(l=s.length,i=9-l,r=0,h=0;r=0&&h<16))return A.d(j,h) j[h]=0 e=h+1 if(!(e<16))return A.d(j,e) j[e]=0 h+=2}else{e=B.c.M(g,8) if(!(h>=0&&h<16))return A.d(j,h) j[h]=e e=h+1 if(!(e<16))return A.d(j,e) j[e]=g&255 h+=2}}return j}, mG(a,b,c,d,e,f,g){return new A.eR(a,b,c,d,e,f,g)}, pu(a){if(a==="http")return 80 if(a==="https")return 443 return 0}, di(a,b,c){throw A.b(A.ad(c,a,b))}, u5(a,b){var s,r,q for(s=a.length,r=0;r")),r=r.h("a3.E");s.p();){q=s.d if(q==null)q=r.a(q) if(B.a.S(q,A.b1('["*/:<>?\\\\|]',!0))){s=A.x("Illegal character in path: "+q) throw A.b(s)}}}, u6(a,b){var s if(!(65<=a&&a<=90))s=97<=a&&a<=122 else s=!0 if(s)return s=A.x("Illegal drive letter "+A.tk(a)) throw A.b(s)}, o3(a,b){if(a!=null&&a===A.pu(b))return null return a}, py(a,b,c,d){var s,r,q,p,o,n if(a==null)return null if(b===c)return"" if(B.a.B(a,b)===91){s=c-1 if(B.a.B(a,s)!==93)A.di(a,b,"Missing end `]` to match `[` in host") r=b+1 q=A.u7(a,r,s) if(q=b&&q=b&&s>>4 if(!(n<8))return A.d(B.o,n) n=(B.o[n]&1<<(p&15))!==0}else n=!1 if(n){if(q&&65<=p&&90>=p){if(i==null)i=new A.ah("") if(r>>4 if(!(m<8))return A.d(B.A,m) m=(B.A[m]&1<<(o&15))!==0}else m=!1 if(m){if(p&&65<=o&&90>=o){if(q==null)q=new A.ah("") if(r>>4 if(!(m<8))return A.d(B.j,m) m=(B.j[m]&1<<(o&15))!==0}else m=!1 if(m)A.di(a,s,"Invalid character") else{if((o&64512)===55296&&s+1>>4 if(!(p<8))return A.d(B.l,p) p=(B.l[p]&1<<(q&15))!==0}else p=!1 if(!p)A.di(a,s,"Illegal scheme character") if(65<=q&&q<=90)r=!0}a=B.a.n(a,b,c) return A.u4(r?a.toLowerCase():a)}, u4(a){if(a==="http")return"http" if(a==="file")return"file" if(a==="https")return"https" if(a==="package")return"package" return a}, pB(a,b,c){if(a==null)return"" return A.eS(a,b,c,B.a0,!1,!1)}, pz(a,b,c,d,e,f){var s=e==="file",r=s||f,q=A.eS(a,b,c,B.B,!0,!0) if(q.length===0){if(s)return"/"}else if(r&&!B.a.J(q,"/"))q="/"+q return A.ua(q,e,f)}, ua(a,b,c){var s=b.length===0 if(s&&!c&&!B.a.J(a,"/")&&!B.a.J(a,"\\"))return A.o5(a,!s||c) return A.bJ(a)}, pA(a,b,c,d){if(a!=null)return A.eS(a,b,c,B.k,!0,!1) return null}, px(a,b,c){if(a==null)return null return A.eS(a,b,c,B.k,!0,!1)}, o4(a,b,c){var s,r,q,p,o,n=b+2 if(n>=a.length)return"%" s=B.a.B(a,b+1) r=B.a.B(a,n) q=A.n9(s) p=A.n9(r) if(q<0||p<0)return"%" o=q*16+p if(o<127){n=B.c.M(o,4) if(!(n<8))return A.d(B.o,n) n=(B.o[n]&1<<(o&15))!==0}else n=!1 if(n)return A.bx(c&&65<=o&&90>=o?(o|32)>>>0:o) if(s>=97||r>=97)return B.a.n(a,b,b+3).toUpperCase() return null}, o2(a){var s,r,q,p,o,n,m,l,k="0123456789ABCDEF" if(a<128){s=new Uint8Array(3) s[0]=37 s[1]=B.a.t(k,a>>>4) s[2]=B.a.t(k,a&15)}else{if(a>2047)if(a>65535){r=240 q=4}else{r=224 q=3}else{r=192 q=2}p=3*q s=new Uint8Array(p) for(o=0;--q,q>=0;r=128){n=B.c.fn(a,6*q)&63|r if(!(o>>4) if(!(m>>4 if(!(n<8))return A.d(d,n) n=(d[n]&1<<(o&15))!==0}else n=!1 if(n)++r else{if(o===37){m=A.o4(a,r,!1) if(m==null){r+=3 continue}if("%"===m){m="%25" l=1}else l=3}else if(o===92&&f){m="/" l=1}else{if(s)if(o<=93){n=o>>>4 if(!(n<8))return A.d(B.j,n) n=(B.j[n]&1<<(o&15))!==0}else n=!1 else n=!1 if(n){A.di(a,r,"Invalid character") l=i m=l}else{if((o&64512)===55296){n=r+1 if(n=m)return A.d(s,-1) s.pop() if(s.length===0)B.b.m(s,"")}p=!0}else if("."===n)p=!0 else{B.b.m(s,n) p=!1}}if(p)B.b.m(s,"") return B.b.au(s,"/")}, o5(a,b){var s,r,q,p,o,n if(!A.pC(a))return!b?A.pv(a):a s=A.t([],t.s) for(r=a.split("/"),q=r.length,p=!1,o=0;o=s.length)return A.d(s,-1) s.pop() p=!0}else{B.b.m(s,"..") p=!1}else if("."===n)p=!0 else{B.b.m(s,n) p=!1}}r=s.length if(r!==0)if(r===1){if(0>=r)return A.d(s,0) r=s[0].length===0}else r=!1 else r=!0 if(r)return"./" if(p||B.b.gai(s)==="..")B.b.m(s,"") if(!b){if(0>=s.length)return A.d(s,0) B.b.k(s,0,A.pv(s[0]))}return B.b.au(s,"/")}, pv(a){var s,r,q,p=a.length if(p>=2&&A.pw(B.a.t(a,0)))for(s=1;s>>4 if(!(q<8))return A.d(B.l,q) q=(B.l[q]&1<<(r&15))===0}else q=!0 if(q)break}return a}, uc(a,b){if(a.h6("package")&&a.c==null)return A.q0(b,0,b.length) return-1}, pF(a){var s,r,q,p=a.gcJ(),o=p.length if(o>0&&J.Y(p[0])===2&&J.os(p[0],1)===58){if(0>=o)return A.d(p,0) A.u6(J.os(p[0],0),!1) A.pt(p,!1,1) s=!0}else{A.pt(p,!1,0) s=!1}r=a.gbG()&&!s?""+"\\":"" if(a.gb9()){q=a.gah(a) if(q.length!==0)r=r+"\\"+q+"\\"}r=A.lb(r,p,"\\") o=s&&o===1?r+"\\":r return o.charCodeAt(0)==0?o:o}, u8(a,b){var s,r,q for(s=0,r=0;r<2;++r){q=B.a.t(a,b+r) if(48<=q&&q<=57)s=s*16+q-48 else{q|=32 if(97<=q&&q<=102)s=s*16+q-87 else throw A.b(A.ao("Invalid URL encoding",null))}}return s}, ud(a,b,c,d,e){var s,r,q,p,o=b while(!0){if(!(o127)throw A.b(A.ao("Illegal percent encoding in URI",null)) if(r===37){if(o+3>q)throw A.b(A.ao("Truncated URI",null)) B.b.m(p,A.u8(a,o+1)) o+=2}else B.b.m(p,r)}}return d.b5(0,p)}, pw(a){var s=a|32 return 97<=s&&s<=122}, p8(a,b,c){var s,r,q,p,o,n,m,l,k="Invalid MIME type",j=A.t([b-1],t.t) for(s=a.length,r=b,q=-1,p=null;rb)throw A.b(A.ad(k,a,r)) for(;p!==44;){B.b.m(j,r);++r for(o=-1;r=0)B.b.m(j,o) else{n=B.b.gai(j) if(p!==44||r!==n+7||!B.a.H(a,"base64",n+1))throw A.b(A.ad("Expecting '='",a,r)) break}}B.b.m(j,r) m=r+1 if((j.length&1)===1)a=B.I.he(0,a,m,s) else{l=A.pD(a,m,s,B.k,!0,!1) if(l!=null)a=B.a.az(a,m,s,l)}return new A.lg(a,j,c)}, us(){var s,r,q,p,o,n,m="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-._~!$&'()*+,;=",l=".",k=":",j="/",i="\\",h="?",g="#",f="/\\",e=A.t(new Array(22),t.bs) for(s=0;s<22;++s)e[s]=new Uint8Array(96) r=new A.mR(e) q=new A.mS() p=new A.mT() o=t.p.a(r.$2(0,225)) q.$3(o,m,1) q.$3(o,l,14) q.$3(o,k,34) q.$3(o,j,3) q.$3(o,i,227) q.$3(o,h,172) q.$3(o,g,205) n=r.$2(14,225) q.$3(n,m,1) q.$3(n,l,15) q.$3(n,k,34) q.$3(n,f,234) q.$3(n,h,172) q.$3(n,g,205) n=r.$2(15,225) q.$3(n,m,1) q.$3(n,"%",225) q.$3(n,k,34) q.$3(n,j,9) q.$3(n,i,233) q.$3(n,h,172) q.$3(n,g,205) n=r.$2(1,225) q.$3(n,m,1) q.$3(n,k,34) q.$3(n,j,10) q.$3(n,i,234) q.$3(n,h,172) q.$3(n,g,205) n=r.$2(2,235) q.$3(n,m,139) q.$3(n,j,131) q.$3(n,i,131) q.$3(n,l,146) q.$3(n,h,172) q.$3(n,g,205) n=r.$2(3,235) q.$3(n,m,11) q.$3(n,j,68) q.$3(n,i,68) q.$3(n,l,18) q.$3(n,h,172) q.$3(n,g,205) n=r.$2(4,229) q.$3(n,m,5) p.$3(n,"AZ",229) q.$3(n,k,102) q.$3(n,"@",68) q.$3(n,"[",232) q.$3(n,j,138) q.$3(n,i,138) q.$3(n,h,172) q.$3(n,g,205) n=r.$2(5,229) q.$3(n,m,5) p.$3(n,"AZ",229) q.$3(n,k,102) q.$3(n,"@",68) q.$3(n,j,138) q.$3(n,i,138) q.$3(n,h,172) q.$3(n,g,205) n=r.$2(6,231) p.$3(n,"19",7) q.$3(n,"@",68) q.$3(n,j,138) q.$3(n,i,138) q.$3(n,h,172) q.$3(n,g,205) n=r.$2(7,231) p.$3(n,"09",7) q.$3(n,"@",68) q.$3(n,j,138) q.$3(n,i,138) q.$3(n,h,172) q.$3(n,g,205) q.$3(r.$2(8,8),"]",5) n=r.$2(9,235) q.$3(n,m,11) q.$3(n,l,16) q.$3(n,f,234) q.$3(n,h,172) q.$3(n,g,205) n=r.$2(16,235) q.$3(n,m,11) q.$3(n,l,17) q.$3(n,f,234) q.$3(n,h,172) q.$3(n,g,205) n=r.$2(17,235) q.$3(n,m,11) q.$3(n,j,9) q.$3(n,i,233) q.$3(n,h,172) q.$3(n,g,205) n=r.$2(10,235) q.$3(n,m,11) q.$3(n,l,18) q.$3(n,j,10) q.$3(n,i,234) q.$3(n,h,172) q.$3(n,g,205) n=r.$2(18,235) q.$3(n,m,11) q.$3(n,l,19) q.$3(n,f,234) q.$3(n,h,172) q.$3(n,g,205) n=r.$2(19,235) q.$3(n,m,11) q.$3(n,f,234) q.$3(n,h,172) q.$3(n,g,205) n=r.$2(11,235) q.$3(n,m,11) q.$3(n,j,10) q.$3(n,i,234) q.$3(n,h,172) q.$3(n,g,205) n=r.$2(12,236) q.$3(n,m,12) q.$3(n,h,12) q.$3(n,g,205) n=r.$2(13,237) q.$3(n,m,13) q.$3(n,h,13) p.$3(r.$2(20,245),"az",21) n=r.$2(21,245) p.$3(n,"az",21) p.$3(n,"09",21) q.$3(n,"+-.",21) return e}, pZ(a,b,c,d,e){var s,r,q,p,o=$.qK() for(s=b;s=0&&d95?31:q] d=p&31 B.b.k(e,p>>>5,s)}return d}, po(a){if(a.b===7&&B.a.J(a.a,"package")&&a.c<=0)return A.q0(a.a,a.e,a.f) return-1}, q0(a,b,c){var s,r,q for(s=b,r=0;s")) s.dG() return s}, q4(a,b){var s=$.D if(s===B.d)return a return s.dM(a,b)}, o:function o(){}, f4:function f4(){}, f5:function f5(){}, f6:function f6(){}, bR:function bR(){}, bi:function bi(){}, fm:function fm(){}, P:function P(){}, cE:function cE(){}, jx:function jx(){}, aq:function aq(){}, bb:function bb(){}, fn:function fn(){}, fo:function fo(){}, fq:function fq(){}, ft:function ft(){}, dz:function dz(){}, dA:function dA(){}, fu:function fu(){}, fv:function fv(){}, n:function n(){}, l:function l(){}, f:function f(){}, az:function az(){}, cI:function cI(){}, fz:function fz(){}, fB:function fB(){}, aA:function aA(){}, fD:function fD(){}, ci:function ci(){}, cL:function cL(){}, fO:function fO(){}, fP:function fP(){}, cV:function cV(){}, ck:function ck(){}, fQ:function fQ(){}, k2:function k2(a){this.a=a}, k3:function k3(a){this.a=a}, fR:function fR(){}, k4:function k4(a){this.a=a}, k5:function k5(a){this.a=a}, aB:function aB(){}, fS:function fS(){}, G:function G(){}, dV:function dV(){}, aC:function aC(){}, h7:function h7(){}, hb:function hb(){}, kl:function kl(a){this.a=a}, km:function km(a){this.a=a}, hd:function hd(){}, cY:function cY(){}, cZ:function cZ(){}, aD:function aD(){}, hf:function hf(){}, aE:function aE(){}, hg:function hg(){}, aF:function aF(){}, hl:function hl(){}, l5:function l5(a){this.a=a}, l6:function l6(a){this.a=a}, al:function al(){}, aH:function aH(){}, am:function am(){}, hp:function hp(){}, hq:function hq(){}, hr:function hr(){}, aI:function aI(){}, hs:function hs(){}, ht:function ht(){}, hB:function hB(){}, hD:function hD(){}, c1:function c1(){}, hS:function hS(){}, eo:function eo(){}, i5:function i5(){}, ez:function ez(){}, iA:function iA(){}, iJ:function iJ(){}, nu:function nu(a,b){this.a=a this.$ti=b}, lM:function lM(a,b,c,d){var _=this _.a=a _.b=b _.c=c _.$ti=d}, eq:function eq(a,b,c,d,e){var _=this _.a=0 _.b=a _.c=b _.d=c _.e=d _.$ti=e}, lN:function lN(a){this.a=a}, lO:function lO(a){this.a=a}, u:function u(){}, dD:function dD(a,b,c){var _=this _.a=a _.b=b _.c=-1 _.d=null _.$ti=c}, hT:function hT(){}, hW:function hW(){}, hX:function hX(){}, hY:function hY(){}, hZ:function hZ(){}, i1:function i1(){}, i2:function i2(){}, i6:function i6(){}, i7:function i7(){}, id:function id(){}, ie:function ie(){}, ig:function ig(){}, ih:function ih(){}, ii:function ii(){}, ij:function ij(){}, io:function io(){}, ip:function ip(){}, ix:function ix(){}, eF:function eF(){}, eG:function eG(){}, iy:function iy(){}, iz:function iz(){}, iC:function iC(){}, iL:function iL(){}, iM:function iM(){}, eL:function eL(){}, eM:function eM(){}, iN:function iN(){}, iO:function iO(){}, iU:function iU(){}, iV:function iV(){}, iW:function iW(){}, iX:function iX(){}, iY:function iY(){}, iZ:function iZ(){}, j_:function j_(){}, j0:function j0(){}, j1:function j1(){}, j2:function j2(){}, pM(a){var s,r,q if(a==null)return a if(typeof a=="string"||typeof a=="number"||A.cz(a))return a if(A.qh(a))return A.b7(a) s=Array.isArray(a) s.toString if(s){r=[] q=0 while(!0){s=a.length s.toString if(!(q")),q=new A.aa(r,b.h("aa<0>")),p=t.a,o=p.a(new A.mP(a,q,b)) t.Z.a(null) s=t.A A.bf(a,"success",o,!1,s) A.bf(a,"error",p.a(q.gfI()),!1,s) return r}, rE(a,b,c){var s,r=null,q=c.h("df<0>"),p=new A.df(r,r,r,r,q),o=t.a,n=o.a(p.gfz()) t.Z.a(null) s=t.A A.bf(a,"error",n,!1,s) A.bf(a,"success",o.a(new A.k7(a,p,b,c)),!1,s) return new A.d5(p,q.h("d5<1>"))}, bT:function bT(){}, br:function br(){}, bj:function bj(){}, cj:function cj(){}, mP:function mP(a,b,c){this.a=a this.b=b this.c=c}, dG:function dG(){}, dX:function dX(){}, k7:function k7(a,b,c,d){var _=this _.a=a _.b=b _.c=c _.d=d}, bz:function bz(){}, ec:function ec(){}, bD:function bD(){}, n4(a,b,c,d){return d.a(a[b].apply(a,c))}, j8(a,b){var s=new A.E($.D,b.h("E<0>")),r=new A.cr(s,b.h("cr<0>")) a.then(A.c8(new A.nj(r,b),1),A.c8(new A.nk(r),1)) return s}, nj:function nj(a,b){this.a=a this.b=b}, nk:function nk(a){this.a=a}, h0:function h0(a){this.a=a}, i8:function i8(a){this.a=a}, aN:function aN(){}, fL:function fL(){}, aR:function aR(){}, h3:function h3(){}, h8:function h8(){}, hn:function hn(){}, aW:function aW(){}, hu:function hu(){}, i9:function i9(){}, ia:function ia(){}, ik:function ik(){}, il:function il(){}, iG:function iG(){}, iH:function iH(){}, iP:function iP(){}, iQ:function iQ(){}, f9:function f9(){}, fa:function fa(){}, jp:function jp(a){this.a=a}, jq:function jq(a){this.a=a}, fb:function fb(){}, bQ:function bQ(){}, h4:function h4(){}, hQ:function hQ(){}, tq(){throw A.b(A.x("Cannot modify an unmodifiable Map"))}, h_:function h_(){}, hy:function hy(){}, q3(a,b){var s,r,q,p,o,n,m,l for(s=b.length,r=1;r=1;s=q){q=s-1 if(b[q]!=null)break}p=new A.ah("") o=""+(a+"(") p.a=o n=A.av(b) m=n.h("cn<1>") l=new A.cn(b,0,s,m) l.ey(b,0,s,n.c) m=o+new A.af(l,m.h("i(a3.E)").a(new A.n1()),m.h("af")).au(0,", ") p.a=m p.a=m+("): part "+(r-1)+" was null, but part "+r+" was not.") throw A.b(A.ao(p.l(0),null))}}, fk:function fk(a,b){this.a=a this.b=b}, jw:function jw(){}, n1:function n1(){}, bV:function bV(){}, rF(a,b){var s,r,q,p,o,n=b.eg(a) b.ar(a) if(n!=null)a=B.a.O(a,n.length) s=t.s r=A.t([],s) q=A.t([],s) s=a.length if(s!==0&&b.bI(B.a.t(a,0))){if(0>=s)return A.d(a,0) B.b.m(q,a[0]) p=1}else{B.b.m(q,"") p=0}for(o=p;o50)return B.a.n(s,0,50)+"..." return s}, uZ(a){if(t.p.b(a))return"Blob("+a.length+")" return A.ug(a)}, q6(a){var s=a.$ti return"["+new A.af(a,s.h("i?(h.E)").a(new A.n3()),s.h("af")).au(0,", ")+"]"}, n3:function n3(){}, dy:function dy(){}, e3:function e3(){}, ko:function ko(a){this.a=a}, kp:function kp(a){this.a=a}, jB:function jB(){}, ri(a){var s=J.T(a),r=s.i(a,"method"),q=s.i(a,"arguments") if(r!=null)return new A.fx(A.S(r),q) return null}, fx:function fx(a,b){this.a=a this.b=b}, cH:function cH(a,b){this.a=a this.b=b}, hh(a,b,c,d){var s=new A.bl(a,b,b,c) s.b=d return s}, bl:function bl(a,b,c,d){var _=this _.r=_.f=_.e=null _.w=a _.x=b _.b=null _.c=c _.a=d}, mX(a,b,c,d){var s,r,q,p if(a instanceof A.bl){s=a.e if(s==null)s=a.e=b r=a.f if(r==null)r=a.f=c q=a.r if(q==null)q=a.r=d p=s==null if(!p||r!=null||q!=null)if(a.x==null){r=A.X(t.N,t.X) if(!p)r.k(0,"database",s.ec()) s=a.f if(s!=null)r.k(0,"sql",s) s=a.r if(s!=null)r.k(0,"arguments",s) a.sfP(0,r)}return a}else if(a instanceof A.d_){s=a.l(0) return A.mX(A.hh("sqlite_error",null,s,a.c),b,c,d)}else return A.mX(A.hh("error",null,J.bp(a),null),b,c,d)}, kX(a){return A.tf(a)}, tf(a){var s=0,r=A.B(t.z),q,p=2,o,n,m,l,k,j,i var $async$kX=A.C(function(b,c){if(b===1){o=c s=p}while(true)switch(s){case 0:p=4 s=7 return A.p(A.at(a),$async$kX) case 7:n=c q=n s=1 break p=2 s=6 break case 4:p=3 i=o m=A.M(i) l=A.a_(i) k=A.mX(m,A.p1(a),A.cm(a,"sql",t.N),A.hi(a)) throw A.b(k) s=6 break case 3:s=2 break case 6:case 1:return A.z(q,r) case 2:return A.y(o,r)}}) return A.A($async$kX,r)}, e5(a,b){var s=A.kH(a) return s.b8(A.dj(J.ab(t.f.a(a.b),"transactionId")),new A.kG(b,s))}, e4(a,b){return $.qJ().a7(new A.kF(b),t.z)}, at(a){var s=0,r=A.B(t.z),q,p var $async$at=A.C(function(b,c){if(b===1)return A.y(c,r) while(true)switch(s){case 0:p=a.a case 3:switch(p){case"openDatabase":s=5 break case"closeDatabase":s=6 break case"query":s=7 break case"queryCursorNext":s=8 break case"execute":s=9 break case"insert":s=10 break case"update":s=11 break case"batch":s=12 break case"getDatabasesPath":s=13 break case"deleteDatabase":s=14 break case"databaseExists":s=15 break case"options":s=16 break case"debugMode":s=17 break default:s=18 break}break case 5:s=19 return A.p(A.e4(a,A.t9(a)),$async$at) case 19:q=c s=1 break case 6:s=20 return A.p(A.e4(a,A.t3(a)),$async$at) case 20:q=c s=1 break case 7:s=21 return A.p(A.e5(a,A.tb(a)),$async$at) case 21:q=c s=1 break case 8:s=22 return A.p(A.e5(a,A.tc(a)),$async$at) case 22:q=c s=1 break case 9:s=23 return A.p(A.e5(a,A.t6(a)),$async$at) case 23:q=c s=1 break case 10:s=24 return A.p(A.e5(a,A.t8(a)),$async$at) case 24:q=c s=1 break case 11:s=25 return A.p(A.e5(a,A.td(a)),$async$at) case 25:q=c s=1 break case 12:s=26 return A.p(A.e5(a,A.t2(a)),$async$at) case 26:q=c s=1 break case 13:s=27 return A.p(A.e4(a,A.t7(a)),$async$at) case 27:q=c s=1 break case 14:s=28 return A.p(A.e4(a,A.t5(a)),$async$at) case 28:q=c s=1 break case 15:s=29 return A.p(A.e4(a,A.t4(a)),$async$at) case 29:q=c s=1 break case 16:s=30 return A.p(A.e4(a,A.ta(a)),$async$at) case 30:q=c s=1 break case 17:s=31 return A.p(A.nI(a),$async$at) case 31:q=c s=1 break case 18:throw A.b(A.ao("Invalid method "+p+" "+a.l(0),null)) case 4:case 1:return A.z(q,r)}}) return A.A($async$at,r)}, t9(a){return new A.kQ(a)}, kY(a){return A.tg(a)}, tg(a){var s=0,r=A.B(t.f),q,p=2,o,n,m,l,k,j,i,h,g,f,e,d,c var $async$kY=A.C(function(b,a0){if(b===1){o=a0 s=p}while(true)switch(s){case 0:i=t.f.a(a.b) h=J.T(i) g=A.S(h.i(i,"path")) f=new A.kZ() e=A.eW(h.i(i,"singleInstance")) d=e===!0 h=A.eW(h.i(i,"readOnly")) if(d){l=$.j6.i(0,g) if(l!=null){i=$.nf if(typeof i!=="number"){q=i.hq() s=1 break}if(i>=2)l.av("Reopening existing single database "+l.l(0)) q=f.$1(l.e) s=1 break}}n=null p=4 e=$.b6 s=7 return A.p((e==null?$.b6=A.f1():e).bM(i),$async$kY) case 7:n=a0 p=2 s=6 break case 4:p=3 c=o i=A.M(c) if(i instanceof A.d_){m=i i=m h=i.l(0) throw A.b(A.hh("sqlite_error",null,"open_failed: "+h,i.c))}else throw c s=6 break case 3:s=2 break case 6:j=$.pT=$.pT+1 i=n e=$.nf l=new A.aU(A.t([],t.it),A.nC(),j,d,g,h===!0,i,e,A.X(t.S,t.lz),A.nC()) $.qc.k(0,j,l) l.av("Opening database "+l.l(0)) if(d)$.j6.k(0,g,l) q=f.$1(j) s=1 break case 1:return A.z(q,r) case 2:return A.y(o,r)}}) return A.A($async$kY,r)}, t3(a){return new A.kK(a)}, nG(a){var s=0,r=A.B(t.z),q var $async$nG=A.C(function(b,c){if(b===1)return A.y(c,r) while(true)switch(s){case 0:q=A.kH(a) if(q.f){$.j6.G(0,q.r) if($.q2==null)$.q2=new A.jB()}q.af(0) return A.z(null,r)}}) return A.A($async$nG,r)}, kH(a){var s=A.p1(a) if(s==null)throw A.b(A.K("Database "+A.q(A.p2(a))+" not found")) return s}, p1(a){var s=A.p2(a) if(s!=null)return $.qc.i(0,s) return null}, p2(a){var s=a.b if(t.f.b(s))return A.dj(J.ab(s,"id")) return null}, cm(a,b,c){var s=a.b if(t.f.b(s))return c.h("0?").a(J.ab(s,b)) return null}, th(a){var s,r="transactionId",q=a.b if(t.f.b(q)){s=J.a2(q) return s.F(q,r)&&s.i(q,r)==null}return!1}, p3(a){var s=null,r=A.cm(a,"path",t.N) if(r!=null&&r!==":memory:"&&$.or().a.ak(r)<=0){if($.b6==null)$.b6=A.f1() r=$.or().dZ(0,"/",r,s,s,s,s,s,s,s,s,s,s,s,s,s,s)}return r}, hi(a){var s,r,q,p,o=A.cm(a,"arguments",t.j) if(o!=null)for(s=J.an(o),r=t.k,q=t.p;s.p();){p=s.gu(s) if(p!=null)if(typeof p!="number")if(typeof p!="string")if(!q.b(p))if(!r.b(p))throw A.b(A.ao("Invalid sql argument type '"+J.jf(p).l(0)+"': "+A.q(p),null))}return o==null?null:J.jd(o,t.X)}, t1(a){var s=A.t([],t.bw),r=t.f r=J.jd(t.j.a(J.ab(r.a(a.b),"operations")),r) r.D(r,new A.kI(s)) return s}, tb(a){return new A.kT(a)}, nL(a,b){var s=0,r=A.B(t.z),q,p,o var $async$nL=A.C(function(c,d){if(c===1)return A.y(d,r) while(true)switch(s){case 0:o=A.cm(a,"sql",t.N) o.toString p=A.hi(a) q=b.h0(A.dj(J.ab(t.f.a(a.b),"cursorPageSize")),o,p) s=1 break case 1:return A.z(q,r)}}) return A.A($async$nL,r)}, tc(a){return new A.kS(a)}, nM(a,b){var s=0,r=A.B(t.z),q,p,o,n var $async$nM=A.C(function(c,d){if(c===1)return A.y(d,r) while(true)switch(s){case 0:b=A.kH(a) p=t.f.a(a.b) o=J.T(p) n=A.j(o.i(p,"cursorId")) q=b.h1(A.eW(o.i(p,"cancel")),n) s=1 break case 1:return A.z(q,r)}}) return A.A($async$nM,r)}, kE(a,b){var s=0,r=A.B(t.X),q,p var $async$kE=A.C(function(c,d){if(c===1)return A.y(d,r) while(true)switch(s){case 0:b=A.kH(a) p=A.cm(a,"sql",t.N) p.toString s=3 return A.p(b.fZ(p,A.hi(a)),$async$kE) case 3:q=null s=1 break case 1:return A.z(q,r)}}) return A.A($async$kE,r)}, t6(a){return new A.kN(a)}, kW(a,b){return A.te(a,b)}, te(a,b){var s=0,r=A.B(t.X),q,p=2,o,n,m,l,k var $async$kW=A.C(function(c,d){if(c===1){o=d s=p}while(true)switch(s){case 0:m=A.cm(a,"inTransaction",t.y) l=m===!0&&A.th(a) if(A.aK(l))b.b=++b.a p=4 s=7 return A.p(A.kE(a,b),$async$kW) case 7:p=2 s=6 break case 4:p=3 k=o if(A.aK(l))b.b=null throw k s=6 break case 3:s=2 break case 6:if(A.aK(l)){q=A.aO(["transactionId",b.b],t.N,t.X) s=1 break}else if(m===!1)b.b=null q=null s=1 break case 1:return A.z(q,r) case 2:return A.y(o,r)}}) return A.A($async$kW,r)}, ta(a){return new A.kR(a)}, l_(a){var s=0,r=A.B(t.z),q,p,o var $async$l_=A.C(function(b,c){if(b===1)return A.y(c,r) while(true)switch(s){case 0:o=a.b s=t.f.b(o)?3:4 break case 3:p=J.a2(o) if(p.F(o,"logLevel")){p=A.dj(p.i(o,"logLevel")) $.nf=p==null?0:p}p=$.b6 s=5 return A.p((p==null?$.b6=A.f1():p).cz(o),$async$l_) case 5:case 4:q=null s=1 break case 1:return A.z(q,r)}}) return A.A($async$l_,r)}, nI(a){var s=0,r=A.B(t.z),q var $async$nI=A.C(function(b,c){if(b===1)return A.y(c,r) while(true)switch(s){case 0:if(J.a7(a.b,!0))$.nf=2 q=null s=1 break case 1:return A.z(q,r)}}) return A.A($async$nI,r)}, t8(a){return new A.kP(a)}, nK(a,b){var s=0,r=A.B(t.I),q,p var $async$nK=A.C(function(c,d){if(c===1)return A.y(d,r) while(true)switch(s){case 0:p=A.cm(a,"sql",t.N) p.toString q=b.h_(p,A.hi(a)) s=1 break case 1:return A.z(q,r)}}) return A.A($async$nK,r)}, td(a){return new A.kU(a)}, nN(a,b){var s=0,r=A.B(t.S),q,p var $async$nN=A.C(function(c,d){if(c===1)return A.y(d,r) while(true)switch(s){case 0:p=A.cm(a,"sql",t.N) p.toString q=b.h3(p,A.hi(a)) s=1 break case 1:return A.z(q,r)}}) return A.A($async$nN,r)}, t2(a){return new A.kJ(a)}, t7(a){return new A.kO(a)}, nJ(a){var s=0,r=A.B(t.z),q var $async$nJ=A.C(function(b,c){if(b===1)return A.y(c,r) while(true)switch(s){case 0:if($.b6==null)$.b6=A.f1() q="/" s=1 break case 1:return A.z(q,r)}}) return A.A($async$nJ,r)}, t5(a){return new A.kM(a)}, kV(a){var s=0,r=A.B(t.H),q=1,p,o,n,m,l,k,j var $async$kV=A.C(function(b,c){if(b===1){p=c s=q}while(true)switch(s){case 0:l=A.p3(a) k=$.j6.i(0,l) if(k!=null){k.af(0) $.j6.G(0,l)}q=3 o=$.b6 if(o==null)o=$.b6=A.f1() n=l n.toString s=6 return A.p(o.b6(n),$async$kV) case 6:q=1 s=5 break case 3:q=2 j=p s=5 break case 2:s=1 break case 5:return A.z(null,r) case 1:return A.y(p,r)}}) return A.A($async$kV,r)}, t4(a){return new A.kL(a)}, nH(a){var s=0,r=A.B(t.y),q,p,o var $async$nH=A.C(function(b,c){if(b===1)return A.y(c,r) while(true)switch(s){case 0:p=A.p3(a) o=$.b6 if(o==null)o=$.b6=A.f1() p.toString q=o.bF(p) s=1 break case 1:return A.z(q,r)}}) return A.A($async$nH,r)}, kC:function kC(){}, e6:function e6(){this.c=this.b=this.a=null}, iB:function iB(a,b,c,d){var _=this _.a=a _.b=b _.c=c _.d=d _.e=!1}, iq:function iq(a,b){this.a=a this.b=b}, aU:function aU(a,b,c,d,e,f,g,h,i,j){var _=this _.a=0 _.b=null _.c=a _.d=b _.e=c _.f=d _.r=e _.w=f _.x=g _.y=h _.z=i _.Q=0 _.as=j}, kx:function kx(a,b,c){this.a=a this.b=b this.c=c}, kv:function kv(a){this.a=a}, kq:function kq(a){this.a=a}, ky:function ky(a,b,c){this.a=a this.b=b this.c=c}, kB:function kB(a,b,c){this.a=a this.b=b this.c=c}, kA:function kA(a,b,c,d){var _=this _.a=a _.b=b _.c=c _.d=d}, kz:function kz(a,b,c){this.a=a this.b=b this.c=c}, kw:function kw(a,b,c,d){var _=this _.a=a _.b=b _.c=c _.d=d}, ku:function ku(){}, kt:function kt(a,b){this.a=a this.b=b}, kr:function kr(a,b,c,d,e,f){var _=this _.a=a _.b=b _.c=c _.d=d _.e=e _.f=f}, ks:function ks(a,b){this.a=a this.b=b}, kG:function kG(a,b){this.a=a this.b=b}, kF:function kF(a){this.a=a}, kQ:function kQ(a){this.a=a}, kZ:function kZ(){}, kK:function kK(a){this.a=a}, kI:function kI(a){this.a=a}, kT:function kT(a){this.a=a}, kS:function kS(a){this.a=a}, kN:function kN(a){this.a=a}, kR:function kR(a){this.a=a}, kP:function kP(a){this.a=a}, kU:function kU(a){this.a=a}, kJ:function kJ(a){this.a=a}, kO:function kO(a){this.a=a}, kM:function kM(a){this.a=a}, kL:function kL(a){this.a=a}, kD:function kD(a){this.a=a this.c=this.b=null}, j4(a){return A.uA(t.A.a(a))}, uA(a8){var s=0,r=A.B(t.H),q=1,p,o,n,m,l,k,j,i,h,g,f,e,d,c,b,a,a0,a1,a2,a3,a4,a5,a6,a7 var $async$j4=A.C(function(a9,b0){if(a9===1){p=b0 s=q}while(true)switch(s){case 0:t.hy.a(a8) o=new A.c2([],[]).aF(a8.data,!0) a1=a8.ports a1.toString n=J.bP(a1) q=3 s=typeof o=="string"?6:8 break case 6:J.cB(n,o) s=7 break case 8:s=t.j.b(o)?9:11 break case 9:m=J.ab(o,0) if(J.a7(m,"varSet")){l=t.f.a(J.ab(o,1)) k=A.S(J.ab(l,"key")) j=J.ab(l,"value") A.b9($.eZ+" "+A.q(m)+" "+A.q(k)+": "+A.q(j)) $.qn.k(0,k,j) J.cB(n,null)}else if(J.a7(m,"varGet")){i=t.f.a(J.ab(o,1)) h=A.S(J.ab(i,"key")) g=$.qn.i(0,h) A.b9($.eZ+" "+A.q(m)+" "+A.q(h)+": "+A.q(g)) a1=t.N J.cB(n,A.aO(["result",A.aO(["key",h,"value",g],a1,t.X)],a1,t.lb))}else{A.b9($.eZ+" "+A.q(m)+" unknown") J.cB(n,null)}s=10 break case 11:s=t.f.b(o)?12:14 break case 12:f=A.ri(o) s=f!=null?15:17 break case 15:f=new A.fx(f.a,A.o7(f.b)) s=$.q1==null?18:19 break case 18:s=20 return A.p(A.dq(new A.l0(),!0),$async$j4) case 20:a1=b0 $.q1=a1 a1.toString $.b6=new A.kD(a1) case 19:e=new A.mY(n) q=22 s=25 return A.p(A.kX(f),$async$j4) case 25:d=b0 d=A.o8(d) e.$1(new A.cH(d,null)) q=3 s=24 break case 22:q=21 a6=p c=A.M(a6) b=A.a_(a6) a1=c a3=b a4=new A.cH($,$) a5=A.X(t.N,t.X) if(a1 instanceof A.bl){a5.k(0,"code",a1.w) a5.k(0,"details",a1.x) a5.k(0,"message",a1.a) a5.k(0,"resultCode",a1.bS())}else a5.k(0,"message",J.bp(a1)) a1=$.pS if(!(a1==null?$.pS=!0:a1)&&a3!=null)a5.k(0,"stackTrace",a3.l(0)) a4.b=a5 a4.a=null e.$1(a4) s=24 break case 21:s=3 break case 24:s=16 break case 17:A.b9($.eZ+" "+A.q(o)+" unknown") J.cB(n,null) case 16:s=13 break case 14:A.b9($.eZ+" "+A.q(o)+" map unknown") J.cB(n,null) case 13:case 10:case 7:q=1 s=5 break case 3:q=2 a7=p a=A.M(a7) a0=A.a_(a7) A.b9($.eZ+" error caught "+A.q(a)+" "+A.q(a0)) J.cB(n,null) s=5 break case 2:s=1 break case 5:return A.z(null,r) case 1:return A.y(p,r)}}) return A.A($async$j4,r)}, vr(a){var s,r,q try{s=self s.toString t.aD.a(s) r=t.a.a(new A.ng()) t.Z.a(null) A.bf(s,"connect",r,!1,t.A)}catch(q){try{s=self s.toString J.qP(s,"message",A.ok())}catch(q){}}}, mY:function mY(a){this.a=a}, ng:function ng(){}, pQ(a){if(a==null)return!0 else if(typeof a=="number"||typeof a=="string"||A.cz(a))return!0 return!1}, pU(a){var s,r=J.T(a) if(r.gj(a)===1){s=J.bP(r.gK(a)) if(typeof s=="string")return B.a.J(s,"@") throw A.b(A.bq(s,null,null))}return!1}, o8(a){var s,r,q,p,o,n,m,l,k={} if(A.pQ(a))return a a.toString for(s=$.oq(),r=0;r<1;++r){q=s[r] p=A.v(q).h("dg.T") if(p.b(a))return A.aO(["@"+q.a,t.k.a(p.a(a)).l(0)],t.N,t.X)}if(t.f.b(a)){if(A.pU(a))return A.aO(["@",a],t.N,t.X) k.a=null J.bo(a,new A.mW(k,a)) s=k.a if(s==null)s=a return s}else if(t.j.b(a)){for(s=J.T(a),p=t.z,o=null,n=0;n")),m=new A.aa(n,c.h("aa<0>")) o.a=o.b=null s=new A.kg(o) r=t.a q=r.a(new A.kh(s,m,b,a,c)) t.Z.a(null) p=t.A o.b=A.bf(a,"success",q,!1,p) o.a=A.bf(a,"error",r.a(new A.ki(o,s,m)),!1,p) return n}, kg:function kg(a){this.a=a}, kh:function kh(a,b,c,d,e){var _=this _.a=a _.b=b _.c=c _.d=d _.e=e}, kf:function kf(a,b,c){this.a=a this.b=b this.c=c}, ki:function ki(a,b,c){this.a=a this.b=b this.c=c}, d7:function d7(a,b){var _=this _.c=_.b=_.a=null _.d=a _.$ti=b}, lJ:function lJ(a,b){this.a=a this.b=b}, lK:function lK(a,b){this.a=a this.b=b}, jA:function jA(){}, lq(a,b){var s=0,r=A.B(t.ax),q,p,o,n,m var $async$lq=A.C(function(c,d){if(c===1)return A.y(d,r) while(true)switch(s){case 0:o={} b.D(0,new A.ls(o)) p=t.N p=new A.hG(A.X(p,t.Y),A.X(p,t.ng)) n=p m=J s=3 return A.p(A.j8(self.WebAssembly.instantiateStreaming(a,o),t.ot),$async$lq) case 3:n.ez(m.qW(d)) q=p s=1 break case 1:return A.z(q,r)}}) return A.A($async$lq,r)}, mK:function mK(){}, dc:function dc(){}, hG:function hG(a,b){this.a=a this.b=b}, ls:function ls(a){this.a=a}, lr:function lr(a){this.a=a}, k1:function k1(){}, cU:function cU(){}, cK:function cK(){}, lt(a,b){var s=0,r=A.B(t.es),q,p,o var $async$lt=A.C(function(c,d){if(c===1)return A.y(d,r) while(true)switch(s){case 0:p=A o=A s=3 return A.p(A.lp(a,b),$async$lt) case 3:q=new p.hH(new o.hI(d)) s=1 break case 1:return A.z(q,r)}}) return A.A($async$lt,r)}, hH:function hH(a){this.a=a}, lp(b9,c0){var s=0,r=A.B(t.n0),q,p,o,n,m,l,k,j,i,h,g,f,e,d,c,b,a,a0,a1,a2,a3,a4,a5,a6,a7,a8,a9,b0,b1,b2,b3,b4,b5,b6,b7,b8 var $async$lp=A.C(function(c1,c2){if(c1===1)return A.y(c2,r) while(true)switch(s){case 0:b7=A.tH(c0) b8=b7.b b8===$&&A.aZ("injectedValues") s=3 return A.p(A.lq(b9,b8),$async$lp) case 3:p=c2 b8=b7.c b8===$&&A.aZ("memory") o=p.a n=o.i(0,"dart_sqlite3_malloc") n.toString m=o.i(0,"dart_sqlite3_free") m.toString o.i(0,"dart_sqlite3_create_scalar_function").toString o.i(0,"dart_sqlite3_create_aggregate_function").toString o.i(0,"dart_sqlite3_create_window_function") o.i(0,"dart_sqlite3_create_collation") l=o.i(0,"dart_sqlite3_updates") l.toString o.i(0,"sqlite3_libversion").toString o.i(0,"sqlite3_sourceid").toString o.i(0,"sqlite3_libversion_number").toString k=o.i(0,"sqlite3_open_v2") k.toString j=o.i(0,"sqlite3_close_v2") j.toString i=o.i(0,"sqlite3_extended_errcode") i.toString h=o.i(0,"sqlite3_errmsg") h.toString g=o.i(0,"sqlite3_errstr") g.toString f=o.i(0,"sqlite3_extended_result_codes") f.toString e=o.i(0,"sqlite3_exec") e.toString o.i(0,"sqlite3_free").toString d=o.i(0,"sqlite3_prepare_v3") d.toString c=o.i(0,"sqlite3_bind_parameter_count") c.toString b=o.i(0,"sqlite3_column_count") b.toString a=o.i(0,"sqlite3_column_name") a.toString a0=o.i(0,"sqlite3_reset") a0.toString a1=o.i(0,"sqlite3_step") a1.toString a2=o.i(0,"sqlite3_finalize") a2.toString a3=o.i(0,"sqlite3_column_type") a3.toString a4=o.i(0,"sqlite3_column_int64") a4.toString a5=o.i(0,"sqlite3_column_double") a5.toString a6=o.i(0,"sqlite3_column_bytes") a6.toString a7=o.i(0,"sqlite3_column_blob") a7.toString a8=o.i(0,"sqlite3_column_text") a8.toString a9=o.i(0,"sqlite3_bind_null") a9.toString b0=o.i(0,"sqlite3_bind_int64") b0.toString b1=o.i(0,"sqlite3_bind_double") b1.toString b2=o.i(0,"sqlite3_bind_text") b2.toString b3=o.i(0,"sqlite3_bind_blob64") b3.toString o.i(0,"sqlite3_bind_parameter_index").toString b4=o.i(0,"sqlite3_changes") b4.toString b5=o.i(0,"sqlite3_last_insert_rowid") b5.toString b6=o.i(0,"sqlite3_user_data") b6.toString o.i(0,"sqlite3_result_null").toString o.i(0,"sqlite3_result_int64").toString o.i(0,"sqlite3_result_double").toString o.i(0,"sqlite3_result_text").toString o.i(0,"sqlite3_result_blob64").toString o.i(0,"sqlite3_result_error").toString o.i(0,"sqlite3_value_type").toString o.i(0,"sqlite3_value_int64").toString o.i(0,"sqlite3_value_double").toString o.i(0,"sqlite3_value_bytes").toString o.i(0,"sqlite3_value_text").toString o.i(0,"sqlite3_value_blob").toString o.i(0,"sqlite3_aggregate_context").toString p.b.i(0,"sqlite3_temp_directory").toString q=b7.a=new A.hE(b8,b7.d,n,m,l,k,j,i,h,g,f,e,d,c,b,a,a0,a1,a3,a4,a5,a6,a8,a7,a9,b0,b1,b2,b3,a2,b4,b5,b6) s=1 break case 1:return A.z(q,r)}}) return A.A($async$lp,r)}, oV(a,b){var s,r=A.b_(J.bO(a),0,null),q=r.length,p=0 while(!0){s=b+p if(!(s>=0&&s=65&&a<=90))s=a>=97&&a<=122 else s=!0 return s}, vm(a,b){var s=a.length,r=b+2 if(s4294967295)throw A.b(A.a1(a,0,4294967295,"length",null)) return J.rr(new Array(a),b)}, rq(a,b){if(a<0)throw A.b(A.ao("Length must be a non-negative integer: "+a,null)) return A.t(new Array(a),b.h("O<0>"))}, rr(a,b){return J.jP(A.t(a,b.h("O<0>")),b)}, jP(a,b){a.fixed$length=Array return a}, oK(a){a.fixed$length=Array a.immutable$list=Array return a}, rs(a,b){var s=t.bP return J.qS(s.a(a),s.a(b))}, oL(a){if(a<256)switch(a){case 9:case 10:case 11:case 12:case 13:case 32:case 133:case 160:return!0 default:return!1}switch(a){case 5760:case 8192:case 8193:case 8194:case 8195:case 8196:case 8197:case 8198:case 8199:case 8200:case 8201:case 8202:case 8232:case 8233:case 8239:case 8287:case 12288:case 65279:return!0 default:return!1}}, rt(a,b){var s,r for(s=a.length;b0;b=s){s=b-1 r=B.a.B(a,s) if(r!==32&&r!==13&&!J.oL(r))break}return b}, bL(a){if(typeof a=="number"){if(Math.floor(a)==a)return J.dJ.prototype return J.fJ.prototype}if(typeof a=="string")return J.bW.prototype if(a==null)return J.dK.prototype if(typeof a=="boolean")return J.fH.prototype if(a.constructor==Array)return J.O.prototype if(typeof a!="object"){if(typeof a=="function")return J.bu.prototype return a}if(a instanceof A.r)return a return J.n8(a)}, T(a){if(typeof a=="string")return J.bW.prototype if(a==null)return a if(a.constructor==Array)return J.O.prototype if(typeof a!="object"){if(typeof a=="function")return J.bu.prototype return a}if(a instanceof A.r)return a return J.n8(a)}, b8(a){if(a==null)return a if(a.constructor==Array)return J.O.prototype if(typeof a!="object"){if(typeof a=="function")return J.bu.prototype return a}if(a instanceof A.r)return a return J.n8(a)}, vc(a){if(typeof a=="number")return J.cO.prototype if(typeof a=="string")return J.bW.prototype if(a==null)return a if(!(a instanceof A.r))return J.c_.prototype return a}, og(a){if(typeof a=="string")return J.bW.prototype if(a==null)return a if(!(a instanceof A.r))return J.c_.prototype return a}, a2(a){if(a==null)return a if(typeof a!="object"){if(typeof a=="function")return J.bu.prototype return a}if(a instanceof A.r)return a return J.n8(a)}, qd(a){if(a==null)return a if(!(a instanceof A.r))return J.c_.prototype return a}, a7(a,b){if(a==null)return b==null if(typeof a!="object")return b!=null&&a===b return J.bL(a).W(a,b)}, ab(a,b){if(typeof b==="number")if(a.constructor==Array||typeof a=="string"||A.vn(a,a[v.dispatchPropertyName]))if(b>>>0===b&&b").q(b).h("ba<1,2>"))}, m(a,b){A.av(a).c.a(b) if(!!a.fixed$length)A.J(A.x("add")) a.push(b)}, hi(a,b){var s if(!!a.fixed$length)A.J(A.x("removeAt")) s=a.length if(b>=s)throw A.b(A.oS(b,null)) return a.splice(b,1)[0]}, G(a,b){var s if(!!a.fixed$length)A.J(A.x("remove")) for(s=0;s").a(b) if(!!a.fixed$length)A.J(A.x("addAll")) if(Array.isArray(b)){this.eF(a,b) return}for(s=J.an(b);s.p();)a.push(s.gu(s))}, eF(a,b){var s,r t.b.a(b) s=b.length if(s===0)return if(a===b)throw A.b(A.ap(a)) for(r=0;r").q(c).h("af<1,2>"))}, au(a,b){var s,r=A.jX(a.length,"",!1,t.N) for(s=0;s=0&&b0)return a[0] throw A.b(A.bt())}, gai(a){var s=a.length if(s>0)return a[s-1] throw A.b(A.bt())}, T(a,b,c,d,e){var s,r,q,p,o A.av(a).h("e<1>").a(d) if(!!a.immutable$list)A.J(A.x("setRange")) A.by(b,c,a.length) s=c-b if(s===0)return A.aT(e,"skipCount") if(t.j.b(d)){r=d q=e}else{r=J.ns(d,e).bQ(0,!1) q=0}p=J.T(r) if(q+s>p.gj(r))throw A.b(A.oJ()) if(q=0;--o)a[b+o]=p.i(r,q+o) else for(o=0;o=r for(s=q;s>=0;--s){if(!(s"))}, gI(a){return A.dZ(a)}, gj(a){return a.length}, i(a,b){if(!(b>=0&&b=0&&b=p){r.sd_(null) return!1}r.sd_(q[s]);++r.c return!0}, sd_(a){this.d=this.$ti.h("1?").a(a)}, $iL:1} J.cO.prototype={ a8(a,b){var s A.uh(b) if(ab)return 1 else if(a===b){if(a===0){s=this.gcF(b) if(this.gcF(a)===s)return 0 if(this.gcF(a))return-1 return 1}return 0}else if(isNaN(a)){if(isNaN(b))return 0 return 1}else return-1}, gcF(a){return a===0?1/a<0:a<0}, fF(a){var s,r if(a>=0){if(a<=2147483647){s=a|0 return a===s?s:s+1}}else if(a>=-2147483648)return a|0 r=Math.ceil(a) if(isFinite(r))return r throw A.b(A.x(""+a+".ceil()"))}, l(a){if(a===0&&1/a<0)return"-0.0" else return""+a}, gI(a){var s,r,q,p,o=a|0 if(a===o)return o&536870911 s=Math.abs(a) r=Math.log(s)/0.6931471805599453|0 q=Math.pow(2,r) p=s<1?s/q:q/s return((p*9007199254740992|0)+(p*3542243181176521|0))*599197+r*1259&536870911}, ab(a,b){var s=a%b if(s===0)return 0 if(s>0)return s return s+b}, ex(a,b){if((a|0)===a)if(b>=1||b<-1)return a/b|0 return this.dC(a,b)}, R(a,b){return(a|0)===a?a/b|0:this.dC(a,b)}, dC(a,b){var s=a/b if(s>=-2147483648&&s<=2147483647)return s|0 if(s>0){if(s!==1/0)return Math.floor(s)}else if(s>-1/0)return Math.ceil(s) throw A.b(A.x("Result of truncating division is "+A.q(s)+": "+A.q(a)+" ~/ "+b))}, aU(a,b){if(b<0)throw A.b(A.cA(b)) return b>31?0:a<>>0}, aV(a,b){var s if(b<0)throw A.b(A.cA(b)) if(a>0)s=this.ck(a,b) else{s=b>31?31:b s=a>>s>>>0}return s}, M(a,b){var s if(a>0)s=this.ck(a,b) else{s=b>31?31:b s=a>>s>>>0}return s}, fn(a,b){if(0>b)throw A.b(A.cA(b)) return this.ck(a,b)}, ck(a,b){return b>31?0:a>>>b}, gN(a){return B.al}, $iaj:1, $iN:1, $iW:1} J.dJ.prototype={ gdN(a){var s,r=a<0?-a-1:a,q=r for(s=32;q>=4294967296;){q=this.R(q,4294967296) s+=32}return s-Math.clz32(q)}, gN(a){return B.ak}, $ic:1} J.fJ.prototype={ gN(a){return B.aj}} J.bW.prototype={ B(a,b){if(b<0)throw A.b(A.dp(a,b)) if(b>=a.length)A.J(A.dp(a,b)) return a.charCodeAt(b)}, t(a,b){if(b>=a.length)throw A.b(A.dp(a,b)) return a.charCodeAt(b)}, cp(a,b,c){var s=b.length if(c>s)throw A.b(A.a1(c,0,s,null,null)) return new A.iE(b,a,c)}, dL(a,b){return this.cp(a,b,0)}, bf(a,b){return a+b}, dR(a,b){var s=b.length,r=a.length if(s>r)return!1 return b===this.O(a,r-s)}, az(a,b,c,d){var s=A.by(b,c,a.length) return A.vv(a,b,s,d)}, H(a,b,c){var s if(c<0||c>a.length)throw A.b(A.a1(c,0,a.length,null,null)) s=c+b.length if(s>a.length)return!1 return b===a.substring(c,s)}, J(a,b){return this.H(a,b,0)}, n(a,b,c){return a.substring(b,A.by(b,c,a.length))}, O(a,b){return this.n(a,b,null)}, hn(a){var s,r,q,p=a.trim(),o=p.length if(o===0)return p if(this.t(p,0)===133){s=J.rt(p,1) if(s===o)return""}else s=0 r=o-1 q=this.B(p,r)===133?J.ru(p,r):o if(s===0&&q===o)return p return p.substring(s,q)}, bg(a,b){var s,r if(0>=b)return"" if(b===1||a.length===0)return a if(b!==b>>>0)throw A.b(B.R) for(s=a,r="";!0;){if((b&1)===1)r=s+r b=b>>>1 if(b===0)break s+=s}return r}, hg(a,b,c){var s=b-a.length if(s<=0)return a return this.bg(c,s)+a}, aq(a,b,c){var s if(c<0||c>a.length)throw A.b(A.a1(c,0,a.length,null,null)) s=a.indexOf(b,c) return s}, cB(a,b){return this.aq(a,b,0)}, e_(a,b,c){var s,r if(c==null)c=a.length else if(c<0||c>a.length)throw A.b(A.a1(c,0,a.length,null,null)) s=b.length r=a.length if(c+s>r)c=r-s return a.lastIndexOf(b,c)}, cG(a,b){return this.e_(a,b,null)}, S(a,b){return A.vu(a,b,0)}, a8(a,b){var s A.S(b) if(a===b)s=0 else s=a>6}r=r+((r&67108863)<<3)&536870911 r^=r>>11 return r+((r&16383)<<15)&536870911}, gN(a){return B.ad}, gj(a){return a.length}, $iaj:1, $ik9:1, $ii:1} A.c3.prototype={ gE(a){var s=A.v(this) return new A.du(J.an(this.ga3()),s.h("@<1>").q(s.z[1]).h("du<1,2>"))}, gj(a){return J.Y(this.ga3())}, gC(a){return J.dr(this.ga3())}, gP(a){return J.f3(this.ga3())}, a2(a,b){var s=A.v(this) return A.fe(J.ns(this.ga3(),b),s.c,s.z[1])}, v(a,b){return A.v(this).z[1].a(J.je(this.ga3(),b))}, gA(a){return A.v(this).z[1].a(J.bP(this.ga3()))}, S(a,b){return J.nr(this.ga3(),b)}, l(a){return J.bp(this.ga3())}} A.du.prototype={ p(){return this.a.p()}, gu(a){var s=this.a return this.$ti.z[1].a(s.gu(s))}, $iL:1} A.cb.prototype={ ga3(){return this.a}} A.ep.prototype={$ik:1} A.ek.prototype={ i(a,b){return this.$ti.z[1].a(J.ab(this.a,b))}, k(a,b,c){var s=this.$ti J.nq(this.a,b,s.c.a(s.z[1].a(c)))}, T(a,b,c,d,e){var s=this.$ti J.r1(this.a,b,c,A.fe(s.h("e<2>").a(d),s.z[1],s.c),e)}, a6(a,b,c,d){return this.T(a,b,c,d,0)}, $ik:1, $im:1} A.ba.prototype={ bz(a,b){return new A.ba(this.a,this.$ti.h("@<1>").q(b).h("ba<1,2>"))}, ga3(){return this.a}} A.dv.prototype={ F(a,b){return J.qT(this.a,b)}, i(a,b){return this.$ti.h("4?").a(J.ab(this.a,b))}, G(a,b){return this.$ti.h("4?").a(J.r0(this.a,b))}, D(a,b){J.bo(this.a,new A.ju(this,this.$ti.h("~(3,4)").a(b)))}, gK(a){var s=this.$ti return A.fe(J.ou(this.a),s.c,s.z[2])}, gU(a){var s=this.$ti return A.fe(J.qX(this.a),s.z[1],s.z[3])}, gj(a){return J.Y(this.a)}, gC(a){return J.dr(this.a)}, gP(a){return J.f3(this.a)}, gaH(a){return J.ot(this.a).aj(0,new A.jt(this),this.$ti.h("a4<3,4>"))}} A.ju.prototype={ $2(a,b){var s=this.a.$ti s.c.a(a) s.z[1].a(b) this.b.$2(s.z[2].a(a),s.z[3].a(b))}, $S(){return this.a.$ti.h("~(1,2)")}} A.jt.prototype={ $1(a){var s,r=this.a.$ti r.h("a4<1,2>").a(a) s=r.z[3] return new A.a4(r.z[2].a(a.a),s.a(a.b),r.h("@<3>").q(s).h("a4<1,2>"))}, $S(){return this.a.$ti.h("a4<3,4>(a4<1,2>)")}} A.cQ.prototype={ l(a){return"LateInitializationError: "+this.a}} A.fh.prototype={ gj(a){return this.a.length}, i(a,b){return B.a.B(this.a,b)}} A.ni.prototype={ $0(){return A.oH(null,t.P)}, $S:10} A.kn.prototype={} A.k.prototype={} A.a3.prototype={ gE(a){var s=this return new A.aP(s,s.gj(s),A.v(s).h("aP"))}, gC(a){return this.gj(this)===0}, gA(a){if(this.gj(this)===0)throw A.b(A.bt()) return this.v(0,0)}, S(a,b){var s,r=this,q=r.gj(r) for(s=0;s").q(c).h("af<1,2>"))}, a2(a,b){return A.eb(this,b,null,A.v(this).h("a3.E"))}} A.cn.prototype={ ey(a,b,c,d){var s,r=this.b A.aT(r,"start") s=this.c if(s!=null){A.aT(s,"end") if(r>s)throw A.b(A.a1(r,0,s,"start",null))}}, geW(){var s=J.Y(this.a),r=this.c if(r==null||r>s)return s return r}, gfq(){var s=J.Y(this.a),r=this.b if(r>s)return s return r}, gj(a){var s,r=J.Y(this.a),q=this.b if(q>=r)return 0 s=this.c if(s==null||s>=r)return r-q if(typeof s!=="number")return s.aX() return s-q}, v(a,b){var s=this,r=s.gfq()+b if(b<0||r>=s.geW())throw A.b(A.V(b,s.gj(s),s,null,"index")) return J.je(s.a,r)}, a2(a,b){var s,r,q=this A.aT(b,"count") s=q.b+b r=q.c if(r!=null&&s>=r)return new A.cf(q.$ti.h("cf<1>")) return A.eb(q.a,s,r,q.$ti.c)}, bQ(a,b){var s,r,q,p=this,o=p.b,n=p.a,m=J.T(n),l=m.gj(n),k=p.c if(k!=null&&k=o){r.saZ(null) return!1}r.saZ(p.v(q,s));++r.c return!0}, saZ(a){this.d=this.$ti.h("1?").a(a)}, $iL:1} A.bw.prototype={ gE(a){var s=A.v(this) return new A.dQ(J.an(this.a),this.b,s.h("@<1>").q(s.z[1]).h("dQ<1,2>"))}, gj(a){return J.Y(this.a)}, gC(a){return J.dr(this.a)}, gA(a){return this.b.$1(J.bP(this.a))}, v(a,b){return this.b.$1(J.je(this.a,b))}} A.ce.prototype={$ik:1} A.dQ.prototype={ p(){var s=this,r=s.b if(r.p()){s.saZ(s.c.$1(r.gu(r))) return!0}s.saZ(null) return!1}, gu(a){var s=this.a return s==null?this.$ti.z[1].a(s):s}, saZ(a){this.a=this.$ti.h("2?").a(a)}} A.af.prototype={ gj(a){return J.Y(this.a)}, v(a,b){return this.b.$1(J.je(this.a,b))}} A.lv.prototype={ gE(a){return new A.cq(J.an(this.a),this.b,this.$ti.h("cq<1>"))}, aj(a,b,c){var s=this.$ti return new A.bw(this,s.q(c).h("1(2)").a(b),s.h("@<1>").q(c).h("bw<1,2>"))}} A.cq.prototype={ p(){var s,r for(s=this.a,r=this.b;s.p();)if(A.aK(r.$1(s.gu(s))))return!0 return!1}, gu(a){var s=this.a return s.gu(s)}} A.bA.prototype={ a2(a,b){A.jg(b,"count",t.S) A.aT(b,"count") return new A.bA(this.a,this.b+b,A.v(this).h("bA<1>"))}, gE(a){return new A.e2(J.an(this.a),this.b,A.v(this).h("e2<1>"))}} A.cG.prototype={ gj(a){var s=J.Y(this.a)-this.b if(s>=0)return s return 0}, a2(a,b){A.jg(b,"count",t.S) A.aT(b,"count") return new A.cG(this.a,this.b+b,this.$ti)}, $ik:1} A.e2.prototype={ p(){var s,r for(s=this.a,r=0;r"))}, a2(a,b){A.aT(b,"count") return this}, bQ(a,b){var s=J.nx(0,this.$ti.c) return s}} A.dB.prototype={ p(){return!1}, gu(a){throw A.b(A.bt())}, $iL:1} A.ef.prototype={ gE(a){return new A.eg(J.an(this.a),this.$ti.h("eg<1>"))}} A.eg.prototype={ p(){var s,r for(s=this.a,r=this.$ti.c;s.p();)if(r.b(s.gu(s)))return!0 return!1}, gu(a){var s=this.a return this.$ti.c.a(s.gu(s))}, $iL:1} A.ar.prototype={} A.c0.prototype={ k(a,b,c){A.v(this).h("c0.E").a(c) throw A.b(A.x("Cannot modify an unmodifiable list"))}, T(a,b,c,d,e){A.v(this).h("e").a(d) throw A.b(A.x("Cannot modify an unmodifiable list"))}, a6(a,b,c,d){return this.T(a,b,c,d,0)}} A.d2.prototype={} A.ic.prototype={ gj(a){return J.Y(this.a)}, v(a,b){var s=J.Y(this.a) if(0>b||b>=s)A.J(A.V(b,s,this,null,"index")) return b}} A.dO.prototype={ i(a,b){return this.F(0,b)?J.ab(this.a,A.j(b)):null}, gj(a){return J.Y(this.a)}, gU(a){return A.eb(this.a,0,null,this.$ti.c)}, gK(a){return new A.ic(this.a)}, gC(a){return J.dr(this.a)}, gP(a){return J.f3(this.a)}, F(a,b){return A.dl(b)&&b>=0&&b"))}, fR(a,b){var s=this return A.uO(function(){var r=a var q=0,p=1,o,n,m,l,k,j return function $async$gaH(c,d){if(c===1){o=d q=p}while(true)switch(q){case 0:n=s.gK(s),n=n.gE(n),m=A.v(s),l=m.z[1],m=m.h("@<1>").q(l).h("a4<1,2>") case 2:if(!n.p()){q=3 break}k=n.gu(n) j=s.i(0,k) q=4 return new A.a4(k,j==null?l.a(j):j,m) case 4:q=2 break case 3:return A.tI() case 1:return A.tJ(o)}}},b)}, $iI:1} A.cc.prototype={ gj(a){return this.a}, F(a,b){if(typeof b!="string")return!1 if("__proto__"===b)return!1 return this.b.hasOwnProperty(b)}, i(a,b){if(!this.F(0,b))return null return this.b[A.S(b)]}, D(a,b){var s,r,q,p,o,n=this.$ti n.h("~(1,2)").a(b) s=this.c for(r=s.length,q=this.b,n=n.z[1],p=0;p"))}, gU(a){var s=this.$ti return A.nD(this.c,new A.jv(this),s.c,s.z[1])}} A.jv.prototype={ $1(a){var s=this.a,r=s.$ti return r.z[1].a(s.b[A.S(r.c.a(a))])}, $S(){return this.a.$ti.h("2(1)")}} A.em.prototype={ gE(a){var s=this.a.c return new J.ca(s,s.length,A.av(s).h("ca<1>"))}, gj(a){return this.a.c.length}} A.fI.prototype={ ge0(){var s=this.a return s}, ge5(){var s,r,q,p,o=this if(o.c===1)return B.m s=o.d r=s.length-o.e.length-o.f if(r===0)return B.m q=[] for(p=0;p=0&&l>>0}, l(a){return"Closure '"+this.$_name+"' of "+("Instance of '"+A.kb(this.a)+"'")}} A.hc.prototype={ l(a){return"RuntimeError: "+this.a}} A.hO.prototype={ l(a){return"Assertion failed: "+A.cg(this.a)}} A.mu.prototype={} A.as.prototype={ gj(a){return this.a}, gC(a){return this.a===0}, gP(a){return this.a!==0}, gK(a){return new A.bv(this,A.v(this).h("bv<1>"))}, gU(a){var s=A.v(this) return A.nD(new A.bv(this,s.h("bv<1>")),new A.jT(this),s.c,s.z[1])}, F(a,b){var s,r if(typeof b=="string"){s=this.b if(s==null)return!1 return s[b]!=null}else if(typeof b=="number"&&(b&0x3fffffff)===b){r=this.c if(r==null)return!1 return r[b]!=null}else return this.dV(b)}, dV(a){var s=this.d if(s==null)return!1 return this.aN(s[this.aM(a)],a)>=0}, b4(a,b){J.bo(A.v(this).h("I<1,2>").a(b),new A.jS(this))}, i(a,b){var s,r,q,p,o=null if(typeof b=="string"){s=this.b if(s==null)return o r=s[b] q=r==null?o:r.b return q}else if(typeof b=="number"&&(b&0x3fffffff)===b){p=this.c if(p==null)return o r=p[b] q=r==null?o:r.b return q}else return this.dW(b)}, dW(a){var s,r,q=this.d if(q==null)return null s=q[this.aM(a)] r=this.aN(s,a) if(r<0)return null return s[r].b}, k(a,b,c){var s,r,q=this,p=A.v(q) p.c.a(b) p.z[1].a(c) if(typeof b=="string"){s=q.b q.d1(s==null?q.b=q.cg():s,b,c)}else if(typeof b=="number"&&(b&0x3fffffff)===b){r=q.c q.d1(r==null?q.c=q.cg():r,b,c)}else q.dY(b,c)}, dY(a,b){var s,r,q,p,o=this,n=A.v(o) n.c.a(a) n.z[1].a(b) s=o.d if(s==null)s=o.d=o.cg() r=o.aM(a) q=s[r] if(q==null)s[r]=[o.ci(a,b)] else{p=o.aN(q,a) if(p>=0)q[p].b=b else q.push(o.ci(a,b))}}, e8(a,b,c){var s,r,q=this,p=A.v(q) p.c.a(b) p.h("2()").a(c) if(q.F(0,b)){s=q.i(0,b) return s==null?p.z[1].a(s):s}r=c.$0() q.k(0,b,r) return r}, G(a,b){var s=this if(typeof b=="string")return s.dv(s.b,b) else if(typeof b=="number"&&(b&0x3fffffff)===b)return s.dv(s.c,b) else return s.dX(b)}, dX(a){var s,r,q,p,o=this,n=o.d if(n==null)return null s=o.aM(a) r=n[s] q=o.aN(r,a) if(q<0)return null p=r.splice(q,1)[0] o.dH(p) if(r.length===0)delete n[s] return p.b}, D(a,b){var s,r,q=this A.v(q).h("~(1,2)").a(b) s=q.e r=q.r for(;s!=null;){b.$2(s.a,s.b) if(r!==q.r)throw A.b(A.ap(q)) s=s.c}}, d1(a,b,c){var s,r=A.v(this) r.c.a(b) r.z[1].a(c) s=a[b] if(s==null)a[b]=this.ci(b,c) else s.b=c}, dv(a,b){var s if(a==null)return null s=a[b] if(s==null)return null this.dH(s) delete a[b] return s.b}, dl(){this.r=this.r+1&1073741823}, ci(a,b){var s=this,r=A.v(s),q=new A.jV(r.c.a(a),r.z[1].a(b)) if(s.e==null)s.e=s.f=q else{r=s.f r.toString q.d=r s.f=r.c=q}++s.a s.dl() return q}, dH(a){var s=this,r=a.d,q=a.c if(r==null)s.e=q else r.c=q if(q==null)s.f=r else q.d=r;--s.a s.dl()}, aM(a){return J.ax(a)&0x3fffffff}, aN(a,b){var s,r if(a==null)return-1 s=a.length for(r=0;r"]=s delete s[""] return s}, $ijU:1} A.jT.prototype={ $1(a){var s=this.a,r=A.v(s) s=s.i(0,r.c.a(a)) return s==null?r.z[1].a(s):s}, $S(){return A.v(this.a).h("2(1)")}} A.jS.prototype={ $2(a,b){var s=this.a,r=A.v(s) s.k(0,r.c.a(a),r.z[1].a(b))}, $S(){return A.v(this.a).h("~(1,2)")}} A.jV.prototype={} A.bv.prototype={ gj(a){return this.a.a}, gC(a){return this.a.a===0}, gE(a){var s=this.a,r=new A.dM(s,s.r,this.$ti.h("dM<1>")) r.c=s.e return r}, S(a,b){return this.a.F(0,b)}} A.dM.prototype={ gu(a){return this.d}, p(){var s,r=this,q=r.a if(r.b!==q.r)throw A.b(A.ap(q)) s=r.c if(s==null){r.sd0(null) return!1}else{r.sd0(s.a) r.c=s.c return!0}}, sd0(a){this.d=this.$ti.h("1?").a(a)}, $iL:1} A.na.prototype={ $1(a){return this.a(a)}, $S:77} A.nb.prototype={ $2(a,b){return this.a(a,b)}, $S:76} A.nc.prototype={ $1(a){return this.a(A.S(a))}, $S:74} A.dL.prototype={ l(a){return"RegExp/"+this.a+"/"+this.b.flags}, gf6(){var s=this,r=s.c if(r!=null)return r r=s.b return s.c=A.oM(s.a,r.multiline,!r.ignoreCase,r.unicode,r.dotAll,!0)}, fS(a){var s=this.b.exec(a) if(s==null)return null return new A.ey(s)}, cp(a,b,c){var s=b.length if(c>s)throw A.b(A.a1(c,0,s,null,null)) return new A.hM(this,b,c)}, dL(a,b){return this.cp(a,b,0)}, eY(a,b){var s,r=this.gf6() if(r==null)r=t.K.a(r) r.lastIndex=b s=r.exec(a) if(s==null)return null return new A.ey(s)}, $ik9:1, $ioW:1} A.ey.prototype={ gfQ(a){var s=this.b return s.index+s[0].length}, $icT:1, $ie_:1} A.hM.prototype={ gE(a){return new A.hN(this.a,this.b,this.c)}} A.hN.prototype={ gu(a){var s=this.d return s==null?t.lu.a(s):s}, p(){var s,r,q,p,o,n=this,m=n.b if(m==null)return!1 s=n.c r=m.length if(s<=r){q=n.a p=q.eY(m,s) if(p!=null){n.d=p o=p.gfQ(p) if(p.b.index===o){if(q.b.unicode){s=n.c q=s+1 if(q=55296&&s<=56319){s=B.a.B(m,q) s=s>=56320&&s<=57343}else s=!1}else s=!1}else s=!1 o=(s?o+1:o)+1}n.c=o return!0}}n.b=n.d=null return!1}, $iL:1} A.ea.prototype={$icT:1} A.iE.prototype={ gE(a){return new A.iF(this.a,this.b,this.c)}, gA(a){var s=this.b,r=this.a.indexOf(s,this.c) if(r>=0)return new A.ea(r,s) throw A.b(A.bt())}} A.iF.prototype={ p(){var s,r,q=this,p=q.c,o=q.b,n=o.length,m=q.a,l=m.length if(p+n>l){q.d=null return!1}s=m.indexOf(o,p) if(s<0){q.c=l+1 q.d=null return!1}r=s+n q.d=new A.ea(s,o) q.c=r===q.c?r+1:r return!0}, gu(a){var s=this.d s.toString return s}, $iL:1} A.lI.prototype={ bq(){var s=this.b if(s===this)throw A.b(new A.cQ("Local '"+this.a+"' has not been initialized.")) return s}, a_(){var s=this.b if(s===this)throw A.b(A.oN(this.a)) return s}} A.cW.prototype={ gN(a){return B.a4}, $icW:1, $int:1} A.a5.prototype={ f5(a,b,c,d){var s=A.a1(b,0,c,d,null) throw A.b(s)}, d5(a,b,c,d){if(b>>>0!==b||b>c)this.f5(a,b,c,d)}, $ia5:1} A.dR.prototype={ gN(a){return B.a5}, f0(a,b,c){return a.getUint32(b,c)}, fm(a,b,c,d){return a.setUint32(b,c,d)}, $ioC:1} A.ag.prototype={ gj(a){return a.length}, dz(a,b,c,d,e){var s,r,q=a.length this.d5(a,b,q,"start") this.d5(a,c,q,"end") if(b>c)throw A.b(A.a1(b,0,c,null,null)) s=c-b if(e<0)throw A.b(A.ao(e,null)) r=d.length if(r-e").b(b))s.d4(b) else s.b0(q.c.a(b))}}, bA(a,b){var s=this.a if(this.b)s.V(a,b) else s.aC(a,b)}, $ifi:1} A.mM.prototype={ $1(a){return this.a.$2(0,a)}, $S:4} A.mN.prototype={ $2(a,b){this.a.$2(1,new A.dC(a,t.l.a(b)))}, $S:68} A.n2.prototype={ $2(a,b){this.a(A.j(a),b)}, $S:67} A.db.prototype={ l(a){return"IterationMarker("+this.b+", "+A.q(this.a)+")"}} A.de.prototype={ gu(a){var s,r=this.c if(r==null){s=this.b return s==null?this.$ti.c.a(s):s}return r.gu(r)}, p(){var s,r,q,p,o,n,m=this for(s=m.$ti.h("L<1>");!0;){r=m.c if(r!=null)if(r.p())return!0 else m.sdm(null) q=function(a,b,c){var l,k=b while(true)try{return a(k,l)}catch(j){l=j k=c}}(m.a,0,1) if(q instanceof A.db){p=q.b if(p===2){o=m.d if(o==null||o.length===0){m.sd2(null) return!1}if(0>=o.length)return A.d(o,-1) m.a=o.pop() continue}else{r=q.a if(p===3)throw r else{n=s.a(J.an(r)) if(n instanceof A.de){r=m.d if(r==null)r=m.d=[] B.b.m(r,m.a) m.a=n.a continue}else{m.sdm(n) continue}}}}else{m.sd2(q) return!0}}return!1}, sd2(a){this.b=this.$ti.h("1?").a(a)}, sdm(a){this.c=this.$ti.h("L<1>?").a(a)}, $iL:1} A.eK.prototype={ gE(a){return new A.de(this.a(),this.$ti.h("de<1>"))}} A.dt.prototype={ l(a){return A.q(this.a)}, $iQ:1, gaW(){return this.b}} A.jI.prototype={ $0(){var s,r,q try{this.a.b_(this.b.$0())}catch(q){s=A.M(q) r=A.a_(q) A.pK(this.a,s,r)}}, $S:0} A.jK.prototype={ $2(a,b){var s,r,q=this t.K.a(a) t.l.a(b) s=q.a r=--s.b if(s.a!=null){s.a=null if(s.b===0||q.c)q.d.V(a,b) else{q.e.b=a q.f.b=b}}else if(r===0&&!q.c)q.d.V(q.e.bq(),q.f.bq())}, $S:21} A.jJ.prototype={ $1(a){var s,r,q=this,p=q.w p.a(a) r=q.a;--r.b s=r.a if(s!=null){J.nq(s,q.b,a) if(r.b===0)q.c.b0(A.jY(s,!0,p))}else if(r.b===0&&!q.e)q.c.V(q.f.bq(),q.r.bq())}, $S(){return this.w.h("R(0)")}} A.cs.prototype={ bA(a,b){var s,r=t.K r.a(a) t.fw.a(b) A.c7(a,"error",r) if((this.a.a&30)!==0)throw A.b(A.K("Future already completed")) s=$.D.b7(a,b) if(s!=null){a=s.a b=s.b}else if(b==null)b=A.f8(a) this.V(a,b)}, ag(a){return this.bA(a,null)}, $ifi:1} A.cr.prototype={ a0(a,b){var s,r=this.$ti r.h("1/?").a(b) s=this.a if((s.a&30)!==0)throw A.b(A.K("Future already completed")) s.bk(r.h("1/").a(b))}, V(a,b){this.a.aC(a,b)}} A.aa.prototype={ a0(a,b){var s,r=this.$ti r.h("1/?").a(b) s=this.a if((s.a&30)!==0)throw A.b(A.K("Future already completed")) s.b_(r.h("1/").a(b))}, fH(a){return this.a0(a,null)}, V(a,b){this.a.V(a,b)}} A.bH.prototype={ ha(a){if((this.c&15)!==6)return!0 return this.b.b.cQ(t.iW.a(this.d),a.a,t.y,t.K)}, fY(a){var s,r=this,q=r.e,p=null,o=t.z,n=t.K,m=a.a,l=r.b.b if(t.Q.b(q))p=l.hl(q,m,a.b,o,n,t.l) else p=l.cQ(t.v.a(q),m,o,n) try{o=r.$ti.h("2/").a(p) return o}catch(s){if(t.do.b(A.M(s))){if((r.c&1)!==0)throw A.b(A.ao("The error handler of Future.then must return a value of the returned future's type","onError")) throw A.b(A.ao("The error handler of Future.catchError must return a value of the future's type","onError"))}else throw s}}} A.E.prototype={ bP(a,b,c){var s,r,q,p=this.$ti p.q(c).h("1/(2)").a(a) s=$.D if(s===B.d){if(b!=null&&!t.Q.b(b)&&!t.v.b(b))throw A.b(A.bq(b,"onError",u.c))}else{a=s.bN(a,c.h("0/"),p.c) if(b!=null)b=A.uS(b,s)}r=new A.E($.D,c.h("E<0>")) q=b==null?1:3 this.bj(new A.bH(r,q,a,b,p.h("@<1>").q(c).h("bH<1,2>"))) return r}, eb(a,b){return this.bP(a,null,b)}, dE(a,b,c){var s,r=this.$ti r.q(c).h("1/(2)").a(a) s=new A.E($.D,c.h("E<0>")) this.bj(new A.bH(s,3,a,b,r.h("@<1>").q(c).h("bH<1,2>"))) return s}, aS(a){var s,r,q t.mY.a(a) s=this.$ti r=$.D q=new A.E(r,s) if(r!==B.d)a=r.cO(a,t.z) this.bj(new A.bH(q,8,a,null,s.h("@<1>").q(s.c).h("bH<1,2>"))) return q}, fk(a){this.a=this.a&1|16 this.c=a}, c2(a){this.a=a.a&30|this.a&1 this.c=a.c}, bj(a){var s,r=this,q=r.a if(q<=3){a.a=t.e.a(r.c) r.c=a}else{if((q&4)!==0){s=t.g.a(r.c) if((s.a&24)===0){s.bj(a) return}r.c2(s)}r.b.aB(new A.lR(r,a))}}, dt(a){var s,r,q,p,o,n,m=this,l={} l.a=a if(a==null)return s=m.a if(s<=3){r=t.e.a(m.c) m.c=a if(r!=null){q=a.a for(p=a;q!=null;p=q,q=o)o=q.a p.a=r}}else{if((s&4)!==0){n=t.g.a(m.c) if((n.a&24)===0){n.dt(a) return}m.c2(n)}l.a=m.bt(a) m.b.aB(new A.lZ(l,m))}}, br(){var s=t.e.a(this.c) this.c=null return this.bt(s)}, bt(a){var s,r,q for(s=a,r=null;s!=null;r=s,s=q){q=s.a s.a=r}return r}, d3(a){var s,r,q,p=this p.a^=2 try{a.bP(new A.lV(p),new A.lW(p),t.P)}catch(q){s=A.M(q) r=A.a_(q) A.qm(new A.lX(p,s,r))}}, b_(a){var s,r=this,q=r.$ti q.h("1/").a(a) if(q.h("H<1>").b(a))if(q.b(a))A.lU(a,r) else r.d3(a) else{s=r.br() q.c.a(a) r.a=8 r.c=a A.da(r,s)}}, b0(a){var s,r=this r.$ti.c.a(a) s=r.br() r.a=8 r.c=a A.da(r,s)}, V(a,b){var s t.K.a(a) t.l.a(b) s=this.br() this.fk(A.jh(a,b)) A.da(this,s)}, bk(a){var s=this.$ti s.h("1/").a(a) if(s.h("H<1>").b(a)){this.d4(a) return}this.eJ(s.c.a(a))}, eJ(a){var s=this s.$ti.c.a(a) s.a^=2 s.b.aB(new A.lT(s,a))}, d4(a){var s=this,r=s.$ti r.h("H<1>").a(a) if(r.b(a)){if((a.a&16)!==0){s.a^=2 s.b.aB(new A.lY(s,a))}else A.lU(a,s) return}s.d3(a)}, aC(a,b){t.l.a(b) this.a^=2 this.b.aB(new A.lS(this,a,b))}, $iH:1} A.lR.prototype={ $0(){A.da(this.a,this.b)}, $S:0} A.lZ.prototype={ $0(){A.da(this.b,this.a.a)}, $S:0} A.lV.prototype={ $1(a){var s,r,q,p=this.a p.a^=2 try{p.b0(p.$ti.c.a(a))}catch(q){s=A.M(q) r=A.a_(q) p.V(s,r)}}, $S:16} A.lW.prototype={ $2(a,b){this.a.V(t.K.a(a),t.l.a(b))}, $S:53} A.lX.prototype={ $0(){this.a.V(this.b,this.c)}, $S:0} A.lT.prototype={ $0(){this.a.b0(this.b)}, $S:0} A.lY.prototype={ $0(){A.lU(this.b,this.a)}, $S:0} A.lS.prototype={ $0(){this.a.V(this.b,this.c)}, $S:0} A.m1.prototype={ $0(){var s,r,q,p,o,n,m=this,l=null try{q=m.a.a l=q.b.b.cP(t.mY.a(q.d),t.z)}catch(p){s=A.M(p) r=A.a_(p) q=m.c&&t.n.a(m.b.a.c).a===s o=m.a if(q)o.c=t.n.a(m.b.a.c) else o.c=A.jh(s,r) o.b=!0 return}if(l instanceof A.E&&(l.a&24)!==0){if((l.a&16)!==0){q=m.a q.c=t.n.a(l.c) q.b=!0}return}if(t.c.b(l)){n=m.b.a q=m.a q.c=l.eb(new A.m2(n),t.z) q.b=!1}}, $S:0} A.m2.prototype={ $1(a){return this.a}, $S:47} A.m0.prototype={ $0(){var s,r,q,p,o,n,m,l try{q=this.a p=q.a o=p.$ti n=o.c m=n.a(this.b) q.c=p.b.b.cQ(o.h("2/(1)").a(p.d),m,o.h("2/"),n)}catch(l){s=A.M(l) r=A.a_(l) q=this.a q.c=A.jh(s,r) q.b=!0}}, $S:0} A.m_.prototype={ $0(){var s,r,q,p,o,n,m=this try{s=t.n.a(m.a.a.c) p=m.b if(p.a.ha(s)&&p.a.e!=null){p.c=p.a.fY(s) p.b=!1}}catch(o){r=A.M(o) q=A.a_(o) p=t.n.a(m.a.a.c) n=m.b if(p.a===r)n.c=p else n.c=A.jh(r,q) n.b=!0}}, $S:0} A.hP.prototype={} A.aV.prototype={ gj(a){var s={},r=new A.E($.D,t.g_) s.a=0 this.cH(new A.l9(s,this),!0,new A.la(s,r),r.gda()) return r}, gA(a){var s=new A.E($.D,A.v(this).h("E")),r=this.cH(null,!0,new A.l7(s),s.gda()) r.e3(new A.l8(this,r,s)) return s}} A.l9.prototype={ $1(a){A.v(this.b).h("aV.T").a(a);++this.a.a}, $S(){return A.v(this.b).h("~(aV.T)")}} A.la.prototype={ $0(){this.b.b_(this.a.a)}, $S:0} A.l7.prototype={ $0(){var s,r,q,p try{q=A.bt() throw A.b(q)}catch(p){s=A.M(p) r=A.a_(p) A.pK(this.a,s,r)}}, $S:0} A.l8.prototype={ $1(a){A.un(this.b,this.c,A.v(this.a).h("aV.T").a(a))}, $S(){return A.v(this.a).h("~(aV.T)")}} A.bm.prototype={} A.hm.prototype={} A.dd.prototype={ gf9(){var s,r=this if((r.b&8)===0)return A.v(r).h("b4<1>?").a(r.a) s=A.v(r) return s.h("b4<1>?").a(s.h("eI<1>").a(r.a).gcW())}, c7(){var s,r,q=this if((q.b&8)===0){s=q.a if(s==null)s=q.a=new A.b4(A.v(q).h("b4<1>")) return A.v(q).h("b4<1>").a(s)}r=A.v(q) s=r.h("eI<1>").a(q.a).gcW() return r.h("b4<1>").a(s)}, gcl(){var s=this.a if((this.b&8)!==0)s=t.gL.a(s).gcW() return A.v(this).h("d6<1>").a(s)}, bY(){if((this.b&4)!==0)return new A.bB("Cannot add event after closing") return new A.bB("Cannot add event while adding a stream")}, de(){var s=this.c if(s==null)s=this.c=(this.b&2)!==0?$.f2():new A.E($.D,t.D) return s}, dK(a,b){var s,r,q=this A.c7(a,"error",t.K) if(q.b>=4)throw A.b(q.bY()) s=$.D.b7(a,b) if(s!=null){a=s.a b=s.b}else b=A.f8(a) r=q.b if((r&1)!==0)q.bx(a,b) else if((r&3)===0)q.c7().m(0,new A.en(a,b))}, fA(a){return this.dK(a,null)}, af(a){var s=this,r=s.b if((r&4)!==0)return s.de() if(r>=4)throw A.b(s.bY()) s.eM() return s.de()}, eM(){var s=this.b|=4 if((s&1)!==0)this.bw() else if((s&3)===0)this.c7().m(0,B.y)}, bW(a,b){var s,r=this,q=A.v(r) q.c.a(b) s=r.b if((s&1)!==0)r.bv(b) else if((s&3)===0)r.c7().m(0,new A.cu(b,q.h("cu<1>")))}, fs(a,b,c,d){var s,r,q,p,o,n,m,l=this,k=A.v(l) k.h("~(1)?").a(a) t.Z.a(c) if((l.b&3)!==0)throw A.b(A.K("Stream has already been listened to.")) s=$.D r=d?1:0 q=A.pi(s,a,k.c) p=A.tG(s,b) o=new A.d6(l,q,p,s.cO(c,t.H),s,r,k.h("d6<1>")) n=l.gf9() s=l.b|=1 if((s&8)!==0){m=k.h("eI<1>").a(l.a) m.scW(o) m.hk(0)}else l.a=o o.fl(n) o.f1(new A.mz(l)) return o}, fc(a){var s,r,q,p,o,n,m,l=this,k=A.v(l) k.h("bm<1>").a(a) s=null if((l.b&8)!==0)s=k.h("eI<1>").a(l.a).Y(0) l.a=null l.b=l.b&4294967286|2 r=l.r if(r!=null)if(s==null)try{q=r.$0() if(t.p8.b(q))s=q}catch(n){p=A.M(n) o=A.a_(n) m=new A.E($.D,t.D) m.aC(p,o) s=m}else s=s.aS(r) k=new A.my(l) if(s!=null)s=s.aS(k) else k.$0() return s}, $ipp:1, $icv:1} A.mz.prototype={ $0(){A.oc(this.a.d)}, $S:0} A.my.prototype={ $0(){var s=this.a.c if(s!=null&&(s.a&30)===0)s.bk(null)}, $S:0} A.iK.prototype={ bv(a){this.$ti.c.a(a) this.gcl().bW(0,a)}, bx(a,b){this.gcl().eG(a,b)}, bw(){this.gcl().eL()}} A.df.prototype={} A.d5.prototype={ gI(a){return(A.dZ(this.a)^892482866)>>>0}, W(a,b){if(b==null)return!1 if(this===b)return!0 return b instanceof A.d5&&b.a===this.a}} A.d6.prototype={ dn(){return this.w.fc(this)}, dr(){var s=this.w,r=A.v(s) r.h("bm<1>").a(this) if((s.b&8)!==0)r.h("eI<1>").a(s.a).ht(0) A.oc(s.e)}, ds(){var s=this.w,r=A.v(s) r.h("bm<1>").a(this) if((s.b&8)!==0)r.h("eI<1>").a(s.a).hk(0) A.oc(s.f)}} A.ej.prototype={ fl(a){var s=this A.v(s).h("b4<1>?").a(a) if(a==null)return s.sbp(a) if(a.c!=null){s.e=(s.e|64)>>>0 a.bT(s)}}, e3(a){var s=A.v(this) this.seI(A.pi(this.d,s.h("~(1)?").a(a),s.c))}, Y(a){var s=this,r=(s.e&4294967279)>>>0 s.e=r if((r&8)===0)s.c0() r=s.f return r==null?$.f2():r}, c0(){var s,r=this,q=r.e=(r.e|8)>>>0 if((q&64)!==0){s=r.r if(s.a===1)s.a=3}if((q&32)===0)r.sbp(null) r.f=r.dn()}, bW(a,b){var s,r=this,q=A.v(r) q.c.a(b) s=r.e if((s&8)!==0)return if(s<32)r.bv(b) else r.bX(new A.cu(b,q.h("cu<1>")))}, eG(a,b){var s=this.e if((s&8)!==0)return if(s<32)this.bx(a,b) else this.bX(new A.en(a,b))}, eL(){var s=this,r=s.e if((r&8)!==0)return r=(r|2)>>>0 s.e=r if(r<32)s.bw() else s.bX(B.y)}, dr(){}, ds(){}, dn(){return null}, bX(a){var s,r=this,q=r.r if(q==null){q=new A.b4(A.v(r).h("b4<1>")) r.sbp(q)}q.m(0,a) s=r.e if((s&64)===0){s=(s|64)>>>0 r.e=s if(s<128)q.bT(r)}}, bv(a){var s,r=this,q=A.v(r).c q.a(a) s=r.e r.e=(s|32)>>>0 r.d.cR(r.a,a,q) r.e=(r.e&4294967263)>>>0 r.c1((s&4)!==0)}, bx(a,b){var s,r=this,q=r.e,p=new A.lH(r,a,b) if((q&1)!==0){r.e=(q|16)>>>0 r.c0() s=r.f if(s!=null&&s!==$.f2())s.aS(p) else p.$0()}else{p.$0() r.c1((q&4)!==0)}}, bw(){var s,r=this,q=new A.lG(r) r.c0() r.e=(r.e|16)>>>0 s=r.f if(s!=null&&s!==$.f2())s.aS(q) else q.$0()}, f1(a){var s,r=this t.M.a(a) s=r.e r.e=(s|32)>>>0 a.$0() r.e=(r.e&4294967263)>>>0 r.c1((s&4)!==0)}, c1(a){var s,r,q=this,p=q.e if((p&64)!==0&&q.r.c==null){p=q.e=(p&4294967231)>>>0 if((p&4)!==0)if(p<128){s=q.r s=s==null?null:s.c==null s=s!==!1}else s=!1 else s=!1 if(s){p=(p&4294967291)>>>0 q.e=p}}for(;!0;a=r){if((p&8)!==0){q.sbp(null) return}r=(p&4)!==0 if(a===r)break q.e=(p^32)>>>0 if(r)q.dr() else q.ds() p=(q.e&4294967263)>>>0 q.e=p}if((p&64)!==0&&p<128)q.r.bT(q)}, seI(a){this.a=A.v(this).h("~(1)").a(a)}, sbp(a){this.r=A.v(this).h("b4<1>?").a(a)}, $ibm:1, $icv:1} A.lH.prototype={ $0(){var s,r,q,p=this.a,o=p.e if((o&8)!==0&&(o&16)===0)return p.e=(o|32)>>>0 s=p.b o=this.b r=t.K q=p.d if(t.b9.b(s))q.hm(s,o,this.c,r,t.l) else q.cR(t.i6.a(s),o,r) p.e=(p.e&4294967263)>>>0}, $S:0} A.lG.prototype={ $0(){var s=this.a,r=s.e if((r&16)===0)return s.e=(r|42)>>>0 s.d.ea(s.c) s.e=(s.e&4294967263)>>>0}, $S:0} A.eJ.prototype={ cH(a,b,c,d){var s=this.$ti s.h("~(1)?").a(a) t.Z.a(c) return this.a.fs(s.h("~(1)?").a(a),d,c,!0)}} A.bG.prototype={ sbb(a,b){this.a=t.lT.a(b)}, gbb(a){return this.a}} A.cu.prototype={ cK(a){this.$ti.h("cv<1>").a(a).bv(this.b)}} A.en.prototype={ cK(a){a.bx(this.b,this.c)}} A.hV.prototype={ cK(a){a.bw()}, gbb(a){return null}, sbb(a,b){throw A.b(A.K("No events after a done."))}, $ibG:1} A.b4.prototype={ bT(a){var s,r=this r.$ti.h("cv<1>").a(a) s=r.a if(s===1)return if(s>=1){r.a=1 return}A.qm(new A.ms(r,a)) r.a=1}, m(a,b){var s=this,r=s.c if(r==null)s.b=s.c=b else{r.sbb(0,b) s.c=b}}} A.ms.prototype={ $0(){var s,r,q,p=this.a,o=p.a p.a=0 if(o===3)return s=p.$ti.h("cv<1>").a(this.b) r=p.b q=r.gbb(r) p.b=q if(q==null)p.c=null r.cK(s)}, $S:0} A.iD.prototype={} A.mO.prototype={ $0(){return this.a.b_(this.b)}, $S:0} A.iT.prototype={} A.eT.prototype={$ibF:1} A.n_.prototype={ $0(){var s=this.a,r=this.b A.c7(s,"error",t.K) A.c7(r,"stackTrace",t.l) A.rh(s,r)}, $S:0} A.iu.prototype={ gfi(){return B.an}, gaI(){return this}, ea(a){var s,r,q t.M.a(a) try{if(B.d===$.D){a.$0() return}A.pV(null,null,this,a,t.H)}catch(q){s=A.M(q) r=A.a_(q) A.mZ(t.K.a(s),t.l.a(r))}}, cR(a,b,c){var s,r,q c.h("~(0)").a(a) c.a(b) try{if(B.d===$.D){a.$1(b) return}A.pX(null,null,this,a,b,t.H,c)}catch(q){s=A.M(q) r=A.a_(q) A.mZ(t.K.a(s),t.l.a(r))}}, hm(a,b,c,d,e){var s,r,q d.h("@<0>").q(e).h("~(1,2)").a(a) d.a(b) e.a(c) try{if(B.d===$.D){a.$2(b,c) return}A.pW(null,null,this,a,b,c,t.H,d,e)}catch(q){s=A.M(q) r=A.a_(q) A.mZ(t.K.a(s),t.l.a(r))}}, fD(a,b){return new A.mw(this,b.h("0()").a(a),b)}, cr(a){return new A.mv(this,t.M.a(a))}, dM(a,b){return new A.mx(this,b.h("~(0)").a(a),b)}, dU(a,b){A.mZ(a,t.l.a(b))}, cP(a,b){b.h("0()").a(a) if($.D===B.d)return a.$0() return A.pV(null,null,this,a,b)}, cQ(a,b,c,d){c.h("@<0>").q(d).h("1(2)").a(a) d.a(b) if($.D===B.d)return a.$1(b) return A.pX(null,null,this,a,b,c,d)}, hl(a,b,c,d,e,f){d.h("@<0>").q(e).q(f).h("1(2,3)").a(a) e.a(b) f.a(c) if($.D===B.d)return a.$2(b,c) return A.pW(null,null,this,a,b,c,d,e,f)}, cO(a,b){return b.h("0()").a(a)}, bN(a,b,c){return b.h("@<0>").q(c).h("1(2)").a(a)}, cN(a,b,c,d){return b.h("@<0>").q(c).q(d).h("1(2,3)").a(a)}, b7(a,b){t.fw.a(b) return null}, aB(a){A.n0(null,null,this,t.M.a(a))}, dQ(a,b){return A.p6(a,t.M.a(b))}} A.mw.prototype={ $0(){return this.a.cP(this.b,this.c)}, $S(){return this.c.h("0()")}} A.mv.prototype={ $0(){return this.a.ea(this.b)}, $S:0} A.mx.prototype={ $1(a){var s=this.c return this.a.cR(this.b,s.a(a),s)}, $S(){return this.c.h("~(0)")}} A.et.prototype={ aM(a){return A.j7(a)&1073741823}, aN(a,b){var s,r,q if(a==null)return-1 s=a.length for(r=0;r")) r.c=s.e return r}, gj(a){return this.a}, gC(a){return this.a===0}, gP(a){return this.a!==0}, S(a,b){var s,r if(b!=="__proto__"){s=this.b if(s==null)return!1 return t.R.a(s[b])!=null}else{r=this.eP(b) return r}}, eP(a){var s=this.d if(s==null)return!1 return this.cb(s[B.a.gI(a)&1073741823],a)>=0}, gA(a){var s=this.e if(s==null)throw A.b(A.K("No elements")) return this.$ti.c.a(s.a)}, m(a,b){var s,r,q=this q.$ti.c.a(b) if(typeof b=="string"&&b!=="__proto__"){s=q.b return q.d6(s==null?q.b=A.nY():s,b)}else if(typeof b=="number"&&(b&1073741823)===b){r=q.c return q.d6(r==null?q.c=A.nY():r,b)}else return q.eN(0,b)}, eN(a,b){var s,r,q,p=this p.$ti.c.a(b) s=p.d if(s==null)s=p.d=A.nY() r=J.ax(b)&1073741823 q=s[r] if(q==null)s[r]=[p.c4(b)] else{if(p.cb(q,b)>=0)return!1 q.push(p.c4(b))}return!0}, G(a,b){var s=this if(typeof b=="string"&&b!=="__proto__")return s.d8(s.b,b) else if(typeof b=="number"&&(b&1073741823)===b)return s.d8(s.c,b) else return s.fe(0,b)}, fe(a,b){var s,r,q,p,o=this.d if(o==null)return!1 s=J.ax(b)&1073741823 r=o[s] q=this.cb(r,b) if(q<0)return!1 p=r.splice(q,1)[0] if(0===r.length)delete o[s] this.d9(p) return!0}, d6(a,b){this.$ti.c.a(b) if(t.R.a(a[b])!=null)return!1 a[b]=this.c4(b) return!0}, d8(a,b){var s if(a==null)return!1 s=t.R.a(a[b]) if(s==null)return!1 this.d9(s) delete a[b] return!0}, d7(){this.r=this.r+1&1073741823}, c4(a){var s,r=this,q=new A.ib(r.$ti.c.a(a)) if(r.e==null)r.e=r.f=q else{s=r.f s.toString q.c=s r.f=s.b=q}++r.a r.d7() return q}, d9(a){var s=this,r=a.c,q=a.b if(r==null)s.e=q else r.b=q if(q==null)s.f=r else q.c=r;--s.a s.d7()}, cb(a,b){var s,r if(a==null)return-1 s=a.length for(r=0;r"))}, gj(a){return this.b}, gA(a){var s if(this.b===0)throw A.b(A.K("No such element")) s=this.c s.toString return s}, gai(a){var s if(this.b===0)throw A.b(A.K("No such element")) s=this.c.c s.toString return s}, gC(a){return this.b===0}, cf(a,b,c){var s=this,r=s.$ti r.h("1?").a(a) r.c.a(b) if(b.a!=null)throw A.b(A.K("LinkedListEntry is already in a LinkedList"));++s.a b.sdj(s) if(s.b===0){b.sao(b) b.sb1(b) s.scc(b);++s.b return}r=a.c r.toString b.sb1(r) b.sao(a) r.sao(b) a.sb1(b);++s.b}, cm(a){var s,r,q=this,p=null q.$ti.c.a(a);++q.a a.b.sb1(a.c) s=a.c r=a.b s.sao(r);--q.b a.sb1(p) a.sao(p) a.sdj(p) if(q.b===0)q.scc(p) else if(a===q.c)q.scc(r)}, scc(a){this.c=this.$ti.h("1?").a(a)}} A.eu.prototype={ gu(a){var s=this.c return s==null?this.$ti.c.a(s):s}, p(){var s=this,r=s.a if(s.b!==r.a)throw A.b(A.ap(s)) if(r.b!==0)r=s.e&&s.d===r.gA(r) else r=!0 if(r){s.sad(null) return!1}s.e=!0 s.sad(s.d) s.sao(s.d.b) return!0}, sad(a){this.c=this.$ti.h("1?").a(a)}, sao(a){this.d=this.$ti.h("1?").a(a)}, $iL:1} A.ae.prototype={ gbc(){var s=this.a if(s==null||this===s.gA(s))return null return this.c}, sdj(a){this.a=A.v(this).h("cR?").a(a)}, sao(a){this.b=A.v(this).h("ae.E?").a(a)}, sb1(a){this.c=A.v(this).h("ae.E?").a(a)}} A.dN.prototype={$ik:1,$ie:1,$im:1} A.h.prototype={ gE(a){return new A.aP(a,this.gj(a),A.a0(a).h("aP"))}, v(a,b){return this.i(a,b)}, D(a,b){var s,r A.a0(a).h("~(h.E)").a(b) s=this.gj(a) for(r=0;r").q(c).h("af<1,2>"))}, a2(a,b){return A.eb(a,b,null,A.a0(a).h("h.E"))}, bz(a,b){return new A.ba(a,A.a0(a).h("@").q(b).h("ba<1,2>"))}, dT(a,b,c,d){var s A.a0(a).h("h.E?").a(d) A.by(b,c,this.gj(a)) for(s=b;s").a(d) A.by(b,c,this.gj(a)) s=c-b if(s===0)return A.aT(e,"skipCount") if(o.h("m").b(d)){r=e q=d}else{q=J.ns(d,e).bQ(0,!1) r=0}o=J.T(q) if(r+s>o.gj(q))throw A.b(A.oJ()) if(r=0;--p)this.k(a,b+p,o.i(q,r+p)) else for(p=0;p").a(c) if(t.j.b(c))this.a6(a,b,b+c.length,c) else for(s=J.an(c);s.p();b=r){r=b+1 this.k(a,b,s.gu(s))}}, l(a){return A.nw(a,"[","]")}} A.dP.prototype={} A.k_.prototype={ $2(a,b){var s,r=this.a if(!r.a)this.b.a+=", " r.a=!1 r=this.b s=r.a+=A.q(a) r.a=s+": " r.a+=A.q(b)}, $S:39} A.w.prototype={ fE(a,b,c){var s=A.a0(a) return A.rA(a,s.h("w.K"),s.h("w.V"),b,c)}, D(a,b){var s,r,q,p=A.a0(a) p.h("~(w.K,w.V)").a(b) for(s=J.an(this.gK(a)),p=p.h("w.V");s.p();){r=s.gu(s) q=this.i(a,r) b.$2(r,q==null?p.a(q):q)}}, gaH(a){return J.ov(this.gK(a),new A.k0(a),A.a0(a).h("a4"))}, h9(a,b,c,d){var s,r,q,p,o,n=A.a0(a) n.q(c).q(d).h("a4<1,2>(w.K,w.V)").a(b) s=A.X(c,d) for(r=J.an(this.gK(a)),n=n.h("w.V");r.p();){q=r.gu(r) p=this.i(a,q) o=b.$2(q,p==null?n.a(p):p) s.k(0,o.a,o.b)}return s}, F(a,b){return J.nr(this.gK(a),b)}, gj(a){return J.Y(this.gK(a))}, gC(a){return J.dr(this.gK(a))}, gP(a){return J.f3(this.gK(a))}, gU(a){var s=A.a0(a) return new A.ew(a,s.h("@").q(s.h("w.V")).h("ew<1,2>"))}, l(a){return A.jZ(a)}, $iI:1} A.k0.prototype={ $1(a){var s=this.a,r=A.a0(s) r.h("w.K").a(a) s=J.ab(s,a) if(s==null)s=r.h("w.V").a(s) return new A.a4(a,s,r.h("@").q(r.h("w.V")).h("a4<1,2>"))}, $S(){return A.a0(this.a).h("a4(w.K)")}} A.d3.prototype={} A.ew.prototype={ gj(a){return J.Y(this.a)}, gC(a){return J.dr(this.a)}, gP(a){return J.f3(this.a)}, gA(a){var s=this.a,r=J.a2(s) s=r.i(s,J.bP(r.gK(s))) return s==null?this.$ti.z[1].a(s):s}, gE(a){var s=this.a,r=this.$ti return new A.ex(J.an(J.ou(s)),s,r.h("@<1>").q(r.z[1]).h("ex<1,2>"))}} A.ex.prototype={ p(){var s=this,r=s.a if(r.p()){s.sad(J.ab(s.b,r.gu(r))) return!0}s.sad(null) return!1}, gu(a){var s=this.c return s==null?this.$ti.z[1].a(s):s}, sad(a){this.c=this.$ti.h("2?").a(a)}, $iL:1} A.c5.prototype={ G(a,b){throw A.b(A.x("Cannot modify unmodifiable map"))}} A.cS.prototype={ i(a,b){return this.a.i(0,b)}, F(a,b){return this.a.F(0,b)}, D(a,b){this.a.D(0,A.v(this).h("~(1,2)").a(b))}, gj(a){var s=this.a return s.gj(s)}, gK(a){var s=this.a return s.gK(s)}, l(a){var s=this.a return s.l(s)}, gU(a){var s=this.a return s.gU(s)}, gaH(a){var s=this.a return s.gaH(s)}, $iI:1} A.ed.prototype={} A.e1.prototype={ gC(a){return this.a===0}, gP(a){return this.a!==0}, aj(a,b,c){var s=this.$ti return new A.ce(this,s.q(c).h("1(2)").a(b),s.h("@<1>").q(c).h("ce<1,2>"))}, l(a){return A.nw(this,"{","}")}, a2(a,b){return A.p0(this,b,this.$ti.c)}, gA(a){var s,r=A.pk(this,this.r,this.$ti.c) if(!r.p())throw A.b(A.bt()) s=r.d return s==null?r.$ti.c.a(s):s}, v(a,b){var s,r,q,p,o=this,n="index" A.c7(b,n,t.S) A.aT(b,n) for(s=A.pk(o,o.r,o.$ti.c),r=s.$ti.c,q=0;s.p();){p=s.d if(p==null)p=r.a(p) if(b===q)return p;++q}throw A.b(A.V(b,q,o,null,n))}} A.eE.prototype={$ik:1,$ie:1,$ip_:1} A.ev.prototype={} A.dh.prototype={} A.eV.prototype={} A.ln.prototype={ $0(){var s,r try{s=new TextDecoder("utf-8",{fatal:true}) return s}catch(r){}return null}, $S:19} A.lm.prototype={ $0(){var s,r try{s=new TextDecoder("utf-8",{fatal:false}) return s}catch(r){}return null}, $S:19} A.fc.prototype={ he(a1,a2,a3,a4){var s,r,q,p,o,n,m,l,k,j,i,h,g,f,e,d,c,b,a,a0="Invalid base64 encoding length " a4=A.by(a3,a4,a2.length) s=$.qE() for(r=s.length,q=a3,p=q,o=null,n=-1,m=-1,l=0;q=0&&f=0){f=B.a.B("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",e) if(f===j)continue j=f}else{if(e===-1){if(n<0){d=o==null?null:o.a.length if(d==null)d=0 n=d+(q-p) m=q}++l if(j===61)continue}j=f}if(e!==-2){if(o==null){o=new A.ah("") d=o}else d=o c=d.a+=B.a.n(a2,p,q) d.a=c+A.bx(j) p=k continue}}throw A.b(A.ad("Invalid base64 data",a2,q))}if(o!=null){r=o.a+=B.a.n(a2,p,a4) d=r.length if(n>=0)A.ow(a2,m,a4,n,l,d) else{b=B.c.ab(d-1,4)+1 if(b===1)throw A.b(A.ad(a0,a2,a4)) for(;b<4;){r+="=" o.a=r;++b}}r=o.a return B.a.az(a2,a3,a4,r.charCodeAt(0)==0?r:r)}a=a4-a3 if(n>=0)A.ow(a2,m,a4,n,l,a) else{b=B.c.ab(a,4) if(b===1)throw A.b(A.ad(a0,a2,a4)) if(b>1)a2=B.a.az(a2,a4,a4,b===2?"==":"=")}return a2}} A.jr.prototype={} A.ay.prototype={} A.fl.prototype={} A.fw.prototype={} A.ee.prototype={ b5(a,b){t.L.a(b) return B.t.a9(b)}, gaG(){return B.S}} A.lo.prototype={ a9(a){var s,r,q=A.by(0,null,a.length),p=q-0 if(p===0)return new Uint8Array(0) s=new Uint8Array(p*3) r=new A.mI(s) if(r.f_(a,0,q)!==q){B.a.B(a,q-1) r.cn()}return B.e.el(s,0,r.b)}} A.mI.prototype={ cn(){var s=this,r=s.c,q=s.b,p=s.b=q+1,o=r.length if(!(q>>18|240 q=n.b=p+1 if(!(p>>12&63|128 p=n.b=q+1 if(!(q>>6&63|128 n.b=p+1 if(!(p=r)break l.b=o+1 s[o]=p}else{o=p&64512 if(o===55296){if(l.b+4>r)break n=q+1 if(l.fv(p,B.a.t(a,n)))q=n}else if(o===56320){if(l.b+3>r)break l.cn()}else if(p<=2047){o=l.b m=o+1 if(m>=r)break l.b=m if(!(o>>6|192 l.b=m+1 s[m]=p&63|128}else{o=l.b if(o+2>=r)break m=l.b=o+1 if(!(o>>12|224 o=l.b=m+1 if(!(m>>6&63|128 l.b=o+1 if(!(o1000){s=B.c.R(b+c,2) r=q.c6(a,b,s,!1) if((q.b&1)!==0)return r return r+q.c6(a,s,c,d)}return q.fN(a,b,c,d)}, fN(a,b,c,d){var s,r,q,p,o,n,m,l,k=this,j=65533,i=k.b,h=k.c,g=new A.ah(""),f=b+1,e=a.length if(!(b>=0&&b>>q:(s&63|h<<6)>>>0 i=B.a.t(" \x000:XECCCCCN:lDb \x000:XECCCCCNvlDb \x000:XECCCCCN:lDb AAAAA\x00\x00\x00\x00\x00AAAAA00000AAAAA:::::AAAAAGG000AAAAA00KKKAAAAAG::::AAAAA:IIIIAAAAA000\x800AAAAA\x00\x00\x00\x00 AAAAA",i+q) if(i===0){g.a+=A.bx(h) if(f===c)break $label0$0 break}else if((i&1)!==0){if(r)switch(i){case 69:case 67:g.a+=A.bx(j) break case 65:g.a+=A.bx(j);--f break default:p=g.a+=A.bx(j) g.a=p+A.bx(j) break}else{k.b=i k.c=f-1 return""}i=0}if(f===c)break $label0$0 o=f+1 if(!(f>=0&&f=0&&f=0&&o=128){n=m-1 o=m break}o=m}if(n-f<20)for(l=f;l32)if(r)g.a+=A.bx(j) else{k.b=77 k.c=c return""}k.b=i k.c=h e=g.a return e.charCodeAt(0)==0?e:e}} A.i3.prototype={} A.k6.prototype={ $2(a,b){var s,r,q t.bR.a(a) s=this.b r=this.a q=s.a+=r.a q+=a.a s.a=q s.a=q+": " s.a+=A.cg(b) r.a=", "}, $S:36} A.a8.prototype={ ac(a){var s,r,q=this,p=q.c if(p===0)return q s=!q.a r=q.b p=A.b3(p,r) return new A.a8(p===0?!1:s,r,p)}, eV(a){var s,r,q,p,o,n,m,l,k=this,j=k.c if(j===0)return $.bN() s=j-a if(s<=0)return k.a?$.oo():$.bN() r=k.b q=new Uint16Array(s) for(p=r.length,o=a;o=0&&o=0&&r>>0!==0)return l.aX(0,$.jb()) for(k=0;k=0)return q.bi(b,r) return b.bi(q,!r)}, aX(a,b){var s,r,q,p=this t.d.a(b) s=p.c if(s===0)return b.ac(0) r=b.c if(r===0)return p q=p.a if(q!==b.a)return p.bV(b,q) if(A.lD(p.b,s,b.b,r)>=0)return p.bi(b,q) return b.bi(p,!q)}, bg(a,b){var s,r,q,p,o,n,m,l,k t.d.a(b) s=this.c r=b.c if(s===0||r===0)return $.bN() q=s+r p=this.b o=b.b n=new Uint16Array(q) for(m=o.length,l=0;l0?p.ac(0):p}, fd(a){var s,r,q,p=this if(p.c0)q=q.aV(0,$.nU.a_()) return p.a&&q.c>0?q.ac(0):q}, dd(a0){var s,r,q,p,o,n,m,l,k,j,i,h,g,f,e,d,c,b=this,a=b.c if(a===$.pe&&a0.c===$.pg&&b.b===$.pd&&a0.b===$.pf)return s=a0.b r=a0.c q=r-1 if(!(q>=0&&q0){o=new Uint16Array(r+5) n=A.pc(s,r,p,o) m=new Uint16Array(a+5) l=A.pc(b.b,a,p,m)}else{m=A.nV(b.b,0,a,a+2) n=r o=s l=a}q=n-1 if(!(q>=0&&q=0){if(!(l>=0&&l=0&&l=0&&n0;){c=A.tA(k,m,d);--j A.ph(c,e,0,m,j,n) if(!(d>=0&&d=l.length)return A.d(l,0) return B.c.l(-l[0])}l=m.b if(0>=l.length)return A.d(l,0) return B.c.l(l[0])}s=A.t([],t.s) l=m.a r=l?m.ac(0):m for(q=t.d;r.c>1;){p=q.a($.on()) if(p.c===0)A.J(B.K) o=r.fd(p).l(0) B.b.m(s,o) n=o.length if(n===1)B.b.m(s,"000") if(n===2)B.b.m(s,"00") if(n===3)B.b.m(s,"0") r=r.eU(p)}q=r.b if(0>=q.length)return A.d(q,0) B.b.m(s,B.c.l(q[0])) if(l)B.b.m(s,"-") return new A.e0(s,t.hF).h7(0)}, $icC:1, $iaj:1} A.lE.prototype={ $2(a,b){a=a+b&536870911 a=a+((a&524287)<<10)&536870911 return a^a>>>6}, $S:8} A.lF.prototype={ $1(a){a=a+((a&67108863)<<3)&536870911 a^=a>>>11 return a+((a&16383)<<15)&536870911}, $S:22} A.bU.prototype={ W(a,b){if(b==null)return!1 return b instanceof A.bU&&this.a===b.a&&this.b===b.b}, a8(a,b){return B.c.a8(this.a,t.cs.a(b).a)}, gI(a){var s=this.a return(s^B.c.M(s,30))&1073741823}, l(a){var s=this,r=A.re(A.rQ(s)),q=A.fs(A.rO(s)),p=A.fs(A.rK(s)),o=A.fs(A.rL(s)),n=A.fs(A.rN(s)),m=A.fs(A.rP(s)),l=A.rf(A.rM(s)),k=r+"-"+q if(s.b)return k+"-"+p+" "+o+":"+n+":"+m+"."+l+"Z" else return k+"-"+p+" "+o+":"+n+":"+m+"."+l}, $iaj:1} A.cd.prototype={ W(a,b){if(b==null)return!1 return b instanceof A.cd&&!0}, gI(a){return B.c.gI(0)}, a8(a,b){t.jS.a(b) return 0}, l(a){return""+Math.abs(0)+":00:00."+B.a.hg(B.c.l(0),6,"0")}, $iaj:1} A.lL.prototype={ l(a){return this.eX()}} A.Q.prototype={ gaW(){return A.a_(this.$thrownJsError)}} A.ds.prototype={ l(a){var s=this.a if(s!=null)return"Assertion failed: "+A.cg(s) return"Assertion failed"}} A.bn.prototype={} A.h1.prototype={ l(a){return"Throw of null."}, $ibn:1} A.bh.prototype={ gc9(){return"Invalid argument"+(!this.a?"(s)":"")}, gc8(){return""}, l(a){var s=this,r=s.c,q=r==null?"":" ("+r+")",p=s.d,o=p==null?"":": "+A.q(p),n=s.gc9()+q+o if(!s.a)return n return n+s.gc8()+": "+A.cg(s.gcE())}, gcE(){return this.b}} A.cX.prototype={ gcE(){return A.ui(this.b)}, gc9(){return"RangeError"}, gc8(){var s,r=this.e,q=this.f if(r==null)s=q!=null?": Not less than or equal to "+A.q(q):"" else if(q==null)s=": Not greater than or equal to "+A.q(r) else if(q>r)s=": Not in inclusive range "+A.q(r)+".."+A.q(q) else s=qe.length else s=!1 if(s)f=null if(f==null){if(e.length>78)e=B.a.n(e,0,75)+"..." return g+"\n"+e}for(r=1,q=0,p=!1,o=0;o1?g+(" (at line "+r+", character "+(f-q+1)+")\n"):g+(" (at character "+(f+1)+")\n") m=e.length for(o=f;o78)if(f-q<75){l=q+75 k=q j="" i="..."}else{if(m-f<75){k=m-75 l=m i=""}else{k=f-36 l=f+36 i="..."}j="..."}else{l=m k=q j="" i=""}return g+j+B.a.n(e,k,l)+i+"\n"+B.a.bg(" ",f-k+j.length)+"^\n"}else return f!=null?g+(" (at offset "+A.q(f)+")"):g}, $iac:1} A.fG.prototype={ gaW(){return null}, l(a){return"IntegerDivisionByZeroException"}, $iQ:1, $iac:1} A.e.prototype={ bz(a,b){return A.fe(this,A.v(this).h("e.E"),b)}, aj(a,b,c){var s=A.v(this) return A.nD(this,s.q(c).h("1(e.E)").a(b),s.h("e.E"),c)}, S(a,b){var s for(s=this.gE(this);s.p();)if(J.a7(s.gu(s),b))return!0 return!1}, D(a,b){var s A.v(this).h("~(e.E)").a(b) for(s=this.gE(this);s.p();)b.$1(s.gu(s))}, bQ(a,b){return A.fM(this,b,A.v(this).h("e.E"))}, gj(a){var s,r=this.gE(this) for(s=0;r.p();)++s return s}, gC(a){return!this.gE(this).p()}, gP(a){return!this.gC(this)}, a2(a,b){return A.p0(this,b,A.v(this).h("e.E"))}, gA(a){var s=this.gE(this) if(!s.p())throw A.b(A.bt()) return s.gu(s)}, v(a,b){var s,r,q A.aT(b,"index") for(s=this.gE(this),r=0;s.p();){q=s.gu(s) if(b===r)return q;++r}throw A.b(A.V(b,r,this,null,"index"))}, l(a){return A.rp(this,"(",")")}} A.L.prototype={} A.a4.prototype={ l(a){return"MapEntry("+A.q(this.a)+": "+A.q(this.b)+")"}} A.R.prototype={ gI(a){return A.r.prototype.gI.call(this,this)}, l(a){return"null"}} A.r.prototype={$ir:1, W(a,b){return this===b}, gI(a){return A.dZ(this)}, l(a){return"Instance of '"+A.kb(this)+"'"}, e2(a,b){t.bg.a(b) throw A.b(A.rC(this,b.ge0(),b.ge5(),b.ge1(),null))}, gN(a){return A.oh(this)}, toString(){return this.l(this)}} A.iI.prototype={ l(a){return""}, $iaG:1} A.ah.prototype={ gj(a){return this.a.length}, l(a){var s=this.a return s.charCodeAt(0)==0?s:s}, $itj:1} A.lh.prototype={ $2(a,b){throw A.b(A.ad("Illegal IPv4 address, "+a,this.a,b))}, $S:28} A.lj.prototype={ $2(a,b){throw A.b(A.ad("Illegal IPv6 address, "+a,this.a,b))}, $S:44} A.lk.prototype={ $2(a,b){var s if(b-a>4)this.a.$2("an IPv6 part can only contain a maximum of 4 hex digits",a) s=A.nd(B.a.n(this.b,a,b),16) if(s<0||s>65535)this.a.$2("each part must be in the range of `0x0..0xFFFF`",a) return s}, $S:8} A.eR.prototype={ gdD(){var s,r,q,p,o=this,n=o.w if(n===$){s=o.a r=s.length!==0?""+s+":":"" q=o.c p=q==null if(!p||s==="file"){s=r+"//" r=o.b if(r.length!==0)s=s+r+"@" if(!p)s+=q r=o.d if(r!=null)s=s+":"+A.q(r)}else s=r s+=o.e r=o.f if(r!=null)s=s+"?"+r r=o.r if(r!=null)s=s+"#"+r n!==$&&A.nm("_text") n=o.w=s.charCodeAt(0)==0?s:s}return n}, gcJ(){var s,r,q=this,p=q.x if(p===$){s=q.e if(s.length!==0&&B.a.t(s,0)===47)s=B.a.O(s,1) r=s.length===0?B.q:A.fN(new A.af(A.t(s.split("/"),t.s),t.ha.a(A.v6()),t.iZ),t.N) q.x!==$&&A.nm("pathSegments") q.seE(r) p=r}return p}, gI(a){var s,r=this,q=r.y if(q===$){s=B.a.gI(r.gdD()) r.y!==$&&A.nm("hashCode") r.y=s q=s}return q}, gbe(){return this.b}, gah(a){var s=this.c if(s==null)return"" if(B.a.J(s,"["))return B.a.n(s,1,s.length-1) return s}, gaP(a){var s=this.d return s==null?A.pu(this.a):s}, gaw(a){var s=this.f return s==null?"":s}, gbE(){var s=this.r return s==null?"":s}, h6(a){var s=this.a if(a.length!==s.length)return!1 return A.uo(a,s,0)>=0}, dk(a,b){var s,r,q,p,o,n for(s=0,r=0;B.a.H(b,"../",r);){r+=3;++s}q=B.a.cG(a,"/") while(!0){if(!(q>0&&s>0))break p=B.a.e_(a,"/",q-1) if(p<0)break o=q-p n=o!==2 if(!n||o===3)if(B.a.B(a,p+1)===46)n=!n||B.a.B(a,p+2)===46 else n=!1 else n=!1 if(n)break;--s q=p}return B.a.az(a,q+1,null,B.a.O(b,r-3*s))}, e9(a){return this.bd(A.li(a))}, bd(a){var s,r,q,p,o,n,m,l,k,j,i=this,h=null if(a.gal().length!==0){s=a.gal() if(a.gb9()){r=a.gbe() q=a.gah(a) p=a.gba()?a.gaP(a):h}else{p=h q=p r=""}o=A.bJ(a.gX(a)) n=a.gaK()?a.gaw(a):h}else{s=i.a if(a.gb9()){r=a.gbe() q=a.gah(a) p=A.o3(a.gba()?a.gaP(a):h,s) o=A.bJ(a.gX(a)) n=a.gaK()?a.gaw(a):h}else{r=i.b q=i.c p=i.d o=i.e if(a.gX(a)==="")n=a.gaK()?a.gaw(a):i.f else{m=A.uc(i,o) if(m>0){l=B.a.n(o,0,m) o=a.gbG()?l+A.bJ(a.gX(a)):l+A.bJ(i.dk(B.a.O(o,l.length),a.gX(a)))}else if(a.gbG())o=A.bJ(a.gX(a)) else if(o.length===0)if(q==null)o=s.length===0?a.gX(a):A.bJ(a.gX(a)) else o=A.bJ("/"+a.gX(a)) else{k=i.dk(o,a.gX(a)) j=s.length===0 if(!j||q!=null||B.a.J(o,"/"))o=A.bJ(k) else o=A.o5(k,!j||q!=null)}n=a.gaK()?a.gaw(a):h}}}return A.mG(s,r,q,p,o,n,a.gcA()?a.gbE():h)}, gb9(){return this.c!=null}, gba(){return this.d!=null}, gaK(){return this.f!=null}, gcA(){return this.r!=null}, gbG(){return B.a.J(this.e,"/")}, cS(){var s,r=this,q=r.a if(q!==""&&q!=="file")throw A.b(A.x("Cannot extract a file path from a "+q+" URI")) q=r.f if((q==null?"":q)!=="")throw A.b(A.x(u.i)) q=r.r if((q==null?"":q)!=="")throw A.b(A.x(u.l)) q=$.op() if(A.aK(q))q=A.pF(r) else{if(r.c!=null&&r.gah(r)!=="")A.J(A.x(u.j)) s=r.gcJ() A.u5(s,!1) q=A.lb(B.a.J(r.e,"/")?""+"/":"",s,"/") q=q.charCodeAt(0)==0?q:q}return q}, l(a){return this.gdD()}, W(a,b){var s,r,q=this if(b==null)return!1 if(q===b)return!0 if(t.jJ.b(b))if(q.a===b.gal())if(q.c!=null===b.gb9())if(q.b===b.gbe())if(q.gah(q)===b.gah(b))if(q.gaP(q)===b.gaP(b))if(q.e===b.gX(b)){s=q.f r=s==null if(!r===b.gaK()){if(r)s="" if(s===b.gaw(b)){s=q.r r=s==null if(!r===b.gcA()){if(r)s="" s=s===b.gbE()}else s=!1}else s=!1}else s=!1}else s=!1 else s=!1 else s=!1 else s=!1 else s=!1 else s=!1 else s=!1 return s}, seE(a){this.x=t.i.a(a)}, $ihA:1, gal(){return this.a}, gX(a){return this.e}} A.lg.prototype={ ged(){var s,r,q,p,o=this,n=null,m=o.c if(m==null){m=o.b if(0>=m.length)return A.d(m,0) s=o.a m=m[0]+1 r=B.a.aq(s,"?",m) q=s.length if(r>=0){p=A.eS(s,r+1,q,B.k,!1,!1) q=r}else p=n m=o.c=new A.hU("data","",n,n,A.eS(s,m,q,B.B,!1,!1),p,n)}return m}, l(a){var s,r=this.b if(0>=r.length)return A.d(r,0) s=this.a return r[0]===-1?"data:"+s:s}} A.mR.prototype={ $2(a,b){var s=this.a if(!(a>>0 if(!(q<96))return A.d(a,q) a[q]=c}}, $S:14} A.b5.prototype={ gb9(){return this.c>0}, gba(){return this.c>0&&this.d+1r?B.a.n(this.a,r,s-1):""}, gah(a){var s=this.c return s>0?B.a.n(this.a,s,this.d):""}, gaP(a){var s,r=this if(r.gba())return A.nd(B.a.n(r.a,r.d+1,r.e),null) s=r.b if(s===4&&B.a.J(r.a,"http"))return 80 if(s===5&&B.a.J(r.a,"https"))return 443 return 0}, gX(a){return B.a.n(this.a,this.e,this.f)}, gaw(a){var s=this.f,r=this.r return s=q.length)return s return new A.b5(B.a.n(q,0,r),s.b,s.c,s.d,s.e,s.f,r,s.w)}, e9(a){return this.bd(A.li(a))}, bd(a){if(a instanceof A.b5)return this.fo(this,a) return this.dF().bd(a)}, fo(a,b){var s,r,q,p,o,n,m,l,k,j,i,h,g,f,e,d,c=b.b if(c>0)return b s=b.c if(s>0){r=a.b if(r<=0)return b q=r===4 if(q&&B.a.J(a.a,"file"))p=b.e!==b.f else if(q&&B.a.J(a.a,"http"))p=!b.di("80") else p=!(r===5&&B.a.J(a.a,"https"))||!b.di("443") if(p){o=r+1 return new A.b5(B.a.n(a.a,0,o)+B.a.O(b.a,c+1),r,s+o,b.d+o,b.e+o,b.f+o,b.r+o,a.w)}else return this.dF().bd(b)}n=b.e c=b.f if(n===c){s=b.r if(c0?l:m o=k-n return new A.b5(B.a.n(a.a,0,k)+B.a.O(s,n),a.b,a.c,a.d,m,c+o,b.r+o,a.w)}j=a.e i=a.f if(j===i&&a.c>0){for(;B.a.H(s,"../",n);)n+=3 o=j-n+1 return new A.b5(B.a.n(a.a,0,j)+"/"+B.a.O(s,n),a.b,a.c,a.d,j,c+o,b.r+o,a.w)}h=a.a l=A.po(this) if(l>=0)g=l else for(g=j;B.a.H(h,"../",g);)g+=3 f=0 while(!0){e=n+3 if(!(e<=c&&B.a.H(s,"../",n)))break;++f n=e}for(d="";i>g;){--i if(B.a.B(h,i)===47){if(f===0){d="/" break}--f d="/"}}if(i===g&&a.b<=0&&!B.a.H(h,"/",j)){n-=f*3 d=""}o=i-n+d.length return new A.b5(B.a.n(h,0,i)+d+B.a.O(s,n),a.b,a.c,a.d,j,c+o,b.r+o,a.w)}, cS(){var s,r,q=this,p=q.b if(p>=0){s=!(p===4&&B.a.J(q.a,"file")) p=s}else p=!1 if(p)throw A.b(A.x("Cannot extract a file path from a "+q.gal()+" URI")) p=q.f s=q.a if(p0?s.gah(s):r,n=s.gba()?s.gaP(s):r,m=s.a,l=s.f,k=B.a.n(m,s.e,l),j=s.r l=l>>0!==b||b>=s r.toString if(r)throw A.b(A.V(b,s,a,null,null)) s=a[b] s.toString return s}, k(a,b,c){t.q.a(c) throw A.b(A.x("Cannot assign element of immutable List."))}, gA(a){var s if(a.length>0){s=a[0] s.toString return s}throw A.b(A.K("No elements"))}, v(a,b){if(!(b>=0&&b>>0!==b||b>=s r.toString if(r)throw A.b(A.V(b,s,a,null,null)) s=a[b] s.toString return s}, k(a,b,c){A.S(c) throw A.b(A.x("Cannot assign element of immutable List."))}, gA(a){var s if(a.length>0){s=a[0] s.toString return s}throw A.b(A.K("No elements"))}, v(a,b){if(!(b>=0&&b>>0!==b||b>=s r.toString if(r)throw A.b(A.V(b,s,a,null,null)) s=a[b] s.toString return s}, k(a,b,c){t.dY.a(c) throw A.b(A.x("Cannot assign element of immutable List."))}, gA(a){var s if(a.length>0){s=a[0] s.toString return s}throw A.b(A.K("No elements"))}, v(a,b){if(!(b>=0&&b>>0!==b||b>=s r.toString if(r)throw A.b(A.V(b,s,a,null,null)) s=a[b] s.toString return s}, k(a,b,c){t.G.a(c) throw A.b(A.x("Cannot assign element of immutable List."))}, gA(a){var s if(a.length>0){s=a[0] s.toString return s}throw A.b(A.K("No elements"))}, v(a,b){if(!(b>=0&&b>>0!==b||b>=s r.toString if(r)throw A.b(A.V(b,s,a,null,null)) s=a[b] s.toString return s}, k(a,b,c){t.ib.a(c) throw A.b(A.x("Cannot assign element of immutable List."))}, gA(a){var s if(a.length>0){s=a[0] s.toString return s}throw A.b(A.K("No elements"))}, v(a,b){if(!(b>=0&&b>>0!==b||b>=s r.toString if(r)throw A.b(A.V(b,s,a,null,null)) s=a[b] s.toString return s}, k(a,b,c){t.G.a(c) throw A.b(A.x("Cannot assign element of immutable List."))}, gA(a){var s if(a.length>0){s=a[0] s.toString return s}throw A.b(A.K("No elements"))}, v(a,b){if(!(b>=0&&b>>0!==b||b>=s r.toString if(r)throw A.b(A.V(b,s,a,null,null)) s=a[b] s.toString return s}, k(a,b,c){t.d8.a(c) throw A.b(A.x("Cannot assign element of immutable List."))}, gA(a){var s if(a.length>0){s=a[0] s.toString return s}throw A.b(A.K("No elements"))}, v(a,b){if(!(b>=0&&b>>0!==b||b>=s r.toString if(r)throw A.b(A.V(b,s,a,null,null)) s=a[b] s.toString return s}, k(a,b,c){t.ls.a(c) throw A.b(A.x("Cannot assign element of immutable List."))}, gA(a){var s if(a.length>0){s=a[0] s.toString return s}throw A.b(A.K("No elements"))}, v(a,b){if(!(b>=0&&b>>0!==b||b>=s r.toString if(r)throw A.b(A.V(b,s,a,null,null)) s=a[b] s.toString return s}, k(a,b,c){t.cA.a(c) throw A.b(A.x("Cannot assign element of immutable List."))}, gA(a){var s if(a.length>0){s=a[0] s.toString return s}throw A.b(A.K("No elements"))}, v(a,b){if(!(b>=0&&b>>0!==b||b>=s r.toString if(r)throw A.b(A.V(b,s,a,null,null)) s=a[b] s.toString return s}, k(a,b,c){t.gJ.a(c) throw A.b(A.x("Cannot assign element of immutable List."))}, gA(a){var s if(a.length>0){s=a[0] s.toString return s}throw A.b(A.K("No elements"))}, v(a,b){if(!(b>=0&&b>>0!==b||b>=s r.toString if(r)throw A.b(A.V(b,s,a,null,null)) s=a[b] s.toString return s}, k(a,b,c){t.dR.a(c) throw A.b(A.x("Cannot assign element of immutable List."))}, gA(a){var s if(a.length>0){s=a[0] s.toString return s}throw A.b(A.K("No elements"))}, v(a,b){if(!(b>=0&&b>>0!==b||b>=s r.toString if(r)throw A.b(A.V(b,s,a,null,null)) s=a[b] s.toString return s}, k(a,b,c){t.ki.a(c) throw A.b(A.x("Cannot assign element of immutable List."))}, gA(a){var s if(a.length>0){s=a[0] s.toString return s}throw A.b(A.K("No elements"))}, v(a,b){if(!(b>=0&&b>>0!==b||b>=s r.toString if(r)throw A.b(A.V(b,s,a,null,null)) s=a[b] s.toString return s}, k(a,b,c){t.d5.a(c) throw A.b(A.x("Cannot assign element of immutable List."))}, gA(a){var s if(a.length>0){s=a[0] s.toString return s}throw A.b(A.K("No elements"))}, v(a,b){if(!(b>=0&&b>>0!==b||b>=s r.toString if(r)throw A.b(A.V(b,s,a,null,null)) return a[b]}, k(a,b,c){t.ef.a(c) throw A.b(A.x("Cannot assign element of immutable List."))}, gA(a){if(a.length>0)return a[0] throw A.b(A.K("No elements"))}, v(a,b){if(!(b>=0&&b>>0!==b||b>=s r.toString if(r)throw A.b(A.V(b,s,a,null,null)) s=a[b] s.toString return s}, k(a,b,c){t.G.a(c) throw A.b(A.x("Cannot assign element of immutable List."))}, gA(a){var s if(a.length>0){s=a[0] s.toString return s}throw A.b(A.K("No elements"))}, v(a,b){if(!(b>=0&&b>>0!==b||b>=s r.toString if(r)throw A.b(A.V(b,s,a,null,null)) s=a[b] s.toString return s}, k(a,b,c){t.hI.a(c) throw A.b(A.x("Cannot assign element of immutable List."))}, gA(a){var s if(a.length>0){s=a[0] s.toString return s}throw A.b(A.K("No elements"))}, v(a,b){if(!(b>=0&&b>>0!==b||b>=s r.toString if(r)throw A.b(A.V(b,s,a,null,null)) s=a[b] s.toString return s}, k(a,b,c){t.lv.a(c) throw A.b(A.x("Cannot assign element of immutable List."))}, gA(a){var s if(a.length>0){s=a[0] s.toString return s}throw A.b(A.K("No elements"))}, v(a,b){if(!(b>=0&&b"))}, T(a,b,c,d,e){A.a0(a).h("e").a(d) throw A.b(A.x("Cannot setRange on immutable List."))}, a6(a,b,c,d){return this.T(a,b,c,d,0)}} A.dD.prototype={ p(){var s=this,r=s.c+1,q=s.b if(r=4)A.J(r.bY()) r.bW(0,s)}}, $S:2} A.bz.prototype={$ibz:1} A.ec.prototype={$iec:1} A.bD.prototype={$ibD:1} A.nj.prototype={ $1(a){return this.a.a0(0,this.b.h("0/?").a(a))}, $S:4} A.nk.prototype={ $1(a){if(a==null)return this.a.ag(new A.h0(a===undefined)) return this.a.ag(a)}, $S:4} A.h0.prototype={ l(a){return"Promise was rejected with a value of `"+(this.a?"undefined":"null")+"`."}, $iac:1} A.i8.prototype={ eB(){var s=self.crypto if(s!=null)if(s.getRandomValues!=null)return throw A.b(A.x("No source of cryptographically secure random numbers available."))}, hd(a){var s,r,q,p,o,n,m,l,k if(a<=0||a>4294967296)throw A.b(A.rV("max must be in range 0 < max \u2264 2^32, was "+a)) if(a>255)if(a>65535)s=a>16777215?4:3 else s=2 else s=1 r=this.a B.E.fm(r,0,0,!1) q=4-s p=A.j(Math.pow(256,s)) for(o=a-1,n=(a&o)===0;!0;){m=r.buffer m=new Uint8Array(m,q,s) crypto.getRandomValues(m) l=B.E.f0(r,0,!1) if(n)return(l&o)>>>0 k=l%a if(l-k+a>>0!==b||b>=s s.toString if(s)throw A.b(A.V(b,this.gj(a),a,null,null)) s=a.getItem(b) s.toString return s}, k(a,b,c){t.kT.a(c) throw A.b(A.x("Cannot assign element of immutable List."))}, gA(a){var s=a.length s.toString if(s>0){s=a[0] s.toString return s}throw A.b(A.K("No elements"))}, v(a,b){return this.i(a,b)}, $ik:1, $ie:1, $im:1} A.aR.prototype={$iaR:1} A.h3.prototype={ gj(a){var s=a.length s.toString return s}, i(a,b){var s=a.length s.toString s=b>>>0!==b||b>=s s.toString if(s)throw A.b(A.V(b,this.gj(a),a,null,null)) s=a.getItem(b) s.toString return s}, k(a,b,c){t.ai.a(c) throw A.b(A.x("Cannot assign element of immutable List."))}, gA(a){var s=a.length s.toString if(s>0){s=a[0] s.toString return s}throw A.b(A.K("No elements"))}, v(a,b){return this.i(a,b)}, $ik:1, $ie:1, $im:1} A.h8.prototype={ gj(a){return a.length}} A.hn.prototype={ gj(a){var s=a.length s.toString return s}, i(a,b){var s=a.length s.toString s=b>>>0!==b||b>=s s.toString if(s)throw A.b(A.V(b,this.gj(a),a,null,null)) s=a.getItem(b) s.toString return s}, k(a,b,c){A.S(c) throw A.b(A.x("Cannot assign element of immutable List."))}, gA(a){var s=a.length s.toString if(s>0){s=a[0] s.toString return s}throw A.b(A.K("No elements"))}, v(a,b){return this.i(a,b)}, $ik:1, $ie:1, $im:1} A.aW.prototype={$iaW:1} A.hu.prototype={ gj(a){var s=a.length s.toString return s}, i(a,b){var s=a.length s.toString s=b>>>0!==b||b>=s s.toString if(s)throw A.b(A.V(b,this.gj(a),a,null,null)) s=a.getItem(b) s.toString return s}, k(a,b,c){t.hk.a(c) throw A.b(A.x("Cannot assign element of immutable List."))}, gA(a){var s=a.length s.toString if(s>0){s=a[0] s.toString return s}throw A.b(A.K("No elements"))}, v(a,b){return this.i(a,b)}, $ik:1, $ie:1, $im:1} A.i9.prototype={} A.ia.prototype={} A.ik.prototype={} A.il.prototype={} A.iG.prototype={} A.iH.prototype={} A.iP.prototype={} A.iQ.prototype={} A.f9.prototype={ gj(a){return a.length}} A.fa.prototype={ F(a,b){return A.b7(a.get(b))!=null}, i(a,b){return A.b7(a.get(A.S(b)))}, D(a,b){var s,r,q t.u.a(b) s=a.entries() for(;!0;){r=s.next() q=r.done q.toString if(q)return q=r.value[0] q.toString b.$2(q,A.b7(r.value[1]))}}, gK(a){var s=A.t([],t.s) this.D(a,new A.jp(s)) return s}, gU(a){var s=A.t([],t.C) this.D(a,new A.jq(s)) return s}, gj(a){var s=a.size s.toString return s}, gC(a){var s=a.size s.toString return s===0}, gP(a){var s=a.size s.toString return s!==0}, G(a,b){throw A.b(A.x("Not supported"))}, $iI:1} A.jp.prototype={ $2(a,b){return B.b.m(this.a,a)}, $S:1} A.jq.prototype={ $2(a,b){return B.b.m(this.a,t.f.a(b))}, $S:1} A.fb.prototype={ gj(a){return a.length}} A.bQ.prototype={} A.h4.prototype={ gj(a){return a.length}} A.hQ.prototype={} A.h_.prototype={} A.hy.prototype={ G(a,b){return A.tq()}} A.fk.prototype={ fw(a,b){var s,r=null A.q3("absolute",A.t([b,null,null,null,null,null,null,null,null,null,null,null,null,null,null],t.mf)) s=this.a s=s.ak(b)>0&&!s.ar(b) if(s)return b s=this.b return this.dZ(0,s==null?A.v9():s,b,r,r,r,r,r,r,r,r,r,r,r,r,r,r)}, dZ(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q){var s=A.t([b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q],t.mf) A.q3("join",s) return this.h8(new A.ef(s,t.lS))}, h8(a){var s,r,q,p,o,n,m,l,k,j t.bq.a(a) for(s=a.$ti,r=s.h("aw(e.E)").a(new A.jw()),q=a.gE(a),s=new A.cq(q,r,s.h("cq")),r=this.a,p=!1,o=!1,n="";s.p();){m=q.gu(q) if(r.ar(m)&&o){l=A.rF(m,r) k=n.charCodeAt(0)==0?n:n n=B.a.n(k,0,r.aR(k,!0)) l.b=n if(r.bK(n))B.b.k(l.e,0,r.gbh()) n=""+l.l(0)}else if(r.ak(m)>0){o=!r.ar(m) n=""+m}else{j=m.length if(j!==0){if(0>=j)return A.d(m,0) j=r.cs(m[0])}else j=!1 if(!j)if(p)n+=r.gbh() n+=m}p=r.bK(m)}return n.charCodeAt(0)==0?n:n}} A.jw.prototype={ $1(a){return A.S(a)!==""}, $S:31} A.n1.prototype={ $1(a){A.o6(a) return a==null?"null":'"'+a+'"'}, $S:32} A.bV.prototype={ eg(a){var s,r=this.ak(a) if(r>0)return B.a.n(a,0,r) if(this.ar(a)){if(0>=a.length)return A.d(a,0) s=a[0]}else s=null return s}} A.k8.prototype={ l(a){var s,r,q,p=this,o=p.b o=o!=null?""+o:"" for(s=0;s0){r=B.a.aq(a,"\\",r+1) if(r>0)return r}return q}if(q<3)return 0 if(!A.qg(s))return 0 if(B.a.t(a,1)!==58)return 0 q=B.a.t(a,2) if(!(q===47||q===92))return 0 return 3}, ak(a){return this.aR(a,!1)}, ar(a){return this.ak(a)===1}, gaO(){return"windows"}, gbh(){return"\\"}} A.n3.prototype={ $1(a){return A.uZ(a)}, $S:33} A.dy.prototype={ l(a){return"DatabaseException("+this.a+")"}, $iac:1} A.e3.prototype={ l(a){return this.em(0)}, bS(){var s=this.b if(s==null){s=new A.ko(this).$0() this.sfg(s)}return s}, sfg(a){this.b=A.dj(a)}} A.ko.prototype={ $0(){var s=new A.kp(this.a.a.toLowerCase()),r=s.$1("(sqlite code ") if(r!=null)return r r=s.$1("(code ") if(r!=null)return r r=s.$1("code=") if(r!=null)return r return null}, $S:34} A.kp.prototype={ $1(a){var s,r,q,p,o,n=this.a,m=B.a.cB(n,a) if(!J.a7(m,-1))try{p=m if(typeof p!=="number")return p.bf() p=B.a.hn(B.a.O(n,p+a.length)).split(" ") if(0>=p.length)return A.d(p,0) s=p[0] r=J.qY(s,")") if(!J.a7(r,-1))s=J.r2(s,0,r) q=A.nE(s,null) if(q!=null)return q}catch(o){}return null}, $S:35} A.jB.prototype={} A.fx.prototype={ l(a){return A.oh(this).l(0)+"("+this.a+", "+A.q(this.b)+")"}} A.cH.prototype={} A.bl.prototype={ l(a){var s,r=this,q=t.N,p=t.X,o=A.X(q,p),n=r.x if(n!=null){n=A.nB(n,q,p) s=n.fE(n,q,p) p=s.a q=J.b8(p) n=s.$ti.h("4?") n.a(q.G(p,"arguments")) n.a(q.G(p,"sql")) if(q.gP(p))o.k(0,"details",s)}q=r.bS()==null?"":": "+A.q(r.bS())+", " q=""+("SqfliteFfiException("+r.w+q+", "+r.a+"})") p=r.f if(p!=null){q+=" sql "+p p=r.r p=p==null?null:!p.gC(p) if(p===!0){p=r.r p.toString p=q+(" args "+A.q6(p)) q=p}}else q+=" "+r.ev(0) if(o.a!==0)q+=" "+o.l(0) return q.charCodeAt(0)==0?q:q}, sfP(a,b){this.x=t.h9.a(b)}} A.kC.prototype={} A.e6.prototype={ l(a){var s=this.a,r=this.b,q=this.c,p=q==null?null:!q.gC(q) if(p===!0){q.toString q=" "+A.q6(q)}else q="" return A.q(s)+" "+(A.q(r)+q)}, sej(a){this.c=t.kR.a(a)}} A.iB.prototype={} A.iq.prototype={ L(){var s=0,r=A.B(t.H),q=1,p,o=this,n,m,l,k var $async$L=A.C(function(a,b){if(a===1){p=b s=q}while(true)switch(s){case 0:q=3 s=6 return A.p(o.a.$0(),$async$L) case 6:n=b o.b.a0(0,n) q=1 s=5 break case 3:q=2 k=p m=A.M(k) o.b.ag(m) s=5 break case 2:s=1 break case 5:return A.z(null,r) case 1:return A.y(p,r)}}) return A.A($async$L,r)}} A.aU.prototype={ ec(){var s=this return A.aO(["path",s.r,"id",s.e,"readOnly",s.w,"singleInstance",s.f],t.N,t.X)}, df(){var s,r=this if(r.dg()===0)return null s=r.x.b s=s.a.rx.$1(s.b) s=self.Number(s==null?t.K.a(s):s) if(r.y>=1)A.b9("[sqflite-"+r.e+"] Inserted "+A.q(s)) return s}, l(a){return A.jZ(this.ec())}, af(a){var s=this s.bl() s.av("Closing database "+s.l(0)) s.x.a1()}, ca(a){var s=a==null?null:new A.ba(a.a,a.$ti.h("ba<1,r?>")) return s==null?B.m:s}, fZ(a,b){return this.d.a7(new A.kx(this,a,b),t.H)}, ae(a,b){return this.f3(a,b)}, f3(a,b){var s=0,r=A.B(t.H),q,p=[],o=this,n,m,l var $async$ae=A.C(function(c,d){if(c===1)return A.y(d,r) while(true)switch(s){case 0:o.cI(a,b) m=b==null?null:!b.gC(b) l=o.x if(m===!0){n=l.cL(a) try{n.bC(o.ca(b)) s=1 break}finally{n.a1()}}else l.bC(a) case 1:return A.z(q,r)}}) return A.A($async$ae,r)}, av(a){if(a!=null&&this.y>=1)A.b9("[sqflite-"+this.e+"] "+A.q(a))}, cI(a,b){var s if(this.y>=1){s=b==null?null:!b.gC(b) s=s===!0?" "+A.q(b):"" A.b9("[sqflite-"+this.e+"] "+a+s) this.av(null)}}, bu(){var s=0,r=A.B(t.H),q=this var $async$bu=A.C(function(a,b){if(a===1)return A.y(b,r) while(true)switch(s){case 0:s=q.c.length!==0?2:3 break case 2:s=4 return A.p(q.as.a7(new A.kv(q),t.P),$async$bu) case 4:case 3:return A.z(null,r)}}) return A.A($async$bu,r)}, bl(){var s=0,r=A.B(t.H),q=this var $async$bl=A.C(function(a,b){if(a===1)return A.y(b,r) while(true)switch(s){case 0:s=q.c.length!==0?2:3 break case 2:s=4 return A.p(q.as.a7(new A.kq(q),t.P),$async$bl) case 4:case 3:return A.z(null,r)}}) return A.A($async$bl,r)}, b8(a,b){return this.h2(a,t.gq.a(b))}, h2(a,b){var s=0,r=A.B(t.z),q,p=2,o,n=[],m=this,l var $async$b8=A.C(function(c,d){if(c===1){o=d s=p}while(true)switch(s){case 0:l=m.b s=l==null?3:5 break case 3:s=6 return A.p(b.$0(),$async$b8) case 6:q=d s=1 break s=4 break case 5:s=a===l||a===-1?7:9 break case 7:p=10 s=13 return A.p(b.$0(),$async$b8) case 13:l=d q=l n=[1] s=11 break n.push(12) s=11 break case 10:n=[2] case 11:p=2 if(m.b==null)m.bu() s=n.pop() break case 12:s=8 break case 9:l=new A.E($.D,t.D) B.b.m(m.c,new A.iq(b,new A.cr(l,t.ou))) q=l s=1 break case 8:case 4:case 1:return A.z(q,r) case 2:return A.y(o,r)}}) return A.A($async$b8,r)}, h_(a,b){return this.d.a7(new A.ky(this,a,b),t.I)}, bm(a,b){var s=0,r=A.B(t.I),q,p=this,o var $async$bm=A.C(function(c,d){if(c===1)return A.y(d,r) while(true)switch(s){case 0:if(p.w)A.J(A.hh("sqlite_error",null,"Database readonly",null)) s=3 return A.p(p.ae(a,b),$async$bm) case 3:o=p.df() if(p.y>=1)A.b9("[sqflite-"+p.e+"] Inserted id "+A.q(o)) q=o s=1 break case 1:return A.z(q,r)}}) return A.A($async$bm,r)}, h3(a,b){return this.d.a7(new A.kB(this,a,b),t.S)}, bo(a,b){var s=0,r=A.B(t.S),q,p=this var $async$bo=A.C(function(c,d){if(c===1)return A.y(d,r) while(true)switch(s){case 0:if(p.w)A.J(A.hh("sqlite_error",null,"Database readonly",null)) s=3 return A.p(p.ae(a,b),$async$bo) case 3:q=p.dg() s=1 break case 1:return A.z(q,r)}}) return A.A($async$bo,r)}, h0(a,b,c){return this.d.a7(new A.kA(this,a,c,b),t.z)}, bn(a,b){return this.f4(a,b)}, f4(a,b){var s=0,r=A.B(t.z),q,p=[],o=this,n,m,l,k,j var $async$bn=A.C(function(c,d){if(c===1)return A.y(d,r) while(true)switch(s){case 0:j=o.x.cL(a) try{o.cI(a,b) m=j l=o.ca(b) k=m.c if(k.d)A.J(A.K(u.n)) k.bs() m.f=null m.bZ(l) n=m.fj() o.av("Found "+n.d.length+" rows") m=n m=A.aO(["columns",m.a,"rows",m.d],t.N,t.X) q=m s=1 break}finally{j.a1()}case 1:return A.z(q,r)}}) return A.A($async$bn,r)}, dw(a){var s,r,q,p,o,n,m,l,k=a.a,j=k try{s=a.d r=s.a q=A.t([],t.dO) for(n=a.c;!0;){if(s.p()){m=s.x m===$&&A.aZ("current") p=m J.qO(q,p.b)}else{a.e=!0 break}if(J.Y(q)>=n)break}o=A.aO(["columns",r,"rows",q],t.N,t.X) if(!a.e)J.nq(o,"cursorId",k) return o}catch(l){this.c3(j) throw l}finally{if(a.e)this.c3(j)}}, cd(a,b,c){var s=0,r=A.B(t.X),q,p=this,o,n,m,l,k var $async$cd=A.C(function(d,e){if(d===1)return A.y(e,r) while(true)switch(s){case 0:k=p.x.cL(b) p.cI(b,c) o=p.ca(c) n=k.c if(n.d)A.J(A.K(u.n)) n.bs() k.f=null k.bZ(o) o=k.gc5() k.gdB() m=new A.hL(k,o,B.C) m.c_() n.c=!1 k.f=m n=++p.Q l=new A.iB(n,k,a,m) p.z.k(0,n,l) q=p.dw(l) s=1 break case 1:return A.z(q,r)}}) return A.A($async$cd,r)}, h1(a,b){return this.d.a7(new A.kz(this,b,a),t.z)}, ce(a,b){var s=0,r=A.B(t.X),q,p=this,o,n var $async$ce=A.C(function(c,d){if(c===1)return A.y(d,r) while(true)switch(s){case 0:if(p.y>=2){o=a===!0?" (cancel)":"" p.av("queryCursorNext "+b+o)}n=p.z.i(0,b) if(a===!0){p.c3(b) q=null s=1 break}if(n==null)throw A.b(A.K("Cursor "+b+" not found")) q=p.dw(n) s=1 break case 1:return A.z(q,r)}}) return A.A($async$ce,r)}, c3(a){var s=this.z.G(0,a) if(s!=null){if(this.y>=2)this.av("Closing cursor "+a) s.b.a1()}}, dg(){var s=this.x.b,r=A.j(s.a.RG.$1(s.b)) if(this.y>=1)A.b9("[sqflite-"+this.e+"] Modified "+r+" rows") return r}, fW(a,b,c){return this.d.a7(new A.kw(this,t.fr.a(c),b,a),t.z)}, an(a,b,c){return this.f2(a,b,t.fr.a(c))}, f2(b3,b4,b5){var s=0,r=A.B(t.z),q,p=2,o,n=this,m,l,k,j,i,h,g,f,e,d,c,b,a,a0,a1,a2,a3,a4,a5,a6,a7,a8,a9,b0,b1,b2 var $async$an=A.C(function(b6,b7){if(b6===1){o=b7 s=p}while(true)switch(s){case 0:a8={} a8.a=null d=!b4 if(d)a8.a=A.t([],t.ke) c=b5.length,b=n.y>=1,a=n.x.b,a0=a.b,a=a.a.RG,a1="[sqflite-"+n.e+"] Modified ",a2=0 case 3:if(!(a20)A.J(A.oF("BigInt value exceeds the range of 64 bits")) A.j(l.$3(b,j,self.BigInt(i.l(0))))}else if(A.cz(i))A.j(l.$3(b,j,self.BigInt(i?1:0))) else if(typeof i=="number")A.j(n.$3(b,j,i)) else if(typeof i=="string"){p.a(i) h=B.f.gaG().a9(i) g=c.cq(h) B.b.m(d,g) A.j(o.$5(b,j,g,h.length,0))}else if(s.b(i)){s.a(i) g=c.cq(i) B.b.m(d,g) A.j(q.$5(b,j,g,self.BigInt(J.Y(i)),0))}else A.J(A.bq(i,"params["+j+"]","Allowed parameters must either be null or bool, int, num, String or List."))}this.e=a0}, a1(){var s,r=this.c if(!r.d){$.jc().a.unregister(this) r.a1() s=this.b if(!s.e)B.b.G(s.c.d,r)}}, bC(a){var s=this,r=s.c if(r.d)A.J(A.K(u.n)) r.bs() s.f=null s.bZ(a) s.eZ()}, $irc:1} A.hL.prototype={ gu(a){var s=this.x s===$&&A.aZ("current") return s}, p(){var s,r,q,p,o=this,n=o.r if(n.c.d||n.f!==o)return!1 s=n.a r=s.c s=s.b q=A.j(r.fx.$1(s)) if(q===100){if(!o.y){o.w=A.j(r.dx.$1(s)) o.sfh(t.i.a(n.gc5())) o.c_() o.y=!0}s=[] for(p=0;p=0&&b>>0!==b||b>=s.length)return A.d(s,b) return s[b]}return null}r=this.a.c.i(0,b) if(r==null)return null s=this.b if(r>>>0!==r||r>=s.length)return A.d(s,r) return s[r]}, gK(a){return this.a.a}, gU(a){return this.b}, $iI:1} A.ir.prototype={ gu(a){var s=this.a,r=s.d,q=this.b if(!(q>=0&&q") l=A.fM(new A.bv(n,m),!0,m.h("e.E")) B.b.eh(l) m=A.av(l) s=3 return A.p(A.nv(new A.af(l,m.h("H<~>(1)").a(new A.jk(new A.jl(k,a),b)),m.h("af<1,H<~>>")),t.H),$async$ap) case 3:k=J.T(o) s=b.c!==k.gj(o)?4:5 break case 4:n=p.objectStore("files") n.toString n=B.i.e4(n,a) j=B.p s=7 return A.p(n.gA(n),$async$ap) case 7:s=6 return A.p(j.cV(d,{name:k.gaO(o),length:b.c}),$async$ap) case 6:case 5:return A.z(null,r)}}) return A.A($async$ap,r)}, aA(a,b,c){return this.ho(0,A.j(b),c)}, ho(a,b,c){var s=0,r=A.B(t.H),q=this,p,o,n,m,l,k,j var $async$aA=A.C(function(d,e){if(d===1)return A.y(e,r) while(true)switch(s){case 0:k=q.a k.toString p=B.h.bR(k,B.n,"readwrite") k=p.objectStore("files") k.toString o=p.objectStore("blocks") o.toString s=2 return A.p(q.cj(p,b),$async$aA) case 2:n=e m=J.T(n) s=m.gj(n)>c?3:4 break case 3:l=t.t s=5 return A.p(B.i.cv(o,self.IDBKeyRange.bound(A.t([b,B.c.R(c,4096)*4096+1],l),A.t([b,9007199254740992],l))),$async$aA) case 5:case 4:k=B.i.e4(k,b) j=B.p s=7 return A.p(k.gA(k),$async$aA) case 7:s=6 return A.p(j.cV(e,{name:m.gaO(n),length:c}),$async$aA) case 6:return A.z(null,r)}}) return A.A($async$aA,r)}, aa(a){var s=0,r=A.B(t.H),q=this,p,o,n,m var $async$aa=A.C(function(b,c){if(b===1)return A.y(c,r) while(true)switch(s){case 0:m=q.a m.toString p=B.h.bR(m,B.n,"readwrite") m=t.t o=self.IDBKeyRange.bound(A.t([a,0],m),A.t([a,9007199254740992],m)) m=p.objectStore("blocks") m.toString m=B.i.cv(m,o) n=p.objectStore("files") n.toString s=2 return A.p(A.nv(A.t([m,B.i.cv(n,a)],t.iw),t.H),$async$aa) case 2:return A.z(null,r)}}) return A.A($async$aa,r)}, seT(a){this.a=t.k5.a(a)}} A.jn.prototype={ $1(a){var s,r,q,p t.bo.a(a) s=t.E.a(new A.c2([],[]).aF(a.target.result,!1)) r=a.oldVersion if(r==null||r===0){q=B.h.dP(s,"files",!0) r=t.z p=A.X(r,r) p.k(0,"unique",!0) B.i.eQ(q,"fileName","name",p) B.h.fM(s,"blocks")}}, $S:57} A.jm.prototype={ $1(a){return this.a.ag("Opening database blocked: "+A.q(a))}, $S:2} A.jj.prototype={ $1(a){t.jV.a(a) if(a==null)throw A.b(A.bq(this.a,"fileId","File not found in database")) else return a}, $S:58} A.jo.prototype={ $0(){var s=0,r=A.B(t.H),q=this,p,o,n,m var $async$$0=A.C(function(a,b){if(a===1)return A.y(b,r) while(true)switch(s){case 0:p=B.e o=q.b n=q.c m=A s=2 return A.p(A.ke(t.w.a(new A.c2([],[]).aF(q.a.value,!1))),$async$$0) case 2:p.am(o,n,m.b_(b.buffer,0,q.d)) return A.z(null,r)}}) return A.A($async$$0,r)}, $S:3} A.jl.prototype={ $2(a,b){var s=0,r=A.B(t.H),q=this,p,o,n,m,l var $async$$2=A.C(function(c,d){if(c===1)return A.y(d,r) while(true)switch(s){case 0:p=q.a o=q.b n=t.t s=2 return A.p(A.nF(A.n4(p,"openCursor",[self.IDBKeyRange.only(A.t([o,a],n))],t.B),!0,t.g9),$async$$2) case 2:m=d l=A.r4(A.t([b],t.bs)) s=m==null?3:5 break case 3:s=6 return A.p(B.i.hh(p,l,A.t([o,a],n)),$async$$2) case 6:s=4 break case 5:s=7 return A.p(B.p.cV(m,l),$async$$2) case 7:case 4:return A.z(null,r)}}) return A.A($async$$2,r)}, $S:59} A.jk.prototype={ $1(a){var s A.j(a) s=this.b.b.i(0,a) s.toString return this.a.$2(a,s)}, $S:78} A.bg.prototype={} A.lP.prototype={ ft(a,b,c){B.e.am(this.b.e8(0,a,new A.lQ(this,a)),b,c)}, fC(a,b){var s,r,q,p,o,n,m,l,k for(s=b.length,r=0;rp)B.e.am(s,0,A.b_(r.buffer,r.byteOffset+p,A.dj(Math.min(4096,q-p)))) return s}, $S:61} A.im.prototype={} A.cM.prototype={ b3(a){var s=this.a.a if(s==null)A.J(A.bd(10,"FileSystem closed")) if(a.cD(this.e)){this.dA() return a.d.a}else return A.oH(null,t.H)}, dA(){var s,r,q=this if(q.c==null){s=q.e s=!s.gC(s)}else s=!1 if(s){s=q.e r=q.c=s.gA(s) s.G(0,r) r.d.a0(0,A.rl(r.gbO(),t.H).aS(new A.jM(q)))}}, aD(a){var s=0,r=A.B(t.S),q,p=this,o,n var $async$aD=A.C(function(b,c){if(b===1)return A.y(c,r) while(true)switch(s){case 0:n=p.r s=n.F(0,a)?3:5 break case 3:n=n.i(0,a) n.toString q=n s=1 break s=4 break case 5:s=6 return A.p(p.a.bD(a),$async$aD) case 6:o=c o.toString n.k(0,a,o) q=o s=1 break case 4:case 1:return A.z(q,r)}}) return A.A($async$aD,r)}, b2(){var s=0,r=A.B(t.H),q=this,p,o,n,m,l,k,j var $async$b2=A.C(function(a,b){if(a===1)return A.y(b,r) while(true)switch(s){case 0:m=q.a s=2 return A.p(m.bJ(),$async$b2) case 2:l=b q.r.b4(0,l) p=J.ot(l),p=p.gE(p),o=q.d.a case 3:if(!p.p()){s=4 break}n=p.gu(p) k=o j=n.a s=5 return A.p(m.aQ(n.b),$async$b2) case 5:k.k(0,j,b) s=3 break case 4:return A.z(null,r)}}) return A.A($async$b2,r)}, fT(a){return this.b3(new A.d9(t.M.a(new A.jN()),new A.aa(new A.E($.D,t.D),t.F)))}, bB(a,b,c,d){var s,r=this,q=r.a.a if(q==null)A.J(A.bd(10,"FileSystem closed")) q=r.d s=q.a.F(0,b) q.bB(0,b,c,d) if(!s)r.b3(new A.ct(r,b,new A.aa(new A.E($.D,t.D),t.F)))}, ct(){var s,r=this.a.a if(r==null)A.J(A.bd(10,"FileSystem closed")) s=this.d.ct() this.f.m(0,s) return s}, aa(a){var s=this s.d.aa(a) if(!s.f.G(0,a))s.b3(new A.d8(s,a,new A.aa(new A.E($.D,t.D),t.F)))}, cw(a){var s=this.a.a if(s==null)A.J(A.bd(10,"FileSystem closed")) return this.d.a.F(0,a)}, cM(a,b,c,d){var s A.j(d) s=this.a.a if(s==null)A.J(A.bd(10,"FileSystem closed")) return this.d.cM(0,b,c,d)}, bU(a){var s=this.a.a if(s==null)A.J(A.bd(10,"FileSystem closed")) return this.d.bU(a)}, cU(a,b){var s=this,r=s.a.a if(r==null)A.J(A.bd(10,"FileSystem closed")) s.d.cU(a,b) if(!s.f.S(0,a))s.b3(new A.d9(t.M.a(new A.jO(s,a,b)),new A.aa(new A.E($.D,t.D),t.F)))}, cX(a,b,c,d){var s,r,q,p=this A.j(d) s=p.a.a if(s==null)A.J(A.bd(10,"FileSystem closed")) s=p.d r=s.a.i(0,b) if(r==null)r=new Uint8Array(0) s.cX(0,b,c,d) if(!p.f.S(0,b)){s=A.t([],t.o6) q=$.D B.b.m(s,new A.im(d,c)) p.b3(new A.cy(p,b,r,s,new A.aa(new A.E(q,t.D),t.F)))}}, $ijD:1} A.jM.prototype={ $0(){var s=this.a s.c=null s.dA()}, $S:6} A.jN.prototype={ $0(){}, $S:6} A.jO.prototype={ $0(){var s=0,r=A.B(t.H),q,p=this,o,n var $async$$0=A.C(function(a,b){if(a===1)return A.y(b,r) while(true)switch(s){case 0:o=p.a n=o.a s=3 return A.p(o.aD(p.b),$async$$0) case 3:q=n.aA(0,b,p.c) s=1 break case 1:return A.z(q,r)}}) return A.A($async$$0,r)}, $S:3} A.a9.prototype={ cD(a){t.h.a(a) a.$ti.c.a(this) a.cf(a.c,this,!1) return!0}} A.d9.prototype={ L(){return this.w.$0()}} A.d8.prototype={ cD(a){var s,r,q,p t.h.a(a) if(!a.gC(a)){s=a.gai(a) for(r=this.x;s!=null;)if(s instanceof A.d8)if(s.x===r)return!1 else s=s.gbc() else if(s instanceof A.cy){q=s.gbc() if(s.x===r){p=s.a p.toString p.cm(A.v(s).h("ae.E").a(s))}s=q}else if(s instanceof A.ct){if(s.x===r){r=s.a r.toString r.cm(A.v(s).h("ae.E").a(s)) return!1}s=s.gbc()}else break}a.$ti.c.a(this) a.cf(a.c,this,!1) return!0}, L(){var s=0,r=A.B(t.H),q=this,p,o,n var $async$L=A.C(function(a,b){if(a===1)return A.y(b,r) while(true)switch(s){case 0:p=q.w o=q.x s=2 return A.p(p.aD(o),$async$L) case 2:n=b p.r.G(0,o) s=3 return A.p(p.a.aa(n),$async$L) case 3:return A.z(null,r)}}) return A.A($async$L,r)}} A.ct.prototype={ L(){var s=0,r=A.B(t.H),q=this,p,o,n,m,l var $async$L=A.C(function(a,b){if(a===1)return A.y(b,r) while(true)switch(s){case 0:p=q.w o=q.x n=p.a.a n.toString n=B.h.cT(n,"files","readwrite").objectStore("files") n.toString m=p.r l=o s=2 return A.p(A.nF(A.rD(n,{name:o,length:0}),!0,t.S),$async$L) case 2:m.k(0,l,b) return A.z(null,r)}}) return A.A($async$L,r)}} A.cy.prototype={ cD(a){var s,r t.h.a(a) s=a.b===0?null:a.gai(a) for(r=this.x;s!=null;)if(s instanceof A.cy)if(s.x===r){B.b.b4(s.z,this.z) return!1}else s=s.gbc() else if(s instanceof A.ct){if(s.x===r)break s=s.gbc()}else break a.$ti.c.a(this) a.cf(a.c,this,!1) return!0}, L(){var s=0,r=A.B(t.H),q=this,p,o,n,m,l,k var $async$L=A.C(function(a,b){if(a===1)return A.y(b,r) while(true)switch(s){case 0:m=q.y l=new A.lP(m,A.X(t.S,t.p),m.length) for(m=q.z,p=m.length,o=0;o")),p=t.ng,o=t.Y,n=t.K,q=q.h("h.E"),m=this.b,l=this.a;r.p();){k=r.d if(k==null)k=q.a(k) j=n.a(s.gdS(a)[k]) if(o.b(j))l.k(0,k,j) else if(p.b(j))m.k(0,k,j)}}} A.ls.prototype={ $2(a,b){var s A.S(a) t.lK.a(b) s={} this.a[a]=s J.bo(b,new A.lr(s))}, $S:62} A.lr.prototype={ $2(a,b){this.a[A.S(a)]=t.K.a(b)}, $S:63} A.k1.prototype={} A.cU.prototype={} A.cK.prototype={} A.hH.prototype={} A.hE.prototype={ by(a,b){var s,r,q t.L.a(a) s=J.T(a) r=A.j(this.d.$1(s.gj(a)+b)) q=A.b_(J.bO(this.b),0,null) B.e.a6(q,r,r+s.gj(a),a) B.e.dT(q,r+s.gj(a),r+s.gj(a)+b,0) return r}, cq(a){return this.by(a,0)}, ek(a,b){var s=this.go.$2(a,b) return new A.cP(s==null?t.K.a(s):s)}} A.m4.prototype={ eA(a){var s,r,q,p=this,o=t.gt.a(new self.WebAssembly.Memory({initial:16})) p.c=o s=t.N r=t.K q=t.Y p.seD(t.n2.a(A.aO(["env",A.aO(["memory",o],s,r),"dart",A.aO(["random",A.a6(new A.m5(o,a),q),"error_log",A.a6(new A.m6(o),q),"now",A.a6(new A.m7(),q),"path_normalize",A.a6(new A.mh(o),q),"function_xFunc",A.a6(new A.mi(p),q),"function_xStep",A.a6(new A.mj(p),q),"function_xInverse",A.a6(new A.mk(p),q),"function_xFinal",A.a6(new A.ml(p),q),"function_xValue",A.a6(new A.mm(p),q),"function_forget",A.a6(new A.mn(p),q),"function_compare",A.a6(new A.mo(p,o),q),"function_hook",A.a6(new A.m8(p,o),q),"fs_create",A.a6(new A.m9(o,a),q),"fs_temp_create",A.a6(new A.ma(p,a),q),"fs_size",A.a6(new A.mb(p,a,o),q),"fs_truncate",A.a6(new A.mc(a,o),q),"fs_read",A.a6(new A.md(a,o),q),"fs_write",A.a6(new A.me(a,o),q),"fs_delete",A.a6(new A.mf(a,o),q),"fs_access",A.a6(new A.mg(p,a,o),q)],s,r)],s,t.lK)))}, seD(a){this.b=t.n2.a(a)}} A.m5.prototype={ $2(a,b){var s,r,q,p A.j(a) A.j(b) s=A.b_(this.a.buffer,a,b) r=this.b.a for(q=s.length,p=0;p=c)return 1 else{B.e.am(A.b_(s.buffer,b,c),0,q) return 0}}, $C:"$3", $R:3, $S:20} A.mi.prototype={ $3(a,b,c){var s,r A.j(a) A.j(b) A.j(c) s=this.a r=s.a r===$&&A.aZ("bindings") s.d.b.i(0,A.j(r.ry.$1(a))).ghv().$2(new A.cp(),new A.d4(s.a,b,c))}, $C:"$3", $R:3, $S:12} A.mj.prototype={ $3(a,b,c){var s,r A.j(a) A.j(b) A.j(c) s=this.a r=s.a r===$&&A.aZ("bindings") s.d.b.i(0,A.j(r.ry.$1(a))).ghx().$2(new A.cp(),new A.d4(s.a,b,c))}, $C:"$3", $R:3, $S:12} A.mk.prototype={ $3(a,b,c){var s,r A.j(a) A.j(b) A.j(c) s=this.a r=s.a r===$&&A.aZ("bindings") s.d.b.i(0,A.j(r.ry.$1(a))).ghw().$2(new A.cp(),new A.d4(s.a,b,c))}, $C:"$3", $R:3, $S:12} A.ml.prototype={ $1(a){var s,r A.j(a) s=this.a r=s.a r===$&&A.aZ("bindings") s.d.b.i(0,A.j(r.ry.$1(a))).ghu().$1(new A.cp())}, $S:9} A.mm.prototype={ $1(a){var s,r A.j(a) s=this.a r=s.a r===$&&A.aZ("bindings") s.d.b.i(0,A.j(r.ry.$1(a))).ghy().$1(new A.cp())}, $S:9} A.mn.prototype={ $1(a){this.a.d.b.G(0,A.j(a))}, $S:9} A.mo.prototype={ $5(a,b,c,d,e){var s,r,q A.j(a) A.j(b) A.j(c) A.j(d) A.j(e) s=this.b r=A.oU(s,c,b) q=A.oU(s,e,d) return this.a.d.b.i(0,a).ghs().$2(r,q)}, $C:"$5", $R:5, $S:69} A.m8.prototype={ $5(a,b,c,d,e){A.j(a) A.j(b) A.j(c) A.j(d) t.K.a(e) A.b0(this.b,d)}, $C:"$5", $R:5, $S:70} A.m9.prototype={ $2(a,b){var s,r,q,p,o,n A.j(a) A.j(b) s=A.b0(this.a,a) r=(b&4)!==0 q=(b&16)!==0 try{this.b.b.bB(0,s,q,!A.aK(r)) return 0}catch(o){n=A.M(o) if(n instanceof A.bc){p=n return p.a}else throw o}}, $S:8} A.ma.prototype={ $0(){var s=this.b.b.ct(),r=this.a.a r===$&&A.aZ("bindings") t.O.h("ay.S").a(s) return r.by(B.f.gaG().a9(s),1)}, $S:71} A.mb.prototype={ $2(a,b){var s,r,q,p,o,n,m A.j(a) A.j(b) try{s=this.b.b.bU(A.b0(this.c,a)) q=this.a.a q===$&&A.aZ("bindings") q=q.b p=J.a2(q) o=A.dS(p.gaE(q),0,null) n=B.c.M(b,2) if(!(n()","~(@)","H<@>()","R()","~(@,@)","c(c,c)","R(c)","H()","~(~())","R(c,c,c)","H<@>(aU)","~(aX,i,c)","c(r?)","R(@)","c(c,c,c,r)","H()","@()","c(c,c,c)","~(r,aG)","c(c)","aw(r?,r?)","~(i,i)","H<~>(l)","~(r[aG?])","H>()","~(i,c)","R(@,@)","@(@,@)","aw(i)","i(i?)","i?(r?)","c?()","c?(i)","~(co,@)","H()","H()","~(r?,r?)","aw(@)","I(bl)","~(@[@])","bl(@)","~(i,c?)","I<@,@>(c)","~(I<@,@>)","E<@>(@)","H(aU)","H(aU)","H(aU)","H()","i(i)","R(r,aG)","a4(c,be)","i(r?)","~(bs)","~(bD)","bg(bg?)","H<~>(c,aX)","aX(@,@)","aX()","~(i,I)","~(i,r)","R(c,c)","~(cH)","cP()","~(c,@)","R(@,aG)","c(c,c,c,c,c)","R(c,c,c,c,r)","c()","R(~())","c(@,@)","@(i)","~(bF?,nR?,bF,~())","@(@,i)","@(@)","H<~>(c)"],interceptorsByTag:null,leafTags:null,arrayRti:Symbol("$ti")} A.u0(v.typeUniverse,JSON.parse('{"h6":"U","c_":"U","bu":"U","bg":"U","jR":"U","lf":"U","jC":"U","kk":"U","kj":"U","mr":"U","l4":"U","fy":"U","jE":"U","jF":"U","jH":"U","m3":"U","mt":"U","jG":"U","jA":"U","dc":"U","cK":"U","mK":"U","k1":"U","cU":"U","vS":"a","vT":"a","vB":"a","vz":"l","vO":"l","vC":"bQ","vA":"f","vY":"f","w1":"f","vU":"n","vX":"bz","vD":"o","vV":"o","vQ":"G","vN":"G","wl":"am","vM":"c1","vE":"bi","w8":"bi","vR":"ci","vF":"P","vH":"bb","vJ":"al","vK":"aq","vG":"aq","vI":"aq","fH":{"aw":[]},"dK":{"R":[]},"U":{"a":[],"ny":[],"bg":[],"dc":[],"cU":[],"cK":[]},"O":{"m":["1"],"k":["1"],"e":["1"]},"jQ":{"O":["1"],"m":["1"],"k":["1"],"e":["1"]},"ca":{"L":["1"]},"cO":{"N":[],"W":[],"aj":["W"]},"dJ":{"N":[],"c":[],"W":[],"aj":["W"]},"fJ":{"N":[],"W":[],"aj":["W"]},"bW":{"i":[],"aj":["i"],"k9":[]},"c3":{"e":["2"]},"du":{"L":["2"]},"cb":{"c3":["1","2"],"e":["2"],"e.E":"2"},"ep":{"cb":["1","2"],"c3":["1","2"],"k":["2"],"e":["2"],"e.E":"2"},"ek":{"h":["2"],"m":["2"],"c3":["1","2"],"k":["2"],"e":["2"]},"ba":{"ek":["1","2"],"h":["2"],"m":["2"],"c3":["1","2"],"k":["2"],"e":["2"],"h.E":"2","e.E":"2"},"dv":{"w":["3","4"],"I":["3","4"],"w.K":"3","w.V":"4"},"cQ":{"Q":[]},"fh":{"h":["c"],"c0":["c"],"m":["c"],"k":["c"],"e":["c"],"h.E":"c","c0.E":"c"},"k":{"e":["1"]},"a3":{"k":["1"],"e":["1"]},"cn":{"a3":["1"],"k":["1"],"e":["1"],"a3.E":"1","e.E":"1"},"aP":{"L":["1"]},"bw":{"e":["2"],"e.E":"2"},"ce":{"bw":["1","2"],"k":["2"],"e":["2"],"e.E":"2"},"dQ":{"L":["2"]},"af":{"a3":["2"],"k":["2"],"e":["2"],"a3.E":"2","e.E":"2"},"lv":{"e":["1"],"e.E":"1"},"cq":{"L":["1"]},"bA":{"e":["1"],"e.E":"1"},"cG":{"bA":["1"],"k":["1"],"e":["1"],"e.E":"1"},"e2":{"L":["1"]},"cf":{"k":["1"],"e":["1"],"e.E":"1"},"dB":{"L":["1"]},"ef":{"e":["1"],"e.E":"1"},"eg":{"L":["1"]},"d2":{"h":["1"],"c0":["1"],"m":["1"],"k":["1"],"e":["1"]},"ic":{"a3":["c"],"k":["c"],"e":["c"],"a3.E":"c","e.E":"c"},"dO":{"w":["c","1"],"c5":["c","1"],"I":["c","1"],"w.K":"c","w.V":"1"},"e0":{"a3":["1"],"k":["1"],"e":["1"],"a3.E":"1","e.E":"1"},"d1":{"co":[]},"dx":{"ed":["1","2"],"dh":["1","2"],"cS":["1","2"],"c5":["1","2"],"I":["1","2"]},"dw":{"I":["1","2"]},"cc":{"dw":["1","2"],"I":["1","2"]},"em":{"e":["1"],"e.E":"1"},"fI":{"oI":[]},"dW":{"bn":[],"Q":[]},"fK":{"Q":[]},"hx":{"Q":[]},"h2":{"ac":[]},"eH":{"aG":[]},"bS":{"ch":[]},"ff":{"ch":[]},"fg":{"ch":[]},"ho":{"ch":[]},"hk":{"ch":[]},"cD":{"ch":[]},"hc":{"Q":[]},"hO":{"Q":[]},"as":{"w":["1","2"],"jU":["1","2"],"I":["1","2"],"w.K":"1","w.V":"2"},"bv":{"k":["1"],"e":["1"],"e.E":"1"},"dM":{"L":["1"]},"dL":{"oW":[],"k9":[]},"ey":{"e_":[],"cT":[]},"hM":{"e":["e_"],"e.E":"e_"},"hN":{"L":["e_"]},"ea":{"cT":[]},"iE":{"e":["cT"],"e.E":"cT"},"iF":{"L":["cT"]},"cW":{"nt":[]},"dR":{"a5":[],"oC":[]},"ag":{"F":["1"],"a5":[]},"bX":{"ag":["N"],"h":["N"],"F":["N"],"m":["N"],"a5":[],"k":["N"],"e":["N"],"ar":["N"]},"aQ":{"ag":["c"],"h":["c"],"F":["c"],"m":["c"],"a5":[],"k":["c"],"e":["c"],"ar":["c"]},"fT":{"bX":[],"ag":["N"],"h":["N"],"F":["N"],"m":["N"],"a5":[],"k":["N"],"e":["N"],"ar":["N"],"h.E":"N"},"fU":{"bX":[],"ag":["N"],"h":["N"],"F":["N"],"m":["N"],"a5":[],"k":["N"],"e":["N"],"ar":["N"],"h.E":"N"},"fV":{"aQ":[],"ag":["c"],"h":["c"],"F":["c"],"m":["c"],"a5":[],"k":["c"],"e":["c"],"ar":["c"],"h.E":"c"},"fW":{"aQ":[],"ag":["c"],"h":["c"],"F":["c"],"m":["c"],"a5":[],"k":["c"],"e":["c"],"ar":["c"],"h.E":"c"},"fX":{"aQ":[],"ag":["c"],"h":["c"],"F":["c"],"m":["c"],"a5":[],"k":["c"],"e":["c"],"ar":["c"],"h.E":"c"},"fY":{"aQ":[],"ag":["c"],"h":["c"],"nP":[],"F":["c"],"m":["c"],"a5":[],"k":["c"],"e":["c"],"ar":["c"],"h.E":"c"},"fZ":{"aQ":[],"ag":["c"],"h":["c"],"F":["c"],"m":["c"],"a5":[],"k":["c"],"e":["c"],"ar":["c"],"h.E":"c"},"dT":{"aQ":[],"ag":["c"],"h":["c"],"F":["c"],"m":["c"],"a5":[],"k":["c"],"e":["c"],"ar":["c"],"h.E":"c"},"cl":{"aQ":[],"ag":["c"],"h":["c"],"aX":[],"F":["c"],"m":["c"],"a5":[],"k":["c"],"e":["c"],"ar":["c"],"h.E":"c"},"i_":{"Q":[]},"eN":{"bn":[],"Q":[]},"E":{"H":["1"]},"eh":{"fi":["1"]},"de":{"L":["1"]},"eK":{"e":["1"],"e.E":"1"},"dt":{"Q":[]},"cs":{"fi":["1"]},"cr":{"cs":["1"],"fi":["1"]},"aa":{"cs":["1"],"fi":["1"]},"dd":{"pp":["1"],"cv":["1"]},"df":{"iK":["1"],"dd":["1"],"pp":["1"],"cv":["1"]},"d5":{"eJ":["1"],"aV":["1"],"aV.T":"1"},"d6":{"ej":["1"],"bm":["1"],"cv":["1"]},"ej":{"bm":["1"],"cv":["1"]},"eJ":{"aV":["1"]},"cu":{"bG":["1"]},"en":{"bG":["@"]},"hV":{"bG":["@"]},"eT":{"bF":[]},"iu":{"eT":[],"bF":[]},"et":{"as":["1","2"],"w":["1","2"],"jU":["1","2"],"I":["1","2"],"w.K":"1","w.V":"2"},"er":{"as":["1","2"],"w":["1","2"],"jU":["1","2"],"I":["1","2"],"w.K":"1","w.V":"2"},"es":{"e1":["1"],"p_":["1"],"k":["1"],"e":["1"]},"cw":{"L":["1"]},"dH":{"e":["1"]},"cR":{"e":["1"],"e.E":"1"},"eu":{"L":["1"]},"dN":{"h":["1"],"m":["1"],"k":["1"],"e":["1"]},"dP":{"w":["1","2"],"I":["1","2"]},"w":{"I":["1","2"]},"d3":{"w":["1","2"],"c5":["1","2"],"I":["1","2"]},"ew":{"k":["2"],"e":["2"],"e.E":"2"},"ex":{"L":["2"]},"cS":{"I":["1","2"]},"ed":{"dh":["1","2"],"cS":["1","2"],"c5":["1","2"],"I":["1","2"]},"eE":{"e1":["1"],"p_":["1"],"k":["1"],"e":["1"]},"fc":{"ay":["m","i"],"ay.S":"m"},"fw":{"ay":["i","m"]},"ee":{"ay":["i","m"],"ay.S":"i"},"cC":{"aj":["cC"]},"bU":{"aj":["bU"]},"N":{"W":[],"aj":["W"]},"cd":{"aj":["cd"]},"c":{"W":[],"aj":["W"]},"m":{"k":["1"],"e":["1"]},"W":{"aj":["W"]},"e_":{"cT":[]},"i":{"aj":["i"],"k9":[]},"a8":{"cC":[],"aj":["cC"]},"ds":{"Q":[]},"bn":{"Q":[]},"h1":{"bn":[],"Q":[]},"bh":{"Q":[]},"cX":{"Q":[]},"fE":{"Q":[]},"dU":{"Q":[]},"hz":{"Q":[]},"hv":{"Q":[]},"bB":{"Q":[]},"fj":{"Q":[]},"h5":{"Q":[]},"e9":{"Q":[]},"fp":{"Q":[]},"i0":{"ac":[]},"fC":{"ac":[]},"fG":{"ac":[],"Q":[]},"iI":{"aG":[]},"ah":{"tj":[]},"eR":{"hA":[]},"b5":{"hA":[]},"hU":{"hA":[]},"P":{"a":[]},"l":{"a":[]},"az":{"bR":[],"a":[]},"aA":{"a":[]},"aB":{"a":[]},"G":{"f":[],"a":[]},"aC":{"a":[]},"aD":{"f":[],"a":[]},"aE":{"a":[]},"aF":{"a":[]},"al":{"a":[]},"aH":{"f":[],"a":[]},"am":{"f":[],"a":[]},"aI":{"a":[]},"o":{"G":[],"f":[],"a":[]},"f4":{"a":[]},"f5":{"G":[],"f":[],"a":[]},"f6":{"G":[],"f":[],"a":[]},"bR":{"a":[]},"bi":{"G":[],"f":[],"a":[]},"fm":{"a":[]},"cE":{"a":[]},"aq":{"a":[]},"bb":{"a":[]},"fn":{"a":[]},"fo":{"a":[]},"fq":{"a":[]},"ft":{"a":[]},"dz":{"h":["bk"],"u":["bk"],"m":["bk"],"F":["bk"],"a":[],"k":["bk"],"e":["bk"],"u.E":"bk","h.E":"bk"},"dA":{"a":[],"bk":["W"]},"fu":{"h":["i"],"u":["i"],"m":["i"],"F":["i"],"a":[],"k":["i"],"e":["i"],"u.E":"i","h.E":"i"},"fv":{"a":[]},"n":{"G":[],"f":[],"a":[]},"f":{"a":[]},"cI":{"h":["az"],"u":["az"],"m":["az"],"F":["az"],"a":[],"k":["az"],"e":["az"],"u.E":"az","h.E":"az"},"fz":{"f":[],"a":[]},"fB":{"G":[],"f":[],"a":[]},"fD":{"a":[]},"ci":{"h":["G"],"u":["G"],"m":["G"],"F":["G"],"a":[],"k":["G"],"e":["G"],"u.E":"G","h.E":"G"},"cL":{"a":[]},"fO":{"a":[]},"fP":{"a":[]},"cV":{"l":[],"a":[]},"ck":{"f":[],"a":[]},"fQ":{"a":[],"w":["i","@"],"I":["i","@"],"w.K":"i","w.V":"@"},"fR":{"a":[],"w":["i","@"],"I":["i","@"],"w.K":"i","w.V":"@"},"fS":{"h":["aB"],"u":["aB"],"m":["aB"],"F":["aB"],"a":[],"k":["aB"],"e":["aB"],"u.E":"aB","h.E":"aB"},"dV":{"h":["G"],"u":["G"],"m":["G"],"F":["G"],"a":[],"k":["G"],"e":["G"],"u.E":"G","h.E":"G"},"h7":{"h":["aC"],"u":["aC"],"m":["aC"],"F":["aC"],"a":[],"k":["aC"],"e":["aC"],"u.E":"aC","h.E":"aC"},"hb":{"a":[],"w":["i","@"],"I":["i","@"],"w.K":"i","w.V":"@"},"hd":{"G":[],"f":[],"a":[]},"cY":{"a":[]},"cZ":{"f":[],"a":[]},"hf":{"h":["aD"],"u":["aD"],"f":[],"m":["aD"],"F":["aD"],"a":[],"k":["aD"],"e":["aD"],"u.E":"aD","h.E":"aD"},"hg":{"h":["aE"],"u":["aE"],"m":["aE"],"F":["aE"],"a":[],"k":["aE"],"e":["aE"],"u.E":"aE","h.E":"aE"},"hl":{"a":[],"w":["i","i"],"I":["i","i"],"w.K":"i","w.V":"i"},"hp":{"h":["am"],"u":["am"],"m":["am"],"F":["am"],"a":[],"k":["am"],"e":["am"],"u.E":"am","h.E":"am"},"hq":{"h":["aH"],"u":["aH"],"f":[],"m":["aH"],"F":["aH"],"a":[],"k":["aH"],"e":["aH"],"u.E":"aH","h.E":"aH"},"hr":{"a":[]},"hs":{"h":["aI"],"u":["aI"],"m":["aI"],"F":["aI"],"a":[],"k":["aI"],"e":["aI"],"u.E":"aI","h.E":"aI"},"ht":{"a":[]},"hB":{"a":[]},"hD":{"f":[],"a":[]},"c1":{"f":[],"a":[]},"hS":{"h":["P"],"u":["P"],"m":["P"],"F":["P"],"a":[],"k":["P"],"e":["P"],"u.E":"P","h.E":"P"},"eo":{"a":[],"bk":["W"]},"i5":{"h":["aA?"],"u":["aA?"],"m":["aA?"],"F":["aA?"],"a":[],"k":["aA?"],"e":["aA?"],"u.E":"aA?","h.E":"aA?"},"ez":{"h":["G"],"u":["G"],"m":["G"],"F":["G"],"a":[],"k":["G"],"e":["G"],"u.E":"G","h.E":"G"},"iA":{"h":["aF"],"u":["aF"],"m":["aF"],"F":["aF"],"a":[],"k":["aF"],"e":["aF"],"u.E":"aF","h.E":"aF"},"iJ":{"h":["al"],"u":["al"],"m":["al"],"F":["al"],"a":[],"k":["al"],"e":["al"],"u.E":"al","h.E":"al"},"lM":{"aV":["1"],"aV.T":"1"},"eq":{"bm":["1"]},"dD":{"L":["1"]},"bT":{"a":[]},"br":{"bT":[],"a":[]},"bj":{"f":[],"a":[]},"cj":{"a":[]},"bz":{"f":[],"a":[]},"bD":{"l":[],"a":[]},"dG":{"a":[]},"dX":{"a":[]},"ec":{"f":[],"a":[]},"h0":{"ac":[]},"i8":{"rU":[]},"aN":{"a":[]},"aR":{"a":[]},"aW":{"a":[]},"fL":{"h":["aN"],"u":["aN"],"m":["aN"],"a":[],"k":["aN"],"e":["aN"],"u.E":"aN","h.E":"aN"},"h3":{"h":["aR"],"u":["aR"],"m":["aR"],"a":[],"k":["aR"],"e":["aR"],"u.E":"aR","h.E":"aR"},"h8":{"a":[]},"hn":{"h":["i"],"u":["i"],"m":["i"],"a":[],"k":["i"],"e":["i"],"u.E":"i","h.E":"i"},"hu":{"h":["aW"],"u":["aW"],"m":["aW"],"a":[],"k":["aW"],"e":["aW"],"u.E":"aW","h.E":"aW"},"f9":{"a":[]},"fa":{"a":[],"w":["i","@"],"I":["i","@"],"w.K":"i","w.V":"@"},"fb":{"f":[],"a":[]},"bQ":{"f":[],"a":[]},"h4":{"f":[],"a":[]},"h9":{"bV":[]},"hC":{"bV":[]},"hK":{"bV":[]},"dy":{"ac":[]},"e3":{"ac":[]},"bl":{"ac":[]},"be":{"dg":["cC"],"dg.T":"cC"},"e8":{"e7":[]},"d_":{"ac":[]},"fA":{"bs":[]},"fr":{"oE":[]},"cJ":{"bs":[]},"d0":{"rc":[]},"hL":{"dI":[],"cF":[],"L":["ak"]},"ak":{"hy":["i","@"],"w":["i","@"],"I":["i","@"],"w.K":"i","w.V":"@"},"dI":{"cF":[],"L":["ak"]},"ha":{"h":["ak"],"h_":["ak"],"m":["ak"],"k":["ak"],"cF":[],"e":["ak"],"h.E":"ak"},"ir":{"L":["ak"]},"hI":{"rW":[]},"hF":{"rX":[]},"hJ":{"oT":[]},"d4":{"h":["bE"],"m":["bE"],"k":["bE"],"e":["bE"],"h.E":"bE"},"bc":{"ac":[]},"cM":{"jD":[]},"a9":{"ae":["a9"]},"d9":{"a9":[],"ae":["a9"],"ae.E":"a9"},"d8":{"a9":[],"ae":["a9"],"ae.E":"a9"},"ct":{"a9":[],"ae":["a9"],"ae.E":"a9"},"cy":{"a9":[],"ae":["a9"],"ae.E":"a9"},"dF":{"jD":[]},"fd":{"rz":[]},"ro":{"m":["c"],"k":["c"],"e":["c"]},"aX":{"m":["c"],"k":["c"],"e":["c"]},"tp":{"m":["c"],"k":["c"],"e":["c"]},"rm":{"m":["c"],"k":["c"],"e":["c"]},"nP":{"m":["c"],"k":["c"],"e":["c"]},"rn":{"m":["c"],"k":["c"],"e":["c"]},"to":{"m":["c"],"k":["c"],"e":["c"]},"rj":{"m":["N"],"k":["N"],"e":["N"]},"rk":{"m":["N"],"k":["N"],"e":["N"]}}')) A.u_(v.typeUniverse,JSON.parse('{"d2":1,"eU":2,"ag":1,"hm":2,"bG":1,"dH":1,"dN":1,"dP":2,"d3":2,"eE":1,"ev":1,"eV":1,"fl":2,"r3":1}')) var u={l:"Cannot extract a file path from a URI with a fragment component",i:"Cannot extract a file path from a URI with a query component",j:"Cannot extract a non-Windows file path from a file URI with an authority",c:"Error handler must accept one Object or one Object and a StackTrace as arguments, and return a value of the returned future's type",n:"Tried to operate on a released prepared statement"} var t=(function rtii(){var s=A.aL return{ie:s("r3"),n:s("dt"),k:s("cC"),w:s("bR"),U:s("nt"),bT:s("oE"),bP:s("aj<@>"),i9:s("dx"),d5:s("P"),nT:s("br"),E:s("bj"),cs:s("bU"),jS:s("cd"),V:s("k<@>"),W:s("Q"),A:s("l"),mA:s("ac"),dY:s("az"),kL:s("cI"),i_:s("jD"),m:s("bs"),Y:s("ch"),c:s("H<@>"),gq:s("H<@>()"),p8:s("H<~>"),ng:s("cK"),ad:s("cL"),cF:s("cM"),bg:s("oI"),bq:s("e"),id:s("e"),e7:s("e<@>"),fm:s("e"),eY:s("O"),iw:s("O>"),dO:s("O>"),C:s("O>"),ke:s("O>"),jP:s("O>"),hf:s("O"),bw:s("O"),lE:s("O"),s:s("O"),bs:s("O"),o6:s("O"),it:s("O"),b:s("O<@>"),t:s("O"),mf:s("O"),T:s("dK"),bp:s("ny"),et:s("bu"),dX:s("F<@>"),d9:s("a"),bX:s("as"),kT:s("aN"),h:s("cR"),fr:s("m"),i:s("m"),j:s("m<@>"),L:s("m"),ag:s("a4"),lK:s("I"),dV:s("I"),f:s("I<@,@>"),n2:s("I>"),lb:s("I"),iZ:s("af"),gt:s("cU"),hy:s("cV"),oA:s("ck"),ib:s("aB"),hH:s("cW"),dQ:s("bX"),aj:s("aQ"),hK:s("a5"),hD:s("cl"),G:s("G"),P:s("R"),ai:s("aR"),K:s("r"),d8:s("aC"),lZ:s("w_"),q:s("bk"),kl:s("oW"),lu:s("e_"),lq:s("w0"),B:s("bz"),hF:s("e0"),oy:s("ak"),kI:s("cY"),aD:s("cZ"),ls:s("aD"),cA:s("aE"),hI:s("aF"),cE:s("e7"),db:s("e8"),kY:s("hj"),l:s("aG"),N:s("i"),lv:s("al"),bR:s("co"),dR:s("aH"),gJ:s("am"),ki:s("aI"),hk:s("aW"),do:s("bn"),p:s("aX"),cx:s("c_"),jJ:s("hA"),O:s("ee"),bo:s("bD"),n0:s("hE"),ax:s("hG"),es:s("hH"),cI:s("bE"),lS:s("ef"),x:s("bF"),ou:s("cr<~>"),ap:s("be"),d:s("a8"),oz:s("d7"),c6:s("d7
"),bc:s("bg"),go:s("E"),g5:s("E"),g:s("E<@>"),g_:s("E"),D:s("E<~>"),ot:s("dc"),lz:s("iB"),gL:s("eI"),my:s("aa"),ex:s("aa"),F:s("aa<~>"),y:s("aw"),iW:s("aw(r)"),dx:s("N"),z:s("@"),mY:s("@()"),v:s("@(r)"),Q:s("@(r,aG)"),ha:s("@(i)"),p1:s("@(@,@)"),S:s("c"),eK:s("0&*"),_:s("r*"),g9:s("br?"),k5:s("bj?"),iB:s("f?"),gK:s("H?"),ef:s("aA?"),kq:s("cj?"),lH:s("m<@>?"),kR:s("m?"),h9:s("I?"),X:s("r?"),fw:s("aG?"),nh:s("aX?"),J:s("bF?"),r:s("nR?"),lT:s("bG<@>?"),jV:s("bg?"),e:s("bH<@,@>?"),R:s("ib?"),o:s("@(l)?"),I:s("c?"),Z:s("~()?"),a:s("~(l)?"),jM:s("~(bD)?"),hC:s("~(c,i,c)?"),cZ:s("W"),H:s("~"),M:s("~()"),i6:s("~(r)"),b9:s("~(r,aG)"),bm:s("~(i,i)"),u:s("~(i,@)")}})();(function constants(){var s=hunkHelpers.makeConstList B.p=A.br.prototype B.h=A.bj.prototype B.U=A.cj.prototype B.V=A.dG.prototype B.W=J.cN.prototype B.b=J.O.prototype B.c=J.dJ.prototype B.X=J.cO.prototype B.a=J.bW.prototype B.Y=J.bu.prototype B.Z=J.a.prototype B.a1=A.ck.prototype B.E=A.dR.prototype B.e=A.cl.prototype B.i=A.dX.prototype B.H=J.h6.prototype B.r=J.c_.prototype B.ao=new A.jr() B.I=new A.fc() B.u=new A.cd() B.J=new A.dB(A.aL("dB<0&>")) B.K=new A.fG() B.v=function getTagFallback(o) { var s = Object.prototype.toString.call(o); return s.substring(8, s.length - 1); } B.L=function() { var toStringFunction = Object.prototype.toString; function getTag(o) { var s = toStringFunction.call(o); return s.substring(8, s.length - 1); } function getUnknownTag(object, tag) { if (/^HTML[A-Z].*Element$/.test(tag)) { var name = toStringFunction.call(object); if (name == "[object Object]") return null; return "HTMLElement"; } } function getUnknownTagGenericBrowser(object, tag) { if (self.HTMLElement && object instanceof HTMLElement) return "HTMLElement"; return getUnknownTag(object, tag); } function prototypeForTag(tag) { if (typeof window == "undefined") return null; if (typeof window[tag] == "undefined") return null; var constructor = window[tag]; if (typeof constructor != "function") return null; return constructor.prototype; } function discriminator(tag) { return null; } var isBrowser = typeof navigator == "object"; return { getTag: getTag, getUnknownTag: isBrowser ? getUnknownTagGenericBrowser : getUnknownTag, prototypeForTag: prototypeForTag, discriminator: discriminator }; } B.Q=function(getTagFallback) { return function(hooks) { if (typeof navigator != "object") return hooks; var ua = navigator.userAgent; if (ua.indexOf("DumpRenderTree") >= 0) return hooks; if (ua.indexOf("Chrome") >= 0) { function confirm(p) { return typeof window == "object" && window[p] && window[p].name == p; } if (confirm("Window") && confirm("HTMLElement")) return hooks; } hooks.getTag = getTagFallback; }; } B.M=function(hooks) { if (typeof dartExperimentalFixupGetTag != "function") return hooks; hooks.getTag = dartExperimentalFixupGetTag(hooks.getTag); } B.N=function(hooks) { var getTag = hooks.getTag; var prototypeForTag = hooks.prototypeForTag; function getTagFixed(o) { var tag = getTag(o); if (tag == "Document") { if (!!o.xmlVersion) return "!Document"; return "!HTMLDocument"; } return tag; } function prototypeForTagFixed(tag) { if (tag == "Document") return null; return prototypeForTag(tag); } hooks.getTag = getTagFixed; hooks.prototypeForTag = prototypeForTagFixed; } B.P=function(hooks) { var userAgent = typeof navigator == "object" ? navigator.userAgent : ""; if (userAgent.indexOf("Firefox") == -1) return hooks; var getTag = hooks.getTag; var quickMap = { "BeforeUnloadEvent": "Event", "DataTransfer": "Clipboard", "GeoGeolocation": "Geolocation", "Location": "!Location", "WorkerMessageEvent": "MessageEvent", "XMLDocument": "!Document"}; function getTagFirefox(o) { var tag = getTag(o); return quickMap[tag] || tag; } hooks.getTag = getTagFirefox; } B.O=function(hooks) { var userAgent = typeof navigator == "object" ? navigator.userAgent : ""; if (userAgent.indexOf("Trident/") == -1) return hooks; var getTag = hooks.getTag; var quickMap = { "BeforeUnloadEvent": "Event", "DataTransfer": "Clipboard", "HTMLDDElement": "HTMLElement", "HTMLDTElement": "HTMLElement", "HTMLPhraseElement": "HTMLElement", "Position": "Geoposition" }; function getTagIE(o) { var tag = getTag(o); var newTag = quickMap[tag]; if (newTag) return newTag; if (tag == "Object") { if (window.DataView && (o instanceof window.DataView)) return "DataView"; } return tag; } function prototypeForTagIE(tag) { var constructor = window[tag]; if (constructor == null) return null; return constructor.prototype; } hooks.getTag = getTagIE; hooks.prototypeForTag = prototypeForTagIE; } B.w=function(hooks) { return hooks; } B.R=new A.h5() B.x=new A.kn() B.f=new A.ee() B.S=new A.lo() B.y=new A.hV() B.z=new A.mu() B.d=new A.iu() B.T=new A.iI() B.j=A.t(s([0,0,32776,33792,1,10240,0,0]),t.t) B.k=A.t(s([0,0,65490,45055,65535,34815,65534,18431]),t.t) B.l=A.t(s([0,0,26624,1023,65534,2047,65534,2047]),t.t) B.ap=A.t(s([]),t.hf) B.q=A.t(s([]),t.s) B.m=A.t(s([]),t.b) B.n=A.t(s(["files","blocks"]),t.s) B.a0=A.t(s([0,0,32722,12287,65534,34815,65534,18431]),t.t) B.o=A.t(s([0,0,24576,1023,65534,34815,65534,18431]),t.t) B.A=A.t(s([0,0,32754,11263,65534,34815,65534,18431]),t.t) B.B=A.t(s([0,0,65490,12287,65535,34815,65534,18431]),t.t) B.C=new A.cc(0,{},B.q,A.aL("cc")) B.a_=A.t(s([]),A.aL("O")) B.D=new A.cc(0,{},B.a_,A.aL("cc")) B.F=new A.dY("readOnly") B.a2=new A.dY("readWrite") B.G=new A.dY("readWriteCreate") B.a3=new A.d1("call") B.a4=A.ai("nt") B.a5=A.ai("oC") B.a6=A.ai("rj") B.a7=A.ai("rk") B.a8=A.ai("rm") B.a9=A.ai("rn") B.aa=A.ai("ro") B.ab=A.ai("ny") B.ac=A.ai("r") B.ad=A.ai("i") B.ae=A.ai("nP") B.af=A.ai("to") B.ag=A.ai("tp") B.ah=A.ai("aX") B.ai=A.ai("aw") B.aj=A.ai("N") B.ak=A.ai("c") B.al=A.ai("W") B.t=new A.ll(!1) B.am=new A.db(null,2) B.an=new A.iT(B.d,A.v4(),A.aL("iT<~(bF,nR,bF,~())>"))})();(function staticFields(){$.mp=null $.qk=null $.oR=null $.oA=null $.oz=null $.qe=null $.q5=null $.ql=null $.n6=null $.ne=null $.oi=null $.dm=null $.eX=null $.eY=null $.oa=!1 $.D=B.d $.aY=A.t([],t.hf) $.pd=null $.pe=null $.pf=null $.pg=null $.nS=A.el("_lastQuoRemDigits") $.nT=A.el("_lastQuoRemUsed") $.ei=A.el("_lastRemUsed") $.nU=A.el("_lastRem_nsh") $.pN=null $.mU=null $.q2=null $.pS=null $.qc=A.X(t.S,A.aL("aU")) $.j6=A.X(A.aL("i?"),A.aL("aU")) $.pT=0 $.nf=0 $.b6=null $.qn=A.X(t.N,t.X) $.q1=null $.eZ="/shw2"})();(function lazyInitializers(){var s=hunkHelpers.lazyFinal,r=hunkHelpers.lazy s($,"vL","ol",()=>A.vd("_$dart_dartClosure")) s($,"wO","np",()=>B.d.cP(new A.ni(),A.aL("H"))) s($,"w9","qs",()=>A.bC(A.le({ toString:function(){return"$receiver$"}}))) s($,"wa","qt",()=>A.bC(A.le({$method$:null, toString:function(){return"$receiver$"}}))) s($,"wb","qu",()=>A.bC(A.le(null))) s($,"wc","qv",()=>A.bC(function(){var $argumentsExpr$="$arguments$" try{null.$method$($argumentsExpr$)}catch(q){return q.message}}())) s($,"wf","qy",()=>A.bC(A.le(void 0))) s($,"wg","qz",()=>A.bC(function(){var $argumentsExpr$="$arguments$" try{(void 0).$method$($argumentsExpr$)}catch(q){return q.message}}())) s($,"we","qx",()=>A.bC(A.p7(null))) s($,"wd","qw",()=>A.bC(function(){try{null.$method$}catch(q){return q.message}}())) s($,"wi","qB",()=>A.bC(A.p7(void 0))) s($,"wh","qA",()=>A.bC(function(){try{(void 0).$method$}catch(q){return q.message}}())) s($,"wm","om",()=>A.tv()) s($,"vP","f2",()=>A.aL("E").a($.np())) s($,"wj","qC",()=>new A.ln().$0()) s($,"wk","qD",()=>new A.lm().$0()) s($,"wn","qE",()=>A.rB(A.uv(A.t([-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-1,-2,-2,-2,-2,-2,62,-2,62,-2,63,52,53,54,55,56,57,58,59,60,61,-2,-2,-2,-1,-2,-2,-2,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,-2,-2,-2,-2,63,-2,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,-2,-2,-2,-2,-2],t.t)))) s($,"wu","op",()=>typeof process!="undefined"&&Object.prototype.toString.call(process)=="[object process]"&&process.platform=="win32") s($,"ws","bN",()=>A.lC(0)) s($,"wr","jb",()=>A.lC(1)) s($,"wp","oo",()=>$.jb().ac(0)) s($,"wo","on",()=>A.lC(1e4)) r($,"wq","qF",()=>A.b1("^\\s*([+-]?)((0x[a-f0-9]+)|(\\d+)|([a-z0-9]+))\\s*$",!1)) s($,"wH","no",()=>A.j7(B.ac)) s($,"wI","qK",()=>A.us()) s($,"vZ","qp",()=>{var q=new A.i8(new DataView(new ArrayBuffer(A.up(8)))) q.eB() return q}) s($,"wL","or",()=>new A.fk(A.aL("bV").a($.nn()),null)) s($,"w5","qq",()=>new A.h9(A.b1("/",!0),A.b1("[^/]$",!0),A.b1("^/",!0))) s($,"w7","qr",()=>new A.hK(A.b1("[/\\\\]",!0),A.b1("[^/\\\\]$",!0),A.b1("^(\\\\\\\\[^\\\\]+\\\\[^\\\\/]+|[a-zA-Z]:[/\\\\])",!0),A.b1("^[/\\\\](?![/\\\\])",!0))) s($,"w6","ja",()=>new A.hC(A.b1("/",!0),A.b1("(^[a-zA-Z][-+.a-zA-Z\\d]*://|[^/])$",!0),A.b1("[a-zA-Z][-+.a-zA-Z\\d]*://[^/]*",!0),A.b1("^/",!0))) s($,"w4","nn",()=>A.tm()) s($,"wG","qJ",()=>A.nC()) r($,"wv","oq",()=>A.t([new A.be("BigInt")],A.aL("O"))) r($,"ww","qG",()=>{var q=$.oq() q=A.ry(q,A.av(q).c) return q.h9(q,new A.mL(),t.N,t.ap)}) r($,"wF","qI",()=>A.li("sqlite3.wasm")) s($,"wK","qM",()=>A.ox("-9223372036854775808")) s($,"wJ","qL",()=>A.ox("9223372036854775807")) s($,"wN","jc",()=>new A.i3(new FinalizationRegistry(A.c8(A.vy(new A.n7(),t.m),1)),A.aL("i3"))) s($,"wE","qH",()=>{var q=$.ja() if(q==null)q=$.nn() return new A.fk(A.aL("bV").a(q),"/")})})();(function nativeSupport(){!function(){var s=function(a){var m={} m[a]=1 return Object.keys(hunkHelpers.convertToFastObject(m))[0]} v.getIsolateTag=function(a){return s("___dart_"+a+v.isolateTag)} var r="___dart_isolate_tags_" var q=Object[r]||(Object[r]=Object.create(null)) var p="_ZxYxX" for(var o=0;;o++){var n=s(p+"_"+o+"_") if(!(n in q)){q[n]=1 v.isolateTag=n break}}v.dispatchPropertyName=v.getIsolateTag("dispatch_record")}() hunkHelpers.setOrUpdateInterceptorsByTag({WebGL:J.cN,AnimationEffectReadOnly:J.a,AnimationEffectTiming:J.a,AnimationEffectTimingReadOnly:J.a,AnimationTimeline:J.a,AnimationWorkletGlobalScope:J.a,AuthenticatorAssertionResponse:J.a,AuthenticatorAttestationResponse:J.a,AuthenticatorResponse:J.a,BackgroundFetchFetch:J.a,BackgroundFetchManager:J.a,BackgroundFetchSettledFetch:J.a,BarProp:J.a,BarcodeDetector:J.a,BluetoothRemoteGATTDescriptor:J.a,Body:J.a,BudgetState:J.a,CacheStorage:J.a,CanvasGradient:J.a,CanvasPattern:J.a,CanvasRenderingContext2D:J.a,Client:J.a,Clients:J.a,CookieStore:J.a,Coordinates:J.a,Credential:J.a,CredentialUserData:J.a,CredentialsContainer:J.a,Crypto:J.a,CryptoKey:J.a,CSS:J.a,CSSVariableReferenceValue:J.a,CustomElementRegistry:J.a,DataTransfer:J.a,DataTransferItem:J.a,DeprecatedStorageInfo:J.a,DeprecatedStorageQuota:J.a,DeprecationReport:J.a,DetectedBarcode:J.a,DetectedFace:J.a,DetectedText:J.a,DeviceAcceleration:J.a,DeviceRotationRate:J.a,DirectoryEntry:J.a,webkitFileSystemDirectoryEntry:J.a,FileSystemDirectoryEntry:J.a,DirectoryReader:J.a,WebKitDirectoryReader:J.a,webkitFileSystemDirectoryReader:J.a,FileSystemDirectoryReader:J.a,DocumentOrShadowRoot:J.a,DocumentTimeline:J.a,DOMError:J.a,DOMImplementation:J.a,Iterator:J.a,DOMMatrix:J.a,DOMMatrixReadOnly:J.a,DOMParser:J.a,DOMPoint:J.a,DOMPointReadOnly:J.a,DOMQuad:J.a,DOMStringMap:J.a,Entry:J.a,webkitFileSystemEntry:J.a,FileSystemEntry:J.a,External:J.a,FaceDetector:J.a,FederatedCredential:J.a,FileEntry:J.a,webkitFileSystemFileEntry:J.a,FileSystemFileEntry:J.a,DOMFileSystem:J.a,WebKitFileSystem:J.a,webkitFileSystem:J.a,FileSystem:J.a,FontFace:J.a,FontFaceSource:J.a,FormData:J.a,GamepadButton:J.a,GamepadPose:J.a,Geolocation:J.a,Position:J.a,GeolocationPosition:J.a,Headers:J.a,HTMLHyperlinkElementUtils:J.a,IdleDeadline:J.a,ImageBitmap:J.a,ImageBitmapRenderingContext:J.a,ImageCapture:J.a,InputDeviceCapabilities:J.a,IntersectionObserver:J.a,IntersectionObserverEntry:J.a,InterventionReport:J.a,KeyframeEffect:J.a,KeyframeEffectReadOnly:J.a,MediaCapabilities:J.a,MediaCapabilitiesInfo:J.a,MediaDeviceInfo:J.a,MediaError:J.a,MediaKeyStatusMap:J.a,MediaKeySystemAccess:J.a,MediaKeys:J.a,MediaKeysPolicy:J.a,MediaMetadata:J.a,MediaSession:J.a,MediaSettingsRange:J.a,MemoryInfo:J.a,MessageChannel:J.a,Metadata:J.a,MutationObserver:J.a,WebKitMutationObserver:J.a,MutationRecord:J.a,NavigationPreloadManager:J.a,Navigator:J.a,NavigatorAutomationInformation:J.a,NavigatorConcurrentHardware:J.a,NavigatorCookies:J.a,NavigatorUserMediaError:J.a,NodeFilter:J.a,NodeIterator:J.a,NonDocumentTypeChildNode:J.a,NonElementParentNode:J.a,NoncedElement:J.a,OffscreenCanvasRenderingContext2D:J.a,OverconstrainedError:J.a,PaintRenderingContext2D:J.a,PaintSize:J.a,PaintWorkletGlobalScope:J.a,PasswordCredential:J.a,Path2D:J.a,PaymentAddress:J.a,PaymentInstruments:J.a,PaymentManager:J.a,PaymentResponse:J.a,PerformanceEntry:J.a,PerformanceLongTaskTiming:J.a,PerformanceMark:J.a,PerformanceMeasure:J.a,PerformanceNavigation:J.a,PerformanceNavigationTiming:J.a,PerformanceObserver:J.a,PerformanceObserverEntryList:J.a,PerformancePaintTiming:J.a,PerformanceResourceTiming:J.a,PerformanceServerTiming:J.a,PerformanceTiming:J.a,Permissions:J.a,PhotoCapabilities:J.a,PositionError:J.a,GeolocationPositionError:J.a,Presentation:J.a,PresentationReceiver:J.a,PublicKeyCredential:J.a,PushManager:J.a,PushMessageData:J.a,PushSubscription:J.a,PushSubscriptionOptions:J.a,Range:J.a,RelatedApplication:J.a,ReportBody:J.a,ReportingObserver:J.a,ResizeObserver:J.a,ResizeObserverEntry:J.a,RTCCertificate:J.a,RTCIceCandidate:J.a,mozRTCIceCandidate:J.a,RTCLegacyStatsReport:J.a,RTCRtpContributingSource:J.a,RTCRtpReceiver:J.a,RTCRtpSender:J.a,RTCSessionDescription:J.a,mozRTCSessionDescription:J.a,RTCStatsResponse:J.a,Screen:J.a,ScrollState:J.a,ScrollTimeline:J.a,Selection:J.a,SpeechRecognitionAlternative:J.a,SpeechSynthesisVoice:J.a,StaticRange:J.a,StorageManager:J.a,StyleMedia:J.a,StylePropertyMap:J.a,StylePropertyMapReadonly:J.a,SyncManager:J.a,TaskAttributionTiming:J.a,TextDetector:J.a,TextMetrics:J.a,TrackDefault:J.a,TreeWalker:J.a,TrustedHTML:J.a,TrustedScriptURL:J.a,TrustedURL:J.a,UnderlyingSourceBase:J.a,URLSearchParams:J.a,VRCoordinateSystem:J.a,VRDisplayCapabilities:J.a,VREyeParameters:J.a,VRFrameData:J.a,VRFrameOfReference:J.a,VRPose:J.a,VRStageBounds:J.a,VRStageBoundsPoint:J.a,VRStageParameters:J.a,ValidityState:J.a,VideoPlaybackQuality:J.a,VideoTrack:J.a,VTTRegion:J.a,WindowClient:J.a,WorkletAnimation:J.a,WorkletGlobalScope:J.a,XPathEvaluator:J.a,XPathExpression:J.a,XPathNSResolver:J.a,XPathResult:J.a,XMLSerializer:J.a,XSLTProcessor:J.a,Bluetooth:J.a,BluetoothCharacteristicProperties:J.a,BluetoothRemoteGATTServer:J.a,BluetoothRemoteGATTService:J.a,BluetoothUUID:J.a,BudgetService:J.a,Cache:J.a,DOMFileSystemSync:J.a,DirectoryEntrySync:J.a,DirectoryReaderSync:J.a,EntrySync:J.a,FileEntrySync:J.a,FileReaderSync:J.a,FileWriterSync:J.a,HTMLAllCollection:J.a,Mojo:J.a,MojoHandle:J.a,MojoWatcher:J.a,NFC:J.a,PagePopupController:J.a,Report:J.a,Request:J.a,Response:J.a,SubtleCrypto:J.a,USBAlternateInterface:J.a,USBConfiguration:J.a,USBDevice:J.a,USBEndpoint:J.a,USBInTransferResult:J.a,USBInterface:J.a,USBIsochronousInTransferPacket:J.a,USBIsochronousInTransferResult:J.a,USBIsochronousOutTransferPacket:J.a,USBIsochronousOutTransferResult:J.a,USBOutTransferResult:J.a,WorkerLocation:J.a,WorkerNavigator:J.a,Worklet:J.a,IDBKeyRange:J.a,IDBObservation:J.a,IDBObserver:J.a,IDBObserverChanges:J.a,SVGAngle:J.a,SVGAnimatedAngle:J.a,SVGAnimatedBoolean:J.a,SVGAnimatedEnumeration:J.a,SVGAnimatedInteger:J.a,SVGAnimatedLength:J.a,SVGAnimatedLengthList:J.a,SVGAnimatedNumber:J.a,SVGAnimatedNumberList:J.a,SVGAnimatedPreserveAspectRatio:J.a,SVGAnimatedRect:J.a,SVGAnimatedString:J.a,SVGAnimatedTransformList:J.a,SVGMatrix:J.a,SVGPoint:J.a,SVGPreserveAspectRatio:J.a,SVGRect:J.a,SVGUnitTypes:J.a,AudioListener:J.a,AudioParam:J.a,AudioTrack:J.a,AudioWorkletGlobalScope:J.a,AudioWorkletProcessor:J.a,PeriodicWave:J.a,WebGLActiveInfo:J.a,ANGLEInstancedArrays:J.a,ANGLE_instanced_arrays:J.a,WebGLBuffer:J.a,WebGLCanvas:J.a,WebGLColorBufferFloat:J.a,WebGLCompressedTextureASTC:J.a,WebGLCompressedTextureATC:J.a,WEBGL_compressed_texture_atc:J.a,WebGLCompressedTextureETC1:J.a,WEBGL_compressed_texture_etc1:J.a,WebGLCompressedTextureETC:J.a,WebGLCompressedTexturePVRTC:J.a,WEBGL_compressed_texture_pvrtc:J.a,WebGLCompressedTextureS3TC:J.a,WEBGL_compressed_texture_s3tc:J.a,WebGLCompressedTextureS3TCsRGB:J.a,WebGLDebugRendererInfo:J.a,WEBGL_debug_renderer_info:J.a,WebGLDebugShaders:J.a,WEBGL_debug_shaders:J.a,WebGLDepthTexture:J.a,WEBGL_depth_texture:J.a,WebGLDrawBuffers:J.a,WEBGL_draw_buffers:J.a,EXTsRGB:J.a,EXT_sRGB:J.a,EXTBlendMinMax:J.a,EXT_blend_minmax:J.a,EXTColorBufferFloat:J.a,EXTColorBufferHalfFloat:J.a,EXTDisjointTimerQuery:J.a,EXTDisjointTimerQueryWebGL2:J.a,EXTFragDepth:J.a,EXT_frag_depth:J.a,EXTShaderTextureLOD:J.a,EXT_shader_texture_lod:J.a,EXTTextureFilterAnisotropic:J.a,EXT_texture_filter_anisotropic:J.a,WebGLFramebuffer:J.a,WebGLGetBufferSubDataAsync:J.a,WebGLLoseContext:J.a,WebGLExtensionLoseContext:J.a,WEBGL_lose_context:J.a,OESElementIndexUint:J.a,OES_element_index_uint:J.a,OESStandardDerivatives:J.a,OES_standard_derivatives:J.a,OESTextureFloat:J.a,OES_texture_float:J.a,OESTextureFloatLinear:J.a,OES_texture_float_linear:J.a,OESTextureHalfFloat:J.a,OES_texture_half_float:J.a,OESTextureHalfFloatLinear:J.a,OES_texture_half_float_linear:J.a,OESVertexArrayObject:J.a,OES_vertex_array_object:J.a,WebGLProgram:J.a,WebGLQuery:J.a,WebGLRenderbuffer:J.a,WebGLRenderingContext:J.a,WebGL2RenderingContext:J.a,WebGLSampler:J.a,WebGLShader:J.a,WebGLShaderPrecisionFormat:J.a,WebGLSync:J.a,WebGLTexture:J.a,WebGLTimerQueryEXT:J.a,WebGLTransformFeedback:J.a,WebGLUniformLocation:J.a,WebGLVertexArrayObject:J.a,WebGLVertexArrayObjectOES:J.a,WebGL2RenderingContextBase:J.a,ArrayBuffer:A.cW,ArrayBufferView:A.a5,DataView:A.dR,Float32Array:A.fT,Float64Array:A.fU,Int16Array:A.fV,Int32Array:A.fW,Int8Array:A.fX,Uint16Array:A.fY,Uint32Array:A.fZ,Uint8ClampedArray:A.dT,CanvasPixelArray:A.dT,Uint8Array:A.cl,HTMLAudioElement:A.o,HTMLBRElement:A.o,HTMLBaseElement:A.o,HTMLBodyElement:A.o,HTMLButtonElement:A.o,HTMLCanvasElement:A.o,HTMLContentElement:A.o,HTMLDListElement:A.o,HTMLDataElement:A.o,HTMLDataListElement:A.o,HTMLDetailsElement:A.o,HTMLDialogElement:A.o,HTMLDivElement:A.o,HTMLEmbedElement:A.o,HTMLFieldSetElement:A.o,HTMLHRElement:A.o,HTMLHeadElement:A.o,HTMLHeadingElement:A.o,HTMLHtmlElement:A.o,HTMLIFrameElement:A.o,HTMLImageElement:A.o,HTMLInputElement:A.o,HTMLLIElement:A.o,HTMLLabelElement:A.o,HTMLLegendElement:A.o,HTMLLinkElement:A.o,HTMLMapElement:A.o,HTMLMediaElement:A.o,HTMLMenuElement:A.o,HTMLMetaElement:A.o,HTMLMeterElement:A.o,HTMLModElement:A.o,HTMLOListElement:A.o,HTMLObjectElement:A.o,HTMLOptGroupElement:A.o,HTMLOptionElement:A.o,HTMLOutputElement:A.o,HTMLParagraphElement:A.o,HTMLParamElement:A.o,HTMLPictureElement:A.o,HTMLPreElement:A.o,HTMLProgressElement:A.o,HTMLQuoteElement:A.o,HTMLScriptElement:A.o,HTMLShadowElement:A.o,HTMLSlotElement:A.o,HTMLSourceElement:A.o,HTMLSpanElement:A.o,HTMLStyleElement:A.o,HTMLTableCaptionElement:A.o,HTMLTableCellElement:A.o,HTMLTableDataCellElement:A.o,HTMLTableHeaderCellElement:A.o,HTMLTableColElement:A.o,HTMLTableElement:A.o,HTMLTableRowElement:A.o,HTMLTableSectionElement:A.o,HTMLTemplateElement:A.o,HTMLTextAreaElement:A.o,HTMLTimeElement:A.o,HTMLTitleElement:A.o,HTMLTrackElement:A.o,HTMLUListElement:A.o,HTMLUnknownElement:A.o,HTMLVideoElement:A.o,HTMLDirectoryElement:A.o,HTMLFontElement:A.o,HTMLFrameElement:A.o,HTMLFrameSetElement:A.o,HTMLMarqueeElement:A.o,HTMLElement:A.o,AccessibleNodeList:A.f4,HTMLAnchorElement:A.f5,HTMLAreaElement:A.f6,Blob:A.bR,CDATASection:A.bi,CharacterData:A.bi,Comment:A.bi,ProcessingInstruction:A.bi,Text:A.bi,CSSPerspective:A.fm,CSSCharsetRule:A.P,CSSConditionRule:A.P,CSSFontFaceRule:A.P,CSSGroupingRule:A.P,CSSImportRule:A.P,CSSKeyframeRule:A.P,MozCSSKeyframeRule:A.P,WebKitCSSKeyframeRule:A.P,CSSKeyframesRule:A.P,MozCSSKeyframesRule:A.P,WebKitCSSKeyframesRule:A.P,CSSMediaRule:A.P,CSSNamespaceRule:A.P,CSSPageRule:A.P,CSSRule:A.P,CSSStyleRule:A.P,CSSSupportsRule:A.P,CSSViewportRule:A.P,CSSStyleDeclaration:A.cE,MSStyleCSSProperties:A.cE,CSS2Properties:A.cE,CSSImageValue:A.aq,CSSKeywordValue:A.aq,CSSNumericValue:A.aq,CSSPositionValue:A.aq,CSSResourceValue:A.aq,CSSUnitValue:A.aq,CSSURLImageValue:A.aq,CSSStyleValue:A.aq,CSSMatrixComponent:A.bb,CSSRotation:A.bb,CSSScale:A.bb,CSSSkew:A.bb,CSSTranslation:A.bb,CSSTransformComponent:A.bb,CSSTransformValue:A.fn,CSSUnparsedValue:A.fo,DataTransferItemList:A.fq,DOMException:A.ft,ClientRectList:A.dz,DOMRectList:A.dz,DOMRectReadOnly:A.dA,DOMStringList:A.fu,DOMTokenList:A.fv,MathMLElement:A.n,SVGAElement:A.n,SVGAnimateElement:A.n,SVGAnimateMotionElement:A.n,SVGAnimateTransformElement:A.n,SVGAnimationElement:A.n,SVGCircleElement:A.n,SVGClipPathElement:A.n,SVGDefsElement:A.n,SVGDescElement:A.n,SVGDiscardElement:A.n,SVGEllipseElement:A.n,SVGFEBlendElement:A.n,SVGFEColorMatrixElement:A.n,SVGFEComponentTransferElement:A.n,SVGFECompositeElement:A.n,SVGFEConvolveMatrixElement:A.n,SVGFEDiffuseLightingElement:A.n,SVGFEDisplacementMapElement:A.n,SVGFEDistantLightElement:A.n,SVGFEFloodElement:A.n,SVGFEFuncAElement:A.n,SVGFEFuncBElement:A.n,SVGFEFuncGElement:A.n,SVGFEFuncRElement:A.n,SVGFEGaussianBlurElement:A.n,SVGFEImageElement:A.n,SVGFEMergeElement:A.n,SVGFEMergeNodeElement:A.n,SVGFEMorphologyElement:A.n,SVGFEOffsetElement:A.n,SVGFEPointLightElement:A.n,SVGFESpecularLightingElement:A.n,SVGFESpotLightElement:A.n,SVGFETileElement:A.n,SVGFETurbulenceElement:A.n,SVGFilterElement:A.n,SVGForeignObjectElement:A.n,SVGGElement:A.n,SVGGeometryElement:A.n,SVGGraphicsElement:A.n,SVGImageElement:A.n,SVGLineElement:A.n,SVGLinearGradientElement:A.n,SVGMarkerElement:A.n,SVGMaskElement:A.n,SVGMetadataElement:A.n,SVGPathElement:A.n,SVGPatternElement:A.n,SVGPolygonElement:A.n,SVGPolylineElement:A.n,SVGRadialGradientElement:A.n,SVGRectElement:A.n,SVGScriptElement:A.n,SVGSetElement:A.n,SVGStopElement:A.n,SVGStyleElement:A.n,SVGElement:A.n,SVGSVGElement:A.n,SVGSwitchElement:A.n,SVGSymbolElement:A.n,SVGTSpanElement:A.n,SVGTextContentElement:A.n,SVGTextElement:A.n,SVGTextPathElement:A.n,SVGTextPositioningElement:A.n,SVGTitleElement:A.n,SVGUseElement:A.n,SVGViewElement:A.n,SVGGradientElement:A.n,SVGComponentTransferFunctionElement:A.n,SVGFEDropShadowElement:A.n,SVGMPathElement:A.n,Element:A.n,AbortPaymentEvent:A.l,AnimationEvent:A.l,AnimationPlaybackEvent:A.l,ApplicationCacheErrorEvent:A.l,BackgroundFetchClickEvent:A.l,BackgroundFetchEvent:A.l,BackgroundFetchFailEvent:A.l,BackgroundFetchedEvent:A.l,BeforeInstallPromptEvent:A.l,BeforeUnloadEvent:A.l,BlobEvent:A.l,CanMakePaymentEvent:A.l,ClipboardEvent:A.l,CloseEvent:A.l,CompositionEvent:A.l,CustomEvent:A.l,DeviceMotionEvent:A.l,DeviceOrientationEvent:A.l,ErrorEvent:A.l,ExtendableEvent:A.l,ExtendableMessageEvent:A.l,FetchEvent:A.l,FocusEvent:A.l,FontFaceSetLoadEvent:A.l,ForeignFetchEvent:A.l,GamepadEvent:A.l,HashChangeEvent:A.l,InstallEvent:A.l,KeyboardEvent:A.l,MediaEncryptedEvent:A.l,MediaKeyMessageEvent:A.l,MediaQueryListEvent:A.l,MediaStreamEvent:A.l,MediaStreamTrackEvent:A.l,MIDIConnectionEvent:A.l,MIDIMessageEvent:A.l,MouseEvent:A.l,DragEvent:A.l,MutationEvent:A.l,NotificationEvent:A.l,PageTransitionEvent:A.l,PaymentRequestEvent:A.l,PaymentRequestUpdateEvent:A.l,PointerEvent:A.l,PopStateEvent:A.l,PresentationConnectionAvailableEvent:A.l,PresentationConnectionCloseEvent:A.l,ProgressEvent:A.l,PromiseRejectionEvent:A.l,PushEvent:A.l,RTCDataChannelEvent:A.l,RTCDTMFToneChangeEvent:A.l,RTCPeerConnectionIceEvent:A.l,RTCTrackEvent:A.l,SecurityPolicyViolationEvent:A.l,SensorErrorEvent:A.l,SpeechRecognitionError:A.l,SpeechRecognitionEvent:A.l,SpeechSynthesisEvent:A.l,StorageEvent:A.l,SyncEvent:A.l,TextEvent:A.l,TouchEvent:A.l,TrackEvent:A.l,TransitionEvent:A.l,WebKitTransitionEvent:A.l,UIEvent:A.l,VRDeviceEvent:A.l,VRDisplayEvent:A.l,VRSessionEvent:A.l,WheelEvent:A.l,MojoInterfaceRequestEvent:A.l,ResourceProgressEvent:A.l,USBConnectionEvent:A.l,AudioProcessingEvent:A.l,OfflineAudioCompletionEvent:A.l,WebGLContextEvent:A.l,Event:A.l,InputEvent:A.l,SubmitEvent:A.l,AbsoluteOrientationSensor:A.f,Accelerometer:A.f,AccessibleNode:A.f,AmbientLightSensor:A.f,Animation:A.f,ApplicationCache:A.f,DOMApplicationCache:A.f,OfflineResourceList:A.f,BackgroundFetchRegistration:A.f,BatteryManager:A.f,BroadcastChannel:A.f,CanvasCaptureMediaStreamTrack:A.f,EventSource:A.f,FileReader:A.f,FontFaceSet:A.f,Gyroscope:A.f,XMLHttpRequest:A.f,XMLHttpRequestEventTarget:A.f,XMLHttpRequestUpload:A.f,LinearAccelerationSensor:A.f,Magnetometer:A.f,MediaDevices:A.f,MediaKeySession:A.f,MediaQueryList:A.f,MediaRecorder:A.f,MediaSource:A.f,MediaStream:A.f,MediaStreamTrack:A.f,MIDIAccess:A.f,MIDIInput:A.f,MIDIOutput:A.f,MIDIPort:A.f,NetworkInformation:A.f,Notification:A.f,OffscreenCanvas:A.f,OrientationSensor:A.f,PaymentRequest:A.f,Performance:A.f,PermissionStatus:A.f,PresentationAvailability:A.f,PresentationConnection:A.f,PresentationConnectionList:A.f,PresentationRequest:A.f,RelativeOrientationSensor:A.f,RemotePlayback:A.f,RTCDataChannel:A.f,DataChannel:A.f,RTCDTMFSender:A.f,RTCPeerConnection:A.f,webkitRTCPeerConnection:A.f,mozRTCPeerConnection:A.f,ScreenOrientation:A.f,Sensor:A.f,ServiceWorker:A.f,ServiceWorkerContainer:A.f,ServiceWorkerRegistration:A.f,SharedWorker:A.f,SpeechRecognition:A.f,SpeechSynthesis:A.f,SpeechSynthesisUtterance:A.f,VR:A.f,VRDevice:A.f,VRDisplay:A.f,VRSession:A.f,VisualViewport:A.f,WebSocket:A.f,Window:A.f,DOMWindow:A.f,Worker:A.f,WorkerPerformance:A.f,BluetoothDevice:A.f,BluetoothRemoteGATTCharacteristic:A.f,Clipboard:A.f,MojoInterfaceInterceptor:A.f,USB:A.f,AnalyserNode:A.f,RealtimeAnalyserNode:A.f,AudioBufferSourceNode:A.f,AudioDestinationNode:A.f,AudioNode:A.f,AudioScheduledSourceNode:A.f,AudioWorkletNode:A.f,BiquadFilterNode:A.f,ChannelMergerNode:A.f,AudioChannelMerger:A.f,ChannelSplitterNode:A.f,AudioChannelSplitter:A.f,ConstantSourceNode:A.f,ConvolverNode:A.f,DelayNode:A.f,DynamicsCompressorNode:A.f,GainNode:A.f,AudioGainNode:A.f,IIRFilterNode:A.f,MediaElementAudioSourceNode:A.f,MediaStreamAudioDestinationNode:A.f,MediaStreamAudioSourceNode:A.f,OscillatorNode:A.f,Oscillator:A.f,PannerNode:A.f,AudioPannerNode:A.f,webkitAudioPannerNode:A.f,ScriptProcessorNode:A.f,JavaScriptAudioNode:A.f,StereoPannerNode:A.f,WaveShaperNode:A.f,EventTarget:A.f,File:A.az,FileList:A.cI,FileWriter:A.fz,HTMLFormElement:A.fB,Gamepad:A.aA,History:A.fD,HTMLCollection:A.ci,HTMLFormControlsCollection:A.ci,HTMLOptionsCollection:A.ci,ImageData:A.cL,Location:A.fO,MediaList:A.fP,MessageEvent:A.cV,MessagePort:A.ck,MIDIInputMap:A.fQ,MIDIOutputMap:A.fR,MimeType:A.aB,MimeTypeArray:A.fS,Document:A.G,DocumentFragment:A.G,HTMLDocument:A.G,ShadowRoot:A.G,XMLDocument:A.G,Attr:A.G,DocumentType:A.G,Node:A.G,NodeList:A.dV,RadioNodeList:A.dV,Plugin:A.aC,PluginArray:A.h7,RTCStatsReport:A.hb,HTMLSelectElement:A.hd,SharedArrayBuffer:A.cY,SharedWorkerGlobalScope:A.cZ,SourceBuffer:A.aD,SourceBufferList:A.hf,SpeechGrammar:A.aE,SpeechGrammarList:A.hg,SpeechRecognitionResult:A.aF,Storage:A.hl,CSSStyleSheet:A.al,StyleSheet:A.al,TextTrack:A.aH,TextTrackCue:A.am,VTTCue:A.am,TextTrackCueList:A.hp,TextTrackList:A.hq,TimeRanges:A.hr,Touch:A.aI,TouchList:A.hs,TrackDefaultList:A.ht,URL:A.hB,VideoTrackList:A.hD,DedicatedWorkerGlobalScope:A.c1,ServiceWorkerGlobalScope:A.c1,WorkerGlobalScope:A.c1,CSSRuleList:A.hS,ClientRect:A.eo,DOMRect:A.eo,GamepadList:A.i5,NamedNodeMap:A.ez,MozNamedAttrMap:A.ez,SpeechRecognitionResultList:A.iA,StyleSheetList:A.iJ,IDBCursor:A.bT,IDBCursorWithValue:A.br,IDBDatabase:A.bj,IDBFactory:A.cj,IDBIndex:A.dG,IDBObjectStore:A.dX,IDBOpenDBRequest:A.bz,IDBVersionChangeRequest:A.bz,IDBRequest:A.bz,IDBTransaction:A.ec,IDBVersionChangeEvent:A.bD,SVGLength:A.aN,SVGLengthList:A.fL,SVGNumber:A.aR,SVGNumberList:A.h3,SVGPointList:A.h8,SVGStringList:A.hn,SVGTransform:A.aW,SVGTransformList:A.hu,AudioBuffer:A.f9,AudioParamMap:A.fa,AudioTrackList:A.fb,AudioContext:A.bQ,webkitAudioContext:A.bQ,BaseAudioContext:A.bQ,OfflineAudioContext:A.h4}) hunkHelpers.setOrUpdateLeafTags({WebGL:true,AnimationEffectReadOnly:true,AnimationEffectTiming:true,AnimationEffectTimingReadOnly:true,AnimationTimeline:true,AnimationWorkletGlobalScope:true,AuthenticatorAssertionResponse:true,AuthenticatorAttestationResponse:true,AuthenticatorResponse:true,BackgroundFetchFetch:true,BackgroundFetchManager:true,BackgroundFetchSettledFetch:true,BarProp:true,BarcodeDetector:true,BluetoothRemoteGATTDescriptor:true,Body:true,BudgetState:true,CacheStorage:true,CanvasGradient:true,CanvasPattern:true,CanvasRenderingContext2D:true,Client:true,Clients:true,CookieStore:true,Coordinates:true,Credential:true,CredentialUserData:true,CredentialsContainer:true,Crypto:true,CryptoKey:true,CSS:true,CSSVariableReferenceValue:true,CustomElementRegistry:true,DataTransfer:true,DataTransferItem:true,DeprecatedStorageInfo:true,DeprecatedStorageQuota:true,DeprecationReport:true,DetectedBarcode:true,DetectedFace:true,DetectedText:true,DeviceAcceleration:true,DeviceRotationRate:true,DirectoryEntry:true,webkitFileSystemDirectoryEntry:true,FileSystemDirectoryEntry:true,DirectoryReader:true,WebKitDirectoryReader:true,webkitFileSystemDirectoryReader:true,FileSystemDirectoryReader:true,DocumentOrShadowRoot:true,DocumentTimeline:true,DOMError:true,DOMImplementation:true,Iterator:true,DOMMatrix:true,DOMMatrixReadOnly:true,DOMParser:true,DOMPoint:true,DOMPointReadOnly:true,DOMQuad:true,DOMStringMap:true,Entry:true,webkitFileSystemEntry:true,FileSystemEntry:true,External:true,FaceDetector:true,FederatedCredential:true,FileEntry:true,webkitFileSystemFileEntry:true,FileSystemFileEntry:true,DOMFileSystem:true,WebKitFileSystem:true,webkitFileSystem:true,FileSystem:true,FontFace:true,FontFaceSource:true,FormData:true,GamepadButton:true,GamepadPose:true,Geolocation:true,Position:true,GeolocationPosition:true,Headers:true,HTMLHyperlinkElementUtils:true,IdleDeadline:true,ImageBitmap:true,ImageBitmapRenderingContext:true,ImageCapture:true,InputDeviceCapabilities:true,IntersectionObserver:true,IntersectionObserverEntry:true,InterventionReport:true,KeyframeEffect:true,KeyframeEffectReadOnly:true,MediaCapabilities:true,MediaCapabilitiesInfo:true,MediaDeviceInfo:true,MediaError:true,MediaKeyStatusMap:true,MediaKeySystemAccess:true,MediaKeys:true,MediaKeysPolicy:true,MediaMetadata:true,MediaSession:true,MediaSettingsRange:true,MemoryInfo:true,MessageChannel:true,Metadata:true,MutationObserver:true,WebKitMutationObserver:true,MutationRecord:true,NavigationPreloadManager:true,Navigator:true,NavigatorAutomationInformation:true,NavigatorConcurrentHardware:true,NavigatorCookies:true,NavigatorUserMediaError:true,NodeFilter:true,NodeIterator:true,NonDocumentTypeChildNode:true,NonElementParentNode:true,NoncedElement:true,OffscreenCanvasRenderingContext2D:true,OverconstrainedError:true,PaintRenderingContext2D:true,PaintSize:true,PaintWorkletGlobalScope:true,PasswordCredential:true,Path2D:true,PaymentAddress:true,PaymentInstruments:true,PaymentManager:true,PaymentResponse:true,PerformanceEntry:true,PerformanceLongTaskTiming:true,PerformanceMark:true,PerformanceMeasure:true,PerformanceNavigation:true,PerformanceNavigationTiming:true,PerformanceObserver:true,PerformanceObserverEntryList:true,PerformancePaintTiming:true,PerformanceResourceTiming:true,PerformanceServerTiming:true,PerformanceTiming:true,Permissions:true,PhotoCapabilities:true,PositionError:true,GeolocationPositionError:true,Presentation:true,PresentationReceiver:true,PublicKeyCredential:true,PushManager:true,PushMessageData:true,PushSubscription:true,PushSubscriptionOptions:true,Range:true,RelatedApplication:true,ReportBody:true,ReportingObserver:true,ResizeObserver:true,ResizeObserverEntry:true,RTCCertificate:true,RTCIceCandidate:true,mozRTCIceCandidate:true,RTCLegacyStatsReport:true,RTCRtpContributingSource:true,RTCRtpReceiver:true,RTCRtpSender:true,RTCSessionDescription:true,mozRTCSessionDescription:true,RTCStatsResponse:true,Screen:true,ScrollState:true,ScrollTimeline:true,Selection:true,SpeechRecognitionAlternative:true,SpeechSynthesisVoice:true,StaticRange:true,StorageManager:true,StyleMedia:true,StylePropertyMap:true,StylePropertyMapReadonly:true,SyncManager:true,TaskAttributionTiming:true,TextDetector:true,TextMetrics:true,TrackDefault:true,TreeWalker:true,TrustedHTML:true,TrustedScriptURL:true,TrustedURL:true,UnderlyingSourceBase:true,URLSearchParams:true,VRCoordinateSystem:true,VRDisplayCapabilities:true,VREyeParameters:true,VRFrameData:true,VRFrameOfReference:true,VRPose:true,VRStageBounds:true,VRStageBoundsPoint:true,VRStageParameters:true,ValidityState:true,VideoPlaybackQuality:true,VideoTrack:true,VTTRegion:true,WindowClient:true,WorkletAnimation:true,WorkletGlobalScope:true,XPathEvaluator:true,XPathExpression:true,XPathNSResolver:true,XPathResult:true,XMLSerializer:true,XSLTProcessor:true,Bluetooth:true,BluetoothCharacteristicProperties:true,BluetoothRemoteGATTServer:true,BluetoothRemoteGATTService:true,BluetoothUUID:true,BudgetService:true,Cache:true,DOMFileSystemSync:true,DirectoryEntrySync:true,DirectoryReaderSync:true,EntrySync:true,FileEntrySync:true,FileReaderSync:true,FileWriterSync:true,HTMLAllCollection:true,Mojo:true,MojoHandle:true,MojoWatcher:true,NFC:true,PagePopupController:true,Report:true,Request:true,Response:true,SubtleCrypto:true,USBAlternateInterface:true,USBConfiguration:true,USBDevice:true,USBEndpoint:true,USBInTransferResult:true,USBInterface:true,USBIsochronousInTransferPacket:true,USBIsochronousInTransferResult:true,USBIsochronousOutTransferPacket:true,USBIsochronousOutTransferResult:true,USBOutTransferResult:true,WorkerLocation:true,WorkerNavigator:true,Worklet:true,IDBKeyRange:true,IDBObservation:true,IDBObserver:true,IDBObserverChanges:true,SVGAngle:true,SVGAnimatedAngle:true,SVGAnimatedBoolean:true,SVGAnimatedEnumeration:true,SVGAnimatedInteger:true,SVGAnimatedLength:true,SVGAnimatedLengthList:true,SVGAnimatedNumber:true,SVGAnimatedNumberList:true,SVGAnimatedPreserveAspectRatio:true,SVGAnimatedRect:true,SVGAnimatedString:true,SVGAnimatedTransformList:true,SVGMatrix:true,SVGPoint:true,SVGPreserveAspectRatio:true,SVGRect:true,SVGUnitTypes:true,AudioListener:true,AudioParam:true,AudioTrack:true,AudioWorkletGlobalScope:true,AudioWorkletProcessor:true,PeriodicWave:true,WebGLActiveInfo:true,ANGLEInstancedArrays:true,ANGLE_instanced_arrays:true,WebGLBuffer:true,WebGLCanvas:true,WebGLColorBufferFloat:true,WebGLCompressedTextureASTC:true,WebGLCompressedTextureATC:true,WEBGL_compressed_texture_atc:true,WebGLCompressedTextureETC1:true,WEBGL_compressed_texture_etc1:true,WebGLCompressedTextureETC:true,WebGLCompressedTexturePVRTC:true,WEBGL_compressed_texture_pvrtc:true,WebGLCompressedTextureS3TC:true,WEBGL_compressed_texture_s3tc:true,WebGLCompressedTextureS3TCsRGB:true,WebGLDebugRendererInfo:true,WEBGL_debug_renderer_info:true,WebGLDebugShaders:true,WEBGL_debug_shaders:true,WebGLDepthTexture:true,WEBGL_depth_texture:true,WebGLDrawBuffers:true,WEBGL_draw_buffers:true,EXTsRGB:true,EXT_sRGB:true,EXTBlendMinMax:true,EXT_blend_minmax:true,EXTColorBufferFloat:true,EXTColorBufferHalfFloat:true,EXTDisjointTimerQuery:true,EXTDisjointTimerQueryWebGL2:true,EXTFragDepth:true,EXT_frag_depth:true,EXTShaderTextureLOD:true,EXT_shader_texture_lod:true,EXTTextureFilterAnisotropic:true,EXT_texture_filter_anisotropic:true,WebGLFramebuffer:true,WebGLGetBufferSubDataAsync:true,WebGLLoseContext:true,WebGLExtensionLoseContext:true,WEBGL_lose_context:true,OESElementIndexUint:true,OES_element_index_uint:true,OESStandardDerivatives:true,OES_standard_derivatives:true,OESTextureFloat:true,OES_texture_float:true,OESTextureFloatLinear:true,OES_texture_float_linear:true,OESTextureHalfFloat:true,OES_texture_half_float:true,OESTextureHalfFloatLinear:true,OES_texture_half_float_linear:true,OESVertexArrayObject:true,OES_vertex_array_object:true,WebGLProgram:true,WebGLQuery:true,WebGLRenderbuffer:true,WebGLRenderingContext:true,WebGL2RenderingContext:true,WebGLSampler:true,WebGLShader:true,WebGLShaderPrecisionFormat:true,WebGLSync:true,WebGLTexture:true,WebGLTimerQueryEXT:true,WebGLTransformFeedback:true,WebGLUniformLocation:true,WebGLVertexArrayObject:true,WebGLVertexArrayObjectOES:true,WebGL2RenderingContextBase:true,ArrayBuffer:true,ArrayBufferView:false,DataView:true,Float32Array:true,Float64Array:true,Int16Array:true,Int32Array:true,Int8Array:true,Uint16Array:true,Uint32Array:true,Uint8ClampedArray:true,CanvasPixelArray:true,Uint8Array:false,HTMLAudioElement:true,HTMLBRElement:true,HTMLBaseElement:true,HTMLBodyElement:true,HTMLButtonElement:true,HTMLCanvasElement:true,HTMLContentElement:true,HTMLDListElement:true,HTMLDataElement:true,HTMLDataListElement:true,HTMLDetailsElement:true,HTMLDialogElement:true,HTMLDivElement:true,HTMLEmbedElement:true,HTMLFieldSetElement:true,HTMLHRElement:true,HTMLHeadElement:true,HTMLHeadingElement:true,HTMLHtmlElement:true,HTMLIFrameElement:true,HTMLImageElement:true,HTMLInputElement:true,HTMLLIElement:true,HTMLLabelElement:true,HTMLLegendElement:true,HTMLLinkElement:true,HTMLMapElement:true,HTMLMediaElement:true,HTMLMenuElement:true,HTMLMetaElement:true,HTMLMeterElement:true,HTMLModElement:true,HTMLOListElement:true,HTMLObjectElement:true,HTMLOptGroupElement:true,HTMLOptionElement:true,HTMLOutputElement:true,HTMLParagraphElement:true,HTMLParamElement:true,HTMLPictureElement:true,HTMLPreElement:true,HTMLProgressElement:true,HTMLQuoteElement:true,HTMLScriptElement:true,HTMLShadowElement:true,HTMLSlotElement:true,HTMLSourceElement:true,HTMLSpanElement:true,HTMLStyleElement:true,HTMLTableCaptionElement:true,HTMLTableCellElement:true,HTMLTableDataCellElement:true,HTMLTableHeaderCellElement:true,HTMLTableColElement:true,HTMLTableElement:true,HTMLTableRowElement:true,HTMLTableSectionElement:true,HTMLTemplateElement:true,HTMLTextAreaElement:true,HTMLTimeElement:true,HTMLTitleElement:true,HTMLTrackElement:true,HTMLUListElement:true,HTMLUnknownElement:true,HTMLVideoElement:true,HTMLDirectoryElement:true,HTMLFontElement:true,HTMLFrameElement:true,HTMLFrameSetElement:true,HTMLMarqueeElement:true,HTMLElement:false,AccessibleNodeList:true,HTMLAnchorElement:true,HTMLAreaElement:true,Blob:false,CDATASection:true,CharacterData:true,Comment:true,ProcessingInstruction:true,Text:true,CSSPerspective:true,CSSCharsetRule:true,CSSConditionRule:true,CSSFontFaceRule:true,CSSGroupingRule:true,CSSImportRule:true,CSSKeyframeRule:true,MozCSSKeyframeRule:true,WebKitCSSKeyframeRule:true,CSSKeyframesRule:true,MozCSSKeyframesRule:true,WebKitCSSKeyframesRule:true,CSSMediaRule:true,CSSNamespaceRule:true,CSSPageRule:true,CSSRule:true,CSSStyleRule:true,CSSSupportsRule:true,CSSViewportRule:true,CSSStyleDeclaration:true,MSStyleCSSProperties:true,CSS2Properties:true,CSSImageValue:true,CSSKeywordValue:true,CSSNumericValue:true,CSSPositionValue:true,CSSResourceValue:true,CSSUnitValue:true,CSSURLImageValue:true,CSSStyleValue:false,CSSMatrixComponent:true,CSSRotation:true,CSSScale:true,CSSSkew:true,CSSTranslation:true,CSSTransformComponent:false,CSSTransformValue:true,CSSUnparsedValue:true,DataTransferItemList:true,DOMException:true,ClientRectList:true,DOMRectList:true,DOMRectReadOnly:false,DOMStringList:true,DOMTokenList:true,MathMLElement:true,SVGAElement:true,SVGAnimateElement:true,SVGAnimateMotionElement:true,SVGAnimateTransformElement:true,SVGAnimationElement:true,SVGCircleElement:true,SVGClipPathElement:true,SVGDefsElement:true,SVGDescElement:true,SVGDiscardElement:true,SVGEllipseElement:true,SVGFEBlendElement:true,SVGFEColorMatrixElement:true,SVGFEComponentTransferElement:true,SVGFECompositeElement:true,SVGFEConvolveMatrixElement:true,SVGFEDiffuseLightingElement:true,SVGFEDisplacementMapElement:true,SVGFEDistantLightElement:true,SVGFEFloodElement:true,SVGFEFuncAElement:true,SVGFEFuncBElement:true,SVGFEFuncGElement:true,SVGFEFuncRElement:true,SVGFEGaussianBlurElement:true,SVGFEImageElement:true,SVGFEMergeElement:true,SVGFEMergeNodeElement:true,SVGFEMorphologyElement:true,SVGFEOffsetElement:true,SVGFEPointLightElement:true,SVGFESpecularLightingElement:true,SVGFESpotLightElement:true,SVGFETileElement:true,SVGFETurbulenceElement:true,SVGFilterElement:true,SVGForeignObjectElement:true,SVGGElement:true,SVGGeometryElement:true,SVGGraphicsElement:true,SVGImageElement:true,SVGLineElement:true,SVGLinearGradientElement:true,SVGMarkerElement:true,SVGMaskElement:true,SVGMetadataElement:true,SVGPathElement:true,SVGPatternElement:true,SVGPolygonElement:true,SVGPolylineElement:true,SVGRadialGradientElement:true,SVGRectElement:true,SVGScriptElement:true,SVGSetElement:true,SVGStopElement:true,SVGStyleElement:true,SVGElement:true,SVGSVGElement:true,SVGSwitchElement:true,SVGSymbolElement:true,SVGTSpanElement:true,SVGTextContentElement:true,SVGTextElement:true,SVGTextPathElement:true,SVGTextPositioningElement:true,SVGTitleElement:true,SVGUseElement:true,SVGViewElement:true,SVGGradientElement:true,SVGComponentTransferFunctionElement:true,SVGFEDropShadowElement:true,SVGMPathElement:true,Element:false,AbortPaymentEvent:true,AnimationEvent:true,AnimationPlaybackEvent:true,ApplicationCacheErrorEvent:true,BackgroundFetchClickEvent:true,BackgroundFetchEvent:true,BackgroundFetchFailEvent:true,BackgroundFetchedEvent:true,BeforeInstallPromptEvent:true,BeforeUnloadEvent:true,BlobEvent:true,CanMakePaymentEvent:true,ClipboardEvent:true,CloseEvent:true,CompositionEvent:true,CustomEvent:true,DeviceMotionEvent:true,DeviceOrientationEvent:true,ErrorEvent:true,ExtendableEvent:true,ExtendableMessageEvent:true,FetchEvent:true,FocusEvent:true,FontFaceSetLoadEvent:true,ForeignFetchEvent:true,GamepadEvent:true,HashChangeEvent:true,InstallEvent:true,KeyboardEvent:true,MediaEncryptedEvent:true,MediaKeyMessageEvent:true,MediaQueryListEvent:true,MediaStreamEvent:true,MediaStreamTrackEvent:true,MIDIConnectionEvent:true,MIDIMessageEvent:true,MouseEvent:true,DragEvent:true,MutationEvent:true,NotificationEvent:true,PageTransitionEvent:true,PaymentRequestEvent:true,PaymentRequestUpdateEvent:true,PointerEvent:true,PopStateEvent:true,PresentationConnectionAvailableEvent:true,PresentationConnectionCloseEvent:true,ProgressEvent:true,PromiseRejectionEvent:true,PushEvent:true,RTCDataChannelEvent:true,RTCDTMFToneChangeEvent:true,RTCPeerConnectionIceEvent:true,RTCTrackEvent:true,SecurityPolicyViolationEvent:true,SensorErrorEvent:true,SpeechRecognitionError:true,SpeechRecognitionEvent:true,SpeechSynthesisEvent:true,StorageEvent:true,SyncEvent:true,TextEvent:true,TouchEvent:true,TrackEvent:true,TransitionEvent:true,WebKitTransitionEvent:true,UIEvent:true,VRDeviceEvent:true,VRDisplayEvent:true,VRSessionEvent:true,WheelEvent:true,MojoInterfaceRequestEvent:true,ResourceProgressEvent:true,USBConnectionEvent:true,AudioProcessingEvent:true,OfflineAudioCompletionEvent:true,WebGLContextEvent:true,Event:false,InputEvent:false,SubmitEvent:false,AbsoluteOrientationSensor:true,Accelerometer:true,AccessibleNode:true,AmbientLightSensor:true,Animation:true,ApplicationCache:true,DOMApplicationCache:true,OfflineResourceList:true,BackgroundFetchRegistration:true,BatteryManager:true,BroadcastChannel:true,CanvasCaptureMediaStreamTrack:true,EventSource:true,FileReader:true,FontFaceSet:true,Gyroscope:true,XMLHttpRequest:true,XMLHttpRequestEventTarget:true,XMLHttpRequestUpload:true,LinearAccelerationSensor:true,Magnetometer:true,MediaDevices:true,MediaKeySession:true,MediaQueryList:true,MediaRecorder:true,MediaSource:true,MediaStream:true,MediaStreamTrack:true,MIDIAccess:true,MIDIInput:true,MIDIOutput:true,MIDIPort:true,NetworkInformation:true,Notification:true,OffscreenCanvas:true,OrientationSensor:true,PaymentRequest:true,Performance:true,PermissionStatus:true,PresentationAvailability:true,PresentationConnection:true,PresentationConnectionList:true,PresentationRequest:true,RelativeOrientationSensor:true,RemotePlayback:true,RTCDataChannel:true,DataChannel:true,RTCDTMFSender:true,RTCPeerConnection:true,webkitRTCPeerConnection:true,mozRTCPeerConnection:true,ScreenOrientation:true,Sensor:true,ServiceWorker:true,ServiceWorkerContainer:true,ServiceWorkerRegistration:true,SharedWorker:true,SpeechRecognition:true,SpeechSynthesis:true,SpeechSynthesisUtterance:true,VR:true,VRDevice:true,VRDisplay:true,VRSession:true,VisualViewport:true,WebSocket:true,Window:true,DOMWindow:true,Worker:true,WorkerPerformance:true,BluetoothDevice:true,BluetoothRemoteGATTCharacteristic:true,Clipboard:true,MojoInterfaceInterceptor:true,USB:true,AnalyserNode:true,RealtimeAnalyserNode:true,AudioBufferSourceNode:true,AudioDestinationNode:true,AudioNode:true,AudioScheduledSourceNode:true,AudioWorkletNode:true,BiquadFilterNode:true,ChannelMergerNode:true,AudioChannelMerger:true,ChannelSplitterNode:true,AudioChannelSplitter:true,ConstantSourceNode:true,ConvolverNode:true,DelayNode:true,DynamicsCompressorNode:true,GainNode:true,AudioGainNode:true,IIRFilterNode:true,MediaElementAudioSourceNode:true,MediaStreamAudioDestinationNode:true,MediaStreamAudioSourceNode:true,OscillatorNode:true,Oscillator:true,PannerNode:true,AudioPannerNode:true,webkitAudioPannerNode:true,ScriptProcessorNode:true,JavaScriptAudioNode:true,StereoPannerNode:true,WaveShaperNode:true,EventTarget:false,File:true,FileList:true,FileWriter:true,HTMLFormElement:true,Gamepad:true,History:true,HTMLCollection:true,HTMLFormControlsCollection:true,HTMLOptionsCollection:true,ImageData:true,Location:true,MediaList:true,MessageEvent:true,MessagePort:true,MIDIInputMap:true,MIDIOutputMap:true,MimeType:true,MimeTypeArray:true,Document:true,DocumentFragment:true,HTMLDocument:true,ShadowRoot:true,XMLDocument:true,Attr:true,DocumentType:true,Node:false,NodeList:true,RadioNodeList:true,Plugin:true,PluginArray:true,RTCStatsReport:true,HTMLSelectElement:true,SharedArrayBuffer:true,SharedWorkerGlobalScope:true,SourceBuffer:true,SourceBufferList:true,SpeechGrammar:true,SpeechGrammarList:true,SpeechRecognitionResult:true,Storage:true,CSSStyleSheet:true,StyleSheet:true,TextTrack:true,TextTrackCue:true,VTTCue:true,TextTrackCueList:true,TextTrackList:true,TimeRanges:true,Touch:true,TouchList:true,TrackDefaultList:true,URL:true,VideoTrackList:true,DedicatedWorkerGlobalScope:true,ServiceWorkerGlobalScope:true,WorkerGlobalScope:false,CSSRuleList:true,ClientRect:true,DOMRect:true,GamepadList:true,NamedNodeMap:true,MozNamedAttrMap:true,SpeechRecognitionResultList:true,StyleSheetList:true,IDBCursor:false,IDBCursorWithValue:true,IDBDatabase:true,IDBFactory:true,IDBIndex:true,IDBObjectStore:true,IDBOpenDBRequest:true,IDBVersionChangeRequest:true,IDBRequest:true,IDBTransaction:true,IDBVersionChangeEvent:true,SVGLength:true,SVGLengthList:true,SVGNumber:true,SVGNumberList:true,SVGPointList:true,SVGStringList:true,SVGTransform:true,SVGTransformList:true,AudioBuffer:true,AudioParamMap:true,AudioTrackList:true,AudioContext:true,webkitAudioContext:true,BaseAudioContext:false,OfflineAudioContext:true}) A.ag.$nativeSuperclassTag="ArrayBufferView" A.eA.$nativeSuperclassTag="ArrayBufferView" A.eB.$nativeSuperclassTag="ArrayBufferView" A.bX.$nativeSuperclassTag="ArrayBufferView" A.eC.$nativeSuperclassTag="ArrayBufferView" A.eD.$nativeSuperclassTag="ArrayBufferView" A.aQ.$nativeSuperclassTag="ArrayBufferView" A.eF.$nativeSuperclassTag="EventTarget" A.eG.$nativeSuperclassTag="EventTarget" A.eL.$nativeSuperclassTag="EventTarget" A.eM.$nativeSuperclassTag="EventTarget"})() Function.prototype.$2=function(a,b){return this(a,b)} Function.prototype.$1=function(a){return this(a)} Function.prototype.$0=function(){return this()} Function.prototype.$3$3=function(a,b,c){return this(a,b,c)} Function.prototype.$2$2=function(a,b){return this(a,b)} Function.prototype.$1$1=function(a){return this(a)} Function.prototype.$2$1=function(a){return this(a)} Function.prototype.$3=function(a,b,c){return this(a,b,c)} Function.prototype.$4=function(a,b,c,d){return this(a,b,c,d)} Function.prototype.$3$1=function(a){return this(a)} Function.prototype.$1$0=function(){return this()} Function.prototype.$5=function(a,b,c,d,e){return this(a,b,c,d,e)} Function.prototype.$6=function(a,b,c,d,e,f){return this(a,b,c,d,e,f)} Function.prototype.$2$3=function(a,b,c){return this(a,b,c)} Function.prototype.$1$2=function(a,b){return this(a,b)} convertAllToFastObject(w) convertToFastObject($);(function(a){if(typeof document==="undefined"){a(null) return}if(typeof document.currentScript!="undefined"){a(document.currentScript) return}var s=document.scripts function onLoad(b){for(var q=0;q:_DEBUG>") endfunction() # Flutter library and tool build rules. set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") add_subdirectory(${FLUTTER_MANAGED_DIR}) # Application build; see runner/CMakeLists.txt. add_subdirectory("runner") # Generated plugin build rules, which manage building the plugins and adding # them to the application. include(flutter/generated_plugins.cmake) # === Installation === # Support files are copied into place next to the executable, so that it can # run in place. This is done instead of making a separate bundle (as on Linux) # so that building and running from within Visual Studio will work. set(BUILD_BUNDLE_DIR "$") # Make the "install" step default, as it's required to run. set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) endif() set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" COMPONENT Runtime) install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) if(PLUGIN_BUNDLED_LIBRARIES) install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) endif() # 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) # Install sqlite3.dll install(FILES "sqlite3.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" CONFIGURATIONS Profile;Release COMPONENT Runtime) install(FILES "msvcp140.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" CONFIGURATIONS Profile;Release COMPONENT Runtime) install(FILES "vcruntime140.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" CONFIGURATIONS Profile;Release COMPONENT Runtime) install(FILES "vcruntime140_1.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" CONFIGURATIONS Profile;Release COMPONENT Runtime) ================================================ FILE: windows/flutter/CMakeLists.txt ================================================ # This file controls Flutter-level build steps. It should not be edited. cmake_minimum_required(VERSION 3.14) set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") # Configuration provided via flutter tool. include(${EPHEMERAL_DIR}/generated_config.cmake) # TODO: Move the rest of this into files in ephemeral. See # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") # Set fallback configurations for older versions of the flutter tool. if (NOT DEFINED FLUTTER_TARGET_PLATFORM) set(FLUTTER_TARGET_PLATFORM "windows-x64") endif() # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") # Published to parent scope for install step. set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) list(APPEND FLUTTER_LIBRARY_HEADERS "flutter_export.h" "flutter_windows.h" "flutter_messenger.h" "flutter_plugin_registrar.h" "flutter_texture_registrar.h" ) list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") add_library(flutter INTERFACE) target_include_directories(flutter INTERFACE "${EPHEMERAL_DIR}" ) target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") add_dependencies(flutter flutter_assemble) # === Wrapper === list(APPEND CPP_WRAPPER_SOURCES_CORE "core_implementations.cc" "standard_codec.cc" ) list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") list(APPEND CPP_WRAPPER_SOURCES_PLUGIN "plugin_registrar.cc" ) list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") list(APPEND CPP_WRAPPER_SOURCES_APP "flutter_engine.cc" "flutter_view_controller.cc" ) list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") # Wrapper sources needed for a plugin. add_library(flutter_wrapper_plugin STATIC ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} ) apply_standard_settings(flutter_wrapper_plugin) set_target_properties(flutter_wrapper_plugin PROPERTIES POSITION_INDEPENDENT_CODE ON) set_target_properties(flutter_wrapper_plugin PROPERTIES CXX_VISIBILITY_PRESET hidden) target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) target_include_directories(flutter_wrapper_plugin PUBLIC "${WRAPPER_ROOT}/include" ) add_dependencies(flutter_wrapper_plugin flutter_assemble) # Wrapper sources needed for the runner. add_library(flutter_wrapper_app STATIC ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_APP} ) apply_standard_settings(flutter_wrapper_app) target_link_libraries(flutter_wrapper_app PUBLIC flutter) target_include_directories(flutter_wrapper_app PUBLIC "${WRAPPER_ROOT}/include" ) add_dependencies(flutter_wrapper_app flutter_assemble) # === Flutter tool backend === # _phony_ is a non-existent file to force this command to run every time, # since currently there's no way to get a full input/output list from the # flutter tool. set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) add_custom_command( OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} ${CPP_WRAPPER_SOURCES_APP} ${PHONY_OUTPUT} COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS "${FLUTTER_LIBRARY}" ${FLUTTER_LIBRARY_HEADERS} ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} ${CPP_WRAPPER_SOURCES_APP} ) ================================================ FILE: windows/flutter/generated_plugin_registrant.cc ================================================ // // Generated file. Do not edit. // // clang-format off #include "generated_plugin_registrant.h" #include #include #include #include #include #include #include #include #include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { AudioplayersWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin")); BitsdojoWindowPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("BitsdojoWindowPlugin")); FileSaverPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSaverPlugin")); FlutterLocalizationPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterLocalizationPluginCApi")); FlutterTtsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterTtsPlugin")); MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi")); MediaKitVideoPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi")); RecordWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("RecordWindowsPluginCApi")); ScreenBrightnessWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ScreenBrightnessWindowsPlugin")); SharePlusWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } ================================================ 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 audioplayers_windows bitsdojo_window_windows file_saver flutter_localization flutter_tts media_kit_libs_windows_video media_kit_video record_windows screen_brightness_windows share_plus url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST media_kit_native_event_loop ) 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.14) project(runner LANGUAGES CXX) # Define the application target. To change its name, change BINARY_NAME in the # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer # work. # # Any new source files that you add to the application should be added here. add_executable(${BINARY_NAME} WIN32 "flutter_window.cpp" "main.cpp" "utils.cpp" "win32_window.cpp" "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" "Runner.rc" "runner.exe.manifest" ) # Apply the standard set of build settings. This can be removed for applications # that need different build settings. apply_standard_settings(${BINARY_NAME}) # Add preprocessor definitions for the build version. target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") # Disable Windows macros that collide with C++ standard library functions. target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") # Add dependency libraries and include directories. Add any application-specific # dependencies here. target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") # Run the Flutter tool portions of the build. This must not be removed. add_dependencies(${BINARY_NAME} flutter_assemble) ================================================ 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", "cc.aicode.flutter.askaide" "\0" VALUE "FileDescription", "askaide" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "InternalName", "askaide" "\0" VALUE "LegalCopyright", "Copyright (C) 2023 cc.aicode.flutter.askaide. All rights reserved." "\0" VALUE "OriginalFilename", "askaide.exe" "\0" VALUE "ProductName", "askaide" "\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(const flutter::DartProject& project) : project_(project) {} FlutterWindow::~FlutterWindow() {} bool FlutterWindow::OnCreate() { if (!Win32Window::OnCreate()) { return false; } RECT frame = GetClientArea(); // The size here must match the window dimensions to avoid unnecessary surface // creation / destruction in the startup path. flutter_controller_ = std::make_unique( frame.right - frame.left, frame.bottom - frame.top, project_); // Ensure that basic setup of the controller was successful. if (!flutter_controller_->engine() || !flutter_controller_->view()) { return false; } RegisterPlugins(flutter_controller_->engine()); SetChildContent(flutter_controller_->view()->GetNativeWindow()); flutter_controller_->engine()->SetNextFrameCallback([&]() { this->Show(); }); flutter_controller_->ForceRedraw(); return true; } void FlutterWindow::OnDestroy() { if (flutter_controller_) { flutter_controller_ = nullptr; } Win32Window::OnDestroy(); } LRESULT FlutterWindow::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { // Give Flutter, including plugins, an opportunity to handle window messages. if (flutter_controller_) { std::optional result = flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, lparam); if (result) { return *result; } } switch (message) { case WM_FONTCHANGE: flutter_controller_->engine()->ReloadSystemFonts(); break; } return Win32Window::MessageHandler(hwnd, message, wparam, lparam); } ================================================ FILE: windows/runner/flutter_window.h ================================================ #ifndef RUNNER_FLUTTER_WINDOW_H_ #define RUNNER_FLUTTER_WINDOW_H_ #include #include #include #include "win32_window.h" // A window that does nothing but host a Flutter view. class FlutterWindow : public Win32Window { public: // Creates a new FlutterWindow hosting a Flutter view running |project|. explicit FlutterWindow(const flutter::DartProject& project); virtual ~FlutterWindow(); protected: // Win32Window: bool OnCreate() override; void OnDestroy() override; LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept override; private: // The project to run. flutter::DartProject project_; // The Flutter instance hosted by this window. std::unique_ptr flutter_controller_; }; #endif // RUNNER_FLUTTER_WINDOW_H_ ================================================ FILE: windows/runner/main.cpp ================================================ #include #include #include #include "flutter_window.h" #include "utils.h" #include auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP); int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { // Attach to console when present (e.g., 'flutter run') or create a // new console when running with a debugger. if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { CreateAndAttachConsole(); } // Initialize COM, so that it is available for use in the library and/or // plugins. ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); flutter::DartProject project(L"data"); std::vector command_line_arguments = GetCommandLineArguments(); project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(850, 750); if (!window.Create(L"AIdea", origin, size)) { return EXIT_FAILURE; } window.SetQuitOnClose(true); ::MSG msg; while (::GetMessage(&msg, nullptr, 0, 0)) { ::TranslateMessage(&msg); ::DispatchMessage(&msg); } ::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/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); std::string utf8_string; if (target_length == 0 || target_length > utf8_string.max_size()) { return utf8_string; } utf8_string.resize(target_length); int converted_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -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 #include "resource.h" namespace { /// Window attribute that enables dark mode window decorations. /// /// Redefined in case the developer's machine has a Windows SDK older than /// version 10.0.22000.0. /// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute #ifndef DWMWA_USE_IMMERSIVE_DARK_MODE #define DWMWA_USE_IMMERSIVE_DARK_MODE 20 #endif constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; /// Registry key for app theme preference. /// /// A value of 0 indicates apps should use dark mode. A non-zero or missing /// value indicates apps should use light mode. constexpr const wchar_t kGetPreferredBrightnessRegKey[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; // The number of Win32Window objects that currently exist. static int g_active_window_count = 0; using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); // Scale helper to convert logical scaler values to physical using passed in // scale factor int Scale(int source, double scale_factor) { return static_cast(source * scale_factor); } // Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. // This API is only needed for PerMonitor V1 awareness mode. void EnableFullDpiSupportIfAvailable(HWND hwnd) { HMODULE user32_module = LoadLibraryA("User32.dll"); if (!user32_module) { return; } auto enable_non_client_dpi_scaling = reinterpret_cast( GetProcAddress(user32_module, "EnableNonClientDpiScaling")); if (enable_non_client_dpi_scaling != nullptr) { enable_non_client_dpi_scaling(hwnd); } FreeLibrary(user32_module); } } // namespace // Manages the Win32Window's window class registration. class WindowClassRegistrar { public: ~WindowClassRegistrar() = default; // Returns the singleton 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::Create(const std::wstring& title, const Point& origin, const Size& size) { Destroy(); const wchar_t* window_class = WindowClassRegistrar::GetInstance()->GetWindowClass(); const POINT target_point = {static_cast(origin.x), static_cast(origin.y)}; HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); double scale_factor = dpi / 96.0; HWND window = CreateWindow( window_class, title.c_str(), WS_OVERLAPPEDWINDOW, Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), Scale(size.width, scale_factor), Scale(size.height, scale_factor), nullptr, nullptr, GetModuleHandle(nullptr), this); if (!window) { return false; } UpdateTheme(window); return OnCreate(); } bool Win32Window::Show() { return ShowWindow(window_handle_, SW_SHOWNORMAL); } // static LRESULT CALLBACK Win32Window::WndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { if (message == WM_NCCREATE) { auto window_struct = reinterpret_cast(lparam); SetWindowLongPtr(window, GWLP_USERDATA, reinterpret_cast(window_struct->lpCreateParams)); auto that = static_cast(window_struct->lpCreateParams); EnableFullDpiSupportIfAvailable(window); that->window_handle_ = window; } else if (Win32Window* that = GetThisFromHandle(window)) { return that->MessageHandler(window, message, wparam, lparam); } return DefWindowProc(window, message, wparam, lparam); } LRESULT Win32Window::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { switch (message) { case WM_DESTROY: window_handle_ = nullptr; Destroy(); if (quit_on_close_) { PostQuitMessage(0); } return 0; case WM_DPICHANGED: { auto newRectSize = reinterpret_cast(lparam); LONG newWidth = newRectSize->right - newRectSize->left; LONG newHeight = newRectSize->bottom - newRectSize->top; SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, newHeight, SWP_NOZORDER | SWP_NOACTIVATE); return 0; } case WM_SIZE: { RECT rect = GetClientArea(); if (child_content_ != nullptr) { // Size and position the child window. MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, TRUE); } return 0; } case WM_ACTIVATE: if (child_content_ != nullptr) { SetFocus(child_content_); } return 0; case WM_DWMCOLORIZATIONCOLORCHANGED: UpdateTheme(hwnd); return 0; } return DefWindowProc(window_handle_, message, wparam, lparam); } void Win32Window::Destroy() { OnDestroy(); if (window_handle_) { DestroyWindow(window_handle_); window_handle_ = nullptr; } if (g_active_window_count == 0) { WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); } } Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { return reinterpret_cast( GetWindowLongPtr(window, GWLP_USERDATA)); } void Win32Window::SetChildContent(HWND content) { child_content_ = content; SetParent(content, window_handle_); RECT frame = GetClientArea(); MoveWindow(content, frame.left, frame.top, frame.right - frame.left, frame.bottom - frame.top, true); SetFocus(child_content_); } RECT Win32Window::GetClientArea() { RECT frame; GetClientRect(window_handle_, &frame); return frame; } HWND Win32Window::GetHandle() { return window_handle_; } void Win32Window::SetQuitOnClose(bool quit_on_close) { quit_on_close_ = quit_on_close; } bool Win32Window::OnCreate() { // No-op; provided for subclasses. return true; } void Win32Window::OnDestroy() { // No-op; provided for subclasses. } void Win32Window::UpdateTheme(HWND const window) { DWORD light_mode; DWORD light_mode_size = sizeof(light_mode); LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, kGetPreferredBrightnessRegValue, RRF_RT_REG_DWORD, nullptr, &light_mode, &light_mode_size); if (result == ERROR_SUCCESS) { BOOL enable_dark_mode = light_mode == 0; DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, &enable_dark_mode, sizeof(enable_dark_mode)); } } ================================================ 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 a win32 window with |title| that is positioned and sized using // |origin| and |size|. New windows are created on the default monitor. Window // sizes are specified to the OS in physical pixels, hence to ensure a // consistent size this function will scale the inputted width and height as // as appropriate for the default monitor. The window is invisible until // |Show| is called. Returns true if the window was created successfully. bool Create(const std::wstring& title, const Point& origin, const Size& size); // Show the current window. Returns true if the window was successfully shown. bool Show(); // Release OS resources associated with window. void Destroy(); // Inserts |content| into the window tree. void SetChildContent(HWND content); // Returns the backing Window handle to enable clients to set icon and other // window properties. Returns nullptr if the window has been destroyed. HWND GetHandle(); // If true, closing this window will quit the application. void SetQuitOnClose(bool quit_on_close); // Return a RECT representing the bounds of the current client area. RECT GetClientArea(); protected: // Processes and route salient window messages for mouse handling, // size change and DPI. Delegates handling of these to member overloads that // inheriting classes can handle. virtual LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept; // Called when CreateAndShow is called, allowing subclass window-related // setup. Subclasses should return false if setup fails. virtual bool OnCreate(); // Called when Destroy is called. virtual void OnDestroy(); private: friend class WindowClassRegistrar; // OS callback called by message pump. Handles the WM_NCCREATE message which // is passed when the non-client area is being created and enables automatic // non-client DPI scaling so that the non-client area automatically // 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; // Update the window frame's theme to match the system theme. static void UpdateTheme(HWND const window); bool quit_on_close_ = false; // window handle for top level window. HWND window_handle_ = nullptr; // window handle for hosted content. HWND child_content_ = nullptr; }; #endif // RUNNER_WIN32_WINDOW_H_