├── .gitignore ├── .metadata ├── CHANGELOG.md ├── README.md ├── _cus_model_jsons ├── model_无问芯穹(少量自选).json ├── model_智谱.json ├── model_深度求索.json ├── model_百度(免费模型).json ├── model_硅基流动(少量自选).json ├── model_腾讯(免费模型).json ├── model_阿里云(少量自选).json ├── model_零一万物.json └── 平台ApiKey示例.json ├── _dishes └── HowToCook │ ├── HowToCook-All合并-持续更新-20240711.json │ ├── HowToCook-主食-20240708done.json │ ├── HowToCook-早餐-20240701done.json │ ├── HowToCook-水产-20240711done.json │ ├── HowToCook-汤粥-20240626done.json │ ├── HowToCook-甜品-20240701done.json │ ├── HowToCook-素菜-20240328done.json │ ├── HowToCook-荤菜-20240703done.json │ └── HowToCook-饮料-20240701done.json ├── _doc ├── changelog_pics │ ├── 0.2.0-beta.1新增内容截图.png │ └── 0.3.0-beta.1新增内容截图.jpg ├── screenshots │ ├── 1-1智能对话1.jpg │ ├── 1-2智能对话2.jpg │ ├── 2智能多聊.jpg │ ├── 3文档解读和图片解读.jpg │ ├── 4文本生图-文生视频.jpg │ ├── 5创意文字.jpg │ ├── 6图片生图.jpg │ ├── 7支持的角色管理模型列表,使用自己的密钥.jpg │ ├── 8极简记账.jpg │ ├── 9-1随机菜品1.jpg │ ├── 9-2随机菜品2.jpg │ └── brief_version │ │ ├── 助手工具.jpg │ │ ├── 对话页面.jpg │ │ ├── 导入配置等.jpg │ │ ├── 生活工具.jpg │ │ └── 角色扮演.jpg └── suchat_snapshots │ ├── snapshot.jpg │ └── suchat_chat_page.jpg ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── swm │ │ │ │ └── swmate │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets ├── brand.png ├── characters │ └── default_avatar.png ├── chat_backgrounds │ ├── bg1.jpg │ └── bg2.jpg └── images │ └── no_image.png ├── devtools_options.yaml ├── lib ├── apis │ ├── chat_completion │ │ ├── chat_helper.dart │ │ ├── common_cc_apis.dart │ │ └── openai_compatible_apis.dart │ ├── gen_access_token │ │ └── xfyun_signature.dart │ ├── get_app_key_helper.dart │ ├── life_tools │ │ ├── animal_lover │ │ │ ├── dogceo_apis.dart │ │ │ ├── random_fact_apis.dart │ │ │ └── thatapi_apis.dart │ │ ├── bangumi │ │ │ └── bangumi_apis.dart │ │ ├── food │ │ │ ├── nutritionix │ │ │ │ ├── nix_nutrient_enum_list.dart │ │ │ │ └── nutritionix_apis.dart │ │ │ └── usda_food_data_central │ │ │ │ └── usda_food_data_apis.dart │ │ ├── free_dictionary │ │ │ └── free_dictionary_apis.dart │ │ ├── hitokoto │ │ │ └── hitokoto_apis.dart │ │ ├── jikan │ │ │ └── get_jikan_apis.dart │ │ ├── news │ │ │ ├── momoyu_apis.dart │ │ │ ├── newsapi_apis.dart │ │ │ ├── readhub_apis.dart │ │ │ └── sina_roll_news_apis.dart │ │ └── waifu_pic │ │ │ └── index.dart │ └── voice_recognition │ │ └── xunfei_apis.dart ├── common │ ├── components │ │ ├── advanced_options_bottom_sheet.dart │ │ ├── advanced_options_panel.dart │ │ ├── bar_chart_widget.dart │ │ ├── chat_list_area.dart │ │ ├── cus_cards.dart │ │ ├── cus_code_field.dart │ │ ├── cus_markdown_renderer.dart │ │ ├── cus_platform_and_llm_row.dart │ │ ├── cus_toggle_button_selector.dart │ │ ├── custom_entrance_card.dart │ │ ├── feature_grid_card.dart │ │ ├── image_pick_and_preview_area.dart │ │ ├── loading_overlay.dart │ │ ├── message_item.dart │ │ ├── modern_feature_card.dart │ │ ├── optimized_custom_markdown_renderer.dart │ │ ├── searchable_dropdown.dart │ │ ├── simple_marquee_or_text.dart │ │ ├── sounds_message_button │ │ │ ├── button_widget │ │ │ │ ├── custom_canvas.dart │ │ │ │ ├── recording_status_mask.dart │ │ │ │ └── sounds_message_button.dart │ │ │ └── utils │ │ │ │ ├── recording_mask_overlay_data.dart │ │ │ │ └── sounds_recorder_controller.dart │ │ ├── style_grid_selector.dart │ │ ├── tool_widget.dart │ │ └── voice_chat_bubble.dart │ ├── constants │ │ ├── advanced_options_presets.dart │ │ ├── constants.dart │ │ ├── default_image_generation_models.dart │ │ ├── default_models.dart │ │ ├── default_video_generation_models.dart │ │ └── inner_system_prompt.dart │ ├── llm_spec │ │ ├── constant_llm_enum.dart │ │ ├── cus_brief_llm_model.dart │ │ └── cus_brief_llm_model.g.dart │ └── utils │ │ ├── advanced_options_utils.dart │ │ ├── cus_logger.dart │ │ ├── db_tools │ │ ├── db_brief_ai_tool_helper.dart │ │ ├── db_life_tool_helper.dart │ │ ├── ddl_brief_ai_tool.dart │ │ ├── ddl_life_tool.dart │ │ └── init_db.dart │ │ ├── dio_client │ │ ├── cus_http_client.dart │ │ ├── cus_http_options.dart │ │ ├── cus_http_request.dart │ │ ├── intercepter_response.dart │ │ ├── interceptor_error.dart │ │ └── interceptor_request.dart │ │ ├── document_parser.dart │ │ ├── handle_cc_response.dart │ │ └── tools.dart ├── main.dart ├── models │ ├── brief_ai_tools │ │ ├── branch_chat │ │ │ ├── branch_chat_export_data.dart │ │ │ ├── branch_chat_export_data.g.dart │ │ │ ├── branch_chat_message.dart │ │ │ ├── branch_chat_session.dart │ │ │ ├── branch_manager.dart │ │ │ └── branch_store.dart │ │ ├── character_chat │ │ │ ├── character_card.dart │ │ │ ├── character_chat_message.dart │ │ │ ├── character_chat_session.dart │ │ │ └── character_store.dart │ │ ├── chat_competion │ │ │ ├── com_cc_req.dart │ │ │ ├── com_cc_req.g.dart │ │ │ ├── com_cc_resp.dart │ │ │ ├── com_cc_resp.g.dart │ │ │ ├── com_cc_state.dart │ │ │ └── com_cc_state.g.dart │ │ ├── chat_completions │ │ │ ├── chat_completion_request.dart │ │ │ ├── chat_completion_response.dart │ │ │ ├── chat_completion_response.g.dart │ │ │ └── chat_completion_tool.dart │ │ ├── image_generation │ │ │ ├── image_generation_request.dart │ │ │ ├── image_generation_request.g.dart │ │ │ ├── image_generation_response.dart │ │ │ └── image_generation_response.g.dart │ │ ├── media_generation_history │ │ │ ├── media_generation_history.dart │ │ │ └── media_generation_history.g.dart │ │ ├── video_generation │ │ │ ├── video_generation_request.dart │ │ │ ├── video_generation_request.g.dart │ │ │ ├── video_generation_response.dart │ │ │ └── video_generation_response.g.dart │ │ └── voice_recognition │ │ │ ├── xunfei_voice_dictation.dart │ │ │ └── xunfei_voice_dictation.g.dart │ ├── life_tools │ │ ├── animal_lover │ │ │ ├── the_dog_cat_api_breed.dart │ │ │ ├── the_dog_cat_api_breed.g.dart │ │ │ ├── the_dog_cat_api_image.dart │ │ │ └── the_dog_cat_api_image.g.dart │ │ ├── bangumi │ │ │ ├── bangumi.dart │ │ │ └── bangumi.g.dart │ │ ├── brief_accounting_state.dart │ │ ├── dish_state.dart │ │ ├── dog_lover │ │ │ ├── dog_ceo_resp.dart │ │ │ └── dog_ceo_resp.g.dart │ │ ├── food │ │ │ ├── nutritionix │ │ │ │ ├── nix_natural_exercise_resp.dart │ │ │ │ ├── nix_natural_exercise_resp.g.dart │ │ │ │ ├── nix_natural_nutrient_resp.dart │ │ │ │ ├── nix_natural_nutrient_resp.g.dart │ │ │ │ ├── nix_search_instant_resp.dart │ │ │ │ └── nix_search_instant_resp.g.dart │ │ │ └── usda_food_data │ │ │ │ ├── usda_food_item.dart │ │ │ │ ├── usda_food_item.g.dart │ │ │ │ ├── usda_food_search_resp.dart │ │ │ │ └── usda_food_search_resp.g.dart │ │ ├── free_dictionary │ │ │ ├── free_dictionary_resp.dart │ │ │ └── free_dictionary_resp.g.dart │ │ ├── hitokoto │ │ │ ├── hitokoto.dart │ │ │ └── hitokoto.g.dart │ │ ├── jikan │ │ │ ├── jikan_data.dart │ │ │ ├── jikan_data.g.dart │ │ │ ├── jikan_related_character_resp.dart │ │ │ ├── jikan_related_character_resp.g.dart │ │ │ ├── jikan_statistic.dart │ │ │ └── jikan_statistic.g.dart │ │ └── news │ │ │ ├── momoyu_info_resp.dart │ │ │ ├── momoyu_info_resp.g.dart │ │ │ ├── news_api_resp.dart │ │ │ ├── news_api_resp.g.dart │ │ │ ├── readhub_hot_topic_resp.dart │ │ │ ├── readhub_hot_topic_resp.g.dart │ │ │ ├── sina_roll_news_resp.dart │ │ │ └── sina_roll_news_resp.g.dart │ └── mapper_utils.dart ├── objectbox-model.json ├── objectbox.g.dart ├── services │ ├── chat_service.dart │ ├── cus_get_storage.dart │ ├── image_generation_service.dart │ ├── model_manager_service.dart │ ├── network_service.dart │ └── video_generation_service.dart └── views │ ├── brief_ai_assistant │ ├── _chat_components │ │ ├── _small_tool_widgets.dart │ │ ├── _small_tools.dart │ │ ├── chat_input_bar.dart │ │ ├── model_filter.dart │ │ ├── model_selector.dart │ │ └── text_selection_dialog.dart │ ├── _chat_pages │ │ ├── chat_background_picker_page.dart │ │ └── chat_export_import_page.dart │ ├── branch_chat │ │ ├── branch_chat_page.dart │ │ ├── components │ │ │ ├── branch_chat_history_drawer.dart │ │ │ ├── branch_message_actions.dart │ │ │ ├── branch_message_item.dart │ │ │ └── branch_tree_dialog.dart │ │ └── pages │ │ │ └── add_model_page.dart │ ├── character_chat │ │ ├── character_chat_page.dart │ │ ├── character_editor_page.dart │ │ ├── character_list_page.dart │ │ └── components │ │ │ ├── character_avatar_preview.dart │ │ │ ├── character_card_item.dart │ │ │ ├── character_chat_history_drawer.dart │ │ │ ├── character_message_item.dart │ │ │ └── model_selector_dialog.dart │ ├── chat │ │ ├── components │ │ │ ├── chat_history_drawer.dart │ │ │ ├── chat_input.dart │ │ │ ├── chat_message_item.dart │ │ │ └── message_actions.dart │ │ └── index.dart │ ├── common │ │ ├── media_generation_base.dart │ │ ├── media_manager_base.dart │ │ ├── media_preview_base.dart │ │ ├── mime_media_manager_base.dart │ │ ├── mime_media_preview_base.dart │ │ └── show_media_info_dialog.dart │ ├── image │ │ ├── image_manager.dart │ │ ├── image_preview.dart │ │ ├── index.dart │ │ ├── mime_image_manager.dart │ │ ├── mime_image_preview.dart │ │ └── show_media_info_dialog.dart │ ├── index.dart │ ├── model_config │ │ ├── components │ │ │ ├── api_key_config.dart │ │ │ └── model_list.dart │ │ └── index.dart │ └── video │ │ ├── index.dart │ │ ├── mime_video_manager.dart │ │ ├── mime_video_preview.dart │ │ ├── video_manager.dart │ │ ├── video_player_screen.dart │ │ └── video_preview.dart │ ├── home.dart │ ├── life_tools │ ├── accounting │ │ ├── bill_item_modify │ │ │ └── index.dart │ │ ├── bill_report │ │ │ └── index.dart │ │ └── index.dart │ ├── animal_lover │ │ ├── animal_system_prompt.dart │ │ └── dog_cat_index.dart │ ├── anime_top │ │ ├── _components.dart │ │ ├── bangumi_calendar.dart │ │ ├── bangumi_item_detail.dart │ │ ├── bangumi_item_episode_detail.dart │ │ ├── mal_anime_schedule.dart │ │ ├── mal_item_detail.dart │ │ └── mal_top_index.dart │ ├── food │ │ ├── nutritionix │ │ │ ├── index.dart │ │ │ ├── nix_food_item_nutrients_page.dart │ │ │ └── nix_natural_language_query.dart │ │ ├── nutritionix_calculator │ │ │ ├── index.dart │ │ │ └── two_step_index.dart │ │ └── usda_food_data │ │ │ ├── food_item_nutrients.dart │ │ │ └── index.dart │ ├── free_dictionary │ │ └── index.dart │ ├── index.dart │ ├── news │ │ ├── _components │ │ │ ├── cus_news_card.dart │ │ │ ├── cus_scrollable_category_list.dart │ │ │ └── news_item_container.dart │ │ ├── base_news_page │ │ │ ├── base_news_page_state.dart │ │ │ ├── newsapi_page.dart │ │ │ └── sina_roll_news_page.dart │ │ ├── daily_60s │ │ │ └── index.dart │ │ ├── momoyu │ │ │ ├── index.dart │ │ │ └── list.dart │ │ └── readhub │ │ │ └── index.dart │ ├── random_dish │ │ ├── dish_detail.dart │ │ ├── dish_json_import.dart │ │ ├── dish_list.dart │ │ ├── dish_modify.dart │ │ └── dish_wheel_index.dart │ └── waifu_pics │ │ └── index.dart │ └── user_and_settings │ ├── backup_and_restore │ └── index.dart │ └── index.dart ├── pubspec.yaml └── test └── widget_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # Do not remove or rename entries in this file, only add new ones 2 | # See https://github.com/flutter/flutter/issues/128635 for more context. 3 | 4 | # Miscellaneous 5 | *.class 6 | *.lock 7 | *.log 8 | *.pyc 9 | *.swp 10 | .DS_Store 11 | .atom/ 12 | .buildlog/ 13 | .history 14 | .svn/ 15 | 16 | # IntelliJ related 17 | *.iml 18 | *.ipr 19 | *.iws 20 | .idea/ 21 | 22 | # Visual Studio Code related 23 | .classpath 24 | .project 25 | .settings/ 26 | .vscode/* 27 | 28 | # Flutter repo-specific 29 | /bin/cache/ 30 | /bin/internal/bootstrap.bat 31 | /bin/internal/bootstrap.sh 32 | /bin/mingit/ 33 | /dev/benchmarks/mega_gallery/ 34 | /dev/bots/.recipe_deps 35 | /dev/bots/android_tools/ 36 | /dev/devicelab/ABresults*.json 37 | /dev/docs/doc/ 38 | /dev/docs/api_docs.zip 39 | /dev/docs/flutter.docs.zip 40 | /dev/docs/lib/ 41 | /dev/docs/pubspec.yaml 42 | /dev/integration_tests/**/xcuserdata 43 | /dev/integration_tests/**/Pods 44 | /packages/flutter/coverage/ 45 | version 46 | analysis_benchmark.json 47 | 48 | # packages file containing multi-root paths 49 | .packages.generated 50 | 51 | # Flutter/Dart/Pub related 52 | **/doc/api/ 53 | .dart_tool/ 54 | .flutter-plugins 55 | .flutter-plugins-dependencies 56 | **/generated_plugin_registrant.dart 57 | .packages 58 | .pub-preload-cache/ 59 | .pub-cache/ 60 | .pub/ 61 | build/ 62 | flutter_*.png 63 | linked_*.ds 64 | unlinked.ds 65 | unlinked_spec.ds 66 | 67 | # Android related 68 | **/android/**/gradle-wrapper.jar 69 | .gradle/ 70 | **/android/captures/ 71 | **/android/gradlew 72 | **/android/gradlew.bat 73 | **/android/local.properties 74 | **/android/**/GeneratedPluginRegistrant.java 75 | **/android/key.properties 76 | *.jks 77 | 78 | # iOS/XCode related 79 | **/ios/**/*.mode1v3 80 | **/ios/**/*.mode2v3 81 | **/ios/**/*.moved-aside 82 | **/ios/**/*.pbxuser 83 | **/ios/**/*.perspectivev3 84 | **/ios/**/*sync/ 85 | **/ios/**/.sconsign.dblite 86 | **/ios/**/.tags* 87 | **/ios/**/.vagrant/ 88 | **/ios/**/DerivedData/ 89 | **/ios/**/Icon? 90 | **/ios/**/Pods/ 91 | **/ios/**/.symlinks/ 92 | **/ios/**/profile 93 | **/ios/**/xcuserdata 94 | **/ios/.generated/ 95 | **/ios/Flutter/.last_build_id 96 | **/ios/Flutter/App.framework 97 | **/ios/Flutter/Flutter.framework 98 | **/ios/Flutter/Flutter.podspec 99 | **/ios/Flutter/Generated.xcconfig 100 | **/ios/Flutter/ephemeral 101 | **/ios/Flutter/app.flx 102 | **/ios/Flutter/app.zip 103 | **/ios/Flutter/flutter_assets/ 104 | **/ios/Flutter/flutter_export_environment.sh 105 | **/ios/ServiceDefinitions.json 106 | **/ios/Runner/GeneratedPluginRegistrant.* 107 | 108 | # macOS 109 | **/Flutter/ephemeral/ 110 | **/Pods/ 111 | **/macos/Flutter/GeneratedPluginRegistrant.swift 112 | **/macos/Flutter/ephemeral 113 | **/xcuserdata/ 114 | 115 | # Windows 116 | **/windows/flutter/generated_plugin_registrant.cc 117 | **/windows/flutter/generated_plugin_registrant.h 118 | **/windows/flutter/generated_plugins.cmake 119 | 120 | # Linux 121 | **/linux/flutter/generated_plugin_registrant.cc 122 | **/linux/flutter/generated_plugin_registrant.h 123 | **/linux/flutter/generated_plugins.cmake 124 | 125 | # Coverage 126 | coverage/ 127 | 128 | # Symbols 129 | app.*.symbols 130 | 131 | # Exceptions to above rules. 132 | !**/ios/**/default.mode1v3 133 | !**/ios/**/default.mode2v3 134 | !**/ios/**/default.pbxuser 135 | !**/ios/**/default.perspectivev3 136 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 137 | !/dev/ci/**/Gemfile.lock 138 | !.vscode/settings.json 139 | 140 | ## 上面的内容来自:https://github.com/flutter/flutter/blob/master/.gitignore 141 | 142 | # 下面的内容是自己测试的 143 | 144 | # 暂时不关注ios、web、windows、mocos、linux等的构建 145 | ios 146 | web 147 | windows 148 | macos 149 | linux 150 | 151 | # 统计行插件自己生成的文件夹 152 | .VSCodeCounter 153 | 154 | # 一些本地测试的组件页面等 155 | **/_self* 156 | **/*mock* 157 | **/*bak* 158 | **/*demo* 159 | **/*bugreport* 160 | .aider* 161 | 162 | # 2025-03-21 升级后运行时生成的文件夹,应该没用 163 | android/app/.cxx/ -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "761747bfc538b5af34aa0d3fac380f1bc331ec49" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 17 | base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 18 | - platform: android 19 | create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 20 | base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 21 | - platform: ios 22 | create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 23 | base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 24 | - platform: linux 25 | create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 26 | base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 27 | - platform: macos 28 | create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 29 | base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 30 | - platform: web 31 | create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 32 | base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 33 | - platform: windows 34 | create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 35 | base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /_cus_model_jsons/model_无问芯穹(少量自选).json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "platform": "infini", 4 | "model": "deepseek-r1", 5 | "modelType": "cc", 6 | "name": "deepseek-r1", 7 | "contextLength": 64000, 8 | "isFree": false, 9 | "inputPrice": 4, 10 | "outputPrice": 16, 11 | "gmtRelease": "2025-02-06" 12 | }, 13 | { 14 | "platform": "infini", 15 | "model": "deepseek-v3", 16 | "modelType": "cc", 17 | "name": "deepseek-v3", 18 | "contextLength": 64000, 19 | "isFree": false, 20 | "inputPrice": 2, 21 | "outputPrice": 8, 22 | "gmtRelease": "2025-02-07" 23 | }, 24 | { 25 | "platform": "infini", 26 | "model": "deepseek-r1-distill-qwen-32b", 27 | "modelType": "cc", 28 | "name": "deepseek-r1-distill-qwen-32b", 29 | "contextLength": 32000, 30 | "isFree": false, 31 | "inputPrice": 0, 32 | "outputPrice": 0, 33 | "gmtRelease": "2025-01-21" 34 | }, 35 | { 36 | "platform": "infini", 37 | "model": "qwq-32b-preview", 38 | "modelType": "cc", 39 | "name": "qwq-32b-preview", 40 | "contextLength": 32, 41 | "isFree": false, 42 | "inputPrice": 0, 43 | "outputPrice": 0, 44 | "gmtRelease": "2024-12-02" 45 | }, 46 | { 47 | "platform": "infini", 48 | "model": "qwen2.5-72b-instruct", 49 | "modelType": "cc", 50 | "name": "qwen2.5-72b-instruct", 51 | "contextLength": 32000, 52 | "isFree": false, 53 | "inputPrice": 0, 54 | "outputPrice": 0, 55 | "gmtRelease": "2024-09-19" 56 | }, 57 | { 58 | "platform": "infini", 59 | "model": "qwen2.5-32b-instruct", 60 | "modelType": "cc", 61 | "name": "qwen2.5-32b-instruct", 62 | "contextLength": 32000, 63 | "isFree": false, 64 | "inputPrice": 0, 65 | "outputPrice": 0, 66 | "gmtRelease": "2024-09-20" 67 | }, 68 | { 69 | "platform": "infini", 70 | "model": "qwen2.5-14b-instruct", 71 | "modelType": "cc", 72 | "name": "qwen2.5-14b-instruct", 73 | "contextLength": 32000, 74 | "isFree": false, 75 | "inputPrice": 0, 76 | "outputPrice": 0, 77 | "gmtRelease": "2024-09-20" 78 | }, 79 | { 80 | "platform": "infini", 81 | "model": "qwen2-72b-instruct", 82 | "modelType": "cc", 83 | "name": "qwen2-72b-instruct", 84 | "contextLength": 32000, 85 | "isFree": false, 86 | "inputPrice": 0, 87 | "outputPrice": 0, 88 | "gmtRelease": "2024-06-12" 89 | } 90 | ] 91 | -------------------------------------------------------------------------------- /_cus_model_jsons/model_智谱.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "platform": "zhipu", 4 | "model": "glm-4-plus", 5 | "modelType": "cc", 6 | "name": "GLM-4-Plus", 7 | "contextLength": 128000, 8 | "isFree": false, 9 | "inputPrice": 50, 10 | "outputPrice": 50, 11 | "gmtRelease": "1970-01-01" 12 | }, 13 | { 14 | "platform": "zhipu", 15 | "model": "glm-4-air", 16 | "modelType": "cc", 17 | "name": "GLM-4-Air", 18 | "contextLength": 128000, 19 | "isFree": false, 20 | "inputPrice": 0.5, 21 | "outputPrice": 0.5, 22 | "gmtRelease": "1970-01-01" 23 | }, 24 | { 25 | "platform": "zhipu", 26 | "model": "glm-4-long", 27 | "modelType": "cc", 28 | "name": "GLM-4-Long", 29 | "contextLength": 1000000, 30 | "isFree": false, 31 | "inputPrice": 1, 32 | "outputPrice": 1, 33 | "gmtRelease": "1970-01-01" 34 | }, 35 | { 36 | "platform": "zhipu", 37 | "model": "glm-zero-preview", 38 | "modelType": "cc", 39 | "name": "GLM-Zero-Preview", 40 | "contextLength": 16000, 41 | "isFree": false, 42 | "inputPrice": 10, 43 | "outputPrice": 10, 44 | "gmtRelease": "1970-01-01" 45 | }, 46 | { 47 | "platform": "zhipu", 48 | "model": "glm-4-airx", 49 | "modelType": "cc", 50 | "name": "GLM-4-AirX", 51 | "contextLength": 8000, 52 | "isFree": false, 53 | "inputPrice": 10, 54 | "outputPrice": 10, 55 | "gmtRelease": "1970-01-01" 56 | }, 57 | { 58 | "platform": "zhipu", 59 | "model": "glm-4-flashx", 60 | "modelType": "cc", 61 | "name": "GLM-4-FlashX", 62 | "contextLength": 128000, 63 | "isFree": false, 64 | "inputPrice": 0.1, 65 | "outputPrice": 0.1, 66 | "gmtRelease": "1970-01-01" 67 | }, 68 | { 69 | "platform": "zhipu", 70 | "model": "glm-4v-plus-0111", 71 | "modelType": "vision", 72 | "name": "GLM-4V-Plus-0111", 73 | "contextLength": 8000, 74 | "isFree": false, 75 | "inputPrice": 4, 76 | "outputPrice": 4, 77 | "gmtRelease": "1970-01-01" 78 | }, 79 | { 80 | "platform": "zhipu", 81 | "model": "cogview-4", 82 | "modelType": "image", 83 | "name": "CogView-4", 84 | "isFree": false, 85 | "costPer": 0.06, 86 | "gmtRelease": "1970-01-01" 87 | }, 88 | { 89 | "platform": "zhipu", 90 | "model": "cogvideox-2", 91 | "modelType": "video", 92 | "name": "CogVideoX-2", 93 | "isFree": false, 94 | "costPer": 0.5, 95 | "gmtRelease": "1970-01-01" 96 | }, 97 | 98 | { 99 | "platform": "zhipu", 100 | "model": "glm-4-flash", 101 | "modelType": "cc", 102 | "name": "CGLM-4-Flash", 103 | "isFree": true, 104 | "inputPrice": 0.0, 105 | "outputPrice": 0.0, 106 | "gmtRelease": "1970-01-01" 107 | }, 108 | { 109 | "platform": "zhipu", 110 | "model": "glm-4v-flash", 111 | "modelType": "vision", 112 | "name": "GLM-4V-Flash", 113 | "isFree": true, 114 | "inputPrice": 0.0, 115 | "outputPrice": 0.0, 116 | "gmtRelease": "1970-01-01" 117 | }, 118 | { 119 | "platform": "zhipu", 120 | "model": "cogview-3-flash", 121 | "modelType": "image", 122 | "name": "CogView-3-Flash", 123 | "isFree": true, 124 | "costPer": 0.0, 125 | "gmtRelease": "1970-01-01" 126 | }, 127 | { 128 | "platform": "zhipu", 129 | "model": "ccogvideox-flash", 130 | "modelType": "video", 131 | "name": "CogVideoX-Flash", 132 | "isFree": true, 133 | "costPer": 0.0, 134 | "gmtRelease": "1970-01-01" 135 | } 136 | ] 137 | -------------------------------------------------------------------------------- /_cus_model_jsons/model_深度求索.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "platform": "deepseek", 4 | "model": "deepseek-chat", 5 | "modelType": "cc", 6 | "name": "DeepSeek-V3", 7 | "contextLength": 64000, 8 | "isFree": false, 9 | "inputPrice": 2, 10 | "outputPrice": 8, 11 | "gmtRelease": "2024-12-26" 12 | }, 13 | { 14 | "platform": "deepseek", 15 | "model": "deepseek-reasoner", 16 | "modelType": "cc", 17 | "name": "DeepSeek-R1", 18 | "contextLength": 64000, 19 | "isFree": false, 20 | "inputPrice": 4, 21 | "outputPrice": 16, 22 | "gmtRelease": "2025-01-20" 23 | } 24 | ] 25 | -------------------------------------------------------------------------------- /_cus_model_jsons/model_百度(免费模型).json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "platform": "baidu", 4 | "model": "ernie-tiny-8k", 5 | "modelType": "cc", 6 | "name": "ERNIE-Tiny-8K", 7 | "contextLength": 8192, 8 | "isFree": true, 9 | "inputPrice": 0.0, 10 | "outputPrice": 0.0, 11 | "gmtRelease": "2024-10-10" 12 | }, 13 | { 14 | "platform": "baidu", 15 | "model": "ernie-lite-8k", 16 | "modelType": "cc", 17 | "name": "ERNIE-Lite-8K", 18 | "contextLength": 8192, 19 | "isFree": true, 20 | "inputPrice": 0.0, 21 | "outputPrice": 0.0, 22 | "gmtRelease": "2024-03-08" 23 | }, 24 | { 25 | "platform": "baidu", 26 | "model": "ernie-speed-8k", 27 | "modelType": "cc", 28 | "name": "ERNIE-Speed-8K", 29 | "contextLength": 8192, 30 | "isFree": true, 31 | "inputPrice": 0.0, 32 | "outputPrice": 0.0, 33 | "gmtRelease": "2024-01-11" 34 | }, 35 | { 36 | "platform": "baidu", 37 | "model": "ernie-speed-128k", 38 | "modelType": "cc", 39 | "name": "ERNIE-Speed-128K", 40 | "contextLength": 128000, 41 | "isFree": true, 42 | "inputPrice": 0.0, 43 | "outputPrice": 0.0, 44 | "gmtRelease": "2024-03-14" 45 | } 46 | ] 47 | -------------------------------------------------------------------------------- /_cus_model_jsons/model_腾讯(免费模型).json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "platform": "tencent", 4 | "model": "hunyuan-lite", 5 | "modelType": "cc", 6 | "name": "混元-Lite", 7 | "contextLength": 256000, 8 | "isFree": true, 9 | "inputPrice": 0.0, 10 | "outputPrice": 0.0, 11 | "gmtRelease": "2024-10-30" 12 | } 13 | ] 14 | -------------------------------------------------------------------------------- /_cus_model_jsons/model_零一万物.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "platform": "lingyiwanwu", 4 | "model": "yi-lightning", 5 | "modelType": "cc", 6 | "name": "yi-lightning", 7 | "contextLength": 16000, 8 | "isFree": false, 9 | "inputPrice": 0.99, 10 | "outputPrice": 0.99, 11 | "gmtRelease": "2024-12-23" 12 | }, 13 | { 14 | "platform": "lingyiwanwu", 15 | "model": "yi-vision-v2", 16 | "modelType": "vision", 17 | "name": "yi-vision-v2", 18 | "contextLength": 16000, 19 | "isFree": false, 20 | "inputPrice": 6, 21 | "outputPrice": 6, 22 | "gmtRelease": "2024-12-23" 23 | } 24 | ] 25 | -------------------------------------------------------------------------------- /_cus_model_jsons/平台ApiKey示例.json: -------------------------------------------------------------------------------- 1 | { 2 | "USER_ALIYUN_API_KEY": "sk-xxx", 3 | "USER_BAIDU_API_KEY_V2": "bce-v3/ALTAK-xx/xxx", 4 | "USER_TENCENT_API_KEY": "sk-xxx", 5 | 6 | "USER_DEEPSEEK_API_KEY": "sk-xxx", 7 | "USER_LINGYIWANWU_API_KEY": "xxx", 8 | "USER_ZHIPU_API_KEY": "xxx.xxx", 9 | 10 | "USER_SILICONCLOUD_API_KEY": "sk-xxx", 11 | "USER_INFINI_GEN_STUDIO_API_KEY": "sk-xxx", 12 | 13 | "USER_VOLCENGINE_API_KEY": "xxx", 14 | "USER_VOLCESBOT_API_KEY": "xxx", 15 | 16 | "USER_XFYUN_APP_ID": "xxx", 17 | "USER_XFYUN_API_KEY": "xxx", 18 | "USER_XFYUN_API_SECRET": "xxx", 19 | 20 | "USER_NUTRITIONIX_APP_ID": "xxx", 21 | "USER_NUTRITIONIX_APP_KEY": "xxx", 22 | 23 | "USER_NEWS_API_KEY": "xxx" 24 | } 25 | -------------------------------------------------------------------------------- /_doc/changelog_pics/0.2.0-beta.1新增内容截图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanotsu/swmate/c2749549a4b9dea523351b517576b4ec110422e5/_doc/changelog_pics/0.2.0-beta.1新增内容截图.png -------------------------------------------------------------------------------- /_doc/changelog_pics/0.3.0-beta.1新增内容截图.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanotsu/swmate/c2749549a4b9dea523351b517576b4ec110422e5/_doc/changelog_pics/0.3.0-beta.1新增内容截图.jpg -------------------------------------------------------------------------------- /_doc/screenshots/1-1智能对话1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanotsu/swmate/c2749549a4b9dea523351b517576b4ec110422e5/_doc/screenshots/1-1智能对话1.jpg -------------------------------------------------------------------------------- /_doc/screenshots/1-2智能对话2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanotsu/swmate/c2749549a4b9dea523351b517576b4ec110422e5/_doc/screenshots/1-2智能对话2.jpg -------------------------------------------------------------------------------- /_doc/screenshots/2智能多聊.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanotsu/swmate/c2749549a4b9dea523351b517576b4ec110422e5/_doc/screenshots/2智能多聊.jpg -------------------------------------------------------------------------------- /_doc/screenshots/3文档解读和图片解读.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanotsu/swmate/c2749549a4b9dea523351b517576b4ec110422e5/_doc/screenshots/3文档解读和图片解读.jpg -------------------------------------------------------------------------------- /_doc/screenshots/4文本生图-文生视频.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanotsu/swmate/c2749549a4b9dea523351b517576b4ec110422e5/_doc/screenshots/4文本生图-文生视频.jpg -------------------------------------------------------------------------------- /_doc/screenshots/5创意文字.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanotsu/swmate/c2749549a4b9dea523351b517576b4ec110422e5/_doc/screenshots/5创意文字.jpg -------------------------------------------------------------------------------- /_doc/screenshots/6图片生图.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanotsu/swmate/c2749549a4b9dea523351b517576b4ec110422e5/_doc/screenshots/6图片生图.jpg -------------------------------------------------------------------------------- /_doc/screenshots/7支持的角色管理模型列表,使用自己的密钥.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanotsu/swmate/c2749549a4b9dea523351b517576b4ec110422e5/_doc/screenshots/7支持的角色管理模型列表,使用自己的密钥.jpg -------------------------------------------------------------------------------- /_doc/screenshots/8极简记账.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanotsu/swmate/c2749549a4b9dea523351b517576b4ec110422e5/_doc/screenshots/8极简记账.jpg -------------------------------------------------------------------------------- /_doc/screenshots/9-1随机菜品1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanotsu/swmate/c2749549a4b9dea523351b517576b4ec110422e5/_doc/screenshots/9-1随机菜品1.jpg -------------------------------------------------------------------------------- /_doc/screenshots/9-2随机菜品2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanotsu/swmate/c2749549a4b9dea523351b517576b4ec110422e5/_doc/screenshots/9-2随机菜品2.jpg -------------------------------------------------------------------------------- /_doc/screenshots/brief_version/助手工具.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanotsu/swmate/c2749549a4b9dea523351b517576b4ec110422e5/_doc/screenshots/brief_version/助手工具.jpg -------------------------------------------------------------------------------- /_doc/screenshots/brief_version/对话页面.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanotsu/swmate/c2749549a4b9dea523351b517576b4ec110422e5/_doc/screenshots/brief_version/对话页面.jpg -------------------------------------------------------------------------------- /_doc/screenshots/brief_version/导入配置等.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanotsu/swmate/c2749549a4b9dea523351b517576b4ec110422e5/_doc/screenshots/brief_version/导入配置等.jpg -------------------------------------------------------------------------------- /_doc/screenshots/brief_version/生活工具.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanotsu/swmate/c2749549a4b9dea523351b517576b4ec110422e5/_doc/screenshots/brief_version/生活工具.jpg -------------------------------------------------------------------------------- /_doc/screenshots/brief_version/角色扮演.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanotsu/swmate/c2749549a4b9dea523351b517576b4ec110422e5/_doc/screenshots/brief_version/角色扮演.jpg -------------------------------------------------------------------------------- /_doc/suchat_snapshots/snapshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanotsu/swmate/c2749549a4b9dea523351b517576b4ec110422e5/_doc/suchat_snapshots/snapshot.jpg -------------------------------------------------------------------------------- /_doc/suchat_snapshots/suchat_chat_page.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanotsu/swmate/c2749549a4b9dea523351b517576b4ec110422e5/_doc/suchat_snapshots/suchat_chat_page.jpg -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at https://dart.dev/lints. 17 | # 18 | # Instead of disabling a lint rule for the entire project in the 19 | # section below, it can also be suppressed for a single line of code 20 | # or a specific dart file by using the `// ignore: name_of_lint` and 21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 22 | # producing the lint. 23 | rules: 24 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 25 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 26 | 27 | # Additional information about this file can be found at 28 | # https://dart.dev/guides/language/analysis-options 29 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # This is generated automatically by the Android Gradle plugin. 2 | -dontwarn java.awt.Color 3 | -dontwarn java.awt.Dimension 4 | -dontwarn java.awt.Rectangle 5 | -dontwarn java.awt.color.ColorSpace 6 | -dontwarn java.awt.geom.AffineTransform 7 | -dontwarn java.awt.geom.Dimension2D 8 | -dontwarn java.awt.geom.Path2D 9 | -dontwarn java.awt.geom.PathIterator 10 | -dontwarn java.awt.geom.Point2D 11 | -dontwarn java.awt.geom.Rectangle2D 12 | -dontwarn java.awt.image.BufferedImage 13 | -dontwarn java.awt.image.ColorModel 14 | -dontwarn java.awt.image.ComponentColorModel 15 | -dontwarn java.awt.image.DirectColorModel 16 | -dontwarn java.awt.image.IndexColorModel 17 | -dontwarn java.awt.image.PackedColorModel 18 | -dontwarn javax.xml.stream.Location 19 | -dontwarn javax.xml.stream.XMLStreamException 20 | -dontwarn javax.xml.stream.XMLStreamReader 21 | -dontwarn net.sf.saxon.Configuration 22 | -dontwarn net.sf.saxon.dom.DOMNodeWrapper 23 | -dontwarn net.sf.saxon.om.Item 24 | -dontwarn net.sf.saxon.om.NodeInfo 25 | -dontwarn net.sf.saxon.om.Sequence 26 | -dontwarn net.sf.saxon.om.SequenceTool 27 | -dontwarn net.sf.saxon.sxpath.IndependentContext 28 | -dontwarn net.sf.saxon.sxpath.XPathDynamicContext 29 | -dontwarn net.sf.saxon.sxpath.XPathEvaluator 30 | -dontwarn net.sf.saxon.sxpath.XPathExpression 31 | -dontwarn net.sf.saxon.sxpath.XPathStaticContext 32 | -dontwarn net.sf.saxon.sxpath.XPathVariable 33 | -dontwarn net.sf.saxon.tree.wrapper.VirtualNode 34 | -dontwarn org.apache.batik.anim.dom.SAXSVGDocumentFactory 35 | -dontwarn org.apache.batik.bridge.BridgeContext 36 | -dontwarn org.apache.batik.bridge.DocumentLoader 37 | -dontwarn org.apache.batik.bridge.GVTBuilder 38 | -dontwarn org.apache.batik.bridge.UserAgent 39 | -dontwarn org.apache.batik.bridge.UserAgentAdapter 40 | -dontwarn org.apache.batik.util.XMLResourceDescriptor -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 19 | 28 | 32 | 36 | 37 | 38 | 39 | 40 | 41 | 43 | 46 | 47 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/swm/swmate/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.swm.swmate 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanotsu/swmate/c2749549a4b9dea523351b517576b4ec110422e5/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanotsu/swmate/c2749549a4b9dea523351b517576b4ec110422e5/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanotsu/swmate/c2749549a4b9dea523351b517576b4ec110422e5/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanotsu/swmate/c2749549a4b9dea523351b517576b4ec110422e5/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanotsu/swmate/c2749549a4b9dea523351b517576b4ec110422e5/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | rootProject.buildDir = "../build" 9 | subprojects { 10 | project.buildDir = "${rootProject.buildDir}/${project.name}" 11 | } 12 | subprojects { 13 | project.evaluationDependsOn(":app") 14 | } 15 | 16 | tasks.register("clean", Delete) { 17 | delete rootProject.buildDir 18 | } 19 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G -XX:+HeapDumpOnOutOfMemoryError 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | android.enableR8=true 5 | # org.gradle.java.home=D:/DvptTools/Java/openlogic-openjdk-17.0.10+7-windows-x64 6 | org.gradle.java.home=/home/david/.jdks/temurin-17.0.6 -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | # distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip 6 | # distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip 7 | distributionUrl = https\://services.gradle.org/distributions/gradle-8.7-all.zip -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | }() 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | // 2024-11-04 更新到最新(注意,这里的插件版本最小需要的gradle版本不一定一样) 20 | // 文档:https://developer.android.com/build/releases/gradle-plugin?hl=zh-cn#kts 21 | plugins { 22 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 23 | // id "com.android.application" version "8.3.1" apply false 24 | // id "org.jetbrains.kotlin.android" version "1.8.22" apply false 25 | 26 | id 'com.android.application' version '8.6.0' apply false 27 | id 'com.android.library' version '8.6.0' apply false 28 | id 'org.jetbrains.kotlin.android' version '2.0.20' apply false 29 | } 30 | 31 | 32 | include ":app" 33 | -------------------------------------------------------------------------------- /assets/brand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanotsu/swmate/c2749549a4b9dea523351b517576b4ec110422e5/assets/brand.png -------------------------------------------------------------------------------- /assets/characters/default_avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanotsu/swmate/c2749549a4b9dea523351b517576b4ec110422e5/assets/characters/default_avatar.png -------------------------------------------------------------------------------- /assets/chat_backgrounds/bg1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanotsu/swmate/c2749549a4b9dea523351b517576b4ec110422e5/assets/chat_backgrounds/bg1.jpg -------------------------------------------------------------------------------- /assets/chat_backgrounds/bg2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanotsu/swmate/c2749549a4b9dea523351b517576b4ec110422e5/assets/chat_backgrounds/bg2.jpg -------------------------------------------------------------------------------- /assets/images/no_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sanotsu/swmate/c2749549a4b9dea523351b517576b4ec110422e5/assets/images/no_image.png -------------------------------------------------------------------------------- /devtools_options.yaml: -------------------------------------------------------------------------------- 1 | description: This file stores settings for Dart & Flutter DevTools. 2 | documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states 3 | extensions: 4 | -------------------------------------------------------------------------------- /lib/apis/chat_completion/chat_helper.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import '../../common/utils/tools.dart'; 4 | 5 | /// 6 | /// dio 中处理SSE的解析器 7 | /// 来源: https://github.com/cfug/dio/issues/1279#issuecomment-1326121953 8 | /// 9 | class SseTransformer extends StreamTransformerBase { 10 | const SseTransformer(); 11 | @override 12 | Stream bind(Stream stream) { 13 | return Stream.eventTransformed(stream, (sink) => SseEventSink(sink)); 14 | } 15 | } 16 | 17 | class SseEventSink implements EventSink { 18 | final EventSink _eventSink; 19 | 20 | String? _id; 21 | String _event = "message"; 22 | String _data = ""; 23 | int? _retry; 24 | 25 | SseEventSink(this._eventSink); 26 | 27 | @override 28 | void add(String event) { 29 | if (event.startsWith("id:")) { 30 | _id = event.substring(3); 31 | return; 32 | } 33 | if (event.startsWith("event:")) { 34 | _event = event.substring(6); 35 | return; 36 | } 37 | if (event.startsWith("data:")) { 38 | _data = event.substring(5); 39 | return; 40 | } 41 | if (event.startsWith("retry:")) { 42 | _retry = int.tryParse(event.substring(6)); 43 | return; 44 | } 45 | if (event.isEmpty) { 46 | _eventSink.add( 47 | SseMessage(id: _id, event: _event, data: _data, retry: _retry), 48 | ); 49 | _id = null; 50 | _event = "message"; 51 | _data = ""; 52 | _retry = null; 53 | } 54 | 55 | // 自己加的,请求报错时不是一个正常的流的结构,是个json,直接添加即可 56 | if (isJsonString(event)) { 57 | _eventSink.add( 58 | SseMessage(id: _id, event: _event, data: event, retry: _retry), 59 | ); 60 | 61 | _id = null; 62 | _event = "message"; 63 | _data = ""; 64 | _retry = null; 65 | } 66 | } 67 | 68 | @override 69 | void addError(Object error, [StackTrace? stackTrace]) { 70 | _eventSink.addError(error, stackTrace); 71 | } 72 | 73 | @override 74 | void close() { 75 | _eventSink.close(); 76 | } 77 | } 78 | 79 | class SseMessage { 80 | final String? id; 81 | final String event; 82 | final String data; 83 | final int? retry; 84 | 85 | const SseMessage({ 86 | this.id, 87 | required this.event, 88 | required this.data, 89 | this.retry, 90 | }); 91 | } 92 | -------------------------------------------------------------------------------- /lib/apis/gen_access_token/xfyun_signature.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | import 'package:crypto/crypto.dart'; 4 | 5 | /// 讯飞平台的 WebAPI 通用鉴权 6 | //@hostUrl : 比如语言识别的 wss://iat-api.xfyun.cn/v2/iat 7 | //@apiKey : apiKey 8 | //@apiSecret : apiSecret 9 | //@method :get 或者post请求 10 | String genXfyunAssembleAuthUrl( 11 | String hosturl, 12 | String apiKey, 13 | String apiSecret, 14 | String method, 15 | ) { 16 | var ul = Uri.parse(hosturl); 17 | 18 | // 签名时间 19 | String date = HttpDate.format(DateTime.now().toUtc()); 20 | 21 | // 参与签名的字段 host, date, request-line 22 | List signString = [ 23 | "host: ${ul.host}", 24 | "date: $date", 25 | // "GET ${ul.path} HTTP/1.1", 26 | "$method ${ul.path} HTTP/1.1", 27 | ]; 28 | 29 | // 拼接签名字符串 30 | String sgin = signString.join("\n"); 31 | 32 | // 签名结果 33 | String sha = hmacSha256ToBase64(sgin, apiSecret); 34 | 35 | // 构建请求参数 此时不需要urlencoding 36 | String authUrl = 37 | 'api_key="$apiKey", algorithm="hmac-sha256", headers="host date request-line", signature="$sha"'; 38 | 39 | // 将请求参数使用base64编码 40 | String authorization = base64Encode(utf8.encode(authUrl)); 41 | 42 | // 将编码后的字符串url encode后添加到url后面 43 | String callurl = 44 | '$hosturl?host=${Uri.encodeComponent(ul.host)}&date=${Uri.encodeComponent(date)}&authorization=${Uri.encodeComponent(authorization)}'; 45 | 46 | return callurl; 47 | } 48 | 49 | String hmacSha256ToBase64(String data, String key) { 50 | var keyBytes = utf8.encode(key); 51 | var dataBytes = utf8.encode(data); 52 | var hmacSha256 = Hmac(sha256, keyBytes); 53 | var digest = hmacSha256.convert(dataBytes); 54 | return base64Encode(digest.bytes); 55 | } 56 | -------------------------------------------------------------------------------- /lib/apis/get_app_key_helper.dart: -------------------------------------------------------------------------------- 1 | // 用户自定义输入的平台的密钥的相关key枚举, 2 | // 在表单验证、保存到缓存、读取时都使用的关键字,避免过多魔法值出错 3 | // SelfKeyName 4 | import '../services/cus_get_storage.dart'; 5 | 6 | // 从缓存中获取用户自定义的密钥,没取到就用预设的 7 | String getStoredUserKey(String key, String defaultValue) { 8 | return MyGetStorage().getUserAKMap()[key] != null && 9 | MyGetStorage().getUserAKMap()[key]!.isNotEmpty 10 | ? MyGetStorage().getUserAKMap()[key]! 11 | : defaultValue; 12 | } 13 | -------------------------------------------------------------------------------- /lib/apis/life_tools/animal_lover/random_fact_apis.dart: -------------------------------------------------------------------------------- 1 | // 收集 2 | 3 | import 'dart:convert'; 4 | import 'dart:math'; 5 | 6 | import '../../../common/utils/dio_client/cus_http_client.dart'; 7 | 8 | /// 获取犬科事实(dog facts)的地址:https://dogapi.dog/api/v2/facts 9 | /// 随机cat facts :https://meowfacts.herokuapp.com/?lang=zho 10 | /// 随机1个事实(总共332个事实):https://catfact.ninja/fact 11 | /// 12 | /// 随机一张狗图:https://random.dog/woof.json 13 | /// 随机一张猫图:https://cataas.com/cat 14 | 15 | enum FactSource { 16 | dogapi, 17 | // meowfacts, 18 | catfact, 19 | } 20 | 21 | const Map apiUrls = { 22 | FactSource.dogapi: "https://dogapi.dog/api/v2/facts", 23 | // 2024-10-07 国内不能访问 24 | // FactSource.meowfacts: "https://meowfacts.herokuapp.com/?lang=zho", 25 | FactSource.catfact: "https://catfact.ninja/fact", 26 | }; 27 | 28 | Future getAnimalFact() async { 29 | var apikey = FactSource.values[Random().nextInt(FactSource.values.length)]; 30 | 31 | try { 32 | var url = apiUrls[apikey]!; 33 | 34 | var respData = await HttpUtils.get( 35 | path: url, 36 | headers: {"Content-Type": "application/json"}, 37 | ); 38 | 39 | /// 2024-06-06 注意,这里报错的时候,响应的是String,而正常获取回复响应是_Map 40 | if (respData.runtimeType == String) { 41 | respData = json.decode(respData); 42 | } 43 | 44 | // 不同的API响应的结果不一样,但最终都是返回一个字符串 45 | if (apikey == FactSource.dogapi) { 46 | // 结构类似 {"data":[{"id":"a264e88f-ef76-48b5-ab35-9a1c2e71cab9","type":"fact", 47 | // "attributes":{"body":"Fifty-eight percent of people put pets in family and holiday portraits."}}]} 48 | return ((respData["data"] as List).first["attributes"] 49 | as Map)["body"]; 50 | } 51 | // else if (apikey == FactSource.meowfacts) { 52 | // // 结构类似 {"data":["貓咪極速奔跑可達時速 50 公里。"]} 53 | // return (respData["data"] as List).first; 54 | // } 55 | else if (apikey == FactSource.catfact) { 56 | // 结构类似 {"fact":"Cats take between 20-40 breaths per minute.","length":43} 57 | return respData["fact"]; 58 | } 59 | 60 | return "<暂未获取数据>"; 61 | } catch (e) { 62 | // API请求报错,显示报错信息 63 | rethrow; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/apis/life_tools/animal_lover/thatapi_apis.dart: -------------------------------------------------------------------------------- 1 | import '../../../common/utils/dio_client/cus_http_client.dart'; 2 | import '../../../models/life_tools/animal_lover/the_dog_cat_api_breed.dart'; 3 | import '../../../models/life_tools/animal_lover/the_dog_cat_api_image.dart'; 4 | 5 | /// 猫狗都有,需要ak,可以用来获取品种和图片(说是有60k+ Images. Breeds. Facts.),其他使用大模型来获取 6 | /// https://portal.thatapicompany.com/ 7 | /// 8 | /// 随机图片不需要AK,但指定品种的话,需要AK(如果是狗的话,把thecatapi改为thedogapi即可) 9 | /// 随机1张图片:https://api.thecatapi.com/v1/images/search 10 | /// 随机10张图片:https://api.thecatapi.com/v1/images/search?limit=10 11 | /// 指定品种:https://api.thecatapi.com/v1/images/search?limit=10&breed_ids=beng&api_key=REPLACE_ME 12 | /// 随机指定品种的一张图(没有ak只有1张):https://api.thecatapi.com/v1/images/search?breed_ids=beng 13 | /// 获取所有品种:https://api.thecatapi.com/v1/breeds 14 | /// 15 | /// 获取指定图片:https://api.thecatapi.com/v1/images/{reference_image_id} 16 | /// 在上面获取所有品种时,每个品种都有一个reference_image_id,替换上面编号可以得到指定图片的详情 17 | 18 | /// 19 | /// 获取品种信息 20 | /// 21 | Future> getThatApiBreeds({ 22 | String type = 'cat', // 查询猫或者狗 23 | }) async { 24 | try { 25 | var url = "https://api.the${type}api.com/v1/breeds"; 26 | 27 | List respData = await HttpUtils.get(path: url); 28 | 29 | // 响应是json格式的列表 List 30 | return respData.map((e) => Breed.fromJson(e)).toList(); 31 | } catch (e) { 32 | // API请求报错,显示报错信息 33 | rethrow; 34 | } 35 | } 36 | 37 | /// 38 | /// 获取图片信息 39 | /// 40 | Future> getThatApiImages({ 41 | String? type = 'dog', // 猫还是狗 cat dog 42 | bool isRandom = false, // 是否随机 43 | int? number, // 随机数量 44 | // 品种,多个用逗号连接(thecarapi栏位是String,thedogapi栏位是int) 45 | dynamic breedIds, 46 | }) async { 47 | // 如果传了number,则是随机某几个 48 | // 如果只传breed,则是查询单个主品种信息 49 | // 如果传了breed和subBreed,则是查询单个子品种信息 50 | 51 | try { 52 | var url = "https://api.the${type}api.com/v1/images/search"; 53 | 54 | if (isRandom) { 55 | if (number != null) { 56 | url += "?limit=${number <= 10 ? 1 : 10}"; 57 | } else { 58 | url = url; 59 | } 60 | } else if (breedIds != null) { 61 | url += "?breed_ids=$breedIds"; 62 | } 63 | 64 | List respData = await HttpUtils.get(path: url); 65 | 66 | // 响应是json格式的列表 List 67 | return respData.map((e) => TheDogCatApiImage.fromJson(e)).toList(); 68 | } catch (e) { 69 | // API请求报错,显示报错信息 70 | rethrow; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/apis/life_tools/food/usda_food_data_central/usda_food_data_apis.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import '../../../../common/utils/dio_client/cus_http_client.dart'; 4 | import '../../../../models/life_tools/food/usda_food_data/usda_food_item.dart'; 5 | import '../../../../models/life_tools/food/usda_food_data/usda_food_search_resp.dart'; 6 | 7 | /// 来源:https://fdc.nal.usda.gov/api-guide.html 8 | /// 关于数据类型的说明:https://fdc.nal.usda.gov/data-documentation.html 9 | /// 2024-10-17 目前API是测试用的demo,后续可以去网站注册,可以提高到API调用每日上限到1000次。 10 | 11 | var baseUsda = "https://api.nal.usda.gov/fdc/v1"; 12 | 13 | /// 条件查询食品信息 14 | Future searchUSDAFoods( 15 | // 关键字,不可为null,但可以为空字串 16 | String query, { 17 | List? dataType, // 数据类型 [Branded,Foundation,Survey (FNDDS),SR Legacy] 18 | int? pageSize = 10, 19 | int? pageNumber = 1, 20 | // 排序的字段:dataType.keyword, lowercaseDescription.keyword, fdcId, publishedDate 21 | String? sortBy = "dataType.keyword", 22 | // 排序的方式 23 | String? sortOrder = "asc", // asc desc 24 | // 品牌,只适用在品牌分类的食品中 25 | String? brandOwner, 26 | }) async { 27 | try { 28 | var respData = await HttpUtils.get( 29 | // 这里不传就随机一个类型 30 | path: "$baseUsda/foods/search", 31 | // 因为上拉下拉有加载圈,就不显示请求的加载了 32 | showLoading: false, 33 | // 因为存在404找不到单词也保存,但单独处理了,就不在http拦截器中报错了 34 | showErrorMessage: false, 35 | queryParameters: { 36 | "api_key": "DEMO_KEY", 37 | "query": query, 38 | // 2024-10-14 根据定义,暂时固定为查询基础数据 39 | // "dataType": ["Foundation"], 40 | "dataType": dataType, 41 | "pageSize": pageSize, 42 | "pageNumber": pageNumber, 43 | }, 44 | ); 45 | 46 | if (respData.runtimeType == String) { 47 | respData = json.decode(respData); 48 | } 49 | 50 | return USDASearchResultResp.fromJson(respData); 51 | } catch (e) { 52 | rethrow; 53 | } 54 | } 55 | 56 | // 查询指定编号的食品详情 57 | Future getUSDAFoodById( 58 | // 食品编号 59 | int fdcId, { 60 | // 返回的栏位 61 | String? format = "full", // abridged, full 62 | }) async { 63 | try { 64 | var respData = await HttpUtils.get( 65 | // 这里不传就随机一个类型 66 | path: "$baseUsda/food/$fdcId", 67 | // 因为上拉下拉有加载圈,就不显示请求的加载了 68 | showLoading: false, 69 | // 因为存在404找不到单词也保存,但单独处理了,就不在http拦截器中报错了 70 | showErrorMessage: false, 71 | queryParameters: { 72 | "api_key": "DEMO_KEY", 73 | "format": format, 74 | }, 75 | ); 76 | 77 | if (respData.runtimeType == String) { 78 | respData = json.decode(respData); 79 | } 80 | 81 | return USDAFoodItem.fromJson(respData); 82 | } catch (e) { 83 | rethrow; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lib/apis/life_tools/free_dictionary/free_dictionary_apis.dart: -------------------------------------------------------------------------------- 1 | // 知道分类编号查询 2 | import 'dart:convert'; 3 | 4 | import '../../../common/utils/dio_client/cus_http_client.dart'; 5 | import '../../../common/utils/dio_client/interceptor_error.dart'; 6 | import '../../../models/life_tools/free_dictionary/free_dictionary_resp.dart'; 7 | 8 | /// API来源和说明:https://github.com/meetDeveloper/freeDictionaryAPI 9 | 10 | var fdBase = "https://api.dictionaryapi.dev/api/v2/entries/en"; 11 | 12 | Future getFreeDictionaryItem(String word) async { 13 | try { 14 | var respData = await HttpUtils.get( 15 | path: "$fdBase/$word", 16 | // 因为上拉下拉有加载圈,就不显示请求的加载了 17 | showLoading: false, 18 | // 因为存在404找不到单词也保存,但单独处理了,就不在http拦截器中报错了 19 | showErrorMessage: false, 20 | ); 21 | 22 | if (respData.runtimeType == String) { 23 | respData = json.decode(respData); 24 | } 25 | 26 | if (respData is List) { 27 | return FreeDictionaryItem.fromJson( 28 | respData.first as Map, 29 | ); 30 | } else { 31 | return FreeDictionaryItem.fromJson(respData); 32 | } 33 | } on CusHttpException catch (e) { 34 | // API请求报错,显示报错信息 35 | // 如果找不到输入的单词,是响应404错误 36 | // 可以构建一下,方便展示 37 | return FreeDictionaryItem.fromJson(json.decode(e.errRespString)); 38 | } catch (e) { 39 | rethrow; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/apis/life_tools/hitokoto/hitokoto_apis.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import '../../../common/utils/dio_client/cus_http_client.dart'; 4 | import '../../../models/life_tools/hitokoto/hitokoto.dart'; 5 | 6 | var hitoBase = "https://v1.hitokoto.cn"; 7 | 8 | Future getHitokoto({ 9 | // 类型:a动画;b漫画;c游戏;d文学;e原创;f来自网络;g其他;h影视;i诗词;j网易云;k哲学;l抖机灵;其他作为 动画 类型处理 10 | String? cate, 11 | }) async { 12 | try { 13 | var respData = await HttpUtils.get( 14 | // 这里不传就随机一个类型 15 | path: "$hitoBase?c=${cate ?? ''}", 16 | // 因为上拉下拉有加载圈,就不显示请求的加载了 17 | showLoading: false, 18 | // 因为存在404找不到单词也保存,但单独处理了,就不在http拦截器中报错了 19 | showErrorMessage: false, 20 | ); 21 | 22 | if (respData.runtimeType == String) { 23 | respData = json.decode(respData); 24 | } 25 | 26 | return Hitokoto.fromJson(respData); 27 | } catch (e) { 28 | rethrow; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/apis/life_tools/news/newsapi_apis.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import '../../../common/constants/default_models.dart'; 4 | import '../../../common/utils/dio_client/cus_http_client.dart'; 5 | import '../../../models/life_tools/news/news_api_resp.dart'; 6 | import '../../get_app_key_helper.dart'; 7 | 8 | var newsapiBase = "https://newsapi.org/v2"; 9 | 10 | // 查询热门话题 11 | Future getNewsapiList({ 12 | int page = 1, 13 | int pageSize = 100, 14 | // 热点 top-headlines | 所有 everything 15 | String type = "top-headlines", 16 | String? query, 17 | String? category, 18 | }) async { 19 | var params = { 20 | "apiKey": getStoredUserKey("USER_NEWS_API_KEY", DefaultApiKeys.newsApiKey), 21 | 22 | // 新闻来源,可以在 https://newsapi.org/v2/top-headlines/sources 查到相关信息(结构体的id栏位) 23 | 24 | /// 不能和 country 或 category 一起用 25 | // "sources": '', 26 | 27 | "page": page, 28 | // 默认100。为了减少请求次数,可以保留个大数字 29 | "pageSize": pageSize, 30 | }; 31 | 32 | // 2024-11-06 就只给两个选项,热榜和所有的查询 33 | if (type == "top-headlines") { 34 | /// 热榜时,这几个栏位不可都为空: sources, q, country, category 35 | params.addAll({ 36 | // 查询热榜暂时不用关键字查询,默认分类显示所有 37 | // "q": query ?? "", 38 | 39 | // ISO 3166-1编码的两个字母的国家编号【目前免费的看起来只能用 us 才有值】 40 | // 不能和 sources 参数一起用 41 | // https://www.iso.org/obp/ui/#search 42 | // "country": 'us', 43 | 44 | // 新闻的分类: business entertainment general health science sports technology 45 | // 不能和 sources 参数一起用 46 | "category": category ?? 'general', 47 | }); 48 | } else { 49 | // 所有搜索时,后面栏位不可全为空: q, qInTitle, sources, domains. 50 | params.addAll({ 51 | // 搜索的关键字(带双引号可以强制匹配) 52 | // 2024-11-07 查询热榜时只有分类就不带上查询了,查询就从所有新闻来 53 | "q": "$query", 54 | 55 | // 搜索限制到的字段,可以用逗号添加多个 56 | // title | description | content 57 | "searchIn": "title,description", 58 | 59 | // 新闻的网域,多个用逗号连接,例如 bbc.co.uk,techcrunch.com,engadget.com 60 | // "domains": "", 61 | 62 | // 排除的域,多个用逗号连接,例如 bbc.co.uk,techcrunch.com,engadget.com 63 | // "excludeDomains": "", 64 | 65 | // 搜索的时间范围,ISO 8601 格式字符串 66 | // "from": '', 67 | // "to": '', 68 | 69 | // 标题的语言,可选性 70 | // ar de en es fr he it nl no pt ru sv ud zh 71 | // "language": "zh", 72 | 73 | // 排序方式 74 | // relevancy: 与q关系更密切的文章排在前面 75 | // popularity: 来自流行来源和出版商的文章优先 76 | // publishedAt(默认): 最新文章排在第一位 77 | "sortBy": "publishedAt", 78 | }); 79 | } 80 | 81 | try { 82 | var respData = await HttpUtils.get( 83 | path: "$newsapiBase/$type", 84 | // 因为上拉下拉有加载圈,就不显示请求的加载了 85 | showLoading: false, 86 | queryParameters: params, 87 | ); 88 | 89 | if (respData.runtimeType == String) { 90 | respData = json.decode(respData); 91 | } 92 | 93 | return NewsApiResp.fromJson(respData); 94 | } catch (e) { 95 | // API请求报错,显示报错信息 96 | rethrow; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /lib/apis/life_tools/news/readhub_apis.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import '../../../common/utils/dio_client/cus_http_client.dart'; 4 | import '../../../models/life_tools/news/readhub_hot_topic_resp.dart'; 5 | 6 | var readhubBase = "https://api.readhub.cn"; 7 | 8 | // 查询热门话题 9 | Future getReadhubHotTopicList({ 10 | int page = 1, 11 | int size = 10, 12 | }) async { 13 | try { 14 | var respData = await HttpUtils.get( 15 | path: "$readhubBase/topic/list", 16 | // 因为上拉下拉有加载圈,就不显示请求的加载了 17 | showLoading: false, 18 | queryParameters: {"page": page, "size": size}, 19 | ); 20 | 21 | if (respData.runtimeType == String) { 22 | respData = json.decode(respData); 23 | } 24 | 25 | if (respData["data"] == null) { 26 | throw Exception("返回结果不正确: $respData"); 27 | } 28 | 29 | return ReadhubHotTopicResp.fromJson(respData["data"]); 30 | } catch (e) { 31 | // API请求报错,显示报错信息 32 | rethrow; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/apis/life_tools/news/sina_roll_news_apis.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import '../../../common/utils/dio_client/cus_http_client.dart'; 4 | import '../../../models/life_tools/news/sina_roll_news_resp.dart'; 5 | 6 | var sinaRollNewsBase = "https://feed.mix.sina.com.cn/api/roll/get"; 7 | 8 | // 查询热门话题 9 | Future getSinaRollNewsList({ 10 | int page = 1, 11 | int size = 10, 12 | int lid = 2509, 13 | }) async { 14 | try { 15 | var respData = await HttpUtils.get( 16 | path: sinaRollNewsBase, 17 | // 因为上拉下拉有加载圈,就不显示请求的加载了 18 | showLoading: false, 19 | queryParameters: { 20 | "pageid": 153, 21 | "lid": lid, 22 | "page": page, 23 | "num": size, 24 | }, 25 | ); 26 | 27 | if (respData.runtimeType == String) { 28 | respData = json.decode(respData); 29 | } 30 | 31 | if (respData["result"] == null) { 32 | throw Exception("返回结果不正确: $respData"); 33 | } 34 | 35 | return SinaRollNewsResp.fromJson(respData["result"]); 36 | } catch (e) { 37 | // API请求报错,显示报错信息 38 | rethrow; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/apis/life_tools/waifu_pic/index.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import '../../../common/utils/dio_client/cus_http_client.dart'; 4 | import '../../../common/utils/dio_client/cus_http_request.dart'; 5 | 6 | /// 7 | /// 获取 waifu.pics 图片 8 | /// 9 | /// 单个图片: GET https://api.waifu.pics/{type}/{category} 10 | /// 随机30张图片: POST https://api.waifu.pics/many/{type}/{category} 11 | /// 12 | /// 获取waifu.im 的图片 https://docs.waifu.im/reference/api-reference/search 13 | /// 参数更多,但不知道是不是同一个源 14 | /// https://api.waifu.im/search?included_tags={array[string]}&is_nsfw={string} 15 | /// 16 | /* 17 | curl --request POST \ 18 | --url https://api.waifu.pics/many/sfw/pat \ 19 | --header 'accept: application/json' \ 20 | --header 'content-type: application/json' \ 21 | --data ' 22 | { 23 | "type": "sfw", 24 | "category":"pat" 25 | } 26 | ' 27 | */ 28 | Future> getWaifuPicImages({ 29 | String? type = "sfw", // 只有2个 sfw | nsfw 30 | String? category, // 分类 31 | bool isMany = false, // 是否多个,是则为POST 否则为GET 32 | // 现在两个源 pics | im 33 | // 上面3个参数是默认 pics 的,im 的带上前缀(虽然原本支持参数很多,但这里只提供少数) 34 | String source = "pics", 35 | String imIncludedTags = "waifu", // 虽说是支持多个,但添加多个tag时报错 36 | // 如果tag本身就是nsfw的,那这个bool没效果,都是工作场合不安全; 37 | // 但tag不是nsfw,不加上这个false,则可能出现工作场合不安全 38 | bool imIsNsfw = false, 39 | int imLimit = 1, 40 | }) async { 41 | try { 42 | if (source == "im") { 43 | var respData = await HttpUtils.get( 44 | path: 45 | "https://api.waifu.im/search?included_tags=$imIncludedTags&is_nsfw=$imIsNsfw${imLimit > 1 ? "&limit=$imLimit" : ""}", 46 | ); 47 | 48 | if (respData.runtimeType == String) { 49 | respData = json.decode(respData); 50 | } 51 | 52 | // 这个响应体内容很多,暂时只取得地址即可 53 | return (respData["images"] as List) 54 | .map((e) => (e["url"] as String)) 55 | .toList(); 56 | } else { 57 | var respData = (isMany) 58 | ? await HttpUtils.post( 59 | path: "https://api.waifu.pics/many/$type/$category", 60 | method: CusHttpMethod.post, 61 | headers: {"Content-Type": "application/json"}, 62 | data: {"type": type, "category": category}) 63 | : await HttpUtils.get( 64 | path: "https://api.waifu.pics/$type/$category", 65 | ); 66 | 67 | if (respData.runtimeType == String) { 68 | respData = json.decode(respData); 69 | } 70 | 71 | if (isMany) { 72 | return (respData["files"] as List).cast(); 73 | } else { 74 | return [respData["url"]]; 75 | } 76 | } 77 | } catch (e) { 78 | // API请求报错,显示报错信息 79 | rethrow; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/apis/voice_recognition/xunfei_apis.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:convert'; 3 | import 'dart:async'; 4 | import 'package:flutter_easyloading/flutter_easyloading.dart'; 5 | import 'package:web_socket_channel/web_socket_channel.dart'; 6 | 7 | import '../../common/constants/default_models.dart'; 8 | import '../../models/brief_ai_tools/voice_recognition/xunfei_voice_dictation.dart'; 9 | import '../gen_access_token/xfyun_signature.dart'; 10 | import '../get_app_key_helper.dart'; 11 | 12 | const voiceRegUrl = "wss://iat-api.xfyun.cn/v2/iat"; 13 | 14 | Future getTextFromAudioFromXFYun(String audioPath) async { 15 | // 生成鉴权url 16 | var authUrl = genXfyunAssembleAuthUrl( 17 | voiceRegUrl, 18 | getStoredUserKey("USER_XFYUN_API_KEY", DefaultApiKeys.xfyunApiKey), 19 | getStoredUserKey("USER_XFYUN_API_SECRET", DefaultApiKeys.xfyunApiSecret), 20 | "GET", 21 | ); 22 | 23 | // 把音频文件转为base64 24 | final file = File(audioPath); 25 | final audioBytes = await file.readAsBytes(); 26 | final audioBase64 = base64Encode(audioBytes); 27 | 28 | // 建立WebSocket连接 29 | final channel = WebSocketChannel.connect(Uri.parse(authUrl)); 30 | 31 | String transcription = ''; 32 | final completer = Completer(); 33 | 34 | try { 35 | EasyLoading.show(status: '【语音转文字中...】'); 36 | 37 | await channel.ready; 38 | } catch (e) { 39 | // print("channel.ready error: $e"); 40 | rethrow; 41 | } 42 | 43 | channel.stream.listen( 44 | (message) { 45 | ///??? 2024-08-03 保存的时候类型就是 String而不是Map 46 | if (message.runtimeType == String) { 47 | message = json.decode(message); 48 | } 49 | 50 | var data = XunfeiVoiceDictation.fromJson(message); 51 | 52 | transcription = data.data?.result?.ws 53 | ?.map((e) => e.cw?.map((e) => e.w).join()) 54 | .join() ?? 55 | ""; 56 | 57 | // 2024-08-17??具体哪里释放还拿不准 58 | EasyLoading.dismiss(); 59 | 60 | // 2024-09-03 这里语言转文字时可能有报错 Bad state: Future already completed 61 | if (!completer.isCompleted) { 62 | completer.complete(transcription); 63 | } 64 | }, 65 | onDone: () { 66 | if (!completer.isCompleted) { 67 | completer.complete(transcription); 68 | } 69 | }, 70 | onError: (error) { 71 | // print('WebSocket error: ${error.toString()}'); 72 | completer.completeError(error); 73 | 74 | EasyLoading.dismiss(); 75 | }, 76 | ); 77 | 78 | // 发送参数 79 | var params = { 80 | 'common': { 81 | 'app_id': getStoredUserKey( 82 | "USER_XFYUN_APP_ID", 83 | DefaultApiKeys.xfyunAppId, 84 | ), 85 | }, 86 | 'business': { 87 | // 语种。zh_cn:中文(支持简单的英文识别) 88 | 'language': 'zh_cn', 89 | // 应用领域。iat:日常用语 90 | 'domain': 'iat', 91 | // 方言,当前仅在language为中文时,支持方言选择。 92 | 'accent': 'mandarin', // 默认中文普通话 93 | // 设置端点检测的静默时间,单位是毫秒。 94 | // 'vad_eos': 5000, 95 | // 动态修正返回参数 96 | // 'dwa': 'wpgs', 97 | }, 98 | 'data': { 99 | // 音频的状态。0 :第一帧音频; 1 :中间的音频;2 :最后一帧音频,最后一帧必须要发送 100 | 'status': 0, 101 | // 音频的采样率支持16k和8k 102 | 'format': 'audio/L16;rate=16000', 103 | // 音频数据格式:raw、speex、speex-wb、lame 104 | 'encoding': 'raw', 105 | // 音频内容,采用base64编码 106 | 'audio': audioBase64, 107 | }, 108 | }; 109 | channel.sink.add(jsonEncode(params)); 110 | 111 | // 发送结束信号 112 | var endParams = { 113 | 'data': {'status': 2}, 114 | }; 115 | channel.sink.add(jsonEncode(endParams)); 116 | 117 | return completer.future; 118 | } 119 | -------------------------------------------------------------------------------- /lib/common/components/cus_toggle_button_selector.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 3 | 4 | class CusToggleButtonSelector extends StatefulWidget { 5 | // 用于选择的列表 6 | final List items; 7 | // 被选中后的回调 8 | final Function(T) onItemSelected; 9 | // 用于从 T 类型的 item 中提取 label 字段 10 | final String Function(T) labelBuilder; 11 | 12 | const CusToggleButtonSelector({ 13 | super.key, 14 | required this.items, 15 | required this.onItemSelected, 16 | required this.labelBuilder, 17 | }); 18 | 19 | @override 20 | State createState() => _CusToggleButtonSelectorState(); 21 | } 22 | 23 | class _CusToggleButtonSelectorState 24 | extends State> { 25 | // 选中状态列表 26 | List _selections = []; 27 | 28 | @override 29 | void initState() { 30 | super.initState(); 31 | 32 | // 传入的列表默认选中第一个 33 | _selections = List.generate(widget.items.length, (index) { 34 | if (index == 0) { 35 | return true; 36 | } 37 | return false; 38 | }); 39 | } 40 | 41 | @override 42 | Widget build(BuildContext context) { 43 | return SingleChildScrollView( 44 | scrollDirection: Axis.horizontal, 45 | child: Row( 46 | children: [ 47 | ToggleButtons( 48 | isSelected: _selections, 49 | onPressed: (int index) { 50 | setState(() { 51 | for (int i = 0; i < _selections.length; i++) { 52 | _selections[i] = i == index; 53 | } 54 | 55 | widget.onItemSelected(widget.items[index]); 56 | }); 57 | }, 58 | // 这个是预设只有3个的情况,宽度各3分之一 59 | // constraints: BoxConstraints(minHeight: 36.sp, minWidth: 0.32.sw), 60 | // 一般长度不定,还是这个 61 | // 设置按钮的最小高度和最小宽度(得根据传入的label来判断) 62 | constraints: BoxConstraints(minHeight: 36.sp, minWidth: 80.sp), 63 | // 设置选中按钮的文本颜色 64 | selectedColor: Colors.white, 65 | // 设置选中按钮的边框颜色 66 | selectedBorderColor: Colors.blue, 67 | // 设置选中按钮的填充颜色 68 | fillColor: Colors.blue, 69 | // 设置按钮的圆角半径 70 | borderRadius: BorderRadius.circular(5.sp), 71 | // 设置按钮的边框宽度 72 | borderWidth: 1.sp, 73 | // 设置按钮的边框颜色 74 | borderColor: Colors.grey, 75 | children: List.generate( 76 | widget.items.length, 77 | (index) => Text(widget.labelBuilder(widget.items[index])), 78 | ), 79 | ), 80 | ], 81 | ), 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/common/components/feature_grid_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 3 | 4 | class FeatureGridCard extends StatelessWidget { 5 | final Widget targetPage; 6 | final String title; 7 | final IconData icon; 8 | final Color? accentColor; 9 | final bool isNew; 10 | 11 | const FeatureGridCard({ 12 | super.key, 13 | required this.targetPage, 14 | required this.title, 15 | required this.icon, 16 | this.accentColor, 17 | this.isNew = false, 18 | }); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | final theme = Theme.of(context); 23 | final color = accentColor ?? theme.primaryColor; 24 | 25 | return Card( 26 | elevation: 3, 27 | shadowColor: color.withValues(alpha: 0.3), 28 | shape: RoundedRectangleBorder( 29 | borderRadius: BorderRadius.circular(20.sp), 30 | ), 31 | child: InkWell( 32 | borderRadius: BorderRadius.circular(20.sp), 33 | onTap: () { 34 | Navigator.of(context).push( 35 | MaterialPageRoute(builder: (context) => targetPage), 36 | ); 37 | }, 38 | child: Stack( 39 | children: [ 40 | Padding( 41 | padding: EdgeInsets.all(16.sp), 42 | child: Column( 43 | mainAxisAlignment: MainAxisAlignment.center, 44 | crossAxisAlignment: CrossAxisAlignment.center, 45 | children: [ 46 | Container( 47 | width: 64.sp, 48 | height: 64.sp, 49 | decoration: BoxDecoration( 50 | color: color.withValues(alpha: 0.1), 51 | borderRadius: BorderRadius.circular(16.sp), 52 | ), 53 | child: Icon( 54 | icon, 55 | color: color, 56 | size: 32.sp, 57 | ), 58 | ), 59 | SizedBox(height: 16.sp), 60 | Text( 61 | title, 62 | style: TextStyle( 63 | fontSize: 16.sp, 64 | fontWeight: FontWeight.w600, 65 | ), 66 | textAlign: TextAlign.center, 67 | ), 68 | ], 69 | ), 70 | ), 71 | if (isNew) 72 | Positioned( 73 | top: 12.sp, 74 | right: 12.sp, 75 | child: Container( 76 | padding: EdgeInsets.symmetric( 77 | horizontal: 8.sp, 78 | vertical: 4.sp, 79 | ), 80 | decoration: BoxDecoration( 81 | color: Colors.red, 82 | borderRadius: BorderRadius.circular(12.sp), 83 | boxShadow: [ 84 | BoxShadow( 85 | color: Colors.red.withValues(alpha: 0.3), 86 | blurRadius: 4, 87 | offset: const Offset(0, 2), 88 | ), 89 | ], 90 | ), 91 | child: Text( 92 | '新', 93 | style: TextStyle( 94 | color: Colors.white, 95 | fontSize: 12.sp, 96 | fontWeight: FontWeight.bold, 97 | ), 98 | ), 99 | ), 100 | ), 101 | ], 102 | ), 103 | ), 104 | ); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /lib/common/components/image_pick_and_preview_area.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 5 | import 'package:image_picker/image_picker.dart'; 6 | 7 | import 'tool_widget.dart'; 8 | 9 | // 新版本可以用在图片选择和预览的地方 10 | class ImagePickAndPreviewArea extends StatelessWidget { 11 | final Function(ImageSource) imageSelectedHandle; 12 | final Function() imageClearHandle; 13 | final File? selectedImage; 14 | final String imagePlaceholder; 15 | 16 | const ImagePickAndPreviewArea({ 17 | super.key, 18 | required this.imageSelectedHandle, 19 | required this.imageClearHandle, 20 | required this.selectedImage, 21 | this.imagePlaceholder = "请选择参考图", 22 | }); 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return Container( 27 | height: 100.sp, 28 | margin: EdgeInsets.fromLTRB(5.sp, 5.sp, 5.sp, 0), 29 | decoration: BoxDecoration( 30 | border: Border.all(color: Colors.grey, width: 1.sp), 31 | borderRadius: BorderRadius.circular(5.sp), 32 | ), 33 | child: Row( 34 | mainAxisAlignment: MainAxisAlignment.center, 35 | children: [ 36 | IconButton( 37 | onPressed: () { 38 | showDialog( 39 | context: context, 40 | builder: (BuildContext context) { 41 | return AlertDialog( 42 | title: Text( 43 | "选择图片来源", 44 | style: TextStyle(fontSize: 18.sp), 45 | ), 46 | actions: [ 47 | TextButton( 48 | onPressed: () { 49 | Navigator.of(context).pop(); 50 | imageSelectedHandle(ImageSource.camera); 51 | }, 52 | child: Text( 53 | "拍照", 54 | style: TextStyle(fontSize: 16.sp), 55 | ), 56 | ), 57 | TextButton( 58 | onPressed: () { 59 | Navigator.of(context).pop(); 60 | imageSelectedHandle(ImageSource.gallery); 61 | }, 62 | child: Text( 63 | "相册", 64 | style: TextStyle(fontSize: 16.sp), 65 | ), 66 | ), 67 | ], 68 | ); 69 | }, 70 | ); 71 | }, 72 | icon: const Icon(Icons.file_upload), 73 | ), 74 | Expanded( 75 | flex: 3, 76 | child: buildImageView( 77 | selectedImage, 78 | context, 79 | imagePlaceholder: imagePlaceholder, 80 | ), 81 | ), 82 | if (selectedImage != null) 83 | IconButton( 84 | onPressed: imageClearHandle, 85 | icon: const Icon(Icons.clear), 86 | ), 87 | ], 88 | ), 89 | ); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /lib/common/components/loading_overlay.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 3 | 4 | class LoadingOverlay { 5 | static OverlayEntry? _overlayEntry; 6 | 7 | static void show(BuildContext context, {VoidCallback? onCancel}) { 8 | if (_overlayEntry != null) return; 9 | 10 | OverlayState overlayState = Overlay.of(context); 11 | _overlayEntry = OverlayEntry( 12 | builder: (context) { 13 | return Container( 14 | width: double.infinity, 15 | height: double.infinity, 16 | color: Colors.black.withValues(alpha: 0.8), 17 | child: Center( 18 | child: Column( 19 | crossAxisAlignment: CrossAxisAlignment.center, 20 | mainAxisAlignment: MainAxisAlignment.center, 21 | children: [ 22 | const CircularProgressIndicator(color: Colors.white), 23 | SizedBox(height: 10.sp), 24 | Text( 25 | "图片或视频生成中", 26 | style: TextStyle(fontSize: 16.sp, color: Colors.white), 27 | ), 28 | Text( 29 | "请耐心等待一会儿", 30 | style: TextStyle(fontSize: 16.sp, color: Colors.white), 31 | ), 32 | Text( 33 | "请勿退出当前页面", 34 | style: TextStyle(fontSize: 16.sp, color: Colors.white), 35 | ), 36 | SizedBox(height: 16.sp), 37 | ElevatedButton( 38 | onPressed: () { 39 | hide(); 40 | onCancel?.call(); 41 | }, 42 | child: const Text("取消"), 43 | ), 44 | ], 45 | ), 46 | ), 47 | ); 48 | }, 49 | ); 50 | overlayState.insert(_overlayEntry!); 51 | } 52 | 53 | static void hide() { 54 | _overlayEntry?.remove(); 55 | _overlayEntry = null; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/common/components/modern_feature_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 3 | 4 | class ModernFeatureCard extends StatelessWidget { 5 | final Widget targetPage; 6 | final String title; 7 | final String subtitle; 8 | final IconData icon; 9 | final Color? accentColor; 10 | final bool showArrow; 11 | 12 | const ModernFeatureCard({ 13 | super.key, 14 | required this.targetPage, 15 | required this.title, 16 | required this.subtitle, 17 | required this.icon, 18 | this.accentColor, 19 | this.showArrow = true, 20 | }); 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | final theme = Theme.of(context); 25 | final textColor = theme.textTheme.titleLarge?.color ?? Colors.black; 26 | final color = accentColor ?? theme.primaryColor; 27 | 28 | return Card( 29 | elevation: 0, 30 | shape: RoundedRectangleBorder( 31 | borderRadius: BorderRadius.circular(16.sp), 32 | side: BorderSide(color: Colors.grey.shade200, width: 1), 33 | ), 34 | child: InkWell( 35 | borderRadius: BorderRadius.circular(16.sp), 36 | onTap: () { 37 | Navigator.of(context).push( 38 | MaterialPageRoute(builder: (context) => targetPage), 39 | ); 40 | }, 41 | child: Padding( 42 | padding: EdgeInsets.all(20.sp), 43 | child: Row( 44 | children: [ 45 | // 左侧图标 46 | Container( 47 | width: 56.sp, 48 | height: 56.sp, 49 | decoration: BoxDecoration( 50 | color: color.withValues(alpha: 0.1), 51 | borderRadius: BorderRadius.circular(12.sp), 52 | ), 53 | child: Icon( 54 | icon, 55 | color: color, 56 | size: 28.sp, 57 | ), 58 | ), 59 | SizedBox(width: 16.sp), 60 | 61 | // 中间文本 62 | Expanded( 63 | child: Column( 64 | crossAxisAlignment: CrossAxisAlignment.start, 65 | children: [ 66 | Text( 67 | title, 68 | style: TextStyle( 69 | fontSize: 18.sp, 70 | fontWeight: FontWeight.bold, 71 | color: textColor, 72 | ), 73 | ), 74 | SizedBox(height: 4.sp), 75 | Text( 76 | subtitle, 77 | style: TextStyle( 78 | fontSize: 14.sp, 79 | color: Colors.grey.shade600, 80 | ), 81 | maxLines: 2, 82 | overflow: TextOverflow.ellipsis, 83 | ), 84 | ], 85 | ), 86 | ), 87 | 88 | // 右侧箭头 89 | if (showArrow) 90 | Icon( 91 | Icons.arrow_forward_ios, 92 | size: 16.sp, 93 | color: Colors.grey.shade400, 94 | ), 95 | ], 96 | ), 97 | ), 98 | ), 99 | ); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /lib/common/components/simple_marquee_or_text.dart: -------------------------------------------------------------------------------- 1 | // text文字超过一行则水平滚动(预设特性不可修改),不超过一行则单纯Text显示 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 4 | import 'package:marquee/marquee.dart'; 5 | 6 | class SimpleMarqueeOrText extends StatefulWidget { 7 | const SimpleMarqueeOrText({ 8 | super.key, 9 | required this.data, 10 | required this.style, 11 | this.velocity, 12 | this.showLines, 13 | this.height, 14 | this.width, 15 | this.textAlignment = Alignment.center, 16 | }); 17 | 18 | final String data; 19 | final TextStyle style; 20 | // 传入速度 21 | final double? velocity; 22 | // 传入显示的行数(大于此才滚动) 23 | final int? showLines; 24 | // 滚动条的高度 25 | final double? height; 26 | // 滚动条的宽度 27 | final double? width; 28 | // 文字的显示位置 29 | final AlignmentGeometry? textAlignment; 30 | 31 | @override 32 | State createState() => _SimpleMarqueeOrTextState(); 33 | } 34 | 35 | class _SimpleMarqueeOrTextState extends State { 36 | @override 37 | Widget build(BuildContext context) { 38 | return LayoutBuilder(builder: (context, constraints) { 39 | // 这里是获取文本的行数 40 | final span = TextSpan(text: widget.data, style: widget.style); 41 | final tp = TextPainter(text: span, textDirection: TextDirection.ltr); 42 | 43 | // tp.layout(maxWidth: constraints.maxWidth); 44 | // 注意,如果是builder的约束,默认为设备的总宽度(测试机为360.sp),不是全宽跑马灯的话宽度要自定,才能得到准确的行数 45 | // tp.layout(maxWidth: widget.width ?? 300.sp); // 和下面的sizedbox宽度一致 46 | 47 | // 看起来上面的不太准确,这个最小宽度可能是本widget的父widget的宽度。但是在播放详情页面标题和专辑信息时,传过来就是0.0。 48 | // 所以,要么在使用的时候指定父组件,要么就在这里设定一个最小值。 49 | // 综上优先级:手动传入宽度 > 大于50的父组件 > 预设的300 50 | var showWidth = widget.width ?? 51 | (constraints.minWidth > 50.sp ? constraints.minWidth : 300.sp); 52 | tp.layout(maxWidth: showWidth); 53 | final numLines = tp.computeLineMetrics().length; 54 | 55 | return SizedBox( 56 | height: widget.height ?? 30.sp, 57 | width: showWidth, 58 | child: numLines > (widget.showLines ?? 1) 59 | ? Marquee( 60 | text: "${widget.data} ", // 超过一行时滚动的字串加点空白以便识别文字起止 61 | style: widget.style, 62 | velocity: widget.velocity ?? 10.0, // 滚动速度 63 | ) 64 | : Align( 65 | alignment: widget.textAlignment ?? Alignment.center, 66 | child: Text(widget.data, style: widget.style), 67 | ), 68 | ); 69 | }); 70 | } 71 | } -------------------------------------------------------------------------------- /lib/common/components/sounds_message_button/button_widget/custom_canvas.dart: -------------------------------------------------------------------------------- 1 | part of 'sounds_message_button.dart'; 2 | 3 | /// 自定义画布 4 | /// 按住说话下那个小扇形 5 | class _RecordingPainter extends CustomPainter { 6 | final bool isFocus; 7 | _RecordingPainter(this.isFocus); 8 | 9 | @override 10 | void paint(Canvas canvas, Size size) async { 11 | final bgOvalRect = Rect.fromCenter( 12 | center: Offset(size.width / 2, size.height * 3 / 2), 13 | width: size.width * 1.8, 14 | height: size.height * 3, 15 | ); 16 | 17 | final paint = Paint() 18 | ..color = const Color(0xff393939) 19 | ..style = PaintingStyle.fill; 20 | Path path = Path()..addOval(bgOvalRect); 21 | 22 | if (isFocus) { 23 | paint.color = const Color(0xffb0b0b0); 24 | canvas.drawPath(path, paint); 25 | 26 | final scale = (size.height * 3 - 8.sp) / (size.height * 3); 27 | 28 | final bgShaderRect = Rect.fromCenter( 29 | center: bgOvalRect.center, 30 | width: bgOvalRect.width * scale, 31 | height: bgOvalRect.height * scale, 32 | ); 33 | canvas.drawPath( 34 | Path()..addOval(bgShaderRect), 35 | Paint() 36 | ..shader = ui.Gradient.linear( 37 | Offset(size.width / 2, size.height), 38 | Offset(size.width / 2, 0), 39 | [ 40 | const Color(0xffd5d5d5), 41 | const Color(0xff999999), 42 | ], 43 | ), 44 | ); 45 | } else { 46 | canvas.drawPath(path, paint); 47 | } 48 | } 49 | 50 | @override 51 | bool shouldRepaint(_RecordingPainter oldDelegate) => true; 52 | 53 | @override 54 | bool shouldRebuildSemantics(_RecordingPainter oldDelegate) => false; 55 | } 56 | 57 | /// 绘制气泡 58 | /// 录音振幅或者语言转文字那个像对话框的画布 59 | class _BubblePainter extends CustomPainter { 60 | final RecordingMaskOverlayData data; 61 | final SoundsMessageStatus status; 62 | final double paddingSide; 63 | _BubblePainter(this.data, this.status, this.paddingSide); 64 | 65 | @override 66 | void paint(Canvas canvas, Size size) { 67 | final paint = Paint() 68 | ..color = const Color(0xff95ec6a) 69 | ..style = PaintingStyle.fill; 70 | 71 | if (status == SoundsMessageStatus.canceling) { 72 | paint.color = const Color(0xfffa5251); 73 | } 74 | 75 | final rect = Offset.zero & size; 76 | final rrect = RRect.fromRectAndRadius(rect, const Radius.circular(12)); 77 | 78 | final path = Path(); 79 | 80 | // 三角形 81 | var dx = rect.center.dx; 82 | if (status == SoundsMessageStatus.textProcessing || 83 | status == SoundsMessageStatus.textProcessed) { 84 | dx = size.width + 24.sp - paddingSide - data.iconFocusSize / 2; 85 | } 86 | path.moveTo(dx - 7.sp, size.height); 87 | path.lineTo(dx, size.height + 6.sp); 88 | path.lineTo(dx + 7.sp, size.height); 89 | 90 | // 矩形 91 | path.addRRect(rrect); 92 | 93 | canvas.drawPath(path, paint); 94 | } 95 | 96 | @override 97 | bool shouldRepaint(_BubblePainter oldDelegate) => false; 98 | 99 | @override 100 | bool shouldRebuildSemantics(_BubblePainter oldDelegate) => false; 101 | } 102 | -------------------------------------------------------------------------------- /lib/common/components/sounds_message_button/utils/recording_mask_overlay_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// 当正在录制的时候,页面显示的 `OverlayEntry` 4 | class RecordingMaskOverlayData { 5 | /// 底部圆形的高度 6 | final double sendAreaHeight; 7 | 8 | /// 圆形图形大小 9 | final double iconSize; 10 | 11 | /// 圆形图形大小 - 响应 12 | final double iconFocusSize; 13 | 14 | /// 录音气泡大小 15 | // final EdgeInsets soundsMargin; 16 | 17 | /// 圆形图形颜色 18 | final Color iconColor; 19 | 20 | /// 圆形图形颜色 - 响应 21 | final Color iconFocusColor; 22 | 23 | /// 文字颜色 24 | final Color iconTxtColor; 25 | 26 | /// 文字颜色 - 响应 27 | final Color iconFocusTxtColor; 28 | 29 | /// 遮罩文字样式 30 | final TextStyle maskTxtStyle; 31 | 32 | const RecordingMaskOverlayData({ 33 | this.sendAreaHeight = 120, 34 | this.iconSize = 68, 35 | this.iconFocusSize = 80, 36 | this.iconColor = const Color(0xff393939), 37 | this.iconFocusColor = const Color(0xffffffff), 38 | this.iconTxtColor = const Color(0xff909090), 39 | this.iconFocusTxtColor = const Color(0xff000000), 40 | // this.soundsMargin = const EdgeInsets.symmetric(horizontal: 24), 41 | this.maskTxtStyle = const TextStyle( 42 | fontSize: 14, 43 | fontWeight: FontWeight.bold, 44 | color: Color(0xff909090), 45 | ), 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /lib/common/components/style_grid_selector.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 3 | 4 | /// 5 | /// 文生图样式选择grid 6 | /// 7 | class StyleGrid extends StatelessWidget { 8 | final List imageUrls; 9 | final List labels; 10 | final List subLabels; 11 | final int selectedIndex; 12 | final Function(int) onTap; 13 | final int crossAxisCount; 14 | 15 | const StyleGrid({ 16 | super.key, 17 | required this.imageUrls, 18 | required this.labels, 19 | required this.subLabels, 20 | required this.selectedIndex, 21 | required this.onTap, 22 | this.crossAxisCount = 4, 23 | }); 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return GridView.count( 28 | crossAxisCount: crossAxisCount, 29 | shrinkWrap: true, 30 | physics: const NeverScrollableScrollPhysics(), 31 | children: List.generate(imageUrls.length, (index) { 32 | return GridTile( 33 | child: GestureDetector( 34 | onTap: () => onTap(index), 35 | child: Container( 36 | decoration: BoxDecoration( 37 | border: Border.all( 38 | color: 39 | selectedIndex == index ? Colors.blue : Colors.transparent, 40 | width: 3.sp, 41 | ), 42 | borderRadius: BorderRadius.circular(5.0), 43 | ), 44 | child: ImageStack( 45 | url: imageUrls[index], 46 | label: labels[index], 47 | subLabel: subLabels[index], 48 | ), 49 | ), 50 | ), 51 | ); 52 | }), 53 | ); 54 | } 55 | } 56 | 57 | /// 58 | /// 文生图每种样式展示小部件 59 | /// 60 | class ImageStack extends StatelessWidget { 61 | final String url; 62 | final String label; 63 | final String subLabel; 64 | 65 | const ImageStack({ 66 | super.key, 67 | required this.url, 68 | required this.label, 69 | required this.subLabel, 70 | }); 71 | 72 | @override 73 | Widget build(BuildContext context) { 74 | return Stack( 75 | fit: StackFit.expand, 76 | children: [ 77 | Positioned.fill( 78 | child: Image.asset( 79 | url, 80 | fit: BoxFit.cover, 81 | errorBuilder: (BuildContext context, Object exception, 82 | StackTrace? stackTrace) { 83 | return Container(color: Colors.grey.shade300); 84 | }, 85 | ), 86 | ), 87 | Align( 88 | alignment: Alignment.bottomCenter, 89 | child: RichText( 90 | softWrap: true, 91 | textAlign: TextAlign.center, 92 | overflow: TextOverflow.ellipsis, 93 | maxLines: 3, 94 | text: TextSpan( 95 | children: [ 96 | TextSpan( 97 | text: label, 98 | style: TextStyle( 99 | fontSize: 10.sp, 100 | fontWeight: FontWeight.bold, 101 | ), 102 | ), 103 | TextSpan( 104 | text: "\n$subLabel", 105 | style: TextStyle(fontWeight: FontWeight.bold, fontSize: 9.sp), 106 | ), 107 | ], 108 | ), 109 | ), 110 | ), 111 | ], 112 | ); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /lib/common/constants/default_image_generation_models.dart: -------------------------------------------------------------------------------- 1 | import '../llm_spec/cus_brief_llm_model.dart'; 2 | import '../llm_spec/constant_llm_enum.dart'; 3 | 4 | /// 内置的默认模型列表 5 | final defaultImageGenerationModels = [ 6 | /// 智谱AI 7 | CusBriefLLMSpec( 8 | ApiPlatform.zhipu, 9 | 'cogview-3-flash', 10 | LLModelType.tti, 11 | name: 'CogView-3-Flash', 12 | isFree: true, 13 | costPer: 0.0, 14 | cusLlmSpecId: 'zhipu_cogview_3_flash_builtin', 15 | gmtRelease: DateTime.parse('1970-01-01'), 16 | gmtCreate: DateTime.now(), 17 | isBuiltin: true, 18 | ), 19 | ]; 20 | -------------------------------------------------------------------------------- /lib/common/constants/default_video_generation_models.dart: -------------------------------------------------------------------------------- 1 | import '../llm_spec/cus_brief_llm_model.dart'; 2 | import '../llm_spec/constant_llm_enum.dart'; 3 | 4 | final defaultVideoGenerationModels = [ 5 | CusBriefLLMSpec( 6 | ApiPlatform.zhipu, 7 | 'cogvideox-flash', 8 | LLModelType.video, 9 | name: 'cogvideox-flash', 10 | isFree: true, 11 | costPer: 0, 12 | cusLlmSpecId: 'zhipu_cogvideox_flash_builtin', 13 | gmtRelease: DateTime.parse('1970-01-01'), 14 | gmtCreate: DateTime.now(), 15 | isBuiltin: true, 16 | ), 17 | ]; 18 | -------------------------------------------------------------------------------- /lib/common/llm_spec/constant_llm_enum.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: constant_identifier_names, non_constant_identifier_names 2 | 3 | /// 4 | /// 这里是付费的大模型通用 5 | /// 6 | /// 定义云平台 7 | /// 2024-07-08 这里的AI助手,估计只需要这个付费的就好了 8 | /// 9 | /// 因为零一万物的API兼容openAI的api,后续付费的应该都是这样的,而不是之前免费的三大平台乱七八糟的 10 | /// 11 | enum ApiPlatform { 12 | // 用户使用的模型不属于预设平台(比如谷歌等) 13 | // 那么就是统一custom,并在自定义模型中直接新增url、apikey等栏位去取用 14 | // 这个不是默认的有效平台,不需要用户导入啥的 15 | custom, 16 | 17 | aliyun, 18 | baidu, 19 | tencent, 20 | 21 | deepseek, 22 | lingyiwanwu, 23 | zhipu, 24 | 25 | siliconCloud, 26 | infini, 27 | 28 | // 2025-03-24 火山引擎默认调用和关联应用(比如配置了联网搜索)使用的url不一样 29 | // 避免出现冲突,分成两个且互不包含 30 | volcengine, 31 | volcesBot, 32 | } 33 | 34 | // 用户自行导入密钥时,json文件的key 35 | // 2025-03-14 这里的label,需要完整包含上面平台的枚举值, 36 | // 否则无法用户单个添加指定平台的模型时,正确识别 37 | enum ApiPlatformAKLabel { 38 | // 传统主流多模型平台(自研+第三方) 39 | USER_ALIYUN_API_KEY, 40 | USER_BAIDU_API_KEY_V2, 41 | USER_TENCENT_API_KEY, 42 | 43 | // 自平台(只有自研) 44 | USER_DEEPSEEK_API_KEY, // 深度求索 45 | USER_LINGYIWANWU_API_KEY, // 零一万物 46 | USER_ZHIPU_API_KEY, // 智谱AI 47 | 48 | // 第三方多模型平台(只有第三方) 49 | USER_SILICONCLOUD_API_KEY, // 硅基流动 50 | USER_INFINI_GEN_STUDIO_API_KEY, // 无问芯穹的genStudio 51 | USER_VOLCENGINE_API_KEY, // 火山引擎 52 | USER_VOLCESBOT_API_KEY, // 火山引擎的bot 53 | 54 | // 讯飞, 语音转写需要 55 | USER_XFYUN_APP_ID, 56 | USER_XFYUN_API_KEY, 57 | USER_XFYUN_API_SECRET, 58 | } 59 | 60 | // 模型对应的中文名 61 | final Map CP_NAME_MAP = { 62 | ApiPlatform.aliyun: '阿里', 63 | ApiPlatform.baidu: '百度', 64 | ApiPlatform.tencent: '腾讯', 65 | ApiPlatform.deepseek: '深度求索', 66 | ApiPlatform.lingyiwanwu: '零一万物', 67 | ApiPlatform.zhipu: '智谱', 68 | ApiPlatform.siliconCloud: '硅基流动', 69 | ApiPlatform.infini: '无问芯穹', 70 | ApiPlatform.volcengine: '火山引擎', 71 | ApiPlatform.volcesBot: '火山Bot', 72 | ApiPlatform.custom: '[自定义]', 73 | }; 74 | 75 | // 大模型的分类,在不同页面可以用作模型的筛选 76 | enum LLModelType { 77 | cc, // Chat Completions 78 | // 2025-03-06 推理模型(深度思考)有思考过程,且支持的参数和对话模型差异很大,所以单独分类 79 | reasoner, 80 | vision, // 视觉大模型 81 | vision_reasoner, // 视觉推理 82 | 83 | // 图片生成大模型分3种: 单独文生图、单独图生图、文生图生都可以 84 | tti, // Text To Image 85 | iti, // Image To Image 86 | image, 87 | 88 | // 视频生成大模型分3种: 单独文生视频、单独图生视频、文生图生都可以 89 | ttv, // Text To Video 90 | itv, // Image To Video 91 | video, 92 | } 93 | 94 | // 模型类型对应的中文名 95 | final Map MT_NAME_MAP = { 96 | LLModelType.cc: '文本对话', 97 | LLModelType.reasoner: '深度思考', 98 | LLModelType.vision: '图片解读', 99 | LLModelType.vision_reasoner: '视觉推理', 100 | LLModelType.tti: '文本生图', 101 | LLModelType.iti: '图片生图', 102 | LLModelType.image: '图片生成', 103 | LLModelType.ttv: '文生视频', 104 | LLModelType.itv: '图生视频', 105 | LLModelType.video: '视频生成', 106 | }; 107 | -------------------------------------------------------------------------------- /lib/common/llm_spec/cus_brief_llm_model.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'cus_brief_llm_model.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | CusBriefLLMSpec _$CusBriefLLMSpecFromJson(Map json) => 10 | CusBriefLLMSpec( 11 | $enumDecode(_$ApiPlatformEnumMap, json['platform']), 12 | json['model'] as String, 13 | $enumDecode(_$LLModelTypeEnumMap, json['modelType']), 14 | name: json['name'] as String?, 15 | isFree: json['isFree'] as bool?, 16 | inputPrice: (json['inputPrice'] as num?)?.toDouble(), 17 | outputPrice: (json['outputPrice'] as num?)?.toDouble(), 18 | costPer: (json['costPer'] as num?)?.toDouble(), 19 | contextLength: (json['contextLength'] as num?)?.toInt(), 20 | cusLlmSpecId: json['cusLlmSpecId'] as String, 21 | gmtRelease: json['gmtRelease'] == null 22 | ? null 23 | : DateTime.parse(json['gmtRelease'] as String), 24 | gmtCreate: json['gmtCreate'] == null 25 | ? null 26 | : DateTime.parse(json['gmtCreate'] as String), 27 | isBuiltin: json['isBuiltin'] as bool? ?? false, 28 | baseUrl: json['baseUrl'] as String?, 29 | apiKey: json['apiKey'] as String?, 30 | description: json['description'] as String?, 31 | ); 32 | 33 | Map _$CusBriefLLMSpecToJson(CusBriefLLMSpec instance) => 34 | { 35 | 'cusLlmSpecId': instance.cusLlmSpecId, 36 | 'platform': _$ApiPlatformEnumMap[instance.platform]!, 37 | 'model': instance.model, 38 | 'modelType': _$LLModelTypeEnumMap[instance.modelType]!, 39 | 'name': instance.name, 40 | 'isFree': instance.isFree, 41 | 'inputPrice': instance.inputPrice, 42 | 'outputPrice': instance.outputPrice, 43 | 'costPer': instance.costPer, 44 | 'contextLength': instance.contextLength, 45 | 'gmtRelease': instance.gmtRelease?.toIso8601String(), 46 | 'gmtCreate': instance.gmtCreate?.toIso8601String(), 47 | 'isBuiltin': instance.isBuiltin, 48 | 'baseUrl': instance.baseUrl, 49 | 'apiKey': instance.apiKey, 50 | 'description': instance.description, 51 | }; 52 | 53 | const _$ApiPlatformEnumMap = { 54 | ApiPlatform.custom: 'custom', 55 | ApiPlatform.aliyun: 'aliyun', 56 | ApiPlatform.baidu: 'baidu', 57 | ApiPlatform.tencent: 'tencent', 58 | ApiPlatform.deepseek: 'deepseek', 59 | ApiPlatform.lingyiwanwu: 'lingyiwanwu', 60 | ApiPlatform.zhipu: 'zhipu', 61 | ApiPlatform.siliconCloud: 'siliconCloud', 62 | ApiPlatform.infini: 'infini', 63 | ApiPlatform.volcengine: 'volcengine', 64 | ApiPlatform.volcesBot: 'volcesBot', 65 | }; 66 | 67 | const _$LLModelTypeEnumMap = { 68 | LLModelType.cc: 'cc', 69 | LLModelType.reasoner: 'reasoner', 70 | LLModelType.vision: 'vision', 71 | LLModelType.vision_reasoner: 'vision_reasoner', 72 | LLModelType.tti: 'tti', 73 | LLModelType.iti: 'iti', 74 | LLModelType.image: 'image', 75 | LLModelType.ttv: 'ttv', 76 | LLModelType.itv: 'itv', 77 | LLModelType.video: 'video', 78 | }; 79 | -------------------------------------------------------------------------------- /lib/common/utils/advanced_options_utils.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: avoid_print 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 5 | import '../components/advanced_options_bottom_sheet.dart'; 6 | import '../components/advanced_options_panel.dart'; 7 | import '../llm_spec/constant_llm_enum.dart'; 8 | import '../constants/advanced_options_presets.dart'; 9 | 10 | /// 高级选项结果 11 | class AdvancedOptionsResult { 12 | /// 是否启用高级选项 13 | final bool enabled; 14 | 15 | /// 高级选项参数值 16 | final Map options; 17 | 18 | const AdvancedOptionsResult({ 19 | required this.enabled, 20 | required this.options, 21 | }); 22 | } 23 | 24 | /// 高级选项工具类 25 | class AdvancedOptionsUtils { 26 | /// 显示高级选项弹窗 27 | static Future showAdvancedOptions({ 28 | required BuildContext context, 29 | required ApiPlatform platform, 30 | required LLModelType modelType, 31 | required bool currentEnabled, 32 | required Map currentOptions, 33 | }) async { 34 | final List options = 35 | AdvancedOptionsManager.getAvailableOptions(platform, modelType); 36 | 37 | for (var i = 0; i < options.length; i++) { 38 | print('显示高级选项弹窗中的参数 $i: ${options[i].key}'); 39 | } 40 | 41 | if (options.isEmpty) { 42 | if (!context.mounted) return null; 43 | ScaffoldMessenger.of(context).showSnackBar( 44 | const SnackBar(content: Text('当前模型没有可配置的高级参数')), 45 | ); 46 | return null; 47 | } 48 | 49 | return await showModalBottomSheet( 50 | context: context, 51 | isScrollControlled: true, // 允许弹窗内容滚动 52 | shape: RoundedRectangleBorder( 53 | borderRadius: BorderRadius.vertical(top: Radius.circular(15.sp)), 54 | ), 55 | builder: (context) { 56 | return DraggableScrollableSheet( 57 | initialChildSize: 0.7, // 初始高度为屏幕的70% 58 | minChildSize: 0.5, // 最小高度为50% 59 | maxChildSize: 0.95, // 最大高度为95% 60 | expand: false, 61 | builder: (context, scrollController) { 62 | return AdvancedOptionsBottomSheet( 63 | enabled: currentEnabled, 64 | currentOptions: currentOptions, 65 | options: options, 66 | ); 67 | }, 68 | ); 69 | }, 70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/common/utils/cus_logger.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: avoid_print 2 | 3 | import 'package:logger/logger.dart'; 4 | 5 | class NoLimitLogPrinter extends LogPrinter { 6 | final LogPrinter _defaultPrinter; 7 | 8 | NoLimitLogPrinter(this._defaultPrinter); 9 | 10 | @override 11 | List log(LogEvent event) { 12 | final lines = _defaultPrinter.log(event); 13 | return lines.map((line) => line.replaceAll('\n', ' ')).toList(); 14 | } 15 | } 16 | 17 | class NoLimitLogOutput extends LogOutput { 18 | @override 19 | void output(OutputEvent event) { 20 | for (var line in event.lines) { 21 | print(line); // 使用 print 输出日志,没有长度限制 22 | } 23 | } 24 | } 25 | 26 | var logger = Logger( 27 | output: NoLimitLogOutput(), // 使用自定义的 LogOutput 28 | printer: NoLimitLogPrinter(PrettyPrinter()), // 使用自定义的 LogPrinter 29 | ); 30 | -------------------------------------------------------------------------------- /lib/common/utils/db_tools/ddl_brief_ai_tool.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: constant_identifier_names 2 | 3 | import 'init_db.dart'; 4 | 5 | /// 数据库中【简洁版AI工具】相关表的创建 6 | /// 2025-02-28 之后AI工具相关都只有简洁版本了 7 | class BriefAIToolDdl { 8 | /// 2025-02-24 新版本的对话留存表栏位和旧版本差不多,只是表名不同,不互相干扰 9 | static const tableNameOfBriefChatHistory = 10 | '${DB_TABLE_PREFIX}brief_chat_history'; 11 | 12 | static const String ddlForBriefChatHistory = """ 13 | CREATE TABLE $tableNameOfBriefChatHistory ( 14 | uuid TEXT NOT NULL, 15 | title TEXT NOT NULL, 16 | gmtCreate TEXT NOT NULL, 17 | gmtModified TEXT NOT NULL, 18 | messages TEXT NOT NULL, 19 | modelType TEXT NOT NULL, 20 | llmSpec TEXT NOT NULL, 21 | PRIMARY KEY(uuid) 22 | ); 23 | """; 24 | 25 | // 2025-02-14 新的简洁版生成式任务记录 26 | // 2025-02-19 图片生成、视频生成任务都放在这里面,后续可能音频生成相关的也放在这里 27 | // 图片可能直接返回结果,那么task相关栏位就为空 28 | // 但阿里云的图片和所有的视频生成,都是先返回任务提交结果,然后查询任务状态,这些内容都放在这个生成记录中 29 | // 栏位只保留必要的,其他参数通过otherParams字段存入json 30 | // 不同平台的任务状态枚举不一样,所以除了存放taskStatus,还存放了是否完成等栏位 31 | // isSuccess + isProcessing + isFailed ,都是前端根据taskStatus来判断,方便直接查询 32 | // 因为多种媒体资源生成任务和结果都在这里,所以需要指定调用模型的类型modelType和模型信息llmSpec 33 | static const tableNameOfMediaGenerationHistory = 34 | '${DB_TABLE_PREFIX}brief_media_generation_history'; 35 | 36 | static const String ddlForMediaGenerationHistory = """ 37 | CREATE TABLE $tableNameOfMediaGenerationHistory ( 38 | requestId TEXT NOT NULL, 39 | prompt TEXT NOT NULL, 40 | negativePrompt TEXT, 41 | refImageUrls TEXT, 42 | modelType TEXT NOT NULL, 43 | llmSpec TEXT NOT NULL, 44 | taskId TEXT, 45 | taskStatus TEXT, 46 | isSuccess INTEGER, 47 | isProcessing INTEGER, 48 | isFailed INTEGER, 49 | imageUrls TEXT, 50 | videoUrls TEXT, 51 | audioUrls TEXT, 52 | otherParams TEXT, 53 | gmtCreate TEXT NOT NULL, 54 | gmtModified TEXT, 55 | PRIMARY KEY(requestId) 56 | ); 57 | """; 58 | 59 | // 2025-02-14 新的简洁版用户自动导入的模型配置 60 | // (栏位更新,和旧版本的cus_llm_spec区分开,不影响旧版本业务) 61 | static const tableNameOfCusBriefLlmSpec = 62 | '${DB_TABLE_PREFIX}brief_cus_llm_spec'; 63 | 64 | static const String ddlForCusBriefLlmSpec = """ 65 | CREATE TABLE $tableNameOfCusBriefLlmSpec ( 66 | cusLlmSpecId TEXT NOT NULL, 67 | platform TEXT NOT NULL, 68 | model TEXT NOT NULL, 69 | name TEXT, 70 | contextLength INTEGER, 71 | isFree INTEGER, 72 | modelType TEXT NOT NULL, 73 | inputPrice REAL, 74 | outputPrice REAL, 75 | costPer REAL, 76 | gmtRelease TEXT, 77 | gmtCreate TEXT NOT NULL, 78 | isBuiltin INTEGER NOT NULL, 79 | baseUrl TEXT, 80 | apiKey TEXT, 81 | description TEXT, 82 | PRIMARY KEY(cusLlmSpecId), 83 | UNIQUE(platform,model,modelType) 84 | ); 85 | """; 86 | } 87 | -------------------------------------------------------------------------------- /lib/common/utils/db_tools/ddl_life_tool.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: constant_identifier_names 2 | 3 | import 'init_db.dart'; 4 | 5 | /// 数据库中【生活工具】相关表的创建 6 | class LifeToolDdl { 7 | // 账单条目表 8 | static const tableNameOfBillItem = '${DB_TABLE_PREFIX}bill_item'; 9 | 10 | static const String ddlForBillItem = """ 11 | CREATE TABLE $tableNameOfBillItem ( 12 | bill_item_id TEXT NOT NULL, 13 | item_type INTEGER NOT NULL, 14 | date TEXT, 15 | category TEXT, 16 | item TEXT NOT NULL, 17 | value REAL NOT NULL, 18 | gmt_modified TEXT NOT NULL, 19 | PRIMARY KEY(bill_item_id) 20 | ); 21 | """; 22 | 23 | /// 24 | /// 菜品基础表 25 | /// 26 | static const tableNameOfDish = '${DB_TABLE_PREFIX}dish'; 27 | 28 | // 2023-03-10 避免导入时重复导入,还是加一个unique 29 | static const String ddlForDish = """ 30 | CREATE TABLE $tableNameOfDish ( 31 | dish_id TEXT NOT NULL PRIMARY KEY, 32 | dish_name TEXT NOT NULL, 33 | description TEXT, 34 | photos TEXT, 35 | videos TEXT, 36 | tags TEXT, 37 | meal_categories TEXT, 38 | recipe TEXT, 39 | recipe_picture TEXT, 40 | UNIQUE(dish_name,tags) 41 | ); 42 | """; 43 | 44 | /// 45 | /// 动物的品种,简单保留品种和亚种即可(统一的话就一个品种信息,品种亚种都有则保留有更详细的) 46 | /// 2024-09-14 暂时没用到 47 | /// 48 | static const tableNameOfAnimalBreed = '${DB_TABLE_PREFIX}animal_breed'; 49 | 50 | static const String ddlForAnimalBreed = """ 51 | CREATE TABLE $tableNameOfAnimalBreed ( 52 | id TEXT NOT NULL, 53 | breed TEXT NOT NULL, 54 | subBreed TEXT, 55 | temperament TEXT, 56 | origin TEXT, 57 | description TEXT, 58 | lifeSpan TEXT, 59 | altNames TEXT, 60 | wikipediaUrl TEXT, 61 | referenceImageUrl TEXT, 62 | dataSource TEXT, 63 | PRIMARY KEY(id) 64 | ); 65 | """; 66 | } 67 | -------------------------------------------------------------------------------- /lib/common/utils/dio_client/cus_http_client.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | 3 | import 'cus_http_request.dart'; 4 | 5 | // 来源:https://www.cnblogs.com/luoshang/p/16987781.html 6 | 7 | /// 调用底层的request,重新提供get,post等方便方法 8 | 9 | class HttpUtils { 10 | static HttpRequest httpRequest = HttpRequest(); 11 | 12 | /// get 13 | static Future get({ 14 | required String path, 15 | Map? queryParameters, 16 | dynamic headers, 17 | CusRespType? responseType, 18 | bool showLoading = true, 19 | bool showErrorMessage = true, 20 | }) { 21 | return httpRequest.request( 22 | path: path, 23 | method: CusHttpMethod.get, 24 | queryParameters: queryParameters, 25 | responseType: responseType, 26 | headers: headers, 27 | showLoading: showLoading, 28 | showErrorMessage: showErrorMessage, 29 | ); 30 | } 31 | 32 | /// post 33 | static Future post({ 34 | required String path, 35 | required CusHttpMethod method, 36 | dynamic data, 37 | dynamic headers, 38 | CusRespType? responseType, 39 | CancelToken? cancelToken, 40 | bool showLoading = true, 41 | bool showErrorMessage = true, 42 | }) { 43 | return httpRequest.request( 44 | path: path, 45 | method: CusHttpMethod.post, 46 | data: data, 47 | responseType: responseType, 48 | headers: headers, 49 | cancelToken: cancelToken, 50 | showLoading: showLoading, 51 | showErrorMessage: showErrorMessage, 52 | ); 53 | } 54 | } 55 | 56 | /* 57 | 使用方法: 58 | import 'cus_dio_client.dart'; 59 | 60 | HttpUtils.get( 61 |   path: '11111' 62 | ); 63 | 64 |  HttpUtils.post( 65 |   path: '1111', 66 |   method: HttpMethod.post //可以更改其他的 67 | ); 68 | */ -------------------------------------------------------------------------------- /lib/common/utils/dio_client/cus_http_options.dart: -------------------------------------------------------------------------------- 1 | // 超时时间 2 | class HttpOptions { 3 | // 请求地址,这个应该别处传来(使用时带上完整地址即可) 4 | static const String baseUrl = ''; 5 | //单位时间是ms 6 | static const Duration connectTimeout = Duration(milliseconds: 60 * 1000); 7 | static const Duration receiveTimeout = Duration(milliseconds: 5 * 60 * 1000); 8 | static const Duration sendTimeout = Duration(milliseconds: 30 * 1000); 9 | // 自定义content-type 10 | static const String contentType = "application/json;charset=utf-8"; 11 | } 12 | -------------------------------------------------------------------------------- /lib/common/utils/dio_client/intercepter_response.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: avoid_print 2 | 3 | import 'package:dio/dio.dart'; 4 | 5 | class ResponseIntercepter extends Interceptor { 6 | const ResponseIntercepter(); 7 | 8 | @override 9 | Future onResponse( 10 | Response response, 11 | ResponseInterceptorHandler handler, 12 | ) async { 13 | print('【onResponse】进入了dio的响应拦截器'); 14 | 15 | // ??? 这里打印response和response.data是一样的 16 | 17 | // print("************************** ${response}"); 18 | // print("************************** ${response.data}"); 19 | 20 | // 判断返回数据中是否包含"token失效"的信息 21 | // if (response.data.contains("token失效")) { 22 | // // 导航到登录页面 23 | // navigatorKey.currentState!.pushReplacementNamed('/login'); 24 | // } 25 | 26 | handler.next(response); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/common/utils/dio_client/interceptor_request.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: avoid_print 2 | 3 | import 'package:dio/dio.dart'; 4 | 5 | class RequestInterceptor extends Interceptor { 6 | const RequestInterceptor(); 7 | 8 | @override 9 | Future onRequest( 10 | RequestOptions options, 11 | RequestInterceptorHandler handler, 12 | ) async { 13 | print('【onRequest】进入了dio的请求拦截器'); 14 | 15 | // 可以在这里添加 authorization 自定义头等操作 16 | 17 | return handler.next(options); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/models/brief_ai_tools/branch_chat/branch_chat_message.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:objectbox/objectbox.dart'; 4 | import 'branch_chat_session.dart'; 5 | 6 | @Entity() 7 | class BranchChatMessage { 8 | @Id(assignable: true) 9 | int id; 10 | 11 | String messageId; 12 | String role; 13 | String content; 14 | DateTime createTime; 15 | 16 | // 可选字段 17 | String? reasoningContent; 18 | int? thinkingDuration; 19 | String? contentVoicePath; 20 | String? imagesUrl; 21 | String? videosUrl; 22 | 23 | // 2025-03-24 联网搜索参考内容 24 | // 使用字符串存储序列化后的JSON 25 | String? referencesJson; 26 | 27 | // 非持久化字段,用于运行时 28 | @Transient() 29 | List>? _references; 30 | 31 | int? promptTokens; 32 | int? completionTokens; 33 | int? totalTokens; 34 | String? modelLabel; 35 | 36 | // 树形结构关系 37 | @Backlink('parent') 38 | final children = ToMany(); 39 | 40 | final parent = ToOne(); 41 | final session = ToOne(); 42 | 43 | // 分支相关 44 | int branchIndex; // 当前分支在同级分支中的索引 45 | int depth; // 分支深度,根节点为0 46 | String branchPath; // 存储从根到当前节点的分支路径,如 "0/1/0" 47 | 48 | // Getter和Setter用于处理references字段 49 | List>? get references { 50 | if (_references == null && referencesJson != null) { 51 | try { 52 | final List decoded = jsonDecode(referencesJson!); 53 | _references = decoded.cast>(); 54 | } catch (e) { 55 | if (kDebugMode) { 56 | print('解析references失败: $e'); 57 | } 58 | _references = null; 59 | } 60 | } 61 | return _references; 62 | } 63 | 64 | set references(List>? value) { 65 | _references = value; 66 | if (value != null) { 67 | try { 68 | referencesJson = jsonEncode(value); 69 | } catch (e) { 70 | if (kDebugMode) { 71 | print('序列化references失败: $e'); 72 | } 73 | referencesJson = null; 74 | } 75 | } else { 76 | referencesJson = null; 77 | } 78 | } 79 | 80 | BranchChatMessage({ 81 | this.id = 0, 82 | required this.messageId, 83 | required this.role, 84 | required this.content, 85 | required this.createTime, 86 | this.branchIndex = 0, 87 | this.depth = 0, 88 | this.branchPath = "0", 89 | this.reasoningContent, 90 | this.thinkingDuration, 91 | this.contentVoicePath, 92 | this.imagesUrl, 93 | this.videosUrl, 94 | List>? references, 95 | this.promptTokens, 96 | this.completionTokens, 97 | this.totalTokens, 98 | this.modelLabel, 99 | }) { 100 | this.references = references; // 使用setter来设置references和referencesJson 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /lib/models/brief_ai_tools/branch_chat/branch_chat_session.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:objectbox/objectbox.dart'; 3 | import '../../../common/llm_spec/cus_brief_llm_model.dart'; 4 | import '../../../common/llm_spec/constant_llm_enum.dart'; 5 | import 'branch_chat_message.dart'; 6 | 7 | @Entity() 8 | class BranchChatSession { 9 | @Id() 10 | int id; 11 | 12 | String title; 13 | DateTime createTime; 14 | DateTime updateTime; 15 | 16 | // 修改字段名,使其成为普通属性而不是私有属性 17 | String? llmSpecJson; 18 | String? modelTypeStr; 19 | 20 | @Transient() // 标记为非持久化字段 21 | CusBriefLLMSpec? _llmSpec; 22 | 23 | @Transient() // 标记为非持久化字段 24 | LLModelType? _modelType; 25 | 26 | // Getter 和 Setter 27 | CusBriefLLMSpec get llmSpec { 28 | if (_llmSpec == null && llmSpecJson != null) { 29 | try { 30 | _llmSpec = CusBriefLLMSpec.fromJson(jsonDecode(llmSpecJson!)); 31 | } catch (e) { 32 | rethrow; 33 | } 34 | } 35 | return _llmSpec!; 36 | } 37 | 38 | set llmSpec(CusBriefLLMSpec value) { 39 | _llmSpec = value; 40 | llmSpecJson = jsonEncode(value.toJson()); 41 | } 42 | 43 | LLModelType get modelType { 44 | if (_modelType == null && modelTypeStr != null) { 45 | _modelType = LLModelType.values.firstWhere( 46 | (e) => e.toString() == modelTypeStr, 47 | ); 48 | } 49 | return _modelType!; 50 | } 51 | 52 | set modelType(LLModelType value) { 53 | _modelType = value; 54 | modelTypeStr = value.toString(); 55 | } 56 | 57 | @Backlink('session') 58 | final messages = ToMany(); 59 | 60 | // 添加默认构造函数 61 | BranchChatSession({ 62 | this.id = 0, 63 | required this.title, 64 | required this.createTime, 65 | required this.updateTime, 66 | this.llmSpecJson, 67 | this.modelTypeStr, 68 | }); 69 | 70 | // 添加命名构造函数用于创建新会话 71 | factory BranchChatSession.create({ 72 | required String title, 73 | required CusBriefLLMSpec llmSpec, 74 | required LLModelType modelType, 75 | DateTime? createTime, 76 | DateTime? updateTime, 77 | }) { 78 | final session = BranchChatSession( 79 | title: title, 80 | createTime: createTime ?? DateTime.now(), 81 | updateTime: updateTime ?? DateTime.now(), 82 | ); 83 | session.llmSpec = llmSpec; 84 | session.modelType = modelType; 85 | return session; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lib/models/brief_ai_tools/character_chat/character_chat_message.dart: -------------------------------------------------------------------------------- 1 | import 'package:uuid/uuid.dart'; 2 | 3 | class CharacterChatMessage { 4 | final String id; 5 | String content; 6 | // 思考内容和时长 7 | String? reasoningContent; 8 | int? thinkingDuration; 9 | String role; // 'user', 'assistant', 'system' 10 | String? characterId; // 对应角色ID,用户消息为null 11 | DateTime timestamp; 12 | String? contentVoicePath; // 语音消息路径 13 | String? imagesUrl; // 图片URL,多个用逗号分隔 14 | int? promptTokens; // 提示词token数 15 | int? completionTokens; // 回复token数 16 | int? totalTokens; // 总token数 17 | 18 | CharacterChatMessage({ 19 | String? id, 20 | required this.content, 21 | required this.role, 22 | this.characterId, 23 | this.reasoningContent, 24 | this.thinkingDuration, 25 | DateTime? timestamp, 26 | this.contentVoicePath, 27 | this.imagesUrl, 28 | this.promptTokens, 29 | this.completionTokens, 30 | this.totalTokens, 31 | }) : id = id ?? const Uuid().v4(), 32 | timestamp = timestamp ?? DateTime.now(); 33 | 34 | // JSON序列化和反序列化方法 35 | Map toJson() { 36 | return { 37 | 'id': id, 38 | 'content': content, 39 | 'role': role, 40 | 'characterId': characterId, 41 | 'reasoningContent': reasoningContent, 42 | 'thinkingDuration': thinkingDuration, 43 | 'timestamp': timestamp.toIso8601String(), 44 | 'contentVoicePath': contentVoicePath, 45 | 'imagesUrl': imagesUrl, 46 | 'promptTokens': promptTokens, 47 | 'completionTokens': completionTokens, 48 | 'totalTokens': totalTokens, 49 | }; 50 | } 51 | 52 | factory CharacterChatMessage.fromJson(Map json) { 53 | return CharacterChatMessage( 54 | id: json['id'], 55 | content: json['content'], 56 | role: json['role'], 57 | characterId: json['characterId'], 58 | reasoningContent: json['reasoningContent'], 59 | thinkingDuration: json['thinkingDuration'], 60 | timestamp: DateTime.parse(json['timestamp']), 61 | contentVoicePath: json['contentVoicePath'], 62 | imagesUrl: json['imagesUrl'], 63 | promptTokens: json['promptTokens'], 64 | completionTokens: json['completionTokens'], 65 | totalTokens: json['totalTokens'], 66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/models/brief_ai_tools/character_chat/character_chat_session.dart: -------------------------------------------------------------------------------- 1 | import 'package:uuid/uuid.dart'; 2 | import '../../../common/llm_spec/cus_brief_llm_model.dart'; 3 | import 'character_card.dart'; 4 | import 'character_chat_message.dart'; 5 | 6 | class CharacterChatSession { 7 | final String id; 8 | String title; 9 | List characters; // 支持多角色 10 | List messages; 11 | DateTime createTime; 12 | DateTime updateTime; 13 | CusBriefLLMSpec? activeModel; // 当前使用的模型 14 | 15 | CharacterChatSession({ 16 | String? id, 17 | required this.title, 18 | required this.characters, 19 | List? messages, 20 | DateTime? createTime, 21 | DateTime? updateTime, 22 | this.activeModel, 23 | }) : id = id ?? const Uuid().v4(), 24 | messages = messages ?? [], 25 | createTime = createTime ?? DateTime.now(), 26 | updateTime = updateTime ?? DateTime.now(); 27 | 28 | // JSON序列化和反序列化方法 29 | Map toJson() { 30 | return { 31 | 'id': id, 32 | 'title': title, 33 | 'characters': characters.map((c) => c.toJson()).toList(), 34 | 'messages': messages.map((m) => m.toJson()).toList(), 35 | 'createTime': createTime.toIso8601String(), 36 | 'updateTime': updateTime.toIso8601String(), 37 | 'activeModel': activeModel?.toJson(), 38 | }; 39 | } 40 | 41 | factory CharacterChatSession.fromJson(Map json) { 42 | return CharacterChatSession( 43 | id: json['id'], 44 | title: json['title'], 45 | characters: (json['characters'] as List) 46 | .map((c) => CharacterCard.fromJson(c)) 47 | .toList(), 48 | messages: (json['messages'] as List) 49 | .map((m) => CharacterChatMessage.fromJson(m)) 50 | .toList(), 51 | createTime: DateTime.parse(json['createTime']), 52 | updateTime: DateTime.parse(json['updateTime']), 53 | activeModel: json['activeModel'] != null 54 | ? CusBriefLLMSpec.fromJson(json['activeModel']) 55 | : null, 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/models/brief_ai_tools/chat_completions/chat_completion_request.dart: -------------------------------------------------------------------------------- 1 | class ChatCompletionRequest { 2 | // 简化的参数 3 | final String model; 4 | final List> messages; 5 | final bool stream; 6 | // 所有额外参数(根据平台或者模型参数不固定) 7 | final Map? additionalParams; 8 | 9 | const ChatCompletionRequest({ 10 | required this.model, 11 | required this.messages, 12 | this.stream = true, 13 | // 添加额外参数字段 14 | this.additionalParams, 15 | }); 16 | 17 | Map toRequestBody() { 18 | // 基础请求体 19 | final Map base = { 20 | 'model': model, 21 | 'messages': messages, 22 | 'stream': stream, 23 | }; 24 | 25 | if (additionalParams != null) { 26 | base.addAll(additionalParams!); 27 | } 28 | 29 | return base; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/models/brief_ai_tools/chat_completions/chat_completion_response.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'chat_completion_response.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | ChatCompletionResponse _$ChatCompletionResponseFromJson( 10 | Map json) => 11 | ChatCompletionResponse( 12 | id: readJsonValue(json, 'id') as String, 13 | object: readJsonValue(json, 'object') as String?, 14 | created: (readJsonValue(json, 'created') as num?)?.toInt(), 15 | model: readJsonValue(json, 'model') as String, 16 | choices: (readJsonValue(json, 'choices') as List) 17 | .map((e) => ChatCompletionChoice.fromJson(e as Map)) 18 | .toList(), 19 | usage: readJsonValue(json, 'usage') == null 20 | ? null 21 | : ChatCompletionUsage.fromJson( 22 | readJsonValue(json, 'usage') as Map), 23 | searchResults: 24 | (readReferenceValue(json, 'searchResults') as List?) 25 | ?.map((e) => e as Map) 26 | .toList(), 27 | systemFingerprint: readJsonValue(json, 'systemFingerprint') as String?, 28 | cusText: json['cusText'] as String?, 29 | ); 30 | 31 | Map _$ChatCompletionResponseToJson( 32 | ChatCompletionResponse instance) => 33 | { 34 | 'id': instance.id, 35 | 'object': instance.object, 36 | 'created': instance.created, 37 | 'model': instance.model, 38 | 'choices': instance.choices.map((e) => e.toJson()).toList(), 39 | 'usage': instance.usage?.toJson(), 40 | 'searchResults': instance.searchResults, 41 | 'systemFingerprint': instance.systemFingerprint, 42 | 'cusText': instance.cusText, 43 | }; 44 | 45 | ChatCompletionChoice _$ChatCompletionChoiceFromJson( 46 | Map json) => 47 | ChatCompletionChoice( 48 | index: (readJsonValue(json, 'index') as num?)?.toInt(), 49 | message: readJsonValue(json, 'message') as Map?, 50 | delta: readJsonValue(json, 'delta') as Map?, 51 | finishReason: readJsonValue(json, 'finishReason') as String?, 52 | toolCalls: (readJsonValue(json, 'toolCalls') as List?) 53 | ?.map((e) => e as Map) 54 | .toList(), 55 | ); 56 | 57 | Map _$ChatCompletionChoiceToJson( 58 | ChatCompletionChoice instance) => 59 | { 60 | 'index': instance.index, 61 | 'message': instance.message, 62 | 'delta': instance.delta, 63 | 'finishReason': instance.finishReason, 64 | 'toolCalls': instance.toolCalls, 65 | }; 66 | 67 | ChatCompletionUsage _$ChatCompletionUsageFromJson(Map json) => 68 | ChatCompletionUsage( 69 | promptTokens: (readJsonValue(json, 'promptTokens') as num).toInt(), 70 | completionTokens: 71 | (readJsonValue(json, 'completionTokens') as num).toInt(), 72 | totalTokens: (readJsonValue(json, 'totalTokens') as num).toInt(), 73 | ); 74 | 75 | Map _$ChatCompletionUsageToJson( 76 | ChatCompletionUsage instance) => 77 | { 78 | 'promptTokens': instance.promptTokens, 79 | 'completionTokens': instance.completionTokens, 80 | 'totalTokens': instance.totalTokens, 81 | }; 82 | -------------------------------------------------------------------------------- /lib/models/brief_ai_tools/chat_completions/chat_completion_tool.dart: -------------------------------------------------------------------------------- 1 | class ChatCompletionTool { 2 | final String type; 3 | final ChatCompletionFunction function; 4 | 5 | const ChatCompletionTool({ 6 | required this.type, 7 | required this.function, 8 | }); 9 | 10 | Map toJson() => { 11 | 'type': type, 12 | 'function': function.toJson(), 13 | }; 14 | } 15 | 16 | class ChatCompletionFunction { 17 | final String name; 18 | final String description; 19 | final Map parameters; 20 | 21 | const ChatCompletionFunction({ 22 | required this.name, 23 | required this.description, 24 | required this.parameters, 25 | }); 26 | 27 | Map toJson() => { 28 | 'name': name, 29 | 'description': description, 30 | 'parameters': parameters, 31 | }; 32 | } 33 | 34 | class ChatCompletionToolChoice { 35 | final String type; 36 | final String? function; 37 | 38 | const ChatCompletionToolChoice({ 39 | required this.type, 40 | this.function, 41 | }); 42 | 43 | Map toJson() => { 44 | 'type': type, 45 | if (function != null) 'function': function, 46 | }; 47 | 48 | static const none = ChatCompletionToolChoice(type: 'none'); 49 | static const auto = ChatCompletionToolChoice(type: 'auto'); 50 | } -------------------------------------------------------------------------------- /lib/models/brief_ai_tools/media_generation_history/media_generation_history.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'media_generation_history.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | MediaGenerationHistory _$MediaGenerationHistoryFromJson( 10 | Map json) => 11 | MediaGenerationHistory( 12 | requestId: json['requestId'] as String, 13 | prompt: json['prompt'] as String, 14 | negativePrompt: json['negativePrompt'] as String?, 15 | refImageUrls: (json['refImageUrls'] as List?) 16 | ?.map((e) => e as String) 17 | .toList(), 18 | modelType: $enumDecode(_$LLModelTypeEnumMap, json['modelType']), 19 | llmSpec: 20 | CusBriefLLMSpec.fromJson(json['llmSpec'] as Map), 21 | taskId: json['taskId'] as String?, 22 | taskStatus: json['taskStatus'] as String?, 23 | isSuccess: json['isSuccess'] as bool? ?? false, 24 | isProcessing: json['isProcessing'] as bool? ?? false, 25 | isFailed: json['isFailed'] as bool? ?? false, 26 | imageUrls: (json['imageUrls'] as List?) 27 | ?.map((e) => e as String) 28 | .toList(), 29 | videoUrls: (json['videoUrls'] as List?) 30 | ?.map((e) => e as String) 31 | .toList(), 32 | audioUrls: (json['audioUrls'] as List?) 33 | ?.map((e) => e as String) 34 | .toList(), 35 | otherParams: json['otherParams'] as String?, 36 | gmtCreate: DateTime.parse(json['gmtCreate'] as String), 37 | gmtModified: json['gmtModified'] == null 38 | ? null 39 | : DateTime.parse(json['gmtModified'] as String), 40 | ); 41 | 42 | Map _$MediaGenerationHistoryToJson( 43 | MediaGenerationHistory instance) => 44 | { 45 | 'requestId': instance.requestId, 46 | 'prompt': instance.prompt, 47 | 'negativePrompt': instance.negativePrompt, 48 | 'refImageUrls': instance.refImageUrls, 49 | 'modelType': _$LLModelTypeEnumMap[instance.modelType]!, 50 | 'llmSpec': instance.llmSpec.toJson(), 51 | 'taskId': instance.taskId, 52 | 'taskStatus': instance.taskStatus, 53 | 'isSuccess': instance.isSuccess, 54 | 'isProcessing': instance.isProcessing, 55 | 'isFailed': instance.isFailed, 56 | 'imageUrls': instance.imageUrls, 57 | 'videoUrls': instance.videoUrls, 58 | 'audioUrls': instance.audioUrls, 59 | 'otherParams': instance.otherParams, 60 | 'gmtCreate': instance.gmtCreate.toIso8601String(), 61 | 'gmtModified': instance.gmtModified?.toIso8601String(), 62 | }; 63 | 64 | const _$LLModelTypeEnumMap = { 65 | LLModelType.cc: 'cc', 66 | LLModelType.reasoner: 'reasoner', 67 | LLModelType.vision: 'vision', 68 | LLModelType.vision_reasoner: 'vision_reasoner', 69 | LLModelType.tti: 'tti', 70 | LLModelType.iti: 'iti', 71 | LLModelType.image: 'image', 72 | LLModelType.ttv: 'ttv', 73 | LLModelType.itv: 'itv', 74 | LLModelType.video: 'video', 75 | }; 76 | -------------------------------------------------------------------------------- /lib/models/brief_ai_tools/video_generation/video_generation_request.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'video_generation_request.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | VideoGenerationRequest _$VideoGenerationRequestFromJson( 10 | Map json) => 11 | VideoGenerationRequest( 12 | model: json['model'] as String, 13 | prompt: json['prompt'] as String, 14 | quality: json['quality'] as String?, 15 | withAudio: json['with_audio'] as bool?, 16 | imageUrl: json['image_url'] as String?, 17 | size: json['size'] as String?, 18 | fps: (json['fps'] as num?)?.toInt(), 19 | requestId: json['request_id'] as String?, 20 | userId: json['user_id'] as String?, 21 | image: json['image'] as String?, 22 | seed: (json['seed'] as num?)?.toInt(), 23 | guidanceScale: (json['guidance_scale'] as num?)?.toDouble(), 24 | input: json['input'] == null 25 | ? null 26 | : AliyunVideoInput.fromJson(json['input'] as Map), 27 | parameters: json['parameters'] == null 28 | ? null 29 | : AliyunVideoParameter.fromJson( 30 | json['parameters'] as Map), 31 | ); 32 | 33 | Map _$VideoGenerationRequestToJson( 34 | VideoGenerationRequest instance) => 35 | { 36 | 'model': instance.model, 37 | 'prompt': instance.prompt, 38 | 'input': instance.input?.toJson(), 39 | 'parameters': instance.parameters?.toJson(), 40 | 'quality': instance.quality, 41 | 'with_audio': instance.withAudio, 42 | 'image_url': instance.imageUrl, 43 | 'size': instance.size, 44 | 'fps': instance.fps, 45 | 'request_id': instance.requestId, 46 | 'user_id': instance.userId, 47 | 'image': instance.image, 48 | 'seed': instance.seed, 49 | 'guidance_scale': instance.guidanceScale, 50 | }; 51 | 52 | AliyunVideoInput _$AliyunVideoInputFromJson(Map json) => 53 | AliyunVideoInput( 54 | prompt: json['prompt'] as String?, 55 | imgUrl: json['img_url'] as String?, 56 | ); 57 | 58 | Map _$AliyunVideoInputToJson(AliyunVideoInput instance) => 59 | { 60 | 'prompt': instance.prompt, 61 | 'img_url': instance.imgUrl, 62 | }; 63 | 64 | AliyunVideoParameter _$AliyunVideoParameterFromJson( 65 | Map json) => 66 | AliyunVideoParameter( 67 | size: json['size'] as String?, 68 | seed: (json['seed'] as num?)?.toInt(), 69 | duration: (json['duration'] as num?)?.toInt() ?? 5, 70 | promptExtend: json['prompt_extend'] as bool? ?? true, 71 | ); 72 | 73 | Map _$AliyunVideoParameterToJson( 74 | AliyunVideoParameter instance) => 75 | { 76 | 'size': instance.size, 77 | 'duration': instance.duration, 78 | 'prompt_extend': instance.promptExtend, 79 | 'seed': instance.seed, 80 | }; 81 | -------------------------------------------------------------------------------- /lib/models/brief_ai_tools/voice_recognition/xunfei_voice_dictation.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'xunfei_voice_dictation.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | XunfeiVoiceDictation _$XunfeiVoiceDictationFromJson( 10 | Map json) => 11 | XunfeiVoiceDictation( 12 | json['sid'] as String?, 13 | (json['code'] as num?)?.toInt(), 14 | json['message'] as String?, 15 | json['data'] == null 16 | ? null 17 | : XVDData.fromJson(json['data'] as Map), 18 | ); 19 | 20 | Map _$XunfeiVoiceDictationToJson( 21 | XunfeiVoiceDictation instance) => 22 | { 23 | 'sid': instance.sid, 24 | 'code': instance.code, 25 | 'message': instance.message, 26 | 'data': instance.data?.toJson(), 27 | }; 28 | 29 | XVDData _$XVDDataFromJson(Map json) => XVDData( 30 | (json['status'] as num?)?.toInt(), 31 | json['result'] == null 32 | ? null 33 | : XVDDataResult.fromJson(json['result'] as Map), 34 | ); 35 | 36 | Map _$XVDDataToJson(XVDData instance) => { 37 | 'status': instance.status, 38 | 'result': instance.result?.toJson(), 39 | }; 40 | 41 | XVDDataResult _$XVDDataResultFromJson(Map json) => 42 | XVDDataResult( 43 | json['ls'] as bool?, 44 | (json['bg'] as num?)?.toInt(), 45 | (json['ed'] as num?)?.toInt(), 46 | (json['ws'] as List?) 47 | ?.map((e) => XVDDataResultWs.fromJson(e as Map)) 48 | .toList(), 49 | (json['sn'] as num?)?.toInt(), 50 | ); 51 | 52 | Map _$XVDDataResultToJson(XVDDataResult instance) => 53 | { 54 | 'sn': instance.sn, 55 | 'ls': instance.ls, 56 | 'bg': instance.bg, 57 | 'ed': instance.ed, 58 | 'ws': instance.ws?.map((e) => e.toJson()).toList(), 59 | }; 60 | 61 | XVDDataResultWs _$XVDDataResultWsFromJson(Map json) => 62 | XVDDataResultWs( 63 | (json['bg'] as num?)?.toInt(), 64 | (json['cw'] as List?) 65 | ?.map((e) => XVDDataResultWsCw.fromJson(e as Map)) 66 | .toList(), 67 | ); 68 | 69 | Map _$XVDDataResultWsToJson(XVDDataResultWs instance) => 70 | { 71 | 'bg': instance.bg, 72 | 'cw': instance.cw?.map((e) => e.toJson()).toList(), 73 | }; 74 | 75 | XVDDataResultWsCw _$XVDDataResultWsCwFromJson(Map json) => 76 | XVDDataResultWsCw( 77 | (json['sc'] as num?)?.toInt(), 78 | json['w'] as String?, 79 | ); 80 | 81 | Map _$XVDDataResultWsCwToJson(XVDDataResultWsCw instance) => 82 | { 83 | 'w': instance.w, 84 | 'sc': instance.sc, 85 | }; 86 | -------------------------------------------------------------------------------- /lib/models/life_tools/animal_lover/the_dog_cat_api_breed.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'the_dog_cat_api_breed.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | TheDogCatApiResp _$TheDogCatApiRespFromJson(Map json) => 10 | TheDogCatApiResp( 11 | json['id'] as String, 12 | json['url'] as String, 13 | (json['breeds'] as List) 14 | .map((e) => Breed.fromJson(e as Map)) 15 | .toList(), 16 | ); 17 | 18 | Map _$TheDogCatApiRespToJson(TheDogCatApiResp instance) => 19 | { 20 | 'id': instance.id, 21 | 'url': instance.url, 22 | 'breeds': instance.breeds.map((e) => e.toJson()).toList(), 23 | }; 24 | 25 | Breed _$BreedFromJson(Map json) => Breed( 26 | id: json['id'], 27 | name: json['name'] as String?, 28 | breedGroup: json['breed_group'] as String?, 29 | temperament: json['temperament'] as String?, 30 | origin: json['origin'] as String?, 31 | description: json['description'] as String?, 32 | lifeSpan: json['life_span'] as String?, 33 | altNames: json['alt_names'] as String?, 34 | wikipediaUrl: json['wikipedia_url'] as String?, 35 | referenceImageId: json['reference_image_id'] as String?, 36 | dataSource: json['data_source'] as String?, 37 | ); 38 | 39 | Map _$BreedToJson(Breed instance) => { 40 | 'id': instance.id, 41 | 'name': instance.name, 42 | 'breed_group': instance.breedGroup, 43 | 'temperament': instance.temperament, 44 | 'origin': instance.origin, 45 | 'description': instance.description, 46 | 'life_span': instance.lifeSpan, 47 | 'alt_names': instance.altNames, 48 | 'wikipedia_url': instance.wikipediaUrl, 49 | 'reference_image_id': instance.referenceImageId, 50 | 'data_source': instance.dataSource, 51 | }; 52 | -------------------------------------------------------------------------------- /lib/models/life_tools/animal_lover/the_dog_cat_api_image.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | part 'the_dog_cat_api_image.g.dart'; 4 | 5 | @JsonSerializable() 6 | class TheDogCatApiImage { 7 | @JsonKey(name: 'id') 8 | String id; 9 | 10 | @JsonKey(name: 'url') 11 | String url; 12 | 13 | @JsonKey(name: 'width') 14 | int? width; 15 | 16 | @JsonKey(name: 'height') 17 | int? height; 18 | 19 | TheDogCatApiImage( 20 | this.id, 21 | this.url, { 22 | this.width, 23 | this.height, 24 | }); 25 | 26 | factory TheDogCatApiImage.fromJson(Map srcJson) => 27 | _$TheDogCatApiImageFromJson(srcJson); 28 | 29 | Map toJson() => _$TheDogCatApiImageToJson(this); 30 | } 31 | -------------------------------------------------------------------------------- /lib/models/life_tools/animal_lover/the_dog_cat_api_image.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'the_dog_cat_api_image.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | TheDogCatApiImage _$TheDogCatApiImageFromJson(Map json) => 10 | TheDogCatApiImage( 11 | json['id'] as String, 12 | json['url'] as String, 13 | width: (json['width'] as num?)?.toInt(), 14 | height: (json['height'] as num?)?.toInt(), 15 | ); 16 | 17 | Map _$TheDogCatApiImageToJson(TheDogCatApiImage instance) => 18 | { 19 | 'id': instance.id, 20 | 'url': instance.url, 21 | 'width': instance.width, 22 | 'height': instance.height, 23 | }; 24 | -------------------------------------------------------------------------------- /lib/models/life_tools/dish_state.dart: -------------------------------------------------------------------------------- 1 | // 食物 2 | class Dish { 3 | String dishId; // 用uuid生成 4 | String dishName; // 名称 5 | String? description; // 一两句的介绍描述 6 | String? recipe; // 菜谱 7 | // 2024-03-22 输入菜谱可能太慢了,可以拍照片,但固定只能一张照片 8 | String? recipePicture; // 菜谱照片地址 9 | // 照片、类型、餐次一个食物可以对应多个 10 | String? photos; // 食物照片。实际照片缓存内部或者网页照片,这里是地址列表 11 | String? videos; // 食物视频。这里是地址,外部访问 12 | String? tags; // 食物类型,比如凉菜、汤菜、煎、炒、烹、炸、焖、溜、熬、炖、汆等 13 | String? mealCategories; // 早餐、午餐、下午茶、晚餐、夜宵、甜点 14 | 15 | Dish({ 16 | required this.dishId, 17 | required this.dishName, 18 | this.description, 19 | this.photos, 20 | this.videos, 21 | this.tags, 22 | this.mealCategories, 23 | this.recipe, 24 | this.recipePicture, 25 | }); 26 | 27 | Map toMap() { 28 | return { 29 | 'dish_id': dishId, 30 | 'dish_name': dishName, 31 | 'description': description, 32 | 'photos': photos, 33 | 'videos': videos, 34 | 'tags': tags, 35 | 'meal_categories': mealCategories, 36 | 'recipe': recipe, 37 | 'recipe_picture': recipePicture, 38 | }; 39 | } 40 | 41 | // 用于从数据库行映射到 ServingInfo 对象的 fromMap 方法 42 | factory Dish.fromMap(Map map) { 43 | return Dish( 44 | dishId: map['dish_id'] as String, 45 | dishName: map['dish_name'] as String, 46 | description: map['description'] as String?, 47 | photos: map['photos'] as String?, 48 | videos: map['videos'] as String?, 49 | tags: map['tags'] as String?, 50 | mealCategories: map['meal_categories'] as String?, 51 | recipe: map['recipe'] as String?, 52 | recipePicture: map['recipe_picture'] as String?, 53 | ); 54 | } 55 | 56 | @override 57 | String toString() { 58 | return ''' 59 | Food{ 60 | dishId: $dishId, dishName: $dishName, description:$description, 61 | photos: $photos, videos: $videos, recipePicture: $recipePicture, 62 | tags: $tags, mealCategories: $mealCategories, recipe: $recipe, 63 | } 64 | '''; 65 | } 66 | } 67 | 68 | /// json 文件转换时对应的类 69 | 70 | class JsonFileDish { 71 | String? dishId; // 用uuid生成 (就是上面的food,后面再改) 72 | String? dishName; // 名称 73 | String? description; // 一两句的介绍描述 74 | // 照片、类型、餐次一个食物可以对应多个 75 | String? tags; // 食物类型,比如凉菜、汤菜、煎、炒、烹、炸、焖、溜、熬、炖、汆等 76 | String? mealCategories; // 早餐、午餐、下午茶、晚餐、夜宵、甜点 77 | List? recipe; // 菜谱,用字符串数组装步骤了 78 | // 2024-03-22 输入菜谱可能太慢了,可以拍照片,但固定只能一张照片 79 | String? recipePicture; // 菜谱照片地址 80 | List? images; // 食物照片。照片地址也用字符串数组 81 | List? videos; // 食物视频。视频地址也用字符串数组 82 | 83 | JsonFileDish({ 84 | this.dishId, 85 | this.dishName, 86 | this.description, 87 | this.tags, 88 | this.mealCategories, 89 | this.recipe, 90 | this.recipePicture, 91 | this.images, 92 | this.videos, 93 | }); 94 | 95 | JsonFileDish.fromJson(Map json) { 96 | dishId = json['dish_id']; 97 | dishName = json['dish_name'] ?? ""; 98 | description = json['description']; 99 | tags = json['tags']; 100 | mealCategories = json['meal_categories']; 101 | recipe = json['recipe']?.cast(); 102 | recipePicture = json['recipe_picture']; 103 | images = json['images']?.cast(); 104 | videos = json['videos']?.cast(); 105 | } 106 | 107 | Map toJson() { 108 | final Map data = {}; 109 | data['dish_id'] = dishId; 110 | data['dish_name'] = dishName; 111 | data['description'] = description; 112 | data['tags'] = tags; 113 | data['meal_categories'] = mealCategories; 114 | data['recipe'] = recipe; 115 | data['recipe_picture'] = recipePicture; 116 | data['images'] = images; 117 | data['videos'] = videos; 118 | 119 | return data; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /lib/models/life_tools/dog_lover/dog_ceo_resp.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | part 'dog_ceo_resp.g.dart'; 4 | 5 | /// 6 | /// dog ceo 请求用get即可,所有端点的返回,都放在这里来 7 | /// 8 | /// https://github.com/ElliottLandsborough/dog-ceo-api 9 | /// 10 | @JsonSerializable(explicitToJson: true) 11 | class DogCeoResp { 12 | // 可能是List,比如查询主品种信息、自定数量的随机图片 13 | // 可能是List,比如查询所有品种带子品种信息,就是个二维数据 14 | // 可能是String,比如随机单个图片 15 | @JsonKey(name: 'message') 16 | dynamic message; 17 | 18 | @JsonKey(name: 'status') 19 | String status; 20 | 21 | DogCeoResp( 22 | this.message, 23 | this.status, 24 | ); 25 | 26 | factory DogCeoResp.fromJson(Map srcJson) => 27 | _$DogCeoRespFromJson(srcJson); 28 | 29 | Map toJson() => _$DogCeoRespToJson(this); 30 | } 31 | -------------------------------------------------------------------------------- /lib/models/life_tools/dog_lover/dog_ceo_resp.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'dog_ceo_resp.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | DogCeoResp _$DogCeoRespFromJson(Map json) => DogCeoResp( 10 | json['message'], 11 | json['status'] as String, 12 | ); 13 | 14 | Map _$DogCeoRespToJson(DogCeoResp instance) => 15 | { 16 | 'message': instance.message, 17 | 'status': instance.status, 18 | }; 19 | -------------------------------------------------------------------------------- /lib/models/life_tools/food/nutritionix/nix_natural_exercise_resp.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | import 'nix_natural_nutrient_resp.dart'; 4 | 5 | part 'nix_natural_exercise_resp.g.dart'; 6 | 7 | /// nutritionix 食物营养素请求的响应 8 | /// https://www.nutritionix.com/business/api 9 | /// 10 | /// 自然语言查询运动消耗信息 11 | /// 具体接口文档: 12 | /// https://docx.riversand.com/developers/docs/natural-language-for-nutrients 13 | /// 14 | /// 前缀缩写: Nix 15 | @JsonSerializable(explicitToJson: true) 16 | class NixNaturalExerciseResp { 17 | @JsonKey(name: 'exercises') 18 | List? exercises; 19 | 20 | NixNaturalExerciseResp({ 21 | this.exercises, 22 | }); 23 | 24 | factory NixNaturalExerciseResp.fromJson(Map srcJson) => 25 | _$NixNaturalExerciseRespFromJson(srcJson); 26 | 27 | Map toJson() => _$NixNaturalExerciseRespToJson(this); 28 | } 29 | 30 | @JsonSerializable(explicitToJson: true) 31 | class NixExercise { 32 | @JsonKey(name: 'tag_id') 33 | int? tagId; 34 | 35 | @JsonKey(name: 'user_input') 36 | String? userInput; 37 | 38 | @JsonKey(name: 'duration_min') 39 | int? durationMin; 40 | 41 | @JsonKey(name: 'met') 42 | double? met; 43 | 44 | @JsonKey(name: 'nf_calories') 45 | int? nfCalories; 46 | 47 | @JsonKey(name: 'photo') 48 | NixPhoto? photo; 49 | 50 | @JsonKey(name: 'compendium_code') 51 | int? compendiumCode; 52 | 53 | @JsonKey(name: 'name') 54 | String? name; 55 | 56 | @JsonKey(name: 'description') 57 | String? description; 58 | 59 | @JsonKey(name: 'benefits') 60 | String? benefits; 61 | 62 | NixExercise({ 63 | this.tagId, 64 | this.userInput, 65 | this.durationMin, 66 | this.met, 67 | this.nfCalories, 68 | this.photo, 69 | this.compendiumCode, 70 | this.name, 71 | this.description, 72 | this.benefits, 73 | }); 74 | 75 | factory NixExercise.fromJson(Map srcJson) => 76 | _$NixExerciseFromJson(srcJson); 77 | 78 | Map toJson() => _$NixExerciseToJson(this); 79 | } 80 | -------------------------------------------------------------------------------- /lib/models/life_tools/food/nutritionix/nix_natural_exercise_resp.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'nix_natural_exercise_resp.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | NixNaturalExerciseResp _$NixNaturalExerciseRespFromJson( 10 | Map json) => 11 | NixNaturalExerciseResp( 12 | exercises: (json['exercises'] as List?) 13 | ?.map((e) => NixExercise.fromJson(e as Map)) 14 | .toList(), 15 | ); 16 | 17 | Map _$NixNaturalExerciseRespToJson( 18 | NixNaturalExerciseResp instance) => 19 | { 20 | 'exercises': instance.exercises?.map((e) => e.toJson()).toList(), 21 | }; 22 | 23 | NixExercise _$NixExerciseFromJson(Map json) => NixExercise( 24 | tagId: (json['tag_id'] as num?)?.toInt(), 25 | userInput: json['user_input'] as String?, 26 | durationMin: (json['duration_min'] as num?)?.toInt(), 27 | met: (json['met'] as num?)?.toDouble(), 28 | nfCalories: (json['nf_calories'] as num?)?.toInt(), 29 | photo: json['photo'] == null 30 | ? null 31 | : NixPhoto.fromJson(json['photo'] as Map), 32 | compendiumCode: (json['compendium_code'] as num?)?.toInt(), 33 | name: json['name'] as String?, 34 | description: json['description'] as String?, 35 | benefits: json['benefits'] as String?, 36 | ); 37 | 38 | Map _$NixExerciseToJson(NixExercise instance) => 39 | { 40 | 'tag_id': instance.tagId, 41 | 'user_input': instance.userInput, 42 | 'duration_min': instance.durationMin, 43 | 'met': instance.met, 44 | 'nf_calories': instance.nfCalories, 45 | 'photo': instance.photo?.toJson(), 46 | 'compendium_code': instance.compendiumCode, 47 | 'name': instance.name, 48 | 'description': instance.description, 49 | 'benefits': instance.benefits, 50 | }; 51 | -------------------------------------------------------------------------------- /lib/models/life_tools/food/nutritionix/nix_search_instant_resp.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | import 'nix_natural_nutrient_resp.dart'; 4 | 5 | part 'nix_search_instant_resp.g.dart'; 6 | 7 | /// nutritionix 食物营养素请求的响应 8 | /// https://www.nutritionix.com/business/api 9 | /// 10 | /// 关键字查询食品概要信息 11 | /// 具体接口文档: 12 | /// https://docx.riversand.com/developers/docs/instant-endpoint 13 | /// 14 | /// 前缀缩写: Nix 15 | /// 16 | @JsonSerializable(explicitToJson: true) 17 | class NixSearchInstantResp { 18 | @JsonKey(name: 'common') 19 | List? common; 20 | 21 | @JsonKey(name: 'branded') 22 | List? branded; 23 | 24 | NixSearchInstantResp({ 25 | this.common, 26 | this.branded, 27 | }); 28 | 29 | factory NixSearchInstantResp.fromJson(Map srcJson) => 30 | _$NixSearchInstantRespFromJson(srcJson); 31 | 32 | Map toJson() => _$NixSearchInstantRespToJson(this); 33 | } 34 | 35 | @JsonSerializable(explicitToJson: true) 36 | class NixCommon { 37 | @JsonKey(name: 'food_name') 38 | String? foodName; 39 | 40 | @JsonKey(name: 'serving_unit') 41 | String? servingUnit; 42 | 43 | @JsonKey(name: 'tag_name') 44 | String? tagName; 45 | 46 | @JsonKey(name: 'serving_qty') 47 | int? servingQty; 48 | 49 | @JsonKey(name: 'common_type') 50 | dynamic commonType; 51 | 52 | @JsonKey(name: 'tag_id') 53 | String? tagId; 54 | 55 | @JsonKey(name: 'photo') 56 | NixPhoto? photo; 57 | 58 | @JsonKey(name: 'locale') 59 | String? locale; 60 | 61 | NixCommon( 62 | this.foodName, 63 | this.servingUnit, 64 | this.tagName, 65 | this.servingQty, 66 | this.commonType, 67 | this.tagId, 68 | this.photo, 69 | this.locale, 70 | ); 71 | 72 | factory NixCommon.fromJson(Map srcJson) => 73 | _$NixCommonFromJson(srcJson); 74 | 75 | Map toJson() => _$NixCommonToJson(this); 76 | } 77 | 78 | @JsonSerializable(explicitToJson: true) 79 | class NixBranded { 80 | @JsonKey(name: 'food_name') 81 | String? foodName; 82 | 83 | @JsonKey(name: 'serving_unit') 84 | String? servingUnit; 85 | 86 | @JsonKey(name: 'nix_brand_id') 87 | String? nixBrandId; 88 | 89 | @JsonKey(name: 'brand_name_item_name') 90 | String? brandNameItemName; 91 | 92 | @JsonKey(name: 'serving_qty') 93 | int? servingQty; 94 | 95 | @JsonKey(name: 'nf_calories') 96 | int? nfCalories; 97 | 98 | @JsonKey(name: 'photo') 99 | NixPhoto? photo; 100 | 101 | @JsonKey(name: 'brand_name') 102 | String? brandName; 103 | 104 | @JsonKey(name: 'region') 105 | int? region; 106 | 107 | @JsonKey(name: 'brand_type') 108 | int? brandType; 109 | 110 | @JsonKey(name: 'nix_item_id') 111 | String? nixItemId; 112 | 113 | @JsonKey(name: 'locale') 114 | String? locale; 115 | 116 | NixBranded({ 117 | this.foodName, 118 | this.servingUnit, 119 | this.nixBrandId, 120 | this.brandNameItemName, 121 | this.servingQty, 122 | this.nfCalories, 123 | this.photo, 124 | this.brandName, 125 | this.region, 126 | this.brandType, 127 | this.nixItemId, 128 | this.locale, 129 | }); 130 | 131 | factory NixBranded.fromJson(Map srcJson) => 132 | _$NixBrandedFromJson(srcJson); 133 | 134 | Map toJson() => _$NixBrandedToJson(this); 135 | } 136 | -------------------------------------------------------------------------------- /lib/models/life_tools/food/nutritionix/nix_search_instant_resp.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'nix_search_instant_resp.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | NixSearchInstantResp _$NixSearchInstantRespFromJson( 10 | Map json) => 11 | NixSearchInstantResp( 12 | common: (json['common'] as List?) 13 | ?.map((e) => NixCommon.fromJson(e as Map)) 14 | .toList(), 15 | branded: (json['branded'] as List?) 16 | ?.map((e) => NixBranded.fromJson(e as Map)) 17 | .toList(), 18 | ); 19 | 20 | Map _$NixSearchInstantRespToJson( 21 | NixSearchInstantResp instance) => 22 | { 23 | 'common': instance.common?.map((e) => e.toJson()).toList(), 24 | 'branded': instance.branded?.map((e) => e.toJson()).toList(), 25 | }; 26 | 27 | NixCommon _$NixCommonFromJson(Map json) => NixCommon( 28 | json['food_name'] as String?, 29 | json['serving_unit'] as String?, 30 | json['tag_name'] as String?, 31 | (json['serving_qty'] as num?)?.toInt(), 32 | json['common_type'], 33 | json['tag_id'] as String?, 34 | json['photo'] == null 35 | ? null 36 | : NixPhoto.fromJson(json['photo'] as Map), 37 | json['locale'] as String?, 38 | ); 39 | 40 | Map _$NixCommonToJson(NixCommon instance) => { 41 | 'food_name': instance.foodName, 42 | 'serving_unit': instance.servingUnit, 43 | 'tag_name': instance.tagName, 44 | 'serving_qty': instance.servingQty, 45 | 'common_type': instance.commonType, 46 | 'tag_id': instance.tagId, 47 | 'photo': instance.photo?.toJson(), 48 | 'locale': instance.locale, 49 | }; 50 | 51 | NixBranded _$NixBrandedFromJson(Map json) => NixBranded( 52 | foodName: json['food_name'] as String?, 53 | servingUnit: json['serving_unit'] as String?, 54 | nixBrandId: json['nix_brand_id'] as String?, 55 | brandNameItemName: json['brand_name_item_name'] as String?, 56 | servingQty: (json['serving_qty'] as num?)?.toInt(), 57 | nfCalories: (json['nf_calories'] as num?)?.toInt(), 58 | photo: json['photo'] == null 59 | ? null 60 | : NixPhoto.fromJson(json['photo'] as Map), 61 | brandName: json['brand_name'] as String?, 62 | region: (json['region'] as num?)?.toInt(), 63 | brandType: (json['brand_type'] as num?)?.toInt(), 64 | nixItemId: json['nix_item_id'] as String?, 65 | locale: json['locale'] as String?, 66 | ); 67 | 68 | Map _$NixBrandedToJson(NixBranded instance) => 69 | { 70 | 'food_name': instance.foodName, 71 | 'serving_unit': instance.servingUnit, 72 | 'nix_brand_id': instance.nixBrandId, 73 | 'brand_name_item_name': instance.brandNameItemName, 74 | 'serving_qty': instance.servingQty, 75 | 'nf_calories': instance.nfCalories, 76 | 'photo': instance.photo?.toJson(), 77 | 'brand_name': instance.brandName, 78 | 'region': instance.region, 79 | 'brand_type': instance.brandType, 80 | 'nix_item_id': instance.nixItemId, 81 | 'locale': instance.locale, 82 | }; 83 | -------------------------------------------------------------------------------- /lib/models/life_tools/food/usda_food_data/usda_food_search_resp.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'usda_food_search_resp.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | USDASearchResultResp _$USDASearchResultRespFromJson( 10 | Map json) => 11 | USDASearchResultResp( 12 | (json['totalHits'] as num).toInt(), 13 | (json['currentPage'] as num).toInt(), 14 | (json['totalPages'] as num).toInt(), 15 | (json['pageList'] as List) 16 | .map((e) => (e as num).toInt()) 17 | .toList(), 18 | USDAFoodSearchCriteria.fromJson( 19 | json['foodSearchCriteria'] as Map), 20 | (json['foods'] as List) 21 | .map((e) => USDAFoodItem.fromJson(e as Map)) 22 | .toList(), 23 | USDAAggregation.fromJson(json['aggregations'] as Map), 24 | error: json['error'] == null 25 | ? null 26 | : USDAError.fromJson(json['error'] as Map), 27 | ); 28 | 29 | Map _$USDASearchResultRespToJson( 30 | USDASearchResultResp instance) => 31 | { 32 | 'totalHits': instance.totalHits, 33 | 'currentPage': instance.currentPage, 34 | 'totalPages': instance.totalPages, 35 | 'pageList': instance.pageList, 36 | 'foodSearchCriteria': instance.foodSearchCriteria.toJson(), 37 | 'foods': instance.foods.map((e) => e.toJson()).toList(), 38 | 'aggregations': instance.aggregations.toJson(), 39 | 'error': instance.error?.toJson(), 40 | }; 41 | 42 | USDAFoodSearchCriteria _$USDAFoodSearchCriteriaFromJson( 43 | Map json) => 44 | USDAFoodSearchCriteria( 45 | query: json['query'] as String?, 46 | generalSearchInput: json['generalSearchInput'] as String?, 47 | pageNumber: (json['pageNumber'] as num?)?.toInt(), 48 | numberOfResultsPerPage: (json['numberOfResultsPerPage'] as num?)?.toInt(), 49 | pageSize: (json['pageSize'] as num?)?.toInt(), 50 | requireAllWords: json['requireAllWords'] as bool?, 51 | ); 52 | 53 | Map _$USDAFoodSearchCriteriaToJson( 54 | USDAFoodSearchCriteria instance) => 55 | { 56 | 'query': instance.query, 57 | 'generalSearchInput': instance.generalSearchInput, 58 | 'pageNumber': instance.pageNumber, 59 | 'numberOfResultsPerPage': instance.numberOfResultsPerPage, 60 | 'pageSize': instance.pageSize, 61 | 'requireAllWords': instance.requireAllWords, 62 | }; 63 | 64 | USDAAggregation _$USDAAggregationFromJson(Map json) => 65 | USDAAggregation( 66 | Map.from(json['dataType'] as Map), 67 | json['nutrients'] as Map, 68 | ); 69 | 70 | Map _$USDAAggregationToJson(USDAAggregation instance) => 71 | { 72 | 'dataType': instance.dataType, 73 | 'nutrients': instance.nutrients, 74 | }; 75 | 76 | USDAError _$USDAErrorFromJson(Map json) => USDAError( 77 | json['code'] as String, 78 | json['message'] as String, 79 | ); 80 | 81 | Map _$USDAErrorToJson(USDAError instance) => { 82 | 'code': instance.code, 83 | 'message': instance.message, 84 | }; 85 | -------------------------------------------------------------------------------- /lib/models/life_tools/free_dictionary/free_dictionary_resp.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'free_dictionary_resp.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | FreeDictionaryItem _$FreeDictionaryItemFromJson(Map json) => 10 | FreeDictionaryItem( 11 | word: json['word'] as String?, 12 | phonetics: (json['phonetics'] as List?) 13 | ?.map((e) => FDPhonetic.fromJson(e as Map)) 14 | .toList(), 15 | meanings: (json['meanings'] as List?) 16 | ?.map((e) => FDMeaning.fromJson(e as Map)) 17 | .toList(), 18 | license: json['license'] == null 19 | ? null 20 | : FDLicense.fromJson(json['license'] as Map), 21 | sourceUrls: (json['sourceUrls'] as List?) 22 | ?.map((e) => e as String) 23 | .toList(), 24 | title: json['title'] as String?, 25 | message: json['message'] as String?, 26 | resolution: json['resolution'] as String?, 27 | ); 28 | 29 | Map _$FreeDictionaryItemToJson(FreeDictionaryItem instance) => 30 | { 31 | 'word': instance.word, 32 | 'phonetics': instance.phonetics?.map((e) => e.toJson()).toList(), 33 | 'meanings': instance.meanings?.map((e) => e.toJson()).toList(), 34 | 'license': instance.license?.toJson(), 35 | 'sourceUrls': instance.sourceUrls, 36 | 'title': instance.title, 37 | 'message': instance.message, 38 | 'resolution': instance.resolution, 39 | }; 40 | 41 | FDPhonetic _$FDPhoneticFromJson(Map json) => FDPhonetic( 42 | text: json['text'] as String?, 43 | audio: json['audio'] as String?, 44 | sourceUrl: json['sourceUrl'] as String?, 45 | license: json['license'] == null 46 | ? null 47 | : FDLicense.fromJson(json['license'] as Map), 48 | ); 49 | 50 | Map _$FDPhoneticToJson(FDPhonetic instance) => 51 | { 52 | 'text': instance.text, 53 | 'audio': instance.audio, 54 | 'sourceUrl': instance.sourceUrl, 55 | 'license': instance.license?.toJson(), 56 | }; 57 | 58 | FDLicense _$FDLicenseFromJson(Map json) => FDLicense( 59 | name: json['name'] as String?, 60 | url: json['url'] as String?, 61 | ); 62 | 63 | Map _$FDLicenseToJson(FDLicense instance) => { 64 | 'name': instance.name, 65 | 'url': instance.url, 66 | }; 67 | 68 | FDMeaning _$FDMeaningFromJson(Map json) => FDMeaning( 69 | json['partOfSpeech'] as String?, 70 | (json['definitions'] as List?) 71 | ?.map((e) => FDDefinition.fromJson(e as Map)) 72 | .toList(), 73 | (json['synonyms'] as List?)?.map((e) => e as String).toList(), 74 | json['antonyms'] as List?, 75 | ); 76 | 77 | Map _$FDMeaningToJson(FDMeaning instance) => { 78 | 'partOfSpeech': instance.partOfSpeech, 79 | 'definitions': instance.definitions?.map((e) => e.toJson()).toList(), 80 | 'synonyms': instance.synonyms, 81 | 'antonyms': instance.antonyms, 82 | }; 83 | 84 | FDDefinition _$FDDefinitionFromJson(Map json) => FDDefinition( 85 | definition: json['definition'] as String?, 86 | synonyms: json['synonyms'] as List?, 87 | antonyms: json['antonyms'] as List?, 88 | example: json['example'] as String?, 89 | ); 90 | 91 | Map _$FDDefinitionToJson(FDDefinition instance) => 92 | { 93 | 'definition': instance.definition, 94 | 'synonyms': instance.synonyms, 95 | 'antonyms': instance.antonyms, 96 | 'example': instance.example, 97 | }; 98 | -------------------------------------------------------------------------------- /lib/models/life_tools/hitokoto/hitokoto.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:json_annotation/json_annotation.dart'; 4 | 5 | part 'hitokoto.g.dart'; 6 | 7 | /// 一言的API响应 8 | /// https://developer.hitokoto.cn/sentence/ 9 | /// 语句库数据来源:https://github.com/hitokoto-osc/sentences-bundle 10 | /// 11 | @JsonSerializable(explicitToJson: true) 12 | class Hitokoto { 13 | @JsonKey(name: 'id') 14 | int? id; 15 | 16 | @JsonKey(name: 'uuid') 17 | String? uuid; 18 | 19 | @JsonKey(name: 'hitokoto') 20 | String? hitokoto; 21 | 22 | @JsonKey(name: 'type') 23 | String? type; 24 | 25 | @JsonKey(name: 'from') 26 | String? from; 27 | 28 | @JsonKey(name: 'from_who') 29 | String? fromWho; 30 | 31 | @JsonKey(name: 'creator') 32 | String? creator; 33 | 34 | @JsonKey(name: 'creator_uid') 35 | int? creatorUid; 36 | 37 | @JsonKey(name: 'reviewer') 38 | int? reviewer; 39 | 40 | @JsonKey(name: 'commit_from') 41 | String? commitFrom; 42 | 43 | @JsonKey(name: 'created_at') 44 | String? createdAt; 45 | 46 | @JsonKey(name: 'length') 47 | int? length; 48 | 49 | Hitokoto({ 50 | this.id, 51 | this.uuid, 52 | this.hitokoto, 53 | this.type, 54 | this.from, 55 | this.fromWho, 56 | this.creator, 57 | this.creatorUid, 58 | this.reviewer, 59 | this.commitFrom, 60 | this.createdAt, 61 | this.length, 62 | }); 63 | 64 | factory Hitokoto.fromRawJson(String str) => 65 | Hitokoto.fromJson(json.decode(str)); 66 | 67 | String toRawJson() => json.encode(toJson()); 68 | 69 | factory Hitokoto.fromJson(Map srcJson) => 70 | _$HitokotoFromJson(srcJson); 71 | 72 | Map toJson() => _$HitokotoToJson(this); 73 | } 74 | -------------------------------------------------------------------------------- /lib/models/life_tools/hitokoto/hitokoto.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'hitokoto.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | Hitokoto _$HitokotoFromJson(Map json) => Hitokoto( 10 | id: (json['id'] as num?)?.toInt(), 11 | uuid: json['uuid'] as String?, 12 | hitokoto: json['hitokoto'] as String?, 13 | type: json['type'] as String?, 14 | from: json['from'] as String?, 15 | fromWho: json['from_who'] as String?, 16 | creator: json['creator'] as String?, 17 | creatorUid: (json['creator_uid'] as num?)?.toInt(), 18 | reviewer: (json['reviewer'] as num?)?.toInt(), 19 | commitFrom: json['commit_from'] as String?, 20 | createdAt: json['created_at'] as String?, 21 | length: (json['length'] as num?)?.toInt(), 22 | ); 23 | 24 | Map _$HitokotoToJson(Hitokoto instance) => { 25 | 'id': instance.id, 26 | 'uuid': instance.uuid, 27 | 'hitokoto': instance.hitokoto, 28 | 'type': instance.type, 29 | 'from': instance.from, 30 | 'from_who': instance.fromWho, 31 | 'creator': instance.creator, 32 | 'creator_uid': instance.creatorUid, 33 | 'reviewer': instance.reviewer, 34 | 'commit_from': instance.commitFrom, 35 | 'created_at': instance.createdAt, 36 | 'length': instance.length, 37 | }; 38 | -------------------------------------------------------------------------------- /lib/models/life_tools/jikan/jikan_related_character_resp.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:json_annotation/json_annotation.dart'; 4 | 5 | import 'jikan_data.dart'; 6 | 7 | part 'jikan_related_character_resp.g.dart'; 8 | 9 | /// 10 | /// 动漫关联的角色、漫画关联的角色 11 | /// getAnimeCharacters 12 | /// getMangaCharacters 13 | /// 两者类似,前者有额外的favorites和voice_actors 14 | /// 15 | @JsonSerializable(explicitToJson: true) 16 | class JikanRelatedCharacterResp { 17 | /// 报错的相关栏位没有加 18 | 19 | @JsonKey(name: 'data') 20 | List data; 21 | 22 | JikanRelatedCharacterResp( 23 | this.data, 24 | ); 25 | 26 | // 从字符串转 27 | factory JikanRelatedCharacterResp.fromRawJson(String str) => 28 | JikanRelatedCharacterResp.fromJson(json.decode(str)); 29 | // 转为字符串 30 | String toRawJson() => json.encode(toJson()); 31 | 32 | factory JikanRelatedCharacterResp.fromJson(Map srcJson) => 33 | _$JikanRelatedCharacterRespFromJson(srcJson); 34 | 35 | Map toJson() => _$JikanRelatedCharacterRespToJson(this); 36 | } 37 | 38 | @JsonSerializable(explicitToJson: true) 39 | class JKRelatedCharacter { 40 | // 结果好像只保留了mail_id url images name 栏位, 41 | @JsonKey(name: 'character') 42 | JKData? character; 43 | 44 | @JsonKey(name: 'role') 45 | String? role; 46 | 47 | @JsonKey(name: 'favorites') 48 | int? favorites; 49 | 50 | @JsonKey(name: 'voice_actors') 51 | List? voiceActors; 52 | 53 | JKRelatedCharacter({ 54 | this.character, 55 | this.role, 56 | this.favorites, 57 | this.voiceActors, 58 | }); 59 | 60 | // 从字符串转 61 | factory JKRelatedCharacter.fromRawJson(String str) => 62 | JKRelatedCharacter.fromJson(json.decode(str)); 63 | // 转为字符串 64 | String toRawJson() => json.encode(toJson()); 65 | 66 | factory JKRelatedCharacter.fromJson(Map srcJson) => 67 | _$JKRelatedCharacterFromJson(srcJson); 68 | 69 | Map toJson() => _$JKRelatedCharacterToJson(this); 70 | } 71 | 72 | @JsonSerializable(explicitToJson: true) 73 | class JKVoiceActor { 74 | // 结果好像只保留了mail_id url images name 栏位, 75 | @JsonKey(name: 'person') 76 | JKData? person; 77 | 78 | @JsonKey(name: 'language') 79 | String? language; 80 | 81 | JKVoiceActor({ 82 | this.person, 83 | this.language, 84 | }); 85 | 86 | // 从字符串转 87 | factory JKVoiceActor.fromRawJson(String str) => 88 | JKVoiceActor.fromJson(json.decode(str)); 89 | // 转为字符串 90 | String toRawJson() => json.encode(toJson()); 91 | 92 | factory JKVoiceActor.fromJson(Map srcJson) => 93 | _$JKVoiceActorFromJson(srcJson); 94 | 95 | Map toJson() => _$JKVoiceActorToJson(this); 96 | } 97 | -------------------------------------------------------------------------------- /lib/models/life_tools/jikan/jikan_related_character_resp.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'jikan_related_character_resp.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | JikanRelatedCharacterResp _$JikanRelatedCharacterRespFromJson( 10 | Map json) => 11 | JikanRelatedCharacterResp( 12 | (json['data'] as List) 13 | .map((e) => JKRelatedCharacter.fromJson(e as Map)) 14 | .toList(), 15 | ); 16 | 17 | Map _$JikanRelatedCharacterRespToJson( 18 | JikanRelatedCharacterResp instance) => 19 | { 20 | 'data': instance.data.map((e) => e.toJson()).toList(), 21 | }; 22 | 23 | JKRelatedCharacter _$JKRelatedCharacterFromJson(Map json) => 24 | JKRelatedCharacter( 25 | character: json['character'] == null 26 | ? null 27 | : JKData.fromJson(json['character'] as Map), 28 | role: json['role'] as String?, 29 | favorites: (json['favorites'] as num?)?.toInt(), 30 | voiceActors: (json['voice_actors'] as List?) 31 | ?.map((e) => JKVoiceActor.fromJson(e as Map)) 32 | .toList(), 33 | ); 34 | 35 | Map _$JKRelatedCharacterToJson(JKRelatedCharacter instance) => 36 | { 37 | 'character': instance.character?.toJson(), 38 | 'role': instance.role, 39 | 'favorites': instance.favorites, 40 | 'voice_actors': instance.voiceActors?.map((e) => e.toJson()).toList(), 41 | }; 42 | 43 | JKVoiceActor _$JKVoiceActorFromJson(Map json) => JKVoiceActor( 44 | person: json['person'] == null 45 | ? null 46 | : JKData.fromJson(json['person'] as Map), 47 | language: json['language'] as String?, 48 | ); 49 | 50 | Map _$JKVoiceActorToJson(JKVoiceActor instance) => 51 | { 52 | 'person': instance.person?.toJson(), 53 | 'language': instance.language, 54 | }; 55 | -------------------------------------------------------------------------------- /lib/models/life_tools/jikan/jikan_statistic.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:json_annotation/json_annotation.dart'; 4 | 5 | part 'jikan_statistic.g.dart'; 6 | 7 | /// 8 | /// 非官方的 MyAnimeList(MAL) API 指定动漫ID返回评分构成的数据结构 9 | /// 10 | /// GET https://api.jikan.moe/v4/anime/{id}/statistics 11 | /// 12 | @JsonSerializable(explicitToJson: true) 13 | class JikanStatistic { 14 | @JsonKey(name: 'data') 15 | JikanStatisticData data; 16 | 17 | JikanStatistic( 18 | this.data, 19 | ); 20 | 21 | // 从字符串转 22 | factory JikanStatistic.fromRawJson(String str) => 23 | JikanStatistic.fromJson(json.decode(str)); 24 | // 转为字符串 25 | String toRawJson() => json.encode(toJson()); 26 | 27 | factory JikanStatistic.fromJson(Map srcJson) => 28 | _$JikanStatisticFromJson(srcJson); 29 | 30 | Map toJson() => _$JikanStatisticToJson(this); 31 | } 32 | 33 | @JsonSerializable(explicitToJson: true) 34 | class JikanStatisticData { 35 | // 动漫统计和漫画统计稍微不同,动画时watch,漫画时reading 36 | @JsonKey(name: 'watching') 37 | int? watching; 38 | 39 | @JsonKey(name: 'plan_to_watch') 40 | int? planToWatch; 41 | 42 | @JsonKey(name: 'reading') 43 | int? reading; 44 | 45 | @JsonKey(name: 'plan_to_read') 46 | int? planToRead; 47 | 48 | @JsonKey(name: 'completed') 49 | int? completed; 50 | 51 | @JsonKey(name: 'on_hold') 52 | int? onHold; 53 | 54 | @JsonKey(name: 'dropped') 55 | int? dropped; 56 | 57 | @JsonKey(name: 'total') 58 | int? total; 59 | 60 | @JsonKey(name: 'scores') 61 | List? scores; 62 | 63 | JikanStatisticData({ 64 | this.watching, 65 | this.planToWatch, 66 | this.reading, 67 | this.planToRead, 68 | this.completed, 69 | this.onHold, 70 | this.dropped, 71 | this.total, 72 | this.scores, 73 | }); 74 | 75 | // 从字符串转 76 | factory JikanStatisticData.fromRawJson(String str) => 77 | JikanStatisticData.fromJson(json.decode(str)); 78 | // 转为字符串 79 | String toRawJson() => json.encode(toJson()); 80 | 81 | factory JikanStatisticData.fromJson(Map srcJson) => 82 | _$JikanStatisticDataFromJson(srcJson); 83 | 84 | Map toJson() => _$JikanStatisticDataToJson(this); 85 | } 86 | 87 | @JsonSerializable(explicitToJson: true) 88 | class JikanStatisticScore { 89 | @JsonKey(name: 'score') 90 | int score; 91 | 92 | @JsonKey(name: 'votes') 93 | int votes; 94 | 95 | @JsonKey(name: 'percentage') 96 | double percentage; 97 | 98 | JikanStatisticScore( 99 | this.score, 100 | this.votes, 101 | this.percentage, 102 | ); 103 | 104 | // 从字符串转 105 | factory JikanStatisticScore.fromRawJson(String str) => 106 | JikanStatisticScore.fromJson(json.decode(str)); 107 | // 转为字符串 108 | String toRawJson() => json.encode(toJson()); 109 | 110 | factory JikanStatisticScore.fromJson(Map srcJson) => 111 | _$JikanStatisticScoreFromJson(srcJson); 112 | 113 | Map toJson() => _$JikanStatisticScoreToJson(this); 114 | } 115 | -------------------------------------------------------------------------------- /lib/models/life_tools/jikan/jikan_statistic.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'jikan_statistic.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | JikanStatistic _$JikanStatisticFromJson(Map json) => 10 | JikanStatistic( 11 | JikanStatisticData.fromJson(json['data'] as Map), 12 | ); 13 | 14 | Map _$JikanStatisticToJson(JikanStatistic instance) => 15 | { 16 | 'data': instance.data.toJson(), 17 | }; 18 | 19 | JikanStatisticData _$JikanStatisticDataFromJson(Map json) => 20 | JikanStatisticData( 21 | watching: (json['watching'] as num?)?.toInt(), 22 | planToWatch: (json['plan_to_watch'] as num?)?.toInt(), 23 | reading: (json['reading'] as num?)?.toInt(), 24 | planToRead: (json['plan_to_read'] as num?)?.toInt(), 25 | completed: (json['completed'] as num?)?.toInt(), 26 | onHold: (json['on_hold'] as num?)?.toInt(), 27 | dropped: (json['dropped'] as num?)?.toInt(), 28 | total: (json['total'] as num?)?.toInt(), 29 | scores: (json['scores'] as List?) 30 | ?.map((e) => JikanStatisticScore.fromJson(e as Map)) 31 | .toList(), 32 | ); 33 | 34 | Map _$JikanStatisticDataToJson(JikanStatisticData instance) => 35 | { 36 | 'watching': instance.watching, 37 | 'plan_to_watch': instance.planToWatch, 38 | 'reading': instance.reading, 39 | 'plan_to_read': instance.planToRead, 40 | 'completed': instance.completed, 41 | 'on_hold': instance.onHold, 42 | 'dropped': instance.dropped, 43 | 'total': instance.total, 44 | 'scores': instance.scores?.map((e) => e.toJson()).toList(), 45 | }; 46 | 47 | JikanStatisticScore _$JikanStatisticScoreFromJson(Map json) => 48 | JikanStatisticScore( 49 | (json['score'] as num).toInt(), 50 | (json['votes'] as num).toInt(), 51 | (json['percentage'] as num).toDouble(), 52 | ); 53 | 54 | Map _$JikanStatisticScoreToJson( 55 | JikanStatisticScore instance) => 56 | { 57 | 'score': instance.score, 58 | 'votes': instance.votes, 59 | 'percentage': instance.percentage, 60 | }; 61 | -------------------------------------------------------------------------------- /lib/models/life_tools/news/news_api_resp.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:json_annotation/json_annotation.dart'; 4 | 5 | part 'news_api_resp.g.dart'; 6 | 7 | /// 8 | /// newsapi 返回的数据,国内不可访问 9 | /// https://newsapi.org/docs 10 | /// 11 | @JsonSerializable(explicitToJson: true) 12 | class NewsApiResp { 13 | @JsonKey(name: 'status') 14 | String status; 15 | 16 | @JsonKey(name: 'totalResults') 17 | int? totalResults; 18 | 19 | @JsonKey(name: 'articles') 20 | List? articles; 21 | 22 | @JsonKey(name: 'code') 23 | String? code; 24 | 25 | @JsonKey(name: 'message') 26 | String? message; 27 | 28 | NewsApiResp({ 29 | required this.status, 30 | this.totalResults, 31 | this.articles, 32 | this.code, 33 | this.message, 34 | }); 35 | 36 | factory NewsApiResp.fromRawJson(String str) => 37 | NewsApiResp.fromJson(json.decode(str)); 38 | 39 | String toRawJson() => json.encode(toJson()); 40 | 41 | factory NewsApiResp.fromJson(Map srcJson) => 42 | _$NewsApiRespFromJson(srcJson); 43 | 44 | Map toJson() => _$NewsApiRespToJson(this); 45 | } 46 | 47 | @JsonSerializable(explicitToJson: true) 48 | class NewsApiArticle { 49 | @JsonKey(name: 'source') 50 | NewsApiSource? source; 51 | 52 | @JsonKey(name: 'author') 53 | String? author; 54 | 55 | @JsonKey(name: 'title') 56 | String? title; 57 | 58 | @JsonKey(name: 'description') 59 | String? description; 60 | 61 | @JsonKey(name: 'url') 62 | String? url; 63 | 64 | @JsonKey(name: 'urlToImage') 65 | String? urlToImage; 66 | 67 | @JsonKey(name: 'publishedAt') 68 | String? publishedAt; 69 | 70 | @JsonKey(name: 'content') 71 | String? content; 72 | 73 | NewsApiArticle({ 74 | this.source, 75 | this.author, 76 | this.title, 77 | this.description, 78 | this.url, 79 | this.urlToImage, 80 | this.publishedAt, 81 | this.content, 82 | }); 83 | 84 | factory NewsApiArticle.fromRawJson(String str) => 85 | NewsApiArticle.fromJson(json.decode(str)); 86 | 87 | String toRawJson() => json.encode(toJson()); 88 | 89 | factory NewsApiArticle.fromJson(Map srcJson) => 90 | _$NewsApiArticleFromJson(srcJson); 91 | 92 | Map toJson() => _$NewsApiArticleToJson(this); 93 | } 94 | 95 | @JsonSerializable(explicitToJson: true) 96 | class NewsApiSource { 97 | @JsonKey(name: 'id') 98 | String? id; 99 | 100 | @JsonKey(name: 'name') 101 | String? name; 102 | 103 | NewsApiSource({ 104 | this.id, 105 | this.name, 106 | }); 107 | 108 | factory NewsApiSource.fromRawJson(String str) => 109 | NewsApiSource.fromJson(json.decode(str)); 110 | 111 | String toRawJson() => json.encode(toJson()); 112 | 113 | factory NewsApiSource.fromJson(Map srcJson) => 114 | _$NewsApiSourceFromJson(srcJson); 115 | 116 | Map toJson() => _$NewsApiSourceToJson(this); 117 | } 118 | -------------------------------------------------------------------------------- /lib/models/life_tools/news/news_api_resp.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'news_api_resp.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | NewsApiResp _$NewsApiRespFromJson(Map json) => NewsApiResp( 10 | status: json['status'] as String, 11 | totalResults: (json['totalResults'] as num?)?.toInt(), 12 | articles: (json['articles'] as List?) 13 | ?.map((e) => NewsApiArticle.fromJson(e as Map)) 14 | .toList(), 15 | code: json['code'] as String?, 16 | message: json['message'] as String?, 17 | ); 18 | 19 | Map _$NewsApiRespToJson(NewsApiResp instance) => 20 | { 21 | 'status': instance.status, 22 | 'totalResults': instance.totalResults, 23 | 'articles': instance.articles?.map((e) => e.toJson()).toList(), 24 | 'code': instance.code, 25 | 'message': instance.message, 26 | }; 27 | 28 | NewsApiArticle _$NewsApiArticleFromJson(Map json) => 29 | NewsApiArticle( 30 | source: json['source'] == null 31 | ? null 32 | : NewsApiSource.fromJson(json['source'] as Map), 33 | author: json['author'] as String?, 34 | title: json['title'] as String?, 35 | description: json['description'] as String?, 36 | url: json['url'] as String?, 37 | urlToImage: json['urlToImage'] as String?, 38 | publishedAt: json['publishedAt'] as String?, 39 | content: json['content'] as String?, 40 | ); 41 | 42 | Map _$NewsApiArticleToJson(NewsApiArticle instance) => 43 | { 44 | 'source': instance.source?.toJson(), 45 | 'author': instance.author, 46 | 'title': instance.title, 47 | 'description': instance.description, 48 | 'url': instance.url, 49 | 'urlToImage': instance.urlToImage, 50 | 'publishedAt': instance.publishedAt, 51 | 'content': instance.content, 52 | }; 53 | 54 | NewsApiSource _$NewsApiSourceFromJson(Map json) => 55 | NewsApiSource( 56 | id: json['id'] as String?, 57 | name: json['name'] as String?, 58 | ); 59 | 60 | Map _$NewsApiSourceToJson(NewsApiSource instance) => 61 | { 62 | 'id': instance.id, 63 | 'name': instance.name, 64 | }; 65 | -------------------------------------------------------------------------------- /lib/models/life_tools/news/sina_roll_news_resp.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'sina_roll_news_resp.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | SinaRollNewsResp _$SinaRollNewsRespFromJson(Map json) => 10 | SinaRollNewsResp( 11 | status: json['status'] == null 12 | ? null 13 | : SinaRNStatus.fromJson(json['status'] as Map), 14 | timestamp: json['timestamp'] as String?, 15 | total: (json['total'] as num?)?.toInt(), 16 | lid: (json['lid'] as num?)?.toInt(), 17 | rtime: (json['rtime'] as num?)?.toInt(), 18 | data: (json['data'] as List?) 19 | ?.map((e) => SinaRollNews.fromJson(e as Map)) 20 | .toList(), 21 | ); 22 | 23 | Map _$SinaRollNewsRespToJson(SinaRollNewsResp instance) => 24 | { 25 | 'status': instance.status?.toJson(), 26 | 'timestamp': instance.timestamp, 27 | 'rtime': instance.rtime, 28 | 'total': instance.total, 29 | 'lid': instance.lid, 30 | 'data': instance.data?.map((e) => e.toJson()).toList(), 31 | }; 32 | 33 | SinaRNStatus _$SinaRNStatusFromJson(Map json) => SinaRNStatus( 34 | code: (json['code'] as num?)?.toInt(), 35 | msg: json['msg'] as String?, 36 | ); 37 | 38 | Map _$SinaRNStatusToJson(SinaRNStatus instance) => 39 | { 40 | 'code': instance.code, 41 | 'msg': instance.msg, 42 | }; 43 | 44 | SinaRollNews _$SinaRollNewsFromJson(Map json) => SinaRollNews( 45 | title: json['title'] as String?, 46 | intro: json['intro'] as String?, 47 | keywords: json['keywords'] as String?, 48 | lids: json['lids'] as String?, 49 | author: json['author'] as String?, 50 | url: json['url'] as String?, 51 | wapurl: json['wapurl'] as String?, 52 | mediaName: json['media_name'] as String?, 53 | ctime: json['ctime'] as String?, 54 | mtime: json['mtime'] as String?, 55 | img: json['img'], 56 | images: (json['images'] as List?) 57 | ?.map((e) => SinaRNImage.fromJson(e as Map)) 58 | .toList(), 59 | ); 60 | 61 | Map _$SinaRollNewsToJson(SinaRollNews instance) => 62 | { 63 | 'title': instance.title, 64 | 'intro': instance.intro, 65 | 'keywords': instance.keywords, 66 | 'lids': instance.lids, 67 | 'media_name': instance.mediaName, 68 | 'author': instance.author, 69 | 'url': instance.url, 70 | 'wapurl': instance.wapurl, 71 | 'ctime': instance.ctime, 72 | 'mtime': instance.mtime, 73 | 'img': instance.img, 74 | 'images': instance.images?.map((e) => e.toJson()).toList(), 75 | }; 76 | 77 | SinaRNImage _$SinaRNImageFromJson(Map json) => SinaRNImage( 78 | u: json['u'] as String?, 79 | w: json['w'], 80 | h: json['h'], 81 | ); 82 | 83 | Map _$SinaRNImageToJson(SinaRNImage instance) => 84 | { 85 | 'u': instance.u, 86 | 'w': instance.w, 87 | 'h': instance.h, 88 | }; 89 | -------------------------------------------------------------------------------- /lib/models/mapper_utils.dart: -------------------------------------------------------------------------------- 1 | /* 2 | 1、 3 | { 4 | "prompt_tokens": 3, 5 | "completion_tokens": 14, 6 | "total_tokens": 17 7 | } 8 | 2、 9 | { 10 | "PromptTokens": 3, 11 | "CompletionTokens": 14, 12 | "TotalTokens": 17 13 | } 14 | 3、 15 | { 16 | "promptTokens": 3, 17 | "completionTokens": 14, 18 | "totalTokens": 17 19 | } 20 | */ 21 | /// 2024-08-17 22 | /// 专门为响应体匹配多种结构设计的函数 23 | /// 比如如上面有3不同的响应体,一般的API返回的都是第一种,蛇式命名,但像“腾讯”这种异类,请求和响应都是第二种帕斯卡命名的json 24 | /// 为了统一处理,就在【处理响应时】,在json_annotation 的 @JsonKey(readValue: readValue),中,统一处理响应的json栏位 25 | /// 请求体就暂时不必, 就混元Lite的请求在转换时需要帕斯卡,目前只打算几个必要参数,就不多做处理了 26 | dynamic readJsonValue(Map json, String key) { 27 | // 尝试从多个可能的键中读取值 28 | var possibleKeys = generatePossibleKeys(key); 29 | 30 | for (var possibleKey in possibleKeys) { 31 | if (json.containsKey(possibleKey)) { 32 | return json[possibleKey]; 33 | } 34 | } 35 | return null; 36 | } 37 | 38 | List generatePossibleKeys(String key) { 39 | // 生成蛇形命名、帕斯卡命名和驼峰命名的可能键 40 | return [ 41 | // 蛇形命名 42 | key 43 | .replaceAllMapped( 44 | RegExp(r'(?<=[a-z])[A-Z]'), 45 | (match) => '_${match.group(0)}', 46 | ) 47 | .toLowerCase(), 48 | // 帕斯卡命名 49 | key[0].toUpperCase() + key.substring(1), 50 | // 驼峰命名 51 | key, 52 | ]; 53 | } 54 | 55 | // 报错信息可是多种多样 56 | dynamic readErrorMsgValue(Map json, String key) { 57 | // 尝试从多个可能的键中读取值 58 | // 例如时error_msg,会匹配error_msg、ErrorMsg、errorMsg 59 | var possibleKeys = generatePossibleKeys(key); 60 | 61 | for (var possibleKey in possibleKeys) { 62 | if (json.containsKey(possibleKey)) { 63 | return json[possibleKey]; 64 | } 65 | } 66 | 67 | // 单独报错消息,除了error_msg,还可以其他形状 ,比如讯飞的message、无问芯穹的msg 68 | // 2024-11-04 注意,讯飞的报错使用了message栏位,但是成功该栏位也有值"Success",需要特殊处理 69 | return json["message"] ?? json['Message'] ?? json["msg"] ?? json["Msg"]; 70 | } 71 | 72 | // 2025-03-24 不同平台联网搜索参考内容不一样,所以需要单独处理 73 | dynamic readReferenceValue(Map json, String key) { 74 | // 尝试从多个可能的键中读取值 75 | // 例如时error_msg,会匹配error_msg、ErrorMsg、errorMsg 76 | var possibleKeys = generatePossibleKeys(key); 77 | 78 | for (var possibleKey in possibleKeys) { 79 | if (json.containsKey(possibleKey)) { 80 | return json[possibleKey]; 81 | } 82 | } 83 | 84 | // 2025-03-24 火山引擎的联网搜索参考内容是references 85 | // 2025-03-25 智谱的联网搜索参考内容是 web_search 86 | return json["references"] ?? json["web_search"]; 87 | } 88 | -------------------------------------------------------------------------------- /lib/services/network_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:connectivity_plus/connectivity_plus.dart'; 4 | 5 | class NetworkStatusService { 6 | // 创建一个单例实例 7 | static final _instance = NetworkStatusService._internal(); 8 | 9 | factory NetworkStatusService() => _instance; 10 | 11 | NetworkStatusService._internal(); 12 | 13 | // 创建一个 StreamController 来管理网络状态的变化 14 | final _networkStatusController = 15 | StreamController>.broadcast(); 16 | 17 | // 获取网络状态的 Stream 18 | Stream> get networkStatusStream => 19 | _networkStatusController.stream; 20 | 21 | // 初始化网络状态监听 22 | void initialize() { 23 | Connectivity() 24 | .onConnectivityChanged 25 | .listen((List result) { 26 | _networkStatusController.add(result); 27 | }); 28 | } 29 | 30 | // 判断当前网络是否是 Wi-Fi 状态 31 | Future isWifi() async { 32 | List result = await Connectivity().checkConnectivity(); 33 | return result.contains(ConnectivityResult.wifi); 34 | } 35 | 36 | Future isMobile() async { 37 | List result = await Connectivity().checkConnectivity(); 38 | return result.contains(ConnectivityResult.mobile); 39 | } 40 | 41 | Future isNetwork() async { 42 | List result = await Connectivity().checkConnectivity(); 43 | return (result.contains(ConnectivityResult.mobile) || 44 | result.contains(ConnectivityResult.wifi)); 45 | } 46 | 47 | // 关闭 StreamController 48 | void dispose() { 49 | _networkStatusController.close(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/views/brief_ai_assistant/_chat_components/_small_tools.dart: -------------------------------------------------------------------------------- 1 | // 检查URL是否为有效的图片 2 | import 'package:dio/dio.dart'; 3 | 4 | Future isValidImageUrl(String url) async { 5 | final dio = Dio(); // 创建 Dio 实例 6 | try { 7 | // 发送 HEAD 请求 8 | final response = await dio.head(url); 9 | // 检查响应头中的 content-type 10 | final contentType = response.headers['content-type']?.first; 11 | return contentType != null && contentType.startsWith('image/'); 12 | } catch (e) { 13 | return false; // 如果发生异常,返回 false 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/views/brief_ai_assistant/_chat_components/text_selection_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:flutter_easyloading/flutter_easyloading.dart'; 4 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 5 | 6 | class TextSelectionDialog extends StatelessWidget { 7 | final String text; 8 | 9 | const TextSelectionDialog({ 10 | super.key, 11 | required this.text, 12 | }); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return Dialog.fullscreen( 17 | child: Scaffold( 18 | appBar: AppBar( 19 | leading: IconButton( 20 | icon: const Icon(Icons.close), 21 | onPressed: () => Navigator.pop(context), 22 | ), 23 | title: const Text('选择文本'), 24 | actions: [ 25 | IconButton( 26 | icon: const Icon(Icons.copy), 27 | onPressed: () { 28 | Clipboard.setData(ClipboardData(text: text)); 29 | EasyLoading.showToast('已复制到剪贴板'); 30 | Navigator.pop(context); 31 | }, 32 | ), 33 | ], 34 | ), 35 | body: SingleChildScrollView( 36 | padding: EdgeInsets.all(16.sp), 37 | child: SelectableText( 38 | text, 39 | style: TextStyle(fontSize: 16.sp), 40 | ), 41 | ), 42 | ), 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/views/brief_ai_assistant/chat/components/message_actions.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:flutter_easyloading/flutter_easyloading.dart'; 4 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 5 | 6 | class MessageActions extends StatelessWidget { 7 | final String content; 8 | final int? tokens; 9 | final VoidCallback onRegenerate; 10 | final bool isRegenerating; 11 | 12 | const MessageActions({ 13 | super.key, 14 | required this.content, 15 | this.tokens, 16 | required this.onRegenerate, 17 | this.isRegenerating = false, 18 | }); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return Row( 23 | mainAxisAlignment: MainAxisAlignment.start, 24 | children: [ 25 | // 复制按钮 26 | IconButton( 27 | icon: Icon(Icons.copy_outlined, size: 20.sp), 28 | visualDensity: VisualDensity.compact, 29 | tooltip: '复制内容', 30 | onPressed: () { 31 | Clipboard.setData(ClipboardData(text: content)); 32 | EasyLoading.showSuccess('已复制到剪贴板'); 33 | }, 34 | ), 35 | // 重新生成按钮 36 | IconButton( 37 | icon: isRegenerating 38 | ? SizedBox( 39 | width: 20.sp, 40 | height: 20.sp, 41 | child: const CircularProgressIndicator(strokeWidth: 2), 42 | ) 43 | : Icon(Icons.refresh_outlined, size: 20.sp), 44 | visualDensity: VisualDensity.compact, 45 | tooltip: '重新生成', 46 | onPressed: isRegenerating ? null : onRegenerate, 47 | ), 48 | // token 信息 49 | if (tokens != null) 50 | Padding( 51 | padding: EdgeInsets.only(left: 16.sp), 52 | child: Text('$tokens tokens'), 53 | ), 54 | ], 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/views/brief_ai_assistant/common/media_preview_base.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 3 | import 'package:photo_manager/photo_manager.dart'; 4 | import 'package:share_plus/share_plus.dart'; 5 | import 'package:flutter_easyloading/flutter_easyloading.dart'; 6 | import 'show_media_info_dialog.dart'; 7 | 8 | abstract class MediaPreviewBase extends StatelessWidget { 9 | final AssetEntity asset; 10 | final VoidCallback? onDelete; 11 | 12 | const MediaPreviewBase({ 13 | super.key, 14 | required this.asset, 15 | this.onDelete, 16 | }); 17 | 18 | // 子类需要实现的方法 19 | Widget buildPreviewContent(); 20 | String get title; 21 | 22 | // 分享媒体 23 | Future _shareMedia(BuildContext context) async { 24 | try { 25 | final file = await asset.file; 26 | if (file == null) return; 27 | 28 | final result = await Share.shareXFiles( 29 | [XFile(file.path)], 30 | text: '思文AI助手', 31 | ); 32 | 33 | if (result.status == ShareResultStatus.success) { 34 | EasyLoading.showSuccess('分享成功!'); 35 | } 36 | } catch (e) { 37 | if (!context.mounted) return; 38 | ScaffoldMessenger.of(context).showSnackBar( 39 | SnackBar(content: Text('分享失败: $e')), 40 | ); 41 | } 42 | } 43 | 44 | // 删除媒体 45 | Future _deleteMedia(BuildContext context) async { 46 | final confirmed = await showDialog( 47 | context: context, 48 | builder: (context) => AlertDialog( 49 | title: const Text('确认删除'), 50 | content: const Text('确定要删除这个文件吗?'), 51 | actions: [ 52 | TextButton( 53 | onPressed: () => Navigator.pop(context, false), 54 | child: const Text('取消'), 55 | ), 56 | TextButton( 57 | onPressed: () => Navigator.pop(context, true), 58 | child: const Text('删除'), 59 | ), 60 | ], 61 | ), 62 | ); 63 | 64 | if (confirmed != true) return; 65 | 66 | try { 67 | // Android11+ 移动到垃圾桶,低于11的会报错 68 | var list = await PhotoManager.editor.android.moveToTrash([asset]); 69 | 70 | // 实际删除成功后,才执行传入的删除回调 71 | if (list.isNotEmpty) { 72 | EasyLoading.showSuccess('删除成功!'); 73 | onDelete?.call(); 74 | } 75 | 76 | if (!context.mounted) return; 77 | Navigator.pop(context); 78 | } catch (e) { 79 | if (!context.mounted) return; 80 | ScaffoldMessenger.of(context).showSnackBar( 81 | SnackBar(content: Text('删除失败: $e')), 82 | ); 83 | } 84 | } 85 | 86 | @override 87 | Widget build(BuildContext context) { 88 | return Scaffold( 89 | appBar: AppBar( 90 | title: Text(title), 91 | actions: [ 92 | IconButton( 93 | icon: const Icon(Icons.share), 94 | onPressed: () => _shareMedia(context), 95 | ), 96 | IconButton( 97 | icon: const Icon(Icons.delete), 98 | onPressed: () => _deleteMedia(context), 99 | ), 100 | IconButton( 101 | icon: const Icon(Icons.info_outline), 102 | onPressed: () => showMediaInfoDialog(asset, context), 103 | ), 104 | ], 105 | ), 106 | body: Padding( 107 | padding: EdgeInsets.all(5.sp), 108 | child: buildPreviewContent(), 109 | ), 110 | ); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /lib/views/brief_ai_assistant/image/image_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:photo_manager/photo_manager.dart'; 3 | 4 | import '../common/media_manager_base.dart'; 5 | import 'image_preview.dart'; 6 | 7 | class ImageManagerScreen extends MediaManagerBase { 8 | const ImageManagerScreen({super.key}); 9 | 10 | @override 11 | State createState() => _ImageManagerScreenState(); 12 | } 13 | 14 | class _ImageManagerScreenState 15 | extends MediaManagerBaseState { 16 | @override 17 | String get title => '图片管理'; 18 | 19 | @override 20 | RequestType get mediaType => RequestType.image; 21 | 22 | @override 23 | Widget buildPreviewScreen(AssetEntity asset) { 24 | return ImagePreviewScreen( 25 | asset: asset, 26 | onDelete: () { 27 | setState(() { 28 | mediaList.remove(asset); 29 | }); 30 | }, 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/views/brief_ai_assistant/image/image_preview.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:photo_manager_image_provider/photo_manager_image_provider.dart'; 3 | 4 | import '../common/media_preview_base.dart'; 5 | 6 | class ImagePreviewScreen extends MediaPreviewBase { 7 | const ImagePreviewScreen({ 8 | super.key, 9 | required super.asset, 10 | super.onDelete, 11 | }); 12 | 13 | @override 14 | String get title => '图片预览'; 15 | 16 | @override 17 | Widget buildPreviewContent() { 18 | return InteractiveViewer( 19 | child: Center( 20 | child: AssetEntityImage( 21 | asset, 22 | isOriginal: true, 23 | fit: BoxFit.contain, 24 | ), 25 | ), 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/views/brief_ai_assistant/image/mime_image_manager.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | import '../../../common/constants/constants.dart'; 6 | import '../common/mime_media_manager_base.dart'; 7 | import 'mime_image_preview.dart'; 8 | 9 | class MimeImageManager extends MimeMediaManagerBase { 10 | const MimeImageManager({super.key}); 11 | 12 | @override 13 | State createState() => _MimeImageManagerState(); 14 | } 15 | 16 | class _MimeImageManagerState 17 | extends MimeMediaManagerBaseState { 18 | @override 19 | String get title => 'MIME图片管理'; 20 | 21 | @override 22 | CusMimeCls get mediaType => CusMimeCls.IMAGE; 23 | 24 | @override 25 | Widget buildPreviewScreen(File file) { 26 | return MimeImagePreview( 27 | file: file, 28 | onDelete: () { 29 | setState(() { 30 | mediaList.remove(file); 31 | }); 32 | }, 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/views/brief_ai_assistant/image/mime_image_preview.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../common/mime_media_preview_base.dart'; 4 | 5 | class MimeImagePreview extends MimeMediaPreviewBase { 6 | const MimeImagePreview({ 7 | super.key, 8 | required super.file, 9 | super.onDelete, 10 | }); 11 | 12 | @override 13 | String get title => 'MIME图片预览'; 14 | 15 | @override 16 | Widget buildPreviewContent() { 17 | return InteractiveViewer( 18 | child: Center( 19 | child: Image.file( 20 | file, 21 | fit: BoxFit.contain, 22 | ), 23 | ), 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/views/brief_ai_assistant/model_config/index.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'components/api_key_config.dart'; 4 | import 'components/model_list.dart'; 5 | 6 | class BriefModelConfig extends StatefulWidget { 7 | const BriefModelConfig({super.key}); 8 | 9 | @override 10 | State createState() => _BriefModelConfigState(); 11 | } 12 | 13 | class _BriefModelConfigState extends State { 14 | @override 15 | Widget build(BuildContext context) { 16 | return DefaultTabController( 17 | length: 2, 18 | child: Scaffold( 19 | appBar: AppBar( 20 | title: const Text('模型配置'), 21 | bottom: const TabBar( 22 | tabs: [ 23 | Tab(text: '模型列表'), 24 | Tab(text: 'API配置'), 25 | ], 26 | ), 27 | ), 28 | body: const TabBarView( 29 | children: [ 30 | ModelList(), 31 | ApiKeyConfig(), 32 | ], 33 | ), 34 | ), 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/views/brief_ai_assistant/video/mime_video_manager.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | import '../../../common/constants/constants.dart'; 6 | import '../common/mime_media_manager_base.dart'; 7 | import 'mime_video_preview.dart'; 8 | 9 | class MimeVideoManager extends MimeMediaManagerBase { 10 | const MimeVideoManager({super.key}); 11 | 12 | @override 13 | State createState() => _VideoManagerScreenState(); 14 | } 15 | 16 | class _VideoManagerScreenState 17 | extends MimeMediaManagerBaseState { 18 | @override 19 | String get title => 'MIME视频管理'; 20 | 21 | @override 22 | CusMimeCls get mediaType => CusMimeCls.VIDEO; 23 | 24 | @override 25 | Widget buildPreviewScreen(File file) { 26 | return MimeVideoPreview( 27 | file: file, 28 | onDelete: () { 29 | setState(() { 30 | mediaList.remove(file); 31 | }); 32 | }, 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/views/brief_ai_assistant/video/mime_video_preview.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 5 | import 'package:video_player/video_player.dart'; 6 | import '../common/mime_media_preview_base.dart'; 7 | 8 | class MimeVideoPreview extends MimeMediaPreviewBase { 9 | const MimeVideoPreview({ 10 | super.key, 11 | required super.file, 12 | super.onDelete, 13 | }); 14 | 15 | @override 16 | String get title => 'MIME视频预览'; 17 | 18 | @override 19 | Widget buildPreviewContent() { 20 | return FutureBuilder( 21 | future: Future.value(file), 22 | builder: (context, snapshot) { 23 | if (!snapshot.hasData) { 24 | return const Center(child: CircularProgressIndicator()); 25 | } 26 | 27 | return VideoPlayerWidget(videoUrl: file.path); 28 | }, 29 | ); 30 | } 31 | } 32 | 33 | class VideoPlayerWidget extends StatefulWidget { 34 | final String videoUrl; 35 | final String? sourceType; 36 | 37 | const VideoPlayerWidget({ 38 | super.key, 39 | required this.videoUrl, 40 | this.sourceType = 'file', 41 | }); 42 | 43 | @override 44 | State createState() => _VideoPlayerWidgetState(); 45 | } 46 | 47 | class _VideoPlayerWidgetState extends State { 48 | late VideoPlayerController _controller; 49 | bool _isInitialized = false; 50 | 51 | @override 52 | void initState() { 53 | super.initState(); 54 | 55 | if (widget.sourceType == "network") { 56 | _controller = VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl)) 57 | ..initialize().then((_) { 58 | setState(() => _isInitialized = true); 59 | _controller.play(); 60 | }); 61 | } else { 62 | _controller = VideoPlayerController.file(File(widget.videoUrl)) 63 | ..initialize().then((_) { 64 | setState(() => _isInitialized = true); 65 | _controller.play(); 66 | }); 67 | } 68 | } 69 | 70 | @override 71 | Widget build(BuildContext context) { 72 | if (!_isInitialized) { 73 | return const Center(child: CircularProgressIndicator()); 74 | } 75 | 76 | return Column( 77 | mainAxisAlignment: MainAxisAlignment.center, 78 | children: [ 79 | AspectRatio( 80 | aspectRatio: _controller.value.aspectRatio, 81 | child: VideoPlayer(_controller), 82 | ), 83 | VideoProgressIndicator(_controller, allowScrubbing: true), 84 | Row( 85 | mainAxisAlignment: MainAxisAlignment.center, 86 | children: [ 87 | IconButton( 88 | icon: Icon( 89 | _controller.value.isPlaying 90 | ? Icons.pause_circle 91 | : Icons.play_circle, 92 | size: 32.sp, 93 | ), 94 | onPressed: () { 95 | setState(() { 96 | _controller.value.isPlaying 97 | ? _controller.pause() 98 | : _controller.play(); 99 | }); 100 | }, 101 | ), 102 | IconButton( 103 | icon: Icon(Icons.replay_circle_filled, size: 32.sp), 104 | onPressed: () { 105 | _controller.seekTo(Duration.zero); 106 | _controller.play(); 107 | }, 108 | ), 109 | ], 110 | ), 111 | ], 112 | ); 113 | } 114 | 115 | @override 116 | void dispose() { 117 | _controller.dispose(); 118 | super.dispose(); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /lib/views/brief_ai_assistant/video/video_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:photo_manager/photo_manager.dart'; 3 | 4 | import '../common/media_manager_base.dart'; 5 | import 'video_preview.dart'; 6 | 7 | class VideoManagerScreen extends MediaManagerBase { 8 | const VideoManagerScreen({super.key}); 9 | 10 | @override 11 | State createState() => _VideoManagerScreenState(); 12 | } 13 | 14 | class _VideoManagerScreenState 15 | extends MediaManagerBaseState { 16 | @override 17 | String get title => '视频管理'; 18 | 19 | @override 20 | RequestType get mediaType => RequestType.video; 21 | 22 | @override 23 | Widget buildPreviewScreen(AssetEntity asset) { 24 | return VideoPreviewScreen( 25 | asset: asset, 26 | onDelete: () { 27 | setState(() { 28 | mediaList.remove(asset); 29 | }); 30 | }, 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/views/brief_ai_assistant/video/video_player_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'video_preview.dart'; 4 | 5 | class NetworkVideoPlayerScreen extends StatefulWidget { 6 | final String videoUrl; 7 | final String? sourceType; 8 | 9 | const NetworkVideoPlayerScreen({ 10 | super.key, 11 | required this.videoUrl, 12 | this.sourceType = 'file', 13 | }); 14 | 15 | @override 16 | State createState() => 17 | _NetworkVideoPlayerScreenState(); 18 | } 19 | 20 | class _NetworkVideoPlayerScreenState extends State { 21 | @override 22 | Widget build(BuildContext context) { 23 | return Scaffold( 24 | appBar: AppBar(title: const Text('视频播放示例')), 25 | body: VideoPlayerWidget( 26 | videoUrl: widget.videoUrl, 27 | sourceType: widget.sourceType, 28 | ), 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/views/brief_ai_assistant/video/video_preview.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 5 | import 'package:video_player/video_player.dart'; 6 | import '../common/media_preview_base.dart'; 7 | 8 | class VideoPreviewScreen extends MediaPreviewBase { 9 | const VideoPreviewScreen({ 10 | super.key, 11 | required super.asset, 12 | super.onDelete, 13 | }); 14 | 15 | @override 16 | String get title => '视频预览'; 17 | 18 | @override 19 | Widget buildPreviewContent() { 20 | return FutureBuilder( 21 | future: asset.file, 22 | builder: (context, snapshot) { 23 | if (!snapshot.hasData) { 24 | return const Center(child: CircularProgressIndicator()); 25 | } 26 | 27 | return VideoPlayerWidget(videoUrl: snapshot.data!.path); 28 | }, 29 | ); 30 | } 31 | } 32 | 33 | class VideoPlayerWidget extends StatefulWidget { 34 | final String videoUrl; 35 | final String? sourceType; 36 | 37 | const VideoPlayerWidget({ 38 | super.key, 39 | required this.videoUrl, 40 | this.sourceType = 'file', 41 | }); 42 | 43 | @override 44 | State createState() => _VideoPlayerWidgetState(); 45 | } 46 | 47 | class _VideoPlayerWidgetState extends State { 48 | late VideoPlayerController _controller; 49 | bool _isInitialized = false; 50 | 51 | @override 52 | void initState() { 53 | super.initState(); 54 | 55 | if (widget.sourceType == "network") { 56 | _controller = VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl)) 57 | ..initialize().then((_) { 58 | setState(() => _isInitialized = true); 59 | _controller.play(); 60 | }); 61 | } else { 62 | _controller = VideoPlayerController.file(File(widget.videoUrl)) 63 | ..initialize().then((_) { 64 | setState(() => _isInitialized = true); 65 | _controller.play(); 66 | }); 67 | } 68 | } 69 | 70 | @override 71 | Widget build(BuildContext context) { 72 | if (!_isInitialized) { 73 | return const Center(child: CircularProgressIndicator()); 74 | } 75 | 76 | return Column( 77 | mainAxisAlignment: MainAxisAlignment.center, 78 | children: [ 79 | AspectRatio( 80 | aspectRatio: _controller.value.aspectRatio, 81 | child: VideoPlayer(_controller), 82 | ), 83 | VideoProgressIndicator(_controller, allowScrubbing: true), 84 | Row( 85 | mainAxisAlignment: MainAxisAlignment.center, 86 | children: [ 87 | IconButton( 88 | icon: Icon( 89 | _controller.value.isPlaying 90 | ? Icons.pause_circle 91 | : Icons.play_circle, 92 | size: 32.sp, 93 | ), 94 | onPressed: () { 95 | setState(() { 96 | _controller.value.isPlaying 97 | ? _controller.pause() 98 | : _controller.play(); 99 | }); 100 | }, 101 | ), 102 | IconButton( 103 | icon: Icon(Icons.replay_circle_filled, size: 32.sp), 104 | onPressed: () { 105 | _controller.seekTo(Duration.zero); 106 | _controller.play(); 107 | }, 108 | ), 109 | ], 110 | ), 111 | ], 112 | ); 113 | } 114 | 115 | @override 116 | void dispose() { 117 | _controller.dispose(); 118 | super.dispose(); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /lib/views/life_tools/news/_components/cus_scrollable_category_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 3 | 4 | import '../../../../common/constants/constants.dart'; 5 | 6 | /// 7 | /// 横向滚动选择新闻分类列表 8 | /// 9 | class CusScrollableCategoryList extends StatelessWidget { 10 | final ScrollController scrollController; 11 | final List categories; 12 | final int selectedIndex; 13 | final Function(int) onCategorySelected; 14 | 15 | const CusScrollableCategoryList({ 16 | super.key, 17 | required this.scrollController, 18 | required this.categories, 19 | required this.selectedIndex, 20 | required this.onCategorySelected, 21 | }); 22 | 23 | // 是否可以向左滚动 24 | bool get canScrollLeft => 25 | scrollController.positions.isNotEmpty && 26 | scrollController.position.pixels > 0; 27 | 28 | // 是否可以向右滚动 29 | bool get canScrollRight => 30 | scrollController.positions.isNotEmpty && 31 | scrollController.position.pixels < 32 | scrollController.position.maxScrollExtent; 33 | 34 | // 向左滚动 35 | void _scrollLeft() { 36 | scrollController.animateTo( 37 | // 每次点击滚动一个60 38 | scrollController.position.pixels - 60.sp, 39 | duration: const Duration(milliseconds: 300), 40 | curve: Curves.easeInOut, 41 | ); 42 | } 43 | 44 | // 向右滚动 45 | void _scrollRight() { 46 | scrollController.animateTo( 47 | // 每次点击滚动一个60 48 | scrollController.position.pixels + 60.sp, 49 | duration: const Duration(milliseconds: 300), 50 | curve: Curves.easeInOut, 51 | ); 52 | } 53 | 54 | @override 55 | Widget build(BuildContext context) { 56 | return SizedBox( 57 | height: 35.sp, 58 | child: Row( 59 | children: [ 60 | if (canScrollLeft) 61 | IconButton( 62 | icon: const Icon(Icons.arrow_left), 63 | onPressed: _scrollLeft, 64 | ), 65 | Expanded( 66 | child: ListView.builder( 67 | controller: scrollController, 68 | scrollDirection: Axis.horizontal, 69 | itemCount: categories.length, 70 | itemBuilder: (context, index) { 71 | return Padding( 72 | padding: EdgeInsets.symmetric(horizontal: 2.sp), 73 | // 使用 InkWell 可以比较容易自定义样式 74 | child: InkWell( 75 | onTap: () => onCategorySelected(index), 76 | child: Container( 77 | width: 60.sp, 78 | height: 30.sp, 79 | padding: EdgeInsets.all(1.sp), 80 | decoration: BoxDecoration( 81 | color: selectedIndex == index 82 | ? Colors.blue[100] 83 | : Colors.white, 84 | borderRadius: BorderRadius.circular(5.sp), 85 | border: Border.all(color: Colors.grey, width: 1.sp), 86 | ), 87 | child: Center( 88 | child: Text( 89 | categories[index].cnLabel, 90 | style: TextStyle( 91 | fontSize: 12.sp, 92 | color: Colors.black, 93 | ), 94 | ), 95 | ), 96 | ), 97 | ), 98 | ); 99 | }, 100 | ), 101 | ), 102 | if (canScrollRight) 103 | IconButton( 104 | icon: const Icon(Icons.arrow_right), 105 | onPressed: _scrollRight, 106 | ), 107 | ], 108 | ), 109 | ); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /lib/views/life_tools/news/daily_60s/index.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:cached_network_image/cached_network_image.dart'; 4 | import 'package:dio/dio.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 7 | import 'package:intl/intl.dart'; 8 | 9 | import '../../../../common/constants/constants.dart'; 10 | import '../../../../common/utils/tools.dart'; 11 | 12 | class Daily60S extends StatefulWidget { 13 | final String? title; 14 | final String? imageUrl; 15 | 16 | const Daily60S({super.key, this.imageUrl, this.title}); 17 | 18 | @override 19 | State createState() => _Daily60SState(); 20 | } 21 | 22 | class _Daily60SState extends State { 23 | var list = [ 24 | "https://api.jun.la/60s.php?format=image", 25 | "https://api.03c3.cn/api/zb?random=${DateTime.now().millisecondsSinceEpoch}", 26 | ]; 27 | 28 | // 直接获取图片、可直接显示的地址(不稳定) 29 | String imageUrl() => 30 | "https://api.03c3.cn/api/zb?random=${DateTime.now().millisecondsSinceEpoch}"; 31 | // 获取图片二进制,需要进一步处理数据的地址 32 | // 2024-11-04 这个不能用了 33 | String imageDataUrl = 'https://api.jun.la/60s.php?format=image'; 34 | 35 | @override 36 | void initState() { 37 | super.initState(); 38 | } 39 | 40 | Future fetchImageBytes() async { 41 | final response = await Dio().get( 42 | imageUrl(), 43 | options: Options(responseType: ResponseType.bytes), 44 | ); 45 | if (response.statusCode == 200) { 46 | return response.data; 47 | } else { 48 | throw Exception('Failed to load image'); 49 | } 50 | } 51 | 52 | @override 53 | Widget build(BuildContext context) { 54 | return Scaffold( 55 | appBar: AppBar( 56 | title: Text(widget.title ?? '每天60秒读懂世界'), 57 | ), 58 | // 2024-11-04 这个直接获取图片二进制的也不行了 59 | // body: FutureBuilder( 60 | // future: fetchImageBytes(), 61 | // builder: (context, snapshot) { 62 | // if (snapshot.connectionState == ConnectionState.waiting) { 63 | // return const Center(child: CircularProgressIndicator()); 64 | // } else if (snapshot.hasError) { 65 | // return Center(child: Text('Error: ${snapshot.error}')); 66 | // } else if (!snapshot.hasData || snapshot.data == null) { 67 | // return const Center(child: Text('No image found')); 68 | // } else { 69 | // return SingleChildScrollView( 70 | // child: Image.memory( 71 | // snapshot.data!, 72 | // width: 1.sw, 73 | // fit: BoxFit.fitWidth, 74 | // ), 75 | // ); 76 | // } 77 | // }, 78 | // ), 79 | // 2024-10-23 这个图片地址突然不能用了,原因不知 80 | // 2024-10-04 添加长按保存 81 | body: SingleChildScrollView( 82 | child: GestureDetector( 83 | // 长按保存到相册 84 | onLongPress: () async { 85 | // 网络图片就保存都指定位置 86 | await saveImageToLocal( 87 | imageUrl(), 88 | prefix: "每天60秒读懂世界", 89 | imageName: 90 | "${DateFormat(constDateFormat).format(DateTime.now())}.jpg", 91 | dlDir: DL_DIR, 92 | ); 93 | }, 94 | child: CachedNetworkImage( 95 | imageUrl: widget.imageUrl ?? imageUrl(), 96 | // width: MediaQuery.of(context).size.width, 97 | width: 1.sw, 98 | fit: BoxFit.fitWidth, 99 | placeholder: (context, url) => SizedBox( 100 | height: 200.sp, 101 | child: Center(child: CircularProgressIndicator()), 102 | ), 103 | errorWidget: (context, url, error) => const Center( 104 | child: Text("图片暂时无法显示,请稍候重试。"), 105 | ), 106 | ), 107 | ), 108 | ), 109 | ); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility in the flutter_test package. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:swmate/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(const SWMateApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | --------------------------------------------------------------------------------